@arbor-education/design-system.components 0.25.5 → 0.25.7

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 (65) 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/grid/Grid.d.ts +14 -0
  7. package/dist/components/grid/Grid.d.ts.map +1 -0
  8. package/dist/components/grid/Grid.js +11 -0
  9. package/dist/components/grid/Grid.js.map +1 -0
  10. package/dist/components/grid/Grid.stories.d.ts +52 -0
  11. package/dist/components/grid/Grid.stories.d.ts.map +1 -0
  12. package/dist/components/grid/Grid.stories.js +675 -0
  13. package/dist/components/grid/Grid.stories.js.map +1 -0
  14. package/dist/components/grid/Grid.test.d.ts +2 -0
  15. package/dist/components/grid/Grid.test.d.ts.map +1 -0
  16. package/dist/components/grid/Grid.test.js +59 -0
  17. package/dist/components/grid/Grid.test.js.map +1 -0
  18. package/dist/components/grid/GridColumn.d.ts +11 -0
  19. package/dist/components/grid/GridColumn.d.ts.map +1 -0
  20. package/dist/components/grid/GridColumn.js +7 -0
  21. package/dist/components/grid/GridColumn.js.map +1 -0
  22. package/dist/components/grid/GridRow.d.ts +10 -0
  23. package/dist/components/grid/GridRow.d.ts.map +1 -0
  24. package/dist/components/grid/GridRow.js +7 -0
  25. package/dist/components/grid/GridRow.js.map +1 -0
  26. package/dist/components/row/Row.d.ts +2 -0
  27. package/dist/components/row/Row.d.ts.map +1 -1
  28. package/dist/components/row/Row.js +14 -5
  29. package/dist/components/row/Row.js.map +1 -1
  30. package/dist/components/row/Row.stories.d.ts +27 -0
  31. package/dist/components/row/Row.stories.d.ts.map +1 -1
  32. package/dist/components/row/Row.stories.js +101 -0
  33. package/dist/components/row/Row.stories.js.map +1 -1
  34. package/dist/components/row/Row.test.d.ts.map +1 -1
  35. package/dist/components/row/Row.test.js +39 -1
  36. package/dist/components/row/Row.test.js.map +1 -1
  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 +194 -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 +2 -1
  51. package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +3 -1
  52. package/src/components/grid/Grid.stories.tsx +1130 -0
  53. package/src/components/grid/Grid.test.tsx +67 -0
  54. package/src/components/grid/Grid.tsx +34 -0
  55. package/src/components/grid/GridColumn.tsx +31 -0
  56. package/src/components/grid/GridRow.tsx +29 -0
  57. package/src/components/row/Row.stories.tsx +141 -0
  58. package/src/components/row/Row.test.tsx +54 -1
  59. package/src/components/row/Row.tsx +29 -10
  60. package/src/components/row/row.scss +10 -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/docs/Contributing.mdx +16 -0
  64. package/src/index.scss +1 -0
  65. package/src/index.ts +1 -0
