UNPKG

@hashicorp/design-system-components

Version:
1,321 lines (1,315 loc) 70.4 kB
import Component from '@glimmer/component'; import { action } from '@ember/object'; import { assert } from '@ember/debug'; import { tracked, cached } from '@glimmer/tracking'; import { guidFor } from '@ember/object/internals'; import { service } from '@ember/service'; import { modifier } from 'ember-modifier'; import { TrackedSet } from 'tracked-built-ins'; import { concat, hash, fn } from '@ember/helper'; import { not, and, eq, notEq } from 'ember-truth-helpers'; import style from 'ember-style-modifier'; import { on } from '@ember/modifier'; import { sortBy } from '@nullvoxpopuli/ember-composable-helpers'; import { HdsAdvancedTableColumnReorderSideValues, HdsAdvancedTableThSortOrderLabelValues, HdsAdvancedTableThSortOrderValues, HdsAdvancedTableHorizontalAlignmentValues, HdsAdvancedTableDensityValues, HdsAdvancedTableVerticalAlignmentValues } from './components/hds/advanced-table/types.js'; import HdsFilterBar from './components/hds/filter-bar/index.js'; import HdsApplicationState from './components/hds/application-state/index.js'; import Composite from './components/hds/composite/index.js'; import HdsAdvancedTableColumnManager from './components/hds/advanced-table/column-manager/index.js'; import { scheduleOnce } from '@ember/runloop'; import { focusable } from 'tabbable'; import hdsScrollIntoViewOnFocus from './modifiers/hds-scroll-into-view-on-focus.js'; import HdsAdvancedTableCellModifier from './modifiers/hds-advanced-table-cell.js'; import focusTrap from 'ember-focus-trap/modifiers/focus-trap'; import HdsLayoutFlex from './components/hds/layout/flex/index.js'; import HdsAdvancedTableThButtonExpand from './components/hds/advanced-table/th-button-expand.js'; import HdsAdvancedTableThButtonSort from './components/hds/advanced-table/th-button-sort.js'; import HdsAdvancedTableThButtonTooltip from './components/hds/advanced-table/th-button-tooltip.js'; import HdsAdvancedTableThContextMenu from './components/hds/advanced-table/th-context-menu.js'; import HdsAdvancedTableThReorderHandle from './components/hds/advanced-table/th-reorder-handle.js'; import { parsePixel, requestAnimationFrameWaiter } from './components/hds/advanced-table/utils.js'; import HdsAdvancedTableBody from './components/hds/advanced-table/body.js'; import HdsAdvancedTableTd from './components/hds/advanced-table/td.js'; import HdsTHelper from './helpers/hds-t.js'; import { precompileTemplate } from '@ember/template-compilation'; import { setComponentTemplate } from '@ember/component'; import { g, i, n } from 'decorator-transforms/runtime'; import { DEFAULT_MIN_WIDTH, DEFAULT_MAX_WIDTH } from './components/hds/advanced-table/column-manager/width.js'; import { onFocusTrapDeactivate } from './modifiers/hds-advanced-table-cell/dom-management.js'; import HdsFormCheckboxBase from './components/hds/form/checkbox/base.js'; /** * Copyright IBM Corp. 2021, 2025 * SPDX-License-Identifier: MPL-2.0 */ const KEYBOARD_RESIZE_STEP = 10; function calculateEffectiveDelta(deltaX, col, startColW, nextCol, startNextColW) { const colMin = parsePixel(col.minWidth ?? DEFAULT_MIN_WIDTH) ?? 0; const colMax = parsePixel(col.maxWidth ?? DEFAULT_MAX_WIDTH) ?? Infinity; const nextMin = parsePixel(nextCol.minWidth ?? DEFAULT_MIN_WIDTH) ?? 0; const nextMax = parsePixel(nextCol.maxWidth ?? DEFAULT_MAX_WIDTH) ?? Infinity; let effectiveDelta = 0; // expanding col, shrinking nextCol if (deltaX > 0) { const maxCanExpandCol = colMax - startColW; const maxCanShrinkNext = startNextColW - nextMin; effectiveDelta = Math.min(deltaX, maxCanExpandCol, maxCanShrinkNext); effectiveDelta = Math.max(0, effectiveDelta); } else if (deltaX < 0) { const absDeltaX = -deltaX; const maxCanShrinkCol = startColW - colMin; let maxCanExpandNext; if (startNextColW > nextMax) { maxCanExpandNext = Infinity; } else { maxCanExpandNext = nextMax - startNextColW; } effectiveDelta = -Math.min(absDeltaX, maxCanShrinkCol, maxCanExpandNext); effectiveDelta = Math.min(0, effectiveDelta); } return effectiveDelta; } class HdsAdvancedTableThResizeHandle extends Component { static { g(this.prototype, "resizing", [tracked], function () { return null; }); } #resizing = (i(this, "resizing"), void 0); static { g(this.prototype, "_transientDelta", [tracked], function () { return 0; }); } #_transientDelta = (i(this, "_transientDelta"), void 0); // track the width change as it is changing, applied when resizing stops static { g(this.prototype, "_isUpdateQueued", [tracked], function () { return false; }); } #_isUpdateQueued = (i(this, "_isUpdateQueued"), void 0); static { g(this.prototype, "_lastPointerEvent", [tracked], function () { return null; }); } #_lastPointerEvent = (i(this, "_lastPointerEvent"), void 0); _handleElement; _boundResize; _boundStopResize; _registerHandleElement = modifier(element => { this._handleElement = element; }); constructor(owner, args) { super(owner, args); this._boundResize = this._resize.bind(this); this._boundStopResize = this._stopResize.bind(this); } get currentWidthInPixels() { const { column, onGetAppliedWidth } = this.args; if (column === undefined || onGetAppliedWidth === undefined) { return 0; } const appliedWidth = onGetAppliedWidth(column.key); return parsePixel(appliedWidth) ?? 0; } get minWidthInPixels() { return parsePixel(this.args.column?.minWidth ?? DEFAULT_MIN_WIDTH) ?? 0; } get maxWidthInPixels() { return parsePixel(this.args.column?.maxWidth ?? DEFAULT_MAX_WIDTH) ?? Infinity; } get height() { const { tableHeight } = this.args; if (tableHeight === undefined) { return; } return `${tableHeight - BORDER_WIDTH * 2}px`; } get classNames() { const classes = ['hds-advanced-table__th-resize-handle']; if (this.resizing !== null) { classes.push('hds-advanced-table__th-resize-handle--resizing'); } return classes.join(' '); } _applyTransientWidths() { const { column, siblingColumnKeys, onApplyTransientWidth } = this.args; if (column === undefined || onApplyTransientWidth === undefined) { return; } const { next: nextColumnKey } = siblingColumnKeys ?? {}; onApplyTransientWidth(column.key); if (nextColumnKey !== undefined) { onApplyTransientWidth(nextColumnKey); } } onColumnResize(key, width) { const { onColumnResize } = this.args; if (typeof onColumnResize === 'function' && key !== undefined) { onColumnResize(key, width); } } static { n(this.prototype, "onColumnResize", [action]); } handleKeydown(event) { const validKeys = ['ArrowLeft', 'ArrowRight']; if (!validKeys.includes(event.key)) { return; } event.preventDefault(); event.stopPropagation(); const { column, siblingColumnKeys, onApplyTransientWidth, onGetAppliedWidth, onSetTransientColumnWidths, onResetTransientColumnWidths, onUpdateResizeDebt } = this.args; const { next: nextColumnKey } = siblingColumnKeys ?? {}; if (column === undefined || nextColumnKey === undefined || onApplyTransientWidth === undefined || onGetAppliedWidth === undefined || onSetTransientColumnWidths === undefined || onUpdateResizeDebt === undefined || onResetTransientColumnWidths === undefined) { return; } onSetTransientColumnWidths({ roundValues: true }); const startColumnAppliedWidth = onGetAppliedWidth(column.key); const startNextColumnAppliedWidth = onGetAppliedWidth(nextColumnKey); const startColumnPxWidth = Math.round(parsePixel(startColumnAppliedWidth) ?? 0); const startNextColumnPxWidth = Math.round(parsePixel(startNextColumnAppliedWidth) ?? 0); const deltaX = event.key === 'ArrowRight' ? KEYBOARD_RESIZE_STEP : -KEYBOARD_RESIZE_STEP; this._applyResizeDelta(deltaX, startColumnPxWidth, column, nextColumnKey, startNextColumnPxWidth); // ensure the resize handle remains visible during keyboard navigation. this._handleElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); // use a microtask to commit the final state after the render pass. queueMicrotask(() => { if (this._transientDelta !== 0) { onUpdateResizeDebt(this._transientDelta); } // reset transient values onApplyTransientWidth(column.key); onApplyTransientWidth(nextColumnKey); onResetTransientColumnWidths(); this._transientDelta = 0; this.onColumnResize(column.key, column.width); }); } static { n(this.prototype, "handleKeydown", [action]); } startResize(event) { if (event.button !== 0) { return; } event.preventDefault(); event.stopPropagation(); const { column, siblingColumnKeys, onGetAppliedWidth, onSetTransientColumnWidths } = this.args; if (column === undefined || onGetAppliedWidth === undefined || onSetTransientColumnWidths === undefined) { return; } const { next: nextColumnKey } = siblingColumnKeys ?? {}; onSetTransientColumnWidths({}); const startColumnAppliedWidth = onGetAppliedWidth(column.key); const startNextColumnAppliedWidth = nextColumnKey !== undefined ? onGetAppliedWidth(nextColumnKey) : undefined; const startColumnPxWidth = Math.round(parsePixel(startColumnAppliedWidth) ?? 0); const startNextColumnPxWidth = Math.round(parsePixel(startNextColumnAppliedWidth) ?? 0); this.resizing = { startX: event.clientX, startColumnPxWidth, startNextColumnPxWidth }; window.addEventListener('pointermove', this._boundResize); window.addEventListener('pointerup', this._boundStopResize); } static { n(this.prototype, "startResize", [action]); } _setColumnWidth(column, width) { const { onSetTransientColumnWidth } = this.args; if (column === undefined || onSetTransientColumnWidth === undefined) { return; } onSetTransientColumnWidth(column.key, `${width}px`); } _applyResizeDelta(deltaX, startColumnPxWidth, column, nextColumnKey, startNextColumnPxWidth) { const { onGetAppliedWidth, onGetColumnByKey, onSetTransientColumnWidth } = this.args; if (column === undefined || onGetAppliedWidth === undefined || onGetColumnByKey === undefined) { return; } const canResizeNeighbor = nextColumnKey !== undefined && startNextColumnPxWidth !== undefined; if (canResizeNeighbor) { const nextColumn = onGetColumnByKey(nextColumnKey); if (nextColumn === undefined) { return; } const effectiveDelta = calculateEffectiveDelta(deltaX, column, startColumnPxWidth, nextColumn, startNextColumnPxWidth); // set the width for the current column this._setColumnWidth(column, Math.round(startColumnPxWidth + effectiveDelta)); // the actual new column width may differ from the intended width due to min/max constraints. const columnAppliedWidth = onGetAppliedWidth(column.key); const actualNewColumnWidth = parsePixel(columnAppliedWidth) ?? startColumnPxWidth; const actualAppliedDelta = actualNewColumnWidth - startColumnPxWidth; // set the width for the next sibling column this._setColumnWidth(nextColumn, Math.round(startNextColumnPxWidth - actualAppliedDelta)); this._transientDelta = actualAppliedDelta; } else if (onSetTransientColumnWidth !== undefined) { onSetTransientColumnWidth(column.key, `${Math.round(startColumnPxWidth + deltaX)}px`); } } _resize(event) { this._lastPointerEvent = event; if (this._isUpdateQueued) { return; } this._isUpdateQueued = true; requestAnimationFrameWaiter(() => { if (this.resizing === null || this._lastPointerEvent === null) { this._isUpdateQueued = false; return; } const event = this._lastPointerEvent; event.preventDefault(); const { column, siblingColumnKeys } = this.args; const { next: nextColumnKey } = siblingColumnKeys ?? {}; const { startX, startColumnPxWidth, startNextColumnPxWidth } = this.resizing; const deltaX = event.clientX - startX; this._applyResizeDelta(deltaX, startColumnPxWidth, column, nextColumnKey, startNextColumnPxWidth // Width of next col at the start of the drag ); this._isUpdateQueued = false; }); } _stopResize() { const { column, onGetAppliedWidth, onResetTransientColumnWidths, onUpdateResizeDebt } = this.args; if (column === undefined || onGetAppliedWidth === undefined || onResetTransientColumnWidths === undefined || onUpdateResizeDebt === undefined) { return; } window.removeEventListener('pointermove', this._boundResize); window.removeEventListener('pointerup', this._boundStopResize); if (this._transientDelta !== 0) { onUpdateResizeDebt(this._transientDelta); } this._applyTransientWidths(); // reset the transient width onResetTransientColumnWidths(); // reset the resizing state this.resizing = null; this._transientDelta = 0; const appliedWidth = onGetAppliedWidth(column.key); this.onColumnResize(column.key, appliedWidth); } static { setComponentTemplate(precompileTemplate("{{!-- template-lint-disable no-pointer-down-event-binding --}}\n<div class={{this.classNames}} draggable=\"false\" role=\"slider\" aria-orientation=\"horizontal\" aria-valuenow={{this.currentWidthInPixels}} aria-valuemin={{this.minWidthInPixels}} aria-valuemax={{this.maxWidthInPixels}} tabindex=\"0\" aria-label={{hdsT \"hds.components.advanced-table.th-resize-handle.aria-label\" columnLabel=@column.label default=(concat \"Resize \" @column.label \" column\")}} {{this._registerHandleElement}} {{on \"pointerdown\" this.startResize}} {{on \"keydown\" this.handleKeydown}} {{style height=this.height}} ...attributes />", { strictMode: true, scope: () => ({ hdsT: HdsTHelper, concat, on, style }) }), this); } } /** * Copyright IBM Corp. 2021, 2025 * SPDX-License-Identifier: MPL-2.0 */ class HdsAdvancedTableThReorderDropTarget extends Component { static { g(this.prototype, "_dragSide", [tracked], function () { return null; }); } #_dragSide = (i(this, "_dragSide"), void 0); static { g(this.prototype, "_isUpdateQueued", [tracked], function () { return false; }); } #_isUpdateQueued = (i(this, "_isUpdateQueued"), void 0); _element; _registerElement = modifier(element => { this._element = element; }); // determines whether the drag event is occurring on the left or right side of the element _getDragSide(event) { const rect = this._element.getBoundingClientRect(); const mouseX = event.clientX; const elementMiddleX = rect.left + rect.width / 2; return mouseX < elementMiddleX ? HdsAdvancedTableColumnReorderSideValues.Left : HdsAdvancedTableColumnReorderSideValues.Right; } get isBeingDragged() { const { column, draggedColumnKey } = this.args; return column !== undefined && column.key === draggedColumnKey; } get isDraggingOver() { const { column, reorderHoveredColumnKey } = this.args; return column !== undefined && column.key === reorderHoveredColumnKey; } get classNames() { const { isFirstColumn, isLastColumn } = this.args; const classes = ['hds-advanced-table__th-reorder-drop-target']; if (isFirstColumn && !this.args.hasSelectableRows) { classes.push('hds-advanced-table__th-reorder-drop-target--is-first'); } else if (isLastColumn) { classes.push('hds-advanced-table__th-reorder-drop-target--is-last'); } if (this.isBeingDragged) { classes.push('hds-advanced-table__th-reorder-drop-target--is-being-dragged'); } else if (this.isDraggingOver && this._dragSide !== null) { classes.push(...['hds-advanced-table__th-reorder-drop-target--is-dragging-over', `hds-advanced-table__th-reorder-drop-target--is-dragging-over--${this._dragSide}`]); } return classes.join(' '); } get height() { const { tableHeight } = this.args; if (tableHeight === undefined) { return; } return `${tableHeight - BORDER_WIDTH * 2}px`; } handleDragOver(event) { event.preventDefault(); if (this._isUpdateQueued) { return; } this._isUpdateQueued = true; requestAnimationFrameWaiter(() => { const { column, draggedColumnKey, draggedColumnSiblingColumnKeys, onSetReorderHoveredColumnKey } = this.args; if (column === undefined || onSetReorderHoveredColumnKey === undefined) { return; } if (draggedColumnKey !== null) { if (this.isBeingDragged) { onSetReorderHoveredColumnKey(null); } else { onSetReorderHoveredColumnKey(column.key); const { next, previous } = draggedColumnSiblingColumnKeys ?? {}; const dragSide = this._getDragSide(event); if (column.key === previous && dragSide === HdsAdvancedTableColumnReorderSideValues.Left || column.key === next && dragSide === HdsAdvancedTableColumnReorderSideValues.Right || column.key !== previous && column.key !== next) { this._dragSide = dragSide; } } } this._isUpdateQueued = false; }); } static { n(this.prototype, "handleDragOver", [action]); } handleDrop(event) { event.preventDefault(); const { column, onReorderDrop, onSetReorderHoveredColumnKey } = this.args; const { _dragSide } = this; if (column === undefined || _dragSide === null || typeof onReorderDrop !== 'function' || typeof onSetReorderHoveredColumnKey !== 'function') { return; } onReorderDrop(column.key, _dragSide); this._dragSide = null; onSetReorderHoveredColumnKey(null); } static { n(this.prototype, "handleDrop", [action]); } static { setComponentTemplate(precompileTemplate("<div class={{this.classNames}} aria-hidden=\"true\" {{style height=this.height}} {{this._registerElement}} {{on \"dragover\" this.handleDragOver}} {{on \"drop\" this.handleDrop}} ...attributes />", { strictMode: true, scope: () => ({ style, on }) }), this); } } /** * Copyright IBM Corp. 2021, 2025 * SPDX-License-Identifier: MPL-2.0 */ const ALIGNMENTS = Object.values(HdsAdvancedTableHorizontalAlignmentValues); const DEFAULT_ALIGN = HdsAdvancedTableHorizontalAlignmentValues.Left; class HdsAdvancedTableTh extends Component { _labelId; static { g(this.prototype, "_element", [tracked]); } #_element = (i(this, "_element"), void 0); static { g(this.prototype, "_shouldTrapFocus", [tracked], function () { return false; }); } #_shouldTrapFocus = (i(this, "_shouldTrapFocus"), void 0); static { g(this.prototype, "_reorderHandleElement", [tracked]); } #_reorderHandleElement = (i(this, "_reorderHandleElement"), void 0); static { g(this.prototype, "_resizeHandleElement", [tracked]); } #_resizeHandleElement = (i(this, "_resizeHandleElement"), void 0); constructor(owner, args) { super(owner, args); const { newLabel, rowspan, colspan, isStickyColumn } = args; if (isStickyColumn) { assert('Cannot have custom rowspan or colspan if there are nested rows.', rowspan === undefined || colspan === undefined); } this._labelId = newLabel ?? guidFor(this); } get isSortable() { return this.args.column?.isSortable ?? false; } get isFirstColumn() { const { column, firstColumnKey } = this.args; return firstColumnKey !== undefined && column !== undefined && firstColumnKey === column.key; } get isFirstNonStickyColumn() { const { column, firstNonStickyColumnKey } = this.args; return firstNonStickyColumnKey !== undefined && column !== undefined && firstNonStickyColumnKey === column.key; } get isLastColumn() { const { column, lastColumnKey } = this.args; return lastColumnKey !== undefined && column !== undefined && lastColumnKey === column.key; } get tableHasColumnBeingDragged() { const { draggedColumnKey } = this.args; return draggedColumnKey != null; } get isColumnBeingDragged() { const { column, draggedColumnKey } = this.args; return draggedColumnKey != null && column !== undefined && draggedColumnKey === column.key; } get scope() { return this.args.scope ?? 'col'; } get role() { return this.scope === 'col' ? 'columnheader' : 'rowheader'; } get ariaSort() { switch (this.args.sortOrder) { case HdsAdvancedTableThSortOrderValues.Asc: return HdsAdvancedTableThSortOrderLabelValues.Asc; case HdsAdvancedTableThSortOrderValues.Desc: return HdsAdvancedTableThSortOrderLabelValues.Desc; default: // none is the default per the spec. return HdsAdvancedTableThSortOrderLabelValues.None; } } get align() { const { align = DEFAULT_ALIGN } = this.args; assert(`@align for "Hds::Table::Th" must be one of the following: ${ALIGNMENTS.join(', ')}; received: ${align}`, ALIGNMENTS.includes(align)); return align; } // rowspan and colspan have to return 'auto' if not defined because otherwise the style modifier sets grid-area: undefined on the cell, which breaks the grid styles get rowspan() { return this.args.rowspan !== undefined ? `span ${this.args.rowspan}` : 'auto'; } get colspan() { return this.args.colspan !== undefined ? `span ${this.args.colspan}` : 'auto'; } get paddingLeft() { return this.args.depth !== undefined ? `calc(${this.args.depth} * 32px + 16px)` : undefined; } get classNames() { const { isStickyColumn, isStickyColumnPinned } = this.args; const classes = ['hds-advanced-table__th']; if (this.isSortable) { classes.push('hds-advanced-table__th--sort'); } if (this.align) { classes.push(`hds-advanced-table__th--align-${this.align}`); } if (isStickyColumn) { classes.push('hds-advanced-table__th--is-sticky-column'); } if (isStickyColumn && isStickyColumnPinned) { classes.push('hds-advanced-table__th--is-sticky-column-pinned'); } if (this.isColumnBeingDragged) { classes.push('hds-advanced-table__th--is-being-dragged'); } return classes.join(' '); } get showDropTarget() { const { isStickyColumn } = this.args; return this.tableHasColumnBeingDragged === true && !isStickyColumn; } get showResizeHandle() { const { hasResizableColumns } = this.args; return hasResizableColumns === true && !this.isLastColumn && !this.tableHasColumnBeingDragged; } onFocusTrapDeactivate() { this._shouldTrapFocus = false; onFocusTrapDeactivate(this._element); } static { n(this.prototype, "onFocusTrapDeactivate", [action]); } enableFocusTrap() { this._shouldTrapFocus = true; } static { n(this.prototype, "enableFocusTrap", [action]); } getInitialFocus() { const cellFocusableElements = focusable(this._element); return cellFocusableElements[0]; } static { n(this.prototype, "getInitialFocus", [action]); } focusReorderHandle() { if (this._element === undefined) { return; } // focus the th element first (parent) to ensure the handle is visible this._element.focus({ preventScroll: true }); if (this._reorderHandleElement === undefined) { return; } // then focus the reorder handle element this._reorderHandleElement.focus(); } static { n(this.prototype, "focusReorderHandle", [action]); } applyTransientWidth(columnKey) { this.args.onApplyTransientWidth?.(columnKey); } static { n(this.prototype, "applyTransientWidth", [action]); } resetTransientColumnWidths() { this.args.onResetTransientColumnWidths?.(); } static { n(this.prototype, "resetTransientColumnWidths", [action]); } getAppliedWidth(columnKey) { return this.args.onGetAppliedWidth?.(columnKey); } static { n(this.prototype, "getAppliedWidth", [action]); } getColumnByKey(columnKey) { return this.args.onGetColumnByKey?.(columnKey); } static { n(this.prototype, "getColumnByKey", [action]); } setDraggedColumnKey(columnKey) { this.args.onSetDraggedColumnKey?.(columnKey); } static { n(this.prototype, "setDraggedColumnKey", [action]); } setTransientColumnWidth(columnKey, width) { const { column, onSetTransientColumnWidth } = this.args; if (column !== undefined && onSetTransientColumnWidth !== undefined) { onSetTransientColumnWidth(columnKey, width); } } static { n(this.prototype, "setTransientColumnWidth", [action]); } setTransientColumnWidths(options) { this.args.onSetTransientColumnWidths?.(options); } static { n(this.prototype, "setTransientColumnWidths", [action]); } stepColumn(step) { const { column, onStepColumn } = this.args; if (column !== undefined && onStepColumn !== undefined) { onStepColumn(column.key, step); } } static { n(this.prototype, "stepColumn", [action]); } updateResizeDebt(delta) { const { column, onUpdateResizeDebt } = this.args; if (column !== undefined && onUpdateResizeDebt !== undefined) { onUpdateResizeDebt(column.key, delta); } } static { n(this.prototype, "updateResizeDebt", [action]); } setElement(element) { // eslint-disable-next-line ember/no-runloop, ember/no-incorrect-calls-with-inline-anonymous-functions scheduleOnce('afterRender', () => { this._element = element; }); } static { n(this.prototype, "setElement", [action]); } _registerReorderHandleElement = modifier(element => { this._reorderHandleElement = element; }); _registerResizeHandleElement = modifier(element => { this._resizeHandleElement = element; }); _manageExpandButton = modifier(button => { const { didInsertExpandButton, willDestroyExpandButton } = this.args; if (typeof didInsertExpandButton === 'function') { didInsertExpandButton(button); } return () => { if (typeof willDestroyExpandButton === 'function') { willDestroyExpandButton(button); } }; }); static { setComponentTemplate(precompileTemplate("<div class={{this.classNames}} role={{this.role}} aria-sort={{this.ariaSort}} aria-rowspan={{@rowspan}} aria-colspan={{@colspan}} aria-describedby={{@parentId}} {{style grid-row=this.rowspan grid-column=this.colspan padding-left=this.paddingLeft}} {{hdsAdvancedTableCell handleEnableFocusTrap=this.enableFocusTrap shouldTrapFocus=this._shouldTrapFocus setCellElement=this.setElement}} {{focusTrap isActive=this._shouldTrapFocus focusTrapOptions=(hash onDeactivate=this.onFocusTrapDeactivate initialFocus=this.getInitialFocus clickOutsideDeactivates=true)}} {{@compositeItem disabled=@isCompositeItemDisabled}} {{hdsScrollIntoViewOnFocus options=(hash block=\"center\" inline=\"center\")}} ...attributes>\n <HdsLayoutFlex @justify=\"space-between\" @align=\"center\" @gap=\"8\">\n {{#if @column.isVisuallyHidden}}\n <span class=\"sr-only\">{{yield}}</span>\n {{else}}\n {{#if (and @isExpandable (not this.isSortable))}}\n <HdsAdvancedTableThButtonExpand @labelId={{this._labelId}} @onToggle={{@onClickToggle}} @isExpanded={{@isExpanded}} @isExpandAll={{@hasExpandAllButton}} {{this._manageExpandButton}} />\n {{/if}}\n <div class=\"hds-advanced-table__th-content\">\n <span id={{this._labelId}} class=\"hds-advanced-table__th-content-text hds-typography-body-200 hds-font-weight-semibold\">\n {{yield}}\n </span>\n {{#if @tooltip}}\n <HdsAdvancedTableThButtonTooltip @tooltip={{@tooltip}} @labelId={{this._labelId}} />\n {{/if}}\n </div>\n {{#if this.isSortable}}\n <HdsAdvancedTableThButtonSort @sortOrder={{@sortOrder}} @onClick={{@onClickSort}} @labelId={{this._labelId}} />\n {{/if}}\n {{#if @column}}\n <HdsAdvancedTableThContextMenu @column={{@column}} @hasReorderableColumns={{@hasReorderableColumns}} @hasResizableColumns={{@hasResizableColumns}} @isFirstColumn={{this.isFirstColumn}} @isFirstNonStickyColumn={{this.isFirstNonStickyColumn}} @isLastColumn={{this.isLastColumn}} @isStickyColumn={{@isStickyColumn}} @reorderHandleElement={{this._reorderHandleElement}} @resizeHandleElement={{this._resizeHandleElement}} @onColumnResize={{@onColumnResize}} @onFocusReorderHandle={{this.focusReorderHandle}} @onMoveColumnToTerminalPosition={{@onMoveColumnToTerminalPosition}} @onPinFirstColumn={{@onPinFirstColumn}} @onRestoreColumnWidth={{@onRestoreColumnWidth}} />\n {{#if (and @hasReorderableColumns (not @isStickyColumn))}}\n <HdsAdvancedTableThReorderHandle @column={{@column}} @tableHeight={{@tableHeight}} @thElement={{this._element}} @onFocusReorderHandle={{this.focusReorderHandle}} @onSetDraggedColumnKey={{this.setDraggedColumnKey}} @onStepColumn={{this.stepColumn}} {{this._registerReorderHandleElement}} />\n {{/if}}\n {{#if this.showResizeHandle}}\n <HdsAdvancedTableThResizeHandle @column={{@column}} @siblingColumnKeys={{@siblingColumnKeys}} @tableHeight={{@tableHeight}} @onApplyTransientWidth={{this.applyTransientWidth}} @onColumnResize={{@onColumnResize}} @onGetAppliedWidth={{this.getAppliedWidth}} @onGetColumnByKey={{this.getColumnByKey}} @onSetTransientColumnWidth={{this.setTransientColumnWidth}} @onSetTransientColumnWidths={{this.setTransientColumnWidths}} @onResetTransientColumnWidths={{this.resetTransientColumnWidths}} @onUpdateResizeDebt={{this.updateResizeDebt}} {{this._registerResizeHandleElement}} />\n {{/if}}\n {{/if}}\n {{/if}}\n </HdsLayoutFlex>\n {{#if this.showDropTarget}}\n <HdsAdvancedTableThReorderDropTarget @column={{@column}} @draggedColumnKey={{@draggedColumnKey}} @hasSelectableRows={{@hasSelectableRows}} @isFirstColumn={{this.isFirstColumn}} @isLastColumn={{this.isLastColumn}} @reorderHoveredColumnKey={{@reorderHoveredColumnKey}} @draggedColumnSiblingColumnKeys={{@draggedColumnSiblingColumnKeys}} @tableHeight={{@tableHeight}} @onReorderDrop={{@onReorderDrop}} @onSetReorderHoveredColumnKey={{@onSetReorderHoveredColumnKey}} />\n {{/if}}\n</div>", { strictMode: true, scope: () => ({ style, hdsAdvancedTableCell: HdsAdvancedTableCellModifier, focusTrap, hash, hdsScrollIntoViewOnFocus, HdsLayoutFlex, and, not, HdsAdvancedTableThButtonExpand, HdsAdvancedTableThButtonTooltip, HdsAdvancedTableThButtonSort, HdsAdvancedTableThContextMenu, HdsAdvancedTableThReorderHandle, HdsAdvancedTableThResizeHandle, HdsAdvancedTableThReorderDropTarget }) }), this); } } /** * Copyright IBM Corp. 2021, 2025 * SPDX-License-Identifier: MPL-2.0 */ class HdsAdvancedTableThSelectable extends Component { static { g(this.prototype, "hdsIntl", [service]); } #hdsIntl = (i(this, "hdsIntl"), void 0); static { g(this.prototype, "_isSelected", [tracked], function () { return this.args.isSelected ?? false; }); } #_isSelected = (i(this, "_isSelected"), void 0); _guid = guidFor(this); _checkboxId = `checkbox-${this._guid}`; _labelId = `label-${this._guid}`; get isSortable() { return this.args.onClickSortBySelected !== undefined; } get ariaLabel() { const { selectionAriaLabelSuffix = 'row' } = this.args; const defaultString = `Select ${selectionAriaLabelSuffix}`; return this.hdsIntl.t('hds.components.advanced-table.th-selectable.aria-label', { default: defaultString, suffix: selectionAriaLabelSuffix }); } get ariaSort() { switch (this.args.sortBySelectedOrder) { case HdsAdvancedTableThSortOrderValues.Asc: return HdsAdvancedTableThSortOrderLabelValues.Asc; case HdsAdvancedTableThSortOrderValues.Desc: return HdsAdvancedTableThSortOrderLabelValues.Desc; default: // none is the default per the spec. return HdsAdvancedTableThSortOrderLabelValues.None; } } _manageCheckbox = modifier(checkbox => { const { didInsert, willDestroy } = this.args; if (typeof didInsert === 'function') { didInsert(checkbox, this.args.selectionKey); } return () => { if (typeof willDestroy === 'function') { willDestroy(this.args.selectionKey); } }; }); onSelectionChange(event) { // Assert event.target as HdsFormCheckboxBaseSignature['Element'] to access the 'checked' property const target = event.target; this._isSelected = target.checked; const { onSelectionChange } = this.args; if (typeof onSelectionChange === 'function') { onSelectionChange(target, this.args.selectionKey); } } static { n(this.prototype, "onSelectionChange", [action]); } static { setComponentTemplate(precompileTemplate("<HdsAdvancedTableTh class=\"hds-advanced-table__th--is-selectable\" aria-sort={{if this.isSortable this.ariaSort}} @scope={{@selectionScope}} @isStickyColumn={{@isStickyColumn}} @isStickyColumnPinned={{@isStickyColumnPinned}} @compositeItem={{@compositeItem}} @isCompositeItemDisabled={{@isCompositeItemDisabled}} ...attributes>\n <div class=\"hds-advanced-table__th-content\">\n <HdsFormCheckboxBase id={{this._checkboxId}} class=\"hds-advanced-table__checkbox\" checked={{@isSelected}} aria-label={{this.ariaLabel}} {{this._manageCheckbox}} {{on \"change\" this.onSelectionChange}} />\n {{#if this.isSortable}}\n <HdsAdvancedTableThButtonSort @sortOrder={{@sortBySelectedOrder}} @onClick={{@onClickSortBySelected}} @labelId={{this._labelId}} />\n {{/if}}\n </div>\n</HdsAdvancedTableTh>", { strictMode: true, scope: () => ({ HdsAdvancedTableTh, HdsFormCheckboxBase, on, HdsAdvancedTableThButtonSort }) }), this); } } /** * Copyright IBM Corp. 2021, 2025 * SPDX-License-Identifier: MPL-2.0 */ /* * NOTE: There is currently an issue with `WithBoundArgs` or Glint that causes a typing error where @selectionKey is set as always required. * * Until this is fixed, we are holding off on doing a union with the SelectableHdsAdvancedTableTrArgs */ // Extended interface for selectable rows // export interface SelectableHdsAdvancedTableTrArgs // extends BaseHdsAdvancedTableTrSignature { // Args: BaseHdsAdvancedTableTrSignature['Args'] & { // isSelectable: true; // selectionScope?: HdsAdvancedTableScopeValues.Row; // selectionKey: string; // Now required for selectable rows // }; // } // Union type to combine both possible states // | SelectableHdsAdvancedTableTrArgs; class HdsAdvancedTableTr extends Component { get selectionKey() { if (this.args.isSelectable && this.args.selectionScope === 'row') { assert(`@selectionKey must be defined on Table::Tr or B.Tr when @isSelectable is true`, this.args.selectionKey); return this.args.selectionKey; } return undefined; } get classNames() { const { depth, isLastRow, isParentRow, displayRow } = this.args; const classes = ['hds-advanced-table__tr']; if (depth && depth > 0) { classes.push('hds-advanced-table__tr--nested'); } if (isParentRow) { classes.push('hds-advanced-table__tr--parent-row'); } if (displayRow === false) { classes.push('hds-advanced-table__tr--hidden'); } if (isLastRow) { classes.push('hds-advanced-table__tr--last-row'); } return classes.join(' '); } get cells() { const { columnOrder, data } = this.args; if (columnOrder === undefined || data == null) { return []; } return columnOrder.map(columnKey => ({ columnKey, content: data[columnKey] })); } get orderedCells() { const { columnOrder, data, hasReorderableColumns } = this.args; if (columnOrder === undefined || data === undefined) { return this.cells; } if (hasReorderableColumns) { return columnOrder.reduce((acc, key) => { const cell = this.cells.find(cell => cell.columnKey === key); if (cell !== undefined) { acc.push(cell); } return acc; }, []); } else { return this.cells; } } static { setComponentTemplate(precompileTemplate("<div class={{this.classNames}} role=\"row\" {{@compositeGroup}} ...attributes>\n {{#if @isSelectable}}\n <HdsAdvancedTableThSelectable role={{if (eq @selectionScope \"row\") \"gridcell\" \"columnheader\"}} @compositeItem={{@compositeItem}} @isCompositeItemDisabled={{@isCompositeItemDisabled}} @isSelected={{@isSelected}} @selectionScope={{@selectionScope}} @selectionKey={{this.selectionKey}} @selectionAriaLabelSuffix={{@selectionAriaLabelSuffix}} @sortBySelectedOrder={{@sortBySelectedOrder}} @didInsert={{@didInsert}} @willDestroy={{@willDestroy}} @onClickSortBySelected={{@onClickSortBySelected}} @onSelectionChange={{@onSelectionChange}} @isStickyColumn={{@hasStickyColumn}} @isStickyColumnPinned={{@isStickyColumnPinned}} />\n {{/if}}\n\n {{yield (hash orderedCells=this.orderedCells)}}\n</div>", { strictMode: true, scope: () => ({ HdsAdvancedTableThSelectable, eq, hash }) }), this); } } /** * Copyright IBM Corp. 2021, 2025 * SPDX-License-Identifier: MPL-2.0 */ const DENSITIES = Object.values(HdsAdvancedTableDensityValues); const DEFAULT_DENSITY = HdsAdvancedTableDensityValues.Medium; const VALIGNMENTS = Object.values(HdsAdvancedTableVerticalAlignmentValues); const DEFAULT_VALIGN = HdsAdvancedTableVerticalAlignmentValues.Top; const BORDER_WIDTH = 1; const DEFAULT_SCROLL_DIMENSIONS = { bottom: '0px', height: '0px', left: '0px', right: '0px', width: '0px' }; const REORDER_EDGE_SCROLL_TRIGGER_PX = 48; const REORDER_EDGE_SCROLL_STEP_PX = 16; const getScrollIndicatorDimensions = (scrollWrapper, theadElement, hasStickyFirstColumn, isStickyColumnPinned) => { const horizontalScrollBarHeight = scrollWrapper.offsetHeight - scrollWrapper.clientHeight; const verticalScrollBarWidth = scrollWrapper.offsetWidth - scrollWrapper.clientWidth; let leftOffset = 0; if (hasStickyFirstColumn) { const stickyColumnHeaders = theadElement.querySelectorAll('.hds-advanced-table__th--is-sticky-column'); stickyColumnHeaders?.forEach(el => { // querySelectorAll returns Elements, which don't have offsetWidth // need to use offsetWidth to account for the cell borders const elAsHTMLElement = el; leftOffset += elAsHTMLElement.offsetWidth; }); // offsets the left: -1px position if there are multiple sticky columns or the first column has a fixed pixel width if (stickyColumnHeaders.length > 1) { leftOffset -= 1; } // offsets the left: -1px position if the sticky column is already pinned when the scroll indicator is calculated if (isStickyColumnPinned) { leftOffset -= 1; } } return { bottom: `${horizontalScrollBarHeight}px`, height: `${scrollWrapper.offsetHeight - horizontalScrollBarHeight}px`, left: `${leftOffset}px`, right: `${verticalScrollBarWidth}px`, width: `${scrollWrapper.offsetWidth - verticalScrollBarWidth}px` }; }; const getStickyColumnLeftOffset = (theadElement, hasRowSelection, isStickyColumnPinned) => { // if there is no select checkbox column, the sticky column is all the way to the left if (!hasRowSelection) return '0px'; const selectableCell = theadElement.querySelector('.hds-advanced-table__th--is-selectable'); let leftOffset = selectableCell?.offsetWidth ?? 0; // if the sticky column is pinned when the offset is calculated, we need to account for the increased width of the border if (isStickyColumnPinned && leftOffset > 0) { leftOffset -= 2; } return `${leftOffset}px`; }; class HdsAdvancedTable extends Component { static { g(this.prototype, "hdsIntl", [service]); } #hdsIntl = (i(this, "hdsIntl"), void 0); static { g(this.prototype, "_selectAllCheckbox", [tracked], function () { return undefined; }); } #_selectAllCheckbox = (i(this, "_selectAllCheckbox"), void 0); static { g(this.prototype, "_isSelectAllCheckboxSelected", [tracked], function () { return undefined; }); } #_isSelectAllCheckboxSelected = (i(this, "_isSelectAllCheckboxSelected"), void 0); static { g(this.prototype, "_tableHeight", [tracked], function () { return 0; }); } #_tableHeight = (i(this, "_tableHeight"), void 0); _selectableRows = []; _captionId = 'caption-' + guidFor(this); _scrollHandler; _dragOverHandler; _resizeObserver; _theadElement; _scrollWrapperElement; static { g(this.prototype, "scrollIndicatorDimensions", [tracked], function () { return DEFAULT_SCROLL_DIMENSIONS; }); } #scrollIndicatorDimensions = (i(this, "scrollIndicatorDimensions"), void 0); static { g(this.prototype, "isStickyColumnPinned", [tracked], function () { return false; }); } #isStickyColumnPinned = (i(this, "isStickyColumnPinned"), void 0); static { g(this.prototype, "isStickyHeaderPinned", [tracked], function () { return false; }); } #isStickyHeaderPinned = (i(this, "isStickyHeaderPinned"), void 0); static { g(this.prototype, "hasPinnedFirstColumn", [tracked], function () { return undefined; }); } #hasPinnedFirstColumn = (i(this, "hasPinnedFirstColumn"), void 0); static { g(this.prototype, "reorderedMessageText", [tracked], function () { return ''; }); } #reorderedMessageText = (i(this, "reorderedMessageText"), void 0); static { g(this.prototype, "showScrollIndicatorLeft", [tracked], function () { return false; }); } #showScrollIndicatorLeft = (i(this, "showScrollIndicatorLeft"), void 0); static { g(this.prototype, "showScrollIndicatorRight", [tracked], function () { return false; }); } #showScrollIndicatorRight = (i(this, "showScrollIndicatorRight"), void 0); static { g(this.prototype, "showScrollIndicatorTop", [tracked], function () { return false; }); } #showScrollIndicatorTop = (i(this, "showScrollIndicatorTop"), void 0); static { g(this.prototype, "showScrollIndicatorBottom", [tracked], function () { return false; }); } #showScrollIndicatorBottom = (i(this, "showScrollIndicatorBottom"), void 0); static { g(this.prototype, "stickyColumnOffset", [tracked], function () { return '0px'; }); } #stickyColumnOffset = (i(this, "stickyColumnOffset"), void 0); static { g(this.prototype, "currentSortBy", [tracked]); } #currentSortBy = (i(this, "currentSortBy"), void 0); // sorting properties static { g(this.prototype, "currentSortOrder", [tracked], function () { return HdsAdvancedTableThSortOrderValues.Asc; }); } #currentSortOrder = (i(this, "currentSortOrder"), void 0); // row expansion properties expandedRowIds = new TrackedSet(); _setUpScrollWrapper = modifier(element => { this._scrollWrapperElement = element; const updateHorizontalScrollIndicators = () => { this.showScrollIndicatorRight = element.clientWidth < element.scrollWidth; }; this._scrollHandler = () => { this._updateScrollIndicators(element); }; this._dragOverHandler = event => { if (this.args.hasReorderableColumns !== true) { return; } const canScrollHorizontally = element.scrollWidth > element.clientWidth; if (!canScrollHorizontally) { return; } const firstReorderDropTarget = element.querySelector('.hds-advanced-table__th-reorder-drop-target'); if (firstReorderDropTarget === null) { return; } const stickyColumnHeaders = element.querySelectorAll('.hds-advanced-table__th--is-sticky-column'); const lastStickyColumnHeader = stickyColumnHeaders[stickyColumnHeaders.length - 1]; const leftVisibleEdge = lastStickyColumnHeader?.getBoundingClientRect().right ?? firstReorderDropTarget.getBoundingClientRect().left; const leftScrollTrigger = lastStickyColumnHeader !== undefined ? leftVisibleEdge : leftVisibleEdge + REORDER_EDGE_SCROLL_TRIGGER_PX; const { right } = element.getBoundingClientRect(); const rightScrollTrigger = right - REORDER_EDGE_SCROLL_TRIGGER_PX; if (event.clientX <= leftScrollTrigger) { element.scrollBy({ left: -REORDER_EDGE_SCROLL_STEP_PX }); } else if (event.clientX >= rightScrollTrigger) { element.scrollBy({ left: REORDER_EDGE_SCROLL_STEP_PX }); } }; element.addEventListener('scroll', this._scrollHandler); element.addEventListener('dragover', this._dragOverHandler); const updateMeasurements = () => { const { isSelectable = false } = this.args; const newTableHeight = element.offsetHeight; const newDimensions = getScrollIndicatorDimensions(element, this._theadElement, this.hasStickyFirstColumn ? true : false, this.isStickyColumnPinned); const setUpdatedMeasurements = () => { if (this.isDestroying || this.isDestroyed) { return; } const isSameBottom = this.scrollIndicatorDimensions.bottom === newDimensions.bottom; const isSameHeight = this.scrollIndicatorDimensions.height === newDimensions.height; const isSameLeft = this.scrollIndicatorDimensions.left === newDimensions.left; const isSameRight = this.scrollIndicatorDimensions.right === newDimensions.right; if (isSameBottom && isSameHeight && isSameLeft && isSameRight && this._tableHeight === newTableHeight) { return; } this._tableHeight = newTableHeight; this.scrollIndicatorDimensions = newDimensions; if (this.hasStickyFirstColumn) { this.stickyColumnOffset = getStickyColumnLeftOffset(this._theadElement, isSelectable, this.isStickyColumnPinned); } }; window.requestAnimationFrame(setUpdatedMeasurements); }; this._resizeObserver = new ResizeObserver(entries => { entries.forEach(() => { updateMeasurements(); updateHorizontalScrollIndicators(); }); }); this._resizeObserver.observe(element); updateMeasurements(); // on render check if should show right scroll indicator updateHorizontalScrollIndicators(); // on render check if should show bottom scroll indicator if (element.clientHeight < element.scrollHeight) { this.showScrollIndicatorBottom = true; } return () => { element.removeEventListener('scroll', this._scrollHandler); element.removeEventListener('dragover', this._dragOverHandler); this._resizeObserver.disconnect(); }; }); _setUpThead = modifier(element => { this._theadElement = element; }); _syncSortArgs = modifier(() => { const { sortBy, sortOrder } = this.args; if (sortBy !== undefined) { this.currentSortBy = sortBy; } if (sortOrder !== undefined) { this.currentSortOrder = sortOrder; } }); constructor(owner, args) { super(owner, args); const { hasStickyFirstColumn, model } = args; this._runAssertions(); this._initializeExpandedRows(model); if (hasStickyFirstColumn) { this.hasPinnedFirstColumn = true; } } get childrenKey() { return this.args.childrenKey ?? 'children'; } get hasRowsWithChildren() { const { model } = this.args; return model.some(record => { const children = record[this.childrenKey]; return Array.isArray(children) && children.length > 0; }); } get expandableRowIds() { const { model } = this.args; const ids = []; const collect = items => { items.forEach(item => { const children = item[this.childrenKey]; if (Array.isArray(children) && children.length > 0) { ids.push(guidFor(item)); collect(children); } }); }; collect(model); return ids; } static { n(this.prototype, "expandableRowIds", [cached]); } get isAllExpanded() { if (this.expandableRowIds.length === 0) { return false; } return this.expandableRowIds.every(id => this.expandedRowIds.has(id)); } get sortCriteria() { const { columns } = this.args; const currentColumn = columns.find(column => column.key === this.currentSortBy); if (currentColumn?.sortingFunction && typeof currentColumn.sortingFunction === 'function') { return currentColumn.sortingFunction; } else { return `${this.currentSortBy}:${this.currentSortOrder}`; } } get isEmpty() { const { model } = this.args; return model.length === 0; } get identityKey() { // we have to provide a way for the consumer to pass undefined because Ember tries to interpret undefined as missing an arg and therefore falls back to the default if (this.args.identityKey === 'none') { return undefined; } else { return this.args.identityKey ?? '@identity'; } } get hasStickyFirstColumn() { // The user-controll