@astryxdesign/core 0.4.6 → 0.4.7-canary.20bae6c

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 (377) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/AppShell/AppShell.d.ts.map +1 -1
  3. package/dist/AppShell/AppShell.js +3 -2
  4. package/dist/AspectRatio/AspectRatio.d.ts +24 -0
  5. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  6. package/dist/AspectRatio/AspectRatio.js +23 -4
  7. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  8. package/dist/AvatarGroup/AvatarGroup.js +3 -2
  9. package/dist/BottomSheet/BottomSheet.d.ts +1 -0
  10. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  11. package/dist/BottomSheet/BottomSheet.js +54 -15
  12. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +6 -0
  13. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -0
  14. package/dist/BottomSheet/BottomSheetEdgeTint.js +62 -0
  15. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  16. package/dist/BottomSheet/BottomSheetPanel.js +1 -0
  17. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +1 -0
  18. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  19. package/dist/BottomSheet/BottomSheetSwitcher.js +13 -6
  20. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  21. package/dist/BottomSheet/useSheetGestures.js +23 -5
  22. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  23. package/dist/Breadcrumbs/BreadcrumbItem.js +11 -10
  24. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
  25. package/dist/ButtonGroup/ButtonGroup.js +12 -4
  26. package/dist/Calendar/Calendar.d.ts +3 -1
  27. package/dist/Calendar/Calendar.d.ts.map +1 -1
  28. package/dist/Calendar/Calendar.js +13 -14
  29. package/dist/Calendar/getInitialFocusDate.d.ts +46 -0
  30. package/dist/Calendar/getInitialFocusDate.d.ts.map +1 -0
  31. package/dist/Calendar/getInitialFocusDate.js +62 -0
  32. package/dist/Carousel/Carousel.d.ts.map +1 -1
  33. package/dist/Carousel/Carousel.js +3 -3
  34. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  35. package/dist/Chat/ChatLayout.js +3 -3
  36. package/dist/Chat/ChatMessageList.d.ts +22 -3
  37. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  38. package/dist/Chat/ChatMessageList.js +6 -3
  39. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  40. package/dist/CheckboxInput/CheckboxInput.js +3 -2
  41. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  42. package/dist/ClickableCard/ClickableCard.js +3 -3
  43. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  44. package/dist/CommandPalette/CommandPaletteInput.js +3 -2
  45. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  46. package/dist/CommandPalette/CommandPaletteItem.js +3 -2
  47. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  48. package/dist/ContextMenu/ContextMenu.js +4 -3
  49. package/dist/DateInput/DateInput.d.ts.map +1 -1
  50. package/dist/DateInput/DateInput.js +3 -2
  51. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  52. package/dist/DateInput/TouchDateField.js +34 -1
  53. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  54. package/dist/DateTimeInput/DateTimeInput.js +3 -2
  55. package/dist/Dialog/Dialog.d.ts +7 -1
  56. package/dist/Dialog/Dialog.d.ts.map +1 -1
  57. package/dist/Dialog/Dialog.js +89 -58
  58. package/dist/FileInput/FileInput.d.ts.map +1 -1
  59. package/dist/FileInput/FileInput.js +3 -2
  60. package/dist/Heading/Heading.d.ts.map +1 -1
  61. package/dist/Heading/Heading.js +6 -2
  62. package/dist/HoverCard/HoverCard.d.ts +6 -0
  63. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  64. package/dist/HoverCard/useHoverCard.d.ts +5 -0
  65. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  66. package/dist/HoverCard/useHoverCard.js +67 -25
  67. package/dist/Item/Item.d.ts.map +1 -1
  68. package/dist/Item/Item.js +4 -2
  69. package/dist/Kbd/Kbd.d.ts.map +1 -1
  70. package/dist/Kbd/Kbd.js +10 -3
  71. package/dist/Layer/LayerDepthContext.d.ts +52 -0
  72. package/dist/Layer/LayerDepthContext.d.ts.map +1 -0
  73. package/dist/Layer/LayerDepthContext.js +66 -0
  74. package/dist/Layer/index.d.ts +3 -0
  75. package/dist/Layer/index.d.ts.map +1 -1
  76. package/dist/Layer/index.js +4 -0
  77. package/dist/Layer/layerStack.d.ts +98 -0
  78. package/dist/Layer/layerStack.d.ts.map +1 -0
  79. package/dist/Layer/layerStack.js +306 -0
  80. package/dist/Layer/useLayerDismissal.d.ts +93 -0
  81. package/dist/Layer/useLayerDismissal.d.ts.map +1 -0
  82. package/dist/Layer/useLayerDismissal.js +91 -0
  83. package/dist/Lightbox/Lightbox.d.ts +1 -0
  84. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  85. package/dist/Lightbox/Lightbox.js +103 -81
  86. package/dist/Markdown/index.d.ts +1 -1
  87. package/dist/Markdown/index.d.ts.map +1 -1
  88. package/dist/Markdown/parser.d.ts +38 -1
  89. package/dist/Markdown/parser.d.ts.map +1 -1
  90. package/dist/Markdown/parser.js +149 -23
  91. package/dist/MobileNav/MobileNav.d.ts +6 -1
  92. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  93. package/dist/MobileNav/MobileNav.js +61 -36
  94. package/dist/MultiSelector/MultiSelector.d.ts +15 -1
  95. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  96. package/dist/MultiSelector/MultiSelector.js +17 -6
  97. package/dist/MultiSelector/index.d.ts +1 -1
  98. package/dist/MultiSelector/index.d.ts.map +1 -1
  99. package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
  100. package/dist/NavMenu/NavHeadingMenu.js +3 -2
  101. package/dist/Outline/Outline.d.ts.map +1 -1
  102. package/dist/Outline/Outline.js +3 -2
  103. package/dist/OverflowList/OverflowList.d.ts.map +1 -1
  104. package/dist/OverflowList/OverflowList.js +3 -2
  105. package/dist/Overlay/Overlay.d.ts.map +1 -1
  106. package/dist/Overlay/Overlay.js +3 -2
  107. package/dist/Popover/usePopover.d.ts.map +1 -1
  108. package/dist/Popover/usePopover.js +20 -17
  109. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  110. package/dist/PowerSearch/PowerSearch.js +3 -4
  111. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  112. package/dist/Resizable/ResizeHandle.js +3 -2
  113. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  114. package/dist/SegmentedControl/SegmentedControl.js +3 -2
  115. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  116. package/dist/SelectableCard/SelectableCard.js +3 -3
  117. package/dist/Selector/Selector.d.ts.map +1 -1
  118. package/dist/Selector/Selector.js +5 -0
  119. package/dist/SideNav/SideNav.d.ts.map +1 -1
  120. package/dist/SideNav/SideNav.js +4 -3
  121. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  122. package/dist/SideNav/SideNavItem.js +4 -3
  123. package/dist/Slider/Slider.d.ts.map +1 -1
  124. package/dist/Slider/Slider.js +3 -3
  125. package/dist/Stepper/Step.d.ts +140 -0
  126. package/dist/Stepper/Step.d.ts.map +1 -0
  127. package/dist/Stepper/Step.js +1045 -0
  128. package/dist/Stepper/StepStatus.d.ts +24 -0
  129. package/dist/Stepper/StepStatus.d.ts.map +1 -0
  130. package/dist/Stepper/StepStatus.js +1 -0
  131. package/dist/Stepper/Stepper.d.ts +93 -0
  132. package/dist/Stepper/Stepper.d.ts.map +1 -0
  133. package/dist/Stepper/Stepper.js +184 -0
  134. package/dist/Stepper/StepperContext.d.ts +41 -0
  135. package/dist/Stepper/StepperContext.d.ts.map +1 -0
  136. package/dist/Stepper/StepperContext.js +34 -0
  137. package/dist/Stepper/index.d.ts +8 -0
  138. package/dist/Stepper/index.d.ts.map +1 -0
  139. package/dist/Stepper/index.js +7 -0
  140. package/dist/Stepper/stepper.stylex.d.ts +15 -0
  141. package/dist/Stepper/stepper.stylex.d.ts.map +1 -0
  142. package/dist/Stepper/stepper.stylex.js +20 -0
  143. package/dist/Switch/Switch.d.ts.map +1 -1
  144. package/dist/Switch/Switch.js +3 -2
  145. package/dist/TabList/Tab.d.ts.map +1 -1
  146. package/dist/TabList/Tab.js +5 -1
  147. package/dist/TabList/TabList.d.ts.map +1 -1
  148. package/dist/TabList/TabList.js +3 -3
  149. package/dist/Table/BaseTable.d.ts.map +1 -1
  150. package/dist/Table/BaseTable.js +4 -1
  151. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  152. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +20 -8
  153. package/dist/Table/plugins/pagination/useTablePagination.d.ts +3 -0
  154. package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
  155. package/dist/Table/plugins/pagination/useTablePagination.js +21 -4
  156. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  157. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  158. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -1
  159. package/dist/Table/plugins/rowStatus/useTableRowStatus.js +10 -3
  160. package/dist/Table/plugins/selection/useTableSelection.d.ts +16 -0
  161. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  162. package/dist/Table/plugins/selection/useTableSelection.js +19 -5
  163. package/dist/Table/types.d.ts +22 -4
  164. package/dist/Table/types.d.ts.map +1 -1
  165. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  166. package/dist/Table/useBaseTablePlugins.js +5 -0
  167. package/dist/Text/Text.d.ts.map +1 -1
  168. package/dist/Text/Text.js +6 -2
  169. package/dist/TextArea/TextArea.d.ts.map +1 -1
  170. package/dist/TextArea/TextArea.js +3 -2
  171. package/dist/TextInput/TextInput.d.ts.map +1 -1
  172. package/dist/TextInput/TextInput.js +3 -2
  173. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  174. package/dist/TimeInput/TimeInput.js +3 -2
  175. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  176. package/dist/Timestamp/Timestamp.js +5 -3
  177. package/dist/Token/TokenLink.d.ts.map +1 -1
  178. package/dist/Token/TokenLink.js +2 -2
  179. package/dist/Tooltip/Tooltip.d.ts +5 -0
  180. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  181. package/dist/Tooltip/useTooltip.d.ts +5 -0
  182. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  183. package/dist/Tooltip/useTooltip.js +39 -24
  184. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  185. package/dist/TopNav/TopNavMegaMenu.js +3 -3
  186. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  187. package/dist/Typeahead/BaseTypeahead.js +3 -3
  188. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  189. package/dist/Typeahead/Typeahead.js +3 -2
  190. package/dist/astryx.css +43 -1
  191. package/dist/hooks/index.d.ts +1 -0
  192. package/dist/hooks/index.d.ts.map +1 -1
  193. package/dist/hooks/index.js +1 -0
  194. package/dist/hooks/useFocusTrap.d.ts +7 -4
  195. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  196. package/dist/hooks/useFocusTrap.js +52 -91
  197. package/dist/hooks/useHotkeys.d.ts.map +1 -1
  198. package/dist/hooks/useHotkeys.js +10 -3
  199. package/dist/hooks/useListFocus.d.ts +5 -2
  200. package/dist/hooks/useListFocus.d.ts.map +1 -1
  201. package/dist/hooks/useListFocus.js +12 -6
  202. package/dist/hooks/useMergedRefs.d.ts +18 -0
  203. package/dist/hooks/useMergedRefs.d.ts.map +1 -0
  204. package/dist/hooks/useMergedRefs.js +25 -0
  205. package/dist/index.d.ts +1 -0
  206. package/dist/index.d.ts.map +1 -1
  207. package/dist/index.js +1 -0
  208. package/dist/utils/timeParser.d.ts +1 -1
  209. package/dist/utils/timeParser.d.ts.map +1 -1
  210. package/dist/utils/timeParser.js +26 -12
  211. package/locales/en.json +27 -7
  212. package/locales/pseudo.json +15 -0
  213. package/package.json +8 -3
  214. package/src/AppShell/AppShell.tsx +3 -2
  215. package/src/AspectRatio/AspectRatio.doc.mjs +6 -3
  216. package/src/AspectRatio/AspectRatio.test.tsx +57 -10
  217. package/src/AspectRatio/AspectRatio.tsx +40 -1
  218. package/src/Avatar/Avatar.doc.mjs +2 -1
  219. package/src/AvatarGroup/AvatarGroup.tsx +3 -2
  220. package/src/BottomSheet/BottomSheet.test.tsx +123 -0
  221. package/src/BottomSheet/BottomSheet.tsx +36 -4
  222. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +225 -0
  223. package/src/BottomSheet/BottomSheetEdgeTint.tsx +82 -0
  224. package/src/BottomSheet/BottomSheetPanel.test.tsx +66 -0
  225. package/src/BottomSheet/BottomSheetPanel.tsx +19 -0
  226. package/src/BottomSheet/BottomSheetSwitcher.tsx +16 -2
  227. package/src/BottomSheet/useSheetGestures.test.ts +27 -0
  228. package/src/BottomSheet/useSheetGestures.ts +25 -5
  229. package/src/Breadcrumbs/BreadcrumbItem.tsx +9 -5
  230. package/src/Button/Button.doc.mjs +22 -0
  231. package/src/ButtonGroup/ButtonGroup.doc.mjs +7 -1
  232. package/src/ButtonGroup/ButtonGroup.test.tsx +185 -1
  233. package/src/ButtonGroup/ButtonGroup.tsx +11 -3
  234. package/src/Calendar/Calendar.doc.mjs +4 -3
  235. package/src/Calendar/Calendar.test.tsx +52 -0
  236. package/src/Calendar/Calendar.tsx +18 -15
  237. package/src/Calendar/getInitialFocusDate.test.ts +94 -0
  238. package/src/Calendar/getInitialFocusDate.ts +93 -0
  239. package/src/Carousel/Carousel.tsx +3 -2
  240. package/src/Chat/ChatLayout.tsx +3 -2
  241. package/src/Chat/ChatMessageList.doc.mjs +9 -1
  242. package/src/Chat/ChatMessageList.test.tsx +46 -0
  243. package/src/Chat/ChatMessageList.tsx +28 -4
  244. package/src/CheckboxInput/CheckboxInput.tsx +3 -2
  245. package/src/ClickableCard/ClickableCard.tsx +3 -2
  246. package/src/CodeBlock/CodeBlock.doc.mjs +6 -0
  247. package/src/CommandPalette/CommandPaletteInput.tsx +3 -2
  248. package/src/CommandPalette/CommandPaletteItem.tsx +3 -2
  249. package/src/ContextMenu/ContextMenu.doc.mjs +5 -0
  250. package/src/ContextMenu/ContextMenu.tsx +4 -3
  251. package/src/DateInput/DateInput.tsx +3 -2
  252. package/src/DateInput/DateInputTouch.test.tsx +36 -0
  253. package/src/DateInput/TouchDateField.tsx +35 -1
  254. package/src/DateTimeInput/DateTimeInput.tsx +2 -2
  255. package/src/Dialog/Dialog.doc.mjs +8 -3
  256. package/src/Dialog/Dialog.test.tsx +261 -1
  257. package/src/Dialog/Dialog.tsx +109 -58
  258. package/src/DropdownMenu/DropdownMenu.doc.mjs +6 -0
  259. package/src/Field/Field.doc.mjs +13 -2
  260. package/src/FileInput/FileInput.tsx +3 -2
  261. package/src/Heading/Heading.test.tsx +23 -1
  262. package/src/Heading/Heading.tsx +6 -2
  263. package/src/HoverCard/HoverCard.doc.mjs +5 -0
  264. package/src/HoverCard/HoverCard.tsx +6 -0
  265. package/src/HoverCard/useHoverCard.tsx +81 -30
  266. package/src/Item/Item.tsx +5 -4
  267. package/src/Kbd/Kbd.test.tsx +17 -0
  268. package/src/Kbd/Kbd.tsx +10 -3
  269. package/src/Layer/LayerDepthContext.tsx +62 -0
  270. package/src/Layer/index.ts +9 -0
  271. package/src/Layer/layerDismissalFamilies.test.tsx +359 -0
  272. package/src/Layer/layerDismissalInvariants.test.tsx +417 -0
  273. package/src/Layer/layerStack.ts +362 -0
  274. package/src/Layer/useLayerDismissal.test.tsx +469 -0
  275. package/src/Layer/useLayerDismissal.ts +173 -0
  276. package/src/Layout/Layout.doc.mjs +2 -2
  277. package/src/Lightbox/Lightbox.doc.mjs +12 -0
  278. package/src/Lightbox/Lightbox.tsx +124 -102
  279. package/src/Link/Link.doc.mjs +39 -0
  280. package/src/Markdown/index.ts +1 -0
  281. package/src/Markdown/parser.perf.test.ts +71 -1
  282. package/src/Markdown/parser.test.ts +145 -2
  283. package/src/Markdown/parser.ts +208 -22
  284. package/src/MobileNav/MobileNav.tsx +62 -36
  285. package/src/MoreMenu/MoreMenu.doc.mjs +7 -1
  286. package/src/MultiSelector/MultiSelector.doc.mjs +27 -0
  287. package/src/MultiSelector/MultiSelector.test.tsx +105 -0
  288. package/src/MultiSelector/MultiSelector.tsx +50 -6
  289. package/src/MultiSelector/index.ts +1 -0
  290. package/src/NavMenu/NavHeadingMenu.tsx +3 -2
  291. package/src/NumberInput/NumberInput.doc.mjs +5 -0
  292. package/src/Outline/Outline.tsx +3 -2
  293. package/src/OverflowList/OverflowList.tsx +3 -2
  294. package/src/Overlay/Overlay.tsx +3 -2
  295. package/src/Popover/Popover.test.tsx +27 -1
  296. package/src/Popover/usePopover.tsx +31 -28
  297. package/src/PowerSearch/PowerSearch.doc.mjs +26 -0
  298. package/src/PowerSearch/PowerSearch.tsx +5 -2
  299. package/src/Resizable/ResizeHandle.tsx +3 -2
  300. package/src/SegmentedControl/SegmentedControl.tsx +3 -2
  301. package/src/SelectableCard/SelectableCard.tsx +3 -2
  302. package/src/Selector/Selector.doc.mjs +12 -1
  303. package/src/Selector/Selector.test.tsx +21 -0
  304. package/src/Selector/Selector.tsx +11 -1
  305. package/src/SideNav/SideNav.doc.mjs +6 -6
  306. package/src/SideNav/SideNav.tsx +4 -2
  307. package/src/SideNav/SideNavItem.tsx +4 -2
  308. package/src/Slider/Slider.tsx +3 -2
  309. package/src/Stepper/Step.doc.mjs +98 -0
  310. package/src/Stepper/Step.tsx +1638 -0
  311. package/src/Stepper/StepStatus.ts +26 -0
  312. package/src/Stepper/Stepper.doc.mjs +370 -0
  313. package/src/Stepper/Stepper.test.tsx +1024 -0
  314. package/src/Stepper/Stepper.tsx +258 -0
  315. package/src/Stepper/StepperContext.ts +71 -0
  316. package/src/Stepper/index.ts +18 -0
  317. package/src/Stepper/stepper.stylex.ts +19 -0
  318. package/src/Switch/Switch.tsx +3 -2
  319. package/src/TabList/Tab.tsx +5 -1
  320. package/src/TabList/TabList.test.tsx +21 -4
  321. package/src/TabList/TabList.tsx +3 -2
  322. package/src/Table/BaseTable.tsx +6 -3
  323. package/src/Table/Table.test.tsx +35 -0
  324. package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +112 -0
  325. package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +100 -0
  326. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +17 -8
  327. package/src/Table/plugins/pagination/useTablePagination.test.tsx +34 -4
  328. package/src/Table/plugins/pagination/useTablePagination.tsx +35 -3
  329. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +9 -2
  330. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +13 -7
  331. package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +12 -3
  332. package/src/Table/plugins/selection/useTableSelection.test.tsx +76 -0
  333. package/src/Table/plugins/selection/useTableSelection.tsx +40 -7
  334. package/src/Table/types.ts +22 -4
  335. package/src/Table/useBaseTablePlugins.ts +5 -0
  336. package/src/Table/useTableGroupedRows.doc.mjs +5 -4
  337. package/src/Table/useTablePagination.doc.mjs +7 -0
  338. package/src/Table/useTableRowStatus.doc.mjs +1 -1
  339. package/src/Table/useTableSelection.doc.mjs +31 -0
  340. package/src/Text/Text.test.tsx +23 -1
  341. package/src/Text/Text.tsx +6 -2
  342. package/src/TextArea/TextArea.tsx +3 -2
  343. package/src/TextInput/TextInput.doc.mjs +10 -0
  344. package/src/TextInput/TextInput.tsx +3 -2
  345. package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
  346. package/src/TimeInput/TimeInput.tsx +2 -2
  347. package/src/Timestamp/Timestamp.tsx +4 -2
  348. package/src/Toast/Toast.doc.mjs +6 -0
  349. package/src/Token/TokenLink.tsx +2 -2
  350. package/src/Tokenizer/Tokenizer.doc.mjs +16 -0
  351. package/src/Toolbar/Toolbar.doc.mjs +5 -0
  352. package/src/Tooltip/Tooltip.doc.mjs +5 -0
  353. package/src/Tooltip/Tooltip.test.tsx +32 -0
  354. package/src/Tooltip/Tooltip.tsx +5 -0
  355. package/src/Tooltip/useTooltip.tsx +47 -24
  356. package/src/TopNav/TopNavMegaMenu.tsx +3 -2
  357. package/src/Typeahead/BaseTypeahead.tsx +3 -2
  358. package/src/Typeahead/Typeahead.doc.mjs +5 -0
  359. package/src/Typeahead/Typeahead.tsx +3 -2
  360. package/src/__tests__/complexComponentContract.test.tsx +81 -0
  361. package/src/hooks/index.ts +2 -0
  362. package/src/hooks/useContainerReveal.doc.mjs +2 -2
  363. package/src/hooks/useFocusTrap.ts +54 -107
  364. package/src/hooks/useFocusTrapEscapeShim.test.tsx +165 -0
  365. package/src/hooks/useHotkeys.test.ts +18 -0
  366. package/src/hooks/useHotkeys.ts +10 -3
  367. package/src/hooks/useListFocus.doc.mjs +2 -2
  368. package/src/hooks/useListFocus.test.tsx +65 -3
  369. package/src/hooks/useListFocus.ts +15 -7
  370. package/src/hooks/useMergedRefs.doc.mjs +79 -0
  371. package/src/hooks/useMergedRefs.test.tsx +62 -0
  372. package/src/hooks/useMergedRefs.ts +36 -0
  373. package/src/index.ts +1 -0
  374. package/src/theme/MediaTheme.doc.mjs +5 -5
  375. package/src/utils/timeParser.test.ts +6 -1
  376. package/src/utils/timeParser.ts +35 -13
  377. package/src/__tests__/inputWidthContract.test.tsx +0 -69
