Skip to content

Commit fe124ee

Browse files
fix(Dropdown): make ariaHaspopupValue opt-in, no aria-haspopup by default
Dropdown stamped aria-haspopup="true" on every trigger, advertising menu semantics on disclosure-style popups. Allyant (HUB 366, WCAG 4.1.2) flagged this sitewide; QA rejected keeping the attribute with value "menu" and only accepts removal. Consumers that implement a complete ARIA popup pattern can still opt in by passing a value.
1 parent 0a2a860 commit fe124ee

10 files changed

Lines changed: 60 additions & 72 deletions

File tree

src/components/Dropdown/Dropdown.test.tsx

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,11 @@ const ComplexDropdownWithoutAriaHaspopupComponent = (): JSX.Element => {
256256
return (
257257
<Dropdown {...dropdownProps} ariaRef={buttonRef} ariaHaspopupValue={null}>
258258
<div>
259-
<Button ref={buttonRef} data-testid="test-button-id" text="Test button" />
259+
<Button
260+
ref={buttonRef}
261+
data-testid="test-button-id"
262+
text="Test button"
263+
/>
260264
</div>
261265
</Dropdown>
262266
);
@@ -269,7 +273,28 @@ const ComplexDropdownWithCustomAriaHaspopupComponent = (): JSX.Element => {
269273
return (
270274
<Dropdown {...dropdownProps} ariaRef={buttonRef} ariaHaspopupValue="dialog">
271275
<div>
272-
<Button ref={buttonRef} data-testid="test-button-id" text="Test button" />
276+
<Button
277+
ref={buttonRef}
278+
data-testid="test-button-id"
279+
text="Test button"
280+
/>
281+
</div>
282+
</Dropdown>
283+
);
284+
};
285+
286+
const ComplexDropdownDefaultAriaHaspopupComponent = (): JSX.Element => {
287+
const buttonRef: React.MutableRefObject<HTMLButtonElement> =
288+
useRef<HTMLButtonElement>(null);
289+
290+
return (
291+
<Dropdown {...dropdownProps} ariaRef={buttonRef}>
292+
<div>
293+
<Button
294+
ref={buttonRef}
295+
data-testid="test-button-id"
296+
text="Test button"
297+
/>
273298
</div>
274299
</Dropdown>
275300
);
@@ -975,6 +1000,26 @@ describe('Dropdown', () => {
9751000
expect(referenceElement.getAttribute('aria-haspopup')).toBe('dialog');
9761001
});
9771002

1003+
test('Should not set aria-haspopup by default (opt-in only) on the cloned reference element', async () => {
1004+
const { getByTestId } = render(<DropdownComponent />);
1005+
const referenceElement = getByTestId('dropdown-reference');
1006+
1007+
expect(referenceElement.getAttribute('aria-expanded')).toBe('false');
1008+
expect(referenceElement.getAttribute('aria-controls')).toBeTruthy();
1009+
expect(referenceElement.hasAttribute('aria-haspopup')).toBe(false);
1010+
});
1011+
1012+
test('Should not set aria-haspopup by default (opt-in only) on the ariaRef reference element', async () => {
1013+
const { getByTestId } = render(
1014+
<ComplexDropdownDefaultAriaHaspopupComponent />
1015+
);
1016+
const referenceElement = getByTestId('test-button-id');
1017+
1018+
expect(referenceElement.getAttribute('aria-expanded')).toBe('false');
1019+
expect(referenceElement.getAttribute('aria-controls')).toBeTruthy();
1020+
expect(referenceElement.hasAttribute('aria-haspopup')).toBe(false);
1021+
});
1022+
9781023
test('Should open normally with disableAutoFlip set', async () => {
9791024
const { container, getByTestId } = render(
9801025
<Dropdown {...dropdownProps} disableAutoFlip>

src/components/Dropdown/Dropdown.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const Dropdown: FC<DropdownProps> = React.memo(
4848
(
4949
{
5050
ariaRef,
51-
ariaHaspopupValue = 'true',
51+
ariaHaspopupValue,
5252
children,
5353
classNames,
5454
closeOnDropdownClick = true,
@@ -498,7 +498,11 @@ export const Dropdown: FC<DropdownProps> = React.memo(
498498
const currentRole = ariaRef.current.getAttribute('role');
499499
const currentAriaHaspopup =
500500
ariaRef.current.getAttribute('aria-haspopup');
501-
if (currentRole !== 'combobox' && !currentAriaHaspopup && ariaHaspopupValue) {
501+
if (
502+
currentRole !== 'combobox' &&
503+
!currentAriaHaspopup &&
504+
ariaHaspopupValue
505+
) {
502506
ariaRef.current.setAttribute('aria-haspopup', ariaHaspopupValue);
503507
}
504508

src/components/Dropdown/Dropdown.types.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ export interface DropdownProps {
1919
/**
2020
* The value of the aria-haspopup attribute to be applied to the reference element, if not already set
2121
* and the role of the reference element is not 'combobox'.
22-
* Pass `null` to omit the attribute entirely — the intended way to suppress it for
23-
* disclosure-style triggers whose popup is not a menu/listbox/etc. (An empty string also
24-
* omits it, but that is incidental; the type intentionally does not permit `false`.)
25-
* @default 'true'
22+
* aria-haspopup is opt-in: when omitted, the attribute is not rendered — the correct
23+
* default for disclosure-style triggers (aria-expanded/aria-controls only).
24+
* Only pass a value ('menu', 'listbox', 'tree', 'grid', 'dialog', or 'true') when the
25+
* popup genuinely implements that ARIA pattern, including its full keyboard contract
26+
* (for 'menu' this means e.g. `shouldCloseOnTab` and a popup whose container role
27+
* matches the announced value).
28+
* @default undefined
2629
*/
2730
ariaHaspopupValue?: string | null;
2831
/**

src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ exports[`Dropdown Should render a dropdown button 1`] = `
99
aria-controls="dropdown-"
1010
aria-disabled="false"
1111
aria-expanded="false"
12-
aria-haspopup="true"
1312
class="button button-default button-medium pill-shape icon-right"
1413
data-testid="dropdown-reference"
1514
id="test-button-id"

src/components/Menu/__snapshots__/Menu.test.tsx.snap

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ exports[`Menu Menu is large 1`] = `
99
aria-controls="dropdown-"
1010
aria-disabled="false"
1111
aria-expanded="true"
12-
aria-haspopup="true"
1312
class="button button-default button-medium pill-shape"
1413
id="dropdown-reference"
1514
role="button"
@@ -253,7 +252,6 @@ exports[`Menu Menu is medium 1`] = `
253252
aria-controls="dropdown-"
254253
aria-disabled="false"
255254
aria-expanded="true"
256-
aria-haspopup="true"
257255
class="button button-default button-medium pill-shape"
258256
id="dropdown-reference"
259257
role="button"
@@ -497,7 +495,6 @@ exports[`Menu Menu is small 1`] = `
497495
aria-controls="dropdown-"
498496
aria-disabled="false"
499497
aria-expanded="true"
500-
aria-haspopup="true"
501498
class="button button-default button-medium pill-shape"
502499
id="dropdown-reference"
503500
role="button"
@@ -741,7 +738,6 @@ exports[`Menu Should render a menu button 1`] = `
741738
aria-controls="dropdown-"
742739
aria-disabled="false"
743740
aria-expanded="false"
744-
aria-haspopup="true"
745741
class="button button-default button-medium pill-shape"
746742
id="dropdown-reference"
747743
role="button"

src/components/Navbar/__snapshots__/Navbar.test.tsx.snap

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ exports[`Navbar Navbar matches custom theme 1`] = `
3030
<div
3131
aria-controls="dropdown-"
3232
aria-expanded="false"
33-
aria-haspopup="true"
3433
class="reference-wrapper"
3534
id="dropdown-reference"
3635
role="button"
@@ -102,7 +101,6 @@ exports[`Navbar Renders without crashing 1`] = `
102101
<div
103102
aria-controls="dropdown-"
104103
aria-expanded="false"
105-
aria-haspopup="true"
106104
class="reference-wrapper"
107105
id="dropdown-reference"
108106
role="button"

src/components/Pagination/__snapshots__/Pagination.test.tsx.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,6 @@ exports[`Pagination Pagination should render with all elements 1`] = `
335335
aria-controls="dropdown-"
336336
aria-disabled="false"
337337
aria-expanded="false"
338-
aria-haspopup="true"
339338
aria-label="Selected page size"
340339
class="pagination-button button button-neutral button-medium icon-right"
341340
id="dropdown-reference"

src/components/Skill/Tests/__snapshots__/SkillBlock.test.tsx.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4256,7 +4256,6 @@ exports[`SkillBlock Skill overflow item menu 1`] = `
42564256
aria-controls="dropdown-"
42574257
aria-disabled="false"
42584258
aria-expanded="true"
4259-
aria-haspopup="true"
42604259
class="button button button-neutral button-small round-shape icon-left"
42614260
data-testid="test-item-menu"
42624261
id="dropdown-reference"

src/components/Skill/Tests/__snapshots__/SkillTag.test.tsx.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1287,7 +1287,6 @@ exports[`SkillTag Skill tag has a dropdown 1`] = `
12871287
aria-controls="dropdown-"
12881288
aria-disabled="false"
12891289
aria-expanded="true"
1290-
aria-haspopup="true"
12911290
class="reference-wrapper skill tag bordered clickable white medium dropdown"
12921291
data-testid="skill-tag-2"
12931292
id="testSkill"

0 commit comments

Comments
 (0)