@pnx-mixtape/mxds 0.1.0-rc.4 → 0.1.0-rc.6

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 (236) hide show
  1. package/.claude/skills/create-mxds-component/SKILL.md +77 -0
  2. package/DESIGN.md +595 -0
  3. package/README.md +2 -3
  4. package/dist/build/accordion.entry.js +2 -1
  5. package/dist/build/autocomplete.entry.js +1 -1
  6. package/dist/build/back-to-top.css +1 -0
  7. package/dist/build/back-to-top.entry.js +1 -0
  8. package/dist/build/base.css +1 -1
  9. package/dist/build/button.css +1 -1
  10. package/dist/build/card.css +1 -1
  11. package/dist/build/chunks/Accordion-Bqe8vTNR.js +44 -0
  12. package/dist/build/chunks/AccordionGroup-BYtoAFxg.js +47 -0
  13. package/dist/build/chunks/AccordionMobile-D517oXYZ.js +26 -0
  14. package/dist/build/chunks/Autocomplete-kRo8RNbC.js +135 -0
  15. package/dist/build/chunks/BackToTop-4M-Wl7z3.js +64 -0
  16. package/dist/build/chunks/ClosableAlert-CZ114JsL.js +55 -0
  17. package/dist/build/chunks/CookieConsent-wd8gQ2Oe.js +18 -0
  18. package/dist/build/chunks/Dialog-DoYcGqhW.js +48 -0
  19. package/dist/build/chunks/DropMenu-CSTZq0Zt.js +68 -0
  20. package/dist/build/chunks/FeedbackForm-BsL7fv8s.js +64 -0
  21. package/dist/build/chunks/Gallery-DqJPujX5.js +165 -0
  22. package/dist/build/chunks/GlobalToggle-7H3F1Wvc.js +82 -0
  23. package/dist/build/chunks/InPageNavigation-D7m3IN89.js +86 -0
  24. package/dist/build/chunks/Navigation-CRModT40.js +142 -0
  25. package/dist/build/chunks/Popover-CiTAftNU.js +30 -0
  26. package/dist/build/chunks/Sticky-C1H8MNqK.js +51 -0
  27. package/dist/build/chunks/Tabs-BJD3kABL.js +160 -0
  28. package/dist/build/chunks/Translate-yAgySwet.js +46 -0
  29. package/dist/build/chunks/cookie-compliance-CG6x-TcV.js +119 -0
  30. package/dist/build/chunks/enums-BrHxPZZ_.js +36 -0
  31. package/dist/build/chunks/polyfills-D-qh1a_2.js +33 -0
  32. package/dist/build/chunks/utilities-B9mNRs7V.js +255 -0
  33. package/dist/build/content-block.css +1 -1
  34. package/dist/build/cookie-consent.entry.js +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 +147 -1
  38. package/dist/build/feedback-form.css +1 -0
  39. package/dist/build/feedback-form.entry.js +1 -0
  40. package/dist/build/filters.entry.js +145 -1
  41. package/dist/build/form.css +1 -1
  42. package/dist/build/gallery.entry.js +1 -1
  43. package/dist/build/global-alert.entry.js +1 -1
  44. package/dist/build/header.css +1 -1
  45. package/dist/build/header.entry.js +1 -1
  46. package/dist/build/icon.css +1 -1
  47. package/dist/build/in-page-navigation.entry.js +1 -1
  48. package/dist/build/navigation.css +1 -1
  49. package/dist/build/navigation.entry.js +1 -1
  50. package/dist/build/pagination.css +1 -1
  51. package/dist/build/popover.entry.js +1 -1
  52. package/dist/build/react.js +2291 -1
  53. package/dist/build/side-navigation.css +1 -1
  54. package/dist/build/sidebar.css +1 -1
  55. package/dist/build/sticky.entry.js +1 -1
  56. package/dist/build/tabs.entry.js +1 -1
  57. package/dist/build/translate.css +1 -0
  58. package/dist/build/translate.entry.js +1 -0
  59. package/dist/build/utility-list.entry.js +53 -1
  60. package/dist/types/Component/BackToTop/BackToTop.d.ts +20 -0
  61. package/dist/types/Component/BackToTop/BackToTop.d.ts.map +1 -0
  62. package/dist/types/Component/BackToTop/Elements/BackToTop.d.ts +27 -0
  63. package/dist/types/Component/BackToTop/Elements/BackToTop.d.ts.map +1 -0
  64. package/dist/types/Component/BackToTop/back-to-top.entry.d.ts +2 -0
  65. package/dist/types/Component/BackToTop/back-to-top.entry.d.ts.map +1 -0
  66. package/dist/types/Component/DropMenu/DropMenu.d.ts +2 -1
  67. package/dist/types/Component/DropMenu/DropMenu.d.ts.map +1 -1
  68. package/dist/types/Component/Navigation/Elements/Navigation.d.ts +2 -3
  69. package/dist/types/Component/Navigation/Elements/Navigation.d.ts.map +1 -1
  70. package/dist/types/Component/SideNavigation/Components/SideNavigationTree.d.ts +3 -3
  71. package/dist/types/Component/SideNavigation/Components/SideNavigationTree.d.ts.map +1 -1
  72. package/dist/types/Component/SideNavigation/SideNavigation.d.ts +3 -1
  73. package/dist/types/Component/SideNavigation/SideNavigation.d.ts.map +1 -1
  74. package/dist/types/Component/Tabs/Components/TabDropMenu.d.ts.map +1 -1
  75. package/dist/types/Component/Tabs/Elements/Tabs.d.ts.map +1 -1
  76. package/dist/types/Component/Translate/Elements/Translate.d.ts +23 -0
  77. package/dist/types/Component/Translate/Elements/Translate.d.ts.map +1 -0
  78. package/dist/types/Component/Translate/Translate.d.ts +37 -0
  79. package/dist/types/Component/Translate/Translate.d.ts.map +1 -0
  80. package/dist/types/Component/Translate/languages.d.ts +11 -0
  81. package/dist/types/Component/Translate/languages.d.ts.map +1 -0
  82. package/dist/types/Component/Translate/translate.entry.d.ts +2 -0
  83. package/dist/types/Component/Translate/translate.entry.d.ts.map +1 -0
  84. package/dist/types/Form/FeedbackForm/Elements/FeedbackForm.d.ts +37 -0
  85. package/dist/types/Form/FeedbackForm/Elements/FeedbackForm.d.ts.map +1 -0
  86. package/dist/types/Form/FeedbackForm/FeedbackForm.d.ts +36 -0
  87. package/dist/types/Form/FeedbackForm/FeedbackForm.d.ts.map +1 -0
  88. package/dist/types/Form/FeedbackForm/feedback-form.entry.d.ts +2 -0
  89. package/dist/types/Form/FeedbackForm/feedback-form.entry.d.ts.map +1 -0
  90. package/dist/types/Utility/Elements/google-translate-widget.d.ts +91 -0
  91. package/dist/types/Utility/Elements/google-translate-widget.d.ts.map +1 -0
  92. package/dist/types/Utility/utilities.d.ts +2 -0
  93. package/dist/types/Utility/utilities.d.ts.map +1 -1
  94. package/dist/types/elements.d.ts +3 -0
  95. package/dist/types/elements.d.ts.map +1 -1
  96. package/dist/types/enums.d.ts +3 -1
  97. package/dist/types/enums.d.ts.map +1 -1
  98. package/dist/types/react.d.ts +3 -0
  99. package/dist/types/react.d.ts.map +1 -1
  100. package/dist/types/tokens.d.ts +1 -0
  101. package/dist/types/tokens.d.ts.map +1 -1
  102. package/docs/API.md +1215 -0
  103. package/docs/CSSGuidelines.md +893 -0
  104. package/docs/CodeQuality.md +940 -0
  105. package/docs/ComponentArchitecture.md +714 -0
  106. package/docs/Contributing.md +626 -0
  107. package/docs/MemoryManagement.md +598 -0
  108. package/docs/Migrations.md +6 -0
  109. package/docs/README.md +100 -0
  110. package/docs/Security.md +444 -0
  111. package/docs/Testing.md +739 -0
  112. package/docs/Troubleshooting.md +674 -0
  113. package/package.json +9 -6
  114. package/src/Atom/Button/Button.tsx +1 -1
  115. package/src/Atom/Button/__snapshots__/Button.stories.tsx.snap +2 -2
  116. package/src/Atom/Button/_buttons.css +1 -0
  117. package/src/Atom/Icon/README.md +1 -1
  118. package/src/Atom/Icon/_icon.css +1 -0
  119. package/src/Atom/Icon/images/thumb.svg +1 -0
  120. package/src/Atom/Link/_links.css +1 -0
  121. package/src/Atom/Media/__snapshots__/Media.stories.ts.snap +4 -9
  122. package/src/Atom/Media/media.twig +4 -5
  123. package/src/Component/BackToTop/BackToTop.stories.ts +127 -0
  124. package/src/Component/BackToTop/BackToTop.stories.tsx +53 -0
  125. package/src/Component/BackToTop/BackToTop.tsx +120 -0
  126. package/src/Component/BackToTop/Elements/BackToTop.ts +119 -0
  127. package/src/Component/BackToTop/README.md +164 -0
  128. package/src/Component/BackToTop/__snapshots__/BackToTop.stories.ts.snap +24 -0
  129. package/src/Component/BackToTop/__snapshots__/BackToTop.stories.tsx.snap +3 -0
  130. package/src/Component/BackToTop/back-to-top.css +70 -0
  131. package/src/Component/BackToTop/back-to-top.entry.js +1 -0
  132. package/src/Component/BackToTop/back-to-top.twig +26 -0
  133. package/src/Component/Card/Components/CardContent.tsx +1 -1
  134. package/src/Component/Card/Components/CardMedia.tsx +1 -1
  135. package/src/Component/Card/__snapshots__/Card.stories.tsx.snap +3 -3
  136. package/src/Component/Card/card.css +1 -1
  137. package/src/Component/Carousel/Carousel.stories.ts +66 -36
  138. package/src/Component/Carousel/__snapshots__/Carousel.stories.tsx.snap +5 -5
  139. package/src/Component/ContentBlock/Components/ContentBlockMedia.tsx +1 -1
  140. package/src/Component/ContentBlock/content-block.css +1 -1
  141. package/src/Component/DropMenu/DropMenu.stories.ts +21 -2
  142. package/src/Component/DropMenu/DropMenu.tsx +14 -4
  143. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.ts.snap +3 -3
  144. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.tsx.snap +2 -2
  145. package/src/Component/DropMenu/drop-menu.twig +7 -1
  146. package/src/Component/HeroBanner/__snapshots__/HeroBanner.stories.tsx.snap +1 -1
  147. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +48 -13
  148. package/src/Component/InPageNavigation/in-page-navigation.twig +4 -6
  149. package/src/Component/ListItem/Components/ListItemMedia.tsx +1 -1
  150. package/src/Component/Navigation/Elements/Navigation.ts +25 -29
  151. package/src/Component/Navigation/README.md +11 -10
  152. package/src/Component/Navigation/__snapshots__/Navigation.stories.ts.snap +8 -8
  153. package/src/Component/Navigation/_navigation-dropdown.css +9 -11
  154. package/src/Component/Navigation/_navigation-mega.css +5 -11
  155. package/src/Component/Navigation/_navigation.css +5 -3
  156. package/src/Component/Navigation/menu.twig +1 -1
  157. package/src/Component/Navigation/navigation.twig +0 -1
  158. package/src/Component/Pagination/Pagination.stories.ts +57 -27
  159. package/src/Component/Pagination/pagination.css +16 -7
  160. package/src/Component/Popover/__snapshots__/Popover.stories.tsx.snap +3 -3
  161. package/src/Component/SideNavigation/Components/SideNavigationTree.tsx +68 -24
  162. package/src/Component/SideNavigation/README.md +78 -32
  163. package/src/Component/SideNavigation/SideNavigation.stories.ts +41 -2
  164. package/src/Component/SideNavigation/SideNavigation.stories.tsx +31 -0
  165. package/src/Component/SideNavigation/SideNavigation.tsx +13 -3
  166. package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.ts.snap +87 -3
  167. package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.tsx.snap +2 -0
  168. package/src/Component/SideNavigation/side-navigation.css +3 -3
  169. package/src/Component/SideNavigation/side-navigation.twig +10 -3
  170. package/src/Component/Tabs/Components/TabDropMenu.tsx +7 -1
  171. package/src/Component/Tabs/Elements/Tabs.ts +13 -3
  172. package/src/Component/Tabs/Tabs.stories.ts +33 -12
  173. package/src/Component/Tabs/Tabs.stories.tsx +28 -14
  174. package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +3 -3
  175. package/src/Component/Tabs/__snapshots__/Tabs.stories.tsx.snap +2 -2
  176. package/src/Component/Translate/Elements/Translate.ts +76 -0
  177. package/src/Component/Translate/README.md +285 -0
  178. package/src/Component/Translate/Translate.stories.ts +87 -0
  179. package/src/Component/Translate/Translate.stories.tsx +77 -0
  180. package/src/Component/Translate/Translate.tsx +213 -0
  181. package/src/Component/Translate/__snapshots__/Translate.stories.ts.snap +18 -0
  182. package/src/Component/Translate/__snapshots__/Translate.stories.tsx.snap +3 -0
  183. package/src/Component/Translate/languages.ts +39 -0
  184. package/src/Component/Translate/translate.css +62 -0
  185. package/src/Component/Translate/translate.entry.js +1 -0
  186. package/src/Component/Translate/translate.twig +44 -0
  187. package/src/Form/Autocomplete/Autocomplete.stories.tsx +2 -1
  188. package/src/Form/FeedbackForm/Elements/FeedbackForm.ts +122 -0
  189. package/src/Form/FeedbackForm/FeedbackForm.stories.ts +99 -0
  190. package/src/Form/FeedbackForm/FeedbackForm.stories.tsx +73 -0
  191. package/src/Form/FeedbackForm/FeedbackForm.tsx +196 -0
  192. package/src/Form/FeedbackForm/README.md +233 -0
  193. package/src/Form/FeedbackForm/__snapshots__/FeedbackForm.stories.ts.snap +125 -0
  194. package/src/Form/FeedbackForm/__snapshots__/FeedbackForm.stories.tsx.snap +5 -0
  195. package/src/Form/FeedbackForm/feedback-form.css +74 -0
  196. package/src/Form/FeedbackForm/feedback-form.entry.js +1 -0
  197. package/src/Form/FeedbackForm/feedback-form.twig +80 -0
  198. package/src/Form/form.css +17 -19
  199. package/src/Introduction.mdx +1 -1
  200. package/src/Layout/Footer/__snapshots__/Footer.stories.ts.snap +3 -3
  201. package/src/Layout/Header/Components/GlobalToggle.tsx +1 -1
  202. package/src/Layout/Header/Header.stories.ts +53 -48
  203. package/src/Layout/Header/Header.stories.tsx +35 -22
  204. package/src/Layout/Header/Header.tsx +2 -2
  205. package/src/Layout/Header/__snapshots__/Header.stories.ts.snap +18 -14
  206. package/src/Layout/Header/__snapshots__/Header.stories.tsx.snap +2 -2
  207. package/src/Layout/Header/_toggles.css +7 -7
  208. package/src/Layout/Page/__snapshots__/Page.stories.tsx.snap +1 -1
  209. package/src/Layout/Sidebar/sidebar.css +10 -3
  210. package/src/Utility/Elements/google-translate-widget.ts +293 -0
  211. package/src/Utility/global.d.ts +4 -0
  212. package/src/Utility/utilities.ts +2 -0
  213. package/src/_custom-media.css +2 -0
  214. package/src/elements.ts +3 -0
  215. package/src/enums.ts +2 -0
  216. package/src/react.ts +3 -0
  217. package/src/tokens.js +1 -0
  218. package/dist/build/chunks/Accordion-GN0sME4W.js +0 -1
  219. package/dist/build/chunks/AccordionGroup-Bzqp5__q.js +0 -1
  220. package/dist/build/chunks/AccordionMobile-DG5jSe8Q.js +0 -1
  221. package/dist/build/chunks/Autocomplete-B4RtkAyh.js +0 -1
  222. package/dist/build/chunks/ClosableAlert-DEhj8Nxm.js +0 -1
  223. package/dist/build/chunks/CookieConsent-BkCMRVST.js +0 -1
  224. package/dist/build/chunks/Dialog-Haahn-Yn.js +0 -1
  225. package/dist/build/chunks/DropMenu-1baQc8Yc.js +0 -1
  226. package/dist/build/chunks/Gallery-D8NGXy2Q.js +0 -1
  227. package/dist/build/chunks/GlobalToggle-B52fdBQu.js +0 -1
  228. package/dist/build/chunks/InPageNavigation-Dyn2TR-N.js +0 -1
  229. package/dist/build/chunks/Navigation-hOrfLq7h.js +0 -1
  230. package/dist/build/chunks/Popover-Cxg-s5Jn.js +0 -1
  231. package/dist/build/chunks/Sticky-BGCuuEJK.js +0 -1
  232. package/dist/build/chunks/Tabs-DYy1YTdU.js +0 -15
  233. package/dist/build/chunks/cookie-compliance-Bbm-0vpj.js +0 -1
  234. package/dist/build/chunks/enums-DLV_1NAf.js +0 -1
  235. package/dist/build/chunks/polyfills-C7pRt54M.js +0 -1
  236. package/dist/build/chunks/utilities-_Bq3TxAQ.js +0 -1
