react-miui 0.33.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 (343) hide show
  1. package/.claude/settings.json +12 -0
  2. package/.claude/settings.local.json +5 -1
  3. package/.storybook/preview.tsx +10 -4
  4. package/CHANGELOG.md +22 -0
  5. package/dist/components/form/index.d.ts +1 -0
  6. package/dist/components/form/index.d.ts.map +1 -1
  7. package/dist/components/form/index.js +1 -0
  8. package/dist/components/form/index.js.map +1 -1
  9. package/dist/components/form/input/Input.d.ts.map +1 -1
  10. package/dist/components/form/input/Input.js +9 -5
  11. package/dist/components/form/input/Input.js.map +1 -1
  12. package/dist/components/form/timepicker/TimePicker.css.d.ts +99 -0
  13. package/dist/components/form/timepicker/TimePicker.css.d.ts.map +1 -0
  14. package/dist/components/form/timepicker/TimePicker.css.js +116 -0
  15. package/dist/components/form/timepicker/TimePicker.css.js.map +1 -0
  16. package/dist/components/form/timepicker/TimePicker.d.ts +22 -0
  17. package/dist/components/form/timepicker/TimePicker.d.ts.map +1 -0
  18. package/dist/components/form/timepicker/TimePicker.js +141 -0
  19. package/dist/components/form/timepicker/TimePicker.js.map +1 -0
  20. package/dist/components/form/timepicker/TimePicker.styled.d.ts +936 -0
  21. package/dist/components/form/timepicker/TimePicker.styled.d.ts.map +1 -0
  22. package/dist/components/form/timepicker/TimePicker.styled.js +29 -0
  23. package/dist/components/form/timepicker/TimePicker.styled.js.map +1 -0
  24. package/dist/components/form/timepicker/TimePickerModal.d.ts +17 -0
  25. package/dist/components/form/timepicker/TimePickerModal.d.ts.map +1 -0
  26. package/dist/components/form/timepicker/TimePickerModal.js +92 -0
  27. package/dist/components/form/timepicker/TimePickerModal.js.map +1 -0
  28. package/dist/components/form/timepicker/Wheel.d.ts +12 -0
  29. package/dist/components/form/timepicker/Wheel.d.ts.map +1 -0
  30. package/dist/components/form/timepicker/Wheel.js +187 -0
  31. package/dist/components/form/timepicker/Wheel.js.map +1 -0
  32. package/dist/components/form/timepicker/utils.d.ts +4 -0
  33. package/dist/components/form/timepicker/utils.d.ts.map +1 -0
  34. package/dist/components/form/timepicker/utils.js +62 -0
  35. package/dist/components/form/timepicker/utils.js.map +1 -0
  36. package/dist/components/icons/Clock.d.ts +7 -0
  37. package/dist/components/icons/Clock.d.ts.map +1 -0
  38. package/dist/components/icons/Clock.js +45 -0
  39. package/dist/components/icons/Clock.js.map +1 -0
  40. package/dist/components/icons/Icon.d.ts +2 -1
  41. package/dist/components/icons/Icon.d.ts.map +1 -1
  42. package/dist/components/icons/Icon.js +3 -0
  43. package/dist/components/icons/Icon.js.map +1 -1
  44. package/dist/components/ui/drawer/Drawer.d.ts +10 -1
  45. package/dist/components/ui/drawer/Drawer.d.ts.map +1 -1
  46. package/dist/components/ui/drawer/Drawer.js +135 -15
  47. package/dist/components/ui/drawer/Drawer.js.map +1 -1
  48. package/dist/components/ui/drawer/Drawer.styled.d.ts +86 -1
  49. package/dist/components/ui/drawer/Drawer.styled.d.ts.map +1 -1
  50. package/dist/components/ui/drawer/Drawer.styled.js +13 -1
  51. package/dist/components/ui/drawer/Drawer.styled.js.map +1 -1
  52. package/dist/components/ui/modal/Modal.d.ts +1 -2
  53. package/dist/components/ui/modal/Modal.d.ts.map +1 -1
  54. package/dist/components/ui/modal/Modal.js +114 -42
  55. package/dist/components/ui/modal/Modal.js.map +1 -1
  56. package/dist/components/ui/modal/Modal.styled.d.ts +1 -1
  57. package/dist/components/ui/modal/Modal.styled.d.ts.map +1 -1
  58. package/dist/components/ui/modal/Modal.styled.js +40 -25
  59. package/dist/components/ui/modal/Modal.styled.js.map +1 -1
  60. package/dist/components/ui/toaster/Toaster.d.ts.map +1 -1
  61. package/dist/components/ui/toaster/Toaster.js +7 -1
  62. package/dist/components/ui/toaster/Toaster.js.map +1 -1
  63. package/dist/components/ui/tooltip/Tooltip.d.ts +30 -0
  64. package/dist/components/ui/tooltip/Tooltip.d.ts.map +1 -0
  65. package/dist/components/ui/tooltip/Tooltip.js +81 -0
  66. package/dist/components/ui/tooltip/Tooltip.js.map +1 -0
  67. package/dist/components/ui/tooltip/Tooltip.styled.d.ts +173 -0
  68. package/dist/components/ui/tooltip/Tooltip.styled.d.ts.map +1 -0
  69. package/dist/components/ui/tooltip/Tooltip.styled.js +65 -0
  70. package/dist/components/ui/tooltip/Tooltip.styled.js.map +1 -0
  71. package/dist/index.d.ts +1 -0
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +1 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/theme.css-global.d.ts.map +1 -1
  76. package/dist/theme.css-global.js +0 -1
  77. package/dist/theme.css-global.js.map +1 -1
  78. package/dist/utils/index.d.ts +1 -0
  79. package/dist/utils/index.d.ts.map +1 -1
  80. package/dist/utils/index.js +1 -0
  81. package/dist/utils/index.js.map +1 -1
  82. package/dist/utils/useNativeValidity.d.ts +11 -0
  83. package/dist/utils/useNativeValidity.d.ts.map +1 -0
  84. package/dist/utils/useNativeValidity.js +32 -0
  85. package/dist/utils/useNativeValidity.js.map +1 -0
  86. package/docs/assets/highlight.css +7 -0
  87. package/docs/assets/navigation.js +1 -1
  88. package/docs/assets/search.js +1 -1
  89. package/docs/classes/index.Pop.html +7 -7
  90. package/docs/documents/Test.html +2 -2
  91. package/docs/enums/index.ICON.html +3 -2
  92. package/docs/functions/index.Action.html +3 -3
  93. package/docs/functions/index.Button.html +4 -4
  94. package/docs/functions/index.Card.html +3 -3
  95. package/docs/functions/index.Checkbox.html +3 -3
  96. package/docs/functions/index.Choice.html +2 -2
  97. package/docs/functions/index.ColorPicker.html +3 -3
  98. package/docs/functions/index.CoveringLoader.html +3 -3
  99. package/docs/functions/index.DirectionPad.html +2 -2
  100. package/docs/functions/index.Drawer.html +2 -2
  101. package/docs/functions/index.EqualActions.html +2 -2
  102. package/docs/functions/index.FullLoader.html +3 -3
  103. package/docs/functions/index.Gap.html +3 -3
  104. package/docs/functions/index.HandleEsc.html +3 -3
  105. package/docs/functions/index.Header.html +3 -3
  106. package/docs/functions/index.HeaderIconAction.html +3 -3
  107. package/docs/functions/index.Icon-1.html +2 -2
  108. package/docs/functions/index.If.html +3 -3
  109. package/docs/functions/index.Input.html +1 -1
  110. package/docs/functions/index.KeyValue.html +2 -2
  111. package/docs/functions/index.Label.html +2 -2
  112. package/docs/functions/index.Line.html +4 -4
  113. package/docs/functions/index.List.html +2 -2
  114. package/docs/functions/index.Loader.html +3 -3
  115. package/docs/functions/index.Loading.html +3 -3
  116. package/docs/functions/index.Message.html +4 -4
  117. package/docs/functions/index.Modal.html +2 -2
  118. package/docs/functions/index.ModalButtons.html +3 -3
  119. package/docs/functions/index.PopLoader.html +3 -3
  120. package/docs/functions/index.PopOption.html +2 -2
  121. package/docs/functions/index.Progress.html +2 -2
  122. package/docs/functions/index.SearchContainer.html +3 -3
  123. package/docs/functions/index.Section.html +4 -4
  124. package/docs/functions/index.Select.html +3 -3
  125. package/docs/functions/index.Selector.html +2 -2
  126. package/docs/functions/index.Spacer.html +3 -3
  127. package/docs/functions/index.Stats.html +2 -2
  128. package/docs/functions/index.StickyHeader.html +4 -4
  129. package/docs/functions/index.Table.html +3 -3
  130. package/docs/functions/index.TextArea.html +2 -2
  131. package/docs/functions/index.TimePicker.html +10 -0
  132. package/docs/functions/index.ToasterProvider.html +3 -3
  133. package/docs/functions/index.Toggle.html +3 -3
  134. package/docs/functions/index.ToolButton.html +4 -4
  135. package/docs/functions/index.Tooltip.html +18 -0
  136. package/docs/functions/index.TooltipProvider.html +6 -0
  137. package/docs/functions/index.borderPxToRem.html +1 -1
  138. package/docs/functions/index.createTheme.html +1 -1
  139. package/docs/functions/index.css.html +1 -1
  140. package/docs/functions/index.dimensionsPxToRem.html +1 -1
  141. package/docs/functions/index.fontPxToRem.html +1 -1
  142. package/docs/functions/index.getCssText.html +1 -1
  143. package/docs/functions/index.globalCss.html +2 -2
  144. package/docs/functions/index.injectGlobalStyles.html +1 -1
  145. package/docs/functions/index.keyframes.html +1 -1
  146. package/docs/functions/index.pxToRem.html +1 -1
  147. package/docs/functions/index.styled.html +1 -1
  148. package/docs/functions/index.toast.html +2 -2
  149. package/docs/functions/index.useToaster.html +1 -1
  150. package/docs/index.html +2 -2
  151. package/docs/interfaces/index.IconProps.html +2 -2
  152. package/docs/interfaces/index.InputCustomProps.html +3 -3
  153. package/docs/interfaces/index.LoaderProps.html +6 -6
  154. package/docs/interfaces/index.StickyHeaderProps.html +4 -4
  155. package/docs/interfaces/index.ToasterProviderProps.html +3 -3
  156. package/docs/interfaces/index.TooltipProps.html +36 -0
  157. package/docs/interfaces/index.TooltipProviderProps.html +13 -0
  158. package/docs/modules/index.html +1 -1
  159. package/docs/modules.html +1 -1
  160. package/docs/types/index.ActionProps.html +1 -1
  161. package/docs/types/index.CardProps.html +1 -1
  162. package/docs/types/index.CheckboxProps.html +2 -2
  163. package/docs/types/index.ChoiceProps.html +1 -1
  164. package/docs/types/index.ColorPickerProps.html +1 -1
  165. package/docs/types/index.DirectionPadProps.html +1 -1
  166. package/docs/types/index.DrawerFrom.html +1 -0
  167. package/docs/types/index.DrawerProps.html +28 -1
  168. package/docs/types/index.EqualActionsProps.html +1 -1
  169. package/docs/types/index.HeaderProps.html +1 -1
  170. package/docs/types/index.InputProps.html +1 -1
  171. package/docs/types/index.KeyValueProps.html +1 -1
  172. package/docs/types/index.LabelProps.html +1 -1
  173. package/docs/types/index.OverwriteProps.html +1 -1
  174. package/docs/types/index.ProgressProps.html +2 -2
  175. package/docs/types/index.SelectProps.html +1 -1
  176. package/docs/types/index.SelectorProps.html +1 -1
  177. package/docs/types/index.Stat.html +1 -1
  178. package/docs/types/index.StatsProps.html +1 -1
  179. package/docs/types/index.TextAreaProps.html +1 -1
  180. package/docs/types/index.ThemeCSS.html +1 -1
  181. package/docs/types/index.TimePickerProps.html +1 -0
  182. package/docs/types/index.ToggleProps.html +2 -2
  183. package/docs/variables/index.ActionBadgeSelector.html +1 -1
  184. package/docs/variables/index.ActionCircleSelector.html +1 -1
  185. package/docs/variables/index.CheckboxCheckmarkWrapperSelector.html +1 -1
  186. package/docs/variables/index.CheckboxTextLabelSelector.html +1 -1
  187. package/docs/variables/index.ChoiceItemSelector.html +1 -1
  188. package/docs/variables/index.ColorPickerColorDisplaySelector.html +1 -1
  189. package/docs/variables/index.DirectionPadButtonDotSelector.html +1 -1
  190. package/docs/variables/index.DirectionPadButtonSelector.html +1 -1
  191. package/docs/variables/index.DirectionPadLineSelector.html +1 -1
  192. package/docs/variables/index.DirectionPadMiddleSelector.html +1 -1
  193. package/docs/variables/index.DrawerContentSelector.html +1 -1
  194. package/docs/variables/index.HeaderAfterSelector.html +1 -1
  195. package/docs/variables/index.HeaderBeforeSelector.html +1 -1
  196. package/docs/variables/index.HeaderContentsSelector.html +1 -1
  197. package/docs/variables/index.HeaderIconActionIconSelector.html +1 -1
  198. package/docs/variables/index.InputContainerSelector.html +1 -1
  199. package/docs/variables/index.InputInputSelector.html +1 -1
  200. package/docs/variables/index.InputLabelSelector.html +1 -1
  201. package/docs/variables/index.InputPrefixSelector.html +1 -1
  202. package/docs/variables/index.InputSuffixSelector.html +1 -1
  203. package/docs/variables/index.KeyValueIconSelector.html +1 -1
  204. package/docs/variables/index.KeyValueItemSelector.html +1 -1
  205. package/docs/variables/index.KeyValueKeySelector.html +1 -1
  206. package/docs/variables/index.KeyValuePairSelector.html +1 -1
  207. package/docs/variables/index.KeyValueValueSelector.html +1 -1
  208. package/docs/variables/index.LabelTextSelector.html +1 -1
  209. package/docs/variables/index.ListItemInnerContainerClassNameSelector.html +1 -1
  210. package/docs/variables/index.ModalContainerSelector.html +1 -1
  211. package/docs/variables/index.ModalRemovePaddingSelector.html +1 -1
  212. package/docs/variables/index.ModalTitleSelector.html +1 -1
  213. package/docs/variables/index.PopListSelector.html +1 -1
  214. package/docs/variables/index.PopOptionButtonSelector.html +1 -1
  215. package/docs/variables/index.PopOptionIconSelector.html +1 -1
  216. package/docs/variables/index.PopOverlaySelector.html +1 -1
  217. package/docs/variables/index.ProgressBackgroundSelector.html +1 -1
  218. package/docs/variables/index.ProgressValueSelector.html +1 -1
  219. package/docs/variables/index.SelectorItemSelector.html +1 -1
  220. package/docs/variables/index.StatsItemSelector.html +1 -1
  221. package/docs/variables/index.StatsLabelSelector.html +1 -1
  222. package/docs/variables/index.StatsSeparatorSelector.html +1 -1
  223. package/docs/variables/index.StatsValueSelector.html +1 -1
  224. package/docs/variables/index.TextAreaLabelSelector.html +1 -1
  225. package/docs/variables/index.TextAreaTextAreaSelector.html +1 -1
  226. package/docs/variables/index.TextAreaWrapperSelector.html +1 -1
  227. package/docs/variables/index.ToggleStyledToggleSelector.html +1 -1
  228. package/docs/variables/index.TooltipContentSelector.html +1 -0
  229. package/docs/variables/index.config.html +1 -1
  230. package/docs/variables/index.cssReset.html +2 -2
  231. package/docs/variables/index.darkTheme.html +1 -1
  232. package/docs/variables/index.miuiScrollbars.html +1 -1
  233. package/docs/variables/index.theme.html +1 -1
  234. package/esm/components/form/index.d.ts +1 -0
  235. package/esm/components/form/index.d.ts.map +1 -1
  236. package/esm/components/form/index.js +1 -0
  237. package/esm/components/form/index.js.map +1 -1
  238. package/esm/components/form/input/Input.d.ts.map +1 -1
  239. package/esm/components/form/input/Input.js +9 -5
  240. package/esm/components/form/input/Input.js.map +1 -1
  241. package/esm/components/form/timepicker/TimePicker.css.d.ts +99 -0
  242. package/esm/components/form/timepicker/TimePicker.css.d.ts.map +1 -0
  243. package/esm/components/form/timepicker/TimePicker.css.js +102 -0
  244. package/esm/components/form/timepicker/TimePicker.css.js.map +1 -0
  245. package/esm/components/form/timepicker/TimePicker.d.ts +22 -0
  246. package/esm/components/form/timepicker/TimePicker.d.ts.map +1 -0
  247. package/esm/components/form/timepicker/TimePicker.js +93 -0
  248. package/esm/components/form/timepicker/TimePicker.js.map +1 -0
  249. package/esm/components/form/timepicker/TimePicker.styled.d.ts +936 -0
  250. package/esm/components/form/timepicker/TimePicker.styled.d.ts.map +1 -0
  251. package/esm/components/form/timepicker/TimePicker.styled.js +20 -0
  252. package/esm/components/form/timepicker/TimePicker.styled.js.map +1 -0
  253. package/esm/components/form/timepicker/TimePickerModal.d.ts +17 -0
  254. package/esm/components/form/timepicker/TimePickerModal.d.ts.map +1 -0
  255. package/esm/components/form/timepicker/TimePickerModal.js +56 -0
  256. package/esm/components/form/timepicker/TimePickerModal.js.map +1 -0
  257. package/esm/components/form/timepicker/Wheel.d.ts +12 -0
  258. package/esm/components/form/timepicker/Wheel.d.ts.map +1 -0
  259. package/esm/components/form/timepicker/Wheel.js +151 -0
  260. package/esm/components/form/timepicker/Wheel.js.map +1 -0
  261. package/esm/components/form/timepicker/utils.d.ts +4 -0
  262. package/esm/components/form/timepicker/utils.d.ts.map +1 -0
  263. package/esm/components/form/timepicker/utils.js +58 -0
  264. package/esm/components/form/timepicker/utils.js.map +1 -0
  265. package/esm/components/icons/Clock.d.ts +7 -0
  266. package/esm/components/icons/Clock.d.ts.map +1 -0
  267. package/esm/components/icons/Clock.js +9 -0
  268. package/esm/components/icons/Clock.js.map +1 -0
  269. package/esm/components/icons/Icon.d.ts +2 -1
  270. package/esm/components/icons/Icon.d.ts.map +1 -1
  271. package/esm/components/icons/Icon.js +3 -0
  272. package/esm/components/icons/Icon.js.map +1 -1
  273. package/esm/components/ui/drawer/Drawer.d.ts +10 -1
  274. package/esm/components/ui/drawer/Drawer.d.ts.map +1 -1
  275. package/esm/components/ui/drawer/Drawer.js +139 -15
  276. package/esm/components/ui/drawer/Drawer.js.map +1 -1
  277. package/esm/components/ui/drawer/Drawer.styled.d.ts +86 -1
  278. package/esm/components/ui/drawer/Drawer.styled.d.ts.map +1 -1
  279. package/esm/components/ui/drawer/Drawer.styled.js +12 -1
  280. package/esm/components/ui/drawer/Drawer.styled.js.map +1 -1
  281. package/esm/components/ui/modal/Modal.d.ts +1 -2
  282. package/esm/components/ui/modal/Modal.d.ts.map +1 -1
  283. package/esm/components/ui/modal/Modal.js +103 -43
  284. package/esm/components/ui/modal/Modal.js.map +1 -1
  285. package/esm/components/ui/modal/Modal.styled.d.ts +1 -1
  286. package/esm/components/ui/modal/Modal.styled.d.ts.map +1 -1
  287. package/esm/components/ui/modal/Modal.styled.js +40 -25
  288. package/esm/components/ui/modal/Modal.styled.js.map +1 -1
  289. package/esm/components/ui/toaster/Toaster.d.ts.map +1 -1
  290. package/esm/components/ui/toaster/Toaster.js +8 -2
  291. package/esm/components/ui/toaster/Toaster.js.map +1 -1
  292. package/esm/components/ui/tooltip/Tooltip.d.ts +30 -0
  293. package/esm/components/ui/tooltip/Tooltip.d.ts.map +1 -0
  294. package/esm/components/ui/tooltip/Tooltip.js +43 -0
  295. package/esm/components/ui/tooltip/Tooltip.js.map +1 -0
  296. package/esm/components/ui/tooltip/Tooltip.styled.d.ts +173 -0
  297. package/esm/components/ui/tooltip/Tooltip.styled.d.ts.map +1 -0
  298. package/esm/components/ui/tooltip/Tooltip.styled.js +28 -0
  299. package/esm/components/ui/tooltip/Tooltip.styled.js.map +1 -0
  300. package/esm/index.d.ts +1 -0
  301. package/esm/index.d.ts.map +1 -1
  302. package/esm/index.js +1 -0
  303. package/esm/index.js.map +1 -1
  304. package/esm/theme.css-global.d.ts.map +1 -1
  305. package/esm/theme.css-global.js +0 -1
  306. package/esm/theme.css-global.js.map +1 -1
  307. package/esm/utils/index.d.ts +1 -0
  308. package/esm/utils/index.d.ts.map +1 -1
  309. package/esm/utils/index.js +1 -0
  310. package/esm/utils/index.js.map +1 -1
  311. package/esm/utils/useNativeValidity.d.ts +11 -0
  312. package/esm/utils/useNativeValidity.d.ts.map +1 -0
  313. package/esm/utils/useNativeValidity.js +29 -0
  314. package/esm/utils/useNativeValidity.js.map +1 -0
  315. package/package.json +2 -1
  316. package/pnpm-workspace.yaml +3 -0
  317. package/src/bugfixes/ToastsFromModal.stories.tsx +59 -0
  318. package/src/components/form/index.ts +1 -0
  319. package/src/components/form/input/Input.stories.tsx +47 -1
  320. package/src/components/form/input/Input.tsx +11 -5
  321. package/src/components/form/timepicker/TimePicker.css.ts +132 -0
  322. package/src/components/form/timepicker/TimePicker.stories.tsx +107 -0
  323. package/src/components/form/timepicker/TimePicker.styled.ts +52 -0
  324. package/src/components/form/timepicker/TimePicker.tsx +229 -0
  325. package/src/components/form/timepicker/TimePickerModal.tsx +131 -0
  326. package/src/components/form/timepicker/Wheel.tsx +201 -0
  327. package/src/components/form/timepicker/utils.ts +66 -0
  328. package/src/components/icons/Clock.tsx +38 -0
  329. package/src/components/icons/Icon.tsx +3 -0
  330. package/src/components/ui/drawer/Drawer.stories.tsx +143 -59
  331. package/src/components/ui/drawer/Drawer.styled.ts +13 -0
  332. package/src/components/ui/drawer/Drawer.tsx +214 -20
  333. package/src/components/ui/modal/Modal.stories.tsx +43 -7
  334. package/src/components/ui/modal/Modal.styled.ts +46 -25
  335. package/src/components/ui/modal/Modal.tsx +135 -52
  336. package/src/components/ui/toaster/Toaster.tsx +12 -2
  337. package/src/components/ui/tooltip/Tooltip.stories.tsx +285 -0
  338. package/src/components/ui/tooltip/Tooltip.styled.ts +36 -0
  339. package/src/components/ui/tooltip/Tooltip.tsx +195 -0
  340. package/src/index.ts +1 -0
  341. package/src/theme.css-global.ts +0 -1
  342. package/src/utils/index.ts +1 -0
  343. package/src/utils/useNativeValidity.ts +57 -0
