@astryxdesign/core 0.5.0 → 0.5.1-canary.046f6ea

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 (622) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/dist/AlertDialog/AlertDialog.d.ts +1 -1
  3. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  4. package/dist/AlertDialog/AlertDialog.js +45 -20
  5. package/dist/AspectRatio/AspectRatio.d.ts +6 -6
  6. package/dist/Avatar/Avatar.d.ts +25 -1
  7. package/dist/Avatar/Avatar.d.ts.map +1 -1
  8. package/dist/Avatar/Avatar.js +43 -9
  9. package/dist/Avatar/index.d.ts +2 -2
  10. package/dist/Avatar/index.d.ts.map +1 -1
  11. package/dist/Avatar/index.js +1 -1
  12. package/dist/AvatarGroup/AvatarGroup.d.ts +9 -2
  13. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  14. package/dist/AvatarGroup/AvatarGroup.js +5 -2
  15. package/dist/AvatarGroup/AvatarGroupContext.d.ts +2 -1
  16. package/dist/AvatarGroup/AvatarGroupContext.d.ts.map +1 -1
  17. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  18. package/dist/AvatarGroup/AvatarGroupOverflow.js +11 -7
  19. package/dist/Badge/Badge.d.ts.map +1 -1
  20. package/dist/Badge/Badge.js +26 -2
  21. package/dist/Banner/Banner.d.ts +10 -1
  22. package/dist/Banner/Banner.d.ts.map +1 -1
  23. package/dist/Banner/Banner.js +11 -2
  24. package/dist/Button/Button.d.ts.map +1 -1
  25. package/dist/Button/Button.js +8 -7
  26. package/dist/Calendar/Calendar.d.ts +4 -2
  27. package/dist/Calendar/Calendar.d.ts.map +1 -1
  28. package/dist/Calendar/Calendar.js +15 -21
  29. package/dist/Calendar/hooks/useCalendarDays.js +4 -6
  30. package/dist/Calendar/hooks/useCalendarNavigation.js +4 -6
  31. package/dist/Calendar/styles.d.ts +5 -20
  32. package/dist/Calendar/styles.d.ts.map +1 -1
  33. package/dist/Calendar/styles.js +15 -17
  34. package/dist/Card/Card.d.ts +12 -12
  35. package/dist/Card/Card.d.ts.map +1 -1
  36. package/dist/Card/Card.js +17 -9
  37. package/dist/Card/index.d.ts +35 -1
  38. package/dist/Card/index.d.ts.map +1 -1
  39. package/dist/Card/index.js +21 -1
  40. package/dist/Carousel/Carousel.d.ts.map +1 -1
  41. package/dist/Carousel/Carousel.js +108 -7
  42. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  43. package/dist/CheckboxInput/CheckboxInput.js +7 -1
  44. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  45. package/dist/CodeBlock/CodeBlock.js +15 -4
  46. package/dist/CodeBlock/highlightRanges.js +1 -1
  47. package/dist/CodeBlock/highlightStyles.js +13 -13
  48. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  49. package/dist/CommandPalette/CommandPaletteItem.js +2 -7
  50. package/dist/ComplexSelector/ComplexSelector.d.ts +10 -2
  51. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  52. package/dist/ComplexSelector/ComplexSelector.js +14 -7
  53. package/dist/DateInput/DateInput.d.ts +43 -0
  54. package/dist/DateInput/DateInput.d.ts.map +1 -1
  55. package/dist/DateInput/DateInput.js +22 -5
  56. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  57. package/dist/DateInput/MonthScroller.js +5 -3
  58. package/dist/DateInput/MonthYearWheels.d.ts.map +1 -1
  59. package/dist/DateInput/MonthYearWheels.js +6 -4
  60. package/dist/DateInput/NativeDateField.d.ts +10 -0
  61. package/dist/DateInput/NativeDateField.d.ts.map +1 -0
  62. package/dist/DateInput/NativeDateField.js +494 -0
  63. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  64. package/dist/DateInput/TouchDateField.js +52 -25
  65. package/dist/DateInput/Wheel.d.ts.map +1 -1
  66. package/dist/DateInput/Wheel.js +1 -0
  67. package/dist/DateInput/index.d.ts +1 -1
  68. package/dist/DateInput/index.d.ts.map +1 -1
  69. package/dist/DateInput/nativeDateSegments.d.ts +22 -0
  70. package/dist/DateInput/nativeDateSegments.d.ts.map +1 -0
  71. package/dist/DateInput/nativeDateSegments.js +111 -0
  72. package/dist/DateRangeInput/DateRangeInput.d.ts +4 -2
  73. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  74. package/dist/DateRangeInput/DateRangeInput.js +3 -5
  75. package/dist/DateTimeInput/DateTimeInput.d.ts +23 -24
  76. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  77. package/dist/DateTimeInput/DateTimeInput.js +35 -10
  78. package/dist/DateTimeInput/TouchDateTimeField.d.ts +6 -0
  79. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -0
  80. package/dist/DateTimeInput/TouchDateTimeField.js +1021 -0
  81. package/dist/Dialog/Dialog.d.ts +16 -2
  82. package/dist/Dialog/Dialog.d.ts.map +1 -1
  83. package/dist/Dialog/Dialog.js +28 -7
  84. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  85. package/dist/DropdownMenu/DropdownMenu.js +4 -13
  86. package/dist/Field/FieldLabel.d.ts.map +1 -1
  87. package/dist/Field/FieldLabel.js +9 -1
  88. package/dist/Field/InputClearButton.d.ts +9 -3
  89. package/dist/Field/InputClearButton.d.ts.map +1 -1
  90. package/dist/Field/InputClearButton.js +14 -3
  91. package/dist/Heading/Heading.d.ts.map +1 -1
  92. package/dist/Heading/Heading.js +0 -1
  93. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  94. package/dist/HoverCard/HoverCard.js +104 -3
  95. package/dist/HoverCard/useHoverCard.d.ts +14 -2
  96. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  97. package/dist/HoverCard/useHoverCard.js +7 -1
  98. package/dist/Icon/Icon.d.ts +4 -2
  99. package/dist/Icon/Icon.d.ts.map +1 -1
  100. package/dist/Icon/defaultIcons.d.ts +3 -2
  101. package/dist/Icon/defaultIcons.d.ts.map +1 -1
  102. package/dist/Icon/defaultIcons.js +7 -0
  103. package/dist/Icon/globalIconRegistry.d.ts +13 -0
  104. package/dist/Icon/globalIconRegistry.d.ts.map +1 -1
  105. package/dist/Icon/globalIconRegistry.js +34 -6
  106. package/dist/Icon/index.d.ts +1 -1
  107. package/dist/Icon/index.d.ts.map +1 -1
  108. package/dist/Icon/useIcon.d.ts +5 -2
  109. package/dist/Icon/useIcon.d.ts.map +1 -1
  110. package/dist/Icon/useIcon.js +3 -0
  111. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
  112. package/dist/Indicator/CheckboxIndicator.js +6 -2
  113. package/dist/Item/Item.d.ts.map +1 -1
  114. package/dist/Item/Item.js +2 -2
  115. package/dist/Kbd/Kbd.d.ts.map +1 -1
  116. package/dist/Kbd/Kbd.js +2 -26
  117. package/dist/Layer/gestureCounter.d.ts +8 -0
  118. package/dist/Layer/gestureCounter.d.ts.map +1 -0
  119. package/dist/Layer/gestureCounter.js +53 -0
  120. package/dist/Layer/useLayer.d.ts +23 -13
  121. package/dist/Layer/useLayer.d.ts.map +1 -1
  122. package/dist/Layer/useLayer.js +146 -24
  123. package/dist/List/List.d.ts +1 -1
  124. package/dist/List/List.d.ts.map +1 -1
  125. package/dist/List/List.js +15 -4
  126. package/dist/Markdown/Markdown.d.ts +1 -1
  127. package/dist/Markdown/Markdown.d.ts.map +1 -1
  128. package/dist/Markdown/Markdown.js +20 -7
  129. package/dist/Markdown/parser.d.ts.map +1 -1
  130. package/dist/Markdown/parser.js +5 -3
  131. package/dist/MetadataList/MetadataListItem.d.ts +1 -1
  132. package/dist/MetadataList/MetadataListItem.d.ts.map +1 -1
  133. package/dist/MetadataList/MetadataListItem.js +4 -1
  134. package/dist/MultiSelector/MultiSelector.d.ts +18 -1
  135. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  136. package/dist/MultiSelector/MultiSelector.js +58 -11
  137. package/dist/MultiSelector/hooks.d.ts +7 -1
  138. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  139. package/dist/MultiSelector/hooks.js +3 -2
  140. package/dist/NavIcon/NavIcon.d.ts.map +1 -1
  141. package/dist/NavIcon/NavIcon.js +5 -1
  142. package/dist/NavItem/navItemStyles.stylex.d.ts +0 -8
  143. package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
  144. package/dist/NavItem/navItemStyles.stylex.js +0 -2
  145. package/dist/NavMenu/NavHeadingMenu.d.ts +1 -1
  146. package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
  147. package/dist/NavMenu/NavHeadingMenu.js +7 -4
  148. package/dist/NumberInput/NumberInput.d.ts +6 -1
  149. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  150. package/dist/NumberInput/NumberInput.js +95 -126
  151. package/dist/NumberInput/numberInputCommit.d.ts +34 -0
  152. package/dist/NumberInput/numberInputCommit.d.ts.map +1 -0
  153. package/dist/NumberInput/numberInputCommit.js +71 -0
  154. package/dist/NumberInput/numberParser.d.ts +64 -0
  155. package/dist/NumberInput/numberParser.d.ts.map +1 -0
  156. package/dist/NumberInput/numberParser.js +350 -0
  157. package/dist/Outline/Outline.d.ts.map +1 -1
  158. package/dist/Outline/Outline.js +2 -3
  159. package/dist/OverflowList/OverflowList.d.ts +41 -1
  160. package/dist/OverflowList/OverflowList.d.ts.map +1 -1
  161. package/dist/OverflowList/OverflowList.js +43 -2
  162. package/dist/Popover/Popover.d.ts.map +1 -1
  163. package/dist/Popover/Popover.js +8 -14
  164. package/dist/Popover/usePopover.d.ts +15 -41
  165. package/dist/Popover/usePopover.d.ts.map +1 -1
  166. package/dist/Popover/usePopover.js +26 -7
  167. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  168. package/dist/PowerSearch/PowerSearchValueEditor.js +91 -18
  169. package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
  170. package/dist/PowerSearch/formatFilterValue.js +4 -2
  171. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  172. package/dist/ProgressBar/ProgressBar.js +18 -5
  173. package/dist/Resizable/useResizable.d.ts +19 -1
  174. package/dist/Resizable/useResizable.d.ts.map +1 -1
  175. package/dist/Resizable/useResizable.js +104 -38
  176. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  177. package/dist/SelectableCard/SelectableCard.js +9 -0
  178. package/dist/Selector/Selector.d.ts +18 -1
  179. package/dist/Selector/Selector.d.ts.map +1 -1
  180. package/dist/Selector/Selector.js +92 -21
  181. package/dist/Selector/hooks.d.ts +7 -1
  182. package/dist/Selector/hooks.d.ts.map +1 -1
  183. package/dist/Selector/hooks.js +3 -2
  184. package/dist/SideNav/SideNav.d.ts +6 -1
  185. package/dist/SideNav/SideNav.d.ts.map +1 -1
  186. package/dist/SideNav/SideNav.js +57 -27
  187. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  188. package/dist/SideNav/SideNavHeading.js +5 -4
  189. package/dist/SideNav/SideNavItem.d.ts +29 -4
  190. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  191. package/dist/SideNav/SideNavItem.js +75 -20
  192. package/dist/Slider/Slider.d.ts.map +1 -1
  193. package/dist/Slider/Slider.js +34 -2
  194. package/dist/Spinner/Spinner.d.ts +3 -1
  195. package/dist/Spinner/Spinner.d.ts.map +1 -1
  196. package/dist/Spinner/Spinner.js +199 -20
  197. package/dist/StatusDot/StatusDot.d.ts.map +1 -1
  198. package/dist/StatusDot/StatusDot.js +6 -1
  199. package/dist/Stepper/Step.d.ts.map +1 -1
  200. package/dist/Stepper/Step.js +7 -6
  201. package/dist/Switch/Switch.d.ts.map +1 -1
  202. package/dist/Switch/Switch.js +5 -1
  203. package/dist/Table/BaseTable.d.ts.map +1 -1
  204. package/dist/Table/BaseTable.js +6 -1
  205. package/dist/Table/Table.d.ts.map +1 -1
  206. package/dist/Table/Table.js +3 -6
  207. package/dist/Table/plugins/filtering/useTableFiltering.d.ts.map +1 -1
  208. package/dist/Table/plugins/filtering/useTableFiltering.js +73 -4
  209. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts +26 -0
  210. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  211. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +34 -3
  212. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  213. package/dist/Table/plugins/selection/useTableSelection.js +25 -1
  214. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -1
  215. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +3 -4
  216. package/dist/Text/Text.d.ts.map +1 -1
  217. package/dist/Text/Text.js +0 -1
  218. package/dist/TextArea/TextArea.d.ts.map +1 -1
  219. package/dist/TextArea/TextArea.js +13 -8
  220. package/dist/Timestamp/Timestamp.d.ts +1 -1
  221. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  222. package/dist/Timestamp/Timestamp.js +10 -18
  223. package/dist/Timestamp/formatInstant.d.ts.map +1 -1
  224. package/dist/Timestamp/formatInstant.js +10 -5
  225. package/dist/Toast/Toast.d.ts +17 -2
  226. package/dist/Toast/Toast.d.ts.map +1 -1
  227. package/dist/Toast/Toast.js +77 -11
  228. package/dist/Toast/ToastViewport.d.ts +1 -0
  229. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  230. package/dist/Toast/ToastViewport.js +76 -21
  231. package/dist/Toast/index.d.ts +1 -1
  232. package/dist/Toast/index.d.ts.map +1 -1
  233. package/dist/Toast/types.d.ts +38 -0
  234. package/dist/Toast/types.d.ts.map +1 -1
  235. package/dist/Toast/useToastGesture.d.ts +26 -0
  236. package/dist/Toast/useToastGesture.d.ts.map +1 -0
  237. package/dist/Toast/useToastGesture.js +223 -0
  238. package/dist/Token/Token.d.ts +1 -1
  239. package/dist/Token/Token.d.ts.map +1 -1
  240. package/dist/Token/Token.js +23 -12
  241. package/dist/Token/TokenLink.d.ts +1 -1
  242. package/dist/Token/TokenLink.d.ts.map +1 -1
  243. package/dist/Token/TokenLink.js +5 -2
  244. package/dist/Tokenizer/Tokenizer.d.ts +12 -1
  245. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  246. package/dist/Tokenizer/Tokenizer.js +31 -21
  247. package/dist/TopNav/TopNavHeading.js +2 -2
  248. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  249. package/dist/TopNav/TopNavItem.js +3 -3
  250. package/dist/TopNav/TopNavMegaMenu.d.ts +1 -1
  251. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  252. package/dist/TopNav/TopNavMegaMenu.js +30 -9
  253. package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
  254. package/dist/TopNav/TopNavMegaMenuItem.js +3 -3
  255. package/dist/TopNav/TopNavMenu.d.ts +1 -1
  256. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  257. package/dist/TopNav/TopNavMenu.js +21 -6
  258. package/dist/TreeList/TreeList.d.ts +1 -1
  259. package/dist/TreeList/TreeList.d.ts.map +1 -1
  260. package/dist/TreeList/TreeList.js +7 -2
  261. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  262. package/dist/TreeList/TreeListItem.js +2 -2
  263. package/dist/Typeahead/BaseTypeahead.d.ts +33 -0
  264. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  265. package/dist/Typeahead/BaseTypeahead.js +51 -8
  266. package/dist/Typeahead/Typeahead.d.ts +8 -1
  267. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  268. package/dist/Typeahead/Typeahead.js +2 -0
  269. package/dist/Typeahead/TypeaheadItem.d.ts +1 -1
  270. package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
  271. package/dist/Typeahead/TypeaheadItem.js +7 -2
  272. package/dist/astryx.css +87 -22
  273. package/dist/hooks/useHotkeys.d.ts.map +1 -1
  274. package/dist/hooks/useHotkeys.js +3 -25
  275. package/dist/hooks/useOverflow.d.ts.map +1 -1
  276. package/dist/hooks/useOverflow.js +14 -4
  277. package/dist/i18n/InternationalizationContext.d.ts +2 -0
  278. package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
  279. package/dist/i18n/InternationalizationContext.js +3 -1
  280. package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
  281. package/dist/i18n/InternationalizationProvider.js +3 -1
  282. package/dist/i18n/resolve.d.ts +1 -1
  283. package/dist/i18n/resolve.d.ts.map +1 -1
  284. package/dist/i18n/resolve.js +37 -38
  285. package/dist/i18n/useTranslator.d.ts.map +1 -1
  286. package/dist/i18n/useTranslator.js +2 -3
  287. package/dist/index.d.ts +1 -1
  288. package/dist/index.d.ts.map +1 -1
  289. package/dist/theme/Theme.d.ts.map +1 -1
  290. package/dist/theme/Theme.js +46 -10
  291. package/dist/theme/defineTheme.d.ts +9 -3
  292. package/dist/theme/defineTheme.d.ts.map +1 -1
  293. package/dist/theme/defineTheme.js +5 -0
  294. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  295. package/dist/theme/derivedVarRegistry.js +24 -4
  296. package/dist/theme/generateThemeRules.d.ts +21 -0
  297. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  298. package/dist/theme/generateThemeRules.js +26 -0
  299. package/dist/theme/syntax/tokens.d.ts +1 -1
  300. package/dist/theme/syntax/tokens.d.ts.map +1 -1
  301. package/dist/theme/syntax/tokens.js +5 -2
  302. package/dist/theme/useTheme.d.ts.map +1 -1
  303. package/dist/theme/useTheme.js +6 -7
  304. package/dist/utils/dateParser.d.ts +5 -8
  305. package/dist/utils/dateParser.d.ts.map +1 -1
  306. package/dist/utils/dateParser.js +10 -11
  307. package/dist/utils/focusOutline.stylex.d.ts +25 -0
  308. package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
  309. package/dist/utils/focusOutline.stylex.js +15 -0
  310. package/dist/utils/interactionOverlay.stylex.d.ts +26 -0
  311. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -0
  312. package/dist/utils/interactionOverlay.stylex.js +42 -0
  313. package/dist/utils/isApplePlatform.d.ts +18 -0
  314. package/dist/utils/isApplePlatform.d.ts.map +1 -0
  315. package/dist/utils/isApplePlatform.js +37 -0
  316. package/dist/utils/plainDate.d.ts +11 -0
  317. package/dist/utils/plainDate.d.ts.map +1 -1
  318. package/dist/utils/plainDate.js +20 -4
  319. package/locales/af-ZA.json +7 -7
  320. package/locales/ar-SA.json +7 -7
  321. package/locales/ca-ES.json +6 -6
  322. package/locales/cs-CZ.json +7 -7
  323. package/locales/da-DK.json +7 -7
  324. package/locales/de-DE.json +7 -7
  325. package/locales/el-GR.json +7 -7
  326. package/locales/en.json +178 -6
  327. package/locales/es-ES.json +6 -6
  328. package/locales/fi-FI.json +7 -7
  329. package/locales/fr-FR.json +7 -7
  330. package/locales/he-IL.json +7 -7
  331. package/locales/hu-HU.json +7 -7
  332. package/locales/it-IT.json +7 -7
  333. package/locales/ja-JP.json +7 -7
  334. package/locales/ko-KR.json +7 -7
  335. package/locales/nl-NL.json +7 -7
  336. package/locales/no-NO.json +7 -7
  337. package/locales/pl-PL.json +7 -7
  338. package/locales/pseudo.json +130 -1
  339. package/locales/pt-BR.json +7 -7
  340. package/locales/pt-PT.json +7 -7
  341. package/locales/ro-RO.json +7 -7
  342. package/locales/ru-RU.json +7 -7
  343. package/locales/sr-SP.json +7 -7
  344. package/locales/sv-SE.json +7 -7
  345. package/locales/tr-TR.json +7 -7
  346. package/locales/uk-UA.json +7 -7
  347. package/locales/vi-VN.json +7 -7
  348. package/locales/zh-CN.json +7 -7
  349. package/locales/zh-TW.json +7 -7
  350. package/package.json +4 -5
  351. package/src/AlertDialog/AlertDialog.doc.mjs +114 -30
  352. package/src/AlertDialog/AlertDialog.test.tsx +211 -2
  353. package/src/AlertDialog/AlertDialog.tsx +62 -20
  354. package/src/AspectRatio/AspectRatio.doc.mjs +3 -3
  355. package/src/AspectRatio/AspectRatio.tsx +6 -6
  356. package/src/Avatar/Avatar.doc.mjs +12 -2
  357. package/src/Avatar/Avatar.test.tsx +80 -0
  358. package/src/Avatar/Avatar.tsx +64 -14
  359. package/src/Avatar/index.ts +2 -2
  360. package/src/AvatarGroup/AvatarGroup.doc.mjs +8 -2
  361. package/src/AvatarGroup/AvatarGroup.test.tsx +24 -0
  362. package/src/AvatarGroup/AvatarGroup.tsx +12 -4
  363. package/src/AvatarGroup/AvatarGroupContext.ts +5 -3
  364. package/src/AvatarGroup/AvatarGroupOverflow.tsx +12 -12
  365. package/src/Badge/Badge.doc.mjs +3 -2
  366. package/src/Badge/Badge.test.tsx +32 -3
  367. package/src/Badge/Badge.tsx +42 -1
  368. package/src/Banner/Banner.doc.mjs +9 -0
  369. package/src/Banner/Banner.test.tsx +76 -11
  370. package/src/Banner/Banner.tsx +21 -2
  371. package/src/Button/Button.doc.mjs +2 -1
  372. package/src/Button/Button.test.tsx +19 -4
  373. package/src/Button/Button.tsx +9 -29
  374. package/src/ButtonGroup/ButtonGroup.doc.mjs +2 -2
  375. package/src/ButtonGroup/ButtonGroup.test.tsx +13 -0
  376. package/src/Calendar/Calendar.doc.mjs +4 -4
  377. package/src/Calendar/Calendar.test.tsx +112 -0
  378. package/src/Calendar/Calendar.tsx +18 -16
  379. package/src/Calendar/hooks/useCalendarDays.ts +4 -4
  380. package/src/Calendar/hooks/useCalendarNavigation.ts +4 -4
  381. package/src/Calendar/styles.ts +48 -42
  382. package/src/Card/Card.doc.mjs +10 -7
  383. package/src/Card/Card.test.tsx +113 -19
  384. package/src/Card/Card.tsx +67 -89
  385. package/src/Card/index.ts +36 -1
  386. package/src/Carousel/Carousel.doc.mjs +3 -0
  387. package/src/Carousel/Carousel.test.tsx +360 -0
  388. package/src/Carousel/Carousel.tsx +116 -15
  389. package/src/CheckboxInput/CheckboxInput.doc.mjs +3 -1
  390. package/src/CheckboxInput/CheckboxInput.test.tsx +15 -0
  391. package/src/CheckboxInput/CheckboxInput.tsx +5 -0
  392. package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
  393. package/src/CodeBlock/CodeBlock.doc.mjs +10 -4
  394. package/src/CodeBlock/CodeBlock.test.tsx +37 -0
  395. package/src/CodeBlock/CodeBlock.tsx +20 -4
  396. package/src/CodeBlock/highlightRanges.ts +1 -1
  397. package/src/CodeBlock/highlightStyles.ts +13 -13
  398. package/src/CommandPalette/CommandPaletteItem.tsx +2 -12
  399. package/src/ComplexSelector/ComplexSelector.doc.mjs +9 -2
  400. package/src/ComplexSelector/ComplexSelector.test.tsx +183 -0
  401. package/src/ComplexSelector/ComplexSelector.tsx +22 -12
  402. package/src/DateInput/DateInput.doc.mjs +17 -1
  403. package/src/DateInput/DateInput.test.tsx +9 -4
  404. package/src/DateInput/DateInput.tsx +54 -3
  405. package/src/DateInput/DateInputTouch.test.tsx +146 -32
  406. package/src/DateInput/MonthScroller.tsx +5 -1
  407. package/src/DateInput/MonthYearWheels.tsx +7 -4
  408. package/src/DateInput/NativeDateField.test.tsx +635 -0
  409. package/src/DateInput/NativeDateField.tsx +641 -0
  410. package/src/DateInput/TouchDateField.tsx +83 -33
  411. package/src/DateInput/Wheel.tsx +1 -0
  412. package/src/DateInput/index.ts +1 -0
  413. package/src/DateInput/nativeDateSegments.ts +127 -0
  414. package/src/DateRangeInput/DateRangeInput.doc.mjs +3 -3
  415. package/src/DateRangeInput/DateRangeInput.test.tsx +46 -0
  416. package/src/DateRangeInput/DateRangeInput.tsx +8 -13
  417. package/src/DateTimeInput/DateTimeInput.doc.mjs +39 -22
  418. package/src/DateTimeInput/DateTimeInput.test.tsx +76 -0
  419. package/src/DateTimeInput/DateTimeInput.tsx +54 -20
  420. package/src/DateTimeInput/DateTimeInputTouch.test.tsx +980 -0
  421. package/src/DateTimeInput/TouchDateTimeField.tsx +1437 -0
  422. package/src/Dialog/Dialog.test.tsx +185 -18
  423. package/src/Dialog/Dialog.tsx +35 -11
  424. package/src/DropdownMenu/DropdownMenu.test.tsx +44 -10
  425. package/src/DropdownMenu/DropdownMenu.tsx +4 -13
  426. package/src/Field/Field.doc.mjs +1 -1
  427. package/src/Field/FieldLabel.test.tsx +29 -0
  428. package/src/Field/FieldLabel.tsx +7 -0
  429. package/src/Field/InputClearButton.test.tsx +15 -1
  430. package/src/Field/InputClearButton.tsx +28 -4
  431. package/src/FieldStatus/FieldStatus.doc.mjs +1 -1
  432. package/src/FileInput/FileInput.doc.mjs +1 -1
  433. package/src/Heading/Heading.tsx +0 -1
  434. package/src/HoverCard/HoverCard.doc.mjs +8 -2
  435. package/src/HoverCard/HoverCard.test.tsx +154 -4
  436. package/src/HoverCard/HoverCard.tsx +137 -3
  437. package/src/HoverCard/useHoverCard.tsx +23 -3
  438. package/src/Icon/Icon.test.tsx +55 -1
  439. package/src/Icon/Icon.tsx +5 -3
  440. package/src/Icon/defaultIcons.tsx +11 -2
  441. package/src/Icon/globalIconRegistry.test.tsx +16 -3
  442. package/src/Icon/globalIconRegistry.tsx +37 -6
  443. package/src/Icon/index.ts +1 -0
  444. package/src/Icon/useIcon.ts +9 -2
  445. package/src/Indicator/CheckboxIndicator.tsx +14 -8
  446. package/src/Indicator/Indicator.doc.mjs +3 -1
  447. package/src/InputGroup/InputGroup.doc.mjs +1 -1
  448. package/src/Item/Item.tsx +2 -7
  449. package/src/Kbd/Kbd.test.tsx +0 -17
  450. package/src/Kbd/Kbd.tsx +2 -26
  451. package/src/Layer/gestureCounter.ts +57 -0
  452. package/src/Layer/useLayer.test.tsx +368 -16
  453. package/src/Layer/useLayer.tsx +208 -35
  454. package/src/Layout/__tests__/edgeCompensation.test.tsx +1 -1
  455. package/src/List/List.test.tsx +38 -0
  456. package/src/List/List.tsx +8 -1
  457. package/src/Markdown/Markdown.test.tsx +37 -0
  458. package/src/Markdown/Markdown.tsx +16 -5
  459. package/src/Markdown/parser.test.ts +33 -0
  460. package/src/Markdown/parser.ts +9 -3
  461. package/src/MetadataList/MetadataList.test.tsx +75 -2
  462. package/src/MetadataList/MetadataListItem.tsx +5 -2
  463. package/src/MultiSelector/MultiSelector.doc.mjs +23 -2
  464. package/src/MultiSelector/MultiSelector.test.tsx +205 -0
  465. package/src/MultiSelector/MultiSelector.tsx +103 -16
  466. package/src/MultiSelector/hooks.ts +9 -2
  467. package/src/NavIcon/NavIcon.doc.mjs +8 -2
  468. package/src/NavIcon/NavIcon.test.tsx +9 -0
  469. package/src/NavIcon/NavIcon.tsx +5 -1
  470. package/src/NavItem/navItemStyles.stylex.ts +0 -8
  471. package/src/NavMenu/NavHeadingMenu.test.tsx +30 -0
  472. package/src/NavMenu/NavHeadingMenu.tsx +7 -4
  473. package/src/NumberInput/NumberInput.doc.mjs +27 -8
  474. package/src/NumberInput/NumberInput.test.tsx +563 -8
  475. package/src/NumberInput/NumberInput.tsx +77 -131
  476. package/src/NumberInput/numberInputCommit.test.ts +72 -0
  477. package/src/NumberInput/numberInputCommit.ts +103 -0
  478. package/src/NumberInput/numberParser.docblock.test.ts +234 -0
  479. package/src/NumberInput/numberParser.test.ts +222 -0
  480. package/src/NumberInput/numberParser.ts +403 -0
  481. package/src/Outline/Outline.tsx +2 -4
  482. package/src/OverflowList/OverflowList.doc.mjs +102 -21
  483. package/src/OverflowList/OverflowList.test.tsx +550 -4
  484. package/src/OverflowList/OverflowList.tsx +100 -2
  485. package/src/Pagination/Pagination.test.tsx +3 -2
  486. package/src/Popover/Popover.doc.mjs +2 -2
  487. package/src/Popover/Popover.test.tsx +38 -0
  488. package/src/Popover/Popover.tsx +28 -66
  489. package/src/Popover/usePopover.tsx +44 -5
  490. package/src/PowerSearch/PowerSearch.doc.mjs +1 -1
  491. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +87 -0
  492. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +64 -0
  493. package/src/PowerSearch/PowerSearchValueEditor.tsx +109 -10
  494. package/src/PowerSearch/formatFilterValue.test.ts +16 -1
  495. package/src/PowerSearch/formatFilterValue.ts +2 -0
  496. package/src/ProgressBar/ProgressBar.doc.mjs +42 -6
  497. package/src/ProgressBar/ProgressBar.test.tsx +24 -0
  498. package/src/ProgressBar/ProgressBar.tsx +28 -8
  499. package/src/RadioList/RadioList.doc.mjs +1 -1
  500. package/src/Resizable/Resizable.doc.mjs +2 -2
  501. package/src/Resizable/useResizable.doc.mjs +21 -2
  502. package/src/Resizable/useResizable.test.ts +421 -0
  503. package/src/Resizable/useResizable.ts +133 -48
  504. package/src/SelectableCard/SelectableCard.doc.mjs +3 -0
  505. package/src/SelectableCard/SelectableCard.tsx +16 -0
  506. package/src/Selector/Selector.doc.mjs +15 -1
  507. package/src/Selector/Selector.source-build.test.mjs +65 -0
  508. package/src/Selector/Selector.test.tsx +301 -0
  509. package/src/Selector/Selector.tsx +145 -32
  510. package/src/Selector/hooks.ts +17 -2
  511. package/src/SideNav/SideNav.doc.mjs +2 -2
  512. package/src/SideNav/SideNav.test.tsx +1069 -6
  513. package/src/SideNav/SideNav.tsx +109 -38
  514. package/src/SideNav/SideNavHeading.tsx +5 -2
  515. package/src/SideNav/SideNavItem.doc.mjs +24 -3
  516. package/src/SideNav/SideNavItem.tsx +134 -44
  517. package/src/Slider/Slider.doc.mjs +2 -2
  518. package/src/Slider/Slider.test.tsx +106 -1
  519. package/src/Slider/Slider.tsx +52 -1
  520. package/src/Spinner/Spinner.doc.mjs +17 -5
  521. package/src/Spinner/Spinner.test.tsx +164 -0
  522. package/src/Spinner/Spinner.tsx +252 -22
  523. package/src/StatusDot/StatusDot.doc.mjs +8 -2
  524. package/src/StatusDot/StatusDot.test.tsx +9 -0
  525. package/src/StatusDot/StatusDot.tsx +7 -1
  526. package/src/Stepper/Step.doc.mjs +1 -1
  527. package/src/Stepper/Step.tsx +7 -14
  528. package/src/Switch/Switch.doc.mjs +3 -1
  529. package/src/Switch/Switch.test.tsx +10 -0
  530. package/src/Switch/Switch.tsx +3 -0
  531. package/src/Table/BaseTable.tsx +6 -1
  532. package/src/Table/Table.doc.mjs +5 -3
  533. package/src/Table/Table.tsx +3 -8
  534. package/src/Table/TableBody.doc.mjs +2 -2
  535. package/src/Table/TableFooter.doc.mjs +1 -1
  536. package/src/Table/TableHeader.doc.mjs +1 -1
  537. package/src/Table/plugins/filtering/useTableFiltering.test.tsx +44 -6
  538. package/src/Table/plugins/filtering/useTableFiltering.tsx +100 -4
  539. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +50 -4
  540. package/src/Table/plugins/selection/useTableSelection.test.tsx +62 -0
  541. package/src/Table/plugins/selection/useTableSelection.tsx +25 -2
  542. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +1 -2
  543. package/src/Table/useTableGroupedRows.doc.mjs +3 -3
  544. package/src/Table/useTablePagination.doc.mjs +1 -1
  545. package/src/Table/useTableSelection.doc.mjs +1 -1
  546. package/src/Text/Text.test.tsx +44 -0
  547. package/src/Text/Text.tsx +0 -1
  548. package/src/TextArea/TextArea.doc.mjs +13 -3
  549. package/src/TextArea/TextArea.test.tsx +11 -0
  550. package/src/TextArea/TextArea.tsx +34 -22
  551. package/src/TextInput/TextInput.doc.mjs +1 -1
  552. package/src/TimeInput/TimeInput.doc.mjs +1 -1
  553. package/src/Timestamp/Timestamp.test.tsx +89 -0
  554. package/src/Timestamp/Timestamp.tsx +8 -8
  555. package/src/Timestamp/formatInstant.ts +5 -0
  556. package/src/Toast/Toast.doc.mjs +213 -44
  557. package/src/Toast/Toast.tsx +118 -23
  558. package/src/Toast/ToastViewport.test.tsx +1434 -8
  559. package/src/Toast/ToastViewport.tsx +85 -14
  560. package/src/Toast/index.ts +2 -0
  561. package/src/Toast/types.ts +43 -5
  562. package/src/Toast/useToastGesture.ts +361 -0
  563. package/src/Token/Token.test.tsx +95 -0
  564. package/src/Token/Token.tsx +19 -17
  565. package/src/Token/TokenLink.tsx +12 -3
  566. package/src/Tokenizer/Tokenizer.doc.mjs +17 -4
  567. package/src/Tokenizer/Tokenizer.test.tsx +161 -0
  568. package/src/Tokenizer/Tokenizer.tsx +49 -24
  569. package/src/TopNav/TopNavHeading.tsx +2 -2
  570. package/src/TopNav/TopNavItem.tsx +3 -7
  571. package/src/TopNav/TopNavMegaMenu.test.tsx +104 -0
  572. package/src/TopNav/TopNavMegaMenu.tsx +47 -4
  573. package/src/TopNav/TopNavMegaMenuItem.tsx +10 -9
  574. package/src/TopNav/TopNavMenu.test.tsx +133 -1
  575. package/src/TopNav/TopNavMenu.tsx +38 -5
  576. package/src/TreeList/TreeList.test.tsx +49 -0
  577. package/src/TreeList/TreeList.tsx +7 -2
  578. package/src/TreeList/TreeListItem.tsx +2 -7
  579. package/src/Typeahead/BaseTypeahead.doc.mjs +13 -0
  580. package/src/Typeahead/BaseTypeahead.tsx +96 -6
  581. package/src/Typeahead/Typeahead.doc.mjs +7 -1
  582. package/src/Typeahead/Typeahead.test.tsx +154 -0
  583. package/src/Typeahead/Typeahead.tsx +9 -0
  584. package/src/Typeahead/TypeaheadItem.test.tsx +43 -0
  585. package/src/Typeahead/TypeaheadItem.tsx +8 -1
  586. package/src/hooks/useAnnounce.doc.mjs +96 -0
  587. package/src/hooks/useDevWarning.doc.mjs +96 -0
  588. package/src/hooks/useHotkeys.test.ts +0 -18
  589. package/src/hooks/useHotkeys.ts +3 -25
  590. package/src/hooks/useIndicatorFocusRing.doc.mjs +104 -0
  591. package/src/hooks/useInputStatusIcon.doc.mjs +142 -0
  592. package/src/hooks/useInteractiveRole.doc.mjs +119 -0
  593. package/src/hooks/useLongPress.doc.mjs +123 -0
  594. package/src/hooks/useOverflow.doc.mjs +58 -17
  595. package/src/hooks/useOverflow.ts +16 -4
  596. package/src/hooks/useTypeahead.doc.mjs +153 -0
  597. package/src/i18n/InternationalizationContext.ts +3 -0
  598. package/src/i18n/InternationalizationProvider.tsx +8 -1
  599. package/src/i18n/__tests__/resolve.test.ts +23 -1
  600. package/src/i18n/resolve.ts +46 -49
  601. package/src/i18n/useTranslator.ts +2 -7
  602. package/src/index.ts +2 -0
  603. package/src/raw-import.d.ts +14 -0
  604. package/src/theme/Theme.test.tsx +104 -2
  605. package/src/theme/Theme.tsx +49 -14
  606. package/src/theme/defineTheme.test.ts +23 -0
  607. package/src/theme/defineTheme.ts +12 -3
  608. package/src/theme/derivedVarRegistry.test.ts +87 -3
  609. package/src/theme/derivedVarRegistry.ts +23 -4
  610. package/src/theme/generateThemeRules.test.ts +99 -1
  611. package/src/theme/generateThemeRules.ts +28 -0
  612. package/src/theme/syntax/tokens.ts +5 -2
  613. package/src/theme/useTheme.test.tsx +10 -0
  614. package/src/theme/useTheme.ts +16 -10
  615. package/src/utils/dateParser.test.ts +34 -2
  616. package/src/utils/dateParser.ts +11 -14
  617. package/src/utils/focusOutline.stylex.ts +50 -0
  618. package/src/utils/interactionOverlay.stylex.ts +74 -0
  619. package/src/utils/isApplePlatform.test.ts +61 -0
  620. package/src/utils/isApplePlatform.ts +37 -0
  621. package/src/utils/plainDate.test.ts +46 -0
  622. package/src/utils/plainDate.ts +18 -3