@@ -0,0 +1,67 @@
1
+ import { expect, test, describe } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/vitest';
4
+ import { Grid } from './Grid.js';
5
+
6
+ describe('Grid', () => {
7
+ test('renders govuk-main-wrapper class', () => {
8
+ const { container } = render(<Grid />);
9
+ expect(container.firstChild).toHaveClass('govuk-main-wrapper');
10
+ });
11
+
12
+ test('renders children', () => {
13
+ render(<Grid><span>School report</span></Grid>);
14
+ expect(screen.getByText('School report')).toBeInTheDocument();
15
+ });
16
+
17
+ test('merges custom className with govuk-main-wrapper', () => {
18
+ const { container } = render(<Grid className="my-layout" />);
19
+ expect(container.firstChild).toHaveClass('govuk-main-wrapper', 'my-layout');
20
+ });
21
+ });
22
+
23
+ describe('Grid.Row', () => {
24
+ test('renders govuk-grid-row class', () => {
25
+ const { container } = render(<Grid.Row />);
26
+ expect(container.firstChild).toHaveClass('govuk-grid-row');
27
+ });
28
+
29
+ test('renders children', () => {
30
+ render(<Grid.Row><span>Attendance data</span></Grid.Row>);
31
+ expect(screen.getByText('Attendance data')).toBeInTheDocument();
32
+ });
33
+
34
+ test('merges custom className with govuk-grid-row', () => {
35
+ const { container } = render(<Grid.Row className="my-row" />);
36
+ expect(container.firstChild).toHaveClass('govuk-grid-row', 'my-row');
37
+ });
38
+ });
39
+
40
+ describe('Grid.Column', () => {
41
+ test('defaults to govuk-grid-column-full when columnWidth is omitted', () => {
42
+ const { container } = render(<Grid.Column />);
43
+ expect(container.firstChild).toHaveClass('govuk-grid-column-full');
44
+ });
45
+
46
+ test('renders children', () => {
47
+ render(<Grid.Column columnWidth="full"><span>Student profile</span></Grid.Column>);
48
+ expect(screen.getByText('Student profile')).toBeInTheDocument();
49
+ });
50
+
51
+ test('merges custom className', () => {
52
+ const { container } = render(<Grid.Column columnWidth="full" className="my-col" />);
53
+ expect(container.firstChild).toHaveClass('govuk-grid-column-full', 'my-col');
54
+ });
55
+
56
+ test.each([
57
+ ['full', 'govuk-grid-column-full'],
58
+ ['one-half', 'govuk-grid-column-one-half'],
59
+ ['one-third', 'govuk-grid-column-one-third'],
60
+ ['two-thirds', 'govuk-grid-column-two-thirds'],
61
+ ['one-quarter', 'govuk-grid-column-one-quarter'],
62
+ ['three-quarters', 'govuk-grid-column-three-quarters'],
63
+ ] as const)('applies %s class for columnWidth="%s"', (width, expectedClass) => {
64
+ const { container } = render(<Grid.Column columnWidth={width} />);
65
+ expect(container.firstChild).toHaveClass(expectedClass);
66
+ });
67
+ });
@@ -0,0 +1,34 @@
1
+ import classNames from 'classnames';
2
+ import type { ReactNode } from 'react';
3
+ import { GridRow } from './GridRow.js';
4
+ import { GridColumn } from './GridColumn.js';
5
+
6
+ export type GridProps = {
7
+ className?: string;
8
+ children?: ReactNode;
9
+ };
10
+
11
+ export const Grid = (props: GridProps) => {
12
+ const {
13
+ className,
14
+ children,
15
+ } = props;
16
+
17
+ return (
18
+ <div
19
+ className={classNames(
20
+ 'govuk-main-wrapper',
21
+ className,
22
+ )}
23
+ >
24
+ {children}
25
+ </div>
26
+ );
27
+ };
28
+
29
+ Grid.Row = GridRow;
30
+ Grid.Column = GridColumn;
31
+
32
+ export namespace Grid {
33
+ export type Props = GridProps;
34
+ }
@@ -0,0 +1,31 @@
1
+ import classNames from 'classnames';
2
+ import type { ReactNode } from 'react';
3
+
4
+ export type GridColumnProps = {
5
+ className?: string;
6
+ children?: ReactNode;
7
+ columnWidth?: 'one-quarter' | 'three-quarters' | 'one-third' | 'two-thirds' | 'one-half' | 'full';
8
+ };
9
+
10
+ export const GridColumn = (props: GridColumnProps) => {
11
+ const {
12
+ className,
13
+ children,
14
+ columnWidth = 'full',
15
+ } = props;
16
+
17
+ return (
18
+ <div
19
+ className={classNames(
20
+ `govuk-grid-column-${columnWidth}`,
21
+ className,
22
+ )}
23
+ >
24
+ {children}
25
+ </div>
26
+ );
27
+ };
28
+
29
+ export namespace GridColumn {
30
+ export type Props = GridColumnProps;
31
+ }
@@ -0,0 +1,29 @@
1
+ import classNames from 'classnames';
2
+ import type { ReactNode } from 'react';
3
+
4
+ export type GridRowProps = {
5
+ className?: string;
6
+ children?: ReactNode;
7
+ };
8
+
9
+ export const GridRow = (props: GridRowProps) => {
10
+ const {
11
+ className,
12
+ children,
13
+ } = props;
14
+
15
+ return (
16
+ <div
17
+ className={classNames(
18
+ 'govuk-grid-row',
19
+ className,
20
+ )}
21
+ >
22
+ {children}
23
+ </div>
24
+ );
25
+ };
26
+
27
+ export namespace GridRow {
28
+ export type Props = GridRowProps;
29
+ }
@@ -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%;
@@ -211,6 +211,14 @@ const meta = {
211
211
  defaultValue: { summary: 'false' },
212
212
  },
