@pnx-mixtape/mxds 0.1.0-rc.1 → 0.1.0-rc.2

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 (198) hide show
  1. package/.storybook/decorators.ts +15 -0
  2. package/.storybook/preview.ts +4 -1
  3. package/.storybook/themes/campus-vibrant.css +181 -0
  4. package/.storybook/themes/civic-formal.css +246 -0
  5. package/.storybook/{theme-demo.css → themes/demo.css} +14 -7
  6. package/.storybook/themes/health-services.css +255 -0
  7. package/.storybook/themes/research-faculty.css +169 -0
  8. package/dist/build/accordion.css +1 -1
  9. package/dist/build/base.css +1 -1
  10. package/dist/build/button.css +1 -1
  11. package/dist/build/chunks/DropMenu-1baQc8Yc.js +1 -0
  12. package/dist/build/chunks/InPageNavigation-CVieEl9u.js +1 -0
  13. package/dist/build/chunks/Popover-Cxg-s5Jn.js +1 -0
  14. package/dist/build/chunks/Sticky-cWUVSOXN.js +1 -0
  15. package/dist/build/chunks/{Tabs-Dm8g301e.js → Tabs-DA_Ez8pv.js} +2 -2
  16. package/dist/build/chunks/polyfills-C7pRt54M.js +1 -0
  17. package/dist/build/constants.css +1 -1
  18. package/dist/build/drop-menu.entry.js +1 -1
  19. package/dist/build/elements.js +1 -1
  20. package/dist/build/form.css +1 -1
  21. package/dist/build/grid.css +1 -1
  22. package/dist/build/in-page-alert.css +1 -1
  23. package/dist/build/in-page-navigation.entry.js +1 -1
  24. package/dist/build/navigation.css +1 -1
  25. package/dist/build/popover.entry.js +1 -1
  26. package/dist/build/print.css +1 -1
  27. package/dist/build/progress-indicator.css +1 -0
  28. package/dist/build/react.js +1 -1
  29. package/dist/build/sidebar.css +1 -1
  30. package/dist/build/sticky.entry.js +1 -1
  31. package/dist/build/tabs.entry.js +1 -1
  32. package/dist/build/tag.css +1 -1
  33. package/dist/build/utilities.css +1 -1
  34. package/dist/types/Atom/DefinitionList/DefinitionList.d.ts +3 -2
  35. package/dist/types/Atom/DefinitionList/DefinitionList.d.ts.map +1 -1
  36. package/dist/types/Atom/Heading/Heading.d.ts.map +1 -1
  37. package/dist/types/Atom/List/List.d.ts +10 -0
  38. package/dist/types/Atom/List/List.d.ts.map +1 -0
  39. package/dist/types/Component/DropMenu/Elements/DropMenu.d.ts.map +1 -1
  40. package/dist/types/Component/InPageNavigation/Elements/InPageNavigation.d.ts +2 -2
  41. package/dist/types/Component/InPageNavigation/Elements/InPageNavigation.d.ts.map +1 -1
  42. package/dist/types/Component/InPageNavigation/Hooks/useInPageNavigation.d.ts.map +1 -1
  43. package/dist/types/Component/ProgressIndicator/ProgressIndicator.d.ts +9 -0
  44. package/dist/types/Component/ProgressIndicator/ProgressIndicator.d.ts.map +1 -0
  45. package/dist/types/Component/Tabs/Elements/Tabs.d.ts.map +1 -1
  46. package/dist/types/Component/Tag/Tag.d.ts +4 -2
  47. package/dist/types/Component/Tag/Tag.d.ts.map +1 -1
  48. package/dist/types/Form/Autocomplete/Autocomplete.d.ts.map +1 -1
  49. package/dist/types/Form/Checkbox/FormCheckbox.d.ts.map +1 -1
  50. package/dist/types/Form/CheckboxCard/CheckboxCard.d.ts +23 -0
  51. package/dist/types/Form/CheckboxCard/CheckboxCard.d.ts.map +1 -0
  52. package/dist/types/Form/Radio/FormRadio.d.ts.map +1 -1
  53. package/dist/types/Form/RadioCard/RadioCard.d.ts +16 -0
  54. package/dist/types/Form/RadioCard/RadioCard.d.ts.map +1 -0
  55. package/dist/types/Form/Select/FormSelect.d.ts.map +1 -1
  56. package/dist/types/Form/TextInput/FormText.d.ts.map +1 -1
  57. package/dist/types/Form/Textarea/FormTextarea.d.ts.map +1 -1
  58. package/dist/types/enums.d.ts +15 -0
  59. package/dist/types/enums.d.ts.map +1 -1
  60. package/dist/types/react.d.ts +4 -0
  61. package/dist/types/react.d.ts.map +1 -1
  62. package/package.json +7 -7
  63. package/src/Atom/Background/_background.css +2 -2
  64. package/src/Atom/Blockquote/_blockquote.css +9 -2
  65. package/src/Atom/Button/README.md +1 -0
  66. package/src/Atom/Button/_buttons-styles.css +6 -6
  67. package/src/Atom/Button/_buttons.css +5 -3
  68. package/src/Atom/DefinitionList/DefinitionList.stories.ts +39 -2
  69. package/src/Atom/DefinitionList/DefinitionList.stories.tsx +36 -0
  70. package/src/Atom/DefinitionList/DefinitionList.tsx +4 -1
  71. package/src/Atom/DefinitionList/DefinitionListItem.stories.ts +1 -1
  72. package/src/Atom/DefinitionList/README.md +43 -0
  73. package/src/Atom/DefinitionList/__snapshots__/DefinitionList.stories.ts.snap +37 -3
  74. package/src/Atom/DefinitionList/__snapshots__/DefinitionList.stories.tsx.snap +2 -0
  75. package/src/Atom/DefinitionList/__snapshots__/DefinitionListItem.stories.ts.snap +3 -3
  76. package/src/Atom/DefinitionList/definition-list-item.twig +1 -0
  77. package/src/Atom/DefinitionList/definition-list.twig +6 -1
  78. package/src/Atom/Heading/Heading.tsx +6 -0
  79. package/src/Atom/Heading/_headings.css +2 -1
  80. package/src/Atom/Icon/_extended-set.css +30 -30
  81. package/src/Atom/Icon/_icon.css +36 -36
  82. package/src/Atom/Image/README.md +2 -2
  83. package/src/Atom/Image/_image.css +12 -6
  84. package/src/Atom/List/List.stories.ts +102 -0
  85. package/src/Atom/List/List.stories.tsx +103 -0
  86. package/src/Atom/List/List.tsx +22 -0
  87. package/src/Atom/List/README.md +150 -0
  88. package/src/Atom/List/__snapshots__/List.stories.ts.snap +43 -0
  89. package/src/Atom/List/__snapshots__/List.stories.tsx.snap +13 -0
  90. package/src/Atom/List/_list.css +123 -0
  91. package/src/Atom/List/list.twig +5 -0
  92. package/src/Atom/Media/Media.stories.ts +1 -1
  93. package/src/Atom/Media/__snapshots__/Media.stories.ts.snap +2 -2
  94. package/src/Atom/Spacing/_spacing.css +24 -24
  95. package/src/Atom/Table/__snapshots__/Table.stories.ts.snap +24 -4
  96. package/src/Atom/Table/_table.css +15 -3
  97. package/src/Atom/Table/table.twig +6 -1
  98. package/src/Atom/Text/__snapshots__/Text.stories.ts.snap +3 -2
  99. package/src/Atom/Text/_text-styles.css +21 -1
  100. package/src/Atom/Text/twig/text-styles-example.twig +3 -2
  101. package/src/Atom/_base.css +6 -0
  102. package/src/Atom/_flow.css +1 -1
  103. package/src/Atom/_generic.css +18 -11
  104. package/src/Component/Accordion/accordion.css +2 -0
  105. package/src/Component/Card/__snapshots__/Card.stories.ts.snap +2 -2
  106. package/src/Component/Card/card.twig +1 -1
  107. package/src/Component/Carousel/Carousel.stories.ts +46 -23
  108. package/src/Component/DropMenu/Elements/DropMenu.ts +10 -1
  109. package/src/Component/InPageAlert/in-page-alert.css +2 -1
  110. package/src/Component/InPageNavigation/Elements/InPageNavigation.ts +10 -5
  111. package/src/Component/InPageNavigation/Hooks/useInPageNavigation.ts +10 -5
  112. package/src/Component/InPageNavigation/InPageNavigation.stories.ts +33 -0
  113. package/src/Component/InPageNavigation/InPageNavigation.stories.tsx +52 -0
  114. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +26 -0
  115. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.tsx.snap +2 -0
  116. package/src/Component/InPageNavigation/twig/content-example-duplicate-headings.twig +20 -0
  117. package/src/Component/InPageNavigation/twig/in-page-navigation-story-duplicate-headings.twig +2 -0
  118. package/src/Component/ListItem/__snapshots__/ListItem.stories.ts.snap +4 -4
  119. package/src/Component/ListItem/list-item.twig +2 -2
  120. package/src/Component/Navigation/_navigation.css +1 -1
  121. package/src/Component/Pagination/Pagination.stories.ts +8 -4
  122. package/src/Component/Pagination/pagination.css +2 -2
  123. package/src/Component/ProgressIndicator/ProgressIndicator.stories.ts +36 -0
  124. package/src/Component/ProgressIndicator/ProgressIndicator.stories.tsx +30 -0
  125. package/src/Component/ProgressIndicator/ProgressIndicator.tsx +60 -0
  126. package/src/Component/ProgressIndicator/README.md +111 -0
  127. package/src/Component/ProgressIndicator/__snapshots__/ProgressIndicator.stories.ts.snap +67 -0
  128. package/src/Component/ProgressIndicator/__snapshots__/ProgressIndicator.stories.tsx.snap +7 -0
  129. package/src/Component/ProgressIndicator/progress-indicator.css +59 -0
  130. package/src/Component/ProgressIndicator/progress-indicator.twig +22 -0
  131. package/src/Component/Sticky/Elements/Sticky.ts +1 -1
  132. package/src/Component/Sticky/sticky.css +3 -3
  133. package/src/Component/Tabs/Elements/Tabs.ts +3 -1
  134. package/src/Component/Tabs/Tabs.stories.ts +55 -21
  135. package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +2 -2
  136. package/src/Component/Tag/README.md +52 -26
  137. package/src/Component/Tag/Tag.stories.tsx +24 -0
  138. package/src/Component/Tag/Tag.tsx +12 -2
  139. package/src/Component/Tag/TagItem.stories.ts +25 -0
  140. package/src/Component/Tag/__snapshots__/Tag.stories.ts.snap +2 -2
  141. package/src/Component/Tag/__snapshots__/Tag.stories.tsx.snap +2 -0
  142. package/src/Component/Tag/tag-item.twig +2 -2
  143. package/src/Component/Tag/tag.css +76 -2
  144. package/src/Form/Autocomplete/Autocomplete.stories.ts +8 -3
  145. package/src/Form/Autocomplete/Autocomplete.tsx +1 -4
  146. package/src/Form/Autocomplete/README.md +26 -26
  147. package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.ts.snap +1 -1
  148. package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.tsx.snap +1 -1
  149. package/src/Form/Autocomplete/autocomplete.twig +0 -2
  150. package/src/Form/Checkbox/FormCheckbox.tsx +2 -8
  151. package/src/Form/Checkbox/README.md +23 -18
  152. package/src/Form/Checkbox/input-checkbox.twig +0 -2
  153. package/src/Form/CheckboxCard/CheckboxCard.stories.ts +90 -0
  154. package/src/Form/CheckboxCard/CheckboxCard.stories.tsx +43 -0
  155. package/src/Form/CheckboxCard/CheckboxCard.tsx +102 -0
  156. package/src/Form/CheckboxCard/README.md +163 -0
  157. package/src/Form/CheckboxCard/__snapshots__/CheckboxCard.stories.ts.snap +63 -0
  158. package/src/Form/CheckboxCard/__snapshots__/CheckboxCard.stories.tsx.snap +7 -0
  159. package/src/Form/CheckboxCard/checkbox-card.twig +29 -0
  160. package/src/Form/FormItem/__snapshots__/FormItem.stories.ts.snap +2 -2
  161. package/src/Form/README.md +65 -13
  162. package/src/Form/Radio/FormRadio.tsx +1 -4
  163. package/src/Form/Radio/input-radio.twig +0 -2
  164. package/src/Form/RadioCard/README.md +146 -0
  165. package/src/Form/RadioCard/RadioCard.stories.ts +95 -0
  166. package/src/Form/RadioCard/RadioCard.stories.tsx +26 -0
  167. package/src/Form/RadioCard/RadioCard.tsx +59 -0
  168. package/src/Form/RadioCard/__snapshots__/RadioCard.stories.ts.snap +69 -0
  169. package/src/Form/RadioCard/__snapshots__/RadioCard.stories.tsx.snap +3 -0
  170. package/src/Form/RadioCard/radio-card.twig +20 -0
  171. package/src/Form/Select/FormSelect.tsx +1 -4
  172. package/src/Form/Select/input-select.twig +0 -2
  173. package/src/Form/TextInput/FormText.stories.tsx +14 -0
  174. package/src/Form/TextInput/FormText.tsx +1 -4
  175. package/src/Form/TextInput/__snapshots__/FormText.stories.tsx.snap +2 -0
  176. package/src/Form/TextInput/input-text.twig +0 -2
  177. package/src/Form/Textarea/FormTextarea.tsx +1 -4
  178. package/src/Form/Textarea/input-textarea.twig +0 -2
  179. package/src/Form/Validation.stories.ts +153 -0
  180. package/src/Form/__snapshots__/Validation.stories.ts.snap +130 -0
  181. package/src/Form/form.css +155 -28
  182. package/src/Layout/Grid/grid.css +4 -4
  183. package/src/Layout/Sidebar/__snapshots__/Sidebar.stories.ts.snap +5 -5
  184. package/src/Layout/Sidebar/sidebar.css +1 -0
  185. package/src/Layout/Sidebar/sidebar.twig +1 -1
  186. package/src/Utility/_layout-utils.css +8 -0
  187. package/src/Utility/print.css +50 -28
  188. package/src/Utility/utilities.css +2 -2
  189. package/src/constants.css +12 -2
  190. package/src/enums.ts +17 -0
  191. package/src/react.ts +4 -0
  192. package/tools/lib/extract-classnames.mjs +0 -0
  193. package/tools/lint-classnames.mjs +1 -1
  194. package/dist/build/chunks/DropMenu-Co9jZZ4S.js +0 -1
  195. package/dist/build/chunks/InPageNavigation-CzR_KZaz.js +0 -1
  196. package/dist/build/chunks/Popover-De_CQqcd.js +0 -1
  197. package/dist/build/chunks/Sticky-Kyjykb36.js +0 -1
  198. package/dist/build/chunks/polyfills-Dokbkxxk.js +0 -1
