@pnx-mixtape/mxds 0.1.0-rc.1 → 0.1.0-rc.3

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 (344) hide show
  1. package/.storybook/decorators.ts +15 -0
  2. package/.storybook/{preview.ts → preview.tsx} +8 -1
  3. package/.storybook/src/docs.tsx +29 -0
  4. package/.storybook/themes/campus-vibrant.css +181 -0
  5. package/.storybook/themes/civic-formal.css +246 -0
  6. package/.storybook/{theme-demo.css → themes/demo.css} +14 -7
  7. package/.storybook/themes/health-services.css +255 -0
  8. package/.storybook/themes/research-faculty.css +169 -0
  9. package/dist/build/accordion.css +1 -1
  10. package/dist/build/accordion.entry.js +1 -1
  11. package/dist/build/autocomplete.css +1 -1
  12. package/dist/build/autocomplete.entry.js +1 -1
  13. package/dist/build/base.css +1 -1
  14. package/dist/build/breadcrumb.css +1 -1
  15. package/dist/build/button.css +1 -1
  16. package/dist/build/card.css +1 -1
  17. package/dist/build/chunks/{Accordion-z-dH6nFH.js → Accordion-GN0sME4W.js} +1 -1
  18. package/dist/build/chunks/{AccordionMobile-BYPPNyf5.js → AccordionMobile-DG5jSe8Q.js} +1 -1
  19. package/dist/build/chunks/{Autocomplete-2L6tLLuh.js → Autocomplete-B4RtkAyh.js} +1 -1
  20. package/dist/build/chunks/{ClosableAlert-DdXbF1J2.js → ClosableAlert-DEhj8Nxm.js} +1 -1
  21. package/dist/build/chunks/{Dialog-Ds8gzhri.js → Dialog-Haahn-Yn.js} +1 -1
  22. package/dist/build/chunks/DropMenu-1baQc8Yc.js +1 -0
  23. package/dist/build/chunks/Gallery-D8NGXy2Q.js +1 -0
  24. package/dist/build/chunks/GlobalToggle-B52fdBQu.js +1 -0
  25. package/dist/build/chunks/InPageNavigation-Dyn2TR-N.js +1 -0
  26. package/dist/build/chunks/Navigation-hOrfLq7h.js +1 -0
  27. package/dist/build/chunks/Popover-Cxg-s5Jn.js +1 -0
  28. package/dist/build/chunks/Sticky-BGCuuEJK.js +1 -0
  29. package/dist/build/chunks/Tabs-DYy1YTdU.js +15 -0
  30. package/dist/build/chunks/enums-DLV_1NAf.js +1 -0
  31. package/dist/build/chunks/polyfills-C7pRt54M.js +1 -0
  32. package/dist/build/chunks/utilities-_Bq3TxAQ.js +1 -0
  33. package/dist/build/constants.css +1 -1
  34. package/dist/build/cookie-consent.css +1 -1
  35. package/dist/build/dialog.entry.js +1 -1
  36. package/dist/build/drop-menu.entry.js +1 -1
  37. package/dist/build/elements.js +1 -1
  38. package/dist/build/filters.css +1 -1
  39. package/dist/build/filters.entry.js +1 -1
  40. package/dist/build/form.css +1 -1
  41. package/dist/build/gallery.entry.js +1 -1
  42. package/dist/build/global-alert.entry.js +1 -1
  43. package/dist/build/grid.css +1 -1
  44. package/dist/build/header.entry.js +1 -1
  45. package/dist/build/icon.css +1 -1
  46. package/dist/build/in-page-alert.css +1 -1
  47. package/dist/build/in-page-navigation.entry.js +1 -1
  48. package/dist/build/list-item.css +1 -1
  49. package/dist/build/navigation.css +1 -1
  50. package/dist/build/navigation.entry.js +1 -1
  51. package/dist/build/one-time-code.css +1 -0
  52. package/dist/build/popover.css +1 -1
  53. package/dist/build/popover.entry.js +1 -1
  54. package/dist/build/print.css +1 -1
  55. package/dist/build/progress-indicator.css +1 -0
  56. package/dist/build/react.js +1 -1
  57. package/dist/build/sidebar.css +1 -1
  58. package/dist/build/steps.css +1 -1
  59. package/dist/build/sticky.entry.js +1 -1
  60. package/dist/build/tabs.css +1 -1
  61. package/dist/build/tabs.entry.js +1 -1
  62. package/dist/build/tag.css +1 -1
  63. package/dist/build/utilities.css +1 -1
  64. package/dist/types/Atom/DefinitionList/DefinitionList.d.ts +3 -2
  65. package/dist/types/Atom/DefinitionList/DefinitionList.d.ts.map +1 -1
  66. package/dist/types/Atom/Heading/Heading.d.ts.map +1 -1
  67. package/dist/types/Atom/Link/Link.d.ts.map +1 -1
  68. package/dist/types/Atom/List/List.d.ts +10 -0
  69. package/dist/types/Atom/List/List.d.ts.map +1 -0
  70. package/dist/types/Component/Accordion/Accordion.d.ts.map +1 -1
  71. package/dist/types/Component/Dialog/Dialog.d.ts.map +1 -1
  72. package/dist/types/Component/DropMenu/Elements/DropMenu.d.ts.map +1 -1
  73. package/dist/types/Component/Filters/Elements/Filters.d.ts +20 -1
  74. package/dist/types/Component/Filters/Elements/Filters.d.ts.map +1 -1
  75. package/dist/types/Component/Filters/Filters.d.ts +7 -2
  76. package/dist/types/Component/Filters/Filters.d.ts.map +1 -1
  77. package/dist/types/Component/Gallery/Gallery.d.ts.map +1 -1
  78. package/dist/types/Component/InPageNavigation/Elements/InPageNavigation.d.ts +2 -2
  79. package/dist/types/Component/InPageNavigation/Elements/InPageNavigation.d.ts.map +1 -1
  80. package/dist/types/Component/InPageNavigation/Hooks/useInPageNavigation.d.ts.map +1 -1
  81. package/dist/types/Component/Pagination/Components/PaginationContext.d.ts.map +1 -1
  82. package/dist/types/Component/ProgressIndicator/ProgressIndicator.d.ts +9 -0
  83. package/dist/types/Component/ProgressIndicator/ProgressIndicator.d.ts.map +1 -0
  84. package/dist/types/Component/Tabs/Elements/Tabs.d.ts.map +1 -1
  85. package/dist/types/Component/Tabs/Tabs.d.ts.map +1 -1
  86. package/dist/types/Component/Tag/Tag.d.ts +4 -2
  87. package/dist/types/Component/Tag/Tag.d.ts.map +1 -1
  88. package/dist/types/Form/Autocomplete/Autocomplete.d.ts +2 -2
  89. package/dist/types/Form/Autocomplete/Autocomplete.d.ts.map +1 -1
  90. package/dist/types/Form/Autocomplete/Elements/Autocomplete.d.ts +10 -0
  91. package/dist/types/Form/Autocomplete/Elements/Autocomplete.d.ts.map +1 -1
  92. package/dist/types/Form/Checkbox/FormCheckbox.d.ts.map +1 -1
  93. package/dist/types/Form/CheckboxCard/CheckboxCard.d.ts +23 -0
  94. package/dist/types/Form/CheckboxCard/CheckboxCard.d.ts.map +1 -0
  95. package/dist/types/Form/OneTimeCode/OneTimeCode.d.ts +12 -0
  96. package/dist/types/Form/OneTimeCode/OneTimeCode.d.ts.map +1 -0
  97. package/dist/types/Form/Radio/FormRadio.d.ts.map +1 -1
  98. package/dist/types/Form/RadioCard/RadioCard.d.ts +16 -0
  99. package/dist/types/Form/RadioCard/RadioCard.d.ts.map +1 -0
  100. package/dist/types/Form/Select/FormSelect.d.ts.map +1 -1
  101. package/dist/types/Form/TextInput/FormText.d.ts.map +1 -1
  102. package/dist/types/Form/Textarea/FormTextarea.d.ts.map +1 -1
  103. package/dist/types/Utility/Hooks/useIsomorphicLayoutEffect.d.ts +4 -0
  104. package/dist/types/Utility/Hooks/useIsomorphicLayoutEffect.d.ts.map +1 -0
  105. package/dist/types/enums.d.ts +19 -0
  106. package/dist/types/enums.d.ts.map +1 -1
  107. package/dist/types/react.d.ts +5 -0
  108. package/dist/types/react.d.ts.map +1 -1
  109. package/package.json +8 -8
  110. package/src/Atom/Background/_background.css +2 -2
  111. package/src/Atom/Blockquote/_blockquote.css +9 -2
  112. package/src/Atom/Button/README.md +1 -0
  113. package/src/Atom/Button/__snapshots__/Button.stories.ts.snap +2 -3
  114. package/src/Atom/Button/_buttons-styles.css +6 -6
  115. package/src/Atom/Button/_buttons.css +21 -3
  116. package/src/Atom/Button/button.twig +4 -1
  117. package/src/Atom/DefinitionList/DefinitionList.stories.ts +42 -2
  118. package/src/Atom/DefinitionList/DefinitionList.stories.tsx +36 -0
  119. package/src/Atom/DefinitionList/DefinitionList.tsx +4 -1
  120. package/src/Atom/DefinitionList/DefinitionListItem.stories.ts +1 -1
  121. package/src/Atom/DefinitionList/README.md +43 -0
  122. package/src/Atom/DefinitionList/__snapshots__/DefinitionList.stories.ts.snap +37 -3
  123. package/src/Atom/DefinitionList/__snapshots__/DefinitionList.stories.tsx.snap +2 -0
  124. package/src/Atom/DefinitionList/__snapshots__/DefinitionListItem.stories.ts.snap +3 -3
  125. package/src/Atom/DefinitionList/definition-list-item.twig +1 -0
  126. package/src/Atom/DefinitionList/definition-list.twig +6 -1
  127. package/src/Atom/Heading/Heading.tsx +6 -0
  128. package/src/Atom/Heading/_headings.css +2 -1
  129. package/src/Atom/Icon/_extended-set.css +30 -30
  130. package/src/Atom/Icon/_icon.css +39 -36
  131. package/src/Atom/Image/README.md +2 -2
  132. package/src/Atom/Image/_image.css +12 -6
  133. package/src/Atom/Link/Link.tsx +16 -3
  134. package/src/Atom/Link/__snapshots__/Link.stories.ts.snap +7 -12
  135. package/src/Atom/Link/__snapshots__/Link.stories.tsx.snap +1 -1
  136. package/src/Atom/Link/_links.css +9 -0
  137. package/src/Atom/Link/link.twig +8 -2
  138. package/src/Atom/LinkedImage/linked-image.twig +1 -2
  139. package/src/Atom/List/List.stories.ts +102 -0
  140. package/src/Atom/List/List.stories.tsx +103 -0
  141. package/src/Atom/List/List.tsx +22 -0
  142. package/src/Atom/List/README.md +150 -0
  143. package/src/Atom/List/__snapshots__/List.stories.ts.snap +43 -0
  144. package/src/Atom/List/__snapshots__/List.stories.tsx.snap +13 -0
  145. package/src/Atom/List/_list.css +123 -0
  146. package/src/Atom/List/list.twig +5 -0
  147. package/src/Atom/Media/Media.stories.ts +1 -1
  148. package/src/Atom/Media/__snapshots__/Media.stories.ts.snap +2 -2
  149. package/src/Atom/Spacing/_spacing.css +24 -24
  150. package/src/Atom/Table/__snapshots__/Table.stories.ts.snap +24 -4
  151. package/src/Atom/Table/_table.css +15 -3
  152. package/src/Atom/Table/table.twig +6 -1
  153. package/src/Atom/Text/__snapshots__/Text.stories.ts.snap +3 -2
  154. package/src/Atom/Text/_text-styles.css +21 -1
  155. package/src/Atom/Text/twig/text-styles-example.twig +3 -2
  156. package/src/Atom/_base.css +6 -0
  157. package/src/Atom/_flow.css +1 -1
  158. package/src/Atom/_generic.css +18 -11
  159. package/src/Component/Accordion/Accordion.stories.ts +31 -0
  160. package/src/Component/Accordion/Accordion.tsx +3 -2
  161. package/src/Component/Accordion/AccordionItem.stories.ts +27 -0
  162. package/src/Component/Accordion/__snapshots__/Accordion.stories.ts.snap +36 -0
  163. package/src/Component/Accordion/__snapshots__/AccordionItem.stories.ts.snap +16 -0
  164. package/src/Component/Accordion/accordion.css +2 -0
  165. package/src/Component/Breadcrumb/__snapshots__/Breadcrumb.stories.ts.snap +3 -6
  166. package/src/Component/Breadcrumb/breadcrumb.css +4 -0
  167. package/src/Component/Card/__snapshots__/Card.stories.ts.snap +13 -24
  168. package/src/Component/Card/card.css +2 -2
  169. package/src/Component/Card/card.twig +1 -1
  170. package/src/Component/Carousel/Carousel.stories.ts +101 -23
  171. package/src/Component/Carousel/Carousel.stories.tsx +74 -0
  172. package/src/Component/Carousel/__snapshots__/Carousel.stories.ts.snap +273 -88
  173. package/src/Component/Carousel/__snapshots__/Carousel.stories.tsx.snap +5 -3
  174. package/src/Component/ContentBlock/__snapshots__/ContentBlock.stories.ts.snap +12 -24
  175. package/src/Component/CookieConsent/cookie-consent.css +3 -1
  176. package/src/Component/Dialog/Dialog.stories.ts +33 -0
  177. package/src/Component/Dialog/Dialog.stories.tsx +25 -0
  178. package/src/Component/Dialog/Dialog.tsx +2 -2
  179. package/src/Component/Dialog/__snapshots__/Dialog.stories.ts.snap +33 -2
  180. package/src/Component/Dialog/__snapshots__/Dialog.stories.tsx.snap +2 -0
  181. package/src/Component/DropMenu/DropMenu.stories.ts +39 -1
  182. package/src/Component/DropMenu/DropMenu.stories.tsx +29 -0
  183. package/src/Component/DropMenu/DropMenu.tsx +1 -1
  184. package/src/Component/DropMenu/Elements/DropMenu.ts +10 -1
  185. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.ts.snap +14 -0
  186. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.tsx.snap +3 -1
  187. package/src/Component/Filters/Elements/Filters.ts +71 -1
  188. package/src/Component/Filters/FilterItem.stories.ts +2 -1
  189. package/src/Component/Filters/FilterItem.tsx +1 -1
  190. package/src/Component/Filters/Filters.figma.ts +1 -1
  191. package/src/Component/Filters/Filters.stories.ts +95 -3
  192. package/src/Component/Filters/Filters.stories.tsx +246 -17
  193. package/src/Component/Filters/Filters.tsx +145 -29
  194. package/src/Component/Filters/README.md +102 -37
  195. package/src/Component/Filters/__snapshots__/FilterItem.stories.tsx.snap +3 -3
  196. package/src/Component/Filters/__snapshots__/Filters.stories.ts.snap +82 -4
  197. package/src/Component/Filters/__snapshots__/Filters.stories.tsx.snap +9 -3
  198. package/src/Component/Filters/filters.css +11 -0
  199. package/src/Component/Gallery/Gallery.tsx +2 -2
  200. package/src/Component/HeroBanner/HeroBanner.stories.ts +1 -1
  201. package/src/Component/HeroBanner/__snapshots__/HeroBanner.stories.ts.snap +4 -8
  202. package/src/Component/HeroSearch/__snapshots__/HeroSearch.stories.ts.snap +5 -10
  203. package/src/Component/InPageAlert/__snapshots__/InPageAlert.stories.ts.snap +6 -12
  204. package/src/Component/InPageAlert/in-page-alert.css +2 -1
  205. package/src/Component/InPageNavigation/Elements/InPageNavigation.ts +10 -5
  206. package/src/Component/InPageNavigation/Hooks/useInPageNavigation.ts +10 -5
  207. package/src/Component/InPageNavigation/InPageNavigation.stories.ts +83 -0
  208. package/src/Component/InPageNavigation/InPageNavigation.stories.tsx +130 -0
  209. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +61 -1
  210. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.tsx.snap +4 -0
  211. package/src/Component/InPageNavigation/twig/content-example-duplicate-headings.twig +20 -0
  212. package/src/Component/InPageNavigation/twig/in-page-navigation-story-duplicate-headings.twig +2 -0
  213. package/src/Component/LinkList/__snapshots__/LinkList.stories.ts.snap +6 -12
  214. package/src/Component/ListItem/__snapshots__/ListItem.stories.ts.snap +11 -18
  215. package/src/Component/ListItem/list-item.css +1 -1
  216. package/src/Component/ListItem/list-item.twig +2 -2
  217. package/src/Component/Navigation/Dropdown.stories.tsx +46 -0
  218. package/src/Component/Navigation/Navigation.stories.ts +56 -0
  219. package/src/Component/Navigation/__snapshots__/Dropdown.stories.tsx.snap +2 -0
  220. package/src/Component/Navigation/__snapshots__/Navigation.stories.ts.snap +57 -0
  221. package/src/Component/Navigation/_navigation.css +1 -1
  222. package/src/Component/Pagination/Components/PaginationContext.tsx +2 -1
  223. package/src/Component/Pagination/Components/PaginationItem.tsx +1 -1
  224. package/src/Component/Pagination/Pagination.stories.ts +44 -4
  225. package/src/Component/Pagination/Pagination.stories.tsx +31 -0
  226. package/src/Component/Pagination/__snapshots__/Pagination.stories.ts.snap +69 -12
  227. package/src/Component/Pagination/__snapshots__/Pagination.stories.tsx.snap +3 -1
  228. package/src/Component/Pagination/pagination-item.twig +1 -1
  229. package/src/Component/Pagination/pagination.css +2 -2
  230. package/src/Component/Popover/__snapshots__/Popover.stories.ts.snap +13 -26
  231. package/src/Component/Popover/popover.css +1 -1
  232. package/src/Component/ProgressIndicator/ProgressIndicator.stories.ts +36 -0
  233. package/src/Component/ProgressIndicator/ProgressIndicator.stories.tsx +30 -0
  234. package/src/Component/ProgressIndicator/ProgressIndicator.tsx +60 -0
  235. package/src/Component/ProgressIndicator/README.md +111 -0
  236. package/src/Component/ProgressIndicator/__snapshots__/ProgressIndicator.stories.ts.snap +67 -0
  237. package/src/Component/ProgressIndicator/__snapshots__/ProgressIndicator.stories.tsx.snap +7 -0
  238. package/src/Component/ProgressIndicator/progress-indicator.css +59 -0
  239. package/src/Component/ProgressIndicator/progress-indicator.twig +22 -0
  240. package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.ts.snap +1 -2
  241. package/src/Component/SocialLinks/__snapshots__/SocialLinks.stories.ts.snap +6 -9
  242. package/src/Component/Steps/Steps.stories.ts +1 -1
  243. package/src/Component/Steps/steps.css +13 -0
  244. package/src/Component/Sticky/Elements/Sticky.ts +1 -1
  245. package/src/Component/Sticky/sticky.css +3 -3
  246. package/src/Component/Tabs/Elements/Tabs.ts +3 -1
  247. package/src/Component/Tabs/Tabs.stories.ts +77 -21
  248. package/src/Component/Tabs/Tabs.stories.tsx +24 -0
  249. package/src/Component/Tabs/Tabs.tsx +2 -2
  250. package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +20 -2
  251. package/src/Component/Tabs/__snapshots__/Tabs.stories.tsx.snap +3 -1
  252. package/src/Component/Tabs/tabs.css +2 -0
  253. package/src/Component/Tag/README.md +52 -26
  254. package/src/Component/Tag/Tag.stories.tsx +24 -0
  255. package/src/Component/Tag/Tag.tsx +12 -2
  256. package/src/Component/Tag/TagItem.stories.ts +25 -0
  257. package/src/Component/Tag/__snapshots__/Tag.stories.ts.snap +2 -2
  258. package/src/Component/Tag/__snapshots__/Tag.stories.tsx.snap +2 -0
  259. package/src/Component/Tag/tag-item.twig +2 -2
  260. package/src/Component/Tag/tag.css +76 -2
  261. package/src/Form/Autocomplete/Autocomplete.stories.ts +89 -7
  262. package/src/Form/Autocomplete/Autocomplete.stories.tsx +85 -1
  263. package/src/Form/Autocomplete/Autocomplete.tsx +20 -10
  264. package/src/Form/Autocomplete/Elements/Autocomplete.ts +24 -1
  265. package/src/Form/Autocomplete/README.md +26 -26
  266. package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.ts.snap +39 -5
  267. package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.tsx.snap +8 -4
  268. package/src/Form/Autocomplete/autocomplete.css +12 -1
  269. package/src/Form/Autocomplete/autocomplete.twig +1 -3
  270. package/src/Form/Checkbox/FormCheckbox.tsx +2 -8
  271. package/src/Form/Checkbox/README.md +23 -18
  272. package/src/Form/Checkbox/input-checkbox.twig +0 -2
  273. package/src/Form/CheckboxCard/CheckboxCard.stories.ts +90 -0
  274. package/src/Form/CheckboxCard/CheckboxCard.stories.tsx +43 -0
  275. package/src/Form/CheckboxCard/CheckboxCard.tsx +102 -0
  276. package/src/Form/CheckboxCard/README.md +163 -0
  277. package/src/Form/CheckboxCard/__snapshots__/CheckboxCard.stories.ts.snap +63 -0
  278. package/src/Form/CheckboxCard/__snapshots__/CheckboxCard.stories.tsx.snap +7 -0
  279. package/src/Form/CheckboxCard/checkbox-card.twig +29 -0
  280. package/src/Form/FormItem/__snapshots__/FormItem.stories.ts.snap +2 -2
  281. package/src/Form/OneTimeCode/OneTimeCode.stories.ts +20 -0
  282. package/src/Form/OneTimeCode/OneTimeCode.stories.tsx +18 -0
  283. package/src/Form/OneTimeCode/OneTimeCode.tsx +56 -0
  284. package/src/Form/OneTimeCode/README.md +83 -0
  285. package/src/Form/OneTimeCode/__snapshots__/OneTimeCode.stories.ts.snap +7 -0
  286. package/src/Form/OneTimeCode/__snapshots__/OneTimeCode.stories.tsx.snap +3 -0
  287. package/src/Form/OneTimeCode/one-time-code.css +13 -0
  288. package/src/Form/OneTimeCode/one-time-code.twig +31 -0
  289. package/src/Form/README.md +65 -13
  290. package/src/Form/Radio/FormRadio.tsx +1 -4
  291. package/src/Form/Radio/input-radio.twig +0 -2
  292. package/src/Form/RadioCard/README.md +146 -0
  293. package/src/Form/RadioCard/RadioCard.stories.ts +95 -0
  294. package/src/Form/RadioCard/RadioCard.stories.tsx +26 -0
  295. package/src/Form/RadioCard/RadioCard.tsx +59 -0
  296. package/src/Form/RadioCard/__snapshots__/RadioCard.stories.ts.snap +69 -0
  297. package/src/Form/RadioCard/__snapshots__/RadioCard.stories.tsx.snap +3 -0
  298. package/src/Form/RadioCard/radio-card.twig +20 -0
  299. package/src/Form/Select/FormSelect.tsx +1 -4
  300. package/src/Form/Select/input-select.twig +0 -2
  301. package/src/Form/TextInput/FormText.stories.tsx +14 -0
  302. package/src/Form/TextInput/FormText.tsx +1 -4
  303. package/src/Form/TextInput/__snapshots__/FormText.stories.tsx.snap +2 -0
  304. package/src/Form/TextInput/input-text.twig +0 -2
  305. package/src/Form/Textarea/FormTextarea.tsx +1 -4
  306. package/src/Form/Textarea/input-textarea.twig +0 -2
  307. package/src/Form/Validation.stories.ts +153 -0
  308. package/src/Form/__snapshots__/Validation.stories.ts.snap +130 -0
  309. package/src/Form/form.css +240 -55
  310. package/src/Layout/Footer/__snapshots__/Footer.stories.ts.snap +18 -33
  311. package/src/Layout/Grid/grid.css +4 -4
  312. package/src/Layout/Header/Header.stories.ts +33 -0
  313. package/src/Layout/Header/Header.stories.tsx +31 -0
  314. package/src/Layout/Header/__snapshots__/Header.stories.ts.snap +50 -0
  315. package/src/Layout/Header/__snapshots__/Header.stories.tsx.snap +2 -0
  316. package/src/Layout/Masthead/__snapshots__/Masthead.stories.ts.snap +8 -16
  317. package/src/Layout/Section/__snapshots__/Background.stories.ts.snap +2 -4
  318. package/src/Layout/Section/__snapshots__/Section.stories.ts.snap +4 -8
  319. package/src/Layout/Sidebar/__snapshots__/Sidebar.stories.ts.snap +5 -5
  320. package/src/Layout/Sidebar/sidebar.css +1 -0
  321. package/src/Layout/Sidebar/sidebar.twig +1 -1
  322. package/src/Utility/Hooks/useIsomorphicLayoutEffect.ts +8 -0
  323. package/src/Utility/_layout-utils.css +8 -0
  324. package/src/Utility/_mixins.css +24 -0
  325. package/src/Utility/print.css +51 -29
  326. package/src/Utility/utilities.css +22 -2
  327. package/src/_custom-media.css +1 -0
  328. package/src/constants.css +12 -2
  329. package/src/enums.ts +22 -0
  330. package/src/react.ts +5 -0
  331. package/tools/lib/extract-classnames.mjs +0 -0
  332. package/tools/lint-classnames.mjs +1 -1
  333. package/tools/test-utils/screen-reader.ts +33 -0
  334. package/dist/build/chunks/DropMenu-Co9jZZ4S.js +0 -1
  335. package/dist/build/chunks/Gallery-BT3cxS5m.js +0 -1
  336. package/dist/build/chunks/GlobalToggle-DQHho97V.js +0 -1
  337. package/dist/build/chunks/InPageNavigation-CzR_KZaz.js +0 -1
  338. package/dist/build/chunks/Navigation-B9Wug-ok.js +0 -1
  339. package/dist/build/chunks/Popover-De_CQqcd.js +0 -1
  340. package/dist/build/chunks/Sticky-Kyjykb36.js +0 -1
  341. package/dist/build/chunks/Tabs-Dm8g301e.js +0 -15
  342. package/dist/build/chunks/disclosure-widget-BETzOHLO.js +0 -1
  343. package/dist/build/chunks/polyfills-Dokbkxxk.js +0 -1
  344. package/dist/build/chunks/utilities-C5dAO2YA.js +0 -1
