Skip to content

Commit 04e1fc8

Browse files
authored
Merge pull request #1786 from Marghen/fix-ui-shell-accessibility-problems
feat: fix CvUiShell accessibility problems
2 parents 9742739 + 5af4995 commit 04e1fc8

File tree

7 files changed

+125
-22
lines changed

7 files changed

+125
-22
lines changed

src/components/CvUIShell/CvContent.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
<script setup>
22
import { carbonPrefix } from '../../global/settings';
3-
defineProps({
3+
const props = defineProps({
44
tagType: { type: String, default: 'main' },
5+
id: { type: String, default: 'main-content' },
56
});
67
</script>
78

89
<template>
9-
<component :is="tagType" :class="`${carbonPrefix}--content`">
10+
<component
11+
:is="tagType"
12+
:id="id"
13+
:class="`${carbonPrefix}--content`"
14+
tabindex="-1"
15+
>
1016
<slot />
1117
</component>
1218
</template>

src/components/CvUIShell/CvHeaderMenu.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<li
33
ref="el"
44
:class="`cv-header-menu ${carbonPrefix}--header__submenu`"
5+
role="menuitem"
56
@mouseenter="doHoverToggle(true)"
67
@mouseleave="doHoverToggle(false)"
78
>
@@ -10,9 +11,9 @@
1011
:aria-expanded="data.expanded ? 'true' : 'false'"
1112
:class="`${carbonPrefix}--header__menu-item ${carbonPrefix}--header__menu-title`"
1213
href="javascript:void(0)"
13-
role="menuitem"
1414
tabindex="0"
1515
:aria-label="$attrs.ariaLabel"
16+
role="button"
1617
@click="doToggle"
1718
@keydown.space.prevent
1819
@keyup.space.prevent="doToggle"

src/components/CvUIShell/CvHeaderMenuItem.vue

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
<template>
2-
<li class="cv-header-menu-item" :role="role">
2+
<li class="cv-header-menu-item" role="none">
33
<component
44
:is="tagType"
55
v-bind="{ ...$attrs, ...linkProps }"
66
:class="[
77
`${carbonPrefix}--header__menu-item`,
88
{ [`${carbonPrefix}--header__menu-item--current`]: activePage },
99
]"
10-
role="menuitem"
1110
:aria-current="ariaCurrent"
11+
:role="role"
12+
:tabindex="tabindex"
1213
>
1314
<span :class="`${carbonPrefix}--text-truncate--end`">
1415
<slot />
@@ -25,7 +26,7 @@ import { computed } from 'vue';
2526
const props = defineProps({
2627
active: Boolean,
2728
ariaCurrent: { type: String, default: undefined },
28-
role: { type: String, default: undefined },
29+
role: { type: String, default: 'menuitem' },
2930
...propsLink,
3031
});
3132
@@ -35,4 +36,8 @@ const linkProps = useLinkProps(props);
3536
const activePage = computed(() => {
3637
return props.active && props.ariaCurrent !== 'page';
3738
});
39+
40+
const tabindex = computed(() => {
41+
return props.role === 'menuitem' ? '0' : undefined;
42+
});
3843
</script>

src/components/CvUIShell/CvHeaderNav.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,25 @@
33
<ul
44
:class="`${carbonPrefix}--header__menu-bar`"
55
role="menubar"
6-
:ariaLabeldBy="ariaLabelledBy"
6+
aria-orientation="horizontal"
7+
:aria-label="ariaLabel || $attrs['aria-label']"
8+
:aria-labelledby="ariaLabelledBy || $attrs['aria-labelledby']"
79
>
810
<slot />
911
</ul>
1012
</nav>
1113
</template>
1214

15+
<script>
16+
export default {
17+
inheritAttrs: false,
18+
};
19+
</script>
20+
1321
<script setup>
1422
import { carbonPrefix } from '../../global/settings';
1523
defineProps({
24+
ariaLabel: { type: String, default: undefined },
1625
ariaLabelledBy: { type: String, default: undefined },
1726
});
1827
</script>

src/components/CvUIShell/CvHeaderPanel.vue

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
{ [`${carbonPrefix}--header-panel--expanded`]: panelExpanded },
99
]"
1010
:aria-hidden="!panelExpanded ? 'true' : 'false'"
11+
:inert="!panelExpanded"
1112
@focusout="onFocusout"
1213
@mousedown="onMouseDown"
1314
>
@@ -20,6 +21,7 @@ import { carbonPrefix } from '../../global/settings';
2021
import {
2122
computed,
2223
inject,
24+
nextTick,
2325
onBeforeUnmount,
2426
onMounted,
2527
reactive,
@@ -99,9 +101,49 @@ watch(
99101
}
100102
);
101103
104+
function manageFocusableElements(isExpanded) {
105+
if (!el.value) return;
106+
107+
const focusableElements = el.value.querySelectorAll(
108+
'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'
109+
);
110+
111+
focusableElements.forEach(element => {
112+
if (isExpanded) {
113+
// Restore original tabindex when expanded
114+
const originalTabindex = element.getAttribute('data-original-tabindex');
115+
if (originalTabindex !== null) {
116+
if (originalTabindex === 'null') {
117+
element.removeAttribute('tabindex');
118+
} else {
119+
element.setAttribute('tabindex', originalTabindex);
120+
}
121+
element.removeAttribute('data-original-tabindex');
122+
}
123+
} else {
124+
// Store original tabindex and set to -1 when collapsed
125+
const currentTabindex = element.getAttribute('tabindex');
126+
element.setAttribute('data-original-tabindex', currentTabindex || 'null');
127+
element.setAttribute('tabindex', '-1');
128+
}
129+
});
130+
}
131+
102132
const emit = defineEmits(['update:expanded', 'panel-resize']);
103133
watch(panelExpanded, current => {
104134
emit('update:expanded', current);
105135
emit('panel-resize', { id: props.id, expanded: current });
136+
137+
// Manage focusable elements
138+
nextTick(() => {
139+
manageFocusableElements(current);
140+
});
141+
});
142+
143+
// Initial setup of focusable elements
144+
onMounted(() => {
145+
nextTick(() => {
146+
manageFocusableElements(panelExpanded.value);
147+
});
106148
});
107149
</script>

