@arbor-education/design-system.components 0.25.3 → 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/.gather/skills/write-stories/README.md +1 -1
- package/.gather/skills/write-stories/SKILL.md +1 -1
- package/.gather/skills/write-stories/meta.md +1 -1
- package/CHANGELOG.md +16 -0
- package/component-library.md +26 -1
- package/dist/components/breadcrumbs/BreadcrumbCopyButton.d.ts +3 -0
- package/dist/components/breadcrumbs/BreadcrumbCopyButton.d.ts.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbCopyButton.js +13 -0
- package/dist/components/breadcrumbs/BreadcrumbCopyButton.js.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbDropdown.d.ts +3 -0
- package/dist/components/breadcrumbs/BreadcrumbDropdown.d.ts.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbDropdown.js +8 -0
- package/dist/components/breadcrumbs/BreadcrumbDropdown.js.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbEllipsis.d.ts +3 -0
- package/dist/components/breadcrumbs/BreadcrumbEllipsis.d.ts.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbEllipsis.js +7 -0
- package/dist/components/breadcrumbs/BreadcrumbEllipsis.js.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbItemSeparator.d.ts +2 -0
- package/dist/components/breadcrumbs/BreadcrumbItemSeparator.d.ts.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbItemSeparator.js +5 -0
- package/dist/components/breadcrumbs/BreadcrumbItemSeparator.js.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbLink.d.ts +6 -0
- package/dist/components/breadcrumbs/BreadcrumbLink.d.ts.map +1 -0
- package/dist/components/breadcrumbs/BreadcrumbLink.js +9 -0
- package/dist/components/breadcrumbs/BreadcrumbLink.js.map +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.d.ts +21 -0
- package/dist/components/breadcrumbs/Breadcrumbs.d.ts.map +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.js +51 -0
- package/dist/components/breadcrumbs/Breadcrumbs.js.map +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts +81 -0
- package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts.map +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.stories.js +681 -0
- package/dist/components/breadcrumbs/Breadcrumbs.stories.js.map +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.test.d.ts +2 -0
- package/dist/components/breadcrumbs/Breadcrumbs.test.d.ts.map +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.test.js +160 -0
- package/dist/components/breadcrumbs/Breadcrumbs.test.js.map +1 -0
- package/dist/components/breadcrumbs/types.d.ts +27 -0
- package/dist/components/breadcrumbs/types.d.ts.map +1 -0
- package/dist/components/breadcrumbs/types.js +2 -0
- package/dist/components/breadcrumbs/types.js.map +1 -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/components/skeleton/Skeleton.d.ts +10 -0
- package/dist/components/skeleton/Skeleton.d.ts.map +1 -0
- package/dist/components/skeleton/Skeleton.js +9 -0
- package/dist/components/skeleton/Skeleton.js.map +1 -0
- package/dist/components/skeleton/Skeleton.stories.d.ts +99 -0
- package/dist/components/skeleton/Skeleton.stories.d.ts.map +1 -0
- package/dist/components/skeleton/Skeleton.stories.js +539 -0
- package/dist/components/skeleton/Skeleton.stories.js.map +1 -0
- package/dist/components/skeleton/Skeleton.test.d.ts +2 -0
- package/dist/components/skeleton/Skeleton.test.d.ts.map +1 -0
- package/dist/components/skeleton/Skeleton.test.js +49 -0
- package/dist/components/skeleton/Skeleton.test.js.map +1 -0
- package/dist/index.css +225 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/breadcrumbs/BreadcrumbCopyButton.tsx +27 -0
- package/src/components/breadcrumbs/BreadcrumbDropdown.tsx +27 -0
- package/src/components/breadcrumbs/BreadcrumbEllipsis.tsx +16 -0
- package/src/components/breadcrumbs/BreadcrumbItemSeparator.tsx +3 -0
- package/src/components/breadcrumbs/BreadcrumbLink.tsx +29 -0
- package/src/components/breadcrumbs/Breadcrumbs.stories.tsx +870 -0
- package/src/components/breadcrumbs/Breadcrumbs.test.tsx +185 -0
- package/src/components/breadcrumbs/Breadcrumbs.tsx +92 -0
- package/src/components/breadcrumbs/breadcrumbs.scss +158 -0
- package/src/components/breadcrumbs/types.ts +31 -0
- package/src/components/button/button.scss +2 -0
- package/src/components/dropdown/dropdown.scss +1 -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/components/skeleton/Skeleton.stories.tsx +748 -0
- package/src/components/skeleton/Skeleton.test.tsx +55 -0
- package/src/components/skeleton/Skeleton.tsx +36 -0
- package/src/components/skeleton/skeleton.scss +23 -0
- package/src/index.scss +3 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import '@testing-library/jest-dom/vitest';
|
|
5
|
+
import { FileUpload } from './FileUpload.js';
|
|
6
|
+
|
|
7
|
+
const baseFile = {
|
|
8
|
+
fileName: 'report.pdf',
|
|
9
|
+
fileType: 'PDF',
|
|
10
|
+
fileSize: '1.2 MB',
|
|
11
|
+
onDelete: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('FileUpload', () => {
|
|
15
|
+
describe('dropzone', () => {
|
|
16
|
+
test('renders default dropzone content', () => {
|
|
17
|
+
render(<FileUpload uploadedFiles={[]} uploadFiles={vi.fn()} />);
|
|
18
|
+
expect(screen.getByText('click to upload')).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('renders custom dropzoneContent', () => {
|
|
22
|
+
render(
|
|
23
|
+
<FileUpload
|
|
24
|
+
uploadedFiles={[]}
|
|
25
|
+
uploadFiles={vi.fn()}
|
|
26
|
+
dropzoneContent={<p>Custom content here</p>}
|
|
27
|
+
/>,
|
|
28
|
+
);
|
|
29
|
+
expect(screen.getByText('Custom content here')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('calls uploadFiles when files are selected via the input', async () => {
|
|
33
|
+
const uploadFiles = vi.fn();
|
|
34
|
+
const { container } = render(
|
|
35
|
+
<FileUpload uploadedFiles={[]} uploadFiles={uploadFiles} />,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const input = container.querySelector('input[type="file"]') as HTMLInputElement;
|
|
39
|
+
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' });
|
|
40
|
+
await userEvent.upload(input, file);
|
|
41
|
+
|
|
42
|
+
expect(uploadFiles).toHaveBeenCalledWith([file]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('uploaded file list', () => {
|
|
47
|
+
test('renders file name, type, and size', () => {
|
|
48
|
+
render(<FileUpload uploadedFiles={[baseFile]} uploadFiles={vi.fn()} />);
|
|
49
|
+
expect(screen.getByText('report.pdf')).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByText('PDF, 1.2 MB')).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('renders multiple files', () => {
|
|
54
|
+
render(
|
|
55
|
+
<FileUpload
|
|
56
|
+
uploadedFiles={[
|
|
57
|
+
baseFile,
|
|
58
|
+
{ fileName: 'photo.jpg', fileType: 'JPEG', fileSize: '3.7 MB', onDelete: vi.fn() },
|
|
59
|
+
]}
|
|
60
|
+
uploadFiles={vi.fn()}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
expect(screen.getByText('report.pdf')).toBeInTheDocument();
|
|
64
|
+
expect(screen.getByText('photo.jpg')).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('calls onDelete when delete button is clicked', async () => {
|
|
68
|
+
const onDelete = vi.fn();
|
|
69
|
+
render(
|
|
70
|
+
<FileUpload uploadedFiles={[{ ...baseFile, onDelete }]} uploadFiles={vi.fn()} />,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
await userEvent.click(screen.getByRole('button', { name: 'trash' }));
|
|
74
|
+
expect(onDelete).toHaveBeenCalledOnce();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('renders download button when onDownload is provided', () => {
|
|
78
|
+
render(
|
|
79
|
+
<FileUpload
|
|
80
|
+
uploadedFiles={[{ ...baseFile, onDownload: vi.fn() }]}
|
|
81
|
+
uploadFiles={vi.fn()}
|
|
82
|
+
/>,
|
|
83
|
+
);
|
|
84
|
+
expect(screen.getByRole('button', { name: 'download' })).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('does not render download button when onDownload is not provided', () => {
|
|
88
|
+
render(<FileUpload uploadedFiles={[baseFile]} uploadFiles={vi.fn()} />);
|
|
89
|
+
expect(screen.queryByRole('button', { name: 'download' })).not.toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('calls onDownload when download button is clicked', async () => {
|
|
93
|
+
const onDownload = vi.fn();
|
|
94
|
+
render(
|
|
95
|
+
<FileUpload
|
|
96
|
+
uploadedFiles={[{ ...baseFile, onDownload }]}
|
|
97
|
+
uploadFiles={vi.fn()}
|
|
98
|
+
/>,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
await userEvent.click(screen.getByRole('button', { name: 'download' }));
|
|
102
|
+
expect(onDownload).toHaveBeenCalledOnce();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('failed upload state', () => {
|
|
107
|
+
test('shows "Upload failed" text instead of file meta when hasUploadFailed is true', () => {
|
|
108
|
+
render(
|
|
109
|
+
<FileUpload
|
|
110
|
+
uploadedFiles={[{ ...baseFile, hasUploadFailed: true }]}
|
|
111
|
+
uploadFiles={vi.fn()}
|
|
112
|
+
/>,
|
|
113
|
+
);
|
|
114
|
+
expect(screen.getByText('Upload failed')).toBeInTheDocument();
|
|
115
|
+
expect(screen.queryByText('PDF, 1.2 MB')).not.toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('progress bar', () => {
|
|
120
|
+
test('renders progress bar when progressPercentage is less than 100', () => {
|
|
121
|
+
const { container } = render(
|
|
122
|
+
<FileUpload
|
|
123
|
+
uploadedFiles={[{ ...baseFile, progressPercentage: 50 }]}
|
|
124
|
+
uploadFiles={vi.fn()}
|
|
125
|
+
/>,
|
|
126
|
+
);
|
|
127
|
+
expect(container.querySelector('.ds-file-upload__progress-bar')).toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('does not render progress bar when progressPercentage equals 100', () => {
|
|
131
|
+
const { container } = render(
|
|
132
|
+
<FileUpload
|
|
133
|
+
uploadedFiles={[{ ...baseFile, progressPercentage: 100 }]}
|
|
134
|
+
uploadFiles={vi.fn()}
|
|
135
|
+
/>,
|
|
136
|
+
);
|
|
137
|
+
expect(container.querySelector('.ds-file-upload__progress-bar')).not.toBeInTheDocument();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('does not render progress bar when progressPercentage is not set', () => {
|
|
141
|
+
const { container } = render(
|
|
142
|
+
<FileUpload uploadedFiles={[baseFile]} uploadFiles={vi.fn()} />,
|
|
143
|
+
);
|
|
144
|
+
expect(container.querySelector('.ds-file-upload__progress-bar')).not.toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('applies failed indicator class to progress bar when hasUploadFailed is true', () => {
|
|
148
|
+
const { container } = render(
|
|
149
|
+
<FileUpload
|
|
150
|
+
uploadedFiles={[{ ...baseFile, progressPercentage: 45, hasUploadFailed: true }]}
|
|
151
|
+
uploadFiles={vi.fn()}
|
|
152
|
+
/>,
|
|
153
|
+
);
|
|
154
|
+
expect(
|
|
155
|
+
container.querySelector('.ds-file-upload__progress-bar-indicator--failed'),
|
|
156
|
+
).toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('className', () => {
|
|
161
|
+
test('applies custom className to root element', () => {
|
|
162
|
+
const { container } = render(
|
|
163
|
+
<FileUpload
|
|
164
|
+
uploadedFiles={[]}
|
|
165
|
+
uploadFiles={vi.fn()}
|
|
166
|
+
className="my-custom-class"
|
|
167
|
+
/>,
|
|
168
|
+
);
|
|
169
|
+
expect(container.firstChild).toHaveClass('ds-file-upload', 'my-custom-class');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { Button } from 'Components/button/Button';
|
|
3
|
+
import { Card } from 'Components/card/Card';
|
|
4
|
+
import type { IconName } from 'Components/icon/allowedIcons';
|
|
5
|
+
import { Icon } from 'Components/icon/Icon';
|
|
6
|
+
import { Separator } from 'Components/separator/Separator';
|
|
7
|
+
import type { ReactNode } from 'react';
|
|
8
|
+
import Dropzone from 'react-dropzone';
|
|
9
|
+
import { Progress } from 'Components/progress/Progress';
|
|
10
|
+
|
|
11
|
+
type UploadedFile = {
|
|
12
|
+
fileName: string;
|
|
13
|
+
icon?: IconName;
|
|
14
|
+
fileType: string;
|
|
15
|
+
fileSize: string;
|
|
16
|
+
onDownload?: () => void;
|
|
17
|
+
onDelete: () => void;
|
|
18
|
+
progressPercentage?: number;
|
|
19
|
+
hasUploadFailed?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type FileUploadProps = {
|
|
23
|
+
className?: string;
|
|
24
|
+
uploadedFiles: UploadedFile[];
|
|
25
|
+
uploadFiles: (files: File[]) => void;
|
|
26
|
+
dropzoneContent?: ReactNode;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const FileUpload = (props: FileUploadProps) => {
|
|
30
|
+
const {
|
|
31
|
+
className,
|
|
32
|
+
uploadedFiles,
|
|
33
|
+
uploadFiles,
|
|
34
|
+
dropzoneContent = (
|
|
35
|
+
<p>
|
|
36
|
+
Drag and drop, or
|
|
37
|
+
{' '}
|
|
38
|
+
<Button variant="text-link">click to upload</Button>
|
|
39
|
+
</p>
|
|
40
|
+
),
|
|
41
|
+
} = props;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
className={classNames(
|
|
46
|
+
'ds-file-upload',
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<Dropzone
|
|
51
|
+
onDrop={(acceptedFiles) => {
|
|
52
|
+
uploadFiles(acceptedFiles);
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{({ getRootProps, getInputProps }) => (
|
|
56
|
+
<Card>
|
|
57
|
+
<div
|
|
58
|
+
{...getRootProps()}
|
|
59
|
+
className="ds-file-upload__dropzone-input"
|
|
60
|
+
>
|
|
61
|
+
<input {...getInputProps()} />
|
|
62
|
+
<div className="ds-file-upload__dropzone-content">
|
|
63
|
+
<span className="ds-file-upload__icon-wrapper">
|
|
64
|
+
<Icon
|
|
65
|
+
name="cloud-upload"
|
|
66
|
+
size={24}
|
|
67
|
+
color="var(--color-brand-600)"
|
|
68
|
+
/>
|
|
69
|
+
</span>
|
|
70
|
+
<div className="ds-file-upload__dropzone-text-wrapper">
|
|
71
|
+
{dropzoneContent}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</Card>
|
|
76
|
+
)}
|
|
77
|
+
</Dropzone>
|
|
78
|
+
{uploadedFiles.length > 0 && (
|
|
79
|
+
<Card className="ds-file-upload__file-list">
|
|
80
|
+
{uploadedFiles.map((file, index) => {
|
|
81
|
+
const {
|
|
82
|
+
fileName,
|
|
83
|
+
icon = 'file',
|
|
84
|
+
fileSize,
|
|
85
|
+
fileType,
|
|
86
|
+
onDelete,
|
|
87
|
+
onDownload,
|
|
88
|
+
hasUploadFailed,
|
|
89
|
+
progressPercentage,
|
|
90
|
+
} = file;
|
|
91
|
+
return (
|
|
92
|
+
<div key={fileName} className="ds-file-upload__file-outer">
|
|
93
|
+
<span className="ds-file-upload__file">
|
|
94
|
+
<span className="ds-file-upload__icon-wrapper">
|
|
95
|
+
<Icon size={24} name={icon} color="var(--color-brand-600)" />
|
|
96
|
+
</span>
|
|
97
|
+
<span className="ds-file-upload__file-details">
|
|
98
|
+
<span className="ds-file-upload__file-name">{fileName}</span>
|
|
99
|
+
{
|
|
100
|
+
hasUploadFailed
|
|
101
|
+
? (
|
|
102
|
+
<span className="ds-file-upload__upload-failed">
|
|
103
|
+
<Icon name="circle-alert" />
|
|
104
|
+
{' '}
|
|
105
|
+
Upload failed
|
|
106
|
+
</span>
|
|
107
|
+
)
|
|
108
|
+
: <span className="ds-file-upload__file-meta">{`${fileType}, ${fileSize}`}</span>
|
|
109
|
+
}
|
|
110
|
+
</span>
|
|
111
|
+
{onDownload && (
|
|
112
|
+
<Button
|
|
113
|
+
variant="secondary"
|
|
114
|
+
iconLeftName="download"
|
|
115
|
+
onClick={onDownload}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
{onDelete && (
|
|
119
|
+
<Button
|
|
120
|
+
variant="secondary"
|
|
121
|
+
iconLeftName="trash"
|
|
122
|
+
onClick={onDelete}
|
|
123
|
+
/>
|
|
124
|
+
)}
|
|
125
|
+
</span>
|
|
126
|
+
{typeof progressPercentage === 'number' && progressPercentage < 100
|
|
127
|
+
&& (
|
|
128
|
+
<Progress
|
|
129
|
+
value={progressPercentage}
|
|
130
|
+
className="ds-file-upload__progress-bar"
|
|
131
|
+
indicatorClassName={classNames('ds-file-upload__progress-bar-indicator', {
|
|
132
|
+
'ds-file-upload__progress-bar-indicator--failed': hasUploadFailed,
|
|
133
|
+
})}
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
136
|
+
{index < uploadedFiles.length - 1 && <Separator />}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
})}
|
|
140
|
+
</Card>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
.ds-file-upload {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--spacing-small);
|
|
5
|
+
|
|
6
|
+
&__file {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: row;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: var(--spacing-small);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&__file-details {
|
|
14
|
+
flex-grow: 1;
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&__file-name {
|
|
20
|
+
font-weight: var(--font-weight-semi-bold);
|
|
21
|
+
color: var(--color-grey-900);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&__file-meta {
|
|
25
|
+
color: var(--color-grey-500);
|
|
26
|
+
font-size: var(--font-size-2-13);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&__icon-wrapper {
|
|
30
|
+
background-color: var(--color-brand-050);
|
|
31
|
+
border-radius: var(--border-radius-small);
|
|
32
|
+
height: 36px;
|
|
33
|
+
width: 36px;
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&__dropzone-content {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&__dropzone-text-wrapper {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: var(--spacing-x-small);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&__upload-failed {
|
|
54
|
+
color: var(--color-semantic-destructive-600);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&__file-outer {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
gap: var(--spacing-small);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
&__progress-bar {
|
|
64
|
+
height: 2px;
|
|
65
|
+
|
|
66
|
+
&-indicator {
|
|
67
|
+
background-color: var(--color-semantic-info-600);
|
|
68
|
+
|
|
69
|
+
&--failed {
|
|
70
|
+
background-color: var(--color-semantic-destructive-600);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -153,13 +153,12 @@ const subjectOptionsWithIcons = [
|
|
|
153
153
|
{ label: 'Geography', value: 'geography', icon: 'flag' as const },
|
|
154
154
|
];
|
|
155
155
|
|
|
156
|
-
const
|
|
157
|
-
{
|
|
158
|
-
{
|
|
159
|
-
{
|
|
160
|
-
{
|
|
161
|
-
{
|
|
162
|
-
{ label: 'Late arrival', value: 'late', header: 'Unplanned' },
|
|
156
|
+
const staffMemberOptions = [
|
|
157
|
+
{ value: 'jsmith', header: 'Jane Smith', label: 'Head of Mathematics' },
|
|
158
|
+
{ value: 'rwilliams', header: 'Rhys Williams', label: 'English Teacher' },
|
|
159
|
+
{ value: 'apatel', header: 'Anika Patel', label: 'Science Teacher' },
|
|
160
|
+
{ value: 'lobrien', header: "Liam O'Brien", label: 'History Teacher' },
|
|
161
|
+
{ value: 'mgarcia', header: 'Maria Garcia', label: 'Geography Teacher' },
|
|
163
162
|
];
|
|
164
163
|
|
|
165
164
|
const groupedYearOptions = [
|
|
@@ -435,8 +434,8 @@ const WithIconsTemplate = () => (
|
|
|
435
434
|
const WithItemHeadersTemplate = () => (
|
|
436
435
|
<div style={{ maxWidth: '320px' }}>
|
|
437
436
|
<SelectDropdown
|
|
438
|
-
placeholder="Select
|
|
439
|
-
options={
|
|
437
|
+
placeholder="Select a staff member"
|
|
438
|
+
options={staffMemberOptions}
|
|
440
439
|
onSelectionChange={values => console.log(values)}
|
|
441
440
|
/>
|
|
442
441
|
</div>
|
|
@@ -807,13 +806,13 @@ import { SelectDropdown } from '@arbor-education/design-system.components';
|
|
|
807
806
|
function WithItemHeadersExample() {
|
|
808
807
|
return (
|
|
809
808
|
<SelectDropdown
|
|
810
|
-
placeholder="Select
|
|
809
|
+
placeholder="Select a staff member"
|
|
811
810
|
options={[
|
|
812
|
-
{
|
|
813
|
-
{
|
|
814
|
-
{
|
|
815
|
-
{
|
|
816
|
-
{
|
|
811
|
+
{ value: 'jsmith', header: 'Jane Smith', label: 'Head of Mathematics' },
|
|
812
|
+
{ value: 'rwilliams', header: 'Rhys Williams', label: 'English Teacher' },
|
|
813
|
+
{ value: 'apatel', header: 'Anika Patel', label: 'Science Teacher' },
|
|
814
|
+
{ value: 'lobrien', header: "Liam O'Brien", label: 'History Teacher' },
|
|
815
|
+
{ value: 'mgarcia', header: 'Maria Garcia', label: 'Geography Teacher' },
|
|
817
816
|
]}
|
|
818
817
|
onSelectionChange={(values) => console.log(values)}
|
|
819
818
|
/>
|
|
@@ -825,7 +824,7 @@ export default WithItemHeadersExample;
|
|
|
825
824
|
},
|
|
826
825
|
},
|
|
827
826
|
},
|
|
828
|
-
'The `header` prop on an option renders a
|
|
827
|
+
'The `header` prop on an option renders a primary line above the supplementary `label` text, creating a two-line item layout. Use the `header` for the meaningful identifier (e.g. a person\'s name) and the `label` for supporting detail (e.g. their role). When an option with a `header` is selected, the trigger displays the `header` value.',
|
|
829
828
|
);
|
|
830
829
|
|
|
831
830
|
export const WithGroups: Story = withDescription(
|
|
@@ -183,6 +183,25 @@ describe('SelectDropdown component', () => {
|
|
|
183
183
|
expect(screen.getByText('header1')).toBeInTheDocument();
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
test('displays the header (not the label) in the trigger when a multiline item is selected', async () => {
|
|
187
|
+
render(
|
|
188
|
+
<SelectDropdown
|
|
189
|
+
options={[
|
|
190
|
+
{ label: 'Head of Mathematics', value: 'jsmith', header: 'Jane Smith' },
|
|
191
|
+
{ label: 'English Teacher', value: 'rwilliams', header: 'Rhys Williams' },
|
|
192
|
+
]}
|
|
193
|
+
onSelectionChange={vi.fn()}
|
|
194
|
+
/>,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
await userEvent.click(screen.getByText('Select'));
|
|
198
|
+
await userEvent.click(await screen.findByText('Head of Mathematics'));
|
|
199
|
+
|
|
200
|
+
const trigger = screen.getByRole('button');
|
|
201
|
+
expect(trigger).toHaveTextContent('Jane Smith');
|
|
202
|
+
expect(trigger).not.toHaveTextContent('Head of Mathematics');
|
|
203
|
+
});
|
|
204
|
+
|
|
186
205
|
describe('iconClickOnly', () => {
|
|
187
206
|
test('does not apply the icon-click-only modifier by default', () => {
|
|
188
207
|
render(
|
|
@@ -60,7 +60,7 @@ export const SelectDropdown = (props: SelectDropdownInputProps) => {
|
|
|
60
60
|
else if (selectedValues.length === 1) {
|
|
61
61
|
const selectedValue = selectedValues[0];
|
|
62
62
|
const option = options.find(opt => opt.value === selectedValue);
|
|
63
|
-
const displayText = option?.label ?? option?.value ?? String(selectedValue);
|
|
63
|
+
const displayText = option?.header ?? option?.label ?? option?.value ?? String(selectedValue);
|
|
64
64
|
setRenderedSelectContent(displayText || (placeholder ?? 'Select'));
|
|
65
65
|
}
|
|
66
66
|
else {
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
Clipboard,
|
|
30
30
|
ClipboardList,
|
|
31
31
|
Clock3,
|
|
32
|
+
CloudUpload,
|
|
32
33
|
Copy,
|
|
33
34
|
CornerDownLeft,
|
|
34
35
|
Dot,
|
|
@@ -127,6 +128,7 @@ export const allowedIcons = {
|
|
|
127
128
|
'clipboard-list': ClipboardList,
|
|
128
129
|
'clipboard': Clipboard,
|
|
129
130
|
'clock-3': Clock3,
|
|
131
|
+
'cloud-upload': CloudUpload,
|
|
130
132
|
'copy': Copy,
|
|
131
133
|
'corner-down-left': CornerDownLeft,
|
|
132
134
|
'date': CalendarFold,
|