@pnx-mixtape/mxds 0.1.0-rc.2 → 0.1.0-rc.4
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/.storybook/{preview.ts → preview.tsx} +4 -0
- package/.storybook/src/docs.tsx +29 -0
- package/dist/build/accordion.css +1 -1
- package/dist/build/accordion.entry.js +1 -1
- package/dist/build/autocomplete.css +1 -1
- package/dist/build/autocomplete.entry.js +1 -1
- package/dist/build/base.css +1 -1
- package/dist/build/breadcrumb.css +1 -1
- package/dist/build/button.css +1 -1
- package/dist/build/card.css +1 -1
- package/dist/build/carousel.css +1 -1
- package/dist/build/chunks/{Accordion-z-dH6nFH.js → Accordion-GN0sME4W.js} +1 -1
- package/dist/build/chunks/{AccordionMobile-BYPPNyf5.js → AccordionMobile-DG5jSe8Q.js} +1 -1
- package/dist/build/chunks/{Autocomplete-2L6tLLuh.js → Autocomplete-B4RtkAyh.js} +1 -1
- package/dist/build/chunks/{ClosableAlert-DdXbF1J2.js → ClosableAlert-DEhj8Nxm.js} +1 -1
- package/dist/build/chunks/{Dialog-Ds8gzhri.js → Dialog-Haahn-Yn.js} +1 -1
- package/dist/build/chunks/Gallery-D8NGXy2Q.js +1 -0
- package/dist/build/chunks/GlobalToggle-B52fdBQu.js +1 -0
- package/dist/build/chunks/{InPageNavigation-CVieEl9u.js → InPageNavigation-Dyn2TR-N.js} +1 -1
- package/dist/build/chunks/Navigation-hOrfLq7h.js +1 -0
- package/dist/build/chunks/{Sticky-cWUVSOXN.js → Sticky-BGCuuEJK.js} +1 -1
- package/dist/build/chunks/{Tabs-DA_Ez8pv.js → Tabs-DYy1YTdU.js} +1 -1
- package/dist/build/chunks/enums-DLV_1NAf.js +1 -0
- package/dist/build/chunks/utilities-_Bq3TxAQ.js +1 -0
- package/dist/build/constants.css +1 -1
- package/dist/build/cookie-consent.css +1 -1
- package/dist/build/dialog.css +1 -1
- package/dist/build/dialog.entry.js +1 -1
- package/dist/build/elements.js +1 -1
- package/dist/build/filters.css +1 -1
- package/dist/build/filters.entry.js +1 -1
- package/dist/build/form.css +1 -1
- package/dist/build/gallery.entry.js +1 -1
- package/dist/build/global-alert.css +1 -1
- package/dist/build/global-alert.entry.js +1 -1
- package/dist/build/grid.css +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/list-item.css +1 -1
- package/dist/build/navigation.css +1 -1
- package/dist/build/navigation.entry.js +1 -1
- package/dist/build/one-time-code.css +1 -0
- package/dist/build/popover.css +1 -1
- package/dist/build/print.css +1 -1
- package/dist/build/react.js +1 -1
- package/dist/build/steps.css +1 -1
- package/dist/build/sticky.entry.js +1 -1
- package/dist/build/tabs.css +1 -1
- package/dist/build/tabs.entry.js +1 -1
- package/dist/build/utilities.css +1 -1
- package/dist/types/Atom/Link/Link.d.ts.map +1 -1
- package/dist/types/Component/Accordion/Accordion.d.ts.map +1 -1
- package/dist/types/Component/Dialog/Dialog.d.ts.map +1 -1
- package/dist/types/Component/Filters/Elements/Filters.d.ts +20 -1
- package/dist/types/Component/Filters/Elements/Filters.d.ts.map +1 -1
- package/dist/types/Component/Filters/Filters.d.ts +7 -2
- package/dist/types/Component/Filters/Filters.d.ts.map +1 -1
- package/dist/types/Component/Gallery/Gallery.d.ts.map +1 -1
- package/dist/types/Component/Pagination/Components/PaginationContext.d.ts.map +1 -1
- package/dist/types/Component/Tabs/Tabs.d.ts.map +1 -1
- package/dist/types/Form/Autocomplete/Autocomplete.d.ts +2 -2
- package/dist/types/Form/Autocomplete/Autocomplete.d.ts.map +1 -1
- package/dist/types/Form/Autocomplete/Elements/Autocomplete.d.ts +10 -0
- package/dist/types/Form/Autocomplete/Elements/Autocomplete.d.ts.map +1 -1
- package/dist/types/Form/OneTimeCode/OneTimeCode.d.ts +12 -0
- package/dist/types/Form/OneTimeCode/OneTimeCode.d.ts.map +1 -0
- package/dist/types/Utility/Hooks/useIsomorphicLayoutEffect.d.ts +4 -0
- package/dist/types/Utility/Hooks/useIsomorphicLayoutEffect.d.ts.map +1 -0
- package/dist/types/enums.d.ts +4 -0
- package/dist/types/enums.d.ts.map +1 -1
- package/dist/types/react.d.ts +1 -0
- package/dist/types/react.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/Atom/Button/__snapshots__/Button.stories.ts.snap +2 -3
- package/src/Atom/Button/_buttons.css +16 -0
- package/src/Atom/Button/button.twig +4 -1
- package/src/Atom/DefinitionList/DefinitionList.stories.ts +3 -0
- package/src/Atom/Icon/_icon.css +3 -0
- package/src/Atom/Link/Link.tsx +16 -3
- package/src/Atom/Link/__snapshots__/Link.stories.ts.snap +7 -12
- package/src/Atom/Link/__snapshots__/Link.stories.tsx.snap +1 -1
- package/src/Atom/Link/_links.css +9 -0
- package/src/Atom/Link/link.twig +8 -2
- package/src/Atom/LinkedImage/linked-image.twig +1 -2
- package/src/Atom/_generic.css +0 -10
- package/src/Component/Accordion/Accordion.stories.ts +31 -0
- package/src/Component/Accordion/Accordion.tsx +3 -2
- package/src/Component/Accordion/AccordionItem.stories.ts +27 -0
- package/src/Component/Accordion/__snapshots__/Accordion.stories.ts.snap +36 -0
- package/src/Component/Accordion/__snapshots__/AccordionItem.stories.ts.snap +16 -0
- package/src/Component/Accordion/accordion.css +3 -1
- package/src/Component/Breadcrumb/__snapshots__/Breadcrumb.stories.ts.snap +3 -6
- package/src/Component/Breadcrumb/breadcrumb.css +4 -0
- package/src/Component/Card/__snapshots__/Card.stories.ts.snap +11 -22
- package/src/Component/Card/card.css +2 -2
- package/src/Component/Carousel/Carousel.stories.ts +55 -0
- package/src/Component/Carousel/Carousel.stories.tsx +74 -0
- package/src/Component/Carousel/__snapshots__/Carousel.stories.ts.snap +273 -88
- package/src/Component/Carousel/__snapshots__/Carousel.stories.tsx.snap +5 -3
- package/src/Component/Carousel/carousel.css +1 -1
- package/src/Component/ContentBlock/__snapshots__/ContentBlock.stories.ts.snap +12 -24
- package/src/Component/CookieConsent/cookie-consent.css +6 -1
- package/src/Component/Dialog/Dialog.stories.ts +33 -0
- package/src/Component/Dialog/Dialog.stories.tsx +25 -0
- package/src/Component/Dialog/Dialog.tsx +2 -2
- package/src/Component/Dialog/__snapshots__/Dialog.stories.ts.snap +33 -2
- package/src/Component/Dialog/__snapshots__/Dialog.stories.tsx.snap +2 -0
- package/src/Component/Dialog/dialog.css +5 -2
- package/src/Component/DropMenu/DropMenu.stories.ts +39 -1
- package/src/Component/DropMenu/DropMenu.stories.tsx +29 -0
- package/src/Component/DropMenu/DropMenu.tsx +1 -1
- package/src/Component/DropMenu/__snapshots__/DropMenu.stories.ts.snap +14 -0
- package/src/Component/DropMenu/__snapshots__/DropMenu.stories.tsx.snap +3 -1
- package/src/Component/Filters/Elements/Filters.ts +71 -1
- package/src/Component/Filters/FilterItem.stories.ts +2 -1
- package/src/Component/Filters/FilterItem.tsx +1 -1
- package/src/Component/Filters/Filters.figma.ts +1 -1
- package/src/Component/Filters/Filters.stories.ts +95 -3
- package/src/Component/Filters/Filters.stories.tsx +246 -17
- package/src/Component/Filters/Filters.tsx +145 -29
- package/src/Component/Filters/README.md +102 -37
- package/src/Component/Filters/__snapshots__/FilterItem.stories.tsx.snap +3 -3
- package/src/Component/Filters/__snapshots__/Filters.stories.ts.snap +82 -4
- package/src/Component/Filters/__snapshots__/Filters.stories.tsx.snap +9 -3
- package/src/Component/Filters/filters.css +11 -0
- package/src/Component/Gallery/Gallery.tsx +2 -2
- package/src/Component/GlobalAlert/global-alert.css +3 -0
- package/src/Component/HeroBanner/HeroBanner.stories.ts +1 -1
- package/src/Component/HeroBanner/__snapshots__/HeroBanner.stories.ts.snap +4 -8
- package/src/Component/HeroSearch/__snapshots__/HeroSearch.stories.ts.snap +5 -10
- package/src/Component/InPageAlert/__snapshots__/InPageAlert.stories.ts.snap +6 -12
- package/src/Component/InPageNavigation/InPageNavigation.stories.ts +50 -0
- package/src/Component/InPageNavigation/InPageNavigation.stories.tsx +78 -0
- package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +35 -1
- package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.tsx.snap +2 -0
- package/src/Component/LinkList/__snapshots__/LinkList.stories.ts.snap +6 -12
- package/src/Component/ListItem/__snapshots__/ListItem.stories.ts.snap +7 -14
- package/src/Component/ListItem/list-item.css +1 -1
- package/src/Component/Navigation/Dropdown.stories.tsx +46 -0
- package/src/Component/Navigation/Navigation.stories.ts +56 -0
- package/src/Component/Navigation/__snapshots__/Dropdown.stories.tsx.snap +2 -0
- package/src/Component/Navigation/__snapshots__/Navigation.stories.ts.snap +57 -0
- package/src/Component/Navigation/_navigation-collapsible.css +3 -1
- package/src/Component/Navigation/_navigation-dropdown.css +3 -1
- package/src/Component/Pagination/Components/PaginationContext.tsx +2 -1
- package/src/Component/Pagination/Components/PaginationItem.tsx +1 -1
- package/src/Component/Pagination/Pagination.stories.ts +36 -0
- package/src/Component/Pagination/Pagination.stories.tsx +31 -0
- package/src/Component/Pagination/__snapshots__/Pagination.stories.ts.snap +69 -12
- package/src/Component/Pagination/__snapshots__/Pagination.stories.tsx.snap +3 -1
- package/src/Component/Pagination/pagination-item.twig +1 -1
- package/src/Component/Popover/__snapshots__/Popover.stories.ts.snap +13 -26
- package/src/Component/Popover/popover.css +4 -2
- package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.ts.snap +1 -2
- package/src/Component/SocialLinks/__snapshots__/SocialLinks.stories.ts.snap +6 -9
- package/src/Component/Steps/Steps.stories.ts +1 -1
- package/src/Component/Steps/steps.css +13 -0
- package/src/Component/Tabs/Tabs.stories.ts +22 -0
- package/src/Component/Tabs/Tabs.stories.tsx +24 -0
- package/src/Component/Tabs/Tabs.tsx +2 -2
- package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +18 -0
- package/src/Component/Tabs/__snapshots__/Tabs.stories.tsx.snap +3 -1
- package/src/Component/Tabs/tabs.css +6 -5
- package/src/Form/Autocomplete/Autocomplete.stories.ts +81 -4
- package/src/Form/Autocomplete/Autocomplete.stories.tsx +85 -1
- package/src/Form/Autocomplete/Autocomplete.tsx +19 -6
- package/src/Form/Autocomplete/Elements/Autocomplete.ts +24 -1
- package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.ts.snap +38 -4
- package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.tsx.snap +8 -4
- package/src/Form/Autocomplete/autocomplete.css +12 -1
- package/src/Form/Autocomplete/autocomplete.twig +1 -1
- package/src/Form/OneTimeCode/OneTimeCode.stories.ts +20 -0
- package/src/Form/OneTimeCode/OneTimeCode.stories.tsx +18 -0
- package/src/Form/OneTimeCode/OneTimeCode.tsx +56 -0
- package/src/Form/OneTimeCode/README.md +83 -0
- package/src/Form/OneTimeCode/__snapshots__/OneTimeCode.stories.ts.snap +7 -0
- package/src/Form/OneTimeCode/__snapshots__/OneTimeCode.stories.tsx.snap +3 -0
- package/src/Form/OneTimeCode/one-time-code.css +13 -0
- package/src/Form/OneTimeCode/one-time-code.twig +31 -0
- package/src/Form/__snapshots__/Validation.stories.ts.snap +1 -1
- package/src/Form/form.css +95 -35
- package/src/Layout/Footer/__snapshots__/Footer.stories.ts.snap +18 -33
- package/src/Layout/Grid/Grid.stories.ts +1 -0
- package/src/Layout/Grid/Grid.stories.tsx +26 -0
- package/src/Layout/Grid/__snapshots__/Grid.stories.tsx.snap +2 -0
- package/src/Layout/Grid/grid.css +12 -0
- package/src/Layout/Header/Header.stories.ts +33 -0
- package/src/Layout/Header/Header.stories.tsx +31 -0
- package/src/Layout/Header/__snapshots__/Header.stories.ts.snap +50 -0
- package/src/Layout/Header/__snapshots__/Header.stories.tsx.snap +2 -0
- package/src/Layout/Header/_header.css +4 -2
- package/src/Layout/Header/_toggles.css +4 -1
- package/src/Layout/Masthead/__snapshots__/Masthead.stories.ts.snap +8 -16
- package/src/Layout/Section/__snapshots__/Background.stories.ts.snap +2 -4
- package/src/Layout/Section/__snapshots__/Section.stories.ts.snap +4 -8
- package/src/Utility/Hooks/useIsomorphicLayoutEffect.ts +8 -0
- package/src/Utility/_mixins.css +46 -0
- package/src/Utility/print.css +1 -1
- package/src/Utility/utilities.css +20 -0
- package/src/_custom-media.css +3 -2
- package/src/constants.css +3 -13
- package/src/enums.ts +5 -0
- package/src/react.ts +1 -0
- package/tools/test-utils/screen-reader.ts +33 -0
- package/dist/build/chunks/Gallery-BT3cxS5m.js +0 -1
- package/dist/build/chunks/GlobalToggle-DQHho97V.js +0 -1
- package/dist/build/chunks/Navigation-B9Wug-ok.js +0 -1
- package/dist/build/chunks/disclosure-widget-BETzOHLO.js +0 -1
- package/dist/build/chunks/utilities-C5dAO2YA.js +0 -1
|
@@ -4,12 +4,13 @@ import {
|
|
|
4
4
|
ChangeEventHandler,
|
|
5
5
|
KeyboardEventHandler,
|
|
6
6
|
MouseEventHandler,
|
|
7
|
-
useEffect,
|
|
8
7
|
useRef,
|
|
8
|
+
useState,
|
|
9
9
|
ReactNode,
|
|
10
10
|
} from "react"
|
|
11
11
|
import classNames from "classnames"
|
|
12
12
|
import { useComboBoxState, Item } from "react-stately"
|
|
13
|
+
import useIsomorphicLayoutEffect from "../../Utility/Hooks/useIsomorphicLayoutEffect"
|
|
13
14
|
import { FormStatus } from "../../enums"
|
|
14
15
|
|
|
15
16
|
export type AutocompleteOption = {
|
|
@@ -33,8 +34,8 @@ export type AutocompleteProps = Omit<ComponentPropsWithoutRef<"input">, "value"
|
|
|
33
34
|
className?: string
|
|
34
35
|
noResultsLabel?: string
|
|
35
36
|
hideOnBlur?: boolean
|
|
36
|
-
/** Called on every keystroke with the trimmed input value — do the actual search here (debounce it yourself) and feed the results back in via `options`. */
|
|
37
|
-
onSearch?: (query: string) => void
|
|
37
|
+
/** Called on every keystroke with the trimmed input value — do the actual search here (debounce it yourself) and feed the results back in via `options`. Return a promise to drive the built-in pending state (`aria-busy` on the control, which also shows a spinner) while it resolves. */
|
|
38
|
+
onSearch?: (query: string) => void | Promise<void>
|
|
38
39
|
onSelect?: (option: AutocompleteOption) => void
|
|
39
40
|
onSubmit?: MouseEventHandler<HTMLButtonElement>
|
|
40
41
|
children?: ReactNode
|
|
@@ -65,6 +66,11 @@ const Autocomplete = ({
|
|
|
65
66
|
}: AutocompleteProps): JSX.Element => {
|
|
66
67
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
67
68
|
const listboxRef = useRef<HTMLUListElement>(null)
|
|
69
|
+
// Tracks which `onSearch` call is the most recent, so an out-of-order
|
|
70
|
+
// (slower) earlier response can't clear pending after a newer search has
|
|
71
|
+
// already started.
|
|
72
|
+
const searchTokenRef = useRef<number>(0)
|
|
73
|
+
const [pending, setPending] = useState<boolean>(false)
|
|
68
74
|
|
|
69
75
|
// `useComboBoxState` gives us the collection (for Home/End/next/prev key
|
|
70
76
|
// lookups), focused-key tracking, and input-value tracking, replacing
|
|
@@ -106,7 +112,7 @@ const Autocomplete = ({
|
|
|
106
112
|
const listboxId = `${id}-listbox`
|
|
107
113
|
const activeId = activeIndex >= 0 ? `${id}-option-${activeIndex}` : undefined
|
|
108
114
|
|
|
109
|
-
|
|
115
|
+
useIsomorphicLayoutEffect(() => {
|
|
110
116
|
listboxRef.current
|
|
111
117
|
?.querySelector('[aria-selected="true"]')
|
|
112
118
|
?.scrollIntoView({ block: "nearest" })
|
|
@@ -126,7 +132,14 @@ const Autocomplete = ({
|
|
|
126
132
|
state.selectionManager.setFocusedKey(null)
|
|
127
133
|
if (currentTarget.value.trim().length > 0) state.open()
|
|
128
134
|
else closeMenu()
|
|
129
|
-
onSearch?.(currentTarget.value.trim())
|
|
135
|
+
const result = onSearch?.(currentTarget.value.trim())
|
|
136
|
+
if (result) {
|
|
137
|
+
const token = ++searchTokenRef.current
|
|
138
|
+
setPending(true)
|
|
139
|
+
result.finally(() => {
|
|
140
|
+
if (searchTokenRef.current === token) setPending(false)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
const handleFocus = (): void => {
|
|
@@ -200,7 +213,7 @@ const Autocomplete = ({
|
|
|
200
213
|
|
|
201
214
|
return (
|
|
202
215
|
<div className={classNames("mx-autocomplete", className)}>
|
|
203
|
-
<div className="mx-autocomplete__control mx-form--inline-button">
|
|
216
|
+
<div className="mx-autocomplete__control mx-form--inline-button" aria-busy={pending}>
|
|
204
217
|
<input
|
|
205
218
|
{...props}
|
|
206
219
|
ref={inputRef}
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* Elasticsearch call). A MutationObserver watches the listbox for those DOM
|
|
9
9
|
* changes and updates aria-expanded/aria-activedescendant/the status message
|
|
10
10
|
* to match whatever options currently exist.
|
|
11
|
+
*
|
|
12
|
+
* The `pending` property is fully consumer-controlled — there's no promise to
|
|
13
|
+
* await here, only an event — so set `el.pending = true` before your search
|
|
14
|
+
* call and `= false` once it settles. It reflects `aria-busy` on the control,
|
|
15
|
+
* the same contract the React implementation exposes: one attribute drives
|
|
16
|
+
* both the assistive-tech signal and the spinner (see the generic
|
|
17
|
+
* `[aria-busy="true"]` utility in Utility/utilities.css).
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
20
|
export type AutocompleteSearchEvent = CustomEvent<{ query: string }>
|
|
@@ -191,8 +198,16 @@ export default class Autocomplete extends HTMLElement {
|
|
|
191
198
|
return this.dataset.noResultsLabel || "No results found"
|
|
192
199
|
}
|
|
193
200
|
|
|
201
|
+
get control(): HTMLDivElement {
|
|
202
|
+
const div = this.querySelector<HTMLDivElement>("[data-control]")
|
|
203
|
+
if (!div) {
|
|
204
|
+
throw new Error(`${this.localName} must contain a [data-control] element.`)
|
|
205
|
+
}
|
|
206
|
+
return div
|
|
207
|
+
}
|
|
208
|
+
|
|
194
209
|
get input(): HTMLInputElement {
|
|
195
|
-
const input = this.querySelector<HTMLInputElement>('input[role="combobox"]')
|
|
210
|
+
const input = this.control.querySelector<HTMLInputElement>('input[role="combobox"]')
|
|
196
211
|
if (!input) {
|
|
197
212
|
throw new Error(`${this.localName} must contain an <input role="combobox">.`)
|
|
198
213
|
}
|
|
@@ -222,6 +237,14 @@ export default class Autocomplete extends HTMLElement {
|
|
|
222
237
|
get visibleOptions(): HTMLLIElement[] {
|
|
223
238
|
return this.options.filter(option => !option.hidden)
|
|
224
239
|
}
|
|
240
|
+
|
|
241
|
+
get pending(): boolean {
|
|
242
|
+
return this.control.getAttribute("aria-busy") === "true"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
set pending(value: boolean) {
|
|
246
|
+
this.control.setAttribute("aria-busy", String(value))
|
|
247
|
+
}
|
|
225
248
|
}
|
|
226
249
|
|
|
227
250
|
if (!customElements.get("mx-autocomplete")) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
exports[`Autocomplete 1`] = `
|
|
4
4
|
"
|
|
5
5
|
<mx-autocomplete class="mx-autocomplete">
|
|
6
|
-
<div class="mx-autocomplete__control mx-form--inline-button">
|
|
6
|
+
<div class="mx-autocomplete__control mx-form--inline-button" data-control="" aria-busy="false">
|
|
7
7
|
<input class="mx-input__text" id="autocomplete-fruit" name="unique-0" type="text" role="combobox" aria-expanded="false" aria-controls="unique-0" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-0" aria-label="Choose a fruit" autocomplete="off" placeholder="Start typing a fruit…">
|
|
8
8
|
</div>
|
|
9
9
|
<div class="mx-autocomplete__suggestions">
|
|
@@ -18,7 +18,7 @@ exports[`Autocomplete 1`] = `
|
|
|
18
18
|
exports[`Invalid 1`] = `
|
|
19
19
|
"
|
|
20
20
|
<mx-autocomplete class="mx-autocomplete">
|
|
21
|
-
<div class="mx-autocomplete__control mx-form--inline-button">
|
|
21
|
+
<div class="mx-autocomplete__control mx-form--inline-button" data-control="">
|
|
22
22
|
<input class="mx-input__text" id="autocomplete-fruit" name="unique-0" type="text" role="combobox" aria-expanded="false" aria-controls="unique-0" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-0" aria-label="Choose a fruit" autocomplete="off" placeholder="Start typing a fruit…" aria-invalid="true">
|
|
23
23
|
</div>
|
|
24
24
|
<div class="mx-autocomplete__suggestions">
|
|
@@ -31,10 +31,29 @@ exports[`Invalid 1`] = `
|
|
|
31
31
|
"
|
|
32
32
|
`;
|
|
33
33
|
|
|
34
|
+
exports[`Pending 1`] = `
|
|
35
|
+
"
|
|
36
|
+
<mx-autocomplete class="mx-autocomplete">
|
|
37
|
+
<div class="mx-autocomplete__control mx-form--inline-button" data-control="" aria-busy="false">
|
|
38
|
+
<input class="mx-input__text" id="autocomplete-fruit" name="unique-0" type="search" role="combobox" aria-expanded="true" aria-controls="unique-0" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-0" aria-label="Choose a fruit" autocomplete="off" placeholder="Start typing a fruit…">
|
|
39
|
+
<button class="mx-button mx-button--icon-only" type="button">
|
|
40
|
+
<span class="mx-icon mx-icon--search" aria-hidden="true"></span>
|
|
41
|
+
<span class="sr-only">Search</span>
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="mx-autocomplete__suggestions">
|
|
45
|
+
|
|
46
|
+
<ul class="mx-autocomplete__listbox" id="unique-0" role="listbox"><li class="mx-autocomplete__option" id="autocomplete-fruit-option-0" role="option" aria-selected="false" data-value="blueberry">Blueberry</li><li class="mx-autocomplete__option" id="autocomplete-fruit-option-1" role="option" aria-selected="false" data-value="plum">Plum</li></ul>
|
|
47
|
+
<output class="sr-only" aria-live="polite" data-autocomplete-status="">2 results available</output>
|
|
48
|
+
</div>
|
|
49
|
+
</mx-autocomplete>
|
|
50
|
+
"
|
|
51
|
+
`;
|
|
52
|
+
|
|
34
53
|
exports[`Required 1`] = `
|
|
35
54
|
"
|
|
36
55
|
<mx-autocomplete class="mx-autocomplete">
|
|
37
|
-
<div class="mx-autocomplete__control mx-form--inline-button">
|
|
56
|
+
<div class="mx-autocomplete__control mx-form--inline-button" data-control="">
|
|
38
57
|
<input class="mx-input__text" id="autocomplete-fruit" name="unique-0" type="text" role="combobox" aria-expanded="false" aria-controls="unique-0" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-0" aria-label="Choose a fruit" autocomplete="off" placeholder="Start typing a fruit…" required="">
|
|
39
58
|
</div>
|
|
40
59
|
<div class="mx-autocomplete__suggestions">
|
|
@@ -47,10 +66,25 @@ exports[`Required 1`] = `
|
|
|
47
66
|
"
|
|
48
67
|
`;
|
|
49
68
|
|
|
69
|
+
exports[`Screen Reader Announcements 1`] = `
|
|
70
|
+
"
|
|
71
|
+
<mx-autocomplete class="mx-autocomplete">
|
|
72
|
+
<div class="mx-autocomplete__control mx-form--inline-button" data-control="" aria-busy="true">
|
|
73
|
+
<input class="mx-input__text" id="autocomplete-fruit" name="unique-0" type="text" role="combobox" aria-expanded="false" aria-controls="unique-0" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-0" aria-label="Choose a fruit" autocomplete="off" placeholder="Start typing a fruit…">
|
|
74
|
+
</div>
|
|
75
|
+
<div class="mx-autocomplete__suggestions">
|
|
76
|
+
|
|
77
|
+
<ul class="mx-autocomplete__listbox" id="unique-0" role="listbox" hidden=""><li class="mx-autocomplete__option" id="autocomplete-fruit-option-0" role="option" aria-selected="false" data-value="blueberry">Blueberry</li><li class="mx-autocomplete__option" id="autocomplete-fruit-option-1" role="option" aria-selected="false" data-value="plum">Plum</li></ul>
|
|
78
|
+
<output class="sr-only" aria-live="polite" data-autocomplete-status=""></output>
|
|
79
|
+
</div>
|
|
80
|
+
</mx-autocomplete>
|
|
81
|
+
"
|
|
82
|
+
`;
|
|
83
|
+
|
|
50
84
|
exports[`Search 1`] = `
|
|
51
85
|
"
|
|
52
86
|
<mx-autocomplete class="mx-autocomplete">
|
|
53
|
-
<div class="mx-autocomplete__control mx-form--inline-button">
|
|
87
|
+
<div class="mx-autocomplete__control mx-form--inline-button" data-control="">
|
|
54
88
|
<input class="mx-input__text" id="autocomplete-fruit" name="unique-0" type="search" role="combobox" aria-expanded="false" aria-controls="unique-0" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-0" aria-label="Choose a fruit" autocomplete="off" placeholder="Start typing a fruit…">
|
|
55
89
|
<button class="mx-button mx-button--icon-only" type="button">
|
|
56
90
|
<span class="mx-icon mx-icon--search" aria-hidden="true"></span>
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`Autocomplete 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
|
|
3
|
+
exports[`Autocomplete 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button" aria-busy="false"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
|
|
4
4
|
|
|
5
|
-
exports[`Invalid 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="true" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
|
|
5
|
+
exports[`Invalid 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button" aria-busy="false"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="true" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
|
|
6
6
|
|
|
7
|
-
exports[`
|
|
7
|
+
exports[`Pending 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button" aria-busy="false"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="true" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="text" value="lu" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox"><li id="autocomplete-fruit-option-0" role="option" aria-selected="false" class="mx-autocomplete__option">Blueberry</li><li id="autocomplete-fruit-option-1" role="option" aria-selected="false" class="mx-autocomplete__option">Plum</li></ul><output class="sr-only" aria-live="polite">2 results available</output></div></div>"`;
|
|
8
8
|
|
|
9
|
-
exports[`
|
|
9
|
+
exports[`Required 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button" aria-busy="false"><input placeholder="Start typing a fruit…" required="" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
|
|
10
|
+
|
|
11
|
+
exports[`Screen Reader Announcements 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button" aria-busy="false"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
|
|
12
|
+
|
|
13
|
+
exports[`Search 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button" aria-busy="false"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="search" value="" name="fruit"><button class="mx-button mx-button--icon-only" type="button"><span class="mx-icon mx-icon--search" aria-hidden="true"></span><span class="sr-only">Search</span></button></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
max-inline-size: var(--container-max-width);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/* @public --autocomplete-background --autocomplete-foreground --autocomplete-radius --autocomplete-shadow --autocomplete-max-height --autocomplete-padding-inline --autocomplete-padding-block */
|
|
17
|
+
/* @public --autocomplete-background --autocomplete-foreground --autocomplete-radius --autocomplete-shadow --autocomplete-max-height --autocomplete-padding-inline --autocomplete-padding-block --autocomplete-line-width --autocomplete-line-colour */
|
|
18
18
|
.mx-autocomplete__listbox {
|
|
19
19
|
position: absolute;
|
|
20
20
|
z-index: 10;
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
margin: 0;
|
|
24
24
|
list-style: none;
|
|
25
25
|
background-color: var(--autocomplete-background, var(--colour-background));
|
|
26
|
+
border: var(--autocomplete-line-width, 1px) solid var(--autocomplete-line-colour, transparent);
|
|
26
27
|
color: var(--autocomplete-foreground, var(--colour-foreground));
|
|
27
28
|
border-radius: var(--autocomplete-radius, var(--border-radius));
|
|
28
29
|
box-shadow: var(--autocomplete-shadow, var(--box-shadow));
|
|
@@ -57,4 +58,14 @@
|
|
|
57
58
|
outline-offset: var(--outline-offset, -2px);
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
|
|
62
|
+
.mx-autocomplete__control[aria-busy] {
|
|
63
|
+
& button {
|
|
64
|
+
order: 3;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
&::after {
|
|
68
|
+
order: 2;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
60
71
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
{% set status = status|default("default") %}
|
|
6
6
|
|
|
7
7
|
<mx-autocomplete class="mx-autocomplete"{% if noResultsLabel %} data-no-results-label="{{ noResultsLabel }}"{% endif %}>
|
|
8
|
-
<div class="mx-autocomplete__control mx-form--inline-button">
|
|
8
|
+
<div class="mx-autocomplete__control mx-form--inline-button" data-control>
|
|
9
9
|
<input{{ attributes }}
|
|
10
10
|
id="{{ id }}"
|
|
11
11
|
name="{{ name }}"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Meta, StoryObj } from "@storybook/html-vite"
|
|
2
|
+
import Component from "./one-time-code.twig"
|
|
3
|
+
import "../form.css"
|
|
4
|
+
import "./one-time-code.css"
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Component> = {
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
component: Component,
|
|
9
|
+
args: {
|
|
10
|
+
id: "otp",
|
|
11
|
+
name: "otp",
|
|
12
|
+
label: "One-time code",
|
|
13
|
+
length: 6,
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default meta
|
|
18
|
+
type Story = StoryObj<typeof meta>
|
|
19
|
+
|
|
20
|
+
export const OneTimeCode: Story = {}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Meta, StoryObj } from "@storybook/react-vite"
|
|
2
|
+
import Component from "./OneTimeCode"
|
|
3
|
+
import "./one-time-code.css"
|
|
4
|
+
import { Page } from "../../../.storybook-react/decorators"
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Component> = {
|
|
7
|
+
decorators: [Page],
|
|
8
|
+
component: Component,
|
|
9
|
+
args: {
|
|
10
|
+
id: "otp",
|
|
11
|
+
name: "otp",
|
|
12
|
+
"aria-label": "One-time code",
|
|
13
|
+
length: 6,
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default meta
|
|
18
|
+
export const OneTimeCode: StoryObj<typeof Component> = {}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { JSX, ComponentPropsWithoutRef, useState, useMemo, ChangeEventHandler } from "react"
|
|
2
|
+
import classNames from "classnames"
|
|
3
|
+
import { FormStatus } from "../../enums"
|
|
4
|
+
|
|
5
|
+
type OneTimeCodeProps = ComponentPropsWithoutRef<"input"> & {
|
|
6
|
+
value?: string
|
|
7
|
+
className?: string
|
|
8
|
+
status?: FormStatus
|
|
9
|
+
/** Number of digits the code contains. Drives `maxLength` and the field's width. */
|
|
10
|
+
length?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const OneTimeCode = ({
|
|
14
|
+
id,
|
|
15
|
+
required = false,
|
|
16
|
+
value = "",
|
|
17
|
+
status = FormStatus.DEFAULT,
|
|
18
|
+
name = null,
|
|
19
|
+
className = "",
|
|
20
|
+
length = 6,
|
|
21
|
+
style,
|
|
22
|
+
...props
|
|
23
|
+
}: OneTimeCodeProps): JSX.Element => {
|
|
24
|
+
const [inputValue, setInputValue] = useState<string>(value)
|
|
25
|
+
const handleChange: ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) =>
|
|
26
|
+
setInputValue(currentTarget.value.replace(/\D/g, ""))
|
|
27
|
+
|
|
28
|
+
const classes = classNames("mx-input__text", "mx-one-time-code", "mx-validate", className)
|
|
29
|
+
const otpStyle = useMemo(
|
|
30
|
+
() => ({ ...style, "--one-time-code-length": length }) as never,
|
|
31
|
+
[style, length],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<input
|
|
36
|
+
className={classes}
|
|
37
|
+
style={otpStyle}
|
|
38
|
+
onChange={handleChange}
|
|
39
|
+
id={id}
|
|
40
|
+
type="text"
|
|
41
|
+
inputMode="numeric"
|
|
42
|
+
autoComplete="one-time-code"
|
|
43
|
+
pattern="[0-9]*"
|
|
44
|
+
minLength={length}
|
|
45
|
+
maxLength={length}
|
|
46
|
+
name={name}
|
|
47
|
+
value={inputValue}
|
|
48
|
+
required={required}
|
|
49
|
+
aria-describedby={`help-${id}`}
|
|
50
|
+
aria-invalid={status === FormStatus.INVALID}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default OneTimeCode
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# OneTimeCode
|
|
2
|
+
|
|
3
|
+
A single `<input>` for one-time-code / 2FA entry — `autocomplete="one-time-code"` gets native SMS autofill and password-manager support, without splitting the field into six separate inputs. Centred, letter-spaced digits stand in for the usual per-box divider look. See [Form/README.md](../README.md#the-composition-pattern) for the surrounding `FormItem`/`FormLabel`/`FormDescription` pattern.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## CSS Import
|
|
8
|
+
|
|
9
|
+
```css
|
|
10
|
+
@import "@pnx-mixtape/mxds/src/Form/OneTimeCode/one-time-code.css";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Twig
|
|
16
|
+
|
|
17
|
+
```twig
|
|
18
|
+
{% include '@mixtape/Form/OneTimeCode/one-time-code.twig' with {
|
|
19
|
+
id: 'otp',
|
|
20
|
+
name: 'otp',
|
|
21
|
+
label: 'One-time code',
|
|
22
|
+
length: 6,
|
|
23
|
+
} %}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## HTML
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<input
|
|
32
|
+
class="mx-input__text mx-one-time-code mx-validate"
|
|
33
|
+
id="otp"
|
|
34
|
+
name="otp"
|
|
35
|
+
type="text"
|
|
36
|
+
inputmode="numeric"
|
|
37
|
+
autocomplete="one-time-code"
|
|
38
|
+
pattern="[0-9]*"
|
|
39
|
+
minlength="6"
|
|
40
|
+
maxlength="6"
|
|
41
|
+
aria-label="One-time code"
|
|
42
|
+
/>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## React
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { OneTimeCode } from "@pnx-mixtape/mxds/react"
|
|
51
|
+
|
|
52
|
+
;<OneTimeCode id="otp" name="otp" aria-label="One-time code" length={6} />
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**`OneTimeCode` props:** all native `<input>` attributes + `status?: FormStatus` (default: `DEFAULT`) + `length?: number` (default: `6`, sets `minLength`/`maxLength` and the field's width — see Customisation)
|
|
56
|
+
|
|
57
|
+
Renders `mx-input__text mx-one-time-code mx-validate`. Sets `aria-describedby` and `aria-invalid` automatically, same as `FormText`. Strips non-digit characters on change.
|
|
58
|
+
|
|
59
|
+
### React ⇄ Twig parity
|
|
60
|
+
|
|
61
|
+
- `id`/`name`/`required` flow through React via `ComponentPropsWithoutRef<"input">` (spread onto the `<input>`, not named in `OneTimeCodeProps`) rather than explicit props — same shape as `FormText`. Twig takes them as named template variables. Functionally identical, not a real divergence.
|
|
62
|
+
- `label`/`hasDescription` are Twig-only helpers that build the `aria-label`/`aria-describedby` wiring from a single include call, matching `TextInput`'s Twig template. React expects those same ARIA attributes passed directly (`aria-label`, `aria-describedby`) since JSX has no template-include layer to generate them from.
|
|
63
|
+
- `length` sizes the field via the `--one-time-code-length` custom property (React sets it through `style`, Twig through a `style` attribute on the `<input>`) — both stacks resolve `length` the same way. CSS can't read the `maxlength` attribute's value into a `calc()`, so this is the one place the component reaches for an inline style rather than a class.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Modifiers
|
|
68
|
+
|
|
69
|
+
This component has no BEM modifier classes — it composes `mx-input__text`'s shared border/background/status styling from `Form/form.css` (see [Form/README.md](../README.md#input-modifier-classes)) and adds `mx-one-time-code` for the centred, letter-spaced digit layout. Unlike `FormText`, `mx-validate` is always applied (not opt-in) — a code field's native constraints (`pattern`, `minlength`/`maxlength`, `required`) are worth surfacing live as the user types, and there's no plausible case for wanting a OneTimeCode field that never colours on completion.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Accessibility
|
|
74
|
+
|
|
75
|
+
- `autocomplete="one-time-code"` is what lets browsers/OSes autofill an SMS-delivered code into this single field — the reason to prefer one input over six. See [WHATWG HTML: autofill field `one-time-code`](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-field-one-time-code).
|
|
76
|
+
- `inputmode="numeric"` brings up a numeric keypad on mobile without changing the semantic `type="text"` (using `type="number"` would strip leading zeros and add spinner controls).
|
|
77
|
+
- Always pair with a visible or `aria-label`led label; centred digits alone don't announce "one-time code" to screen readers.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Customisation
|
|
82
|
+
|
|
83
|
+
`--one-time-code-length` (default `6`) sizes the field and is set per-instance via the `length` prop/variable, not something you override directly in CSS — see React ⇄ Twig parity, above. Letter-spacing and `tabular-nums` digit width can be tuned by overriding `letter-spacing`/`font-variant-numeric` on `.mx-one-time-code` as with any other input.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`One Time Code 1`] = `
|
|
4
|
+
"
|
|
5
|
+
<input class="mx-input__text mx-one-time-code mx-validate" id="otp" name="otp" type="text" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]*" minlength="6" maxlength="6" style="--one-time-code-length: 6;" value="" aria-label="One-time code">
|
|
6
|
+
"
|
|
7
|
+
`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`One Time Code 1`] = `"<input class="mx-input__text mx-one-time-code mx-validate" id="otp" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]*" minlength="6" maxlength="6" aria-describedby="help-otp" aria-invalid="false" aria-label="One-time code" type="text" value="" name="otp" style="--one-time-code-length: 6;">"`;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneTimeCode
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
@layer design-system.components {
|
|
6
|
+
/* @public --one-time-code-length */
|
|
7
|
+
.mx-one-time-code {
|
|
8
|
+
inline-size: calc((var(--one-time-code-length, 6) * 2.5ch) + 1ch);
|
|
9
|
+
text-align: center;
|
|
10
|
+
letter-spacing: 0.5ch;
|
|
11
|
+
font-variant-numeric: tabular-nums;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{% set classes = [
|
|
2
|
+
'mx-input__text',
|
|
3
|
+
'mx-one-time-code',
|
|
4
|
+
'mx-validate',
|
|
5
|
+
] %}
|
|
6
|
+
{% set attributes = (attributes ?? create_attribute()).addClass(classes) %}
|
|
7
|
+
{% set status = status|default("default") %}
|
|
8
|
+
{% set length = length|default(6) %}
|
|
9
|
+
{% if hasDescription or status != "default" %}
|
|
10
|
+
{% set helpers = [
|
|
11
|
+
hasDescription ? "help-"~ id : null,
|
|
12
|
+
status != "default" ? "status-"~ id : null,
|
|
13
|
+
]|join(" ")|trim() %}
|
|
14
|
+
{% endif %}
|
|
15
|
+
|
|
16
|
+
<input{{ attributes }}
|
|
17
|
+
id="{{ id }}"
|
|
18
|
+
name="{{ name }}"
|
|
19
|
+
type="text"
|
|
20
|
+
inputmode="numeric"
|
|
21
|
+
autocomplete="one-time-code"
|
|
22
|
+
pattern="[0-9]*"
|
|
23
|
+
minlength="{{ length }}"
|
|
24
|
+
maxlength="{{ length }}"
|
|
25
|
+
style="--one-time-code-length: {{ length }};"
|
|
26
|
+
value="{{ value }}"
|
|
27
|
+
{% if label %} aria-label="{{ label }}"{% endif %}
|
|
28
|
+
{% if helpers is not empty %} aria-describedby="{{ helpers }}"{% endif %}
|
|
29
|
+
{% if status == "invalid" %} aria-invalid="true"{% endif %}
|
|
30
|
+
{% if required %} required{% endif %}
|
|
31
|
+
/>
|
|
@@ -103,7 +103,7 @@ exports[`Validation 1`] = `
|
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
<mx-autocomplete class="mx-autocomplete">
|
|
106
|
-
<div class="mx-autocomplete__control mx-form--inline-button">
|
|
106
|
+
<div class="mx-autocomplete__control mx-form--inline-button" data-control="">
|
|
107
107
|
<input class="mx-validate mx-input__text" id="unique-6" name="unique-2" type="text" role="combobox" aria-expanded="false" aria-controls="unique-2" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-2" aria-label="Favourite fruit" autocomplete="off" required="">
|
|
108
108
|
</div>
|
|
109
109
|
<div class="mx-autocomplete__suggestions">
|