react-miui 0.34.0 → 0.35.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 (284) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/CHANGELOG.md +13 -0
  3. package/dist/components/form/index.d.ts +1 -0
  4. package/dist/components/form/index.d.ts.map +1 -1
  5. package/dist/components/form/index.js +1 -0
  6. package/dist/components/form/index.js.map +1 -1
  7. package/dist/components/form/input/Input.d.ts.map +1 -1
  8. package/dist/components/form/input/Input.js +9 -5
  9. package/dist/components/form/input/Input.js.map +1 -1
  10. package/dist/components/form/timepicker/TimePicker.css.d.ts +99 -0
  11. package/dist/components/form/timepicker/TimePicker.css.d.ts.map +1 -0
  12. package/dist/components/form/timepicker/TimePicker.css.js +116 -0
  13. package/dist/components/form/timepicker/TimePicker.css.js.map +1 -0
  14. package/dist/components/form/timepicker/TimePicker.d.ts +22 -0
  15. package/dist/components/form/timepicker/TimePicker.d.ts.map +1 -0
  16. package/dist/components/form/timepicker/TimePicker.js +141 -0
  17. package/dist/components/form/timepicker/TimePicker.js.map +1 -0
  18. package/dist/components/form/timepicker/TimePicker.styled.d.ts +936 -0
  19. package/dist/components/form/timepicker/TimePicker.styled.d.ts.map +1 -0
  20. package/dist/components/form/timepicker/TimePicker.styled.js +29 -0
  21. package/dist/components/form/timepicker/TimePicker.styled.js.map +1 -0
  22. package/dist/components/form/timepicker/TimePickerModal.d.ts +17 -0
  23. package/dist/components/form/timepicker/TimePickerModal.d.ts.map +1 -0
  24. package/dist/components/form/timepicker/TimePickerModal.js +92 -0
  25. package/dist/components/form/timepicker/TimePickerModal.js.map +1 -0
  26. package/dist/components/form/timepicker/Wheel.d.ts +12 -0
  27. package/dist/components/form/timepicker/Wheel.d.ts.map +1 -0
  28. package/dist/components/form/timepicker/Wheel.js +187 -0
  29. package/dist/components/form/timepicker/Wheel.js.map +1 -0
  30. package/dist/components/form/timepicker/utils.d.ts +4 -0
  31. package/dist/components/form/timepicker/utils.d.ts.map +1 -0
  32. package/dist/components/form/timepicker/utils.js +62 -0
  33. package/dist/components/form/timepicker/utils.js.map +1 -0
  34. package/dist/components/icons/Clock.d.ts +7 -0
  35. package/dist/components/icons/Clock.d.ts.map +1 -0
  36. package/dist/components/icons/Clock.js +45 -0
  37. package/dist/components/icons/Clock.js.map +1 -0
  38. package/dist/components/icons/Icon.d.ts +2 -1
  39. package/dist/components/icons/Icon.d.ts.map +1 -1
  40. package/dist/components/icons/Icon.js +3 -0
  41. package/dist/components/icons/Icon.js.map +1 -1
  42. package/dist/components/ui/modal/Modal.d.ts +1 -2
  43. package/dist/components/ui/modal/Modal.d.ts.map +1 -1
  44. package/dist/components/ui/modal/Modal.js +114 -42
  45. package/dist/components/ui/modal/Modal.js.map +1 -1
  46. package/dist/components/ui/modal/Modal.styled.d.ts +1 -1
  47. package/dist/components/ui/modal/Modal.styled.d.ts.map +1 -1
  48. package/dist/components/ui/modal/Modal.styled.js +40 -25
  49. package/dist/components/ui/modal/Modal.styled.js.map +1 -1
  50. package/dist/theme.css-global.d.ts.map +1 -1
  51. package/dist/theme.css-global.js +0 -1
  52. package/dist/theme.css-global.js.map +1 -1
  53. package/dist/utils/index.d.ts +1 -0
  54. package/dist/utils/index.d.ts.map +1 -1
  55. package/dist/utils/index.js +1 -0
  56. package/dist/utils/index.js.map +1 -1
  57. package/dist/utils/useNativeValidity.d.ts +11 -0
  58. package/dist/utils/useNativeValidity.d.ts.map +1 -0
  59. package/dist/utils/useNativeValidity.js +32 -0
  60. package/dist/utils/useNativeValidity.js.map +1 -0
  61. package/docs/assets/navigation.js +1 -1
  62. package/docs/assets/search.js +1 -1
  63. package/docs/classes/index.Pop.html +7 -7
  64. package/docs/documents/Test.html +2 -2
  65. package/docs/enums/index.ICON.html +3 -2
  66. package/docs/functions/index.Action.html +3 -3
  67. package/docs/functions/index.Button.html +3 -3
  68. package/docs/functions/index.Card.html +2 -2
  69. package/docs/functions/index.Checkbox.html +3 -3
  70. package/docs/functions/index.Choice.html +2 -2
  71. package/docs/functions/index.ColorPicker.html +3 -3
  72. package/docs/functions/index.CoveringLoader.html +3 -3
  73. package/docs/functions/index.DirectionPad.html +2 -2
  74. package/docs/functions/index.Drawer.html +2 -2
  75. package/docs/functions/index.EqualActions.html +2 -2
  76. package/docs/functions/index.FullLoader.html +3 -3
  77. package/docs/functions/index.Gap.html +2 -2
  78. package/docs/functions/index.HandleEsc.html +3 -3
  79. package/docs/functions/index.Header.html +3 -3
  80. package/docs/functions/index.HeaderIconAction.html +3 -3
  81. package/docs/functions/index.Icon-1.html +2 -2
  82. package/docs/functions/index.If.html +3 -3
  83. package/docs/functions/index.Input.html +1 -1
  84. package/docs/functions/index.KeyValue.html +2 -2
  85. package/docs/functions/index.Label.html +2 -2
  86. package/docs/functions/index.Line.html +3 -3
  87. package/docs/functions/index.List.html +2 -2
  88. package/docs/functions/index.Loader.html +3 -3
  89. package/docs/functions/index.Loading.html +3 -3
  90. package/docs/functions/index.Message.html +3 -3
  91. package/docs/functions/index.Modal.html +2 -2
  92. package/docs/functions/index.ModalButtons.html +3 -3
  93. package/docs/functions/index.PopLoader.html +3 -3
  94. package/docs/functions/index.PopOption.html +2 -2
  95. package/docs/functions/index.Progress.html +2 -2
  96. package/docs/functions/index.SearchContainer.html +2 -2
  97. package/docs/functions/index.Section.html +4 -4
  98. package/docs/functions/index.Select.html +2 -2
  99. package/docs/functions/index.Selector.html +2 -2
  100. package/docs/functions/index.Spacer.html +2 -2
  101. package/docs/functions/index.Stats.html +2 -2
  102. package/docs/functions/index.StickyHeader.html +4 -4
  103. package/docs/functions/index.Table.html +2 -2
  104. package/docs/functions/index.TextArea.html +2 -2
  105. package/docs/functions/index.TimePicker.html +10 -0
  106. package/docs/functions/index.ToasterProvider.html +3 -3
  107. package/docs/functions/index.Toggle.html +3 -3
  108. package/docs/functions/index.ToolButton.html +3 -3
  109. package/docs/functions/index.Tooltip.html +3 -3
  110. package/docs/functions/index.TooltipProvider.html +2 -2
  111. package/docs/functions/index.borderPxToRem.html +1 -1
  112. package/docs/functions/index.createTheme.html +1 -1
  113. package/docs/functions/index.css.html +1 -1
  114. package/docs/functions/index.dimensionsPxToRem.html +1 -1
  115. package/docs/functions/index.fontPxToRem.html +1 -1
  116. package/docs/functions/index.getCssText.html +1 -1
  117. package/docs/functions/index.globalCss.html +2 -2
  118. package/docs/functions/index.injectGlobalStyles.html +1 -1
  119. package/docs/functions/index.keyframes.html +1 -1
  120. package/docs/functions/index.pxToRem.html +1 -1
  121. package/docs/functions/index.styled.html +1 -1
  122. package/docs/functions/index.toast.html +2 -2
  123. package/docs/functions/index.useToaster.html +1 -1
  124. package/docs/index.html +2 -2
  125. package/docs/interfaces/index.IconProps.html +2 -2
  126. package/docs/interfaces/index.InputCustomProps.html +3 -3
  127. package/docs/interfaces/index.LoaderProps.html +6 -6
  128. package/docs/interfaces/index.StickyHeaderProps.html +4 -4
  129. package/docs/interfaces/index.ToasterProviderProps.html +3 -3
  130. package/docs/interfaces/index.TooltipProps.html +14 -14
  131. package/docs/interfaces/index.TooltipProviderProps.html +5 -5
  132. package/docs/modules/index.html +1 -1
  133. package/docs/modules.html +1 -1
  134. package/docs/types/index.ActionProps.html +1 -1
  135. package/docs/types/index.CardProps.html +1 -1
  136. package/docs/types/index.CheckboxProps.html +2 -2
  137. package/docs/types/index.ChoiceProps.html +1 -1
  138. package/docs/types/index.ColorPickerProps.html +1 -1
  139. package/docs/types/index.DirectionPadProps.html +1 -1
  140. package/docs/types/index.DrawerFrom.html +1 -1
  141. package/docs/types/index.DrawerProps.html +2 -2
  142. package/docs/types/index.EqualActionsProps.html +1 -1
  143. package/docs/types/index.HeaderProps.html +1 -1
  144. package/docs/types/index.InputProps.html +1 -1
  145. package/docs/types/index.KeyValueProps.html +1 -1
  146. package/docs/types/index.LabelProps.html +1 -1
  147. package/docs/types/index.OverwriteProps.html +1 -1
  148. package/docs/types/index.ProgressProps.html +2 -2
  149. package/docs/types/index.SelectProps.html +1 -1
  150. package/docs/types/index.SelectorProps.html +1 -1
  151. package/docs/types/index.Stat.html +1 -1
  152. package/docs/types/index.StatsProps.html +1 -1
  153. package/docs/types/index.TextAreaProps.html +1 -1
  154. package/docs/types/index.ThemeCSS.html +1 -1
  155. package/docs/types/index.TimePickerProps.html +1 -0
  156. package/docs/types/index.ToggleProps.html +2 -2
  157. package/docs/variables/index.ActionBadgeSelector.html +1 -1
  158. package/docs/variables/index.ActionCircleSelector.html +1 -1
  159. package/docs/variables/index.CheckboxCheckmarkWrapperSelector.html +1 -1
  160. package/docs/variables/index.CheckboxTextLabelSelector.html +1 -1
  161. package/docs/variables/index.ChoiceItemSelector.html +1 -1
  162. package/docs/variables/index.ColorPickerColorDisplaySelector.html +1 -1
  163. package/docs/variables/index.DirectionPadButtonDotSelector.html +1 -1
  164. package/docs/variables/index.DirectionPadButtonSelector.html +1 -1
  165. package/docs/variables/index.DirectionPadLineSelector.html +1 -1
  166. package/docs/variables/index.DirectionPadMiddleSelector.html +1 -1
  167. package/docs/variables/index.DrawerContentSelector.html +1 -1
  168. package/docs/variables/index.HeaderAfterSelector.html +1 -1
  169. package/docs/variables/index.HeaderBeforeSelector.html +1 -1
  170. package/docs/variables/index.HeaderContentsSelector.html +1 -1
  171. package/docs/variables/index.HeaderIconActionIconSelector.html +1 -1
  172. package/docs/variables/index.InputContainerSelector.html +1 -1
  173. package/docs/variables/index.InputInputSelector.html +1 -1
  174. package/docs/variables/index.InputLabelSelector.html +1 -1
  175. package/docs/variables/index.InputPrefixSelector.html +1 -1
  176. package/docs/variables/index.InputSuffixSelector.html +1 -1
  177. package/docs/variables/index.KeyValueIconSelector.html +1 -1
  178. package/docs/variables/index.KeyValueItemSelector.html +1 -1
  179. package/docs/variables/index.KeyValueKeySelector.html +1 -1
  180. package/docs/variables/index.KeyValuePairSelector.html +1 -1
  181. package/docs/variables/index.KeyValueValueSelector.html +1 -1
  182. package/docs/variables/index.LabelTextSelector.html +1 -1
  183. package/docs/variables/index.ListItemInnerContainerClassNameSelector.html +1 -1
  184. package/docs/variables/index.ModalContainerSelector.html +1 -1
  185. package/docs/variables/index.ModalRemovePaddingSelector.html +1 -1
  186. package/docs/variables/index.ModalTitleSelector.html +1 -1
  187. package/docs/variables/index.PopListSelector.html +1 -1
  188. package/docs/variables/index.PopOptionButtonSelector.html +1 -1
  189. package/docs/variables/index.PopOptionIconSelector.html +1 -1
  190. package/docs/variables/index.PopOverlaySelector.html +1 -1
  191. package/docs/variables/index.ProgressBackgroundSelector.html +1 -1
  192. package/docs/variables/index.ProgressValueSelector.html +1 -1
  193. package/docs/variables/index.SelectorItemSelector.html +1 -1
  194. package/docs/variables/index.StatsItemSelector.html +1 -1
  195. package/docs/variables/index.StatsLabelSelector.html +1 -1
  196. package/docs/variables/index.StatsSeparatorSelector.html +1 -1
  197. package/docs/variables/index.StatsValueSelector.html +1 -1
  198. package/docs/variables/index.TextAreaLabelSelector.html +1 -1
  199. package/docs/variables/index.TextAreaTextAreaSelector.html +1 -1
  200. package/docs/variables/index.TextAreaWrapperSelector.html +1 -1
  201. package/docs/variables/index.ToggleStyledToggleSelector.html +1 -1
  202. package/docs/variables/index.TooltipContentSelector.html +1 -1
  203. package/docs/variables/index.config.html +1 -1
  204. package/docs/variables/index.cssReset.html +2 -2
  205. package/docs/variables/index.darkTheme.html +1 -1
  206. package/docs/variables/index.miuiScrollbars.html +1 -1
  207. package/docs/variables/index.theme.html +1 -1
  208. package/esm/components/form/index.d.ts +1 -0
  209. package/esm/components/form/index.d.ts.map +1 -1
  210. package/esm/components/form/index.js +1 -0
  211. package/esm/components/form/index.js.map +1 -1
  212. package/esm/components/form/input/Input.d.ts.map +1 -1
  213. package/esm/components/form/input/Input.js +9 -5
  214. package/esm/components/form/input/Input.js.map +1 -1
  215. package/esm/components/form/timepicker/TimePicker.css.d.ts +99 -0
  216. package/esm/components/form/timepicker/TimePicker.css.d.ts.map +1 -0
  217. package/esm/components/form/timepicker/TimePicker.css.js +102 -0
  218. package/esm/components/form/timepicker/TimePicker.css.js.map +1 -0
  219. package/esm/components/form/timepicker/TimePicker.d.ts +22 -0
  220. package/esm/components/form/timepicker/TimePicker.d.ts.map +1 -0
  221. package/esm/components/form/timepicker/TimePicker.js +93 -0
  222. package/esm/components/form/timepicker/TimePicker.js.map +1 -0
  223. package/esm/components/form/timepicker/TimePicker.styled.d.ts +936 -0
  224. package/esm/components/form/timepicker/TimePicker.styled.d.ts.map +1 -0
  225. package/esm/components/form/timepicker/TimePicker.styled.js +20 -0
  226. package/esm/components/form/timepicker/TimePicker.styled.js.map +1 -0
  227. package/esm/components/form/timepicker/TimePickerModal.d.ts +17 -0
  228. package/esm/components/form/timepicker/TimePickerModal.d.ts.map +1 -0
  229. package/esm/components/form/timepicker/TimePickerModal.js +56 -0
  230. package/esm/components/form/timepicker/TimePickerModal.js.map +1 -0
  231. package/esm/components/form/timepicker/Wheel.d.ts +12 -0
  232. package/esm/components/form/timepicker/Wheel.d.ts.map +1 -0
  233. package/esm/components/form/timepicker/Wheel.js +151 -0
  234. package/esm/components/form/timepicker/Wheel.js.map +1 -0
  235. package/esm/components/form/timepicker/utils.d.ts +4 -0
  236. package/esm/components/form/timepicker/utils.d.ts.map +1 -0
  237. package/esm/components/form/timepicker/utils.js +58 -0
  238. package/esm/components/form/timepicker/utils.js.map +1 -0
  239. package/esm/components/icons/Clock.d.ts +7 -0
  240. package/esm/components/icons/Clock.d.ts.map +1 -0
  241. package/esm/components/icons/Clock.js +9 -0
  242. package/esm/components/icons/Clock.js.map +1 -0
  243. package/esm/components/icons/Icon.d.ts +2 -1
  244. package/esm/components/icons/Icon.d.ts.map +1 -1
  245. package/esm/components/icons/Icon.js +3 -0
  246. package/esm/components/icons/Icon.js.map +1 -1
  247. package/esm/components/ui/modal/Modal.d.ts +1 -2
  248. package/esm/components/ui/modal/Modal.d.ts.map +1 -1
  249. package/esm/components/ui/modal/Modal.js +103 -43
  250. package/esm/components/ui/modal/Modal.js.map +1 -1
  251. package/esm/components/ui/modal/Modal.styled.d.ts +1 -1
  252. package/esm/components/ui/modal/Modal.styled.d.ts.map +1 -1
  253. package/esm/components/ui/modal/Modal.styled.js +40 -25
  254. package/esm/components/ui/modal/Modal.styled.js.map +1 -1
  255. package/esm/theme.css-global.d.ts.map +1 -1
  256. package/esm/theme.css-global.js +0 -1
  257. package/esm/theme.css-global.js.map +1 -1
  258. package/esm/utils/index.d.ts +1 -0
  259. package/esm/utils/index.d.ts.map +1 -1
  260. package/esm/utils/index.js +1 -0
  261. package/esm/utils/index.js.map +1 -1
  262. package/esm/utils/useNativeValidity.d.ts +11 -0
  263. package/esm/utils/useNativeValidity.d.ts.map +1 -0
  264. package/esm/utils/useNativeValidity.js +29 -0
  265. package/esm/utils/useNativeValidity.js.map +1 -0
  266. package/package.json +1 -1
  267. package/src/components/form/index.ts +1 -0
  268. package/src/components/form/input/Input.stories.tsx +47 -1
  269. package/src/components/form/input/Input.tsx +11 -5
  270. package/src/components/form/timepicker/TimePicker.css.ts +132 -0
  271. package/src/components/form/timepicker/TimePicker.stories.tsx +107 -0
  272. package/src/components/form/timepicker/TimePicker.styled.ts +52 -0
  273. package/src/components/form/timepicker/TimePicker.tsx +229 -0
  274. package/src/components/form/timepicker/TimePickerModal.tsx +131 -0
  275. package/src/components/form/timepicker/Wheel.tsx +201 -0
  276. package/src/components/form/timepicker/utils.ts +66 -0
  277. package/src/components/icons/Clock.tsx +38 -0
  278. package/src/components/icons/Icon.tsx +3 -0
  279. package/src/components/ui/modal/Modal.stories.tsx +43 -7
  280. package/src/components/ui/modal/Modal.styled.ts +46 -25
  281. package/src/components/ui/modal/Modal.tsx +135 -52
  282. package/src/theme.css-global.ts +0 -1
  283. package/src/utils/index.ts +1 -0
  284. package/src/utils/useNativeValidity.ts +57 -0
