Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update useSelect to the ARIA 1.2 pattern #1402

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions cypress/e2e/combobox.cy.js
Expand Up @@ -22,7 +22,7 @@ describe('combobox', () => {
it('can arrow up to select last item', () => {
cy.findByTestId('combobox-input')
.type('{uparrow}{enter}') // open menu, last option is focused
.should('have.value', 'Purple')
.should('have.value', 'Skyblue')
})

it('can arrow down to select first item', () => {
Expand All @@ -46,7 +46,7 @@ describe('combobox', () => {
it('can use end arrow to select last item', () => {
cy.findByTestId('combobox-input')
.type('{downarrow}{end}{enter}') // open to first, go to last by end.
.should('have.value', 'Purple')
.should('have.value', 'Skyblue')
})

it('resets the item on blur', () => {
Expand Down
16 changes: 16 additions & 0 deletions cypress/e2e/useMultipleCombobox.cy.js
@@ -0,0 +1,16 @@
describe('useMultipleCombobox', () => {
before(() => {
cy.visit('/useMultipleCombobox')
})

it('can select multiple items', () => {
cy.findByRole('button', {name: 'toggle menu'}).click()
cy.findByRole('option', {name: 'Green'}).click()
cy.findByRole('option', {name: 'Gray'}).click()
cy.findByRole('button', {name: 'toggle menu'}).click()
cy.findByText('Black').should('be.visible')
cy.findByText('Red').should('be.visible')
cy.findByText('Green').should('be.visible')
cy.findByText('Gray').should('be.visible')
})
})
16 changes: 16 additions & 0 deletions cypress/e2e/useMultipleSelect.cy.js
@@ -0,0 +1,16 @@
describe('useMultipleSelect', () => {
before(() => {
cy.visit('/useMultipleSelect')
})

it('can select multiple options', () => {
cy.findByRole('combobox').click()
cy.findByRole('option', {name: 'Green'}).click()
cy.findByRole('option', {name: 'Gray'}).click()
cy.findByRole('combobox').click()
cy.findByText('Black').should('be.visible')
cy.findByText('Red').should('be.visible')
cy.findByText('Green').should('be.visible')
cy.findByText('Gray').should('be.visible')
})
})
6 changes: 3 additions & 3 deletions cypress/e2e/useSelect.cy.js
Expand Up @@ -4,15 +4,15 @@ describe('useSelect', () => {
})

it('can open and close a menu', () => {
cy.findByTestId('select-toggle-button')
cy.findByRole('combobox')
.click()
cy.findAllByRole('option')
.should('have.length.above', 0)
cy.findByTestId('select-toggle-button')
cy.findByRole('combobox')
.click()
cy.findAllByRole('option')
.should('have.length', 0)
cy.findByTestId('select-toggle-button')
cy.findByRole('combobox')
.click()
cy.findAllByRole('option')
.should('have.length.above', 0)
Expand Down
7 changes: 3 additions & 4 deletions docusaurus/pages/combobox.js
@@ -1,10 +1,9 @@
import * as React from 'react'

import Downshift from '../../src'
import {colors} from '../utils'

export default function ComboBox() {
const items = ['Black', 'Red', 'Green', 'Blue', 'Orange', 'Purple']

return (
<Downshift>
{({
Expand Down Expand Up @@ -73,10 +72,10 @@ export default function ComboBox() {
>
{isOpen &&
(inputValue
? items.filter(i =>
? colors.filter(i =>
i.toLowerCase().includes(inputValue.toLowerCase()),
)
: items
: colors
).map((item, index) => (
<li
style={{
Expand Down
7 changes: 3 additions & 4 deletions docusaurus/pages/useCombobox.js
@@ -1,11 +1,10 @@
import React, {useState} from 'react'

import {useCombobox} from '../../src'

const items = ['Black', 'Red', 'Green', 'Blue', 'Orange', 'Purple']
import {colors} from '../utils'

export default function DropdownCombobox() {
const [inputItems, setInputItems] = useState(items)
const [inputItems, setInputItems] = useState(colors)
const {
isOpen,
getToggleButtonProps,
Expand All @@ -21,7 +20,7 @@ export default function DropdownCombobox() {
items: inputItems,
onInputValueChange: ({inputValue}) => {
setInputItems(
items.filter(item =>
colors.filter(item =>
item.toLowerCase().startsWith(inputValue.toLowerCase()),
),
)
Expand Down
4 changes: 2 additions & 2 deletions docusaurus/pages/useMultipleCombobox.js
@@ -1,8 +1,8 @@
import React from 'react'

import {useCombobox, useMultipleSelection} from '../../src'
import {colors} from '../utils'

const colors = ['Black', 'Red', 'Green', 'Blue', 'Orange', 'Purple']
const initialSelectedItems = [colors[0], colors[1]]

function getFilteredItems(selectedItems, inputValue) {
Expand Down Expand Up @@ -166,7 +166,7 @@ export default function DropdownMultipleCombobox() {
</button>
<button
style={{padding: '4px 8px'}}
aria-label="toggle menu"
aria-label="clear selection"
data-testid="clear-button"
onClick={clearSelection}
>
Expand Down
22 changes: 14 additions & 8 deletions docusaurus/pages/useMultipleSelect.js
@@ -1,8 +1,8 @@
import React from 'react'

import {useSelect, useMultipleSelection} from '../../src'
import {colors} from '../utils'

const colors = ['Black', 'Red', 'Green', 'Blue', 'Orange', 'Purple']
const initialSelectedItems = [colors[0], colors[1]]

function getFilteredItems(selectedItems) {
Expand Down Expand Up @@ -33,8 +33,8 @@ export default function DropdownMultipleSelect() {
stateReducer: (state, actionAndChanges) => {
const {changes, type} = actionAndChanges
switch (type) {
case useSelect.stateChangeTypes.MenuKeyDownEnter:
case useSelect.stateChangeTypes.MenuKeyDownSpaceButton:
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
case useSelect.stateChangeTypes.ItemClick:
return {
...changes,
Expand All @@ -46,8 +46,8 @@ export default function DropdownMultipleSelect() {
},
onStateChange: ({type, selectedItem: newSelectedItem}) => {
switch (type) {
case useSelect.stateChangeTypes.MenuKeyDownEnter:
case useSelect.stateChangeTypes.MenuKeyDownSpaceButton:
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
case useSelect.stateChangeTypes.ItemClick:
if (newSelectedItem) {
addSelectedItem(newSelectedItem)
Expand Down Expand Up @@ -122,15 +122,21 @@ export default function DropdownMultipleSelect() {
</span>
)
})}
<button
style={{padding: '4px'}}
<div
style={{
padding: '4px',
textAlign: 'center',
border: '1px solid black',
backgroundColor: 'lightgray',
cursor: 'pointer',
}}
type="button"
{...getToggleButtonProps(
getDropdownProps({preventKeyAction: isOpen}),
)}
>
Pick some colors {isOpen ? <>&#8593;</> : <>&#8595;</>}
</button>
</div>
</div>
</div>
<ul
Expand Down
20 changes: 12 additions & 8 deletions docusaurus/pages/useSelect.js
@@ -1,8 +1,7 @@
import React from 'react'

import {useSelect} from '../../src'

const items = ['Black', 'Red', 'Green', 'Blue', 'Orange', 'Purple']
import {colors} from '../utils'

export default function DropdownSelect() {
const {
Expand All @@ -13,7 +12,7 @@ export default function DropdownSelect() {
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({items})
} = useSelect({items: colors})

return (
<div
Expand All @@ -35,14 +34,19 @@ export default function DropdownSelect() {
>
Choose an element:
</label>
<button
data-testid="select-toggle-button"
style={{padding: '4px'}}
<div
style={{
padding: '4px',
textAlign: 'center',
border: '1px solid black',
backgroundColor: 'lightgray',
cursor: 'pointer',
}}
{...getToggleButtonProps()}
>
{selectedItem ?? 'Elements'}
{isOpen ? <>&#8593;</> : <>&#8595;</>}
</button>
</div>
<ul
{...getMenuProps()}
style={{
Expand All @@ -53,7 +57,7 @@ export default function DropdownSelect() {
}}
>
{isOpen &&
items.map((item, index) => (
colors.map((item, index) => (
<li
style={{
padding: '4px',
Expand Down
1 change: 1 addition & 0 deletions docusaurus/utils.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -88,7 +88,7 @@
"@testing-library/preact": "^2.0.1",
"@testing-library/react": "^12.0.0",
"@testing-library/react-hooks": "^7.0.1",
"@testing-library/user-event": "^13.2.1",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^26.0.24",
"@types/react": "^17.0.15",
"@typescript-eslint/eslint-plugin": "^4.28.5",
Expand Down
1 change: 0 additions & 1 deletion src/hooks/reducer.js
@@ -1,6 +1,5 @@
import {getHighlightedIndexOnOpen, getDefaultValue} from './utils'

/* eslint-disable complexity */
export default function downshiftCommonReducer(
state,
action,
Expand Down
74 changes: 67 additions & 7 deletions src/hooks/testUtils.js
@@ -1,7 +1,8 @@
import React from 'react'
import {act} from '@testing-library/react'
import {screen, act} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

const items = [
export const items = [
'Neptunium',
'Plutonium',
'Americium',
Expand Down Expand Up @@ -30,22 +31,30 @@ const items = [
'Oganesson',
]

const defaultIds = {
export const dataTestIds = {
toggleButton: 'toggle-button-id',
menu: 'menu-id',
item: index => `item-id-${index}`,
input: 'input-id',
selectedItemPrefix: 'selected-item-id',
selectedItem: index => `selected-item-id-${index}`,
}

export const defaultIds = {
labelId: 'downshift-test-id-label',
menuId: 'downshift-test-id-menu',
getItemId: index => `downshift-test-id-item-${index}`,
toggleButtonId: 'downshift-test-id-toggle-button',
inputId: 'downshift-test-id-input',
}

const waitForDebouncedA11yStatusUpdate = () =>
export const waitForDebouncedA11yStatusUpdate = () =>
act(() => jest.advanceTimersByTime(200))

const MemoizedItem = React.memo(function Item({
export const MemoizedItem = React.memo(function Item({
index,
item,
getItemProps,
dataTestIds,
stringItem,
...rest
}) {
Expand All @@ -60,4 +69,55 @@ const MemoizedItem = React.memo(function Item({
)
})

export {items, defaultIds, waitForDebouncedA11yStatusUpdate, MemoizedItem}
export const user = userEvent.setup({delay: null})

export function getLabel() {
return screen.getByText(/choose an element/i)
}
export function getMenu() {
return screen.getByRole('listbox')
}
export function getToggleButton() {
return screen.getByTestId(dataTestIds.toggleButton)
}
export function getItemAtIndex(index) {
return getItems()[index]
}
export function getItems() {
return screen.queryAllByRole('option')
}
export function getInput() {
return screen.getByRole('textbox')
}
export async function clickOnItemAtIndex(index) {
await user.click(getItemAtIndex(index))
}
export async function clickOnToggleButton() {
await user.click(getToggleButton())
}
export async function mouseMoveItemAtIndex(index) {
await user.hover(getItemAtIndex(index))
}
export async function mouseLeaveItemAtIndex(index) {
await user.unhover(getItemAtIndex(index))
}
export async function keyDownOnToggleButton(keys) {
if (document.activeElement !== getToggleButton()) {
getToggleButton().focus()
}

await user.keyboard(keys)
}
export async function keyDownOnInput(keys) {
if (document.activeElement !== getInput()) {
getInput().focus()
}

await user.keyboard(keys)
}
export function getA11yStatusContainer() {
return screen.queryByRole('status')
}
export async function tab(shiftKey = false) {
await user.tab({shift: shiftKey})
}
5 changes: 2 additions & 3 deletions src/hooks/useCombobox/__tests__/getComboboxProps.test.js
@@ -1,8 +1,7 @@
import {act, renderHook} from '@testing-library/react-hooks'
import {noop} from '../../../utils'
import {renderUseCombobox} from '../testUtils'
import {defaultIds, items} from '../../testUtils'
import useCombobox from '..' // eslint-disable-next-line import/default
import {renderUseCombobox, defaultIds, items} from '../testUtils'
import useCombobox from '..'
import utils from '../../utils'

describe('getComboboxProps', () => {
Expand Down