@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.
- package/.claude/skills/create-mxds-component/SKILL.md +77 -0
- package/DESIGN.md +595 -0
- package/README.md +2 -3
- package/dist/build/accordion.entry.js +2 -1
- package/dist/build/autocomplete.entry.js +1 -1
- package/dist/build/back-to-top.css +1 -0
- package/dist/build/back-to-top.entry.js +1 -0
- package/dist/build/base.css +1 -1
- package/dist/build/button.css +1 -1
- package/dist/build/card.css +1 -1
- package/dist/build/chunks/Accordion-Bqe8vTNR.js +44 -0
- package/dist/build/chunks/AccordionGroup-BYtoAFxg.js +47 -0
- package/dist/build/chunks/AccordionMobile-D517oXYZ.js +26 -0
- package/dist/build/chunks/Autocomplete-kRo8RNbC.js +135 -0
- package/dist/build/chunks/BackToTop-4M-Wl7z3.js +64 -0
- package/dist/build/chunks/ClosableAlert-CZ114JsL.js +55 -0
- package/dist/build/chunks/CookieConsent-wd8gQ2Oe.js +18 -0
- package/dist/build/chunks/Dialog-DoYcGqhW.js +48 -0
- package/dist/build/chunks/DropMenu-CSTZq0Zt.js +68 -0
- package/dist/build/chunks/FeedbackForm-BsL7fv8s.js +64 -0
- package/dist/build/chunks/Gallery-DqJPujX5.js +165 -0
- package/dist/build/chunks/GlobalToggle-7H3F1Wvc.js +82 -0
- package/dist/build/chunks/InPageNavigation-D7m3IN89.js +86 -0
- package/dist/build/chunks/Navigation-CRModT40.js +142 -0
- package/dist/build/chunks/Popover-CiTAftNU.js +30 -0
- package/dist/build/chunks/Sticky-C1H8MNqK.js +51 -0
- package/dist/build/chunks/Tabs-BJD3kABL.js +160 -0
- package/dist/build/chunks/Translate-yAgySwet.js +46 -0
- package/dist/build/chunks/cookie-compliance-CG6x-TcV.js +119 -0
- package/dist/build/chunks/enums-BrHxPZZ_.js +36 -0
- package/dist/build/chunks/polyfills-D-qh1a_2.js +33 -0
- package/dist/build/chunks/utilities-B9mNRs7V.js +255 -0
- package/dist/build/content-block.css +1 -1
- package/dist/build/cookie-consent.entry.js +1 -1
- package/dist/build/dialog.entry.js +1 -1
- package/dist/build/drop-menu.entry.js +1 -1
- package/dist/build/elements.js +147 -1
- package/dist/build/feedback-form.css +1 -0
- package/dist/build/feedback-form.entry.js +1 -0
- package/dist/build/filters.entry.js +145 -1
- package/dist/build/form.css +1 -1
- package/dist/build/gallery.entry.js +1 -1
- package/dist/build/global-alert.entry.js +1 -1
- package/dist/build/header.css +1 -1
- package/dist/build/header.entry.js +1 -1
- package/dist/build/icon.css +1 -1
- package/dist/build/in-page-navigation.entry.js +1 -1
- package/dist/build/navigation.css +1 -1
- package/dist/build/navigation.entry.js +1 -1
- package/dist/build/pagination.css +1 -1
- package/dist/build/popover.entry.js +1 -1
- package/dist/build/react.js +2291 -1
- package/dist/build/side-navigation.css +1 -1
- package/dist/build/sidebar.css +1 -1
- package/dist/build/sticky.entry.js +1 -1
- package/dist/build/tabs.entry.js +1 -1
- package/dist/build/translate.css +1 -0
- package/dist/build/translate.entry.js +1 -0
- package/dist/build/utility-list.entry.js +53 -1
- package/dist/types/Component/BackToTop/BackToTop.d.ts +20 -0
- package/dist/types/Component/BackToTop/BackToTop.d.ts.map +1 -0
- package/dist/types/Component/BackToTop/Elements/BackToTop.d.ts +27 -0
- package/dist/types/Component/BackToTop/Elements/BackToTop.d.ts.map +1 -0
- package/dist/types/Component/BackToTop/back-to-top.entry.d.ts +2 -0
- package/dist/types/Component/BackToTop/back-to-top.entry.d.ts.map +1 -0
- package/dist/types/Component/DropMenu/DropMenu.d.ts +2 -1
- package/dist/types/Component/DropMenu/DropMenu.d.ts.map +1 -1
- package/dist/types/Component/Navigation/Elements/Navigation.d.ts +2 -3
- package/dist/types/Component/Navigation/Elements/Navigation.d.ts.map +1 -1
- package/dist/types/Component/SideNavigation/Components/SideNavigationTree.d.ts +3 -3
- package/dist/types/Component/SideNavigation/Components/SideNavigationTree.d.ts.map +1 -1
- package/dist/types/Component/SideNavigation/SideNavigation.d.ts +3 -1
- package/dist/types/Component/SideNavigation/SideNavigation.d.ts.map +1 -1
- package/dist/types/Component/Tabs/Components/TabDropMenu.d.ts.map +1 -1
- package/dist/types/Component/Tabs/Elements/Tabs.d.ts.map +1 -1
- package/dist/types/Component/Translate/Elements/Translate.d.ts +23 -0
- package/dist/types/Component/Translate/Elements/Translate.d.ts.map +1 -0
- package/dist/types/Component/Translate/Translate.d.ts +37 -0
- package/dist/types/Component/Translate/Translate.d.ts.map +1 -0
- package/dist/types/Component/Translate/languages.d.ts +11 -0
- package/dist/types/Component/Translate/languages.d.ts.map +1 -0
- package/dist/types/Component/Translate/translate.entry.d.ts +2 -0
- package/dist/types/Component/Translate/translate.entry.d.ts.map +1 -0
- package/dist/types/Form/FeedbackForm/Elements/FeedbackForm.d.ts +37 -0
- package/dist/types/Form/FeedbackForm/Elements/FeedbackForm.d.ts.map +1 -0
- package/dist/types/Form/FeedbackForm/FeedbackForm.d.ts +36 -0
- package/dist/types/Form/FeedbackForm/FeedbackForm.d.ts.map +1 -0
- package/dist/types/Form/FeedbackForm/feedback-form.entry.d.ts +2 -0
- package/dist/types/Form/FeedbackForm/feedback-form.entry.d.ts.map +1 -0
- package/dist/types/Utility/Elements/google-translate-widget.d.ts +91 -0
- package/dist/types/Utility/Elements/google-translate-widget.d.ts.map +1 -0
- package/dist/types/Utility/utilities.d.ts +2 -0
- package/dist/types/Utility/utilities.d.ts.map +1 -1
- package/dist/types/elements.d.ts +3 -0
- package/dist/types/elements.d.ts.map +1 -1
- package/dist/types/enums.d.ts +3 -1
- package/dist/types/enums.d.ts.map +1 -1
- package/dist/types/react.d.ts +3 -0
- package/dist/types/react.d.ts.map +1 -1
- package/dist/types/tokens.d.ts +1 -0
- package/dist/types/tokens.d.ts.map +1 -1
- package/docs/API.md +1215 -0
- package/docs/CSSGuidelines.md +893 -0
- package/docs/CodeQuality.md +940 -0
- package/docs/ComponentArchitecture.md +714 -0
- package/docs/Contributing.md +626 -0
- package/docs/MemoryManagement.md +598 -0
- package/docs/Migrations.md +6 -0
- package/docs/README.md +100 -0
- package/docs/Security.md +444 -0
- package/docs/Testing.md +739 -0
- package/docs/Troubleshooting.md +674 -0
- package/package.json +9 -6
- package/src/Atom/Button/Button.tsx +1 -1
- package/src/Atom/Button/__snapshots__/Button.stories.tsx.snap +2 -2
- package/src/Atom/Button/_buttons.css +1 -0
- package/src/Atom/Icon/README.md +1 -1
- package/src/Atom/Icon/_icon.css +1 -0
- package/src/Atom/Icon/images/thumb.svg +1 -0
- package/src/Atom/Link/_links.css +1 -0
- package/src/Atom/Media/__snapshots__/Media.stories.ts.snap +4 -9
- package/src/Atom/Media/media.twig +4 -5
- package/src/Component/BackToTop/BackToTop.stories.ts +127 -0
- package/src/Component/BackToTop/BackToTop.stories.tsx +53 -0
- package/src/Component/BackToTop/BackToTop.tsx +120 -0
- package/src/Component/BackToTop/Elements/BackToTop.ts +119 -0
- package/src/Component/BackToTop/README.md +164 -0
- package/src/Component/BackToTop/__snapshots__/BackToTop.stories.ts.snap +24 -0
- package/src/Component/BackToTop/__snapshots__/BackToTop.stories.tsx.snap +3 -0
- package/src/Component/BackToTop/back-to-top.css +70 -0
- package/src/Component/BackToTop/back-to-top.entry.js +1 -0
- package/src/Component/BackToTop/back-to-top.twig +26 -0
- package/src/Component/Card/Components/CardContent.tsx +1 -1
- package/src/Component/Card/Components/CardMedia.tsx +1 -1
- package/src/Component/Card/__snapshots__/Card.stories.tsx.snap +3 -3
- package/src/Component/Card/card.css +1 -1
- package/src/Component/Carousel/Carousel.stories.ts +66 -36
- package/src/Component/Carousel/__snapshots__/Carousel.stories.tsx.snap +5 -5
- package/src/Component/ContentBlock/Components/ContentBlockMedia.tsx +1 -1
- package/src/Component/ContentBlock/content-block.css +1 -1
- package/src/Component/DropMenu/DropMenu.stories.ts +21 -2
- package/src/Component/DropMenu/DropMenu.tsx +14 -4
- package/src/Component/DropMenu/__snapshots__/DropMenu.stories.ts.snap +3 -3
- package/src/Component/DropMenu/__snapshots__/DropMenu.stories.tsx.snap +2 -2
- package/src/Component/DropMenu/drop-menu.twig +7 -1
- package/src/Component/HeroBanner/__snapshots__/HeroBanner.stories.tsx.snap +1 -1
- package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +48 -13
- package/src/Component/InPageNavigation/in-page-navigation.twig +4 -6
- package/src/Component/ListItem/Components/ListItemMedia.tsx +1 -1
- package/src/Component/Navigation/Elements/Navigation.ts +25 -29
- package/src/Component/Navigation/README.md +11 -10
- package/src/Component/Navigation/__snapshots__/Navigation.stories.ts.snap +8 -8
- package/src/Component/Navigation/_navigation-dropdown.css +9 -11
- package/src/Component/Navigation/_navigation-mega.css +5 -11
- package/src/Component/Navigation/_navigation.css +5 -3
- package/src/Component/Navigation/menu.twig +1 -1
- package/src/Component/Navigation/navigation.twig +0 -1
- package/src/Component/Pagination/Pagination.stories.ts +57 -27
- package/src/Component/Pagination/pagination.css +16 -7
- package/src/Component/Popover/__snapshots__/Popover.stories.tsx.snap +3 -3
- package/src/Component/SideNavigation/Components/SideNavigationTree.tsx +68 -24
- package/src/Component/SideNavigation/README.md +78 -32
- package/src/Component/SideNavigation/SideNavigation.stories.ts +41 -2
- package/src/Component/SideNavigation/SideNavigation.stories.tsx +31 -0
- package/src/Component/SideNavigation/SideNavigation.tsx +13 -3
- package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.ts.snap +87 -3
- package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.tsx.snap +2 -0
- package/src/Component/SideNavigation/side-navigation.css +3 -3
- package/src/Component/SideNavigation/side-navigation.twig +10 -3
- package/src/Component/Tabs/Components/TabDropMenu.tsx +7 -1
- package/src/Component/Tabs/Elements/Tabs.ts +13 -3
- package/src/Component/Tabs/Tabs.stories.ts +33 -12
- package/src/Component/Tabs/Tabs.stories.tsx +28 -14
- package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +3 -3
- package/src/Component/Tabs/__snapshots__/Tabs.stories.tsx.snap +2 -2
- package/src/Component/Translate/Elements/Translate.ts +76 -0
- package/src/Component/Translate/README.md +285 -0
- package/src/Component/Translate/Translate.stories.ts +87 -0
- package/src/Component/Translate/Translate.stories.tsx +77 -0
- package/src/Component/Translate/Translate.tsx +213 -0
- package/src/Component/Translate/__snapshots__/Translate.stories.ts.snap +18 -0
- package/src/Component/Translate/__snapshots__/Translate.stories.tsx.snap +3 -0
- package/src/Component/Translate/languages.ts +39 -0
- package/src/Component/Translate/translate.css +62 -0
- package/src/Component/Translate/translate.entry.js +1 -0
- package/src/Component/Translate/translate.twig +44 -0
- package/src/Form/Autocomplete/Autocomplete.stories.tsx +2 -1
- package/src/Form/FeedbackForm/Elements/FeedbackForm.ts +122 -0
- package/src/Form/FeedbackForm/FeedbackForm.stories.ts +99 -0
- package/src/Form/FeedbackForm/FeedbackForm.stories.tsx +73 -0
- package/src/Form/FeedbackForm/FeedbackForm.tsx +196 -0
- package/src/Form/FeedbackForm/README.md +233 -0
- package/src/Form/FeedbackForm/__snapshots__/FeedbackForm.stories.ts.snap +125 -0
- package/src/Form/FeedbackForm/__snapshots__/FeedbackForm.stories.tsx.snap +5 -0
- package/src/Form/FeedbackForm/feedback-form.css +74 -0
- package/src/Form/FeedbackForm/feedback-form.entry.js +1 -0
- package/src/Form/FeedbackForm/feedback-form.twig +80 -0
- package/src/Form/form.css +17 -19
- package/src/Introduction.mdx +1 -1
- package/src/Layout/Footer/__snapshots__/Footer.stories.ts.snap +3 -3
- package/src/Layout/Header/Components/GlobalToggle.tsx +1 -1
- package/src/Layout/Header/Header.stories.ts +53 -48
- package/src/Layout/Header/Header.stories.tsx +35 -22
- package/src/Layout/Header/Header.tsx +2 -2
- package/src/Layout/Header/__snapshots__/Header.stories.ts.snap +18 -14
- package/src/Layout/Header/__snapshots__/Header.stories.tsx.snap +2 -2
- package/src/Layout/Header/_toggles.css +7 -7
- package/src/Layout/Page/__snapshots__/Page.stories.tsx.snap +1 -1
- package/src/Layout/Sidebar/sidebar.css +10 -3
- package/src/Utility/Elements/google-translate-widget.ts +293 -0
- package/src/Utility/global.d.ts +4 -0
- package/src/Utility/utilities.ts +2 -0
- package/src/_custom-media.css +2 -0
- package/src/elements.ts +3 -0
- package/src/enums.ts +2 -0
- package/src/react.ts +3 -0
- package/src/tokens.js +1 -0
- package/dist/build/chunks/Accordion-GN0sME4W.js +0 -1
- package/dist/build/chunks/AccordionGroup-Bzqp5__q.js +0 -1
- package/dist/build/chunks/AccordionMobile-DG5jSe8Q.js +0 -1
- package/dist/build/chunks/Autocomplete-B4RtkAyh.js +0 -1
- package/dist/build/chunks/ClosableAlert-DEhj8Nxm.js +0 -1
- package/dist/build/chunks/CookieConsent-BkCMRVST.js +0 -1
- package/dist/build/chunks/Dialog-Haahn-Yn.js +0 -1
- package/dist/build/chunks/DropMenu-1baQc8Yc.js +0 -1
- package/dist/build/chunks/Gallery-D8NGXy2Q.js +0 -1
- package/dist/build/chunks/GlobalToggle-B52fdBQu.js +0 -1
- package/dist/build/chunks/InPageNavigation-Dyn2TR-N.js +0 -1
- package/dist/build/chunks/Navigation-hOrfLq7h.js +0 -1
- package/dist/build/chunks/Popover-Cxg-s5Jn.js +0 -1
- package/dist/build/chunks/Sticky-BGCuuEJK.js +0 -1
- package/dist/build/chunks/Tabs-DYy1YTdU.js +0 -15
- package/dist/build/chunks/cookie-compliance-Bbm-0vpj.js +0 -1
- package/dist/build/chunks/enums-DLV_1NAf.js +0 -1
- package/dist/build/chunks/polyfills-C7pRt54M.js +0 -1
- 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
|