@arbor-education/design-system.components 4.5.0 → 4.6.1

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 (50) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/components/avatar/Avatar.js +2 -2
  3. package/dist/components/avatar/Avatar.js.map +1 -1
  4. package/dist/components/badge/Badge.stories.d.ts +0 -1
  5. package/dist/components/badge/Badge.stories.d.ts.map +1 -1
  6. package/dist/components/badge/Badge.stories.js +0 -69
  7. package/dist/components/badge/Badge.stories.js.map +1 -1
  8. package/dist/components/dot/Dot.d.ts.map +1 -1
  9. package/dist/components/dot/Dot.js +1 -1
  10. package/dist/components/dot/Dot.js.map +1 -1
  11. package/dist/components/formField/inputs/checkbox/CheckboxInput.d.ts +2 -2
  12. package/dist/components/formField/inputs/checkbox/CheckboxInput.d.ts.map +1 -1
  13. package/dist/components/formField/inputs/checkbox/CheckboxInput.js +1 -1
  14. package/dist/components/formField/inputs/checkbox/CheckboxInput.js.map +1 -1
  15. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts +2 -0
  16. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts.map +1 -1
  17. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js +100 -0
  18. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js.map +1 -1
  19. package/dist/components/formField/inputs/checkbox/CheckboxInput.test.js +17 -1
  20. package/dist/components/formField/inputs/checkbox/CheckboxInput.test.js.map +1 -1
  21. package/dist/components/pill/Pill.d.ts.map +1 -1
  22. package/dist/components/pill/Pill.js +1 -1
  23. package/dist/components/pill/Pill.js.map +1 -1
  24. package/dist/components/searchBar/SearchBar.d.ts.map +1 -1
  25. package/dist/components/searchBar/SearchBar.js +1 -1
  26. package/dist/components/searchBar/SearchBar.js.map +1 -1
  27. package/dist/components/searchBar/SearchBar.test.js +3 -3
  28. package/dist/components/searchBar/SearchBar.test.js.map +1 -1
  29. package/dist/components/table/Table.d.ts +1 -1
  30. package/dist/components/table/Table.stories.d.ts.map +1 -1
  31. package/dist/components/table/Table.stories.js +0 -5
  32. package/dist/components/table/Table.stories.js.map +1 -1
  33. package/dist/components/table/Table.test.js +0 -5
  34. package/dist/components/table/Table.test.js.map +1 -1
  35. package/dist/utils/popupParent/PopupParentContext.stories.d.ts.map +1 -1
  36. package/dist/utils/popupParent/PopupParentContext.stories.js +1 -1
  37. package/dist/utils/popupParent/PopupParentContext.stories.js.map +1 -1
  38. package/package.json +4 -4
  39. package/src/components/avatar/Avatar.tsx +2 -2
  40. package/src/components/badge/Badge.stories.tsx +0 -103
  41. package/src/components/dot/Dot.tsx +1 -0
  42. package/src/components/formField/inputs/checkbox/CheckboxInput.stories.tsx +178 -1
  43. package/src/components/formField/inputs/checkbox/CheckboxInput.test.tsx +58 -0
  44. package/src/components/formField/inputs/checkbox/CheckboxInput.tsx +3 -3
  45. package/src/components/pill/Pill.tsx +1 -0
  46. package/src/components/searchBar/SearchBar.test.tsx +3 -3
  47. package/src/components/searchBar/SearchBar.tsx +1 -0
  48. package/src/components/table/Table.stories.tsx +0 -5
  49. package/src/components/table/Table.test.tsx +0 -5
  50. package/src/utils/popupParent/PopupParentContext.stories.tsx +1 -0
@@ -8,7 +8,7 @@ import {
8
8
  Subtitle,
9
9
  Title,
10
10
  } from '@storybook/addon-docs/blocks';
11
- import { useState } from 'react';
11
+ import { useState, type ReactNode } from 'react';
12
12
  import { fn } from 'storybook/test';
13
13
  import { CheckboxInput } from './CheckboxInput.js';
14
14
  import { CheckboxGroup } from './CheckboxGroup.js';
