@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,97 @@
1
+ import React, { useMemo, useRef, useState } from 'react';
2
+ import { Menu, MenuItem } from '@toptal/picasso-menu';
3
+ import { useIsomorphicLayoutEffect } from '@toptal/picasso-utils';
4
+ import { twMerge } from '@toptal/picasso-tailwind-merge';
5
+ import { getCurrentTime, getNearestOptionIndex, getTimeOptions, parseTime, } from '../TimePicker/utils';
6
+ const OPTION = '[role="option"]';
7
+ const preventFocusLoss = (event) => {
8
+ // Clicking a time must keep the caret in the field, like the browser picker does
9
+ event.preventDefault();
10
+ };
11
+ const getStartingIndex = (options, minuteStep, value) => {
12
+ var _a;
13
+ const selectedIndex = options.findIndex(option => option.value === value);
14
+ return selectedIndex >= 0
15
+ ? selectedIndex
16
+ : getNearestOptionIndex((_a = parseTime(value)) !== null && _a !== void 0 ? _a : getCurrentTime(), minuteStep);
17
+ };
18
+ const scrollToOption = (list, option) => {
19
+ const listRect = list.getBoundingClientRect();
20
+ const optionRect = option.getBoundingClientRect();
21
+ list.scrollTop +=
22
+ optionRect.top - listRect.top - (listRect.height - optionRect.height) / 2;
23
+ };
24
+ const getNextIndex = (key, index, count) => {
25
+ switch (key) {
26
+ case 'ArrowDown':
27
+ return Math.min(index + 1, count - 1);
28
+ case 'ArrowUp':
29
+ return Math.max(index - 1, 0);
30
+ case 'Home':
31
+ return 0;
32
+ case 'End':
33
+ return count - 1;
34
+ default:
35
+ return undefined;
36
+ }
37
+ };
38
+ export const TimePickerList = ({ value, minuteStep, hourCycle, autoFocus = false, onChange, onClose, className, style, 'data-testid': dataTestId, }) => {
39
+ const listRef = useRef(null);
40
+ const options = useMemo(() => getTimeOptions(minuteStep, hourCycle), [minuteStep, hourCycle]);
41
+ const [initialIndex] = useState(() => getStartingIndex(options, minuteStep, value));
42
+ const getOptionElements = () => { var _a, _b; return Array.from((_b = (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(OPTION)) !== null && _b !== void 0 ? _b : []); };
43
+ useIsomorphicLayoutEffect(() => {
44
+ const list = listRef.current;
45
+ const option = getOptionElements()[initialIndex];
46
+ if (list && option) {
47
+ scrollToOption(list, option);
48
+ }
49
+ }, [initialIndex]);
50
+ useIsomorphicLayoutEffect(() => {
51
+ if (!autoFocus) {
52
+ return;
53
+ }
54
+ // The value may have been typed since the list opened, so the time to
55
+ // focus is looked up at the moment focus is requested
56
+ const list = listRef.current;
57
+ const option = getOptionElements()[getStartingIndex(options, minuteStep, value)];
58
+ if (list && option) {
59
+ scrollToOption(list, option);
60
+ option.focus({ preventScroll: true });
61
+ }
62
+ }, [autoFocus]);
63
+ const handleKeyDown = (event) => {
64
+ var _a;
65
+ const optionElements = getOptionElements();
66
+ const index = optionElements.indexOf(event.target.closest(OPTION));
67
+ switch (event.key) {
68
+ case 'Enter':
69
+ case ' ':
70
+ event.preventDefault();
71
+ event.stopPropagation();
72
+ if (index >= 0) {
73
+ onChange(options[index].value);
74
+ }
75
+ break;
76
+ case 'Escape':
77
+ // Keeps a surrounding Modal or Drawer open while the list closes
78
+ event.stopPropagation();
79
+ onClose();
80
+ break;
81
+ case 'Tab':
82
+ onClose();
83
+ break;
84
+ default: {
85
+ const nextIndex = getNextIndex(event.key, index, options.length);
86
+ if (nextIndex !== undefined) {
87
+ event.preventDefault();
88
+ (_a = optionElements[nextIndex]) === null || _a === void 0 ? void 0 : _a.focus();
89
+ }
90
+ }
91
+ }
92
+ };
93
+ return (React.createElement(Menu, { ref: listRef, role: 'listbox', "aria-label": 'Choose time', allowNestedNavigation: false, onKeyDown: handleKeyDown, onMouseDown: preventFocusLoss, className: twMerge('bg-white shadow-5 max-h-[15rem] overflow-y-auto', className), style: style, "data-testid": dataTestId }, options.map(option => (React.createElement(MenuItem, { key: option.value, role: 'option', "aria-selected": option.value === value, checkmarked: option.value === value, titleCase: false, className: 'tabular-nums', onClick: () => onChange(option.value) }, option.label)))));
94
+ };
95
+ TimePickerList.displayName = 'TimePickerList';
96
+ export default TimePickerList;
97
+ //# sourceMappingURL=TimePickerList.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimePickerList.js","sourceRoot":"","sources":["../../../src/TimePickerList/TimePickerList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAExD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAA;AAExD,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,SAAS,GACV,MAAM,qBAAqB,CAAA;AAkB5B,MAAM,MAAM,GAAG,iBAAiB,CAAA;AAEhC,MAAM,gBAAgB,GAAG,CAAC,KAAuB,EAAE,EAAE;IACnD,iFAAiF;IACjF,KAAK,CAAC,cAAc,EAAE,CAAA;AACxB,CAAC,CAAA;AAED,MAAM,gBAAgB,GAAG,CACvB,OAAqB,EACrB,UAAkB,EAClB,KAAc,EACd,EAAE;;IACF,MAAM,aAAa,GAAG,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK,CAAC,CAAA;IAEzE,OAAO,aAAa,IAAI,CAAC;QACvB,CAAC,CAAC,aAAa;QACf,CAAC,CAAC,qBAAqB,CAAC,MAAA,SAAS,CAAC,KAAK,CAAC,mCAAI,cAAc,EAAE,EAAE,UAAU,CAAC,CAAA;AAC7E,CAAC,CAAA;AAED,MAAM,cAAc,GAAG,CAAC,IAAiB,EAAE,MAAmB,EAAE,EAAE;IAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAA;IAC7C,MAAM,UAAU,GAAG,MAAM,CAAC,qBAAqB,EAAE,CAAA;IAEjD,IAAI,CAAC,SAAS;QACZ,UAAU,CAAC,GAAG,GAAG,QAAQ,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;AAC7E,CAAC,CAAA;AAED,MAAM,YAAY,GAAG,CAAC,GAAW,EAAE,KAAa,EAAE,KAAa,EAAE,EAAE;IACjE,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,WAAW;YACd,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAA;QACvC,KAAK,SAAS;YACZ,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;QAC/B,KAAK,MAAM;YACT,OAAO,CAAC,CAAA;QACV,KAAK,KAAK;YACR,OAAO,KAAK,GAAG,CAAC,CAAA;QAClB;YACE,OAAO,SAAS,CAAA;IACpB,CAAC;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,EAC7B,KAAK,EACL,UAAU,EACV,SAAS,EACT,SAAS,GAAG,KAAK,EACjB,QAAQ,EACR,OAAO,EACP,SAAS,EACT,KAAK,EACL,aAAa,EAAE,UAAU,GACnB,EAAsB,EAAE;IAC9B,MAAM,OAAO,GAAG,MAAM,CAAmB,IAAI,CAAC,CAAA;IAC9C,MAAM,OAAO,GAAG,OAAO,CACrB,GAAG,EAAE,CAAC,cAAc,CAAC,UAAU,EAAE,SAAS,CAAC,EAC3C,CAAC,UAAU,EAAE,SAAS,CAAC,CACxB,CAAA;IACD,MAAM,CAAC,YAAY,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CACnC,gBAAgB,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,CAC7C,CAAA;IAED,MAAM,iBAAiB,GAAG,GAAG,EAAE,eAC7B,OAAA,KAAK,CAAC,IAAI,CAAC,MAAA,MAAA,OAAO,CAAC,OAAO,0CAAE,gBAAgB,CAAc,MAAM,CAAC,mCAAI,EAAE,CAAC,CAAA,EAAA,CAAA;IAE1E,yBAAyB,CAAC,GAAG,EAAE;QAC7B,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAA;QAC5B,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAC,YAAY,CAAC,CAAA;QAEhD,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;YACnB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAC9B,CAAC;IACH,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAA;IAElB,yBAAyB,CAAC,GAAG,EAAE;QAC7B,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,sEAAsE;QACtE,sDAAsD;QACtD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAA;QAC5B,MAAM,MAAM,GACV,iBAAiB,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC,CAAA;QAEnE,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;YACnB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;YAC5B,MAAM,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAA;QACvC,CAAC;IACH,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAA;IAEf,MAAM,aAAa,GAAG,CAAC,KAA4C,EAAE,EAAE;;QACrE,MAAM,cAAc,GAAG,iBAAiB,EAAE,CAAA;QAC1C,MAAM,KAAK,GAAG,cAAc,CAAC,OAAO,CACjC,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAc,MAAM,CAAgB,CAC1E,CAAA;QAED,QAAQ,KAAK,CAAC,GAAG,EAAE,CAAC;YAClB,KAAK,OAAO,CAAC;YACb,KAAK,GAAG;gBACN,KAAK,CAAC,cAAc,EAAE,CAAA;gBACtB,KAAK,CAAC,eAAe,EAAE,CAAA;gBAEvB,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;oBACf,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAA;gBAChC,CAAC;gBACD,MAAK;YACP,KAAK,QAAQ;gBACX,iEAAiE;gBACjE,KAAK,CAAC,eAAe,EAAE,CAAA;gBACvB,OAAO,EAAE,CAAA;gBACT,MAAK;YACP,KAAK,KAAK;gBACR,OAAO,EAAE,CAAA;gBACT,MAAK;YACP,OAAO,CAAC,CAAC,CAAC;gBACR,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;gBAEhE,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;oBAC5B,KAAK,CAAC,cAAc,EAAE,CAAA;oBACtB,MAAA,cAAc,CAAC,SAAS,CAAC,0CAAE,KAAK,EAAE,CAAA;gBACpC,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC,CAAA;IAED,OAAO,CACL,oBAAC,IAAI,IACH,GAAG,EAAE,OAAO,EACZ,IAAI,EAAC,SAAS,gBACH,aAAa,EACxB,qBAAqB,EAAE,KAAK,EAC5B,SAAS,EAAE,aAAa,EACxB,WAAW,EAAE,gBAAgB,EAC7B,SAAS,EAAE,OAAO,CAChB,iDAAiD,EACjD,SAAS,CACV,EACD,KAAK,EAAE,KAAK,iBACC,UAAU,IAEtB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CACrB,oBAAC,QAAQ,IACP,GAAG,EAAE,MAAM,CAAC,KAAK,EACjB,IAAI,EAAC,QAAQ,mBACE,MAAM,CAAC,KAAK,KAAK,KAAK,EACrC,WAAW,EAAE,MAAM,CAAC,KAAK,KAAK,KAAK,EACnC,SAAS,EAAE,KAAK,EAChB,SAAS,EAAC,cAAc,EACxB,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,IAEpC,MAAM,CAAC,KAAK,CACJ,CACZ,CAAC,CACG,CACR,CAAA;AACH,CAAC,CAAA;AAED,cAAc,CAAC,WAAW,GAAG,gBAAgB,CAAA;AAE7C,eAAe,cAAc,CAAA"}
@@ -0,0 +1,4 @@
1
+ import type { Props } from './TimePickerList';
2
+ export { default as TimePickerList } from './TimePickerList';
3
+ export type TimePickerListProps = Props;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/TimePickerList/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAE7C,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAC5D,MAAM,MAAM,mBAAmB,GAAG,KAAK,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { default as TimePickerList } from './TimePickerList';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/TimePickerList/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import type { BaseProps } from '@toptal/picasso-shared';
3
+ export interface Props extends BaseProps {
4
+ /** Whether the time list is shown */
5
+ open: boolean;
6
+ /** Whether the trigger ignores clicks */
7
+ disabled?: boolean;
8
+ /** Called when the trigger is clicked */
9
+ onClick: () => void;
10
+ }
11
+ export declare const TimePickerTrigger: {
12
+ ({ open, disabled, onClick, className, style, "data-testid": dataTestId, }: Props): React.ReactElement;
13
+ displayName: string;
14
+ };
15
+ export default TimePickerTrigger;
16
+ //# sourceMappingURL=TimePickerTrigger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimePickerTrigger.d.ts","sourceRoot":"","sources":["../../../src/TimePickerTrigger/TimePickerTrigger.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAKvD,MAAM,WAAW,KAAM,SAAQ,SAAS;IACtC,qCAAqC;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,yCAAyC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,yCAAyC;IACzC,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAID,eAAO,MAAM,iBAAiB;gFAO3B,KAAK,GAAG,KAAK,CAAC,YAAY;;CAwB5B,CAAA;AAID,eAAe,iBAAiB,CAAA"}
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { InputAdornment } from '@toptal/picasso-input-adornment';
3
+ import { Time16 } from '@toptal/picasso-icons';
4
+ import { twMerge } from '@toptal/picasso-tailwind-merge';
5
+ const preventFocusLoss = (event) => event.preventDefault();
6
+ export const TimePickerTrigger = ({ open, disabled, onClick, className, style, 'data-testid': dataTestId, }) => (React.createElement(InputAdornment, { position: 'end' },
7
+ React.createElement("button", { type: 'button', tabIndex: -1, "aria-label": 'Choose time', "aria-haspopup": 'listbox', "aria-expanded": open, disabled: disabled, onMouseDown: preventFocusLoss, onClick: onClick, className: twMerge('absolute right-[0.625rem] flex m-0 p-0 border-none bg-white text-inherit select-none', 'cursor-pointer disabled:cursor-default', 'pointer-events-none pointer-fine:pointer-events-auto', className), style: style, "data-testid": dataTestId },
8
+ React.createElement(Time16, null))));
9
+ TimePickerTrigger.displayName = 'TimePickerTrigger';
10
+ export default TimePickerTrigger;
11
+ //# sourceMappingURL=TimePickerTrigger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimePickerTrigger.js","sourceRoot":"","sources":["../../../src/TimePickerTrigger/TimePickerTrigger.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAA;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAA;AAWxD,MAAM,gBAAgB,GAAG,CAAC,KAAuB,EAAE,EAAE,CAAC,KAAK,CAAC,cAAc,EAAE,CAAA;AAE5E,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,EAChC,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,SAAS,EACT,KAAK,EACL,aAAa,EAAE,UAAU,GACnB,EAAsB,EAAE,CAAC,CAC/B,oBAAC,cAAc,IAAC,QAAQ,EAAC,KAAK;IAE5B,gCACE,IAAI,EAAC,QAAQ,EACb,QAAQ,EAAE,CAAC,CAAC,gBACD,aAAa,mBACV,SAAS,mBACR,IAAI,EACnB,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,gBAAgB,EAC7B,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,OAAO,CAChB,sFAAsF,EACtF,wCAAwC,EACxC,sDAAsD,EACtD,SAAS,CACV,EACD,KAAK,EAAE,KAAK,iBACC,UAAU;QAEvB,oBAAC,MAAM,OAAG,CACH,CACM,CAClB,CAAA;AAED,iBAAiB,CAAC,WAAW,GAAG,mBAAmB,CAAA;AAEnD,eAAe,iBAAiB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { default as TimePickerTrigger } from './TimePickerTrigger';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/TimePickerTrigger/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { default as TimePickerTrigger } from './TimePickerTrigger';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/TimePickerTrigger/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toptal/picasso-timepicker",
3
- "version": "100.0.3",
3
+ "version": "100.1.0",
4
4
  "description": "Toptal UI components library - Timepicker",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,7 +22,10 @@
22
22
  "react-input-mask": "^3.0.0-alpha.2",
23
23
  "@toptal/picasso-icons": "100.0.1",
24
24
  "@toptal/picasso-input": "100.0.3",
25
+ "@toptal/picasso-input-adornment": "100.0.1",
26
+ "@toptal/picasso-menu": "100.0.3",
25
27
  "@toptal/picasso-outlined-input": "100.1.0",
28
+ "@toptal/picasso-popper": "100.0.2",
26
29
  "@toptal/picasso-utils": "100.0.1"
27
30
  },