213
213
  },
214
+ onCloseAutoFocus: {
215
+ control: false,
216
+ description: 'Called when focus is about to return to the ag-grid cell after the dropdown closes (e.g. after selecting an option). Call `event.preventDefault()` to stop radix\'s default restore-to-cell behaviour and move focus elsewhere yourself.',
217
+ table: {
218
+ type: { summary: '(event: Event) => void' },
219
+ defaultValue: { summary: 'undefined' },
220
+ },
221
+ },
214
222
  },
215
223
  } satisfies Meta<typeof SelectDropdownCellRenderer>;
216
224
 
@@ -718,3 +726,62 @@ const columnDefs = [
718
726
  },
719
727
  '(fillCell + colored) Marksheet with grade-driven cell colours. Each cell uses `backgroundColor` (derived per-row from the current grade) and `fillCell: true` so the colour spans the cell edge-to-edge, plus `cellStyle: { padding: 0 }` to strip ag-grid\'s default cell padding. `iconClickOnly: true` is set so cell selection works — click anywhere in a cell to select it (or shift/drag for a range), and only the chevron icon opens the grade picker. Text colour auto-switches between dark and white for legibility against each background.',
720
728
  );
729
+
730
+ export const CustomCloseFocus: Story = withDescription(
731
+ {
732
+ parameters: {
733
+ controls: { disable: true },
734
+ docs: {
735
+ source: {
736
+ language: 'tsx',
737
+ code: `
738
+ import { Table } from '@arbor-education/design-system.components';
739
+
740
+ const columnDefs = [
741
+ { field: 'name', headerName: 'Name', flex: 2 },
742
+ {
743
+ field: 'status',
744
+ headerName: 'Status',
745
+ cellRenderer: 'dsSelectDropdownCellRenderer',
746
+ cellRendererParams: {
747
+ options: statusOptions,
748
+ onCloseAutoFocus: (event) => {
749
+ event.preventDefault();
750
+ window.alert('Dropdown closed — running custom focus handler');
751
+ },
752
+ },
753
+ },
754
+ ];
755
+
756
+ <Table rowData={rowData} columnDefs={columnDefs} domLayout="autoHeight" />
757
+ `.trim(),
758
+ },
759
+ },
760
+ },
761
+ render: () => (
762
+ <Table
763
+ rowData={SELECT_IN_TABLE_DATA}
764
+ columnDefs={[
765
+ { field: 'name', headerName: 'Name', flex: 2 },
766
+ {
767
+ field: 'status',
768
+ headerName: 'Status',
769
+ flex: 1,
770
+ editable: false,
771
+ cellRenderer: 'dsSelectDropdownCellRenderer',
772
+ cellRendererParams: {
773
+ options: STATUS_OPTIONS,
774
+ onCloseAutoFocus: (event: Event) => {
775
+ event.preventDefault();
776
+ window.alert('Dropdown closed — running custom focus handler');
777
+ },
778
+ },
779
+ },
780
+ ]}
781
+ defaultColDef={{ flex: 1, minWidth: 120 }}
782
+ domLayout="autoHeight"
783
+ />
784
+ ),
785
+ },
786
+ 'Passes `onCloseAutoFocus` via `cellRendererParams` to override what happens to focus when the dropdown closes. Open a Status dropdown and pick an option (or press Escape) — instead of radix restoring focus to the cell, the handler calls `event.preventDefault()` and fires a `window.alert`. In a real app you would move focus to a specific element here rather than alert.',
787
+ );
@@ -52,6 +52,7 @@ export const SelectDropdownCellRenderer = (
52
52
  backgroundColor,
53
53
  fillCell = false,
54
54
  iconClickOnly = false,
55
+ onCloseAutoFocus,
55
56
  } = props;
