@arbor-education/design-system.components 0.13.0 → 0.14.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/.agent-memory/blanche-designspert/MEMORY.md +189 -0
- package/.agent-memory/dorothy-fact-checker/MEMORY.md +228 -0
- package/.agent-memory/dorothy-fact-checker/numberinput_component.md +53 -0
- package/.agent-memory/dorothy-fact-checker/progress_component.md +36 -0
- package/.agent-memory/rose-storybookspert/MEMORY.md +105 -0
- package/.agent-memory/sophia-componentspert/MEMORY.md +34 -0
- package/{.claude/agent-memory → .agent-memory}/sophia-componentspert/components.md +170 -17
- package/{.claude → .gather}/agents/blanche-designspert.md +7 -2
- package/{.claude → .gather}/agents/dorothy-fact-checker.md +7 -2
- package/{.claude → .gather}/agents/rose-storybookspert.md +80 -11
- package/{.claude → .gather}/agents/sophia-componentspert.md +9 -4
- package/.gather/gather.yaml +9 -0
- package/{CLAUDE.md → .gather/instructions/project-overview.md} +42 -9
- package/{.claude → .gather}/skills/analyze-design/README.md +5 -0
- package/{.claude → .gather}/skills/analyze-design/SKILL.md +1 -1
- package/.gather/skills/analyze-design/meta.md +4 -0
- package/{.claude → .gather}/skills/create-page/README.md +5 -0
- package/{.claude → .gather}/skills/create-page/design-analysis-template.md +5 -0
- package/.gather/skills/create-page/meta.md +4 -0
- package/{.claude → .gather}/skills/create-page/page-template.scss +5 -0
- package/{.claude → .gather}/skills/create-page/page-template.tsx +5 -0
- package/{.claude → .gather}/skills/map-legacy/README.md +5 -0
- package/.gather/skills/map-legacy/meta.md +4 -0
- package/{.claude → .gather}/skills/migrate-page/README.md +5 -0
- package/.gather/skills/migrate-page/meta.md +4 -0
- package/.gather/skills/write-stories/README.md +157 -0
- package/.gather/skills/write-stories/SKILL.md +841 -0
- package/.gather/skills/write-stories/meta.md +4 -0
- package/.ralph/storybook-upgrade/knowledge.md +308 -0
- package/.ralph/storybook-upgrade/prd.json +777 -0
- package/.ralph/storybook-upgrade/progress.md +342 -0
- package/.storybook/DocsTemplate.tsx +122 -0
- package/.storybook/preview.ts +40 -0
- package/.stylelintignore +2 -0
- package/CHANGELOG.md +14 -0
- package/{.claude/component-library.md → component-library.md} +27 -10
- package/dist/components/articleCard/ArticleCard.d.ts +30 -0
- package/dist/components/articleCard/ArticleCard.d.ts.map +1 -0
- package/dist/components/articleCard/ArticleCard.js +24 -0
- package/dist/components/articleCard/ArticleCard.js.map +1 -0
- package/dist/components/articleCard/ArticleCard.stories.d.ts +18 -0
- package/dist/components/articleCard/ArticleCard.stories.d.ts.map +1 -0
- package/dist/components/articleCard/ArticleCard.stories.js +112 -0
- package/dist/components/articleCard/ArticleCard.stories.js.map +1 -0
- package/dist/components/articleCard/ArticleCard.test.d.ts +2 -0
- package/dist/components/articleCard/ArticleCard.test.d.ts.map +1 -0
- package/dist/components/articleCard/ArticleCard.test.js +49 -0
- package/dist/components/articleCard/ArticleCard.test.js.map +1 -0
- package/dist/components/badge/Badge.stories.d.ts +85 -6
- package/dist/components/badge/Badge.stories.d.ts.map +1 -1
- package/dist/components/badge/Badge.stories.js +626 -27
- package/dist/components/badge/Badge.stories.js.map +1 -1
- package/dist/components/banner/Banner.stories.d.ts +129 -63
- package/dist/components/banner/Banner.stories.d.ts.map +1 -1
- package/dist/components/banner/Banner.stories.js +855 -39
- package/dist/components/banner/Banner.stories.js.map +1 -1
- package/dist/components/button/Button.stories.d.ts +148 -8
- package/dist/components/button/Button.stories.d.ts.map +1 -1
- package/dist/components/button/Button.stories.js +1089 -80
- package/dist/components/button/Button.stories.js.map +1 -1
- package/dist/components/card/Card.d.ts +41 -12
- package/dist/components/card/Card.d.ts.map +1 -1
- package/dist/components/card/Card.js +46 -17
- package/dist/components/card/Card.js.map +1 -1
- package/dist/components/card/Card.stories.d.ts +9 -84
- package/dist/components/card/Card.stories.d.ts.map +1 -1
- package/dist/components/card/Card.stories.js +15 -73
- package/dist/components/card/Card.stories.js.map +1 -1
- package/dist/components/card/Card.test.js +50 -152
- package/dist/components/card/Card.test.js.map +1 -1
- package/dist/components/dot/Dot.stories.d.ts +46 -11
- package/dist/components/dot/Dot.stories.d.ts.map +1 -1
- package/dist/components/dot/Dot.stories.js +504 -15
- package/dist/components/dot/Dot.stories.js.map +1 -1
- package/dist/components/dropdown/Dropdown.stories.d.ts +89 -14
- package/dist/components/dropdown/Dropdown.stories.d.ts.map +1 -1
- package/dist/components/dropdown/Dropdown.stories.js +769 -17
- package/dist/components/dropdown/Dropdown.stories.js.map +1 -1
- package/dist/components/formField/FormField.stories.d.ts +95 -35
- package/dist/components/formField/FormField.stories.d.ts.map +1 -1
- package/dist/components/formField/FormField.stories.js +1174 -69
- package/dist/components/formField/FormField.stories.js.map +1 -1
- package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts +96 -9
- package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js +717 -10
- package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js.map +1 -1
- package/dist/components/formField/inputs/number/NumberInput.stories.d.ts +149 -11
- package/dist/components/formField/inputs/number/NumberInput.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/number/NumberInput.stories.js +624 -10
- package/dist/components/formField/inputs/number/NumberInput.stories.js.map +1 -1
- package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts +74 -1
- package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js +673 -44
- package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js.map +1 -1
- package/dist/components/formField/inputs/text/TextInput.stories.d.ts +119 -1
- package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/text/TextInput.stories.js +549 -10
- package/dist/components/formField/inputs/text/TextInput.stories.js.map +1 -1
- package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts +129 -4
- package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/textArea/TextArea.stories.js +577 -3
- package/dist/components/formField/inputs/textArea/TextArea.stories.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/heading/Heading.stories.d.ts +449 -50
- package/dist/components/heading/Heading.stories.d.ts.map +1 -1
- package/dist/components/heading/Heading.stories.js +536 -60
- package/dist/components/heading/Heading.stories.js.map +1 -1
- package/dist/components/icoText/IcoText.d.ts +37 -0
- package/dist/components/icoText/IcoText.d.ts.map +1 -0
- package/dist/components/icoText/IcoText.js +29 -0
- package/dist/components/icoText/IcoText.js.map +1 -0
- package/dist/components/icoText/IcoText.stories.d.ts +34 -0
- package/dist/components/icoText/IcoText.stories.d.ts.map +1 -0
- package/dist/components/icoText/IcoText.stories.js +24 -0
- package/dist/components/icoText/IcoText.stories.js.map +1 -0
- package/dist/components/icoText/IcoText.test.d.ts +2 -0
- package/dist/components/icoText/IcoText.test.d.ts.map +1 -0
- package/dist/components/icoText/IcoText.test.js +27 -0
- package/dist/components/icoText/IcoText.test.js.map +1 -0
- package/dist/components/icon/Icon.stories.d.ts +81 -10
- package/dist/components/icon/Icon.stories.d.ts.map +1 -1
- package/dist/components/icon/Icon.stories.js +979 -8
- package/dist/components/icon/Icon.stories.js.map +1 -1
- package/dist/components/kpiCard/KPICard.d.ts +13 -0
- package/dist/components/kpiCard/KPICard.d.ts.map +1 -0
- package/dist/components/kpiCard/KPICard.js +8 -0
- package/dist/components/kpiCard/KPICard.js.map +1 -0
- package/dist/components/kpiCard/KPICard.stories.d.ts +9 -0
- package/dist/components/kpiCard/KPICard.stories.d.ts.map +1 -0
- package/dist/components/kpiCard/KPICard.stories.js +18 -0
- package/dist/components/kpiCard/KPICard.stories.js.map +1 -0
- package/dist/components/kpiCard/KPICard.test.d.ts +2 -0
- package/dist/components/kpiCard/KPICard.test.d.ts.map +1 -0
- package/dist/components/kpiCard/KPICard.test.js +37 -0
- package/dist/components/kpiCard/KPICard.test.js.map +1 -0
- package/dist/components/kvpList/KVPList.d.ts +34 -0
- package/dist/components/kvpList/KVPList.d.ts.map +1 -0
- package/dist/components/kvpList/KVPList.js +20 -0
- package/dist/components/kvpList/KVPList.js.map +1 -0
- package/dist/components/kvpList/KVPList.stories.d.ts +27 -0
- package/dist/components/kvpList/KVPList.stories.d.ts.map +1 -0
- package/dist/components/kvpList/KVPList.stories.js +18 -0
- package/dist/components/kvpList/KVPList.stories.js.map +1 -0
- package/dist/components/kvpList/KVPList.test.d.ts +2 -0
- package/dist/components/kvpList/KVPList.test.d.ts.map +1 -0
- package/dist/components/kvpList/KVPList.test.js +29 -0
- package/dist/components/kvpList/KVPList.test.js.map +1 -0
- package/dist/components/pill/Pill.stories.d.ts +71 -19
- package/dist/components/pill/Pill.stories.d.ts.map +1 -1
- package/dist/components/pill/Pill.stories.js +573 -14
- package/dist/components/pill/Pill.stories.js.map +1 -1
- package/dist/components/progress/Progress.stories.d.ts +75 -298
- package/dist/components/progress/Progress.stories.d.ts.map +1 -1
- package/dist/components/progress/Progress.stories.js +449 -52
- package/dist/components/progress/Progress.stories.js.map +1 -1
- package/dist/components/separator/Separator.stories.d.ts +58 -5
- package/dist/components/separator/Separator.stories.d.ts.map +1 -1
- package/dist/components/separator/Separator.stories.js +443 -4
- package/dist/components/separator/Separator.stories.js.map +1 -1
- package/dist/components/singleUser/SingleUser.d.ts +1 -1
- package/dist/components/tabs/TabsItem.stories.d.ts +2 -2
- package/dist/components/tag/Tag.stories.d.ts +116 -5
- package/dist/components/tag/Tag.stories.d.ts.map +1 -1
- package/dist/components/tag/Tag.stories.js +581 -28
- package/dist/components/tag/Tag.stories.js.map +1 -1
- package/dist/index.css +194 -23
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/eslint.config.mts +5 -1
- package/package.json +3 -3
- package/src/components/articleCard/ArticleCard.stories.tsx +132 -0
- package/src/components/articleCard/ArticleCard.test.tsx +121 -0
- package/src/components/articleCard/ArticleCard.tsx +100 -0
- package/src/components/articleCard/articleCard.scss +39 -0
- package/src/components/badge/Badge.stories.tsx +869 -42
- package/src/components/banner/Banner.stories.tsx +1081 -63
- package/src/components/button/Button.stories.tsx +1394 -99
- package/src/components/card/Card.stories.tsx +35 -79
- package/src/components/card/Card.test.tsx +72 -190
- package/src/components/card/Card.tsx +117 -58
- package/src/components/card/card.scss +18 -31
- package/src/components/dot/Dot.stories.tsx +723 -32
- package/src/components/dropdown/Dropdown.stories.tsx +1174 -35
- package/src/components/formField/FormField.stories.tsx +1522 -105
- package/src/components/formField/inputs/checkbox/CheckboxInput.stories.tsx +1020 -15
- package/src/components/formField/inputs/number/NumberInput.stories.tsx +908 -15
- package/src/components/formField/inputs/radio/RadioButtonInput.stories.tsx +932 -51
- package/src/components/formField/inputs/text/TextInput.stories.tsx +773 -13
- package/src/components/formField/inputs/textArea/TextArea.stories.tsx +756 -8
- package/src/components/heading/Heading.stories.tsx +752 -120
- package/src/components/icoText/IcoText.stories.tsx +47 -0
- package/src/components/icoText/IcoText.test.tsx +41 -0
- package/src/components/icoText/IcoText.tsx +93 -0
- package/src/components/icoText/icoText.scss +34 -0
- package/src/components/icon/Icon.stories.tsx +1446 -12
- package/src/components/kpiCard/KPICard.stories.tsx +47 -0
- package/src/components/kpiCard/KPICard.test.tsx +60 -0
- package/src/components/kpiCard/KPICard.tsx +45 -0
- package/src/components/kpiCard/kpiCard.scss +35 -0
- package/src/components/kvpList/KVPList.stories.tsx +51 -0
- package/src/components/kvpList/KVPList.test.tsx +66 -0
- package/src/components/kvpList/KVPList.tsx +109 -0
- package/src/components/kvpList/kvpList.scss +64 -0
- package/src/components/pill/Pill.stories.tsx +867 -21
- package/src/components/progress/Progress.stories.tsx +625 -58
- package/src/components/separator/Separator.stories.tsx +730 -8
- package/src/components/separator/separator.scss +12 -3
- package/src/components/tag/Tag.stories.tsx +755 -53
- package/src/index.scss +4 -0
- package/src/index.ts +13 -4
- package/src/tokens.scss +6 -0
- package/tokens/json/Arbor.json +30 -0
- package/.claude/agent-memory/blanche-designspert/MEMORY.md +0 -64
- package/.claude/agent-memory/dorothy-fact-checker/MEMORY.md +0 -129
- package/.claude/agent-memory/rose-storybookspert/MEMORY.md +0 -29
- package/.claude/agent-memory/sophia-componentspert/MEMORY.md +0 -14
- package/.claude/design-assessment-daily-attendance-2026-04-10.md +0 -566
- package/.claude/figma-assessment-7154-58899.md +0 -404
- package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-11086-97537.md +0 -392
- package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-41974.md +0 -474
- package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-43094.md +0 -462
- package/.claude/figma-assessment-fcFK4CGzkz2fVyY3koX8ZE-7154-59061.md +0 -440
- package/.claude/migration-report-custom-report-writer-2026-02-19.md +0 -591
- /package/{.claude/agent-memory → .agent-memory}/blanche-designspert/token-review-patterns.md +0 -0
- /package/{.claude/agent-memory → .agent-memory}/rose-storybookspert/patterns.md +0 -0
- /package/{.claude → .gather}/skills/create-page/SKILL.md +0 -0
- /package/{.claude → .gather}/skills/map-legacy/SKILL.md +0 -0
- /package/{.claude → .gather}/skills/migrate-page/SKILL.md +0 -0
|
@@ -1,97 +1,1202 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Controls, Heading as DocHeading, Markdown, Primary as DocPrimary, Stories, Subtitle, Title, } from '@storybook/addon-docs/blocks';
|
|
2
3
|
import { fn } from 'storybook/test';
|
|
3
4
|
import { comboboxPeopleOptions } from '../../mocks/comboboxStoryOptions';
|
|
4
5
|
import { FormField } from './FormField';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Docs page content
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
const DESCRIPTION_INTRO = [
|
|
10
|
+
'FormField is the **complete form field assembly**: it wires together a label, an optional',
|
|
11
|
+
'description, any of eight input controls, an optional helper link, and an error message — all',
|
|
12
|
+
'with the correct accessibility connections built in automatically.',
|
|
13
|
+
'',
|
|
14
|
+
'Think of it as a filing cabinet drawer with everything pre-labelled: you tell it what kind of',
|
|
15
|
+
'input you need (`inputType`), give it a label and an `id`, and it handles the `htmlFor`,',
|
|
16
|
+
'`aria-describedby`, `aria-invalid`, and `hasError` wiring for you.',
|
|
17
|
+
].join('\n');
|
|
18
|
+
const USAGE_GUIDANCE = [
|
|
19
|
+
'### When to use',
|
|
20
|
+
'',
|
|
21
|
+
'- Any labelled input in an Arbor form — enrolment, student records, timetabling, attendance',
|
|
22
|
+
'- When you need built-in error state management without writing the aria connections yourself',
|
|
23
|
+
'- When a field needs a helper link to external documentation or a field description below the label',
|
|
24
|
+
'',
|
|
25
|
+
'---',
|
|
26
|
+
'',
|
|
27
|
+
'### When NOT to use',
|
|
28
|
+
'',
|
|
29
|
+
'| Scenario | Use instead |',
|
|
30
|
+
'|---|---|',
|
|
31
|
+
'| Raw input without a label | Use the individual input component (`TextInput`, etc.) directly |',
|
|
32
|
+
'| Grouping several related fields (address, name) | Wrap multiple `FormField`s in a `Fieldset` |',
|
|
33
|
+
'| Non-standard input control | Compose the label + input yourself using `Label` + the input component |',
|
|
34
|
+
'',
|
|
35
|
+
'---',
|
|
36
|
+
'',
|
|
37
|
+
'### The eight input types',
|
|
38
|
+
'',
|
|
39
|
+
'| `inputType` | Renders | Typical use |',
|
|
40
|
+
'|---|---|---|',
|
|
41
|
+
'| `"text"` (default) | `TextInput` | Names, codes, short free text |',
|
|
42
|
+
'| `"textarea"` | `TextArea` | Notes, medical conditions, long free text |',
|
|
43
|
+
'| `"number"` | `NumberInput` | Class sizes, ages, counts |',
|
|
44
|
+
'| `"time"` | `TimeInput` | Registration time, period start/end |',
|
|
45
|
+
'| `"colourPicker"` | `ColourPickerDropdown` | Form group or subject colour |',
|
|
46
|
+
'| `"selectDropdown"` | `SelectDropdown` | Year group, term, single or multi select |',
|
|
47
|
+
'| `"datePicker"` | `DatePicker` | Date of birth, enrolment date |',
|
|
48
|
+
'| `"combobox"` | `Combobox` | Searchable people picker, tutor assignment |',
|
|
49
|
+
].join('\n');
|
|
50
|
+
const DEVELOPER_NOTES = [
|
|
51
|
+
'### Critical gotchas — read before using',
|
|
52
|
+
'',
|
|
53
|
+
'**1. Always provide `id`.** The `id` prop drives `label[htmlFor]`, and the `aria-describedby`',
|
|
54
|
+
'IDs for the description and error spans. Omit it and the a11y wiring silently breaks — no TS',
|
|
55
|
+
'error, just inaccessible markup. Treat it as required.',
|
|
56
|
+
'',
|
|
57
|
+
'**2. Never set `hasError`, `aria-invalid`, or `aria-describedby` in `inputProps`.** FormField',
|
|
58
|
+
'builds these automatically from `errorText` and `fieldDescription`. If you set them in',
|
|
59
|
+
'`inputProps`, they will be overridden — but the intent becomes confusing and may produce',
|
|
60
|
+
'conflicting values depending on spread order.',
|
|
61
|
+
'',
|
|
62
|
+
'**3. `helperLinkText` and `helperLinkUrl` must both be provided.** Providing only one renders',
|
|
63
|
+
'nothing — the component renders neither the link text nor the URL if the pair is incomplete.',
|
|
64
|
+
'',
|
|
65
|
+
'**4. `helperLink` and `fieldDescription` are mutually exclusive by design.** The component',
|
|
66
|
+
'will render both if you supply both, but the Confluence design spec prohibits this combination.',
|
|
67
|
+
'Use one or the other, never both.',
|
|
68
|
+
'',
|
|
69
|
+
'**5. `errorText` and `helperLink` CAN coexist.** Both appear inside the same `ds-form-field__message`',
|
|
70
|
+
'container: error rendered first, helper link second (stacked vertically).',
|
|
71
|
+
'',
|
|
72
|
+
'**6. `fieldDescription` persists during errors.** The description span stays visible even when',
|
|
73
|
+
'`errorText` is set. It is not hidden or replaced by the error.',
|
|
74
|
+
'',
|
|
75
|
+
'**7. Always provide `label` when using `helperLinkText`.** The helper link `aria-label` is',
|
|
76
|
+
'constructed as `"${label} helper link"`. Omit `label` and the aria-label becomes `"undefined helper link"`.',
|
|
77
|
+
'',
|
|
78
|
+
'**8. Optional fields: use the label suffix convention.** There is no `required` prop and no',
|
|
79
|
+
'asterisk pattern. Mark optional fields by appending `"(optional)"` to the label text, e.g.',
|
|
80
|
+
'`label="Middle name (optional)"`. All fields without this suffix are implicitly required.',
|
|
81
|
+
'',
|
|
82
|
+
'**9. `NumberInput` renders `type="text"` internally.** Do not pass `type` in `inputProps` when',
|
|
83
|
+
'`inputType="number"` — it is ignored and may produce confusing behaviour. The component uses',
|
|
84
|
+
'`inputMode="numeric"` for mobile keyboards.',
|
|
85
|
+
'',
|
|
86
|
+
'**10. `TimeInput` switches to Combobox mode when `options` is provided.** Pass an array of',
|
|
87
|
+
'`"HH:MM"` strings to `inputProps.options` to replace the native time picker with a searchable',
|
|
88
|
+
'dropdown of preset times.',
|
|
89
|
+
'',
|
|
90
|
+
'**11. The helper link does NOT open in a new tab.** There is no `target="_blank"` on the anchor.',
|
|
91
|
+
'This is a known limitation — Confluence guidance says it should open a new tab, but the current',
|
|
92
|
+
'implementation does not do this. Bear it in mind when linking to external documentation.',
|
|
93
|
+
'',
|
|
94
|
+
'---',
|
|
95
|
+
'',
|
|
96
|
+
'### Accessibility',
|
|
97
|
+
'',
|
|
98
|
+
'- `id` drives all ARIA wiring — treat it as required even though TypeScript allows it to be omitted',
|
|
99
|
+
'- `fieldDescription` is connected via `aria-describedby` automatically — never wire this yourself',
|
|
100
|
+
'- `errorText` sets both `hasError` and `aria-invalid` on the inner input, and a description via `aria-describedby`',
|
|
101
|
+
'- The error span contains a `triangle-alert` icon; the icon is decorative (the text carries the meaning)',
|
|
102
|
+
'- Visible labels are always required — never rely on placeholder text as a substitute for a label',
|
|
103
|
+
'- For grouped fields (address blocks, name fields) wrap in `Fieldset` with a `legend`',
|
|
104
|
+
'',
|
|
105
|
+
'---',
|
|
106
|
+
'',
|
|
107
|
+
'### TypeScript types',
|
|
108
|
+
'',
|
|
109
|
+
'```ts',
|
|
110
|
+
"import { FormField } from '@arbor-education/design-system.components';",
|
|
111
|
+
'',
|
|
112
|
+
'function MyField(props: FormField.Props) { ... }',
|
|
113
|
+
'```',
|
|
114
|
+
'',
|
|
115
|
+
'| Type | Description |',
|
|
116
|
+
'|---|---|',
|
|
117
|
+
'| `FormField.Props` | Full props interface |',
|
|
118
|
+
].join('\n');
|
|
119
|
+
const RELATED_COMPONENTS = [
|
|
120
|
+
'## Related components',
|
|
121
|
+
'',
|
|
122
|
+
'[TextInput](?path=/docs/components-formfield-inputs-textinput--docs) · ',
|
|
123
|
+
'[TextArea](?path=/docs/components-formfield-inputs-textarea--docs) · ',
|
|
124
|
+
'[NumberInput](?path=/docs/components-formfield-inputs-numeric--docs) · ',
|
|
125
|
+
'[TimeInput](?path=/docs/components-formfield-inputs-timeinput--docs) · ',
|
|
126
|
+
'[SelectDropdown](?path=/docs/components-formfield-inputs-selectdropdown--docs) · ',
|
|
127
|
+
'[ColourPickerDropdown](?path=/docs/components-formfield-inputs-colourpickerdropdown--docs) · ',
|
|
128
|
+
'[DatePicker](?path=/docs/components-datepicker--docs) · ',
|
|
129
|
+
'[Combobox](?path=/docs/components-combobox--docs) · ',
|
|
130
|
+
'[Fieldset](?path=/docs/components-formfield-fieldset--docs) · ',
|
|
131
|
+
'[Label](?path=/docs/components-formfield-inputs-label--docs)',
|
|
132
|
+
].join('\n');
|
|
133
|
+
const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
|
|
134
|
+
function FormFieldDocsPage() {
|
|
135
|
+
return (_jsxs(_Fragment, { children: [_jsx(Title, {}), _jsx(Subtitle, {}), _jsx(Markdown, { children: DESCRIPTION_INTRO }), _jsx(DocHeading, { children: "Interactive example" }), _jsx(Markdown, { children: PROPS_INTRO }), _jsx(DocPrimary, {}), _jsx(Controls, {}), _jsx(DocHeading, { children: "Usage guidance" }), _jsx(Markdown, { children: USAGE_GUIDANCE }), _jsx(DocHeading, { children: "Developer notes" }), _jsx(Markdown, { children: DEVELOPER_NOTES }), _jsx(DocHeading, { children: "Examples" }), _jsx(Stories, { title: "" }), _jsx(Markdown, { children: RELATED_COMPONENTS })] }));
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Meta
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
5
140
|
const meta = {
|
|
6
141
|
title: 'Components/FormField',
|
|
7
142
|
component: FormField,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
label: 'Text Input',
|
|
13
|
-
inputProps: {
|
|
14
|
-
placeholder: 'Enter your text',
|
|
15
|
-
onChange: fn(),
|
|
143
|
+
tags: ['autodocs'],
|
|
144
|
+
parameters: {
|
|
145
|
+
docs: {
|
|
146
|
+
page: FormFieldDocsPage,
|
|
16
147
|
},
|
|
17
|
-
helperLinkText: 'More information',
|
|
18
|
-
helperLinkUrl: 'https://www.google.com',
|
|
19
|
-
errorText: 'This is some error text',
|
|
20
|
-
fieldDescription: 'This is some descriptive text for the field',
|
|
21
|
-
inputType: 'text',
|
|
22
148
|
},
|
|
23
149
|
argTypes: {
|
|
24
|
-
|
|
150
|
+
id: {
|
|
25
151
|
control: 'text',
|
|
26
|
-
description:
|
|
152
|
+
description: [
|
|
153
|
+
'**Practically required.** Drives `label[htmlFor]` and the `aria-describedby` IDs for the',
|
|
154
|
+
'field description (`{id}-description`) and error message (`{id}-error`).',
|
|
155
|
+
'Omit it and all accessibility wiring silently breaks — no TypeScript error, just broken a11y.',
|
|
156
|
+
].join(' '),
|
|
157
|
+
table: {
|
|
158
|
+
type: { summary: 'string' },
|
|
159
|
+
},
|
|
27
160
|
},
|
|
28
|
-
|
|
161
|
+
label: {
|
|
29
162
|
control: 'text',
|
|
30
|
-
description:
|
|
163
|
+
description: [
|
|
164
|
+
'Visible label rendered above the input via the `Label` component (`ds-label`).',
|
|
165
|
+
'Short, sentence case. Use `"Field name (optional)"` suffix for optional fields — there is',
|
|
166
|
+
'no `required` prop and no asterisk convention.',
|
|
167
|
+
'**Also required when using `helperLinkText`** — the link\'s `aria-label` uses this value verbatim.',
|
|
168
|
+
].join(' '),
|
|
169
|
+
table: {
|
|
170
|
+
type: { summary: 'string' },
|
|
171
|
+
},
|
|
31
172
|
},
|
|
32
|
-
|
|
33
|
-
control: '
|
|
34
|
-
|
|
173
|
+
inputType: {
|
|
174
|
+
control: 'select',
|
|
175
|
+
options: ['text', 'textarea', 'number', 'time', 'colourPicker', 'selectDropdown', 'datePicker', 'combobox'],
|
|
176
|
+
description: [
|
|
177
|
+
'Determines which input component is rendered.',
|
|
178
|
+
'Defaults to `"text"` (renders `TextInput`).',
|
|
179
|
+
'`"textarea"` → `TextArea` (auto-grows); `"number"` → `NumberInput` (spinner buttons + decimal);',
|
|
180
|
+
'`"time"` → `TimeInput` (native picker or combobox preset mode);',
|
|
181
|
+
'`"colourPicker"` → `ColourPickerDropdown` (hex colour sketch picker in a dropdown);',
|
|
182
|
+
'`"selectDropdown"` → `SelectDropdown` (single or multi select with grouping);',
|
|
183
|
+
'`"datePicker"` → `DatePicker` (calendar popover);',
|
|
184
|
+
'`"combobox"` → `Combobox` (searchable, type-ahead).',
|
|
185
|
+
].join(' '),
|
|
186
|
+
table: {
|
|
187
|
+
type: { summary: "'text' | 'textarea' | 'number' | 'time' | 'colourPicker' | 'selectDropdown' | 'datePicker' | 'combobox'" },
|
|
188
|
+
defaultValue: { summary: "'text'" },
|
|
189
|
+
},
|
|
35
190
|
},
|
|
36
|
-
|
|
191
|
+
fieldDescription: {
|
|
37
192
|
control: 'text',
|
|
38
|
-
description:
|
|
193
|
+
description: [
|
|
194
|
+
'Static guidance rendered below the label, above the input.',
|
|
195
|
+
'Automatically connected to the input via `aria-describedby`.',
|
|
196
|
+
'Target 20–60 characters. Good examples: `"Shown on the parent-facing report"`,',
|
|
197
|
+
'`"Up to 30 characters"`, `"Format: DD/MM/YYYY"`.',
|
|
198
|
+
'**Stays visible when `errorText` is set** — it is not replaced by the error.',
|
|
199
|
+
'**Mutually exclusive with `helperLinkText` / `helperLinkUrl`** per Confluence design spec.',
|
|
200
|
+
].join(' '),
|
|
201
|
+
table: {
|
|
202
|
+
type: { summary: 'ReactNode' },
|
|
203
|
+
},
|
|
39
204
|
},
|
|
40
|
-
|
|
41
|
-
control: '
|
|
42
|
-
|
|
43
|
-
|
|
205
|
+
helperLinkText: {
|
|
206
|
+
control: 'text',
|
|
207
|
+
description: [
|
|
208
|
+
'Text for an external documentation link rendered below the input.',
|
|
209
|
+
'**Both `helperLinkText` AND `helperLinkUrl` must be provided** — supplying only one renders nothing.',
|
|
210
|
+
'Use action-led copy: `"Learn more"`, `"View guidance"`, `"See SEN framework"`.',
|
|
211
|
+
'**Mutually exclusive with `fieldDescription`** per Confluence design spec.',
|
|
212
|
+
'**Known limitation:** the link has no `target="_blank"` and does not open in a new tab.',
|
|
213
|
+
].join(' '),
|
|
214
|
+
table: {
|
|
215
|
+
type: { summary: 'string' },
|
|
216
|
+
},
|
|
44
217
|
},
|
|
45
|
-
|
|
46
|
-
control: '
|
|
47
|
-
|
|
48
|
-
|
|
218
|
+
helperLinkUrl: {
|
|
219
|
+
control: 'text',
|
|
220
|
+
description: [
|
|
221
|
+
'URL for the helper link. **Must be paired with `helperLinkText`** — supplying only one renders nothing.',
|
|
222
|
+
'Typically points to Confluence guidance or external documentation.',
|
|
223
|
+
'**Known limitation:** no `target="_blank"` — does not open in a new tab.',
|
|
224
|
+
].join(' '),
|
|
225
|
+
table: {
|
|
226
|
+
type: { summary: 'string' },
|
|
227
|
+
},
|
|
49
228
|
},
|
|
50
|
-
|
|
51
|
-
control: '
|
|
52
|
-
description:
|
|
229
|
+
errorText: {
|
|
230
|
+
control: 'text',
|
|
231
|
+
description: [
|
|
232
|
+
'Error message rendered below the input with a `triangle-alert` icon.',
|
|
233
|
+
'Setting this automatically applies `hasError` and `aria-invalid` to the inner input,',
|
|
234
|
+
'and adds an `aria-describedby` reference to the error span.',
|
|
235
|
+
'Write errors as: what went wrong + how to fix it.',
|
|
236
|
+
'Example: `"Enter a date in DD/MM/YYYY format"`. One error per field.',
|
|
237
|
+
].join(' '),
|
|
238
|
+
table: {
|
|
239
|
+
type: { summary: 'string' },
|
|
240
|
+
},
|
|
53
241
|
},
|
|
54
|
-
|
|
242
|
+
className: {
|
|
55
243
|
control: 'text',
|
|
56
|
-
description:
|
|
244
|
+
description: [
|
|
245
|
+
'Additional CSS class(es) applied to the **root `div`** (`ds-form-field`), not the inner input.',
|
|
246
|
+
'Use for layout overrides only — do not use to style the input itself.',
|
|
247
|
+
].join(' '),
|
|
248
|
+
table: {
|
|
249
|
+
type: { summary: 'string' },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
inputProps: {
|
|
253
|
+
control: false,
|
|
254
|
+
description: [
|
|
255
|
+
'Props spread onto the inner input component. The shape narrows based on `inputType`.',
|
|
256
|
+
'**Do NOT set `hasError`, `aria-invalid`, or `aria-describedby` here** — FormField builds',
|
|
257
|
+
'these automatically. They are spread BEFORE `inputProps` and will be overridden.',
|
|
258
|
+
'The Controls panel cannot represent this discriminated union — configure it in the story code.',
|
|
259
|
+
].join(' '),
|
|
260
|
+
table: {
|
|
261
|
+
type: { summary: 'TextInputProps | TextAreaProps | NumberInputProps | TimeInputProps | ColourPickerDropdownProps | SelectDropdownInputProps | DatePickerProps | ComboboxProps' },
|
|
262
|
+
},
|
|
57
263
|
},
|
|
58
264
|
},
|
|
59
265
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
266
|
+
export default meta;
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Helper: attach a per-story description to docs
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
const withDescription = (story, description) => ({
|
|
271
|
+
...story,
|
|
272
|
+
parameters: {
|
|
273
|
+
...story.parameters,
|
|
274
|
+
docs: {
|
|
275
|
+
...story.parameters?.docs,
|
|
276
|
+
description: {
|
|
277
|
+
story: description,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Stories
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
export const Default = withDescription({
|
|
286
|
+
args: {
|
|
287
|
+
id: 'student-surname',
|
|
288
|
+
label: 'Surname',
|
|
289
|
+
inputType: 'text',
|
|
290
|
+
inputProps: {
|
|
291
|
+
placeholder: 'e.g. Nylund',
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
render: args => _jsx(FormField, { ...args }),
|
|
295
|
+
}, 'The interactive canvas. Every top-level prop is wired to the Controls panel below. Use the controls to toggle `errorText`, add a `fieldDescription`, switch `inputType`, or add a `helperLinkUrl`. **Note:** `inputProps` cannot be controlled from the panel — it represents a discriminated union that changes shape with `inputType`. Edit the story code to configure it.');
|
|
296
|
+
export const WithFieldDescription = withDescription({
|
|
297
|
+
parameters: {
|
|
298
|
+
docs: {
|
|
299
|
+
source: {
|
|
300
|
+
language: 'tsx',
|
|
301
|
+
code: `
|
|
302
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
303
|
+
|
|
304
|
+
function WithFieldDescriptionExample() {
|
|
305
|
+
return (
|
|
306
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
307
|
+
<FormField
|
|
308
|
+
id="preferred-name"
|
|
309
|
+
label="Preferred name (optional)"
|
|
310
|
+
fieldDescription="Shown on the parent-facing report card"
|
|
311
|
+
inputProps={{ placeholder: 'e.g. Rose' }}
|
|
312
|
+
/>
|
|
313
|
+
<FormField
|
|
314
|
+
id="medical-notes"
|
|
315
|
+
label="Medical conditions"
|
|
316
|
+
fieldDescription="Up to 30 characters. Visible to form tutors only."
|
|
317
|
+
inputType="textarea"
|
|
318
|
+
inputProps={{ placeholder: 'e.g. Asthma — uses inhaler before PE' }}
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
export default WithFieldDescriptionExample;
|
|
324
|
+
`.trim(),
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx(FormField, { id: "preferred-name", label: "Preferred name (optional)", fieldDescription: "Shown on the parent-facing report card", inputProps: { placeholder: 'e.g. Rose' } }), _jsx(FormField, { id: "medical-notes", label: "Medical conditions", fieldDescription: "Up to 30 characters. Visible to form tutors only.", inputType: "textarea", inputProps: { placeholder: 'e.g. Asthma — uses inhaler before PE' } })] })),
|
|
329
|
+
}, [
|
|
330
|
+
'A field description provides static contextual guidance below the label — it tells the user what',
|
|
331
|
+
'the field is for or what format to use. It appears above the input and stays visible even when',
|
|
332
|
+
'an error is shown.',
|
|
333
|
+
'',
|
|
334
|
+
'**Content guidance:** target 20–60 characters. Use it for format hints (`"DD/MM/YYYY"`) or',
|
|
335
|
+
'visibility notes (`"Shown on the parent-facing report"`). Do not use it for validation messages',
|
|
336
|
+
'— that is what `errorText` is for.',
|
|
337
|
+
'',
|
|
338
|
+
'**Never combine with `helperLinkText` / `helperLinkUrl`.** Per Confluence design spec these are',
|
|
339
|
+
'mutually exclusive — `fieldDescription` is for static guidance, the helper link is for external',
|
|
340
|
+
'documentation. Using both creates visual noise and breaks the design intent.',
|
|
341
|
+
].join('\n'));
|
|
342
|
+
export const WithHelperLink = withDescription({
|
|
343
|
+
parameters: {
|
|
344
|
+
docs: {
|
|
345
|
+
source: {
|
|
346
|
+
language: 'tsx',
|
|
347
|
+
code: `
|
|
348
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
349
|
+
|
|
350
|
+
function WithHelperLinkExample() {
|
|
351
|
+
return (
|
|
352
|
+
<FormField
|
|
353
|
+
id="sen-category"
|
|
354
|
+
label="SEN category"
|
|
355
|
+
inputType="selectDropdown"
|
|
356
|
+
inputProps={{
|
|
357
|
+
options: [
|
|
358
|
+
{ label: 'Cognition and learning', value: 'cognition-learning' },
|
|
359
|
+
{ label: 'Communication and interaction', value: 'communication-interaction' },
|
|
360
|
+
{ label: 'Social, emotional and mental health', value: 'semh' },
|
|
361
|
+
{ label: 'Sensory and physical needs', value: 'sensory-physical' },
|
|
362
|
+
],
|
|
363
|
+
placeholder: 'Select a category',
|
|
364
|
+
onSelectionChange: (values) => console.log(values),
|
|
365
|
+
}}
|
|
366
|
+
helperLinkText="View SEN framework"
|
|
367
|
+
helperLinkUrl="https://www.gov.uk/government/publications/send-code-of-practice-0-to-25"
|
|
368
|
+
/>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
export default WithHelperLinkExample;
|
|
372
|
+
`.trim(),
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
render: () => (_jsx("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)' }, children: _jsx(FormField, { id: "sen-category", label: "SEN category", inputType: "selectDropdown", inputProps: {
|
|
377
|
+
options: [
|
|
378
|
+
{ label: 'Cognition and learning', value: 'cognition-learning' },
|
|
379
|
+
{ label: 'Communication and interaction', value: 'communication-interaction' },
|
|
380
|
+
{ label: 'Social, emotional and mental health', value: 'semh' },
|
|
381
|
+
{ label: 'Sensory and physical needs', value: 'sensory-physical' },
|
|
382
|
+
],
|
|
383
|
+
placeholder: 'Select a category',
|
|
384
|
+
onSelectionChange: fn(),
|
|
385
|
+
}, helperLinkText: "View SEN framework", helperLinkUrl: "https://www.gov.uk/government/publications/send-code-of-practice-0-to-25" }) })),
|
|
386
|
+
}, [
|
|
387
|
+
'The helper link renders below the input as an action-led anchor with a `arrow-up-right` icon.',
|
|
388
|
+
'It connects to external documentation — DfE guidance, Confluence pages, or statutory frameworks.',
|
|
389
|
+
'',
|
|
390
|
+
'**Both props required:** `helperLinkText` and `helperLinkUrl` must both be set. Providing only',
|
|
391
|
+
'one renders nothing at all.',
|
|
392
|
+
'',
|
|
393
|
+
'**Known limitation:** the anchor has no `target="_blank"` — it does not open in a new tab.',
|
|
394
|
+
'Confluence design guidance says it should, but the current implementation navigates in the same',
|
|
395
|
+
'tab. Bear this in mind for links to external documentation.',
|
|
396
|
+
'',
|
|
397
|
+
'**Never combine with `fieldDescription`.** Per Confluence design spec these are mutually exclusive.',
|
|
398
|
+
'The helper link is for external documentation; `fieldDescription` is for inline static guidance.',
|
|
399
|
+
'',
|
|
400
|
+
'**Always provide `label` when using `helperLinkText`.** The link\'s `aria-label` is built as',
|
|
401
|
+
'`"${label} helper link"`. Omit `label` and screen readers announce `"undefined helper link"`.',
|
|
402
|
+
].join('\n'));
|
|
403
|
+
export const WithError = withDescription({
|
|
404
|
+
parameters: {
|
|
405
|
+
docs: {
|
|
406
|
+
source: {
|
|
407
|
+
language: 'tsx',
|
|
408
|
+
code: `
|
|
409
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
410
|
+
|
|
411
|
+
function WithErrorExample() {
|
|
412
|
+
return (
|
|
413
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
414
|
+
<FormField
|
|
415
|
+
id="date-of-birth"
|
|
416
|
+
label="Date of birth"
|
|
417
|
+
inputType="datePicker"
|
|
418
|
+
inputProps={{ onChange: (date) => console.log(date) }}
|
|
419
|
+
errorText="Enter a date in DD/MM/YYYY format"
|
|
420
|
+
/>
|
|
421
|
+
<FormField
|
|
422
|
+
id="class-size"
|
|
423
|
+
label="Class size"
|
|
424
|
+
inputType="number"
|
|
425
|
+
inputProps={{ min: 1, max: 35, placeholder: '30' }}
|
|
426
|
+
errorText="Class size must be between 1 and 35"
|
|
427
|
+
/>
|
|
428
|
+
</div>
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
export default WithErrorExample;
|
|
432
|
+
`.trim(),
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx(FormField, { id: "date-of-birth", label: "Date of birth", inputType: "datePicker", inputProps: { onChange: fn() }, errorText: "Enter a date in DD/MM/YYYY format" }), _jsx(FormField, { id: "class-size", label: "Class size", inputType: "number", inputProps: { min: 1, max: 35, placeholder: '30' }, errorText: "Class size must be between 1 and 35" })] })),
|
|
437
|
+
}, [
|
|
438
|
+
'The error state. Setting `errorText` automatically:',
|
|
439
|
+
'',
|
|
440
|
+
'- Renders a `triangle-alert` icon followed by the error message below the input',
|
|
441
|
+
'- Applies `hasError={true}` to the inner input (which adds `ds-input--error` classes)',
|
|
442
|
+
'- Sets `aria-invalid={true}` on the inner input',
|
|
443
|
+
'- Adds the error span to the input\'s `aria-describedby`',
|
|
444
|
+
'',
|
|
445
|
+
'**Write errors as:** what went wrong + how to fix it. `"Enter a date in DD/MM/YYYY format"` is',
|
|
446
|
+
'better than `"Invalid date"`. `"Class size must be between 1 and 35"` beats `"Invalid number"`.',
|
|
447
|
+
'',
|
|
448
|
+
'**One error per field.** FormField only accepts a single `errorText` string — render one clear,',
|
|
449
|
+
'actionable message rather than a list.',
|
|
450
|
+
'',
|
|
451
|
+
'**Do NOT set `hasError` or `aria-invalid` in `inputProps`** — FormField sets these for you.',
|
|
452
|
+
].join('\n'));
|
|
453
|
+
export const WithErrorAndFieldDescription = withDescription({
|
|
454
|
+
parameters: {
|
|
455
|
+
docs: {
|
|
456
|
+
source: {
|
|
457
|
+
language: 'tsx',
|
|
458
|
+
code: `
|
|
459
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
460
|
+
|
|
461
|
+
function WithErrorAndFieldDescriptionExample() {
|
|
462
|
+
return (
|
|
463
|
+
<FormField
|
|
464
|
+
id="student-upn"
|
|
465
|
+
label="Unique Pupil Number (UPN)"
|
|
466
|
+
fieldDescription="13-character code on previous school letter"
|
|
467
|
+
inputProps={{ placeholder: 'e.g. A123456789012' }}
|
|
468
|
+
errorText="Enter a valid 13-character UPN — check the letter from the previous school"
|
|
469
|
+
/>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
export default WithErrorAndFieldDescriptionExample;
|
|
473
|
+
`.trim(),
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
render: () => (_jsx("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)' }, children: _jsx(FormField, { id: "student-upn", label: "Unique Pupil Number (UPN)", fieldDescription: "13-character code on previous school letter", inputProps: { placeholder: 'e.g. A123456789012' }, errorText: "Enter a valid 13-character UPN \u2014 check the letter from the previous school" }) })),
|
|
478
|
+
}, [
|
|
479
|
+
'Both `fieldDescription` and `errorText` can coexist. The description stays visible above the',
|
|
480
|
+
'input as usual; the error appears below. Both are connected to the input via `aria-describedby`',
|
|
481
|
+
'so screen readers announce both the description context and the error.',
|
|
482
|
+
'',
|
|
483
|
+
'This is confirmed behaviour from the source: `fieldDescription` is never hidden when an error',
|
|
484
|
+
'is active. The design rationale is that the description provides context the user still needs',
|
|
485
|
+
'to fix the error correctly.',
|
|
486
|
+
'',
|
|
487
|
+
'The `aria-describedby` value will be `"{id}-description {id}-error"` — both IDs concatenated.',
|
|
488
|
+
].join('\n'));
|
|
489
|
+
export const WithErrorAndHelperLink = withDescription({
|
|
490
|
+
parameters: {
|
|
491
|
+
docs: {
|
|
492
|
+
source: {
|
|
493
|
+
language: 'tsx',
|
|
494
|
+
code: `
|
|
495
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
496
|
+
|
|
497
|
+
function WithErrorAndHelperLinkExample() {
|
|
498
|
+
return (
|
|
499
|
+
<FormField
|
|
500
|
+
id="attendance-code"
|
|
501
|
+
label="Attendance code"
|
|
502
|
+
inputProps={{ placeholder: 'e.g. B' }}
|
|
503
|
+
errorText="Enter a valid DfE attendance code (single letter A–Z)"
|
|
504
|
+
helperLinkText="View attendance code reference"
|
|
505
|
+
helperLinkUrl="https://www.gov.uk/government/publications/school-attendance"
|
|
506
|
+
/>
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
export default WithErrorAndHelperLinkExample;
|
|
510
|
+
`.trim(),
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
render: () => (_jsx("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)' }, children: _jsx(FormField, { id: "attendance-code", label: "Attendance code", inputProps: { placeholder: 'e.g. B' }, errorText: "Enter a valid DfE attendance code (single letter A\u2013Z)", helperLinkText: "View attendance code reference", helperLinkUrl: "https://www.gov.uk/government/publications/school-attendance" }) })),
|
|
515
|
+
}, [
|
|
516
|
+
'`errorText` and the helper link CAN appear together — this is the one exception where the',
|
|
517
|
+
'message container renders both elements simultaneously: the error appears first, the helper link',
|
|
518
|
+
'below it.',
|
|
519
|
+
'',
|
|
520
|
+
'This is useful when the validation failure is likely to require the user to look up a reference',
|
|
521
|
+
'— for example, an attendance code or a statutory identifier.',
|
|
522
|
+
].join('\n'));
|
|
523
|
+
export const Disabled = withDescription({
|
|
524
|
+
parameters: {
|
|
525
|
+
docs: {
|
|
526
|
+
source: {
|
|
527
|
+
language: 'tsx',
|
|
528
|
+
code: `
|
|
529
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
530
|
+
|
|
531
|
+
function DisabledExample() {
|
|
532
|
+
return (
|
|
533
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
534
|
+
<FormField
|
|
535
|
+
id="admissions-number"
|
|
536
|
+
label="Admissions number"
|
|
537
|
+
fieldDescription="Assigned by your local authority — cannot be edited"
|
|
538
|
+
inputProps={{ disabled: true, value: 'ADM-2024-00412', readOnly: true }}
|
|
539
|
+
/>
|
|
540
|
+
<FormField
|
|
541
|
+
id="school-urn"
|
|
542
|
+
label="School URN"
|
|
543
|
+
fieldDescription="Unique Reference Number — set by Ofsted"
|
|
544
|
+
inputProps={{ disabled: true, value: '123456' }}
|
|
545
|
+
/>
|
|
546
|
+
</div>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
export default DisabledExample;
|
|
550
|
+
`.trim(),
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx(FormField, { id: "admissions-number", label: "Admissions number", fieldDescription: "Assigned by your local authority \u2014 cannot be edited", inputProps: { disabled: true, value: 'ADM-2024-00412', readOnly: true } }), _jsx(FormField, { id: "school-urn", label: "School URN", fieldDescription: "Unique Reference Number \u2014 set by Ofsted", inputProps: { disabled: true, value: '123456' } })] })),
|
|
555
|
+
}, [
|
|
556
|
+
'Pass `disabled: true` inside `inputProps` to disable the inner input. The label and description',
|
|
557
|
+
'remain visible for context; the input becomes non-interactive and muted.',
|
|
558
|
+
'',
|
|
559
|
+
'**Disabled vs read-only:** disabled fields are not focusable and are not submitted with form data.',
|
|
560
|
+
'Read-only fields (`readOnly: true`) ARE focusable and their value IS submitted. Use read-only',
|
|
561
|
+
'when the user should be able to select and copy the value (e.g. a generated code or reference).',
|
|
562
|
+
'',
|
|
563
|
+
'**Do not show errors on disabled fields** — a user cannot fix a field they cannot edit.',
|
|
564
|
+
].join('\n'));
|
|
565
|
+
export const TextArea = withDescription({
|
|
566
|
+
parameters: {
|
|
567
|
+
docs: {
|
|
568
|
+
source: {
|
|
569
|
+
language: 'tsx',
|
|
570
|
+
code: `
|
|
571
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
572
|
+
|
|
573
|
+
function TextAreaExample() {
|
|
574
|
+
return (
|
|
575
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
576
|
+
<FormField
|
|
577
|
+
id="pastoral-notes"
|
|
578
|
+
label="Pastoral notes"
|
|
579
|
+
inputType="textarea"
|
|
580
|
+
fieldDescription="Visible to form tutor and head of year only"
|
|
581
|
+
inputProps={{
|
|
582
|
+
placeholder: 'e.g. Supporting bereavement — meeting with school counsellor Tuesdays',
|
|
583
|
+
rows: 3,
|
|
584
|
+
}}
|
|
585
|
+
/>
|
|
586
|
+
<FormField
|
|
587
|
+
id="medical-conditions"
|
|
588
|
+
label="Medical conditions (optional)"
|
|
589
|
+
inputType="textarea"
|
|
590
|
+
inputProps={{
|
|
591
|
+
placeholder: 'e.g. Type 1 diabetes — checks blood sugar before lunch',
|
|
592
|
+
rows: 3,
|
|
593
|
+
}}
|
|
594
|
+
/>
|
|
595
|
+
</div>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
export default TextAreaExample;
|
|
599
|
+
`.trim(),
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx(FormField, { id: "pastoral-notes", label: "Pastoral notes", inputType: "textarea", fieldDescription: "Visible to form tutor and head of year only", inputProps: {
|
|
604
|
+
placeholder: 'e.g. Supporting bereavement — meeting with school counsellor Tuesdays',
|
|
605
|
+
rows: 3,
|
|
606
|
+
} }), _jsx(FormField, { id: "medical-conditions", label: "Medical conditions (optional)", inputType: "textarea", inputProps: {
|
|
607
|
+
placeholder: 'e.g. Type 1 diabetes — checks blood sugar before lunch',
|
|
608
|
+
rows: 3,
|
|
609
|
+
} })] })),
|
|
610
|
+
}, [
|
|
611
|
+
'`inputType="textarea"` renders a `TextArea` — a multi-line input that grows automatically as',
|
|
612
|
+
'the user types (`autoSize={true}` is the default). The initial height is controlled by the',
|
|
613
|
+
'`rows` prop in `inputProps`.',
|
|
614
|
+
'',
|
|
615
|
+
'**Use for:** pastoral notes, medical conditions, free-text feedback, long descriptions.',
|
|
616
|
+
'',
|
|
617
|
+
'**Auto-sizing:** the textarea expands vertically as the user types — it does not scroll.',
|
|
618
|
+
'To opt out, pass `autoSize: false` in `inputProps`.',
|
|
619
|
+
'',
|
|
620
|
+
'**All shared wiring still applies:** `errorText`, `fieldDescription`, and `id` work identically.',
|
|
621
|
+
].join('\n'));
|
|
622
|
+
export const NumberInput = withDescription({
|
|
623
|
+
parameters: {
|
|
624
|
+
docs: {
|
|
625
|
+
source: {
|
|
626
|
+
language: 'tsx',
|
|
627
|
+
code: `
|
|
628
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
629
|
+
|
|
630
|
+
function NumberInputExample() {
|
|
631
|
+
return (
|
|
632
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
633
|
+
<FormField
|
|
634
|
+
id="class-capacity"
|
|
635
|
+
label="Class capacity"
|
|
636
|
+
inputType="number"
|
|
637
|
+
fieldDescription="Maximum number of students in this class"
|
|
638
|
+
inputProps={{
|
|
639
|
+
min: 1,
|
|
640
|
+
max: 35,
|
|
641
|
+
step: 1,
|
|
642
|
+
defaultValue: 30,
|
|
643
|
+
}}
|
|
644
|
+
/>
|
|
645
|
+
<FormField
|
|
646
|
+
id="fsm-percentage"
|
|
647
|
+
label="FSM threshold (%)"
|
|
648
|
+
inputType="number"
|
|
649
|
+
fieldDescription="Free school meals eligibility threshold"
|
|
650
|
+
inputProps={{
|
|
651
|
+
min: 0,
|
|
652
|
+
max: 100,
|
|
653
|
+
step: 5,
|
|
654
|
+
defaultValue: 20,
|
|
655
|
+
}}
|
|
656
|
+
/>
|
|
657
|
+
<FormField
|
|
658
|
+
id="absence-sessions"
|
|
659
|
+
label="Absence sessions"
|
|
660
|
+
inputType="number"
|
|
661
|
+
fieldDescription="Number of half-day sessions (no spinner)"
|
|
662
|
+
inputProps={{
|
|
663
|
+
min: 0,
|
|
664
|
+
max: 200,
|
|
665
|
+
disableSpinners: true,
|
|
666
|
+
placeholder: '0',
|
|
667
|
+
}}
|
|
668
|
+
/>
|
|
669
|
+
</div>
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
export default NumberInputExample;
|
|
673
|
+
`.trim(),
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx(FormField, { id: "class-capacity", label: "Class capacity", inputType: "number", fieldDescription: "Maximum number of students in this class", inputProps: {
|
|
678
|
+
min: 1,
|
|
679
|
+
max: 35,
|
|
680
|
+
step: 1,
|
|
681
|
+
defaultValue: 30,
|
|
682
|
+
} }), _jsx(FormField, { id: "fsm-percentage", label: "FSM threshold (%)", inputType: "number", fieldDescription: "Free school meals eligibility threshold", inputProps: {
|
|
683
|
+
min: 0,
|
|
684
|
+
max: 100,
|
|
685
|
+
step: 5,
|
|
686
|
+
defaultValue: 20,
|
|
687
|
+
} }), _jsx(FormField, { id: "absence-sessions", label: "Absence sessions", inputType: "number", fieldDescription: "Number of half-day sessions (no spinner)", inputProps: {
|
|
688
|
+
min: 0,
|
|
689
|
+
max: 200,
|
|
690
|
+
disableSpinners: true,
|
|
691
|
+
placeholder: '0',
|
|
692
|
+
} })] })),
|
|
693
|
+
}, [
|
|
694
|
+
'`inputType="number"` renders a `NumberInput` — a text input (`type="text"`, `inputMode="numeric"`)',
|
|
695
|
+
'with optional ± spinner buttons and decimal-safe arithmetic via `decimal.js`.',
|
|
696
|
+
'',
|
|
697
|
+
'**Do NOT pass `type` in `inputProps`.** `NumberInput` renders as `type="text"` internally to',
|
|
698
|
+
'avoid browser number input quirks. The component handles formatting and validation itself.',
|
|
699
|
+
'',
|
|
700
|
+
'**`min` / `max` / `step`:** on blur and on spinner click, the value is clamped to the range.',
|
|
701
|
+
'Pass `step={5}` for coarse increments (e.g. FSM thresholds at 5% steps).',
|
|
702
|
+
'',
|
|
703
|
+
'**`disableSpinners={true}`:** removes the ± buttons for fields where the user should type',
|
|
704
|
+
'directly rather than increment — large session counts, reference numbers.',
|
|
705
|
+
].join('\n'));
|
|
706
|
+
export const TimeInput = withDescription({
|
|
707
|
+
parameters: {
|
|
708
|
+
docs: {
|
|
709
|
+
source: {
|
|
710
|
+
language: 'tsx',
|
|
711
|
+
code: `
|
|
712
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
713
|
+
|
|
714
|
+
function TimeInputExample() {
|
|
715
|
+
return (
|
|
716
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
717
|
+
<FormField
|
|
718
|
+
id="registration-time"
|
|
719
|
+
label="Registration time"
|
|
720
|
+
inputType="time"
|
|
721
|
+
fieldDescription="Morning registration start time"
|
|
722
|
+
inputProps={{ defaultValue: '08:45' }}
|
|
723
|
+
/>
|
|
724
|
+
<FormField
|
|
725
|
+
id="period-start"
|
|
726
|
+
label="Period start"
|
|
727
|
+
inputType="time"
|
|
728
|
+
fieldDescription="Choose from preset timetable slots"
|
|
729
|
+
inputProps={{
|
|
730
|
+
options: ['08:00', '08:45', '09:30', '10:15', '11:00', '11:45', '12:30', '13:15', '14:00', '14:45', '15:30'],
|
|
731
|
+
placeholder: 'Select a period',
|
|
732
|
+
}}
|
|
733
|
+
/>
|
|
734
|
+
</div>
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
export default TimeInputExample;
|
|
738
|
+
`.trim(),
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx(FormField, { id: "registration-time", label: "Registration time", inputType: "time", fieldDescription: "Morning registration start time", inputProps: { defaultValue: '08:45' } }), _jsx(FormField, { id: "period-start", label: "Period start", inputType: "time", fieldDescription: "Choose from preset timetable slots", inputProps: {
|
|
743
|
+
options: ['08:00', '08:45', '09:30', '10:15', '11:00', '11:45', '12:30', '13:15', '14:00', '14:45', '15:30'],
|
|
744
|
+
placeholder: 'Select a period',
|
|
745
|
+
} })] })),
|
|
746
|
+
}, [
|
|
747
|
+
'`inputType="time"` renders a `TimeInput`, which has two modes:',
|
|
748
|
+
'',
|
|
749
|
+
'**Native mode** (no `options` prop): renders the browser\'s native `<input type="time">` with a',
|
|
750
|
+
'clock icon button. The value format is `"HH:MM"` or `"HH:MM:SS"`. Use `granularity="second"`',
|
|
751
|
+
'in `inputProps` to add seconds precision.',
|
|
752
|
+
'',
|
|
753
|
+
'**Combobox mode** (`options` array provided): replaces the native picker with a searchable',
|
|
754
|
+
'Combobox preset list — ideal for timetabling screens where slots are fixed.',
|
|
755
|
+
'Pass `options={["08:00", "08:45", ...]}` as an array of `"HH:MM"` strings.',
|
|
756
|
+
'',
|
|
757
|
+
'Use `onValueChange` in `inputProps` to receive value changes (accepts a string).',
|
|
758
|
+
'Use `onChange` if you need the native `ChangeEvent<HTMLInputElement>` interface.',
|
|
759
|
+
].join('\n'));
|
|
760
|
+
export const WithSelectDropdown = withDescription({
|
|
761
|
+
parameters: {
|
|
762
|
+
docs: {
|
|
763
|
+
source: {
|
|
764
|
+
language: 'tsx',
|
|
765
|
+
code: `
|
|
766
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
767
|
+
|
|
768
|
+
function WithSelectDropdownExample() {
|
|
769
|
+
return (
|
|
770
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
771
|
+
<FormField
|
|
772
|
+
id="year-group"
|
|
773
|
+
label="Year group"
|
|
774
|
+
inputType="selectDropdown"
|
|
775
|
+
inputProps={{
|
|
776
|
+
options: [
|
|
777
|
+
{ label: 'Year 7', value: 'y7' },
|
|
778
|
+
{ label: 'Year 8', value: 'y8' },
|
|
779
|
+
{ label: 'Year 9', value: 'y9' },
|
|
780
|
+
{ label: 'Year 10', value: 'y10' },
|
|
781
|
+
{ label: 'Year 11', value: 'y11' },
|
|
782
|
+
],
|
|
783
|
+
placeholder: 'Select year group',
|
|
784
|
+
onSelectionChange: (values) => console.log(values),
|
|
785
|
+
}}
|
|
786
|
+
/>
|
|
787
|
+
<FormField
|
|
788
|
+
id="subject-filter"
|
|
789
|
+
label="Subjects taught (optional)"
|
|
790
|
+
inputType="selectDropdown"
|
|
791
|
+
fieldDescription="Select all that apply"
|
|
792
|
+
inputProps={{
|
|
793
|
+
multiple: true,
|
|
794
|
+
options: [
|
|
795
|
+
{ label: 'Maths', value: 'maths' },
|
|
796
|
+
{ label: 'English', value: 'english' },
|
|
797
|
+
{ label: 'Science', value: 'science' },
|
|
798
|
+
{ label: 'History', value: 'history' },
|
|
799
|
+
{ label: 'Geography', value: 'geography' },
|
|
800
|
+
{ label: 'Art', value: 'art' },
|
|
801
|
+
{ label: 'Music', value: 'music' },
|
|
802
|
+
{ label: 'Physical Education', value: 'pe' },
|
|
803
|
+
],
|
|
804
|
+
placeholder: 'Select subjects',
|
|
805
|
+
onSelectionChange: (values) => console.log(values),
|
|
806
|
+
}}
|
|
807
|
+
/>
|
|
808
|
+
</div>
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
export default WithSelectDropdownExample;
|
|
812
|
+
`.trim(),
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx(FormField, { id: "year-group", label: "Year group", inputType: "selectDropdown", inputProps: {
|
|
817
|
+
options: [
|
|
818
|
+
{ label: 'Year 7', value: 'y7' },
|
|
819
|
+
{ label: 'Year 8', value: 'y8' },
|
|
820
|
+
{ label: 'Year 9', value: 'y9' },
|
|
821
|
+
{ label: 'Year 10', value: 'y10' },
|
|
822
|
+
{ label: 'Year 11', value: 'y11' },
|
|
823
|
+
],
|
|
824
|
+
placeholder: 'Select year group',
|
|
82
825
|
onSelectionChange: fn(),
|
|
83
|
-
} }), _jsx(FormField, { id: "
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
826
|
+
} }), _jsx(FormField, { id: "subject-filter", label: "Subjects taught (optional)", inputType: "selectDropdown", fieldDescription: "Select all that apply", inputProps: {
|
|
827
|
+
multiple: true,
|
|
828
|
+
options: [
|
|
829
|
+
{ label: 'Maths', value: 'maths' },
|
|
830
|
+
{ label: 'English', value: 'english' },
|
|
831
|
+
{ label: 'Science', value: 'science' },
|
|
832
|
+
{ label: 'History', value: 'history' },
|
|
833
|
+
{ label: 'Geography', value: 'geography' },
|
|
834
|
+
{ label: 'Art', value: 'art' },
|
|
835
|
+
{ label: 'Music', value: 'music' },
|
|
836
|
+
{ label: 'Physical Education', value: 'pe' },
|
|
837
|
+
],
|
|
838
|
+
placeholder: 'Select subjects',
|
|
839
|
+
onSelectionChange: fn(),
|
|
840
|
+
} })] })),
|
|
841
|
+
}, [
|
|
842
|
+
'`inputType="selectDropdown"` renders a `SelectDropdown` — a Dropdown-based button trigger with',
|
|
843
|
+
'a list of selectable options. Supports single select (default) and multi-select (`multiple={true}`).',
|
|
844
|
+
'',
|
|
845
|
+
'**Options shape:** each option is `{ label: string; value: string; group?: string }`. Grouped',
|
|
846
|
+
'options render a bold header row above each group.',
|
|
847
|
+
'',
|
|
848
|
+
'**Multi-select:** pass `multiple={true}` in `inputProps`. The trigger shows the count of',
|
|
849
|
+
'selected values when more than one is chosen.',
|
|
850
|
+
'',
|
|
851
|
+
'**Controlled vs uncontrolled:** pass `selectedValues` to control the selection externally;',
|
|
852
|
+
'omit it for uncontrolled (internal state). Use `initialSelectedValues` to set a default.',
|
|
853
|
+
].join('\n'));
|
|
854
|
+
export const WithCombobox = withDescription({
|
|
855
|
+
parameters: {
|
|
856
|
+
docs: {
|
|
857
|
+
source: {
|
|
858
|
+
language: 'tsx',
|
|
859
|
+
code: `
|
|
860
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
861
|
+
|
|
862
|
+
function WithComboboxExample() {
|
|
863
|
+
const tutorOptions = [
|
|
864
|
+
{ value: 'alice', label: 'Alice Johnson', iconName: 'user' },
|
|
865
|
+
{ value: 'bob', label: 'Bob Smith', iconName: 'user' },
|
|
866
|
+
{ value: 'charlie', label: 'Charlie Brown', iconName: 'user' },
|
|
867
|
+
{ value: 'diana', label: 'Diana Prince', iconName: 'user' },
|
|
868
|
+
];
|
|
869
|
+
|
|
870
|
+
return (
|
|
871
|
+
<FormField
|
|
872
|
+
id="form-tutor"
|
|
873
|
+
label="Form tutor"
|
|
874
|
+
inputType="combobox"
|
|
875
|
+
fieldDescription="Assigned tutor for this form group"
|
|
876
|
+
inputProps={{
|
|
877
|
+
options: tutorOptions,
|
|
878
|
+
placeholder: 'Search by name...',
|
|
879
|
+
onValueChange: (values) => console.log(values),
|
|
880
|
+
}}
|
|
881
|
+
/>
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
export default WithComboboxExample;
|
|
885
|
+
`.trim(),
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
render: () => (_jsx("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)' }, children: _jsx(FormField, { id: "form-tutor", label: "Form tutor", inputType: "combobox", fieldDescription: "Assigned tutor for this form group", inputProps: {
|
|
91
890
|
options: comboboxPeopleOptions,
|
|
92
|
-
placeholder: 'Search
|
|
891
|
+
placeholder: 'Search by name...',
|
|
93
892
|
onValueChange: fn(),
|
|
94
893
|
} }) })),
|
|
95
|
-
}
|
|
96
|
-
|
|
894
|
+
}, [
|
|
895
|
+
'`inputType="combobox"` renders a `Combobox` — a searchable, type-ahead selector.',
|
|
896
|
+
'The user types to filter the options list; results highlight matching characters.',
|
|
897
|
+
'',
|
|
898
|
+
'**Use for:** any "select a person" scenario — form tutor assignment, cover teacher lookup,',
|
|
899
|
+
'parent/guardian search. Works with the `comboboxPeopleOptions` mock for stories.',
|
|
900
|
+
'',
|
|
901
|
+
'**`onValueChange`** in `inputProps` receives the selected value as `string[]` (Combobox is',
|
|
902
|
+
'inherently multi-value even in single-select mode — expect an array of one).',
|
|
903
|
+
'',
|
|
904
|
+
'**Options shape:** `{ value: string; label: string; iconName?: IconName; group?: string }`.',
|
|
905
|
+
'Provide `iconName="user"` for people pickers to render the person icon.',
|
|
906
|
+
'',
|
|
907
|
+
'**Note:** `fieldDescription` is shown here alongside the combobox — this is intentional.',
|
|
908
|
+
'Do not add `helperLinkText` when `fieldDescription` is already present.',
|
|
909
|
+
].join('\n'));
|
|
910
|
+
export const WithDatePicker = withDescription({
|
|
911
|
+
parameters: {
|
|
912
|
+
docs: {
|
|
913
|
+
source: {
|
|
914
|
+
language: 'tsx',
|
|
915
|
+
code: `
|
|
916
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
917
|
+
|
|
918
|
+
function WithDatePickerExample() {
|
|
919
|
+
return (
|
|
920
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
921
|
+
<FormField
|
|
922
|
+
id="date-of-birth"
|
|
923
|
+
label="Date of birth"
|
|
924
|
+
inputType="datePicker"
|
|
925
|
+
fieldDescription="Format: DD/MM/YYYY"
|
|
926
|
+
inputProps={{ onChange: (date) => console.log(date) }}
|
|
927
|
+
/>
|
|
928
|
+
<FormField
|
|
929
|
+
id="enrolment-date"
|
|
930
|
+
label="Enrolment date"
|
|
931
|
+
inputType="datePicker"
|
|
932
|
+
inputProps={{
|
|
933
|
+
onChange: (date) => console.log(date),
|
|
934
|
+
displayFormat: 'default',
|
|
935
|
+
}}
|
|
936
|
+
/>
|
|
937
|
+
</div>
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
export default WithDatePickerExample;
|
|
941
|
+
`.trim(),
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx(FormField, { id: "date-of-birth", label: "Date of birth", inputType: "datePicker", fieldDescription: "Format: DD/MM/YYYY", inputProps: { onChange: fn() } }), _jsx(FormField, { id: "enrolment-date", label: "Enrolment date", inputType: "datePicker", inputProps: {
|
|
946
|
+
onChange: fn(),
|
|
947
|
+
displayFormat: 'default',
|
|
948
|
+
} })] })),
|
|
949
|
+
}, [
|
|
950
|
+
'`inputType="datePicker"` renders a `DatePicker` — a text input that opens a calendar popover',
|
|
951
|
+
'when clicked. The user can type the date directly or pick from the calendar.',
|
|
952
|
+
'',
|
|
953
|
+
'**`onChange`** in `inputProps` receives a `Date | undefined` — not a string. Format it for',
|
|
954
|
+
'display or API submission in your application layer.',
|
|
955
|
+
'',
|
|
956
|
+
'**`displayFormat`** controls how the date is shown in the text input. Defaults to `"default"`',
|
|
957
|
+
'(locale-appropriate). Check the DatePicker stories for the full format reference.',
|
|
958
|
+
'',
|
|
959
|
+
'**Portalled component:** the calendar popover is rendered via a portal. Do not wrap this story',
|
|
960
|
+
'or any component using FormField with `overflow: hidden` or `position: relative` on a tight',
|
|
961
|
+
'container — it will clip the calendar.',
|
|
962
|
+
].join('\n'));
|
|
963
|
+
export const OptionalAndRequiredFields = withDescription({
|
|
964
|
+
parameters: {
|
|
965
|
+
docs: {
|
|
966
|
+
source: {
|
|
967
|
+
language: 'tsx',
|
|
968
|
+
code: `
|
|
969
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
970
|
+
|
|
971
|
+
function OptionalAndRequiredFieldsExample() {
|
|
972
|
+
return (
|
|
973
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
974
|
+
<FormField
|
|
975
|
+
id="student-forename"
|
|
976
|
+
label="First name"
|
|
977
|
+
inputProps={{ placeholder: 'e.g. Rose' }}
|
|
978
|
+
/>
|
|
979
|
+
<FormField
|
|
980
|
+
id="student-middle-name"
|
|
981
|
+
label="Middle name (optional)"
|
|
982
|
+
inputProps={{ placeholder: 'e.g. Marie' }}
|
|
983
|
+
/>
|
|
984
|
+
<FormField
|
|
985
|
+
id="student-surname"
|
|
986
|
+
label="Surname"
|
|
987
|
+
inputProps={{ placeholder: 'e.g. Nylund' }}
|
|
988
|
+
/>
|
|
989
|
+
<FormField
|
|
990
|
+
id="student-preferred-name"
|
|
991
|
+
label="Preferred name (optional)"
|
|
992
|
+
fieldDescription="Used in class registers and parent communications"
|
|
993
|
+
inputProps={{ placeholder: 'e.g. Rosie' }}
|
|
994
|
+
/>
|
|
995
|
+
<FormField
|
|
996
|
+
id="student-legal-name"
|
|
997
|
+
label="Legal name"
|
|
998
|
+
fieldDescription="As it appears on official documents"
|
|
999
|
+
inputProps={{ placeholder: 'e.g. Rose Marie Nylund' }}
|
|
1000
|
+
/>
|
|
1001
|
+
</div>
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
export default OptionalAndRequiredFieldsExample;
|
|
1005
|
+
`.trim(),
|
|
1006
|
+
},
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx(FormField, { id: "student-forename", label: "First name", inputProps: { placeholder: 'e.g. Rose' } }), _jsx(FormField, { id: "student-middle-name", label: "Middle name (optional)", inputProps: { placeholder: 'e.g. Marie' } }), _jsx(FormField, { id: "student-surname", label: "Surname", inputProps: { placeholder: 'e.g. Nylund' } }), _jsx(FormField, { id: "student-preferred-name", label: "Preferred name (optional)", fieldDescription: "Used in class registers and parent communications", inputProps: { placeholder: 'e.g. Rosie' } }), _jsx(FormField, { id: "student-legal-name", label: "Legal name", fieldDescription: "As it appears on official documents", inputProps: { placeholder: 'e.g. Rose Marie Nylund' } })] })),
|
|
1010
|
+
}, [
|
|
1011
|
+
'Arbor uses the **label suffix convention** for optional fields — no asterisk, no `required` prop.',
|
|
1012
|
+
'Append `"(optional)"` directly to the label text: `label="Middle name (optional)"`.',
|
|
1013
|
+
'',
|
|
1014
|
+
'Fields without this suffix are implicitly required. This is a deliberate Confluence design',
|
|
1015
|
+
'decision: marking optional fields (the minority) is less visually noisy than marking required',
|
|
1016
|
+
'fields (the majority) with an asterisk that users learn to ignore anyway.',
|
|
1017
|
+
'',
|
|
1018
|
+
'**Never** use a red asterisk (`*`) required indicator — this is not in the Arbor design system.',
|
|
1019
|
+
'**Never** use placeholder text as a substitute for a label, even for optional fields.',
|
|
1020
|
+
].join('\n'));
|
|
1021
|
+
export const WithColourPicker = withDescription({
|
|
1022
|
+
parameters: {
|
|
1023
|
+
docs: {
|
|
1024
|
+
source: {
|
|
1025
|
+
language: 'tsx',
|
|
1026
|
+
code: `
|
|
1027
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
1028
|
+
|
|
1029
|
+
function WithColourPickerExample() {
|
|
1030
|
+
return (
|
|
1031
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
1032
|
+
<FormField
|
|
1033
|
+
id="form-group-colour"
|
|
1034
|
+
label="Form group colour"
|
|
1035
|
+
inputType="colourPicker"
|
|
1036
|
+
fieldDescription="Shown on the timetable and group list"
|
|
1037
|
+
inputProps={{ onChange: (result) => console.log(result.hex) }}
|
|
1038
|
+
/>
|
|
1039
|
+
<FormField
|
|
1040
|
+
id="subject-colour"
|
|
1041
|
+
label="Subject colour (optional)"
|
|
1042
|
+
inputType="colourPicker"
|
|
1043
|
+
inputProps={{
|
|
1044
|
+
value: '#e63946',
|
|
1045
|
+
onChange: (result) => console.log(result.hex),
|
|
1046
|
+
}}
|
|
1047
|
+
/>
|
|
1048
|
+
</div>
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
export default WithColourPickerExample;
|
|
1052
|
+
`.trim(),
|
|
1053
|
+
},
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1056
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx(FormField, { id: "form-group-colour", label: "Form group colour", inputType: "colourPicker", fieldDescription: "Shown on the timetable and group list", inputProps: { onChange: fn() } }), _jsx(FormField, { id: "subject-colour", label: "Subject colour (optional)", inputType: "colourPicker", inputProps: {
|
|
1057
|
+
value: '#e63946',
|
|
1058
|
+
onChange: fn(),
|
|
1059
|
+
} })] })),
|
|
1060
|
+
}, [
|
|
1061
|
+
'`inputType="colourPicker"` renders a `ColourPickerDropdown` — a button trigger that opens a',
|
|
1062
|
+
'Sketch colour picker in a Dropdown. The selected colour is previewed in the trigger button.',
|
|
1063
|
+
'',
|
|
1064
|
+
'**`onChange`** in `inputProps` receives a `ColorResult` object from `@uiw/color-convert` —',
|
|
1065
|
+
'use `.hex` to get the hex string: `onChange={(result) => setColour(result.hex)}`.',
|
|
1066
|
+
'',
|
|
1067
|
+
'**`value`** in `inputProps` accepts a hex string (e.g. `"#3cad51"`). Defaults to Arbor green',
|
|
1068
|
+
'(`"#3cad51"`) if not provided.',
|
|
1069
|
+
'',
|
|
1070
|
+
'**Portalled component:** the colour picker popover is rendered via a portal. Do not wrap',
|
|
1071
|
+
'with `overflow: hidden` — it will clip the picker.',
|
|
1072
|
+
].join('\n'));
|
|
1073
|
+
export const CompleteStudentForm = withDescription({
|
|
1074
|
+
parameters: {
|
|
1075
|
+
docs: {
|
|
1076
|
+
source: {
|
|
1077
|
+
language: 'tsx',
|
|
1078
|
+
code: `
|
|
1079
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
1080
|
+
|
|
1081
|
+
function CompleteStudentFormExample() {
|
|
1082
|
+
const tutorOptions = [
|
|
1083
|
+
{ value: 'alice', label: 'Alice Johnson', iconName: 'user' },
|
|
1084
|
+
{ value: 'bob', label: 'Bob Smith', iconName: 'user' },
|
|
1085
|
+
{ value: 'charlie', label: 'Charlie Brown', iconName: 'user' },
|
|
1086
|
+
];
|
|
1087
|
+
|
|
1088
|
+
return (
|
|
1089
|
+
<div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
1090
|
+
<p style={{ margin: 0, fontWeight: 'var(--font-weight-semi-bold)', color: 'var(--color-grey-900)' }}>
|
|
1091
|
+
New student record
|
|
1092
|
+
</p>
|
|
1093
|
+
<FormField id="student-first-name" label="First name" inputProps={{ placeholder: 'e.g. Dorothy' }} />
|
|
1094
|
+
<FormField id="student-surname" label="Surname" inputProps={{ placeholder: 'e.g. Zbornak' }} />
|
|
1095
|
+
<FormField
|
|
1096
|
+
id="student-preferred-name"
|
|
1097
|
+
label="Preferred name (optional)"
|
|
1098
|
+
fieldDescription="Used in registers and parent communications"
|
|
1099
|
+
inputProps={{ placeholder: 'e.g. Dot' }}
|
|
1100
|
+
/>
|
|
1101
|
+
<FormField
|
|
1102
|
+
id="student-dob"
|
|
1103
|
+
label="Date of birth"
|
|
1104
|
+
inputType="datePicker"
|
|
1105
|
+
fieldDescription="Format: DD/MM/YYYY"
|
|
1106
|
+
inputProps={{ onChange: (date) => console.log(date) }}
|
|
1107
|
+
/>
|
|
1108
|
+
<FormField
|
|
1109
|
+
id="student-year-group"
|
|
1110
|
+
label="Year group"
|
|
1111
|
+
inputType="selectDropdown"
|
|
1112
|
+
inputProps={{
|
|
1113
|
+
options: [
|
|
1114
|
+
{ label: 'Year 7', value: 'y7' },
|
|
1115
|
+
{ label: 'Year 8', value: 'y8' },
|
|
1116
|
+
{ label: 'Year 9', value: 'y9' },
|
|
1117
|
+
{ label: 'Year 10', value: 'y10' },
|
|
1118
|
+
{ label: 'Year 11', value: 'y11' },
|
|
1119
|
+
],
|
|
1120
|
+
placeholder: 'Select year group',
|
|
1121
|
+
onSelectionChange: (values) => console.log(values),
|
|
1122
|
+
}}
|
|
1123
|
+
/>
|
|
1124
|
+
<FormField
|
|
1125
|
+
id="student-form-tutor"
|
|
1126
|
+
label="Form tutor"
|
|
1127
|
+
inputType="combobox"
|
|
1128
|
+
inputProps={{
|
|
1129
|
+
options: tutorOptions,
|
|
1130
|
+
placeholder: 'Search by name...',
|
|
1131
|
+
onValueChange: (values) => console.log(values),
|
|
1132
|
+
}}
|
|
1133
|
+
/>
|
|
1134
|
+
<FormField
|
|
1135
|
+
id="student-registration-time"
|
|
1136
|
+
label="Registration time"
|
|
1137
|
+
inputType="time"
|
|
1138
|
+
inputProps={{ defaultValue: '08:45' }}
|
|
1139
|
+
/>
|
|
1140
|
+
<FormField
|
|
1141
|
+
id="student-class-size"
|
|
1142
|
+
label="Class size"
|
|
1143
|
+
inputType="number"
|
|
1144
|
+
inputProps={{ min: 1, max: 35, defaultValue: 30 }}
|
|
1145
|
+
/>
|
|
1146
|
+
<FormField
|
|
1147
|
+
id="student-group-colour"
|
|
1148
|
+
label="Form group colour"
|
|
1149
|
+
inputType="colourPicker"
|
|
1150
|
+
inputProps={{ onChange: (result) => console.log(result.hex) }}
|
|
1151
|
+
/>
|
|
1152
|
+
<FormField
|
|
1153
|
+
id="student-pastoral-notes"
|
|
1154
|
+
label="Pastoral notes (optional)"
|
|
1155
|
+
inputType="textarea"
|
|
1156
|
+
fieldDescription="Visible to form tutor and SENCO only"
|
|
1157
|
+
inputProps={{
|
|
1158
|
+
placeholder: 'e.g. Transitioning from primary — mild anxiety in crowded spaces',
|
|
1159
|
+
rows: 3,
|
|
1160
|
+
}}
|
|
1161
|
+
/>
|
|
1162
|
+
</div>
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
export default CompleteStudentFormExample;
|
|
1166
|
+
`.trim(),
|
|
1167
|
+
},
|
|
1168
|
+
},
|
|
1169
|
+
},
|
|
1170
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, fontWeight: 'var(--font-weight-semi-bold)', color: 'var(--color-grey-900)' }, children: "New student record" }), _jsx(FormField, { id: "student-first-name", label: "First name", inputProps: { placeholder: 'e.g. Dorothy' } }), _jsx(FormField, { id: "student-surname", label: "Surname", inputProps: { placeholder: 'e.g. Zbornak' } }), _jsx(FormField, { id: "student-preferred-name", label: "Preferred name (optional)", fieldDescription: "Used in registers and parent communications", inputProps: { placeholder: 'e.g. Dot' } }), _jsx(FormField, { id: "student-dob", label: "Date of birth", inputType: "datePicker", fieldDescription: "Format: DD/MM/YYYY", inputProps: { onChange: fn() } }), _jsx(FormField, { id: "student-year-group", label: "Year group", inputType: "selectDropdown", inputProps: {
|
|
1171
|
+
options: [
|
|
1172
|
+
{ label: 'Year 7', value: 'y7' },
|
|
1173
|
+
{ label: 'Year 8', value: 'y8' },
|
|
1174
|
+
{ label: 'Year 9', value: 'y9' },
|
|
1175
|
+
{ label: 'Year 10', value: 'y10' },
|
|
1176
|
+
{ label: 'Year 11', value: 'y11' },
|
|
1177
|
+
],
|
|
1178
|
+
placeholder: 'Select year group',
|
|
1179
|
+
onSelectionChange: fn(),
|
|
1180
|
+
} }), _jsx(FormField, { id: "student-form-tutor", label: "Form tutor", inputType: "combobox", inputProps: {
|
|
1181
|
+
options: comboboxPeopleOptions,
|
|
1182
|
+
placeholder: 'Search by name...',
|
|
1183
|
+
onValueChange: fn(),
|
|
1184
|
+
} }), _jsx(FormField, { id: "student-registration-time", label: "Registration time", inputType: "time", inputProps: { defaultValue: '08:45' } }), _jsx(FormField, { id: "student-class-size", label: "Class size", inputType: "number", inputProps: { min: 1, max: 35, defaultValue: 30 } }), _jsx(FormField, { id: "student-group-colour", label: "Form group colour", inputType: "colourPicker", inputProps: { onChange: fn() } }), _jsx(FormField, { id: "student-pastoral-notes", label: "Pastoral notes (optional)", inputType: "textarea", fieldDescription: "Visible to form tutor and SENCO only", inputProps: {
|
|
1185
|
+
placeholder: 'e.g. Transitioning from primary — mild anxiety in crowded spaces',
|
|
1186
|
+
rows: 3,
|
|
1187
|
+
} })] })),
|
|
1188
|
+
}, [
|
|
1189
|
+
'A realistic school record form using all eight `inputType` values together. This shows how',
|
|
1190
|
+
'FormField composes into a real data-entry screen — consistent spacing (`var(--spacing-large)`',
|
|
1191
|
+
'between fields), a shared `maxWidth` container, and school management domain content throughout.',
|
|
1192
|
+
'',
|
|
1193
|
+
'Notice the conventions in action:',
|
|
1194
|
+
'- Implicitly required fields: no suffix (First name, Surname, Date of birth)',
|
|
1195
|
+
'- Explicitly optional fields: `"(optional)"` suffix (Preferred name, Pastoral notes)',
|
|
1196
|
+
'- `fieldDescription` on fields where format or visibility context helps the user',
|
|
1197
|
+
'- `min` / `max` on the number input to constrain class size',
|
|
1198
|
+
'- `comboboxPeopleOptions` mock for the form tutor Combobox',
|
|
1199
|
+
'',
|
|
1200
|
+
'This replaces the old `FormExample` story which used `"1rem"` gaps and non-school copy.',
|
|
1201
|
+
].join('\n'));
|
|
97
1202
|
//# sourceMappingURL=FormField.stories.js.map
|