Skip to content

Commit 4c89007

Browse files
committed
feat(NcAppSidebar): introduce NcAppSidebarHeader for a11y navigation
Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
1 parent 25974cb commit 4c89007

5 files changed

Lines changed: 92 additions & 13 deletions

File tree

src/components/NcAppSidebar/NcAppSidebar.vue

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,8 @@ export default {
545545
As a simple solution - render it in the content to keep correct position.
546546
-->
547547
<Teleport v-if="ncContentSelector && !open && !noToggle" :selector="ncContentSelector">
548-
<NcButton :aria-label="t('Open sidebar')"
548+
<NcButton ref="toggle"
549+
:aria-label="t('Open sidebar')"
549550
class="app-sidebar__toggle"
550551
:class="toggleClasses"
551552
variant="tertiary"
@@ -613,17 +614,13 @@ export default {
613614
<div class="app-sidebar-header__name-container">
614615
<div class="app-sidebar-header__mainname-container">
615616
<!-- main name -->
616-
<h2 v-show="!nameEditable"
617-
:id="`app-sidebar-vue-${uid}__header`"
618-
ref="header"
619-
v-linkify="{text: name, linkify: linkifyName}"
620-
:aria-label="title"
621-
:title="title"
617+
<NcAppSidebarHeader v-show="!nameEditable"
622618
class="app-sidebar-header__mainname"
619+
:name="name"
620+
:linkify="linkifyName"
621+
:title="title"
623622
:tabindex="nameEditable ? 0 : -1"
624-
@click.self="editName">
625-
{{ name }}
626-
</h2>
623+
@click.self.native="editName" />
627624
<template v-if="nameEditable">
628625
<form v-click-outside="() => onSubmitName()"
629626
class="app-sidebar-header__mainname-form"
@@ -664,6 +661,11 @@ export default {
664661
</div>
665662
</div>
666663
</div>
664+
<!-- a11y fallback for empty content -->
665+
<NcAppSidebarHeader v-if="empty"
666+
class="app-sidebar-header__mainname--hidden"
667+
:name="name"
668+
tabindex="-1" />
667669

668670
<NcButton ref="closeButton"
669671
:aria-label="closeTranslated"
@@ -704,11 +706,11 @@ import { Portal as Teleport } from '@linusborg/vue-simple-portal'
704706
705707
import NcAppSidebarTabs from './NcAppSidebarTabs.vue'
706708
import NcActions from '../NcActions/index.js'
709+
import NcAppSidebarHeader from '../NcAppSidebarHeader/index.ts'
707710
import NcButton from '../NcButton/index.js'
708711
import NcEmptyContent from '../NcEmptyContent/index.js'
709712
import NcLoadingIcon from '../NcLoadingIcon/index.js'
710713
import Focus from '../../directives/Focus/index.js'
711-
import Linkify from '../../directives/Linkify/index.js'
712714
import { useIsSmallMobile } from '../../composables/useIsMobile/index.js'
713715
import GenRandomId from '../../utils/GenRandomId.js'
714716
import { getTrapStack } from '../../utils/focusTrap.ts'
@@ -722,13 +724,15 @@ import StarOutline from 'vue-material-design-icons/StarOutline.vue'
722724
723725
import { vOnClickOutside as ClickOutside } from '@vueuse/components'
724726
import { createFocusTrap } from 'focus-trap'
727+
import Vue, { provide, ref } from 'vue'
725728
726729
export default {
727730
name: 'NcAppSidebar',
728731
729732
components: {
730733
Teleport,
731734
NcActions,
735+
NcAppSidebarHeader,
732736
NcAppSidebarTabs,
733737
ArrowRight,
734738
IconDockRight,
@@ -742,7 +746,6 @@ export default {
742746
743747
directives: {
744748
focus: Focus,
745-
linkify: Linkify,
746749
ClickOutside,
747750
},
748751
@@ -923,9 +926,13 @@ export default {
923926
],
924927
925928
setup() {
929+
const headerRef = ref(null)
930+
provide('NcAppSidebar:header:ref', headerRef)
931+
926932
return {
927933
uid: GenRandomId(),
928934
isMobile: useIsSmallMobile(),
935+
headerRef,
929936
}
930937
},
931938
@@ -1155,7 +1162,16 @@ export default {
11551162
* @public
11561163
*/
11571164
focus() {
1158-
this.$refs.header.focus()
1165+
if (!this.open && !this.noToggle) {
1166+
this.$refs.toggle.$el.focus()
1167+
return
1168+
}
1169+
1170+
try {
1171+
this.headerRef.focus()
1172+
} catch {
1173+
Vue.util.warn('NcAppSidebar should have focusable header for accessibility reasons. Use NcAppSidebarHeader component.')
1174+
}
11591175
},
11601176
11611177
/**
@@ -1517,6 +1533,17 @@ $top-buttons-spacing: $app-navigation-padding; // align with app navigation
15171533
}
15181534
}
15191535
1536+
// Hidden a11y fallback
1537+
.app-sidebar-header__mainname--hidden {
1538+
position: absolute;
1539+
top: 0;
1540+
inset-inline-start: 0;
1541+
margin: 0;
1542+
width: 1px;
1543+
height: 1px;
1544+
overflow: hidden;
1545+
}
1546+
15201547
// sidebar description slot
15211548
&__description {
15221549
display: flex;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup>
7+
import { inject } from 'vue'
8+
import vLinkify from '../../directives/Linkify/index.js'
9+
10+
defineProps({
11+
/**
12+
* The name used in NcAppSidebar header.
13+
*/
14+
name: {
15+
type: String,
16+
required: true,
17+
},
18+
19+
/**
20+
* Title to display for the name.
21+
*/
22+
title: {
23+
type: String,
24+
},
25+
26+
/**
27+
* Linkify the name.
28+
*/
29+
linkify: {
30+
type: Boolean,
31+
},
32+
})
33+
34+
const headerRef = inject('NcAppSidebar:header:ref')
35+
</script>
36+
37+
<template>
38+
<h2 ref="headerRef"
39+
v-linkify="{ text: name, linkify }"
40+
tabindex="-1"
41+
:title="title">
42+
{{ name }}
43+
</h2>
44+
</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.js

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.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ module.exports = async () => {
236236
name: 'NcAppSidebar',
237237
components: [
238238
'src/components/NcAppSidebar/NcAppSidebar.vue',
239+
'src/components/NcAppSidebarHeader/NcAppSidebarHeader.vue',
239240
'src/components/NcAppSidebarTab/NcAppSidebarTab.vue',
240241
],
241242
},

0 commit comments

Comments
 (0)