@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
@@ -0,0 +1,18 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Translate 1`] = `
4
+ "
5
+ <mx-translate class="mx-translate" closeonclick="" data-default-language="en" data-update-trigger-label="true" data-notice-content="Our website uses an automatic service to translate content. These translations should be used as a guide only.">
6
+ <button id="unique-0" type="button" class="mx-button mx-button--light mx-button--small mx-translate__trigger mx-popover__trigger notranslate" popovertarget="translate" style="anchor-name: --translate" data-label="Language" aria-label="Language: English, EN">
7
+
8
+ <span class="mx-icon mx-icon--globe"></span>
9
+ <span class="mx-translate__code">EN</span>
10
+ </button>
11
+ <div class="mx-popover mx-translate__menu notranslate" id="translate" aria-describedby="unique-0" aria-labelledby="unique-0" style="position-anchor: --translate" data-placement="bottom-start" popover="" role="menu">
12
+ <a class="mx-translate__item" href="?hl=en" lang="en" hreflang="en" role="menuitem" data-language="en" aria-current="true">English</a>
13
+ <a class="mx-translate__item" href="?hl=fr" lang="fr" hreflang="fr" role="menuitem" data-language="fr">Français</a>
14
+ <a class="mx-translate__item" href="?hl=es" lang="es" hreflang="es" role="menuitem" data-language="es">Español</a>
15
+ </div>
16
+ </mx-translate>
17
+ "
18
+ `;
@@ -0,0 +1,3 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Translate 1`] = `"<div class="mx-translate"><button type="button" id="translate-target" class="mx-translate__trigger mx-popover__trigger notranslate" popovertarget="translate" data-label="Language" aria-label="Language: English, EN" style="anchor-name: --translate;"><span class="mx-icon mx-icon--globe"></span><span class="mx-translate__code">EN</span></button><div id="translate" class="mx-popover mx-translate__menu notranslate" aria-describedby="translate-target" aria-labelledby="translate-target" data-placement="bottom-start" popover="" role="menu" style="position-anchor: --translate;"><span data-focus-scope-start="true" hidden=""></span><a class="mx-translate__item" href="?hl=en" lang="en" hreflang="en" role="menuitem" data-language="en" aria-current="true">English</a><a class="mx-translate__item" href="?hl=fr" lang="fr" hreflang="fr" role="menuitem" data-language="fr">Français</a><a class="mx-translate__item" href="?hl=es" lang="es" hreflang="es" role="menuitem" data-language="es">Español</a><span data-focus-scope-end="true" hidden=""></span></div></div>"`;
@@ -0,0 +1,39 @@
1
+ export type TranslateLanguage = {
2
+ code: string
3
+ label: string
4
+ }
5
+
6
+ /**
7
+ * A representative set of languages Google's Website Translator supports, in each language's
8
+ * own name. Not bundled into `Translate` by default — import and pass this explicitly (or your
9
+ * own list) via the `languages` prop.
10
+ */
11
+ export const DEFAULT_LANGUAGES: TranslateLanguage[] = [
12
+ { code: "en", label: "English" },
13
+ { code: "ar", label: "العربية" },
14
+ { code: "zh-CN", label: "中文 (简体)" },
15
+ { code: "zh-TW", label: "中文 (繁體)" },
16
+ { code: "nl", label: "Nederlands" },
17
+ { code: "fr", label: "Français" },
18
+ { code: "de", label: "Deutsch" },
19
+ { code: "el", label: "Ελληνικά" },
20
+ { code: "hi", label: "हिन्दी" },
21
+ { code: "id", label: "Bahasa Indonesia" },
22
+ { code: "it", label: "Italiano" },
23
+ { code: "ja", label: "日本語" },
24
+ { code: "ko", label: "한국어" },
25
+ { code: "ms", label: "Bahasa Melayu" },
26
+ { code: "fa", label: "فارسی" },
27
+ { code: "pl", label: "Polski" },
28
+ { code: "pt", label: "Português" },
29
+ { code: "ru", label: "Русский" },
30
+ { code: "es", label: "Español" },
31
+ { code: "sw", label: "Kiswahili" },
32
+ { code: "sv", label: "Svenska" },
33
+ { code: "tl", label: "Tagalog" },
34
+ { code: "th", label: "ไทย" },
35
+ { code: "tr", label: "Türkçe" },
36
+ { code: "uk", label: "Українська" },
37
+ { code: "ur", label: "اردو" },
38
+ { code: "vi", label: "Tiếng Việt" },
39
+ ]
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Translate
3
+ *
4
+ * Composes DropMenu/Popover — import their CSS alongside this file.
5
+ */
6
+
7
+ @layer design-system.defaults {
8
+ mx-translate {
9
+ display: inline-block;
10
+ }
11
+ }
12
+
13
+ @layer design-system.components {
14
+ .mx-translate {
15
+ position: relative;
16
+ }
17
+
18
+ .mx-translate__trigger {
19
+ display: inline-flex;
20
+ align-items: center;
21
+ gap: var(--spacing-xxs);
22
+ }
23
+
24
+ .mx-translate__code {
25
+ text-transform: uppercase;
26
+ font-size: var(--font-size-xs);
27
+ }
28
+
29
+ /* @public --translate-menu-max-block-size, --translate-popover-padding */
30
+ .mx-translate__menu {
31
+ --popover-padding: var(--translate-popover-padding, 0px);
32
+
33
+ max-block-size: var(--translate-menu-max-block-size, 60vh);
34
+ overflow-y: auto;
35
+ }
36
+
37
+ .mx-translate__item {
38
+ display: block;
39
+ padding-block: var(--vertical-padding, var(--spacing-xxs));
40
+ padding-inline: var(--horizontal-padding, var(--spacing-m));
41
+ text-decoration: none;
42
+
43
+ &[aria-current="true"] {
44
+ font-weight: var(--font-weight-bold);
45
+ text-decoration: underline;
46
+ }
47
+ }
48
+ }
49
+
50
+ /* Stop Google giving us top padding */
51
+ body {
52
+ inset-block-start: 0 !important;
53
+ min-block-size: 0 !important;
54
+ }
55
+
56
+ /* Hide Google elements */
57
+ /* stylelint-disable selector-id-pattern */
58
+ .skiptranslate,
59
+ #googleTranslateElement,
60
+ #goog-gt-tt {
61
+ display: none !important;
62
+ }
@@ -0,0 +1 @@
1
+ import "./Elements/Translate"
@@ -0,0 +1,44 @@
1
+ {% set attributes = (attributes ?? create_attribute()).addClass('mx-translate') %}
2
+ {% set id = id|default('translate') %}
3
+ {% set label = label|default('Language') %}
4
+ {% set defaultLanguage = defaultLanguage|default('en') %}
5
+ {% set updateTriggerLabel = updateTriggerLabel|default(true) %}
6
+
7
+ <mx-translate{{ attributes }} closeonclick
8
+ data-default-language="{{ defaultLanguage }}"
9
+ {%- if blockedPaths %} data-blocked-paths="{{ blockedPaths|join(',') }}"{% endif -%}
10
+ {%- if scriptUrl %} data-script-url="{{ scriptUrl }}"{% endif -%}
11
+ {%- if storageKey %} data-storage-key="{{ storageKey }}"{% endif -%}
12
+ data-update-trigger-label="{{ updateTriggerLabel ? 'true' : 'false' }}"
13
+ {%- if noticeContent %} data-notice-content="{{ noticeContent }}"{% endif -%}
14
+ {%- if noticeBlockedContent %} data-notice-blocked-content="{{ noticeBlockedContent }}"{% endif -%}
15
+ {%- if noticeAnchor %} data-notice-anchor="{{ noticeAnchor }}"{% endif -%}
16
+ {%- if noticePosition %} data-notice-position="{{ noticePosition }}"{% endif -%}
17
+ >
18
+ <button
19
+ id="{{ id }}-target"
20
+ type="button"
21
+ class="mx-button mx-button--light mx-button--small mx-translate__trigger mx-popover__trigger notranslate"
22
+ popovertarget="{{ id }}"
23
+ style="anchor-name: --{{ id }}"
24
+ data-label="{{ label }}"
25
+ aria-label="{{ label }}, {{ defaultLanguage|upper }}"
26
+ >
27
+ {% include '@mixtape/Atom/Icon/icon.twig' with { icon: 'globe' } only %}
28
+ <span class="mx-translate__code">{{ defaultLanguage|upper }}</span>
29
+ </button>
30
+ <div
31
+ class="mx-popover mx-translate__menu notranslate"
32
+ id="{{ id }}"
33
+ aria-describedby="{{ id }}-target"
34
+ aria-labelledby="{{ id }}-target"
35
+ style="position-anchor: --{{ id }}"
36
+ data-placement="{{ placement|default('bottom-start') }}"
37
+ popover
38
+ role="menu"
39
+ >
40
+ {% for language in languages %}
41
+ <a class="mx-translate__item" href="?hl={{ language.code }}" lang="{{ language.code }}" hreflang="{{ language.code }}" role="menuitem" data-language="{{ language.code }}">{{ language.label }}</a>
42
+ {% endfor %}
43
+ </div>
44
+ </mx-translate>
@@ -204,7 +204,7 @@ export const Pending: StoryObj<typeof meta> = {
204
204
  * `docs/Testing.md`'s Automated Screen Reader Testing section.
205
205
  */
206
206
  export const ScreenReaderAnnouncements: StoryObj<typeof meta> = {
207
- play: async ({ canvas, userEvent }) => {
207
+ play: async ({ canvas, canvasElement, userEvent }) => {
208
208
  const input = canvas.getByRole("combobox", { name: "Choose a fruit" })
209
209
 
210
210
  // "lu" matches exactly two fruits: Blueberry, then Plum.
@@ -229,5 +229,6 @@ export const ScreenReaderAnnouncements: StoryObj<typeof meta> = {
229
229
 
230
230
  await userEvent.clear(input)
231
231
  await expect(input).toHaveAttribute("aria-expanded", "false")
232
+ await userEvent.click(canvasElement)
232
233
  },
233
234
  }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * FeedbackForm
3
+ * @file Reveals a comment field once a rating is chosen, and dispatches a
4
+ * cancelable `mx-feedback-form-submit` event on submit so any backend can be
5
+ * wired up by the consumer — this component never performs a fetch/AJAX call
6
+ * itself. If the consuming app calls `preventDefault()` on that event, the
7
+ * native form submission is also cancelled, handing full control to the
8
+ * consumer; otherwise the browser submits normally via the form's
9
+ * `action`/`method`.
10
+ */
11
+
12
+ import { validators } from "../../../Utility/utilities"
13
+
14
+ export type FeedbackFormSubmitEvent = CustomEvent<{
15
+ rating: string | null
16
+ comment: string
17
+ form: HTMLFormElement
18
+ }>
19
+
20
+ export default class FeedbackForm extends HTMLElement {
21
+ internals_: ElementInternals
22
+ controller: AbortController
23
+ revealed: boolean
24
+
25
+ constructor() {
26
+ super()
27
+ this.internals_ = this.attachInternals()
28
+ this.controller = new AbortController()
29
+ this.revealed = false
30
+ }
31
+
32
+ static get observedAttributes(): string[] {
33
+ return ["pending"]
34
+ }
35
+
36
+ connectedCallback(): void {
37
+ const { signal } = this.controller
38
+
39
+ // Only gate the reveal behind a checked rating once JS is confirmed
40
+ // running — see feedback-form.css.
41
+ this.classList.add("is-enhanced")
42
+
43
+ // Validation is driven by this component's own status message instead of
44
+ // the browser's native validation bubble.
45
+ this.form?.setAttribute("novalidate", "")
46
+
47
+ this.querySelectorAll('input[type="radio"]').forEach(radio => {
48
+ radio.addEventListener("change", this.handleRatingChange, { signal })
49
+ })
50
+
51
+ this.form?.addEventListener("submit", this.handleSubmit, { signal })
52
+ }
53
+
54
+ disconnectedCallback(): void {
55
+ this.controller.abort()
56
+ }
57
+
58
+ attributeChangedCallback(name: string): void {
59
+ if (name !== "pending") return
60
+ const isPending = this.hasAttribute("pending")
61
+ this.internals_.states[isPending ? "add" : "delete"]("pending")
62
+ this.submitButton?.toggleAttribute("disabled", isPending)
63
+ this.submitButton?.setAttribute("aria-busy", String(isPending))
64
+ }
65
+
66
+ handleRatingChange = (): void => {
67
+ if (this.revealed) return
68
+ this.revealed = true
69
+ this.comment?.focus()
70
+ }
71
+
72
+ handleSubmit = (event: SubmitEvent): void => {
73
+ const comment = this.comment
74
+
75
+ if (comment?.required && !validators.notEmpty(comment.value)) {
76
+ event.preventDefault()
77
+ comment.setAttribute("aria-invalid", "true")
78
+ this.status?.removeAttribute("hidden")
79
+ comment.focus()
80
+ return
81
+ }
82
+
83
+ comment?.removeAttribute("aria-invalid")
84
+ this.status?.setAttribute("hidden", "")
85
+
86
+ const rating =
87
+ this.querySelector<HTMLInputElement>('input[type="radio"]:checked')?.value ?? null
88
+ const submitEvent: FeedbackFormSubmitEvent = new CustomEvent("mx-feedback-form-submit", {
89
+ bubbles: true,
90
+ cancelable: true,
91
+ detail: { rating, comment: comment?.value ?? "", form: this.form as HTMLFormElement },
92
+ })
93
+ const shouldProceed = this.dispatchEvent(submitEvent)
94
+ if (!shouldProceed) event.preventDefault()
95
+ }
96
+
97
+ get form(): HTMLFormElement | null {
98
+ return this.querySelector("form")
99
+ }
100
+
101
+ get comment(): HTMLTextAreaElement | null {
102
+ return this.querySelector(".mx-feedback-form__reveal textarea")
103
+ }
104
+
105
+ get status(): HTMLElement | null {
106
+ return this.querySelector('.mx-feedback-form__reveal [role="alert"]')
107
+ }
108
+
109
+ get submitButton(): HTMLButtonElement | null {
110
+ return this.querySelector(".mx-form-actions button")
111
+ }
112
+ }
113
+
114
+ if (!customElements.get("mx-feedback-form")) {
115
+ customElements.define("mx-feedback-form", FeedbackForm)
116
+ }
117
+
118
+ declare global {
119
+ interface HTMLElementTagNameMap {
120
+ "mx-feedback-form": FeedbackForm
121
+ }
122
+ }
@@ -0,0 +1,99 @@
1
+ import { Meta, StoryObj } from "@storybook/html-vite"
2
+ import { expect, waitFor } from "storybook/test"
3
+ import Component from "./feedback-form.twig"
4
+ import "./feedback-form.css"
5
+ import "./Elements/FeedbackForm"
6
+
7
+ type FeedbackFormArgs = {
8
+ id?: string
9
+ name?: string
10
+ question?: string
11
+ yesLabel?: string
12
+ noLabel?: string
13
+ secondaryText?: string
14
+ secondaryLinkText?: string
15
+ secondaryLinkHref?: string
16
+ commentLabel?: string
17
+ commentName?: string
18
+ commentPlaceholder?: string
19
+ commentRequired?: boolean
20
+ requiredErrorMessage?: string
21
+ submitLabel?: string
22
+ action?: string
23
+ method?: string
24
+ errorMessage?: string
25
+ }
26
+
27
+ /**
28
+ * A yes/no "was this page useful?" control that reveals a free-text comment field once a
29
+ * rating is chosen. It requires the `FeedbackForm` custom element javascript.
30
+ *
31
+ * This component never submits anywhere itself — it dispatches a cancelable
32
+ * `mx-feedback-form-submit` event with the chosen rating/comment before the form submits, so any
33
+ * backend can be wired up by the consumer.
34
+ */
35
+ const meta: Meta<FeedbackFormArgs> = {
36
+ tags: ["autodocs"],
37
+ component: Component,
38
+ args: {
39
+ id: "feedback",
40
+ secondaryText: "Your rating will help us improve the website.",
41
+ },
42
+ }
43
+
44
+ export default meta
45
+ type Story = StoryObj<FeedbackFormArgs>
46
+
47
+ export const FeedbackForm: Story = {
48
+ play: async ({ canvasElement, canvas, userEvent, step }) => {
49
+ const wrapper = canvasElement.querySelector("mx-feedback-form") as HTMLElement
50
+ const comment = canvas.getByLabelText("How can we make it better?") as HTMLTextAreaElement
51
+
52
+ await step("Comment field is hidden until a rating is chosen", async () => {
53
+ await expect(comment).not.toBeVisible()
54
+ })
55
+
56
+ await step("Choosing a rating reveals and focuses the comment field", async () => {
57
+ await userEvent.click(canvas.getByRole("radio", { name: "Yes" }))
58
+ await waitFor(() => expect(comment).toBeVisible())
59
+ await expect(comment).toHaveFocus()
60
+ })
61
+
62
+ await step(
63
+ "Submitting dispatches mx-feedback-form-submit with the rating and comment",
64
+ async () => {
65
+ let detail: { rating: string | null; comment: string } | undefined
66
+ wrapper.addEventListener("mx-feedback-form-submit", (event: Event) => {
67
+ event.preventDefault()
68
+ detail = (event as CustomEvent<typeof detail>).detail
69
+ })
70
+ await userEvent.type(comment, "More examples would help.")
71
+ await userEvent.click(canvas.getByRole("button", { name: "Submit" }))
72
+ await waitFor(() =>
73
+ expect(detail).toEqual(
74
+ expect.objectContaining({ rating: "yes", comment: "More examples would help." }),
75
+ ),
76
+ )
77
+ },
78
+ )
79
+ },
80
+ }
81
+
82
+ /** With `commentRequired`, an empty comment blocks submission and shows an inline error. */
83
+ export const RequiredComment: Story = {
84
+ args: {
85
+ id: "feedback-required",
86
+ commentRequired: true,
87
+ },
88
+ play: async ({ canvas, userEvent, step }) => {
89
+ await step("Reveal the comment field", async () => {
90
+ await userEvent.click(canvas.getByRole("radio", { name: "No" }))
91
+ await waitFor(() => expect(canvas.getByLabelText("How can we make it better?")).toBeVisible())
92
+ })
93
+
94
+ await step("Submitting an empty required comment shows the error", async () => {
95
+ await userEvent.click(canvas.getByRole("button", { name: "Submit" }))
96
+ await waitFor(() => expect(canvas.getByRole("alert")).toBeVisible())
97
+ })
98
+ },
99
+ }
@@ -0,0 +1,73 @@
1
+ import { Meta, StoryObj } from "@storybook/react-vite"
2
+ import { expect, fn, waitFor } from "storybook/test"
3
+ import Component from "./FeedbackForm"
4
+ import "./feedback-form.css"
5
+ import { Page } from "../../../.storybook-react/decorators"
6
+
7
+ const meta: Meta<typeof Component> = {
8
+ tags: ["autodocs"],
9
+ component: Component,
10
+ decorators: [Page],
11
+ args: {
12
+ id: "feedback",
13
+ secondaryText: "If you need a response, ",
14
+ secondaryLinkText: "contact us",
15
+ secondaryLinkHref: "#contact",
16
+ onSubmit: fn(),
17
+ },
18
+ }
19
+
20
+ export default meta
21
+
22
+ export const FeedbackForm: StoryObj<typeof meta> = {
23
+ play: async ({ canvasElement, canvas, userEvent, step, args }) => {
24
+ const wrapper = canvasElement.querySelector(".mx-feedback-form") as HTMLElement
25
+ const comment = canvas.getByLabelText("How can we make it better?") as HTMLTextAreaElement
26
+
27
+ await step("Comment field is hidden until a rating is chosen", async () => {
28
+ await expect(comment).not.toBeVisible()
29
+ })
30
+
31
+ await step("Choosing a rating reveals and focuses the comment field", async () => {
32
+ await userEvent.click(canvas.getByRole("radio", { name: "Yes" }))
33
+ await waitFor(() => expect(comment).toBeVisible())
34
+ await expect(comment).toHaveFocus()
35
+ })
36
+
37
+ await step("Submitting calls onSubmit with the chosen rating and comment", async () => {
38
+ await userEvent.type(comment, "More examples would help.")
39
+ await userEvent.click(canvas.getByRole("button", { name: "Submit" }))
40
+ await waitFor(() =>
41
+ expect(args.onSubmit).toHaveBeenCalledWith(
42
+ expect.objectContaining({ rating: "yes", comment: "More examples would help." }),
43
+ ),
44
+ )
45
+ })
46
+
47
+ await expect(wrapper).toBeInTheDocument()
48
+ },
49
+ }
50
+
51
+ /** With `commentRequired`, an empty comment blocks submission and shows an inline error. */
52
+ export const RequiredComment: StoryObj<typeof meta> = {
53
+ args: {
54
+ id: "feedback-required",
55
+ commentRequired: true,
56
+ onSubmit: fn(),
57
+ },
58
+ play: async ({ canvas, userEvent, step, args }) => {
59
+ await step("Reveal the comment field", async () => {
60
+ await userEvent.click(canvas.getByRole("radio", { name: "No" }))
61
+ await waitFor(() => expect(canvas.getByLabelText("How can we make it better?")).toBeVisible())
62
+ })
63
+
64
+ await step(
65
+ "Submitting an empty required comment shows the error and blocks onSubmit",
66
+ async () => {
67
+ await userEvent.click(canvas.getByRole("button", { name: "Submit" }))
68
+ await waitFor(() => expect(canvas.getByRole("alert")).toBeVisible())
69
+ await expect(args.onSubmit).not.toHaveBeenCalled()
70
+ },
71
+ )
72
+ },
73
+ }
@@ -0,0 +1,196 @@
1
+ import { JSX, FormEventHandler, ChangeEventHandler, useId, useRef, useState } from "react"
2
+ import classNames from "classnames"
3
+ import { ButtonModifiers, IconRotateModifier } from "@pnx-mixtape/ids-shape"
4
+ import Button from "../../Atom/Button/Button"
5
+ import Icon from "../../Atom/Icon/Icon"
6
+ import Link from "../../Atom/Link/Link"
7
+ import FormItem from "../FormItem/FormItem"
8
+ import FormLabel from "../Label/FormLabel"
9
+ import { Icons } from "../../enums"
10
+ import { validators } from "../../Utility/utilities"
11
+
12
+ const SUBMIT_MODIFIERS = [ButtonModifiers.DARK]
13
+
14
+ export type FeedbackFormSubmitDetail = {
15
+ rating: string | null
16
+ comment: string
17
+ form: HTMLFormElement
18
+ }
19
+
20
+ type FeedbackFormProps = {
21
+ id?: string
22
+ name?: string
23
+ question?: string
24
+ yesLabel?: string
25
+ noLabel?: string
26
+ /** Optional secondary copy, e.g. pointing readers to an alternative contact channel. */
27
+ secondaryText?: string
28
+ secondaryLinkText?: string
29
+ secondaryLinkHref?: string
30
+ commentLabel?: string
31
+ commentName?: string
32
+ commentPlaceholder?: string
33
+ commentRequired?: boolean
34
+ requiredErrorMessage?: string
35
+ submitLabel?: string
36
+ /** Progressive-enhancement form target — the consumer's `onSubmit` can still take over. */
37
+ action?: string
38
+ method?: string
39
+ /** Consumer-supplied top-level notice, e.g. after a failed async submit. */
40
+ errorMessage?: string
41
+ /** Reflected as `aria-busy`/`disabled` on the submit button while the consumer's own submit handling is in flight. */
42
+ pending?: boolean
43
+ className?: string
44
+ /** Called with the form's values on a valid submit — this component never fetches/submits over the network itself. */
45
+ onSubmit?: (detail: FeedbackFormSubmitDetail) => void
46
+ }
47
+
48
+ const FeedbackForm = ({
49
+ id,
50
+ name = "rating",
51
+ question = "Was the information on this page useful?",
52
+ yesLabel = "Yes",
53
+ noLabel = "No",
54
+ secondaryText,
55
+ secondaryLinkText,
56
+ secondaryLinkHref,
57
+ commentLabel = "How can we make it better?",
58
+ commentName = "comment",
59
+ commentPlaceholder,
60
+ commentRequired = false,
61
+ requiredErrorMessage = "This field is required.",
62
+ submitLabel = "Submit",
63
+ action,
64
+ method = "post",
65
+ errorMessage,
66
+ pending = false,
67
+ className,
68
+ onSubmit,
69
+ }: FeedbackFormProps): JSX.Element => {
70
+ const generatedId = useId()
71
+ const rootId = id ?? generatedId
72
+ const commentId = `${rootId}-comment`
73
+ const commentRef = useRef<HTMLTextAreaElement>(null)
74
+ const revealedRef = useRef(false)
75
+ const [showError, setShowError] = useState(false)
76
+
77
+ const handleRatingChange: ChangeEventHandler<HTMLInputElement> = () => {
78
+ if (revealedRef.current) return
79
+ revealedRef.current = true
80
+ commentRef.current?.focus()
81
+ }
82
+
83
+ const handleSubmit: FormEventHandler<HTMLFormElement> = event => {
84
+ const comment = commentRef.current
85
+
86
+ if (commentRequired && comment && !validators.notEmpty(comment.value)) {
87
+ event.preventDefault()
88
+ setShowError(true)
89
+ comment.focus()
90
+ return
91
+ }
92
+
93
+ setShowError(false)
94
+ if (!onSubmit) return
95
+ // Hand full control to the consumer — this component never submits over
96
+ // the network itself, so the native POST must not also fire.
97
+ event.preventDefault()
98
+ const rating = new FormData(event.currentTarget).get(name) as string | null
99
+ onSubmit({ rating, comment: comment?.value ?? "", form: event.currentTarget })
100
+ }
101
+
102
+ return (
103
+ <div className={classNames("mx-feedback-form", "is-enhanced", className)}>
104
+ <form action={action} method={method} noValidate onSubmit={handleSubmit}>
105
+ {errorMessage && (
106
+ <div className="mx-feedback-form__error" role="alert">
107
+ {errorMessage}
108
+ </div>
109
+ )}
110
+
111
+ <fieldset className="mx-fieldset mx-fieldset--plain mx-feedback-form__rating">
112
+ <legend className="mx-legend">{question}</legend>
113
+ <div className="mx-feedback-form__option">
114
+ <input
115
+ type="radio"
116
+ className="sr-only"
117
+ id={`${rootId}-yes`}
118
+ name={name}
119
+ value="yes"
120
+ onChange={handleRatingChange}
121
+ />
122
+ <label
123
+ className="mx-button mx-button--dark mx-button--outline"
124
+ htmlFor={`${rootId}-yes`}
125
+ >
126
+ <Icon icon={Icons.THUMB} />
127
+ <span>{yesLabel}</span>
128
+ </label>
129
+ </div>
130
+ <div className="mx-feedback-form__option">
131
+ <input
132
+ type="radio"
133
+ className="sr-only"
134
+ id={`${rootId}-no`}
135
+ name={name}
136
+ value="no"
137
+ onChange={handleRatingChange}
138
+ />
139
+ <label
140
+ className="mx-button mx-button--dark mx-button--outline"
141
+ htmlFor={`${rootId}-no`}
142
+ >
143
+ <Icon icon={Icons.THUMB} rotate={IconRotateModifier.ONEEIGHTY} />
144
+ <span>{noLabel}</span>
145
+ </label>
146
+ </div>
147
+ </fieldset>
148
+
149
+ {(secondaryText || secondaryLinkText) && (
150
+ <p className="mx-feedback-form__secondary">
151
+ {secondaryText}
152
+ {secondaryLinkText && secondaryLinkHref && (
153
+ <Link to={secondaryLinkHref}>{secondaryLinkText}</Link>
154
+ )}
155
+ </p>
156
+ )}
157
+
158
+ <div className="mx-feedback-form__reveal">
159
+ <FormItem>
160
+ <FormLabel id={commentId} text={commentLabel} />
161
+ <textarea
162
+ ref={commentRef}
163
+ className="mx-input__textarea"
164
+ id={commentId}
165
+ name={commentName}
166
+ placeholder={commentPlaceholder}
167
+ required={commentRequired}
168
+ aria-describedby={`status-${commentId}`}
169
+ />
170
+ </FormItem>
171
+ <div
172
+ className="mx-error-message"
173
+ id={`status-${commentId}`}
174
+ role="alert"
175
+ hidden={!showError}
176
+ >
177
+ {requiredErrorMessage}
178
+ </div>
179
+ <div className="mx-form-actions">
180
+ <Button
181
+ type="submit"
182
+ modifiers={SUBMIT_MODIFIERS}
183
+ disabled={pending}
184
+ data-pending={pending}
185
+ aria-busy={pending}
186
+ >
187
+ {submitLabel}
188
+ </Button>
189
+ </div>
190
+ </div>
191
+ </form>
192
+ </div>
193
+ )
194
+ }
195
+
196
+ export default FeedbackForm