@@ -6,13 +6,13 @@ exports[`Checkbox 1`] = `
6
6
  <li>
7
7
  <div class="mx-tag">
8
8
  <input type="checkbox" id="music" name="tag" value="music">
9
- <label for="music">Music</label>
9
+ <label class="mx-icon mx-icon--tick" for="music">Music</label>
10
10
  </div>
11
11
  </li>
12
12
  <li>
13
13
  <div class="mx-tag">
14
14
  <input type="checkbox" id="news" name="tag" value="news">
15
- <label for="news">News</label>
15
+ <label class="mx-icon mx-icon--tick" for="news">News</label>
16
16
  </div>
17
17
  </li>
18
18
 
@@ -1,3 +1,5 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`Tag 1`] = `"<div class="mx-tag">Tag name</div>"`;
4
+
5
+ exports[`Variants 1`] = `"<div style="display: flex; gap: 1rem;"><div class="mx-tag mx-tag--info">info</div><div class="mx-tag mx-tag--success">success</div><div class="mx-tag mx-tag--warning">warning</div><div class="mx-tag mx-tag--error">error</div><div class="mx-tag mx-tag--featured">featured</div></div>"`;
@@ -1,4 +1,4 @@
1
- {% set tagClasses = ['mx-tag', dismissible ? 'mx-tag--dismissible' : null] %}
1
+ {% set tagClasses = ['mx-tag', dismissible ? 'mx-tag--dismissible' : null, variant ? 'mx-tag--' ~ variant : null] %}
2
2
  {% set tagAttributes = (tagAttributes ?? create_attribute()).addClass(tagClasses) %}
3
3
  <li>
4
4
  {% if type == "link" %}
@@ -10,7 +10,7 @@
10
10
  name="{{ item.name }}"
11
11
  value="{{ item.value }}"
12
12
  {% if item.checked %} checked{% endif %}>
13
- <label for="{{ item.id }}">{{ item.label }}</label>
13
+ <label class="mx-icon mx-icon--tick" for="{{ item.id }}">{{ item.label }}</label>
14
14
  </div>
15
15
  {% else %}
16
16
  <span{{ tagAttributes }}>
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  @layer design-system.components {
6
- /* @public --tag-background --tag-foreground --tag-font-size --tags-gap */
6
+ /* @public --tag-background --tag-foreground --tag-font-size --tags-gap --tag-letter-spacing */
7
7
  .mx-tag {
8
8
  display: inline-flex;
9
9
  inline-size: fit-content;
@@ -16,6 +16,7 @@
16
16
  line-height: var(--line-height, 2);
17
17
  border-radius: var(--border-radius);
18
18
  border: var(--line-width, 1px) solid var(--line-colour, currentcolor);
19
+ letter-spacing: var(--tag-letter-spacing, 0);
19
20
  text-decoration: none;
20
21
  overflow: clip;
21
22
  overflow-clip-margin: var(--spacing-xxxs);
@@ -36,12 +37,35 @@
36
37
  font-size: inherit;
37
38
  color: inherit;
38
39
  margin: 0;
40
+ gap: var(--spacing-xxxs);
39
41
  }
40
42
 
41
- &:is(a:hover, :has(input:checked)) {
43
+ /* the checkmark icon (from the shared mx-icon set) is always reserved so
44
+ checking the tag doesn't change its size — only its visibility toggles */
45
+ & label.mx-icon--tick::before {
46
+ --icon-size: 1em;
47
+
48
+ opacity: 0;
49
+ }
50
+
51
+ &:is(a:hover):not(
52
+ :is(.mx-tag--info, .mx-tag--success, .mx-tag--warning, .mx-tag--error, .mx-tag--featured)
53
+ ) {
42
54
  --tag-foreground: var(--link-colour, var(--colour-primary));
43
55
  }
44
56
 
57
+ &:has(input:checked):not(
58
+ :is(.mx-tag--info, .mx-tag--success, .mx-tag--warning, .mx-tag--error, .mx-tag--featured)
59
+ ) {
60
+ --tag-background: var(--link-colour, var(--colour-primary));
61
+ --tag-foreground: var(--colour-foreground-reverse);
62
+ --line-colour: var(--tag-background);
63
+ }
64
+
65
+ &:has(input:checked) label.mx-icon--tick::before {
66
+ opacity: 1;
67
+ }
68
+
45
69
  &:empty {
46
70
  display: none;
47
71
  }
@@ -76,6 +100,56 @@
76
100
  }
77
101
  }
78
102
 
103
+ .mx-tag--info,
104
+ .mx-tag--success,
105
+ .mx-tag--warning,
106
+ .mx-tag--error,
107
+ .mx-tag--featured {
108
+ --line-colour: var(--tag-background);
109
+ --outline-color: var(--tag-background);
110
+ }
111
+
112
+ .mx-tag--info {
113
+ --tag-accent: var(--colour-info);
114
+ --tag-background: var(--tag-accent);
115
+ --tag-foreground: var(--colour-foreground-reverse);
116
+ }
117
+
118
+ .mx-tag--success {
119
+ --tag-accent: var(--colour-success);
120
+ --tag-background: var(--tag-accent);
121
+ --tag-foreground: var(--colour-foreground-reverse);
122
+ }
123
+
124
+ .mx-tag--warning {
125
+ --tag-accent: var(--colour-warning);
126
+ --tag-background: var(--tag-accent);
127
+ --tag-foreground: var(--colour-foreground-reverse);
128
+ }
129
+
130
+ .mx-tag--error {
131
+ --tag-accent: var(--colour-error);
132
+ --tag-background: var(--tag-accent);
133
+ --tag-foreground: var(--colour-foreground-reverse);
134
+ }
135
+
136
+ .mx-tag--featured {
137
+ --tag-accent: var(--colour-featured, var(--colour-accent));
138
+ --tag-background: var(--tag-accent);
139
+ --tag-foreground: var(--colour-featured-foreground, var(--colour-foreground));
140
+ }
141
+
142
+ .mx-tag:is(.mx-tag--info, .mx-tag--success, .mx-tag--warning, .mx-tag--error):is(a:hover) {
143
+ --tag-background: color-mix(in oklch, var(--tag-accent), black 20%);
144
+ --line-colour: var(--tag-background);
145
+ }
146
+
147
+ .mx-tag--featured:is(a:hover) {
148
+ /* featured keeps dark text, so a smaller darken step protects contrast */
149
+ --tag-background: color-mix(in oklch, var(--tag-accent), black 10%);
150
+ --line-colour: var(--tag-background);
151
+ }
152
+
79
153
  .mx-tags {
80
154
  padding-inline: 0;
81
155
  list-style: none;
@@ -185,9 +185,14 @@ export const Autocomplete: Story = {
185
185
  await userEvent.clear(input)
186
186
  await expect(input).toHaveValue("")
187
187
  await expect(input).toHaveAttribute("aria-expanded", "false")
188
- await waitFor(() => expect(canvas.queryByRole("option")).not.toBeInTheDocument(), {
189
- timeout: 2000,
190
- })
188
+ // `hidden: true` includes the listbox even while it's closed (`hidden`
189
+ // attribute set) - otherwise this resolves immediately regardless of
190
+ // whether the stale options were actually removed, since closed
191
+ // elements are excluded from accessibility queries by default.
192
+ await waitFor(
193
+ () => expect(canvas.queryByRole("option", { hidden: true })).not.toBeInTheDocument(),
194
+ { timeout: 2000 },
195
+ )
191
196
  // Clear focus.
192
197
  await userEvent.click(canvasElement)
193
198
  })
@@ -207,10 +207,7 @@ const Autocomplete = ({
207
207
  id={id}
208
208
  type={type}
209
209
  role="combobox"
210
- className={classNames("mx-input__text", {
211
- error: status === FormStatus.INVALID,
212
- success: status === FormStatus.VALID,
213
- })}
210
+ className="mx-input__text"
214
211
  value={state.inputValue}
215
212
  autoComplete="off"
216
213
  aria-expanded={state.isOpen}
@@ -32,18 +32,18 @@ This is a field-level component only — pair it with `FormItem` and `FormLabel`
32
32
 
33
33
  **autocomplete.twig props:**
34
34
 
35
- | Prop | Type | Required | Default | Description |
36
- | ---------------- | ------------------------------------ | -------- | -------------------- | ---------------------------------------------------------------------------------------------------- |
37
- | `id` | `string` | Yes | — | `id` on the `<input>`; also used to derive the listbox/option ids |
38
- | `name` | `string` | No | — | `name` attribute on the `<input>` |
39
- | `label` | `string` | No | — | `aria-label` on the `<input>` (visually hidden) |
40
- | `type` | `"text" \| "search"` | No | `"text"` | `"search"` also renders an icon-only submit button |
41
- | `options` | `{ label: string, value: string }[]` | No | — | The **current** suggestions to render as `<li role="option">` — not a full list to filter, see below |
42
- | `value` | `string` | No | — | Initial `<input>` value |
43
- | `placeholder` | `string` | No | — | `<input>` placeholder |
44
- | `required` | `boolean` | No | `false` | Adds `required` to the `<input>` |
45
- | `status` | `"default" \| "valid" \| "invalid"` | No | `"default"` | Adds `.error`/`.success` and `aria-invalid` to the `<input>` |
46
- | `noResultsLabel` | `string` | No | `"No results found"` | Message announced when there are zero current options and a query |
35
+ | Prop | Type | Required | Default | Description |
36
+ | ---------------- | ------------------------------------ | -------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
37
+ | `id` | `string` | Yes | — | `id` on the `<input>`; also used to derive the listbox/option ids |
38
+ | `name` | `string` | No | — | `name` attribute on the `<input>` |
39
+ | `label` | `string` | No | — | `aria-label` on the `<input>` (visually hidden) |
40
+ | `type` | `"text" \| "search"` | No | `"text"` | `"search"` also renders an icon-only submit button |
41
+ | `options` | `{ label: string, value: string }[]` | No | — | The **current** suggestions to render as `<li role="option">` — not a full list to filter, see below |
42
+ | `value` | `string` | No | — | Initial `<input>` value |
43
+ | `placeholder` | `string` | No | — | `<input>` placeholder |
44
+ | `required` | `boolean` | No | `false` | Adds `required` to the `<input>` |
45
+ | `status` | `"default" \| "valid" \| "invalid"` | No | `"default"` | Sets `aria-invalid` on the `<input>` (red border). Live `:user-*` colours need `mx-validate` on the `<input>` or the `.mx-autocomplete` wrapper |
46
+ | `noResultsLabel` | `string` | No | `"No results found"` | Message announced when there are zero current options and a query |
47
47
 
48
48
  Registers as `<mx-autocomplete>` — import the element to activate its behaviour:
49
49
 
@@ -140,19 +140,19 @@ const Example = () => {
140
140
 
141
141
  **`Autocomplete` props:**
142
142
 
143
- | Prop | Type | Default | Description |
144
- | ---------------- | -------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------- |
145
- | `id` | `string` | — | `id` on the `<input>`; also used to derive the listbox/option ids |
146
- | `label` | `string` | — | `aria-label` on the `<input>` (visually hidden) |
147
- | `type` | `"text" \| "search"` | `"text"` | `"search"` also renders an icon-only submit button (`onSubmit`) |
148
- | `options` | `AutocompleteOption[]` | — | The **current** results to display — not filtered further, see below |
149
- | `onSearch` | `(query: string) => void` | — | Called on every keystroke with the trimmed input value — debounce it yourself and feed matches back in via `options` |
150
- | `value` | `string` | `""` | Initial `<input>` value (uncontrolled after that) |
151
- | `status` | `FormStatus` | `FormStatus.DEFAULT` | Adds `.error`/`.success` and `aria-invalid` |
152
- | `noResultsLabel` | `string` | `"No results found"` | Message announced when `options` is empty and there's a query |
153
- | `onSelect` | `(option: AutocompleteOption) => void` | — | Called when an option is chosen by click or <kbd>Enter</kbd> |
154
- | `hideOnBlur` | `boolean` | `true` | Close the listbox on blur |
155
- | `className` | `string` | — | Extra class on the root |
143
+ | Prop | Type | Default | Description |
144
+ | ---------------- | -------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
145
+ | `id` | `string` | — | `id` on the `<input>`; also used to derive the listbox/option ids |
146
+ | `label` | `string` | — | `aria-label` on the `<input>` (visually hidden) |
147
+ | `type` | `"text" \| "search"` | `"text"` | `"search"` also renders an icon-only submit button (`onSubmit`) |
148
+ | `options` | `AutocompleteOption[]` | — | The **current** results to display — not filtered further, see below |
149
+ | `onSearch` | `(query: string) => void` | — | Called on every keystroke with the trimmed input value — debounce it yourself and feed matches back in via `options` |
150
+ | `value` | `string` | `""` | Initial `<input>` value (uncontrolled after that) |
151
+ | `status` | `FormStatus` | `FormStatus.DEFAULT` | Sets `aria-invalid` (red border). Live `:user-*` colours need `mx-validate` on the `.mx-autocomplete` wrapper — `className` lands there, not on the input |
152
+ | `noResultsLabel` | `string` | `"No results found"` | Message announced when `options` is empty and there's a query |
153
+ | `onSelect` | `(option: AutocompleteOption) => void` | — | Called when an option is chosen by click or <kbd>Enter</kbd> |
154
+ | `hideOnBlur` | `boolean` | `true` | Close the listbox on blur |
155
+ | `className` | `string` | — | Extra class on the root |
156
156
 
157
157
  Any other native `<input>` prop (e.g. `placeholder`, `required`, `disabled`) is passed through to the `<input>`.
158
158
 
@@ -165,7 +165,7 @@ Any other native `<input>` prop (e.g. `placeholder`, `required`, `disabled`) is
165
165
 
166
166
  ## Modifiers
167
167
 
168
- This component has no BEM modifier classes — its only visual states (`error`/`success`) come from the shared `.mx-input__text` classes documented in `Form/form.css`.
168
+ This component has no BEM modifier classes — its border states come from the shared validation rules in `Form/form.css`: `aria-invalid="true"` for explicit errors, and `:user-valid` / `:user-invalid` once `mx-validate` is added to the `<input>` (Twig, via `attributes`) or the `.mx-autocomplete` wrapper (React, via `className`).
169
169
 
170
170
  ---
171
171
 
@@ -19,7 +19,7 @@ exports[`Invalid 1`] = `
19
19
  "
