Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
70d5559
add reorder components
zamoore Jul 7, 2025
2ad6f29
picking reorder code
zamoore Jul 7, 2025
788eef0
pulling in styling
zamoore Jul 7, 2025
733baf9
set up initial showcase example
zamoore Jul 8, 2025
4ed5a9f
working on styling
zamoore Jul 8, 2025
adcc00d
adding tests
zamoore Jul 9, 2025
cdf739b
working on adding tests for drag/drop behavior
zamoore Jul 9, 2025
00ace6f
cleaning up visuals
zamoore Jul 11, 2025
433e9f4
styling the reorder handle
zamoore Jul 11, 2025
77a4e10
styling reorderable columns
zamoore Jul 15, 2025
f9c04c7
adding dropdown context options for reordering
zamoore Jul 15, 2025
a629f0c
working on context menu options for reordering
zamoore Jul 15, 2025
66c79d2
can send column to first/last
zamoore Jul 16, 2025
8238e27
applying new reorder handle styles
zamoore Jul 16, 2025
6787efa
can step column with keyboard
zamoore Jul 16, 2025
fe43c94
keyboard movement works
zamoore Jul 16, 2025
c5698dd
added tests for context menu opptions
zamoore Jul 16, 2025
00a31db
adding tests for context menu action execution
zamoore Jul 16, 2025
67da9d3
added tests for reordering columns with focus/arrowkeys
zamoore Jul 16, 2025
b168775
styling srag target
zamoore Jul 16, 2025
8facd98
fixed reorder handle styles
zamoore Jul 17, 2025
9cbefdd
added styles for focus-within
zamoore Jul 17, 2025
c5f4f78
working on programatic focus for reorder handle
zamoore Jul 17, 2025
310a7d8
added reorder capabilities to sortable columns
zamoore Jul 21, 2025
19108ba
reordering resized columns preserves the column size
zamoore Jul 21, 2025
6154a12
resizing reordered columns works
zamoore Jul 21, 2025
f92dbf0
making resizing and reordering work together
zamoore Jul 21, 2025
c89cbdb
working on width ledger
zamoore Jul 22, 2025
b2f2054
resizing works again but with a ledger system now
zamoore Jul 22, 2025
688ca3e
reordering and resizing work together
zamoore Jul 22, 2025
c125fbd
Adding reordering documentation and accompanying images
jorytindall Jul 29, 2025
57937f2
Cleanup of prose and structure of documentation, add placeholder for …
jorytindall Jul 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@
"./components/hds/advanced-table/th-button-sort.js": "./dist/_app_/components/hds/advanced-table/th-button-sort.js",
"./components/hds/advanced-table/th-button-tooltip.js": "./dist/_app_/components/hds/advanced-table/th-button-tooltip.js",
"./components/hds/advanced-table/th-context-menu.js": "./dist/_app_/components/hds/advanced-table/th-context-menu.js",
"./components/hds/advanced-table/th-reorder-drop-target.js": "./dist/_app_/components/hds/advanced-table/th-reorder-drop-target.js",
"./components/hds/advanced-table/th-reorder-handle.js": "./dist/_app_/components/hds/advanced-table/th-reorder-handle.js",
"./components/hds/advanced-table/th-resize-handle.js": "./dist/_app_/components/hds/advanced-table/th-resize-handle.js",
"./components/hds/advanced-table/th-selectable.js": "./dist/_app_/components/hds/advanced-table/th-selectable.js",
"./components/hds/advanced-table/th-sort.js": "./dist/_app_/components/hds/advanced-table/th-sort.js",
Expand Down
31 changes: 22 additions & 9 deletions packages/components/src/components/hds/advanced-table/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
{{! TODO: Remove this when ready }}
{{! @glint-nocheck }}