28
31
  "sideEffects": [
@@ -8,6 +8,11 @@ import type { InputProps } from '@toptal/picasso-input'
8
8
  import type { Status } from '@toptal/picasso-outlined-input'
9
9
  import { twMerge } from '@toptal/picasso-tailwind-merge'
10
10
 
11
+ import { TimePickerDropdown } from '../TimePickerDropdown'
12
+ import { TimePickerTrigger } from '../TimePickerTrigger'
13
+ import { useTimePickerPopover } from './use-time-picker-popover'
14
+ import { VALID_TIME_REGEX, getHourCycle } from './utils'
15
+
11
16
  export interface Props
12
17
  extends BaseProps,
13
18
  Omit<
@@ -38,22 +43,50 @@ export interface Props
38
43
  status?: Extract<Status, 'error' | 'warning' | 'default'>
39
44
  /** Called on input change */
40
45
  onChange?: (value: string) => void
46
+ /** Interval in minutes between the times offered in a dropdown list. When omitted, the browser's own time picker is used */
47
+ minuteStep?: 5 | 10 | 15 | 20 | 30 | 60
41
48
  }
42
49
 
43
- const VALID_TIME_REGEX = new RegExp(/^([0-1][0-9]|2[0-3]):[0-5][0-9]$/)
50
+ const NATIVE_PICKER_CLASS_NAME = `-mr-2 [&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:right-2
51
+ [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:bg-none`
52
+
53
+ const TIME_LIST_CLASS_NAME = `${NATIVE_PICKER_CLASS_NAME} pointer-fine:[&::-webkit-calendar-picker-indicator]:hidden`
54
+
55
+ const nativePickerIcon = (
56
+ <Time16
57
+ classes={{
58
+ root: 'bg-white absolute right-[0.625rem] pointer-events-none m-0 select-none',
59
+ }}
60
+ />
61
+ )
44
62
 