20
20
  <mx-autocomplete class="mx-autocomplete">
21
21
  <div class="mx-autocomplete__control mx-form--inline-button">
22
- <input class="mx-input__text error" 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">
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">
25
25
 
@@ -2,7 +2,7 @@
2
2
 
3
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>"`;
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 error" 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"><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
7
  exports[`Required 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button"><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>"`;
8
8
 
@@ -1,7 +1,5 @@
1
1
  {% set classes = [
2
2
  'mx-input__text',
3
- status == "invalid" ? "error" : null,
4
- status == "valid" ? "success" : null,
5
3
  ] %}
6
4
  {% set attributes = (attributes ?? create_attribute()).addClass(classes) %}
7
5
  {% set status = status|default("default") %}
@@ -51,10 +51,7 @@ const Checkbox = ({
51
51
  const state = use(CheckboxContext)
52
52
  const { inputProps } = useCheckboxGroupItem(props, state, ref)
53
53
 
54
- const classes = classNames("mx-checkbox", className, {
55
- error: status === FormStatus.INVALID,
56
- success: status === FormStatus.VALID,
57
- })
54
+ const classes = classNames("mx-checkbox", className)
58
55
 
59
56
  return (
60
57
  <div className={classes}>
@@ -81,10 +78,7 @@ const SingleCheckbox = ({
81
78
  const state = useToggleState(props)
82
79
  const { inputProps } = useCheckbox(props, state, ref)
83
80
 
84
- const classes = classNames("mx-checkbox", className, {
85
- error: status === FormStatus.INVALID,
86
- success: status === FormStatus.VALID,
87
- })
81
+ const classes = classNames("mx-checkbox", className)
88
82
 
89
83
  return (
90
84
  <div className={classes}>
@@ -24,8 +24,8 @@ Accessible checkbox and radio group components powered by [react-aria](https://r
24
24
  checked: true,
25
25
  } %}
26
26
 
27
- {# Required checkbox with invalid status — adds the "error" class
28
- and aria-invalid, plus aria-describedby pointing at status-{id} #}
27
+ {# Required checkbox with invalid status — adds aria-invalid (which paints
28
+ the red border), plus aria-describedby pointing at status-{id} #}
29
29
  {% include '@mixtape/Form/Checkbox/input-checkbox.twig' with {
30
30
  id: 'terms',
31
31
  name: 'terms',
@@ -39,17 +39,17 @@ Accessible checkbox and radio group components powered by [react-aria](https://r
39
39
 
40
40
  **input-checkbox.twig props** (composed from `FormControl` in `ids-shape` — checkbox/radio share the same template):
41
41
 
42
- | Prop | Type | Required | Description |
43
- | ---------------- | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
44
- | `id` | `string` | Yes | Input id — also used to derive the `aria-describedby` ids (`help-{id}`, `status-{id}`) |
45
- | `name` | `string` | Yes | `name` attribute on the input |
46
- | `value` | `string` | Yes | `value` attribute on the input |
47
- | `label` | `string` | Yes | Label text rendered inside `<label for="{id}">` |
48
- | `checked` | `boolean` | No | Adds `checked="checked"` to the input |
49
- | `required` | `boolean` | No | Adds the `required` attribute |
50
- | `status` | `FormStatusTypes` | No | `invalid` adds the `error` class and `aria-invalid="true"`; `valid` adds the `success` class. Defaults to `default` |
51
- | `hasDescription` | `boolean` | No | When `true`, `aria-describedby` is wired up to `help-{id}` (and `status-{id}` when `status` is set) |
52
- | `attributes` | `Attribute` | No | Optional Drupal-style attributes object — `mx-checkbox` (and the status class) is always merged on top |
42
+ | Prop | Type | Required | Description |
43
+ | ---------------- | ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `id` | `string` | Yes | Input id — also used to derive the `aria-describedby` ids (`help-{id}`, `status-{id}`) |
45
+ | `name` | `string` | Yes | `name` attribute on the input |
46
+ | `value` | `string` | Yes | `value` attribute on the input |
47
+ | `label` | `string` | Yes | Label text rendered inside `<label for="{id}">` |
48
+ | `checked` | `boolean` | No | Adds `checked="checked"` to the input |
49
+ | `required` | `boolean` | No | Adds the `required` attribute |
50
+ | `status` | `FormStatusTypes` | No | `invalid` adds `aria-invalid="true"`, which paints the red border. Live `:user-*` colours need `mx-validate` on the wrapper. Defaults to `default` |
51
+ | `hasDescription` | `boolean` | No | When `true`, `aria-describedby` is wired up to `help-{id}` (and `status-{id}` when `status` is set) |
52
+ | `attributes` | `Attribute` | No | Optional Drupal-style attributes object — `mx-checkbox` is always merged on top |
53
53
 
54
54
  The template renders `type="checkbox"` directly — for radio inputs use a sibling template (or hand-author the markup with `mx-radio` on the wrapper and `type="radio"` on the input).
55
55
 
@@ -164,11 +164,16 @@ Each input auto-generates its own `id` via `useId()` and wires its `<label htmlF
164
164
 
165
165
  ## Status states
166
166
 
167
- | `status` | Class on wrapper | Effect |
168
- | -------------------- | ---------------- | ------------------------- |
169
- | `FormStatus.DEFAULT` | — | Normal border |
170
- | `FormStatus.INVALID` | `error` | Red border on the input |
171
- | `FormStatus.VALID` | `success` | Green border on the input |
167
+ | `status` | Attribute on input | Effect |
168
+ | -------------------- | ---------------------- | ------------------------------------------ |
169
+ | `FormStatus.DEFAULT` | `aria-invalid="false"` | Normal border |
170
+ | `FormStatus.INVALID` | `aria-invalid="true"` | Red border on the input |
171
+ | `FormStatus.VALID` | — | No border change — see `mx-validate` below |
172
+
173
+ Add `mx-validate` to the `.mx-checkbox` wrapper to opt into live
174
+ `:user-valid` / `:user-invalid` border colours as the user interacts. Without
175
+ it, only an explicit `INVALID` status colours the border. See
176
+ [Form/README.md](../README.md#validation-state).
172
177
 
173
178
  ## Touch target sizes
174
179
 
@@ -1,7 +1,5 @@
1
1
  {% set classes = [
2
2
  'mx-checkbox',
3
- status == "invalid" ? "error" : null,
4
- status == "valid" ? "success" : null,
5
3
  ] %}
6
4
  {% set attributes = (attributes ?? create_attribute()).addClass(classes) %}
7
5
  {% set status = status|default("default") %}
@@ -0,0 +1,90 @@
1
+ import { Meta, StoryObj } from "@storybook/html-vite"
2
+ import Component from "./checkbox-card.twig"
3
+ import "../form.css"
4
+
5
+ type CheckboxCardProps = {
6
+ id: string
7
+ name: string
8
+ value: string
9
+ label: string
10
+ hint?: string
11
+ checked?: boolean
12
+ required?: boolean
13
+ status?: "default" | "invalid" | "valid"
14
+ }
15
+
16
+ const meta: Meta<CheckboxCardProps> = {
17
+ tags: ["autodocs"],
18
+ component: Component,
19
+ args: {
20
+ id: "pay-carer",
21
+ name: "payments",
22
+ value: "Carer Payment or Allowance",
23
+ label: "Carer Payment or Carer Allowance",
24
+ },
25
+ parameters: {
26
+ a11y: {
27
+ config: {
28
+ rules: [
29
+ {
30
+ // ID's are not unique due to test-runner.
31
+ id: "label",
32
+ enabled: false,
33
+ },
34
+ ],
35
+ },
36
+ },
37
+ },
38
+ }
39
+
40
+ export default meta
41
+ type Story = StoryObj<CheckboxCardProps>
42
+
43
+ export const CheckboxCard: Story = {}
44
+
45
+ export const WithHint: Story = {
46
+ args: {
47
+ id: "pay-other",
48
+ value: "Another income support payment",
49
+ label: "Another income support payment",
50
+ hint: "You will be asked which one before you submit",
51
+ },
52
+ }
53
+
54
+ export const Checked: Story = {
55
+ args: {
56
+ checked: true,
57
+ },
58
+ }
59
+
60
+ export const Invalid: Story = {
61
+ args: {
62
+ status: "invalid",
63
+ },
64
+ }
65
+
66
+ /**
67
+ * `.mx-choice-divider` splits a set of card choices into two groups — e.g.
68
+ * "pick one of these — or — pick this other option". Plain markup, not a
69
+ * component prop.
70
+ */
71
+ export const Divider: Story = {
72
+ render: () =>
73
+ [
74
+ Component({
75
+ id: "card-yes",
76
+ name: "card",
77
+ value: "Yes, I hold a card",
78
+ label: "Yes, I hold a card",
79
+ hint: "Pensioner Concession, Health Care or DVA card",
80
+ }),
81
+ '<div class="mx-choice-divider">or</div>',
82
+ Component({
83
+ id: "card-unsure",
84
+ name: "card",
85
+ value: "Not sure",
86
+ label: "I am not sure",
87
+ hint: "We will ask a few more questions to work it out",
88
+ }),
89
+ ].join(""),
90
+ }
@@ -0,0 +1,43 @@
1
+ import { Meta, StoryObj } from "@storybook/react-vite"
2
+ import CheckboxCard, { FormCheckboxCard, SingleCheckboxCard } from "./CheckboxCard"
3
+ import "../form.css"
4
+ import { Page } from "../../../.storybook-react/decorators"
5
+
6
+ const meta: Meta<typeof SingleCheckboxCard> = {
7
+ decorators: [Page],
8
+ component: SingleCheckboxCard,
9
+ }
10
+
11
+ export default meta
12
+
13
+ export const Single: StoryObj<typeof SingleCheckboxCard> = {
14
+ args: {
15
+ children: "Carer Payment or Carer Allowance",
16
+ value: "carer",
17
+ },
18
+ }
19
+
20
+ export const WithHint: StoryObj<typeof SingleCheckboxCard> = {
21
+ args: {
22
+ children: "Another income support payment",
23
+ hint: "You will be asked which one before you submit",
24
+ value: "other",
25
+ },
26
+ }
27
+
28
+ export const Group: StoryObj<typeof FormCheckboxCard> = {
29
+ render: ({ children, ...args }) => <FormCheckboxCard {...args}>{children}</FormCheckboxCard>,
30
+ args: {
31
+ label: "Which payments do you currently receive?",
32
+ children: (
33
+ <>
34
+ <CheckboxCard value="age">Age Pension</CheckboxCard>
35
+ <CheckboxCard value="dsp">Disability Support Pension</CheckboxCard>
36
+ <CheckboxCard value="carer">Carer Payment or Carer Allowance</CheckboxCard>
37
+ <CheckboxCard value="other" hint="You will be asked which one before you submit">
38
+ Another income support payment
39
+ </CheckboxCard>
40
+ </>
41
+ ),
42
+ },
43
+ }
@@ -0,0 +1,102 @@
1
+ import { createContext, JSX, PropsWithChildren, use, useId, useRef } from "react"
2
+ import {
3
+ AriaCheckboxGroupItemProps,
4
+ AriaCheckboxProps,
5
+ useCheckbox,
6
+ useCheckboxGroup,
7
+ useCheckboxGroupItem,
8
+ } from "react-aria"
9
+ import {
10
+ CheckboxGroupProps,
11
+ CheckboxGroupState,
12
+ useCheckboxGroupState,
13
+ useToggleState,
14
+ } from "react-stately"
15
+ import classNames from "classnames"
16
+ import FormItem from "../FormItem/FormItem"
17
+ import { FormStatus } from "../../enums"
18
+
19
+ const CheckboxCardContext = createContext<CheckboxGroupState>(null)
20
+
21
+ type FormCheckboxCardProps = CheckboxGroupProps &
22
+ PropsWithChildren & {
23
+ label?: string
24
+ }
25
+
26
+ const FormCheckboxCard = ({ label, children, ...props }: FormCheckboxCardProps): JSX.Element => {
27
+ const state = useCheckboxGroupState(props)
28
+ const { groupProps } = useCheckboxGroup(props, state)
29
+
30
+ return (
31
+ <FormItem fieldset {...groupProps} legend={label}>
32
+ <CheckboxCardContext value={state}>{children}</CheckboxCardContext>
33
+ </FormItem>
34
+ )
35
+ }
36
+
37
+ type CheckboxCardProps = AriaCheckboxGroupItemProps &
38
+ PropsWithChildren & {
39
+ hint?: string
40
+ status?: FormStatus
41
+ className?: string
42
+ }
43
+
44
+ const CheckboxCard = ({
45
+ children,
46
+ hint = null,
47
+ status = FormStatus.DEFAULT,
48
+ className = "",
49
+ ...props
50
+ }: CheckboxCardProps): JSX.Element => {
51
+ const id = useId()
52
+ const ref = useRef<HTMLInputElement>(null)
53
+ const state = use(CheckboxCardContext)
54
+ const { inputProps } = useCheckboxGroupItem(props, state, ref)
55
+
56
+ const classes = classNames("mx-checkbox", "mx-choice-card", className)
57
+
58
+ return (
59
+ <div className={classes}>
60
+ <input id={id} ref={ref} aria-invalid={status === FormStatus.INVALID} {...inputProps} />
61
+ <label htmlFor={id}>
62
+ {children}
63
+ {hint && <span className="mx-choice-card__hint">{hint}</span>}
64
+ </label>
65
+ </div>
66
+ )
67
+ }
68
+
69
+ type SingleCheckboxCardProps = AriaCheckboxProps &
70
+ PropsWithChildren & {
71
+ hint?: string
72
+ status?: FormStatus
73
+ className?: string
74
+ }
75
+
76
+ const SingleCheckboxCard = ({
77
+ children,
78
+ hint = null,
79
+ status = FormStatus.DEFAULT,
80
+ className = "",
81
+ ...props
82
+ }: SingleCheckboxCardProps): JSX.Element => {
83
+ const id = useId()
84
+ const ref = useRef<HTMLInputElement>(null)
85
+ const state = useToggleState(props)
86
+ const { inputProps } = useCheckbox(props, state, ref)
87
+
88
+ const classes = classNames("mx-checkbox", "mx-choice-card", className)
89
+
90
+ return (
91
+ <div className={classes}>
92
+ <input id={id} ref={ref} aria-invalid={status === FormStatus.INVALID} {...inputProps} />
93
+ <label htmlFor={id}>
94
+ {children}
95
+ {hint && <span className="mx-choice-card__hint">{hint}</span>}
96
+ </label>
97
+ </div>
98
+ )
99
+ }
100
+
101
+ export default CheckboxCard
102
+ export { FormCheckboxCard, SingleCheckboxCard }