@@ -0,0 +1,201 @@
1
+ import React, { useCallback, useEffect, useLayoutEffect, useRef } from "react";
2
+
3
+ import {
4
+ StyledWheelFocusRing,
5
+ StyledWheelItem,
6
+ StyledWheelPadder,
7
+ StyledWheelViewport,
8
+ StyledWheelWrapper,
9
+ } from "./TimePicker.styled";
10
+ import { padTwo } from "./utils";
11
+
12
+ interface Props {
13
+ count: number;
14
+ step: number;
15
+ value: number;
16
+ onChange: (v: number) => void;
17
+ ariaLabel: string;
18
+ autoFocus?: boolean;
19
+ }
20
+
21
+ const PAGE_JUMP = 3;
22
+
23
+ const Wheel = ({ count, step, value, onChange, ariaLabel, autoFocus = false }: Props) => {
24
+ const viewportRef = useRef<HTMLDivElement>(null);
25
+ const itemRef = useRef<HTMLDivElement>(null);
26
+ const itemHeightRef = useRef<number>(0);
27
+ const scrollRafRef = useRef<number | null>(null);
28
+ const valueRef = useRef(value);
29
+ valueRef.current = value;
30
+
31
+ const items = Math.ceil(count / step);
32
+ const lastValue = (items - 1) * step;
33
+
34
+ const valueToIndex = useCallback((v: number) => {
35
+ const idx = Math.round(v / step);
36
+ if (idx < 0) {
37
+ return 0;
38
+ }
39
+ if (idx >= items) {
40
+ return items - 1;
41
+ }
42
+ return idx;
43
+ }, [items, step]);
44
+
45
+ const scrollToValue = useCallback((v: number, behavior: ScrollBehavior) => {
46
+ const vp = viewportRef.current;
47
+ const h = itemHeightRef.current;
48
+ if (!vp || h === 0) {
49
+ return;
50
+ }
51
+ const idx = valueToIndex(v);
52
+ vp.scrollTo({ top: idx * h, behavior });
53
+ }, [valueToIndex]);
54
+
55
+ useLayoutEffect(() => {
56
+ const item = itemRef.current;
57
+ if (!item) {
58
+ return;
59
+ }
60
+ itemHeightRef.current = item.getBoundingClientRect().height;
61
+ scrollToValue(valueRef.current, "auto");
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
63
+ }, []);
64
+
65
+ useEffect(() => {
66
+ const item = itemRef.current;
67
+ if (!item || typeof ResizeObserver === "undefined") {
68
+ return;
69
+ }
70
+ const ro = new ResizeObserver((entries) => {
71
+ const entry = entries[0];
72
+ if (!entry) {
73
+ return;
74
+ }
75
+ const h = entry.contentRect.height;
76
+ if (h > 0 && h !== itemHeightRef.current) {
77
+ itemHeightRef.current = h;
78
+ scrollToValue(valueRef.current, "auto");
79
+ }
80
+ });
81
+ ro.observe(item);
82
+ return () => {
83
+ ro.disconnect();
84
+ };
85
+ }, [scrollToValue]);
86
+
87
+ useEffect(() => {
88
+ const vp = viewportRef.current;
89
+ const h = itemHeightRef.current;
90
+ if (!vp || h === 0) {
91
+ return;
92
+ }
93
+ const currentIdx = Math.round(vp.scrollTop / h);
94
+ const desiredIdx = valueToIndex(value);
95
+ if (currentIdx !== desiredIdx) {
96
+ scrollToValue(value, "smooth");
97
+ }
98
+ }, [value, valueToIndex, scrollToValue]);
99
+
100
+ const handleScroll = useCallback(() => {
101
+ const vp = viewportRef.current;
102
+ if (!vp) {
103
+ return;
104
+ }
105
+ if (scrollRafRef.current != null) {
106
+ cancelAnimationFrame(scrollRafRef.current);
107
+ }
108
+ scrollRafRef.current = requestAnimationFrame(() => {
109
+ scrollRafRef.current = null;
110
+ const vpInner = viewportRef.current;
111
+ const h = itemHeightRef.current;
112
+ if (!vpInner || h === 0) {
113
+ return;
114
+ }
115
+ const rawIdx = Math.round(vpInner.scrollTop / h);
116
+ const clamped = Math.max(0, Math.min(items - 1, rawIdx));
117
+ const newValue = clamped * step;
118
+ if (newValue !== valueRef.current) {
119
+ onChange(newValue);
120
+ }
121
+ });
122
+ }, [items, step, onChange]);
123
+
124
+ const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
125
+ let next: number;
126
+ switch (e.key) {
127
+ case "ArrowDown":
128
+ next = valueRef.current + step;
129
+ break;
130
+ case "ArrowUp":
131
+ next = valueRef.current - step;
132
+ break;
133
+ case "PageDown":
134
+ next = valueRef.current + (step * PAGE_JUMP);
135
+ break;
136
+ case "PageUp":
137
+ next = valueRef.current - (step * PAGE_JUMP);
138
+ break;
139
+ case "Home":
140
+ next = 0;
141
+ break;
142
+ case "End":
143
+ next = lastValue;
144
+ break;
145
+ default:
146
+ return;
147
+ }
148
+ e.preventDefault();
149
+ const clamped = Math.max(0, Math.min(lastValue, next));
150
+ if (clamped !== valueRef.current) {
151
+ onChange(clamped);
152
+ }
153
+ }, [step, lastValue, onChange]);
154
+
155
+ useEffect(() => () => {
156
+ if (scrollRafRef.current != null) {
157
+ cancelAnimationFrame(scrollRafRef.current);
158
+ }
159
+ }, []);
160
+
161
+ useEffect(() => {
162
+ if (autoFocus) {
163
+ viewportRef.current?.focus({ preventScroll: true });
164
+ }
165
+ // eslint-disable-next-line react-hooks/exhaustive-deps
166
+ }, []);
167
+
168
+ const itemElems: React.ReactElement[] = [];
169
+ for (let i = 0; i < items; i++) {
170
+ const v = i * step;
171
+ itemElems.push(
172
+ <StyledWheelItem key={i} ref={i === 0 ? itemRef : undefined}>
173
+ {padTwo(v)}
174
+ </StyledWheelItem>,
175
+ );
176
+ }
177
+
178
+ return (
179
+ <StyledWheelWrapper>
180
+ <StyledWheelViewport
181
+ ref={viewportRef}
182
+ onScroll={handleScroll}
183
+ onKeyDown={handleKeyDown}
184
+ role={"spinbutton"}
185
+ aria-label={ariaLabel}
186
+ aria-valuemin={0}
187
+ aria-valuemax={lastValue}
188
+ aria-valuenow={value}
189
+ aria-valuetext={padTwo(value)}
190
+ tabIndex={0}
191
+ >
192
+ <StyledWheelPadder />
193
+ {itemElems}
194
+ <StyledWheelPadder />
195
+ </StyledWheelViewport>
196
+ <StyledWheelFocusRing aria-hidden={true} />
197
+ </StyledWheelWrapper>
198
+ );
199
+ };
200
+
201
+ export { Wheel };
@@ -0,0 +1,66 @@
1
+ /** Pads a number to a 2-digit string with a leading zero (e.g. `7` → `"07"`). */
2
+ const padTwo = (n: number): string => n.toString().padStart(2, "0");
3
+
4
+ /** Clamps each completed pair in the digit buffer to a valid range: HH≤23, MM≤59, SS≤59. */
5
+ const clampDigits = (digits: string, withSeconds: boolean): string => {
6
+ if (digits.length < 2) {
7
+ return digits;
8
+ }
9
+ let hh = parseInt(digits.slice(0, 2), 10);
10
+ if (hh > 23) {
11
+ hh = 23;
12
+ }
13
+ let out = padTwo(hh);
14
+ if (digits.length < 4) {
15
+ return out + digits.slice(2);
16
+ }
17
+ let mm = parseInt(digits.slice(2, 4), 10);
18
+ if (mm > 59) {
19
+ mm = 59;
20
+ }
21
+ out += padTwo(mm);
22
+ if (!withSeconds || digits.length < 6) {
23
+ return out + digits.slice(4);
24
+ }
25
+ let ss = parseInt(digits.slice(4, 6), 10);
26
+ if (ss > 59) {
27
+ ss = 59;
28
+ }
29
+ out += padTwo(ss);
30
+ return out;
31
+ };
32
+
33
+ /**
34
+ * Parses a free-form time string into a clamped 2/4/6-digit buffer.
35
+ * Colon-aware: each colon-separated segment is padded to 2 digits, so `"3:40"` → `"0340"`.
36
+ * Used by the modal to extract HH/MM/SS from whatever the user typed.
37
+ */
38
+ const parseTimeText = (text: string, withSeconds: boolean): string => {
39
+ if (!text) {
40
+ return "";
41
+ }
42
+ const max = withSeconds ? 6 : 4;
43
+ const segments = text.split(":");
44
+ let raw: string;
45
+ if (segments.length === 1) {
46
+ raw = (segments[0] ?? "").replace(/\D/gu, "");
47
+ if (raw.length === 1) {
48
+ raw = "0" + raw;
49
+ }
50
+ }
51
+ else {
52
+ raw = segments.map((s) => {
53
+ const d = s.replace(/\D/gu, "");
54
+ if (d.length === 0) {
55
+ return "";
56
+ }
57
+ if (d.length === 1) {
58
+ return "0" + d;
59
+ }
60
+ return d.slice(0, 2);
61
+ }).join("");
62
+ }
63
+ return clampDigits(raw.slice(0, max), withSeconds);
64
+ };
65
+
66
+ export { padTwo, parseTimeText };
@@ -0,0 +1,38 @@
1
+ import React, { forwardRef } from "react";
2
+
3
+ interface Props {
4
+ className?: string;
5
+ }
6
+
7
+ const Clock = forwardRef<SVGSVGElement, Props>((props, ref) => {
8
+ return (
9
+ <svg
10
+ ref={ref}
11
+ width={"16"}
12
+ height={"16"}
13
+ viewBox={"0 0 16 16"}
14
+ xmlns={"http://www.w3.org/2000/svg"}
15
+ className={props.className}
16
+ >
17
+ <circle
18
+ cx={"8"}
19
+ cy={"8"}
20
+ r={"6.5"}
21
+ fill={"none"}
22
+ stroke={"currentColor"}
23
+ strokeWidth={"1.5"}
24
+ />
25
+ <path
26
+ fill={"none"}
27
+ stroke={"currentColor"}
28
+ strokeWidth={"1.5"}
29
+ strokeLinecap={"round"}
30
+ d={"M8 4.5V8l2.5 1.75"}
31
+ />
32
+ </svg>
33
+ );
34
+ });
35
+
36
+ Clock.displayName = "Clock";
37
+
38
+ export { Clock };
@@ -3,6 +3,7 @@ import React, { forwardRef } from "react";
3
3
  import { Back } from "./Back";