@@ -1036,3 +1036,180 @@ export default LockedReportOptionsExample;
1036
1036
  '`disabled` prop inside `options` if you need partial disabling.',
1037
1037
  ].join(' '),
1038
1038
  );
1039
+
1040
+ // ---------------------------------------------------------------------------
1041
+ // Rich labels
1042
+ // Each row shows the plain-text label (what the adaptor shipped before) beside
1043
+ // the rich label, so the before/after difference is visible in one story.
1044
+ // ---------------------------------------------------------------------------
1045
+
1046
+ const EMAIL_ICON_SRC = `data:image/svg+xml;utf8,${encodeURIComponent(
1047
+ '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><rect x="1" y="3" width="12" height="8" rx="1" fill="none" stroke="#1b4f8a" stroke-width="1.5"/><path d="M1.5 3.5 7 8l5.5-4.5" fill="none" stroke="#1b4f8a" stroke-width="1.5"/></svg>',
1048
+ )}`;
1049
+
1050
+ const RICH_LABEL_EXAMPLES: { type: string; plain: string; rich: ReactNode }[] = [
1051
+ {
1052
+ type: 'Bold text',
1053
+ plain: 'Smith, Jane',
1054
+ rich: (
1055
+ <>
1056
+ <b>Smith</b>
1057
+ , Jane
1058
+ </>
1059
+ ),
1060
+ },
1061
+ {
1062
+ type: 'Image',
1063
+ plain: 'Email Send to email',
1064
+ rich: (
1065
+ <>
1066
+ <img src={EMAIL_ICON_SRC} alt="Email" style={{ verticalAlign: 'middle', marginRight: 'var(--spacing-xsmall)' }} />
1067
+ Send to email
1068
+ </>
1069
+ ),
1070
+ },
1071
+ {
1072
+ type: 'Coloured text (warning)',
1073
+ plain: 'No email address',
1074
+ rich: <span style={{ color: 'var(--color-semantic-destructive-700)' }}>No email address</span>,
1075
+ },
1076
+ {
1077
+ type: 'Mixed formatting',
1078
+ plain: 'Maths: 0 students below target',
1079
+ rich: (
1080
+ <>
1081
+ Maths:
1082
+ {' '}
1083
+ <u><span style={{ color: 'var(--color-semantic-destructive-700)', fontWeight: 'bold' }}>0</span></u>
1084
+ {' '}
1085
+ students below target
1086
+ </>
1087
+ ),
1088
+ },
1089
+ ];
1090
+
1091
+ const RichLabelComparisonTemplate = () => {
1092
+ const [checked, setChecked] = useState<Record<string, boolean>>({});
1093
+
1094
+ return (
1095
+ <div style={{ maxWidth: '36rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
1096
+ {RICH_LABEL_EXAMPLES.map(example => (
1097
+ <div key={example.type} style={{ display: 'grid', gridTemplateColumns: '10rem 1fr 1fr', gap: 'var(--spacing-medium)', alignItems: 'center' }}>
1098
+ <span style={{ color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }}>{example.type}</span>
1099
+ <CheckboxInput
1100
+ id={`plain-${example.type}`}
1101
+ checked={!!checked[`plain-${example.type}`]}
1102
+ onChange={e => setChecked(prev => ({ ...prev, [`plain-${example.type}`]: e.target.checked }))}
1103
+ label={example.plain}
1104
+ />
1105
+ <CheckboxInput
1106
+ id={`rich-${example.type}`}
1107
+ checked={!!checked[`rich-${example.type}`]}
1108
+ onChange={e => setChecked(prev => ({ ...prev, [`rich-${example.type}`]: e.target.checked }))}
1109
+ label={example.rich}
1110
+ />
1111
+ </div>
1112
+ ))}
1113
+ </div>
1114
+ );
1115
+ };
1116
+
1117
+ export const RichLabelComparison: Story = withDescription(
1118
+ {
1119
+ render: () => <RichLabelComparisonTemplate />,
1120
+ parameters: {
1121
+ docs: {
1122
+ source: {
1123
+ language: 'tsx',
1124
+ code: `
1125
+ import { CheckboxInput } from '@arbor-education/design-system.components';
1126
+
1127
+ // label accepts any ReactNode. Keep formatting to the approved set
1128
+ // (bold, coloured text, icon) and keep the text itself meaningful on its own,
1129
+ // because it becomes the checkbox's accessible name.
1130
+ <CheckboxInput
1131
+ id="student-smith"
1132
+ checked={checked}
1133
+ onChange={(e) => setChecked(e.target.checked)}
1134
+ label={<><b>Smith</b>, Jane</>}
1135
+ />
1136
+ `.trim(),
1137
+ },
1138
+ },
1139
+ },
1140
+ },
1141
+ [
1142
+ 'Each row compares a plain-text label (left) with the rich label (right). The rich label keeps the',
1143
+ 'formatting that legacy SIS labels carry: bold names, icons, coloured warnings and highlighted counts.',
1144
+ 'The whole label remains the checkbox accessible name, so the text must make sense without the styling.',
1145
+ ].join(' '),
1146
+ );
1147
+
1148
+ const RICH_GUARDIAN_OPTIONS = [
1149
+ {
1150
+ id: 'rich-guardian-1',
1151
+ label: (
1152
+ <>
1153
+ <b>Smith</b>
1154
+ , Jane (mother)
1155
+ </>
1156
+ ),
1157
+ checked: true,
1158
+ onChange: fn(),
1159
+ },
1160
+ {
1161
+ id: 'rich-guardian-2',
1162
+ label: (
1163
+ <>
1164
+ <img src={EMAIL_ICON_SRC} alt="Email" style={{ verticalAlign: 'middle', marginRight: 'var(--spacing-xsmall)' }} />
1165
+ Smith, Tom (father)
1166
+ </>
1167
+ ),
1168
+ checked: false,
1169
+ onChange: fn(),
1170
+ },
1171
+ {
1172
+ id: 'rich-guardian-3',
1173
+ label: (
1174
+ <>
1175
+ <span style={{ color: 'var(--color-semantic-destructive-700)' }}>No email address</span>
1176
+ {' '}
1177
+ — Smith, Ann
1178
+ </>
1179
+ ),
1180
+ checked: false,
1181
+ onChange: fn(),
1182
+ },
1183
+ ];
1184
+
1185
+ export const RichLabelInCheckboxGroup: Story = withDescription(
1186
+ {
1187
+ render: () => (
1188
+ <div style={{ maxWidth: '28rem', padding: 'var(--spacing-xlarge)' }}>
1189
+ <CheckboxGroup
1190
+ legend="Absence alert recipients"
1191
+ options={RICH_GUARDIAN_OPTIONS}
1192
+ />
1193
+ </div>
1194
+ ),
1195
+ parameters: {
1196
+ docs: {
1197
+ source: {
1198
+ language: 'tsx',
1199
+ code: `
1200
+ import { CheckboxGroup } from '@arbor-education/design-system.components';
1201
+
1202
+ <CheckboxGroup
1203
+ legend="Absence alert recipients"
1204
+ options={[
1205
+ { id: 'guardian-1', label: <><b>Smith</b>, Jane (mother)</>, checked: true, onChange },
1206
+ { id: 'guardian-2', label: <><img src={emailIcon} alt="Email" />Smith, Tom (father)</>, checked: false, onChange },
1207
+ ]}
1208
+ />
1209
+ `.trim(),
1210
+ },
1211
+ },
1212
+ },
1213
+ },
1214
+ 'Rich labels in a CheckboxGroup. Each option keeps its own formatting; the group legend still labels the set.',
1215
+ );
@@ -32,4 +32,62 @@ describe('CheckboxInput component', () => {
32
32
  expect(checkboxElement).toBeDisabled();
33
33
  expect(checkboxElement.matches(':disabled')).toBe(true);
34
34
  });
35
+
36
+ test('CheckboxInput renders a rich label with its formatting', () => {
37
+ render(
38
+ <CheckboxInput
39
+ onChange={() => {}}
40
+ label={(
41
+ <>
42
+ <b>Smith</b>
43
+ , Jane
44
+ </>
45
+ )}
46
+ />,
47
+ );
48
+
49
+ expect(screen.getByText('Smith').tagName).toBe('B');
50
+ });
51
+
52
+ test('CheckboxInput keeps the rich label as its accessible name', () => {
53
+ render(
54
+ <CheckboxInput
55
+ onChange={() => {}}
56
+ label={(
57
+ <>
58
+ <span style={{ color: 'red' }}>No email address</span>
59
+ </>
60
+ )}
61
+ />,
62
+ );
63
+
64
+ expect(
65
+ screen.getByRole('checkbox', { name: 'No email address' }),
66
+ ).toBeInTheDocument();
67
+ });
68
+
69
+ test('CheckboxInput renders a numeric zero label inside the label text', () => {
70
+ const { container } = render(<CheckboxInput label={0} onChange={() => {}} />);
71
+
72
+ expect(container.querySelector('.ds-checkbox-label__text')?.textContent).toBe('0');
73
+ });
74
+
75
+ test('CheckboxInput keeps the accessible name of a rich image label', () => {
76
+ render(
77
+ <CheckboxInput
78
+ onChange={() => {}}
79
+ label={(
80
+ <>
81
+ <img src="/email.svg" alt="Email" />
82
+ {' '}
83
+ Send to email
84
+ </>
85
+ )}
86
+ />,
87
+ );
88
+
89
+ expect(
90
+ screen.getByRole('checkbox', { name: 'Email Send to email' }),
91
+ ).toBeInTheDocument();
92
+ });
35
93
  });
