downshift-js / downshift

Compare c188cc1 ... +2 ... 3bff9d5

Showing 9 of 41 files from the diff.
Other files ignored by Codecov
package.json has changed.
cypress.config.js has changed.
babel.config.js has changed.

@@ -1,11 +1,22 @@
Loading
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,75 +39,24 @@
Loading
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,7 +65,7 @@
Loading
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,7 +86,7 @@
Loading
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,4 +102,210 @@
Loading
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,16 +1,11 @@
Loading
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,74 +29,25 @@
Loading
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,7 +79,7 @@
Loading
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,8 +95,6 @@
Loading
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,13 +8,12 @@
Loading
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,7 +213,6 @@
Loading
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,7 +269,7 @@
Loading
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,16 +288,7 @@
Loading
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,7 +305,7 @@
Loading
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,17 +1,16 @@
Loading
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,8 +35,6 @@
Loading
36 35
    items,
37 36
    scrollIntoView,
38 37
    environment,
39 -
    initialIsOpen,
40 -
    defaultIsOpen,
41 38
    itemToString,
42 39
    getA11ySelectionMessage,
43 40
    getA11yStatusMessage,
@@ -55,8 +52,6 @@
Loading
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,34 +134,6 @@
Loading
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,7 +148,7 @@
Loading
181 148
    environment,
182 149
    () => {
183 150
      dispatch({
184 -
        type: stateChangeTypes.MenuBlur,
151 +
        type: stateChangeTypes.ToggleButtonBlur,
185 152
      })
186 153
    },
187 154
  )
@@ -209,7 +176,7 @@
Loading
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,69 +185,72 @@
Loading
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,32 +315,6 @@
Loading
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,38 +331,33 @@
Loading
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,10 +374,18 @@
Loading
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,18 +410,19 @@
Loading
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,6 +431,9 @@
Loading
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,7 +459,7 @@
Loading
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,7 +480,7 @@
Loading
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,7 +1,8 @@
Loading
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,22 +31,30 @@
Loading
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,4 +69,55 @@
Loading
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 +
}

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Everything is accounted for!

No changes detected that need to be reviewed.
What changes does Codecov check for?
Lines, not adjusted in diff, that have changed coverage data.
Files that introduced coverage data that had none before.
Files that have missing coverage data that once were tracked.
Files Coverage
src 100.00%
Project Totals (18 files) 100.00%
Loading