@toptal/picasso-timepicker 100.0.3 → 100.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist-package/src/TimePicker/TimePicker.d.ts +4 -1
  2. package/dist-package/src/TimePicker/TimePicker.d.ts.map +1 -1
  3. package/dist-package/src/TimePicker/TimePicker.js +60 -21
  4. package/dist-package/src/TimePicker/TimePicker.js.map +1 -1
  5. package/dist-package/src/TimePicker/use-time-picker-popover.d.ts +21 -0
  6. package/dist-package/src/TimePicker/use-time-picker-popover.d.ts.map +1 -0
  7. package/dist-package/src/TimePicker/use-time-picker-popover.js +96 -0
  8. package/dist-package/src/TimePicker/use-time-picker-popover.js.map +1 -0
  9. package/dist-package/src/TimePicker/utils/get-hour-cycle.d.ts +3 -0
  10. package/dist-package/src/TimePicker/utils/get-hour-cycle.d.ts.map +1 -0
  11. package/dist-package/src/TimePicker/utils/get-hour-cycle.js +42 -0
  12. package/dist-package/src/TimePicker/utils/get-hour-cycle.js.map +1 -0
  13. package/dist-package/src/TimePicker/utils/index.d.ts +5 -0
  14. package/dist-package/src/TimePicker/utils/index.d.ts.map +1 -0
  15. package/dist-package/src/TimePicker/utils/index.js +3 -0
  16. package/dist-package/src/TimePicker/utils/index.js.map +1 -0
  17. package/dist-package/src/TimePicker/utils/time.d.ts +16 -0
  18. package/dist-package/src/TimePicker/utils/time.d.ts.map +1 -0
  19. package/dist-package/src/TimePicker/utils/time.js +41 -0
  20. package/dist-package/src/TimePicker/utils/time.js.map +1 -0
  21. package/dist-package/src/TimePickerDropdown/TimePickerDropdown.d.ts +13 -0
  22. package/dist-package/src/TimePickerDropdown/TimePickerDropdown.d.ts.map +1 -0
  23. package/dist-package/src/TimePickerDropdown/TimePickerDropdown.js +28 -0
  24. package/dist-package/src/TimePickerDropdown/TimePickerDropdown.js.map +1 -0
  25. package/dist-package/src/TimePickerDropdown/index.d.ts +2 -0
  26. package/dist-package/src/TimePickerDropdown/index.d.ts.map +1 -0
  27. package/dist-package/src/TimePickerDropdown/index.js +2 -0
  28. package/dist-package/src/TimePickerDropdown/index.js.map +1 -0
  29. package/dist-package/src/TimePickerList/TimePickerList.d.ts +23 -0
  30. package/dist-package/src/TimePickerList/TimePickerList.d.ts.map +1 -0
  31. package/dist-package/src/TimePickerList/TimePickerList.js +97 -0
  32. package/dist-package/src/TimePickerList/TimePickerList.js.map +1 -0
  33. package/dist-package/src/TimePickerList/index.d.ts +4 -0
  34. package/dist-package/src/TimePickerList/index.d.ts.map +1 -0
  35. package/dist-package/src/TimePickerList/index.js +2 -0
  36. package/dist-package/src/TimePickerList/index.js.map +1 -0
  37. package/dist-package/src/TimePickerTrigger/TimePickerTrigger.d.ts +16 -0
  38. package/dist-package/src/TimePickerTrigger/TimePickerTrigger.d.ts.map +1 -0
  39. package/dist-package/src/TimePickerTrigger/TimePickerTrigger.js +11 -0
  40. package/dist-package/src/TimePickerTrigger/TimePickerTrigger.js.map +1 -0
  41. package/dist-package/src/TimePickerTrigger/index.d.ts +2 -0
  42. package/dist-package/src/TimePickerTrigger/index.d.ts.map +1 -0
  43. package/dist-package/src/TimePickerTrigger/index.js +2 -0
  44. package/dist-package/src/TimePickerTrigger/index.js.map +1 -0
  45. package/package.json +4 -1
  46. package/src/TimePicker/TimePicker.tsx +161 -65
  47. package/src/TimePicker/story/MinuteStep.example.tsx +16 -0
  48. package/src/TimePicker/story/index.jsx +10 -0
  49. package/src/TimePicker/test.tsx +649 -1
  50. package/src/TimePicker/use-time-picker-popover.ts +120 -0
  51. package/src/TimePicker/utils/get-hour-cycle.test.ts +76 -0
  52. package/src/TimePicker/utils/get-hour-cycle.ts +63 -0
  53. package/src/TimePicker/utils/index.ts +10 -0
  54. package/src/TimePicker/utils/time.test.ts +143 -0
  55. package/src/TimePicker/utils/time.ts +76 -0
  56. package/src/TimePickerDropdown/TimePickerDropdown.tsx +45 -0
  57. package/src/TimePickerDropdown/index.ts +1 -0
  58. package/src/TimePickerList/TimePickerList.tsx +190 -0
  59. package/src/TimePickerList/index.ts +4 -0
  60. package/src/TimePickerTrigger/TimePickerTrigger.tsx +53 -0
  61. package/src/TimePickerTrigger/index.ts +1 -0