@@ -39,14 +39,15 @@ A small inline label used to categorise content. Renders as plain text metadata,
39
39
 
40
40
  **tags.twig props** (`Tags` type from `ids-shape`):
41
41
 
42
- | Prop | Type | Description |
43
- | --------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | `type` | `TagTypes` | Controls which markup is rendered per item. One of `text`, `link`, `checkbox`. Changing the type also changes the required shape of `items`. |
45
- | `items` | `string[] \| Link[] \| FormControl[]` | List of tag items. Shape depends on `type` — see below. |
46
- | `attributes` | `Attribute` | Extra attributes on the outer `<ul class="mx-tags">` |
47
- | `tagAttributes` | `Attribute` | Extra attributes on each `.mx-tag` element |
48
- | `dismissible` | `boolean` | When true, plain (`text`) tags render a remove button (`mx-tag--dismissible`). Wire up removal behaviour in the consuming app. |
49
- | `removeLabel` | `string` | Accessible label for the remove button. Defaults to `"Remove"`. |
42
+ | Prop | Type | Description |
43
+ | --------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `type` | `TagTypes` | Controls which markup is rendered per item. One of `text`, `link`, `checkbox`. Changing the type also changes the required shape of `items`. |
45
+ | `items` | `string[] \| Link[] \| FormControl[]` | List of tag items. Shape depends on `type` — see below. |
46
+ | `attributes` | `Attribute` | Extra attributes on the outer `<ul class="mx-tags">` |
47
+ | `tagAttributes` | `Attribute` | Extra attributes on each `.mx-tag` element |
48
+ | `dismissible` | `boolean` | When true, plain (`text`) tags render a remove button (`mx-tag--dismissible`). Wire up removal behaviour in the consuming app. |
49
+ | `removeLabel` | `string` | Accessible label for the remove button. Defaults to `"Remove"`. |
50
+ | `variant` | `TagVariants` | Colour variant: `info`, `success`, `warning`, `error`, `featured`. Applied per-item via `tag-item.twig`. Defaults to the neutral, unstyled tag. |
50
51
 
