@pnx-mixtape/mxds 0.1.0-rc.2 → 0.1.0-rc.3
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.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/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/cookie-consent.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.entry.js +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.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/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/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/ContentBlock/__snapshots__/ContentBlock.stories.ts.snap +12 -24
- package/src/Component/CookieConsent/cookie-consent.css +3 -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/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/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/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 +1 -1
- 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 +2 -0
- 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 +90 -32
- package/src/Layout/Footer/__snapshots__/Footer.stories.ts.snap +18 -33
- 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/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 +24 -0
- package/src/Utility/print.css +1 -1
- package/src/Utility/utilities.css +20 -0
- package/src/_custom-media.css +1 -0
- 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
|
@@ -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">
|
package/src/Form/form.css
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import "../Utility/_mixins.css";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Form
|
|
3
5
|
*/
|
|
@@ -65,6 +67,15 @@
|
|
|
65
67
|
&[disabled] {
|
|
66
68
|
opacity: var(--disabled-opacity, 0.6);
|
|
67
69
|
cursor: not-allowed;
|
|
70
|
+
|
|
71
|
+
@media (--forced-colors) {
|
|
72
|
+
/* opt out so GrayText survives */
|
|
73
|
+
forced-color-adjust: none;
|
|
74
|
+
opacity: 1;
|
|
75
|
+
color: GrayText;
|
|
76
|
+
border-color: GrayText;
|
|
77
|
+
background-color: Canvas;
|
|
78
|
+
}
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
81
|
|
|
@@ -198,6 +209,7 @@
|
|
|
198
209
|
.mx-checkbox [type="checkbox"] {
|
|
199
210
|
box-shadow: none;
|
|
200
211
|
appearance: none;
|
|
212
|
+
display: flex;
|
|
201
213
|
inline-size: var(--control-size, var(--spacing-m));
|
|
202
214
|
block-size: var(--control-size, var(--spacing-m));
|
|
203
215
|
background-color: var(--background, var(--colour-background));
|
|
@@ -209,26 +221,44 @@
|
|
|
209
221
|
--control-size: 2rem;
|
|
210
222
|
}
|
|
211
223
|
|
|
212
|
-
&:checked {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
&:checked::after {
|
|
225
|
+
content: "";
|
|
226
|
+
display: block;
|
|
227
|
+
inline-size: 0.75rem;
|
|
228
|
+
block-size: 0.75rem;
|
|
229
|
+
margin: auto;
|
|
230
|
+
mask-image: url("./images/checkmark.svg");
|
|
231
|
+
mask-size: contain;
|
|
232
|
+
mask-position: center;
|
|
233
|
+
mask-repeat: no-repeat;
|
|
234
|
+
background-color: var(--checkbox-colour, currentcolor);
|
|
235
|
+
|
|
236
|
+
@add-mixin forced-colors-paint;
|
|
216
237
|
}
|
|
217
238
|
|
|
218
239
|
&[disabled] {
|
|
219
|
-
|
|
220
|
-
opacity: var(--disabled-opacity, 0.6);
|
|
221
|
-
cursor: not-allowed;
|
|
222
|
-
content: "";
|
|
223
|
-
display: block;
|
|
224
|
-
inline-size: 100%;
|
|
225
|
-
block-size: 100%;
|
|
226
|
-
border-radius: var(--border-radius);
|
|
227
|
-
}
|
|
240
|
+
cursor: not-allowed;
|
|
228
241
|
|
|
229
242
|
& + label {
|
|
230
243
|
opacity: var(--disabled-opacity, 0.6);
|
|
231
244
|
cursor: not-allowed;
|
|
245
|
+
|
|
246
|
+
@media (--forced-colors) {
|
|
247
|
+
/* opt out or the UA forces this straight back to CanvasText */
|
|
248
|
+
forced-color-adjust: none;
|
|
249
|
+
opacity: 1;
|
|
250
|
+
color: GrayText;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@media (--forced-colors) {
|
|
255
|
+
/* opt out so GrayText survives */
|
|
256
|
+
--forced-colour: GrayText;
|
|
257
|
+
|
|
258
|
+
forced-color-adjust: none;
|
|
259
|
+
color: GrayText;
|
|
260
|
+
border-color: GrayText;
|
|
261
|
+
background-color: Canvas;
|
|
232
262
|
}
|
|
233
263
|
}
|
|
234
264
|
}
|
|
@@ -258,32 +288,41 @@
|
|
|
258
288
|
--control-size: 2rem;
|
|
259
289
|
}
|
|
260
290
|
|
|
261
|
-
&:checked {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
291
|
+
&:checked::after {
|
|
292
|
+
content: "";
|
|
293
|
+
display: block;
|
|
294
|
+
inline-size: 0.5rem;
|
|
295
|
+
block-size: 0.5rem;
|
|
296
|
+
margin: auto;
|
|
297
|
+
border-radius: 50%;
|
|
298
|
+
background-color: var(--radio-colour, var(--colour-primary));
|
|
299
|
+
|
|
300
|
+
@add-mixin forced-colors-paint;
|
|
271
301
|
}
|
|
272
302
|
|
|
273
303
|
&[disabled] {
|
|
274
|
-
|
|
275
|
-
opacity: var(--disabled-opacity, 0.6);
|
|
276
|
-
cursor: not-allowed;
|
|
277
|
-
content: "";
|
|
278
|
-
display: block;
|
|
279
|
-
inline-size: 100%;
|
|
280
|
-
block-size: 100%;
|
|
281
|
-
border-radius: 50%;
|
|
282
|
-
}
|
|
304
|
+
cursor: not-allowed;
|
|
283
305
|
|
|
284
306
|
& + label {
|
|
285
307
|
opacity: var(--disabled-opacity, 0.6);
|
|
286
308
|
cursor: not-allowed;
|
|
309
|
+
|
|
310
|
+
@media (--forced-colors) {
|
|
311
|
+
/* opt out or the UA forces this straight back to CanvasText */
|
|
312
|
+
forced-color-adjust: none;
|
|
313
|
+
opacity: 1;
|
|
314
|
+
color: GrayText;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@media (--forced-colors) {
|
|
319
|
+
/* opt out so GrayText survives */
|
|
320
|
+
--forced-colour: GrayText;
|
|
321
|
+
|
|
322
|
+
forced-color-adjust: none;
|
|
323
|
+
color: GrayText;
|
|
324
|
+
border-color: GrayText;
|
|
325
|
+
background-color: Canvas;
|
|
287
326
|
}
|
|
288
327
|
}
|
|
289
328
|
}
|
|
@@ -467,12 +506,25 @@
|
|
|
467
506
|
}
|
|
468
507
|
}
|
|
469
508
|
|
|
509
|
+
/* A <select> has no pseudo-element to mask, and the chevron asset is a
|
|
510
|
+
fixed black, so on the legacy path hand the arrow back to the UA under
|
|
511
|
+
forced colours. The base-select path masks ::picker-icon instead. */
|
|
512
|
+
@supports not (appearance: base-select) {
|
|
513
|
+
@media (--forced-colors) {
|
|
514
|
+
appearance: auto;
|
|
515
|
+
background-image: none;
|
|
516
|
+
padding-inline-end: revert;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
470
520
|
&::picker-icon {
|
|
471
521
|
mask-image: url("./images/form-chevron-down.svg");
|
|
472
522
|
mask-repeat: no-repeat;
|
|
473
523
|
mask-position: center;
|
|
474
524
|
background-color: currentcolor;
|
|
475
525
|
aspect-ratio: 1 / 1;
|
|
526
|
+
|
|
527
|
+
@add-mixin forced-colors-paint;
|
|
476
528
|
}
|
|
477
529
|
|
|
478
530
|
&:open::picker-icon {
|
|
@@ -486,6 +538,8 @@
|
|
|
486
538
|
display: block;
|
|
487
539
|
mask-image: url("./images/checkmark.svg");
|
|
488
540
|
background-color: currentcolor;
|
|
541
|
+
|
|
542
|
+
@add-mixin forced-colors-paint;
|
|
489
543
|
}
|
|
490
544
|
|
|
491
545
|
&[multiple] {
|
|
@@ -606,11 +660,15 @@
|
|
|
606
660
|
.mx-error-message::before {
|
|
607
661
|
mask-image: url("./images/form-error.svg");
|
|
608
662
|
background-color: var(--error-foreground, var(--colour-error));
|
|
663
|
+
|
|
664
|
+
@add-mixin forced-colors-paint;
|
|
609
665
|
}
|
|
610
666
|
|
|
611
667
|
.mx-success-message::before {
|
|
612
668
|
mask-image: url("./images/form-success.svg");
|
|
613
669
|
background-color: var(--success-foreground, var(--colour-success));
|
|
670
|
+
|
|
671
|
+
@add-mixin forced-colors-paint;
|
|
614
672
|
}
|
|
615
673
|
}
|
|
616
674
|
|
|
@@ -78,28 +78,25 @@ exports[`Everything 1`] = `
|
|
|
78
78
|
|
|
79
79
|
<nav class="mx-social-links" aria-label="Social media links">
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
<a href="#">
|
|
81
|
+
|
|
82
|
+
<a href="#" aria-label="Facebook">
|
|
83
83
|
|
|
84
84
|
<span class="mx-icon mx-icon--facebook mx-icon--size-md"></span>
|
|
85
85
|
|
|
86
|
-
<span class="sr-only">Facebook</span>
|
|
87
86
|
|
|
88
87
|
</a>
|
|
89
|
-
|
|
90
|
-
<a href="#">
|
|
88
|
+
|
|
89
|
+
<a href="#" aria-label="Instagram">
|
|
91
90
|
|
|
92
91
|
<span class="mx-icon mx-icon--instagram mx-icon--size-md"></span>
|
|
93
92
|
|
|
94
|
-
<span class="sr-only">Instagram</span>
|
|
95
93
|
|
|
96
94
|
</a>
|
|
97
|
-
|
|
98
|
-
<a href="#">
|
|
95
|
+
|
|
96
|
+
<a href="#" aria-label="LinkedIn">
|
|
99
97
|
|
|
100
98
|
<span class="mx-icon mx-icon--linkedin mx-icon--size-md"></span>
|
|
101
99
|
|
|
102
|
-
<span class="sr-only">LinkedIn</span>
|
|
103
100
|
|
|
104
101
|
</a>
|
|
105
102
|
|
|
@@ -112,14 +109,12 @@ exports[`Everything 1`] = `
|
|
|
112
109
|
|
|
113
110
|
<a href="#">
|
|
114
111
|
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
Accessibility
|
|
117
113
|
</a>
|
|
118
114
|
|
|
119
115
|
<a href="#">
|
|
120
116
|
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
Terms & Conditions
|
|
123
118
|
</a>
|
|
124
119
|
|
|
125
120
|
</nav>
|
|
@@ -169,14 +164,12 @@ exports[`Footer 1`] = `
|
|
|
169
164
|
|
|
170
165
|
<a href="#">
|
|
171
166
|
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
Accessibility
|
|
174
168
|
</a>
|
|
175
169
|
|
|
176
170
|
<a href="#">
|
|
177
171
|
|
|
178
|
-
|
|
179
|
-
|
|
172
|
+
Terms & Conditions
|
|
180
173
|
</a>
|
|
181
174
|
|
|
182
175
|
</nav>
|
|
@@ -226,14 +219,12 @@ exports[`Light 1`] = `
|
|
|
226
219
|
|
|
227
220
|
<a href="#">
|
|
228
221
|
|
|
229
|
-
|
|
230
|
-
|
|
222
|
+
Accessibility
|
|
231
223
|
</a>
|
|
232
224
|
|
|
233
225
|
<a href="#">
|
|
234
226
|
|
|
235
|
-
|
|
236
|
-
|
|
227
|
+
Terms & Conditions
|
|
237
228
|
</a>
|
|
238
229
|
|
|
239
230
|
</nav>
|
|
@@ -283,14 +274,12 @@ exports[`No Logo 1`] = `
|
|
|
283
274
|
|
|
284
275
|
<a href="#">
|
|
285
276
|
|
|
286
|
-
|
|
287
|
-
|
|
277
|
+
Accessibility
|
|
288
278
|
</a>
|
|
289
279
|
|
|
290
280
|
<a href="#">
|
|
291
281
|
|
|
292
|
-
|
|
293
|
-
|
|
282
|
+
Terms & Conditions
|
|
294
283
|
</a>
|
|
295
284
|
|
|
296
285
|
</nav>
|
|
@@ -313,14 +302,12 @@ exports[`No Menu 1`] = `
|
|
|
313
302
|
|
|
314
303
|
<a href="#">
|
|
315
304
|
|
|
316
|
-
|
|
317
|
-
|
|
305
|
+
Accessibility
|
|
318
306
|
</a>
|
|
319
307
|
|
|
320
308
|
<a href="#">
|
|
321
309
|
|
|
322
|
-
|
|
323
|
-
|
|
310
|
+
Terms & Conditions
|
|
324
311
|
</a>
|
|
325
312
|
|
|
326
313
|
</nav>
|
|
@@ -371,14 +358,12 @@ exports[`With Acknowledgment 1`] = `
|
|
|
371
358
|
|
|
372
359
|
<a href="#">
|
|
373
360
|
|
|
374
|
-
|
|
375
|
-
|
|
361
|
+
Accessibility
|
|
376
362
|
</a>
|
|
377
363
|
|
|
378
364
|
<a href="#">
|
|
379
365
|
|
|
380
|
-
|
|
381
|
-
|
|
366
|
+
Terms & Conditions
|
|
382
367
|
</a>
|
|
383
368
|
|
|
384
369
|
</nav>
|
|
@@ -18,6 +18,7 @@ import "../../Component/Navigation/Elements/Navigation"
|
|
|
18
18
|
// css.
|
|
19
19
|
import "../../Component/Navigation/navigation.css"
|
|
20
20
|
import { modes } from "../../../.storybook/modes"
|
|
21
|
+
import { runScreenReaderWalkthrough } from "../../../tools/test-utils/screen-reader"
|
|
21
22
|
|
|
22
23
|
type MxHeaderType = Omit<HeaderType, "controls"> & {
|
|
23
24
|
stacked: boolean
|
|
@@ -117,6 +118,38 @@ export const Header: Story = {
|
|
|
117
118
|
},
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Steps a virtual screen reader through the page and asserts on what it announces.
|
|
123
|
+
*/
|
|
124
|
+
export const ScreenReaderAnnouncements: Story = {
|
|
125
|
+
play: async () => {
|
|
126
|
+
const log = await runScreenReaderWalkthrough()
|
|
127
|
+
await expect(log).toEqual([
|
|
128
|
+
"document",
|
|
129
|
+
"banner",
|
|
130
|
+
"image, Mixtape",
|
|
131
|
+
"navigation, Primary navigation",
|
|
132
|
+
"list",
|
|
133
|
+
"listitem, level 1, position 1, set size 4",
|
|
134
|
+
"link, News",
|
|
135
|
+
"end of listitem, level 1, position 1, set size 4",
|
|
136
|
+
"listitem, level 1, position 2, set size 4",
|
|
137
|
+
"link, About us",
|
|
138
|
+
"end of listitem, level 1, position 2, set size 4",
|
|
139
|
+
"listitem, level 1, position 3, set size 4",
|
|
140
|
+
"link, Contact",
|
|
141
|
+
"end of listitem, level 1, position 3, set size 4",
|
|
142
|
+
"listitem, level 1, position 4, set size 4",
|
|
143
|
+
"link, Resources",
|
|
144
|
+
"end of listitem, level 1, position 4, set size 4",
|
|
145
|
+
"end of list",
|
|
146
|
+
"end of navigation, Primary navigation",
|
|
147
|
+
"end of banner",
|
|
148
|
+
"end of document",
|
|
149
|
+
])
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
|
|
120
153
|
export const WithTitle: Story = {
|
|
121
154
|
args: {
|
|
122
155
|
title: "Design System",
|