Skip to content

Commit

Permalink
feat(getA11yStatusMessage): simplify the a11y message in hooks (#1579)
Browse files Browse the repository at this point in the history
* wip

* finish implementation

* fix test

* update types

* update readmes

* some cleanups

* migration guide
  • Loading branch information
silviuaavram committed Mar 9, 2024
1 parent f79c593 commit 7d687be
Show file tree
Hide file tree
Showing 22 changed files with 401 additions and 847 deletions.
2 changes: 1 addition & 1 deletion src/__mocks__/set-a11y-status.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
module.exports = jest.fn()
module.exports = {setStatus: jest.fn(), cleanupStatusDiv: jest.fn()}
8 changes: 4 additions & 4 deletions src/__tests__/downshift.lifecycle.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react'
import {act, fireEvent, render, screen} from '@testing-library/react'
import Downshift from '../'
import setA11yStatus from '../set-a11y-status'
import {setStatus} from '../set-a11y-status'
import * as utils from '../utils'

jest.useFakeTimers()
Expand Down Expand Up @@ -124,7 +124,7 @@ test('handles state change for touchevent events', () => {
})

test('props update causes the a11y status to be updated', () => {
setA11yStatus.mockReset()
setStatus.mockReset()
const MyComponent = () => (
<Downshift isOpen={false}>
{({getInputProps, getItemProps, isOpen}) => (
Expand All @@ -139,11 +139,11 @@ test('props update causes the a11y status to be updated', () => {
const {container, unmount} = render(<MyComponent />)
render(<MyComponent isOpen={true} />, {container})
jest.runAllTimers()
expect(setA11yStatus).toHaveBeenCalledTimes(1)
expect(setStatus).toHaveBeenCalledTimes(1)
render(<MyComponent isOpen={false} />, {container})
unmount()
jest.runAllTimers()
expect(setA11yStatus).toHaveBeenCalledTimes(1)
expect(setStatus).toHaveBeenCalledTimes(1)
})

test('inputValue initializes properly if the selectedItem is controlled and set', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/set-a11y-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,5 @@ test('creates no status div if there is no document', () => {

function setup() {
jest.resetModules()
return require('../set-a11y-status').default
return require('../set-a11y-status').setStatus
}
4 changes: 2 additions & 2 deletions src/downshift.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
import {Component, cloneElement} from 'react'
import {isForwardRef} from 'react-is'
import {isPreact, isReactNative, isReactNativeWeb} from './is.macro'
import setA11yStatus from './set-a11y-status'
import {setStatus} from './set-a11y-status'
import * as stateChangeTypes from './stateChangeTypes'
import {
handleRefs,
Expand Down Expand Up @@ -1057,7 +1057,7 @@ class Downshift extends Component {
})
this.previousResultCount = resultCount

setA11yStatus(status, this.props.environment.document)
setStatus(status, this.props.environment.document)
}, 200)

componentDidMount() {
Expand Down
50 changes: 49 additions & 1 deletion src/hooks/MIGRATION_V9.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ hooks and are detailed below.
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [onChange Typescript Improvements](#onchange-typescript-improvements)
- [getA11ySelectionMessage](#geta11yselectionmessage)
- [getA11yRemovalMessage](#geta11yremovalmessage)
- [getA11yStatusMessage](#geta11ystatusmessage)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand All @@ -31,4 +34,49 @@ get called with their corresponding state prop:

- useMultipleSelection
- onActiveIndexChange: activeIndex is non optional
- onSelectedItemsChange: selectedItems is non optional
- onSelectedItemsChange: selectedItems is non optional

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## getA11ySelectionMessage

The prop has been removed from useSelect and useCombobox. If you still need an
a11y selection message, use either `getA11yStatusMessage` or your own aria-live
implementation inside a `onStateChange` callback.

## getA11yRemovalMessage

The prop has been removed from useMultipleSelection. If you still need an a11y
removal message, use either `getA11yStatusMessage` or your own aria-live
implementation inside a `onStateChange` callback.

## getA11yStatusMessage

The prop has been also added to useMultipleSelection, but has some changes
reflected in each of the hook's readme.

- there is no default function provided, so you will not get any aria-live
message anymore if you don't provide the prop directly to the hooks.
- the function is called only with the hook's state, and you should already have
access to the props, such as items or itemToString. Values such as
highlightedItem or resultsCount have been removed, so you need to compute them
yourself if needed.
- `Downshift` is not affected, it has the same `getA11yStatusMessage` as before,
no changes there at all.

The HTML markup with the ARIA attributes we provide through the getter props
should be enough for screen readers to report:

- results count.
- highlighted item.
- item selection.
- what actions the user can take.

If you need anything more specific as part of an aria-live region, please use
the new version of `getA11yStatusMessage` or your own aria-live implementation.

References:

- [useCombobox docs](https://github.com/downshift-js/downshift/blob/master/src/hooks/useCombobox/README.md#geta11ystatusmessage)
- [useSelect docs](https://github.com/downshift-js/downshift/blob/master/src/hooks/useSelect/README.md#geta11ystatusmessage)
- [useMultipleSelection docs](https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/README.md#geta11ystatusmessage)
4 changes: 2 additions & 2 deletions src/hooks/testUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export const defaultIds = {
inputId: 'downshift-test-id-input',
}

export const waitForDebouncedA11yStatusUpdate = () =>
act(() => jest.advanceTimersByTime(200))
export const waitForDebouncedA11yStatusUpdate = (shouldBeCleared = false) =>
act(() => jest.advanceTimersByTime(shouldBeCleared ? 700 : 200))

export const MemoizedItem = React.memo(function Item({
index,
Expand Down
62 changes: 28 additions & 34 deletions src/hooks/useCombobox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ and update if necessary.
- [itemToKey](#itemtokey)
- [selectedItemChanged](#selecteditemchanged)
- [getA11yStatusMessage](#geta11ystatusmessage)
- [getA11ySelectionMessage](#geta11yselectionmessage)
- [onHighlightedIndexChange](#onhighlightedindexchange)
- [onIsOpenChange](#onisopenchange)
- [onInputValueChange](#oninputvaluechange)
Expand Down Expand Up @@ -454,45 +453,40 @@ Used to determine if the new `selectedItem` has changed compared to the previous
> `function({/* see below */})` | default messages provided in English
This function is passed as props to a status updating function nested within
that allows you to create your own ARIA statuses. It is called when one of the
following props change: `items`, `highlightedIndex`, `inputValue` or `isOpen`.
that allows you to create your own ARIA statuses. It is called when the state
changes: `selectedItem`, `highlightedIndex`, `inputValue` or `isOpen`.

A default `getA11yStatusMessage` function is provided that will check
`resultCount` and return "No results are available." or if there are results ,
"`resultCount` results are available, use up and down arrow keys to navigate.
Press Enter key to select."
There is no default function provided anymore since v9, so if there's no prop
passed, no aria live status message is created. An implementation that resembles
the previous default is written below, should you want to keep pre v9 behaviour.

> Note: `resultCount` is `items.length` in our default version of the function.
We don't provide this as a default anymore since we consider that screen readers
have been significantly improved and they can convey information about items
count, possible actions and highlighted items only from the HTML markup, without
the need for aria-live regions.

### getA11ySelectionMessage

> `function({/* see below */})` | default messages provided in English
This function is similar to the `getA11yStatusMessage` but it is generating a
message when an item is selected. It is passed as props to a status updating
function nested within that allows you to create your own ARIA statuses. It is
called when `selectedItem` changes.

A default `getA11ySelectionMessage` function is provided. When an item is
selected, the message is a selection related one, narrating
"`itemToString(selectedItem)` has been selected".
```js
function getA11yStatusMessage(state) {
if (!state.isOpen) {
return ''
}
// you need to get resultCount and previousResultCount yourself now, since we don't pass them as arguments anymore
const resultCount = items.length
const previousResultCount = previousResultCountRef.current

The object you are passed to generate your status message, for both
`getA11yStatusMessage` and `getA11ySelectionMessage`, has the following
properties:
if (!resultCount) {
return 'No results are available.'
}

<!-- This table was generated via http://www.tablesgenerator.com/markdown_tables -->
if (resultCount !== previousResultCount) {
return `${resultCount} result${
resultCount === 1 ? ' is' : 's are'
} available, use up and down arrow keys to navigate. Press Enter key to select.`
}

| property | type | description |
| --------------------- | --------------- | -------------------------------------------------------------------------------------------- |
| `highlightedIndex` | `number` | The currently highlighted index |
| `highlightedItem` | `any` | The value of the highlighted item |
| `isOpen` | `boolean` | The `isOpen` state |
| `inputValue` | `string` | The value in the text input. |
| `itemToString` | `function(any)` | The `itemToString` function (see props) for getting the string value from one of the options |
| `previousResultCount` | `number` | The total items showing in the dropdown the last time the status was updated |
| `resultCount` | `number` | The total items showing in the dropdown |
| `selectedItem` | `any` | The value of the currently selected item |
return ''
}
```

### onHighlightedIndexChange

Expand Down
Loading

0 comments on commit 7d687be

Please sign in to comment.