@@ -1,10 +1,10 @@
1
1
  import classNames from 'classnames';
2
- import { useRef, useEffect, type InputHTMLAttributes } from 'react';
2
+ import { useRef, useEffect, type InputHTMLAttributes, type ReactNode } from 'react';
3
3
  import { Icon } from 'Components/icon/Icon';
4
4
 
5
5
  export type CheckboxInputProps = {
6
6
  indeterminate?: boolean;
7
- label?: string;
7
+ label?: ReactNode;
8
8
  hasError?: boolean;
9
9
  } & InputHTMLAttributes<HTMLInputElement>;
10
10
 
@@ -76,7 +76,7 @@ export const CheckboxInput = (props: CheckboxInputProps) => {
76
76
  />
77
77
  )}
78
78
  </span>
79
- {label && (
79
+ {label !== undefined && label !== null && label !== false && label !== '' && (
80
80
  <span className="ds-checkbox-label__text">{label}</span>
81
81
  )}
82
82
  </label>
@@ -56,6 +56,7 @@ export const Pill = (props: PillProps) => {
56
56
  checked={checked}
57
57
  onChange={() => {}}
58
58
  onClick={handleCheckboxClick}
59
+ aria-label={text}
59
60
  />
60
61
  </span>
61
62
  )}
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test, vi, beforeEach } from 'vitest';
2
- import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { render, screen, fireEvent, within } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom/vitest';
4
4
  import { SearchBar } from './SearchBar.js';
