@arbor-education/design-system.components 3.0.2 → 3.2.0
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/.github/pull_request_template.md +4 -3
- package/CHANGELOG.md +16 -0
- package/CONTRIBUTING.md +1 -0
- package/dist/components/formField/FormField.d.ts +34 -18
- package/dist/components/formField/FormField.d.ts.map +1 -1
- package/dist/components/formField/FormField.js +5 -1
- package/dist/components/formField/FormField.js.map +1 -1
- package/dist/components/formField/FormField.stories.d.ts +4 -0
- package/dist/components/formField/FormField.stories.d.ts.map +1 -1
- package/dist/components/formField/FormField.stories.js +314 -7
- package/dist/components/formField/FormField.stories.js.map +1 -1
- package/dist/components/formField/FormField.test.js +66 -0
- package/dist/components/formField/FormField.test.js.map +1 -1
- package/dist/components/formField/inputs/checkbox/CheckboxGroup.d.ts +1 -0
- package/dist/components/formField/inputs/checkbox/CheckboxGroup.d.ts.map +1 -1
- package/dist/components/formField/inputs/checkbox/CheckboxGroup.js +2 -2
- package/dist/components/formField/inputs/checkbox/CheckboxGroup.js.map +1 -1
- package/dist/components/formField/inputs/checkbox/CheckboxInput.d.ts +1 -0
- package/dist/components/formField/inputs/checkbox/CheckboxInput.d.ts.map +1 -1
- package/dist/components/formField/inputs/checkbox/CheckboxInput.js +2 -2
- package/dist/components/formField/inputs/checkbox/CheckboxInput.js.map +1 -1
- package/dist/components/formField/inputs/radio/RadioButtonGroup.d.ts +1 -0
- package/dist/components/formField/inputs/radio/RadioButtonGroup.d.ts.map +1 -1
- package/dist/components/formField/inputs/radio/RadioButtonGroup.js +2 -2
- package/dist/components/formField/inputs/radio/RadioButtonGroup.js.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.d.ts +2 -2
- package/dist/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.d.ts.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.js +0 -1
- package/dist/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.js.map +1 -1
- package/dist/components/formField/inputs/time/TimeInput.d.ts +1 -1
- package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +1 -1
- package/dist/components/icon/Icon.d.ts.map +1 -1
- package/dist/components/icon/Icon.js +18 -3
- package/dist/components/icon/Icon.js.map +1 -1
- package/dist/components/icon/Icon.stories.d.ts +4 -2
- package/dist/components/icon/Icon.stories.d.ts.map +1 -1
- package/dist/components/icon/Icon.stories.js +150 -102
- package/dist/components/icon/Icon.stories.js.map +1 -1
- package/dist/components/icon/Icon.test.js +146 -6
- package/dist/components/icon/Icon.test.js.map +1 -1
- package/dist/components/icon/LucideIcon.d.ts +17 -0
- package/dist/components/icon/LucideIcon.d.ts.map +1 -0
- package/dist/components/icon/LucideIcon.js +73 -0
- package/dist/components/icon/LucideIcon.js.map +1 -0
- package/dist/components/icon/allowedIcons.d.ts +44 -108
- package/dist/components/icon/allowedIcons.d.ts.map +1 -1
- package/dist/components/icon/allowedIcons.js +52 -112
- package/dist/components/icon/allowedIcons.js.map +1 -1
- package/dist/components/icon/types.d.ts +5 -4
- package/dist/components/icon/types.d.ts.map +1 -1
- package/dist/components/tabs/Tabs.d.ts +1 -1
- package/dist/components/tabs/Tabs.stories.d.ts +1 -1
- package/dist/components/tabs/Tabs.stories.js +1 -1
- package/dist/components/tabs/Tabs.stories.js.map +1 -1
- package/dist/components/tabs/TabsItem.d.ts +2 -2
- package/dist/components/tabs/TabsItem.d.ts.map +1 -1
- package/dist/components/tabs/TabsItem.stories.d.ts +5 -5
- package/dist/components/tabs/TabsItem.stories.d.ts.map +1 -1
- package/dist/components/tabs/TabsItem.stories.js +7 -4
- package/dist/components/tabs/TabsItem.stories.js.map +1 -1
- package/dist/components/toggle/Toggle.d.ts +7 -1
- package/dist/components/toggle/Toggle.d.ts.map +1 -1
- package/dist/components/toggle/Toggle.js +2 -2
- package/dist/components/toggle/Toggle.js.map +1 -1
- package/dist/components/toggle/Toggle.stories.d.ts +1 -1
- package/dist/index.css +21 -0
- package/dist/index.css.map +1 -1
- package/package.json +1 -1
- package/src/components/formField/FormField.stories.tsx +398 -7
- package/src/components/formField/FormField.test.tsx +100 -0
- package/src/components/formField/FormField.tsx +46 -27
- package/src/components/formField/inputs/checkbox/CheckboxGroup.tsx +3 -2
- package/src/components/formField/inputs/checkbox/CheckboxInput.tsx +3 -0
- package/src/components/formField/inputs/checkbox/checkboxInput.scss +4 -0
- package/src/components/formField/inputs/radio/RadioButtonGroup.tsx +11 -1
- package/src/components/formField/inputs/radio/radioButtonInput.scss +8 -0
- package/src/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.tsx +2 -2
- package/src/components/icon/Icon.stories.tsx +224 -119
- package/src/components/icon/Icon.test.tsx +178 -6
- package/src/components/icon/Icon.tsx +25 -3
- package/src/components/icon/LucideIcon.tsx +94 -0
- package/src/components/icon/allowedIcons.tsx +67 -207
- package/src/components/icon/lucideDynamic.d.ts +13 -0
- package/src/components/icon/types.ts +5 -4
- package/src/components/tabs/Tabs.stories.tsx +1 -1
- package/src/components/tabs/TabsItem.stories.tsx +8 -4
- package/src/components/tabs/TabsItem.tsx +2 -2
- package/src/components/toggle/Toggle.tsx +13 -3
- package/src/components/toggle/toggle.scss +22 -0
|
@@ -18,7 +18,7 @@ import { FormField } from './FormField.js';
|
|
|
18
18
|
|
|
19
19
|
const DESCRIPTION_INTRO = [
|
|
20
20
|
'FormField is the **complete form field assembly**: it wires together a label, an optional',
|
|
21
|
-
'description, any of
|
|
21
|
+
'description, any of thirteen input controls, an optional helper link, and an error message — all',
|
|
22
22
|
'with the correct accessibility connections built in automatically.',
|
|
23
23
|
'',
|
|
24
24
|
'Think of it as a filing cabinet drawer with everything pre-labelled: you tell it what kind of',
|
|
@@ -45,7 +45,7 @@ const USAGE_GUIDANCE = [
|
|
|
45
45
|
'',
|
|
46
46
|
'---',
|
|
47
47
|
'',
|
|
48
|
-
'### The
|
|
48
|
+
'### The thirteen input types',
|
|
49
49
|
'',
|
|
50
50
|
'| `inputType` | Renders | Typical use |',
|
|
51
51
|
'|---|---|---|',
|
|
@@ -56,7 +56,12 @@ const USAGE_GUIDANCE = [
|
|
|
56
56
|
'| `"colourPicker"` | `ColourPickerDropdown` | Form group or subject colour |',
|
|
57
57
|
'| `"selectDropdown"` | `SelectDropdown` | Year group, term, single or multi select |',
|
|
58
58
|
'| `"datePicker"` | `DatePicker` | Date of birth, enrolment date |',
|
|
59
|
+
'| `"dateTimePicker"` | `DateTimePicker` | Event date and time together |',
|
|
59
60
|
'| `"combobox"` | `Combobox` | Searchable people picker, tutor assignment |',
|
|
61
|
+
'| `"checkbox"` | `CheckboxInput` | Single opt-in, consent, acknowledgement |',
|
|
62
|
+
'| `"checkboxGroup"` | `CheckboxGroup` | Several independent selections (support needs, alert recipients) |',
|
|
63
|
+
'| `"toggle"` | `Toggle` | Instant on/off settings embedded in a form |',
|
|
64
|
+
'| `"radio"` | `RadioButtonGroup` | One choice from a short, mutually-exclusive list |',
|
|
60
65
|
].join('\n');
|
|
61
66
|
|
|
62
67
|
const DEVELOPER_NOTES = [
|
|
@@ -103,6 +108,25 @@ const DEVELOPER_NOTES = [
|
|
|
103
108
|
'This is a known limitation — Confluence guidance says it should open a new tab, but the current',
|
|
104
109
|
'implementation does not do this. Bear it in mind when linking to external documentation.',
|
|
105
110
|
'',
|
|
111
|
+
'**12. `"checkbox"`, `"checkboxGroup"`, `"toggle"`, and `"radio"` all require `inputProps`.** Every',
|
|
112
|
+
'other `inputType` has optional `inputProps`; these four do not — the union marks them as required',
|
|
113
|
+
'because each underlying component needs at least `checked`/`checkedValue` and an `onChange`',
|
|
114
|
+
'(or `onCheckedChange`) to be usable at all.',
|
|
115
|
+
'',
|
|
116
|
+
'**13. `"checkbox"` and `"checkboxGroup"` name themselves via `inputProps`, not the top-level',
|
|
117
|
+
'`label`.** `CheckboxInput` renders its own inline `label` beside the box; `CheckboxGroup` renders',
|
|
118
|
+
'a `<legend>` via `inputProps.legend`. Providing FormField\'s top-level `label` as well produces a',
|
|
119
|
+
'second, redundant heading — pick one.',
|
|
120
|
+
'',
|
|
121
|
+
'**14. `"toggle"` is the one case where the top-level `label` prop is doing real work.** Standalone',
|
|
122
|
+
'`Toggle` has no visible label and needs one hand-rolled; inside FormField, the top-level `label`',
|
|
123
|
+
'renders the `<Label htmlFor={id}>` for you, so `Toggle` gets a proper accessible label for free.',
|
|
124
|
+
'',
|
|
125
|
+
'**15. `"radio"` matches `checkedValue` against `option.value`, not `option.id`.** `id` only',
|
|
126
|
+
'supplies the React key. Also note `RadioButtonGroup` is controlled from one `checkedValue` +',
|
|
127
|
+
'`onChange` at the group level, unlike `CheckboxGroup` where every option carries its own',
|
|
128
|
+
'`checked` + `onChange`.',
|
|
129
|
+
'',
|
|
106
130
|
'---',
|
|
107
131
|
'',
|
|
108
132
|
'### Accessibility',
|
|
@@ -141,7 +165,10 @@ const RELATED_COMPONENTS = [
|
|
|
141
165
|
'[DatePicker](?path=/docs/components-datepicker--docs) · ',
|
|
142
166
|
'[Combobox](?path=/docs/components-combobox--docs) · ',
|
|
143
167
|
'[Fieldset](?path=/docs/components-formfield-fieldset--docs) · ',
|
|
144
|
-
'[Label](?path=/docs/components-formfield-inputs-label--docs)',
|
|
168
|
+
'[Label](?path=/docs/components-formfield-inputs-label--docs) · ',
|
|
169
|
+
'[CheckboxInput](?path=/docs/components-formfield-inputs-checkbox--docs) · ',
|
|
170
|
+
'[RadioButtonInput](?path=/docs/components-formfield-inputs-radiobutton--docs) · ',
|
|
171
|
+
'[Toggle](?path=/docs/components-toggle--docs)',
|
|
145
172
|
].join('\n');
|
|
146
173
|
|
|
147
174
|
const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
|
|
@@ -206,7 +233,7 @@ const meta = {
|
|
|
206
233
|
},
|
|
207
234
|
inputType: {
|
|
208
235
|
control: 'select',
|
|
209
|
-
options: ['text', 'textarea', 'number', 'time', 'colourPicker', 'selectDropdown', 'datePicker', 'combobox'],
|
|
236
|
+
options: ['text', 'textarea', 'number', 'time', 'colourPicker', 'selectDropdown', 'datePicker', 'combobox', 'checkbox', 'checkboxGroup', 'toggle', 'radio'],
|
|
210
237
|
description: [
|
|
211
238
|
'Determines which input component is rendered.',
|
|
212
239
|
'Defaults to `"text"` (renders `TextInput`).',
|
|
@@ -215,10 +242,15 @@ const meta = {
|
|
|
215
242
|
'`"colourPicker"` → `ColourPickerDropdown` (hex colour sketch picker in a dropdown);',
|
|
216
243
|
'`"selectDropdown"` → `SelectDropdown` (single or multi select with grouping);',
|
|
217
244
|
'`"datePicker"` → `DatePicker` (calendar popover);',
|
|
218
|
-
'`"combobox"` → `Combobox` (searchable, type-ahead)
|
|
245
|
+
'`"combobox"` → `Combobox` (searchable, type-ahead);',
|
|
246
|
+
'`"checkbox"` → `CheckboxInput` (single opt-in/consent);',
|
|
247
|
+
'`"checkboxGroup"` → `CheckboxGroup` (several independent selections);',
|
|
248
|
+
'`"toggle"` → `Toggle` (instant on/off switch);',
|
|
249
|
+
'`"radio"` → `RadioButtonGroup` (one choice from a short list).',
|
|
250
|
+
'The last four require `inputProps` — see the developer notes.',
|
|
219
251
|
].join(' '),
|
|
220
252
|
table: {
|
|
221
|
-
type: { summary: "'text' | 'textarea' | 'number' | 'time' | 'colourPicker' | 'selectDropdown' | 'datePicker' | 'combobox'" },
|
|
253
|
+
type: { summary: "'text' | 'textarea' | 'number' | 'time' | 'colourPicker' | 'selectDropdown' | 'datePicker' | 'combobox' | 'checkbox' | 'checkboxGroup' | 'toggle' | 'radio'" },
|
|
222
254
|
defaultValue: { summary: "'text'" },
|
|
223
255
|
},
|
|
224
256
|
},
|
|
@@ -290,9 +322,10 @@ const meta = {
|
|
|
290
322
|
'**Do NOT set `hasError`, `aria-invalid`, or `aria-describedby` here** — FormField builds',
|
|
291
323
|
'these automatically. They are spread BEFORE `inputProps` and will be overridden.',
|
|
292
324
|
'The Controls panel cannot represent this discriminated union — configure it in the story code.',
|
|
325
|
+
'**Required, not optional, for `"checkbox"`, `"checkboxGroup"`, `"toggle"`, and `"radio"`.**',
|
|
293
326
|
].join(' '),
|
|
294
327
|
table: {
|
|
295
|
-
type: { summary: 'TextInputProps | TextAreaProps | NumberInputProps | TimeInputProps | ColourPickerDropdownProps | SelectDropdownInputProps | DatePickerProps | ComboboxProps' },
|
|
328
|
+
type: { summary: 'TextInputProps | TextAreaProps | NumberInputProps | TimeInputProps | ColourPickerDropdownProps | SelectDropdownInputProps | DatePickerProps | DateTimePickerProps | ComboboxProps | CheckboxInputProps | CheckboxGroupProps | Toggle.Props | RadioButtonGroupProps' },
|
|
296
329
|
},
|
|
297
330
|
},
|
|
298
331
|
},
|
|
@@ -1596,3 +1629,361 @@ export default CompleteStudentFormExample;
|
|
|
1596
1629
|
'This replaces the old `FormExample` story which used `"1rem"` gaps and non-school copy.',
|
|
1597
1630
|
].join('\n'),
|
|
1598
1631
|
);
|
|
1632
|
+
|
|
1633
|
+
export const WithCheckbox: Story = withDescription(
|
|
1634
|
+
{
|
|
1635
|
+
parameters: {
|
|
1636
|
+
docs: {
|
|
1637
|
+
source: {
|
|
1638
|
+
language: 'tsx',
|
|
1639
|
+
code: `
|
|
1640
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
1641
|
+
|
|
1642
|
+
function WithCheckboxExample() {
|
|
1643
|
+
return (
|
|
1644
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
1645
|
+
<FormField
|
|
1646
|
+
id="sms-alerts-opt-in"
|
|
1647
|
+
inputType="checkbox"
|
|
1648
|
+
fieldDescription="Sent for unexplained absences and behaviour incidents"
|
|
1649
|
+
inputProps={{
|
|
1650
|
+
label: 'Notify parents by SMS',
|
|
1651
|
+
checked: true,
|
|
1652
|
+
onChange: (event) => console.log(event.target.checked),
|
|
1653
|
+
}}
|
|
1654
|
+
/>
|
|
1655
|
+
<FormField
|
|
1656
|
+
id="data-accuracy-ack"
|
|
1657
|
+
inputType="checkbox"
|
|
1658
|
+
inputProps={{
|
|
1659
|
+
label: 'I confirm this record is accurate and up to date',
|
|
1660
|
+
checked: false,
|
|
1661
|
+
onChange: (event) => console.log(event.target.checked),
|
|
1662
|
+
}}
|
|
1663
|
+
/>
|
|
1664
|
+
<FormField
|
|
1665
|
+
id="parental-consent"
|
|
1666
|
+
inputType="checkbox"
|
|
1667
|
+
errorText="You must confirm parental consent before submitting"
|
|
1668
|
+
inputProps={{
|
|
1669
|
+
label: 'I have obtained parental consent',
|
|
1670
|
+
checked: false,
|
|
1671
|
+
onChange: (event) => console.log(event.target.checked),
|
|
1672
|
+
}}
|
|
1673
|
+
/>
|
|
1674
|
+
</div>
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
export default WithCheckboxExample;
|
|
1678
|
+
`.trim(),
|
|
1679
|
+
},
|
|
1680
|
+
},
|
|
1681
|
+
},
|
|
1682
|
+
render: () => (
|
|
1683
|
+
<div style={{ maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
1684
|
+
<FormField
|
|
1685
|
+
id="sms-alerts-opt-in"
|
|
1686
|
+
inputType="checkbox"
|
|
1687
|
+
fieldDescription="Sent for unexplained absences and behaviour incidents"
|
|
1688
|
+
inputProps={{
|
|
1689
|
+
label: 'Notify parents by SMS',
|
|
1690
|
+
checked: true,
|
|
1691
|
+
onChange: fn(),
|
|
1692
|
+
}}
|
|
1693
|
+
/>
|
|
1694
|
+
<FormField
|
|
1695
|
+
id="data-accuracy-ack"
|
|
1696
|
+
inputType="checkbox"
|
|
1697
|
+
inputProps={{
|
|
1698
|
+
label: 'I confirm this record is accurate and up to date',
|
|
1699
|
+
checked: false,
|
|
1700
|
+
onChange: fn(),
|
|
1701
|
+
}}
|
|
1702
|
+
/>
|
|
1703
|
+
<FormField
|
|
1704
|
+
id="parental-consent"
|
|
1705
|
+
inputType="checkbox"
|
|
1706
|
+
errorText="You must confirm parental consent before submitting"
|
|
1707
|
+
inputProps={{
|
|
1708
|
+
label: 'I have obtained parental consent',
|
|
1709
|
+
checked: false,
|
|
1710
|
+
onChange: fn(),
|
|
1711
|
+
}}
|
|
1712
|
+
/>
|
|
1713
|
+
</div>
|
|
1714
|
+
),
|
|
1715
|
+
},
|
|
1716
|
+
[
|
|
1717
|
+
'`inputType="checkbox"` renders a `CheckboxInput` and, unlike most other `inputType`s, its',
|
|
1718
|
+
'`inputProps` are **required** — `CheckboxInput.Props` has no optional variant in the union.',
|
|
1719
|
+
'',
|
|
1720
|
+
'**Skip the top-level `label` prop for checkboxes.** `CheckboxInput` renders its own inline',
|
|
1721
|
+
'`label` next to the box, inside the same `<label>` element as the input. Setting FormField\'s',
|
|
1722
|
+
'top-level `label` as well produces two separate label texts stacked above and beside the box.',
|
|
1723
|
+
'Use `fieldDescription` for supporting copy instead — it still renders above the input.',
|
|
1724
|
+
'',
|
|
1725
|
+
'**`checked` has no default and must be controlled.** There is no `defaultChecked` escape hatch',
|
|
1726
|
+
'the way there is on `Toggle` — pair `checked` with `onChange` or the box will never update.',
|
|
1727
|
+
].join('\n'),
|
|
1728
|
+
);
|
|
1729
|
+
|
|
1730
|
+
export const WithCheckboxGroup: Story = withDescription(
|
|
1731
|
+
{
|
|
1732
|
+
parameters: {
|
|
1733
|
+
docs: {
|
|
1734
|
+
source: {
|
|
1735
|
+
language: 'tsx',
|
|
1736
|
+
code: `
|
|
1737
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
1738
|
+
|
|
1739
|
+
function WithCheckboxGroupExample() {
|
|
1740
|
+
return (
|
|
1741
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
1742
|
+
<FormField
|
|
1743
|
+
id="support-needs"
|
|
1744
|
+
inputType="checkboxGroup"
|
|
1745
|
+
fieldDescription="Select all that currently apply — this can be updated at any time"
|
|
1746
|
+
inputProps={{
|
|
1747
|
+
legend: 'Support needs',
|
|
1748
|
+
options: [
|
|
1749
|
+
{ id: 'autism', label: 'Autism spectrum', checked: true, onChange: (event) => console.log(event.target.checked) },
|
|
1750
|
+
{ id: 'dyslexia', label: 'Dyslexia', checked: false, onChange: (event) => console.log(event.target.checked) },
|
|
1751
|
+
{ id: 'eal', label: 'EAL support', checked: false, onChange: (event) => console.log(event.target.checked) },
|
|
1752
|
+
],
|
|
1753
|
+
}}
|
|
1754
|
+
/>
|
|
1755
|
+
<FormField
|
|
1756
|
+
id="consent-forms"
|
|
1757
|
+
inputType="checkboxGroup"
|
|
1758
|
+
errorText="Select at least one consent option before continuing"
|
|
1759
|
+
inputProps={{
|
|
1760
|
+
legend: 'Consent forms received',
|
|
1761
|
+
options: [
|
|
1762
|
+
{ id: 'medical', label: 'Medical consent', checked: false, onChange: (event) => console.log(event.target.checked) },
|
|
1763
|
+
{ id: 'photo', label: 'Photo consent', checked: false, onChange: (event) => console.log(event.target.checked) },
|
|
1764
|
+
],
|
|
1765
|
+
}}
|
|
1766
|
+
/>
|
|
1767
|
+
</div>
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
export default WithCheckboxGroupExample;
|
|
1771
|
+
`.trim(),
|
|
1772
|
+
},
|
|
1773
|
+
},
|
|
1774
|
+
},
|
|
1775
|
+
render: () => (
|
|
1776
|
+
<div style={{ maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
1777
|
+
<FormField
|
|
1778
|
+
id="support-needs"
|
|
1779
|
+
inputType="checkboxGroup"
|
|
1780
|
+
fieldDescription="Select all that currently apply — this can be updated at any time"
|
|
1781
|
+
inputProps={{
|
|
1782
|
+
legend: 'Support needs',
|
|
1783
|
+
options: [
|
|
1784
|
+
{ id: 'autism', label: 'Autism spectrum', checked: true, onChange: fn() },
|
|
1785
|
+
{ id: 'dyslexia', label: 'Dyslexia', checked: false, onChange: fn() },
|
|
1786
|
+
{ id: 'eal', label: 'EAL support', checked: false, onChange: fn() },
|
|
1787
|
+
],
|
|
1788
|
+
}}
|
|
1789
|
+
/>
|
|
1790
|
+
<FormField
|
|
1791
|
+
id="consent-forms"
|
|
1792
|
+
inputType="checkboxGroup"
|
|
1793
|
+
errorText="Select at least one consent option before continuing"
|
|
1794
|
+
inputProps={{
|
|
1795
|
+
legend: 'Consent forms received',
|
|
1796
|
+
options: [
|
|
1797
|
+
{ id: 'medical', label: 'Medical consent', checked: false, onChange: fn() },
|
|
1798
|
+
{ id: 'photo', label: 'Photo consent', checked: false, onChange: fn() },
|
|
1799
|
+
],
|
|
1800
|
+
}}
|
|
1801
|
+
/>
|
|
1802
|
+
</div>
|
|
1803
|
+
),
|
|
1804
|
+
},
|
|
1805
|
+
[
|
|
1806
|
+
'`inputType="checkboxGroup"` renders a `CheckboxGroup` — a `Fieldset` wrapping one `CheckboxInput`',
|
|
1807
|
+
'per entry in `inputProps.options`. Like `"checkbox"`, `inputProps` is **required**.',
|
|
1808
|
+
'',
|
|
1809
|
+
'**Use `inputProps.legend`, not FormField\'s top-level `label`, to name the group.** `Fieldset`',
|
|
1810
|
+
'renders its own `<legend>` — set it via `legend` inside `inputProps`. FormField\'s top-level',
|
|
1811
|
+
'`label` still renders (as a standalone `<Label>` above the fieldset) if you provide one, so pick',
|
|
1812
|
+
'one or the other to avoid two headings for the same group.',
|
|
1813
|
+
'',
|
|
1814
|
+
'**Each option manages its own `checked` and `onChange`.** Unlike `RadioButtonGroup`, there is no',
|
|
1815
|
+
'single group-level value — every entry in `options` needs its own `checked` boolean and `onChange`',
|
|
1816
|
+
'handler. `option.id` doubles as the React key.',
|
|
1817
|
+
].join('\n'),
|
|
1818
|
+
);
|
|
1819
|
+
|
|
1820
|
+
export const WithToggle: Story = withDescription(
|
|
1821
|
+
{
|
|
1822
|
+
parameters: {
|
|
1823
|
+
docs: {
|
|
1824
|
+
source: {
|
|
1825
|
+
language: 'tsx',
|
|
1826
|
+
code: `
|
|
1827
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
1828
|
+
|
|
1829
|
+
function WithToggleExample() {
|
|
1830
|
+
return (
|
|
1831
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
1832
|
+
<FormField
|
|
1833
|
+
id="weekly-summary-emails"
|
|
1834
|
+
label="Weekly summary emails"
|
|
1835
|
+
inputType="toggle"
|
|
1836
|
+
fieldDescription="Sent every Friday afternoon with attendance and behaviour highlights"
|
|
1837
|
+
inputProps={{
|
|
1838
|
+
defaultChecked: true,
|
|
1839
|
+
onCheckedChange: (checked) => console.log(checked),
|
|
1840
|
+
}}
|
|
1841
|
+
/>
|
|
1842
|
+
<FormField
|
|
1843
|
+
id="two-factor-auth"
|
|
1844
|
+
label="Require two-factor authentication"
|
|
1845
|
+
inputType="toggle"
|
|
1846
|
+
inputProps={{
|
|
1847
|
+
defaultChecked: false,
|
|
1848
|
+
onCheckedChange: (checked) => console.log(checked),
|
|
1849
|
+
}}
|
|
1850
|
+
/>
|
|
1851
|
+
<FormField
|
|
1852
|
+
id="marketing-emails"
|
|
1853
|
+
label="Marketing emails"
|
|
1854
|
+
inputType="toggle"
|
|
1855
|
+
errorText="You must accept the updated marketing preferences to continue"
|
|
1856
|
+
inputProps={{
|
|
1857
|
+
defaultChecked: false,
|
|
1858
|
+
onCheckedChange: (checked) => console.log(checked),
|
|
1859
|
+
}}
|
|
1860
|
+
/>
|
|
1861
|
+
</div>
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
export default WithToggleExample;
|
|
1865
|
+
`.trim(),
|
|
1866
|
+
},
|
|
1867
|
+
},
|
|
1868
|
+
},
|
|
1869
|
+
render: () => (
|
|
1870
|
+
<div style={{ maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
1871
|
+
<FormField
|
|
1872
|
+
id="weekly-summary-emails"
|
|
1873
|
+
label="Weekly summary emails"
|
|
1874
|
+
inputType="toggle"
|
|
1875
|
+
fieldDescription="Sent every Friday afternoon with attendance and behaviour highlights"
|
|
1876
|
+
inputProps={{
|
|
1877
|
+
defaultChecked: true,
|
|
1878
|
+
onCheckedChange: fn(),
|
|
1879
|
+
}}
|
|
1880
|
+
/>
|
|
1881
|
+
<FormField
|
|
1882
|
+
id="two-factor-auth"
|
|
1883
|
+
label="Require two-factor authentication"
|
|
1884
|
+
inputType="toggle"
|
|
1885
|
+
inputProps={{
|
|
1886
|
+
defaultChecked: false,
|
|
1887
|
+
onCheckedChange: fn(),
|
|
1888
|
+
}}
|
|
1889
|
+
/>
|
|
1890
|
+
<FormField
|
|
1891
|
+
id="marketing-emails"
|
|
1892
|
+
label="Marketing emails"
|
|
1893
|
+
inputType="toggle"
|
|
1894
|
+
errorText="You must accept the updated marketing preferences to continue"
|
|
1895
|
+
inputProps={{
|
|
1896
|
+
defaultChecked: false,
|
|
1897
|
+
onCheckedChange: fn(),
|
|
1898
|
+
}}
|
|
1899
|
+
/>
|
|
1900
|
+
</div>
|
|
1901
|
+
),
|
|
1902
|
+
},
|
|
1903
|
+
[
|
|
1904
|
+
'`inputType="toggle"` renders a `Toggle` (`inputProps` is `Toggle.Props` — Radix UI\'s `Switch.SwitchProps` plus `hasError` — and',
|
|
1905
|
+
'— like the other three new types — it is **required**).',
|
|
1906
|
+
'',
|
|
1907
|
+
'**FormField solves Toggle\'s biggest standalone gotcha for you.** Used on its own, `Toggle` has',
|
|
1908
|
+
'no visible label and needs a hand-rolled `<label htmlFor={id}>` (see the `Toggle` stories\' `WithLabel`',
|
|
1909
|
+
'example). Inside FormField, the top-level `label` prop renders that `<Label htmlFor={id}>` for you',
|
|
1910
|
+
'automatically — just provide `label` and `id` as normal, no manual pairing required.',
|
|
1911
|
+
'',
|
|
1912
|
+
'**`onCheckedChange` receives a boolean, not an event.** `(checked: boolean) => void` — there is',
|
|
1913
|
+
'no `event.target.checked` to unwrap, unlike the native-input-backed `checkbox` and `radio` types.',
|
|
1914
|
+
].join('\n'),
|
|
1915
|
+
);
|
|
1916
|
+
|
|
1917
|
+
export const WithRadio: Story = withDescription(
|
|
1918
|
+
{
|
|
1919
|
+
parameters: {
|
|
1920
|
+
docs: {
|
|
1921
|
+
source: {
|
|
1922
|
+
language: 'tsx',
|
|
1923
|
+
code: `
|
|
1924
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
1925
|
+
|
|
1926
|
+
function WithRadioExample() {
|
|
1927
|
+
return (
|
|
1928
|
+
<FormField
|
|
1929
|
+
id="contact-method"
|
|
1930
|
+
inputType="radio"
|
|
1931
|
+
fieldDescription="Used for attendance and behaviour notifications only"
|
|
1932
|
+
errorText="Please select a contact method"
|
|
1933
|
+
inputProps={{
|
|
1934
|
+
legend: 'Preferred contact method',
|
|
1935
|
+
name: 'contact-method',
|
|
1936
|
+
checkedValue: 'sms',
|
|
1937
|
+
options: [
|
|
1938
|
+
{ id: 'phone', value: 'phone', label: 'Phone call' },
|
|
1939
|
+
{ id: 'email', value: 'email', label: 'Email' },
|
|
1940
|
+
{ id: 'sms', value: 'sms', label: 'SMS' },
|
|
1941
|
+
],
|
|
1942
|
+
onChange: (event) => console.log(event.target.value),
|
|
1943
|
+
}}
|
|
1944
|
+
/>
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
export default WithRadioExample;
|
|
1948
|
+
`.trim(),
|
|
1949
|
+
},
|
|
1950
|
+
},
|
|
1951
|
+
},
|
|
1952
|
+
render: () => (
|
|
1953
|
+
<div style={{ maxWidth: '28rem', padding: 'var(--spacing-xlarge)' }}>
|
|
1954
|
+
<FormField
|
|
1955
|
+
id="contact-method"
|
|
1956
|
+
inputType="radio"
|
|
1957
|
+
fieldDescription="Used for attendance and behaviour notifications only"
|
|
1958
|
+
errorText="Please select a contact method"
|
|
1959
|
+
inputProps={{
|
|
1960
|
+
legend: 'Preferred contact method',
|
|
1961
|
+
name: 'contact-method',
|
|
1962
|
+
checkedValue: 'sms',
|
|
1963
|
+
options: [
|
|
1964
|
+
{ id: 'phone', value: 'phone', label: 'Phone call' },
|
|
1965
|
+
{ id: 'email', value: 'email', label: 'Email' },
|
|
1966
|
+
{ id: 'sms', value: 'sms', label: 'SMS' },
|
|
1967
|
+
],
|
|
1968
|
+
onChange: fn(),
|
|
1969
|
+
}}
|
|
1970
|
+
/>
|
|
1971
|
+
</div>
|
|
1972
|
+
),
|
|
1973
|
+
},
|
|
1974
|
+
[
|
|
1975
|
+
'`inputType="radio"` renders a `RadioButtonGroup` — a `Fieldset` wrapping one radio per entry in',
|
|
1976
|
+
'`inputProps.options`. `inputProps` is **required** and, unlike `CheckboxGroup`, the group is',
|
|
1977
|
+
'controlled from a single place: `checkedValue` + one `onChange`, not per-option state.',
|
|
1978
|
+
'',
|
|
1979
|
+
'**`checkedValue` is compared against each `option.value`, not `option.id`.** `id` only supplies',
|
|
1980
|
+
'the React key — set `value` on every option and match it against `checkedValue` to control which',
|
|
1981
|
+
'radio is selected.',
|
|
1982
|
+
'',
|
|
1983
|
+
'**`name` is required by the type, and functionally mandatory** — without a shared `name`, the',
|
|
1984
|
+
'native radios do not group and multiple can appear selected at once.',
|
|
1985
|
+
'',
|
|
1986
|
+
'**Use `inputProps.legend`, not FormField\'s top-level `label`,** to name the group — the same',
|
|
1987
|
+
'convention as `checkboxGroup`.',
|
|
1988
|
+
].join('\n'),
|
|
1989
|
+
);
|
|
@@ -75,6 +75,106 @@ describe('FormField component', () => {
|
|
|
75
75
|
expect(screen.getByRole('button', { name: 'Open date and time picker' })).toBeInTheDocument();
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
test('renders a checkbox when inputType is checkbox', () => {
|
|
79
|
+
render(<FormField id="niceid" inputType="checkbox" inputProps={{ label: 'Subscribe to newsletter', checked: false, onChange: () => {} }} />);
|
|
80
|
+
expect(screen.getByRole('checkbox', { name: 'Subscribe to newsletter' })).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('marks a checkbox as invalid when errorText is provided', () => {
|
|
84
|
+
render(<FormField id="niceid" errorText="Required" inputType="checkbox" inputProps={{ label: 'Subscribe to newsletter', checked: false, onChange: () => {} }} />);
|
|
85
|
+
expect(screen.getByRole('checkbox', { name: 'Subscribe to newsletter' })).toHaveAttribute('aria-invalid', 'true');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('renders a checkbox group when inputType is checkboxGroup', () => {
|
|
89
|
+
render(
|
|
90
|
+
<FormField
|
|
91
|
+
id="niceid"
|
|
92
|
+
inputType="checkboxGroup"
|
|
93
|
+
inputProps={{
|
|
94
|
+
legend: 'Preferences',
|
|
95
|
+
options: [
|
|
96
|
+
{ id: 'optionA', label: 'Option A', checked: false, onChange: () => {} },
|
|
97
|
+
{ id: 'optionB', label: 'Option B', checked: true, onChange: () => {} },
|
|
98
|
+
],
|
|
99
|
+
}}
|
|
100
|
+
/>,
|
|
101
|
+
);
|
|
102
|
+
expect(screen.getByRole('group', { name: 'Preferences' })).toBeInTheDocument();
|
|
103
|
+
expect(screen.getByRole('checkbox', { name: 'Option A' })).not.toBeChecked();
|
|
104
|
+
expect(screen.getByRole('checkbox', { name: 'Option B' })).toBeChecked();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('marks checkboxes in a checkbox group as invalid when errorText is provided', () => {
|
|
108
|
+
render(
|
|
109
|
+
<FormField
|
|
110
|
+
id="niceid"
|
|
111
|
+
errorText="Please select at least one option"
|
|
112
|
+
inputType="checkboxGroup"
|
|
113
|
+
inputProps={{
|
|
114
|
+
legend: 'Preferences',
|
|
115
|
+
options: [
|
|
116
|
+
{ id: 'optionA', label: 'Option A', checked: false, onChange: () => {} },
|
|
117
|
+
],
|
|
118
|
+
}}
|
|
119
|
+
/>,
|
|
120
|
+
);
|
|
121
|
+
expect(screen.getByRole('checkbox', { name: 'Option A' })).toHaveAttribute('aria-invalid', 'true');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('renders a toggle when inputType is toggle', () => {
|
|
125
|
+
render(<FormField id="niceid" label="Enable notifications" inputType="toggle" inputProps={{ onCheckedChange: () => {} }} />);
|
|
126
|
+
expect(screen.getByRole('switch', { name: 'Enable notifications' })).toBeInTheDocument();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('marks a toggle as invalid when errorText is provided', () => {
|
|
130
|
+
render(<FormField id="niceid" label="Enable notifications" errorText="Required" inputType="toggle" inputProps={{ onCheckedChange: () => {} }} />);
|
|
131
|
+
expect(screen.getByRole('switch', { name: 'Enable notifications' })).toHaveAttribute('aria-invalid', 'true');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('renders a radio button group when inputType is radio', () => {
|
|
135
|
+
render(
|
|
136
|
+
<FormField
|
|
137
|
+
id="niceid"
|
|
138
|
+
inputType="radio"
|
|
139
|
+
inputProps={{
|
|
140
|
+
legend: 'Contact method',
|
|
141
|
+
name: 'contact-method',
|
|
142
|
+
checkedValue: 'sms',
|
|
143
|
+
options: [
|
|
144
|
+
{ id: 'sms', value: 'sms', label: 'SMS' },
|
|
145
|
+
{ id: 'email', value: 'email', label: 'Email' },
|
|
146
|
+
],
|
|
147
|
+
onChange: () => {},
|
|
148
|
+
}}
|
|
149
|
+
/>,
|
|
150
|
+
);
|
|
151
|
+
expect(screen.getByRole('group', { name: 'Contact method' })).toBeInTheDocument();
|
|
152
|
+
expect(screen.getByRole('radio', { name: 'SMS' })).toBeChecked();
|
|
153
|
+
expect(screen.getByRole('radio', { name: 'Email' })).not.toBeChecked();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('marks radio buttons in a radio group as invalid when errorText is provided', () => {
|
|
157
|
+
render(
|
|
158
|
+
<FormField
|
|
159
|
+
id="niceid"
|
|
160
|
+
errorText="Please choose a contact method"
|
|
161
|
+
inputType="radio"
|
|
162
|
+
inputProps={{
|
|
163
|
+
legend: 'Contact method',
|
|
164
|
+
name: 'contact-method',
|
|
165
|
+
checkedValue: 'sms',
|
|
166
|
+
options: [
|
|
167
|
+
{ id: 'sms', value: 'sms', label: 'SMS' },
|
|
168
|
+
{ id: 'email', value: 'email', label: 'Email' },
|
|
169
|
+
],
|
|
170
|
+
onChange: () => {},
|
|
171
|
+
}}
|
|
172
|
+
/>,
|
|
173
|
+
);
|
|
174
|
+
expect(screen.getByRole('radio', { name: 'SMS' })).toHaveAttribute('aria-invalid', 'true');
|
|
175
|
+
expect(screen.getByRole('radio', { name: 'Email' })).toHaveAttribute('aria-invalid', 'true');
|
|
176
|
+
});
|
|
177
|
+
|
|
78
178
|
test('passes error state through to Combobox when errorText is provided', () => {
|
|
79
179
|
render(
|
|
80
180
|
<FormField
|