No flags found
Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.
e.g., #unittest #integration
#production #enterprise
#frontend #backend
c188cc1
... +2 ...
3bff9d5
Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.
e.g., #unittest #integration
#production #enterprise
#frontend #backend
1 | 1 | import * as React from 'react' |
|
2 | - | import {render, fireEvent, screen} from '@testing-library/react' |
|
2 | + | import {render, act} from '@testing-library/react' |
|
3 | 3 | import {renderHook} from '@testing-library/react-hooks' |
|
4 | - | import userEvent from '@testing-library/user-event' |
|
5 | 4 | import {defaultProps} from '../utils' |
|
6 | - | import {items} from '../testUtils' |
|
5 | + | import { |
|
6 | + | clickOnItemAtIndex, |
|
7 | + | clickOnToggleButton, |
|
8 | + | dataTestIds, |
|
9 | + | items, |
|
10 | + | keyDownOnToggleButton, |
|
11 | + | mouseLeaveItemAtIndex, |
|
12 | + | mouseMoveItemAtIndex, |
|
13 | + | tab, |
|
14 | + | } from '../testUtils' |
|
15 | + | import * as stateChangeTypes from './stateChangeTypes' |
|
7 | 16 | import useSelect from '.' |
|
8 | 17 | ||
18 | + | export * from '../testUtils' |
|
19 | + | ||
9 | 20 | jest.mock('../../utils', () => { |
|
10 | 21 | const utils = jest.requireActual('../../utils') |
|
11 | 22 |
28 | 39 | beforeEach(jest.resetAllMocks) |
|
29 | 40 | afterAll(jest.restoreAllMocks) |
|
30 | 41 | ||
31 | - | const dataTestIds = { |
|
32 | - | toggleButton: 'toggle-button-id', |
|
33 | - | menu: 'menu-id', |
|
34 | - | item: index => `item-id-${index}`, |
|
35 | - | } |
|
36 | - | ||
37 | - | const renderUseSelect = props => { |
|
42 | + | export function renderUseSelect(props) { |
|
38 | 43 | return renderHook(() => useSelect({items, ...props})) |
|
39 | 44 | } |
|
40 | - | ||
41 | - | const renderSelect = (props, uiCallback) => { |
|
45 | + | export function renderSelect(props, uiCallback) { |
|
42 | 46 | const renderSpy = jest.fn() |
|
43 | 47 | const ui = <DropdownSelect renderSpy={renderSpy} {...props} /> |
|
44 | 48 | const utils = render(uiCallback ? uiCallback(ui) : ui) |
|
45 | 49 | const rerender = p => |
|
46 | 50 | utils.rerender(<DropdownSelect renderSpy={renderSpy} {...p} />) |
|
47 | - | const label = screen.getByText(/choose an element/i) |
|
48 | - | const menu = screen.getByRole('listbox') |
|
49 | - | const toggleButton = screen.getByTestId(dataTestIds.toggleButton) |
|
50 | - | const getItemAtIndex = index => screen.getByTestId(dataTestIds.item(index)) |
|
51 | - | const getItems = () => screen.queryAllByRole('option') |
|
52 | - | const clickOnItemAtIndex = index => { |
|
53 | - | fireEvent.click(getItemAtIndex(index)) |
|
54 | - | } |
|
55 | - | const clickOnToggleButton = () => { |
|
56 | - | fireEvent.click(toggleButton) |
|
57 | - | } |
|
58 | - | const mouseMoveItemAtIndex = index => { |
|
59 | - | fireEvent.mouseMove(getItemAtIndex(index)) |
|
60 | - | } |
|
61 | - | const keyDownOnToggleButton = (key, options = {}) => { |
|
62 | - | fireEvent.keyDown(toggleButton, {key, ...options}) |
|
63 | - | } |
|
64 | - | const keyDownOnMenu = (key, options = {}) => { |
|
65 | - | fireEvent.keyDown(menu, {key, ...options}) |
|
66 | - | } |
|
67 | - | const blurMenu = () => { |
|
68 | - | fireEvent.blur(menu) |
|
69 | - | } |
|
70 | - | const getA11yStatusContainer = () => screen.queryByRole('status') |
|
71 | - | const mouseLeaveMenu = () => { |
|
72 | - | userEvent.unhover(menu) |
|
73 | - | } |
|
74 | - | const tab = (shiftKey = false) => { |
|
75 | - | userEvent.tab({shift: shiftKey}) |
|
76 | - | } |
|
77 | 51 | ||
78 | 52 | return { |
|
79 | 53 | ...utils, |
|
80 | 54 | renderSpy, |
|
81 | 55 | rerender, |
|
82 | - | label, |
|
83 | - | menu, |
|
84 | - | toggleButton, |
|
85 | - | getItemAtIndex, |
|
86 | - | clickOnItemAtIndex, |
|
87 | - | mouseMoveItemAtIndex, |
|
88 | - | getItems, |
|
89 | - | keyDownOnToggleButton, |
|
90 | - | clickOnToggleButton, |
|
91 | - | blurMenu, |
|
92 | - | getA11yStatusContainer, |
|
93 | - | mouseLeaveMenu, |
|
94 | - | keyDownOnMenu, |
|
95 | - | tab, |
|
96 | 56 | } |
|
97 | 57 | } |
|
98 | 58 | ||
99 | - | const DropdownSelect = ({renderSpy, renderItem, ...props}) => { |
|
59 | + | export function DropdownSelect({renderSpy, renderItem, ...props}) { |
|
100 | 60 | const { |
|
101 | 61 | isOpen, |
|
102 | 62 | selectedItem, |
105 | 65 | getMenuProps, |
|
106 | 66 | getItemProps, |
|
107 | 67 | } = useSelect({items, ...props}) |
|
108 | - | const {itemToString} = props.itemToString ? props : defaultProps |
|
68 | + | const itemToString = props?.itemToString ?? defaultProps.itemToString |
|
109 | 69 | ||
110 | 70 | renderSpy() |
|
111 | 71 |
126 | 86 | const stringItem = |
|
127 | 87 | item instanceof Object ? itemToString(item) : item |
|
128 | 88 | return renderItem ? ( |
|
129 | - | renderItem({index, item, getItemProps, dataTestIds, stringItem}) |
|
89 | + | renderItem({index, item, getItemProps, stringItem}) |
|
130 | 90 | ) : ( |
|
131 | 91 | <li |
|
132 | 92 | data-testid={dataTestIds.item(index)} |
142 | 102 | ) |
|
143 | 103 | } |
|
144 | 104 | ||
145 | - | export {items, renderUseSelect, renderSelect, DropdownSelect} |
|
105 | + | /** |
|
106 | + | * Return the id of the item that strats with the caracter. |
|
107 | + | * @param {string} character The start of the item string. |
|
108 | + | * @param {number} startIndex The index to start searching. |
|
109 | + | * @returns number The index of the item. |
|
110 | + | */ |
|
111 | + | export function getItemIndexByCharacter(character, startIndex = 0) { |
|
112 | + | return ( |
|
113 | + | items.slice(startIndex).findIndex(item => { |
|
114 | + | // console.log(item.toLowerCase(), character.toLowerCase(), item.toLowerCase().startsWith(character.toLowerCase())) |
|
115 | + | ||
116 | + | return item.toLowerCase().startsWith(character.toLowerCase()) |
|
117 | + | }) + startIndex |
|
118 | + | ) |
|
119 | + | } |
|
120 | + | ||
121 | + | export const stateChangeTestCases = [ |
|
122 | + | { |
|
123 | + | step: keyDownOnToggleButton, |
|
124 | + | arg: '{ArrowDown}', |
|
125 | + | state: { |
|
126 | + | isOpen: true, |
|
127 | + | highlightedIndex: 0, |
|
128 | + | inputValue: '', |
|
129 | + | selectedItem: null, |
|
130 | + | }, |
|
131 | + | type: stateChangeTypes.ToggleButtonKeyDownArrowDown, |
|
132 | + | }, |
|
133 | + | { |
|
134 | + | step: keyDownOnToggleButton, |
|
135 | + | arg: '{Enter}', |
|
136 | + | state: { |
|
137 | + | isOpen: false, |
|
138 | + | highlightedIndex: -1, |
|
139 | + | inputValue: '', |
|
140 | + | selectedItem: items[0], |
|
141 | + | }, |
|
142 | + | type: stateChangeTypes.ToggleButtonKeyDownEnter, |
|
143 | + | }, |
|
144 | + | { |
|
145 | + | step: keyDownOnToggleButton, |
|
146 | + | arg: '{End}', |
|
147 | + | state: { |
|
148 | + | isOpen: true, |
|
149 | + | highlightedIndex: items.length - 1, |
|
150 | + | inputValue: '', |
|
151 | + | selectedItem: items[0], |
|
152 | + | }, |
|
153 | + | type: stateChangeTypes.ToggleButtonKeyDownEnd, |
|
154 | + | }, |
|
155 | + | { |
|
156 | + | step: keyDownOnToggleButton, |
|
157 | + | arg: ' ', |
|
158 | + | state: { |
|
159 | + | isOpen: false, |
|
160 | + | highlightedIndex: -1, |
|
161 | + | inputValue: '', |
|
162 | + | selectedItem: items[items.length - 1], |
|
163 | + | }, |
|
164 | + | type: stateChangeTypes.ToggleButtonKeyDownSpaceButton, |
|
165 | + | }, |
|
166 | + | { |
|
167 | + | step: clickOnToggleButton, |
|
168 | + | state: { |
|
169 | + | isOpen: true, |
|
170 | + | highlightedIndex: items.length - 1, |
|
171 | + | inputValue: '', |
|
172 | + | selectedItem: items[items.length - 1], |
|
173 | + | }, |
|
174 | + | type: stateChangeTypes.ToggleButtonClick, |
|
175 | + | }, |
|
176 | + | { |
|
177 | + | step: keyDownOnToggleButton, |
|
178 | + | arg: '{PageUp}', |
|
179 | + | state: { |
|
180 | + | isOpen: true, |
|
181 | + | highlightedIndex: items.length - 11, |
|
182 | + | inputValue: '', |
|
183 | + | selectedItem: items[items.length - 1], |
|
184 | + | }, |
|
185 | + | type: stateChangeTypes.ToggleButtonKeyDownPageUp, |
|
186 | + | }, |
|
187 | + | { |
|
188 | + | step: keyDownOnToggleButton, |
|
189 | + | arg: '{PageDown}', |
|
190 | + | state: { |
|
191 | + | isOpen: true, |
|
192 | + | highlightedIndex: items.length - 1, |
|
193 | + | inputValue: '', |
|
194 | + | selectedItem: items[items.length - 1], |
|
195 | + | }, |
|
196 | + | type: stateChangeTypes.ToggleButtonKeyDownPageDown, |
|
197 | + | }, |
|
198 | + | { |
|
199 | + | step: mouseMoveItemAtIndex, |
|
200 | + | arg: items.length - 2, |
|
201 | + | state: { |
|
202 | + | isOpen: true, |
|
203 | + | highlightedIndex: items.length - 2, |
|
204 | + | inputValue: '', |
|
205 | + | selectedItem: items[items.length - 1], |
|
206 | + | }, |
|
207 | + | type: stateChangeTypes.ItemMouseMove, |
|
208 | + | }, |
|
209 | + | { |
|
210 | + | step: mouseLeaveItemAtIndex, |
|
211 | + | arg: items.length - 2, |
|
212 | + | state: { |
|
213 | + | isOpen: true, |
|
214 | + | highlightedIndex: -1, |
|
215 | + | inputValue: '', |
|
216 | + | selectedItem: items[items.length - 1], |
|
217 | + | }, |
|
218 | + | type: stateChangeTypes.MenuMouseLeave, |
|
219 | + | }, |
|
220 | + | { |
|
221 | + | step: mouseMoveItemAtIndex, |
|
222 | + | arg: 2, |
|
223 | + | state: { |
|
224 | + | isOpen: true, |
|
225 | + | highlightedIndex: 2, |
|
226 | + | inputValue: '', |
|
227 | + | selectedItem: items[items.length - 1], |
|
228 | + | }, |
|
229 | + | type: stateChangeTypes.ItemMouseMove, |
|
230 | + | }, |
|
231 | + | { |
|
232 | + | step: clickOnItemAtIndex, |
|
233 | + | arg: 2, |
|
234 | + | state: { |
|
235 | + | isOpen: false, |
|
236 | + | highlightedIndex: -1, |
|
237 | + | inputValue: '', |
|
238 | + | selectedItem: items[2], |
|
239 | + | }, |
|
240 | + | type: stateChangeTypes.ItemClick, |
|
241 | + | }, |
|
242 | + | { |
|
243 | + | step: keyDownOnToggleButton, |
|
244 | + | arg: '{Alt>}{ArrowDown}{/Alt}', |
|
245 | + | state: { |
|
246 | + | isOpen: true, |
|
247 | + | highlightedIndex: 2, |
|
248 | + | inputValue: '', |
|
249 | + | selectedItem: items[2], |
|
250 | + | }, |
|
251 | + | type: stateChangeTypes.ToggleButtonKeyDownArrowDown, |
|
252 | + | }, |
|
253 | + | { |
|
254 | + | step: keyDownOnToggleButton, |
|
255 | + | arg: '{Alt>}{ArrowDown}{/Alt}', |
|
256 | + | state: { |
|
257 | + | isOpen: true, |
|
258 | + | highlightedIndex: 3, |
|
259 | + | inputValue: '', |
|
260 | + | selectedItem: items[2], |
|
261 | + | }, |
|
262 | + | type: stateChangeTypes.ToggleButtonKeyDownArrowDown, |
|
263 | + | }, |
|
264 | + | { |
|
265 | + | step: keyDownOnToggleButton, |
|
266 | + | arg: '{Alt>}{ArrowUp}{/Alt}', |
|
267 | + | state: { |
|
268 | + | isOpen: false, |
|
269 | + | highlightedIndex: -1, |
|
270 | + | inputValue: '', |
|
271 | + | selectedItem: items[3], |
|
272 | + | }, |
|
273 | + | type: stateChangeTypes.ToggleButtonKeyDownArrowUp, |
|
274 | + | }, |
|
275 | + | { |
|
276 | + | step: keyDownOnToggleButton, |
|
277 | + | arg: 'c', |
|
278 | + | state: { |
|
279 | + | isOpen: true, |
|
280 | + | highlightedIndex: 5, |
|
281 | + | inputValue: 'c', |
|
282 | + | selectedItem: items[3], |
|
283 | + | }, |
|
284 | + | type: stateChangeTypes.ToggleButtonKeyDownCharacter, |
|
285 | + | }, |
|
286 | + | { |
|
287 | + | step: () => |
|
288 | + | // just to have all steps async. |
|
289 | + | new Promise(resolve => { |
|
290 | + | act(() => jest.runAllTimers()) |
|
291 | + | resolve() |
|
292 | + | }), |
|
293 | + | state: { |
|
294 | + | isOpen: true, |
|
295 | + | highlightedIndex: 5, |
|
296 | + | inputValue: '', |
|
297 | + | selectedItem: items[3], |
|
298 | + | }, |
|
299 | + | type: stateChangeTypes.FunctionSetInputValue, |
|
300 | + | }, |
|
301 | + | { |
|
302 | + | step: tab, |
|
303 | + | state: { |
|
304 | + | isOpen: false, |
|
305 | + | highlightedIndex: -1, |
|
306 | + | inputValue: '', |
|
307 | + | selectedItem: items[5], |
|
308 | + | }, |
|
309 | + | type: stateChangeTypes.ToggleButtonBlur, |
|
310 | + | }, |
|
311 | + | ] |
1 | 1 | import * as React from 'react' |
|
2 | - | import {render, fireEvent, screen} from '@testing-library/react' |
|
2 | + | import {render} from '@testing-library/react' |
|
3 | 3 | import {renderHook} from '@testing-library/react-hooks' |
|
4 | - | import userEvent from '@testing-library/user-event' |
|
5 | 4 | import {defaultProps} from '../utils' |
|
6 | - | import {items} from '../testUtils' |
|
5 | + | import {dataTestIds, items, user, getInput} from '../testUtils' |
|
7 | 6 | import useCombobox from '.' |
|
8 | 7 | ||
9 | - | const dataTestIds = { |
|
10 | - | toggleButton: 'toggle-button-id', |
|
11 | - | item: index => `item-id-${index}`, |
|
12 | - | input: 'input-id', |
|
13 | - | } |
|
8 | + | export * from '../testUtils' |
|
14 | 9 | ||
15 | 10 | jest.mock('../../utils', () => { |
|
16 | 11 | const utils = jest.requireActual('../../utils') |
34 | 29 | beforeEach(jest.resetAllMocks) |
|
35 | 30 | afterAll(jest.restoreAllMocks) |
|
36 | 31 | ||
37 | - | const renderCombobox = (props, uiCallback) => { |
|
32 | + | export async function changeInputValue(inputValue) { |
|
33 | + | await user.type(getInput(), inputValue) |
|
34 | + | } |
|
35 | + | ||
36 | + | export const renderCombobox = (props, uiCallback) => { |
|
38 | 37 | const renderSpy = jest.fn() |
|
39 | 38 | const ui = <DropdownCombobox renderSpy={renderSpy} {...props} /> |
|
40 | 39 | const utils = render(uiCallback ? uiCallback(ui) : ui) |
|
41 | 40 | const rerender = newProps => |
|
42 | 41 | utils.rerender(<DropdownCombobox renderSpy={renderSpy} {...newProps} />) |
|
43 | - | const label = screen.getByText(/choose an element/i) |
|
44 | - | const menu = screen.getByRole('listbox') |
|
45 | - | const toggleButton = screen.getByTestId(dataTestIds.toggleButton) |
|
46 | - | const input = screen.getByTestId(dataTestIds.input) |
|
47 | - | const combobox = screen.getByRole('combobox') |
|
48 | - | const getItemAtIndex = index => screen.getByTestId(dataTestIds.item(index)) |
|
49 | - | const getItems = () => screen.queryAllByRole('option') |
|
50 | - | const clickOnItemAtIndex = index => { |
|
51 | - | // keeping fireEvent so we don't trigger input blur via user event |
|
52 | - | fireEvent.click(getItemAtIndex(index)) |
|
53 | - | } |
|
54 | - | const clickOnToggleButton = () => { |
|
55 | - | userEvent.click(toggleButton) |
|
56 | - | } |
|
57 | - | const mouseMoveItemAtIndex = index => { |
|
58 | - | userEvent.hover(getItemAtIndex(index)) |
|
59 | - | } |
|
60 | - | const getA11yStatusContainer = () => screen.queryByRole('status') |
|
61 | - | const mouseLeaveMenu = () => { |
|
62 | - | userEvent.unhover(menu) |
|
63 | - | } |
|
64 | - | const changeInputValue = inputValue => { |
|
65 | - | userEvent.type(input, inputValue) |
|
66 | - | } |
|
67 | - | const focusInput = () => { |
|
68 | - | fireEvent.focus(input) |
|
69 | - | } |
|
70 | - | const keyDownOnInput = (key, options = {}) => { |
|
71 | - | if (document.activeElement !== input) { |
|
72 | - | focusInput() |
|
73 | - | } |
|
74 | - | ||
75 | - | fireEvent.keyDown(input, {key, ...options}) |
|
76 | - | } |
|
77 | - | const blurInput = () => { |
|
78 | - | fireEvent.blur(input) |
|
79 | - | } |
|
80 | 42 | ||
81 | 43 | return { |
|
82 | 44 | ...utils, |
|
83 | 45 | renderSpy, |
|
84 | 46 | rerender, |
|
85 | - | label, |
|
86 | - | menu, |
|
87 | - | toggleButton, |
|
88 | - | getItemAtIndex, |
|
89 | - | clickOnItemAtIndex, |
|
90 | - | mouseMoveItemAtIndex, |
|
91 | - | getItems, |
|
92 | - | clickOnToggleButton, |
|
93 | - | getA11yStatusContainer, |
|
94 | - | mouseLeaveMenu, |
|
95 | - | input, |
|
96 | - | combobox, |
|
97 | - | changeInputValue, |
|
98 | - | keyDownOnInput, |
|
99 | - | blurInput, |
|
100 | - | focusInput, |
|
101 | 47 | } |
|
102 | 48 | } |
|
103 | 49 | ||
104 | - | const DropdownCombobox = ({renderSpy, renderItem, ...props}) => { |
|
50 | + | function DropdownCombobox({renderSpy, renderItem, ...props}) { |
|
105 | 51 | const { |
|
106 | 52 | isOpen, |
|
107 | 53 | getToggleButtonProps, |
133 | 79 | const stringItem = |
|
134 | 80 | item instanceof Object ? itemToString(item) : item |
|
135 | 81 | return renderItem ? ( |
|
136 | - | renderItem({index, item, getItemProps, dataTestIds, stringItem}) |
|
82 | + | renderItem({index, item, getItemProps, stringItem}) |
|
137 | 83 | ) : ( |
|
138 | 84 | <li |
|
139 | 85 | data-testid={dataTestIds.item(index)} |
149 | 95 | ) |
|
150 | 96 | } |
|
151 | 97 | ||
152 | - | const renderUseCombobox = props => { |
|
98 | + | export const renderUseCombobox = props => { |
|
153 | 99 | return renderHook(() => useCombobox({items, ...props})) |
|
154 | 100 | } |
|
155 | - | ||
156 | - | export {renderUseCombobox, dataTestIds, renderCombobox} |
8 | 8 | import {isReactNative} from '../is.macro' |
|
9 | 9 | import { |
|
10 | 10 | scrollIntoView, |
|
11 | - | getNextWrappingIndex, |
|
12 | 11 | getState, |
|
13 | 12 | generateId, |
|
14 | 13 | debounce, |
|
15 | - | targetWithinDownshift, |
|
16 | 14 | validateControlledUnchanged, |
|
17 | 15 | noop, |
|
16 | + | targetWithinDownshift, |
|
18 | 17 | } from '../utils' |
|
19 | 18 | import setStatus from '../set-a11y-status' |
|
20 | 19 |
214 | 213 | stateReducer, |
|
215 | 214 | getA11ySelectionMessage, |
|
216 | 215 | scrollIntoView, |
|
217 | - | circularNavigation: false, |
|
218 | 216 | environment: |
|
219 | 217 | /* istanbul ignore next (ssr) */ |
|
220 | 218 | typeof window === 'undefined' ? {} : window, |
271 | 269 | } |
|
272 | 270 | } |
|
273 | 271 | ||
274 | - | function getHighlightedIndexOnOpen(props, state, offset, getItemNodeFromIndex) { |
|
272 | + | function getHighlightedIndexOnOpen(props, state, offset) { |
|
275 | 273 | const {items, initialHighlightedIndex, defaultHighlightedIndex} = props |
|
276 | 274 | const {selectedItem, highlightedIndex} = state |
|
277 | 275 |
290 | 288 | return defaultHighlightedIndex |
|
291 | 289 | } |
|
292 | 290 | if (selectedItem) { |
|
293 | - | if (offset === 0) { |
|
294 | - | return items.indexOf(selectedItem) |
|
295 | - | } |
|
296 | - | return getNextWrappingIndex( |
|
297 | - | offset, |
|
298 | - | items.indexOf(selectedItem), |
|
299 | - | items.length, |
|
300 | - | getItemNodeFromIndex, |
|
301 | - | false, |
|
302 | - | ) |
|
291 | + | return items.indexOf(selectedItem) |
|
303 | 292 | } |
|
304 | 293 | if (offset === 0) { |
|
305 | 294 | return -1 |
316 | 305 | * @param {Function} handleBlur Handler on blur from mouse or touch. |
|
317 | 306 | * @returns {Object} Ref containing whether mouseDown or touchMove event is happening |
|
318 | 307 | */ |
|
319 | - | function useMouseAndTouchTracker( |
|
308 | + | function useMouseAndTouchTracker( |
|
320 | 309 | isOpen, |
|
321 | 310 | downshiftElementRefs, |
|
322 | 311 | environment, |
1 | - | /* eslint-disable max-statements */ |
|
2 | 1 | import {useRef, useEffect, useCallback, useMemo} from 'react' |
|
3 | 2 | import { |
|
4 | 3 | getItemIndex, |
|
5 | 4 | isAcceptedCharacterKey, |
|
6 | 5 | useControlledReducer, |
|
7 | 6 | getInitialState, |
|
8 | - | useMouseAndTouchTracker, |
|
9 | 7 | useGetterPropsCalledChecker, |
|
10 | 8 | useLatestRef, |
|
11 | 9 | useA11yMessageSetter, |
|
12 | 10 | useScrollIntoView, |
|
13 | 11 | useControlPropsValidator, |
|
14 | 12 | useElementIds, |
|
13 | + | useMouseAndTouchTracker, |
|
15 | 14 | } from '../utils' |
|
16 | 15 | import { |
|
17 | 16 | callAllEventHandlers, |
36 | 35 | items, |
|
37 | 36 | scrollIntoView, |
|
38 | 37 | environment, |
|
39 | - | initialIsOpen, |
|
40 | - | defaultIsOpen, |
|
41 | 38 | itemToString, |
|
42 | 39 | getA11ySelectionMessage, |
|
43 | 40 | getA11yStatusMessage, |
55 | 52 | const toggleButtonRef = useRef(null) |
|
56 | 53 | const menuRef = useRef(null) |
|
57 | 54 | const itemRefs = useRef({}) |
|
58 | - | // used not to trigger menu blur action in some scenarios. |
|
59 | - | const shouldBlurRef = useRef(true) |
|
60 | 55 | // used to keep the inputValue clearTimeout object between renders. |
|
61 | 56 | const clearTimeoutRef = useRef(null) |
|
62 | 57 | // prevent id re-generation between renders. |
139 | 134 | props, |
|
140 | 135 | state, |
|
141 | 136 | }) |
|
142 | - | /* Controls the focus on the menu or the toggle button. */ |
|
143 | - | useEffect(() => { |
|
144 | - | // Don't focus menu on first render. |
|
145 | - | if (isInitialMountRef.current) { |
|
146 | - | // Unless it was initialised as open. |
|
147 | - | if ((initialIsOpen || defaultIsOpen || isOpen) && menuRef.current) { |
|
148 | - | menuRef.current.focus() |
|
149 | - | } |
|
150 | - | return |
|
151 | - | } |
|
152 | - | // Focus menu on open. |
|
153 | - | if (isOpen) { |
|
154 | - | // istanbul ignore else |
|
155 | - | if (menuRef.current) { |
|
156 | - | menuRef.current.focus() |
|
157 | - | } |
|
158 | - | return |
|
159 | - | } |
|
160 | - | // Focus toggleButton on close, but not if it was closed with (Shift+)Tab. |
|
161 | - | if (environment.document.activeElement === menuRef.current) { |
|
162 | - | // istanbul ignore else |
|
163 | - | if (toggleButtonRef.current) { |
|
164 | - | shouldBlurRef.current = false |
|
165 | - | toggleButtonRef.current.focus() |
|
166 | - | } |
|
167 | - | } |
|
168 | - | // eslint-disable-next-line react-hooks/exhaustive-deps |
|
169 | - | }, [isOpen]) |
|
170 | 137 | useEffect(() => { |
|
171 | 138 | if (isInitialMountRef.current) { |
|
172 | 139 | return |
181 | 148 | environment, |
|
182 | 149 | () => { |
|
183 | 150 | dispatch({ |
|
184 | - | type: stateChangeTypes.MenuBlur, |
|
151 | + | type: stateChangeTypes.ToggleButtonBlur, |
|
185 | 152 | }) |
|
186 | 153 | }, |
|
187 | 154 | ) |
209 | 176 | dispatch({ |
|
210 | 177 | type: stateChangeTypes.ToggleButtonKeyDownArrowDown, |
|
211 | 178 | getItemNodeFromIndex, |
|
212 | - | shiftKey: event.shiftKey, |
|
179 | + | altKey: event.altKey, |
|
213 | 180 | }) |
|
214 | 181 | }, |
|
215 | 182 | ArrowUp(event) { |
218 | 185 | dispatch({ |
|
219 | 186 | type: stateChangeTypes.ToggleButtonKeyDownArrowUp, |
|
220 | 187 | getItemNodeFromIndex, |
|
221 | - | shiftKey: event.shiftKey, |
|
222 | - | }) |
|
223 | - | }, |
|
224 | - | }), |
|
225 | - | [dispatch, getItemNodeFromIndex], |
|
226 | - | ) |
|
227 | - | const menuKeyDownHandlers = useMemo( |
|
228 | - | () => ({ |
|
229 | - | ArrowDown(event) { |
|
230 | - | event.preventDefault() |
|
231 | - | ||
232 | - | dispatch({ |
|
233 | - | type: stateChangeTypes.MenuKeyDownArrowDown, |
|
234 | - | getItemNodeFromIndex, |
|
235 | - | shiftKey: event.shiftKey, |
|
236 | - | }) |
|
237 | - | }, |
|
238 | - | ArrowUp(event) { |
|
239 | - | event.preventDefault() |
|
240 | - | ||
241 | - | dispatch({ |
|
242 | - | type: stateChangeTypes.MenuKeyDownArrowUp, |
|
243 | - | getItemNodeFromIndex, |
|
244 | - | shiftKey: event.shiftKey, |
|
188 | + | altKey: event.altKey, |
|
245 | 189 | }) |
|
246 | 190 | }, |
|
247 | 191 | Home(event) { |
|
248 | 192 | event.preventDefault() |
|
249 | 193 | ||
250 | 194 | dispatch({ |
|
251 | - | type: stateChangeTypes.MenuKeyDownHome, |
|
195 | + | type: stateChangeTypes.ToggleButtonKeyDownHome, |
|
252 | 196 | getItemNodeFromIndex, |
|
253 | 197 | }) |
|
254 | 198 | }, |
|
255 | 199 | End(event) { |
|
256 | 200 | event.preventDefault() |
|
257 | 201 | ||
258 | 202 | dispatch({ |
|
259 | - | type: stateChangeTypes.MenuKeyDownEnd, |
|
203 | + | type: stateChangeTypes.ToggleButtonKeyDownEnd, |
|
260 | 204 | getItemNodeFromIndex, |
|
261 | 205 | }) |
|
262 | 206 | }, |
|
263 | 207 | Escape() { |
|
264 | - | dispatch({ |
|
265 | - | type: stateChangeTypes.MenuKeyDownEscape, |
|
266 | - | }) |
|
208 | + | if (latest.current.state.isOpen) { |
|
209 | + | dispatch({ |
|
210 | + | type: stateChangeTypes.ToggleButtonKeyDownEscape, |
|
211 | + | }) |
|
212 | + | } |
|
267 | 213 | }, |
|
268 | 214 | Enter(event) { |
|
269 | - | event.preventDefault() |
|
215 | + | if (latest.current.state.isOpen) { |
|
216 | + | event.preventDefault() |
|
270 | 217 | ||
271 | - | dispatch({ |
|
272 | - | type: stateChangeTypes.MenuKeyDownEnter, |
|
273 | - | }) |
|
218 | + | dispatch({ |
|
219 | + | type: stateChangeTypes.ToggleButtonKeyDownEnter, |
|
220 | + | }) |
|
221 | + | } |
|
222 | + | }, |
|
223 | + | PageUp(event) { |
|
224 | + | if (latest.current.state.isOpen) { |
|
225 | + | event.preventDefault() |
|
226 | + | ||
227 | + | dispatch({ |
|
228 | + | type: stateChangeTypes.ToggleButtonKeyDownPageUp, |
|
229 | + | getItemNodeFromIndex, |
|
230 | + | }) |
|
231 | + | } |
|
232 | + | }, |
|
233 | + | PageDown(event) { |
|
234 | + | if (latest.current.state.isOpen) { |
|
235 | + | event.preventDefault() |
|
236 | + | ||
237 | + | dispatch({ |
|
238 | + | type: stateChangeTypes.ToggleButtonKeyDownPageDown, |
|
239 | + | getItemNodeFromIndex, |
|
240 | + | }) |
|
241 | + | } |
|
274 | 242 | }, |
|
275 | 243 | ' '(event) { |
|
276 | - | event.preventDefault() |
|
244 | + | if (latest.current.state.isOpen) { |
|
245 | + | event.preventDefault() |
|
277 | 246 | ||
278 | - | dispatch({ |
|
279 | - | type: stateChangeTypes.MenuKeyDownSpaceButton, |
|
280 | - | }) |
|
247 | + | dispatch({ |
|
248 | + | type: stateChangeTypes.ToggleButtonKeyDownSpaceButton, |
|
249 | + | }) |
|
250 | + | } |
|
281 | 251 | }, |
|
282 | 252 | }), |
|
283 | - | [dispatch, getItemNodeFromIndex], |
|
253 | + | [dispatch, getItemNodeFromIndex, latest], |
|
284 | 254 | ) |
|
285 | 255 | ||
286 | 256 | // Action functions. |
345 | 315 | {onMouseLeave, refKey = 'ref', onKeyDown, onBlur, ref, ...rest} = {}, |
|
346 | 316 | {suppressRefError = false} = {}, |
|
347 | 317 | ) => { |
|
348 | - | const latestState = latest.current.state |
|
349 | - | const menuHandleKeyDown = event => { |
|
350 | - | const key = normalizeArrowKey(event) |
|
351 | - | if (key && menuKeyDownHandlers[key]) { |
|
352 | - | menuKeyDownHandlers[key](event) |
|
353 | - | } else if (isAcceptedCharacterKey(key)) { |
|
354 | - | dispatch({ |
|
355 | - | type: stateChangeTypes.MenuKeyDownCharacter, |
|
356 | - | key, |
|
357 | - | getItemNodeFromIndex, |
|
358 | - | }) |
|
359 | - | } |
|
360 | - | } |
|
361 | - | const menuHandleBlur = () => { |
|
362 | - | // if the blur was a result of selection, we don't trigger this action. |
|
363 | - | if (shouldBlurRef.current === false) { |
|
364 | - | shouldBlurRef.current = true |
|
365 | - | return |
|
366 | - | } |
|
367 | - | ||
368 | - | const shouldBlur = !mouseAndTouchTrackersRef.current.isMouseDown |
|
369 | - | /* istanbul ignore else */ |
|
370 | - | if (shouldBlur) { |
|
371 | - | dispatch({type: stateChangeTypes.MenuBlur}) |
|
372 | - | } |
|
373 | - | } |
|
374 | 318 | const menuHandleMouseLeave = () => { |
|
375 | 319 | dispatch({ |
|
376 | 320 | type: stateChangeTypes.MenuMouseLeave, |
387 | 331 | role: 'listbox', |
|
388 | 332 | 'aria-labelledby': elementIds.labelId, |
|
389 | 333 | tabIndex: -1, |
|
390 | - | ...(latestState.isOpen && |
|
391 | - | latestState.highlightedIndex > -1 && { |
|
392 | - | 'aria-activedescendant': elementIds.getItemId( |
|
393 | - | latestState.highlightedIndex, |
|
394 | - | ), |
|
395 | - | }), |
|
396 | 334 | onMouseLeave: callAllEventHandlers(onMouseLeave, menuHandleMouseLeave), |
|
397 | - | onKeyDown: callAllEventHandlers(onKeyDown, menuHandleKeyDown), |
|
398 | - | onBlur: callAllEventHandlers(onBlur, menuHandleBlur), |
|
399 | 335 | ...rest, |
|
400 | 336 | } |
|
401 | 337 | }, |
|
402 | - | [ |
|
403 | - | dispatch, |
|
404 | - | latest, |
|
405 | - | menuKeyDownHandlers, |
|
406 | - | mouseAndTouchTrackersRef, |
|
407 | - | setGetterPropCallInfo, |
|
408 | - | elementIds, |
|
409 | - | getItemNodeFromIndex, |
|
410 | - | ], |
|
338 | + | [dispatch, setGetterPropCallInfo, elementIds], |
|
411 | 339 | ) |
|
412 | 340 | const getToggleButtonProps = useCallback( |
|
413 | 341 | ( |
|
414 | - | {onClick, onKeyDown, refKey = 'ref', ref, ...rest} = {}, |
|
342 | + | {onBlur, onClick, onKeyDown, refKey = 'ref', ref, ...rest} = {}, |
|
415 | 343 | {suppressRefError = false} = {}, |
|
416 | 344 | ) => { |
|
345 | + | const latestState = latest.current.state |
|
417 | 346 | const toggleButtonHandleClick = () => { |
|
418 | 347 | dispatch({ |
|
419 | 348 | type: stateChangeTypes.ToggleButtonClick, |
|
420 | 349 | }) |
|
421 | 350 | } |
|
351 | + | const toggleButtonHandleBlur = () => { |
|
352 | + | if ( |
|
353 | + | latestState.isOpen && |
|
354 | + | !mouseAndTouchTrackersRef.current.isMouseDown |
|
355 | + | ) { |
|
356 | + | dispatch({ |
|
357 | + | type: stateChangeTypes.ToggleButtonBlur, |
|
358 | + | }) |
|
359 | + | } |
|
360 | + | } |
|
422 | 361 | const toggleButtonHandleKeyDown = event => { |
|
423 | 362 | const key = normalizeArrowKey(event) |
|
424 | 363 | if (key && toggleButtonKeyDownHandlers[key]) { |
435 | 374 | [refKey]: handleRefs(ref, toggleButtonNode => { |
|
436 | 375 | toggleButtonRef.current = toggleButtonNode |
|
437 | 376 | }), |
|
438 | - | id: elementIds.toggleButtonId, |
|
439 | - | 'aria-haspopup': 'listbox', |
|
377 | + | 'aria-activedescendant': |
|
378 | + | latestState.isOpen && latestState.highlightedIndex > -1 |
|
379 | + | ? elementIds.getItemId(latestState.highlightedIndex) |
|
380 | + | : '', |
|
381 | + | 'aria-controls': elementIds.menuId, |
|
440 | 382 | 'aria-expanded': latest.current.state.isOpen, |
|
383 | + | 'aria-haspopup': 'listbox', |
|
441 | 384 | 'aria-labelledby': `${elementIds.labelId} ${elementIds.toggleButtonId}`, |
|
385 | + | id: elementIds.toggleButtonId, |
|
386 | + | role: 'combobox', |
|
387 | + | tabIndex: 0, |
|
388 | + | onBlur: callAllEventHandlers(onBlur, toggleButtonHandleBlur), |
|
442 | 389 | ...rest, |
|
443 | 390 | } |
|
444 | 391 |
463 | 410 | return toggleProps |
|
464 | 411 | }, |
|
465 | 412 | [ |
|
466 | - | dispatch, |
|
467 | 413 | latest, |
|
468 | - | toggleButtonKeyDownHandlers, |
|
469 | - | setGetterPropCallInfo, |
|
470 | 414 | elementIds, |
|
415 | + | setGetterPropCallInfo, |
|
416 | + | dispatch, |
|
417 | + | mouseAndTouchTrackersRef, |
|
418 | + | toggleButtonKeyDownHandlers, |
|
471 | 419 | getItemNodeFromIndex, |
|
472 | 420 | ], |
|
473 | 421 | ) |
|
474 | 422 | const getItemProps = useCallback( |
|
475 | 423 | ({ |
|
476 | - | item, |
|
477 | - | index, |
|
424 | + | item: itemProp, |
|
425 | + | index: indexProp, |
|
478 | 426 | onMouseMove, |
|
479 | 427 | onClick, |
|
480 | 428 | refKey = 'ref', |
483 | 431 | ...rest |
|
484 | 432 | } = {}) => { |
|
485 | 433 | const {state: latestState, props: latestProps} = latest.current |
|
434 | + | const item = itemProp ?? items[indexProp] |
|
435 | + | const index = getItemIndex(indexProp, item, latestProps.items) |
|
436 | + | ||
486 | 437 | const itemHandleMouseMove = () => { |
|
487 | 438 | if (index === latestState.highlightedIndex) { |
|
488 | 439 | return |
508 | 459 | const itemProps = { |
|
509 | 460 | disabled, |
|
510 | 461 | role: 'option', |
|
511 | - | 'aria-selected': `${itemIndex === latestState.highlightedIndex}`, |
|
462 | + | 'aria-selected': `${item === selectedItem}`, |
|
512 | 463 | id: elementIds.getItemId(itemIndex), |
|
513 | 464 | [refKey]: handleRefs(ref, itemNode => { |
|
514 | 465 | if (itemNode) { |
529 | 480 | ||
530 | 481 | return itemProps |
|
531 | 482 | }, |
|
532 | - | [dispatch, latest, shouldScrollRef, elementIds], |
|
483 | + | [latest, items, selectedItem, elementIds, shouldScrollRef, dispatch], |
|
533 | 484 | ) |
|
534 | 485 | ||
535 | 486 | return { |
1 | 1 | import React from 'react' |
|
2 | - | import {act} from '@testing-library/react' |
|
2 | + | import {screen, act} from '@testing-library/react' |
|
3 | + | import userEvent from '@testing-library/user-event' |
|
3 | 4 | ||
4 | - | const items = [ |
|
5 | + | export const items = [ |
|
5 | 6 | 'Neptunium', |
|
6 | 7 | 'Plutonium', |
|
7 | 8 | 'Americium', |
30 | 31 | 'Oganesson', |
|
31 | 32 | ] |
|
32 | 33 | ||
33 | - | const defaultIds = { |
|
34 | + | export const dataTestIds = { |
|
35 | + | toggleButton: 'toggle-button-id', |
|
36 | + | menu: 'menu-id', |
|
37 | + | item: index => `item-id-${index}`, |
|
38 | + | input: 'input-id', |
|
39 | + | selectedItemPrefix: 'selected-item-id', |
|
40 | + | selectedItem: index => `selected-item-id-${index}`, |
|
41 | + | } |
|
42 | + | ||
43 | + | export const defaultIds = { |
|
34 | 44 | labelId: 'downshift-test-id-label', |
|
35 | 45 | menuId: 'downshift-test-id-menu', |
|
36 | 46 | getItemId: index => `downshift-test-id-item-${index}`, |
|
37 | 47 | toggleButtonId: 'downshift-test-id-toggle-button', |
|
38 | 48 | inputId: 'downshift-test-id-input', |
|
39 | 49 | } |
|
40 | 50 | ||
41 | - | const waitForDebouncedA11yStatusUpdate = () => |
|
51 | + | export const waitForDebouncedA11yStatusUpdate = () => |
|
42 | 52 | act(() => jest.advanceTimersByTime(200)) |
|
43 | 53 | ||
44 | - | const MemoizedItem = React.memo(function Item({ |
|
54 | + | export const MemoizedItem = React.memo(function Item({ |
|
45 | 55 | index, |
|
46 | 56 | item, |
|
47 | 57 | getItemProps, |
|
48 | - | dataTestIds, |
|
49 | 58 | stringItem, |
|
50 | 59 | ...rest |
|
51 | 60 | }) { |
60 | 69 | ) |
|
61 | 70 | }) |
|
62 | 71 | ||
63 | - | export {items, defaultIds, waitForDebouncedA11yStatusUpdate, MemoizedItem} |
|
72 | + | export const user = userEvent.setup({delay: null}) |
|
73 | + | ||
74 | + | export function getLabel() { |
|
75 | + | return screen.getByText(/choose an element/i) |
|
76 | + | } |
|
77 | + | export function getMenu() { |
|
78 | + | return screen.getByRole('listbox') |
|
79 | + | } |
|
80 | + | export function getToggleButton() { |
|
81 | + | return screen.getByTestId(dataTestIds.toggleButton) |
|
82 | + | } |
|
83 | + | export function getItemAtIndex(index) { |
|
84 | + | return getItems()[index] |
|
85 | + | } |
|
86 | + | export function getItems() { |
|
87 | + | return screen.queryAllByRole('option') |
|
88 | + | } |
|
89 | + | export function getInput() { |
|
90 | + | return screen.getByRole('textbox') |
|
91 | + | } |
|
92 | + | export async function clickOnItemAtIndex(index) { |
|
93 | + | await user.click(getItemAtIndex(index)) |
|
94 | + | } |
|
95 | + | export async function clickOnToggleButton() { |
|
96 | + | await user.click(getToggleButton()) |
|
97 | + | } |
|
98 | + | export async function mouseMoveItemAtIndex(index) { |
|
99 | + | await user.hover(getItemAtIndex(index)) |
|
100 | + | } |
|
101 | + | export async function mouseLeaveItemAtIndex(index) { |
|
102 | + | await user.unhover(getItemAtIndex(index)) |
|
103 | + | } |
|
104 | + | export async function keyDownOnToggleButton(keys) { |
|
105 | + | if (document.activeElement !== getToggleButton()) { |
|
106 | + | getToggleButton().focus() |
|
107 | + | } |
|
108 | + | ||
109 | + | await user.keyboard(keys) |
|
110 | + | } |
|
111 | + | export async function keyDownOnInput(keys) { |
|
112 | + | if (document.activeElement !== getInput()) { |
|
113 | + | getInput().focus() |
|
114 | + | } |
|
115 | + | ||
116 | + | await user.keyboard(keys) |
|
117 | + | } |
|
118 | + | export function getA11yStatusContainer() { |
|
119 | + | return screen.queryByRole('status') |
|
120 | + | } |
|
121 | + | export async function tab(shiftKey = false) { |
|
122 | + | await user.tab({shift: shiftKey}) |
|
123 | + | } |
Files | Coverage |
---|---|
src | 100.00% |
Project Totals (18 files) | 100.00% |
#1402
3bff9d5
a05a09f
78ce9e9
c188cc1