diff --git a/packages/nimble-components/src/table-column/base/models/column-internals.ts b/packages/nimble-components/src/table-column/base/models/column-internals.ts index ef1488db54..2452ac9c4e 100644 --- a/packages/nimble-components/src/table-column/base/models/column-internals.ts +++ b/packages/nimble-components/src/table-column/base/models/column-internals.ts @@ -135,6 +135,12 @@ export class ColumnInternals { @observable public minPixelWidth = defaultMinPixelWidth; + /** + * Whether or not resizing the column has been disabled. + */ + @observable + public resizingDisabled = false; + /** * @internal Do not write to this value directly. It is used by the Table in order to store * the resolved value of the fractionalWidth after updates programmatic or interactive updates. diff --git a/packages/nimble-components/src/table-column/icon/index.ts b/packages/nimble-components/src/table-column/icon/index.ts index 18f3631aa9..4010a3367d 100644 --- a/packages/nimble-components/src/table-column/icon/index.ts +++ b/packages/nimble-components/src/table-column/icon/index.ts @@ -30,13 +30,18 @@ declare global { * Table column that maps values to icons / spinners */ export class TableColumnIcon extends mixinGroupableColumnAPI( - mixinFractionalWidthColumnAPI( - TableColumnEnumBase< - TableColumnEnumColumnConfig, - TableColumnIconValidator - > - ) + TableColumnEnumBase< + TableColumnEnumColumnConfig, + TableColumnIconValidator + > ) { + public constructor() { + super(); + this.columnInternals.resizingDisabled = true; + this.columnInternals.pixelWidth = 32; + this.columnInternals.minPixelWidth = 0; + } + public override createValidator(): TableColumnIconValidator { return new TableColumnIconValidator(this.columnInternals); } diff --git a/packages/nimble-components/src/table/components/header/index.ts b/packages/nimble-components/src/table/components/header/index.ts index 562df7edb2..cdc728af88 100644 --- a/packages/nimble-components/src/table/components/header/index.ts +++ b/packages/nimble-components/src/table/components/header/index.ts @@ -21,6 +21,9 @@ export class TableHeader extends FoundationElement { @attr({ attribute: 'first-sorted-column', mode: 'boolean' }) public firstSortedColumn = false; + @attr({ attribute: 'decorations-hidden', mode: 'boolean' }) + public decorationsHidden = false; + @observable public isGrouped = false; diff --git a/packages/nimble-components/src/table/components/header/template.ts b/packages/nimble-components/src/table/components/header/template.ts index a8c8fd5e80..bce32269ab 100644 --- a/packages/nimble-components/src/table/components/header/template.ts +++ b/packages/nimble-components/src/table/components/header/template.ts @@ -24,6 +24,7 @@ export const template = html` class="sort-indicator" title="${x => tableColumnHeaderSortedAscendingLabel.getValueFor(x)}" aria-hidden="true" + ?hidden="${x => x.decorationsHidden}" > `)} ${when(x => x.sortDirection === TableColumnSortDirection.descending, html` @@ -31,6 +32,7 @@ export const template = html` class="sort-indicator" title="${x => tableColumnHeaderSortedDescendingLabel.getValueFor(x)}" aria-hidden="true" + ?hidden="${x => x.decorationsHidden}" > `)} ${when(x => x.isGrouped, html` @@ -39,6 +41,7 @@ export const template = html` title="${x => tableColumnHeaderGroupedLabel.getValueFor(x)}" role="img" aria-label="${x => tableColumnHeaderGroupedLabel.getValueFor(x)}" + ?hidden="${x => x.decorationsHidden}" > `)} diff --git a/packages/nimble-components/src/table/models/table-layout-manager.ts b/packages/nimble-components/src/table/models/table-layout-manager.ts index b6ff323382..a69a3124f5 100644 --- a/packages/nimble-components/src/table/models/table-layout-manager.ts +++ b/packages/nimble-components/src/table/models/table-layout-manager.ts @@ -27,7 +27,8 @@ export class TableLayoutManager { private initialColumnWidths: { initalColumnFractionalWidth: number, initialPixelWidth: number, - minPixelWidth: number + minPixelWidth: number, + resizingDisabled: boolean }[] = []; public constructor(private readonly table: Table) {} @@ -80,6 +81,14 @@ export class TableLayoutManager { document.addEventListener('mouseup', this.onDividerMouseUp); } + public hasResizableColumnToLeft(columnIndex: number, visibleColumns = this.visibleColumns): boolean { + return this.getFirstLeftResizableColumnIndex(columnIndex, visibleColumns) !== -1; + } + + public hasResizableColumnToRight(columnIndex: number, visibleColumns = this.visibleColumns): boolean { + return this.getFirstRightResizableColumnIndex(columnIndex, visibleColumns) !== -1; + } + private readonly onDividerMouseMove = (event: Event): void => { const mouseEvent = event as MouseEvent; for (let i = 0; i < this.visibleColumns.length; i++) { @@ -135,27 +144,60 @@ export class TableLayoutManager { let availableSpace = 0; if (requestedResizeAmount > 0) { // size right - return requestedResizeAmount; + return this.hasResizableColumnToLeft(this.leftColumnIndex!) ? requestedResizeAmount : 0; } // size left let currentIndex = this.leftColumnIndex!; while (currentIndex >= 0) { const columnInitialWidths = this.initialColumnWidths[currentIndex]!; - availableSpace - += columnInitialWidths.initialPixelWidth - - columnInitialWidths.minPixelWidth; + if (!columnInitialWidths.resizingDisabled) { + availableSpace + += columnInitialWidths.initialPixelWidth + - columnInitialWidths.minPixelWidth; + } currentIndex -= 1; } return Math.max(requestedResizeAmount, -availableSpace); } + private getFirstLeftResizableColumnIndex(columnIndex: number, visibleColumns = this.visibleColumns): number { + for (let i = columnIndex; i >= 0; i--) { + const column = visibleColumns[i]; + if (!column) { + return -1; + } + if (!column.columnInternals.resizingDisabled) { + return i; + } + } + return -1; + } + + private getFirstRightResizableColumnIndex(columnIndex: number, visibleColumns = this.visibleColumns): number { + for (let i = columnIndex; i < visibleColumns.length; i++) { + const column = visibleColumns[i]; + if (!column) { + return -1; + } + if (!column.columnInternals.resizingDisabled) { + return i; + } + } + return -1; + } + private performCascadeSizeLeft( leftColumnIndex: number, delta: number ): void { + const firstLeftResizableColumn = this.getFirstLeftResizableColumnIndex(leftColumnIndex); + if (firstLeftResizableColumn === -1) { + return; + } + let currentDelta = delta; - const leftColumnInitialWidths = this.initialColumnWidths[leftColumnIndex]!; + const leftColumnInitialWidths = this.initialColumnWidths[firstLeftResizableColumn]!; const allowedDelta = delta < 0 ? Math.max( leftColumnInitialWidths.minPixelWidth @@ -164,12 +206,12 @@ export class TableLayoutManager { ) : delta; const actualDelta = allowedDelta; - const leftColumn = this.visibleColumns[leftColumnIndex]!; + const leftColumn = this.visibleColumns[firstLeftResizableColumn]!; leftColumn.columnInternals.currentPixelWidth! += actualDelta; - if (actualDelta > currentDelta && leftColumnIndex > 0 && delta < 0) { + if (actualDelta > currentDelta && firstLeftResizableColumn > 0 && delta < 0) { currentDelta -= allowedDelta; - this.performCascadeSizeLeft(leftColumnIndex - 1, currentDelta); + this.performCascadeSizeLeft(firstLeftResizableColumn - 1, currentDelta); } } @@ -177,26 +219,36 @@ export class TableLayoutManager { rightColumnIndex: number, delta: number ): void { + const firstRightResizableColumn = this.getFirstRightResizableColumnIndex(rightColumnIndex); + if (firstRightResizableColumn === -1) { + return; + } + let currentDelta = delta; - const rightColumnInitialWidths = this.initialColumnWidths[rightColumnIndex]!; - const allowedDelta = delta > 0 - ? Math.min( + const rightColumnInitialWidths = this.initialColumnWidths[firstRightResizableColumn]!; + let allowedDelta: number; + if (rightColumnInitialWidths.resizingDisabled) { + allowedDelta = 0; + } else if (delta > 0) { + allowedDelta = Math.min( rightColumnInitialWidths.initialPixelWidth - rightColumnInitialWidths.minPixelWidth, currentDelta - ) - : delta; + ); + } else { + allowedDelta = delta; + } const actualDelta = allowedDelta; - const rightColumn = this.visibleColumns[rightColumnIndex]!; + const rightColumn = this.visibleColumns[firstRightResizableColumn]!; rightColumn.columnInternals.currentPixelWidth! -= actualDelta; if ( actualDelta < currentDelta - && rightColumnIndex < this.visibleColumns.length - 1 + && firstRightResizableColumn < this.visibleColumns.length - 1 && delta > 0 ) { currentDelta -= allowedDelta; - this.performCascadeSizeRight(rightColumnIndex + 1, currentDelta); + this.performCascadeSizeRight(firstRightResizableColumn + 1, currentDelta); } } @@ -216,7 +268,8 @@ export class TableLayoutManager { initalColumnFractionalWidth: column.columnInternals.currentFractionalWidth, initialPixelWidth: column.columnInternals.currentPixelWidth!, - minPixelWidth: column.columnInternals.minPixelWidth + minPixelWidth: column.columnInternals.minPixelWidth, + resizingDisabled: column.columnInternals.resizingDisabled }); } } diff --git a/packages/nimble-components/src/table/styles.ts b/packages/nimble-components/src/table/styles.ts index af18cb9d02..010c7ef81d 100644 --- a/packages/nimble-components/src/table/styles.ts +++ b/packages/nimble-components/src/table/styles.ts @@ -131,13 +131,13 @@ export const styles = css` ); } - .column-divider.active { + .column-divider.active.resizable { display: block; z-index: ${ZIndexLevels.zIndex1}; } - .header-container:hover .column-divider.left, - .header-container:hover .column-divider.right { + .header-container:hover .column-divider.left.resizable, + .header-container:hover .column-divider.right.resizable { display: block; z-index: ${ZIndexLevels.zIndex1}; } diff --git a/packages/nimble-components/src/table/template.ts b/packages/nimble-components/src/table/template.ts index 181e423237..0c2ee472a8 100644 --- a/packages/nimble-components/src/table/template.ts +++ b/packages/nimble-components/src/table/template.ts @@ -83,22 +83,27 @@ export const template = html` ${repeat(x => x.visibleColumns, html`
${when((_, c) => c.index > 0, html` -
+
`)} <${tableHeaderTag} class="header" sort-direction="${x => (typeof x.columnInternals.currentSortIndex === 'number' ? x.columnInternals.currentSortDirection : TableColumnSortDirection.none)}" ?first-sorted-column="${(x, c) => x === c.parent.firstSortedColumn}" + ?decorations-hidden="${x => x.columnInternals.resizingDisabled}" @click="${(x, c) => c.parent.toggleColumnSort(x, (c.event as MouseEvent).shiftKey)}" :isGrouped=${x => (typeof x.columnInternals.groupIndex === 'number' && !x.columnInternals.groupingDisabled)} > ${when((_, c) => c.index < c.length - 1, html` -
+
`)}