@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.
Files changed (189) hide show
  1. package/.storybook/{preview.ts → preview.tsx} +4 -0
  2. package/.storybook/src/docs.tsx +29 -0
  3. package/dist/build/accordion.entry.js +1 -1
  4. package/dist/build/autocomplete.css +1 -1
  5. package/dist/build/autocomplete.entry.js +1 -1
  6. package/dist/build/base.css +1 -1
  7. package/dist/build/breadcrumb.css +1 -1
  8. package/dist/build/button.css +1 -1
  9. package/dist/build/card.css +1 -1
  10. package/dist/build/chunks/{Accordion-z-dH6nFH.js → Accordion-GN0sME4W.js} +1 -1
  11. package/dist/build/chunks/{AccordionMobile-BYPPNyf5.js → AccordionMobile-DG5jSe8Q.js} +1 -1
  12. package/dist/build/chunks/{Autocomplete-2L6tLLuh.js → Autocomplete-B4RtkAyh.js} +1 -1
  13. package/dist/build/chunks/{ClosableAlert-DdXbF1J2.js → ClosableAlert-DEhj8Nxm.js} +1 -1
  14. package/dist/build/chunks/{Dialog-Ds8gzhri.js → Dialog-Haahn-Yn.js} +1 -1
  15. package/dist/build/chunks/Gallery-D8NGXy2Q.js +1 -0
  16. package/dist/build/chunks/GlobalToggle-B52fdBQu.js +1 -0
  17. package/dist/build/chunks/{InPageNavigation-CVieEl9u.js → InPageNavigation-Dyn2TR-N.js} +1 -1
  18. package/dist/build/chunks/Navigation-hOrfLq7h.js +1 -0
  19. package/dist/build/chunks/{Sticky-cWUVSOXN.js → Sticky-BGCuuEJK.js} +1 -1
  20. package/dist/build/chunks/{Tabs-DA_Ez8pv.js → Tabs-DYy1YTdU.js} +1 -1
  21. package/dist/build/chunks/enums-DLV_1NAf.js +1 -0
  22. package/dist/build/chunks/utilities-_Bq3TxAQ.js +1 -0
  23. package/dist/build/cookie-consent.css +1 -1
  24. package/dist/build/dialog.entry.js +1 -1
  25. package/dist/build/elements.js +1 -1
  26. package/dist/build/filters.css +1 -1
  27. package/dist/build/filters.entry.js +1 -1
  28. package/dist/build/form.css +1 -1
  29. package/dist/build/gallery.entry.js +1 -1
  30. package/dist/build/global-alert.entry.js +1 -1
  31. package/dist/build/header.entry.js +1 -1
  32. package/dist/build/icon.css +1 -1
  33. package/dist/build/in-page-navigation.entry.js +1 -1
  34. package/dist/build/list-item.css +1 -1
  35. package/dist/build/navigation.entry.js +1 -1
  36. package/dist/build/one-time-code.css +1 -0
  37. package/dist/build/popover.css +1 -1
  38. package/dist/build/print.css +1 -1
  39. package/dist/build/react.js +1 -1
  40. package/dist/build/steps.css +1 -1
  41. package/dist/build/sticky.entry.js +1 -1
  42. package/dist/build/tabs.css +1 -1
  43. package/dist/build/tabs.entry.js +1 -1
  44. package/dist/build/utilities.css +1 -1
  45. package/dist/types/Atom/Link/Link.d.ts.map +1 -1
  46. package/dist/types/Component/Accordion/Accordion.d.ts.map +1 -1
  47. package/dist/types/Component/Dialog/Dialog.d.ts.map +1 -1
  48. package/dist/types/Component/Filters/Elements/Filters.d.ts +20 -1
  49. package/dist/types/Component/Filters/Elements/Filters.d.ts.map +1 -1
  50. package/dist/types/Component/Filters/Filters.d.ts +7 -2
  51. package/dist/types/Component/Filters/Filters.d.ts.map +1 -1
  52. package/dist/types/Component/Gallery/Gallery.d.ts.map +1 -1
  53. package/dist/types/Component/Pagination/Components/PaginationContext.d.ts.map +1 -1
  54. package/dist/types/Component/Tabs/Tabs.d.ts.map +1 -1
  55. package/dist/types/Form/Autocomplete/Autocomplete.d.ts +2 -2
  56. package/dist/types/Form/Autocomplete/Autocomplete.d.ts.map +1 -1
  57. package/dist/types/Form/Autocomplete/Elements/Autocomplete.d.ts +10 -0
  58. package/dist/types/Form/Autocomplete/Elements/Autocomplete.d.ts.map +1 -1
  59. package/dist/types/Form/OneTimeCode/OneTimeCode.d.ts +12 -0
  60. package/dist/types/Form/OneTimeCode/OneTimeCode.d.ts.map +1 -0
  61. package/dist/types/Utility/Hooks/useIsomorphicLayoutEffect.d.ts +4 -0
  62. package/dist/types/Utility/Hooks/useIsomorphicLayoutEffect.d.ts.map +1 -0
  63. package/dist/types/enums.d.ts +4 -0
  64. package/dist/types/enums.d.ts.map +1 -1
  65. package/dist/types/react.d.ts +1 -0
  66. package/dist/types/react.d.ts.map +1 -1
  67. package/package.json +3 -3
  68. package/src/Atom/Button/__snapshots__/Button.stories.ts.snap +2 -3
  69. package/src/Atom/Button/_buttons.css +16 -0
  70. package/src/Atom/Button/button.twig +4 -1
  71. package/src/Atom/DefinitionList/DefinitionList.stories.ts +3 -0
  72. package/src/Atom/Icon/_icon.css +3 -0
  73. package/src/Atom/Link/Link.tsx +16 -3
  74. package/src/Atom/Link/__snapshots__/Link.stories.ts.snap +7 -12
  75. package/src/Atom/Link/__snapshots__/Link.stories.tsx.snap +1 -1
  76. package/src/Atom/Link/_links.css +9 -0
  77. package/src/Atom/Link/link.twig +8 -2
  78. package/src/Atom/LinkedImage/linked-image.twig +1 -2
  79. package/src/Component/Accordion/Accordion.stories.ts +31 -0
  80. package/src/Component/Accordion/Accordion.tsx +3 -2
  81. package/src/Component/Accordion/AccordionItem.stories.ts +27 -0
  82. package/src/Component/Accordion/__snapshots__/Accordion.stories.ts.snap +36 -0
  83. package/src/Component/Accordion/__snapshots__/AccordionItem.stories.ts.snap +16 -0
  84. package/src/Component/Breadcrumb/__snapshots__/Breadcrumb.stories.ts.snap +3 -6
  85. package/src/Component/Breadcrumb/breadcrumb.css +4 -0
  86. package/src/Component/Card/__snapshots__/Card.stories.ts.snap +11 -22
  87. package/src/Component/Card/card.css +2 -2
  88. package/src/Component/Carousel/Carousel.stories.ts +55 -0
  89. package/src/Component/Carousel/Carousel.stories.tsx +74 -0
  90. package/src/Component/Carousel/__snapshots__/Carousel.stories.ts.snap +273 -88
  91. package/src/Component/Carousel/__snapshots__/Carousel.stories.tsx.snap +5 -3
  92. package/src/Component/ContentBlock/__snapshots__/ContentBlock.stories.ts.snap +12 -24
  93. package/src/Component/CookieConsent/cookie-consent.css +3 -1
  94. package/src/Component/Dialog/Dialog.stories.ts +33 -0
  95. package/src/Component/Dialog/Dialog.stories.tsx +25 -0
  96. package/src/Component/Dialog/Dialog.tsx +2 -2
  97. package/src/Component/Dialog/__snapshots__/Dialog.stories.ts.snap +33 -2
  98. package/src/Component/Dialog/__snapshots__/Dialog.stories.tsx.snap +2 -0
  99. package/src/Component/DropMenu/DropMenu.stories.ts +39 -1
  100. package/src/Component/DropMenu/DropMenu.stories.tsx +29 -0
  101. package/src/Component/DropMenu/DropMenu.tsx +1 -1
  102. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.ts.snap +14 -0
  103. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.tsx.snap +3 -1
  104. package/src/Component/Filters/Elements/Filters.ts +71 -1
  105. package/src/Component/Filters/FilterItem.stories.ts +2 -1
  106. package/src/Component/Filters/FilterItem.tsx +1 -1
  107. package/src/Component/Filters/Filters.figma.ts +1 -1
  108. package/src/Component/Filters/Filters.stories.ts +95 -3
  109. package/src/Component/Filters/Filters.stories.tsx +246 -17
  110. package/src/Component/Filters/Filters.tsx +145 -29
  111. package/src/Component/Filters/README.md +102 -37
  112. package/src/Component/Filters/__snapshots__/FilterItem.stories.tsx.snap +3 -3
  113. package/src/Component/Filters/__snapshots__/Filters.stories.ts.snap +82 -4
  114. package/src/Component/Filters/__snapshots__/Filters.stories.tsx.snap +9 -3
  115. package/src/Component/Filters/filters.css +11 -0
  116. package/src/Component/Gallery/Gallery.tsx +2 -2
  117. package/src/Component/HeroBanner/HeroBanner.stories.ts +1 -1
  118. package/src/Component/HeroBanner/__snapshots__/HeroBanner.stories.ts.snap +4 -8
  119. package/src/Component/HeroSearch/__snapshots__/HeroSearch.stories.ts.snap +5 -10
  120. package/src/Component/InPageAlert/__snapshots__/InPageAlert.stories.ts.snap +6 -12
  121. package/src/Component/InPageNavigation/InPageNavigation.stories.ts +50 -0
  122. package/src/Component/InPageNavigation/InPageNavigation.stories.tsx +78 -0
  123. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +35 -1
  124. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.tsx.snap +2 -0
  125. package/src/Component/LinkList/__snapshots__/LinkList.stories.ts.snap +6 -12
  126. package/src/Component/ListItem/__snapshots__/ListItem.stories.ts.snap +7 -14
  127. package/src/Component/ListItem/list-item.css +1 -1
  128. package/src/Component/Navigation/Dropdown.stories.tsx +46 -0
  129. package/src/Component/Navigation/Navigation.stories.ts +56 -0
  130. package/src/Component/Navigation/__snapshots__/Dropdown.stories.tsx.snap +2 -0
  131. package/src/Component/Navigation/__snapshots__/Navigation.stories.ts.snap +57 -0
  132. package/src/Component/Pagination/Components/PaginationContext.tsx +2 -1
  133. package/src/Component/Pagination/Components/PaginationItem.tsx +1 -1
  134. package/src/Component/Pagination/Pagination.stories.ts +36 -0
  135. package/src/Component/Pagination/Pagination.stories.tsx +31 -0
  136. package/src/Component/Pagination/__snapshots__/Pagination.stories.ts.snap +69 -12
  137. package/src/Component/Pagination/__snapshots__/Pagination.stories.tsx.snap +3 -1
  138. package/src/Component/Pagination/pagination-item.twig +1 -1
  139. package/src/Component/Popover/__snapshots__/Popover.stories.ts.snap +13 -26
  140. package/src/Component/Popover/popover.css +1 -1
  141. package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.ts.snap +1 -2
  142. package/src/Component/SocialLinks/__snapshots__/SocialLinks.stories.ts.snap +6 -9
  143. package/src/Component/Steps/Steps.stories.ts +1 -1
  144. package/src/Component/Steps/steps.css +13 -0
  145. package/src/Component/Tabs/Tabs.stories.ts +22 -0
  146. package/src/Component/Tabs/Tabs.stories.tsx +24 -0
  147. package/src/Component/Tabs/Tabs.tsx +2 -2
  148. package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +18 -0
  149. package/src/Component/Tabs/__snapshots__/Tabs.stories.tsx.snap +3 -1
  150. package/src/Component/Tabs/tabs.css +2 -0
  151. package/src/Form/Autocomplete/Autocomplete.stories.ts +81 -4
  152. package/src/Form/Autocomplete/Autocomplete.stories.tsx +85 -1
  153. package/src/Form/Autocomplete/Autocomplete.tsx +19 -6
  154. package/src/Form/Autocomplete/Elements/Autocomplete.ts +24 -1
  155. package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.ts.snap +38 -4
  156. package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.tsx.snap +8 -4
  157. package/src/Form/Autocomplete/autocomplete.css +12 -1
  158. package/src/Form/Autocomplete/autocomplete.twig +1 -1
  159. package/src/Form/OneTimeCode/OneTimeCode.stories.ts +20 -0
  160. package/src/Form/OneTimeCode/OneTimeCode.stories.tsx +18 -0
  161. package/src/Form/OneTimeCode/OneTimeCode.tsx +56 -0
  162. package/src/Form/OneTimeCode/README.md +83 -0
  163. package/src/Form/OneTimeCode/__snapshots__/OneTimeCode.stories.ts.snap +7 -0
  164. package/src/Form/OneTimeCode/__snapshots__/OneTimeCode.stories.tsx.snap +3 -0
  165. package/src/Form/OneTimeCode/one-time-code.css +13 -0
  166. package/src/Form/OneTimeCode/one-time-code.twig +31 -0
  167. package/src/Form/__snapshots__/Validation.stories.ts.snap +1 -1
  168. package/src/Form/form.css +90 -32
  169. package/src/Layout/Footer/__snapshots__/Footer.stories.ts.snap +18 -33
  170. package/src/Layout/Header/Header.stories.ts +33 -0
  171. package/src/Layout/Header/Header.stories.tsx +31 -0
  172. package/src/Layout/Header/__snapshots__/Header.stories.ts.snap +50 -0
  173. package/src/Layout/Header/__snapshots__/Header.stories.tsx.snap +2 -0
  174. package/src/Layout/Masthead/__snapshots__/Masthead.stories.ts.snap +8 -16
  175. package/src/Layout/Section/__snapshots__/Background.stories.ts.snap +2 -4
  176. package/src/Layout/Section/__snapshots__/Section.stories.ts.snap +4 -8
  177. package/src/Utility/Hooks/useIsomorphicLayoutEffect.ts +8 -0
  178. package/src/Utility/_mixins.css +24 -0
  179. package/src/Utility/print.css +1 -1
  180. package/src/Utility/utilities.css +20 -0
  181. package/src/_custom-media.css +1 -0
  182. package/src/enums.ts +5 -0
  183. package/src/react.ts +1 -0
  184. package/tools/test-utils/screen-reader.ts +33 -0
  185. package/dist/build/chunks/Gallery-BT3cxS5m.js +0 -1
  186. package/dist/build/chunks/GlobalToggle-DQHho97V.js +0 -1
  187. package/dist/build/chunks/Navigation-B9Wug-ok.js +0 -1
  188. package/dist/build/chunks/disclosure-widget-BETzOHLO.js +0 -1
  189. 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
