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..865daa1556 --- /dev/null +++ b/src/internal/components/dropdown/__tests__/dropdown-aria.test.tsx @@ -0,0 +1,88 @@ +// 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
} /> + ); + const dropdownContainer = wrapper.getElement().querySelector('[data-open]'); + 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.getElement().querySelector('[data-open]'); + expect(dropdown).toHaveAttribute('aria-hidden', 'false'); + }); + }); +}); diff --git a/src/internal/components/dropdown/index.tsx b/src/internal/components/dropdown/index.tsx index d0b3806874..6a17430e55 100644 --- a/src/internal/components/dropdown/index.tsx +++ b/src/internal/components/dropdown/index.tsx @@ -80,7 +80,8 @@ interface TransitionContentProps { open?: boolean; onMouseDown?: React.MouseEventHandler; id?: string; - role?: string; + ariaRole?: string; + ariaLabel?: string; ariaLabelledby?: string; ariaDescribedby?: string; } @@ -105,7 +106,8 @@ const TransitionContent = ({ open, onMouseDown, id, - role, + ariaRole, + ariaLabel, ariaLabelledby, ariaDescribedby, }: TransitionContentProps) => { @@ -130,7 +132,8 @@ const TransitionContent = ({ })} ref={contentRef} id={id} - role={role} + role={ariaRole} + 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} + ariaRole={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) => {