@arbor-education/design-system.components 0.25.6 → 0.25.8

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 (63) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts +1 -0
  3. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts.map +1 -1
  4. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +2 -2
  5. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
  6. package/dist/components/row/Row.d.ts +2 -0
  7. package/dist/components/row/Row.d.ts.map +1 -1
  8. package/dist/components/row/Row.js +14 -5
  9. package/dist/components/row/Row.js.map +1 -1
  10. package/dist/components/row/Row.stories.d.ts +27 -0
  11. package/dist/components/row/Row.stories.d.ts.map +1 -1
  12. package/dist/components/row/Row.stories.js +101 -0
  13. package/dist/components/row/Row.stories.js.map +1 -1
  14. package/dist/components/row/Row.test.d.ts.map +1 -1
  15. package/dist/components/row/Row.test.js +39 -1
  16. package/dist/components/row/Row.test.js.map +1 -1
  17. package/dist/components/table/Table.d.ts +6 -0
  18. package/dist/components/table/Table.d.ts.map +1 -1
  19. package/dist/components/table/Table.js +3 -0
  20. package/dist/components/table/Table.js.map +1 -1
  21. package/dist/components/table/Table.stories.d.ts +1 -0
  22. package/dist/components/table/Table.stories.d.ts.map +1 -1
  23. package/dist/components/table/Table.stories.js +41 -0
  24. package/dist/components/table/Table.stories.js.map +1 -1
  25. package/dist/components/table/cellRenderers/NumberInputCellRenderer.d.ts +10 -0
  26. package/dist/components/table/cellRenderers/NumberInputCellRenderer.d.ts.map +1 -0
  27. package/dist/components/table/cellRenderers/NumberInputCellRenderer.js +13 -0
  28. package/dist/components/table/cellRenderers/NumberInputCellRenderer.js.map +1 -0
  29. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.d.ts +90 -0
  30. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.d.ts.map +1 -0
  31. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.js +243 -0
  32. package/dist/components/table/cellRenderers/NumberInputCellRenderer.stories.js.map +1 -0
  33. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.d.ts +2 -0
  34. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.d.ts.map +1 -0
  35. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.js +72 -0
  36. package/dist/components/table/cellRenderers/NumberInputCellRenderer.test.js.map +1 -0
  37. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.d.ts.map +1 -1
  38. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js +2 -2
  39. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js.map +1 -1
  40. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts +13 -0
  41. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts.map +1 -1
  42. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js +56 -0
  43. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js.map +1 -1
  44. package/dist/index.css +8 -0
  45. package/dist/index.css.map +1 -1
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +1 -0
  49. package/dist/index.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +3 -1
  52. package/src/components/row/Row.stories.tsx +141 -0
  53. package/src/components/row/Row.test.tsx +54 -1
  54. package/src/components/row/Row.tsx +29 -10
  55. package/src/components/row/row.scss +10 -0
  56. package/src/components/table/Table.stories.tsx +45 -0
  57. package/src/components/table/Table.tsx +3 -0
  58. package/src/components/table/cellRenderers/NumberInputCellRenderer.stories.tsx +346 -0
  59. package/src/components/table/cellRenderers/NumberInputCellRenderer.test.tsx +99 -0
  60. package/src/components/table/cellRenderers/NumberInputCellRenderer.tsx +36 -0
  61. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.stories.tsx +67 -0
  62. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.tsx +2 -0
  63. package/src/index.ts +1 -0
@@ -24,6 +24,7 @@ const DESCRIPTION_INTRO = [
24
24
  '`Row` is a single-line display component for presenting a labelled piece of data inside summary panels, detail cards, or section-style layouts.',
25
25
  'Pass an `onClick` handler to make the row fully interactive — it enters the tab order, gains hover and focus styles, and renders a pair of animated directional icons.',
26
26
  'Pass `href` to turn the value into a native anchor link, supporting any protocol including `mailto:` and `tel:`.',
27
+ 'Pass `tooltipText` to show an explanatory tooltip when a user hovers the row — useful for read-only fields or values that need additional context.',
27
28
  ].join(' ');
28
29
 
29
30
  const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
