Skip to content

Commit b6219e8

Browse files
fix: role for aside element when nested within sectioning content (#30)
* fix: role for aside element when nested within landmark * Update .changeset/purple-trees-serve.md
1 parent e7f3891 commit b6219e8

File tree

5 files changed

+90
-9
lines changed

5 files changed

+90
-9
lines changed

.changeset/purple-trees-serve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"html-aria": patch
3+
---
4+
5+
Fix role for aside element when nested within sectioning content

src/get-role.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { ALL_ROLES, roles } from './lib/aria-roles.js';
1+
import { ALL_ROLES } from './lib/aria-roles.js';
22
import { NO_CORRESPONDING_ROLE, tags } from './lib/html.js';
33
import { calculateAccessibleName, firstMatchingToken, isEmptyAncestorList, virtualizeElement } from './lib/util.js';
4+
import { getAsideRole } from './tags/aside.js';
45
import { getFooterRole } from './tags/footer.js';
56
import { getHeaderRole } from './tags/header.js';
67
import { getInputRole } from './tags/input.js';
@@ -56,6 +57,10 @@ export function getRole(element: VirtualElement | HTMLElement, options?: GetRole
5657
case 'area': {
5758
return attributes && !('href' in attributes) ? 'generic' : tag.defaultRole;
5859
}
60+
case 'aside': {
61+
const name = calculateAccessibleName({ tagName, attributes });
62+
return name ? tag.defaultRole : getAsideRole(options);
63+
}
5964
case 'header': {
6065
return getHeaderRole(options);
6166
}

src/lib/util.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
import type {
2-
ARIAAttribute,
3-
AncestorList,
4-
AttributeData,
5-
NameProhibitedAttributes,
6-
TagName,
7-
VirtualElement,
8-
} from '../types.js';
1+
import type { ARIAAttribute, AncestorList, NameProhibitedAttributes, TagName, VirtualElement } from '../types.js';
92

103
/** Parse a list of roles, e.g. role="graphics-symbol img" */
114
export function parseTokenList(tokenList: string): string[] {
@@ -62,6 +55,12 @@ export function calculateAccessibleName(element: VirtualElement): string | undef
6255
const { tagName, attributes } = element;
6356

6457
switch (tagName) {
58+
case 'aside': {
59+
/**
60+
* @see https://www.w3.org/TR/html-aam-1.0/#section-and-grouping-element-accessible-name-computation
61+
*/
62+
return (attributes?.['aria-label'] || attributes?.['aria-labelledby']) as string;
63+
}
6564
case 'img': {
6665
/**
6766
* According to spec, aria-label is technically allowed for <img> (even if alt is preferred)

src/tags/aside.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { tags } from '../lib/html.js';
2+
import { firstMatchingAncestor } from '../lib/util.js';
3+
import type { AncestorList } from '../types.js';
4+
5+
function hasSectioningContentParent(ancestors: AncestorList) {
6+
return !!firstMatchingAncestor(
7+
[
8+
{ tagName: 'article', attributes: { role: 'article' } },
9+
{ tagName: 'aside', attributes: { role: 'complementary' } },
10+
{ tagName: 'nav', attributes: { role: 'navigation' } },
11+
{ tagName: 'section', attributes: { role: 'region' } },
12+
],
13+
ancestors,
14+
);
15+
}
16+
17+
export function getAsideRole({ ancestors }: { ancestors?: AncestorList } = {}) {
18+
if (!ancestors) {
19+
return tags.aside.defaultRole;
20+
}
21+
return hasSectioningContentParent(ancestors) ? 'generic' : tags.aside.defaultRole;
22+
}

test/get-role.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,56 @@ describe('getRole', () => {
3131
['area (no href)', { given: [{ tagName: 'area', attributes: {} }], want: 'generic' }],
3232
['article', { given: [{ tagName: 'article' }], want: 'article' }],
3333
['aside', { given: [{ tagName: 'aside' }], want: 'complementary' }],
34+
[
35+
'aside (name, sectioning article)',
36+
{
37+
given: [
38+
{ tagName: 'aside', attributes: { 'aria-label': 'My aside' } },
39+
{ ancestors: [{ tagName: 'article' }] },
40+
],
41+
want: 'complementary',
42+
},
43+
],
44+
[
45+
'aside (name, sectioning aside)',
46+
{
47+
given: [{ tagName: 'aside', attributes: { 'aria-label': 'My aside' } }, { ancestors: [{ tagName: 'aside' }] }],
48+
want: 'complementary',
49+
},
50+
],
51+
[
52+
'aside (name, sectioning nav)',
53+
{
54+
given: [{ tagName: 'aside', attributes: { 'aria-label': 'My aside' } }, { ancestors: [{ tagName: 'nav' }] }],
55+
want: 'complementary',
56+
},
57+
],
58+
[
59+
'aside (name, sectioning section)',
60+
{
61+
given: [
62+
{ tagName: 'aside', attributes: { 'aria-label': 'My aside' } },
63+
{ ancestors: [{ tagName: 'section' }] },
64+
],
65+
want: 'complementary',
66+
},
67+
],
68+
[
69+
'aside (no name, sectioning article)',
70+
{ given: [{ tagName: 'aside' }, { ancestors: [{ tagName: 'article' }] }], want: 'generic' },
71+
],
72+
[
73+
'aside (no name, sectioning aside)',
74+
{ given: [{ tagName: 'aside' }, { ancestors: [{ tagName: 'aside' }] }], want: 'generic' },
75+
],
76+
[
77+
'aside (no name, sectioning nav)',
78+
{ given: [{ tagName: 'aside' }, { ancestors: [{ tagName: 'nav' }] }], want: 'generic' },
79+
],
80+
[
81+
'aside (no name, sectioning section)',
82+
{ given: [{ tagName: 'aside' }, { ancestors: [{ tagName: 'section' }] }], want: 'generic' },
83+
],
3484
['audio', { given: [{ tagName: 'audio' }], want: undefined }],
3585
['b', { given: [{ tagName: 'b' }], want: 'generic' }],
3686
['base', { given: [{ tagName: 'base' }], want: undefined }],

0 commit comments

Comments
 (0)