@@ -20,6 +20,10 @@
20
20
  &:is(.mx-nav__has-subnav, :has(> ul)) {
21
21
  position: relative;
22
22
 
23
+ /* Each sub-menu anchors to its own parent item. */
24
+ anchor-scope: --mx-nav-item;
25
+ anchor-name: --mx-nav-item;
26
+
23
27
  & > a {
24
28
  --toggle-gap: var(--spacing-l);
25
29
  }
@@ -90,7 +94,11 @@
90
94
  margin: 0;
91
95
 
92
96
  @media (--global-nav-up) {
93
- position: absolute;
97
+ /* Fixed so fallbacks flip against the viewport, not the parent item. */
98
+ position: fixed;
99
+ position-anchor: --mx-nav-item;
100
+ position-area: block-end span-inline-end;
101
+ position-try-fallbacks: flip-inline;
94
102
  inline-size: var(--dropdown-width, 270px);
95
103
  border-radius: var(--border-radius);
96
104
  background-color: var(
@@ -113,16 +121,6 @@
113
121
  }
114
122
  }
115
123
 
116
- & .mx-nav--reverse .nav__level-2 {
117
- @media (--global-nav-up) {
118
- inset-inline: auto 0;
119
-
120
- & .mx-nav__toggle::before {
121
- transform: rotate(90deg);
122
- }
123
- }
124
- }
125
-
126
124
  & .mx-nav__separator {
127
125
  border-block-start: var(--line-width, 1px) solid var(--line-colour, var(--colour-border));
128
126
  }
@@ -17,11 +17,12 @@
17
17
  inline-size: 100%;
18
18
 
19
19
  @media (--global-nav-up) {
20
- position: absolute;
21
- inset-inline-start: var(--dropdown-width, 270px);
22
- inset-block-start: 0;
20
+ position: fixed;
21
+ position-anchor: --mx-nav-item;
22
+ position-area: span-block-end inline-end;
23
+ position-try-fallbacks: flip-inline;
23
24
  inline-size: var(--dropdown-width, 270px);
24
- min-block-size: 100%;
25
+ min-block-size: anchor-size(block);
25
26
  padding-inline-start: 0;
26
27
  border-block-start: 0;
27
28
  border-radius: 0 var(--border-radius) var(--border-radius) 0;
@@ -37,12 +38,5 @@
37
38
  }
38
39
  }