4
4
  import { Battery } from "./Battery";
5
5
  import { Checkmark } from "./Checkmark";
6
+ import { Clock } from "./Clock";
6
7
  import { Config } from "./Config";
7
8
  import { Dots } from "./Dots";
8
9
  import { Forward } from "./Forward";
@@ -20,6 +21,7 @@ enum ICON {
20
21
  trash = "trash",
21
22
  config = "config",
22
23
  dots = "dots",
24
+ clock = "clock",
23
25
  }
24
26
 
25
27
  interface Props {
@@ -39,6 +41,7 @@ const iconsMap = new Map<ICON, IconComponent>([
39
41
  [ICON.trash, Trash],
40
42
  [ICON.config, Config],
41
43
  [ICON.dots, Dots],
44
+ [ICON.clock, Clock],
42
45
  ]);
43
46
 
44
47
  const Icon = forwardRef<SVGSVGElement, Props>(({ name: iconName, ...props }, ref) => {
@@ -8,7 +8,6 @@ import { Label } from "../../form/Label";
8
8
  import { List } from "../../layout/list/List";
9
9
  import { Button } from "../button/Button";
10
10
  import { Modal } from "./Modal";
11
- import { RemovePadding } from "./Modal.styled";
12
11
  import { ModalButtons } from "./ModalButtons";
13
12
 
14
13
  const meta: Meta = {
@@ -77,19 +76,19 @@ const WithRemovedPaddingSections: Story = {
77
76
  <div>
78
77
  <Button onClick={handleOpen}>Open modal</Button>
79
78
  <Modal onClose={handleClose} isOpen={open} position={"bottom"} full={true}>
80
- <RemovePadding>
79
+ <Modal.RemovePadding>
81
80
  <Label>
82
81
  <Input placeholder={"New station"} />
83
82
  </Label>
84
- </RemovePadding>
83
+ </Modal.RemovePadding>
85
84
  <Label>
86
85
  <Input placeholder={"New station"} />
87
86
  </Label>
88
- <RemovePadding>
87
+ <Modal.RemovePadding>
89
88
  <Label>
90
89
  <Input placeholder={"New station"} />
91
90
  </Label>
92
- </RemovePadding>
91
+ </Modal.RemovePadding>
93
92
  </Modal>
94
93
  </div>
95
94
  );
@@ -116,14 +115,50 @@ const WithList: Story = {
116
115
  <div>
117
116
  <Button onClick={handleOpen}>Open modal</Button>
118
117
  <Modal onClose={handleClose} isOpen={open} position={"bottom"} full={true}>
119
- <RemovePadding>
118
+ <Modal.RemovePadding>
120
119
  <List inset={true}>
121
120
  <List.Item selected={false} onClick={handleClose}>First item</List.Item>
122
121
  <List.Item selected={true} onClick={handleClose}>Second item</List.Item>
123
122
  <List.Item selected={false} onClick={handleClose}>Third item</List.Item>
124
123
  <List.Item selected={false} onClick={handleClose}>Last option</List.Item>
125
124
  </List>
126
- </RemovePadding>
125
+ </Modal.RemovePadding>
126
+ </Modal>
127
+ </div>
128
+ );
129
+ },
130
+ };
131
+
132
+ /**
133
+ * Demonstrates a known issue: when the modal content is taller than the viewport,
134
+ * it gets clipped at the top and bottom because `ContainerStyled` has
135
+ * `maxHeight: 100%` but no `overflow: auto`. There is no scrolling inside the modal.
136
+ */
137
+ const WithLongContent: Story = {
138
+ args: {},
139
+ render: () => {
140
+ const [open, setIsOpen] = useState(false);
141
+
142
+ const handleClose = useCallback(() => {
143
+ setIsOpen(false);
144
+ }, []);
145
+
146
+ const handleOpen = useCallback(() => {
147
+ setIsOpen(true);
148
+ }, []);
149
+
150
+ const lines = Array.from({ length: 60 }, (_, i) => i + 1);
151
+
152
+ return (
153
+ <div>
154
+ <Button onClick={handleOpen}>Open modal</Button>
155
+ <Modal onClose={handleClose} isOpen={open} title={"Long content"}>
156
+ {lines.map((n) => (
157
+ <p key={n}>
158
+ Line {n} — Lorem ipsum dolor sit amet, consectetur adipiscing elit.
159
+ Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
160
+ </p>
161
+ ))}
127
162
  </Modal>
128
163
  </div>
129
164
  );
@@ -134,6 +169,7 @@ export {
134
169
  Primary,
135
170
  WithRemovedPaddingSections,
136
171
  WithList,
172
+ WithLongContent,
137
173
  };
138
174
 
139
175
  export default meta;
@@ -13,20 +13,25 @@ const overlay = keyframes({
13
13
  });
14
14
 
15
15
  const OverlayStyled = styled("div", {
16
- position: "fixed",
17
- zIndex: 4,
18
- top: 0,
19
- bottom: 0,
20
- left: 0,
21
- right: 0,
22
- display: "flex",
23
- alignItems: "center",
24
- justifyContent: "center",
25
- animation: `${overlay.toString()} 300ms`,
26
- animationFillMode: "forwards",
27
- backdropFilter: "blur(5px)",
16
+ "position": "fixed",
17
+ "zIndex": 4,
18
+ "top": 0,
19
+ "bottom": 0,
20
+ "left": 0,
21
+ "right": 0,
22
+ "display": "flex",
23
+ "alignItems": "center",
24
+ "justifyContent": "center",
25
+ "animation": `${overlay.toString()} 300ms`,
26
+ "animationFillMode": "forwards",
27
+ "backdropFilter": "blur(5px)",
28
28
 
29
- variants: {
29
+ "@media (prefers-reduced-motion: reduce)": {
30
+ animation: "none",
31
+ background: "rgba($background, 0.3)",
32
+ },
33
+
34
+ "variants": {
30
35
  position: {
31
36
  bottom: {
32
37
  alignItems: "flex-end",
@@ -57,18 +62,33 @@ const RemovePadding = styled("div", {
57
62
  });
58
63
 
59
64
  const ContainerStyled = styled("div", {
60
- background: "$modalBg",
61
- borderRadius: dimensionsPxToRem(12),
62
- maxWidth: pxToRem(333),
63
- maxHeight: "100%",
64
- width: "calc(100% - 30px)",
65
- padding: PADDING,
66
- position: "relative",
67
- boxSizing: "border-box",
68
- animation: `${container.toString()} 300ms`,
69
- animationFillMode: "forwards",
65
+ "background": "$modalBg",
66
+ "borderRadius": dimensionsPxToRem(12),
67
+ "maxWidth": pxToRem(333),
68
+ "maxHeight": "100%",
69
+ "width": "calc(100% - 30px)",
70
+ "padding": PADDING,
71
+ "position": "relative",
72
+ "boxSizing": "border-box",
73
+ "overflowY": "auto",
74
+ "overscrollBehavior": "contain",
75
+ "animation": `${container.toString()} 300ms`,
76
+ "animationFillMode": "forwards",
77
+
78
+ // The dialog uses tabindex=-1 to be programmatically focusable for initial focus
79
+ // fallback (when there is no focusable child). Hide the outline since it's only
80
+ // there as a focus target, not a user-visible affordance.
81
+ "&:focus": {
82
+ outline: "none",
83
+ },
84
+
85
+ "@media (prefers-reduced-motion: reduce)": {
86
+ animation: "none",
87
+ opacity: 1,
88
+ transform: "none",
89
+ },
70
90
 
71
- variants: {
91
+ "variants": {
72
92
  // TODO this is very not rwd, it should be a media query
73
93
  full: {
74
94
  true: {
@@ -84,11 +104,12 @@ const ContainerStyled = styled("div", {
84
104
  },
85
105
  });
86
106
 
87
- const TitleStyled = styled("div", { // TODO header by default? expose this as `titleAs`?
107
+ const TitleStyled = styled("h2", {
88
108
  fontSize: fontPxToRem(40),
89
109
  textAlign: "center",
90
110
  color: "$text3",
91
111
  margin: `${dimensionsPxToRem(90)} 0`,
112
+ fontWeight: "inherit",
92
113
  });
93
114
 
94
115
  export {