Skip to content

Commit 815882a

Browse files
authored
AdvancedTable - First column pinning (HDS-5237) (#3104)
1 parent bfb6007 commit 815882a

File tree

11 files changed

+481
-152
lines changed

11 files changed

+481
-152
lines changed

.changeset/tangy-bushes-matter.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@hashicorp/design-system-components": minor
3+
---
4+
5+
<!-- START components/table/advanced-table -->
6+
`AdvancedTable` - Added features and fixed issues for column pinning including:
7+
- Added support for pinning first column in context menu
8+
- Translated template strings in context menu
9+
- Fixed style for scroll indicator when first column is sticky and has a px width
10+
<!-- END -->

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@
3737
@didInsert={{this.didInsertSelectAllCheckbox}}
3838
@willDestroy={{this.willDestroySelectAllCheckbox}}
3939
@selectionAriaLabelSuffix="all rows"
40-
@hasStickyColumn={{@hasStickyFirstColumn}}
40+
@hasStickyColumn={{this.hasStickyFirstColumn}}
4141
@isStickyColumnPinned={{this.isStickyColumnPinned}}
4242
>
43-
{{#each this._tableModel.columns as |column index|}}
43+
{{#each this._tableModel.columns as |column|}}
4444
{{#if column.isSortable}}
4545
<Hds::AdvancedTable::ThSort
4646
@column={{column}}
@@ -49,10 +49,11 @@
4949
@align={{column.align}}
5050
@tooltip={{column.tooltip}}
5151
@hasResizableColumns={{@hasResizableColumns}}
52-
@isStickyColumn={{if (and (eq index 0) @hasStickyFirstColumn) true}}
52+
@isStickyColumn={{this._isStickyColumn column}}
5353
@isStickyColumnPinned={{this.isStickyColumnPinned}}
5454
@tableHeight={{this._tableHeight}}
5555
@onColumnResize={{@onColumnResize}}
56+
@onPinFirstColumn={{this._onPinFirstColumn}}
5657
{{this._setColumnWidth column}}
5758
>
5859
{{column.label}}
@@ -65,13 +66,14 @@
6566
@hasResizableColumns={{@hasResizableColumns}}
6667
@isExpanded={{this._tableModel.expandState}}
6768
@isExpandable={{column.isExpandable}}
68-
@isStickyColumn={{if (and (eq index 0) @hasStickyFirstColumn) true}}
69+
@isStickyColumn={{this._isStickyColumn column}}
6970
@isStickyColumnPinned={{this.isStickyColumnPinned}}
7071
@isVisuallyHidden={{column.isVisuallyHidden}}
7172
@tableHeight={{this._tableHeight}}
7273
@tooltip={{column.tooltip}}
7374
@onClickToggle={{this._tableModel.toggleAll}}
7475
@onColumnResize={{@onColumnResize}}
76+
@onPinFirstColumn={{this._onPinFirstColumn}}
7577
{{this._setColumnWidth column}}
7678
>
7779
{{column.label}}
@@ -135,13 +137,13 @@
135137
didInsert=this.didInsertRowCheckbox
136138
willDestroy=this.willDestroyRowCheckbox
137139
selectionAriaLabelSuffix=@selectionAriaLabelSuffix
138-
hasStickyColumn=@hasStickyFirstColumn
140+
hasStickyColumn=this.hasStickyFirstColumn
139141
isStickyColumnPinned=this.isStickyColumnPinned
140142
)
141143
Th=(component
142144
"hds/advanced-table/th"
143145
scope="row"
144-
isStickyColumn=@hasStickyFirstColumn
146+
isStickyColumn=this.hasStickyFirstColumn
145147
isStickyColumnPinned=this.isStickyColumnPinned
146148
)
147149
Td=(component "hds/advanced-table/td" align=@align)

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

Lines changed: 118 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ const getScrollIndicatorDimensions = (
5959
scrollWrapper: HTMLDivElement,
6060
theadElement: HTMLDivElement,
6161
hasStickyHeader: boolean,
62-
hasStickyFirstColumn: boolean
62+
hasStickyFirstColumn: boolean,
63+
hasFirstColumnPxWidth: boolean,
64+
isStickyColumnPinned: boolean
6365
) => {
6466
const horizontalScrollBarHeight =
6567
scrollWrapper.offsetHeight - scrollWrapper.clientHeight;
@@ -80,8 +82,13 @@ const getScrollIndicatorDimensions = (
8082
leftOffset += elAsHTMLElement.offsetWidth;
8183
});
8284

83-
// offsets the left: -1px position if there are multiple sticky columns
84-
if (stickyColumnHeaders.length > 1) {
85+
// offsets the left: -1px position if there are multiple sticky columns or the first column has a fixed pixel width
86+
if (stickyColumnHeaders.length > 1 || hasFirstColumnPxWidth) {
87+
leftOffset -= 1;
88+
}
89+
90+
// offsets the left: -1px position if the sticky column is already pinned when the scroll indicator is calculated
91+
if (isStickyColumnPinned) {
8592
leftOffset -= 1;
8693
}
8794
}
@@ -98,7 +105,8 @@ const getScrollIndicatorDimensions = (
98105

99106
const getStickyColumnLeftOffset = (
100107
theadElement: HTMLDivElement,
101-
hasRowSelection: boolean
108+
hasRowSelection: boolean,
109+
isStickyColumnPinned: boolean
102110
) => {
103111
// if there is no select checkbox column, the sticky column is all the way to the left
104112
if (!hasRowSelection) return '0px';
@@ -107,7 +115,14 @@ const getStickyColumnLeftOffset = (
107115
'.hds-advanced-table__th--is-selectable'
108116
) as HTMLElement;
109117

110-
return `${selectableCell?.offsetWidth}px`;
118+
let leftOffset = selectableCell?.offsetWidth ?? 0;
119+
120+
// if the sticky column is pinned when the offset is calculated, we need to account for the increased width of the border
121+
if (isStickyColumnPinned && leftOffset > 0) {
122+
leftOffset -= 2;
123+
}
124+
125+
return `${leftOffset}px`;
111126
};
112127

113128
export interface HdsAdvancedTableSignature {
@@ -189,10 +204,12 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
189204
private _scrollHandler!: (event: Event) => void;
190205
private _resizeObserver!: ResizeObserver;
191206
private _theadElement!: HTMLDivElement;
207+
private _scrollWrapperElement!: HTMLDivElement;
192208

193209
@tracked scrollIndicatorDimensions = DEFAULT_SCROLL_DIMENSIONS;
194210
@tracked isStickyColumnPinned = false;
195211
@tracked isStickyHeaderPinned = false;
212+
@tracked hasPinnedFirstColumn: boolean | undefined = undefined;
196213
@tracked showScrollIndicatorLeft = false;
197214
@tracked showScrollIndicatorRight = false;
198215
@tracked showScrollIndicatorTop = false;
@@ -244,6 +261,10 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
244261
!hasResizableColumns
245262
);
246263
}
264+
265+
if (hasStickyFirstColumn) {
266+
this.hasPinnedFirstColumn = true;
267+
}
247268
}
248269

249270
get identityKey(): string | undefined {
@@ -261,8 +282,19 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
261282
return childrenKey;
262283
}
263284

285+
get hasStickyFirstColumn(): boolean | undefined {
286+
// The user-controlled `hasPinnedFirstColumn` variable takes precedence over the model's `hasStickyFirstColumn` property.
287+
if (this.hasPinnedFirstColumn !== undefined) {
288+
return this.hasPinnedFirstColumn;
289+
} else if (this.args.hasStickyFirstColumn === false) {
290+
return this.args.hasStickyFirstColumn;
291+
}
292+
293+
return undefined;
294+
}
295+
264296
get hasScrollIndicator(): boolean {
265-
if (this.args.hasStickyFirstColumn) {
297+
if (this.hasStickyFirstColumn) {
266298
return true;
267299
}
268300

@@ -429,78 +461,39 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
429461
);
430462

431463
private _setUpScrollWrapper = modifier((element: HTMLDivElement) => {
432-
this._scrollHandler = () => {
433-
// 6px as a buffer so the shadow doesn't appear over the border radius on the edge of the table
434-
const SCROLL_BUFFER = 6;
435-
436-
// left scroll indicator and sticky column styles
437-
if (element.scrollLeft > SCROLL_BUFFER && !this.showScrollIndicatorLeft) {
438-
if (this.args.hasStickyFirstColumn) {
439-
this.isStickyColumnPinned = true;
440-
}
441-
this.showScrollIndicatorLeft = true;
442-
} else if (element.scrollLeft === 0 && this.showScrollIndicatorLeft) {
443-
this.isStickyColumnPinned = false;
444-
this.showScrollIndicatorLeft = false;
445-
}
446-
447-
// the right edge is how far the user can scroll, which is the full width of the table - the visible section of the table (also subtract the buffer)
448-
const rightEdge =
449-
element.scrollWidth - element.clientWidth - SCROLL_BUFFER;
450-
451-
// right scroll indicator
452-
if (element.scrollLeft < rightEdge) {
453-
this.showScrollIndicatorRight = true;
454-
} else {
455-
this.showScrollIndicatorRight = false;
456-
}
457-
458-
// sticky header
459-
if (element.scrollTop > 0) {
460-
if (this.hasStickyHeader) {
461-
this.isStickyHeaderPinned = true;
462-
}
463-
this.showScrollIndicatorTop = true;
464-
} else {
465-
if (this.hasStickyHeader) {
466-
this.isStickyHeaderPinned = false;
467-
}
468-
this.showScrollIndicatorTop = false;
469-
}
470-
471-
// the bottom edge is how far the user can scroll, which is the full height of the table - the visible section of the table (also subtract the buffer)
472-
const bottomEdge =
473-
element.scrollHeight - element.clientHeight - SCROLL_BUFFER;
464+
this._scrollWrapperElement = element;
474465

475-
// bottom scroll indicator
476-
if (element.scrollTop < bottomEdge) {
477-
this.showScrollIndicatorBottom = true;
478-
} else {
479-
this.showScrollIndicatorBottom = false;
480-
}
466+
this._scrollHandler = () => {
467+
this._updateScrollIndicators(element);
481468
};
482469

483470
element.addEventListener('scroll', this._scrollHandler);
484471

485472
const updateMeasurements = () => {
486473
this._tableHeight = element.clientHeight;
487474

475+
const hasFirstColumnPxWidth =
476+
this._tableModel.columns[0]?.pxWidth !== undefined;
477+
488478
this.scrollIndicatorDimensions = getScrollIndicatorDimensions(
489479
element,
490480
this._theadElement,
491481
this.hasStickyHeader,
492-
hasStickyFirstColumn
482+
this.hasStickyFirstColumn ? true : false,
483+
hasFirstColumnPxWidth,
484+
this.isStickyColumnPinned
493485
);
494486

495-
if (hasStickyFirstColumn) {
487+
if (this.hasStickyFirstColumn) {
496488
this.stickyColumnOffset = getStickyColumnLeftOffset(
497489
this._theadElement,
498-
isSelectable
490+
isSelectable,
491+
this.isStickyColumnPinned
499492
);
500493
}
501494
};
502495

503-
const { hasStickyFirstColumn = false, isSelectable = false } = this.args;
496+
const { isSelectable = false } = this.args;
504497

505498
this._resizeObserver = new ResizeObserver((entries) => {
506499
entries.forEach(() => {
@@ -637,4 +630,71 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
637630
this._isSelectAllCheckboxSelected = this._selectAllCheckbox.checked;
638631
}
639632
}
633+
634+
private _updateScrollIndicators(element: HTMLElement): void {
635+
// 6px as a buffer so the shadow doesn't appear over the border radius on the edge of the table
636+
const SCROLL_BUFFER = 6;
637+
638+
// left scroll indicator and sticky column styles
639+
if (element.scrollLeft > SCROLL_BUFFER) {
640+
if (this.hasStickyFirstColumn) {
641+
this.isStickyColumnPinned = true;
642+
}
643+
if (!this.showScrollIndicatorLeft) {
644+
this.showScrollIndicatorLeft = true;
645+
}
646+
} else if (element.scrollLeft === 0 && this.showScrollIndicatorLeft) {
647+
this.isStickyColumnPinned = false;
648+
this.showScrollIndicatorLeft = false;
649+
}
650+
651+
// the right edge is how far the user can scroll, which is the full width of the table - the visible section of the table (also subtract the buffer)
652+
const rightEdge = element.scrollWidth - element.clientWidth - SCROLL_BUFFER;
653+
654+
// right scroll indicator
655+
if (element.scrollLeft < rightEdge) {
656+
this.showScrollIndicatorRight = true;
657+
} else {
658+
this.showScrollIndicatorRight = false;
659+
}
660+
661+
// sticky header
662+
if (element.scrollTop > 0) {
663+
if (this.hasStickyHeader) {
664+
this.isStickyHeaderPinned = true;
665+
}
666+
this.showScrollIndicatorTop = true;
667+
} else {
668+
if (this.hasStickyHeader) {
669+
this.isStickyHeaderPinned = false;
670+
}
671+
this.showScrollIndicatorTop = false;
672+
}
673+
674+
// the bottom edge is how far the user can scroll, which is the full height of the table - the visible section of the table (also subtract the buffer)
675+
const bottomEdge =
676+
element.scrollHeight - element.clientHeight - SCROLL_BUFFER;
677+
678+
// bottom scroll indicator
679+
if (element.scrollTop < bottomEdge) {
680+
this.showScrollIndicatorBottom = true;
681+
} else {
682+
this.showScrollIndicatorBottom = false;
683+
}
684+
}
685+
686+
private _onPinFirstColumn = (): void => {
687+
this.hasPinnedFirstColumn = this.hasPinnedFirstColumn ? false : true;
688+
// we need to retrigger the scroll indicator updates if the pinned state is changed when the table is already scrolled
689+
this._updateScrollIndicators(this._scrollWrapperElement);
690+
};
691+
692+
private _isStickyColumn = (
693+
column: HdsAdvancedTableColumnType
694+
): boolean | undefined => {
695+
if (column.isFirst && this.hasStickyFirstColumn !== undefined) {
696+
return this.hasStickyFirstColumn;
697+
}
698+
return undefined;
699+
};
640700
}

0 commit comments

Comments
 (0)