51
52
  **Item shapes per `type`:**
52
53
 
@@ -78,20 +79,20 @@ Link tags:
78
79
  </ul>
79
80
  ```
80
81
 
81
- Checkbox tags (the `<input>` is visually hidden off-screen; the `<label>` is the click target):
82
+ Checkbox tags (the `<input>` is visually hidden off-screen; the `<label>` is the click target). The `mx-icon mx-icon--tick` classes on the label render the shared checkmark icon (from `@pnx-mixtape/mxds/src/Atom/Icon/icon.css`) — its space is always reserved, but it's only visible once the input is checked, so checking a tag doesn't change its size:
82
83
 
83
84
  ```html
84
85
  <ul class="mx-tags">
85
86
  <li>
86
87
  <div class="mx-tag">
87
88
  <input type="checkbox" id="music" name="tag" value="music" />
88
- <label for="music">Music</label>
89
+ <label class="mx-icon mx-icon--tick" for="music">Music</label>
89
90
  </div>
90
91
  </li>
91
92
  <li>
92
93
  <div class="mx-tag">
93
94
  <input type="checkbox" id="news" name="tag" value="news" checked />
94
- <label for="news">News</label>
95
+ <label class="mx-icon mx-icon--tick" for="news">News</label>
95
96
  </div>