@@ -50,7 +50,7 @@ import {AppShellMobileContext} from './AppShellMobileContext';
50
50
  import type {AppShellMobileContextValue} from './AppShellMobileContext';
51
51
  import type {SpacingStep} from '../utils/types';
52
52
  import type {BaseProps} from '../BaseProps';
53
- import {mergeProps, mergeRefs, isRenderable} from '../utils';
53
+ import {mergeProps, isRenderable} from '../utils';
54
54
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
55
55
  import {useMediaQuery} from '../hooks/useMediaQuery';
56
56
  import {observeResize, unobserveResize} from '../utils/sharedResizeObserver';
@@ -58,6 +58,7 @@ import {themeProps} from '../utils/themeProps';
58
58
  import {useTranslator} from '../i18n';
59
59
  import type {AppShellVariantMap} from './index';
60
60
 
61
+ import {useMergedRefs} from '../hooks/useMergedRefs';
61
62
  const HasActivity = typeof React.Activity !== 'undefined';
62
63
  const ActivityWrapper = HasActivity
63
64
  ? ({
@@ -792,7 +793,7 @@ export function AppShell({
792
793
  <AppShellMobileContext value={mobileContextValue}>
793
794
  <div
794
795
  {...rest}
795
- ref={mergeRefs(ref, shellRef)}
796
+ ref={useMergedRefs(ref, shellRef)}
796
797
  data-testid={dataTestId}
797
798
  {...mergeProps(
798
799
  themeProps('app-shell', {variant}),
@@ -16,6 +16,7 @@ export const docs = {
16
16
  {guidance: true, description: 'Use `fit="cover"` for images and video so the component sizes the child; the child should not repeat `width`/`height`/`objectFit` styles.'},
17
17
  {guidance: true, description: 'Pass one child. With `fit` set, every direct child is stretched to fill the box, so put an overlay or caption inside a single wrapper child rather than passing it as a second child.'},
18
18
  {guidance: true, description: 'Describe media children with `alt`, or `alt=""` when the image is decorative. AspectRatio adds no role and no accessible name of its own, so the child carries the whole accessible description.'},
19
+ {guidance: true, description: 'For a breakpoint-dependent ratio, override the ratio responsively: pass an `aspectRatio` rule via `xstyle` (StyleX), or override `aspect-ratio` from your own unlayered CSS under a `@media`/`@container` rule — component styles live in the `astryx-base` cascade layer, so unlayered consumer CSS wins. Give an `xstyle` rule a `default` branch alongside the conditional one (`{default: 3, \'@container ...\': \'3 / 2\'}`) — with only the conditional branch the ratio is unset outside the query and the box collapses.'},
19
20
  {guidance: false, description: 'Use for general layout containers; use standard layout components instead.'},
20
21
  {guidance: false, description: 'Nest AspectRatio containers; one level is sufficient.'},
21
22
  {guidance: false, description: 'Constrain the height on its own. The width comes from the container, so a `height` or `maxHeight` by itself clamps the box off ratio; pair it with `width: "auto"` to size from the height instead.'},
@@ -30,7 +31,7 @@ export const docs = {
30
31
  {
31
32
  name: 'ratio',
32
33
  type: 'number',
33
- description: 'Aspect ratio as width/height (e.g. 16/9, 1).',
34
+ description: 'Aspect ratio as width/height (e.g. 16/9, 1). Emitted as a class-level declaration (never inline), so `xstyle` rules or unlayered consumer CSS can override it responsively.',
34
35
  required: true,
35
36
  },
36
37
  {
@@ -83,6 +84,7 @@ export const docsZh = {
83
84
  {guidance: true, description: 'Use `fit="cover"` for images and video so the component sizes the child; the child should not repeat `width`/`height`/`objectFit` styles.'},
84
85
  {guidance: true, description: 'Pass one child. With `fit` set, every direct child is stretched to fill the box, so put an overlay or caption inside a single wrapper child rather than passing it as a second child.'},
85
86
  {guidance: true, description: 'Describe media children with `alt`, or `alt=""` when the image is decorative. AspectRatio adds no role and no accessible name of its own, so the child carries the whole accessible description.'},
87
+ {guidance: true, description: 'For a breakpoint-dependent ratio, override the ratio responsively: pass an `aspectRatio` rule via `xstyle` (StyleX), or override `aspect-ratio` from your own unlayered CSS under a `@media`/`@container` rule — component styles live in the `astryx-base` cascade layer, so unlayered consumer CSS wins. Give an `xstyle` rule a `default` branch alongside the conditional one (`{default: 3, \'@container ...\': \'3 / 2\'}`) — with only the conditional branch the ratio is unset outside the query and the box collapses.'},
86
88
  {guidance: false, description: 'Use for general layout containers; use standard layout components instead.'},
87
89
  {guidance: false, description: 'Nest AspectRatio containers; one level is sufficient.'},
88
90
  {guidance: false, description: 'Constrain the height on its own. The width comes from the container, so a `height` or `maxHeight` by itself clamps the box off ratio; pair it with `width: "auto"` to size from the height instead.'},
@@ -94,7 +96,7 @@ export const docsZh = {
94
96
  ],
95
97
  },
96
98
  props: [
97
- {name: 'ratio', type: 'number', description: '宽高比,以宽/高表示(例如 16/9、1)。', required: true},
99
+ {name: 'ratio', type: 'number', description: '宽高比,以宽/高表示(例如 16/9、1)。以类级声明输出(非内联样式),可通过 `xstyle` 规则或未分层的(unlayered)消费者 CSS 做响应式覆盖。', required: true},
98
100
  {
99
101
  name: 'shape',
100
102
  type: "'rectangle' | 'ellipse'",
@@ -133,6 +135,7 @@ export const docsDense = {
133
135
  {guidance: true, description: 'Use `fit="cover"` for images and video so the component sizes the child; the child should not repeat `width`/`height`/`objectFit` styles.'},
134
136
  {guidance: true, description: 'Pass one child. With `fit` set, every direct child is stretched to fill the box, so put an overlay or caption inside a single wrapper child rather than passing it as a second child.'},
135
137
  {guidance: true, description: 'Describe media children with `alt`, or `alt=""` when the image is decorative. AspectRatio adds no role and no accessible name of its own, so the child carries the whole accessible description.'},
138
+ {guidance: true, description: 'For a breakpoint-dependent ratio, override the ratio responsively: pass an `aspectRatio` rule via `xstyle` (StyleX), or override `aspect-ratio` from your own unlayered CSS under a `@media`/`@container` rule — component styles live in the `astryx-base` cascade layer, so unlayered consumer CSS wins. Give an `xstyle` rule a `default` branch alongside the conditional one (`{default: 3, \'@container ...\': \'3 / 2\'}`) — with only the conditional branch the ratio is unset outside the query and the box collapses.'},
136
139
  {guidance: false, description: 'Use for general layout containers; use standard layout components instead.'},
137
140
  {guidance: false, description: 'Nest AspectRatio containers; one level is sufficient.'},
138
141
  {guidance: false, description: 'Constrain the height on its own. The width comes from the container, so a `height` or `maxHeight` by itself clamps the box off ratio; pair it with `width: "auto"` to size from the height instead.'},
@@ -144,7 +147,7 @@ export const docsDense = {
144
147
  ],
145
148
  },
146
149
  propDescriptions: {
147
- ratio: 'width/height ratio (e.g. 16/9, 1)',
150
+ ratio: 'width/height ratio (e.g. 16/9, 1); class-level declaration (never inline) so xstyle/unlayered-CSS @container overrides win',
148
151
  shape: "container shape: 'rectangle' (default) | 'ellipse' (circle at ratio 1)",
149
152
  fit: "child layout: 'cover' fill+crop | 'contain' fill+letterbox | 'center' natural size; omitted = child styles itself",
150
153
  children: 'content positioned absolutely to fill container',
@@ -13,6 +13,20 @@ import {describe, it, expect, vi, beforeAll} from 'vitest';
13
13
  import {render, screen} from '@testing-library/react';
14
14
  import {AspectRatio} from './AspectRatio';
15
15
 
16
+ /**
17
+ * The ratio compiles to a class-level `aspect-ratio: var(--x-aspectRatio)`
18
+ * declaration with the value carried by a CSS variable (so xstyle/@media
19
+ * and unlayered consumer CSS overrides can win). `--x-aspectRatio` is the
20
+ * debug-mode variable name emitted for the `aspectRatio` property.
21
+ */
22
+ function ratioVar(el: HTMLElement): string {
23
+ return el.style.getPropertyValue('--x-aspectRatio');
24
+ }
25
+
26
+ function ratioValue(ratio: number): string {
27
+ return String(ratio);
28
+ }
29
+
16
30
  describe('AspectRatio', () => {
17
31
  it('renders with correct aspect ratio', () => {
18
32
  render(
@@ -22,7 +36,7 @@ describe('AspectRatio', () => {
22
36
  );
23
37
  const element = screen.getByTestId('aspect-ratio');
24
38
  expect(element).toBeInTheDocument();
25
- expect(element.style.aspectRatio).toBe(String(16 / 9));
39
+ expect(ratioVar(element)).toBe(ratioValue(16 / 9));
26
40
  });
27
41
 
28
42
  it('children fill the container', () => {
@@ -47,7 +61,7 @@ describe('AspectRatio', () => {
47
61
  </AspectRatio>,
48
62
  );
49
63
  const element = screen.getByTestId('aspect-ratio');
50
- expect(element.style.aspectRatio).toBe(String(ratio));
64
+ expect(ratioVar(element)).toBe(ratioValue(ratio));
51
65
  });
52
66
 
53
67
  it('renders with 4:3 ratio', () => {
@@ -58,7 +72,7 @@ describe('AspectRatio', () => {
58
72
  </AspectRatio>,
59
73
  );
60
74
  const element = screen.getByTestId('aspect-ratio');
61
- expect(element.style.aspectRatio).toBe(String(ratio));
75
+ expect(ratioVar(element)).toBe(ratioValue(ratio));
62
76
  });
63
77
 
64
78
  it('renders with 1:1 square ratio', () => {
@@ -68,7 +82,7 @@ describe('AspectRatio', () => {
68
82
  </AspectRatio>,
69
83
  );
70
84
  const element = screen.getByTestId('aspect-ratio');
71
- expect(element.style.aspectRatio).toBe('1');
85
+ expect(ratioVar(element)).toBe(ratioValue(1));
72
86
  });
73
87
 
74
88
  it('renders with 21:9 ultrawide ratio', () => {
@@ -79,7 +93,7 @@ describe('AspectRatio', () => {
79
93
  </AspectRatio>,
80
94
  );
81
95
  const element = screen.getByTestId('aspect-ratio');
82
- expect(element.style.aspectRatio).toBe(String(ratio));
96
+ expect(ratioVar(element)).toBe(ratioValue(ratio));
83
97
  });
84
98
 
85
99
  it('renders an ellipse that respects the ratio (circle at 1:1)', () => {
@@ -89,7 +103,7 @@ describe('AspectRatio', () => {
89
103
  </AspectRatio>,
90
104
  );
91
105
  const element = screen.getByTestId('aspect-ratio');
92
- expect(element.style.aspectRatio).toBe('1');
106
+ expect(ratioVar(element)).toBe(ratioValue(1));
93
107
  expect(element.className).toContain('ellipse');
94
108
  });
95
109
 
@@ -101,7 +115,7 @@ describe('AspectRatio', () => {
101
115
  );
102
116
  const element = screen.getByTestId('aspect-ratio');
103
117
  // Ratio is preserved — the ellipse does not force 1:1.
104
- expect(element.style.aspectRatio).toBe(String(16 / 9));
118
+ expect(ratioVar(element)).toBe(ratioValue(16 / 9));
105
119
  expect(element.className).toContain('ellipse');
106
120
  });
107
121
 
@@ -112,7 +126,7 @@ describe('AspectRatio', () => {
112
126
  </AspectRatio>,
113
127
  );
114
128
  const element = screen.getByTestId('aspect-ratio');
115
- expect(element.style.aspectRatio).toBe('1');
129
+ expect(ratioVar(element)).toBe(ratioValue(1));
116
130
  expect(element.className).toContain('rectangle');
117
131
  // No ellipse border-radius when shape is the default rectangle
118
132
  expect(element.style.borderRadius).toBe('');
@@ -177,6 +191,38 @@ describe('AspectRatio', () => {
177
191
  expect(video).toBeInTheDocument();
178
192
  });
179
193
 
194
+ describe('responsive ratio', () => {
195
+ it('emits the ratio as a class-level declaration, not an inline style', () => {
196
+ render(
197
+ <AspectRatio ratio={3 / 1} data-testid="aspect-ratio">
198
+ <div>Hero</div>
199
+ </AspectRatio>,
200
+ );
201
+ const element = screen.getByTestId('aspect-ratio');
202
+ // No inline aspect-ratio — the declaration lives in a class reading a
203
+ // CSS variable, so @media/@container rules (via xstyle, or via
204
+ // unlayered consumer CSS beating the astryx-base layer) can override
205
+ // it.
206
+ expect(element.style.aspectRatio).toBe('');
207
+ expect(ratioVar(element)).toBe(ratioValue(3 / 1));
208
+ });
209
+
210
+ it('lets a consumer inline style win over the class-level ratio', () => {
211
+ render(
212
+ <AspectRatio
213
+ ratio={3 / 1}
214
+ style={{aspectRatio: '3 / 2'}}
215
+ data-testid="aspect-ratio">
216
+ <div>Hero</div>
217
+ </AspectRatio>,
218
+ );
219
+ const element = screen.getByTestId('aspect-ratio');
220
+ // The consumer's style prop passes through untouched (it is no longer
221
+ // merged with a component-owned inline aspect-ratio).
222
+ expect(element.style.aspectRatio).toBe('3 / 2');
223
+ });
224
+ });
225
+
180
226
  describe('fit', () => {
181
227
  it('marks the child\'s direct parent with data-astryx-aspect-ratio-override for fit="cover"', () => {
182
228
  render(
@@ -277,7 +323,7 @@ describe('AspectRatio', () => {
277
323
  });
278
324
 
279
325
  describe('style merging', () => {
280
- it('keeps the consumer style and applies the ratio over it', () => {
326
+ it('keeps the consumer style and lets an inline ratio win', () => {
281
327
  render(
282
328
  <AspectRatio
283
329
  ratio={16 / 9}
@@ -288,7 +334,8 @@ describe('AspectRatio', () => {
288
334
  );
289
335
  const element = screen.getByTestId('aspect-ratio');
290
336
  expect(element.style.opacity).toBe('0.5');
291
- expect(element.style.aspectRatio).toBe(String(16 / 9));
337
+ expect(element.style.aspectRatio).toBe('3 / 1');
338
+ expect(ratioVar(element)).toBe(ratioValue(16 / 9));
292
339
  });
293
340
 
294
341
  it('keeps a consumer className beside the theme target', () => {
@@ -43,6 +43,25 @@ export interface AspectRatioProps extends BaseProps<HTMLDivElement> {
43
43
  ref?: React.Ref<HTMLDivElement>;
44
44
  /**
45
45
  * The aspect ratio as width/height (e.g., 16/9 = 1.777..., 4/3 = 1.333..., 1 for square).
46
+ *
47
+ * The ratio is emitted as a class-level declaration — never an inline
48
+ * style — so it can be made responsive:
49
+ * - StyleX consumers pass an `aspect-ratio` rule via `xstyle`, including
50
+ * under `@media`/`@container` conditions.
51
+ * - Plain-CSS/Tailwind consumers override `aspect-ratio` from their own
52
+ * (unlayered) rules: the compiled component styles live in the
53
+ * `astryx-base` cascade layer, and unlayered CSS wins over layered
54
+ * rules regardless of specificity.
55
+ *
56
+ * @example
57
+ * ```
58
+ * // 3:1 hero that becomes 3:2 when its container stacks
59
+ * <AspectRatio ratio={3 / 1} className="gallery-hero">…</AspectRatio>
60
+ *
61
+ * // CSS: @container gallery (max-width: 720px) {
62
+ * // .gallery-hero { aspect-ratio: 3 / 2; }
63
+ * // }
64
+ * ```
46
65
  */
47
66
  ratio: number;
48
67
 
@@ -132,6 +151,20 @@ const styles = stylex.create({
132
151
  },
133
152
  });
134
153
 
154
+ // The ratio compiles to a CSS variable + a class-level declaration
155
+ // (aspect-ratio: var(--x)) instead of a raw inline style, so consumer
156
+ // overrides — `xstyle` rules, including ones inside @media/@container
157
+ // queries — can still win. A raw inline `aspect-ratio` would beat any class
158
+ // (same rationale as Grid's dynamic track values). Non-StyleX consumers
159
+ // don't need a dedicated hook: the compiled declaration sits in the
160
+ // `astryx-base` cascade layer, so any unlayered consumer rule wins
161
+ // regardless of specificity.
162
+ const dynamicStyles = stylex.create({
163
+ ratio: (ratio: number) => ({
164
+ aspectRatio: ratio,
165
+ }),
166
+ });
167
+
135
168
  /**
136
169
  * AspectRatio component for maintaining a specific aspect ratio for its children.
137
170
  *
@@ -147,6 +180,11 @@ const styles = stylex.create({
147
180
  * `ratio={1}` or an oval at other ratios. Both shapes respect the provided
148
181
  * `ratio`.
149
182
  *
183
+ * The ratio is a class-level declaration (not an inline style), so it can be
184
+ * overridden responsively — via `xstyle` for StyleX consumers, or with a
185
+ * plain unlayered `aspect-ratio` rule for plain-CSS consumers (component
186
+ * styles live in the `astryx-base` cascade layer, which unlayered CSS beats).
187
+ *
150
188
  * Sizing: the box takes its width from its container and derives its height
151
189
  * from the ratio, so it needs an ancestor with a definite width. Two
152
190
  * consequences are worth knowing. Constraining only the height (`height` or
@@ -188,11 +226,12 @@ export function AspectRatio({
188
226
  themeProps('aspect-ratio', {shape}),
189
227
  stylex.props(
190
228
  styles.container,
229
+ dynamicStyles.ratio(ratio),
191
230
  shape === 'ellipse' && styles.ellipse,
192
231
  xstyle,
193
232
  ),
194
233
  className,
195
- {...style, aspectRatio: ratio},
234
+ style,
196
235
  )}
197
236
  {...props}>
198
237
  {/* The marker attribute carries the fit value so the reset.css child
@@ -148,7 +148,8 @@ export const docsDense = {
148
148
  {guidance: true, description: 'Match size to context: xsm/sm inline, md/lg in lists, xl for profiles.'},
149
149
  {guidance: true, description: 'Add a status dot in chat or team views where availability matters.'},
150
150
  {guidance: true, description: 'When wrapping Avatar in your own Tooltip or HoverCard, set tooltip={false} so the built-in name tooltip does not overlap yours.'},
151
- {guidance: true, description: 'Interactive avatars (href/onClick) need name or alt; without one they warn in development. A status label is not a name.'},
151
+ {guidance: true, description: 'Interactive avatars (href/onClick) need name or alt; without one they warn in development.'},
152
+ {guidance: false, description: 'Rely on a status label to name an interactive avatar. "Online" says nothing about where the link goes.'},
152
153
  {guidance: false, description: 'Use for logos or product images. Use an image or icon instead.'},
153
154
  {guidance: false, description: 'Force a square or custom shape. Avatars are always circular.'},
154
155
  ],
@@ -28,7 +28,7 @@ import {useId, useMemo, useState, type ReactNode} from 'react';
28
28
  import type {BaseProps} from '../BaseProps';
29
29
  import {resolveSize, type AvatarSize} from '../Avatar';
30
30
  import * as stylex from '@stylexjs/stylex';
31
- import {mergeProps, mergeRefs} from '../utils';
31
+ import {mergeProps} from '../utils';
32
32
  import {composeEventHandlers} from '../utils/composeEventHandlers';
33
33
  import {AvatarGroupContext} from './AvatarGroupContext';
34
34
  import {themeProps} from '../utils/themeProps';
@@ -37,6 +37,7 @@ import {useListFocus} from '../hooks/useListFocus';
37
37
  import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
38
38
  import {VisuallyHidden} from '../VisuallyHidden';
39
39
 
40
+ import {useMergedRefs} from '../hooks/useMergedRefs';
40
41
  const OVERLAP_RATIO = 0.25;
41
42
 
42
43
  export interface AvatarGroupProps extends BaseProps<HTMLDivElement> {
@@ -143,7 +144,7 @@ export function AvatarGroup({
143
144
  <AvatarGroupContext value={contextValue}>
144
145
  <div
145
146
  {...props}
146
- ref={mergeRefs(ref, listRef)}
147
+ ref={useMergedRefs(ref, listRef)}
147
148
  role="group"
148
149
  aria-label={ariaLabel}
149
150
  aria-describedby={describedBy}
@@ -334,6 +334,106 @@ describe('BottomSheet', () => {
334
334
  expect(onOpenChange).toHaveBeenCalledWith(false);
335
335
  });
336
336
 
337
+ it('ignores Escape while an IME composition is active', () => {
338
+ const onOpenChange = vi.fn();
339
+ render(
340
+ <BottomSheet isOpen onOpenChange={onOpenChange} label="Filters">
341
+ <input aria-label="Search" />
342
+ </BottomSheet>,
343
+ );
344
+ const dialog = screen.getByRole('dialog');
345
+
346
+ fireEvent.keyDown(dialog, {key: 'Escape', isComposing: true});
347
+ fireEvent.keyDown(dialog, {key: 'Escape', keyCode: 229});
348
+
349
+ expect(onOpenChange).not.toHaveBeenCalled();
350
+ });
351
+
352
+ it('claims the composing Escape so no native close request follows', () => {
353
+ const onOpenChange = vi.fn();
354
+ render(
355
+ <BottomSheet isOpen onOpenChange={onOpenChange} label="Filters">
356
+ <input aria-label="Search" />
357
+ </BottomSheet>,
358
+ );
359
+ const dialog = screen.getByRole('dialog');
360
+
361
+ // An unclaimed Escape lets the browser raise its own close request, which
362
+ // arrives as `cancel` and dismisses the sheet on the same keypress. The
363
+ // guard has to swallow the composing Escape, not merely skip dismissal.
364
+ const wasNotClaimed = fireEvent.keyDown(dialog, {
365
+ key: 'Escape',
366
+ isComposing: true,
367
+ });
368
+
369
+ expect(wasNotClaimed).toBe(false);
370
+ expect(onOpenChange).not.toHaveBeenCalled();
371
+ });
372
+
373
+ it('keeps a form sheet open when Escape cancels a field composition', () => {
374
+ const onOpenChange = vi.fn();
375
+ render(
376
+ <BottomSheet
377
+ isOpen
378
+ purpose="form"
379
+ onOpenChange={onOpenChange}
380
+ label="Edit profile">
381
+ <input aria-label="Name" />
382
+ </BottomSheet>,
383
+ );
384
+
385
+ // The reported path: the keydown starts at the composing field and bubbles
386
+ // to the sheet's <dialog>, so the guard has to survive the trip.
387
+ fireEvent.keyDown(screen.getByRole('textbox', {name: 'Name'}), {
388
+ key: 'Escape',
389
+ isComposing: true,
390
+ });
391
+
392
+ expect(onOpenChange).not.toHaveBeenCalled();
393
+ });
394
+
395
+ it('still dismisses on the Escape after a composition is cancelled', () => {
396
+ const onOpenChange = vi.fn();
397
+ render(
398
+ <BottomSheet isOpen onOpenChange={onOpenChange} label="Filters">
399
+ <input aria-label="Search" />
400
+ </BottomSheet>,
401
+ );
402
+ const dialog = screen.getByRole('dialog');
403
+
404
+ fireEvent.keyDown(dialog, {key: 'Escape', isComposing: true});
405
+ expect(onOpenChange).not.toHaveBeenCalled();
406
+
407
+ fireEvent.keyDown(dialog, {key: 'Escape'});
408
+
409
+ expect(onOpenChange).toHaveBeenCalledWith(false);
410
+ });
411
+
412
+ it('leaves the IME the composition keys that are not Escape', () => {
413
+ const onOpenChange = vi.fn();
414
+ render(
415
+ <BottomSheet isOpen onOpenChange={onOpenChange} label="Filters">
416
+ <input aria-label="Search" />
417
+ </BottomSheet>,
418
+ );
419
+ const dialog = screen.getByRole('dialog');
420
+
421
+ // Enter commits a candidate and the arrows walk the candidate window. The
422
+ // sheet must claim neither, or the IME loses keys it owns.
423
+ const enterUnclaimed = fireEvent.keyDown(dialog, {
424
+ key: 'Enter',
425
+ isComposing: true,
426
+ });
427
+ const arrowUnclaimed = fireEvent.keyDown(dialog, {
428
+ key: 'ArrowDown',
429
+ isComposing: true,
430
+ });
431
+
432
+ expect(enterUnclaimed).toBe(true);
433
+ expect(arrowUnclaimed).toBe(true);
434
+ expect(onOpenChange).not.toHaveBeenCalled();
435
+ });
436
+
337
437
  it('requests close when the scrim (dialog element itself) is clicked', () => {
338
438
  const onOpenChange = vi.fn();
339
439
  render(
@@ -474,6 +574,29 @@ describe('BottomSheet', () => {
474
574
  expect(onOpenChange).toHaveBeenCalledWith(false);
475
575
  });
476
576
 
577
+ it('ignores Escape while an IME composition is active', () => {
578
+ const onOpenChange = vi.fn();
579
+ render(
580
+ <BottomSheet
581
+ isOpen
582
+ hasScrim={false}
583
+ onOpenChange={onOpenChange}
584
+ label="Filters">
585
+ <input aria-label="Search" />
586
+ </BottomSheet>,
587
+ );
588
+
589
+ // Escape is this sheet's only keyboard route out — there is no native
590
+ // close request behind it — so the guard is all that stands between a
591
+ // composing CJK user and a dismissed sheet.
592
+ fireEvent.keyDown(screen.getByRole('dialog'), {
593
+ key: 'Escape',
594
+ isComposing: true,
595
+ });
596
+
597
+ expect(onOpenChange).not.toHaveBeenCalled();
598
+ });
599
+
477
600
  it('still dismisses on a downward swipe past the threshold', () => {
478
601
  const onOpenChange = vi.fn();
479
602
  render(
@@ -16,6 +16,7 @@
16
16
  *
17
17
  * SYNC: When modified, update these files to stay in sync:
18
18
  * - /packages/core/src/BottomSheet/BottomSheetPanel.tsx
19
+ * - /packages/core/src/BottomSheet/BottomSheetEdgeTint.tsx
19
20
  * - /packages/core/src/BottomSheet/BottomSheet.doc.mjs
20
21
  * - /packages/core/src/BottomSheet/BottomSheet.test.tsx
21
22
  * - /packages/core/src/BottomSheet/BottomSheetSwitcher.tsx
@@ -37,12 +38,13 @@ import * as stylex from '@stylexjs/stylex';
37
38
  import type {BaseProps} from '../BaseProps';
38
39
  import type {DialogPurpose} from '../Dialog';
39
40
  import {colorVars, durationVars, easeVars} from '../theme/tokens.stylex';
40
- import {useDevWarning, useScrollLock} from '../hooks';
41
+ import {isImeKeyEvent, useDevWarning, useScrollLock} from '../hooks';
41
42
  import {
42
43
  BottomSheetPanel,
43
44
  type BottomSheetPanelMotion,
44
45
  type BottomSheetPanelState,
45
46
  } from './BottomSheetPanel';
47
+ import {BottomSheetEdgeTint} from './BottomSheetEdgeTint';
46
48
  import {
47
49
  BottomSheetSwitcherContext,
48
50
  type BottomSheetSwitcherContextValue,
@@ -93,6 +95,20 @@ const styles = stylex.create({
93
95
  },
94
96
  },
95
97
  },
98
+ /**
99
+ * The dim leaves with the sheet, on a curve that matches.
100
+ *
101
+ * A fade covers no distance, so the decelerate token front-loads its
102
+ * progress and simply ends it early: `--ease-standard` puts the scrim at 90%
103
+ * faded in 163ms of a 410ms close, leaving an undimmed page under a sheet
104
+ * that is still sliding across it. `linear` spends the duration it is given.
105
+ * Same reasoning the touch date picker's surface swap already carries.
106
+ */
107
+ scrimClosing: {
108
+ '::backdrop': {
109
+ transitionTimingFunction: 'linear',
110
+ },
111
+ },
96
112
  positioner: {
97
113
  position: 'absolute',
98
114
  insetInline: 0,
@@ -298,6 +314,9 @@ function StandaloneBottomSheet({
298
314
 
299
315
  const handleCancel = useCallback(
300
316
  (event: React.SyntheticEvent<HTMLDialogElement>) => {
317
+ // No IME guard here: `cancel` is a plain Event carrying no composition
318
+ // state, and handleKeyDown claims a composing Escape before the browser
319
+ // can raise the close request that would arrive here.
301
320
  event.preventDefault();
302
321
  dismissOnEscape();
303
322
  },
@@ -305,10 +324,20 @@ function StandaloneBottomSheet({
305
324
  );
306
325
  const handleKeyDown = useCallback(
307
326
  (event: React.KeyboardEvent<HTMLDialogElement>) => {
308
- if (event.key === 'Escape') {
309
- event.preventDefault();
310
- dismissOnEscape();
327
+ if (event.key !== 'Escape') {
328
+ return;
311
329
  }
330
+ // Claim the key before reading it: an unclaimed Escape lets the browser
331
+ // raise its own close request, which lands on handleCancel and dismisses
332
+ // on the same keypress.
333
+ event.preventDefault();
334
+ // An IME fires this keydown to cancel an in-progress composition, ahead
335
+ // of compositionend. It is a composition cancel, not a dismissal command
336
+ // — see utils/ime; Dialog and BottomSheetSwitcher guard the same way.
337
+ if (isImeKeyEvent(event.nativeEvent)) {
338
+ return;
339
+ }
340
+ dismissOnEscape();
312
341
  },
313
342
  [dismissOnEscape],
314
343
  );
@@ -327,6 +356,7 @@ function StandaloneBottomSheet({
327
356
  styles.dialog,
328
357
  shouldPresent && styles.dialogOpen,
329
358
  hasScrim && styles.scrim,
359
+ hasScrim && !isOpen && isPresented && styles.scrimClosing,
330
360
  !hasScrim && styles.dialogNonModal,
331
361
  )}
332
362
  ref={dialogRef}
@@ -355,6 +385,8 @@ function StandaloneBottomSheet({
355
385
  {children}
356
386
  </BottomSheetPanel>
357
387
  </div>
388
+ {/* A modal sheet's ::backdrop already answers Safari's edge sampler. */}
389
+ {hasScrim ? null : <BottomSheetEdgeTint />}
358
390
  </dialog>
359
391
  );
360
392
  }