56
57
 
57
58
  const textContrast = backgroundColor ? getTextContrast(backgroundColor) : null;
@@ -113,6 +114,7 @@ export const SelectDropdownCellRenderer = (
113
114
  onOpenChange={setIsOpen}
114
115
  multiple={false}
115
116
  iconClickOnly={iconClickOnly}
117
+ onCloseAutoFocus={onCloseAutoFocus}
116
118
  onSelectionChange={(newValue) => {
117
119
  if (column && newValue[0] != null) {
118
120
  const selectedOption = rawOptions.find(
@@ -52,6 +52,8 @@ It also automatically adds the component's exports to `src/index.ts` and `src/in
52
52
 
53
53
  ## Key conventions
54
54
 
55
+ Most of these are enforced automatically by ESLint — run `yarn eslint --fix` and let it guide you. A few the linter can't catch are worth knowing.
56
+
55
57
  ### File and directory naming
56
58
 
57
59
  <table>
@@ -97,6 +99,20 @@ import { Button } from '../../components/button/Button'; // ✗
97
99
  </tbody>
98
100
  </table>
99
101
 
102
+ ### Prefer team hooks
103
+
104
+ Before writing your own hook, check `Utils/hooks` — reuse one that already exists, and reach for one that encapsulates a common pattern rather than hand-rolling it. For example, use `useComponentDidMount` for run-once-on-mount effects rather than `useEffect(fn, [])`. Always give `useEffect` a dependency array, even an empty one.
105
+
106
+ ### One component per file
107
+
108
+ Keep one component per file. This applies to compound components too: define each part in its own file and assemble the public API in the root component's file by importing the parts. `Table` is a good example — it pulls in `TableHeader`, `TableFooter`, and its cell renderers from their own files. See the **Compound components** design principle below for the API pattern itself.
109
+
110
+ ### Keep comments rare and meaningful
111
+
112
+ Good code is self-documenting, so comments should be the exception rather than the rule. Before writing one, make the code explain itself instead: choose descriptive names, and lift complex expressions or conditions into well-named constants. A clear name is worth more than a sentence describing it.
113
+
114
+ Reserve comments for the _why_ behind something unusual — a constraint, a workaround, a non-obvious decision — never the _what_ the code already says plainly. If you feel the need to explain a piece of code, that's often a sign the code itself should be clearer. This matters especially for agent-assisted changes, which tend to over-narrate — strip the running commentary before opening a PR.
115
+
100
116
  ---
101
117
 
102
118
  ## Design principles
package/src/index.scss CHANGED
@@ -1,4 +1,5 @@
1
1
  @use "../node_modules/react-day-picker/src/style.css";
2
+ @use "../node_modules/govuk-frontend/dist/govuk/objects/grid.import.scss";
2
3
  @use "./tokens.scss";
3
4
  @use "./global.scss";
4
5
  @use "components/breadcrumbs/breadcrumbs.scss";
package/src/index.ts CHANGED
@@ -89,3 +89,4 @@ export { SlideoverUtils } from 'Utils/SlideoverUtils';
89
89
  export { ArborLogo, type ArborLogoProps } from 'Components/arborLogo/ArborLogo';
90
90
  export { DataViewCard, type DataViewCardProps } from 'Components/dataViewCard/DataViewCard';
91
91
  export { FileUpload, type FileUploadProps } from 'Components/fileUpload/FileUpload';
92
+ export { Grid, type GridProps } from 'Components/grid/Grid';