Skip to content

Commit e53afd3

Browse files
authored
AdvancedTable: add expand all button (#2688)
1 parent 83dbd26 commit e53afd3

File tree

26 files changed

+479
-187
lines changed

26 files changed

+479
-187
lines changed

.changeset/swift-elephants-play.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hashicorp/design-system-components": minor
3+
---
4+
5+
`AdvancedTable` - Added an expand all button to `AdvancedTable`s with nested rows and changed the structure of the component so now nested rows are always in the DOM, even when they are not visible. To add the expand all button, add `isExpandable: true` to the desired column in the `@columns` argument.

packages/components/src/components/hds/advanced-table/expandable-tr-group.hbs

+13-3
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
isExpandable=this.hasChildren
99
id=this._id
1010
depth=this.depth
11-
onClickToggle=this.onClickToggle
1211
isExpanded=this._isExpanded
1312
parentId=@parentId
1413
rowIndex=this.rowIndex
14+
didInsertExpandButton=@didInsertExpandButton
15+
willDestroyExpandButton=@willDestroyExpandButton
16+
shouldDisplayChildRows=@shouldDisplayChildRows
17+
onClickToggle=this.onClickToggle
1518
)
1619
}}
17-
{{#if (and this.hasChildren this._isExpanded)}}
20+
{{#if this.hasChildren}}
1821
{{#each this.children as |childRecord|}}
1922
<Hds::AdvancedTable::ExpandableTrGroup
2023
@record={{childRecord}}
@@ -23,17 +26,24 @@
2326
@parentId={{this._id}}
2427
@childrenKey={{this.childrenKey}}
2528
@rowIndex="{{this.rowIndex}}.{{this.childrenDepth}}"
29+
@onClickToggle={{@onClickToggle}}
30+
@didInsertExpandButton={{@didInsertExpandButton}}
31+
@willDestroyExpandButton={{@willDestroyExpandButton}}
32+
@shouldDisplayChildRows={{this.shouldDisplayChildRows}}
2633
as |T|
2734
>
2835
{{yield
2936
(hash
3037
data=T.data
3138
isExpandable=T.isExpandable
3239
depth=T.depth
33-
onClickToggle=T.onClickToggle
3440
isExpanded=T.isExpanded
3541
parentId=T.parentId
3642
id=T.id
43+
didInsertExpandButton=T.didInsertExpandButton
44+
willDestroyExpandButton=T.willDestroyExpandButton
45+
shouldDisplayChildRows=T.shouldDisplayChildRows
46+
onClickToggle=T.onClickToggle
3747
)
3848
}}
3949
</Hds::AdvancedTable::ExpandableTrGroup>

packages/components/src/components/hds/advanced-table/expandable-tr-group.ts

+35-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import { guidFor } from '@ember/object/internals';
77
import { tracked } from '@glimmer/tracking';
88
import { action } from '@ember/object';
99

10-
import type { HdsAdvancedTableHorizontalAlignment } from './types.ts';
10+
import type {
11+
HdsAdvancedTableExpandState,
12+
HdsAdvancedTableHorizontalAlignment,
13+
} from './types.ts';
1114
import type Owner from '@ember/owner';
1215
export interface HdsAdvancedTableExpandableTrGroupSignature {
1316
Args: {
@@ -17,6 +20,10 @@ export interface HdsAdvancedTableExpandableTrGroupSignature {
1720
parentId?: string;
1821
childrenKey?: string;
1922
rowIndex: number | string;
23+
didInsertExpandButton?: (button: HTMLButtonElement) => void;
24+
willDestroyExpandButton?: (button: HTMLButtonElement) => void;
25+
onClickToggle?: () => void;
26+
shouldDisplayChildRows?: boolean;
2027
};
2128
Blocks: {
2229
default?: [
@@ -26,17 +33,20 @@ export interface HdsAdvancedTableExpandableTrGroupSignature {
2633
id?: string;
2734
parentId?: string;
2835
depth: number;
29-
onClickToggle?: () => void;
30-
isExpanded?: boolean;
36+
onClickToggle?: (newValue?: HdsAdvancedTableExpandState) => void;
37+
isExpanded?: HdsAdvancedTableExpandState;
3138
rowIndex?: string;
39+
didInsertExpandButton?: (button: HTMLButtonElement) => void;
40+
willDestroyExpandButton?: (button: HTMLButtonElement) => void;
41+
shouldDisplayChildRows?: boolean;
3242
},
3343
];
3444
};
3545
Element: HTMLDivElement;
3646
}
3747

3848
export default class HdsAdvancedTableExpandableTrGroup extends Component<HdsAdvancedTableExpandableTrGroupSignature> {
39-
@tracked private _isExpanded = false;
49+
@tracked private _isExpanded: HdsAdvancedTableExpandState = false;
4050

4151
private _id = guidFor(this);
4252

@@ -92,7 +102,26 @@ export default class HdsAdvancedTableExpandableTrGroup extends Component<HdsAdva
92102
return this.depth + 1;
93103
}
94104

95-
@action onClickToggle() {
96-
this._isExpanded = !this._isExpanded;
105+
get shouldDisplayChildRows(): boolean {
106+
if (
107+
typeof this._isExpanded === 'boolean' &&
108+
this.args.shouldDisplayChildRows !== false
109+
) {
110+
return this.hasChildren && this._isExpanded;
111+
}
112+
113+
return false;
114+
}
115+
116+
@action onClickToggle(newValue?: boolean | 'mixed') {
117+
if (newValue) {
118+
this._isExpanded = newValue;
119+
} else {
120+
this._isExpanded = !this._isExpanded;
121+
}
122+
123+
if (this.args.onClickToggle) {
124+
this.args.onClickToggle();
125+
}
97126
}
98127
}

packages/components/src/components/hds/advanced-table/index.hbs

+15-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
role="grid"
1010
aria-describedby={{this._captionId}}
1111
{{style grid-template-columns=this.gridTemplateColumns}}
12-
{{this._setUpObserver}}
12+
{{this._setUpObservers}}
1313
>
1414
<div id={{this._captionId}} class="sr-only hds-advanced-table__caption" aria-live="polite">
1515
{{@caption}}
@@ -41,6 +41,12 @@
4141
@align={{column.align}}
4242
@tooltip={{column.tooltip}}
4343
@isVisuallyHidden={{column.isVisuallyHidden}}
44+
@isExpandable={{column.isExpandable}}
45+
@onClickToggle={{this.onExpandAllClick}}
46+
@isExpanded={{this._expandAllButtonState}}
47+
@hasExpandAllButton={{this.hasNestedRows}}
48+
@didInsertExpandButton={{this.didInsertExpandAllButton}}
49+
@willDestroyExpandButton={{this.willDestroyExpandAllButton}}
4450
>{{column.label}}</Hds::AdvancedTable::Th>
4551
{{/if}}
4652
{{/each}}
@@ -59,11 +65,16 @@
5965
@record={{record}}
6066
@childrenKey={{this.childrenKey}}
6167
@rowIndex={{index}}
68+
@didInsertExpandButton={{this.didInsertExpandButton}}
69+
@willDestroyExpandButton={{this.willDestroyExpandButton}}
70+
@onClickToggle={{this.setExpandAllState}}
6271
as |T|
6372
>
6473
{{yield
6574
(hash
66-
Tr=(component "hds/advanced-table/tr" isParentRow=T.isExpandable depth=T.depth)
75+
Tr=(component
76+
"hds/advanced-table/tr" isParentRow=T.isExpandable depth=T.depth displayRow=T.shouldDisplayChildRows
77+
)
6778
Th=(component
6879
"hds/advanced-table/th"
6980
scope="row"
@@ -73,6 +84,8 @@
7384
onClickToggle=T.onClickToggle
7485
isExpanded=T.isExpanded
7586
depth=T.depth
87+
didInsertExpandButton=T.didInsertExpandButton
88+
willDestroyExpandButton=T.willDestroyExpandButton
7689
)
7790
Td=(component "hds/advanced-table/td" align=@align)
7891
data=T.data

packages/components/src/components/hds/advanced-table/index.ts

+76-11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { tracked } from '@glimmer/tracking';
1010
import type { ComponentLike } from '@glint/template';
1111
import { guidFor } from '@ember/object/internals';
1212
import { modifier } from 'ember-modifier';
13+
import { next } from '@ember/runloop';
1314

1415
import {
1516
HdsAdvancedTableDensityValues,
@@ -26,11 +27,13 @@ import type {
2627
HdsAdvancedTableThSortOrder,
2728
HdsAdvancedTableVerticalAlignment,
2829
HdsAdvancedTableModel,
30+
HdsAdvancedTableExpandState,
2931
} from './types.ts';
3032
import type { HdsFormCheckboxBaseSignature } from '../form/checkbox/base.ts';
3133
import type { HdsAdvancedTableTdSignature } from './td.ts';
3234
import type { HdsAdvancedTableThSignature } from './th.ts';
3335
import type { HdsAdvancedTableTrSignature } from './tr.ts';
36+
import { updateLastRowClass } from '../../../modifiers/hds-advanced-table-cell/dom-management.ts';
3437

3538
export const DENSITIES: HdsAdvancedTableDensities[] = Object.values(
3639
HdsAdvancedTableDensityValues
@@ -73,7 +76,7 @@ export interface HdsAdvancedTableSignature {
7376
Th?: ComponentLike<HdsAdvancedTableThSignature>;
7477
data?: Record<string, unknown>;
7578
rowIndex?: number | string;
76-
isOpen?: boolean;
79+
isOpen?: HdsAdvancedTableExpandState;
7780
},
7881
];
7982
};
@@ -88,10 +91,14 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
8891
private _selectAllCheckbox?: HdsFormCheckboxBaseSignature['Element'] =
8992
undefined;
9093
@tracked private _isSelectAllCheckboxSelected?: boolean = undefined;
94+
@tracked _expandAllButton?: HTMLButtonElement = undefined;
95+
@tracked private _expandAllButtonState?: boolean | 'mixed' = undefined;
9196

9297
private _selectableRows: HdsAdvancedTableSelectableRow[] = [];
98+
private _expandableRows: HTMLButtonElement[] = [];
9399
private _captionId = 'caption-' + guidFor(this);
94-
private _observer: IntersectionObserver | undefined = undefined;
100+
private _intersectionObserver: IntersectionObserver | undefined = undefined;
101+
private _element!: HTMLDivElement;
95102

96103
get getSortCriteria(): string | HdsAdvancedTableSortingFunction<unknown> {
97104
// get the current column
@@ -282,13 +289,16 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
282289
return classes.join(' ');
283290
}
284291

285-
private _setUpObserver = modifier((element: HTMLElement) => {
292+
private _setUpObservers = modifier((element: HTMLDivElement) => {
286293
const stickyGridHeader = element.querySelector(
287294
'.hds-advanced-table__thead.hds-advanced-table__thead--sticky'
288295
);
289296

297+
this._element = element;
298+
this.setExpandAllState();
299+
290300
if (stickyGridHeader !== null) {
291-
this._observer = new IntersectionObserver(
301+
this._intersectionObserver = new IntersectionObserver(
292302
([element]) =>
293303
element?.target.classList.toggle(
294304
'hds-advanced-table__thead--is-pinned',
@@ -297,12 +307,14 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
297307
{ threshold: [1] }
298308
);
299309

300-
this._observer.observe(stickyGridHeader);
310+
this._intersectionObserver.observe(stickyGridHeader);
301311
}
302312

313+
updateLastRowClass(element);
314+
303315
return () => {
304-
if (this._observer) {
305-
this._observer.disconnect();
316+
if (this._intersectionObserver) {
317+
this._intersectionObserver.disconnect();
306318
}
307319
};
308320
});
@@ -365,7 +377,6 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
365377
onSelectionAllChange(): void {
366378
this._selectableRows.forEach((row) => {
367379
row.checkbox.checked = this._selectAllCheckbox?.checked ?? false;
368-
row.checkbox.dispatchEvent(new Event('toggle', { bubbles: false }));
369380
});
370381
this._isSelectAllCheckboxSelected =
371382
this._selectAllCheckbox?.checked ?? false;
@@ -425,9 +436,63 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
425436
this._selectAllCheckbox.indeterminate =
426437
selectedRowsCount > 0 && selectedRowsCount < selectableRowsCount;
427438
this._isSelectAllCheckboxSelected = this._selectAllCheckbox.checked;
428-
this._selectAllCheckbox.dispatchEvent(
429-
new Event('toggle', { bubbles: false })
430-
);
439+
}
440+
}
441+
442+
@action didInsertExpandAllButton(button: HTMLButtonElement): void {
443+
this._expandAllButton = button;
444+
}
445+
446+
@action willDestroyExpandAllButton(): void {
447+
this._expandAllButton = undefined;
448+
}
449+
450+
@action
451+
didInsertExpandButton(button: HTMLButtonElement): void {
452+
this._expandableRows.push(button);
453+
this.setExpandAllState();
454+
}
455+
456+
@action
457+
willDestroyExpandButton(button: HTMLButtonElement): void {
458+
this._expandableRows.filter((btn) => button === btn);
459+
this.setExpandAllState();
460+
}
461+
462+
@action
463+
setExpandAllState(): void {
464+
if (this._expandAllButton && this._element) {
465+
// eslint-disable-next-line ember/no-runloop
466+
next(() => {
467+
const parentRowsCount = this._expandableRows.length;
468+
const expandedRowsCount = this._expandableRows.filter(
469+
(button) => button.getAttribute('aria-expanded') === 'true'
470+
).length;
471+
472+
let expandAllState: HdsAdvancedTableExpandState;
473+
474+
if (parentRowsCount === expandedRowsCount) expandAllState = true;
475+
else if (expandedRowsCount === 0) expandAllState = false;
476+
else expandAllState = 'mixed';
477+
478+
this._expandAllButtonState = expandAllState;
479+
updateLastRowClass(this._element);
480+
});
481+
}
482+
}
483+
484+
@action
485+
onExpandAllClick(): void {
486+
if (this._expandAllButton && this._element) {
487+
const newState = this._expandAllButtonState === true ? false : true;
488+
489+
this._expandableRows.forEach((button) => {
490+
button.setAttribute('aria-expanded', `${newState}`);
491+
button.dispatchEvent(new Event('toggle', { bubbles: false }));
492+
});
493+
494+
this._expandAllButtonState = newState;
495+
updateLastRowClass(this._element);
431496
}
432497
}
433498
}

packages/components/src/components/hds/advanced-table/th-button-expand.hbs

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
aria-labelledby="{{this._prefixLabelId}} {{@labelId}}"
1212
aria-expanded="{{this.isExpanded}}"
1313
aria-description="Toggle the visibility of the related rows."
14+
{{this._setUpEventHandler}}
1415
...attributes
1516
>
1617
{{! template-lint-enable no-unsupported-role-attributes}}

0 commit comments

Comments
 (0)