src/components/CvUIShell/CvSideNav.vue

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
},
1515
]"
1616
:aria-hidden="!panelExpanded && !fixed ? 'true' : 'false'"
17+
:inert="!panelExpanded && !fixed"
1718
@focusout="onFocusout"
1819
@mousedown="onMouseDown"
1920
@mouseenter="onHoverToggle(true)"
@@ -33,6 +34,7 @@
3334
import {
3435
computed,
3536
inject,
37+
nextTick,
3638
onBeforeUnmount,
3739
onMounted,
3840
reactive,
@@ -110,13 +112,47 @@ watch(
110112
panelExpanded.value = props.expanded;
111113
}
112114
);
115+
// Manage focusable elements when panel visibility changes
116+
function manageFocusableElements(isExpanded, isFixed) {
117+
if (!el.value || isFixed) return;
118+
119+
const focusableElements = el.value.querySelectorAll(
120+
'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'
121+
);
122+
123+
focusableElements.forEach(element => {
124+
if (isExpanded) {
125+
// Restore original tabindex when expanded
126+
const originalTabindex = element.getAttribute('data-original-tabindex');
127+
if (originalTabindex !== null) {
128+
if (originalTabindex === 'null') {
129+
element.removeAttribute('tabindex');
130+
} else {
131+
element.setAttribute('tabindex', originalTabindex);
132+
}
133+
element.removeAttribute('data-original-tabindex');
134+
}
135+
} else {
136+
// Store original tabindex and set to -1 when collapsed
137+
const currentTabindex = element.getAttribute('tabindex');
138+
element.setAttribute('data-original-tabindex', currentTabindex || 'null');
139+
element.setAttribute('tabindex', '-1');
140+
}
141+
});
142+
}
143+
113144
const emit = defineEmits(['update:expanded', 'panel-resize']);
114145
const isPanelExpanded = computed(
115146
() => panelExpanded.value || expandedViaHoverState.value
116147
);
117148
watch(isPanelExpanded, current => {
118149
emit('update:expanded', current);
119150
emit('panel-resize', { id: props.id, expanded: current });
151+
152+
// Manage focusable elements for non-fixed nav
153+
nextTick(() => {
154+
manageFocusableElements(current, props.fixed);
155+
});
120156
});
121157
122158
const isChildOfHeader = computed(() => {

src/components/CvUIShell/CvUIShell.stories.mdx

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import CvSideNavLink from './CvSideNavLink.vue';
1919
import CvSideNavMenu from './CvSideNavMenu.vue';
2020
import CvSideNavMenuDivider from './CvSideNavMenuDivider.vue';
2121
import CvSideNavMenuItem from './CvSideNavMenuItem.vue';
22+
import CvContent from './CvContent.vue';
2223
import {
2324
Notification20,
2425
UserAvatar20,
@@ -55,6 +56,7 @@ export const Template = args => ({
5556
CvSideNavMenu,
5657
CvSideNavMenuDivider,
5758
CvSideNavMenuItem,
59+
CvContent,
5860
Notification20,
5961
UserAvatar20,
6062
Login20,
@@ -89,22 +91,12 @@ const defaultTemplate = `
8991
[Platform]
9092
</cv-header-name>
9193
<cv-header-nav aria-label="Carbon nav">
92-
<cv-header-menu-item href="javascript:void(0)">
93-
Link 1
94-
</cv-header-menu-item>
95-
<cv-header-menu-item href="javascript:void(0)">
96-
Link 2
97-
</cv-header-menu-item>
94+
<cv-header-menu-item href="javascript:void(0)">Link 1</cv-header-menu-item>
95+
<cv-header-menu-item href="javascript:void(0)">Link 2</cv-header-menu-item>
9896
<cv-header-menu aria-label="Link 3" title="Link 3">
99-
<cv-header-menu-item href="javascript:void(0)">
100-
Submenu Link 1
101-
</cv-header-menu-item>
102-
<cv-header-menu-item href="javascript:void(0)">
103-
Submenu Link 2
104-
</cv-header-menu-item>
105-
<cv-header-menu-item href="javascript:void(0)">
106-
Submenu Link 3
107-
</cv-header-menu-item>
97+
<cv-header-menu-item href="javascript:void(0)">Submenu Link 1</cv-header-menu-item>
98+
<cv-header-menu-item href="javascript:void(0)">Submenu Link 2</cv-header-menu-item>
99+
<cv-header-menu-item href="javascript:void(0)">Submenu Link 3</cv-header-menu-item>
108100
</cv-header-menu>
109101
</cv-header-nav>
110102
<template v-slot:header-global>
@@ -162,6 +154,10 @@ const defaultTemplate = `
162154
</cv-header-panel>
163155
</template>
164156
</cv-header>
157+
<cv-content>
158+
<h1>Main Content</h1>
159+
<p>This is the main content area that the skip link targets.</p>
160+
</cv-content>
165161
`;
166162
const railTemplate = `
167163
<cv-header aria-label="Carbon header">
@@ -196,6 +192,10 @@ const railTemplate = `
196192
</cv-side-nav>
197193
</template>
198194
</cv-header>
195+
<cv-content>
196+
<h1>Main Content</h1>
197+
<p>This is the main content area that the skip link targets.</p>
198+
</cv-content>
199199
`;
200200
const fixedSideNavTemplate = `
201201
<cv-header aria-label="Carbon header">
@@ -231,6 +231,10 @@ const fixedSideNavTemplate = `
231231
</cv-side-nav-link>
232232
</cv-side-nav-items>
233233
</cv-side-nav>
234+
<cv-content>
235+
<h1>Main Content</h1>
236+
<p>This is the main content area that the skip link targets.</p>
237+
</cv-content>
234238
`;
235239

236240
# UI Shell components

0 commit comments

Comments
 (0)