@astryxdesign/core 0.6.2 → 0.6.3-canary.0899f21

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 (549) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +1 -1
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheet.js +3 -1
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts +7 -5
  6. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  7. package/dist/BottomSheet/BottomSheetPanel.js +56 -19
  8. package/dist/Button/Button.d.ts.map +1 -1
  9. package/dist/Button/Button.js +13 -28
  10. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  11. package/dist/Chat/ChatComposer.js +10 -5
  12. package/dist/Chat/ChatComposerDrawer.d.ts +19 -5
  13. package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
  14. package/dist/Chat/ChatComposerDrawer.js +48 -23
  15. package/dist/Chat/ChatComposerInput.d.ts +11 -3
  16. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  17. package/dist/Chat/ChatComposerInput.js +63 -16
  18. package/dist/Chat/ChatDictationButton.d.ts +3 -2
  19. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  20. package/dist/Chat/ChatDictationButton.js +6 -4
  21. package/dist/Chat/ChatLayout.d.ts +19 -0
  22. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  23. package/dist/Chat/ChatLayout.js +19 -0
  24. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  25. package/dist/Chat/ChatLayoutScrollButton.js +44 -34
  26. package/dist/Chat/ChatMessageBubble.d.ts +2 -0
  27. package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
  28. package/dist/Chat/ChatMessageBubble.js +5 -3
  29. package/dist/Chat/ChatMessageList.d.ts +2 -1
  30. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  31. package/dist/Chat/ChatMessageList.js +4 -3
  32. package/dist/Chat/ChatMessageMetadata.d.ts +5 -4
  33. package/dist/Chat/ChatMessageMetadata.d.ts.map +1 -1
  34. package/dist/Chat/ChatMessageMetadata.js +12 -9
  35. package/dist/Chat/ChatSendButton.js +1 -1
  36. package/dist/Chat/index.d.ts +1 -1
  37. package/dist/Chat/index.d.ts.map +1 -1
  38. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  39. package/dist/CheckboxInput/CheckboxInput.js +12 -2
  40. package/dist/Citation/Citation.d.ts +1 -1
  41. package/dist/Citation/Citation.d.ts.map +1 -1
  42. package/dist/Citation/Citation.js +3 -2
  43. package/dist/CodeBlock/highlightRanges.d.ts.map +1 -1
  44. package/dist/CodeBlock/highlightRanges.js +14 -3
  45. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  46. package/dist/Collapsible/Collapsible.js +6 -1
  47. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  48. package/dist/CommandPalette/CommandPaletteInput.js +1 -1
  49. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  50. package/dist/ComplexSelector/ComplexSelector.js +1 -1
  51. package/dist/DateInput/DateInput.d.ts.map +1 -1
  52. package/dist/DateInput/DateInput.js +4 -4
  53. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  54. package/dist/DateInput/NativeDateField.js +16 -16
  55. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  56. package/dist/DateInput/TouchDateField.js +2 -2
  57. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  58. package/dist/DateRangeInput/DateRangeInput.js +4 -4
  59. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  60. package/dist/DateTimeInput/DateTimeInput.js +8 -8
  61. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  62. package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
  63. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
  64. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
  65. package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
  66. package/dist/Dialog/DialogHeader.d.ts +8 -1
  67. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  68. package/dist/Dialog/DialogHeader.js +13 -4
  69. package/dist/Field/InputClearButton.d.ts +4 -2
  70. package/dist/Field/InputClearButton.d.ts.map +1 -1
  71. package/dist/Field/InputClearButton.js +4 -2
  72. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  73. package/dist/Field/PanelSearchInput.js +1 -1
  74. package/dist/FileInput/FileInput.d.ts.map +1 -1
  75. package/dist/FileInput/FileInput.js +7 -7
  76. package/dist/Icon/Icon.d.ts +6 -31
  77. package/dist/Icon/Icon.d.ts.map +1 -1
  78. package/dist/Icon/Icon.js +8 -71
  79. package/dist/Icon/IconDefaultSizeContext.d.ts +4 -0
  80. package/dist/Icon/IconDefaultSizeContext.d.ts.map +1 -0
  81. package/dist/Icon/IconDefaultSizeContext.js +18 -0
  82. package/dist/Icon/IconSize.stylex.d.ts +65 -0
  83. package/dist/Icon/IconSize.stylex.d.ts.map +1 -0
  84. package/dist/Icon/IconSize.stylex.js +75 -0
  85. package/dist/Item/Item.d.ts +1 -1
  86. package/dist/Item/Item.d.ts.map +1 -1
  87. package/dist/Item/Item.js +4 -3
  88. package/dist/Kbd/Kbd.d.ts +5 -3
  89. package/dist/Kbd/Kbd.d.ts.map +1 -1
  90. package/dist/Kbd/Kbd.js +36 -42
  91. package/dist/Link/Link.d.ts.map +1 -1
  92. package/dist/Link/Link.js +6 -2
  93. package/dist/Link/useLinkComponent.d.ts +5 -15
  94. package/dist/Link/useLinkComponent.d.ts.map +1 -1
  95. package/dist/Link/useLinkComponent.js +34 -51
  96. package/dist/List/List.d.ts +19 -1
  97. package/dist/List/List.d.ts.map +1 -1
  98. package/dist/List/List.js +9 -2
  99. package/dist/List/ListContext.d.ts +1 -0
  100. package/dist/List/ListContext.d.ts.map +1 -1
  101. package/dist/List/ListContext.js +2 -1
  102. package/dist/List/ListItem.d.ts +1 -1
  103. package/dist/List/ListItem.d.ts.map +1 -1
  104. package/dist/List/ListItem.js +9 -2
  105. package/dist/Markdown/Markdown.d.ts +8 -2
  106. package/dist/Markdown/Markdown.d.ts.map +1 -1
  107. package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
  108. package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
  109. package/dist/Markdown/Markdown.fr23.bench.js +150 -0
  110. package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
  111. package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
  112. package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
  113. package/dist/Markdown/Markdown.js +262 -109
  114. package/dist/Markdown/ast.d.ts +162 -0
  115. package/dist/Markdown/ast.d.ts.map +1 -0
  116. package/dist/Markdown/ast.js +74 -0
  117. package/dist/Markdown/index.d.ts +5 -3
  118. package/dist/Markdown/index.d.ts.map +1 -1
  119. package/dist/Markdown/index.js +3 -2
  120. package/dist/Markdown/parser/index.d.ts +10 -0
  121. package/dist/Markdown/parser/index.d.ts.map +1 -0
  122. package/dist/Markdown/parser/index.js +10 -0
  123. package/dist/Markdown/parser.d.ts +126 -58
  124. package/dist/Markdown/parser.d.ts.map +1 -1
  125. package/dist/Markdown/parser.js +716 -167
  126. package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
  127. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
  128. package/dist/Markdown/plugins/frontmatter.js +132 -0
  129. package/dist/Markdown/plugins/index.d.ts +17 -0
  130. package/dist/Markdown/plugins/index.d.ts.map +1 -0
  131. package/dist/Markdown/plugins/index.js +14 -0
  132. package/dist/Markdown/plugins/protocol.d.ts +191 -0
  133. package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
  134. package/dist/Markdown/plugins/protocol.js +957 -0
  135. package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
  136. package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
  137. package/dist/Markdown/plugins/semanticFence.js +213 -0
  138. package/dist/Markdown/plugins/softBreaks.d.ts +3 -0
  139. package/dist/Markdown/plugins/softBreaks.d.ts.map +1 -0
  140. package/dist/Markdown/plugins/softBreaks.js +172 -0
  141. package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
  142. package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
  143. package/dist/Markdown/plugins/sourceDecoration.js +393 -0
  144. package/dist/Markdown/plugins/textTransform.d.ts +30 -0
  145. package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
  146. package/dist/Markdown/plugins/textTransform.js +374 -0
  147. package/dist/Markdown/remark.d.ts +196 -0
  148. package/dist/Markdown/remark.d.ts.map +1 -0
  149. package/dist/Markdown/remark.js +1062 -0
  150. package/dist/Markdown/url.d.ts +6 -0
  151. package/dist/Markdown/url.d.ts.map +1 -0
  152. package/dist/Markdown/url.js +31 -0
  153. package/dist/Markdown/utils.d.ts +4 -2
  154. package/dist/Markdown/utils.d.ts.map +1 -1
  155. package/dist/Markdown/utils.js +2 -1
  156. package/dist/MetadataList/MetadataList.js +3 -3
  157. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  158. package/dist/NumberInput/NumberInput.js +4 -4
  159. package/dist/Outline/index.d.ts +1 -0
  160. package/dist/Outline/index.d.ts.map +1 -1
  161. package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
  162. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  163. package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
  164. package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
  165. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  166. package/dist/Outline/useOutlineFromMarkdown.js +8 -3
  167. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  168. package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
  169. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  170. package/dist/PowerSearch/PowerSearchValueEditor.js +5 -0
  171. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  172. package/dist/ProgressBar/ProgressBar.js +36 -31
  173. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  174. package/dist/RadioList/RadioListItem.js +13 -1
  175. package/dist/ScrollableArea/ScrollableArea.d.ts +1 -0
  176. package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -1
  177. package/dist/ScrollableArea/ScrollableArea.js +1 -0
  178. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  179. package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
  180. package/dist/Selector/Selector.d.ts +9 -7
  181. package/dist/Selector/Selector.d.ts.map +1 -1
  182. package/dist/Selector/Selector.js +16 -7
  183. package/dist/SideNav/SideNav.d.ts +2 -1
  184. package/dist/SideNav/SideNav.d.ts.map +1 -1
  185. package/dist/SideNav/SideNav.js +9 -3
  186. package/dist/Slider/Slider.d.ts.map +1 -1
  187. package/dist/Slider/Slider.js +28 -12
  188. package/dist/Spinner/Spinner.d.ts.map +1 -1
  189. package/dist/Spinner/Spinner.js +23 -7
  190. package/dist/Switch/Switch.d.ts.map +1 -1
  191. package/dist/Switch/Switch.js +11 -0
  192. package/dist/TabList/Tab.d.ts +1 -1
  193. package/dist/TabList/Tab.d.ts.map +1 -1
  194. package/dist/TabList/Tab.js +20 -7
  195. package/dist/Table/Table.d.ts +1 -0
  196. package/dist/Table/Table.d.ts.map +1 -1
  197. package/dist/Table/Table.js +29 -13
  198. package/dist/TextArea/TextArea.d.ts.map +1 -1
  199. package/dist/TextArea/TextArea.js +1 -1
  200. package/dist/TextInput/TextInput.d.ts.map +1 -1
  201. package/dist/TextInput/TextInput.js +2 -2
  202. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  203. package/dist/TimeInput/TimeInput.js +4 -4
  204. package/dist/Timer/Timer.d.ts +49 -0
  205. package/dist/Timer/Timer.d.ts.map +1 -0
  206. package/dist/Timer/Timer.js +158 -0
  207. package/dist/Timer/index.d.ts +9 -0
  208. package/dist/Timer/index.d.ts.map +1 -0
  209. package/dist/Timer/index.js +10 -0
  210. package/dist/ToggleButton/ToggleButton.d.ts +9 -6
  211. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  212. package/dist/ToggleButton/ToggleButton.js +15 -27
  213. package/dist/TopNav/TopNavMenu.d.ts +1 -1
  214. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  215. package/dist/TopNav/TopNavMenu.js +3 -2
  216. package/dist/TreeList/TreeList.d.ts.map +1 -1
  217. package/dist/TreeList/TreeList.js +3 -0
  218. package/dist/TreeList/TreeListItem.d.ts +6 -1
  219. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  220. package/dist/TreeList/TreeListItem.js +12 -2
  221. package/dist/TreeList/TreeListTypes.d.ts +8 -1
  222. package/dist/TreeList/TreeListTypes.d.ts.map +1 -1
  223. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  224. package/dist/Typeahead/BaseTypeahead.js +16 -7
  225. package/dist/astryx.css +27 -11
  226. package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
  227. package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
  228. package/dist/hooks/scrollKeyboardDelegation.js +146 -0
  229. package/dist/hooks/useClickableContainer.d.ts +10 -1
  230. package/dist/hooks/useClickableContainer.d.ts.map +1 -1
  231. package/dist/hooks/useClickableContainer.js +15 -4
  232. package/dist/hooks/useScrollableArea.d.ts +6 -2
  233. package/dist/hooks/useScrollableArea.d.ts.map +1 -1
  234. package/dist/hooks/useScrollableArea.js +17 -5
  235. package/dist/index.d.ts +1 -0
  236. package/dist/index.d.ts.map +1 -1
  237. package/dist/index.js +3 -0
  238. package/dist/theme/declarationBoundary.d.ts +17 -0
  239. package/dist/theme/declarationBoundary.d.ts.map +1 -0
  240. package/dist/theme/declarationBoundary.js +343 -0
  241. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  242. package/dist/theme/derivedVarRegistry.js +4 -0
  243. package/dist/theme/generateThemeRules.d.ts +13 -7
  244. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  245. package/dist/theme/generateThemeRules.js +140 -34
  246. package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
  247. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  248. package/dist/utils/interactionOverlay.stylex.js +9 -0
  249. package/dist/utils/safeUrl.d.ts +11 -0
  250. package/dist/utils/safeUrl.d.ts.map +1 -0
  251. package/dist/utils/safeUrl.js +78 -0
  252. package/locales/af-ZA.json +530 -1
  253. package/locales/ar-SA.json +490 -1
  254. package/locales/ca-ES.json +514 -1
  255. package/locales/cs-CZ.json +498 -1
  256. package/locales/da-DK.json +522 -1
  257. package/locales/de-DE.json +522 -1
  258. package/locales/el-GR.json +498 -1
  259. package/locales/es-ES.json +510 -1
  260. package/locales/fi-FI.json +498 -1
  261. package/locales/fr-FR.json +534 -1
  262. package/locales/he-IL.json +498 -1
  263. package/locales/hu-HU.json +498 -1
  264. package/locales/it-IT.json +510 -1
  265. package/locales/ja-JP.json +490 -1
  266. package/locales/ko-KR.json +498 -1
  267. package/locales/nl-NL.json +534 -1
  268. package/locales/no-NO.json +510 -1
  269. package/locales/pl-PL.json +506 -1
  270. package/locales/pt-BR.json +506 -1
  271. package/locales/pt-PT.json +506 -1
  272. package/locales/ro-RO.json +506 -1
  273. package/locales/ru-RU.json +498 -1
  274. package/locales/sr-SP.json +498 -1
  275. package/locales/sv-SE.json +502 -1
  276. package/locales/tr-TR.json +498 -1
  277. package/locales/uk-UA.json +498 -1
  278. package/locales/vi-VN.json +502 -1
  279. package/locales/zh-CN.json +490 -1
  280. package/locales/zh-TW.json +490 -1
  281. package/package.json +24 -4
  282. package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
  283. package/src/BottomSheet/BottomSheet.spec.md +46 -20
  284. package/src/BottomSheet/BottomSheet.test.tsx +6 -3
  285. package/src/BottomSheet/BottomSheet.tsx +3 -1
  286. package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
  287. package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
  288. package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
  289. package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
  290. package/src/Button/Button.doc.mjs +5 -5
  291. package/src/Button/Button.tsx +15 -13
  292. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
  293. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
  294. package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
  295. package/src/Button/__tests__/Button.a11y.states.ts +9 -0
  296. package/src/Chat/ChatComposer.doc.mjs +14 -0
  297. package/src/Chat/ChatComposer.spec.md +230 -0
  298. package/src/Chat/ChatComposer.test.tsx +57 -0
  299. package/src/Chat/ChatComposer.tsx +21 -9
  300. package/src/Chat/ChatComposerDrawer.doc.mjs +63 -5
  301. package/src/Chat/ChatComposerDrawer.spec.md +267 -0
  302. package/src/Chat/ChatComposerDrawer.test.tsx +81 -12
  303. package/src/Chat/ChatComposerDrawer.tsx +38 -9
  304. package/src/Chat/ChatComposerInput.doc.mjs +114 -26
  305. package/src/Chat/ChatComposerInput.spec.md +185 -0
  306. package/src/Chat/ChatComposerInput.test.tsx +113 -3
  307. package/src/Chat/ChatComposerInput.tsx +78 -13
  308. package/src/Chat/ChatComposerTokenElement.spec.md +152 -0
  309. package/src/Chat/ChatComposerTokenElement.test.tsx +85 -0
  310. package/src/Chat/ChatDictationButton.doc.mjs +3 -3
  311. package/src/Chat/ChatDictationButton.spec.md +160 -0
  312. package/src/Chat/ChatDictationButton.test.tsx +92 -27
  313. package/src/Chat/ChatDictationButton.tsx +8 -6
  314. package/src/Chat/ChatLayout.doc.mjs +12 -4
  315. package/src/Chat/ChatLayout.spec.md +271 -0
  316. package/src/Chat/ChatLayout.test.tsx +28 -0
  317. package/src/Chat/ChatLayout.tsx +19 -0
  318. package/src/Chat/ChatLayoutScrollButton.spec.md +338 -0
  319. package/src/Chat/ChatLayoutScrollButton.test.tsx +31 -0
  320. package/src/Chat/ChatLayoutScrollButton.tsx +31 -14
  321. package/src/Chat/ChatMessageBubble.doc.mjs +28 -15
  322. package/src/Chat/ChatMessageBubble.spec.md +263 -0
  323. package/src/Chat/ChatMessageBubble.test.tsx +93 -0
  324. package/src/Chat/ChatMessageBubble.tsx +8 -3
  325. package/src/Chat/ChatMessageList.spec.md +222 -0
  326. package/src/Chat/ChatMessageList.test.tsx +64 -2
  327. package/src/Chat/ChatMessageList.tsx +4 -3
  328. package/src/Chat/ChatMessageMetadata.doc.mjs +57 -3
  329. package/src/Chat/ChatMessageMetadata.spec.md +220 -0
  330. package/src/Chat/ChatMessageMetadata.test.tsx +34 -0
  331. package/src/Chat/ChatMessageMetadata.tsx +13 -13
  332. package/src/Chat/ChatSendButton.test.tsx +1 -4
  333. package/src/Chat/ChatSendButton.tsx +1 -1
  334. package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +514 -0
  335. package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +740 -0
  336. package/src/Chat/__tests__/ChatMessageBubble.a11y.chromium.spec.ts +457 -0
  337. package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.test.ts +91 -0
  338. package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.ts +91 -0
  339. package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +456 -0
  340. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +413 -0
  341. package/src/Chat/index.ts +1 -0
  342. package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
  343. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  344. package/src/CheckboxInput/CheckboxInput.tsx +21 -1
  345. package/src/Citation/Citation.doc.mjs +2 -2
  346. package/src/Citation/Citation.test.tsx +17 -0
  347. package/src/Citation/Citation.tsx +4 -2
  348. package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
  349. package/src/ClickableCard/ClickableCard.test.tsx +102 -5
  350. package/src/CodeBlock/highlightRanges.test.ts +143 -1
  351. package/src/CodeBlock/highlightRanges.ts +15 -5
  352. package/src/Collapsible/Collapsible.doc.mjs +11 -0
  353. package/src/Collapsible/Collapsible.test.tsx +21 -0
  354. package/src/Collapsible/Collapsible.tsx +5 -0
  355. package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
  356. package/src/ComplexSelector/ComplexSelector.tsx +6 -1
  357. package/src/DateInput/DateInput.tsx +6 -1
  358. package/src/DateInput/NativeDateField.tsx +24 -8
  359. package/src/DateInput/TouchDateField.tsx +6 -2
  360. package/src/DateRangeInput/DateRangeInput.tsx +6 -1
  361. package/src/DateTimeInput/DateTimeInput.tsx +6 -1
  362. package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
  363. package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
  364. package/src/Dialog/Dialog.doc.mjs +2 -0
  365. package/src/Dialog/DialogHeader.doc.mjs +22 -0
  366. package/src/Dialog/DialogHeader.test.tsx +167 -3
  367. package/src/Dialog/DialogHeader.tsx +37 -6
  368. package/src/Dialog/modules/DialogHeader.spec.md +101 -42
  369. package/src/DropdownMenu/DropdownMenu.test.tsx +5 -2
  370. package/src/Field/InputClearButton.test.tsx +17 -3
  371. package/src/Field/InputClearButton.tsx +5 -2
  372. package/src/Field/PanelSearchInput.tsx +5 -2
  373. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
  374. package/src/FileInput/FileInput.tsx +11 -2
  375. package/src/Icon/Icon.doc.mjs +7 -5
  376. package/src/Icon/Icon.spec.md +81 -40
  377. package/src/Icon/Icon.test.tsx +6 -3
  378. package/src/Icon/Icon.tsx +20 -68
  379. package/src/Icon/IconDefaultSizeContext.ts +23 -0
  380. package/src/Icon/IconSize.stylex.ts +70 -0
  381. package/src/IconButton/IconButton.doc.mjs +3 -3
  382. package/src/IconButton/IconButton.test.tsx +40 -0
  383. package/src/Item/Item.doc.mjs +6 -2
  384. package/src/Item/Item.tsx +9 -3
  385. package/src/Kbd/Kbd.doc.mjs +3 -3
  386. package/src/Kbd/Kbd.test.tsx +57 -1
  387. package/src/Kbd/Kbd.tsx +57 -37
  388. package/src/Layout/LayoutFooter.doc.mjs +17 -0
  389. package/src/Link/Link.doc.mjs +17 -4
  390. package/src/Link/Link.test.tsx +24 -0
  391. package/src/Link/Link.tsx +5 -0
  392. package/src/Link/LinkProvider.doc.mjs +2 -2
  393. package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +261 -0
  394. package/src/Link/useLinkComponent.test.tsx +333 -0
  395. package/src/Link/useLinkComponent.ts +29 -57
  396. package/src/List/List.doc.mjs +6 -0
  397. package/src/List/List.spec.md +200 -0
  398. package/src/List/List.test.tsx +117 -1
  399. package/src/List/List.tsx +22 -2
  400. package/src/List/ListContext.tsx +3 -1
  401. package/src/List/ListItem.doc.mjs +2 -2
  402. package/src/List/ListItem.source-build.test.mjs +41 -0
  403. package/src/List/ListItem.tsx +26 -2
  404. package/src/Markdown/Markdown.doc.mjs +300 -2
  405. package/src/Markdown/Markdown.fr23.bench.ts +174 -0
  406. package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
  407. package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
  408. package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
  409. package/src/Markdown/Markdown.public.test.ts +130 -3
  410. package/src/Markdown/Markdown.renderBoundary.test.tsx +72 -0
  411. package/src/Markdown/Markdown.spec.md +203 -45
  412. package/src/Markdown/Markdown.test.tsx +63 -0
  413. package/src/Markdown/Markdown.tsx +414 -135
  414. package/src/Markdown/ast.test.ts +307 -0
  415. package/src/Markdown/ast.ts +337 -0
  416. package/src/Markdown/incremental.test.ts +12 -0
  417. package/src/Markdown/index.ts +37 -1
  418. package/src/Markdown/modules/remark.spec.md +269 -0
  419. package/src/Markdown/modules/softBreaks.spec.md +137 -0
  420. package/src/Markdown/parser/index.ts +64 -0
  421. package/src/Markdown/parser.public.test.ts +103 -0
  422. package/src/Markdown/parser.test.ts +20 -0
  423. package/src/Markdown/parser.ts +1180 -235
  424. package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
  425. package/src/Markdown/plugins/frontmatter.ts +198 -0
  426. package/src/Markdown/plugins/index.ts +54 -0
  427. package/src/Markdown/plugins/protocol.test.tsx +1022 -0
  428. package/src/Markdown/plugins/protocol.ts +1618 -0
  429. package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
  430. package/src/Markdown/plugins/semanticFence.ts +424 -0
  431. package/src/Markdown/plugins/softBreaks.test.tsx +172 -0
  432. package/src/Markdown/plugins/softBreaks.ts +169 -0
  433. package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
  434. package/src/Markdown/plugins/sourceDecoration.ts +539 -0
  435. package/src/Markdown/plugins/textTransform.test.ts +467 -0
  436. package/src/Markdown/plugins/textTransform.ts +554 -0
  437. package/src/Markdown/remark.test.tsx +1877 -0
  438. package/src/Markdown/remark.ts +1574 -0
  439. package/src/Markdown/url.ts +36 -0
  440. package/src/Markdown/utils.ts +13 -1
  441. package/src/MetadataList/MetadataList.test.tsx +4 -2
  442. package/src/MetadataList/MetadataList.tsx +3 -3
  443. package/src/MultiSelector/MultiSelector.test.tsx +1 -35
  444. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
  445. package/src/NumberInput/NumberInput.tsx +6 -1
  446. package/src/Outline/Outline.doc.mjs +7 -5
  447. package/src/Outline/Outline.spec.md +1 -1
  448. package/src/Outline/index.ts +1 -0
  449. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
  450. package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
  451. package/src/Outline/useOutlineFromMarkdown.ts +15 -3
  452. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  453. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
  454. package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
  455. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +51 -0
  456. package/src/PowerSearch/PowerSearchValueEditor.tsx +4 -0
  457. package/src/ProgressBar/ProgressBar.test.tsx +29 -11
  458. package/src/ProgressBar/ProgressBar.tsx +59 -72
  459. package/src/RadioList/RadioList.doc.mjs +11 -0
  460. package/src/RadioList/RadioList.test.tsx +32 -0
  461. package/src/RadioList/RadioListItem.tsx +25 -1
  462. package/src/ScrollableArea/ScrollableArea.tsx +1 -0
  463. package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
  464. package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
  465. package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
  466. package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
  467. package/src/Selector/Selector.doc.mjs +2 -2
  468. package/src/Selector/Selector.spec.md +51 -49
  469. package/src/Selector/Selector.test.tsx +61 -55
  470. package/src/Selector/Selector.tsx +34 -15
  471. package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
  472. package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
  473. package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
  474. package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
  475. package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
  476. package/src/Selector/__tests__/Selector.a11y.chromium.spec.ts +45 -0
  477. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
  478. package/src/SideNav/SideNav.doc.mjs +1 -1
  479. package/src/SideNav/SideNav.test.tsx +10 -0
  480. package/src/SideNav/SideNav.tsx +14 -2
  481. package/src/Slider/Slider.doc.mjs +17 -3
  482. package/src/Slider/Slider.test.tsx +221 -0
  483. package/src/Slider/Slider.tsx +40 -9
  484. package/src/Spinner/Spinner.test.tsx +4 -0
  485. package/src/Spinner/Spinner.tsx +31 -18
  486. package/src/Switch/Switch.doc.mjs +11 -0
  487. package/src/Switch/Switch.test.tsx +16 -0
  488. package/src/Switch/Switch.tsx +28 -0
  489. package/src/TabList/Tab.tsx +35 -7
  490. package/src/TabList/TabList.doc.mjs +11 -0
  491. package/src/TabList/TabList.spec.md +204 -0
  492. package/src/TabList/TabList.test.tsx +66 -0
  493. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
  494. package/src/Table/Table.doc.mjs +1 -1
  495. package/src/Table/Table.spec.md +54 -40
  496. package/src/Table/Table.test.tsx +83 -10
  497. package/src/Table/Table.tsx +30 -22
  498. package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +137 -0
  499. package/src/TextArea/TextArea.tsx +6 -1
  500. package/src/TextInput/TextInput.test.tsx +20 -1
  501. package/src/TextInput/TextInput.tsx +6 -1
  502. package/src/TimeInput/TimeInput.tsx +6 -1
  503. package/src/Timer/Timer.doc.mjs +183 -0
  504. package/src/Timer/Timer.spec.md +215 -0
  505. package/src/Timer/Timer.test.tsx +302 -0
  506. package/src/Timer/Timer.tsx +258 -0
  507. package/src/Timer/index.ts +11 -0
  508. package/src/ToggleButton/ToggleButton.doc.mjs +3 -3
  509. package/src/ToggleButton/ToggleButton.test.tsx +299 -4
  510. package/src/ToggleButton/ToggleButton.tsx +26 -33
  511. package/src/ToggleButton/ToggleButtonGroup.doc.mjs +3 -3
  512. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
  513. package/src/Token/Token.doc.mjs +2 -2
  514. package/src/Tokenizer/Tokenizer.spec.md +142 -75
  515. package/src/TopNav/TopNavMenu.doc.mjs +2 -2
  516. package/src/TopNav/TopNavMenu.test.tsx +30 -0
  517. package/src/TopNav/TopNavMenu.tsx +7 -2
  518. package/src/TreeList/TreeList.doc.mjs +3 -3
  519. package/src/TreeList/TreeList.spec.md +123 -75
  520. package/src/TreeList/TreeList.test.tsx +29 -0
  521. package/src/TreeList/TreeList.tsx +3 -0
  522. package/src/TreeList/TreeListItem.tsx +15 -1
  523. package/src/TreeList/TreeListTypes.ts +11 -1
  524. package/src/Typeahead/BaseTypeahead.spec.md +4 -3
  525. package/src/Typeahead/BaseTypeahead.tsx +21 -7
  526. package/src/Typeahead/Typeahead.test.tsx +53 -0
  527. package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
  528. package/src/__tests__/pressState.ts +93 -0
  529. package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
  530. package/src/hooks/scrollKeyboardDelegation.ts +233 -0
  531. package/src/hooks/useClickableContainer.test.tsx +236 -0
  532. package/src/hooks/useClickableContainer.ts +20 -4
  533. package/src/hooks/useScrollableArea.doc.mjs +15 -3
  534. package/src/hooks/useScrollableArea.test.tsx +59 -1
  535. package/src/hooks/useScrollableArea.ts +34 -10
  536. package/src/index.ts +3 -0
  537. package/src/inputFontFloor.test.ts +106 -0
  538. package/src/tailwind-theme.css +0 -6
  539. package/src/theme/Theme.test.tsx +32 -0
  540. package/src/theme/declarationBoundary.test.ts +126 -0
  541. package/src/theme/declarationBoundary.ts +347 -0
  542. package/src/theme/derivedVarRegistry.test.ts +7 -0
  543. package/src/theme/derivedVarRegistry.ts +1 -0
  544. package/src/theme/generateThemeRules.test.ts +526 -1
  545. package/src/theme/generateThemeRules.ts +228 -50
  546. package/src/theme/tokenValueCompat.test.ts +4 -1
  547. package/src/utils/interactionOverlay.stylex.ts +20 -0
  548. package/src/utils/safeUrl.test.ts +88 -0
  549. package/src/utils/safeUrl.ts +85 -0