5
5
  import userEvent from '@testing-library/user-event';
@@ -39,14 +39,14 @@ describe('SearchBar component', () => {
39
39
  const button = screen.getByTestId('search-bar-inactive');
40
40
  fireEvent.mouseEnter(button);
41
41
  fireEvent.mouseLeave(button);
42
- expect(screen.getByText('Search')).toBeInTheDocument();
42
+ expect(within(button).getByText('Search', { selector: '.ds-search-bar--inactive__placeholder' })).toBeInTheDocument();
43
43
  });
44
44
 
45
45
  test('keeps placeholder on hover when no hoverText provided', () => {
46
46
  render(<SearchBar setSearchValue={setSearchMock} placeholderText="Search" />);
47
47
  const button = screen.getByTestId('search-bar-inactive');
48
48
  fireEvent.mouseEnter(button);
49
- expect(screen.getByText('Search')).toBeInTheDocument();
49
+ expect(within(button).getByText('Search', { selector: '.ds-search-bar--inactive__placeholder' })).toBeInTheDocument();
50
50
  });
51
51
  });
52
52
 
@@ -80,6 +80,7 @@ export const SearchBar = (props: SearchBarProps) => {
80
80
  >
81
81
  <Icon
82
82
  name="search"
83
+ screenReaderText="Search"
83
84
  size={16}
84
85
  color="var(--search-global-default-color-icon)"
85
86
  className="ds-search-bar__icon"
@@ -516,7 +516,6 @@ const sampleColumnDefs: (ColDef | ColGroupDef)[] = [
516
516
  { field: 'status', valueFormatter: Table.DefaultValueFormatter },
517
517
  {
518
518
  field: 'marks',
519
- cellDataType: 'number',
520
519
  type: 'dsNumberColumn',
521
520
  cellEditorParams: {
522
521
  min: 0,
@@ -528,13 +527,11 @@ const sampleColumnDefs: (ColDef | ColGroupDef)[] = [
528
527
  {
529
528
  field: 'active',
530
529
  filter: { component: BooleanFilter, doesFilterPass: doesBooleanFilterPass },
531
- cellDataType: 'boolean',
532
530
  editable: false,
533
531
  },
534
532
  {
535
533
  field: 'time',
536
534
  filter: { component: TimeFilter, doesFilterPass: doesTimeFilterPass },
537
- cellDataType: 'string',
538
535
  editable: false,
539
536
  },
540
537
  ];
@@ -578,12 +575,10 @@ const verticalHeaderTextColumnDefs: (ColDef | ColGroupDef)[] = [
578
575
  {
579
576
  field: 'marks',
580
577
  headerName: 'Overall Score (%)',
581
- cellDataType: 'number',
582
578
  },
583
579
  {
584
580
  field: 'active',
585
581
  headerName: 'Active?',
586
- cellDataType: 'boolean',
587
582
  editable: false,
588
583
  },
589
584
  {
@@ -1524,7 +1524,6 @@ describe('Table', () => {
1524
1524
  field: 'age',
1525
1525
  headerName: 'Age',
1526
1526
  editable: true,
1527
- cellDataType: 'number',
1528
1527
  cellEditor: 'dsNumberCellEditor',
1529
1528
  cellEditorParams: {
1530
1529
  min: 0,
@@ -1571,7 +1570,6 @@ describe('Table', () => {
1571
1570
  field: 'age',
1572
1571
  headerName: 'Age',
1573
1572
  editable: true,
1574
- cellDataType: 'number',
1575
1573
  cellEditor: 'dsNumberCellEditor',
1576
1574
  cellEditorParams: {
1577
1575
  min: 0,
@@ -1619,7 +1617,6 @@ describe('Table', () => {
1619
1617
  field: 'age',
1620
1618
  headerName: 'Age',
1621
1619
  editable: true,
1622
- cellDataType: 'number',
1623
1620
  cellEditor: 'dsNumberCellEditor',
1624
1621
  cellEditorParams: {
1625
1622
  min: 0,
@@ -1667,7 +1664,6 @@ describe('Table', () => {
1667
1664
  field: 'age',
1668
1665
  headerName: 'Age',
1669
1666
  editable: true,
1670
- cellDataType: 'number',
1671
1667
  cellEditor: 'dsNumberCellEditor',
1672
1668
  cellEditorParams: {
1673
1669
  min: 50,
@@ -1717,7 +1713,6 @@ describe('Table', () => {
1717
1713
  field: 'age',
1718
1714
  headerName: 'Age',
1719
1715
  editable: true,
1720
- cellDataType: 'number',
1721
1716
  cellEditor: 'dsNumberCellEditor',
1722
1717
  cellEditorParams: {
1723
1718
  min: 0,
@@ -247,6 +247,7 @@ const WithCustomContainerTemplate = () => {
247
247
  minHeight: '2rem',
248
248
  }}
249
249
  aria-label="Custom popup container"
250
+ aria-hidden={true}
250
251
  />
251
252
 
252
253
  <PopupParentContext.Provider value={popupContainerRef}>