96
97
  </li>
97
98
  </ul>
@@ -131,7 +132,7 @@ import { AsTags } from "@pnx-mixtape/mxds/src/enums"
131
132
  <li>
132
133
  <div className="mx-tag">
133
134
  <input type="checkbox" id="music" name="tag" value="music" />
134
- <label htmlFor="music">Music</label>
135
+ <label className="mx-icon mx-icon--tick" htmlFor="music">Music</label>
135
136
  </div>
136
137
  </li>
137
138
  </ul>
@@ -139,13 +140,14 @@ import { AsTags } from "@pnx-mixtape/mxds/src/enums"
139
140
 
140
141
  **`Tag` props:**
141
142
 
142
- | Prop | Type | Default | Description |
143
- | ------------- | ------------ | ---------- | ----------------------------------------------------------------------------------------------- |
144
- | `as` | `AsTags` | `div` | Semantic element for the tag: `div`, `li`, `article`, `aside`, `section` |
145
- | `className` | `string` | — | Extra class on the tag element |
146
- | `children` | `ReactNode` | — | Tag label content |
147
- | `onRemove` | `() => void` | — | When set, renders a remove button (`mx-tag--dismissible`). Consumer removes the tag from state. |
148
- | `removeLabel` | `string` | `"Remove"` | Accessible label for the remove button |
143
+ | Prop | Type | Default | Description |
144
+ | ------------- | ------------- | ---------- | --------------------------------------------------------------------------------------------------------- |
145
+ | `as` | `AsTags` | `div` | Semantic element for the tag: `div`, `li`, `article`, `aside`, `section` |
146
+ | `className` | `string` | — | Extra class on the tag element |
147
+ | `children` | `ReactNode` | — | Tag label content |
148
+ | `onRemove` | `() => void` | — | When set, renders a remove button (`mx-tag--dismissible`). Consumer removes the tag from state. |
149
+ | `removeLabel` | `string` | `"Remove"` | Accessible label for the remove button |
150
+ | `variant` | `TagVariants` | — | Colour variant: `info`, `success`, `warning`, `error`, `featured`. Defaults to the neutral, unstyled tag. |
149
151
 