@@ -0,0 +1,236 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file useClickableContainer.test.tsx
5
+ * @input Uses vitest, @testing-library/react, useClickableContainer
6
+ * @output Unit tests for the hook's navigation paths
7
+ * @position Testing; pins the shared navigation-destination rule on every
8
+ * activation method the hook owns — plain click (location.href), new tab
9
+ * (target="_blank"), Cmd/Ctrl-click, middle-click, and the delegated click
10
+ * on the interactive ref. None of these pass through React DOM's own href
11
+ * vetting, so each one has a positive and a blocked case here.
12
+ */
13
+
14
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
15
+ import {useRef} from 'react';
16
+ import {fireEvent, render, screen} from '@testing-library/react';
17
+ import {useClickableContainer} from './useClickableContainer';
18
+
19
+ // The scheme the rule exists to stop (kept in consts so the JSX below stays
20
+ // lint-clean — the tests are ABOUT these URLs never navigating).
21
+ const scriptUrl = 'javascript:alert(1)';
22
+ // The same scheme hidden behind a control character a browser ignores.
23
+ const hiddenScriptUrl = 'java\nscript:alert(1)';
24
+
25
+ function Card({href, target}: {href: string; target?: string}) {
26
+ const containerRef = useRef<HTMLDivElement>(null);
27
+ const {onClick, onMouseUp} = useClickableContainer({
28
+ containerRef,
29
+ href,
30
+ target,
31
+ });
32
+ return (
33
+ <div
34
+ ref={containerRef}
35
+ data-testid="card"
36
+ onClick={onClick}
37
+ onMouseUp={onMouseUp}>
38
+ content
39
+ </div>
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Card that delegates plain clicks to an inner link (the ClickableCard /
45
+ * Item shape). The inner anchor's own click is spied on so the test observes
46
+ * whether the hook activated it.
47
+ */
48
+ function DelegatingCard({
49
+ href,
50
+ onAnchorClick,
51
+ }: {
52
+ href: string;
53
+ onAnchorClick: () => void;
54
+ }) {
55
+ const containerRef = useRef<HTMLDivElement>(null);
56
+ const interactiveRef = useRef<HTMLAnchorElement>(null);
57
+ const {onClick, onMouseUp} = useClickableContainer({
58
+ containerRef,
59
+ interactiveRef,
60
+ href,
61
+ });
62
+ return (
63
+ <div
64
+ ref={containerRef}
65
+ data-testid="card"
66
+ onClick={onClick}
67
+ onMouseUp={onMouseUp}>
68
+ content
69
+ <a
70
+ ref={interactiveRef}
71
+ // The anchor is the delegate, not the subject: the tests below use a
72
+ // fixed inert href here so that only the hook's decision is observed.
73
+ href="#inner"
74
+ data-testid="inner-link"
75
+ onClick={e => {
76
+ e.preventDefault();
77
+ onAnchorClick();
78
+ }}>
79
+ inner
80
+ </a>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ afterEach(() => {
86
+ vi.restoreAllMocks();
87
+ vi.unstubAllGlobals();
88
+ });
89
+
90
+ describe('useClickableContainer navigation — new tab (target="_blank")', () => {
91
+ it('opens ordinary hrefs in a new tab', () => {
92
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
93
+ render(<Card href="https://example.com/docs" target="_blank" />);
94
+ fireEvent.click(screen.getByTestId('card'));
95
+ expect(open).toHaveBeenCalledWith(
96
+ 'https://example.com/docs',
97
+ '_blank',
98
+ 'noopener',
99
+ );
100
+ });
101
+
102
+ it('does not open a script-scheme href in a new tab', () => {
103
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
104
+ // eslint-disable-next-line @eslint-react/dom-no-script-url -- the test proves this URL never navigates
105
+ render(<Card href={scriptUrl} target="_blank" />);
106
+ fireEvent.click(screen.getByTestId('card'));
107
+ expect(open).not.toHaveBeenCalled();
108
+ });
109
+ });
110
+
111
+ describe('useClickableContainer navigation — Cmd/Ctrl-click', () => {
112
+ it('opens ordinary hrefs in a new tab on Cmd-click', () => {
113
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
114
+ render(<Card href="/docs" />);
115
+ fireEvent.click(screen.getByTestId('card'), {metaKey: true});
116
+ expect(open).toHaveBeenCalledWith('/docs', '_blank', 'noopener');
117
+ });
118
+
119
+ it('opens ordinary hrefs in a new tab on Ctrl-click', () => {
120
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
121
+ render(<Card href="/docs" />);
122
+ fireEvent.click(screen.getByTestId('card'), {ctrlKey: true});
123
+ expect(open).toHaveBeenCalledWith('/docs', '_blank', 'noopener');
124
+ });
125
+
126
+ it('does not open a script-scheme href on Cmd-click', () => {
127
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
128
+ // eslint-disable-next-line @eslint-react/dom-no-script-url -- the test proves this URL never navigates
129
+ render(<Card href={scriptUrl} />);
130
+ fireEvent.click(screen.getByTestId('card'), {metaKey: true});
131
+ expect(open).not.toHaveBeenCalled();
132
+ });
133
+
134
+ it('does not open a script-scheme href on Ctrl-click', () => {
135
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
136
+ // eslint-disable-next-line @eslint-react/dom-no-script-url -- the test proves this URL never navigates
137
+ render(<Card href={scriptUrl} />);
138
+ fireEvent.click(screen.getByTestId('card'), {ctrlKey: true});
139
+ expect(open).not.toHaveBeenCalled();
140
+ });
141
+ });
142
+
143
+ describe('useClickableContainer navigation — middle-click', () => {
144
+ it('opens ordinary hrefs on middle-click', () => {
145
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
146
+ render(<Card href="/docs" />);
147
+ fireEvent.mouseUp(screen.getByTestId('card'), {button: 1});
148
+ expect(open).toHaveBeenCalledWith('/docs', '_blank', 'noopener');
149
+ });
150
+
151
+ it('does not open a script-scheme href on middle-click', () => {
152
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
153
+ // eslint-disable-next-line @eslint-react/dom-no-script-url -- the test proves this URL never navigates
154
+ render(<Card href={scriptUrl} />);
155
+ fireEvent.mouseUp(screen.getByTestId('card'), {button: 1});
156
+ expect(open).not.toHaveBeenCalled();
157
+ });
158
+ });
159
+
160
+ describe('useClickableContainer navigation — plain click (location.href)', () => {
161
+ // jsdom does not navigate, so assigning `location.href` is a no-op there
162
+ // and would let a missing guard pass unnoticed. Stand in a plain object so
163
+ // the assignment is observable.
164
+ let location: {href: string};
165
+
166
+ beforeEach(() => {
167
+ location = {href: 'https://app.example/start'};
168
+ vi.stubGlobal('location', location);
169
+ });
170
+
171
+ it('assigns ordinary hrefs to location', () => {
172
+ render(<Card href="/docs" />);
173
+ fireEvent.click(screen.getByTestId('card'));
174
+ expect(location.href).toBe('/docs');
175
+ });
176
+
177
+ it('does not assign a script-scheme href to location', () => {
178
+ // eslint-disable-next-line @eslint-react/dom-no-script-url -- the test proves this URL never navigates
179
+ render(<Card href={scriptUrl} />);
180
+ fireEvent.click(screen.getByTestId('card'));
181
+ expect(location.href).toBe('https://app.example/start');
182
+ });
183
+
184
+ it('does not assign a control-character-hidden script scheme to location', () => {
185
+ // eslint-disable-next-line @eslint-react/dom-no-script-url -- the test proves this URL never navigates
186
+ render(<Card href={hiddenScriptUrl} />);
187
+ fireEvent.click(screen.getByTestId('card'));
188
+ expect(location.href).toBe('https://app.example/start');
189
+ });
190
+ });
191
+
192
+ describe('useClickableContainer navigation — delegated interactive ref', () => {
193
+ it('delegates ordinary hrefs to the inner link', () => {
194
+ const onAnchorClick = vi.fn();
195
+ render(<DelegatingCard href="/docs" onAnchorClick={onAnchorClick} />);
196
+ fireEvent.click(screen.getByTestId('card'));
197
+ expect(onAnchorClick).toHaveBeenCalledTimes(1);
198
+ });
199
+
200
+ it('does not delegate a script-scheme href to the inner link', () => {
201
+ const onAnchorClick = vi.fn();
202
+ // eslint-disable-next-line @eslint-react/dom-no-script-url -- the test proves this URL never navigates
203
+ render(<DelegatingCard href={scriptUrl} onAnchorClick={onAnchorClick} />);
204
+ fireEvent.click(screen.getByTestId('card'));
205
+ expect(onAnchorClick).not.toHaveBeenCalled();
206
+ });
207
+
208
+ it('a click on the inner link itself is left to the link', () => {
209
+ const onAnchorClick = vi.fn();
210
+ render(<DelegatingCard href="/docs" onAnchorClick={onAnchorClick} />);
211
+ fireEvent.click(screen.getByTestId('inner-link'));
212
+ // The anchor's own handler ran once; the hook did not proxy a second one.
213
+ expect(onAnchorClick).toHaveBeenCalledTimes(1);
214
+ });
215
+ });
216
+
217
+ describe('useClickableContainer navigation — blocked href keeps the rest', () => {
218
+ it('still fires the onClick handler for a blocked href', () => {
219
+ const onClick = vi.fn();
220
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
221
+ function Surface() {
222
+ const containerRef = useRef<HTMLDivElement>(null);
223
+ const handlers = useClickableContainer({
224
+ containerRef,
225
+ href: scriptUrl,
226
+ target: '_blank',
227
+ onClick,
228
+ });
229
+ return <div ref={containerRef} data-testid="card" {...handlers} />;
230
+ }
231
+ render(<Surface />);
232
+ fireEvent.click(screen.getByTestId('card'));
233
+ expect(onClick).toHaveBeenCalledTimes(1);
234
+ expect(open).not.toHaveBeenCalled();
235
+ });
236
+ });
@@ -11,11 +11,17 @@
11
11
  * Solves the "nested interactive elements" problem: when a card is clickable
12
12
  * but contains buttons/links, clicking those should NOT trigger the card's action.
13
13
  *
14
+ * Every navigation exit here (window.open, location.href, and the delegated
15
+ * click on the interactive ref) runs after the shared navigation-destination
16
+ * rule in utils/safeUrl.ts; a blocked href does not navigate by any
17
+ * activation method.
18
+ *
14
19
  * SYNC: When modified, update:
15
20
  * - /packages/core/src/hooks/index.ts (export)
16
21
  */
17
22
 
18
23
  import {useCallback, useEffect, type RefObject, type MouseEvent} from 'react';
24
+ import {isSafeUrl} from '../utils/safeUrl';
19
25
 
20
26
  /**
21
27
  * Canonical list of interactive element selectors — native controls plus
@@ -89,7 +95,11 @@ export interface UseClickableContainerOptions {
89
95
  interactiveRef?: RefObject<HTMLElement | null>;
90
96
  /** Click handler */
91
97
  onClick?: (event: MouseEvent<HTMLElement>) => void;
92
- /** Navigation URL — when provided, clicking the container navigates */
98
+ /**
99
+ * Navigation URL — when provided, clicking the container navigates. Checked
100
+ * with the shared navigation rule (utils/safeUrl.ts) before any activation
101
+ * method (plain, new-tab, Cmd/Ctrl-click, middle-click, delegated click).
102
+ */
93
103
  href?: string;
94
104
  /** Link target */
95
105
  target?: string;
@@ -180,8 +190,12 @@ export function useClickableContainer({
180
190
  return;
181
191
  }
182
192
 
183
- // Navigate if href is provided
184
- if (href != null) {
193
+ // Navigate if href is provided. React DOM vets the hrefs it writes as
194
+ // attributes; nothing below passes through React, so the shared
195
+ // navigation rule decides once, before target, modifier key, or the
196
+ // delegated anchor can select a sink. A blocked href simply does not
197
+ // navigate — the click handler above still ran.
198
+ if (href != null && isSafeUrl(href)) {
185
199
  const shouldOpenNewTab =
186
200
  target === '_blank' || event.ctrlKey || event.metaKey;
187
201
  if (shouldOpenNewTab) {
@@ -230,11 +244,13 @@ export function useClickableContainer({
230
244
  return;
231
245
  }
232
246
 
233
- // Middle-click on href opens in new tab
247
+ // Middle-click on href opens in new tab (same scheme rule as onClick —
248
+ // this navigation never passes through React either).
234
249
  const isMiddleClick = event.button === 1;
235
250
  if (
236
251
  isMiddleClick &&
237
252
  href != null &&
253
+ isSafeUrl(href) &&
238
254
  (eventTarget === event.currentTarget ||
239
255
  !hasInteractiveAncestor(eventTarget, containerEl))
240
256
  ) {
@@ -1,5 +1,12 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ /**
4
+ * @file useScrollableArea.doc.mjs
5
+ * @input Shared scroll hook's fixed and entry-time keyboard policies
6
+ * @output Consumer guidance for named viewports and safe focus delegation
7
+ * @position Hook documentation consumed by the CLI and docsite
8
+ */
9
+
3
10
  /** @type {import('@astryxdesign/cli/authoring').HookDoc} */
4
11
  export const docs = {
5
12
  name: 'useScrollableArea',
@@ -18,7 +25,7 @@ export const docs = {
18
25
  name: 'options',
19
26
  type: 'UseScrollableAreaOptions',
20
27
  description:
21
- 'Logical scroll intent, keyboard owner, overscroll policy, and fitting Sticky containment.',
28
+ 'Logical scroll intent, fixed or automatic keyboard owner, overscroll policy, and fitting Sticky containment.',
22
29
  required: true,
23
30
  },
24
31
  ],
@@ -59,7 +66,12 @@ export const docs = {
59
66
  {
60
67
  guidance: true,
61
68
  description:
62
- 'Use content keyboard ownership when an existing focusable descendant gives keyboard users access to all overflowed content.',
69
+ 'Use contentOrViewport to delegate forward Tab entry to the first sequential native link or button when it preserves native scroll keys. Inputs, composite widgets, and nested scroll areas retain the named viewport stop. Shift+Tab from the delegated first child skips the viewport; pointer and programmatic focus stay on it.',
70
+ },
71
+ {
72
+ guidance: true,
73
+ description:
74
+ 'Use content keyboard ownership when your integration already supplies keyboard access to the full scroll range. Automatic delegation checks current content at each keyboard entry without continuously tracking its focusability.',
63
75
  },
64
76
  {
65
77
  guidance: true,
@@ -85,7 +97,7 @@ export const docsDense = {
85
97
  'Composes logical-axis scrolling into caller-owned viewport/content elements with stable effective-axis and edge state.',
86
98
  paramDescriptions: {
87
99
  options:
88
- 'axis, keyboard owner, allow/contain overscroll policy, and fitting Sticky containment.',
100
+ 'axis, fixed/automatic keyboard owner, allow/contain overscroll policy, and fitting Sticky containment.',
89
101
  },
90
102
  returnDescriptions: {
91
103
  getViewportProps:
@@ -1,8 +1,15 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ /**
4
+ * @file useScrollableArea.test.tsx
5
+ * @input Shared scroll hook, mocked geometry, and observed DOM changes
6
+ * @output Regression coverage for measurement, composition, and stable viewport access
7
+ * @position DOM-level hook contract; native traversal is verified in browser tests
8
+ */
9
+
3
10
  import {act, render, screen} from '@testing-library/react';
4
11
  import * as stylex from '@stylexjs/stylex';
5
- import {useRef, type Ref} from 'react';
12
+ import {useRef, type ReactNode, type Ref} from 'react';
6
13
  import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
7
14
  import {
8
15
  useScrollableArea,
@@ -194,6 +201,57 @@ describe('useScrollableArea', () => {
194
201
  expect(viewport.getAttribute('style')).toContain('--x-overflowY: hidden');
195
202
  });
196
203
 
204
+ it('keeps the named viewport available while content eligibility changes', () => {
205
+ function AdaptiveFixture({children}: {children?: ReactNode}) {
206
+ const area = useScrollableArea({
207
+ axis: 'block',
208
+ keyboardAccess: {
209
+ owner: 'contentOrViewport',
210
+ label: 'Adaptive results',
211
+ role: 'region',
212
+ },
213
+ });
214
+ return (
215
+ <div data-testid="adaptive-viewport" {...area.getViewportProps()}>
216
+ <div data-testid="adaptive-content" {...area.getContentProps()}>
217
+ {children}
218
+ </div>
219
+ </div>
220
+ );
221
+ }
222
+
223
+ vi.spyOn(HTMLElement.prototype, 'getClientRects').mockReturnValue([
224
+ new DOMRect(0, 0, 20, 20),
225
+ ] as unknown as DOMRectList);
226
+ const {rerender} = render(<AdaptiveFixture>Plain text</AdaptiveFixture>);
227
+ const viewport = screen.getByTestId('adaptive-viewport');
228
+ const content = screen.getByTestId('adaptive-content');
229
+ makeMeasurable(viewport);
230
+ setGeometry(viewport, {scrollHeight: 180});
231
+
232
+ void act(() => viewport.dispatchEvent(new Event('scroll')));
233
+ flushFrame();
234
+ expect(viewport).toHaveAttribute('role', 'region');
235
+ expect(viewport).toHaveAccessibleName('Adaptive results');
236
+ expect(viewport).toHaveAttribute('tabindex', '0');
237
+
238
+ viewport.focus();
239
+ rerender(
240
+ <AdaptiveFixture>
241
+ <button type="button">Use existing action</button>
242
+ </AdaptiveFixture>,
243
+ );
244
+ void act(() => content.dispatchEvent(new Event('transitionend')));
245
+ flushFrame();
246
+ expect(viewport).toHaveAttribute('tabindex', '0');
247
+ expect(viewport).toHaveFocus();
248
+
249
+ rerender(<AdaptiveFixture>Plain text again</AdaptiveFixture>);
250
+ void act(() => content.dispatchEvent(new Event('transitionend')));
251
+ flushFrame();
252
+ expect(viewport).toHaveAttribute('tabindex', '0');
253
+ });
254
+
197
255
  it('requires scroll-capable computed overflow and more than 1px excess geometry', () => {
198
256
  render(<Fixture />);
199
257
  const viewport = screen.getByTestId('viewport');
@@ -4,8 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file useScrollableArea.ts
7
- * @input Logical scroll intent, keyboard ownership, caller-owned viewport/content props
8
- * @output Safe prop getters and stable per-axis effective scroll state
7
+ * @input Logical scroll intent, focus-time keyboard delegation, caller-owned viewport/content props
8
+ * @output Safe prop getters, native keyboard entry, and stable per-axis effective scroll state
9
9
  * @position Canonical behavior core for ScrollableArea and structure-owning adopters
10
10
  *
11
11
  * SYNC: When modified, update:
@@ -26,6 +26,7 @@ import {
26
26
  } from 'react';
27
27
  import * as stylex from '@stylexjs/stylex';
28
28
  import type {BaseProps} from '../BaseProps';
29
+ import {attachScrollKeyboardDelegation} from './scrollKeyboardDelegation';
29
30
  import {useIsomorphicLayoutEffect} from './useIsomorphicLayoutEffect';
30
31
  import {mergeProps} from '../utils/mergeProps';
31
32
  import {mergeRefs} from '../utils/mergeRefs';
@@ -48,7 +49,12 @@ export type ScrollStickyContainment = 'whenScrollable' | 'always';
48
49
 
49
50
  export type ScrollKeyboardAccess =
50
51
  | {owner: 'content'}
51
- | {owner: 'viewport'; label: string; role?: 'group' | 'region'};
52
+ | {owner: 'viewport'; label: string; role?: 'group' | 'region'}
53
+ | {
54
+ owner: 'contentOrViewport';
55
+ label: string;
56
+ role?: 'group' | 'region';
57
+ };
52
58
 
53
59
  export interface ScrollAxisState {
54
60
  isScrollable: boolean;
@@ -285,6 +291,8 @@ export function useScrollableArea({
285
291
  content.addEventListener('transitionend', scheduleMeasure);
286
292
  content.addEventListener('animationend', scheduleMeasure);
287
293
 
294
+ // Geometry invalidation is independent of keyboard eligibility. The latter
295
+ // is inspected only on native keyboard entry, never by these observers.
288
296
  const mutationObserver =
289
297
  typeof MutationObserver === 'undefined'
290
298
  ? null
@@ -346,6 +354,20 @@ export function useScrollableArea({
346
354
  [axis, measured],
347
355
  );
348
356
 
357
+ const hasEffectiveAxis =
358
+ state.inline.isScrollable || state.block.isScrollable;
359
+ useIsomorphicLayoutEffect(() => {
360
+ if (
361
+ keyboardAccess.owner !== 'contentOrViewport' ||
362
+ !hasEffectiveAxis ||
363
+ viewport == null ||
364
+ content == null
365
+ ) {
366
+ return;
367
+ }
368
+ return attachScrollKeyboardDelegation(viewport, content);
369
+ }, [content, hasEffectiveAxis, keyboardAccess.owner, viewport]);
370
+
349
371
  const getViewportProps = useCallback(
350
372
  <E extends HTMLElement>(
351
373
  props: ScrollableElementProps<E> = {},
@@ -385,8 +407,9 @@ export function useScrollableArea({
385
407
  ),
386
408
  ),
387
409
  ) as ScrollableElementProps<E>;
410
+ const viewportIsKeyboardOwner = keyboardAccess.owner !== 'content';
388
411
  const keepsProgrammaticFocus =
389
- !hasEffectiveAxis &&
412
+ (!viewportIsKeyboardOwner || !hasEffectiveAxis) &&
390
413
  viewport != null &&
391
414
  typeof document !== 'undefined' &&
392
415
  document.activeElement === viewport;
@@ -409,15 +432,16 @@ export function useScrollableArea({
409
432
  }
410
433
 
411
434
  const keyboardProps =
412
- keyboardAccess.owner === 'viewport'
435
+ keyboardAccess.owner !== 'content'
413
436
  ? {
414
437
  role: keyboardAccess.role ?? 'group',
415
438
  'aria-label': keyboardAccess.label,
416
- tabIndex: hasEffectiveAxis
417
- ? 0
418
- : keepsProgrammaticFocus
419
- ? -1
420
- : undefined,
439
+ tabIndex:
440
+ viewportIsKeyboardOwner && hasEffectiveAxis
441
+ ? 0
442
+ : keepsProgrammaticFocus
443
+ ? -1
444
+ : undefined,
421
445
  }
422
446
  : {};
423
447
 
package/src/index.ts CHANGED
@@ -161,6 +161,9 @@ export * from './Spinner';
161
161
  // Timestamp display
162
162
  export * from './Timestamp';
163
163
 
164
+ // Elapsed timer display
165
+ export * from './Timer';
166
+
164
167
  // Overlay
165
168
  export * from './Overlay';
166
169
  export * from './Outline';
@@ -0,0 +1,106 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file inputFontFloor.test.ts
5
+ * @input Uses node:fs and node:path to read the text-control sources
6
+ * @output Invariants for the 16px input floor shared by every text control
7
+ * @position Family-wide source test, in the source-reading style of
8
+ * reset.test.ts. jsdom's CSSOM drops declarations nested inside @supports,
9
+ * so the iOS branch cannot be asserted against injected rules — the
10
+ * runtime half (no bare coarse-pointer floor) lives in TextInput.test.tsx.
11
+ */
12
+
13
+ import {readFileSync, readdirSync} from 'node:fs';
14
+ import path from 'node:path';
15
+
16
+ import {describe, expect, it} from 'vitest';
17
+
18
+ /**
19
+ * Every control that floors its text to 16px. iOS Safari zooms the page when
20
+ * a focused control renders under 16px; no other coarse-pointer browser does.
21
+ * `-webkit-touch-callout` is implemented by iOS WebKit alone, so each floor
22
+ * nests `@supports (-webkit-touch-callout: none)` inside the coarse-pointer
23
+ * query: iPhones and iPads keep the zoom guard, while Android and touch
24
+ * laptops keep the theme's own type scale instead of an inflated 16px.
25
+ */
26
+ const FLOORED_CONTROLS = [
27
+ 'Chat/ChatComposerInput.tsx',
28
+ 'CommandPalette/CommandPaletteInput.tsx',
29
+ 'ComplexSelector/ComplexSelector.tsx',
30
+ 'DateInput/DateInput.tsx',
31
+ 'DateInput/NativeDateField.tsx',
32
+ 'DateInput/TouchDateField.tsx',
33
+ 'DateRangeInput/DateRangeInput.tsx',
34
+ 'DateTimeInput/DateTimeInput.tsx',
35
+ 'DateTimeInput/TouchDateTimeField.tsx',
36
+ 'DateTimeInput/nativePickerSegmentStyles.ts',
37
+ 'Field/PanelSearchInput.tsx',
38
+ 'FileInput/FileInput.tsx',
39
+ 'NumberInput/NumberInput.tsx',
40
+ 'Selector/Selector.tsx',
41
+ 'TextArea/TextArea.tsx',
42
+ 'TextInput/TextInput.tsx',
43
+ 'TimeInput/TimeInput.tsx',
44
+ 'Typeahead/BaseTypeahead.tsx',
45
+ // The rich-text package carries the same floor; its path is relative to
46
+ // packages/core/src.
47
+ '../../richtext/src/RichTextEditor.tsx',
48
+ ];
49
+
50
+ /** A font floor applied to every coarse pointer, iOS or not. */
51
+ const BARE_COARSE_FLOOR = `'@media (pointer: coarse)': \`max(1rem`;
52
+
53
+ /** The same floor, gated to iOS WebKit inside the coarse-pointer query. */
54
+ const IOS_GATED_FLOOR =
55
+ /'@media \(pointer: coarse\)': \{\s*'@supports \(-webkit-touch-callout: none\)': `max\(1rem, \$\{typeScaleVars\['--text-(?:body|label)-size'\]\}\)`/;
56
+
57
+ /** Collect every non-test component source under `dir`, recursively. */
58
+ function componentSources(dir: string): string[] {
59
+ const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
60
+ const out: string[] = [];
61
+ for (const entry of readdirSync(dir, {withFileTypes: true})) {
62
+ const full = path.join(dir, entry.name);
63
+ if (entry.isDirectory()) {
64
+ out.push(...componentSources(full));
65
+ } else if (
66
+ SOURCE_EXTENSIONS.some(ext => entry.name.endsWith(ext)) &&
67
+ !entry.name.endsWith('.test.ts') &&
68
+ !entry.name.endsWith('.test.tsx') &&
69
+ !entry.name.endsWith('.doc.mjs')
70
+ ) {
71
+ out.push(full);
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ describe('the 16px input floor', () => {
78
+ it('gates every floored text control to iOS alone', () => {
79
+ // Guards the loop below against the list silently emptying out.
80
+ expect(FLOORED_CONTROLS.length).toBeGreaterThanOrEqual(18);
81
+ for (const file of FLOORED_CONTROLS) {
82
+ const source = readFileSync(path.resolve(__dirname, file), 'utf8');
83
+ expect(source, file).toMatch(IOS_GATED_FLOOR);
84
+ expect(source, file).not.toContain(BARE_COARSE_FLOOR);
85
+ }
86
+ });
87
+
88
+ it('leaves no bare coarse-pointer font floor anywhere in core or richtext', () => {
89
+ // Catches a new (or renamed) control reintroducing the ungated floor,
90
+ // wherever it lands — the list above only knows the current family. The
91
+ // rich-text package carries the same floor, so it is scanned too.
92
+ const scanRoots = [
93
+ __dirname,
94
+ path.resolve(__dirname, '../../richtext/src'),
95
+ ];
96
+ const offenders = scanRoots
97
+ .flatMap(root => componentSources(root))
98
+ .map(file => ({
99
+ file,
100
+ source: readFileSync(file, 'utf8'),
101
+ }))
102
+ .filter(({source}) => source.includes(BARE_COARSE_FLOOR))
103
+ .map(({file}) => path.relative(__dirname, file));
104
+ expect(offenders).toEqual([]);
105
+ });
106
+ });
@@ -182,12 +182,6 @@
182
182
  --text-4xl: var(--font-size-4xl);
183
183
  --text-5xl: var(--font-size-5xl);
184
184
 
185
- /* --- Font weights --- */
186
- --font-weight-normal: var(--font-weight-normal);
187
- --font-weight-medium: var(--font-weight-medium);
188
- --font-weight-semibold: var(--font-weight-semibold);
189
- --font-weight-bold: var(--font-weight-bold);
190
-
191
185
  /* =========================================================================
192
186
  * Border radius
193
187
  *
@@ -55,6 +55,38 @@ describe('Theme', () => {
55
55
  expect(getByText('hello')).toBeTruthy();
56
56
  });
57
57
 
58
+ it('mounts the remaining CSS when one authored declaration is dropped', () => {
59
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
60
+ try {
61
+ const theme = defineTheme({
62
+ name: 'declaration-boundary',
63
+ tokens: {'--color-accent': 'red; } body { color: red'},
64
+ components: {button: {base: {borderRadius: '4px'}}},
65
+ });
66
+ render(
67
+ <Theme theme={theme}>
68
+ <span>child</span>
69
+ </Theme>,
70
+ );
71
+ const css = Array.from(document.querySelectorAll('style'))
72
+ .map(tag => tag.textContent ?? '')
73
+ .join('\n');
74
+ expect(css).not.toContain('body { color: red');
75
+ expect(css).toContain('border-radius: 4px;');
76
+ expect(css).toContain(':where(p)');
77
+ expect(
78
+ warn.mock.calls.filter(([message]) =>
79
+ String(message).startsWith('[astryx theme] dropped'),
80
+ ),
81
+ ).toHaveLength(1);
82
+ expect(warn).toHaveBeenCalledWith(
83
+ expect.stringContaining('dropped "--color-accent" in tokens'),
84
+ );
85
+ } finally {
86
+ warn.mockRestore();
87
+ }
88
+ });
89
+
58
90
  it('sets data-astryx-theme on wrapper div', () => {
59
91
  const {container} = render(
60
92
  <Theme theme={testTheme}>