@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.
Files changed (110) hide show
  1. package/.gather/skills/write-stories/README.md +1 -1
  2. package/.gather/skills/write-stories/SKILL.md +1 -1
  3. package/.gather/skills/write-stories/meta.md +1 -1
  4. package/CHANGELOG.md +16 -0
  5. package/component-library.md +26 -1
  6. package/dist/components/breadcrumbs/BreadcrumbCopyButton.d.ts +3 -0
  7. package/dist/components/breadcrumbs/BreadcrumbCopyButton.d.ts.map +1 -0
  8. package/dist/components/breadcrumbs/BreadcrumbCopyButton.js +13 -0
  9. package/dist/components/breadcrumbs/BreadcrumbCopyButton.js.map +1 -0
  10. package/dist/components/breadcrumbs/BreadcrumbDropdown.d.ts +3 -0
  11. package/dist/components/breadcrumbs/BreadcrumbDropdown.d.ts.map +1 -0
  12. package/dist/components/breadcrumbs/BreadcrumbDropdown.js +8 -0
  13. package/dist/components/breadcrumbs/BreadcrumbDropdown.js.map +1 -0
  14. package/dist/components/breadcrumbs/BreadcrumbEllipsis.d.ts +3 -0
  15. package/dist/components/breadcrumbs/BreadcrumbEllipsis.d.ts.map +1 -0
  16. package/dist/components/breadcrumbs/BreadcrumbEllipsis.js +7 -0
  17. package/dist/components/breadcrumbs/BreadcrumbEllipsis.js.map +1 -0
  18. package/dist/components/breadcrumbs/BreadcrumbItemSeparator.d.ts +2 -0
  19. package/dist/components/breadcrumbs/BreadcrumbItemSeparator.d.ts.map +1 -0
  20. package/dist/components/breadcrumbs/BreadcrumbItemSeparator.js +5 -0
  21. package/dist/components/breadcrumbs/BreadcrumbItemSeparator.js.map +1 -0
  22. package/dist/components/breadcrumbs/BreadcrumbLink.d.ts +6 -0
  23. package/dist/components/breadcrumbs/BreadcrumbLink.d.ts.map +1 -0
  24. package/dist/components/breadcrumbs/BreadcrumbLink.js +9 -0
  25. package/dist/components/breadcrumbs/BreadcrumbLink.js.map +1 -0
  26. package/dist/components/breadcrumbs/Breadcrumbs.d.ts +21 -0
  27. package/dist/components/breadcrumbs/Breadcrumbs.d.ts.map +1 -0
  28. package/dist/components/breadcrumbs/Breadcrumbs.js +51 -0
  29. package/dist/components/breadcrumbs/Breadcrumbs.js.map +1 -0
  30. package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts +81 -0
  31. package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts.map +1 -0
  32. package/dist/components/breadcrumbs/Breadcrumbs.stories.js +681 -0
  33. package/dist/components/breadcrumbs/Breadcrumbs.stories.js.map +1 -0
  34. package/dist/components/breadcrumbs/Breadcrumbs.test.d.ts +2 -0
  35. package/dist/components/breadcrumbs/Breadcrumbs.test.d.ts.map +1 -0
  36. package/dist/components/breadcrumbs/Breadcrumbs.test.js +160 -0
  37. package/dist/components/breadcrumbs/Breadcrumbs.test.js.map +1 -0
  38. package/dist/components/breadcrumbs/types.d.ts +27 -0
  39. package/dist/components/breadcrumbs/types.d.ts.map +1 -0
  40. package/dist/components/breadcrumbs/types.js +2 -0
  41. package/dist/components/breadcrumbs/types.js.map +1 -0
  42. package/dist/components/fileUpload/FileUpload.d.ts +21 -0
  43. package/dist/components/fileUpload/FileUpload.d.ts.map +1 -0
  44. package/dist/components/fileUpload/FileUpload.js +23 -0
  45. package/dist/components/fileUpload/FileUpload.js.map +1 -0
  46. package/dist/components/fileUpload/FileUpload.stories.d.ts +71 -0
  47. package/dist/components/fileUpload/FileUpload.stories.d.ts.map +1 -0
  48. package/dist/components/fileUpload/FileUpload.stories.js +736 -0
  49. package/dist/components/fileUpload/FileUpload.stories.js.map +1 -0
  50. package/dist/components/fileUpload/FileUpload.test.d.ts +2 -0
  51. package/dist/components/fileUpload/FileUpload.test.d.ts.map +1 -0
  52. package/dist/components/fileUpload/FileUpload.test.js +99 -0
  53. package/dist/components/fileUpload/FileUpload.test.js.map +1 -0
  54. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +1 -1
  55. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
  56. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.d.ts.map +1 -1
  57. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js +14 -15
  58. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js.map +1 -1
  59. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js +11 -0
  60. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js.map +1 -1
  61. package/dist/components/icon/allowedIcons.d.ts +1 -0
  62. package/dist/components/icon/allowedIcons.d.ts.map +1 -1
  63. package/dist/components/icon/allowedIcons.js +2 -1
  64. package/dist/components/icon/allowedIcons.js.map +1 -1
  65. package/dist/components/skeleton/Skeleton.d.ts +10 -0
  66. package/dist/components/skeleton/Skeleton.d.ts.map +1 -0
  67. package/dist/components/skeleton/Skeleton.js +9 -0
  68. package/dist/components/skeleton/Skeleton.js.map +1 -0
  69. package/dist/components/skeleton/Skeleton.stories.d.ts +99 -0
  70. package/dist/components/skeleton/Skeleton.stories.d.ts.map +1 -0
  71. package/dist/components/skeleton/Skeleton.stories.js +539 -0
  72. package/dist/components/skeleton/Skeleton.stories.js.map +1 -0
  73. package/dist/components/skeleton/Skeleton.test.d.ts +2 -0
  74. package/dist/components/skeleton/Skeleton.test.d.ts.map +1 -0
  75. package/dist/components/skeleton/Skeleton.test.js +49 -0
  76. package/dist/components/skeleton/Skeleton.test.js.map +1 -0
  77. package/dist/index.css +225 -0
  78. package/dist/index.css.map +1 -1
  79. package/dist/index.d.ts +3 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +3 -0
  82. package/dist/index.js.map +1 -1
  83. package/package.json +2 -1
  84. package/src/components/breadcrumbs/BreadcrumbCopyButton.tsx +27 -0
  85. package/src/components/breadcrumbs/BreadcrumbDropdown.tsx +27 -0
  86. package/src/components/breadcrumbs/BreadcrumbEllipsis.tsx +16 -0
  87. package/src/components/breadcrumbs/BreadcrumbItemSeparator.tsx +3 -0
  88. package/src/components/breadcrumbs/BreadcrumbLink.tsx +29 -0
  89. package/src/components/breadcrumbs/Breadcrumbs.stories.tsx +870 -0
  90. package/src/components/breadcrumbs/Breadcrumbs.test.tsx +185 -0
  91. package/src/components/breadcrumbs/Breadcrumbs.tsx +92 -0
  92. package/src/components/breadcrumbs/breadcrumbs.scss +158 -0
  93. package/src/components/breadcrumbs/types.ts +31 -0
  94. package/src/components/button/button.scss +2 -0
  95. package/src/components/dropdown/dropdown.scss +1 -0
  96. package/src/components/fileUpload/FileUpload.stories.tsx +920 -0
  97. package/src/components/fileUpload/FileUpload.test.tsx +172 -0
  98. package/src/components/fileUpload/FileUpload.tsx +144 -0
  99. package/src/components/fileUpload/fileUpload.scss +74 -0
  100. package/src/components/formField/inputs/selectDropdown/SelectDropdown.stories.tsx +15 -16
  101. package/src/components/formField/inputs/selectDropdown/SelectDropdown.test.tsx +19 -0
  102. package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +1 -1
  103. package/src/components/icon/allowedIcons.tsx +2 -0
  104. package/src/components/progress/progress.scss +1 -0
  105. package/src/components/skeleton/Skeleton.stories.tsx +748 -0
  106. package/src/components/skeleton/Skeleton.test.tsx +55 -0
  107. package/src/components/skeleton/Skeleton.tsx +36 -0
  108. package/src/components/skeleton/skeleton.scss +23 -0
  109. package/src/index.scss +3 -0
  110. 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 attendanceReasonOptions = [
157
- { label: 'Authorised absence', value: 'authorised', header: 'Planned' },
158
- { label: 'Holiday', value: 'holiday', header: 'Planned' },
159
- { label: 'Medical appointment', value: 'medical', header: 'Health' },
160
- { label: 'Illness', value: 'illness', header: 'Health' },
161
- { label: 'Unauthorised absence', value: 'unauthorised', header: 'Unplanned' },
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 absence reason"
439
- options={attendanceReasonOptions}
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 absence reason"
809
+ placeholder="Select a staff member"
811
810
  options={[
812
- { label: 'Authorised absence', value: 'authorised', header: 'Planned' },
813
- { label: 'Holiday', value: 'holiday', header: 'Planned' },
814
- { label: 'Medical appointment', value: 'medical', header: 'Health' },
815
- { label: 'Illness', value: 'illness', header: 'Health' },
816
- { label: 'Unauthorised absence', value: 'unauthorised', header: 'Unplanned' },
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 small label above the option text, creating a two-line item layout. Use this to add context to complex options for example, showing the category of an absence type alongside its name.',
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,
@@ -9,5 +9,6 @@
9
9
  height: 100%;
10
10
  background-color: var(--color-brand-600);
11
11
  border-radius: var(--border-radius-large);
12
+ transition: transform 250ms linear;
12
13
  }
13
14
  }