150
152
  > For filter UIs, tag removal can also be handled upstream (e.g. via the checkbox type toggling its checked state, or via the Filters component). Use `onRemove` when you need a self-contained dismissible chip.
151
153
 
@@ -155,11 +157,11 @@ import { AsTags } from "@pnx-mixtape/mxds/src/enums"
155
157
 
156
158
  The `type` prop on the Twig template maps to `TagTypes` values from `@pnx-mixtape/ids-shape`. The same base `mx-tag` class is used for all variants — the variant changes the inner element, not the class.
157
159
 
158
- | `TagTypes` value | Element rendered | Visual intent |
159
- | ---------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
160
- | `text` | `<span class="mx-tag">` | Read-only metadata label (e.g. category shown on a Card) |
161
- | `link` | `<a class="mx-tag" href>` | Navigational tag — clicking takes the user to a filtered / category view |
162
- | `checkbox` | `<div class="mx-tag">` wrapping `<input type="checkbox">` + `<label>` | Toggleable filter tag inside a form; checked state is visually indicated via the primary link colour |
160
+ | `TagTypes` value | Element rendered | Visual intent |
161
+ | ---------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
162
+ | `text` | `<span class="mx-tag">` | Read-only metadata label (e.g. category shown on a Card) |
163
+ | `link` | `<a class="mx-tag" href>` | Navigational tag — clicking takes the user to a filtered / category view |
164
+ | `checkbox` | `<div class="mx-tag">` wrapping `<input type="checkbox">` + `<label>` | Toggleable filter tag inside a form; checked state fills the tag with the primary link colour (or the variant's own colour) and prefixes the label with a checkmark icon |
163
165
 
164
166
  ---
165
167
 
@@ -168,7 +170,7 @@ The `type` prop on the Twig template maps to `TagTypes` values from `@pnx-mixtap
168
170
  - **Text tags** are plain `<span>` elements — they are metadata, not interactive. Do not add click handlers or `role` attributes; if a tag needs to be clickable, use the `link` or `checkbox` variant.
169
171
  - **Link tags** use a real `<a href>`. Ensure the destination makes sense out of context — the visible label is the accessible name, so labels like "Music" should lead to an unambiguous page (e.g. `/category/music`).
170
172
  - **Checkbox tags** rely on a native `<input type="checkbox">` paired with a `<label for="…">`. The `<input>` is positioned off-screen (not `display: none`) so it remains focusable and announceable. The whole `.mx-tag` gets a focus outline via `:has(input:focus)` — do not remove this selector.
171
- - The checked state is communicated by colour alone in the default styling. When rendering checkbox tags in a form, ensure surrounding context (a fieldset legend, heading, or `aria-label`) describes what group of filters the tags belong to.
173
+ - The checked state is communicated by colour (a solid fill) **and** a checkmark icon prefixed to the label — not colour alone. When rendering checkbox tags in a form, ensure surrounding context (a fieldset legend, heading, or `aria-label`) describes what group of filters the tags belong to.
172
174
  - If you need to render a list of tags for assistive-tech as a single labelled group, add `aria-label` (or `aria-labelledby`) to the `<ul class="mx-tags">`:
173
175
 
174
176
  ```html
@@ -201,7 +203,31 @@ The `type` prop on the Twig template maps to `TagTypes` values from `@pnx-mixtap
201
203
  }
202
204
  ```
203
205
 
204
- Hover (on `a.mx-tag`) and checked (`:has(input:checked)`) states both set `--tag-foreground` to `--link-colour` (falls back to `--colour-primary`).
206
+ Hover (on `a.mx-tag`) sets `--tag-foreground` to `--link-colour` (falls back to `--colour-primary`). Checked (`:has(input:checked)`) fills `--tag-background`/`--line-colour` with that same colour and switches `--tag-foreground` to `--colour-foreground-reverse`, plus prefixes the label with a checkmark icon — except on a colour variant (see below), where checked keeps the variant's own colours and hover darkens them instead.
207
+
208
+ ---
209
+
210
+ ## Colour variants — `TagVariants` enum
211
+
212
+ Apply `mx-tag--{variant}` for `info`, `success`, `warning`, `error`, or `featured`. Each variant fills `--tag-background` and sets the border/outline to match; foreground uses `--colour-foreground-reverse` (`featured` excepted, which stays dark-on-light).
213
+
214
+ ```tsx
215
+ <Tag variant={TagVariants.INFO}>New</Tag>
216
+ ```
217
+
218
+ `tags.twig` itself only prints pre-rendered `items` markup — `variant`, like `dismissible`, is applied per item when building that markup with `tag-item.twig`:
219
+
220
+ ```twig
221
+ {{ include('@mixtape/Component/Tag/tag-item.twig', {
222
+ type: 'text',
223
+ item: 'New',
224
+ variant: 'info',
225
+ }) }}
226
+ ```
227
+
228
+ - A checked variant tag keeps its own colours (the solid fill and "checked" link colour swap only apply to non-variant tags) — it's already a solid fill. The checkmark icon on checked still applies.
229
+ - The focus outline (`:has(input:focus)`) on a variant tag uses the variant's own colour instead of the standard blue.
230
+ - Hovering a variant **link** tag darkens its background/border by 20% (via `color-mix`) rather than switching to the link hover colour; the text colour is unchanged. `featured` only darkens by 10% — it keeps dark text on its light background, so a smaller step protects contrast.
205
231
 
206
232
  ---
207
233
 
@@ -2,6 +2,7 @@ import { Meta, StoryObj } from "@storybook/react-vite"
2
2
  import Component from "./Tag"
3
3
  import "./tag.css"
4
4
  import { Page } from "../../../.storybook-react/decorators"
5
+ import { TagVariants } from "../../enums"
5
6
 
6
7
  const meta: Meta<typeof Component> = {
7
8
  component: Component,
@@ -9,7 +10,30 @@ const meta: Meta<typeof Component> = {
9
10
  args: {
10
11
  children: "Tag name",
11
12
  },
13
+ argTypes: {
14
+ variant: {
15
+ control: {
16
+ type: "select",
17
+ labels: { undefined: "default" },
18
+ },
19
+ options: [undefined, ...Object.values(TagVariants)],
20
+ },
21
+ },
12
22
  }
13
23
 
14
24
  export default meta
15
25
  export const Tag: StoryObj<typeof meta> = {}
26
+
27
+ const variantsListStyle = { display: "flex", gap: "1rem" }
28
+
29
+ export const Variants: StoryObj<typeof meta> = {
30
+ render: args => (
31
+ <div style={variantsListStyle}>
32
+ {Object.values(TagVariants).map(variant => (
33
+ <Component key={variant} {...args} variant={variant}>
34
+ {variant}
35
+ </Component>
36
+ ))}
37
+ </div>
38
+ ),
39
+ }
@@ -1,10 +1,12 @@
1
1
  import { JSX, PropsWithChildren } from "react"
2
2
  import classNames from "classnames"
3
- import { AsTags, Icons } from "../../enums"
3
+ import { AsTags, Icons, TagVariants } from "../../enums"
4
4
 
5
5
  type TagProps = PropsWithChildren & {
6
6
  className?: string
7
7
  as?: AsTags
8
+ /** Colour variant. Defaults to the neutral, unstyled tag. */
9
+ variant?: TagVariants
8
10
  /**
9
11
  * When provided, renders a remove button. Called when the button is
10
12
  * activated — the consumer is responsible for removing the tag from state.
@@ -18,12 +20,20 @@ const Tag = ({
18
20
  children,
19
21
  className,
20
22
  as = AsTags.DIV,
23
+ variant,
21
24
  onRemove,
22
25
  removeLabel = "Remove",
23
26
  }: TagProps): JSX.Element => {
24
27
  const TagEl: JSX.ElementType = `${as}` as unknown as JSX.ElementType
25
28
  return (
26
- <TagEl className={classNames("mx-tag", { "mx-tag--dismissible": onRemove }, className)}>
29
+ <TagEl
30
+ className={classNames(
31
+ "mx-tag",
32
+ { "mx-tag--dismissible": onRemove },
33
+ variant && `mx-tag--${variant}`,
34
+ className,
35
+ )}
36
+ >
27
37
  {children}
28
38
  {onRemove && (
29
39
  <button
@@ -2,6 +2,7 @@ import { Meta, StoryObj } from "@storybook/html-vite"
2
2
  import Component from "./tag-item.twig"
3
3
  import "./tag.css"
4
4
  import { TagTypes } from "@pnx-mixtape/ids-shape"
5
+ import { TagVariants } from "../../enums"
5
6
 
6
7
  type TagItemType = {
7
8
  type: TagTypes
@@ -11,6 +12,7 @@ type TagItemType = {
11
12
  | { label: string; id: string; name: string; value: string; checked?: boolean }
12
13
  dismissible?: boolean
13
14
  removeLabel?: string
15
+ variant?: TagVariants
14
16
  }
15
17
 
16
18
  /**
@@ -49,6 +51,17 @@ const meta: Meta<TagItemType> = {
49
51
  "When true, plain (text) tags render a remove button. Wire up removal behaviour in the consuming app.",
50
52
  control: "boolean",
51
53
  },
54
+ variant: {
55
+ description: "Colour variant.",
56
+ control: {
57
+ type: "select",
58
+ labels: { undefined: "default" },
59
+ },
60
+ options: [undefined, ...Object.values(TagVariants)],
61
+ table: {
62
+ type: { summary: "enum" },
63
+ },
64
+ },
52
65
  },
53
66
  render: args => {
54
67
  return `<ul class="mx-tags">${Component({ ...args })}</ul>`
@@ -99,3 +112,15 @@ export const Checkbox: Story = {
99
112
  },
100
113
  },
101
114
  }
115
+
116
+ /**
117
+ * Colour variants for conveying status or emphasis.
118
+ */
119
+ export const Variants: Story = {
120
+ render: () => {
121
+ const items = Object.values(TagVariants)
122
+ .map(variant => Component({ type: TagTypes.TEXT, item: variant, variant }))
123
+ .join("")
124
+ return `<ul class="mx-tags">${items}</ul>`
125
+ },
126
+ }
@@ -6,13 +6,13 @@ exports[`Checkbox 1`] = `
6
6
  <li>
7
7
  <div class="mx-tag">
8
8
  <input type="checkbox" id="music" name="tag" value="music">
9
- <label for="music">Music</label>
9
+ <label class="mx-icon mx-icon--tick" for="music">Music</label>
10
10
  </div>
11
11
  </li>
12
12
  <li>
13
13
  <div class="mx-tag">
14
14
  <input type="checkbox" id="news" name="tag" value="news">
15
- <label for="news">News</label>
15
+ <label class="mx-icon mx-icon--tick" for="news">News</label>
16
16
  </div>
17
17
  </li>
18
18
 
@@ -1,3 +1,5 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`Tag 1`] = `"<div class="mx-tag">Tag name</div>"`;
4
+
5
+ exports[`Variants 1`] = `"<div style="display: flex; gap: 1rem;"><div class="mx-tag mx-tag--info">info</div><div class="mx-tag mx-tag--success">success</div><div class="mx-tag mx-tag--warning">warning</div><div class="mx-tag mx-tag--error">error</div><div class="mx-tag mx-tag--featured">featured</div></div>"`;
@@ -1,4 +1,4 @@
1
- {% set tagClasses = ['mx-tag', dismissible ? 'mx-tag--dismissible' : null] %}
1
+ {% set tagClasses = ['mx-tag', dismissible ? 'mx-tag--dismissible' : null, variant ? 'mx-tag--' ~ variant : null] %}
2
2
  {% set tagAttributes = (tagAttributes ?? create_attribute()).addClass(tagClasses) %}
3
3
  <li>
4
4
  {% if type == "link" %}
@@ -10,7 +10,7 @@
10
10
  name="{{ item.name }}"
11
11
  value="{{ item.value }}"
12
12
  {% if item.checked %} checked{% endif %}>
13
- <label for="{{ item.id }}">{{ item.label }}</label>
13
+ <label class="mx-icon mx-icon--tick" for="{{ item.id }}">{{ item.label }}</label>
14
14
  </div>
15
15
  {% else %}
16
16
  <span{{ tagAttributes }}>
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  @layer design-system.components {
6
- /* @public --tag-background --tag-foreground --tag-font-size --tags-gap */
6
+ /* @public --tag-background --tag-foreground --tag-font-size --tags-gap --tag-letter-spacing */
7
7
  .mx-tag {
8
8
  display: inline-flex;
9
9
  inline-size: fit-content;
@@ -16,6 +16,7 @@
16
16
  line-height: var(--line-height, 2);
17
17
  border-radius: var(--border-radius);
18
18
  border: var(--line-width, 1px) solid var(--line-colour, currentcolor);
19
+ letter-spacing: var(--tag-letter-spacing, 0);
19
20
  text-decoration: none;
20
21
  overflow: clip;
21
22
  overflow-clip-margin: var(--spacing-xxxs);
@@ -36,12 +37,35 @@
36
37
  font-size: inherit;
37
38
  color: inherit;
38
39
  margin: 0;
40
+ gap: var(--spacing-xxxs);
39
41
  }
40
42
 
41
- &:is(a:hover, :has(input:checked)) {
43
+ /* the checkmark icon (from the shared mx-icon set) is always reserved so
44
+ checking the tag doesn't change its size — only its visibility toggles */
45
+ & label.mx-icon--tick::before {
46
+ --icon-size: 1em;
47
+
48
+ opacity: 0;
49
+ }
50
+
51
+ &:is(a:hover):not(
52
+ :is(.mx-tag--info, .mx-tag--success, .mx-tag--warning, .mx-tag--error, .mx-tag--featured)
53
+ ) {
42
54
  --tag-foreground: var(--link-colour, var(--colour-primary));
43
55
  }
44
56
 
57
+ &:has(input:checked):not(
58
+ :is(.mx-tag--info, .mx-tag--success, .mx-tag--warning, .mx-tag--error, .mx-tag--featured)
59
+ ) {
60
+ --tag-background: var(--link-colour, var(--colour-primary));
61
+ --tag-foreground: var(--colour-foreground-reverse);
62
+ --line-colour: var(--tag-background);
63
+ }
64
+
65
+ &:has(input:checked) label.mx-icon--tick::before {
66
+ opacity: 1;
67
+ }
68
+
45
69
  &:empty {
46
70
  display: none;
47
71
  }
@@ -76,6 +100,56 @@
76
100
  }
77
101
  }
78
102
 
103
+ .mx-tag--info,
104
+ .mx-tag--success,
105
+ .mx-tag--warning,
106
+ .mx-tag--error,
107
+ .mx-tag--featured {
108
+ --line-colour: var(--tag-background);
109
+ --outline-color: var(--tag-background);
110
+ }
111
+
112
+ .mx-tag--info {
113
+ --tag-accent: var(--colour-info);
114
+ --tag-background: var(--tag-accent);
115
+ --tag-foreground: var(--colour-foreground-reverse);
116
+ }
117
+
118
+ .mx-tag--success {
119
+ --tag-accent: var(--colour-success);
120
+ --tag-background: var(--tag-accent);
121
+ --tag-foreground: var(--colour-foreground-reverse);
122
+ }
123
+
124
+ .mx-tag--warning {
125
+ --tag-accent: var(--colour-warning);
126
+ --tag-background: var(--tag-accent);
127
+ --tag-foreground: var(--colour-foreground-reverse);
128
+ }
129
+
130
+ .mx-tag--error {
131
+ --tag-accent: var(--colour-error);
132
+ --tag-background: var(--tag-accent);
133
+ --tag-foreground: var(--colour-foreground-reverse);
134
+ }
135
+
136
+ .mx-tag--featured {
137
+ --tag-accent: var(--colour-featured, var(--colour-accent));
138
+ --tag-background: var(--tag-accent);
139
+ --tag-foreground: var(--colour-featured-foreground, var(--colour-foreground));
140
+ }
141
+
142
+ .mx-tag:is(.mx-tag--info, .mx-tag--success, .mx-tag--warning, .mx-tag--error):is(a:hover) {
143
+ --tag-background: color-mix(in oklch, var(--tag-accent), black 20%);
144
+ --line-colour: var(--tag-background);
145
+ }
146
+
147
+ .mx-tag--featured:is(a:hover) {
148
+ /* featured keeps dark text, so a smaller darken step protects contrast */
149
+ --tag-background: color-mix(in oklch, var(--tag-accent), black 10%);
150
+ --line-colour: var(--tag-background);
151
+ }
152
+
79
153
  .mx-tags {
80
154
  padding-inline: 0;
81
155
  list-style: none;
@@ -4,6 +4,7 @@ import Component from "./autocomplete.twig"
4
4
  import "./autocomplete.css"
5
5
  import "./Elements/Autocomplete"
6
6
  import type { AutocompleteSearchEvent } from "./Elements/Autocomplete"
7
+ import { runScreenReaderWalkthrough } from "../../../tools/test-utils/screen-reader"
7
8
 
8
9
  const fruits = [
9
10
  "Apple",
@@ -59,7 +60,7 @@ type AutocompleteTwigProps = {
59
60
  // covers the part that can be shared - each play function still calls
60
61
  // `mount()` itself before wiring this up.
61
62
  const wireSearchDemo = (canvasElement: HTMLElement, id: string): void => {
62
- const root = canvasElement.querySelector<HTMLElement>("mx-autocomplete")
63
+ const root = canvasElement.querySelector("mx-autocomplete")
63
64
  const listbox = root?.querySelector<HTMLUListElement>('[role="listbox"]')
64
65
  if (!root || !listbox) return
65
66
 
@@ -67,17 +68,23 @@ const wireSearchDemo = (canvasElement: HTMLElement, id: string): void => {
67
68
  // pending response so a stale, since-superseded query can't land after a
68
69
  // newer one, which would yank the currently-tracked active option out from
69
70
  // under the widget mid-interaction.
70
- let pending: ReturnType<typeof setTimeout> | null = null
71
+ let pendingTimeout: ReturnType<typeof setTimeout> | null = null
71
72
 
72
73
  root.addEventListener("mx-autocomplete-search", event => {
73
- if (pending) clearTimeout(pending)
74
+ if (pendingTimeout) clearTimeout(pendingTimeout)
74
75
  const { query } = (event as AutocompleteSearchEvent).detail
75
76
  const matches = query
76
77
  ? fruits.filter(fruit => fruit.label.toLowerCase().includes(query.toLowerCase()))
77
78
  : []
78
79
 
80
+ // `pending` is fully consumer-controlled (see Elements/Autocomplete.ts) -
81
+ // set it before the simulated request starts, clear it once it settles.
82
+ // Only the most recent timeout ever fires (earlier ones were cleared
83
+ // above), so this can't be cleared out of order by a stale response.
84
+ root.pending = true
85
+
79
86
  // Simulate the latency of a real search API.
80
- pending = setTimeout(() => {
87
+ pendingTimeout = setTimeout(() => {
81
88
  listbox.replaceChildren(
82
89
  ...matches.map((option, index) => {
83
90
  const li = document.createElement("li")
@@ -90,6 +97,7 @@ const wireSearchDemo = (canvasElement: HTMLElement, id: string): void => {
90
97
  return li
91
98
  }),
92
99
  )
100
+ root.pending = false
93
101
  }, 150)
94
102
  })
95
103
  }
@@ -185,9 +193,14 @@ export const Autocomplete: Story = {
185
193
  await userEvent.clear(input)
186
194
  await expect(input).toHaveValue("")
187
195
  await expect(input).toHaveAttribute("aria-expanded", "false")
188
- await waitFor(() => expect(canvas.queryByRole("option")).not.toBeInTheDocument(), {
189
- timeout: 2000,
190
- })
196
+ // `hidden: true` includes the listbox even while it's closed (`hidden`
197
+ // attribute set) - otherwise this resolves immediately regardless of
198
+ // whether the stale options were actually removed, since closed
199
+ // elements are excluded from accessibility queries by default.
200
+ await waitFor(
201
+ () => expect(canvas.queryByRole("option", { hidden: true })).not.toBeInTheDocument(),
202
+ { timeout: 2000 },
203
+ )
191
204
  // Clear focus.
192
205
  await userEvent.click(canvasElement)
193
206
  })
@@ -211,3 +224,72 @@ export const Search: Story = {
211
224
  type: "search",
212
225
  },
213
226
  }
227
+
228
+ // Demonstrates the built-in pending state: `wireSearchDemo` above toggles the
229
+ // consumer-controlled `pending` property around its simulated search delay,
230
+ // which the Web Component reflects as `aria-busy` on the control - the same
231
+ // attribute React's implementation sets. A single generic CSS rule
232
+ // (`[aria-busy="true"]` in Utility/utilities.css) drives the spinner for
233
+ // both, and for any other component that reflects `aria-busy`.
234
+ export const Pending: Story = {
235
+ args: {
236
+ type: "search",
237
+ },
238
+ play: async ({ canvasElement, args, mount, canvas, userEvent, step }) => {
239
+ await mount()
240
+ wireSearchDemo(canvasElement, args.id)
241
+ const input = canvas.getByRole("combobox", { name: "Choose a fruit" })
242
+ const control = canvasElement.querySelector(".mx-autocomplete__control")
243
+ if (!control) throw new Error("mx-autocomplete__control not found")
244
+
245
+ await step("Shows a pending state while the simulated search resolves", async () => {
246
+ await userEvent.type(input, "lu")
247
+ await expect(control).toHaveAttribute("aria-busy", "true")
248
+ })
249
+
250
+ await step("Clears pending once the search settles", async () => {
251
+ await waitFor(
252
+ () => {
253
+ expect(control).toHaveAttribute("aria-busy", "false")
254
+ expect(canvas.getByRole("option", { name: "Blueberry" })).toBeInTheDocument()
255
+ },
256
+ { timeout: 2000 },
257
+ )
258
+ })
259
+ },
260
+ }
261
+
262
+ /**
263
+ * Steps a virtual screen reader through the page with matching results open, and asserts on what it announces.
264
+ */
265
+ export const ScreenReaderAnnouncements: Story = {
266
+ play: async ({ canvasElement, args, mount, canvas, userEvent }) => {
267
+ await mount()
268
+ wireSearchDemo(canvasElement, args.id)
269
+ const input = canvas.getByRole("combobox", { name: "Choose a fruit" })
270
+
271
+ // "lu" matches exactly two fruits: Blueberry, then Plum.
272
+ await userEvent.type(input, "lu")
273
+ await waitFor(() => expect(input).toHaveAttribute("aria-expanded", "true"), { timeout: 2000 })
274
+
275
+ const log = await runScreenReaderWalkthrough()
276
+ await expect(log).toEqual([
277
+ "document",
278
+ "not busy",
279
+ "combobox, Choose a fruit, lu, has popup listbox, expanded, autocomplete in list, 1 control",
280
+ "end, not busy",
281
+ "listbox, orientated vertically",
282
+ "option, Blueberry, not selected, position 1, set size 2",
283
+ "option, Plum, not selected, position 2, set size 2",
284
+ "end of listbox, orientated vertically",
285
+ "status",
286
+ "2 results available",
287
+ "end of status",
288
+ "end of document",
289
+ ])
290
+
291
+ await userEvent.clear(input)
292
+ await expect(input).toHaveAttribute("aria-expanded", "false")
293
+ await userEvent.click(canvasElement)
294
+ },
295
+ }
@@ -1,10 +1,11 @@
1
1
  import { useState } from "react"
2
2
  import { Meta, StoryObj } from "@storybook/react-vite"
3
- import { expect } from "storybook/test"
3
+ import { expect, waitFor } from "storybook/test"
4
4
  import Component, { AutocompleteOption } from "./Autocomplete"
5
5
  import "./autocomplete.css"
6
6
  import { Page } from "../../../.storybook-react/decorators"
7
7
  import { FormStatus } from "../../enums"
8
+ import { runScreenReaderWalkthrough } from "../../../tools/test-utils/screen-reader"
8
9
 
9
10
  const fruits = [
10
11
  "Apple",
@@ -147,3 +148,86 @@ export const Search: StoryObj<typeof meta> = {
147
148
  type: "search",
148
149
  },
149
150
  }
151
+
152
+ // Demonstrates the built-in pending state: `onSearch` returns a promise
153
+ // (standing in for a real debounced REST/Elasticsearch call), so the
154
+ // component sets `aria-busy` on the control until it resolves. A generic CSS
155
+ // rule (`[aria-busy="true"]` in Utility/utilities.css) shows the spinner -
156
+ // no component-specific markup or class needed.
157
+ export const Pending: StoryObj<typeof meta> = {
158
+ render: args => {
159
+ const [options, setOptions] = useState<AutocompleteOption[]>([])
160
+
161
+ const handleSearch = (query: string): Promise<void> =>
162
+ new Promise(resolve => {
163
+ setTimeout(() => {
164
+ setOptions(
165
+ query
166
+ ? fruits.filter(fruit => fruit.label.toLowerCase().includes(query.toLowerCase()))
167
+ : [],
168
+ )
169
+ resolve()
170
+ }, 800)
171
+ })
172
+
173
+ return (
174
+ <Component {...args} options={options} onSearch={handleSearch}>
175
+ {null}
176
+ </Component>
177
+ )
178
+ },
179
+ play: async ({ canvas, userEvent, step }) => {
180
+ const input = canvas.getByRole("combobox", { name: "Choose a fruit" })
181
+ const control = input.closest(".mx-autocomplete__control")
182
+
183
+ await step("Shows a pending spinner while the pending resolves", async () => {
184
+ await userEvent.type(input, "lu")
185
+ await expect(control).toHaveAttribute("aria-busy", "true")
186
+ })
187
+
188
+ await step("Clears pending once the search settles", async () => {
189
+ await waitFor(
190
+ () => {
191
+ expect(control).toHaveAttribute("aria-busy", "false")
192
+ expect(canvas.getByRole("option", { name: "Blueberry" })).toBeInTheDocument()
193
+ },
194
+ { timeout: 2000 },
195
+ )
196
+ })
197
+ },
198
+ }
199
+
200
+ /**
201
+ * Steps a virtual screen reader through the page with matching results open, and asserts on what it announces.
202
+ *
203
+ * Kept as the last story in this file - see the note on ordering in
204
+ * `docs/Testing.md`'s Automated Screen Reader Testing section.
205
+ */
206
+ export const ScreenReaderAnnouncements: StoryObj<typeof meta> = {
207
+ play: async ({ canvas, userEvent }) => {
208
+ const input = canvas.getByRole("combobox", { name: "Choose a fruit" })
209
+
210
+ // "lu" matches exactly two fruits: Blueberry, then Plum.
211
+ await userEvent.type(input, "lu")
212
+ await expect(input).toHaveAttribute("aria-expanded", "true")
213
+
214
+ const log = await runScreenReaderWalkthrough()
215
+ await expect(log).toEqual([
216
+ "document",
217
+ "not busy",
218
+ "combobox, Choose a fruit, lu, has popup listbox, expanded, autocomplete in list, 1 control, not invalid",
219
+ "end, not busy",
220
+ "listbox, orientated vertically",
221
+ "option, Blueberry, not selected, position 1, set size 2",
222
+ "option, Plum, not selected, position 2, set size 2",
223
+ "end of listbox, orientated vertically",
224
+ "status",
225
+ "2 results available",
226
+ "end of status",
227
+ "end of document",
228
+ ])
229
+
230
+ await userEvent.clear(input)
231
+ await expect(input).toHaveAttribute("aria-expanded", "false")
232
+ },
233
+ }