@@ -1,9 +1,72 @@
1
1
  import React from 'react'
2
- import { render, fireEvent } from '@toptal/picasso-test-utils'
2
+ import {
3
+ act,
4
+ render,
5
+ fireEvent,
6
+ screen,
7
+ within,
8
+ } from '@toptal/picasso-test-utils'
9
+ import { detect } from 'detect-browser'
3
10
 
11
+ import type { Props } from './TimePicker'
4
12
  import { TimePicker } from './TimePicker'
13
+ import { getHourCycle } from './utils/get-hour-cycle'
5
14
 
15
+ jest.mock('./utils/get-hour-cycle')
16
+ jest.mock('detect-browser')
17
+
18
+ const mockedGetHourCycle = getHourCycle as jest.MockedFunction<
19
+ typeof getHourCycle
20
+ >
21
+ const mockedDetect = detect as jest.MockedFunction<typeof detect>
22
+
23
+ const renderComponent = (props: Partial<Props> = {}) => {
24
+ const handleChange = jest.fn()
25
+
26
+ render(
27
+ <TimePicker
28
+ value='21:30'
29
+ minuteStep={30}
30
+ onChange={handleChange}
31
+ {...props}
32
+ />
33
+ )
34
+
35
+ return { handleChange }
36
+ }
37
+
38
+ const getField = (value = '21:30') => screen.getByDisplayValue(value)
39
+
40
+ const openList = () =>
41
+ fireEvent.click(screen.getByRole('button', { name: 'Choose time' }))
42
+
43
+ const openListWithKeyboard = (value = '21:30') =>
44
+ fireEvent.keyDown(getField(value), { key: 'ArrowDown', altKey: true })
45
+
46
+ const queryList = () => screen.queryByRole('listbox', { name: 'Choose time' })
47
+
48
+ const getOptions = () =>
49
+ within(screen.getByRole('listbox', { name: 'Choose time' })).getAllByRole(
50
+ 'option'
51
+ )
52
+
53
+ const getOption = (name: string) => screen.getByRole('option', { name })
54
+
55
+ const getOptionLabels = () => getOptions().map(option => option.textContent)
56
+
57
+ const pressOnOption = (name: string, key: string) => {
58
+ openList()
59
+ act(() => getOption(name).focus())
60
+ fireEvent.keyDown(getOption(name), { key })
61
+ }
62
+
63
+ // eslint-disable-next-line max-lines-per-function, max-statements
6
64
  describe('TimePicker', () => {
65
+ beforeEach(() => {
66
+ mockedGetHourCycle.mockReturnValue(24)
67
+ mockedDetect.mockReturnValue(null)
68
+ })
69
+
7
70
  it('renders', () => {
8
71
  const time = '21:00'
9
72
 
@@ -51,4 +114,589 @@ describe('TimePicker', () => {
51
114
  expect(handleChange).toHaveBeenCalledWith(newTime)
52
115
  })
53
116
  })
117
+
118
+ describe('when minuteStep is not provided', () => {
119
+ it('renders no button for a time list', () => {
120
+ renderComponent({ minuteStep: undefined })
121
+
122
+ expect(
123
+ screen.queryByRole('button', { name: 'Choose time' })
124
+ ).not.toBeInTheDocument()
125
+ })
126
+ })
127
+
128
+ describe('when minuteStep is provided and the clock button is clicked', () => {
129
+ it('opens a list of times at that interval', () => {
130
+ renderComponent({ minuteStep: 30 })
131
+
132
+ openList()
133
+
134
+ expect(getOptionLabels()).toHaveLength(48)
135
+ expect(getOptionLabels().slice(0, 3)).toEqual(['00:00', '00:30', '01:00'])
136
+ expect(getOptionLabels().slice(-1)).toEqual(['23:30'])
137
+ })
138
+
139
+ it('marks the time of the value as selected', () => {
140
+ renderComponent()
141
+
142
+ openList()
143
+
144
+ expect(getOption('21:30')).toHaveAttribute('aria-selected', 'true')
145
+ expect(getOption('09:00')).toHaveAttribute('aria-selected', 'false')
146
+ })
147
+ })
148
+
149
+ describe('when minuteStep is 15', () => {
150
+ it('offers a time every quarter of an hour', () => {
151
+ renderComponent({ minuteStep: 15 })
152
+
153
+ openList()
154
+
155
+ expect(getOptionLabels()).toHaveLength(96)
156
+ expect(getOptionLabels().slice(0, 4)).toEqual([
157
+ '00:00',
158
+ '00:15',
159
+ '00:30',
160
+ '00:45',
161
+ ])
162
+ })
163
+ })
164
+
165
+ describe('when the clock button is clicked twice', () => {
166
+ it('closes the list', () => {
167
+ renderComponent()
168
+
169
+ openList()
170
+ openList()
171
+
172
+ expect(queryList()).not.toBeInTheDocument()
173
+ })
174
+ })
175
+
176
+ describe('when a time is picked', () => {
177
+ const setup = () => {
178
+ const { handleChange } = renderComponent()
179
+
180
+ openList()
181
+ fireEvent.click(getOption('09:00'))
182
+
183
+ return handleChange
184
+ }
185
+
186
+ it('calls onChange with the picked time', () => {
187
+ const handleChange = setup()
188
+
189
+ expect(handleChange).toHaveBeenCalledTimes(1)
190
+ expect(handleChange).toHaveBeenCalledWith('09:00')
191
+ })
192
+
193
+ it('closes the list', () => {
194
+ setup()
195
+
196
+ expect(queryList()).not.toBeInTheDocument()
197
+ })
198
+
199
+ it('shows the picked time in the field', () => {
200
+ setup()
201
+
202
+ expect(getField('09:00')).toBeInTheDocument()
203
+ })
204
+ })
205
+
206
+ describe('when the value is not on the step', () => {
207
+ it('marks no time as selected', () => {
208
+ renderComponent({ value: '21:07', minuteStep: 15 })
209
+
210
+ openList()
211
+
212
+ expect(
213
+ getOptions().filter(
214
+ option => option.getAttribute('aria-selected') === 'true'
215
+ )
216
+ ).toHaveLength(0)
217
+ })
218
+
219
+ it('moves keyboard focus to the nearest time', () => {
220
+ renderComponent({ value: '21:07', minuteStep: 15 })
221
+
222
+ openListWithKeyboard('21:07')
223
+
224
+ expect(getOption('21:00')).toHaveFocus()
225
+ })
226
+ })
227
+
228
+ describe('when the field shows a 12-hour clock', () => {
229
+ beforeEach(() => {
230
+ mockedGetHourCycle.mockReturnValue(12)
231
+ })
232
+
233
+ it('labels the times with AM and PM', () => {
234
+ renderComponent()
235
+
236
+ openList()
237
+
238
+ expect(getOptionLabels().slice(0, 3)).toEqual([
239
+ '12:00 AM',
240
+ '12:30 AM',
241
+ '01:00 AM',
242
+ ])
243
+ expect(getOptionLabels().slice(-1)).toEqual(['11:30 PM'])
244
+ })
245
+
246
+ it('marks the 12-hour label of the value as selected', () => {
247
+ renderComponent()
248
+
249
+ openList()
250
+
251
+ expect(getOption('09:30 PM')).toHaveAttribute('aria-selected', 'true')
252
+ })
253
+ })
254
+
255
+ describe('when a PM time is picked on a 12-hour clock', () => {
256
+ beforeEach(() => {
257
+ mockedGetHourCycle.mockReturnValue(12)
258
+ })
259
+
260
+ it('calls onChange with the 24-hour value', () => {
261
+ const { handleChange } = renderComponent()
262
+
263
+ openList()
264
+ fireEvent.click(getOption('07:00 PM'))
265
+
266
+ expect(handleChange).toHaveBeenCalledWith('19:00')
267
+ })
268
+ })
269
+
270
+ describe('when the browser is Safari and the locale uses a 12-hour clock', () => {
271
+ beforeEach(() => {
272
+ mockedGetHourCycle.mockReturnValue(12)
273
+ mockedDetect.mockReturnValue({
274
+ name: 'safari',
275
+ version: '17.0.0',
276
+ os: 'Mac OS',
277
+ type: 'browser',
278
+ })
279
+ })
280
+
281
+ it('labels the times in 24-hour format to match its field', () => {
282
+ renderComponent()
283
+
284
+ openList()
285
+
286
+ expect(getOptionLabels().slice(-1)).toEqual(['23:30'])
287
+ })
288
+ })
289
+
290
+ describe('when a time is picked for an empty value', () => {
291
+ it('calls onChange with the picked time', () => {
292
+ const { handleChange } = renderComponent({ value: undefined })
293
+
294
+ openList()
295
+ fireEvent.click(getOption('14:30'))
296
+
297
+ expect(handleChange).toHaveBeenCalledWith('14:30')
298
+ })
299
+ })
300
+
301
+ describe('when Escape is pressed in the field with the list open', () => {
302
+ const setup = () => {
303
+ const { handleChange } = renderComponent()
304
+
305
+ openList()
306
+ fireEvent.keyDown(getField(), { key: 'Escape' })
307
+
308
+ return handleChange
309
+ }
310
+
311
+ it('closes the list', () => {
312
+ setup()
313
+
314
+ expect(queryList()).not.toBeInTheDocument()
315
+ })
316
+
317
+ it('keeps the value', () => {
318
+ const handleChange = setup()
319
+
320
+ expect(handleChange).not.toHaveBeenCalled()
321
+ expect(getField()).toBeInTheDocument()
322
+ })
323
+ })
324
+
325
+ describe('when clicking outside of the open list', () => {
326
+ beforeEach(() => {
327
+ jest.useFakeTimers()
328
+ })
329
+
330
+ afterEach(() => {
331
+ jest.useRealTimers()
332
+ })
333
+
334
+ it('closes the list', () => {
335
+ renderComponent()
336
+
337
+ openList()
338
+ // ClickAwayListener starts listening on the next tick after it mounts
339
+ act(() => {
340
+ jest.runOnlyPendingTimers()
341
+ })
342
+ fireEvent.click(document.body)
343
+
344
+ expect(queryList()).not.toBeInTheDocument()
345
+ })
346
+ })
347
+
348
+ describe('when disabled', () => {
349
+ it('does not open the list', () => {
350
+ renderComponent({ disabled: true })
351
+
352
+ openList()
353
+
354
+ expect(queryList()).not.toBeInTheDocument()
355
+ })
356
+ })
357
+
358
+ describe('when the field becomes disabled while the list is open', () => {
359
+ it('closes the list', () => {
360
+ const { rerender } = render(
361
+ <TimePicker value='21:30' minuteStep={30} onChange={jest.fn()} />
362
+ )
363
+
364
+ openList()
365
+ expect(queryList()).toBeInTheDocument()
366
+
367
+ rerender(
368
+ <TimePicker
369
+ value='21:30'
370
+ minuteStep={30}
371
+ onChange={jest.fn()}
372
+ disabled
373
+ />
374
+ )
375
+
376
+ expect(queryList()).not.toBeInTheDocument()
377
+ })
378
+ })
379
+
380
+ describe('when read-only', () => {
381
+ it('does not open the list', () => {
382
+ renderComponent({ readOnly: true })
383
+
384
+ openList()
385
+
386
+ expect(queryList()).not.toBeInTheDocument()
387
+ })
388
+ })
389
+
390
+ describe('when onKeyDown is provided', () => {
391
+ it('calls it for key presses in the field', () => {
392
+ const handleKeyDown = jest.fn()
393
+
394
+ renderComponent({ onKeyDown: handleKeyDown })
395
+
396
+ fireEvent.keyDown(getField(), { key: 'a' })
397
+
398
+ expect(handleKeyDown).toHaveBeenCalledTimes(1)
399
+ expect(handleKeyDown.mock.calls[0][0].key).toBe('a')
400
+ })
401
+
402
+ it('still opens the list on Alt+ArrowDown', () => {
403
+ renderComponent({ onKeyDown: jest.fn() })
404
+
405
+ openListWithKeyboard()
406
+
407
+ expect(queryList()).toBeInTheDocument()
408
+ })
409
+ })
410
+
411
+ describe('when Alt+ArrowDown is pressed in the field with the list open', () => {
412
+ const pressAltArrowDownWithListOpen = () => {
413
+ openList()
414
+
415
+ return fireEvent.keyDown(getField(), { key: 'ArrowDown', altKey: true })
416
+ }
417
+
418
+ it('keeps a single list open', () => {
419
+ renderComponent()
420
+
421
+ pressAltArrowDownWithListOpen()
422
+
423
+ expect(
424
+ screen.getAllByRole('listbox', { name: 'Choose time' })
425
+ ).toHaveLength(1)
426
+ })
427
+
428
+ it('prevents the browser from opening its own picker', () => {
429
+ renderComponent()
430
+
431
+ const defaultNotPrevented = pressAltArrowDownWithListOpen()
432
+
433
+ expect(defaultNotPrevented).toBe(false)
434
+ })
435
+
436
+ it('moves keyboard focus into the list', () => {
437
+ renderComponent()
438
+
439
+ pressAltArrowDownWithListOpen()
440
+
441
+ expect(getOption('21:30')).toHaveFocus()
442
+ })
443
+ })
444
+
445
+ describe('when the list is opened with the keyboard', () => {
446
+ it('does not call onBlur although focus moves into the list', () => {
447
+ const handleBlur = jest.fn()
448
+
449
+ renderComponent({ onBlur: handleBlur })
450
+
451
+ openListWithKeyboard()
452
+
453
+ expect(getOption('21:30')).toHaveFocus()
454
+ expect(handleBlur).not.toHaveBeenCalled()
455
+ })
456
+ })
457
+
458
+ describe('when a time is picked with the keyboard', () => {
459
+ it('does not call onFocus although focus returns to the field', () => {
460
+ const handleFocus = jest.fn()
461
+
462
+ renderComponent({ onFocus: handleFocus })
463
+
464
+ openListWithKeyboard()
465
+ fireEvent.keyDown(getOption('21:30'), { key: 'Enter' })
466
+
467
+ expect(getField()).toHaveFocus()
468
+ expect(handleFocus).not.toHaveBeenCalled()
469
+ })
470
+ })
471
+
472
+ describe('when focus leaves the field for something else than the list', () => {
473
+ it('calls onBlur', () => {
474
+ const handleBlur = jest.fn()
475
+
476
+ renderComponent({ onBlur: handleBlur })
477
+
478
+ act(() => getField().focus())
479
+ act(() => getField().blur())
480
+
481
+ expect(handleBlur).toHaveBeenCalledTimes(1)
482
+ })
483
+ })
484
+
485
+ describe('when the field is focused from something else than the list', () => {
486
+ it('calls onFocus', () => {
487
+ const handleFocus = jest.fn()
488
+
489
+ renderComponent({ onFocus: handleFocus })
490
+
491
+ act(() => getField().focus())
492
+
493
+ expect(handleFocus).toHaveBeenCalledTimes(1)
494
+ })
495
+ })
496
+
497
+ describe('when the clock button is clicked while the list has focus', () => {
498
+ it('closes the list and returns focus to the field', () => {
499
+ renderComponent()
500
+
501
+ openListWithKeyboard()
502
+ expect(getOption('21:30')).toHaveFocus()
503
+
504
+ fireEvent.click(screen.getByRole('button', { name: 'Choose time' }))
505
+
506
+ expect(queryList()).not.toBeInTheDocument()
507
+ expect(getField()).toHaveFocus()
508
+ })
509
+ })
510
+
511
+ describe('when the value changes while the list is open', () => {
512
+ it('focuses the time of the new value when the list is focused with the keyboard', () => {
513
+ const { rerender } = render(
514
+ <TimePicker value='21:30' minuteStep={30} onChange={jest.fn()} />
515
+ )
516
+
517
+ openList()
518
+ rerender(
519
+ <TimePicker value='10:00' minuteStep={30} onChange={jest.fn()} />
520
+ )
521
+ fireEvent.keyDown(getField('10:00'), { key: ' ' })
522
+
523
+ expect(getOption('10:00')).toHaveFocus()
524
+ })
525
+ })
526
+
527
+ describe('when focus returns to the field while the list stays open', () => {
528
+ it('moves focus into the list again on the next Space', () => {
529
+ renderComponent()
530
+
531
+ openListWithKeyboard()
532
+ expect(getOption('21:30')).toHaveFocus()
533
+
534
+ act(() => getField().focus())
535
+ fireEvent.keyDown(getField(), { key: ' ' })
536
+
537
+ expect(queryList()).toBeInTheDocument()
538
+ expect(getOption('21:30')).toHaveFocus()
539
+ })
540
+ })
541
+
542
+ describe('when Space is pressed in the field', () => {
543
+ it('opens the list with keyboard focus on the selected time', () => {
544
+ renderComponent()
545
+
546
+ fireEvent.keyDown(getField(), { key: ' ' })
547
+
548
+ expect(queryList()).toBeInTheDocument()
549
+ expect(getOption('21:30')).toHaveFocus()
550
+ })
551
+
552
+ it('prevents the browser from opening its own picker', () => {
553
+ renderComponent()
554
+
555
+ const defaultNotPrevented = fireEvent.keyDown(getField(), { key: ' ' })
556
+
557
+ expect(defaultNotPrevented).toBe(false)
558
+ })
559
+ })
560
+
561
+ describe('when Space is pressed in the field with the list open', () => {
562
+ it('keeps a single list open and focuses it', () => {
563
+ renderComponent()
564
+
565
+ openList()
566
+ fireEvent.keyDown(getField(), { key: ' ' })
567
+
568
+ expect(
569
+ screen.getAllByRole('listbox', { name: 'Choose time' })
570
+ ).toHaveLength(1)
571
+ expect(getOption('21:30')).toHaveFocus()
572
+ })
573
+ })
574
+
575
+ describe('when Alt+ArrowDown is pressed in the field', () => {
576
+ it('opens the list', () => {
577
+ renderComponent()
578
+
579
+ openListWithKeyboard()
580
+
581
+ expect(queryList()).toBeInTheDocument()
582
+ })
583
+
584
+ it('moves focus to the selected time', () => {
585
+ renderComponent()
586
+
587
+ openListWithKeyboard()
588
+
589
+ expect(getOption('21:30')).toHaveFocus()
590
+ })
591
+ })
592
+
593
+ describe('when ArrowDown is pressed on a time', () => {
594
+ it('moves focus to the next time', () => {
595
+ renderComponent()
596
+
597
+ pressOnOption('21:30', 'ArrowDown')
598
+
599
+ expect(getOption('22:00')).toHaveFocus()
600
+ })
601
+
602
+ it('keeps the value', () => {
603
+ const { handleChange } = renderComponent()
604
+
605
+ pressOnOption('21:30', 'ArrowDown')
606
+
607
+ expect(handleChange).not.toHaveBeenCalled()
608
+ })
609
+ })
610
+
611
+ describe('when ArrowUp is pressed on a time', () => {
612
+ it('moves focus to the previous time', () => {
613
+ renderComponent()
614
+
615
+ pressOnOption('21:30', 'ArrowUp')
616
+
617
+ expect(getOption('21:00')).toHaveFocus()
618
+ })
619
+ })
620
+
621
+ describe('when ArrowDown is pressed on the last time', () => {
622
+ it('keeps focus on the last time', () => {
623
+ renderComponent()
624
+
625
+ pressOnOption('23:30', 'ArrowDown')
626
+
627
+ expect(getOption('23:30')).toHaveFocus()
628
+ })
629
+ })
630
+
631
+ describe('when Home is pressed on a time', () => {
632
+ it('moves focus to the first time', () => {
633
+ renderComponent()
634
+
635
+ pressOnOption('21:30', 'Home')
636
+
637
+ expect(getOption('00:00')).toHaveFocus()
638
+ })
639
+ })
640
+
641
+ describe('when End is pressed on a time', () => {
642
+ it('moves focus to the last time', () => {
643
+ renderComponent()
644
+
645
+ pressOnOption('21:30', 'End')
646
+
647
+ expect(getOption('23:30')).toHaveFocus()
648
+ })
649
+ })
650
+
651
+ describe('when Enter is pressed on a time', () => {
652
+ it('calls onChange with that time', () => {
653
+ const { handleChange } = renderComponent()
654
+
655
+ pressOnOption('09:00', 'Enter')
656
+
657
+ expect(handleChange).toHaveBeenCalledWith('09:00')
658
+ })
659
+
660
+ it('closes the list', () => {
661
+ renderComponent()
662
+
663
+ pressOnOption('09:00', 'Enter')
664
+
665
+ expect(queryList()).not.toBeInTheDocument()
666
+ })
667
+
668
+ it('returns focus to the field', () => {
669
+ renderComponent()
670
+
671
+ pressOnOption('09:00', 'Enter')
672
+
673
+ expect(getField('09:00')).toHaveFocus()
674
+ })
675
+ })
676
+
677
+ describe('when Escape is pressed on a time', () => {
678
+ it('closes the list', () => {
679
+ renderComponent()
680
+
681
+ pressOnOption('09:00', 'Escape')
682
+
683
+ expect(queryList()).not.toBeInTheDocument()
684
+ })
685
+
686
+ it('keeps the value', () => {
687
+ const { handleChange } = renderComponent()
688
+
689
+ pressOnOption('09:00', 'Escape')
690
+
691
+ expect(handleChange).not.toHaveBeenCalled()
692
+ })
693
+
694
+ it('returns focus to the field', () => {
695
+ renderComponent()
696
+
697
+ pressOnOption('09:00', 'Escape')
698
+
699
+ expect(getField()).toHaveFocus()
700
+ })
701
+ })
54
702
  })