@@ -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 {
@@ -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>
@@ -1,4 +1,5 @@
1
- import React, { useCallback } from "react";
1
+ import React, { useCallback, useEffect, useState } from "react";
2
+ import { createPortal } from "react-dom";
2
3
 
3
4
  import { toast, Toaster as SonnerToaster } from "sonner";
4
5
 
@@ -9,10 +10,19 @@ interface ToasterProviderProps extends SonnerToasterProps {
9
10
  }
10
11
 
11
12
  const ToasterProvider: React.FC<ToasterProviderProps> = ({ children, position = "bottom-center", ...rest }) => {
13
+ const [body, setBody] = useState<HTMLElement | null>(null);
14
+
15
+ useEffect(() => {
16
+ // eslint-disable-next-line react-hooks/set-state-in-effect
17
+ setBody(document.body);
18
+ }, []);
19
+
20
+ const toaster = <SonnerToaster position={position} {...rest} />;
21
+
12
22
  return (
13
23
  <>
14
24
  {children}
15
- <SonnerToaster position={position} {...rest} />
25
+ {body ? createPortal(toaster, body) : null}
16
26
  </>
17
27
  );
18
28
  };
@@ -0,0 +1,285 @@
1
+ /* eslint-disable max-lines */
2
+ import React, { useCallback, useState } from "react";
3
+
4
+ import type { Meta, StoryObj } from "@storybook/react-vite";
5
+
6
+ import { Gap } from "../../utils/Gap";
7
+ import { Button } from "../button/Button";
8
+ import { Drawer } from "../drawer/Drawer";
9
+ import { Modal } from "../modal/Modal";
10
+ import { ModalButtons } from "../modal/ModalButtons";
11
+ import { Tooltip, TooltipProvider } from "./Tooltip";
12
+
13
+ const meta: Meta<typeof Tooltip> = {
14
+ title: "Components/UI/Tooltip",
15
+ component: Tooltip,
16
+ tags: ["autodocs", "ui"],
17
+ argTypes: {
18
+ side: {
19
+ control: "inline-radio",
20
+ options: ["top", "right", "bottom", "left"],
21
+ },
22
+ align: {
23
+ control: "inline-radio",
24
+ options: ["start", "center", "end"],
25
+ },
26
+ sideOffset: { control: { type: "number", min: 0, max: 40 } },
27
+ alignOffset: { control: { type: "number", min: -40, max: 40 } },
28
+ delayDuration: { control: { type: "number", min: 0, max: 2000, step: 100 } },
29
+ avoidCollisions: { type: "boolean" },
30
+ arrow: { type: "boolean" },
31
+ content: { control: "text" },
32
+ children: { table: { disable: true } },
33
+ open: { table: { disable: true } },
34
+ defaultOpen: { table: { disable: true } },
35
+ onOpenChange: { table: { disable: true } },
36
+ },
37
+ };
38
+
39
+ type Story = StoryObj<typeof Tooltip>;
40
+
41
+ /**
42
+ * Playground. Tweak `side`, `align`, `sideOffset`, `delayDuration`, `arrow`,
43
+ * and `avoidCollisions` from the controls panel.
44
+ */
45
+ const Primary: Story = {
46
+ args: {
47
+ content: "Save your changes",
48
+ side: "top",
49
+ align: "center",
50
+ sideOffset: 6,
51
+ alignOffset: 0,
52
+ delayDuration: 500,
53
+ avoidCollisions: true,
54
+ arrow: true,
55
+ },
56
+ render: (args) => (
57
+ <div style={{ padding: 80, display: "flex", justifyContent: "center" }}>
58
+ <Tooltip {...args}>
59
+ <Button>Hover me</Button>
60
+ </Tooltip>
61
+ </div>
62
+ ),
63
+ };
64
+
65
+ /**
66
+ * All four sides shown at once.
67
+ */
68
+ const AllSides: Story = {
69
+ render: () => (
70
+ <div
71
+ style={{
72
+ padding: 120,
73
+ display: "grid",
74
+ gridTemplateColumns: "repeat(2, max-content)",
75
+ gap: 40,
76
+ justifyContent: "center",
77
+ }}
78
+ >
79
+ <Tooltip content={"Top"} side={"top"} arrow={true}>
80
+ <Button>Top</Button>
81
+ </Tooltip>
82
+ <Tooltip content={"Right"} side={"right"} arrow={true}>
83
+ <Button>Right</Button>
84
+ </Tooltip>
85
+ <Tooltip content={"Bottom"} side={"bottom"} arrow={true}>
86
+ <Button>Bottom</Button>
87
+ </Tooltip>
88
+ <Tooltip content={"Left"} side={"left"} arrow={true}>
89
+ <Button>Left</Button>
90
+ </Tooltip>
91
+ </div>
92
+ ),
93
+ };
94
+
95
+ /**
96
+ * `TooltipProvider` enables group-delay behavior: once any tooltip is open, the next ones
97
+ * appear without waiting for `delayDuration` (until `skipDelayDuration` elapses with all
98
+ * tooltips closed). Hover one button slowly, then sweep across the rest — they pop instantly.
99
+ */
100
+ const GroupDelay: Story = {
101
+ render: () => (
102
+ <TooltipProvider delayDuration={500} skipDelayDuration={400}>
103
+ <div style={{ padding: 80, display: "flex", gap: 12, justifyContent: "center" }}>
104
+ <Tooltip content={"First"}>
105
+ <Button>One</Button>
106
+ </Tooltip>
107
+ <Tooltip content={"Second"}>
108
+ <Button>Two</Button>
109
+ </Tooltip>
110
+ <Tooltip content={"Third"}>
111
+ <Button>Three</Button>
112
+ </Tooltip>
113
+ <Tooltip content={"Fourth"}>
114
+ <Button>Four</Button>
115
+ </Tooltip>
116
+ </div>
117
+ </TooltipProvider>
118
+ ),
119
+ };
120
+
121
+ /**
122
+ * Triggers placed near viewport edges. With `avoidCollisions` (default) the tooltip
123
+ * flips to fit; toggle the arg in the playground story to see the difference.
124
+ */
125
+ const NearViewportEdge: Story = {
126
+ render: () => (
127
+ <div style={{ height: "90vh", position: "relative" }}>
128
+ <div style={{ position: "absolute", top: 4, left: 4 }}>
129
+ <Tooltip content={"I would overflow up-left, so I flip"} side={"top"} arrow={true}>
130
+ <Button>Top-left corner</Button>
131
+ </Tooltip>
132
+ </div>
133
+ <div style={{ position: "absolute", top: 4, right: 4 }}>
134
+ <Tooltip content={"I would overflow up-right, so I flip"} side={"top"} arrow={true}>
135
+ <Button>Top-right corner</Button>
136
+ </Tooltip>
137
+ </div>
138
+ <div style={{ position: "absolute", bottom: 4, left: 4 }}>
139
+ <Tooltip content={"I would overflow down-left"} side={"bottom"} arrow={true}>
140
+ <Button>Bottom-left corner</Button>
141
+ </Tooltip>
142
+ </div>
143
+ <div style={{ position: "absolute", bottom: 4, right: 4 }}>
144
+ <Tooltip content={"I would overflow down-right"} side={"bottom"} arrow={true}>
145
+ <Button>Bottom-right corner</Button>
146
+ </Tooltip>
147
+ </div>
148
+ </div>
149
+ ),
150
+ };
151
+
152
+ /**
153
+ * Long content wraps to the configured `maxWidth`.
154
+ */
155
+ const LongContent: Story = {
156
+ render: () => (
157
+ <div style={{ padding: 80, display: "flex", justifyContent: "center" }}>
158
+ <Tooltip
159
+ content={"This tooltip has a longer description that wraps onto multiple lines once it "
160
+ + "exceeds the maximum content width set in the styles."}
161
+ side={"bottom"}
162
+ >
163
+ <Button>Long description</Button>
164
+ </Tooltip>
165
+ </div>
166
+ ),
167
+ };
168
+
169
+ /**
170
+ * Edge case: tooltip rendered from inside a `Modal`. Because the content is portalled to
171
+ * `document.body`, it escapes the modal's stacking context and renders above it.
172
+ */
173
+ const InModal: Story = {
174
+ render: () => {
175
+ const [open, setOpen] = useState(false);
176
+
177
+ const handleOpen = useCallback(() => { setOpen(true); }, []);
178
+ const handleClose = useCallback(() => { setOpen(false); }, []);
179
+
180
+ return (
181
+ <div>
182
+ <Button onClick={handleOpen}>Open modal</Button>
183
+ <Modal isOpen={open} onClose={handleClose} title={"Tooltip in a modal"}>
184
+ <Gap>
185
+ <div>Hover the buttons below — the tooltip should appear above the modal.</div>
186
+ <div style={{ display: "flex", gap: 12 }}>
187
+ <Tooltip content={"Above modal, side=top"} side={"top"} arrow={true}>
188
+ <Button>Top</Button>
189
+ </Tooltip>
190
+ <Tooltip content={"Above modal, side=right"} side={"right"} arrow={true}>
191
+ <Button>Right</Button>
192
+ </Tooltip>
193
+ <Tooltip content={"Above modal, side=bottom"} side={"bottom"} arrow={true}>
194
+ <Button>Bottom</Button>
195
+ </Tooltip>
196
+ </div>
197
+ </Gap>
198
+ <ModalButtons>
199
+ <ModalButtons.Button variant={"main"} onClick={handleClose}>Close</ModalButtons.Button>
200
+ </ModalButtons>
201
+ </Modal>
202
+ </div>
203
+ );
204
+ },
205
+ };
206
+
207
+ /**
208
+ * Edge case: tooltip rendered from inside a `Drawer`. Same portal behavior — content
209
+ * floats above the drawer regardless of the drawer's stacking context.
210
+ */
211
+ const InDrawer: Story = {
212
+ render: () => {
213
+ const [open, setOpen] = useState(false);
214
+
215
+ const handleOpen = useCallback(() => { setOpen(true); }, []);
216
+ const handleClose = useCallback(() => { setOpen(false); }, []);
217
+
218
+ return (
219
+ <div>
220
+ <Button onClick={handleOpen}>Open drawer</Button>
221
+ <Drawer isOpen={open} onClose={handleClose}>
222
+ <div style={{ padding: 24, display: "flex", flexDirection: "column", gap: 16 }}>
223
+ <div>Tooltips work from inside a drawer.</div>
224
+ <Tooltip content={"Hello from inside the drawer"} side={"bottom"} arrow={true}>
225
+ <Button>Hover me</Button>
226
+ </Tooltip>
227
+ <Button onClick={handleClose}>Close drawer</Button>
228
+ </div>
229
+ </Drawer>
230
+ </div>
231
+ );
232
+ },
233
+ };
234
+
235
+ /**
236
+ * Controlled tooltip — open state is driven by parent. Useful for tutorials,
237
+ * onboarding tours, or pinning a tooltip open while a user interacts elsewhere.
238
+ */
239
+ const Controlled: Story = {
240
+ render: () => {
241
+ const [open, setOpen] = useState(false);
242
+
243
+ const handleToggle = useCallback(() => { setOpen(p => !p); }, []);
244
+
245
+ return (
246
+ <div style={{ padding: 80, display: "flex", gap: 16, justifyContent: "center" }}>
247
+ <Button onClick={handleToggle}>{open ? "Hide" : "Show"} tooltip</Button>
248
+ <Tooltip content={"I'm controlled"} open={open} side={"right"}>
249
+ <Button>Target</Button>
250
+ </Tooltip>
251
+ </div>
252
+ );
253
+ },
254
+ };
255
+
256
+ /**
257
+ * Tooltip wrapping a non-button trigger. Any element that can receive a ref works
258
+ * — here, an inline word with a dotted underline.
259
+ */
260
+ const InlineTrigger: Story = {
261
+ render: () => (
262
+ <div style={{ padding: 80, fontSize: 18, lineHeight: 1.6, maxWidth: 500, margin: "0 auto" }}>
263
+ This sentence contains a{" "}
264
+ <Tooltip content={"A short, contextual explanation."}>
265
+ <span style={{ borderBottom: "1px dotted currentColor", cursor: "help" }}>
266
+ glossed term
267
+ </span>
268
+ </Tooltip>
269
+ {" "}that reveals more on hover or focus.
270
+ </div>
271
+ ),
272
+ };
273
+
274
+ export default meta;
275
+ export {
276
+ Primary,
277
+ AllSides,
278
+ GroupDelay,
279
+ NearViewportEdge,
280
+ LongContent,
281
+ InModal,
282
+ InDrawer,
283
+ Controlled,
284
+ InlineTrigger,
285
+ };