@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.
- package/CHANGELOG.md +16 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts +1 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +2 -2
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
- package/dist/components/grid/Grid.d.ts +14 -0
- package/dist/components/grid/Grid.d.ts.map +1 -0
- package/dist/components/grid/Grid.js +11 -0
- package/dist/components/grid/Grid.js.map +1 -0
- package/dist/components/grid/Grid.stories.d.ts +52 -0
- package/dist/components/grid/Grid.stories.d.ts.map +1 -0
- package/dist/components/grid/Grid.stories.js +675 -0
- package/dist/components/grid/Grid.stories.js.map +1 -0
- package/dist/components/grid/Grid.test.d.ts +2 -0
- package/dist/components/grid/Grid.test.d.ts.map +1 -0
- package/dist/components/grid/Grid.test.js +59 -0
- package/dist/components/grid/Grid.test.js.map +1 -0
- package/dist/components/grid/GridColumn.d.ts +11 -0
- package/dist/components/grid/GridColumn.d.ts.map +1 -0
- package/dist/components/grid/GridColumn.js +7 -0
- package/dist/components/grid/GridColumn.js.map +1 -0
- package/dist/components/grid/GridRow.d.ts +10 -0
- package/dist/components/grid/GridRow.d.ts.map +1 -0
- package/dist/components/grid/GridRow.js +7 -0
- package/dist/components/grid/GridRow.js.map +1 -0
- package/dist/components/row/Row.d.ts +2 -0
- package/dist/components/row/Row.d.ts.map +1 -1
- package/dist/components/row/Row.js +14 -5
- package/dist/components/row/Row.js.map +1 -1
- package/dist/components/row/Row.stories.d.ts +27 -0
- package/dist/components/row/Row.stories.d.ts.map +1 -1
- package/dist/components/row/Row.stories.js +101 -0
- package/dist/components/row/Row.stories.js.map +1 -1
- package/dist/components/row/Row.test.d.ts.map +1 -1
- package/dist/components/row/Row.test.js +39 -1
- package/dist/components/row/Row.test.js.map +1 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.d.ts.map +1 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js +2 -2
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js.map +1 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts +13 -0
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts.map +1 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js +56 -0
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js.map +1 -1
- package/dist/index.css +194 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +3 -1
- package/src/components/grid/Grid.stories.tsx +1130 -0
- package/src/components/grid/Grid.test.tsx +67 -0
- package/src/components/grid/Grid.tsx +34 -0
- package/src/components/grid/GridColumn.tsx +31 -0
- package/src/components/grid/GridRow.tsx +29 -0
- package/src/components/row/Row.stories.tsx +141 -0
- package/src/components/row/Row.test.tsx +54 -1
- package/src/components/row/Row.tsx +29 -10
- package/src/components/row/row.scss +10 -0
- package/src/components/table/cellRenderers/SelectDropdownCellRenderer.stories.tsx +67 -0
- package/src/components/table/cellRenderers/SelectDropdownCellRenderer.tsx +2 -0
- package/src/docs/Contributing.mdx +16 -0
- package/src/index.scss +1 -0
- 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
|
-
|
|
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
|
-
|
|
50
|
-
{
|
|
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 {
|
|
@@ -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
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';
|