@arbor-education/design-system.components 4.7.1 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/components/fileUpload/FileList.d.ts +7 -0
  3. package/dist/components/fileUpload/FileList.d.ts.map +1 -0
  4. package/dist/components/fileUpload/FileList.js +29 -0
  5. package/dist/components/fileUpload/FileList.js.map +1 -0
  6. package/dist/components/fileUpload/FileList.stories.d.ts +47 -0
  7. package/dist/components/fileUpload/FileList.stories.d.ts.map +1 -0
  8. package/dist/components/fileUpload/FileList.stories.js +136 -0
  9. package/dist/components/fileUpload/FileList.stories.js.map +1 -0
  10. package/dist/components/fileUpload/FileList.test.d.ts +2 -0
  11. package/dist/components/fileUpload/FileList.test.d.ts.map +1 -0
  12. package/dist/components/fileUpload/FileList.test.js +147 -0
  13. package/dist/components/fileUpload/FileList.test.js.map +1 -0
  14. package/dist/components/fileUpload/FileUpload.d.ts +14 -3
  15. package/dist/components/fileUpload/FileUpload.d.ts.map +1 -1
  16. package/dist/components/fileUpload/FileUpload.js +8 -25
  17. package/dist/components/fileUpload/FileUpload.js.map +1 -1
  18. package/dist/components/fileUpload/FileUpload.stories.d.ts +16 -1
  19. package/dist/components/fileUpload/FileUpload.stories.d.ts.map +1 -1
  20. package/dist/components/fileUpload/FileUpload.stories.js +51 -0
  21. package/dist/components/fileUpload/FileUpload.stories.js.map +1 -1
  22. package/dist/components/fileUpload/FileUpload.test.js +42 -0
  23. package/dist/components/fileUpload/FileUpload.test.js.map +1 -1
  24. package/dist/components/heading/Heading.d.ts +2 -387
  25. package/dist/components/heading/Heading.d.ts.map +1 -1
  26. package/dist/components/heading/Heading.js +12 -4
  27. package/dist/components/heading/Heading.js.map +1 -1
  28. package/dist/components/heading/Heading.stories.d.ts +12 -387
  29. package/dist/components/heading/Heading.stories.d.ts.map +1 -1
  30. package/dist/components/heading/Heading.stories.js +19 -0
  31. package/dist/components/heading/Heading.stories.js.map +1 -1
  32. package/dist/components/heading/Heading.test.js +24 -0
  33. package/dist/components/heading/Heading.test.js.map +1 -1
  34. package/dist/components/table/Table.d.ts +2 -0
  35. package/dist/components/table/Table.d.ts.map +1 -1
  36. package/dist/components/table/cellRenderers/CheckboxCellRenderer.stories.d.ts +1 -0
  37. package/dist/components/table/cellRenderers/CheckboxCellRenderer.stories.d.ts.map +1 -1
  38. package/dist/components/table/cellRenderers/CheckboxCellRenderer.stories.js +4 -0
  39. package/dist/components/table/cellRenderers/CheckboxCellRenderer.stories.js.map +1 -1
  40. package/dist/components/table/cellRenderers/CheckboxCellRenderer.test.js +7 -0
  41. package/dist/components/table/cellRenderers/CheckboxCellRenderer.test.js.map +1 -1
  42. package/dist/components/table/cellRenderers/InlineTextCellRenderer.d.ts +1 -0
  43. package/dist/components/table/cellRenderers/InlineTextCellRenderer.d.ts.map +1 -1
  44. package/dist/components/table/cellRenderers/InlineTextCellRenderer.js +2 -2
  45. package/dist/components/table/cellRenderers/InlineTextCellRenderer.js.map +1 -1
  46. package/dist/components/table/cellRenderers/InlineTextCellRenderer.stories.d.ts +14 -0
  47. package/dist/components/table/cellRenderers/InlineTextCellRenderer.stories.d.ts.map +1 -1
  48. package/dist/components/table/cellRenderers/InlineTextCellRenderer.stories.js +12 -0
  49. package/dist/components/table/cellRenderers/InlineTextCellRenderer.stories.js.map +1 -1
  50. package/dist/components/table/cellRenderers/InlineTextCellRenderer.test.d.ts +2 -0
  51. package/dist/components/table/cellRenderers/InlineTextCellRenderer.test.d.ts.map +1 -0
  52. package/dist/components/table/cellRenderers/InlineTextCellRenderer.test.js +28 -0
  53. package/dist/components/table/cellRenderers/InlineTextCellRenderer.test.js.map +1 -0
  54. package/dist/components/table/cellRenderers/NumberInputCellRenderer.d.ts +1 -0
  55. package/dist/components/table/cellRenderers/NumberInputCellRenderer.d.ts.map +1 -1
  56. package/dist/components/table/cellRenderers/NumberInputCellRenderer.js +2 -2
  57. package/dist/components/table/cellRenderers/NumberInputCellRenderer.js.map +1 -1
  58. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.d.ts +14 -0
  59. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.d.ts.map +1 -1
  60. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.js +12 -0
  61. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.js.map +1 -1
  62. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.js +10 -0
  63. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.js.map +1 -1
  64. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js +1 -1
  65. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js.map +1 -1
  66. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts +1 -0
  67. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts.map +1 -1
  68. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js +4 -0
  69. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js.map +1 -1
  70. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.test.js +9 -0
  71. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.test.js.map +1 -1
  72. package/dist/index.css +23 -0
  73. package/dist/index.css.map +1 -1
  74. package/package.json +1 -1
  75. package/src/components/fileUpload/FileList.stories.tsx +145 -0
  76. package/src/components/fileUpload/FileList.test.tsx +190 -0
  77. package/src/components/fileUpload/FileList.tsx +125 -0
  78. package/src/components/fileUpload/FileUpload.stories.tsx +79 -0
  79. package/src/components/fileUpload/FileUpload.test.tsx +82 -0
  80. package/src/components/fileUpload/FileUpload.tsx +29 -108
  81. package/src/components/fileUpload/fileUpload.scss +8 -0
  82. package/src/components/heading/Heading.stories.tsx +23 -0
  83. package/src/components/heading/Heading.test.tsx +38 -0
  84. package/src/components/heading/Heading.tsx +27 -3
  85. package/src/components/heading/heading.scss +18 -0
  86. package/src/components/table/cellRenderers/CheckboxCellRenderer.stories.tsx +16 -0
  87. package/src/components/table/cellRenderers/CheckboxCellRenderer.test.tsx +8 -0
  88. package/src/components/table/cellRenderers/InlineTextCellRenderer.stories.tsx +22 -0
  89. package/src/components/table/cellRenderers/InlineTextCellRenderer.test.tsx +31 -0
  90. package/src/components/table/cellRenderers/InlineTextCellRenderer.tsx +4 -1
  91. package/src/components/table/cellRenderers/NumberInputCellRenderer.stories.tsx +24 -0
  92. package/src/components/table/cellRenderers/NumberInputCellRenderer.test.tsx +13 -1
  93. package/src/components/table/cellRenderers/NumberInputCellRenderer.tsx +4 -1
  94. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.stories.tsx +19 -0
  95. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.test.tsx +11 -0
  96. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.tsx +1 -1
