From 942de8f1fc31c32ceef04a9f7a32ceb5092a3a01 Mon Sep 17 00:00:00 2001 From: Lingzhi Xu Date: Sat, 2 Sep 2023 19:45:17 -0700 Subject: [PATCH 1/7] Improve Select Component a11y by adding combobox role and aria-controls attribute Resolves: #35586 --- packages/mui-material/src/Select/Select.test.js | 6 ++++++ packages/mui-material/src/Select/SelectInput.js | 2 ++ 2 files changed, 8 insertions(+) diff --git a/packages/mui-material/src/Select/Select.test.js b/packages/mui-material/src/Select/Select.test.js index 0ee8903cc2cd83..d92ff9a96d9a3f 100644 --- a/packages/mui-material/src/Select/Select.test.js +++ b/packages/mui-material/src/Select/Select.test.js @@ -478,6 +478,12 @@ describe('); + + expect(getByRole('combobox')).to.have.attribute('aria-controls', 'menu-'); + }); + specify('the listbox is focusable', () => { const { getByRole } = render(', () => { , ); - expect(getByRole('button')).to.have.property('tabIndex', 0); + expect(getByRole('combobox')).to.have.property('tabIndex', 0); }); it('should accept null child', () => { @@ -120,7 +120,7 @@ describe(', ); - const trigger = getByRole('button'); + const trigger = getByRole('combobox'); fireEvent.mouseDown(trigger); @@ -157,7 +157,7 @@ describe(', ); - const trigger = screen.getByRole('button'); + const trigger = screen.getByRole('combobox'); act(() => { trigger.focus(); }); @@ -177,7 +177,7 @@ describe(', ); - const button = getByRole('button'); + const button = getByRole('combobox'); act(() => { button.focus(); }); @@ -236,13 +236,13 @@ describe('); - fireEvent.mouseDown(getByRole('button')); + fireEvent.mouseDown(getByRole('combobox')); // TODO not matching WAI-ARIA authoring practices. It should focus the first (or selected) item. expect(getByRole('listbox')).toHaveFocus(); @@ -258,7 +258,7 @@ describe(', ); - fireEvent.mouseDown(getByRole('button')); + fireEvent.mouseDown(getByRole('combobox')); act(() => { getAllByRole('option')[1].click(); }); @@ -279,7 +279,7 @@ describe(', ); - fireEvent.mouseDown(getByRole('button')); + fireEvent.mouseDown(getByRole('combobox')); act(() => { getAllByRole('option')[1].click(); }); @@ -296,7 +296,7 @@ describe(', ); - fireEvent.mouseDown(getByRole('button')); + fireEvent.mouseDown(getByRole('combobox')); act(() => { getAllByRole('option')[1].click(); }); @@ -308,7 +308,7 @@ describe('); - expect(getByRole('button', { hidden: true })).to.have.attribute('aria-expanded', 'true'); + expect(getByRole('combobox', { hidden: true })).to.have.attribute('aria-expanded', 'true'); }); }); @@ -371,7 +371,7 @@ describe(', ); - expect(getByRole('button')).to.have.text('Twenty'); + expect(getByRole('combobox')).to.have.text('Twenty'); }); describe('warnings', () => { @@ -439,19 +439,19 @@ describe('); - expect(getByRole('button', { hidden: true })).to.have.attribute('aria-expanded', 'true'); + expect(getByRole('combobox', { hidden: true })).to.have.attribute('aria-expanded', 'true'); }); specify('ARIA 1.2: aria-expanded="false" if the listbox isn\'t displayed', () => { const { getByRole } = render(); - expect(getByRole('button')).to.have.attribute('aria-disabled', 'true'); + expect(getByRole('combobox')).to.have.attribute('aria-disabled', 'true'); }); it('sets disabled attribute in input when component is disabled', () => { @@ -463,13 +463,13 @@ describe('); - expect(getByRole('button')).not.to.have.attribute('aria-disabled'); + expect(getByRole('combobox')).not.to.have.attribute('aria-disabled'); }); it('indicates that activating the button displays a listbox', () => { const { getByRole } = render(', () => { expect(getByRole('listbox')).toBeVisible(); }); - it('indicates that input element has combobox role and aria-controls set to id of popup', () => { + it('indicates that input element has combobox role and aria-controls set to id of listbox', () => { const { getByRole } = render(', () => { const { getByRole } = render(); - expect(getByRole('button')).to.have.attribute( + expect(getByRole('combobox')).to.have.attribute( 'aria-labelledby', - getByRole('button').getAttribute('id'), + getByRole('combobox').getAttribute('id'), ); }); @@ -731,7 +732,7 @@ describe('', () => { , ); - expect(getByRole('button')).to.have.attribute( + expect(getByRole('combobox')).to.have.attribute( 'aria-labelledby', - `select-label ${getByRole('button').getAttribute('id')}`, + `select-label ${getByRole('combobox').getAttribute('id')}`, ); }); @@ -782,7 +783,7 @@ describe('', () => { Twenty , ); - const trigger = screen.getByRole('button'); + const trigger = screen.getByRole('combobox'); act(() => { trigger.focus(); }); @@ -818,7 +819,7 @@ describe(', ); - fireEvent.mouseDown(getByRole('button')); + fireEvent.mouseDown(getByRole('combobox')); clock.tick(99); expect(onEntered.callCount).to.equal(0); @@ -855,7 +856,7 @@ describe('', () => { , ); - expect(getByRole('button')).to.have.attribute('data-test', 'SelectDisplay'); + expect(getByRole('combobox')).to.have.attribute('data-test', 'SelectDisplay'); }); }); @@ -883,7 +884,7 @@ describe(', ); - expect(getByRole('button')).to.have.text('Ten'); + expect(getByRole('combobox')).to.have.text('Ten'); }); }); @@ -897,7 +898,7 @@ describe(', ); - expect(getByRole('button')).to.have.text('0b100'); + expect(getByRole('combobox')).to.have.text('0b100'); }); }); @@ -955,7 +956,7 @@ describe('', () => { , ); - const trigger = getByRole('button'); + const trigger = getByRole('combobox'); // If clicked by the right/middle mouse button, no options list should be opened fireEvent.mouseDown(trigger, { button: 1 }); @@ -1014,7 +1015,7 @@ describe(', ); const parentEl = container.querySelector('.MuiInputBase-root'); - const button = getByRole('button'); + const button = getByRole('combobox'); stub(parentEl, 'clientWidth').get(() => 14); fireEvent.mouseDown(button); @@ -1028,7 +1029,7 @@ describe(', ); const parentEl = container.querySelector('.MuiInputBase-root'); - const button = getByRole('button'); + const button = getByRole('combobox'); stub(parentEl, 'clientWidth').get(() => 14); fireEvent.mouseDown(button); @@ -1064,7 +1065,7 @@ describe(', ); - expect(getByRole('button')).to.have.text('Ten, Twenty, Thirty'); + expect(getByRole('combobox')).to.have.text('Ten, Twenty, Thirty'); }); it('should not throw an error if `value` is an empty array', () => { @@ -1180,7 +1181,7 @@ describe('', () => { it('should focus select after Select did mount', () => { const { getByRole } = render(', () => { ref.current.focus(); }); - expect(getByRole('button')).toHaveFocus(); + expect(getByRole('combobox')).toHaveFocus(); }); }); @@ -1296,13 +1297,13 @@ describe('); - expect(getByRole('button')).not.to.have.attribute('id'); + expect(getByRole('combobox')).not.to.have.attribute('id'); }); it('should have select-`name` id when name is provided', () => { const { getByRole } = render(', () => { }); expect(onChangeHandler.calledOnce).to.equal(true); - expect(getByRole('button')).to.have.text('France'); + expect(getByRole('combobox')).to.have.text('France'); }); it('should support native form validation', function test() { @@ -1425,7 +1426,7 @@ describe(', ); - expect(document.activeElement).to.equal(getByRole('button')); + expect(document.activeElement).to.equal(getByRole('combobox')); }); it('should not override the event.target on mouse events', () => { @@ -1664,6 +1665,6 @@ describe(' integration', () => { it('should focus the selected item', () => { const { getByTestId, getAllByRole, getByRole, queryByRole } = render(); - const trigger = getByRole('button'); + const trigger = getByRole('combobox'); // Let's open the select component // in the browser user click also focuses fireEvent.mouseDown(trigger); @@ -62,8 +62,8 @@ describe(' integration', () => { , ); - expect(getByRole('button')).toHaveAccessibleName('Age Ten'); + expect(getByRole('combobox')).toHaveAccessibleName('Age'); }); // we're somewhat abusing "focus" here. What we're actually interested in is @@ -120,7 +120,7 @@ describe(' integration', () => { , ); - const trigger = getByRole('button'); + const trigger = getByRole('combobox'); act(() => { trigger.focus(); From 5859dc348e5a6937ebdda2d2c022028ac68c9375 Mon Sep 17 00:00:00 2001 From: Lingzhi Xu Date: Tue, 19 Sep 2023 17:31:02 -0700 Subject: [PATCH 3/7] address comments and fix tests --- .../TablePagination/TablePagination.test.js | 22 +++++++++-------- .../mui-material/src/Select/Select.test.js | 2 +- .../mui-material/src/Select/SelectInput.js | 4 ++-- .../TablePagination/TablePagination.test.js | 24 ++++++++++--------- .../src/TextField/TextField.test.js | 4 ++-- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/mui-material-next/src/TablePagination/TablePagination.test.js b/packages/mui-material-next/src/TablePagination/TablePagination.test.js index e3052bb9bbe3ab..9c2e4ac19e36bb 100644 --- a/packages/mui-material-next/src/TablePagination/TablePagination.test.js +++ b/packages/mui-material-next/src/TablePagination/TablePagination.test.js @@ -101,8 +101,8 @@ describe('', () => { ); // will be `getByRole('combobox')` in aria 1.2 - const [combobox] = getAllByRole('button'); - expect(combobox).toHaveAccessibleName('lines per page: 10'); + const [combobox] = getAllByRole('combobox'); + expect(combobox).toHaveAccessibleName('lines per page:'); }); it('accepts React nodes', () => { @@ -128,14 +128,14 @@ describe('', () => { ); // will be `getByRole('combobox')` in aria 1.2 - const [combobox] = getAllByRole('button'); - expect(combobox).toHaveAccessibleName('lines per page: 10'); + const [combobox] = getAllByRole('combobox'); + expect(combobox).toHaveAccessibleName('lines per page:'); }); }); describe('prop: page', () => { it('should disable the back button on the first page', () => { - const { getAllByRole } = render( + const { getByRole } = render( @@ -151,13 +151,14 @@ describe('', () => {
, ); - const [, backButton, nextButton] = getAllByRole('button'); + const backButton = getByRole('button', { name: 'Go to previous page' }); + const nextButton = getByRole('button', { name: 'Go to next page' }); expect(backButton).to.have.property('disabled', true); expect(nextButton).to.have.property('disabled', false); }); it('should disable the next button on the last page', () => { - const { getAllByRole } = render( + const { getByRole } = render( @@ -173,7 +174,8 @@ describe('', () => {
, ); - const [, backButton, nextButton] = getAllByRole('button'); + const backButton = getByRole('button', { name: 'Go to previous page' }); + const nextButton = getByRole('button', { name: 'Go to next page' }); expect(backButton).to.have.property('disabled', false); expect(nextButton).to.have.property('disabled', true); }); @@ -398,8 +400,8 @@ describe('', () => { ); // will be `getByRole('combobox')` in aria 1.2 - const [combobox] = getAllByRole('button'); - expect(combobox).toHaveAccessibleName('Rows per page: 10'); + const [combobox] = getAllByRole('combobox'); + expect(combobox).toHaveAccessibleName('Rows per page:'); }); }); diff --git a/packages/mui-material/src/Select/Select.test.js b/packages/mui-material/src/Select/Select.test.js index f48e9218a84686..c861a904ac5055 100644 --- a/packages/mui-material/src/Select/Select.test.js +++ b/packages/mui-material/src/Select/Select.test.js @@ -482,7 +482,7 @@ describe('); const listboxId = getByRole('listbox').id; - expect(getByRole('combobox')).to.have.attribute('aria-controls', listboxId); + expect(getByRole('combobox', { hidden: true })).to.have.attribute('aria-controls', listboxId); }); specify('the listbox is focusable', () => { diff --git a/packages/mui-material/src/Select/SelectInput.js b/packages/mui-material/src/Select/SelectInput.js index 91e56162fc1b58..de66ec05f1ff68 100644 --- a/packages/mui-material/src/Select/SelectInput.js +++ b/packages/mui-material/src/Select/SelectInput.js @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import clsx from 'clsx'; import MuiError from '@mui/utils/macros/MuiError.macro'; import { unstable_composeClasses as composeClasses } from '@mui/base/composeClasses'; -import { refType } from '@mui/utils'; +import { refType, unstable_useId as useId } from '@mui/utils'; import ownerDocument from '../utils/ownerDocument'; import capitalize from '../utils/capitalize'; import Menu from '../Menu/Menu'; @@ -487,7 +487,7 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) { ...MenuProps.slotProps?.paper, }; - const listboxId = React.useId(); + const listboxId = useId(); return ( diff --git a/packages/mui-material/src/TablePagination/TablePagination.test.js b/packages/mui-material/src/TablePagination/TablePagination.test.js index 53a3f7969ce6b4..2f9eed9e158a14 100644 --- a/packages/mui-material/src/TablePagination/TablePagination.test.js +++ b/packages/mui-material/src/TablePagination/TablePagination.test.js @@ -102,8 +102,8 @@ describe('', () => { ); // will be `getByRole('combobox')` in aria 1.2 - const [combobox] = getAllByRole('button'); - expect(combobox).toHaveAccessibleName('lines per page: 10'); + const [combobox] = getAllByRole('combobox'); + expect(combobox).toHaveAccessibleName('lines per page:'); }); it('accepts React nodes', () => { @@ -129,14 +129,14 @@ describe('', () => { ); // will be `getByRole('combobox')` in aria 1.2 - const [combobox] = getAllByRole('button'); - expect(combobox).toHaveAccessibleName('lines per page: 10'); + const [combobox] = getAllByRole('combobox'); + expect(combobox).toHaveAccessibleName('lines per page:'); }); }); describe('prop: page', () => { it('should disable the back button on the first page', () => { - const { getAllByRole } = render( + const { getByRole } = render( @@ -152,13 +152,14 @@ describe('', () => {
, ); - const [, backButton, nextButton] = getAllByRole('button'); + const backButton = getByRole('button', { name: 'Go to previous page' }); + const nextButton = getByRole('button', { name: 'Go to next page' }); expect(backButton).to.have.property('disabled', true); expect(nextButton).to.have.property('disabled', false); }); it('should disable the next button on the last page', () => { - const { getAllByRole } = render( + const { getByRole } = render( @@ -174,7 +175,8 @@ describe('', () => {
, ); - const [, backButton, nextButton] = getAllByRole('button'); + const backButton = getByRole('button', { name: 'Go to previous page' }); + const nextButton = getByRole('button', { name: 'Go to next page' }); expect(backButton).to.have.property('disabled', false); expect(nextButton).to.have.property('disabled', true); }); @@ -399,8 +401,8 @@ describe('', () => { ); // will be `getByRole('combobox')` in aria 1.2 - const [combobox] = getAllByRole('button'); - expect(combobox).toHaveAccessibleName('Rows per page: 10'); + const [combobox] = getAllByRole('combobox'); + expect(combobox).toHaveAccessibleName('Rows per page:'); }); ['standard', 'outlined', 'filled'].forEach((variant) => { @@ -422,7 +424,7 @@ describe('', () => { , ); - const [combobox] = getAllByRole('button'); + const [combobox] = getAllByRole('combobox'); const comboboxContainer = combobox.parentElement; if (variant === 'standard') { diff --git a/packages/mui-material/src/TextField/TextField.test.js b/packages/mui-material/src/TextField/TextField.test.js index b0910a0b22bbdb..ab60aabe55cd16 100644 --- a/packages/mui-material/src/TextField/TextField.test.js +++ b/packages/mui-material/src/TextField/TextField.test.js @@ -202,7 +202,7 @@ describe('', () => { , ); - expect(getByRole('button')).toHaveAccessibleName('Release: Stable'); + expect(getByRole('combobox')).toHaveAccessibleName('Release:'); }); it('creates an input[hidden] that has no accessible properties', () => { @@ -224,7 +224,7 @@ describe('', () => { , ); - expect(getByRole('button')).toHaveAccessibleDescription('Foo bar'); + expect(getByRole('combobox')).toHaveAccessibleDescription('Foo bar'); }); }); From 9f0e07dd5732fe0374524edccdcf0b6fcab2ee91 Mon Sep 17 00:00:00 2001 From: Diego Andai Date: Fri, 22 Sep 2023 14:37:13 -0300 Subject: [PATCH 4/7] Update table pagination tests --- .../TablePagination/TablePagination.test.js | 15 ++++++--------- .../TablePagination/TablePagination.test.js | 19 ++++++++----------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/mui-material-next/src/TablePagination/TablePagination.test.js b/packages/mui-material-next/src/TablePagination/TablePagination.test.js index 9c2e4ac19e36bb..91b842b1b3464f 100644 --- a/packages/mui-material-next/src/TablePagination/TablePagination.test.js +++ b/packages/mui-material-next/src/TablePagination/TablePagination.test.js @@ -83,7 +83,7 @@ describe('', () => { describe('prop: labelRowsPerPage', () => { it('labels the select for the current page', () => { - const { getAllByRole } = render( + const { getByRole } = render( @@ -100,13 +100,12 @@ describe('', () => {
, ); - // will be `getByRole('combobox')` in aria 1.2 - const [combobox] = getAllByRole('combobox'); + const combobox = getByRole('combobox'); expect(combobox).toHaveAccessibleName('lines per page:'); }); it('accepts React nodes', () => { - const { getAllByRole } = render( + const { getByRole } = render( @@ -127,8 +126,7 @@ describe('', () => {
, ); - // will be `getByRole('combobox')` in aria 1.2 - const [combobox] = getAllByRole('combobox'); + const combobox = getByRole('combobox'); expect(combobox).toHaveAccessibleName('lines per page:'); }); }); @@ -382,7 +380,7 @@ describe('', () => { describe('prop: SelectProps', () => { it('does allow manual label ids', () => { - const { getAllByRole } = render( + const { getByRole } = render( @@ -399,8 +397,7 @@ describe('', () => {
, ); - // will be `getByRole('combobox')` in aria 1.2 - const [combobox] = getAllByRole('combobox'); + const combobox = getByRole('combobox'); expect(combobox).toHaveAccessibleName('Rows per page:'); }); }); diff --git a/packages/mui-material/src/TablePagination/TablePagination.test.js b/packages/mui-material/src/TablePagination/TablePagination.test.js index 2f9eed9e158a14..04c51ce86b031a 100644 --- a/packages/mui-material/src/TablePagination/TablePagination.test.js +++ b/packages/mui-material/src/TablePagination/TablePagination.test.js @@ -84,7 +84,7 @@ describe('', () => { describe('prop: labelRowsPerPage', () => { it('labels the select for the current page', () => { - const { getAllByRole } = render( + const { getByRole } = render( @@ -101,13 +101,12 @@ describe('', () => {
, ); - // will be `getByRole('combobox')` in aria 1.2 - const [combobox] = getAllByRole('combobox'); + const combobox = getByRole('combobox'); expect(combobox).toHaveAccessibleName('lines per page:'); }); it('accepts React nodes', () => { - const { getAllByRole } = render( + const { getByRole } = render( @@ -128,8 +127,7 @@ describe('', () => {
, ); - // will be `getByRole('combobox')` in aria 1.2 - const [combobox] = getAllByRole('combobox'); + const combobox = getByRole('combobox'); expect(combobox).toHaveAccessibleName('lines per page:'); }); }); @@ -383,7 +381,7 @@ describe('', () => { describe('prop: SelectProps', () => { it('does allow manual label ids', () => { - const { getAllByRole } = render( + const { getByRole } = render( @@ -400,14 +398,13 @@ describe('', () => {
, ); - // will be `getByRole('combobox')` in aria 1.2 - const [combobox] = getAllByRole('combobox'); + const combobox = getByRole('combobox'); expect(combobox).toHaveAccessibleName('Rows per page:'); }); ['standard', 'outlined', 'filled'].forEach((variant) => { it(`should be able to apply the ${variant} variant to select`, () => { - const { getAllByRole } = render( + const { getByRole } = render( @@ -424,7 +421,7 @@ describe('', () => {
, ); - const [combobox] = getAllByRole('combobox'); + const combobox = getByRole('combobox'); const comboboxContainer = combobox.parentElement; if (variant === 'standard') { From 7e3539c528ee279f887bb25927f79715979abca6 Mon Sep 17 00:00:00 2001 From: Diego Andai Date: Fri, 22 Sep 2023 14:46:39 -0300 Subject: [PATCH 5/7] Remove unnecessary hidden option in test queries --- packages/mui-material/src/Select/Select.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/mui-material/src/Select/Select.test.js b/packages/mui-material/src/Select/Select.test.js index c861a904ac5055..112837807ff78b 100644 --- a/packages/mui-material/src/Select/Select.test.js +++ b/packages/mui-material/src/Select/Select.test.js @@ -308,7 +308,7 @@ describe('); - expect(getByRole('combobox', { hidden: true })).to.have.attribute('aria-expanded', 'true'); + expect(getByRole('combobox')).to.have.attribute('aria-expanded', 'true'); }); }); @@ -439,7 +439,7 @@ describe('); - expect(getByRole('combobox', { hidden: true })).to.have.attribute('aria-expanded', 'true'); + expect(getByRole('combobox')).to.have.attribute('aria-expanded', 'true'); }); specify('ARIA 1.2: aria-expanded="false" if the listbox isn\'t displayed', () => { @@ -482,7 +482,7 @@ describe('); const listboxId = getByRole('listbox').id; - expect(getByRole('combobox', { hidden: true })).to.have.attribute('aria-controls', listboxId); + expect(getByRole('combobox')).to.have.attribute('aria-controls', listboxId); }); specify('the listbox is focusable', () => { @@ -856,7 +856,7 @@ describe('', () => { describe('prop: defaultOpen', () => { it('should be open on mount', () => { const { getByRole } = render(', () => { // technically matter. This is only here in case we keep the rest accessible const { getByRole } = render(', () => { const { getByRole } = render(', () => { ); const paper = getByTestId('paper'); - const selectButton = getByRole('combobox'); + const selectButton = getByRole('combobox', { hidden: true }); expect(paper.style).to.have.property('minWidth', `${selectButton.clientWidth}px`); }); From 1c437532eaeb6d5295de9029938a2dc154ff93c6 Mon Sep 17 00:00:00 2001 From: Diego Andai Date: Fri, 22 Sep 2023 17:55:22 -0300 Subject: [PATCH 7/7] Update new test --- packages/mui-material/src/Select/Select.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-material/src/Select/Select.test.js b/packages/mui-material/src/Select/Select.test.js index 483aabf73752fe..19f6e007018bd4 100644 --- a/packages/mui-material/src/Select/Select.test.js +++ b/packages/mui-material/src/Select/Select.test.js @@ -1063,7 +1063,7 @@ describe(', ); - fireEvent.mouseDown(getByRole('button')); + fireEvent.mouseDown(getByRole('combobox')); expect(getByRole('listbox')).to.have.attribute('aria-multiselectable', 'true'); });