From 76b5dc13d8b19543cf1b4a10be16f0d356787537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Tue, 14 Jun 2022 14:43:58 +0200 Subject: [PATCH 1/2] [MenuUnstyled] Fix keyboard accessibility of menu items --- .../src/ListboxUnstyled/useListbox.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/mui-base/src/ListboxUnstyled/useListbox.ts b/packages/mui-base/src/ListboxUnstyled/useListbox.ts index f6d769f103c335..40a96391b717ab 100644 --- a/packages/mui-base/src/ListboxUnstyled/useListbox.ts +++ b/packages/mui-base/src/ListboxUnstyled/useListbox.ts @@ -160,16 +160,15 @@ export default function useListbox(props: UseListboxParameters return; } - const keysToPreventDefault = [ - ' ', - 'Enter', - 'ArrowUp', - 'ArrowDown', - 'Home', - 'End', - 'PageUp', - 'PageDown', - ]; + const keysToPreventDefault = ['ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown']; + + if (focusManagement === 'activeDescendant') { + // When the child element is focused using the activeDescendant attribute, + // the listbox handles keyboard events on its behalf. + // We have to `preventDefault()` is this case to prevent the browser from + // scrolling the view when space is pressed or submitting forms when enter is pressed. + keysToPreventDefault.push(' ', 'Enter'); + } if (keysToPreventDefault.includes(event.key)) { event.preventDefault(); From 5c2ab97ed0a60233298bda5a9651f6e335dd803e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Tue, 21 Jun 2022 13:21:59 +0200 Subject: [PATCH 2/2] Tests --- .../src/ListboxUnstyled/useListbox.test.tsx | 73 ++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/mui-base/src/ListboxUnstyled/useListbox.test.tsx b/packages/mui-base/src/ListboxUnstyled/useListbox.test.tsx index 1b510ccf881553..f3109b79987bb1 100644 --- a/packages/mui-base/src/ListboxUnstyled/useListbox.test.tsx +++ b/packages/mui-base/src/ListboxUnstyled/useListbox.test.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; -import { useListbox } from '@mui/base/ListboxUnstyled'; import { expect } from 'chai'; -import { createRenderer } from 'test/utils'; +import { SinonSpy, spy } from 'sinon'; +import { useListbox } from '@mui/base/ListboxUnstyled'; +import { createRenderer, createEvent, fireEvent } from 'test/utils'; describe('useListbox', () => { const { render } = createRenderer(); @@ -56,4 +57,72 @@ describe('useListbox', () => { expect(listboxes[0].id).not.to.equal(listboxes[1].id); }); }); + + describe('preventing default behavior on keyDown', () => { + ['ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown', 'Enter', ' '].forEach((key) => + it(`prevents default behavior when ${key} is pressed in activeDescendant focus management mode`, () => { + const Listbox = () => { + const { getRootProps } = useListbox({ options: [], focusManagement: 'activeDescendant' }); + return
; + }; + + const { getByRole } = render(); + const listbox = getByRole('listbox'); + listbox.focus(); + + const event = createEvent.keyDown(listbox, { + key, + }); + + event.preventDefault = spy(); + fireEvent(listbox, event); + + expect((event.preventDefault as SinonSpy).calledOnce).to.equal(true); + }), + ); + + ['ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].forEach((key) => + it(`prevents default behavior when ${key} is pressed in DOM focus management mode`, () => { + const Listbox = () => { + const { getRootProps } = useListbox({ options: [], focusManagement: 'DOM' }); + return
; + }; + + const { getByRole } = render(); + const listbox = getByRole('listbox'); + listbox.focus(); + + const event = createEvent.keyDown(listbox, { + key, + }); + + event.preventDefault = spy(); + fireEvent(listbox, event); + + expect((event.preventDefault as SinonSpy).calledOnce).to.equal(true); + }), + ); + + ['Enter', ' '].forEach((key) => + it(`does not prevent default behavior when ${key} is pressed in DOM focus management mode`, () => { + const Listbox = () => { + const { getRootProps } = useListbox({ options: [], focusManagement: 'DOM' }); + return
; + }; + + const { getByRole } = render(); + const listbox = getByRole('listbox'); + listbox.focus(); + + const event = createEvent.keyDown(listbox, { + key, + }); + + event.preventDefault = spy(); + fireEvent(listbox, event); + + expect((event.preventDefault as SinonSpy).notCalled).to.equal(true); + }), + ); + }); });