@@ -12,8 +12,17 @@
12
12
  * announcement flow changes, update these tests
13
13
  */
14
14
 
15
- import {describe, it, expect, vi, beforeAll, afterEach} from 'vitest';
16
15
  import {
16
+ describe,
17
+ it,
18
+ expect,
19
+ expectTypeOf,
20
+ vi,
21
+ beforeAll,
22
+ afterEach,
23
+ } from 'vitest';
24
+ import {
25
+ cleanup,
17
26
  render,
18
27
  screen,
19
28
  fireEvent,
@@ -22,7 +31,10 @@ import {
22
31
  within,
23
32
  } from '@testing-library/react';
24
33
  import React from 'react';
34
+ import {readFileSync} from 'node:fs';
25
35
  import {type AnnounceFn, __resetLiveRegionsForTest} from '../hooks/useAnnounce';
36
+ import {Button} from '../Button';
37
+ import {Toast, type ToastProps} from './Toast';
26
38
  import {ToastViewport} from './ToastViewport';
27
39
  import {useToast} from './useToast';
28
40
  import type {ToastOptions} from './types';
@@ -59,6 +71,10 @@ beforeAll(() => {
59
71
  HTMLElement.prototype.showPopover = vi.fn();
60
72
  HTMLElement.prototype.hidePopover = vi.fn();
61
73
  }
74
+ if (typeof HTMLElement.prototype.setPointerCapture === 'undefined') {
75
+ HTMLElement.prototype.setPointerCapture = vi.fn();
76
+ HTMLElement.prototype.releasePointerCapture = vi.fn();
77
+ }
62
78
  });