@@ -16,6 +16,24 @@
16
16
  }
17
17
  }
18
18
 
19
+ .ds-heading__wrapper {
20
+ display: flex;
21
+ flex-direction: column;
22
+ width: 100%;
23
+ }
24
+
25
+ .ds-heading__error-message {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: var(--form-field-spacing-horizontal);
29
+ padding: 0 var(--page-heading-spacing-gap) var(--page-heading-spacing-gap);
30
+ color: var(--form-field-text-error-color-error-text);
31
+
32
+ .ds-icon {
33
+ color: var(--form-field-text-error-color-error-icon);
34
+ }
35
+ }
36
+
19
37
  h1 {
20
38
  &.ds-heading, &.ds-heading .ds-heading__inner-container, .ds-editable-text__button, .ds-editable-text__input {
21
39
  font-family: var(--type-headings-h1-family);
@@ -267,6 +267,22 @@ export const DisabledChecked: Story = withDescription(
267
267
  'Disabled state — pass `disabled: true` via `cellRendererParams` to prevent the user from toggling the value. Useful for read-only rows or conditional editing logic.',
268
268
  );
269
269
 
270
+ export const WithError: Story = withDescription(
271
+ {
272
+ parameters: { controls: { disable: true } },
273
+ render: () => (
274
+ <CheckboxCellRenderer
275
+ {...({} as unknown as CustomCellRendererProps)}
276
+ value={false}
277
+ node={{ setDataValue: () => {} } as unknown as CustomCellRendererProps['node']}
278
+ column={{} as CustomCellRendererProps['column']}
279
+ colDef={{ headerName: 'Enrolled', cellRendererParams: { hasError: true } }}
280
+ />
281
+ ),
282
+ },
283
+ 'Error state — pass `hasError: true` via `cellRendererParams` to flag the checkbox. It forwards to `CheckboxInput`, which sets `aria-invalid="true"` and the error styling. Use it to show a required acknowledgement that has not been given.',
284
+ );
285
+
270
286
  const CHECKBOX_IN_TABLE_DATA = [
271
287
  { name: 'Alice Johnson', enrolled: true },
272
288
  { name: 'Bob Smith', enrolled: false },
@@ -33,6 +33,14 @@ describe('CheckboxCellRenderer', () => {
33
33
  expect(screen.getByRole('checkbox')).toBeChecked();
34
34
  });
35
35
 
36
+ test('sets aria-invalid when hasError is passed via cellRendererParams', () => {
37
+ const props = createMockProps({
38
+ colDef: { cellRendererParams: { hasError: true } },
39
+ });
40
+ render(<CheckboxCellRenderer {...props} />);
41
+ expect(screen.getByRole('checkbox')).toHaveAttribute('aria-invalid', 'true');
42
+ });
43
+
36
44
  test('calls node.setDataValue with toggled value on change', async () => {
37
45
  const setDataValue = vi.fn();
38
46
  const props = createMockProps({
@@ -158,6 +158,14 @@ const meta = {
158
158
  description: 'AG Grid column instance. Passed as the first argument to `node.setDataValue`.',
159
159
  table: { type: { summary: 'Column | null' } },
160
160
  },
161
+ hasError: {
162
+ control: 'boolean',
163
+ description: 'Applies the error style to the `TextInput` and sets `aria-invalid`. Pass via `cellRendererParams` (or a `cellRendererParams` function) to flag a cell. Visual and a11y only — the error message itself is not rendered by the cell.',
164
+ table: {
165
+ type: { summary: 'boolean' },
166
+ defaultValue: { summary: 'false' },
167
+ },
168
+ },
161
169
  },
162
170
  } satisfies Meta<typeof InlineTextCellRenderer>;
163
171
 
@@ -193,6 +201,20 @@ export const Default: Story = withDescription(
193
201
  'Pre-populated with an existing value. The `TextInput` is always visible — type to update the internal value, then press Enter to commit (in a real table, Enter calls `node.setDataValue`).',
194
202
  );
195
203
 
204
+ export const WithError: Story = withDescription(
205
+ {
206
+ parameters: { controls: { disable: true } },
207
+ render: () => (
208
+ <InlineTextCellRenderer
209
+ {...MOCK_CELL_PROPS}
210
+ value=""
211
+ hasError
212
+ />
213
+ ),
214
+ },
215
+ 'Error state — `hasError` applies the error border to the `TextInput` and sets `aria-invalid="true"`. Pass it via `cellRendererParams` to flag a cell, for example when a required value is missing. The error message itself is not rendered in the cell; surface it through the grid\'s own validation or a tooltip if users need the wording.',
216
+ );
217
+
196
218
  export const Empty: Story = withDescription(
197
219
  {
198
220
  parameters: {
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test, vi } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/vitest';
4
+ import type { CustomCellRendererProps } from 'ag-grid-react';
5
+ import { InlineTextCellRenderer } from './InlineTextCellRenderer.js';
6
+
7
+ const createMockProps = (overrides: Partial<CustomCellRendererProps> & { hasError?: boolean } = {}) => ({
8
+ value: 'Alice',
9
+ node: { setDataValue: vi.fn() },
10
+ column: 'name',
11
+ ...overrides,
12
+ } as unknown as CustomCellRendererProps & { value: string; hasError?: boolean });
13
+
14
+ describe('InlineTextCellRenderer', () => {
15
+ test('renders a text input with the supplied value', () => {
16
+ render(<InlineTextCellRenderer {...createMockProps()} />);
17
+ expect(screen.getByRole('textbox')).toHaveValue('Alice');
18
+ });
19
+
20
+ test('applies the error style and aria-invalid when hasError is true', () => {
21
+ render(<InlineTextCellRenderer {...createMockProps({ hasError: true })} />);
22
+ expect(screen.getByRole('textbox')).toHaveClass('ds-input--error');
23
+ expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'true');
24
+ });
25
+
26
+ test('does not apply the error style when hasError is not set', () => {
27
+ render(<InlineTextCellRenderer {...createMockProps()} />);
28
+ expect(screen.getByRole('textbox')).not.toHaveClass('ds-input--error');
29
+ expect(screen.getByRole('textbox')).not.toHaveAttribute('aria-invalid', 'true');
30
+ });
31
+ });
@@ -5,10 +5,11 @@ import { useComponentDidMount } from 'Utils/hooks/useComponentDidMount';
5
5
 
6
6
  type InlineTextCellRendererProps = CustomCellRendererProps & {
7
7
  value: string;
8
+ hasError?: boolean;
8
9
  };
9
10
 
10
11
  export const InlineTextCellRenderer = (props: InlineTextCellRendererProps) => {
11
- const { value, valueFormatted, column, node } = props;
12
+ const { value, valueFormatted, column, node, hasError } = props;
12
13
  const [valueToRender, setValueToRender] = useState(String(valueFormatted ?? value ?? ''));
13
14
  const valueRef = useRef(valueToRender);
14
15
  const wrapperRef = useRef<HTMLDivElement>(null);
@@ -41,6 +42,8 @@ export const InlineTextCellRenderer = (props: InlineTextCellRendererProps) => {
41
42
  <TextInput
42
43
  value={valueToRender}
43
44
  onChange={handleChange}
45
+ hasError={hasError}
46
+ aria-invalid={hasError}
44
47
  />
45
48
  </div>
46
49
  );
@@ -159,6 +159,14 @@ const meta = {
159
159
  description: '`colDef.field` is used as the key passed to `node.setDataValue`. Changes are silently ignored when `field` is absent.',
160
160
  table: { type: { summary: 'ColDef' } },
161
161
  },
162
+ hasError: {
163
+ control: 'boolean',
164
+ description: 'Applies the error style to the input container and sets `aria-invalid`. Pass via `cellRendererParams` (or a `cellRendererParams` function) to flag a cell. Visual and a11y only — the error message itself is not rendered by the cell.',
165
+ table: {
166
+ type: { summary: 'boolean' },
167
+ defaultValue: { summary: 'false' },
168
+ },
169
+ },
162
170
  },
163
171
  } satisfies Meta<typeof NumberInputCellRenderer>;
164
172
 
@@ -244,6 +252,22 @@ export const NullValue: Story = withDescription(
244
252
  '`value={null}` is coerced to `0` before being passed to `NumberInput`. This keeps the input always controlled with a valid numeric display.',
245
253
  );
246
254
 
255
+ export const WithError: Story = withDescription(
256
+ {
257
+ parameters: { controls: { disable: true } },
258
+ render: () => (
259
+ <NumberInputCellRenderer
260
+ {...({} as unknown as CustomCellRendererProps)}
261
+ value={0}
262
+ hasError
263
+ node={{ setDataValue: () => {} } as unknown as CustomCellRendererProps['node']}
264
+ colDef={{ field: 'count' }}
265
+ />
266
+ ),
267
+ },
268
+ 'Error state — `hasError` adds the error border to the input container and sets `aria-invalid="true"`. Pass it via `cellRendererParams` to flag a cell, for example when the value falls outside an allowed range. The error message itself is not rendered in the cell.',
269
+ );
270
+
247
271
  export const Disabled: Story = withDescription(
248
272
  {
249
273
  parameters: { controls: { disable: true } },
@@ -5,7 +5,7 @@ import userEvent from '@testing-library/user-event';
5
5
  import type { CustomCellRendererProps } from 'ag-grid-react';
6
6
  import { NumberInputCellRenderer } from './NumberInputCellRenderer.js';
7
7
 
8
- const createMockProps = (overrides: Partial<CustomCellRendererProps & { value: number | null; min?: number; max?: number; disabled?: boolean }> = {}): CustomCellRendererProps => ({
8
+ const createMockProps = (overrides: Partial<CustomCellRendererProps & { value: number | null; min?: number; max?: number; disabled?: boolean; hasError?: boolean }> = {}): CustomCellRendererProps => ({
9
9
  value: 0,
10
10
  node: { setDataValue: vi.fn() },
11
11
  colDef: { field: 'count' },
@@ -28,6 +28,18 @@ describe('NumberInputCellRenderer', () => {
28
28
  expect(screen.getByRole('textbox')).toHaveValue('0');
29
29
  });
30
30
 
31
+ test('applies the error style and aria-invalid when hasError is true', () => {
32
+ render(<NumberInputCellRenderer {...createMockProps({ hasError: true })} />);
33
+ expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'true');
34
+ expect(screen.getByRole('textbox').closest('.ds-number-input__container')).toHaveClass('ds-number-input__container--error');
35
+ });
36
+
37
+ test('does not apply the error style when hasError is not set', () => {
38
+ render(<NumberInputCellRenderer {...createMockProps()} />);
39
+ expect(screen.getByRole('textbox')).not.toHaveAttribute('aria-invalid', 'true');
40
+ expect(screen.getByRole('textbox').closest('.ds-number-input__container')).not.toHaveClass('ds-number-input__container--error');
41
+ });
42
+
31
43
  test('disables the input when disabled prop is true', () => {
32
44
  render(<NumberInputCellRenderer {...createMockProps({ disabled: true })} />);
33
45
  expect(screen.getByRole('textbox')).toBeDisabled();
@@ -6,12 +6,13 @@ type NumberInputCellRendererProps = {
6
6
  min?: number;
7
7
  max?: number;
8
8
  disabled?: boolean;
9
+ hasError?: boolean;
9
10
  } & CustomCellRendererProps;
10
11
 
11
12
  export const NumberInputCellRenderer = (
12
13
  props: NumberInputCellRendererProps,
13
14
  ) => {
14
- const { value, min, max, disabled, node, colDef = {} } = props;
15
+ const { value, min, max, disabled, hasError, node, colDef = {} } = props;
15
16
 
16
17
  return (
17
18
  <div className="cell-input-wrapper">
@@ -19,6 +20,8 @@ export const NumberInputCellRenderer = (
19
20
  min={min}
20
21
  max={max}
21
22
  disabled={disabled}
23
+ hasError={hasError}
24
+ aria-invalid={hasError}
22
25
  value={value ?? 0}
23
26
  onChange={(e) => {
24
27
  if (!colDef.field) {
@@ -326,6 +326,25 @@ export const Disabled: Story = withDescription(
326
326
  'Pass `disabled: true` via `cellRendererParams` to prevent the user from changing the selection for specific rows.',
327
327
  );
328
328
 
329
+ export const WithError: Story = withDescription(
330
+ {
331
+ parameters: { controls: { disable: true } },
332
+ render: () => (
333
+ <SelectDropdownCellRenderer
334
+ {...MOCK_CELL_PROPS}
335
+ api={MOCK_API}
336
+ value=""
337
+ options={STATUS_OPTIONS}
338
+ placeholder="Select status"
339
+ hasError={true}
340
+ node={{ setDataValue: () => {} } as unknown as CustomCellRendererProps['node']}
341
+ column={{} as CustomCellRendererProps['column']}
342
+ />
343
+ ),
344
+ },
345
+ 'Error state — pass `hasError: true` via `cellRendererParams` to flag the trigger. It applies the error style and sets `aria-invalid="true"` for screen readers automatically. The error message itself is not rendered in the cell.',
346
+ );
347
+
329
348
  export const WithBackgroundColor: Story = withDescription(
330
349
  {
331
350
  parameters: { controls: { disable: true } },
@@ -38,6 +38,17 @@ describe('SelectDropdownCellRenderer', () => {
38
38
  expect(screen.getByRole('button')).toBeInTheDocument();
39
39
  });
40
40
 
41
+ test('applies the error style and aria-invalid when hasError is true', () => {
42
+ render(<SelectDropdownCellRenderer {...createMockProps()} hasError={true} />);
43
+ expect(screen.getByRole('button')).toHaveClass('ds-button--error');
44
+ expect(screen.getByRole('button')).toHaveAttribute('aria-invalid', 'true');
45
+ });
46
+
47
+ test('does not apply the error style when hasError is not set', () => {
48
+ render(<SelectDropdownCellRenderer {...createMockProps()} />);
49
+ expect(screen.getByRole('button')).not.toHaveClass('ds-button--error');
50
+ });
51
+
41
52
  describe('backgroundColor', () => {
42
53
  test('does not apply the custom-bg or text-contrast modifiers when not provided', () => {
43
54
  const { container } = render(<SelectDropdownCellRenderer {...createMockProps()} />);
@@ -47,7 +47,7 @@ export const SelectDropdownCellRenderer = (
47
47
  hasError,
48
48
  id,
49
49
  'aria-describedBy': ariaDescribedBy,
50
- 'aria-invalid': ariaInvalid,
50
+ 'aria-invalid': ariaInvalid = hasError,
51
51
  alwaysShowPlaceholder = false,
52
52
  backgroundColor,
53
53
  fillCell = false,