From 6630f2d459018cd9c88fb94286f2a317c4f3fed6 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Thu, 12 Feb 2026 21:30:40 +0100 Subject: [PATCH 1/3] refactor: rename dropdownContentRole to ariaRole --- .../dropdown/__tests__/dropdown-aria.test.tsx | 89 +++++++++++++++++++ src/internal/components/dropdown/index.tsx | 9 +- .../components/dropdown/interfaces.ts | 7 +- src/multiselect/internal.tsx | 6 +- src/select/internal.tsx | 6 +- src/select/utils/use-select.ts | 7 +- 6 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 src/internal/components/dropdown/__tests__/dropdown-aria.test.tsx diff --git a/src/internal/components/dropdown/__tests__/dropdown-aria.test.tsx b/src/internal/components/dropdown/__tests__/dropdown-aria.test.tsx new file mode 100644 index 0000000000..3e63c89789 --- /dev/null +++ b/src/internal/components/dropdown/__tests__/dropdown-aria.test.tsx @@ -0,0 +1,89 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { render } from '@testing-library/react'; + +import Dropdown from '../../../../../lib/components/internal/components/dropdown'; +import DropdownWrapper from '../../../../../lib/components/test-utils/dom/internal/dropdown'; + +function renderDropdown(jsx: React.ReactElement) { + const { container } = render(jsx); + const dropdownElement = container.querySelector(`.${DropdownWrapper.rootSelector}`)!; + return new DropdownWrapper(dropdownElement); +} + +describe('Dropdown ARIA attributes', () => { + describe('ariaRole', () => { + test('applies role attribute when ariaRole is provided', () => { + const wrapper = renderDropdown( + } open={true} ariaRole="dialog" content={
Content
} /> + ); + const dropdown = wrapper.findOpenDropdown()!.getElement(); + expect(dropdown).toHaveAttribute('role', 'dialog'); + }); + }); + + describe('ariaLabel', () => { + test('applies aria-label attribute when ariaLabel is provided', () => { + const wrapper = renderDropdown( + } open={true} ariaLabel="Select options" content={
Content
} /> + ); + const dropdown = wrapper.findOpenDropdown()!.getElement(); + expect(dropdown).toHaveAttribute('aria-label', 'Select options'); + }); + }); + + describe('ariaLabelledby', () => { + test('applies aria-labelledby attribute when ariaLabelledby is provided', () => { + const wrapper = renderDropdown( + } open={true} ariaLabelledby="label-id" content={
Content
} /> + ); + const dropdown = wrapper.findOpenDropdown()!.getElement(); + expect(dropdown).toHaveAttribute('aria-labelledby', 'label-id'); + }); + }); + + describe('ariaDescribedby', () => { + test('applies aria-describedby attribute when ariaDescribedby is provided', () => { + const wrapper = renderDropdown( + } open={true} ariaDescribedby="description-id" content={
Content
} /> + ); + const dropdown = wrapper.findOpenDropdown()!.getElement(); + expect(dropdown).toHaveAttribute('aria-describedby', 'description-id'); + }); + }); + + describe('dropdownContentId', () => { + test('applies id attribute when dropdownContentId is provided', () => { + const wrapper = renderDropdown( + } + open={true} + dropdownContentId="custom-dropdown-id" + content={
Content
} + /> + ); + const dropdown = wrapper.findOpenDropdown()!.getElement(); + expect(dropdown).toHaveAttribute('id', 'custom-dropdown-id'); + }); + }); + + describe('aria-hidden', () => { + test('sets aria-hidden to true when dropdown is closed', () => { + const wrapper = renderDropdown( + } open={false} ariaRole="dialog" content={
Content
} /> + ); + // Dropdown content exists in DOM even when closed (for transitions) + const dropdownContainer = wrapper.getElement().querySelector('[aria-hidden]'); + expect(dropdownContainer).toHaveAttribute('aria-hidden', 'true'); + }); + + test('sets aria-hidden to false when dropdown is open', () => { + const wrapper = renderDropdown( + } open={true} ariaRole="dialog" content={
Content
} /> + ); + const dropdown = wrapper.findOpenDropdown()!.getElement(); + expect(dropdown).toHaveAttribute('aria-hidden', 'false'); + }); + }); +}); diff --git a/src/internal/components/dropdown/index.tsx b/src/internal/components/dropdown/index.tsx index d0b3806874..75fcde53d3 100644 --- a/src/internal/components/dropdown/index.tsx +++ b/src/internal/components/dropdown/index.tsx @@ -81,6 +81,7 @@ interface TransitionContentProps { onMouseDown?: React.MouseEventHandler; id?: string; role?: string; + ariaLabel?: string; ariaLabelledby?: string; ariaDescribedby?: string; } @@ -106,6 +107,7 @@ const TransitionContent = ({ onMouseDown, id, role, + ariaLabel, ariaLabelledby, ariaDescribedby, }: TransitionContentProps) => { @@ -131,6 +133,7 @@ const TransitionContent = ({ ref={contentRef} id={id} role={role} + aria-label={ariaLabel} aria-labelledby={ariaLabelledby} aria-describedby={ariaDescribedby} data-open={open} @@ -181,7 +184,8 @@ const Dropdown = ({ onBlur, contentKey, dropdownContentId, - dropdownContentRole, + ariaRole, + ariaLabel, ariaLabelledby, ariaDescribedby, }: DropdownProps) => { @@ -489,7 +493,8 @@ const Dropdown = ({ verticalContainerRef={verticalContainerRef} position={position} id={dropdownContentId} - role={dropdownContentRole} + role={ariaRole} + ariaLabel={ariaLabel} ariaLabelledby={ariaLabelledby} ariaDescribedby={ariaDescribedby} /> diff --git a/src/internal/components/dropdown/interfaces.ts b/src/internal/components/dropdown/interfaces.ts index d4603738e3..c3cf22d95e 100644 --- a/src/internal/components/dropdown/interfaces.ts +++ b/src/internal/components/dropdown/interfaces.ts @@ -166,7 +166,12 @@ export interface DropdownProps extends ExpandToViewport { /** * HTML role for the dropdown content wrapper */ - dropdownContentRole?: string; + ariaRole?: string; + + /** + * Aria label for the dropdown content wrapper + */ + ariaLabel?: string; /** * Labelledby for the dropdown (required when role="dialog") diff --git a/src/multiselect/internal.tsx b/src/multiselect/internal.tsx index 90329ace09..accf257535 100644 --- a/src/multiselect/internal.tsx +++ b/src/multiselect/internal.tsx @@ -163,10 +163,8 @@ const InternalMultiselect = React.forwardRef( > Pick< - DropdownProps, - 'onFocus' | 'onBlur' | 'dropdownContentId' | 'dropdownContentRole' - > = () => ({ + const getDropdownProps: () => Pick = () => ({ onFocus: handleFocus, onBlur: handleBlur, dropdownContentId: dialogId, - dropdownContentRole: hasFilter ? 'dialog' : undefined, + ariaRole: hasFilter ? 'dialog' : undefined, }); const getTriggerProps = (disabled = false, autoFocus = false) => { From 2f4804ffb937d0b7d59b4a40010acd0e3f9e0fe3 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Tue, 17 Feb 2026 21:05:46 +0100 Subject: [PATCH 2/3] refactor: rename to --- src/internal/components/dropdown/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/internal/components/dropdown/index.tsx b/src/internal/components/dropdown/index.tsx index 75fcde53d3..6a17430e55 100644 --- a/src/internal/components/dropdown/index.tsx +++ b/src/internal/components/dropdown/index.tsx @@ -80,7 +80,7 @@ interface TransitionContentProps { open?: boolean; onMouseDown?: React.MouseEventHandler; id?: string; - role?: string; + ariaRole?: string; ariaLabel?: string; ariaLabelledby?: string; ariaDescribedby?: string; @@ -106,7 +106,7 @@ const TransitionContent = ({ open, onMouseDown, id, - role, + ariaRole, ariaLabel, ariaLabelledby, ariaDescribedby, @@ -132,7 +132,7 @@ const TransitionContent = ({ })} ref={contentRef} id={id} - role={role} + role={ariaRole} aria-label={ariaLabel} aria-labelledby={ariaLabelledby} aria-describedby={ariaDescribedby} @@ -493,7 +493,7 @@ const Dropdown = ({ verticalContainerRef={verticalContainerRef} position={position} id={dropdownContentId} - role={ariaRole} + ariaRole={ariaRole} ariaLabel={ariaLabel} ariaLabelledby={ariaLabelledby} ariaDescribedby={ariaDescribedby} From c508b7fa50762598b301287e85660ec36ae90912 Mon Sep 17 00:00:00 2001 From: Maximilian Schoell Date: Wed, 18 Feb 2026 12:47:22 +0100 Subject: [PATCH 3/3] fix: use different selector --- .../components/dropdown/__tests__/dropdown-aria.test.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/internal/components/dropdown/__tests__/dropdown-aria.test.tsx b/src/internal/components/dropdown/__tests__/dropdown-aria.test.tsx index 3e63c89789..865daa1556 100644 --- a/src/internal/components/dropdown/__tests__/dropdown-aria.test.tsx +++ b/src/internal/components/dropdown/__tests__/dropdown-aria.test.tsx @@ -73,8 +73,7 @@ describe('Dropdown ARIA attributes', () => { const wrapper = renderDropdown( } open={false} ariaRole="dialog" content={
Content
} /> ); - // Dropdown content exists in DOM even when closed (for transitions) - const dropdownContainer = wrapper.getElement().querySelector('[aria-hidden]'); + const dropdownContainer = wrapper.getElement().querySelector('[data-open]'); expect(dropdownContainer).toHaveAttribute('aria-hidden', 'true'); }); @@ -82,7 +81,7 @@ describe('Dropdown ARIA attributes', () => { const wrapper = renderDropdown( } open={true} ariaRole="dialog" content={
Content
} /> ); - const dropdown = wrapper.findOpenDropdown()!.getElement(); + const dropdown = wrapper.getElement().querySelector('[data-open]'); expect(dropdown).toHaveAttribute('aria-hidden', 'false'); }); });