63
79
 
64
80
  // Toast text is mirrored into the singleton live regions, which outlive each
@@ -66,6 +82,7 @@ beforeAll(() => {
66
82
  afterEach(() => {
67
83
  __resetLiveRegionsForTest();
68
84
  announceSpy.mockClear();
85
+ vi.unstubAllGlobals();
69
86
  });
70
87
 
71
88
  // Module-level constant default props (avoids unstable-default-props lint).
@@ -89,11 +106,34 @@ function ShowToastButton({
89
106
  const INFO_A: ToastOptions = {body: 'Toast A'};
90
107
  const INFO_B: ToastOptions = {body: 'Toast B'};
91
108
  const AUTO_TOAST: ToastOptions = {body: 'Auto toast', autoHideDuration: 3000};
109
+ const SWIPE_TOAST: ToastOptions = {body: 'Swipe toast', isAutoHide: false};
110
+ const LONG_TOAST_BODY =
111
+ 'Arbeitsbereichsbenachrichtigungseinstellungen gespeichert';
92
112
 
93
113
  function renderViewport(children: React.ReactNode) {
94
114
  return render(<ToastViewport isTopLayer={false}>{children}</ToastViewport>);
95
115
  }
96
116
 
117
+ function getToastWrapperByText(text: string): HTMLElement {
118
+ const visualToast = screen.getByText(text).closest('[data-type]');
119
+ if (!(visualToast instanceof HTMLElement)) {
120
+ throw new Error(`Toast visual for ${text} not found`);
121
+ }
122
+ const wrapper = visualToast.closest('[data-toast-id]');
123
+ if (!(wrapper instanceof HTMLElement)) {
124
+ throw new Error(`Toast wrapper for ${text} not found`);
125
+ }
126
+ return wrapper;
127
+ }
128
+
129
+ function getVisualToastByText(text: string): HTMLElement {
130
+ const visualToast = screen.getByText(text).closest('[data-type]');
131
+ if (!(visualToast instanceof HTMLElement)) {
132
+ throw new Error(`Toast visual for ${text} not found`);
133
+ }
134
+ return visualToast;
135
+ }
136
+
97
137
  // Fire the transition-end that ToastViewport listens for to unmount an
98
138
  // exiting toast (jsdom does not run CSS transitions).
99
139
  function completeExit(toastId: string) {
@@ -105,6 +145,274 @@ function completeExit(toastId: string) {
105
145
  }
106
146
  }
107
147
 
148
+ describe('Toast responsive layout', () => {
149
+ it('keeps placement on ToastViewport rather than exposing a non-positioning Toast prop', () => {
150
+ expectTypeOf<ToastProps>().not.toHaveProperty('position');
151
+ expectTypeOf<React.ComponentProps<typeof ToastViewport>>().toHaveProperty(
152
+ 'position',
153
+ );
154
+ });
155
+
156
+ it('keeps real trailing controls on the first line while long body text wraps', () => {
157
+ renderViewport(
158
+ <ShowToastButton
159
+ options={{
160
+ body: LONG_TOAST_BODY,
161
+ isAutoHide: false,
162
+ endContent: (
163
+ <Button
164
+ label="Änderungen wiederherstellen"
165
+ variant="secondary"
166
+ size="lg"
167
+ />
168
+ ),
169
+ }}
170
+ />,
171
+ );
172
+ act(() => {
173
+ fireEvent.click(screen.getByText('Trigger'));
174
+ });
175
+
176
+ const viewport = screen.getByRole('region', {name: 'Notifications'});
177
+ const body = within(viewport).getByText(LONG_TOAST_BODY);
178
+ const toast = body.closest('[data-type]') as HTMLElement;
179
+ const mediaWrapper = toast.firstElementChild as HTMLElement;
180
+ const layoutRow = mediaWrapper.firstElementChild as HTMLElement;
181
+ const endArea = body.nextElementSibling as HTMLElement;
182
+ const actionControl = endArea.firstElementChild as HTMLElement;
183
+ const dismissControl = within(endArea).getByRole('button', {
184
+ name: 'Dismiss notification',
185
+ });
186
+
187
+ expect(getComputedStyle(layoutRow).flexWrap).toBe('nowrap');
188
+ expect(getComputedStyle(layoutRow).alignItems).toBe('flex-start');
189
+ expect(getComputedStyle(body).minWidth).toBe('0');
190
+ expect(getComputedStyle(body).overflowWrap).toBe('anywhere');
191
+ expect(getComputedStyle(endArea).alignItems).toBe('center');
192
+ expect(getComputedStyle(endArea).height).toBe(
193
+ 'calc(var(--text-body-size) * var(--text-body-leading))',
194
+ );
195
+ expect(getComputedStyle(actionControl).height).toBe(
196
+ 'var(--size-element-lg)',
197
+ );
198
+ expect(getComputedStyle(dismissControl).height).toBe(
199
+ 'var(--size-element-sm)',
200
+ );
201
+ });
202
+
203
+ it('uses the viewport as the inline constraint for edge placements', () => {
204
+ renderViewport(<ShowToastButton options={INFO_A} />);
205
+ act(() => {
206
+ fireEvent.click(screen.getByText('Trigger'));
207
+ });
208
+
209
+ const viewport = screen.getByRole('region', {name: 'Notifications'});
210
+
211
+ const style = getComputedStyle(viewport);
212
+ const viewportSource = readFileSync(
213
+ 'packages/core/src/Toast/ToastViewport.tsx',
214
+ 'utf8',
215
+ );
216
+ expect(viewportSource).not.toContain('100lvh - 100dvh');
217
+ expect(style.boxSizing).toBe('border-box');
218
+ expect(style.width).not.toBe('100%');
219
+ expect(style.paddingInlineEnd).not.toBe('0px');
220
+ expect(style.insetInlineStart).toBe('0');
221
+ expect(style.insetInlineEnd).toBe('0');
222
+ });
223
+
224
+ it('preserves custom start and end insets in LTR and RTL without forcing full width', () => {
225
+ const renderWithDirection = (direction: 'ltr' | 'rtl') =>
226
+ render(
227
+ <div dir={direction}>
228
+ <ToastViewport
229
+ isTopLayer={false}
230
+ inset={{bottom: 24, start: 24, end: 40}}>
231
+ <ShowToastButton options={INFO_A} />
232
+ </ToastViewport>
233
+ </div>,
234
+ );
235
+
236
+ renderWithDirection('ltr');
237
+ act(() => {
238
+ fireEvent.click(screen.getByText('Trigger'));
239
+ });
240
+ let viewport = screen.getByRole('region', {name: 'Notifications'});
241
+ expect(viewport.style.bottom).toBe('24px');
242
+ expect(viewport.style.insetInlineStart).toBe('24px');
243
+ expect(viewport.style.insetInlineEnd).toBe('40px');
244
+ expect(getComputedStyle(viewport).width).not.toBe('100%');
245
+
246
+ cleanup();
247
+ renderWithDirection('rtl');
248
+ act(() => {
249
+ fireEvent.click(screen.getByText('Trigger'));
250
+ });
251
+ viewport = screen.getByRole('region', {name: 'Notifications'});
252
+ expect(viewport.style.bottom).toBe('24px');
253
+ expect(viewport.style.insetInlineStart).toBe('24px');
254
+ expect(viewport.style.insetInlineEnd).toBe('40px');
255
+ expect(getComputedStyle(viewport).width).not.toBe('100%');
256
+ });
257
+ });
258
+
259
+ describe('ToastViewport placement', () => {
260
+ function renderPlacement({
261
+ position,
262
+ triggerLabel = 'Trigger',
263
+ body = 'Placed',
264
+ }: {
265
+ position?: React.ComponentProps<typeof ToastViewport>['position'];
266
+ triggerLabel?: string;
267
+ body?: string;
268
+ } = {}) {
269
+ const result = render(
270
+ <ToastViewport isTopLayer={false} position={position}>
271
+ <ShowToastButton
272
+ options={{body, isAutoHide: false}}
273
+ triggerLabel={triggerLabel}
274
+ />
275
+ </ToastViewport>,
276
+ );
277
+ act(() => {
278
+ fireEvent.click(screen.getByText(triggerLabel));
279
+ });
280
+ return {
281
+ ...result,
282
+ viewport: screen.getByRole('region', {name: 'Notifications'}),
283
+ };
284
+ }
285
+
286
+ it('defaults to bottomEnd with an end-aligned full-inline viewport', () => {
287
+ const {viewport} = renderPlacement();
288
+
289
+ expect(getComputedStyle(viewport).bottom).toBe('0px');
290
+ expect(getComputedStyle(viewport).alignItems).toBe('flex-end');
291
+ expect(getComputedStyle(viewport).width).not.toBe('100%');
292
+ });
293
+
294
+ it('maps explicit top and bottom placements to their configured edge', () => {
295
+ const top = renderPlacement({position: 'topEnd'});
296
+ expect(getComputedStyle(top.viewport).top).toBe('0px');
297
+ expect(getComputedStyle(top.viewport).alignItems).toBe('flex-end');
298
+ expect(getComputedStyle(top.viewport).flexDirection).toBe('column-reverse');
299
+ top.unmount();
300
+
301
+ const bottom = renderPlacement({
302
+ position: 'bottomStart',
303
+ triggerLabel: 'Bottom trigger',
304
+ body: 'Bottom start',
305
+ });
306
+ expect(getComputedStyle(bottom.viewport).bottom).toBe('0px');
307
+ expect(getComputedStyle(bottom.viewport).alignItems).toBe('flex-start');
308
+ bottom.unmount();
309
+ });
310
+ });
311
+
312
+ it('keeps newest toasts nearest the configured edge in wide stacks', () => {
313
+ render(
314
+ <>
315
+ <ToastViewport isTopLayer={false} position="bottomEnd" maxVisible={3}>
316
+ <ShowToastButton
317
+ options={{body: 'Bottom first', isAutoHide: false}}
318
+ triggerLabel="Bottom first"
319
+ />
320
+ <ShowToastButton
321
+ options={{body: 'Bottom second', isAutoHide: false}}
322
+ triggerLabel="Bottom second"
323
+ />
324
+ <ShowToastButton
325
+ options={{body: 'Bottom third', isAutoHide: false}}
326
+ triggerLabel="Bottom third"
327
+ />
328
+ </ToastViewport>
329
+ <ToastViewport isTopLayer={false} position="topEnd" maxVisible={3}>
330
+ <ShowToastButton
331
+ options={{body: 'Top first', isAutoHide: false}}
332
+ triggerLabel="Top first"
333
+ />
334
+ <ShowToastButton
335
+ options={{body: 'Top second', isAutoHide: false}}
336
+ triggerLabel="Top second"
337
+ />
338
+ <ShowToastButton
339
+ options={{body: 'Top third', isAutoHide: false}}
340
+ triggerLabel="Top third"
341
+ />
342
+ </ToastViewport>
343
+ </>,
344
+ );
345
+ for (const label of [
346
+ 'Bottom first',
347
+ 'Bottom second',
348
+ 'Bottom third',
349
+ 'Top first',
350
+ 'Top second',
351
+ 'Top third',
352
+ ]) {
353
+ act(() => {
354
+ fireEvent.click(screen.getByText(label));
355
+ });
356
+ }
357
+
358
+ const [bottom, top] = screen.getAllByRole('region', {
359
+ name: 'Notifications',
360
+ });
361
+ expect(getComputedStyle(bottom).flexDirection).toBe('column');
362
+ expect(
363
+ within(bottom)
364
+ .getAllByText(/Bottom/)
365
+ .map(el => el.textContent),
366
+ ).toEqual(['Bottom first', 'Bottom second', 'Bottom third']);
367
+ expect(getComputedStyle(top).flexDirection).toBe('column-reverse');
368
+ expect(
369
+ within(top)
370
+ .getAllByText(/Top/)
371
+ .map(el => el.textContent),
372
+ ).toEqual(['Top first', 'Top second', 'Top third']);
373
+ });
374
+
375
+ describe('ToastViewport visible limit', () => {
376
+ function renderMany(maxVisible?: number) {
377
+ render(
378
+ <ToastViewport isTopLayer={false} maxVisible={maxVisible}>
379
+ <ShowToastButton
380
+ options={{body: 'One', isAutoHide: false}}
381
+ triggerLabel="One"
382
+ />
383
+ <ShowToastButton
384
+ options={{body: 'Two', isAutoHide: false}}
385
+ triggerLabel="Two"
386
+ />
387
+ <ShowToastButton
388
+ options={{body: 'Three', isAutoHide: false}}
389
+ triggerLabel="Three"
390
+ />
391
+ </ToastViewport>,
392
+ );
393
+ act(() => {
394
+ fireEvent.click(screen.getByText('One'));
395
+ fireEvent.click(screen.getByText('Two'));
396
+ fireEvent.click(screen.getByText('Three'));
397
+ });
398
+ return screen.getByRole('region', {name: 'Notifications'});
399
+ }
400
+
401
+ it('keeps the five-toast default and supports an explicit one-visible cap', () => {
402
+ let viewport = renderMany();
403
+ expect(
404
+ within(viewport).getAllByRole('button', {name: 'Dismiss notification'}),
405
+ ).toHaveLength(3);
406
+
407
+ cleanup();
408
+ viewport = renderMany(1);
409
+ expect(
410
+ within(viewport).getAllByRole('button', {name: 'Dismiss notification'}),
411
+ ).toHaveLength(1);
412
+ expect(within(viewport).getByText('Three')).toBeInTheDocument();
413
+ });
414
+ });
415
+
108
416
  describe('ToastViewport keyboard reach + focus', () => {
109
417
  it('F6 moves focus into the newest toast', () => {
110
418
  renderViewport(
@@ -252,14 +560,928 @@ describe('Toast blur timer pause', () => {
252
560
  });
253
561
  });
254
562
 
563
+ describe('Toast native motion contract', () => {
564
+ function renderMotionToast(
565
+ position: React.ComponentProps<typeof ToastViewport>['position'],
566
+ body = `Motion ${position}`,
567
+ wrapper: (children: React.ReactNode) => React.ReactElement = children => (
568
+ <>{children}</>
569
+ ),
570
+ ) {
571
+ const result = render(
572
+ wrapper(
573
+ <ToastViewport isTopLayer={false} position={position} maxVisible={3}>
574
+ <ShowToastButton
575
+ options={{body, isAutoHide: false}}
576
+ triggerLabel={`Show ${position}`}
577
+ />
578
+ </ToastViewport>,
579
+ ),
580
+ );
581
+ act(() => {
582
+ fireEvent.click(screen.getByText(`Show ${position}`));
583
+ });
584
+ return {
585
+ ...result,
586
+ visualToast: getVisualToastByText(body),
587
+ wrapper: getToastWrapperByText(body),
588
+ };
589
+ }
590
+
591
+ const normalizeCssList = (value: string): string =>
592
+ value
593
+ .split(',')
594
+ .map(part => part.trim())
595
+ .join(', ');
596
+
597
+ const normalizeCssFunction = (value: string): string =>
598
+ value.replace(/,\s*/g, ', ');
599
+
600
+ it('uses tokenized transform and wrapper timing, with reduced motion kept eventful', () => {
601
+ const {visualToast, wrapper} = renderMotionToast('bottomEnd');
602
+ const toastStyle = getComputedStyle(visualToast);
603
+ const wrapperStyle = getComputedStyle(wrapper);
604
+
605
+ expect(normalizeCssFunction(toastStyle.transform)).toBe(
606
+ 'translateY(var(--_toast-swipe-y, 0px)) scale(var(--_toast-swipe-scale, 1))',
607
+ );
608
+ expect(normalizeCssList(toastStyle.transitionProperty)).toBe(
609
+ 'opacity, transform',
610
+ );
611
+ expect(toastStyle.transitionDuration).toBe('var(--duration-fast)');
612
+ expect(toastStyle.transitionTimingFunction).toBe('var(--ease-standard)');
613
+ expect(normalizeCssList(wrapperStyle.transitionProperty)).toBe(
614
+ 'grid-template-rows, padding',
615
+ );
616
+ expect(wrapperStyle.transitionDuration).toBe('var(--duration-fast)');
617
+ expect(wrapperStyle.transitionTimingFunction).toBe('var(--ease-standard)');
618
+ expect(wrapperStyle.width).toBe('100%');
619
+ expect(wrapperStyle.maxWidth).toBe('400px');
620
+ expect(wrapperStyle.minWidth).toBe('0');
621
+ expect(
622
+ getComputedStyle(wrapper.firstElementChild as HTMLElement).minHeight,
623
+ ).toBe('0');
624
+ expect(
625
+ getComputedStyle(wrapper.firstElementChild as HTMLElement).overflow,
626
+ ).toBe('hidden');
627
+ });
628
+
629
+ it('keeps reduced motion transitions eventful for exit cleanup', () => {
630
+ const toastSource = readFileSync(
631
+ 'packages/core/src/Toast/Toast.tsx',
632
+ 'utf8',
633
+ );
634
+ const viewportSource = readFileSync(
635
+ 'packages/core/src/Toast/ToastViewport.tsx',
636
+ 'utf8',
637
+ );
638
+
639
+ expect(toastSource).toContain(
640
+ "'@media (prefers-reduced-motion: reduce)': '0.01ms'",
641
+ );
642
+ expect(viewportSource).toContain(
643
+ "'@media (prefers-reduced-motion: reduce)': '0.01ms'",
644
+ );
645
+ });
646
+
647
+ it('slides from the top or bottom edge without adding corner or scale motion', () => {
648
+ const bottom = renderMotionToast('bottomEnd');
649
+ expect(
650
+ getComputedStyle(bottom.wrapper).getPropertyValue('--_toast-slide-y'),
651
+ ).toBe('var(--spacing-2)');
652
+ bottom.unmount();
653
+
654
+ const top = renderMotionToast('topStart');
655
+ expect(
656
+ getComputedStyle(top.wrapper).getPropertyValue('--_toast-slide-y'),
657
+ ).toBe('calc(-1 * var(--spacing-2))');
658
+ top.unmount();
659
+
660
+ const toastSource = readFileSync(
661
+ 'packages/core/src/Toast/Toast.tsx',
662
+ 'utf8',
663
+ );
664
+ expect(toastSource).not.toContain('scale(0.98)');
665
+ expect(toastSource).not.toContain('transformOrigin');
666
+ });
667
+
668
+ it('uses an 8px inter-Toast gap without adding space at the viewport edge', () => {
669
+ renderViewport(
670
+ <>
671
+ <ShowToastButton options={INFO_A} triggerLabel="Show A" />
672
+ <ShowToastButton options={INFO_B} triggerLabel="Show B" />
673
+ </>,
674
+ );
675
+ act(() => {
676
+ fireEvent.click(screen.getByText('Show A'));
677
+ fireEvent.click(screen.getByText('Show B'));
678
+ });
679
+ const viewport = screen.getByRole('region', {name: 'Notifications'});
680
+ const wrappers = viewport.querySelectorAll<HTMLElement>('[data-toast-id]');
681
+
682
+ expect(getComputedStyle(wrappers[0]).paddingBottom).toBe(
683
+ 'var(--spacing-2)',
684
+ );
685
+ expect(getComputedStyle(wrappers[1]).paddingBottom).toBe('0px');
686
+ expect(
687
+ normalizeCssFunction(getComputedStyle(viewport).paddingInlineEnd),
688
+ ).toBe('max(var(--spacing-4), env(safe-area-inset-right, 0px))');
689
+
690
+ cleanup();
691
+ render(
692
+ <ToastViewport isTopLayer={false} position="topEnd">
693
+ <ShowToastButton options={INFO_A} triggerLabel="Top A" />
694
+ <ShowToastButton options={INFO_B} triggerLabel="Top B" />
695
+ </ToastViewport>,
696
+ );
697
+ act(() => {
698
+ fireEvent.click(screen.getByText('Top A'));
699
+ fireEvent.click(screen.getByText('Top B'));
700
+ });
701
+ const topWrappers = screen
702
+ .getByRole('region', {name: 'Notifications'})
703
+ .querySelectorAll<HTMLElement>('[data-toast-id]');
704
+ expect(getComputedStyle(topWrappers[0]).paddingBottom).toBe('0px');
705
+ expect(getComputedStyle(topWrappers[1]).paddingBottom).toBe(
706
+ 'var(--spacing-2)',
707
+ );
708
+ });
709
+
710
+ it('collapses a dismissed Toast before unmounting it', () => {
711
+ const {wrapper} = renderMotionToast('bottomEnd');
712
+ const id = wrapper.getAttribute('data-toast-id')!;
713
+ act(() => {
714
+ fireEvent.click(
715
+ within(wrapper).getByRole('button', {name: 'Dismiss notification'}),
716
+ );
717
+ });
718
+
719
+ expect(wrapper).toBeInTheDocument();
720
+ expect(getComputedStyle(wrapper).gridTemplateRows).toBe('0fr');
721
+ expect(getComputedStyle(wrapper).paddingBottom).toBe('0px');
722
+ act(() => {
723
+ completeExit(id);
724
+ });
725
+ expect(document.querySelector(`[data-toast-id="${id}"]`)).toBeNull();
726
+ });
727
+ });
728
+
729
+ describe('Toast swipe dismissal', () => {
730
+ function renderSwipeToast(
731
+ options: ToastOptions = SWIPE_TOAST,
732
+ position: React.ComponentProps<
733
+ typeof ToastViewport
734
+ >['position'] = 'bottomEnd',
735
+ bodyText = 'Swipe toast',
736
+ surfaceHeight = 80,
737
+ ) {
738
+ const onHide = vi.fn();
739
+ const result = render(
740
+ <ToastViewport isTopLayer={false} position={position}>
741
+ <ShowToastButton options={{...options, onHide}} />
742
+ </ToastViewport>,
743
+ );
744
+ act(() => {
745
+ fireEvent.click(screen.getByText('Trigger'));
746
+ });
747
+ const visualToast = screen
748
+ .getByText(bodyText)
749
+ .closest('[data-type]') as HTMLElement;
750
+ vi.spyOn(visualToast, 'getBoundingClientRect').mockReturnValue({
751
+ x: 0,
752
+ y: 0,
753
+ width: 400,
754
+ height: surfaceHeight,
755
+ top: 0,
756
+ right: 400,
757
+ bottom: surfaceHeight,
758
+ left: 0,
759
+ toJSON: () => ({}),
760
+ });
761
+ return {...result, visualToast, onHide};
762
+ }
763
+
764
+ it('dismisses on a pen swipe toward the configured block edge past the threshold', () => {
765
+ const {visualToast, onHide} = renderSwipeToast();
766
+
767
+ act(() => {
768
+ fireEvent.pointerDown(visualToast, {
769
+ pointerId: 7,
770
+ pointerType: 'pen',
771
+ button: 0,
772
+ clientX: 0,
773
+ clientY: 0,
774
+ });
775
+ fireEvent.pointerMove(visualToast, {
776
+ pointerId: 7,
777
+ pointerType: 'pen',
778
+ clientX: 0,
779
+ clientY: 60,
780
+ });
781
+ fireEvent.pointerUp(visualToast, {
782
+ pointerId: 7,
783
+ pointerType: 'pen',
784
+ clientX: 0,
785
+ clientY: 60,
786
+ });
787
+ });
788
+
789
+ expect(visualToast.setPointerCapture).toHaveBeenCalledWith(7);
790
+ expect(visualToast.releasePointerCapture).toHaveBeenCalledWith(7);
791
+ expect(onHide).toHaveBeenCalledWith('manual');
792
+ expect(onHide).toHaveBeenCalledTimes(1);
793
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-exit-y')).toBe(
794
+ '120%',
795
+ );
796
+ expect(getComputedStyle(visualToast).transform).not.toContain('translateX');
797
+ });
798
+
799
+ it('dismisses a fast flick below the distance threshold', () => {
800
+ const now = vi
801
+ .spyOn(Date, 'now')
802
+ .mockReturnValueOnce(1_000)
803
+ .mockReturnValueOnce(1_020);
804
+ try {
805
+ const {visualToast, onHide} = renderSwipeToast(
806
+ SWIPE_TOAST,
807
+ 'bottomEnd',
808
+ 'Swipe toast',
809
+ 400,
810
+ );
811
+
812
+ act(() => {
813
+ fireEvent.pointerDown(visualToast, {
814
+ pointerId: 8,
815
+ pointerType: 'pen',
816
+ button: 0,
817
+ clientX: 0,
818
+ clientY: 0,
819
+ });
820
+ fireEvent.pointerMove(visualToast, {
821
+ pointerId: 8,
822
+ pointerType: 'pen',
823
+ clientX: 0,
824
+ clientY: 60,
825
+ });
826
+ fireEvent.pointerUp(visualToast, {
827
+ pointerId: 8,
828
+ pointerType: 'pen',
829
+ clientX: 0,
830
+ clientY: 60,
831
+ });
832
+ });
833
+
834
+ expect(onHide).toHaveBeenCalledWith('manual');
835
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-exit-y')).toBe(
836
+ '120%',
837
+ );
838
+ } finally {
839
+ now.mockRestore();
840
+ }
841
+ });
842
+
843
+ it('snaps back without dismissing after a short drag', () => {
844
+ const {visualToast, onHide} = renderSwipeToast();
845
+
846
+ act(() => {
847
+ fireEvent.pointerDown(visualToast, {
848
+ pointerId: 7,
849
+ pointerType: 'pen',
850
+ button: 0,
851
+ clientX: 0,
852
+ clientY: 0,
853
+ });
854
+ fireEvent.pointerMove(visualToast, {
855
+ pointerId: 7,
856
+ pointerType: 'pen',
857
+ clientX: 0,
858
+ clientY: 24,
859
+ });
860
+ fireEvent.pointerUp(visualToast, {
861
+ pointerId: 7,
862
+ pointerType: 'pen',
863
+ clientX: 0,
864
+ clientY: 24,
865
+ });
866
+ });
867
+
868
+ expect(onHide).not.toHaveBeenCalled();
869
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
870
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
871
+ '',
872
+ );
873
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-scale')).toBe('');
874
+ });
875
+
876
+ it('fades and subtly scales only after accepted vertical edge swipe intent', () => {
877
+ const {visualToast} = renderSwipeToast();
878
+
879
+ act(() => {
880
+ fireEvent.pointerDown(visualToast, {
881
+ pointerId: 7,
882
+ pointerType: 'pen',
883
+ button: 0,
884
+ clientX: 0,
885
+ clientY: 0,
886
+ });
887
+ fireEvent.pointerMove(visualToast, {
888
+ pointerId: 7,
889
+ pointerType: 'pen',
890
+ clientX: 0,
891
+ clientY: -40,
892
+ });
893
+ });
894
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
895
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
896
+ '',
897
+ );
898
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-scale')).toBe('');
899
+ act(() => {
900
+ fireEvent.pointerCancel(visualToast, {
901
+ pointerId: 7,
902
+ pointerType: 'pen',
903
+ clientX: 0,
904
+ clientY: -40,
905
+ });
906
+ });
907
+
908
+ act(() => {
909
+ fireEvent.pointerDown(visualToast, {
910
+ pointerId: 8,
911
+ pointerType: 'pen',
912
+ button: 0,
913
+ clientX: 0,
914
+ clientY: 0,
915
+ });
916
+ fireEvent.pointerMove(visualToast, {
917
+ pointerId: 8,
918
+ pointerType: 'pen',
919
+ clientX: 0,
920
+ clientY: 40,
921
+ });
922
+ });
923
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('40px');
924
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
925
+ '0.800',
926
+ );
927
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-scale')).toBe(
928
+ '0.990',
929
+ );
930
+ });
931
+
932
+ it('does not fade for horizontal intent and resets swipe vars on pointercancel', () => {
933
+ const {visualToast, onHide} = renderSwipeToast();
934
+
935
+ act(() => {
936
+ fireEvent.pointerDown(visualToast, {
937
+ pointerId: 7,
938
+ pointerType: 'pen',
939
+ button: 0,
940
+ clientX: 0,
941
+ clientY: 0,
942
+ });
943
+ fireEvent.pointerMove(visualToast, {
944
+ pointerId: 7,
945
+ pointerType: 'pen',
946
+ clientX: 80,
947
+ clientY: 20,
948
+ });
949
+ });
950
+ expect(onHide).not.toHaveBeenCalled();
951
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
952
+ '',
953
+ );
954
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-scale')).toBe('');
955
+
956
+ act(() => {
957
+ fireEvent.pointerDown(visualToast, {
958
+ pointerId: 8,
959
+ pointerType: 'pen',
960
+ button: 0,
961
+ clientX: 0,
962
+ clientY: 0,
963
+ });
964
+ fireEvent.pointerMove(visualToast, {
965
+ pointerId: 8,
966
+ pointerType: 'pen',
967
+ clientX: 0,
968
+ clientY: 40,
969
+ });
970
+ });
971
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
972
+ '0.800',
973
+ );
974
+ act(() => {
975
+ fireEvent.pointerCancel(visualToast, {
976
+ pointerId: 8,
977
+ pointerType: 'pen',
978
+ clientX: 0,
979
+ clientY: 40,
980
+ });
981
+ });
982
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
983
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
984
+ '',
985
+ );
986
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-scale')).toBe('');
987
+ });
988
+
989
+ it('hands the opposite vertical direction back without moving the toast', () => {
990
+ const {visualToast, onHide} = renderSwipeToast(SWIPE_TOAST, 'bottomEnd');
991
+
992
+ act(() => {
993
+ fireEvent.pointerDown(visualToast, {
994
+ pointerId: 7,
995
+ pointerType: 'pen',
996
+ button: 0,
997
+ clientX: 0,
998
+ clientY: 100,
999
+ });
1000
+ fireEvent.pointerMove(visualToast, {
1001
+ pointerId: 7,
1002
+ pointerType: 'pen',
1003
+ clientX: 0,
1004
+ clientY: 40,
1005
+ });
1006
+ fireEvent.pointerUp(visualToast, {
1007
+ pointerId: 7,
1008
+ pointerType: 'pen',
1009
+ clientX: 0,
1010
+ clientY: 40,
1011
+ });
1012
+ });
1013
+
1014
+ expect(onHide).not.toHaveBeenCalled();
1015
+ });
1016
+
1017
+ it('keeps bottom placement dismissing downward under RTL', () => {
1018
+ const onHide = vi.fn();
1019
+ render(
1020
+ <div dir="rtl">
1021
+ <ToastViewport isTopLayer={false} position="bottomStart">
1022
+ <ShowToastButton options={{...SWIPE_TOAST, onHide}} />
1023
+ </ToastViewport>
1024
+ </div>,
1025
+ );
1026
+ act(() => {
1027
+ fireEvent.click(screen.getByText('Trigger'));
1028
+ });
1029
+ const visualToast = screen
1030
+ .getByText('Swipe toast')
1031
+ .closest('[data-type]') as HTMLElement;
1032
+ vi.spyOn(visualToast, 'getBoundingClientRect').mockReturnValue({
1033
+ x: 0,
1034
+ y: 0,
1035
+ width: 400,
1036
+ height: 80,
1037
+ top: 0,
1038
+ right: 400,
1039
+ bottom: 80,
1040
+ left: 0,
1041
+ toJSON: () => ({}),
1042
+ });
1043
+
1044
+ act(() => {
1045
+ fireEvent.pointerDown(visualToast, {
1046
+ pointerId: 7,
1047
+ pointerType: 'pen',
1048
+ button: 0,
1049
+ clientX: 0,
1050
+ clientY: 0,
1051
+ });
1052
+ fireEvent.pointerMove(visualToast, {
1053
+ pointerId: 7,
1054
+ pointerType: 'pen',
1055
+ clientX: 0,
1056
+ clientY: 60,
1057
+ });
1058
+ fireEvent.pointerUp(visualToast, {
1059
+ pointerId: 7,
1060
+ pointerType: 'pen',
1061
+ clientX: 0,
1062
+ clientY: 60,
1063
+ });
1064
+ });
1065
+
1066
+ expect(onHide).toHaveBeenCalledWith('manual');
1067
+ });
1068
+
1069
+ it('keeps top placement dismissing upward under RTL', () => {
1070
+ const onHide = vi.fn();
1071
+ render(
1072
+ <div dir="rtl">
1073
+ <ToastViewport isTopLayer={false} position="topEnd">
1074
+ <ShowToastButton options={{...SWIPE_TOAST, onHide}} />
1075
+ </ToastViewport>
1076
+ </div>,
1077
+ );
1078
+ act(() => {
1079
+ fireEvent.click(screen.getByText('Trigger'));
1080
+ });
1081
+ const visualToast = getVisualToastByText('Swipe toast');
1082
+ vi.spyOn(visualToast, 'getBoundingClientRect').mockReturnValue({
1083
+ x: 0,
1084
+ y: 0,
1085
+ width: 400,
1086
+ height: 80,
1087
+ top: 0,
1088
+ right: 400,
1089
+ bottom: 80,
1090
+ left: 0,
1091
+ toJSON: () => ({}),
1092
+ });
1093
+
1094
+ act(() => {
1095
+ fireEvent.pointerDown(visualToast, {
1096
+ pointerId: 7,
1097
+ pointerType: 'pen',
1098
+ button: 0,
1099
+ clientX: 0,
1100
+ clientY: 100,
1101
+ });
1102
+ fireEvent.pointerMove(visualToast, {
1103
+ pointerId: 7,
1104
+ pointerType: 'pen',
1105
+ clientX: 0,
1106
+ clientY: 40,
1107
+ });
1108
+ fireEvent.pointerUp(visualToast, {
1109
+ pointerId: 7,
1110
+ pointerType: 'pen',
1111
+ clientX: 0,
1112
+ clientY: 40,
1113
+ });
1114
+ });
1115
+
1116
+ expect(onHide).toHaveBeenCalledWith('manual');
1117
+ });
1118
+
1119
+ it('sets swipe exit to a vertical throw with no horizontal drift', () => {
1120
+ const {visualToast, onHide} = renderSwipeToast(
1121
+ SWIPE_TOAST,
1122
+ 'topEnd',
1123
+ 'Swipe toast',
1124
+ );
1125
+
1126
+ act(() => {
1127
+ fireEvent.pointerDown(visualToast, {
1128
+ pointerId: 7,
1129
+ pointerType: 'pen',
1130
+ button: 0,
1131
+ clientX: 0,
1132
+ clientY: 100,
1133
+ });
1134
+ fireEvent.pointerMove(visualToast, {
1135
+ pointerId: 7,
1136
+ pointerType: 'pen',
1137
+ clientX: 0,
1138
+ clientY: 40,
1139
+ });
1140
+ fireEvent.pointerUp(visualToast, {
1141
+ pointerId: 7,
1142
+ pointerType: 'pen',
1143
+ clientX: 0,
1144
+ clientY: 40,
1145
+ });
1146
+ });
1147
+
1148
+ expect(onHide).toHaveBeenCalledWith('manual');
1149
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
1150
+ '0.700',
1151
+ );
1152
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-scale')).toBe(
1153
+ '0.985',
1154
+ );
1155
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-exit-y')).toBe(
1156
+ 'calc(-1 * 120%)',
1157
+ );
1158
+ expect(getComputedStyle(visualToast).transform).not.toContain('translateX');
1159
+ });
1160
+
1161
+ it('allows native page scrolling until touch intent matches the dismiss edge', () => {
1162
+ const {visualToast, onHide} = renderSwipeToast(
1163
+ SWIPE_TOAST,
1164
+ 'topEnd',
1165
+ 'Swipe toast',
1166
+ );
1167
+ const touchEvent = (
1168
+ type: 'touchstart' | 'touchmove' | 'touchend',
1169
+ clientY: number,
1170
+ ) => {
1171
+ const event = new Event(type, {bubbles: true, cancelable: true});
1172
+ const touch = {identifier: 31, clientX: 10, clientY};
1173
+ Object.defineProperty(event, 'touches', {
1174
+ value: type === 'touchend' ? [] : [touch],
1175
+ });
1176
+ Object.defineProperty(event, 'changedTouches', {value: [touch]});
1177
+ visualToast.dispatchEvent(event);
1178
+ return event;
1179
+ };
1180
+
1181
+ act(() => {
1182
+ touchEvent('touchstart', 100);
1183
+ });
1184
+ let move: Event;
1185
+ act(() => {
1186
+ move = touchEvent('touchmove', 140);
1187
+ });
1188
+ expect(move!.defaultPrevented).toBe(false);
1189
+ expect(onHide).not.toHaveBeenCalled();
1190
+
1191
+ act(() => {
1192
+ touchEvent('touchstart', 100);
1193
+ });
1194
+ Object.defineProperty(
1195
+ (move = new Event('touchmove', {bubbles: true, cancelable: true})),
1196
+ 'changedTouches',
1197
+ {value: [{identifier: 31, clientX: 80, clientY: 104}]},
1198
+ );
1199
+ act(() => {
1200
+ visualToast.dispatchEvent(move);
1201
+ });
1202
+ expect(move.defaultPrevented).toBe(false);
1203
+
1204
+ act(() => {
1205
+ touchEvent('touchstart', 100);
1206
+ });
1207
+ act(() => {
1208
+ move = touchEvent('touchmove', 40);
1209
+ });
1210
+ expect(move!.defaultPrevented).toBe(true);
1211
+ act(() => {
1212
+ touchEvent('touchend', 40);
1213
+ });
1214
+ expect(onHide).toHaveBeenCalledWith('manual');
1215
+ });
1216
+
1217
+ it('resets an accepted touch gesture on native touchcancel', () => {
1218
+ const {visualToast, onHide} = renderSwipeToast();
1219
+ const touch = (type: 'touchstart' | 'touchmove', clientY: number) => {
1220
+ const event = new Event(type, {bubbles: true, cancelable: true});
1221
+ const point = {identifier: 44, clientX: 10, clientY};
1222
+ Object.defineProperty(event, 'touches', {value: [point]});
1223
+ Object.defineProperty(event, 'changedTouches', {value: [point]});
1224
+ visualToast.dispatchEvent(event);
1225
+ return event;
1226
+ };
1227
+
1228
+ act(() => {
1229
+ touch('touchstart', 0);
1230
+ });
1231
+ let move: Event;
1232
+ act(() => {
1233
+ move = touch('touchmove', 40);
1234
+ });
1235
+ expect(move!.defaultPrevented).toBe(true);
1236
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('40px');
1237
+
1238
+ act(() => {
1239
+ visualToast.dispatchEvent(
1240
+ new Event('touchcancel', {bubbles: true, cancelable: true}),
1241
+ );
1242
+ });
1243
+
1244
+ expect(onHide).not.toHaveBeenCalled();
1245
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
1246
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-opacity')).toBe(
1247
+ '',
1248
+ );
1249
+ });
1250
+
1251
+ it('removes native touch listeners when a Toast unmounts', () => {
1252
+ const {visualToast, unmount} = renderSwipeToast();
1253
+ const removeListener = vi.spyOn(visualToast, 'removeEventListener');
1254
+
1255
+ unmount();
1256
+
1257
+ for (const type of ['touchstart', 'touchmove', 'touchend', 'touchcancel']) {
1258
+ expect(removeListener).toHaveBeenCalledWith(type, expect.any(Function));
1259
+ }
1260
+ });
1261
+
1262
+ it('resets safely on pointercancel and resumes the auto-hide timer', () => {
1263
+ vi.useFakeTimers();
1264
+ try {
1265
+ const {visualToast, onHide} = renderSwipeToast({
1266
+ body: 'Swipe toast',
1267
+ autoHideDuration: 3000,
1268
+ });
1269
+
1270
+ act(() => {
1271
+ fireEvent.pointerDown(visualToast, {
1272
+ pointerId: 7,
1273
+ pointerType: 'pen',
1274
+ button: 0,
1275
+ clientX: 0,
1276
+ clientY: 0,
1277
+ });
1278
+ fireEvent.pointerMove(visualToast, {
1279
+ pointerId: 7,
1280
+ pointerType: 'pen',
1281
+ clientX: 0,
1282
+ clientY: 40,
1283
+ });
1284
+ vi.advanceTimersByTime(10_000);
1285
+ });
1286
+ expect(onHide).not.toHaveBeenCalled();
1287
+
1288
+ act(() => {
1289
+ fireEvent.pointerCancel(visualToast, {
1290
+ pointerId: 7,
1291
+ pointerType: 'pen',
1292
+ clientX: 0,
1293
+ clientY: 40,
1294
+ });
1295
+ vi.advanceTimersByTime(3_000);
1296
+ });
1297
+
1298
+ expect(onHide).toHaveBeenCalledWith('auto');
1299
+ } finally {
1300
+ vi.useRealTimers();
1301
+ }
1302
+ });
1303
+
1304
+ it('keeps horizontal pan intent and mouse drag from dismissing', () => {
1305
+ const {visualToast, onHide} = renderSwipeToast();
1306
+
1307
+ act(() => {
1308
+ fireEvent.pointerDown(visualToast, {
1309
+ pointerId: 7,
1310
+ pointerType: 'pen',
1311
+ button: 0,
1312
+ clientX: 0,
1313
+ clientY: 0,
1314
+ });
1315
+ fireEvent.pointerMove(visualToast, {
1316
+ pointerId: 7,
1317
+ pointerType: 'pen',
1318
+ clientX: 80,
1319
+ clientY: 20,
1320
+ });
1321
+ fireEvent.pointerUp(visualToast, {
1322
+ pointerId: 7,
1323
+ pointerType: 'pen',
1324
+ clientX: 220,
1325
+ clientY: 80,
1326
+ });
1327
+ fireEvent.pointerDown(visualToast, {
1328
+ pointerId: 8,
1329
+ pointerType: 'mouse',
1330
+ button: 0,
1331
+ clientX: 0,
1332
+ clientY: 0,
1333
+ });
1334
+ fireEvent.pointerMove(visualToast, {
1335
+ pointerId: 8,
1336
+ pointerType: 'mouse',
1337
+ clientX: 220,
1338
+ clientY: 0,
1339
+ });
1340
+ fireEvent.pointerUp(visualToast, {
1341
+ pointerId: 8,
1342
+ pointerType: 'mouse',
1343
+ clientX: 220,
1344
+ clientY: 0,
1345
+ });
1346
+ });
1347
+
1348
+ expect(onHide).not.toHaveBeenCalled();
1349
+ });
1350
+
1351
+ it('does not start a swipe from interactive descendants', () => {
1352
+ const onAction = vi.fn();
1353
+ const {visualToast, onHide} = renderSwipeToast({
1354
+ ...SWIPE_TOAST,
1355
+ endContent: (
1356
+ <>
1357
+ <Button label="Undo" size="sm" onClick={onAction} />
1358
+ <span role="switch" tabIndex={-1}>
1359
+ Mode
1360
+ </span>
1361
+ </>
1362
+ ),
1363
+ });
1364
+
1365
+ const targets = [
1366
+ screen.getByRole('button', {name: 'Undo'}),
1367
+ screen.getByRole('switch', {name: 'Mode'}),
1368
+ ];
1369
+ targets.forEach((target, index) => {
1370
+ const pointerId = 20 + index;
1371
+ act(() => {
1372
+ fireEvent.pointerDown(target, {
1373
+ pointerId,
1374
+ pointerType: 'pen',
1375
+ button: 0,
1376
+ clientX: 0,
1377
+ clientY: 0,
1378
+ });
1379
+ fireEvent.pointerMove(target, {
1380
+ pointerId,
1381
+ pointerType: 'pen',
1382
+ clientX: 0,
1383
+ clientY: 80,
1384
+ });
1385
+ fireEvent.pointerUp(target, {
1386
+ pointerId,
1387
+ pointerType: 'pen',
1388
+ clientX: 0,
1389
+ clientY: 80,
1390
+ });
1391
+ });
1392
+ });
1393
+
1394
+ expect(onHide).not.toHaveBeenCalled();
1395
+ expect(visualToast.style.getPropertyValue('--_toast-swipe-y')).toBe('');
1396
+ act(() => {
1397
+ fireEvent.click(targets[0]);
1398
+ });
1399
+ expect(onAction).toHaveBeenCalledTimes(1);
1400
+ });
1401
+
1402
+ it('keeps the visible dismiss button as a non-gesture alternative', () => {
1403
+ const {onHide} = renderSwipeToast();
1404
+
1405
+ act(() => {
1406
+ fireEvent.click(
1407
+ screen.getByRole('button', {name: 'Dismiss notification'}),
1408
+ );
1409
+ });
1410
+
1411
+ expect(onHide).toHaveBeenCalledWith('manual');
1412
+ });
1413
+ });
1414
+
1415
+ describe('Toast live-region fallback semantics', () => {
1416
+ it('keeps standalone info Toast content in a polite status region', () => {
1417
+ render(
1418
+ <Toast
1419
+ type="info"
1420
+ body="Saved"
1421
+ isAutoHide={false}
1422
+ autoHideDuration={5000}
1423
+ onDismiss={() => {}}
1424
+ />,
1425
+ );
1426
+
1427
+ const visualToast = screen.getByText('Saved').closest('[data-type]');
1428
+ expect(visualToast).toHaveAttribute('role', 'status');
1429
+ expect(visualToast).toHaveAttribute('aria-live', 'polite');
1430
+ expect(visualToast).toHaveAttribute('aria-atomic', 'true');
1431
+ });
1432
+
1433
+ it('keeps standalone error Toast content in an assertive alert region', () => {
1434
+ render(
1435
+ <Toast
1436
+ type="error"
1437
+ body="Upload failed"
1438
+ isAutoHide={false}
1439
+ autoHideDuration={5000}
1440
+ onDismiss={() => {}}
1441
+ />,
1442
+ );
1443
+
1444
+ const visualToast = screen
1445
+ .getByText('Upload failed')
1446
+ .closest('[data-type]');
1447
+ expect(visualToast).toHaveAttribute('role', 'alert');
1448
+ expect(visualToast).toHaveAttribute('aria-live', 'assertive');
1449
+ expect(visualToast).toHaveAttribute('aria-atomic', 'true');
1450
+ });
1451
+ });
1452
+
255
1453
  describe('ToastViewport region ARIA', () => {
256
- it('exposes the notifications region without a prohibited aria-modal', () => {
1454
+ it('does not expose an empty notifications landmark', () => {
1455
+ renderViewport(<ShowToastButton />);
1456
+ expect(
1457
+ screen.queryByRole('region', {name: 'Notifications'}),
1458
+ ).not.toBeInTheDocument();
1459
+ });
1460
+
1461
+ it('exposes the notifications region without a prohibited aria-modal when a toast is visible', () => {
257
1462
  renderViewport(<ShowToastButton />);
1463
+ act(() => {
1464
+ fireEvent.click(screen.getByText('Trigger'));
1465
+ });
258
1466
  const region = screen.getByRole('region', {name: 'Notifications'});
259
- // aria-modal is only valid on role="dialog"/"alertdialog"; a region must
260
- // not declare it (axe: aria-allowed-attr).
261
1467
  expect(region).not.toHaveAttribute('aria-modal');
262
1468
  });
1469
+
1470
+ it('does not duplicate the landmark for nested viewports', () => {
1471
+ render(
1472
+ <ToastViewport isTopLayer={false}>
1473
+ <ToastViewport isTopLayer={false}>
1474
+ <ShowToastButton options={INFO_A} />
1475
+ </ToastViewport>
1476
+ </ToastViewport>,
1477
+ );
1478
+ act(() => {
1479
+ fireEvent.click(screen.getByText('Trigger'));
1480
+ });
1481
+ expect(screen.getAllByRole('region', {name: 'Notifications'})).toHaveLength(
1482
+ 1,
1483
+ );
1484
+ });
263
1485
  });
264
1486
 
265
1487
  describe('toast announcements via singleton live regions', () => {
@@ -297,7 +1519,7 @@ describe('toast announcements via singleton live regions', () => {
297
1519
  act(() => {
298
1520
  fireEvent.click(screen.getByText('Show'));
299
1521
  });
300
- // Announced synchronously at dispatch, exactly once.
1522
+ // Announced once when the toast is dispatched.
301
1523
  expect(announceSpy).toHaveBeenCalledTimes(1);
302
1524
  expect(announceSpy).toHaveBeenCalledWith(
303
1525
  'Update ready Restart to apply',
@@ -337,9 +1559,8 @@ describe('toast announcements via singleton live regions', () => {
337
1559
  act(() => {
338
1560
  fireEvent.click(screen.getByText('Show'));
339
1561
  });
340
- // The announcement lives in the imperative dispatch path (addToast), not a
341
- // render effect, so StrictMode's double render and double-invoked state
342
- // updater cannot announce the same toast twice.
1562
+ // Announcement happens in the event-driven dispatch path, so StrictMode's
1563
+ // double render cannot announce the same toast twice.
343
1564
  expect(announceSpy).toHaveBeenCalledTimes(1);
344
1565
  expect(announceSpy).toHaveBeenCalledWith('Toast A', 'polite');
345
1566
  });
@@ -485,4 +1706,209 @@ describe('toast timer lifecycle (#3589)', () => {
485
1706
  vi.useRealTimers();
486
1707
  }
487
1708
  });
1709
+
1710
+ describe('renderContent', () => {
1711
+ const CUSTOM: ToastOptions = {
1712
+ body: 'Toast A',
1713
+ renderContent: toast => (
1714
+ <div data-testid="custom-content">
1715
+ {toast.body}
1716
+ {toast.endContent}
1717
+ <Button label="Dismiss custom toast" onClick={toast.dismiss} />
1718
+ </div>
1719
+ ),
1720
+ };
1721
+
1722
+ it('replaces the default layout for that toast', () => {
1723
+ renderViewport(<ShowToastButton options={CUSTOM} triggerLabel="Show" />);
1724
+ act(() => {
1725
+ fireEvent.click(screen.getByText('Show'));
1726
+ });
1727
+ expect(screen.getByTestId('custom-content')).toHaveTextContent('Toast A');
1728
+ });
1729
+
1730
+ it('keeps the default translated and themeable dismiss Button unchanged', () => {
1731
+ const onHide = vi.fn();
1732
+ renderViewport(
1733
+ <ShowToastButton
1734
+ options={{body: 'Plain', isAutoHide: false, onHide}}
1735
+ triggerLabel="Show"
1736
+ />,
1737
+ );
1738
+ act(() => {
1739
+ fireEvent.click(screen.getByText('Show'));
1740
+ });
1741
+ const toast = screen.getByText('Plain').closest('.astryx-toast');
1742
+ expect(toast).not.toBeNull();
1743
+ const dismissButton = within(toast as HTMLElement).getByRole('button', {
1744
+ name: 'Dismiss notification',
1745
+ });
1746
+ expect(dismissButton).toHaveClass('astryx-button');
1747
+ act(() => {
1748
+ fireEvent.click(dismissButton);
1749
+ });
1750
+ expect(onHide).toHaveBeenCalledTimes(1);
1751
+ expect(onHide).toHaveBeenCalledWith('manual');
1752
+ });
1753
+
1754
+ it('dismisses exactly once from a composed custom Button and reports manual', () => {
1755
+ const onHide = vi.fn();
1756
+ renderViewport(
1757
+ <ShowToastButton options={{...CUSTOM, onHide}} triggerLabel="Show" />,
1758
+ );
1759
+ act(() => {
1760
+ fireEvent.click(screen.getByText('Show'));
1761
+ });
1762
+ const dismissButton = within(
1763
+ screen.getByTestId('custom-content'),
1764
+ ).getByRole('button', {name: 'Dismiss custom toast'});
1765
+ expect(dismissButton).toHaveClass('astryx-button');
1766
+ act(() => {
1767
+ fireEvent.click(dismissButton);
1768
+ });
1769
+ expect(onHide).toHaveBeenCalledTimes(1);
1770
+ expect(onHide).toHaveBeenCalledWith('manual');
1771
+ });
1772
+
1773
+ it('lets the dismiss callback travel through nested components', () => {
1774
+ function NestedDismiss({dismiss}: {dismiss: () => void}) {
1775
+ return <Button label="Nested dismiss" onClick={dismiss} />;
1776
+ }
1777
+
1778
+ const onHide = vi.fn();
1779
+ renderViewport(
1780
+ <ShowToastButton
1781
+ options={{
1782
+ body: 'Nested',
1783
+ isAutoHide: false,
1784
+ onHide,
1785
+ renderContent: ({body, dismiss}) => (
1786
+ <div>
1787
+ {body}
1788
+ <NestedDismiss dismiss={dismiss} />
1789
+ </div>
1790
+ ),
1791
+ }}
1792
+ triggerLabel="Show"
1793
+ />,
1794
+ );
1795
+ act(() => {
1796
+ fireEvent.click(screen.getByText('Show'));
1797
+ });
1798
+ act(() => {
1799
+ fireEvent.click(screen.getByRole('button', {name: 'Nested dismiss'}));
1800
+ });
1801
+ expect(onHide).toHaveBeenCalledTimes(1);
1802
+ expect(onHide).toHaveBeenCalledWith('manual');
1803
+ });
1804
+
1805
+ it('does not inject a dismiss control into custom content', () => {
1806
+ renderViewport(
1807
+ <ShowToastButton
1808
+ options={{
1809
+ body: 'Control-free',
1810
+ type: 'error',
1811
+ renderContent: ({body}) => (
1812
+ <div data-testid="control-free-content">{body}</div>
1813
+ ),
1814
+ }}
1815
+ triggerLabel="Show"
1816
+ />,
1817
+ );
1818
+ act(() => {
1819
+ fireEvent.click(screen.getByText('Show'));
1820
+ });
1821
+ const content = screen.getByTestId('control-free-content');
1822
+ expect(content).toHaveTextContent('Control-free');
1823
+ const toast = content.closest('.astryx-toast');
1824
+ expect(toast).not.toBeNull();
1825
+ expect(within(toast as HTMLElement).queryByRole('button')).toBeNull();
1826
+ });
1827
+
1828
+ it('leaves a toast that did not ask for custom content alone', () => {
1829
+ renderViewport(
1830
+ <>
1831
+ <ShowToastButton options={CUSTOM} triggerLabel="Custom" />
1832
+ <ShowToastButton options={INFO_B} triggerLabel="Plain" />
1833
+ </>,
1834
+ );
1835
+ act(() => {
1836
+ fireEvent.click(screen.getByText('Custom'));
1837
+ fireEvent.click(screen.getByText('Plain'));
1838
+ });
1839
+ const plain = screen.getByText('Toast B').closest('.astryx-toast');
1840
+ expect(plain).not.toBeNull();
1841
+ expect(
1842
+ within(plain as HTMLElement).getByRole('button', {
1843
+ name: 'Dismiss notification',
1844
+ }),
1845
+ ).toBeInTheDocument();
1846
+ expect(
1847
+ within(plain as HTMLElement).queryByTestId('custom-content'),
1848
+ ).not.toBeInTheDocument();
1849
+ });
1850
+
1851
+ it('hands endContent to the layout rather than dropping it', () => {
1852
+ renderViewport(
1853
+ <ShowToastButton
1854
+ options={{
1855
+ ...CUSTOM,
1856
+ endContent: <button type="button">Undo</button>,
1857
+ }}
1858
+ triggerLabel="Show"
1859
+ />,
1860
+ );
1861
+ act(() => {
1862
+ fireEvent.click(screen.getByText('Show'));
1863
+ });
1864
+ expect(
1865
+ within(screen.getByTestId('custom-content')).getByRole('button', {
1866
+ name: 'Undo',
1867
+ }),
1868
+ ).toBeInTheDocument();
1869
+ });
1870
+
1871
+ it('still auto-dismisses and exposes its resolved timing', () => {
1872
+ vi.useFakeTimers();
1873
+ try {
1874
+ const seen: {isAutoHide: boolean; autoHideDuration: number}[] = [];
1875
+ const onHide = vi.fn();
1876
+ renderViewport(
1877
+ <ShowToastButton
1878
+ options={{
1879
+ body: 'Fleeting',
1880
+ autoHideDuration: 3000,
1881
+ onHide,
1882
+ renderContent: toast => {
1883
+ seen.push({
1884
+ isAutoHide: toast.isAutoHide,
1885
+ autoHideDuration: toast.autoHideDuration,
1886
+ });
1887
+ return <div>{toast.body}</div>;
1888
+ },
1889
+ }}
1890
+ triggerLabel="Show"
1891
+ />,
1892
+ );
1893
+ act(() => {
1894
+ fireEvent.click(screen.getByText('Show'));
1895
+ });
1896
+ expect(seen[0]).toEqual({isAutoHide: true, autoHideDuration: 3000});
1897
+ act(() => {
1898
+ vi.advanceTimersByTime(3000);
1899
+ });
1900
+ expect(onHide).toHaveBeenCalledWith('auto');
1901
+ } finally {
1902
+ vi.useRealTimers();
1903
+ }
1904
+ });
1905
+
1906
+ it('keeps announcing through the live region', () => {
1907
+ renderViewport(<ShowToastButton options={CUSTOM} triggerLabel="Show" />);
1908
+ act(() => {
1909
+ fireEvent.click(screen.getByText('Show'));
1910
+ });
1911
+ expect(announceSpy).toHaveBeenCalledWith('Toast A', 'polite');
1912
+ });
1913
+ });
488
1914
  });