@arbor-education/design-system.components 3.2.1 → 3.4.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.
- package/CHANGELOG.md +20 -0
- package/dist/components/dataViewCard/DataViewCard.d.ts +5 -0
- package/dist/components/dataViewCard/DataViewCard.d.ts.map +1 -1
- package/dist/components/dataViewCard/DataViewCard.js +7 -3
- package/dist/components/dataViewCard/DataViewCard.js.map +1 -1
- package/dist/components/dataViewCard/DataViewCard.stories.d.ts +10 -0
- package/dist/components/dataViewCard/DataViewCard.stories.d.ts.map +1 -1
- package/dist/components/dataViewCard/DataViewCard.stories.js +31 -0
- package/dist/components/dataViewCard/DataViewCard.stories.js.map +1 -1
- package/dist/components/dataViewCard/DataViewCard.test.js +20 -0
- package/dist/components/dataViewCard/DataViewCard.test.js.map +1 -1
- package/dist/components/filterBar/FilterBar.d.ts +2 -2
- package/dist/components/filterBar/FilterBar.d.ts.map +1 -1
- package/dist/components/filterBar/FilterBar.js +7 -3
- package/dist/components/filterBar/FilterBar.js.map +1 -1
- package/dist/components/filterBar/FilterBar.stories.d.ts +16 -0
- package/dist/components/filterBar/FilterBar.stories.d.ts.map +1 -1
- package/dist/components/filterBar/FilterBar.stories.js +101 -7
- package/dist/components/filterBar/FilterBar.stories.js.map +1 -1
- package/dist/components/filterBar/FilterBar.test.js +30 -0
- package/dist/components/filterBar/FilterBar.test.js.map +1 -1
- package/dist/components/table/Table.d.ts.map +1 -1
- package/dist/components/table/Table.js +2 -1
- package/dist/components/table/Table.js.map +1 -1
- package/dist/components/table/Table.stories.d.ts +7 -0
- package/dist/components/table/Table.stories.d.ts.map +1 -1
- package/dist/components/table/Table.stories.js +44 -0
- package/dist/components/table/Table.stories.js.map +1 -1
- package/dist/components/table/Table.test.js +23 -0
- package/dist/components/table/Table.test.js.map +1 -1
- package/dist/components/table/tableControls/TableControls.d.ts +2 -0
- package/dist/components/table/tableControls/TableControls.d.ts.map +1 -1
- package/dist/components/table/tableControls/TableControls.js +2 -2
- package/dist/components/table/tableControls/TableControls.js.map +1 -1
- package/dist/components/table/tableControls/TableControls.stories.d.ts +25 -0
- package/dist/components/table/tableControls/TableControls.stories.d.ts.map +1 -1
- package/dist/components/table/tableControls/TableControls.stories.js +24 -0
- package/dist/components/table/tableControls/TableControls.stories.js.map +1 -1
- package/dist/components/table/tableControls/TableControls.test.js +16 -0
- package/dist/components/table/tableControls/TableControls.test.js.map +1 -1
- package/dist/components/table/useTableSettings.d.ts +2 -0
- package/dist/components/table/useTableSettings.d.ts.map +1 -1
- package/dist/components/table/useTableSettings.js +12 -7
- package/dist/components/table/useTableSettings.js.map +1 -1
- package/dist/index.css +7 -0
- package/dist/index.css.map +1 -1
- package/package.json +1 -1
- package/src/components/dataViewCard/DataViewCard.stories.tsx +45 -0
- package/src/components/dataViewCard/DataViewCard.test.tsx +44 -0
- package/src/components/dataViewCard/DataViewCard.tsx +28 -1
- package/src/components/dataViewCard/dataViewCard.scss +7 -0
- package/src/components/filterBar/FilterBar.stories.tsx +126 -4
- package/src/components/filterBar/FilterBar.test.tsx +51 -0
- package/src/components/filterBar/FilterBar.tsx +18 -12
- package/src/components/table/Table.stories.tsx +57 -0
- package/src/components/table/Table.test.tsx +42 -0
- package/src/components/table/Table.tsx +2 -0
- package/src/components/table/tableControls/TableControls.stories.tsx +28 -0
- package/src/components/table/tableControls/TableControls.test.tsx +20 -0
- package/src/components/table/tableControls/TableControls.tsx +6 -0
- package/src/components/table/useTableSettings.ts +20 -6
|
@@ -5,6 +5,7 @@ import { Card, getCardInteractionProps } from 'Components/card/Card';
|
|
|
5
5
|
import { Heading } from 'Components/heading/Heading';
|
|
6
6
|
import { Row } from 'Components/row/Row';
|
|
7
7
|
import { Separator } from 'Components/separator/Separator';
|
|
8
|
+
import { Tag } from 'Components/tag/Tag';
|
|
8
9
|
|
|
9
10
|
export type DataViewCardRow = Row.Props;
|
|
10
11
|
|
|
@@ -14,12 +15,20 @@ type DataViewCardBaseProps = {
|
|
|
14
15
|
avatar?: Avatar.Props;
|
|
15
16
|
rows: DataViewCardRow[];
|
|
16
17
|
disabled?: boolean;
|
|
18
|
+
tags?: { color: Tag.Color; label: string }[];
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
export type DataViewCardProps = DataViewCardBaseProps & Card.InteractionProps;
|
|
20
22
|
|
|
21
23
|
export const DataViewCard = (props: DataViewCardProps): React.JSX.Element => {
|
|
22
|
-
const {
|
|
24
|
+
const {
|
|
25
|
+
className,
|
|
26
|
+
header,
|
|
27
|
+
avatar,
|
|
28
|
+
rows,
|
|
29
|
+
disabled = false,
|
|
30
|
+
tags,
|
|
31
|
+
} = props;
|
|
23
32
|
|
|
24
33
|
return (
|
|
25
34
|
<Card
|
|
@@ -50,6 +59,24 @@ export const DataViewCard = (props: DataViewCardProps): React.JSX.Element => {
|
|
|
50
59
|
{index < rows.length - 1 && <Separator />}
|
|
51
60
|
</Fragment>
|
|
52
61
|
))}
|
|
62
|
+
{tags && tags.length > 0 && (
|
|
63
|
+
<>
|
|
64
|
+
<Separator />
|
|
65
|
+
<span className="ds-data-view-card__tag-list">
|
|
66
|
+
{tags.map((tag) => {
|
|
67
|
+
const { label, color } = tag;
|
|
68
|
+
return (
|
|
69
|
+
<Tag
|
|
70
|
+
key={`dvc-${label}-${color}`}
|
|
71
|
+
color={color}
|
|
72
|
+
>
|
|
73
|
+
{label}
|
|
74
|
+
</Tag>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</span>
|
|
78
|
+
</>
|
|
79
|
+
)}
|
|
53
80
|
</div>
|
|
54
81
|
</article>
|
|
55
82
|
</Card>
|
|
@@ -75,6 +75,12 @@ const DEVELOPER_NOTES = [
|
|
|
75
75
|
'compose custom `FilterBar.Tag` children when you need bespoke markup. Avoid mixing both patterns in',
|
|
76
76
|
'the same production instance.',
|
|
77
77
|
'',
|
|
78
|
+
'**`label` is optional.** Omit it when the value alone is self-explanatory, such as a free-text',
|
|
79
|
+
'search term or a single unambiguous criterion. Without a `label`, the tag renders just the value',
|
|
80
|
+
'(no `Label:` prefix) and the generated `actionLabel` drops the label segment too, e.g. `Open filter',
|
|
81
|
+
'Interventions` instead of `Open filter Event Type: Interventions`. Most applied tags in a filter,',
|
|
82
|
+
'sort, or group list benefit from a label, so reach for this only when it would be redundant.',
|
|
83
|
+
'',
|
|
78
84
|
'---',
|
|
79
85
|
'',
|
|
80
86
|
'### Accessibility',
|
|
@@ -106,7 +112,7 @@ const DEVELOPER_NOTES = [
|
|
|
106
112
|
'| `FilterBar.ActiveListProps` | Applied-item summary props |',
|
|
107
113
|
'| `FilterBar.TagProps` | Manually composed tag props |',
|
|
108
114
|
'| `FilterBar.TagItem` | Data shape for `ActiveList.items` |',
|
|
109
|
-
'| `FilterBarType` | Union of `filter`, `sort`, and `
|
|
115
|
+
'| `FilterBarType` | Union of `filter`, `sort`, `group`, and `add` |',
|
|
110
116
|
].join('\n');
|
|
111
117
|
|
|
112
118
|
const RELATED_COMPONENTS = [
|
|
@@ -134,7 +140,7 @@ const TOOLBAR_PROPS = [
|
|
|
134
140
|
const BUTTON_PROPS = [
|
|
135
141
|
'| Prop | Type | Default | Description |',
|
|
136
142
|
'|---|---|---|---|',
|
|
137
|
-
'| `type` | `` `filter` \\| `sort` \\| `group` `` | — | **Required.** Chooses the default label, icon, and count announcement text |',
|
|
143
|
+
'| `type` | `` `filter` \\| `sort` \\| `group` \\| `add` `` | — | **Required.** Chooses the default label, icon, and count announcement text |',
|
|
138
144
|
'| `label` | `string` | inferred from `type` | Override the visible button label |',
|
|
139
145
|
'| `count` | `number \\| null` | — | Shows a badge when `> 0`. The badge caps visually at `99+`, but the accessible label keeps the full number |',
|
|
140
146
|
'| `icon` | `IconName` | inferred from `type` | Override the default icon for that trigger type |',
|
|
@@ -163,8 +169,8 @@ const ACTIVE_LIST_PROPS = [
|
|
|
163
169
|
const TAG_PROPS = [
|
|
164
170
|
'| Prop | Type | Default | Description |',
|
|
165
171
|
'|---|---|---|---|',
|
|
166
|
-
'| `type` | `` `filter` \\| `sort` \\| `group` `` | — | **Required.** Sets the default icon and action language |',
|
|
167
|
-
'| `label` | `string` | — |
|
|
172
|
+
'| `type` | `` `filter` \\| `sort` \\| `group` \\| `add` `` | — | **Required.** Sets the default icon and action language |',
|
|
173
|
+
'| `label` | `string` | — | Criterion label, e.g. `Year group`. Optional — omit it when the value alone is self-explanatory; the tag then renders just the value with no `Label:` prefix, and the generated `actionLabel` drops the label segment too |',
|
|
168
174
|
'| `value` | `ReactNode` | — | **Required.** Current applied value, e.g. `Year 10` |',
|
|
169
175
|
'| `id` | `string` | derived from `fieldId` or label | Stable identifier for list rendering and event payloads |',
|
|
170
176
|
'| `fieldId` | `string` | — | Optional DOM id for the field that should receive focus after opening an editor |',
|
|
@@ -188,6 +194,7 @@ type FilterBarStoryArgs = FilterBar.Props & {
|
|
|
188
194
|
filterCount: number;
|
|
189
195
|
sortCount: number;
|
|
190
196
|
groupCount: number;
|
|
197
|
+
addCount: number;
|
|
191
198
|
wrap: boolean;
|
|
192
199
|
collapseOverflow: boolean;
|
|
193
200
|
overflowActionLabel: string;
|
|
@@ -229,6 +236,7 @@ const StoryShell = ({
|
|
|
229
236
|
const onOpenFilter = fn();
|
|
230
237
|
const onOpenSort = fn();
|
|
231
238
|
const onOpenGroup = fn();
|
|
239
|
+
const onOpenAdd = fn();
|
|
232
240
|
const onOpenAppliedItem = fn();
|
|
233
241
|
const onRemoveAppliedItem = fn();
|
|
234
242
|
const onShowOverflow = fn();
|
|
@@ -337,12 +345,14 @@ const PANEL_FIELD_IDS: Record<FilterBar.Type, string> = {
|
|
|
337
345
|
filter: 'attendance-filter-year-group',
|
|
338
346
|
sort: 'attendance-sort-order',
|
|
339
347
|
group: 'attendance-group-by',
|
|
348
|
+
add: 'attendance-add-criteria',
|
|
340
349
|
};
|
|
341
350
|
|
|
342
351
|
const PANEL_TITLES: Record<FilterBar.Type, string> = {
|
|
343
352
|
filter: 'Filters',
|
|
344
353
|
sort: 'Sorting',
|
|
345
354
|
group: 'Grouping',
|
|
355
|
+
add: 'Add criteria',
|
|
346
356
|
};
|
|
347
357
|
|
|
348
358
|
// ---------------------------------------------------------------------------
|
|
@@ -455,6 +465,14 @@ const meta = {
|
|
|
455
465
|
type: { summary: 'number' },
|
|
456
466
|
},
|
|
457
467
|
},
|
|
468
|
+
'addCount': {
|
|
469
|
+
control: { type: 'number', min: 0, max: 250 },
|
|
470
|
+
description: 'Applied add count shown on the Add trigger badge.',
|
|
471
|
+
table: {
|
|
472
|
+
category: 'FilterBar.Button',
|
|
473
|
+
type: { summary: 'number' },
|
|
474
|
+
},
|
|
475
|
+
},
|
|
458
476
|
'wrap': {
|
|
459
477
|
control: 'boolean',
|
|
460
478
|
description: 'Allows the applied-item summary to wrap onto multiple lines.',
|
|
@@ -514,6 +532,7 @@ const DefaultTemplate = ({
|
|
|
514
532
|
filterCount,
|
|
515
533
|
sortCount,
|
|
516
534
|
groupCount,
|
|
535
|
+
addCount,
|
|
517
536
|
wrap,
|
|
518
537
|
collapseOverflow,
|
|
519
538
|
overflowActionLabel,
|
|
@@ -525,6 +544,7 @@ const DefaultTemplate = ({
|
|
|
525
544
|
<FilterBar.Button type="filter" count={filterCount} onClick={onOpenFilter} />
|
|
526
545
|
<FilterBar.Button type="sort" count={sortCount} onClick={onOpenSort} />
|
|
527
546
|
<FilterBar.Button type="group" count={groupCount} onClick={onOpenGroup} />
|
|
547
|
+
<FilterBar.Button type="add" count={addCount} onClick={onOpenAdd} />
|
|
528
548
|
</FilterBar.Toolbar>
|
|
529
549
|
<FilterBar.ActiveList
|
|
530
550
|
items={DEFAULT_ITEMS}
|
|
@@ -580,6 +600,7 @@ const ModalWiringTemplate = () => {
|
|
|
580
600
|
filter: 0,
|
|
581
601
|
sort: 0,
|
|
582
602
|
group: 0,
|
|
603
|
+
add: 0,
|
|
583
604
|
});
|
|
584
605
|
|
|
585
606
|
const items: FilterBar.TagItem[] = APPLIED_ITEM_SEEDS.flatMap((item) => {
|
|
@@ -639,6 +660,14 @@ const ModalWiringTemplate = () => {
|
|
|
639
660
|
aria-haspopup="dialog"
|
|
640
661
|
onClick={() => openModal('group')}
|
|
641
662
|
/>
|
|
663
|
+
<FilterBar.Button
|
|
664
|
+
type="add"
|
|
665
|
+
count={counts.add}
|
|
666
|
+
aria-controls={modalId}
|
|
667
|
+
aria-expanded={open && activeType === 'add'}
|
|
668
|
+
aria-haspopup="dialog"
|
|
669
|
+
onClick={() => openModal('add')}
|
|
670
|
+
/>
|
|
642
671
|
</FilterBar.Toolbar>
|
|
643
672
|
<FilterBar.ActiveList
|
|
644
673
|
ariaLabel="Applied attendance filters, sorting, and grouping"
|
|
@@ -744,6 +773,7 @@ export const Default: Story = withDescription(
|
|
|
744
773
|
'filterCount': 3,
|
|
745
774
|
'sortCount': 1,
|
|
746
775
|
'groupCount': 1,
|
|
776
|
+
'addCount': 0,
|
|
747
777
|
'wrap': false,
|
|
748
778
|
'collapseOverflow': true,
|
|
749
779
|
'overflowActionLabel': 'Show more applied items',
|
|
@@ -783,6 +813,7 @@ function HighCountsExample() {
|
|
|
783
813
|
<FilterBar.Button type="filter" count={142} onClick={() => {}} />
|
|
784
814
|
<FilterBar.Button type="sort" count={103} onClick={() => {}} />
|
|
785
815
|
<FilterBar.Button type="group" count={12} onClick={() => {}} />
|
|
816
|
+
<FilterBar.Button type="add" onClick={() => {}} />
|
|
786
817
|
</FilterBar.Toolbar>
|
|
787
818
|
<FilterBar.ActiveList items={items} ariaLabel="Applied attendance filters, sorting, and grouping" />
|
|
788
819
|
</FilterBar>
|
|
@@ -801,6 +832,7 @@ export default HighCountsExample;
|
|
|
801
832
|
<FilterBar.Button type="filter" count={142} onClick={onOpenFilter} />
|
|
802
833
|
<FilterBar.Button type="sort" count={103} onClick={onOpenSort} />
|
|
803
834
|
<FilterBar.Button type="group" count={12} onClick={onOpenGroup} />
|
|
835
|
+
<FilterBar.Button type="add" onClick={onOpenAdd} />
|
|
804
836
|
</FilterBar.Toolbar>
|
|
805
837
|
<FilterBar.ActiveList
|
|
806
838
|
items={DEFAULT_ITEMS}
|
|
@@ -841,12 +873,14 @@ const panelFieldIds: Record<FilterBarType, string> = {
|
|
|
841
873
|
filter: 'attendance-filter-year-group',
|
|
842
874
|
sort: 'attendance-sort-order',
|
|
843
875
|
group: 'attendance-group-by',
|
|
876
|
+
add: 'attendance-add-criteria',
|
|
844
877
|
};
|
|
845
878
|
|
|
846
879
|
const PANEL_TITLES: Record<FilterBarType, string> = {
|
|
847
880
|
filter: 'Filters',
|
|
848
881
|
sort: 'Sorting',
|
|
849
882
|
group: 'Grouping',
|
|
883
|
+
add: 'Add criteria',
|
|
850
884
|
};
|
|
851
885
|
|
|
852
886
|
const seedItems: FilterBarTagItem[] = [
|
|
@@ -931,6 +965,7 @@ function FilterBarModalWiringExample() {
|
|
|
931
965
|
filter: 0,
|
|
932
966
|
sort: 0,
|
|
933
967
|
group: 0,
|
|
968
|
+
add: 0,
|
|
934
969
|
});
|
|
935
970
|
|
|
936
971
|
const items = seedItems.flatMap((item) => {
|
|
@@ -985,6 +1020,14 @@ function FilterBarModalWiringExample() {
|
|
|
985
1020
|
aria-haspopup="dialog"
|
|
986
1021
|
onClick={() => openModal('group')}
|
|
987
1022
|
/>
|
|
1023
|
+
<FilterBar.Button
|
|
1024
|
+
type="add"
|
|
1025
|
+
count={counts.add}
|
|
1026
|
+
aria-controls={modalId}
|
|
1027
|
+
aria-expanded={open && activeType === 'add'}
|
|
1028
|
+
aria-haspopup="dialog"
|
|
1029
|
+
onClick={() => openModal('add')}
|
|
1030
|
+
/>
|
|
988
1031
|
</FilterBar.Toolbar>
|
|
989
1032
|
|
|
990
1033
|
<FilterBar.ActiveList
|
|
@@ -1156,6 +1199,85 @@ export default OverflowExample;
|
|
|
1156
1199
|
].join(' '),
|
|
1157
1200
|
);
|
|
1158
1201
|
|
|
1202
|
+
export const WithoutLabels: Story = withDescription(
|
|
1203
|
+
{
|
|
1204
|
+
parameters: {
|
|
1205
|
+
controls: { disable: true },
|
|
1206
|
+
docs: {
|
|
1207
|
+
source: {
|
|
1208
|
+
language: 'tsx',
|
|
1209
|
+
code: `
|
|
1210
|
+
import { FilterBar, type FilterBarTagItem } from '@arbor-education/design-system.components';
|
|
1211
|
+
|
|
1212
|
+
const items: FilterBarTagItem[] = [
|
|
1213
|
+
{ id: 'search-term', type: 'filter', value: 'Jordan Casey' },
|
|
1214
|
+
{ id: 'year-group', type: 'filter', label: 'Year group', value: 'Year 10' },
|
|
1215
|
+
{ id: 'attendance-band', type: 'filter', label: 'Attendance band', value: 'Below 90%' },
|
|
1216
|
+
];
|
|
1217
|
+
|
|
1218
|
+
function WithoutLabelsExample() {
|
|
1219
|
+
return (
|
|
1220
|
+
<div style={{ width: '100%', maxWidth: 'min(100%, 72rem)', padding: 'var(--spacing-large)', boxSizing: 'border-box' }}>
|
|
1221
|
+
<FilterBar aria-label="Attendance view controls">
|
|
1222
|
+
<FilterBar.Toolbar aria-label="Filter, sort, and group controls">
|
|
1223
|
+
<FilterBar.Button type="filter" count={3} onClick={() => {}} />
|
|
1224
|
+
</FilterBar.Toolbar>
|
|
1225
|
+
<FilterBar.ActiveList items={items} ariaLabel="Applied attendance filters" />
|
|
1226
|
+
</FilterBar>
|
|
1227
|
+
</div>
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
export default WithoutLabelsExample;
|
|
1231
|
+
`.trim(),
|
|
1232
|
+
},
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
render: () => (
|
|
1236
|
+
<StoryShell>
|
|
1237
|
+
<FilterBar aria-label="Attendance view controls">
|
|
1238
|
+
<FilterBar.Toolbar aria-label="Filter, sort, and group controls">
|
|
1239
|
+
<FilterBar.Button type="filter" count={3} onClick={onOpenFilter} />
|
|
1240
|
+
</FilterBar.Toolbar>
|
|
1241
|
+
<FilterBar.ActiveList
|
|
1242
|
+
items={[
|
|
1243
|
+
{
|
|
1244
|
+
id: 'search-term',
|
|
1245
|
+
type: 'filter',
|
|
1246
|
+
value: 'Jordan Casey',
|
|
1247
|
+
onClick: onOpenAppliedItem,
|
|
1248
|
+
onRemove: onRemoveAppliedItem,
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
id: 'year-group',
|
|
1252
|
+
type: 'filter',
|
|
1253
|
+
label: 'Year group',
|
|
1254
|
+
value: 'Year 10',
|
|
1255
|
+
onClick: onOpenAppliedItem,
|
|
1256
|
+
onRemove: onRemoveAppliedItem,
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
id: 'attendance-band',
|
|
1260
|
+
type: 'filter',
|
|
1261
|
+
label: 'Attendance band',
|
|
1262
|
+
value: 'Below 90%',
|
|
1263
|
+
onClick: onOpenAppliedItem,
|
|
1264
|
+
onRemove: onRemoveAppliedItem,
|
|
1265
|
+
},
|
|
1266
|
+
]}
|
|
1267
|
+
ariaLabel="Applied attendance filters"
|
|
1268
|
+
/>
|
|
1269
|
+
</FilterBar>
|
|
1270
|
+
</StoryShell>
|
|
1271
|
+
),
|
|
1272
|
+
},
|
|
1273
|
+
[
|
|
1274
|
+
'Omit `label` when the value alone is self-explanatory, such as the free-text search term shown',
|
|
1275
|
+
'first here. That tag renders as just `Jordan Casey` with a shorter generated action label',
|
|
1276
|
+
'(`Open filter Jordan Casey` instead of `Open filter Search: Jordan Casey`), while the labelled',
|
|
1277
|
+
'tags alongside it keep their `Label: Value` layout for comparison.',
|
|
1278
|
+
].join(' '),
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1159
1281
|
export const Empty: Story = withDescription(
|
|
1160
1282
|
{
|
|
1161
1283
|
parameters: {
|
|
@@ -171,6 +171,57 @@ describe('FilterBar', () => {
|
|
|
171
171
|
}
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
test('renders active list tags without a label and omits the label segment from the content and action label', async () => {
|
|
175
|
+
const user = userEvent.setup();
|
|
176
|
+
const onItemClick = vi.fn();
|
|
177
|
+
|
|
178
|
+
render(
|
|
179
|
+
<FilterBar>
|
|
180
|
+
<FilterBar.ActiveList
|
|
181
|
+
items={[
|
|
182
|
+
{
|
|
183
|
+
id: 'search',
|
|
184
|
+
type: 'filter',
|
|
185
|
+
value: 'Interventions',
|
|
186
|
+
onClick: onItemClick,
|
|
187
|
+
},
|
|
188
|
+
]}
|
|
189
|
+
/>
|
|
190
|
+
</FilterBar>,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const tagButton = screen.getByRole('button', { name: 'Open filter Interventions' });
|
|
194
|
+
expect(tagButton).toBeInTheDocument();
|
|
195
|
+
expect(screen.queryByText(':')).not.toBeInTheDocument();
|
|
196
|
+
|
|
197
|
+
await user.click(tagButton);
|
|
198
|
+
|
|
199
|
+
expect(onItemClick).toHaveBeenCalledOnce();
|
|
200
|
+
const [clickedItem] = onItemClick.mock.calls[0] ?? [];
|
|
201
|
+
expect(clickedItem).toMatchObject({ id: 'search', type: 'filter', value: 'Interventions' });
|
|
202
|
+
expect(clickedItem?.label).toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('renders a manually composed FilterBar.Tag without a label', async () => {
|
|
206
|
+
const user = userEvent.setup();
|
|
207
|
+
const onClick = vi.fn();
|
|
208
|
+
|
|
209
|
+
render(
|
|
210
|
+
<FilterBar>
|
|
211
|
+
<FilterBar.ActiveList>
|
|
212
|
+
<FilterBar.Tag type="sort" value="Priority first" onClick={onClick} />
|
|
213
|
+
</FilterBar.ActiveList>
|
|
214
|
+
</FilterBar>,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const tagButton = screen.getByRole('button', { name: 'Open sort Priority first' });
|
|
218
|
+
expect(screen.getByText('Priority first')).toBeInTheDocument();
|
|
219
|
+
expect(screen.queryByText(':')).not.toBeInTheDocument();
|
|
220
|
+
|
|
221
|
+
await user.click(tagButton);
|
|
222
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
223
|
+
});
|
|
224
|
+
|
|
174
225
|
test('returns focus to the first toolbar button after sequentially removing all tags from their remove buttons', async () => {
|
|
175
226
|
const user = userEvent.setup();
|
|
176
227
|
|
|
@@ -7,12 +7,12 @@ import { TagList } from 'Components/tagList/TagList';
|
|
|
7
7
|
import classNames from 'classnames';
|
|
8
8
|
import { Children, forwardRef, type ButtonHTMLAttributes, type HTMLAttributes, type MouseEventHandler, type RefObject } from 'react';
|
|
9
9
|
|
|
10
|
-
export type FilterBarType = 'filter' | 'sort' | 'group';
|
|
10
|
+
export type FilterBarType = 'filter' | 'sort' | 'group' | 'add';
|
|
11
11
|
|
|
12
12
|
export type FilterBarTagItem = {
|
|
13
13
|
id: string;
|
|
14
14
|
type: FilterBarType;
|
|
15
|
-
label
|
|
15
|
+
label?: string;
|
|
16
16
|
value: React.ReactNode;
|
|
17
17
|
fieldId?: string;
|
|
18
18
|
icon?: IconName;
|
|
@@ -64,12 +64,14 @@ const FILTER_BAR_TYPE_LABELS: Record<FilterBarType, string> = {
|
|
|
64
64
|
filter: 'Filter',
|
|
65
65
|
sort: 'Sort',
|
|
66
66
|
group: 'Group',
|
|
67
|
+
add: 'Add',
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
const FILTER_BAR_TYPE_ICONS: Record<FilterBarType, IconName> = {
|
|
70
71
|
filter: 'sliders-horizontal',
|
|
71
72
|
sort: 'sorting',
|
|
72
73
|
group: 'list-tree',
|
|
74
|
+
add: 'circle-plus',
|
|
73
75
|
};
|
|
74
76
|
|
|
75
77
|
const getVisibleCount = (count?: number | null): string | null => {
|
|
@@ -87,17 +89,21 @@ const getButtonA11yLabel = (type: FilterBarType, label: string, count?: number |
|
|
|
87
89
|
return `Open ${label.toLowerCase()}. ${count} ${type} item${count === 1 ? '' : 's'} applied`;
|
|
88
90
|
};
|
|
89
91
|
|
|
90
|
-
const getTagActionLabel = (item: FilterBarTagItem): string => `Open ${item.type} ${item.label}: ${item.value}`;
|
|
92
|
+
const getTagActionLabel = (item: FilterBarTagItem): string => item.label ? `Open ${item.type} ${item.label}: ${item.value}` : `Open ${item.type} ${item.value}`;
|
|
91
93
|
|
|
92
|
-
const FilterBarTagContent = ({ label, value }: Pick<FilterBarTagProps, 'label' | 'value'>) =>
|
|
93
|
-
|
|
94
|
-
<span className="ds-filter-bar__tag-
|
|
95
|
-
{label
|
|
96
|
-
|
|
94
|
+
const FilterBarTagContent = ({ label, value }: Pick<FilterBarTagProps, 'label' | 'value'>) => {
|
|
95
|
+
return (
|
|
96
|
+
<span className="ds-filter-bar__tag-content">
|
|
97
|
+
{label && (
|
|
98
|
+
<span className="ds-filter-bar__tag-label">
|
|
99
|
+
{label}
|
|
100
|
+
:
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
<span className="ds-filter-bar__tag-value">{value}</span>
|
|
97
104
|
</span>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
);
|
|
105
|
+
);
|
|
106
|
+
};
|
|
101
107
|
|
|
102
108
|
export const FilterBar = ({
|
|
103
109
|
children,
|
|
@@ -193,7 +199,7 @@ const FilterBarTag = ({
|
|
|
193
199
|
onRemove,
|
|
194
200
|
}: FilterBarTagProps): React.JSX.Element => {
|
|
195
201
|
const item: FilterBarTagItem = {
|
|
196
|
-
id: id ?? fieldId ?? `${type}-${label}`,
|
|
202
|
+
id: id ?? fieldId ?? `${type}-${label}-${value}`,
|
|
197
203
|
type,
|
|
198
204
|
label,
|
|
199
205
|
value,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '@storybook/addon-docs/blocks';
|
|
12
12
|
import { type Column, type ColDef, type ColGroupDef } from 'ag-grid-enterprise';
|
|
13
13
|
import { Table } from './Table.js';
|
|
14
|
+
import { TABLE_SPACING } from './tableConsts.js';
|
|
14
15
|
import { Button } from 'Components/button/Button';
|
|
15
16
|
import { Icon } from 'Components/icon/Icon';
|
|
16
17
|
|
|
@@ -348,6 +349,14 @@ const meta: Meta<TableProps> = {
|
|
|
348
349
|
defaultValue: { summary: 'false' },
|
|
349
350
|
},
|
|
350
351
|
},
|
|
352
|
+
initialSettings: {
|
|
353
|
+
description: 'Seeds the table settings (column borders, spacing, cell colours) on mount, e.g. to restore a user\'s previously persisted preferences. Only affects the initial render — `resetSettings` still falls back to the DS defaults, not these values.',
|
|
354
|
+
control: false,
|
|
355
|
+
table: {
|
|
356
|
+
type: { summary: 'Partial<TableSettings>' },
|
|
357
|
+
defaultValue: { summary: 'undefined' },
|
|
358
|
+
},
|
|
359
|
+
},
|
|
351
360
|
onTableSettingsChanged: {
|
|
352
361
|
description: 'Fired when the user changes table settings (column borders, spacing, cell colours) via the settings dropdown.',
|
|
353
362
|
control: false,
|
|
@@ -2212,6 +2221,54 @@ export const WithTableSettingsDropdownCustomItems: Story = {
|
|
|
2212
2221
|
},
|
|
2213
2222
|
};
|
|
2214
2223
|
|
|
2224
|
+
type WithInitialSettingsArgs = TableProps & {
|
|
2225
|
+
hasColumnBorders?: boolean;
|
|
2226
|
+
tableSpacing?: TABLE_SPACING;
|
|
2227
|
+
areCellColorsEnabled?: boolean;
|
|
2228
|
+
};
|
|
2229
|
+
|
|
2230
|
+
export const WithInitialSettings: StoryObj<WithInitialSettingsArgs> = {
|
|
2231
|
+
parameters: {
|
|
2232
|
+
docs: {
|
|
2233
|
+
description: {
|
|
2234
|
+
story:
|
|
2235
|
+
'`initialSettings` seeds column borders, spacing and cell colours on mount — useful for restoring a user\'s previously persisted preferences without a flash of the DS defaults. Resetting via the settings dropdown still falls back to the DS defaults, not these values. Use the Controls panel below to try different combinations.',
|
|
2236
|
+
},
|
|
2237
|
+
},
|
|
2238
|
+
},
|
|
2239
|
+
argTypes: {
|
|
2240
|
+
hasColumnBorders: {
|
|
2241
|
+
description: 'Seeds `initialSettings.hasColumnBorders`.',
|
|
2242
|
+
control: 'boolean',
|
|
2243
|
+
},
|
|
2244
|
+
tableSpacing: {
|
|
2245
|
+
description: 'Seeds `initialSettings.tableSpacing`.',
|
|
2246
|
+
control: 'select',
|
|
2247
|
+
options: Object.values(TABLE_SPACING),
|
|
2248
|
+
},
|
|
2249
|
+
areCellColorsEnabled: {
|
|
2250
|
+
description: 'Seeds `initialSettings.areCellColorsEnabled`.',
|
|
2251
|
+
control: 'boolean',
|
|
2252
|
+
},
|
|
2253
|
+
},
|
|
2254
|
+
args: {
|
|
2255
|
+
rowData: sampleData,
|
|
2256
|
+
columnDefs: sampleColumnDefs,
|
|
2257
|
+
defaultColDef,
|
|
2258
|
+
domLayout: 'autoHeight',
|
|
2259
|
+
headerContent: <Table.TableSettingsDropdown />,
|
|
2260
|
+
hasColumnBorders: true,
|
|
2261
|
+
tableSpacing: Table.Spacing.L,
|
|
2262
|
+
areCellColorsEnabled: false,
|
|
2263
|
+
},
|
|
2264
|
+
render: ({ hasColumnBorders, tableSpacing, areCellColorsEnabled, ...rest }) => (
|
|
2265
|
+
<Table
|
|
2266
|
+
{...rest}
|
|
2267
|
+
initialSettings={{ hasColumnBorders, tableSpacing, areCellColorsEnabled }}
|
|
2268
|
+
/>
|
|
2269
|
+
),
|
|
2270
|
+
};
|
|
2271
|
+
|
|
2215
2272
|
export const WithCheckboxCellRenderer: Story = {
|
|
2216
2273
|
parameters: {
|
|
2217
2274
|
docs: {
|
|
@@ -894,6 +894,48 @@ describe('Table', () => {
|
|
|
894
894
|
expect(onTableSettingsChanged).toHaveBeenNthCalledWith(2, expect.objectContaining({ hasColumnBorders: true }));
|
|
895
895
|
});
|
|
896
896
|
|
|
897
|
+
test('renders with initialSettings applied instead of the hardcoded defaults', async () => {
|
|
898
|
+
render(
|
|
899
|
+
<Table
|
|
900
|
+
headerContent={<TableSettingsDropdown />}
|
|
901
|
+
initialSettings={{
|
|
902
|
+
hasColumnBorders: true,
|
|
903
|
+
tableSpacing: Table.Spacing.L,
|
|
904
|
+
areCellColorsEnabled: false,
|
|
905
|
+
}}
|
|
906
|
+
/>,
|
|
907
|
+
);
|
|
908
|
+
|
|
909
|
+
await userEvent.click(screen.getByRole('button', { name: /Table settings/i }));
|
|
910
|
+
|
|
911
|
+
expect((screen.getByLabelText('Large') as HTMLInputElement).checked).toBe(true);
|
|
912
|
+
expect((screen.getByLabelText('Column borders') as HTMLInputElement).checked).toBe(true);
|
|
913
|
+
expect((screen.getByLabelText('Cell colours') as HTMLInputElement).checked).toBe(false);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
test('resetSettings falls back to the DS defaults, not initialSettings', async () => {
|
|
917
|
+
const onTableSettingsReset = vi.fn();
|
|
918
|
+
|
|
919
|
+
render(
|
|
920
|
+
<Table
|
|
921
|
+
headerContent={<TableSettingsDropdown />}
|
|
922
|
+
initialSettings={{ hasColumnBorders: true, tableSpacing: Table.Spacing.L }}
|
|
923
|
+
onTableSettingsReset={onTableSettingsReset}
|
|
924
|
+
/>,
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
await userEvent.click(screen.getByRole('button', { name: /Table settings/i }));
|
|
928
|
+
|
|
929
|
+
const resetButton = screen.getByRole('button', { name: /Reset Table Settings/i });
|
|
930
|
+
await userEvent.click(resetButton);
|
|
931
|
+
|
|
932
|
+
expect(onTableSettingsReset).toHaveBeenCalledOnce();
|
|
933
|
+
await waitFor(() => {
|
|
934
|
+
expect((screen.getByLabelText('Medium') as HTMLInputElement).checked).toBe(true);
|
|
935
|
+
expect((screen.getByLabelText('Column borders') as HTMLInputElement).checked).toBe(false);
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
|
|
897
939
|
test('resetSettings is called appropriately', async () => {
|
|
898
940
|
const onTableSettingsReset = vi.fn();
|
|
899
941
|
|
|
@@ -69,6 +69,7 @@ export const Table = (props: TableProps) => {
|
|
|
69
69
|
footerContent,
|
|
70
70
|
footerTestId,
|
|
71
71
|
onGridReady,
|
|
72
|
+
initialSettings,
|
|
72
73
|
onTableSettingsChanged,
|
|
73
74
|
onTableSettingsReset,
|
|
74
75
|
onColumnBordersChanged,
|
|
@@ -100,6 +101,7 @@ export const Table = (props: TableProps) => {
|
|
|
100
101
|
setTableSpacing,
|
|
101
102
|
setAreCellColorsEnabled,
|
|
102
103
|
} = useTableSettings({
|
|
104
|
+
initialSettings,
|
|
103
105
|
onTableSettingsChanged,
|
|
104
106
|
onTableSettingsReset,
|
|
105
107
|
onColumnBordersChanged,
|
|
@@ -51,7 +51,9 @@ const USAGE_GUIDANCE = [
|
|
|
51
51
|
'|---|---|---|',
|
|
52
52
|
'| `bulkActions` | Start | Array of `{ displayName, callback, disabled? }` — shown as a dropdown when rows are selected |',
|
|
53
53
|
'| `onUndo` | Start | Renders an undo icon button |',
|
|
54
|
+
'| `isUndoDisabled` | Start | Disables the undo button (e.g. when there is nothing to undo) |',
|
|
54
55
|
'| `onRedo` | Start | Renders a redo icon button |',
|
|
56
|
+
'| `isRedoDisabled` | Start | Disables the redo button (e.g. when there is nothing to redo) |',
|
|
55
57
|
'| `showHideColumns` | Start | Renders the `HideColumnsDropdown` |',
|
|
56
58
|
'| `hasSearch` | End | Renders the `SearchBar` |',
|
|
57
59
|
'| `onDownload` | End | Renders a download icon button |',
|
|
@@ -79,6 +81,7 @@ const DEVELOPER_NOTES = [
|
|
|
79
81
|
' { displayName: "Delete selected", callback: api => deleteRows(api) },',
|
|
80
82
|
' ]}',
|
|
81
83
|
' onUndo={handleUndo}',
|
|
84
|
+
' isUndoDisabled={!canUndo}',
|
|
82
85
|
' showHideColumns',
|
|
83
86
|
' onDownload={handleDownload}',
|
|
84
87
|
' showSettings',
|
|
@@ -195,11 +198,21 @@ const meta = {
|
|
|
195
198
|
control: false,
|
|
196
199
|
table: { type: { summary: '() => void' }, defaultValue: { summary: 'undefined' } },
|
|
197
200
|
},
|
|
201
|
+
isUndoDisabled: {
|
|
202
|
+
description: 'When `true`, disables the undo button (e.g. when there is nothing to undo). Has no effect if `onUndo` is not provided.',
|
|
203
|
+
control: 'boolean',
|
|
204
|
+
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } },
|
|
205
|
+
},
|
|
198
206
|
onRedo: {
|
|
199
207
|
description: 'Renders a redo icon button in the start group. Called with no arguments on click.',
|
|
200
208
|
control: false,
|
|
201
209
|
table: { type: { summary: '() => void' }, defaultValue: { summary: 'undefined' } },
|
|
202
210
|
},
|
|
211
|
+
isRedoDisabled: {
|
|
212
|
+
description: 'When `true`, disables the redo button (e.g. when there is nothing to redo). Has no effect if `onRedo` is not provided.',
|
|
213
|
+
control: 'boolean',
|
|
214
|
+
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } },
|
|
215
|
+
},
|
|
203
216
|
showHideColumns: {
|
|
204
217
|
description: 'When `true`, renders the `HideColumnsDropdown` in the start group. Reads column visibility from `GridApiContext` (provided by `<Table />`). Pass `hideColumnsProps` for manual column control.',
|
|
205
218
|
control: 'boolean',
|
|
@@ -299,6 +312,21 @@ export const StartGroupOnly: Story = withDescription(
|
|
|
299
312
|
+ 'see the **Within a Table** story for a fully wired example.',
|
|
300
313
|
);
|
|
301
314
|
|
|
315
|
+
export const UndoRedoDisabled: Story = withDescription(
|
|
316
|
+
{
|
|
317
|
+
args: {
|
|
318
|
+
onUndo: fn(),
|
|
319
|
+
isUndoDisabled: true,
|
|
320
|
+
onRedo: fn(),
|
|
321
|
+
isRedoDisabled: true,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
'Undo and redo rendered with `isUndoDisabled` / `isRedoDisabled` set — use these when there is '
|
|
325
|
+
+ 'nothing left to undo or redo, so the buttons stay visible but communicate they are unavailable '
|
|
326
|
+
+ 'instead of silently no-opping when clicked. Toggle `isUndoDisabled` / `isRedoDisabled` off in the '
|
|
327
|
+
+ 'Controls panel to compare against the enabled state.',
|
|
328
|
+
);
|
|
329
|
+
|
|
302
330
|
export const EndGroupOnly: Story = withDescription(
|
|
303
331
|
{
|
|
304
332
|
parameters: { controls: { disable: true } },
|