Skip to content

Commit

Permalink
fix(select a11y): make PageUp keypress respect disabled options
Browse files Browse the repository at this point in the history
  • Loading branch information
Mohammer5 committed Oct 23, 2024
1 parent c3bd419 commit 20f07ae
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 123 deletions.
4 changes: 2 additions & 2 deletions collections/forms/i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}))
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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])
}
Original file line number Diff line number Diff line change
@@ -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]
)
}
Original file line number Diff line number Diff line change
@@ -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,
])
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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,
])
}

0 comments on commit 20f07ae

Please sign in to comment.