- background-image: url("./images/checkmark.svg");
214
- background-size: 0.75rem;
215
- background-position: center;
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
- &::after {
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
- &::after {
263
- content: "";
264
- display: block;
265
- inline-size: 0.5rem;
266
- block-size: 0.5rem;
267
- margin: auto;
268
- border-radius: 50%;
269
- background-color: var(--radio-colour, var(--colour-primary));
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
- &::after {
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
- <span>Accessibility</span>
116
-
112
+ Accessibility
117
113
  </a>
118
114
 
119
115
  <a href="#">
120
116
 
121
- <span>Terms &amp; Conditions</span>
122
-
117
+ Terms &amp; 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
- <span>Accessibility</span>
173
-
167
+ Accessibility
174
168
  </a>
175
169
 
176
170
  <a href="#">
177
171
 
178
- <span>Terms &amp; Conditions</span>
179
-
172
+ Terms &amp; 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
- <span>Accessibility</span>
230
-
222
+ Accessibility
231
223
  </a>
232
224
 
233
225
  <a href="#">
234
226
 
235
- <span>Terms &amp; Conditions</span>
236
-
227
+ Terms &amp; 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
- <span>Accessibility</span>
287
-
277
+ Accessibility
288
278
  </a>
289
279
 
290
280
  <a href="#">
291
281
 
292
- <span>Terms &amp; Conditions</span>
293
-
282
+ Terms &amp; 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
- <span>Accessibility</span>
317
-
305
+ Accessibility
318
306
  </a>
319
307
 
320
308
  <a href="#">
321
309
 
322
- <span>Terms &amp; Conditions</span>
323
-
310
+ Terms &amp; 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
- <span>Accessibility</span>
375
-
361
+ Accessibility
376
362
  </a>
377
363
 
378
364
  <a href="#">
379
365
 
380
- <span>Terms &amp; Conditions</span>
381
-
366
+ Terms &amp; 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",