From 20f07ae022f8333786a9b17058cc19c6556ad1e7 Mon Sep 17 00:00:00 2001 From: Mohammer5 Date: Wed, 23 Oct 2024 16:45:32 +0800 Subject: [PATCH] fix(select a11y): make PageUp keypress respect disabled options --- collections/forms/i18n/en.pot | 4 +- .../single-select-a11y.e2e.stories.js | 4 +- .../use-handle-key-press.js | 12 +- ...highlight-first-option-on-previous-page.js | 63 ++++++++++ .../use-highlight-first-visible-option.js | 38 ++++++ .../use-handle-key-press/use-page-down.js | 50 ++++++++ .../use-handle-key-press/use-page-up-down.js | 117 ------------------ .../use-handle-key-press/use-page-up.js | 50 ++++++++ 8 files changed, 215 insertions(+), 123 deletions(-) create mode 100644 components/select/src/single-select-a11y/use-handle-key-press/use-highlight-first-option-on-previous-page.js create mode 100644 components/select/src/single-select-a11y/use-handle-key-press/use-highlight-first-visible-option.js create mode 100644 components/select/src/single-select-a11y/use-handle-key-press/use-page-down.js delete mode 100644 components/select/src/single-select-a11y/use-handle-key-press/use-page-up-down.js create mode 100644 components/select/src/single-select-a11y/use-handle-key-press/use-page-up.js diff --git a/collections/forms/i18n/en.pot b/collections/forms/i18n/en.pot index 1c0f7570c..577f10009 100644 --- a/collections/forms/i18n/en.pot +++ b/collections/forms/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-10-23T01:23:16.733Z\n" -"PO-Revision-Date: 2024-10-23T01:23:16.734Z\n" +"POT-Creation-Date: 2024-10-23T07:57:36.332Z\n" +"PO-Revision-Date: 2024-10-23T07:57:36.333Z\n" msgid "Upload file" msgstr "Upload file" diff --git a/components/select/src/single-select-a11y/single-select-a11y.e2e.stories.js b/components/select/src/single-select-a11y/single-select-a11y.e2e.stories.js index d4a4b6df1..b9f622e9a 100644 --- a/components/select/src/single-select-a11y/single-select-a11y.e2e.stories.js +++ b/components/select/src/single-select-a11y/single-select-a11y.e2e.stories.js @@ -176,12 +176,12 @@ export const HundretOptions = () => { } export const HundretOptionsWithDisabled = () => { - const [value, setValue] = useState('0') + const [value, setValue] = useState('10') const [hundretOptions] = useState( Array.apply(null, Array(100)).map((x, i) => ({ value: `${i}`, label: `Select option ${i}`, - disabled: i === 17 || i === 18, + disabled: i === 1 || i === 17 || i === 18, })) ) diff --git a/components/select/src/single-select-a11y/use-handle-key-press/use-handle-key-press.js b/components/select/src/single-select-a11y/use-handle-key-press/use-handle-key-press.js index 6ac3b1538..04a978763 100644 --- a/components/select/src/single-select-a11y/use-handle-key-press/use-handle-key-press.js +++ b/components/select/src/single-select-a11y/use-handle-key-press/use-handle-key-press.js @@ -1,6 +1,7 @@ import { useCallback } from 'react' import { useHandleTyping } from './use-handle-typing.js' -import { usePageUpDown } from './use-page-up-down.js' +import { usePageDown } from './use-page-down.js' +import { usePageUp } from './use-page-up.js' export function useHandleKeyPress({ value, @@ -21,7 +22,14 @@ export function useHandleKeyPress({ onChange, }) - const { pageDown, pageUp } = usePageUpDown({ + const pageDown = usePageDown({ + options, + focussedOptionIndex, + setFocussedOptionIndex, + listBoxRef, + }) + + const pageUp = usePageUp({ options, listBoxRef, focussedOptionIndex, diff --git a/components/select/src/single-select-a11y/use-handle-key-press/use-highlight-first-option-on-previous-page.js b/components/select/src/single-select-a11y/use-handle-key-press/use-highlight-first-option-on-previous-page.js new file mode 100644 index 000000000..494a6ba51 --- /dev/null +++ b/components/select/src/single-select-a11y/use-handle-key-press/use-highlight-first-option-on-previous-page.js @@ -0,0 +1,63 @@ +import { useCallback } from 'react' +import { isOptionHidden } from '../is-option-hidden.js' + +export function useHighlightFirstOptionOnPreviousPage({ + options, + focussedOptionIndex, + setFocussedOptionIndex, + listBoxRef, +}) { + return useCallback(() => { + const listBoxParent = listBoxRef.current.parentNode + const optionElements = Array.from(listBoxRef.current.childNodes) + const visibleOptionsAmount = optionElements.filter( + (optionElement) => !isOptionHidden(optionElement, listBoxParent) + ).length + + const nextTopOptionIndex = Math.max( + 0, + focussedOptionIndex - visibleOptionsAmount + ) + + // If there's no next option and we already have the last option in the list highlighted + if (!options[nextTopOptionIndex]) { + return + } + + if (!options[nextTopOptionIndex].disabled) { + const nextTopOption = optionElements[nextTopOptionIndex] + const scrollPosition = nextTopOption.offsetTop + listBoxParent.scrollTop = scrollPosition + setFocussedOptionIndex(nextTopOptionIndex) + return + } + + const lowerEnabledOptionIndex = options + .slice(0, nextTopOptionIndex) + .findLastIndex((option) => !option.disabled) + + if (lowerEnabledOptionIndex !== -1) { + const lowerEnabledOption = optionElements[lowerEnabledOptionIndex] + const lowerScrollPosition = lowerEnabledOption.offsetTop + listBoxParent.scrollTop = lowerScrollPosition + setFocussedOptionIndex(lowerEnabledOptionIndex) + return + } + + const inbetweenEnabledOptionIndex = + nextTopOptionIndex + + options + .slice(nextTopOptionIndex, focussedOptionIndex) + .findIndex((option) => !option.disabled) + + if (inbetweenEnabledOptionIndex === -1) { + // We're already on the first enabled option + return + } + + const inbetweenTopOption = optionElements[inbetweenEnabledOptionIndex] + const scrollPosition = inbetweenTopOption.offsetTop + listBoxParent.scrollTop = scrollPosition + setFocussedOptionIndex(inbetweenEnabledOptionIndex) + }, [options, focussedOptionIndex, setFocussedOptionIndex, listBoxRef]) +} diff --git a/components/select/src/single-select-a11y/use-handle-key-press/use-highlight-first-visible-option.js b/components/select/src/single-select-a11y/use-handle-key-press/use-highlight-first-visible-option.js new file mode 100644 index 000000000..ef2746485 --- /dev/null +++ b/components/select/src/single-select-a11y/use-handle-key-press/use-highlight-first-visible-option.js @@ -0,0 +1,38 @@ +import { useCallback } from 'react' + +export function useHighlightFirstVisibleOption({ + options, + focussedOptionIndex, + setFocussedOptionIndex, +}) { + return useCallback( + (lowestVisibleIndex) => { + if (!options[lowestVisibleIndex].disabled) { + setFocussedOptionIndex(lowestVisibleIndex) + return + } + + const previousEnabledOptionIndex = options + .slice(0, lowestVisibleIndex) + .findLastIndex((option) => !option.disabled) + + if (previousEnabledOptionIndex !== -1) { + setFocussedOptionIndex(previousEnabledOptionIndex) + return + } + + const nextEnabledOptionIndex = + lowestVisibleIndex + + options + .slice(lowestVisibleIndex, focussedOptionIndex) + .findIndex((option) => !option.disabled) + + if (nextEnabledOptionIndex !== -1) { + return + } + + setFocussedOptionIndex(nextEnabledOptionIndex) + }, + [options, focussedOptionIndex, setFocussedOptionIndex] + ) +} diff --git a/components/select/src/single-select-a11y/use-handle-key-press/use-page-down.js b/components/select/src/single-select-a11y/use-handle-key-press/use-page-down.js new file mode 100644 index 000000000..8e373b29b --- /dev/null +++ b/components/select/src/single-select-a11y/use-handle-key-press/use-page-down.js @@ -0,0 +1,50 @@ +import { useCallback } from 'react' +import { isOptionHidden } from '../is-option-hidden.js' +import { useHighlightLastOptionOnNextPage } from './use-highlight-last-option-on-next-page.js' +import { useHighlightLastVisibleOption } from './use-highlight-last-visible-option.js' + +export function usePageDown({ + options, + focussedOptionIndex, + setFocussedOptionIndex, + listBoxRef, +}) { + const highlightLastVisibleOption = useHighlightLastVisibleOption({ + options, + focussedOptionIndex, + setFocussedOptionIndex, + }) + + const highlightLastOptionOnNextPage = useHighlightLastOptionOnNextPage({ + options, + focussedOptionIndex, + setFocussedOptionIndex, + listBoxRef, + }) + + return useCallback(() => { + const listBoxParent = listBoxRef.current.parentNode + const options = Array.from(listBoxRef.current.childNodes) + const highestVisibleIndex = options.findLastIndex( + (option) => !isOptionHidden(option, listBoxParent) + ) + + if (highestVisibleIndex > focussedOptionIndex) { + highlightLastVisibleOption(highestVisibleIndex) + return + } + + if (highestVisibleIndex > -1) { + highlightLastOptionOnNextPage(listBoxParent) + return + } + + // No visible option (e.g. when menu is empty) + return + }, [ + focussedOptionIndex, + listBoxRef, + highlightLastVisibleOption, + highlightLastOptionOnNextPage, + ]) +} diff --git a/components/select/src/single-select-a11y/use-handle-key-press/use-page-up-down.js b/components/select/src/single-select-a11y/use-handle-key-press/use-page-up-down.js deleted file mode 100644 index 636bf7dc9..000000000 --- a/components/select/src/single-select-a11y/use-handle-key-press/use-page-up-down.js +++ /dev/null @@ -1,117 +0,0 @@ -import { useCallback } from 'react' -import { isOptionHidden } from '../is-option-hidden.js' -import { useHighlightLastOptionOnNextPage } from './use-highlight-last-option-on-next-page.js' -import { useHighlightLastVisibleOption } from './use-highlight-last-visible-option.js' - -function usePageDown({ - options, - focussedOptionIndex, - setFocussedOptionIndex, - listBoxRef, -}) { - const highlightLastVisibleOption = useHighlightLastVisibleOption({ - options, - focussedOptionIndex, - setFocussedOptionIndex, - }) - - const highlightLastOptionOnNextPage = useHighlightLastOptionOnNextPage({ - options, - focussedOptionIndex, - setFocussedOptionIndex, - listBoxRef, - }) - - return useCallback(() => { - const listBoxParent = listBoxRef.current.parentNode - const options = Array.from(listBoxRef.current.childNodes) - const highestVisibleIndex = options.findLastIndex( - (option) => !isOptionHidden(option, listBoxParent) - ) - - if (highestVisibleIndex > focussedOptionIndex) { - highlightLastVisibleOption(highestVisibleIndex) - return - } - - if (highestVisibleIndex > -1) { - highlightLastOptionOnNextPage(listBoxParent) - return - } - - // No visible option (e.g. when menu is empty) - return - }, [ - focussedOptionIndex, - listBoxRef, - highlightLastVisibleOption, - highlightLastOptionOnNextPage, - ]) -} - -function usePageUp({ - listBoxRef, - focussedOptionIndex, - setFocussedOptionIndex, -}) { - return useCallback(() => { - const listBoxParent = listBoxRef.current.parentNode - const options = Array.from(listBoxRef.current.childNodes) - const lowestVisibleIndex = options.findIndex( - (option) => !isOptionHidden(option, listBoxParent) - ) - - // No visible option (e.g. when menu is empty) - if (lowestVisibleIndex === -1) { - return - } - - // Highlight last visible option - if (lowestVisibleIndex < focussedOptionIndex) { - setFocussedOptionIndex(lowestVisibleIndex) - return - } - - const visibleOptionsAmount = options.filter( - (option) => !isOptionHidden(option, listBoxParent) - ).length - - const nextTopOptionIndex = Math.max( - 0, - focussedOptionIndex - visibleOptionsAmount - ) - - // If there's no next option and we already have the last option in the list highlighted - if (!options[nextTopOptionIndex]) { - return - } - - const nextTopOption = options[nextTopOptionIndex] - const scrollPosition = nextTopOption.offsetTop - listBoxParent.scrollTop = scrollPosition - setFocussedOptionIndex(nextTopOptionIndex) - }, [focussedOptionIndex, setFocussedOptionIndex, listBoxRef]) -} - -export function usePageUpDown({ - options, - listBoxRef, - focussedOptionIndex, - setFocussedOptionIndex, -}) { - const pageDown = usePageDown({ - options, - focussedOptionIndex, - setFocussedOptionIndex, - listBoxRef, - }) - - const pageUp = usePageUp({ - options, - listBoxRef, - focussedOptionIndex, - setFocussedOptionIndex, - }) - - return { pageDown, pageUp } -} diff --git a/components/select/src/single-select-a11y/use-handle-key-press/use-page-up.js b/components/select/src/single-select-a11y/use-handle-key-press/use-page-up.js new file mode 100644 index 000000000..fd8e871e9 --- /dev/null +++ b/components/select/src/single-select-a11y/use-handle-key-press/use-page-up.js @@ -0,0 +1,50 @@ +import { useCallback } from 'react' +import { isOptionHidden } from '../is-option-hidden.js' +import { useHighlightFirstOptionOnPreviousPage } from './use-highlight-first-option-on-previous-page.js' +import { useHighlightFirstVisibleOption } from './use-highlight-first-visible-option.js' + +export function usePageUp({ + options, + listBoxRef, + focussedOptionIndex, + setFocussedOptionIndex, +}) { + const highlightFirstVisibleOption = useHighlightFirstVisibleOption({ + options, + focussedOptionIndex, + setFocussedOptionIndex, + }) + + const highlightFirstOptionOnPreviousPage = + useHighlightFirstOptionOnPreviousPage({ + options, + focussedOptionIndex, + setFocussedOptionIndex, + listBoxRef, + }) + + return useCallback(() => { + const listBoxParent = listBoxRef.current.parentNode + const optionElements = Array.from(listBoxRef.current.childNodes) + const lowestVisibleIndex = optionElements.findIndex( + (optionElement) => !isOptionHidden(optionElement, listBoxParent) + ) + + // No visible option (e.g. when menu is empty) + if (lowestVisibleIndex === -1) { + return + } + + if (lowestVisibleIndex < focussedOptionIndex) { + highlightFirstVisibleOption(lowestVisibleIndex) + return + } + + highlightFirstOptionOnPreviousPage() + }, [ + focussedOptionIndex, + listBoxRef, + highlightFirstOptionOnPreviousPage, + highlightFirstVisibleOption, + ]) +}