@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,90 +1,657 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import {
|
|
3
|
+
Controls,
|
|
4
|
+
Heading as DocHeading,
|
|
5
|
+
Markdown,
|
|
6
|
+
Primary as DocPrimary,
|
|
7
|
+
Stories,
|
|
8
|
+
Subtitle,
|
|
9
|
+
Title,
|
|
10
|
+
} from '@storybook/addon-docs/blocks';
|
|
11
|
+
import { useEffect, useState } from 'react';
|
|
12
|
+
import { Button } from 'Components/button/Button';
|
|
2
13
|
import { Progress } from './Progress';
|
|
3
14
|
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Docs page content
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
const DESCRIPTION_INTRO = [
|
|
20
|
+
'Progress is a horizontal progress bar for measurable operations — it fills from left to right as a',
|
|
21
|
+
'known quantity advances toward a known total. Built on [Radix UI Progress](https://www.radix-ui.com/primitives/docs/components/progress)',
|
|
22
|
+
'for reliable ARIA semantics.',
|
|
23
|
+
].join('\n');
|
|
24
|
+
|
|
25
|
+
const USAGE_GUIDANCE = [
|
|
26
|
+
'### When to use',
|
|
27
|
+
'',
|
|
28
|
+
'- **File uploads and imports** — "3 of 5 files uploaded", where the total count is known',
|
|
29
|
+
'- **Multi-step wizards** — completed steps out of total steps',
|
|
30
|
+
'- **Long-running operations** — any process where progress is calculable and meaningful to show',
|
|
31
|
+
'',
|
|
32
|
+
'---',
|
|
33
|
+
'',
|
|
34
|
+
'### When NOT to use',
|
|
35
|
+
'',
|
|
36
|
+
'| Situation | Use instead |',
|
|
37
|
+
'|---|---|',
|
|
38
|
+
'| Unknown-duration operation | Spinner (Icon `name="loader"` with CSS animation) or estimate a `value` |',
|
|
39
|
+
'| Full page / section loading placeholder | Skeleton |',
|
|
40
|
+
'| Operation completed message | [`Banner`](?path=/docs/components-banner--docs) (success state) or [`Toast`](?path=/docs/components-toast--docs) (transient) |',
|
|
41
|
+
'| Decorative loading bar without a value | Do not — violates WCAG 2.1 4.1.2 |',
|
|
42
|
+
].join('\n');
|
|
43
|
+
|
|
44
|
+
const DEVELOPER_NOTES = [
|
|
45
|
+
'### Critical usage patterns',
|
|
46
|
+
'',
|
|
47
|
+
'**`aria-label` or `aria-labelledby` is REQUIRED.** Radix renders `role="progressbar"` — a progressbar',
|
|
48
|
+
'with no accessible name is a WCAG 2.1 failure (4.1.2 Name, Role, Value). Always pass one of these.',
|
|
49
|
+
'',
|
|
50
|
+
'**Width is 100% of parent.** Never size `<Progress>` directly. Put a width or `maxWidth` on the',
|
|
51
|
+
'wrapper element and let Progress fill it.',
|
|
52
|
+
'',
|
|
53
|
+
'**Pass raw values, not percentages.** If you have "3 of 5 items", pass `value={3}` and `max={5}` —',
|
|
54
|
+
'the component calculates the percentage internally. Do not pre-calculate to `value={60}` and `max={100}`.',
|
|
55
|
+
'',
|
|
56
|
+
'**`value={null}` does NOT produce a visible indeterminate animation.** Radix will set',
|
|
57
|
+
'`data-state="indeterminate"` on the root, but this component has no CSS animation defined for that',
|
|
58
|
+
'state — the indicator bar just disappears. See the IndeterminateAntiPattern story. Use a Spinner instead.',
|
|
59
|
+
'',
|
|
60
|
+
'**Built on [Radix UI Progress](https://www.radix-ui.com/primitives/docs/components/progress).** Props not listed above are passed through to the Radix primitive — see the Radix docs for the full API.',
|
|
61
|
+
'',
|
|
62
|
+
'---',
|
|
63
|
+
'',
|
|
64
|
+
'### Accessibility',
|
|
65
|
+
'',
|
|
66
|
+
'- `aria-label` (or `aria-labelledby` pointing to a visible element) is required on every Progress',
|
|
67
|
+
'- `getValueLabel` shapes the screen-reader announcement — it becomes `aria-valuetext`. Without it,',
|
|
68
|
+
' Radix falls back to a rounded percentage string (e.g. `"50%"`). Provide a custom function when you',
|
|
69
|
+
' need a fuller sentence, e.g. `(v, m) => v + " of " + m + " files uploaded"`.',
|
|
70
|
+
'- Radix auto-manages `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, and `aria-valuetext`.',
|
|
71
|
+
' Do not set these yourself.',
|
|
72
|
+
'- Colour alone is not sufficient — always pair the bar with a visible label showing the value or percentage.',
|
|
73
|
+
'',
|
|
74
|
+
'---',
|
|
75
|
+
'',
|
|
76
|
+
'### TypeScript types',
|
|
77
|
+
'',
|
|
78
|
+
'```ts',
|
|
79
|
+
"import { Progress } from '@arbor-education/design-system.components';",
|
|
80
|
+
'',
|
|
81
|
+
'function MyProgress(props: Progress.Props) { ... }',
|
|
82
|
+
'```',
|
|
83
|
+
'',
|
|
84
|
+
'| Type | Description |',
|
|
85
|
+
'|---|---|',
|
|
86
|
+
'| `Progress.Props` | Full props interface |',
|
|
87
|
+
].join('\n');
|
|
88
|
+
|
|
89
|
+
const RELATED_COMPONENTS = [
|
|
90
|
+
'## Related components',
|
|
91
|
+
'',
|
|
92
|
+
'[Banner](?path=/docs/components-banner--docs) · [Toast](?path=/docs/components-toast--docs) · [Icon](?path=/docs/components-icon--docs) · [Badge](?path=/docs/components-badge--docs)',
|
|
93
|
+
].join('\n');
|
|
94
|
+
|
|
95
|
+
const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
|
|
96
|
+
|
|
97
|
+
function ProgressDocsPage() {
|
|
98
|
+
return (
|
|
99
|
+
<>
|
|
100
|
+
<Title />
|
|
101
|
+
<Subtitle />
|
|
102
|
+
<Markdown>{DESCRIPTION_INTRO}</Markdown>
|
|
103
|
+
<DocHeading>Interactive example</DocHeading>
|
|
104
|
+
<Markdown>{PROPS_INTRO}</Markdown>
|
|
105
|
+
<DocPrimary />
|
|
106
|
+
<Controls />
|
|
107
|
+
<DocHeading>Usage guidance</DocHeading>
|
|
108
|
+
<Markdown>{USAGE_GUIDANCE}</Markdown>
|
|
109
|
+
<DocHeading>Developer notes</DocHeading>
|
|
110
|
+
<Markdown>{DEVELOPER_NOTES}</Markdown>
|
|
111
|
+
<DocHeading>Examples</DocHeading>
|
|
112
|
+
<Stories title="" />
|
|
113
|
+
<Markdown>{RELATED_COMPONENTS}</Markdown>
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Meta
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
4
122
|
const meta = {
|
|
5
123
|
title: 'Components/Progress',
|
|
6
124
|
component: Progress,
|
|
7
125
|
parameters: {
|
|
8
|
-
layout: '
|
|
126
|
+
layout: 'padded',
|
|
127
|
+
docs: {
|
|
128
|
+
page: ProgressDocsPage,
|
|
129
|
+
},
|
|
9
130
|
},
|
|
10
131
|
tags: ['autodocs'],
|
|
11
|
-
args: {
|
|
12
|
-
'aria-label': 'Progress bar',
|
|
13
|
-
},
|
|
14
132
|
argTypes: {
|
|
15
|
-
value: {
|
|
133
|
+
'value': {
|
|
16
134
|
control: { type: 'number', min: 0, max: 100 },
|
|
17
|
-
description:
|
|
135
|
+
description: [
|
|
136
|
+
'Current progress value. Defaults to `0`.',
|
|
137
|
+
'Pass `null` only if you provide a custom animated `indicatorClassName` for an indeterminate state',
|
|
138
|
+
'— see the IndeterminateAntiPattern story for why bare `null` does nothing visible.',
|
|
139
|
+
].join(' '),
|
|
140
|
+
table: {
|
|
141
|
+
type: { summary: 'number | null' },
|
|
142
|
+
defaultValue: { summary: '0' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
'max': {
|
|
146
|
+
control: { type: 'number', min: 1 },
|
|
147
|
+
description: [
|
|
148
|
+
'Denominator for the progress calculation. Progress calculates the percentage internally',
|
|
149
|
+
'— pass raw values, not percentages. Defaults to `100`.',
|
|
150
|
+
].join(' '),
|
|
151
|
+
table: {
|
|
152
|
+
type: { summary: 'number' },
|
|
153
|
+
defaultValue: { summary: '100' },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
'aria-label': {
|
|
157
|
+
control: 'text',
|
|
158
|
+
description: [
|
|
159
|
+
'**Required** (unless `aria-labelledby` is provided).',
|
|
160
|
+
'A progressbar with no accessible name fails WCAG 2.1 (4.1.2 Name, Role, Value).',
|
|
161
|
+
'Use `aria-labelledby` instead when a visible label element exists in the DOM.',
|
|
162
|
+
].join(' '),
|
|
163
|
+
table: {
|
|
164
|
+
type: { summary: 'string' },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
'getValueLabel': {
|
|
168
|
+
control: false,
|
|
169
|
+
description: [
|
|
170
|
+
'Function that returns the string announced by screen readers as `aria-valuetext`.',
|
|
171
|
+
'Without it, Radix falls back to a rounded percentage string (e.g. `"50%"`).',
|
|
172
|
+
'Provide a custom function for a fuller sentence, e.g. `(v, m) => v + " of " + m + " files uploaded"`.',
|
|
173
|
+
].join(' '),
|
|
174
|
+
table: {
|
|
175
|
+
type: { summary: '(value: number, max: number) => string' },
|
|
176
|
+
defaultValue: { summary: 'Radix default (rounded percentage, e.g. "50%")' },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
'indicatorClassName': {
|
|
180
|
+
control: 'text',
|
|
181
|
+
description: [
|
|
182
|
+
'Escape hatch for styling the moving indicator bar. Rarely needed.',
|
|
183
|
+
'Pass a CSS class to override colours, add animations, or customise the indeterminate state.',
|
|
184
|
+
].join(' '),
|
|
185
|
+
table: {
|
|
186
|
+
type: { summary: 'string' },
|
|
187
|
+
defaultValue: { summary: "''" },
|
|
188
|
+
},
|
|
18
189
|
},
|
|
19
|
-
|
|
20
|
-
control:
|
|
21
|
-
description: '
|
|
190
|
+
'className': {
|
|
191
|
+
control: false,
|
|
192
|
+
description: 'Additional CSS class names on the root progress element. Use sparingly.',
|
|
193
|
+
table: {
|
|
194
|
+
type: { summary: 'string' },
|
|
195
|
+
defaultValue: { summary: "''" },
|
|
196
|
+
},
|
|
22
197
|
},
|
|
23
198
|
},
|
|
24
|
-
decorators: [
|
|
25
|
-
Story => (
|
|
26
|
-
<div style={{ width: '400px' }}>
|
|
27
|
-
<Story />
|
|
28
|
-
</div>
|
|
29
|
-
),
|
|
30
|
-
],
|
|
31
199
|
} satisfies Meta<typeof Progress>;
|
|
32
200
|
|
|
33
201
|
export default meta;
|
|
34
|
-
|
|
202
|
+
// Use StoryObj<typeof Progress> (not typeof meta) so render-only stories are
|
|
203
|
+
// not forced to provide required args — template components handle their own instances.
|
|
204
|
+
type Story = StoryObj<typeof Progress>;
|
|
35
205
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Helper: attach a per-story description to docs
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
const withDescription = (story: Story, description: string): Story => ({
|
|
211
|
+
...story,
|
|
212
|
+
parameters: {
|
|
213
|
+
...story.parameters,
|
|
214
|
+
docs: {
|
|
215
|
+
...story.parameters?.docs,
|
|
216
|
+
description: {
|
|
217
|
+
story: description,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
41
220
|
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Template components for composition and stateful stories.
|
|
225
|
+
// Named components avoid react-hooks lint issues — the react-hooks ESLint
|
|
226
|
+
// plugin is NOT configured in this project, so do NOT add eslint-disable.
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
const AllStopsTemplate = () => (
|
|
230
|
+
<div
|
|
231
|
+
style={{
|
|
232
|
+
padding: 'var(--spacing-xlarge)',
|
|
233
|
+
display: 'flex',
|
|
234
|
+
flexDirection: 'column',
|
|
235
|
+
gap: 'var(--spacing-large)',
|
|
236
|
+
maxWidth: '60%',
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
{([0, 25, 50, 75, 100] as const).map(stop => (
|
|
240
|
+
<div
|
|
241
|
+
key={stop}
|
|
242
|
+
style={{
|
|
243
|
+
display: 'flex',
|
|
244
|
+
alignItems: 'center',
|
|
245
|
+
gap: 'var(--spacing-xsmall)',
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
<span
|
|
249
|
+
className="ds-text"
|
|
250
|
+
style={{
|
|
251
|
+
color: 'var(--color-grey-600)',
|
|
252
|
+
minWidth: '4rem',
|
|
253
|
+
flexShrink: 0,
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
{stop}
|
|
257
|
+
{' '}
|
|
258
|
+
/ 100
|
|
259
|
+
</span>
|
|
260
|
+
<div style={{ flex: 1 }}>
|
|
261
|
+
<Progress
|
|
262
|
+
value={stop}
|
|
263
|
+
max={100}
|
|
264
|
+
aria-label={`${stop}% complete`}
|
|
265
|
+
/>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
))}
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const InteractiveLoadingTemplate = (args: React.ComponentProps<typeof Progress>) => {
|
|
273
|
+
const [progress, setProgress] = useState(0);
|
|
274
|
+
const [running, setRunning] = useState(true);
|
|
275
|
+
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (!running) return;
|
|
278
|
+
if (progress >= 100) {
|
|
279
|
+
setRunning(false);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const id = setInterval(() => {
|
|
283
|
+
setProgress((prev) => {
|
|
284
|
+
const next = prev + 2;
|
|
285
|
+
if (next >= 100) {
|
|
286
|
+
clearInterval(id);
|
|
287
|
+
setRunning(false);
|
|
288
|
+
return 100;
|
|
289
|
+
}
|
|
290
|
+
return next;
|
|
291
|
+
});
|
|
292
|
+
}, 60);
|
|
293
|
+
return () => clearInterval(id);
|
|
294
|
+
}, [running, progress]);
|
|
295
|
+
|
|
296
|
+
const handleReset = () => {
|
|
297
|
+
setProgress(0);
|
|
298
|
+
setRunning(true);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<div
|
|
303
|
+
style={{
|
|
304
|
+
padding: 'var(--spacing-xlarge)',
|
|
305
|
+
display: 'flex',
|
|
306
|
+
flexDirection: 'column',
|
|
307
|
+
gap: 'var(--spacing-large)',
|
|
308
|
+
maxWidth: '60%',
|
|
309
|
+
}}
|
|
310
|
+
>
|
|
311
|
+
<span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
|
|
312
|
+
File import progress
|
|
313
|
+
</span>
|
|
314
|
+
<Progress
|
|
315
|
+
{...args}
|
|
316
|
+
value={progress}
|
|
317
|
+
max={100}
|
|
318
|
+
aria-label="File import progress"
|
|
319
|
+
getValueLabel={v => `${v}% complete`}
|
|
320
|
+
/>
|
|
321
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
|
|
322
|
+
<span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
|
|
323
|
+
{progress}
|
|
324
|
+
% complete
|
|
325
|
+
</span>
|
|
326
|
+
{!running && (
|
|
327
|
+
<Button variant="secondary" size="S" onClick={handleReset}>
|
|
328
|
+
Reset
|
|
329
|
+
</Button>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
42
334
|
};
|
|
43
335
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
336
|
+
const WithAccessibleLabelTemplate = () => (
|
|
337
|
+
<div
|
|
338
|
+
style={{
|
|
339
|
+
padding: 'var(--spacing-xlarge)',
|
|
340
|
+
display: 'flex',
|
|
341
|
+
flexDirection: 'column',
|
|
342
|
+
gap: 'var(--spacing-xlarge)',
|
|
343
|
+
maxWidth: '60%',
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
{/* Without getValueLabel */}
|
|
347
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
348
|
+
<p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
|
|
349
|
+
Without getValueLabel — screen reader announces Radix's default (a rounded percentage, e.g. “65%”)
|
|
350
|
+
</p>
|
|
351
|
+
<Progress
|
|
352
|
+
value={65}
|
|
353
|
+
max={100}
|
|
354
|
+
aria-label="Upload progress"
|
|
355
|
+
/>
|
|
356
|
+
<p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
|
|
357
|
+
Screen reader: "Upload progress, 65%"
|
|
358
|
+
</p>
|
|
359
|
+
</div>
|
|
360
|
+
{/* With getValueLabel */}
|
|
361
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
362
|
+
<p className="ds-text" style={{ margin: 0, color: 'var(--color-semantic-success-600)' }}>
|
|
363
|
+
With getValueLabel — screen reader announces a full sentence
|
|
364
|
+
</p>
|
|
365
|
+
<Progress
|
|
366
|
+
value={65}
|
|
367
|
+
max={100}
|
|
368
|
+
aria-label="Upload progress"
|
|
369
|
+
getValueLabel={v => `${v}% uploaded`}
|
|
370
|
+
/>
|
|
371
|
+
<p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
|
|
372
|
+
Screen reader: "Upload progress, 65% uploaded"
|
|
373
|
+
</p>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const IndeterminateAntiPatternTemplate = () => (
|
|
379
|
+
<div
|
|
380
|
+
style={{
|
|
381
|
+
padding: 'var(--spacing-xlarge)',
|
|
382
|
+
display: 'flex',
|
|
383
|
+
flexDirection: 'column',
|
|
384
|
+
gap: 'var(--spacing-large)',
|
|
385
|
+
maxWidth: '60%',
|
|
386
|
+
}}
|
|
387
|
+
>
|
|
388
|
+
<p className="ds-text" style={{ margin: 0, color: 'var(--color-semantic-destructive-600)' }}>
|
|
389
|
+
The blank bar below is the ACTUAL render of value=null — this is what NOT to do
|
|
390
|
+
</p>
|
|
391
|
+
{/* value={null} causes Number(null) = 0, translateX(-100%), indicator invisible */}
|
|
392
|
+
<Progress
|
|
393
|
+
value={null}
|
|
394
|
+
max={100}
|
|
395
|
+
aria-label="Assessment marksheet export"
|
|
396
|
+
/>
|
|
397
|
+
<p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
|
|
398
|
+
The indicator bar has translated fully out of view. No animation plays. The user sees nothing.
|
|
399
|
+
</p>
|
|
400
|
+
<p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
|
|
401
|
+
For unknown-duration operations, use a Spinner (Icon with name="loader" and a CSS rotation
|
|
402
|
+
animation) or set an approximate value so the bar is at least partially visible.
|
|
403
|
+
</p>
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
// Stories
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
export const Default: Story = withDescription(
|
|
412
|
+
{
|
|
413
|
+
args: {
|
|
414
|
+
'value': 50,
|
|
415
|
+
'max': 100,
|
|
416
|
+
'aria-label': 'Page loading',
|
|
417
|
+
},
|
|
418
|
+
render: args => <Progress {...args} />,
|
|
49
419
|
},
|
|
50
|
-
|
|
420
|
+
[
|
|
421
|
+
'The interactive canvas — every prop is wired to the Controls panel below.',
|
|
422
|
+
'Adjust `value` and `max` to see the bar move. Control the width by sizing the parent container',
|
|
423
|
+
'— Progress fills 100% of its parent, so the wrapper\'s `maxWidth` is what you control.',
|
|
424
|
+
].join(' '),
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
export const AllStops: Story = withDescription(
|
|
428
|
+
{
|
|
429
|
+
render: AllStopsTemplate,
|
|
430
|
+
parameters: {
|
|
431
|
+
controls: { disable: true },
|
|
432
|
+
docs: {
|
|
433
|
+
source: {
|
|
434
|
+
language: 'tsx',
|
|
435
|
+
code: `
|
|
436
|
+
import { Progress } from '@arbor-education/design-system.components';
|
|
51
437
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
438
|
+
function AllStopsExample() {
|
|
439
|
+
return (
|
|
440
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', maxWidth: '60%', padding: 'var(--spacing-xlarge)' }}>
|
|
441
|
+
<Progress value={0} max={100} aria-label="0% complete" />
|
|
442
|
+
<Progress value={25} max={100} aria-label="25% complete" />
|
|
443
|
+
<Progress value={50} max={100} aria-label="50% complete" />
|
|
444
|
+
<Progress value={75} max={100} aria-label="75% complete" />
|
|
445
|
+
<Progress value={100} max={100} aria-label="100% complete" />
|
|
446
|
+
</div>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
export default AllStopsExample;
|
|
450
|
+
`.trim(),
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
},
|
|
57
454
|
},
|
|
58
|
-
|
|
455
|
+
[
|
|
456
|
+
'All five canonical stops — 0, 25, 50, 75, and 100 — rendered side by side for visual reference.',
|
|
457
|
+
'Each bar has an `aria-label` describing its completion percentage.',
|
|
458
|
+
'The "0 / 100" bar shows that an empty state is a valid initial render, not a missing or broken component.',
|
|
459
|
+
].join(' '),
|
|
460
|
+
);
|
|
59
461
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
462
|
+
export const CustomMax: Story = withDescription(
|
|
463
|
+
{
|
|
464
|
+
args: {
|
|
465
|
+
'value': 30,
|
|
466
|
+
'max': 50,
|
|
467
|
+
'aria-label': 'Assessment sections completed',
|
|
468
|
+
'getValueLabel': (v: number, m: number) => `${v} of ${m}`,
|
|
469
|
+
},
|
|
470
|
+
render: args => (
|
|
471
|
+
<div style={{ maxWidth: '60%', padding: 'var(--spacing-xlarge)' }}>
|
|
472
|
+
<Progress {...args} />
|
|
473
|
+
</div>
|
|
474
|
+
),
|
|
475
|
+
parameters: {
|
|
476
|
+
docs: {
|
|
477
|
+
source: {
|
|
478
|
+
language: 'tsx',
|
|
479
|
+
code: `
|
|
480
|
+
import { Progress } from '@arbor-education/design-system.components';
|
|
481
|
+
|
|
482
|
+
// Pass raw values — Progress calculates the percentage internally.
|
|
483
|
+
// value={30} max={50} renders a 60% filled bar.
|
|
484
|
+
function CustomMaxExample() {
|
|
485
|
+
return (
|
|
486
|
+
<Progress
|
|
487
|
+
value={30}
|
|
488
|
+
max={50}
|
|
489
|
+
aria-label="Assessment sections completed"
|
|
490
|
+
getValueLabel={(v, m) => \`\${v} of \${m}\`}
|
|
491
|
+
/>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
export default CustomMaxExample;
|
|
495
|
+
`.trim(),
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
},
|
|
65
499
|
},
|
|
66
|
-
|
|
500
|
+
[
|
|
501
|
+
'When your total is not 100, set `max` to the real denominator and `value` to the raw count.',
|
|
502
|
+
'Here `value={30}` and `max={50}` — Progress renders a 60% filled bar without you calculating anything.',
|
|
503
|
+
'The `getValueLabel` prop shapes the screen-reader announcement — `(v, m) => v + " of " + m` produces',
|
|
504
|
+
'"30 of 50" as `aria-valuetext` instead of just the raw percentage.',
|
|
505
|
+
].join(' '),
|
|
506
|
+
);
|
|
67
507
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
508
|
+
export const InteractiveLoading: Story = withDescription(
|
|
509
|
+
{
|
|
510
|
+
render: InteractiveLoadingTemplate,
|
|
511
|
+
parameters: {
|
|
512
|
+
controls: { disable: true },
|
|
513
|
+
docs: {
|
|
514
|
+
source: {
|
|
515
|
+
language: 'tsx',
|
|
516
|
+
code: `
|
|
517
|
+
import { useEffect, useState } from 'react';
|
|
518
|
+
import { Button, Progress } from '@arbor-education/design-system.components';
|
|
519
|
+
|
|
520
|
+
function FileImportProgress() {
|
|
521
|
+
const [progress, setProgress] = useState(0);
|
|
522
|
+
const [running, setRunning] = useState(true);
|
|
523
|
+
|
|
524
|
+
useEffect(() => {
|
|
525
|
+
if (!running) return;
|
|
526
|
+
if (progress >= 100) { setRunning(false); return; }
|
|
527
|
+
const id = setInterval(() => {
|
|
528
|
+
setProgress((prev) => {
|
|
529
|
+
const next = prev + 2;
|
|
530
|
+
if (next >= 100) { clearInterval(id); setRunning(false); return 100; }
|
|
531
|
+
return next;
|
|
532
|
+
});
|
|
533
|
+
}, 60);
|
|
534
|
+
return () => clearInterval(id);
|
|
535
|
+
}, [running, progress]);
|
|
536
|
+
|
|
537
|
+
return (
|
|
538
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', maxWidth: '60%' }}>
|
|
539
|
+
<span>File import progress</span>
|
|
540
|
+
<Progress
|
|
541
|
+
value={progress}
|
|
542
|
+
max={100}
|
|
543
|
+
aria-label="File import progress"
|
|
544
|
+
getValueLabel={v => \`\${v}% complete\`}
|
|
545
|
+
/>
|
|
546
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
|
|
547
|
+
<span>{progress}% complete</span>
|
|
548
|
+
{!running && (
|
|
549
|
+
<Button variant="secondary" size="S" onClick={() => { setProgress(0); setRunning(true); }}>
|
|
550
|
+
Reset
|
|
551
|
+
</Button>
|
|
552
|
+
)}
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
export default FileImportProgress;
|
|
558
|
+
`.trim(),
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
},
|
|
73
562
|
},
|
|
74
|
-
|
|
563
|
+
[
|
|
564
|
+
'A timed simulation of a file import: 0 → 100% over ~3 seconds using `setInterval` in a',
|
|
565
|
+
'named template component (the react-hooks ESLint plugin is not configured here, so hooks inside',
|
|
566
|
+
'a named component are used directly without disable comments).',
|
|
567
|
+
'The Reset button reruns the animation. `getValueLabel` returns a richer phrase (e.g. "42% complete")',
|
|
568
|
+
'instead of Radix\'s default rounded-percentage string on each `aria-valuenow` update.',
|
|
569
|
+
'In a real import, drive `value` from an API polling response — never fake it like this demo.',
|
|
570
|
+
].join(' '),
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
export const WithAccessibleLabel: Story = withDescription(
|
|
574
|
+
{
|
|
575
|
+
render: WithAccessibleLabelTemplate,
|
|
576
|
+
parameters: {
|
|
577
|
+
controls: { disable: true },
|
|
578
|
+
docs: {
|
|
579
|
+
source: {
|
|
580
|
+
language: 'tsx',
|
|
581
|
+
code: `
|
|
582
|
+
import { Progress } from '@arbor-education/design-system.components';
|
|
75
583
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
584
|
+
function WithAccessibleLabelExample() {
|
|
585
|
+
return (
|
|
586
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)', maxWidth: '60%', padding: 'var(--spacing-xlarge)' }}>
|
|
587
|
+
{/* Without getValueLabel — screen reader announces Radix's default (rounded %) */}
|
|
588
|
+
<Progress value={65} max={100} aria-label="Upload progress" />
|
|
589
|
+
{/* With getValueLabel — screen reader announces a full sentence via aria-valuetext */}
|
|
590
|
+
<Progress
|
|
591
|
+
value={65}
|
|
592
|
+
max={100}
|
|
593
|
+
aria-label="Upload progress"
|
|
594
|
+
getValueLabel={v => \`\${v}% uploaded\`}
|
|
595
|
+
/>
|
|
596
|
+
</div>
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
export default WithAccessibleLabelExample;
|
|
600
|
+
`.trim(),
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
},
|
|
81
604
|
},
|
|
82
|
-
|
|
605
|
+
[
|
|
606
|
+
'Side-by-side contrast: the top bar has only `aria-label` — screen readers announce the raw Radix',
|
|
607
|
+
'default (the percentage as a number). The bottom bar adds `getValueLabel` returning "65% uploaded",',
|
|
608
|
+
'which populates `aria-valuetext` and gives a screen reader a complete sentence.',
|
|
609
|
+
'Inspect the DOM to see `aria-valuetext` populated on the lower bar — it will read "65% uploaded"',
|
|
610
|
+
'rather than "65".',
|
|
611
|
+
].join(' '),
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
export const IndeterminateAntiPattern: Story = withDescription(
|
|
615
|
+
{
|
|
616
|
+
render: IndeterminateAntiPatternTemplate,
|
|
617
|
+
parameters: {
|
|
618
|
+
controls: { disable: true },
|
|
619
|
+
docs: {
|
|
620
|
+
source: {
|
|
621
|
+
language: 'tsx',
|
|
622
|
+
code: `
|
|
623
|
+
import { Progress } from '@arbor-education/design-system.components';
|
|
83
624
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
625
|
+
function IndeterminateAntiPatternExample() {
|
|
626
|
+
return (
|
|
627
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', maxWidth: '60%', padding: 'var(--spacing-xlarge)' }}>
|
|
628
|
+
{/* ⚠️ ANTI-PATTERN — value={null} does NOT produce an indeterminate animation. */}
|
|
629
|
+
{/* Number(null) = 0, so the indicator bar translates fully off-screen. */}
|
|
630
|
+
<Progress value={null} max={100} aria-label="Assessment marksheet export" />
|
|
631
|
+
|
|
632
|
+
{/* ✅ Or set an approximate value so the bar is at least partially visible: */}
|
|
633
|
+
<Progress value={50} max={100} aria-label="Assessment marksheet export" />
|
|
634
|
+
</div>
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
export default IndeterminateAntiPatternExample;
|
|
638
|
+
`.trim(),
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
},
|
|
89
642
|
},
|
|
90
|
-
|
|
643
|
+
[
|
|
644
|
+
'**Do not use `value={null}` with this component.** When `value` is `null`,',
|
|
645
|
+
'`Number(null)` evaluates to `0`, the indicator translates fully off-screen (`translateX(-100%)`),',
|
|
646
|
+
'and the user sees a blank bar. Radix sets `data-state="indeterminate"` on the root element,',
|
|
647
|
+
'but this component has NO animation CSS defined for that state.',
|
|
648
|
+
'',
|
|
649
|
+
'The blank bar in this story IS the actual render — it is not a Storybook display issue.',
|
|
650
|
+
'',
|
|
651
|
+
'For unknown-duration operations, use a Spinner (Icon with `name="loader"` plus a CSS rotation',
|
|
652
|
+
'animation) or set an approximate `value` so the bar is at least partially visible.',
|
|
653
|
+
'',
|
|
654
|
+
'If you genuinely need an animated indeterminate bar, pass a custom `indicatorClassName` with',
|
|
655
|
+
'a keyframe animation targeting `[data-state="indeterminate"]`.',
|
|
656
|
+
].join('\n'),
|
|
657
|
+
);
|