@arbor-education/design-system.components 0.25.4 → 0.25.5
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/CHANGELOG.md +8 -0
- package/dist/components/fileUpload/FileUpload.d.ts +21 -0
- package/dist/components/fileUpload/FileUpload.d.ts.map +1 -0
- package/dist/components/fileUpload/FileUpload.js +23 -0
- package/dist/components/fileUpload/FileUpload.js.map +1 -0
- package/dist/components/fileUpload/FileUpload.stories.d.ts +71 -0
- package/dist/components/fileUpload/FileUpload.stories.d.ts.map +1 -0
- package/dist/components/fileUpload/FileUpload.stories.js +736 -0
- package/dist/components/fileUpload/FileUpload.stories.js.map +1 -0
- package/dist/components/fileUpload/FileUpload.test.d.ts +2 -0
- package/dist/components/fileUpload/FileUpload.test.d.ts.map +1 -0
- package/dist/components/fileUpload/FileUpload.test.js +99 -0
- package/dist/components/fileUpload/FileUpload.test.js.map +1 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js +14 -15
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js +11 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js.map +1 -1
- package/dist/components/icon/allowedIcons.d.ts +1 -0
- package/dist/components/icon/allowedIcons.d.ts.map +1 -1
- package/dist/components/icon/allowedIcons.js +2 -1
- package/dist/components/icon/allowedIcons.js.map +1 -1
- package/dist/index.css +66 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/button/button.scss +2 -0
- package/src/components/fileUpload/FileUpload.stories.tsx +920 -0
- package/src/components/fileUpload/FileUpload.test.tsx +172 -0
- package/src/components/fileUpload/FileUpload.tsx +144 -0
- package/src/components/fileUpload/fileUpload.scss +74 -0
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.stories.tsx +15 -16
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.test.tsx +19 -0
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +1 -1
- package/src/components/icon/allowedIcons.tsx +2 -0
- package/src/components/progress/progress.scss +1 -0
- package/src/index.scss +1 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,736 @@
|
|
|
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';
|
|
3
|
+
import { fn } from 'storybook/test';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { Button } from '../button/Button.js';
|
|
6
|
+
import { FileUpload } from './FileUpload.js';
|
|
7
|
+
// ─── Content strings ──────────────────────────────────────────────────────────
|
|
8
|
+
const DESCRIPTION_INTRO = `
|
|
9
|
+
The \`FileUpload\` component provides a drag-and-drop file upload area combined
|
|
10
|
+
with a managed list of uploaded (or uploading) files. It is built on top of
|
|
11
|
+
[react-dropzone](https://react-dropzone.js.org/) and handles the visual
|
|
12
|
+
presentation of the upload state machine — idle dropzone, in-progress upload
|
|
13
|
+
with a progress bar, upload failure, and completed file with download/delete
|
|
14
|
+
actions.
|
|
15
|
+
|
|
16
|
+
Use it wherever school staff need to attach documents to records: term reports,
|
|
17
|
+
policy PDFs, student photographs, spreadsheet imports, and so on.
|
|
18
|
+
`.trim();
|
|
19
|
+
const USAGE_GUIDANCE = `
|
|
20
|
+
### When to use
|
|
21
|
+
|
|
22
|
+
- Uploading one or more files as part of a form flow (e.g. attaching a
|
|
23
|
+
behaviour letter to a student record).
|
|
24
|
+
- Displaying the progress of an ongoing upload alongside files that have
|
|
25
|
+
already completed.
|
|
26
|
+
- Giving staff a clear way to remove or re-download files they have attached.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### When NOT to use
|
|
31
|
+
|
|
32
|
+
| Situation | Prefer instead |
|
|
33
|
+
|-----------|---------------|
|
|
34
|
+
| You need a plain \`<input type="file">\` without drag-and-drop | A \`FormField\` wrapping a native file input |
|
|
35
|
+
| The user can only ever upload a single file and progress feedback is not needed | A simple \`<input type="file">\` or a \`Button\` that opens a file picker |
|
|
36
|
+
| The upload is hidden from the user (background sync) | No UI component needed — handle it silently in a service layer |
|
|
37
|
+
`.trim();
|
|
38
|
+
const DEVELOPER_NOTES = `
|
|
39
|
+
### Critical usage patterns
|
|
40
|
+
|
|
41
|
+
The component is **not stateful by itself** — it is a controlled component. You
|
|
42
|
+
are responsible for:
|
|
43
|
+
|
|
44
|
+
1. Calling your upload API inside \`uploadFiles\` and pushing the new
|
|
45
|
+
file (with \`progressPercentage: 0\`) into your own state.
|
|
46
|
+
2. Updating \`progressPercentage\` as the upload progresses (e.g. via an
|
|
47
|
+
\`XMLHttpRequest\` progress event or polling).
|
|
48
|
+
3. Setting \`progressPercentage: 100\` (or removing the property) when the
|
|
49
|
+
upload completes — the progress bar is hidden automatically at 100.
|
|
50
|
+
4. Setting \`hasUploadFailed: true\` if the upload errors — this replaces the
|
|
51
|
+
file-type/size metadata with a red "Upload failed" message and turns the
|
|
52
|
+
progress bar red.
|
|
53
|
+
|
|
54
|
+
The progress bar is rendered **only** when \`progressPercentage\` is a number
|
|
55
|
+
and is strictly less than 100. At exactly 100 the bar disappears.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### Accessibility
|
|
60
|
+
|
|
61
|
+
The dropzone area is rendered by \`react-dropzone\` and is fully keyboard
|
|
62
|
+
accessible: it receives focus, responds to \`Enter\` and \`Space\` to open the
|
|
63
|
+
file picker, and announces itself to screen readers. The file list uses
|
|
64
|
+
semantic \`<span>\` elements for layout; make sure the \`fileName\` values
|
|
65
|
+
you pass are human-readable rather than GUIDs.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### TypeScript types
|
|
70
|
+
|
|
71
|
+
The \`UploadedFile\` type is **not exported** from the component module. Until
|
|
72
|
+
it is exported you can inline the shape or declare your own local type:
|
|
73
|
+
|
|
74
|
+
\`\`\`tsx
|
|
75
|
+
type UploadedFile = {
|
|
76
|
+
fileName: string;
|
|
77
|
+
icon?: string; // any IconName from the design system Icon component
|
|
78
|
+
fileType: string; // e.g. 'PDF', 'XLSX', 'DOCX'
|
|
79
|
+
fileSize: string; // human-readable, e.g. '1.2 MB'
|
|
80
|
+
onDownload?: () => void;
|
|
81
|
+
onDelete: () => void;
|
|
82
|
+
progressPercentage?: number; // 0–100; hidden at exactly 100
|
|
83
|
+
hasUploadFailed?: boolean;
|
|
84
|
+
};
|
|
85
|
+
\`\`\`
|
|
86
|
+
`.trim();
|
|
87
|
+
const RELATED_COMPONENTS = `
|
|
88
|
+
## Related components
|
|
89
|
+
|
|
90
|
+
- [Progress](?path=/docs/components-progress--docs) — the progress bar used
|
|
91
|
+
inside each file row to indicate upload completion.
|
|
92
|
+
- [Button](?path=/docs/components-button--docs) — the "click to upload",
|
|
93
|
+
download, and delete buttons inside the component.
|
|
94
|
+
- [Card](?path=/docs/components-card--docs) — the visual wrapper for both the
|
|
95
|
+
dropzone area and the file list.
|
|
96
|
+
- [FormField](?path=/docs/components-formfield--docs) — use when you need a
|
|
97
|
+
labelled file input without drag-and-drop behaviour.
|
|
98
|
+
- [Icon](?path=/docs/components-icon--docs) — all file-type and action icons
|
|
99
|
+
rendered inside the component.
|
|
100
|
+
`.trim();
|
|
101
|
+
const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
|
|
102
|
+
// ─── Docs page ────────────────────────────────────────────────────────────────
|
|
103
|
+
function FileUploadDocsPage() {
|
|
104
|
+
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 })] }));
|
|
105
|
+
}
|
|
106
|
+
// ─── Meta ─────────────────────────────────────────────────────────────────────
|
|
107
|
+
const meta = {
|
|
108
|
+
title: 'Components/FileUpload',
|
|
109
|
+
component: FileUpload,
|
|
110
|
+
tags: ['autodocs'],
|
|
111
|
+
parameters: {
|
|
112
|
+
layout: 'padded',
|
|
113
|
+
docs: {
|
|
114
|
+
page: FileUploadDocsPage,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
argTypes: {
|
|
118
|
+
uploadedFiles: {
|
|
119
|
+
description: 'Array of file objects to display in the file list. Each object describes '
|
|
120
|
+
+ 'a file that has been selected, is uploading, has completed, or has failed. '
|
|
121
|
+
+ 'See the **TypeScript types** section for the full shape.',
|
|
122
|
+
control: false,
|
|
123
|
+
table: {
|
|
124
|
+
type: { summary: 'UploadedFile[]' },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
uploadFiles: {
|
|
128
|
+
description: 'Callback fired when the user drops files onto the dropzone or selects them '
|
|
129
|
+
+ 'via the file picker. Receives the native `File[]` array. You are responsible '
|
|
130
|
+
+ 'for starting the upload and adding the files to `uploadedFiles` state.',
|
|
131
|
+
control: false,
|
|
132
|
+
table: {
|
|
133
|
+
type: { summary: '(files: File[]) => void' },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
dropzoneContent: {
|
|
137
|
+
description: 'Custom content rendered inside the dropzone area. Defaults to a '
|
|
138
|
+
+ '"Drag and drop, or click to upload" paragraph. Pass a `ReactNode` to '
|
|
139
|
+
+ 'replace this with your own copy or layout.',
|
|
140
|
+
control: false,
|
|
141
|
+
table: {
|
|
142
|
+
type: { summary: 'ReactNode' },
|
|
143
|
+
defaultValue: {
|
|
144
|
+
summary: '<p>Drag and drop, or <Button variant="text-link">click to upload</Button></p>',
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
className: {
|
|
149
|
+
description: 'Additional CSS class name(s) applied to the root element.',
|
|
150
|
+
control: 'text',
|
|
151
|
+
table: {
|
|
152
|
+
type: { summary: 'string' },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
args: {
|
|
157
|
+
uploadedFiles: [],
|
|
158
|
+
uploadFiles: fn(),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
export default meta;
|
|
162
|
+
// ─── Helper ───────────────────────────────────────────────────────────────────
|
|
163
|
+
const withDescription = (story, description) => ({
|
|
164
|
+
...story,
|
|
165
|
+
parameters: {
|
|
166
|
+
...story.parameters,
|
|
167
|
+
docs: {
|
|
168
|
+
...story.parameters?.docs,
|
|
169
|
+
description: {
|
|
170
|
+
story: description,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
// ─── Shared helpers ───────────────────────────────────────────────────────────
|
|
176
|
+
const noop = () => { };
|
|
177
|
+
const completedFiles = [
|
|
178
|
+
{
|
|
179
|
+
fileName: 'spring-term-report-2024.pdf',
|
|
180
|
+
icon: 'file',
|
|
181
|
+
fileType: 'PDF',
|
|
182
|
+
fileSize: '1.2 MB',
|
|
183
|
+
onDownload: noop,
|
|
184
|
+
onDelete: noop,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
fileName: 'attendance-summary-jan.xlsx',
|
|
188
|
+
icon: 'file',
|
|
189
|
+
fileType: 'XLSX',
|
|
190
|
+
fileSize: '348 KB',
|
|
191
|
+
onDownload: noop,
|
|
192
|
+
onDelete: noop,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
fileName: 'behaviour-policy-2024.docx',
|
|
196
|
+
icon: 'file',
|
|
197
|
+
fileType: 'DOCX',
|
|
198
|
+
fileSize: '89 KB',
|
|
199
|
+
onDownload: noop,
|
|
200
|
+
onDelete: noop,
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
// ─── Stories ──────────────────────────────────────────────────────────────────
|
|
204
|
+
export const Default = withDescription({
|
|
205
|
+
args: {
|
|
206
|
+
uploadedFiles: [],
|
|
207
|
+
uploadFiles: fn(),
|
|
208
|
+
},
|
|
209
|
+
render: args => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { ...args }) })),
|
|
210
|
+
}, 'The default state shows the dropzone area with no files attached. '
|
|
211
|
+
+ 'Drop files onto the area or click it to open the native file picker.');
|
|
212
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
213
|
+
export const WithUploadedFiles = withDescription({
|
|
214
|
+
render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: completedFiles, uploadFiles: noop }) })),
|
|
215
|
+
parameters: {
|
|
216
|
+
controls: { disable: true },
|
|
217
|
+
docs: {
|
|
218
|
+
source: {
|
|
219
|
+
language: 'tsx',
|
|
220
|
+
code: `
|
|
221
|
+
import { FileUpload } from '@arbor-education/design-system.components';
|
|
222
|
+
|
|
223
|
+
function UploadWithFilesExample() {
|
|
224
|
+
return (
|
|
225
|
+
<FileUpload
|
|
226
|
+
uploadedFiles={[
|
|
227
|
+
{
|
|
228
|
+
fileName: 'spring-term-report-2024.pdf',
|
|
229
|
+
fileType: 'PDF',
|
|
230
|
+
fileSize: '1.2 MB',
|
|
231
|
+
onDownload: () => { /* download logic */ },
|
|
232
|
+
onDelete: () => { /* remove from state */ },
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
fileName: 'attendance-summary-jan.xlsx',
|
|
236
|
+
fileType: 'XLSX',
|
|
237
|
+
fileSize: '348 KB',
|
|
238
|
+
onDownload: () => { /* download logic */ },
|
|
239
|
+
onDelete: () => { /* remove from state */ },
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
fileName: 'behaviour-policy-2024.docx',
|
|
243
|
+
fileType: 'DOCX',
|
|
244
|
+
fileSize: '89 KB',
|
|
245
|
+
onDownload: () => { /* download logic */ },
|
|
246
|
+
onDelete: () => { /* remove from state */ },
|
|
247
|
+
},
|
|
248
|
+
]}
|
|
249
|
+
uploadFiles={(files) => { /* start upload */ }}
|
|
250
|
+
/>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
export default UploadWithFilesExample;
|
|
254
|
+
`.trim(),
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
}, 'Multiple completed files with both download and delete actions. '
|
|
259
|
+
+ 'Each file shows its type and size. The download button only appears when '
|
|
260
|
+
+ '`onDownload` is provided.');
|
|
261
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
262
|
+
export const SingleFile = withDescription({
|
|
263
|
+
render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: [{
|
|
264
|
+
fileName: 'spring-term-report-2024.pdf',
|
|
265
|
+
fileType: 'PDF',
|
|
266
|
+
fileSize: '1.2 MB',
|
|
267
|
+
onDownload: noop,
|
|
268
|
+
onDelete: noop,
|
|
269
|
+
}], uploadFiles: noop }) })),
|
|
270
|
+
parameters: {
|
|
271
|
+
controls: { disable: true },
|
|
272
|
+
docs: {
|
|
273
|
+
source: {
|
|
274
|
+
language: 'tsx',
|
|
275
|
+
code: `
|
|
276
|
+
import { FileUpload } from '@arbor-education/design-system.components';
|
|
277
|
+
|
|
278
|
+
function SingleFileExample() {
|
|
279
|
+
return (
|
|
280
|
+
<FileUpload
|
|
281
|
+
uploadedFiles={[
|
|
282
|
+
{
|
|
283
|
+
fileName: 'spring-term-report-2024.pdf',
|
|
284
|
+
fileType: 'PDF',
|
|
285
|
+
fileSize: '1.2 MB',
|
|
286
|
+
onDownload: () => { /* download logic */ },
|
|
287
|
+
onDelete: () => { /* remove from state */ },
|
|
288
|
+
},
|
|
289
|
+
]}
|
|
290
|
+
uploadFiles={(files) => { /* start upload */ }}
|
|
291
|
+
/>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
export default SingleFileExample;
|
|
295
|
+
`.trim(),
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
}, 'A single completed file. When there is only one file in the list the '
|
|
300
|
+
+ '`Separator` between rows is not rendered.');
|
|
301
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
302
|
+
export const WithoutDownloadButton = withDescription({
|
|
303
|
+
render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: [
|
|
304
|
+
{
|
|
305
|
+
fileName: 'spring-term-report-2024.pdf',
|
|
306
|
+
fileType: 'PDF',
|
|
307
|
+
fileSize: '1.2 MB',
|
|
308
|
+
onDelete: noop,
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
fileName: 'attendance-summary-jan.xlsx',
|
|
312
|
+
fileType: 'XLSX',
|
|
313
|
+
fileSize: '348 KB',
|
|
314
|
+
onDelete: noop,
|
|
315
|
+
},
|
|
316
|
+
], uploadFiles: noop }) })),
|
|
317
|
+
parameters: {
|
|
318
|
+
controls: { disable: true },
|
|
319
|
+
docs: {
|
|
320
|
+
source: {
|
|
321
|
+
language: 'tsx',
|
|
322
|
+
code: `
|
|
323
|
+
import { FileUpload } from '@arbor-education/design-system.components';
|
|
324
|
+
|
|
325
|
+
function NoDownloadExample() {
|
|
326
|
+
return (
|
|
327
|
+
<FileUpload
|
|
328
|
+
uploadedFiles={[
|
|
329
|
+
{
|
|
330
|
+
fileName: 'spring-term-report-2024.pdf',
|
|
331
|
+
fileType: 'PDF',
|
|
332
|
+
fileSize: '1.2 MB',
|
|
333
|
+
// onDownload omitted — download button will not render
|
|
334
|
+
onDelete: () => { /* remove from state */ },
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
fileName: 'attendance-summary-jan.xlsx',
|
|
338
|
+
fileType: 'XLSX',
|
|
339
|
+
fileSize: '348 KB',
|
|
340
|
+
onDelete: () => { /* remove from state */ },
|
|
341
|
+
},
|
|
342
|
+
]}
|
|
343
|
+
uploadFiles={(files) => { /* start upload */ }}
|
|
344
|
+
/>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
export default NoDownloadExample;
|
|
348
|
+
`.trim(),
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
}, 'When `onDownload` is omitted from a file object the download button is not '
|
|
353
|
+
+ 'rendered for that row. Only the delete (trash) button appears. Use this when '
|
|
354
|
+
+ 'the uploaded file should not be retrievable after submission.');
|
|
355
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
356
|
+
export const UploadInProgress = withDescription({
|
|
357
|
+
render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: [
|
|
358
|
+
{
|
|
359
|
+
fileName: 'spring-term-report-2024.pdf',
|
|
360
|
+
fileType: 'PDF',
|
|
361
|
+
fileSize: '1.2 MB',
|
|
362
|
+
onDelete: noop,
|
|
363
|
+
progressPercentage: 25,
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
fileName: 'attendance-summary-jan.xlsx',
|
|
367
|
+
fileType: 'XLSX',
|
|
368
|
+
fileSize: '348 KB',
|
|
369
|
+
onDelete: noop,
|
|
370
|
+
progressPercentage: 72,
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
fileName: 'behaviour-policy-2024.docx',
|
|
374
|
+
fileType: 'DOCX',
|
|
375
|
+
fileSize: '89 KB',
|
|
376
|
+
onDownload: noop,
|
|
377
|
+
onDelete: noop,
|
|
378
|
+
},
|
|
379
|
+
], uploadFiles: noop }) })),
|
|
380
|
+
parameters: {
|
|
381
|
+
controls: { disable: true },
|
|
382
|
+
docs: {
|
|
383
|
+
source: {
|
|
384
|
+
language: 'tsx',
|
|
385
|
+
code: `
|
|
386
|
+
import { FileUpload } from '@arbor-education/design-system.components';
|
|
387
|
+
|
|
388
|
+
// progressPercentage < 100 shows the progress bar.
|
|
389
|
+
// progressPercentage === 100 (or undefined) hides it.
|
|
390
|
+
function UploadInProgressExample() {
|
|
391
|
+
return (
|
|
392
|
+
<FileUpload
|
|
393
|
+
uploadedFiles={[
|
|
394
|
+
{
|
|
395
|
+
fileName: 'spring-term-report-2024.pdf',
|
|
396
|
+
fileType: 'PDF',
|
|
397
|
+
fileSize: '1.2 MB',
|
|
398
|
+
onDelete: () => { /* cancel / remove */ },
|
|
399
|
+
progressPercentage: 25,
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
fileName: 'attendance-summary-jan.xlsx',
|
|
403
|
+
fileType: 'XLSX',
|
|
404
|
+
fileSize: '348 KB',
|
|
405
|
+
onDelete: () => { /* cancel / remove */ },
|
|
406
|
+
progressPercentage: 72,
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
fileName: 'behaviour-policy-2024.docx',
|
|
410
|
+
fileType: 'DOCX',
|
|
411
|
+
fileSize: '89 KB',
|
|
412
|
+
onDownload: () => { /* download logic */ },
|
|
413
|
+
onDelete: () => { /* remove */ },
|
|
414
|
+
// No progressPercentage — this file finished earlier
|
|
415
|
+
},
|
|
416
|
+
]}
|
|
417
|
+
uploadFiles={(files) => { /* start upload */ }}
|
|
418
|
+
/>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
export default UploadInProgressExample;
|
|
422
|
+
`.trim(),
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
}, 'Multiple files at different stages of upload. A progress bar appears beneath '
|
|
427
|
+
+ 'each row where `progressPercentage` is a number between 0 and 99 (inclusive). '
|
|
428
|
+
+ 'The third file has no `progressPercentage` — it completed earlier and shows '
|
|
429
|
+
+ 'only the download and delete actions.');
|
|
430
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
431
|
+
export const FailedUpload = withDescription({
|
|
432
|
+
render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: [
|
|
433
|
+
{
|
|
434
|
+
fileName: 'corrupted-report.pdf',
|
|
435
|
+
fileType: 'PDF',
|
|
436
|
+
fileSize: '540 KB',
|
|
437
|
+
onDelete: noop,
|
|
438
|
+
hasUploadFailed: true,
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
fileName: 'spring-term-report-2024.pdf',
|
|
442
|
+
fileType: 'PDF',
|
|
443
|
+
fileSize: '1.2 MB',
|
|
444
|
+
onDownload: noop,
|
|
445
|
+
onDelete: noop,
|
|
446
|
+
},
|
|
447
|
+
], uploadFiles: noop }) })),
|
|
448
|
+
parameters: {
|
|
449
|
+
controls: { disable: true },
|
|
450
|
+
docs: {
|
|
451
|
+
source: {
|
|
452
|
+
language: 'tsx',
|
|
453
|
+
code: `
|
|
454
|
+
import { FileUpload } from '@arbor-education/design-system.components';
|
|
455
|
+
|
|
456
|
+
function FailedUploadExample() {
|
|
457
|
+
return (
|
|
458
|
+
<FileUpload
|
|
459
|
+
uploadedFiles={[
|
|
460
|
+
{
|
|
461
|
+
fileName: 'corrupted-report.pdf',
|
|
462
|
+
fileType: 'PDF',
|
|
463
|
+
fileSize: '540 KB',
|
|
464
|
+
onDelete: () => { /* remove from state */ },
|
|
465
|
+
hasUploadFailed: true,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
fileName: 'spring-term-report-2024.pdf',
|
|
469
|
+
fileType: 'PDF',
|
|
470
|
+
fileSize: '1.2 MB',
|
|
471
|
+
onDownload: () => { /* download logic */ },
|
|
472
|
+
onDelete: () => { /* remove from state */ },
|
|
473
|
+
},
|
|
474
|
+
]}
|
|
475
|
+
uploadFiles={(files) => { /* retry or start new upload */ }}
|
|
476
|
+
/>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
export default FailedUploadExample;
|
|
480
|
+
`.trim(),
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
}, "When `hasUploadFailed` is `true` the file's type/size metadata is replaced "
|
|
485
|
+
+ 'with a red "Upload failed" message. A completed file alongside the failed one '
|
|
486
|
+
+ 'shows the contrast between the two states.');
|
|
487
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
488
|
+
export const FailedDuringUpload = withDescription({
|
|
489
|
+
render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: [
|
|
490
|
+
{
|
|
491
|
+
fileName: 'oversized-photo.png',
|
|
492
|
+
fileType: 'PNG',
|
|
493
|
+
fileSize: '18 MB',
|
|
494
|
+
onDelete: noop,
|
|
495
|
+
hasUploadFailed: true,
|
|
496
|
+
progressPercentage: 63,
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
fileName: 'corrupted-report.pdf',
|
|
500
|
+
fileType: 'PDF',
|
|
501
|
+
fileSize: '540 KB',
|
|
502
|
+
onDelete: noop,
|
|
503
|
+
hasUploadFailed: true,
|
|
504
|
+
},
|
|
505
|
+
], uploadFiles: noop }) })),
|
|
506
|
+
parameters: {
|
|
507
|
+
controls: { disable: true },
|
|
508
|
+
docs: {
|
|
509
|
+
source: {
|
|
510
|
+
language: 'tsx',
|
|
511
|
+
code: `
|
|
512
|
+
import { FileUpload } from '@arbor-education/design-system.components';
|
|
513
|
+
|
|
514
|
+
// Both hasUploadFailed: true AND progressPercentage < 100 renders
|
|
515
|
+
// a red progress bar in addition to the "Upload failed" message.
|
|
516
|
+
function FailedDuringUploadExample() {
|
|
517
|
+
return (
|
|
518
|
+
<FileUpload
|
|
519
|
+
uploadedFiles={[
|
|
520
|
+
{
|
|
521
|
+
fileName: 'oversized-photo.png',
|
|
522
|
+
fileType: 'PNG',
|
|
523
|
+
fileSize: '18 MB',
|
|
524
|
+
onDelete: () => { /* remove from state */ },
|
|
525
|
+
hasUploadFailed: true,
|
|
526
|
+
progressPercentage: 63,
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
fileName: 'corrupted-report.pdf',
|
|
530
|
+
fileType: 'PDF',
|
|
531
|
+
fileSize: '540 KB',
|
|
532
|
+
onDelete: () => { /* remove from state */ },
|
|
533
|
+
hasUploadFailed: true,
|
|
534
|
+
// No progressPercentage — failed before any progress was recorded
|
|
535
|
+
},
|
|
536
|
+
]}
|
|
537
|
+
uploadFiles={(files) => { /* start upload */ }}
|
|
538
|
+
/>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
export default FailedDuringUploadExample;
|
|
542
|
+
`.trim(),
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
}, 'When both `hasUploadFailed: true` **and** `progressPercentage < 100` are set '
|
|
547
|
+
+ 'together the progress bar is rendered in its error (red) state below the '
|
|
548
|
+
+ '"Upload failed" message. The first file shows this combined state; the second '
|
|
549
|
+
+ 'failed before any progress was recorded so no bar appears.');
|
|
550
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
551
|
+
export const CustomDropzoneContent = withDescription({
|
|
552
|
+
render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: [], uploadFiles: noop, dropzoneContent: (_jsxs(_Fragment, { children: [_jsx("p", { className: "ds-text", children: "Attach a PDF or spreadsheet (max\u00A010\u00A0MB per file)" }), _jsx(Button, { variant: "secondary", children: "Browse files" })] })) }) })),
|
|
553
|
+
parameters: {
|
|
554
|
+
controls: { disable: true },
|
|
555
|
+
docs: {
|
|
556
|
+
source: {
|
|
557
|
+
language: 'tsx',
|
|
558
|
+
code: `
|
|
559
|
+
import { FileUpload, Button } from '@arbor-education/design-system.components';
|
|
560
|
+
|
|
561
|
+
function CustomDropzoneExample() {
|
|
562
|
+
return (
|
|
563
|
+
<FileUpload
|
|
564
|
+
uploadedFiles={[]}
|
|
565
|
+
uploadFiles={(files) => { /* start upload */ }}
|
|
566
|
+
dropzoneContent={
|
|
567
|
+
<>
|
|
568
|
+
<p>Attach a PDF or spreadsheet (max 10 MB per file)</p>
|
|
569
|
+
<Button variant="secondary">Browse files</Button>
|
|
570
|
+
</>
|
|
571
|
+
}
|
|
572
|
+
/>
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
export default CustomDropzoneExample;
|
|
576
|
+
`.trim(),
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
}, 'Pass a `ReactNode` to `dropzoneContent` to replace the default '
|
|
581
|
+
+ '"Drag and drop, or click to upload" copy. This is useful when you need '
|
|
582
|
+
+ 'context-specific instructions or a branded call-to-action button.');
|
|
583
|
+
const DEMO_FILENAME = 'spring-term-report-2024.pdf';
|
|
584
|
+
const SimulatedUploadLifecycleTemplate = () => {
|
|
585
|
+
const [files, setFiles] = useState([]);
|
|
586
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
587
|
+
const intervalRef = useRef(null);
|
|
588
|
+
const clearTimer = () => {
|
|
589
|
+
if (intervalRef.current !== null) {
|
|
590
|
+
clearInterval(intervalRef.current);
|
|
591
|
+
intervalRef.current = null;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
const startUpload = () => {
|
|
595
|
+
clearTimer();
|
|
596
|
+
setIsRunning(true);
|
|
597
|
+
setFiles([{ fileName: DEMO_FILENAME, fileType: 'PDF', fileSize: '1.2 MB', progressPercentage: 0 }]);
|
|
598
|
+
};
|
|
599
|
+
const restart = () => {
|
|
600
|
+
clearTimer();
|
|
601
|
+
setIsRunning(false);
|
|
602
|
+
setFiles([]);
|
|
603
|
+
};
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
if (!isRunning) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
intervalRef.current = setInterval(() => {
|
|
609
|
+
setFiles((prev) => {
|
|
610
|
+
const current = prev[0];
|
|
611
|
+
if (!current || typeof current.progressPercentage !== 'number') {
|
|
612
|
+
return prev;
|
|
613
|
+
}
|
|
614
|
+
const next = current.progressPercentage + 8;
|
|
615
|
+
if (next >= 100) {
|
|
616
|
+
clearTimer();
|
|
617
|
+
setIsRunning(false);
|
|
618
|
+
return [{ ...current, progressPercentage: 100 }];
|
|
619
|
+
}
|
|
620
|
+
return [{ ...current, progressPercentage: next }];
|
|
621
|
+
});
|
|
622
|
+
}, 250);
|
|
623
|
+
return () => {
|
|
624
|
+
clearTimer();
|
|
625
|
+
};
|
|
626
|
+
}, [isRunning]);
|
|
627
|
+
return (_jsxs("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-medium)' }, children: [_jsx(Button, { variant: "primary", onClick: startUpload, disabled: isRunning, children: "Simulate upload" }), _jsx(Button, { variant: "secondary", onClick: restart, disabled: files.length === 0 && !isRunning, children: "Restart" })] }), _jsx(FileUpload, { uploadedFiles: files.map(f => ({
|
|
628
|
+
...f,
|
|
629
|
+
onDelete: restart,
|
|
630
|
+
...(typeof f.progressPercentage === 'number' && f.progressPercentage >= 100
|
|
631
|
+
? { onDownload: noop }
|
|
632
|
+
: {}),
|
|
633
|
+
})), uploadFiles: startUpload })] }));
|
|
634
|
+
};
|
|
635
|
+
export const SimulatedUploadLifecycle = withDescription({
|
|
636
|
+
render: SimulatedUploadLifecycleTemplate,
|
|
637
|
+
parameters: {
|
|
638
|
+
controls: { disable: true },
|
|
639
|
+
docs: {
|
|
640
|
+
source: {
|
|
641
|
+
language: 'tsx',
|
|
642
|
+
code: `
|
|
643
|
+
import { useState, useEffect, useRef } from 'react';
|
|
644
|
+
import { FileUpload, Button } from '@arbor-education/design-system.components';
|
|
645
|
+
|
|
646
|
+
type ManagedFile = {
|
|
647
|
+
fileName: string;
|
|
648
|
+
fileType: string;
|
|
649
|
+
fileSize: string;
|
|
650
|
+
progressPercentage?: number;
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
function UploadLifecycleDemo() {
|
|
654
|
+
const [files, setFiles] = useState<ManagedFile[]>([]);
|
|
655
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
656
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
657
|
+
|
|
658
|
+
const clearTimer = () => {
|
|
659
|
+
if (intervalRef.current !== null) {
|
|
660
|
+
clearInterval(intervalRef.current);
|
|
661
|
+
intervalRef.current = null;
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const startUpload = () => {
|
|
666
|
+
clearTimer();
|
|
667
|
+
setIsRunning(true);
|
|
668
|
+
setFiles([{
|
|
669
|
+
fileName: 'spring-term-report-2024.pdf',
|
|
670
|
+
fileType: 'PDF',
|
|
671
|
+
fileSize: '1.2 MB',
|
|
672
|
+
progressPercentage: 0,
|
|
673
|
+
}]);
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
useEffect(() => {
|
|
677
|
+
if (!isRunning) return;
|
|
678
|
+
|
|
679
|
+
intervalRef.current = setInterval(() => {
|
|
680
|
+
setFiles((prev) => {
|
|
681
|
+
const current = prev[0];
|
|
682
|
+
if (!current || typeof current.progressPercentage !== 'number') return prev;
|
|
683
|
+
|
|
684
|
+
const next = current.progressPercentage + 8;
|
|
685
|
+
if (next >= 100) {
|
|
686
|
+
clearTimer();
|
|
687
|
+
setIsRunning(false);
|
|
688
|
+
return [{ ...current, progressPercentage: 100 }];
|
|
689
|
+
}
|
|
690
|
+
return [{ ...current, progressPercentage: next }];
|
|
691
|
+
});
|
|
692
|
+
}, 250);
|
|
693
|
+
|
|
694
|
+
return () => {
|
|
695
|
+
clearTimer();
|
|
696
|
+
};
|
|
697
|
+
}, [isRunning]);
|
|
698
|
+
|
|
699
|
+
return (
|
|
700
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
701
|
+
<div style={{ display: 'flex', gap: 'var(--spacing-medium)' }}>
|
|
702
|
+
<Button variant="primary" onClick={startUpload} disabled={isRunning}>
|
|
703
|
+
Simulate upload
|
|
704
|
+
</Button>
|
|
705
|
+
<Button
|
|
706
|
+
variant="secondary"
|
|
707
|
+
onClick={() => { clearTimer(); setIsRunning(false); setFiles([]); }}
|
|
708
|
+
disabled={files.length === 0 && !isRunning}
|
|
709
|
+
>
|
|
710
|
+
Restart
|
|
711
|
+
</Button>
|
|
712
|
+
</div>
|
|
713
|
+
<FileUpload
|
|
714
|
+
uploadedFiles={files.map((f) => ({
|
|
715
|
+
...f,
|
|
716
|
+
onDelete: () => { clearTimer(); setIsRunning(false); setFiles([]); },
|
|
717
|
+
...(typeof f.progressPercentage === 'number' && f.progressPercentage >= 100
|
|
718
|
+
? { onDownload: () => { /* download */ } }
|
|
719
|
+
: {}),
|
|
720
|
+
}))}
|
|
721
|
+
uploadFiles={startUpload}
|
|
722
|
+
/>
|
|
723
|
+
</div>
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
export default UploadLifecycleDemo;
|
|
727
|
+
`.trim(),
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
}, 'An interactive demo of the full upload lifecycle. Click **"Simulate upload"** '
|
|
732
|
+
+ '(or drop a real file) to watch a file enter the list at 0%, tick up through '
|
|
733
|
+
+ 'the progress bar, and complete at 100% — at which point the bar disappears '
|
|
734
|
+
+ 'and the download button becomes available. Click **"Restart"** to reset and '
|
|
735
|
+
+ 'run the demo again.');
|
|
736
|
+
//# sourceMappingURL=FileUpload.stories.js.map
|