Skip to content

Commit 12f45e0

Browse files
committed
feat(NcAppSidebar): introduce NcAppSidebarHeader for a11y navigation
Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
1 parent 5fa6412 commit 12f45e0

File tree

5 files changed

+86
-13
lines changed

5 files changed

+86
-13
lines changed

src/components/NcAppSidebar/NcAppSidebar.vue

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,8 @@ export default {
550550
As a simple solution - render it in the content to keep correct position.
551551
-->
552552
<Teleport v-if="ncContentSelector && !open && !noToggle" :to="ncContentSelector">
553-
<NcButton :aria-label="t('Open sidebar')"
553+
<NcButton ref="toggle"
554+
:aria-label="t('Open sidebar')"
554555
class="app-sidebar__toggle"
555556
:class="toggleClasses"
556557
variant="tertiary"
@@ -570,6 +571,12 @@ export default {
570571
'app-sidebar-header--compact': compact,
571572
}"
572573
class="app-sidebar-header">
574+
<!-- a11y fallback for empty content -->
575+
<NcAppSidebarHeader v-if="empty"
576+
class="app-sidebar-header__mainname--hidden"
577+
:name
578+
tabindex="-1" />
579+
573580
<!-- container for figure and description, allows easy switching to compact mode -->
574581
<div class="app-sidebar-header__info">
575582
<!-- sidebar header illustration/figure -->
@@ -618,17 +625,13 @@ export default {
618625
<div class="app-sidebar-header__name-container">
619626
<div class="app-sidebar-header__mainname-container">
620627
<!-- main name -->
621-
<h2 v-show="!nameEditable"
622-
:id="`app-sidebar-vue-${uid}__header`"
623-
ref="header"
624-
v-linkify="{text: name, linkify: linkifyName}"
625-
:aria-label="title"
626-
:title="title"
628+
<NcAppSidebarHeader v-show="!nameEditable"
627629
class="app-sidebar-header__mainname"
630+
:name
631+
:linkify="linkifyName"
632+
:title
628633
:tabindex="nameEditable ? 0 : -1"
629-
@click.self="editName">
630-
{{ name }}
631-
</h2>
634+
@click.self="editName" />
632635
<template v-if="nameEditable">
633636
<form v-click-outside="() => onSubmitName()"
634637
class="app-sidebar-header__mainname-form"
@@ -706,13 +709,14 @@ export default {
706709
<script>
707710
import NcAppSidebarTabs from './NcAppSidebarTabs.vue'
708711
import NcActions from '../NcActions/index.js'
712+
import NcAppSidebarHeader from '../NcAppSidebarHeader/index.ts'
709713
import NcButton from '../NcButton/index.ts'
710714
import NcEmptyContent from '../NcEmptyContent/index.js'
711715
import NcLoadingIcon from '../NcLoadingIcon/index.js'
712716
import Focus from '../../directives/Focus/index.js'
713-
import Linkify from '../../directives/Linkify/index.ts'
714717
import { vOnClickOutside as ClickOutside } from '@vueuse/components'
715718
import { createFocusTrap } from 'focus-trap'
719+
import { provide, ref, warn } from 'vue'
716720
import { useIsSmallMobile } from '../../composables/useIsMobile/index.js'
717721
import { createElementId } from '../../utils/createElementId.ts'
718722
import { getTrapStack } from '../../utils/focusTrap.ts'
@@ -730,6 +734,7 @@ export default {
730734
731735
components: {
732736
NcActions,
737+
NcAppSidebarHeader,
733738
NcAppSidebarTabs,
734739
NcButton,
735740
NcLoadingIcon,
@@ -743,7 +748,6 @@ export default {
743748
744749
directives: {
745750
Focus,
746-
Linkify,
747751
ClickOutside,
748752
},
749753
@@ -922,9 +926,13 @@ export default {
922926
],
923927
924928
setup() {
929+
const headerRef = ref(null)
930+
provide('NcAppSidebar:header:ref', headerRef)
931+
925932
return {
926933
uid: createElementId(),
927934
isMobile: useIsSmallMobile(),
935+
headerRef,
928936
}
929937
},
930938
@@ -1135,7 +1143,16 @@ export default {
11351143
* @public
11361144
*/
11371145
focus() {
1138-
(this.$refs.header ?? this.$refs.toggle)?.focus()
1146+
if (!this.open && !this.noToggle) {
1147+
this.$refs.toggle.$el.focus()
1148+
return
1149+
}
1150+
1151+
try {
1152+
this.headerRef.focus()
1153+
} catch {
1154+
warn('NcAppSidebar should have focusable header for accessibility reasons. Use NcAppSidebarHeader component.')
1155+
}
11391156
},
11401157
11411158
/**
@@ -1497,6 +1514,17 @@ $top-buttons-spacing: 6px;
14971514
}
14981515
}
14991516
1517+
// Hidden a11y fallback
1518+
.app-sidebar-header__mainname--hidden {
1519+
position: absolute;
1520+
top: 0;
1521+
inset-inline-start: 0;
1522+
margin: 0;
1523+
width: 1px;
1524+
height: 1px;
1525+
overflow: hidden;
1526+
}
1527+
15001528
// sidebar description slot
15011529
&__description {
15021530
display: flex;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { inject } from 'vue'
8+
import vLinkify from '../../directives/Linkify/index.ts'
9+
10+
defineProps<{
11+
/**
12+
* The name used in NcAppSidebar header.
13+
*/
14+
name: string,
15+
16+
/**
17+
* Title to display for the name.
18+
*/
19+
title?: string,
20+
21+
/**
22+
* Linkify the name.
23+
*/
24+
linkify?: boolean,
25+
}>()
26+
27+
const headerRef = inject('NcAppSidebar:header:ref')
28+
</script>
29+
30+
<template>
31+
<h2 ref="headerRef"
32+
v-linkify="{ text: name, linkify }"
33+
tabindex="-1"
34+
:title>
35+
{{ name }}
36+
</h2>
37+
</template>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
export { default } from './NcAppSidebarHeader.vue'

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export { default as NcAppNavigationSpacer } from './NcAppNavigationSpacer/index.
3131
export { default as NcAppSettingsDialog } from './NcAppSettingsDialog/index.js'
3232
export { default as NcAppSettingsSection } from './NcAppSettingsSection/index.js'
3333
export { default as NcAppSidebar } from './NcAppSidebar/index.js'
34+
export { default as NcAppSidebarHeader } from './NcAppSidebarHeader/index.ts'
3435
export { default as NcAppSidebarTab } from './NcAppSidebarTab/index.js'
3536
export { default as NcAvatar } from './NcAvatar/index.js'
3637
export { default as NcBlurHash } from './NcBlurHash/index.js'

styleguide.config.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ module.exports = async () => {
233233
name: 'NcAppSidebar',
234234
components: [
235235
'src/components/NcAppSidebar/NcAppSidebar.vue',
236+
'src/components/NcAppSidebarHeader/NcAppSidebarHeader.vue',
236237
'src/components/NcAppSidebarTab/NcAppSidebarTab.vue',
237238
],
238239
},

0 commit comments

Comments
 (0)