<div
class="hds-advanced-table__container
{{(if this.isStickyHeaderPinned 'hds-advanced-table__container--header-is-pinned')}}"
Expand Down Expand Up @@ -40,22 +43,26 @@
@hasStickyColumn={{@hasStickyFirstColumn}}
@isStickyColumnPinned={{this.isStickyColumnPinned}}
>
{{#each this._tableModel.columns as |column index|}}
{{#each this._tableModel.orderedColumns as |column index|}}
{{#if column.isSortable}}
<Hds::AdvancedTable::ThSort
@column={{column}}
@sortOrder={{if (eq column.key this._tableModel.sortBy) this._tableModel.sortOrder}}
@onClickSort={{if column.key (fn this._tableModel.setSortBy column.key)}}
@align={{column.align}}
@tooltip={{column.tooltip}}
@hasReorderableColumns={{@hasReorderableColumns}}
@hasResizableColumns={{@hasResizableColumns}}
@isLastColumn={{eq index (sub this._tableModel.columns.length 1)}}
@isLastColumn={{eq index (sub this._tableModel.orderedColumns.length 1)}}
@isStickyColumn={{if (and (eq index 0) @hasStickyFirstColumn) true}}
@isStickyColumnPinned={{this.isStickyColumnPinned}}
@previousColumn={{get this._tableModel.columns (sub index 1)}}
@nextColumn={{get this._tableModel.columns (add index 1)}}
@previousColumn={{get this._tableModel.orderedColumns (sub index 1)}}
@nextColumn={{get this._tableModel.orderedColumns (add index 1)}}
@tableHeight={{this._tableHeight}}
@onColumnResize={{@onColumnResize}}
@onReorderDragEnd={{fn (mut this._tableModel.reorderDraggedColumn) null}}
@onReorderDragStart={{fn (mut this._tableModel.reorderDraggedColumn)}}
@onReorderDrop={{this._tableModel.moveColumnToDropTarget}}
{{this._setColumnWidth column}}
>
{{column.label}}
Expand All @@ -65,19 +72,23 @@
@align={{column.align}}
@column={{column}}
@hasExpandAllButton={{this._tableModel.hasRowsWithChildren}}
@hasReorderableColumns={{@hasReorderableColumns}}
@hasResizableColumns={{@hasResizableColumns}}
@isExpanded={{this._tableModel.expandState}}
@isExpandable={{column.isExpandable}}
@isLastColumn={{eq index (sub this._tableModel.columns.length 1)}}
@isLastColumn={{eq index (sub this._tableModel.orderedColumns.length 1)}}
@isStickyColumn={{if (and (eq index 0) @hasStickyFirstColumn) true}}
@isStickyColumnPinned={{this.isStickyColumnPinned}}
@isVisuallyHidden={{column.isVisuallyHidden}}
@previousColumn={{get this._tableModel.columns (sub index 1)}}
@nextColumn={{get this._tableModel.columns (add index 1)}}
@previousColumn={{get this._tableModel.orderedColumns (sub index 1)}}
@nextColumn={{get this._tableModel.orderedColumns (add index 1)}}
@tableHeight={{this._tableHeight}}
@tooltip={{column.tooltip}}
@onClickToggle={{this._tableModel.toggleAll}}
@onColumnResize={{@onColumnResize}}
@onReorderDragEnd={{fn (mut this._tableModel.reorderDraggedColumn) null}}
@onReorderDragStart={{fn (mut this._tableModel.reorderDraggedColumn)}}
@onReorderDrop={{this._tableModel.moveColumnToDropTarget}}
{{this._setColumnWidth column}}
>
{{column.label}}
Expand Down Expand Up @@ -110,6 +121,7 @@
isParentRow=T.isExpandable
depth=T.depth
displayRow=T.shouldDisplayChildRows
data=T.data
)
Th=(component
"hds/advanced-table/th"
Expand All @@ -121,7 +133,7 @@
scope="row"
onClickToggle=T.onClickToggle
)
Td=(component "hds/advanced-table/td" align=@align)
Td=(component "hds/advanced-table/td" align=@align columns=this._tableModel.columns)
data=T.data
isOpen=T.isExpanded
rowIndex=T.rowIndex
Expand All @@ -143,14 +155,15 @@
selectionAriaLabelSuffix=@selectionAriaLabelSuffix
hasStickyColumn=@hasStickyFirstColumn
isStickyColumnPinned=this.isStickyColumnPinned
data=record
)
Th=(component
"hds/advanced-table/th"
scope="row"
isStickyColumn=@hasStickyFirstColumn
isStickyColumnPinned=this.isStickyColumnPinned
)
Td=(component "hds/advanced-table/td" align=@align)
Td=(component "hds/advanced-table/td" align=@align columns=this._tableModel.columns)
data=record
rowIndex=index
)
Expand Down
16 changes: 11 additions & 5 deletions packages/components/src/components/hds/advanced-table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
HdsAdvancedTableVerticalAlignment,
HdsAdvancedTableModel,
HdsAdvancedTableExpandState,
HdsAdvancedTableColumnReorderCallback,
} from './types.ts';
import type HdsAdvancedTableColumnType from './models/column.ts';
import type { HdsFormCheckboxBaseSignature } from '../form/checkbox/base.ts';
Expand Down Expand Up @@ -115,6 +116,7 @@ export interface HdsAdvancedTableSignature {
align?: HdsAdvancedTableHorizontalAlignment;
caption?: string;
columns: HdsAdvancedTableColumn[];
columnOrder?: string[];
density?: HdsAdvancedTableDensities;
identityKey?: string;
isSelectable?: boolean;
Expand All @@ -126,11 +128,13 @@ export interface HdsAdvancedTableSignature {
sortedMessageText?: string;
sortOrder?: HdsAdvancedTableThSortOrder;
valign?: HdsAdvancedTableVerticalAlignment;
hasReorderableColumns?: boolean;
hasResizableColumns?: boolean;
hasStickyHeader?: boolean;
hasStickyFirstColumn?: boolean;
childrenKey?: string;
maxHeight?: string;
onColumnReorder?: HdsAdvancedTableColumnReorderCallback;
onColumnResize?: (columnKey: string, newWidth?: string) => void;
onSelectionChange?: (
selection: HdsAdvancedTableOnSelectionChangeSignature
Expand Down Expand Up @@ -205,6 +209,7 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
const {
model,
columns,
columnOrder,
childrenKey,
hasResizableColumns,
sortBy,
Expand All @@ -216,6 +221,7 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
this._tableModel = new HdsAdvancedTableTableModel({
model,
columns,
columnOrder,
childrenKey,
hasResizableColumns,
sortBy,
Expand Down Expand Up @@ -350,25 +356,25 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
// returns the grid-template-columns CSS attribute for the grid
get gridTemplateColumns(): string {
const { isSelectable } = this.args;
const { columns } = this._tableModel;
const { orderedColumns } = this._tableModel;

const DEFAULT_COLUMN_WIDTH = '1fr';

// if there is a select checkbox, the first column has a 'min-content' width to hug the checkbox content
let style = isSelectable ? 'min-content ' : '';

const hasCustomColumnWidths = columns.some(
const hasCustomColumnWidths = orderedColumns.some(
(column) => column.width !== undefined
);

if (hasCustomColumnWidths) {
// check the custom column widths, if the current column has a custom width use the custom width. otherwise take the available space.
for (let i = 0; i < columns.length; i++) {
style += ` ${columns[i]!.width ?? DEFAULT_COLUMN_WIDTH}`;
for (let i = 0; i < orderedColumns.length; i++) {
style += ` ${orderedColumns[i]!.width ?? DEFAULT_COLUMN_WIDTH}`;
}
} else {
// if there are no custom column widths, each column is the same width and they take up the available space
style += `repeat(${columns.length}, ${DEFAULT_COLUMN_WIDTH})`;
style += `repeat(${orderedColumns.length}, ${DEFAULT_COLUMN_WIDTH})`;
}

return style;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

import type { HdsAdvancedTableThReorderHandleSignature } from '../th-reorder-handle.ts';
import type HdsAdvancedTableModel from './table.ts';
import type {
HdsAdvancedTableCell,
HdsAdvancedTableHorizontalAlignment,
HdsAdvancedTableColumn as HdsAdvancedTableColumnType,
} from '../types';
Expand All @@ -31,7 +33,6 @@ export default class HdsAdvancedTableColumn {
@tracked label: string = '';
@tracked align?: HdsAdvancedTableHorizontalAlignment = 'left';
@tracked isExpandable?: boolean = false;
@tracked isReorderable?: boolean = false;
@tracked isSortable?: boolean = false;
@tracked isVisuallyHidden?: boolean = false;
@tracked key?: string = undefined;
Expand All @@ -41,11 +42,25 @@ export default class HdsAdvancedTableColumn {
@tracked width?: string = undefined;
@tracked originalWidth?: string = undefined; // used to restore the width when resetting
@tracked imposedWidthDelta: number = 0; // used to track the width change imposed by the previous column
@tracked widthDebts: Record<string, number> = {}; // used to track width changes imposed by other columns

@tracked isBeingDragged: boolean = false;
@tracked thElement?: HTMLDivElement = undefined;
@tracked
reorderHandleElement?: HdsAdvancedTableThReorderHandleSignature['Element'] =
undefined;
@tracked sortingFunction?: (a: unknown, b: unknown) => number = undefined;

table: HdsAdvancedTableModel;

get cells(): HdsAdvancedTableCell[] {
return this.table.flattenedVisibleRows.map((row) => {
const cell = row.cells.find((cell) => cell.columnKey === this.key);

return cell!;
});
}

get pxWidth(): number | undefined {
if (isPxSize(this.width)) {
return pxToNumber(this.width!);
Expand All @@ -67,6 +82,44 @@ export default class HdsAdvancedTableColumn {
}
}

get index(): number {
const { orderedColumns } = this.table;

if (orderedColumns.length === 0) {
return -1;
}

return orderedColumns.findIndex((column) => column.key === this.key);
}

get isFirst(): boolean {
return this.index === 0;
}

get isLast(): boolean {
return this.index === this.table.orderedColumns.length - 1;
}

get siblings(): {
previous?: HdsAdvancedTableColumn;
next?: HdsAdvancedTableColumn;
} {
const { index, table } = this;
const { orderedColumns } = table;

if (index === -1) {
return {};
}

return {
previous: index > 0 ? orderedColumns[index - 1] : undefined,
next:
index < orderedColumns.length - 1
? orderedColumns[index + 1]
: undefined,
};
}

constructor(args: {
column: HdsAdvancedTableColumnType;
table: HdsAdvancedTableModel;
Expand Down Expand Up @@ -106,6 +159,60 @@ export default class HdsAdvancedTableColumn {
this.maxWidth = maxWidth ?? DEFAULT_MAX_WIDTH;
}

private payWidthDebts(): void {
Object.entries(this.widthDebts).forEach(([lenderKey, amount]) => {
const lender = this.table.getColumnByKey(lenderKey);

if (lender) {
// Give the width back to the column that lent it to us
lender.setPxWidth((lender.pxWidth ?? 0) + amount);
}
});

// Clear our own debt ledger, as we've paid everyone back
this.widthDebts = {};
}

private collectWidthDebts(): void {
const { key: thisKey, table } = this;

if (thisKey === undefined) {
return;
}

table.columns.forEach((otherColumn) => {
const debtToCollect = otherColumn.widthDebts[thisKey] ?? 0;

if (debtToCollect > 0) {
// Take the width back from the column that owes us
otherColumn.setPxWidth((otherColumn.pxWidth ?? 0) - debtToCollect);
// Clear the debt from their ledger
delete otherColumn.widthDebts[thisKey];
}
});
}

private settleWidthDebts(): void {
this.payWidthDebts();
this.collectWidthDebts();
}

@action focusReorderHandle(): void {
if (this.thElement === undefined) {
return;
}

// focus the th element first (parent) to ensure the handle is visible
this.thElement.focus({ preventScroll: true });

if (this.reorderHandleElement === undefined) {
return;
}

// then focus the reorder handle element
this.reorderHandleElement.focus();
}

// Sets the column width in pixels, ensuring it respects the min and max width constraints.
@action
setPxWidth(newPxWidth: number): void {
Expand All @@ -122,29 +229,10 @@ export default class HdsAdvancedTableColumn {
}
}

// This method is called when the column width is changed by the previous column.
@action
onPreviousColumnWidthRestored(): void {
const restoredWidth = (this.pxWidth ?? 0) + this.imposedWidthDelta;

this.setPxWidth(restoredWidth);

this.imposedWidthDelta = 0;
}

// This method is called when the next column width is restored.
@action
onNextColumnWidthRestored(imposedWidthDelta: number): void {
this.setPxWidth((this.pxWidth ?? 0) - imposedWidthDelta);
}

@action
restoreWidth(): void {
this.width = this.originalWidth;
this.imposedWidthDelta = 0;

if (this.key === undefined) {
return;
}
this.settleWidthDebts();
}
}
Loading