@pnx-mixtape/mxds 0.1.0-rc.4 → 0.1.0-rc.5
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 +1 -2
- 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/button.css +1 -1
- package/dist/build/card.css +1 -1
- package/dist/build/chunks/Accordion-D2wp8hBG.js +44 -0
- package/dist/build/chunks/AccordionGroup-BYtoAFxg.js +47 -0
- package/dist/build/chunks/AccordionMobile-D3sTeiM5.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-yy84gae6.js +55 -0
- package/dist/build/chunks/CookieConsent-wd8gQ2Oe.js +18 -0
- package/dist/build/chunks/Dialog-DWoXW-7Z.js +48 -0
- package/dist/build/chunks/DropMenu-CSTZq0Zt.js +68 -0
- package/dist/build/chunks/FeedbackForm-BochXEDx.js +64 -0
- package/dist/build/chunks/Gallery-Rkfv7Mbb.js +165 -0
- package/dist/build/chunks/GlobalToggle-CzlkpLpc.js +82 -0
- package/dist/build/chunks/InPageNavigation-D1Kk-rCm.js +86 -0
- package/dist/build/chunks/Navigation-CDQnN61h.js +147 -0
- package/dist/build/chunks/Popover-CiTAftNU.js +30 -0
- package/dist/build/chunks/Sticky-DoVndENm.js +51 -0
- package/dist/build/chunks/Tabs-BDlxCDNh.js +160 -0
- package/dist/build/chunks/Translate-bmM38j6z.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-Dd-08SW6.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/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.entry.js +1 -1
- package/dist/build/pagination.css +1 -1
- package/dist/build/popover.entry.js +1 -1
- package/dist/build/react.js +2270 -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/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/docs/API.md +1215 -0
- package/docs/CSSGuidelines.md +891 -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 +6 -3
- 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/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/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/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 +283 -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/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 +14 -10
- 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/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/elements.ts +3 -0
- package/src/enums.ts +2 -0
- package/src/react.ts +3 -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,213 @@
|
|
|
1
|
+
import { JSX, KeyboardEvent, MouseEvent, useEffect, useId, useMemo, useRef } from "react"
|
|
2
|
+
import { FocusScope, useFocusManager } from "react-aria"
|
|
3
|
+
import classNames from "classnames"
|
|
4
|
+
import { PopoverPlacementModifier } from "@pnx-mixtape/ids-shape"
|
|
5
|
+
import Icon from "../../Atom/Icon/Icon"
|
|
6
|
+
import { Icons } from "../../enums"
|
|
7
|
+
import { GoogleTranslateWidget } from "../../Utility/utilities"
|
|
8
|
+
import { TranslateLanguage } from "./languages"
|
|
9
|
+
|
|
10
|
+
type TranslateItemsProps = {
|
|
11
|
+
languages: TranslateLanguage[]
|
|
12
|
+
onSelect: (code: string, event: MouseEvent<HTMLAnchorElement>) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TranslateItems = ({ languages, onSelect }: TranslateItemsProps): JSX.Element => {
|
|
16
|
+
const focusManager = useFocusManager()
|
|
17
|
+
|
|
18
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLAnchorElement>): void => {
|
|
19
|
+
switch (event.key) {
|
|
20
|
+
case "ArrowDown":
|
|
21
|
+
case "ArrowRight":
|
|
22
|
+
event.preventDefault()
|
|
23
|
+
focusManager?.focusNext({ wrap: true, tabbable: true })
|
|
24
|
+
break
|
|
25
|
+
case "ArrowUp":
|
|
26
|
+
case "ArrowLeft":
|
|
27
|
+
event.preventDefault()
|
|
28
|
+
focusManager?.focusPrevious({ wrap: true, tabbable: true })
|
|
29
|
+
break
|
|
30
|
+
default:
|
|
31
|
+
break
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
{languages.map(language => (
|
|
38
|
+
<a
|
|
39
|
+
key={language.code}
|
|
40
|
+
className="mx-translate__item"
|
|
41
|
+
href={`?hl=${language.code}`}
|
|
42
|
+
lang={language.code}
|
|
43
|
+
hrefLang={language.code}
|
|
44
|
+
role="menuitem"
|
|
45
|
+
data-language={language.code}
|
|
46
|
+
onClick={event => onSelect(language.code, event)}
|
|
47
|
+
onKeyDown={handleKeyDown}
|
|
48
|
+
>
|
|
49
|
+
{language.label}
|
|
50
|
+
</a>
|
|
51
|
+
))}
|
|
52
|
+
</>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type TranslateProps = {
|
|
57
|
+
id?: string
|
|
58
|
+
label?: string
|
|
59
|
+
/** The language list — no built-in default, import `DEFAULT_LANGUAGES` from this component to opt into one. */
|
|
60
|
+
languages: TranslateLanguage[]
|
|
61
|
+
defaultLanguage?: string
|
|
62
|
+
/** Paths where translation is disallowed — the blocked notice shows instead. */
|
|
63
|
+
blockedPaths?: string[]
|
|
64
|
+
scriptUrl?: string
|
|
65
|
+
storageKey?: string
|
|
66
|
+
/**
|
|
67
|
+
* Updates the trigger's visible language code + `aria-label` to reflect the active language.
|
|
68
|
+
* On by default — pass `false` to keep the trigger static instead, which some sites prefer.
|
|
69
|
+
*/
|
|
70
|
+
updateTriggerLabel?: boolean
|
|
71
|
+
/** Consumer-supplied copy shown once a non-default language is active — entirely opt-in, omit to never show a notice. */
|
|
72
|
+
noticeContent?: string
|
|
73
|
+
/** Consumer-supplied copy shown instead, on a blocked path. */
|
|
74
|
+
noticeBlockedContent?: string
|
|
75
|
+
/** Where to insert the notice. Defaults to the top of `<body>` if omitted — the notice doesn't have to live near this component, e.g. if the trigger is in the header. */
|
|
76
|
+
noticeAnchor?: string
|
|
77
|
+
/** Insertion position relative to `noticeAnchor`. Defaults to `afterend` when set, `afterbegin` otherwise. */
|
|
78
|
+
noticePosition?: InsertPosition
|
|
79
|
+
placement?: PopoverPlacementModifier
|
|
80
|
+
className?: string
|
|
81
|
+
/** Closes the menu after a language is chosen. */
|
|
82
|
+
closeOnSelect?: boolean
|
|
83
|
+
onOpen?: () => void
|
|
84
|
+
onClose?: () => void
|
|
85
|
+
onLanguageChange?: (code: string) => void
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const Translate = ({
|
|
89
|
+
id,
|
|
90
|
+
label = "Language",
|
|
91
|
+
languages,
|
|
92
|
+
defaultLanguage = "en",
|
|
93
|
+
blockedPaths,
|
|
94
|
+
scriptUrl,
|
|
95
|
+
storageKey,
|
|
96
|
+
updateTriggerLabel = true,
|
|
97
|
+
noticeContent,
|
|
98
|
+
noticeBlockedContent,
|
|
99
|
+
noticeAnchor,
|
|
100
|
+
noticePosition,
|
|
101
|
+
placement = PopoverPlacementModifier.BOTTOM_START,
|
|
102
|
+
className,
|
|
103
|
+
closeOnSelect = true,
|
|
104
|
+
onOpen,
|
|
105
|
+
onClose,
|
|
106
|
+
onLanguageChange,
|
|
107
|
+
}: TranslateProps): JSX.Element => {
|
|
108
|
+
const generatedId = useId()
|
|
109
|
+
const rootId = id ?? generatedId
|
|
110
|
+
const rootRef = useRef<HTMLDivElement>(null)
|
|
111
|
+
const popoverRef = useRef<HTMLDivElement>(null)
|
|
112
|
+
const widgetRef = useRef<GoogleTranslateWidget | null>(null)
|
|
113
|
+
const polyfillRef = useRef<any>(null)
|
|
114
|
+
|
|
115
|
+
const triggerStyle = useMemo(() => ({ anchorName: `--${rootId}` }), [rootId])
|
|
116
|
+
const popoverStyle = useMemo(() => ({ positionAnchor: `--${rootId}` }), [rootId])
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!rootRef.current) return undefined
|
|
120
|
+
const widget = new GoogleTranslateWidget(rootRef.current, {
|
|
121
|
+
defaultLanguage,
|
|
122
|
+
blockedPaths,
|
|
123
|
+
scriptUrl,
|
|
124
|
+
storageKey,
|
|
125
|
+
updateTriggerLabel,
|
|
126
|
+
notice: noticeContent
|
|
127
|
+
? {
|
|
128
|
+
content: noticeContent,
|
|
129
|
+
blockedContent: noticeBlockedContent,
|
|
130
|
+
anchorSelector: noticeAnchor,
|
|
131
|
+
position: noticePosition,
|
|
132
|
+
}
|
|
133
|
+
: undefined,
|
|
134
|
+
onLanguageChange,
|
|
135
|
+
})
|
|
136
|
+
widget.init()
|
|
137
|
+
widgetRef.current = widget
|
|
138
|
+
return () => widget.destroy()
|
|
139
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- only re-runs on mount/unmount; config changes apply on the next setLanguage call
|
|
140
|
+
}, [])
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const popover = popoverRef.current
|
|
144
|
+
if (!popover) return undefined
|
|
145
|
+
|
|
146
|
+
const polyfill = async (): Promise<void> => {
|
|
147
|
+
if (
|
|
148
|
+
!("anchorName" in document.documentElement.style) ||
|
|
149
|
+
!("popover" in HTMLElement.prototype)
|
|
150
|
+
) {
|
|
151
|
+
const { default: Polyfills } = await import("../Popover/polyfills")
|
|
152
|
+
polyfillRef.current = new Polyfills(popover)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
polyfill().catch(() => {})
|
|
156
|
+
|
|
157
|
+
const controller = new AbortController()
|
|
158
|
+
popover.addEventListener(
|
|
159
|
+
"toggle",
|
|
160
|
+
(event: Event) => {
|
|
161
|
+
const { newState } = event as ToggleEvent
|
|
162
|
+
if (newState === "open") onOpen?.()
|
|
163
|
+
else onClose?.()
|
|
164
|
+
},
|
|
165
|
+
{ signal: controller.signal },
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return () => {
|
|
169
|
+
controller.abort()
|
|
170
|
+
polyfillRef.current?.destroy()
|
|
171
|
+
}
|
|
172
|
+
}, [onOpen, onClose])
|
|
173
|
+
|
|
174
|
+
const handleSelect = (code: string, event: MouseEvent<HTMLAnchorElement>): void => {
|
|
175
|
+
event.preventDefault()
|
|
176
|
+
widgetRef.current?.setLanguage(code)
|
|
177
|
+
if (closeOnSelect) popoverRef.current?.hidePopover()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div ref={rootRef} className={classNames("mx-translate", className)}>
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
id={`${rootId}-target`}
|
|
185
|
+
className="mx-translate__trigger mx-popover__trigger notranslate"
|
|
186
|
+
popoverTarget={rootId}
|
|
187
|
+
style={triggerStyle}
|
|
188
|
+
data-label={label}
|
|
189
|
+
aria-label={label}
|
|
190
|
+
>
|
|
191
|
+
<Icon icon={Icons.GLOBE} />
|
|
192
|
+
<span className="mx-translate__code">{defaultLanguage.toUpperCase()}</span>
|
|
193
|
+
</button>
|
|
194
|
+
<div
|
|
195
|
+
ref={popoverRef}
|
|
196
|
+
id={rootId}
|
|
197
|
+
className="mx-popover mx-translate__menu notranslate"
|
|
198
|
+
aria-describedby={`${rootId}-target`}
|
|
199
|
+
aria-labelledby={`${rootId}-target`}
|
|
200
|
+
style={popoverStyle}
|
|
201
|
+
data-placement={placement}
|
|
202
|
+
popover=""
|
|
203
|
+
role="menu"
|
|
204
|
+
>
|
|
205
|
+
<FocusScope>
|
|
206
|
+
<TranslateItems languages={languages} onSelect={handleSelect} />
|
|
207
|
+
</FocusScope>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default Translate
|
|
@@ -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">
|
|
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" 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 }}"
|
|
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
|
+
}
|