39
40
  }
40
-
41
- & .mx-nav--reverse .mx-nav__level-2 ul {
42
- @media (--global-nav-up) {
43
- inset-inline-start: calc(var(--dropdown-width, 270px) * -1);
44
- border-radius: var(--border-radius) 0 0 var(--border-radius);
45
- }
46
- }
47
41
  }
48
42
  }
@@ -45,11 +45,13 @@
45
45
  inset-block-start: var(--toggle-top, 12px);
46
46
  inset-inline-end: 0;
47
47
  cursor: pointer;
48
- }
49
48
 
50
- .mx-nav--open {
51
- & > .mx-nav__toggle::before {
49
+ &[aria-expanded="true"]::before {
52
50
  transform: rotate(180deg);
53
51
  }
54
52
  }
53
+
54
+ mx-nav:not(:defined) .mx-nav__toggle {
55
+ display: none;
56
+ }
55
57
  }
@@ -23,7 +23,7 @@
23
23
  {{ item.link.title }}
24
24
  {% endif %}
25
25
  {% if item.items is not empty %}
26
- <button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only">Sub-navigation</button>
26
+ <button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only" type="button">Sub-navigation</button>
27
27
  {{ nav.menutree(item.items, level + 1, setInert, baseClass) }}
28
28
  {% endif %}