@@ -130,6 +131,8 @@ const DEVELOPER_NOTES = [
130
131
  ' value?: ReactNode;',
131
132
  ' href?: string;',
132
133
  ' note?: ReactNode;',
134
+ ' placeholder?: string;',
135
+ ' tooltipText?: string;',
133
136
  ' onClick?: MouseEventHandler<HTMLDivElement>;',
134
137
  '};',
135
138
  '```',
@@ -229,6 +232,30 @@ const meta = {
229
232
  defaultValue: { summary: 'undefined' },
230
233
  },
231
234
  },
235
+ tooltipText: {
236
+ description: [
237
+ 'When provided, wraps the row in a `Tooltip` and shows this string as the tooltip content on hover.',
238
+ 'Use it to surface contextual help for read-only fields, abbreviations, or values that benefit from further explanation.',
239
+ 'Omit the prop (or pass `undefined`) when no tooltip is needed — the Tooltip wrapper is still rendered but remains invisible.',
240
+ ].join(' '),
241
+ control: 'text',
242
+ table: {
243
+ type: { summary: 'string' },
244
+ defaultValue: { summary: 'undefined' },
245
+ },
246
+ },
247
+ placeholder: {
248
+ description: [
249
+ 'Fallback text shown in the value slot when no `value` prop is provided.',
250
+ 'Rendered in italics to signal that no real data is set.',
251
+ 'Ignored when `value` is present — `value` always takes precedence.',
252
+ ].join(' '),
253
+ control: 'text',
254
+ table: {
255
+ type: { summary: 'string' },
256
+ defaultValue: { summary: 'undefined' },
257
+ },
258
+ },
232
259
  onClick: {
233
260
  description: [
234
261
  'When provided, activates all clickable behaviour: the row enters the tab order (`tabIndex={0}`),',
@@ -426,6 +453,74 @@ export default ActiveStatusRow;
426
453
  'A value with no label — the label column renders empty. Valid but uncommon; most real-world rows pair a label with every value.',
427
454
  );
428
455
 
456
+ export const WithPlaceholder: Story = withDescription(
457
+ {
458
+ parameters: {
459
+ controls: { disable: true },
460
+ docs: {
461
+ source: {
462
+ language: 'tsx',
463
+ code: `
464
+ import { Row } from '@arbor-education/design-system.components';
465
+
466
+ function MedicalConditionRow() {
467
+ return (
468
+ <Row
469
+ label="Medical condition"
470
+ placeholder="Not recorded"
471
+ />
472
+ );
473
+ }
474
+ export default MedicalConditionRow;
475
+ `.trim(),
476
+ },
477
+ },
478
+ },
479
+ render: () => (
480
+ <Row
481
+ label="Medical condition"
482
+ placeholder="Not recorded"
483
+ />
484
+ ),
485
+ },
486
+ 'When `placeholder` is provided and no `value` is set, the placeholder renders in the value slot with italic styling — visually distinct from real data. Use it for optional fields where an empty state is meaningful (e.g. "Not recorded", "Not applicable").',
487
+ );
488
+
489
+ export const PlaceholderIgnoredWhenValuePresent: Story = withDescription(
490
+ {
491
+ parameters: {
492
+ controls: { disable: true },
493
+ docs: {
494
+ source: {
495
+ language: 'tsx',
496
+ code: `
497
+ import { Row } from '@arbor-education/design-system.components';
498
+
499
+ function MedicalConditionRow() {
500
+ return (
501
+ <Row
502
+ label="Medical condition"
503
+ value="Asthma"
504
+ placeholder="Not recorded"
505
+ />
506
+ );
507
+ }
508
+ export default MedicalConditionRow;
509
+ `.trim(),
510
+ },
511
+ },
512
+ },
513
+ render: () => (
514
+ <Row
515
+ label="Medical condition"
516
+ value="Asthma"
517
+ placeholder="Not recorded"
518
+ />
519
+ ),
520
+ },
521
+ 'When both `value` and `placeholder` are provided, `value` always wins — the placeholder is never shown. Consumers can safely pass a `placeholder` as a permanent fallback without needing to conditionally omit it when data is present.',
522
+ );
523
+
429
524
  export const WithNote: Story = withDescription(
430
525
  {
431
526
  parameters: {
@@ -997,6 +1092,52 @@ import { Row } from '@arbor-education/design-system.components';
997
1092
  ].join(' '),
998
1093
  );
999
1094
 
1095
+ export const WithTooltip: Story = withDescription(
1096
+ {
1097
+ parameters: {
1098
+ controls: { disable: true },
1099
+ docs: {
1100
+ source: {
1101
+ language: 'tsx',
1102
+ code: `
1103
+ import { Row } from '@arbor-education/design-system.components';
1104
+
1105
+ function ReadOnlyRow() {
1106
+ return (
1107
+ <Row
1108
+ label="UPN"
1109
+ value="A823456789012"
1110
+ tooltipText="Unique Pupil Number — assigned by the DfE and cannot be changed here"
1111
+ />
1112
+ );
1113
+ }
1114
+ export default ReadOnlyRow;
1115
+ `.trim(),
1116
+ },
1117
+ },
1118
+ },
1119
+ render: () => (
1120
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
1121
+ <Row
1122
+ label="UPN"
1123
+ value="A823456789012"
1124
+ tooltipText="Unique Pupil Number — assigned by the DfE and cannot be changed here"
1125
+ />
1126
+ <Row
1127
+ label="Admission number"
1128
+ value="001847"
1129
+ tooltipText="Set at admission — contact the office to request a change"
1130
+ />
1131
+ <Row
1132
+ label="Year group"
1133
+ value="Year 9"
1134
+ />
1135
+ </div>
1136
+ ),
1137
+ },
1138
+ 'Hover either of the first two rows to see the tooltip. The third row has no `tooltipText` — hovering it shows nothing. Use `tooltipText` to clarify read-only fields, abbreviations, or values that benefit from a short explanation without cluttering the label column.',
1139
+ );
1140
+
1000
1141
  export const CustomClassAndStyle: Story = withDescription(
1001
1142
  {
1002
1143
  parameters: {
@@ -1,5 +1,6 @@
1
1
  import { expect, test, describe, vi } from 'vitest';
2
- import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
3
4
  import { Row } from './Row.js';
4
5
  import '@testing-library/jest-dom/vitest';
5
6
 
@@ -105,6 +106,58 @@ describe('Row component', () => {
105
106
  });
106
107
  });
107
108
 
109
+ describe('placeholder behaviour', () => {
110
+ test('renders placeholder text when no value is provided', () => {
111
+ render(<Row label="Medical condition" placeholder="Not recorded" />);
112
+
113
+ expect(screen.getByText('Not recorded')).toBeInTheDocument();
114
+ });
115
+
116
+ test('renders placeholder element when no value is provided', () => {
117
+ const { container } = render(<Row placeholder="Not recorded" />);
118
+
119
+ expect(container.querySelector('.ds-row__placeholder')).toBeInTheDocument();
120
+ });
121
+
122
+ test('renders value instead of placeholder when both are provided', () => {
123
+ render(<Row value="Asthma" placeholder="Not recorded" />);
124
+
125
+ expect(screen.getByText('Asthma')).toBeInTheDocument();
126
+ expect(screen.queryByText('Not recorded')).not.toBeInTheDocument();
127
+ });
128
+
129
+ test('renders value element (not placeholder element) when both are provided', () => {
130
+ const { container } = render(<Row value="Asthma" placeholder="Not recorded" />);
131
+
132
+ expect(container.querySelector('.ds-row__value')).toBeInTheDocument();
133
+ expect(container.querySelector('.ds-row__placeholder')).not.toBeInTheDocument();
134
+ });
135
+
136
+ test('does not render placeholder element when placeholder is not provided', () => {
137
+ const { container } = render(<Row label="Medical condition" />);
138
+
139
+ expect(container.querySelector('.ds-row__placeholder')).not.toBeInTheDocument();
140
+ });
141
+ });
142
+
143
+ describe('tooltip behaviour', () => {
144
+ test('shows tooltip text when hovering the row', async () => {
145
+ render(<Row label="Year group" value="Year 9" tooltipText="This field is read-only" />);
146
+
147
+ await userEvent.hover(screen.getByText('Year group'));
148
+
149
+ await waitFor(() => {
150
+ expect(screen.getAllByText('This field is read-only')[0]).toBeInTheDocument();
151
+ });
152
+ });
153
+
154
+ test('does not render tooltip text when tooltipText is not provided', () => {
155
+ render(<Row label="Year group" value="Year 9" />);
156
+
157
+ expect(screen.queryByText('This field is read-only')).not.toBeInTheDocument();
158
+ });
159
+ });
160
+
108
161
  describe('ReactNode content', () => {
109
162
  test('accepts ReactNode label', () => {
110
163
  render(<Row label={<span className="foo">bar</span>} />);
@@ -1,5 +1,6 @@
1
1
  import classNames from 'classnames';
2
2
  import { Icon } from 'Components/icon/Icon';
3
+ import { TooltipWrapper } from 'Components/tooltip/TooltipWrapper';
3
4
  import type { CSSProperties, MouseEvent, MouseEventHandler, ReactNode } from 'react';
4
5
  import { ENTER_KEY, SPACE_KEY } from 'Utils/keyboardConstants';
5
6
 
@@ -10,6 +11,8 @@ export type RowProps = {
10
11
  value?: ReactNode;
11
12
  href?: string;
12
13
  note?: ReactNode;
14
+ placeholder?: string;
15
+ tooltipText?: string;
13
16
  onClick?: MouseEventHandler<HTMLDivElement>;
14
17
  };
15
18
 
@@ -21,12 +24,24 @@ export const Row = (props: RowProps) => {
21
24
  value,
22
25
  href,
23
26
  note,
27
+ placeholder,
28
+ tooltipText,
24
29
  onClick,
25
30
  } = props;
26
31
 
27
32
  const isClickable = !!onClick;
28
33
 
29
- return (
34
+ const shouldShowPlaceholder = !value && !href && !!placeholder;
35
+
36
+ const rowContent = href
37
+ ? (
38
+ <a href={href}>
39
+ {value || href}
40
+ </a>
41
+ )
42
+ : value;
43
+
44
+ const row = (
30
45
  <div
31
46
  className={classNames(
32
47
  'ds-row',
@@ -46,15 +61,9 @@ export const Row = (props: RowProps) => {
46
61
  tabIndex={isClickable ? 0 : -1}
47
62
  >
48
63
  <span className="ds-row__label">{label}</span>
49
- <span className="ds-row__value">
50
- {href
51
- ? (
52
- <a href={href}>
53
- {value || href}
54
- </a>
55
- )
56
- : value}
57
- </span>
64
+ {shouldShowPlaceholder
65
+ ? <span className="ds-row__placeholder">{placeholder}</span>
66
+ : <span className="ds-row__value">{rowContent}</span>}
58
67
  <span className="ds-row__note">{note}</span>
59
68
  {isClickable && (
60
69
  <>
@@ -64,6 +73,16 @@ export const Row = (props: RowProps) => {
64
73
  )}
65
74
  </div>
66
75
  );
76
+
77
+ if (tooltipText) {
78
+ return (
79
+ <TooltipWrapper tooltipContent={tooltipText}>
80
+ {row}
81
+ </TooltipWrapper>
82
+ );
83
+ }
84
+
85
+ return row;
67
86
  };
68
87
 
69
88
  export namespace Row {
@@ -61,6 +61,16 @@
61
61
  }
62
62
  }
63
63
 
64
+ &__placeholder {
65
+ flex-grow: 1;
66
+ flex-basis: 70%;
67
+ font-style: italic;
68
+
69
+ &:has(+ .ds-row__note) {
70
+ flex-basis: 50%;
71
+ }
72
+ }
73
+
64
74
  &__note {
65
75
  font-style: italic;
66
76
  flex-basis: 20%;
@@ -87,6 +87,7 @@ const DEVELOPER_NOTES = [
87
87
  '| `dsSelectDropdownCellRenderer` | `Table.SelectDropdownCellRenderer` | Renders a `SelectDropdown` in a cell |',
88
88
  '| `dsBooleanCellRenderer` | `Table.BooleanCellRenderer` | Read-only boolean display |',
89
89
  '| `dsCheckboxCellRenderer` | `Table.CheckboxCellRenderer` | Editable boolean checkbox |',
90
+ '| `dsNumberInputCellRenderer` | `Table.NumberInputCellRenderer` | Editable numeric input |',
90
91
  '| `dsDateCellEditor` | `Table.DateCellEditor` | Date picker cell editor |',
91
92
  '',
92
93
  '---',
@@ -2408,4 +2409,48 @@ export const WithComboboxCellRenderer: Story = {
2408
2409
  },
2409
2410
  };
2410
2411
 
2412
+ const numberInputSampleData = [
2413
+ { name: 'Alice Johnson', sessions: 12, score: 95 },
2414
+ { name: 'Bob Smith', sessions: 8, score: 72 },
2415
+ { name: 'Charlie Brown', sessions: 15, score: 88 },
2416
+ { name: 'Diana Prince', sessions: 3, score: 60 },
2417
+ ];
2418
+
2419
+ const numberInputColDefs: (ColDef | ColGroupDef)[] = [
2420
+ { field: 'name', headerName: 'Name', flex: 2 },
2421
+ {
2422
+ field: 'sessions',
2423
+ headerName: 'Sessions',
2424
+ flex: 1,
2425
+ editable: false,
2426
+ cellRenderer: 'dsNumberInputCellRenderer',
2427
+ cellRendererParams: { min: 0, suppressCellFocusAndFocusFirstElement: true },
2428
+ },
2429
+ {
2430
+ field: 'score',
2431
+ headerName: 'Score (/100)',
2432
+ flex: 1,
2433
+ editable: false,
2434
+ cellRenderer: 'dsNumberInputCellRenderer',
2435
+ cellRendererParams: { min: 0, max: 100, suppressCellFocusAndFocusFirstElement: true },
2436
+ },
2437
+ ];
2438
+
2439
+ export const WithNumberInputCellRenderer: Story = {
2440
+ parameters: {
2441
+ docs: {
2442
+ description: {
2443
+ story:
2444
+ 'Columns can use the `dsNumberInputCellRenderer` cell renderer to display an editable numeric input inside the cell. Pass `min` and `max` via `cellRendererParams` to constrain the allowed range — the spinners disable at the bounds.',
2445
+ },
2446
+ },
2447
+ },
2448
+ args: {
2449
+ rowData: numberInputSampleData,
2450
+ columnDefs: numberInputColDefs,
2451
+ defaultColDef,
2452
+ domLayout: 'autoHeight',
2453
+ },
2454
+ };
2455
+
2411
2456
  export default meta;
@@ -35,6 +35,7 @@ import { BooleanCellRenderer } from './cellRenderers/BooleanCellRenderer.js';
35
35
  import { CheckboxCellRenderer } from './cellRenderers/CheckboxCellRenderer.js';
36
36
  import { ComboboxCellRenderer } from './cellRenderers/ComboboxCellRenderer.js';
37
37
  import { DefaultCellRenderer } from './cellRenderers/DefaultCellRenderer.js';
38
+ import { NumberInputCellRenderer } from './cellRenderers/NumberInputCellRenderer.js';
38
39
 
39
40
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
41
  type TableProps<TData = any> = {
@@ -210,6 +211,7 @@ export const Table = (props: TableProps) => {
210
211
  dsCheckboxCellRenderer: CheckboxCellRenderer,
211
212
  dsBooleanCellRenderer: BooleanCellRenderer,
212
213
  dsComboboxCellRenderer: ComboboxCellRenderer,
214
+ dsNumberInputCellRenderer: NumberInputCellRenderer,
213
215
  ...components,
214
216
  }}
215
217
  {...rest}
@@ -245,6 +247,7 @@ Table.ComboboxCellRenderer = ComboboxCellRenderer;
245
247
  Table.DefaultValueFormatter = defaultValueFormatter;
246
248
  Table.TableSettingsDropdown = TableSettingsDropdown;
247
249
  Table.TableControls = TableControls;
250
+ Table.NumberInputCellRenderer = NumberInputCellRenderer;
248
251
 
249
252
  Table.Spacing = TABLE_SPACING;
250
253
  Table.SettingsItems = TABLE_SETTINGS_ITEMS;