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
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, useCallback, useEffect, useRef, useState } from "react";
1
+ import React, { forwardRef, useCallback, useEffect, useId, useRef, useState } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
 
4
4
  import { useForwardedRef } from "@bedrock-layout/use-forwarded-ref";
@@ -6,18 +6,34 @@ import { useForwardedRef } from "@bedrock-layout/use-forwarded-ref";
6
6
  import type { ThemeCSS } from "../../../theme";
7
7
 
8
8
  import { fnWithProps } from "../../../types/fnWithProps";
9
+ import { HandleEsc } from "../../utils/HandleEsc";
9
10
  import { ContainerStyled, NEGATIVE_PADDING, OverlayStyled, RemovePadding, TitleStyled } from "./Modal.styled";
10
11
 
11
12
  type OverlayProps = React.ComponentProps<typeof OverlayStyled>;
12
13
  type ContainerProps = React.ComponentProps<typeof ContainerStyled>;
13
14
 
14
- interface Props {
15
+ const FOCUSABLE_SELECTOR = [
16
+ "a[href]",
17
+ "button:not([disabled])",
18
+ "input:not([disabled])",
19
+ "select:not([disabled])",
20
+ "textarea:not([disabled])",
21
+ "[tabindex]:not([tabindex=\"-1\"])",
22
+ ].join(",");
23
+
24
+ const prefersReducedMotion = () => {
25
+ if (typeof window === "undefined") {
26
+ return false;
27
+ }
28
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
29
+ };
30
+
31
+ interface Props extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
15
32
  onOverlayClick?: (() => void) | "close" | null;
16
33
  closeOnEsc?: boolean;
17
34
  onClose: () => void;
18
35
  isOpen: boolean;
19
36
  title?: React.ReactNode;
20
- className?: string;
21
37
  portal?: boolean | HTMLElement;
22
38
  children: React.ReactNode;
23
39
 
@@ -39,57 +55,107 @@ const ModalBase = forwardRef<HTMLDivElement, Props>(({
39
55
  portal = true,
40
56
  position,
41
57
  full,
58
+ ...rest
42
59
  }, ref) => {
43
60
  const [isClosing, setIsClosing] = useState(false);
44
61
  const [isRendered, setIsRendered] = useState(false);
45
62
  const overlayRef = useRef<HTMLDivElement>(null);
46
63
  const containerRef = useForwardedRef(ref);
64
+ const previouslyFocusedRef = useRef<HTMLElement | null>(null);
65
+ const titleId = useId();
47
66
 
48
67
  useEffect(() => {
49
- if (!isOpen || !closeOnEsc) {
68
+ if (isOpen) {
69
+ // eslint-disable-next-line react-hooks/set-state-in-effect
70
+ setIsRendered(true);
71
+ setIsClosing(false);
50
72
  return;
51
73
  }
52
-
53
- const onKeyDown = (e: KeyboardEvent) => {
54
- if (e.key === "Escape") {
55
- onClose();
56
- }
57
- };
58
- document.addEventListener("keydown", onKeyDown);
59
- return () => {
60
- document.removeEventListener("keydown", onKeyDown);
61
- };
62
- }, [isOpen, closeOnEsc, onClose]);
63
-
64
- useEffect(() => {
65
- if (!isOpen) {
66
- // eslint-disable-next-line react-hooks/set-state-in-effect
67
- setIsClosing(true);
74
+ // Under reduced motion the close animation is disabled, so onAnimationEnd will
75
+ // never fire unmount synchronously instead.
76
+ if (prefersReducedMotion()) {
77
+ setIsRendered(false);
78
+ setIsClosing(false);
68
79
  return;
69
80
  }
70
- setIsRendered(true);
71
- setIsClosing(false);
81
+ setIsClosing(true);
72
82
  }, [isOpen]);
73
83
 
74
84
  useEffect(() => {
75
85
  if (!isClosing) {
76
86
  return;
77
87
  }
78
-
88
+ const overlay = overlayRef.current;
89
+ const container = containerRef.current;
79
90
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
80
- if (!overlayRef.current || !containerRef.current) {
91
+ if (!overlay || !container) {
81
92
  return;
82
93
  }
83
-
84
- overlayRef.current.style.animation = "none";
85
- containerRef.current.style.animation = "none";
94
+ // animationFillMode: forwards leaves the keyframe in its end state, so flipping
95
+ // animationDirection to reverse alone wouldn't replay it. Force a restart by
96
+ // clearing animation, triggering reflow, then letting the variant's reverse run.
97
+ overlay.style.animation = "none";
98
+ container.style.animation = "none";
86
99
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
87
- overlayRef.current.offsetHeight; // force sync document reflow
88
- overlayRef.current.style.removeProperty("animation");
89
- containerRef.current.style.removeProperty("animation");
100
+ overlay.offsetHeight;
101
+ overlay.style.removeProperty("animation");
102
+ container.style.removeProperty("animation");
90
103
  }, [isClosing, containerRef]);
91
104
 
92
- const titleElem = title ? <TitleStyled>{title}</TitleStyled> : null;
105
+ // Focus management: capture previous focus on open, set initial focus inside the
106
+ // dialog, restore focus on close. Tab containment + AT-hiding is delegated to the
107
+ // `inert` attribute on everything outside the modal subtree (native browser handles
108
+ // Tab cycling, screen-reader pruning, and pointer-event blocking).
109
+ useEffect(() => {
110
+ if (!isOpen || !isRendered) {
111
+ return;
112
+ }
113
+ const container = containerRef.current;
114
+ const overlay = overlayRef.current;
115
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
116
+ if (!container || !overlay) {
117
+ return;
118
+ }
119
+
120
+ previouslyFocusedRef.current = document.activeElement instanceof HTMLElement
121
+ ? document.activeElement
122
+ : null;
123
+
124
+ const focusables = container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
125
+ (focusables[0] ?? container).focus();
126
+
127
+ // Walk up from the overlay to <body>; at each level mark every sibling of the
128
+ // cursor as inert. Already-inert nodes (e.g. from a nesting modal) are skipped
129
+ // so that the outer owner remains responsible for cleanup — this gives correct
130
+ // stacking for nested modals without a separate registry.
131
+ const inerted: HTMLElement[] = [];
132
+ let cursor: HTMLElement = overlay;
133
+ while (cursor !== document.body) {
134
+ const parentEl: HTMLElement | null = cursor.parentElement;
135
+ if (!parentEl) {
136
+ break;
137
+ }
138
+ for (const child of Array.from(parentEl.children)) {
139
+ if (child === cursor || !(child instanceof HTMLElement)) {
140
+ continue;
141
+ }
142
+ if (!child.hasAttribute("inert")) {
143
+ child.setAttribute("inert", "");
144
+ inerted.push(child);
145
+ }
146
+ }
147
+ cursor = parentEl;
148
+ }
149
+
150
+ return () => {
151
+ inerted.forEach((el) => {
152
+ el.removeAttribute("inert");
153
+ });
154
+ previouslyFocusedRef.current?.focus();
155
+ };
156
+ }, [isOpen, isRendered, containerRef]);
157
+
158
+ const titleElem = title ? <TitleStyled id={titleId}>{title}</TitleStyled> : null;
93
159
 
94
160
  const handleOverlayClick = useCallback((e: React.MouseEvent) => {
95
161
  if (e.target !== e.currentTarget) {
@@ -98,16 +164,18 @@ const ModalBase = forwardRef<HTMLDivElement, Props>(({
98
164
  if (onOverlayClick === "close") {
99
165
  onClose();
100
166
  }
101
- if (typeof onOverlayClick === "function") {
167
+ else if (typeof onOverlayClick === "function") {
102
168
  onOverlayClick();
103
169
  }
104
170
  }, [onOverlayClick, onClose]);
105
171
 
106
- const handleAnimationEnd = useCallback(() => {
172
+ const handleAnimationEnd = useCallback((e: React.AnimationEvent) => {
173
+ if (e.target !== e.currentTarget) {
174
+ return;
175
+ }
107
176
  if (isOpen) {
108
177
  return;
109
178
  }
110
-
111
179
  setIsRendered(false);
112
180
  }, [isOpen]);
113
181
 
@@ -116,30 +184,35 @@ const ModalBase = forwardRef<HTMLDivElement, Props>(({
116
184
  }
117
185
 
118
186
  const overlayVariants: Pick<OverlayProps, "isClosing" | "position"> = {};
119
- isClosing && (overlayVariants.isClosing = true);
120
- position != null && (overlayVariants.position = position);
187
+ if (isClosing) {
188
+ overlayVariants.isClosing = true;
189
+ }
190
+ if (position != null) {
191
+ overlayVariants.position = position;
192
+ }
121
193
 
122
194
  const containerVariants: Pick<ContainerProps, "isClosing" | "full"> = {};
123
- isClosing && (containerVariants.isClosing = true);
124
- full != null && (containerVariants.full = full);
195
+ if (isClosing) {
196
+ containerVariants.isClosing = true;
197
+ }
198
+ if (full != null) {
199
+ containerVariants.full = full;
200
+ }
125
201
 
126
202
  const childrenCount = React.Children.count(children);
127
203
 
128
204
  const chld = React.Children.map(children, (child, index) => {
129
- if (React.isValidElement(child)) {
130
- if (child.type === RemovePadding) {
131
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
132
- const css: ThemeCSS = child.props.css ?? {};
133
- if (index === 0 && titleElem == null) {
134
- css.marginTop = NEGATIVE_PADDING;
135
- }
136
- if (index === childrenCount - 1) {
137
- css.marginBottom = NEGATIVE_PADDING;
138
- }
139
-
140
- return React.cloneElement(child, { css });
205
+ if (React.isValidElement(child) && child.type === RemovePadding) {
206
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
207
+ const css: ThemeCSS = { ...(child.props.css ?? {}) };
208
+ if (index === 0 && titleElem == null) {
209
+ css.marginTop = NEGATIVE_PADDING;
141
210
  }
142
- return child;
211
+ if (index === childrenCount - 1) {
212
+ css.marginBottom = NEGATIVE_PADDING;
213
+ }
214
+
215
+ return React.cloneElement(child, { css });
143
216
  }
144
217
  return child;
145
218
  });
@@ -151,7 +224,17 @@ const ModalBase = forwardRef<HTMLDivElement, Props>(({
151
224
  ref={overlayRef}
152
225
  onAnimationEnd={handleAnimationEnd}
153
226
  >
154
- <ContainerStyled className={className} {...containerVariants} ref={containerRef}>
227
+ {closeOnEsc ? <HandleEsc onPress={onClose} /> : null}
228
+ <ContainerStyled
229
+ role={"dialog"}
230
+ aria-modal={true}
231
+ aria-labelledby={titleElem ? titleId : undefined}
232
+ tabIndex={-1}
233
+ className={className}
234
+ {...containerVariants}
235
+ ref={containerRef}
236
+ {...rest}
237
+ >
155
238
  {titleElem}
156
239
  {chld}
157
240
  </ContainerStyled>
@@ -7,7 +7,6 @@ const injectGlobalStyles = globalCss({
7
7
  "*:focus-visible:focus-visible": {
8
8
  outline: "none !important",
9
9
  borderColor: "$focusColor",
10
- backgroundColor: "var(--focus-bg-set)",
11
10
  },
12
11
  "body": {
13
12
  color: "$text",
@@ -1,3 +1,4 @@
1
1
  export * from "./toObjectValue";
2
2
  export * from "./useKeyPress";
3
+ export * from "./useNativeValidity";
3
4
  export * from "./useTailSpin";
@@ -0,0 +1,57 @@
1
+ import { useCallback, useState } from "react";
2
+
3
+ import type React from "react";
4
+
5
+ interface UseNativeValidityResult {
6
+ /** Combined: the explicit `error` prop OR the input's native invalid state. Pass to your `error`/styling. */
7
+ finalError: boolean;
8
+ /** Re-checks validity. Call from your `onBlur`. */
9
+ onBlur: (e: React.FocusEvent<HTMLInputElement>) => void;
10
+ /** Clears the invalid flag if the value just became valid. Call from your `onChange`. */
11
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
12
+ /** Fires when the form tries to submit an invalid value. Call from your `onInvalid`. */
13
+ onInvalid: () => void;
14
+ /** Clears the invalid flag if the input is currently valid. Useful after programmatic value changes. */
15
+ revalidate: (input: HTMLInputElement | null) => void;
16
+ }
17
+
18
+ /**
19
+ * Tracks native HTML5 validity (pattern, required, type=email/url, etc.) and surfaces it
20
+ * as an `error` boolean — the rule mirrors the CSS `:user-invalid` pseudo-class:
21
+ * invalid is sticky after blur or a failed submit, and clears as soon as the value becomes valid.
22
+ *
23
+ * Compose the returned handlers with your component's own `onBlur` / `onChange` / `onInvalid`.
24
+ */
25
+ const useNativeValidity = (error: boolean | undefined): UseNativeValidityResult => {
26
+ const [nativeInvalid, setNativeInvalid] = useState(false);
27
+
28
+ const onBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
29
+ setNativeInvalid(!e.currentTarget.checkValidity());
30
+ }, []);
31
+
32
+ const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
33
+ if (e.currentTarget.checkValidity()) {
34
+ setNativeInvalid(false);
35
+ }
36
+ }, []);
37
+
38
+ const onInvalid = useCallback(() => {
39
+ setNativeInvalid(true);
40
+ }, []);
41
+
42
+ const revalidate = useCallback((input: HTMLInputElement | null) => {
43
+ if (input?.checkValidity()) {
44
+ setNativeInvalid(false);
45
+ }
46
+ }, []);
47
+
48
+ return {
49
+ finalError: Boolean(error) || nativeInvalid,
50
+ onBlur,
51
+ onChange,
52
+ onInvalid,
53
+ revalidate,
54
+ };
55
+ };
56
+
57
+ export { useNativeValidity };