29
29
  </li>
@@ -14,7 +14,6 @@
14
14
  {% if type == "mega" %}
15
15
  {# Horizontal menu with 3 levels deep, no hover flyout. #}
16
16
  {% set needsJs = true %}
17
- {% set componentAttributes = componentAttributes.addAttribute("edgeCheck") %}
18
17
  {% set modifiers = ['dropdown', 'mega-nav'] %}
19
18
  {% endif %}
20
19
  {% if type == "footer" %}
@@ -136,35 +136,65 @@ export const Pagination: Story = {
136
136
 
137
137
  /**
138
138
  * Steps a virtual screen reader through the page and asserts on what it announces.
139
+ *
140
+ * Pagination's narrow/wide layouts are driven by a CSS container query (not
141
+ * `matchMedia`) that swaps the numbered link list for a "Page X of Y" label -
142
+ * see `pagination.css`. Chromatic exercises both `chromatic.modes` (mobile and
143
+ * desktop), so this branches on the container's own rendered state (which of
144
+ * the two is actually visible) rather than hardcoding one shape. Whichever
145
+ * mode a local run happens to render at, only one branch runs meaningfully for
146
+ * that run - the other is exercised by Chromatic's other mode in CI. See
147
+ * "Testing Mobile / matchMedia-Driven Behavior in Play Functions" in
148
+ * docs/Testing.md.
139
149
  */
140
150
  export const ScreenReaderAnnouncements: Story = {
141
- play: async () => {
151
+ play: async ({ canvasElement }) => {
152
+ const label = canvasElement.querySelector(".mx-pagination__label")
153
+ const isNarrow = label !== null && getComputedStyle(label).display !== "none"
142
154
  const log = await runScreenReaderWalkthrough()
143
- await expect(log).toEqual([
144
- "document",
145
- "navigation, Pagination",
146
- "link, Previous",
147
- "Previous",
148
- "end of link, Previous",
149
- "list",
150
- "listitem, level 1, position 1, set size 4",
151
- "link, 1",
152
- "end of listitem, level 1, position 1, set size 4",
153
- "listitem, level 1, position 2, set size 4",
154
- "link, 2, current page",
155
- "end of listitem, level 1, position 2, set size 4",
156
- "listitem, level 1, position 3, set size 4",
157
- "link, 3",
158
- "end of listitem, level 1, position 3, set size 4",
159
- "listitem, level 1, position 4, set size 4",
160
- "link, 10",
161
- "end of listitem, level 1, position 4, set size 4",
162
- "end of list",
163
- "link, Next",
164
- "Next",
165
- "end of link, Next",
166
- "end of navigation, Pagination",
167
- "end of document",
168
- ])
155
+
156
+ if (isNarrow) {
157
+ await expect(log).toEqual([
158
+ "document",
159
+ "navigation, Pagination",
160
+ "link, Previous",
161
+ "Previous",
162
+ "end of link, Previous",
163
+ "Page 2 of 10",
164
+ "list",
165
+ "link, Next",
166
+ "Next",
167
+ "end of link, Next",
168
+ "end of navigation, Pagination",
169
+ "end of document",
170
+ ])
171
+ } else {
172
+ await expect(log).toEqual([
173
+ "document",
174
+ "navigation, Pagination",
175
+ "link, Previous",
176
+ "Previous",
177
+ "end of link, Previous",
178
+ "list",
179
+ "listitem, level 1, position 1, set size 4",
180
+ "link, 1",
181
+ "end of listitem, level 1, position 1, set size 4",
182
+ "listitem, level 1, position 2, set size 4",
183
+ "link, 2, current page",
184
+ "end of listitem, level 1, position 2, set size 4",
185
+ "listitem, level 1, position 3, set size 4",
186
+ "link, 3",
187
+ "end of listitem, level 1, position 3, set size 4",
188
+ "listitem, level 1, position 4, set size 4",
189
+ "link, 10",
190
+ "end of listitem, level 1, position 4, set size 4",
191
+ "end of list",
192
+ "link, Next",
193
+ "Next",
194
+ "end of link, Next",
195
+ "end of navigation, Pagination",
196
+ "end of document",
197
+ ])
198
+ }
169
199
  },
170
200
  }
@@ -2,8 +2,6 @@
2
2
  * Pagination
3
3
  */
4
4
 
5
- @import "../../Utility/_mixins.css";
6
-
7
5
  @layer design-system.components {
8
6
  .mx-pagination {
9
7
  margin-inline: 0;
@@ -102,11 +100,22 @@
102
100
  & :is(.mx-pagination__previous, .mx-pagination__next) {
103
101
  text-transform: capitalize;
104
102
 
105
- & span:where(:not(.mx-icon)) {
106
- @add-mixin sr-only;
107
-
108
- @container pagination (min-width: 25ch) {
109
- @add-mixin sr-undo;
103
+ & :is(a, button) {
104
+ @container pagination (max-width: 32ch) {
105
+ inline-size: var(--button-min-height, var(--spacing-xl));
106
+ block-size: var(--button-min-height, var(--spacing-xl));
107
+ flex-wrap: wrap;
108
+ position: relative;
109
+ padding: 0;
110
+ overflow: hidden;
111
+
112
+ & .mx-icon {
113
+ inline-size: var(--button-min-height, var(--spacing-xl));
114
+ block-size: var(--button-min-height, var(--spacing-xl));
115
+ place-content: center;
116
+ display: flex;
117
+ order: -1;
118
+ }
110
119
  }
111
120
  }
112
121
  }
@@ -1,7 +1,7 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`Content Only 1`] = `"<button class="mx-button mx-icon--end" type="button" id="story-popover-minimal-trigger" popovertarget="story-popover-minimal" style="anchor-name: --story-popover-minimal;">Popover trigger<span class="mx-icon mx-icon--question"></span></button><div class="mx-popover" id="story-popover-minimal" aria-describedby="story-popover-minimal-trigger" data-placement="bottom" popover="" style="position-anchor: --story-popover-minimal;"><div class="mx-rich-text mx-vertical-flow"><p>A simple hint with no heading or action link.</p></div></div>"`;
3
+ exports[`Content Only 1`] = `"<button class="mx-button mx-icon--end" type="button" id="story-popover-minimal-trigger" popovertarget="story-popover-minimal" style="anchor-name: --story-popover-minimal;"><span>Popover trigger</span><span class="mx-icon mx-icon--question"></span></button><div class="mx-popover" id="story-popover-minimal" aria-describedby="story-popover-minimal-trigger" data-placement="bottom" popover="" style="position-anchor: --story-popover-minimal;"><div class="mx-rich-text mx-vertical-flow"><p>A simple hint with no heading or action link.</p></div></div>"`;
4
4
 
5
- exports[`Default 1`] = `"<button class="mx-button mx-icon--end" type="button" id="story-popover-trigger" popovertarget="story-popover" style="anchor-name: --story-popover;">Popover trigger<span class="mx-icon mx-icon--question"></span></button><div class="mx-popover" id="story-popover" aria-describedby="story-popover-trigger" data-placement="bottom" popover="" style="position-anchor: --story-popover;"><div class="mx-rich-text mx-vertical-flow"><h2>Popover heading</h2><p>Short supplementary content. Use Popover for non-interactive hints; for menus use DropMenu, for modal content use Dialog.</p><a href="#" class="mx-link mx-link--more">Learn more</a></div></div>"`;
5
+ exports[`Default 1`] = `"<button class="mx-button mx-icon--end" type="button" id="story-popover-trigger" popovertarget="story-popover" style="anchor-name: --story-popover;"><span>Popover trigger</span><span class="mx-icon mx-icon--question"></span></button><div class="mx-popover" id="story-popover" aria-describedby="story-popover-trigger" data-placement="bottom" popover="" style="position-anchor: --story-popover;"><div class="mx-rich-text mx-vertical-flow"><h2>Popover heading</h2><p>Short supplementary content. Use Popover for non-interactive hints; for menus use DropMenu, for modal content use Dialog.</p><a href="#" class="mx-link mx-link--more">Learn more</a></div></div>"`;
6
6
 
7
- exports[`Placement 1`] = `"<button class="mx-button mx-icon--end" type="button" id="story-popover-placement-trigger" popovertarget="story-popover-placement" style="anchor-name: --story-popover-placement;">Popover trigger<span class="mx-icon mx-icon--question"></span></button><div class="mx-popover" id="story-popover-placement" aria-describedby="story-popover-placement-trigger" data-placement="top" popover="" style="position-anchor: --story-popover-placement;"><div class="mx-rich-text mx-vertical-flow"><h2>Popover heading</h2><p>Short supplementary content. Use Popover for non-interactive hints; for menus use DropMenu, for modal content use Dialog.</p><a href="#" class="mx-link mx-link--more">Learn more</a></div></div>"`;
7
+ exports[`Placement 1`] = `"<button class="mx-button mx-icon--end" type="button" id="story-popover-placement-trigger" popovertarget="story-popover-placement" style="anchor-name: --story-popover-placement;"><span>Popover trigger</span><span class="mx-icon mx-icon--question"></span></button><div class="mx-popover" id="story-popover-placement" aria-describedby="story-popover-placement-trigger" data-placement="top" popover="" style="position-anchor: --story-popover-placement;"><div class="mx-rich-text mx-vertical-flow"><h2>Popover heading</h2><p>Short supplementary content. Use Popover for non-interactive hints; for menus use DropMenu, for modal content use Dialog.</p><a href="#" class="mx-link mx-link--more">Learn more</a></div></div>"`;
@@ -1,37 +1,81 @@
1
- import { JSX } from "react"
1
+ import { JSX, ComponentPropsWithoutRef, useContext, useId } from "react"
2
+ import { useDisclosureState } from "react-stately"
2
3
  import { MenuTree } from "@pnx-mixtape/ids-shape"
3
4
  import Link from "../../../Atom/Link/Link"
5
+ import { SideNavigationContext } from "../SideNavigation"
4
6
 
5
- type SideNavigationTreeProps = {
7
+ type SideNavigationTreeProps = ComponentPropsWithoutRef<"ul"> & {
6
8
  items: MenuTree[]
7
9
  level?: number
8
10
  }
9
11
 
10
- const SideNavigationTree = ({ items, level = 0 }: SideNavigationTreeProps): JSX.Element => (
11
- <ul className={`mx-nav__level-${level + 1}`}>
12
+ type SideNavigationItemProps = {
13
+ item: MenuTree
14
+ level: number
15
+ }
16
+
17
+ // True when this item, or any descendant, is the current page.
18
+ const hasCurrent = ({ link, items }: MenuTree): boolean =>
19
+ Boolean(link.current) || Boolean(items?.some(hasCurrent))
20
+
21
+ const SideNavigationItem = ({ item, level }: SideNavigationItemProps): JSX.Element => {
22
+ const collapsible = useContext(SideNavigationContext)
23
+ const { isExpanded, toggle } = useDisclosureState({ defaultExpanded: hasCurrent(item) })
24
+ const listId = useId()
25
+ const labelId = useId()
26
+ const hasItems = item.items && item.items.length > 0
27
+
28
+ return (
29
+ <li className={hasItems ? "mx-nav__has-subnav" : undefined}>
30
+ {item.link.href ? (
31
+ <Link
32
+ to={item.link.href}
33
+ id={hasItems && collapsible ? labelId : undefined}
34
+ aria-current={item.link.current ? "page" : undefined}
35
+ >
36
+ {item.link.title}
37
+ </Link>
38
+ ) : (
39
+ item.link.title
40
+ )}
41
+ {hasItems && (
42
+ <>
43
+ <button
44
+ className="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only"
45
+ type="button"
46
+ {...(collapsible && {
47
+ "aria-expanded": isExpanded,
48
+ "aria-controls": listId,
49
+ onClick: toggle,
50
+ })}
51
+ >
52
+ Sub-navigation
53
+ </button>
54
+ <SideNavigationTree
55
+ items={item.items}
56
+ level={level + 1}
57
+ {...(collapsible && {
58
+ id: listId,
59
+ "aria-labelledby": item.link.href ? labelId : undefined,
60
+ inert: !isExpanded,
61
+ })}
62
+ />
63
+ </>
64
+ )}
65
+ </li>
66
+ )
67
+ }
68
+
69
+ const SideNavigationTree = ({
70
+ items,
71
+ level = 0,
72
+ ...props
73
+ }: SideNavigationTreeProps): JSX.Element => (
74
+ <ul className={`mx-nav__level-${level + 1}`} {...props}>
12
75
  {items.map((item, i) => (
13
76
  // Menu items have no stable id; index is the only available key.
14
77
  // oxlint-disable-next-line react/no-array-index-key
15
- <li key={i} className={item.items?.length ? "mx-nav__has-subnav" : undefined}>
16
- {item.link.href ? (
17
- <Link to={item.link.href} aria-current={item.link.current ? "page" : undefined}>
18
- {item.link.title}
19
- </Link>
20
- ) : (
21
- item.link.title
22
- )}
23
- {item.items && item.items.length > 0 && (
24
- <>
25
- <button
26
- className="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only"
27
- type="button"
28
- >
29
- Sub-navigation
30
- </button>
31
- <SideNavigationTree items={item.items} level={level + 1} />
32
- </>
33
- )}
34
- </li>
78
+ <SideNavigationItem key={i} item={item} level={level} />
35
79
  ))}
36
80
  </ul>
37
81
  )
@@ -2,18 +2,26 @@
2
2
 
3
3
  A vertical, multi-level navigation rendered as a `<nav>` landmark with a parent page link at the top and a nested list of sibling/child pages below. Designed to live in the aside of content layouts to expose a section's local page tree. Extends the base `.mx-nav` styles from the Navigation component.
4
4
 
5
+ By default every level is rendered open. Set `collapsible` to wrap the nav in `<mx-nav openActive>`, which turns each sub-list into a click-to-expand disclosure and opens the branch containing the current page.
6
+
5
7
  ## CSS Import
6
8
 
7
9
  ```css
8
10
  @import "@pnx-mixtape/mxds/src/Component/SideNavigation/side-navigation.css";
9
11
  ```
10
12
 
11
- Depends on the Navigation component's base styles (for `.mx-nav` and sub-list level classes):
13
+ Depends on the Navigation component's base styles (for `.mx-nav`, sub-list level classes and, when collapsible, the `.mx-nav--collapsible` toggle/transition styles):
12
14
 
13
15
  ```css
14
16
  @import "@pnx-mixtape/mxds/src/Component/Navigation/navigation.css";
15
17
  ```
16
18
 
19
+ ## JS Import (collapsible only)
20
+
21
+ ```js
22
+ import "@pnx-mixtape/mxds/src/Component/Navigation/Elements/Navigation"
23
+ ```
24
+
17
25
  ---
18
26
 
19
27
  ## Twig
@@ -24,6 +32,7 @@ Depends on the Navigation component's base styles (for `.mx-nav` and sub-list le
24
32
  title: 'Section navigation',
25
33
  parent: parent_link,
26
34
  items: menu_tree,
35
+ collapsible: true,
27
36
  } %}
28
37
  ```
29
38
 
@@ -31,13 +40,14 @@ Depends on the Navigation component's base styles (for `.mx-nav` and sub-list le
31
40
 
32
41
  **side-navigation.twig props** (`SideNavigation` type from `ids-shape`):
33
42
 
34
- | Prop | Type | Description |
35
- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
36
- | `id` | `string` | Written to `.mx-side-nav__parent` and used as the `<nav>`'s `aria-labelledby` fallback when `title` is empty |
37
- | `title` | `string` | `aria-label` for the `<nav>` landmark. Provide a meaningful value so assistive tech can distinguish this nav from the site's primary nav |
38
- | `parent` | `Link` | Pre-rendered Link atom representing the current section's parent page, shown above the list |
39
- | `items` | `MenuTree[]` | Recursive menu tree — see shape below |
40
- | `attributes` | `Attribute` | Optional Drupal-style attributes object; the template merges `mx-nav mx-side-nav` onto its classes |
43
+ | Prop | Type | Description |
44
+ | ------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
+ | `id` | `string` | Written to `.mx-side-nav__parent` and used as the `<nav>`'s `aria-labelledby` fallback when `title` is empty |
46
+ | `title` | `string` | `aria-label` for the `<nav>` landmark. Provide a meaningful value so assistive tech can distinguish this nav from the site's primary nav |
47
+ | `parent` | `Link` | Pre-rendered Link atom representing the current section's parent page, shown above the list |
48
+ | `items` | `MenuTree[]` | Recursive menu tree — see shape below |
49
+ | `collapsible` | `boolean` | Optional. Wraps the `<nav>` in `<mx-nav openActive>` and adds `mx-nav--collapsible` so sub-lists collapse behind their toggle buttons. Not part of the `ids-shape` `SideNavigation` type |
50
+ | `attributes` | `Attribute` | Optional Drupal-style attributes object; the template merges `mx-nav mx-side-nav` onto its classes |
41
51
 
42
52
  **`MenuTree` shape** (from `@pnx-mixtape/ids-shape`):
43
53
 
@@ -98,13 +108,43 @@ Rules for the markup:
98
108
  - The top list is `mx-nav__level-1`, children are `mx-nav__level-2`, grandchildren are `mx-nav__level-3`. Each level adds more inline-start indent.
99
109
  - Any `<li>` that contains a nested `<ul>` must carry `mx-nav__has-subnav`.
100
110
  - The current page's `<a>` carries `aria-current="page"` — CSS uses this to bold the label and apply the active background.
101
- - Sub-lists are **not** collapsed or inert — unlike the dropdown/collapsible navigation, every level is rendered open.
111
+ - Sub-lists are **not** collapsed or inert by default — every level is rendered open.
112
+
113
+ ### Collapsible
114
+
115
+ ```html
116
+ <mx-nav openActive>
117
+ <nav class="mx-nav mx-side-nav mx-nav--collapsible" aria-label="Section navigation">
118
+ <div class="mx-side-nav__parent" id="section-nav">
119
+ <a href="/about">About</a>
120
+ </div>
121
+ <ul class="mx-nav__level-1">
122
+ <li class="mx-nav__has-subnav">
123
+ <a href="/about/what-we-do">What we do</a>
124
+ <button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only" type="button">
125
+ Sub-navigation
126
+ </button>
127
+ <ul class="mx-nav__level-2">
128
+ <li>
129
+ <a href="/about/what-we-do/events" aria-current="page">Events</a>
130
+ </li>
131
+ </ul>
132
+ </li>
133
+ </ul>
134
+ </nav>
135
+ </mx-nav>
136
+ ```
137
+
138
+ - Sub-lists are rendered open in the markup (no `inert`), so the full tree is available without JS.
139
+ - On connect, `<mx-nav>` wires each `.mx-nav__toggle` to its sibling `<ul>` (`aria-controls`, `aria-expanded`, `inert`), labels each sub-list by its parent link, then (because of `openActive`) re-opens any sub-list whose parent link or descendants carry `aria-current="page"` or `.is-active`.
140
+ - Sub-lists not on the current page's trail start collapsed.
141
+ - Toggles are hidden until `<mx-nav>` is defined (`mx-nav:not(:defined) .mx-nav__toggle`).
102
142
 
103
143
  ---
104
144
 
105
145
  ## React
106
146
 
107
- Uses a composition pattern — always use `SideNavigationTree` inside `SideNavigation`.
147
+ Uses a composition pattern — always use `SideNavigationTree` inside `SideNavigation`. `SideNavigationTree` reads `collapsible` from `SideNavigation` via context, so it is only set once.
108
148
 
109
149
  ```tsx
110
150
  import { SideNavigation, SideNavigationTree, Link } from "@pnx-mixtape/mxds/react"
@@ -126,6 +166,7 @@ const items: MenuTree[] = [
126
166
  id="section-nav"
127
167
  title="Section navigation"
128
168
  parent={<Link to="/about">About</Link>}
169
+ collapsible
129
170
  >
130
171
  <SideNavigationTree items={items} />
131
172
  </SideNavigation>
@@ -133,18 +174,20 @@ const items: MenuTree[] = [
133
174
 
134
175
  **`SideNavigation` Props:**
135
176
 
136
- | Prop | Type | Required | Description |
137
- | ----------- | ----------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
138
- | `parent` | `ReactNode` | No | Link/element shown above the list inside `.mx-side-nav__parent` |
139
- | `title` | `string` | No | `aria-label` for the `<nav>` landmark. When omitted, falls back to `aria-labelledby` pointing at the parent wrapper |
140
- | `id` | `string` | No | `id` on `.mx-side-nav__parent`, also used as the `aria-labelledby` target when `title` is not set. Auto-generated if omitted |
141
- | `className` | `string` | No | Extra class on the `<nav>` |
177
+ | Prop | Type | Required | Description |
178
+ | ------------- | ----------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
179
+ | `parent` | `ReactNode` | No | Link/element shown above the list inside `.mx-side-nav__parent` |
180
+ | `title` | `string` | No | `aria-label` for the `<nav>` landmark. When omitted, falls back to `aria-labelledby` pointing at the parent wrapper |
181
+ | `id` | `string` | No | `id` on `.mx-side-nav__parent`, also used as the `aria-labelledby` target when `title` is not set. Auto-generated if omitted |
182
+ | `collapsible` | `boolean` | No | Adds `mx-nav--collapsible` and makes every nested `SideNavigationTree` sub-list a disclosure (`aria-expanded`/`aria-controls` on the toggle, `inert` when closed). Branches containing the current item start open. Default `false` |
183
+ | `className` | `string` | No | Extra class on the `<nav>` |
142
184
 
143
185
  **`SideNavigationTree` Props:**
144
186
 
145
- | Prop | Type | Required | Description |
146
- | ------- | ------------ | -------- | ----------------------------------------------------------------------------- |
147
- | `items` | `MenuTree[]` | Yes | Recursive menu tree — rendered as nested `<ul class="mx-nav__level-N">` lists |
187
+ | Prop | Type | Required | Description |
188
+ | ---------- | --------------- | -------- | ----------------------------------------------------------------------------- |
189
+ | `items` | `MenuTree[]` | Yes | Recursive menu tree — rendered as nested `<ul class="mx-nav__level-N">` lists |
190
+ | `...props` | `ul` attributes | No | Spread onto the `<ul>` |
148
191
 
149
192
  **`MenuTree` shape** (from `@pnx-mixtape/ids-shape`):
150
193
 
@@ -159,17 +202,18 @@ const items: MenuTree[] = [
159
202
 
160
203
  ## Modifiers
161
204
 
162
- SideNavigation uses the base `.mx-nav` class for list reset and the component-specific classes below. There are no variant modifiers — the single layout covers all cases.
205
+ SideNavigation uses the base `.mx-nav` class for list reset and the component-specific classes below. The only variant is `mx-nav--collapsible`.
163
206
 
164
- | Class | Applied by | Visual intent |
165
- | --------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
166
- | `mx-nav` | Always (shared with Navigation) | Base list reset — no bullets, vertical layout |
167
- | `mx-side-nav` | Always | Side-nav specific styling: per-item dividers, indent ramp across levels, current-page background |
168
- | `mx-side-nav__parent` | Always | Parent-page wrapper above the list — bold label, thicker bottom border |
169
- | `mx-nav__level-1` / `mx-nav__level-2` / `mx-nav__level-3` | Structural | Sub-list nesting level — each level increases `--indent` |
170
- | `mx-nav__has-subnav` | Structural | Applied to any `<li>` that contains a child `<ul>` |
171
- | `mx-nav__toggle` | Structural (inherited from Navigation) | Hidden in side-nav (`display: none`) — the template emits it for markup parity with dropdown nav, but every level is always expanded |
172
- | `is-active` | Authoring | Alternative to `aria-current="page"` for marking the current item — styled identically |
207
+ | Class | Applied by | Visual intent |
208
+ | --------------------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
209
+ | `mx-nav` | Always (shared with Navigation) | Base list reset — no bullets, vertical layout |
210
+ | `mx-side-nav` | Always | Side-nav specific styling: per-item dividers, indent ramp across levels, current-page background |
211
+ | `mx-side-nav__parent` | Always | Parent-page wrapper above the list — bold label, thicker bottom border |
212
+ | `mx-nav--collapsible` | Twig `collapsible: true` | Shows the sub-list toggles and applies the collapsible nav's `inert`/transition styles |
213
+ | `mx-nav__level-1` / `mx-nav__level-2` / `mx-nav__level-3` | Structural | Sub-list nesting level — each level increases `--indent` |
214
+ | `mx-nav__has-subnav` | Structural | Applied to any `<li>` that contains a child `<ul>` |
215
+ | `mx-nav__toggle` | Structural (inherited from Navigation) | Hidden (`display: none`) unless the nav is `mx-nav--collapsible`, where it opens and closes its sibling sub-list |
216
+ | `is-active` | Authoring | Alternative to `aria-current="page"` for marking the current item — styled identically |
173
217
 
174
218
  ---
175
219
 
@@ -180,8 +224,10 @@ SideNavigation uses the base `.mx-nav` class for list reset and the component-sp
180
224
  - Uses semantic nested `<ul>` / `<li>` lists, so assistive tech announces list depth and item count at each level.
181
225
  - The current page is marked with `aria-current="page"` on the `<a>`. Style-only bolding is a visual reinforcement — the ARIA attribute is what assistive tech announces. Avoid relying on the background/weight alone.
182
226
  - Active-trail ancestors (parent items on the path to the current page) are not given `aria-current` — only the exact current page. Apply the `is-active` class if you want to visually mark ancestors with the same active styling.
183
- - Keyboard navigation: every item is a native `<a>`, so tab order follows source order and no custom key handling is needed. Sub-lists are always visible, so there is no expand/collapse interaction to manage.
184
- - The `.mx-nav__toggle` button is emitted in the Twig markup for structural parity with the dropdown nav but is hidden via CSS — do not rely on it for any interactive behaviour in this component at this time (future iterations may enable it).
227
+ - Keyboard navigation: every item is a native `<a>`, so tab order follows source order. In the default variant sub-lists are always visible, so there is no expand/collapse interaction to manage.
228
+ - The `.mx-nav__toggle` button is hidden via CSS in the default variant.
229
+ - Collapsible (Twig and React): each toggle is a native `<button>` with `aria-expanded` and `aria-controls`; collapsed sub-lists are `inert`, so they drop out of the tab order and the accessibility tree. In Twig, `<mx-nav>` also adds the shared `Keyboard` arrow-key handling between links; React has no equivalent. `Escape` does not close sub-lists in either — that is only wired up for `flyout` navs.
230
+ - Collapsible: every toggle's accessible name is "Sub-navigation". Screen-reader users get the context from the preceding link, but the name alone doesn't say which section it opens.
185
231
 
186
232
  ---
187
233
 
@@ -205,7 +251,7 @@ SideNavigation uses the base `.mx-nav` class for list reset and the component-sp
205
251
  }
206
252
  ```
207
253
 
208
- Level 2 and level 3 sub-lists override `--indent` to step the text further in — at narrow widths with `--spacing-s` / `--spacing-m`, and at the `--global-nav-up` breakpoint with `--spacing-m` / `--spacing-l`.
254
+ Level 2 and level 3 sub-lists override `--indent` to step the text further in — at narrow widths with `--spacing-s` / `--spacing-m`, and at the `--sidebar-up` breakpoint with `--spacing-m` / `--spacing-l`.
209
255
 
210
256
  ---
211
257
 
@@ -1,11 +1,17 @@
1
1
  import { Meta, StoryObj } from "@storybook/html-vite"
2
+ import { expect } from "storybook/test"
2
3
  import Component from "./side-navigation.twig"
3
4
  import Link from "../../Atom/Link/link.twig"
4
5
  import "../Navigation/navigation.css"
5
6
  import "./side-navigation.css"
7
+ import "../Navigation/Elements/Navigation"
6
8
  import { SideNavigation as NavigationType } from "@pnx-mixtape/ids-shape"
7
9
 
8
- const meta: Meta<NavigationType> = {
10
+ type SideNavigationType = NavigationType & {
11
+ collapsible?: boolean
12
+ }
13
+
14
+ const meta: Meta<SideNavigationType> = {
9
15
  tags: ["autodocs", "ids-mvp"],
10
16
  component: Component,
11
17
  args: {
@@ -128,10 +134,43 @@ const meta: Meta<NavigationType> = {
128
134
  required: true,
129
135
  },
130
136
  },
137
+ collapsible: {
138
+ type: "boolean",
139
+ },
131
140
  },
132
141
  }
133
142
 
134
143
  export default meta
135
- type Story = StoryObj<NavigationType>
144
+ type Story = StoryObj<SideNavigationType>
136
145
 
137
146
  export const SideNavigation: Story = {}
147
+
148
+ export const Collapsible: Story = {
149
+ args: {
150
+ collapsible: true,
151
+ },
152
+ play: async ({ canvas, userEvent, step }) => {
153
+ const item = canvas.getByRole("link", { name: "What we do" }).closest("li")
154
+ const trigger = item.querySelector(":scope > .mx-nav__toggle")
155
+ const list = item.querySelector(":scope > ul")
156
+
157
+ await step("Opens the active trail", async () => {
158
+ await expect(trigger).toHaveAttribute("aria-expanded", "true")
159
+ await expect(trigger).toHaveAttribute("aria-controls", list.id)
160
+ await expect(list).not.toHaveAttribute("inert")
161
+ })
162
+
163
+ await step("Closes on click", async () => {
164
+ await userEvent.click(trigger)
165
+ await expect(trigger).toHaveAttribute("aria-expanded", "false")
166
+ await expect(list).toHaveAttribute("inert")
167
+ })
168
+
169
+ await step("Reopens on click", async () => {
170
+ await userEvent.click(trigger)
171
+ await expect(trigger).toHaveAttribute("aria-expanded", "true")
172
+ await expect(list).not.toHaveAttribute("inert")
173
+ ;(trigger as HTMLElement).blur()
174
+ })
175
+ },
176
+ }