@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
@@ -0,0 +1,120 @@
1
+ import type React from 'react'
2
+ import { useEffect, useRef, useState } from 'react'
3
+
4
+ import type { HourCycle } from './utils'
5
+
6
+ interface Options {
7
+ enabled: boolean
8
+ getHourCycle: () => HourCycle
9
+ }
10
+
11
+ // The shortcuts a browser uses to open the native time picker
12
+ const isOpenShortcut = (event: React.KeyboardEvent) =>
13
+ event.key === ' ' || (event.key === 'ArrowDown' && event.altKey)
14
+
15
+ export const useTimePickerPopover = ({ enabled, getHourCycle }: Options) => {
16
+ const [isOpen, setIsOpen] = useState(false)
17
+ const [focusList, setFocusList] = useState(false)
18
+ const [hourCycle, setHourCycle] = useState<HourCycle>(24)
19
+ const anchorRef = useRef<HTMLElement>(null)
20
+ const listRef = useRef<HTMLDivElement>(null)
21
+
22
+ const focusField = () =>
23
+ anchorRef.current
24
+ ?.querySelector<HTMLInputElement>('input:not([readonly])')
25
+ ?.focus()
26
+
27
+ const open = ({ focusList: shouldFocusList }: { focusList: boolean }) => {
28
+ setFocusList(shouldFocusList)
29
+ setHourCycle(getHourCycle())
30
+ setIsOpen(true)
31
+ }
32
+
33
+ const close = () => setIsOpen(false)
34
+
35
+ useEffect(() => {
36
+ if (!enabled) {
37
+ close()
38
+ }
39
+ }, [enabled])
40
+
41
+ const closeAndFocusField = () => {
42
+ close()
43
+ focusField()
44
+ }
45
+
46
+ const handleTriggerClick = () => {
47
+ if (isOpen) {
48
+ closeAndFocusField()
49
+
50
+ return
51
+ }
52
+
53
+ open({ focusList: false })
54
+ focusField()
55
+ }
56
+
57
+ const handleClickAway = (event: React.MouseEvent) => {
58
+ if (!anchorRef.current?.contains(event.target as Node)) {
59
+ close()
60
+ }
61
+ }
62
+
63
+ const isFocusChangeWithinList = (event: React.FocusEvent) =>
64
+ event.relatedTarget instanceof Node &&
65
+ Boolean(listRef.current?.contains(event.relatedTarget))
66
+
67
+ const handleFieldFocus = () => {
68
+ // Focus is back in the field, so the next shortcut must be able to move it
69
+ // into the list again
70
+ setFocusList(false)
71
+ }
72
+
73
+ const handleFieldKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
74
+ if (!isOpen) {
75
+ if (enabled && isOpenShortcut(event)) {
76
+ event.preventDefault()
77
+ open({ focusList: true })
78
+ }
79
+
80
+ return
81
+ }
82
+
83
+ if (isOpenShortcut(event)) {
84
+ // Keeps the browser from opening its own time picker over the list
85
+ event.preventDefault()
86
+ setFocusList(true)
87
+
88
+ return
89
+ }
90
+
91
+ switch (event.key) {
92
+ case 'Escape':
93
+ // Keeps a surrounding Modal or Drawer open while the list closes
94
+ event.stopPropagation()
95
+ close()
96
+ break
97
+ case 'Enter':
98
+ event.preventDefault()
99
+ close()
100
+ break
101
+ case 'Tab':
102
+ close()
103
+ break
104
+ }
105
+ }
106
+
107
+ return {
108
+ isOpen,
109
+ hourCycle,
110
+ anchorRef,
111
+ listRef,
112
+ focusList,
113
+ isFocusChangeWithinList,
114
+ handleTriggerClick,
115
+ handleClickAway,
116
+ closeAndFocusField,
117
+ handleFieldFocus,
118
+ handleFieldKeyDown,
119
+ }
120
+ }
@@ -0,0 +1,76 @@
1
+ import { getHourCycle } from './get-hour-cycle'
2
+
3
+ const PROBE_HEIGHT = 100
4
+
5
+ const mockNativeFieldLayout = (renderedSegments: string[]) => {
6
+ const appendChild = document.body.appendChild.bind(document.body)
7
+
8
+ jest.spyOn(document.body, 'appendChild').mockImplementation(node => {
9
+ if (node instanceof HTMLElement) {
10
+ const testId = node.getAttribute('data-testid')
11
+ const isStretched = renderedSegments.some(
12
+ segment => testId === `time-picker-probe-${segment}`
13
+ )
14
+
15
+ Object.defineProperty(node, 'offsetHeight', {
16
+ value: isStretched ? PROBE_HEIGHT : 17,
17
+ })
18
+ }
19
+
20
+ return appendChild(node)
21
+ })
22
+ }
23
+
24
+ describe('getHourCycle', () => {
25
+ afterEach(() => {
26
+ jest.restoreAllMocks()
27
+ })
28
+
29
+ describe('when the native time field renders an AM/PM segment', () => {
30
+ beforeEach(() => {
31
+ mockNativeFieldLayout(['hour', 'ampm'])
32
+ })
33
+
34
+ it('leaves no probe elements in the document', () => {
35
+ getHourCycle('de-DE')
36
+
37
+ expect(document.querySelector('input')).not.toBeInTheDocument()
38
+ })
39
+
40
+ describe('when the locale uses a 24-hour clock', () => {
41
+ it('returns 12', () => {
42
+ expect(getHourCycle('de-DE')).toBe(12)
43
+ })
44
+ })
45
+ })
46
+
47
+ describe('when the native time field renders no AM/PM segment', () => {
48
+ beforeEach(() => {
49
+ mockNativeFieldLayout(['hour'])
50
+ })
51
+
52
+ describe('when the locale uses a 12-hour clock', () => {
53
+ it('returns 24', () => {
54
+ expect(getHourCycle('en-US')).toBe(24)
55
+ })
56
+ })
57
+ })
58
+
59
+ describe('when the browser does not expose the segments of the native field', () => {
60
+ beforeEach(() => {
61
+ mockNativeFieldLayout([])
62
+ })
63
+
64
+ describe('when the locale uses a 12-hour clock', () => {
65
+ it('returns 12', () => {
66
+ expect(getHourCycle('en-US')).toBe(12)
67
+ })
68
+ })
69
+
70
+ describe('when the locale uses a 24-hour clock', () => {
71
+ it('returns 24', () => {
72
+ expect(getHourCycle('de-DE')).toBe(24)
73
+ })
74
+ })
75
+ })
76
+ })
@@ -0,0 +1,63 @@
1
+ export type HourCycle = 12 | 24
2
+
3
+ type Segment = 'hour' | 'ampm'
4
+
5
+ const PROBE_CLASS_NAME =
6
+ 'fixed top-0 left-0 invisible pointer-events-none h-auto p-0 border-0'
7
+
8
+ // Blink calls the AM/PM segment `ampm-field`, WebKit calls it `meridiem-field`
9
+ const SEGMENT_CLASS_NAMES: Record<Segment, string> = {
10
+ hour: '[&::-webkit-datetime-edit-hour-field]:inline-block [&::-webkit-datetime-edit-hour-field]:h-screen',
11
+ ampm: '[&::-webkit-datetime-edit-ampm-field]:inline-block [&::-webkit-datetime-edit-ampm-field]:h-screen [&::-webkit-datetime-edit-meridiem-field]:inline-block [&::-webkit-datetime-edit-meridiem-field]:h-screen',
12
+ }
13
+
14
+ const measureProbe = (segment?: Segment) => {
15
+ const probe = document.createElement('input')
16
+
17
+ probe.type = 'time'
18
+ probe.tabIndex = -1
19
+ probe.setAttribute('aria-hidden', 'true')
20
+ probe.className = PROBE_CLASS_NAME
21
+
22
+ if (segment) {
23
+ probe.className += ` ${SEGMENT_CLASS_NAMES[segment]}`
24
+ probe.setAttribute('data-testid', `time-picker-probe-${segment}`)
25
+ }
26
+
27
+ document.body.appendChild(probe)
28
+
29
+ const height = probe.offsetHeight
30
+
31
+ probe.remove()
32
+
33
+ return height
34
+ }
35
+
36
+ // The native field follows the operating system's 12/24-hour setting, which
37
+ // `Intl` does not see, so the field itself is the only reliable source
38
+ const getNativeFieldHourCycle = (): HourCycle | undefined => {
39
+ if (typeof document === 'undefined') {
40
+ return undefined
41
+ }
42
+
43
+ const stretchedHeight = measureProbe() * 2
44
+ const rendersSegment = (segment: Segment) =>
45
+ measureProbe(segment) > stretchedHeight
46
+
47
+ if (!rendersSegment('hour')) {
48
+ return undefined
49
+ }
50
+
51
+ return rendersSegment('ampm') ? 12 : 24
52
+ }
53
+
54
+ const getLocaleHourCycle = (locale?: string): HourCycle => {
55
+ const { hour12 } = new Intl.DateTimeFormat(locale, {
56
+ hour: 'numeric',
57
+ }).resolvedOptions()
58
+
59
+ return hour12 ? 12 : 24
60
+ }
61
+
62
+ export const getHourCycle = (locale?: string): HourCycle =>
63
+ getNativeFieldHourCycle() ?? getLocaleHourCycle(locale)
@@ -0,0 +1,10 @@
1
+ export { getHourCycle } from './get-hour-cycle'
2
+ export type { HourCycle } from './get-hour-cycle'
3
+ export {
4
+ VALID_TIME_REGEX,
5
+ getCurrentTime,
6
+ getNearestOptionIndex,
7
+ getTimeOptions,
8
+ parseTime,
9
+ } from './time'
10
+ export type { Time, TimeOption } from './time'
@@ -0,0 +1,143 @@
1
+ import {
2
+ VALID_TIME_REGEX,
3
+ formatTime,
4
+ getCurrentTime,
5
+ getNearestOptionIndex,
6
+ getTimeOptions,
7
+ parseTime,
8
+ } from './time'
9
+
10
+ describe('VALID_TIME_REGEX', () => {
11
+ it.each(['00:00', '09:05', '12:30', '19:59', '23:59'])(
12
+ 'accepts %s',
13
+ value => {
14
+ expect(VALID_TIME_REGEX.test(value)).toBe(true)
15
+ }
16
+ )
17
+
18
+ it.each([
19
+ ['24:00', 'the hour is out of range'],
20
+ ['12:60', 'the minute is out of range'],
21
+ ['9:30', 'the hour is not padded'],
22
+ ['12:3', 'the minute is not padded'],
23
+ ['12:30:00', 'seconds are included'],
24
+ ['12.30', 'the separator is not a colon'],
25
+ [' 12:30', 'there is surrounding whitespace'],
26
+ ['', 'the value is empty'],
27
+ ])('rejects "%s" because %s', value => {
28
+ expect(VALID_TIME_REGEX.test(value)).toBe(false)
29
+ })
30
+ })
31
+
32
+ describe('parseTime', () => {
33
+ describe('when the value is a valid time', () => {
34
+ it('returns the hours and minutes as numbers', () => {
35
+ expect(parseTime('09:05')).toEqual({ hours: 9, minutes: 5 })
36
+ expect(parseTime('23:59')).toEqual({ hours: 23, minutes: 59 })
37
+ })
38
+ })
39
+
40
+ describe('when the value is not a valid time', () => {
41
+ it('returns undefined', () => {
42
+ expect(parseTime('24:00')).toBeUndefined()
43
+ expect(parseTime('9:05')).toBeUndefined()
44
+ })
45
+ })
46
+
47
+ describe('when the value is missing', () => {
48
+ it('returns undefined', () => {
49
+ expect(parseTime()).toBeUndefined()
50
+ expect(parseTime('')).toBeUndefined()
51
+ })
52
+ })
53
+ })
54
+
55
+ describe('formatTime', () => {
56
+ it('pads hours and minutes to two digits', () => {
57
+ expect(formatTime({ hours: 0, minutes: 0 })).toBe('00:00')
58
+ expect(formatTime({ hours: 9, minutes: 5 })).toBe('09:05')
59
+ expect(formatTime({ hours: 23, minutes: 59 })).toBe('23:59')
60
+ })
61
+ })
62
+
63
+ describe('getCurrentTime', () => {
64
+ beforeEach(() => {
65
+ jest.useFakeTimers().setSystemTime(new Date(2026, 0, 1, 14, 7))
66
+ })
67
+
68
+ afterEach(() => {
69
+ jest.useRealTimers()
70
+ })
71
+
72
+ it('returns the local hours and minutes of now', () => {
73
+ expect(getCurrentTime()).toEqual({ hours: 14, minutes: 7 })
74
+ })
75
+ })
76
+
77
+ describe('getTimeOptions', () => {
78
+ it('offers one option per step across the whole day', () => {
79
+ expect(getTimeOptions(60, 24)).toHaveLength(24)
80
+ expect(getTimeOptions(30, 24)).toHaveLength(48)
81
+ expect(getTimeOptions(15, 24)).toHaveLength(96)
82
+ })
83
+
84
+ it('keeps values in 24-hour HH:mm format regardless of the hour cycle', () => {
85
+ const values = (hourCycle: 12 | 24) =>
86
+ getTimeOptions(30, hourCycle).map(option => option.value)
87
+
88
+ expect(values(12)).toEqual(values(24))
89
+ expect(values(24).slice(0, 3)).toEqual(['00:00', '00:30', '01:00'])
90
+ expect(values(24).slice(-1)).toEqual(['23:30'])
91
+ })
92
+
93
+ describe('when the hour cycle is 24', () => {
94
+ it('labels the options with the 24-hour time', () => {
95
+ const labels = getTimeOptions(30, 24).map(option => option.label)
96
+
97
+ expect(labels).toContain('00:00')
98
+ expect(labels).toContain('12:30')
99
+ expect(labels).toContain('13:00')
100
+ expect(labels).toContain('23:30')
101
+ expect(labels.some(label => /AM|PM/.test(label))).toBe(false)
102
+ })
103
+ })
104
+
105
+ describe('when the hour cycle is 12', () => {
106
+ it('labels the options with a 12-hour time and a period', () => {
107
+ const labelOf = (value: string) =>
108
+ getTimeOptions(30, 12).find(option => option.value === value)?.label
109
+
110
+ expect(labelOf('00:00')).toBe('12:00 AM')
111
+ expect(labelOf('00:30')).toBe('12:30 AM')
112
+ expect(labelOf('01:00')).toBe('01:00 AM')
113
+ expect(labelOf('11:30')).toBe('11:30 AM')
114
+ expect(labelOf('12:00')).toBe('12:00 PM')
115
+ expect(labelOf('13:00')).toBe('01:00 PM')
116
+ expect(labelOf('23:30')).toBe('11:30 PM')
117
+ })
118
+ })
119
+ })
120
+
121
+ describe('getNearestOptionIndex', () => {
122
+ describe('when the time falls on an option', () => {
123
+ it('returns the index of that option', () => {
124
+ expect(getNearestOptionIndex({ hours: 0, minutes: 0 }, 15)).toBe(0)
125
+ expect(getNearestOptionIndex({ hours: 10, minutes: 0 }, 15)).toBe(40)
126
+ expect(getNearestOptionIndex({ hours: 23, minutes: 45 }, 15)).toBe(95)
127
+ })
128
+ })
129
+
130
+ describe('when the time falls between options', () => {
131
+ it('returns the index of the closest option', () => {
132
+ expect(getNearestOptionIndex({ hours: 10, minutes: 7 }, 15)).toBe(40)
133
+ expect(getNearestOptionIndex({ hours: 10, minutes: 8 }, 15)).toBe(41)
134
+ })
135
+ })
136
+
137
+ describe('when the time is closer to midnight than to the last option', () => {
138
+ it('returns the index of the last option', () => {
139
+ expect(getNearestOptionIndex({ hours: 23, minutes: 59 }, 15)).toBe(95)
140
+ expect(getNearestOptionIndex({ hours: 23, minutes: 50 }, 60)).toBe(23)
141
+ })
142
+ })
143
+ })
@@ -0,0 +1,76 @@
1
+ import type { HourCycle } from './get-hour-cycle'
2
+
3
+ export interface Time {
4
+ hours: number
5
+ minutes: number
6
+ }
7
+
8
+ export interface TimeOption {
9
+ value: string
10
+ label: string
11
+ }
12
+
13
+ export const VALID_TIME_REGEX = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/
14
+
15
+ const MINUTES_IN_HOUR = 60
16
+ const MINUTES_IN_DAY = 24 * MINUTES_IN_HOUR
17
+ const HALF_DAY = 12
18
+
19
+ const padTimePart = (part: number) => String(part).padStart(2, '0')
20
+
21
+ export const parseTime = (value?: string): Time | undefined => {
22
+ if (!value || !VALID_TIME_REGEX.test(value)) {
23
+ return undefined
24
+ }
25
+
26
+ const [hours, minutes] = value.split(':').map(Number)
27
+
28
+ return { hours, minutes }
29
+ }
30
+
31
+ export const formatTime = ({ hours, minutes }: Time) =>
32
+ `${padTimePart(hours)}:${padTimePart(minutes)}`
33
+
34
+ export const getCurrentTime = (): Time => {
35
+ const now = new Date()
36
+
37
+ return { hours: now.getHours(), minutes: now.getMinutes() }
38
+ }
39
+
40
+ const formatLabel = (time: Time, hourCycle: HourCycle) => {
41
+ if (hourCycle === 24) {
42
+ return formatTime(time)
43
+ }
44
+
45
+ const hours = time.hours % HALF_DAY || HALF_DAY
46
+ const period = time.hours < HALF_DAY ? 'AM' : 'PM'
47
+
48
+ return `${formatTime({ hours, minutes: time.minutes })} ${period}`
49
+ }
50
+
51
+ export const getTimeOptions = (
52
+ minuteStep: number,
53
+ hourCycle: HourCycle
54
+ ): TimeOption[] => {
55
+ const options: TimeOption[] = []
56
+
57
+ for (let minutes = 0; minutes < MINUTES_IN_DAY; minutes += minuteStep) {
58
+ const time = {
59
+ hours: Math.floor(minutes / MINUTES_IN_HOUR),
60
+ minutes: minutes % MINUTES_IN_HOUR,
61
+ }
62
+
63
+ options.push({
64
+ value: formatTime(time),
65
+ label: formatLabel(time, hourCycle),
66
+ })
67
+ }
68
+
69
+ return options
70
+ }
71
+
72
+ export const getNearestOptionIndex = (time: Time, minuteStep: number) =>
73
+ Math.min(
74
+ Math.round((time.hours * MINUTES_IN_HOUR + time.minutes) / minuteStep),
75
+ Math.ceil(MINUTES_IN_DAY / minuteStep) - 1
76
+ )
@@ -0,0 +1,45 @@
1
+ import React, { forwardRef } from 'react'
2
+ import { Popper } from '@toptal/picasso-popper'
3
+ import { ClickAwayListener } from '@toptal/picasso-utils'
4
+
5
+ import { TimePickerList } from '../TimePickerList'
6
+ import type { TimePickerListProps } from '../TimePickerList'
7
+
8
+ export interface Props extends TimePickerListProps {
9
+ /** Whether the dropdown is shown */
10
+ open: boolean
11
+ /** Element the dropdown is positioned under */
12
+ anchorEl: HTMLElement | null
13
+ /** Called when a click lands outside of the dropdown */
14
+ onClickAway: (event: React.MouseEvent) => void
15
+ }
16
+
17
+ export const TimePickerDropdown = forwardRef<HTMLDivElement, Props>(
18
+ function TimePickerDropdown(
19
+ { open, anchorEl, onClickAway, ...rest }: Props,
20
+ ref
21
+ ): React.ReactElement | null {
22
+ if (!open || !anchorEl) {
23
+ return null
24
+ }
25
+
26
+ return (
27
+ <Popper
28
+ open
29
+ role='presentation'
30
+ anchorEl={anchorEl}
31
+ placement='bottom-start'
32
+ >
33
+ <ClickAwayListener onClickAway={onClickAway}>
34
+ <div ref={ref}>
35
+ <TimePickerList {...rest} />
36
+ </div>
37
+ </ClickAwayListener>
38
+ </Popper>
39
+ )
40
+ }
41
+ )
42
+
43
+ TimePickerDropdown.displayName = 'TimePickerDropdown'
44
+
45
+ export default TimePickerDropdown
@@ -0,0 +1 @@
1
+ export { default as TimePickerDropdown } from './TimePickerDropdown'