45
- export const TimePicker = ({ status = 'default', ...props }: Props) => {
63
+ const getInputMask = (value?: string) => {
64
+ const startsWithTwo = value && value[0] === '2'
65
+
66
+ return [/[0-2]/, startsWithTwo ? /[0-3]/ : /[0-9]/, ':', /[0-5]/, /[0-9]/]
67
+ }
68
+
69
+ export const TimePicker = ({
70
+ status = 'default',
71
+ ...props
72
+ }: Props): React.ReactElement => {
46
73
  const {
47
74
  onChange: externalOnChange,
75
+ onKeyDown: externalOnKeyDown,
76
+ onBlur: externalOnBlur,
77
+ onFocus: externalOnFocus,
48
78
  value: externalValue,
49
79
  width,
50
80
  className,
51
81
  highlight,
52
82
  size,
83
+ minuteStep,
53
84
  ...rest
54
85
  } = props
55
86
 
56
87
  const [value, setValue] = useState(externalValue)
88
+ const hasTimeList = minuteStep !== undefined
89
+ const interactive = hasTimeList && !rest.disabled && !rest.readOnly
57
90
 
58
91
  useEffect(() => {
59
92
  // Set internal value based on the provided one if the later is correct
@@ -62,13 +95,7 @@ export const TimePicker = ({ status = 'default', ...props }: Props) => {
62
95
  }
63
96
  }, [externalValue])
64
97
 
65
- const onChange = (
66
- event: React.ChangeEvent<
67
- HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
68
- >
69
- ) => {
70
- const newValue = event.target.value
71
-
98
+ const changeValue = (newValue: string) => {
72
99
  setValue(newValue)
73
100
 
74
101
  if (newValue && VALID_TIME_REGEX.test(newValue)) {
@@ -78,79 +105,148 @@ export const TimePicker = ({ status = 'default', ...props }: Props) => {
78
105
  }
79
106
  }
80
107
 
81
- const browser = detect()
82
- const isSafari = browser?.name === 'safari'
83
- const startsWithTwo = value && value[0] === '2'
108
+ const onChange = (
109
+ event: React.ChangeEvent<
110
+ HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
111
+ >
112
+ ) => changeValue(event.target.value)
113
+
114
+ const isSafari = detect()?.name === 'safari'
115
+
116
+ const {
117
+ isOpen,
118
+ hourCycle,
119
+ anchorRef,
120
+ listRef,
121
+ focusList,
122
+ isFocusChangeWithinList,
123
+ handleTriggerClick,
124
+ handleClickAway,
125
+ closeAndFocusField,
126
+ handleFieldFocus,
127
+ handleFieldKeyDown,
128
+ } = useTimePickerPopover({
129
+ enabled: interactive,
130
+ // Safari renders its own 24-hour masked field, so the list has to match it
131
+ getHourCycle: () => (isSafari ? 24 : getHourCycle()),
132
+ })
133
+
134
+ const handleTimePick = (time: string) => {
135
+ changeValue(time)
136
+ closeAndFocusField()
137
+ }
138
+
139
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
140
+ externalOnKeyDown?.(event)
141
+ handleFieldKeyDown(event)
142
+ }
143
+
144
+ const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
145
+ if (!isFocusChangeWithinList(event)) {
146
+ externalOnBlur?.(event)
147
+ }
148
+ }
84
149
 
85
- const inputMask = [
86
- /[0-2]/,
87
- startsWithTwo ? /[0-3]/ : /[0-9]/,
88
- ':',
89
- /[0-5]/,
90
- /[0-9]/,
91
- ]
92
-
93
- const icon = (
94
- <Time16
95
- classes={{
96
- root: 'bg-white absolute right-[0.625rem] pointer-events-none m-0 select-none',
97
- }}
150
+ const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
151
+ handleFieldFocus()
152
+
153
+ if (!isFocusChangeWithinList(event)) {
154
+ externalOnFocus?.(event)
155
+ }
156
+ }
157
+
158
+ const adornmentProps = hasTimeList
159
+ ? {
160
+ endAdornment: (
161
+ <TimePickerTrigger
162
+ open={isOpen}
163
+ disabled={!interactive}
164
+ onClick={handleTriggerClick}
165
+ />
166
+ ),
167
+ }
168
+ : { iconPosition: 'end' as const, icon: nativePickerIcon }
169
+
170
+ const timeList = hasTimeList && (
171
+ <TimePickerDropdown
172
+ ref={listRef}
173
+ open={isOpen}
174
+ anchorEl={anchorRef.current}
175
+ onClickAway={handleClickAway}
176
+ value={value}
177
+ minuteStep={minuteStep}
178
+ hourCycle={hourCycle}
179
+ autoFocus={focusList}
180
+ onChange={handleTimePick}
181
+ onClose={closeAndFocusField}
98
182
  />
99
183
  )
100
184
 
101
185
  const inputClassName = twMerge('cursor-default', className)
102
-
103
- const inputPropClassName = `-mr-[8px] [&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:right-2
104
- [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:bg-none`
186
+ const inputPropClassName = hasTimeList
187
+ ? TIME_LIST_CLASS_NAME
188
+ : NATIVE_PICKER_CLASS_NAME
105
189
 
106
190
  if (isSafari) {
107
191
  return (
192
+ <>
193
+ <Input
194
+ type='text'
195
+ readOnly
196
+ {...adornmentProps}
197
+ width={width}
198
+ status={status}
199
+ className={inputClassName}
200
+ highlight={highlight}
201
+ size={size}
202
+ outlineRef={anchorRef}
203
+ inputProps={{
204
+ className: inputPropClassName,
205
+ ...rest,
206
+ }}
207
+ startAdornment={
208
+ <InputMask
209
+ mask={getInputMask(value)}
210
+ alwaysShowMask
211
+ maskPlaceholder='-'
212
+ value={value}
213
+ onChange={onChange}
214
+ onKeyDown={handleKeyDown}
215
+ onBlur={handleBlur}
216
+ onFocus={handleFocus}
217
+ className={'text-sm border-none p-0 m-0 outline-hidden'}
218
+ />
219
+ }
220
+ />
221
+ {timeList}
222
+ </>
223
+ )
224
+ }
225
+
226
+ return (
227
+ <>
108
228
  <Input
109
- type='text'
110
- readOnly
111
- iconPosition='end'
112
- icon={icon}
113
- width={width}
114
- status={status}
229
+ type='time'
230
+ value={value}
115
231
  className={inputClassName}
232
+ onChange={onChange}
233
+ onKeyDown={handleKeyDown}
234
+ onBlur={handleBlur}
235
+ onFocus={handleFocus}
236
+ {...adornmentProps}
116
237
  highlight={highlight}
238
+ width={width}
117
239
  size={size}
240
+ status={status}
241
+ outlineRef={anchorRef}
118
242
  inputProps={{
119
243
  className: inputPropClassName,
244
+ step: 60, // 1 min
120
245
  ...rest,
121
246
  }}
122
- startAdornment={
123
- <InputMask
124
- mask={inputMask}
125
- alwaysShowMask
126
- maskPlaceholder='-'
127
- value={value}
128
- onChange={onChange}
129
- className={'text-sm border-none p-0 m-0 outline-hidden'}
130
- />
131
- }
132
247
  />
133
- )
134
- }
135
-
136
- return (
137
- <Input
138
- type='time'
139
- value={value}
140
- className={inputClassName}
141
- onChange={onChange}
142
- iconPosition='end'
143
- highlight={highlight}
144
- icon={icon}
145
- width={width}
146
- size={size}
147
- status={status}
148
- inputProps={{
149
- className: inputPropClassName,
150
- step: 60, // 1 min
151
- ...rest,
152
- }}
153
- />
248
+ {timeList}
249
+ </>
154
250
  )
155
251
  }
156
252
 
@@ -0,0 +1,16 @@
1
+ import React, { useState } from 'react'
2
+ import { TimePicker } from '@toptal/picasso'
3
+
4
+ const MinuteStepExample = () => {
5
+ const [timepickerValue, setTimepickerValue] = useState('09:30')
6
+
7
+ return (
8
+ <TimePicker
9
+ onChange={setTimepickerValue}
10
+ value={timepickerValue}
11
+ minuteStep={15}
12
+ />
13
+ )
14
+ }
15
+
16
+ export default MinuteStepExample
@@ -30,3 +30,13 @@ page
30
30
  },
31
31
  'base/Timepicker'
32
32
  )
33
+ .addExample(
34
+ 'TimePicker/story/MinuteStep.example.tsx',
35
+ {
36
+ title: 'Minute step',
37
+ description:
38
+ 'Pass `minuteStep` to replace the browser time picker with a list of times at that interval. Any other time can still be typed',
39
+ takeScreenshot: false,
40
+ },
41
+ 'base/Timepicker'
42
+ )