diff --git a/change/@ni-nimble-components-248b2e34-5be4-492b-8512-1e5226e10ca4.json b/change/@ni-nimble-components-248b2e34-5be4-492b-8512-1e5226e10ca4.json new file mode 100644 index 0000000000..23406f6c91 --- /dev/null +++ b/change/@ni-nimble-components-248b2e34-5be4-492b-8512-1e5226e10ca4.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add configuration to make the icon column fixed width", + "packageName": "@ni/nimble-components", + "email": "20542556+mollykreis@users.noreply.github.com", + "dependentChangeType": "patch" +} 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 5515566c39..63de74f3b5 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 @@ -140,6 +140,19 @@ export class ColumnInternals< @observable public minPixelWidth = defaultMinPixelWidth; + /** + * Whether or not resizing the column has been disabled. + */ + @observable + public resizingDisabled = false; + + /** + * Whether or not the grouping and sorting indicators should be hidden in the column header + * when the column is grouped or sorted. + */ + @observable + public hideHeaderIndicators = 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/base/types.ts b/packages/nimble-components/src/table-column/base/types.ts index 41a9d5d7a3..c7ebb215bc 100644 --- a/packages/nimble-components/src/table-column/base/types.ts +++ b/packages/nimble-components/src/table-column/base/types.ts @@ -44,19 +44,22 @@ export type TableColumnSortOperation = // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface TableColumnValidity extends ValidityObject {} -const groupIconSize = 16; -const sortIconSize = 16; -const spacing = 8; -const menuDropdownSize = 24; +const columnIconSize = 16; // `iconSize` token +const columnSpacing = 8; // `mediumPadding` token +const menuDropdownSize = 24; // `controlSlimHeight` token const oneCharPlusEllipsisSize = 21; -export const defaultMinPixelWidth = spacing +export const defaultMinPixelWidth = columnSpacing // left cell padding + oneCharPlusEllipsisSize - + spacing - + sortIconSize - + spacing - + groupIconSize - + spacing + + columnSpacing + + columnIconSize // sort icon + + columnSpacing + + columnIconSize // group icon + + columnSpacing + menuDropdownSize - + spacing; + + columnSpacing; // right cell padding + +export const singleIconColumnWidth = columnSpacing // left cell padding + + columnIconSize + + columnSpacing; // right cell padding export const defaultFractionalWidth = 1; diff --git a/packages/nimble-components/src/table-column/icon/index.ts b/packages/nimble-components/src/table-column/icon/index.ts index aab6876e28..46f0846f48 100644 --- a/packages/nimble-components/src/table-column/icon/index.ts +++ b/packages/nimble-components/src/table-column/icon/index.ts @@ -1,4 +1,5 @@ import { DesignSystem } from '@microsoft/fast-foundation'; +import { attr } from '@microsoft/fast-element'; import { MappingConfigs, TableColumnEnumBase, @@ -6,7 +7,11 @@ import { } from '../enum-base'; import { styles } from '../enum-base/styles'; import { template } from '../enum-base/template'; -import { TableColumnSortOperation } from '../base/types'; +import { + TableColumnSortOperation, + singleIconColumnWidth, + defaultMinPixelWidth +} from '../base/types'; import { mixinGroupableColumnAPI } from '../mixins/groupable-column'; import { mixinFractionalWidthColumnAPI } from '../mixins/fractional-width-column'; import { MappingSpinner } from '../../mapping/spinner'; @@ -21,6 +26,7 @@ import { MappingIconConfig } from '../enum-base/models/mapping-icon-config'; import { MappingSpinnerConfig } from '../enum-base/models/mapping-spinner-config'; import { MappingText } from '../../mapping/text'; import { MappingTextConfig } from '../enum-base/models/mapping-text-config'; +import { TableColumnMappingWidthMode } from './types'; declare global { interface HTMLElementTagNameMap { @@ -39,6 +45,15 @@ export class TableColumnIcon extends mixinGroupableColumnAPI( > ) ) { + @attr({ attribute: 'width-mode' }) + public widthMode: TableColumnMappingWidthMode; + + public override minPixelWidthChanged(): void { + if (this.widthMode !== TableColumnMappingWidthMode.iconSize) { + this.columnInternals.minPixelWidth = this.getConfiguredMinPixelWidth(); + } + } + protected override getColumnInternalsOptions(): ColumnInternalsOptions { return { cellRecordFieldNames: ['value'], @@ -77,6 +92,27 @@ export class TableColumnIcon extends mixinGroupableColumnAPI( // this function from running when there is an unsupported mapping. throw new Error('Unsupported mapping'); } + + private widthModeChanged(): void { + if (this.widthMode === TableColumnMappingWidthMode.iconSize) { + this.columnInternals.resizingDisabled = true; + this.columnInternals.hideHeaderIndicators = true; + this.columnInternals.pixelWidth = singleIconColumnWidth; + this.columnInternals.minPixelWidth = singleIconColumnWidth; + } else { + this.columnInternals.resizingDisabled = false; + this.columnInternals.hideHeaderIndicators = false; + this.columnInternals.pixelWidth = undefined; + this.columnInternals.minPixelWidth = this.getConfiguredMinPixelWidth(); + } + } + + private getConfiguredMinPixelWidth(): number { + if (typeof this.minPixelWidth === 'number') { + return this.minPixelWidth; + } + return defaultMinPixelWidth; + } } const nimbleTableColumnIcon = TableColumnIcon.compose({ diff --git a/packages/nimble-components/src/table-column/icon/tests/table-column-icon-matrix.stories.ts b/packages/nimble-components/src/table-column/icon/tests/table-column-icon-matrix.stories.ts index 83386cf248..73bb8451f9 100644 --- a/packages/nimble-components/src/table-column/icon/tests/table-column-icon-matrix.stories.ts +++ b/packages/nimble-components/src/table-column/icon/tests/table-column-icon-matrix.stories.ts @@ -12,6 +12,8 @@ import { mappingSpinnerTag } from '../../../mapping/spinner'; import { isChromatic } from '../../../utilities/tests/isChromatic'; import { iconXmarkTag } from '../../../icons/xmark'; import { mappingTextTag } from '../../../mapping/text'; +import { TableColumnMappingWidthMode } from '../types'; +import { iconQuestionTag } from '../../../icons/question'; const data = [ { @@ -80,6 +82,19 @@ const component = (): ViewTemplate => html` <${mappingIconTag} key="1" text="One" icon="${iconCheckTag}" severity="warning"> <${mappingIconTag} key="2" text="Two" icon="${iconCheckTag}" severity="error"> + <${tableColumnIconTag} + field-name="code" + key-type="number" + width-mode="${TableColumnMappingWidthMode.iconSize}" + > + <${iconQuestionTag} title="Icon-only column"> + <${mappingIconTag} key="-1" text="Unknown value"> + <${mappingIconTag} key="0" text="Zero" icon="${iconCheckTag}" severity="success" text-hidden> + <${mappingIconTag} key="1" text="One" icon="${iconCheckTag}" severity="warning" text-hidden> + <${mappingIconTag} key="2" text="Two" icon="${iconCheckTag}" severity="error" text-hidden> + <${mappingIconTag} key="3" text="Three" icon="${iconCheckTag}" severity="information" text-hidden> + <${mappingIconTag} key="4" text="Four" icon="${iconCheckTag}" text-hidden> + <${tableColumnIconTag} field-name="code" key-type="number" diff --git a/packages/nimble-components/src/table-column/icon/tests/table-column-icon.spec.ts b/packages/nimble-components/src/table-column/icon/tests/table-column-icon.spec.ts index 244ca49b71..40a2c3b656 100644 --- a/packages/nimble-components/src/table-column/icon/tests/table-column-icon.spec.ts +++ b/packages/nimble-components/src/table-column/icon/tests/table-column-icon.spec.ts @@ -19,6 +19,8 @@ import { spinnerTag } from '../../../spinner'; import { themeProviderTag } from '../../../theme-provider'; import { TableColumnIconPageObject } from '../testing/table-column-icon.pageobject'; import { mappingUserTag } from '../../../mapping/user'; +import { TableColumnMappingWidthMode } from '../types'; +import { defaultMinPixelWidth } from '../../base/types'; interface SimpleTableRecord extends TableRecord { field1?: MappingKey | null; @@ -888,4 +890,99 @@ describe('TableColumnIcon', () => { }); }); }); + + describe('width-mode', () => { + beforeEach(async () => { + ({ connect, disconnect, model } = await setup({ + keyType: MappingKeyType.string + })); + }); + + it('defaults to `default`', () => { + expect(model.col1.widthMode).toBe( + TableColumnMappingWidthMode.default + ); + expect(model.col1.columnInternals.resizingDisabled).toBeFalse(); + }); + + it('column configuration is updated when set to `iconSize`', async () => { + model.col1.widthMode = TableColumnMappingWidthMode.iconSize; + await waitForUpdatesAsync(); + + expect(model.col1.columnInternals.resizingDisabled).toBeTrue(); + expect(model.col1.columnInternals.pixelWidth).toBe(32); + expect(model.col1.columnInternals.minPixelWidth).toBe(32); + }); + + it('column changes back to fractionally sized when changing from `iconSize` to `default`', async () => { + model.col1.widthMode = TableColumnMappingWidthMode.iconSize; + await waitForUpdatesAsync(); + model.col1.widthMode = TableColumnMappingWidthMode.default; + await waitForUpdatesAsync(); + + expect(model.col1.columnInternals.resizingDisabled).toBeFalse(); + expect(model.col1.columnInternals.pixelWidth).toBe(undefined); + expect(model.col1.columnInternals.minPixelWidth).toBe( + defaultMinPixelWidth + ); + }); + + it('changing min-pixel-width with mode of `iconSize` does not change minimum width of column', async () => { + model.col1.widthMode = TableColumnMappingWidthMode.iconSize; + await waitForUpdatesAsync(); + expect(model.col1.columnInternals.minPixelWidth).toBe(32); + + model.col1.minPixelWidth = 500; + await waitForUpdatesAsync(); + expect(model.col1.columnInternals.minPixelWidth).toBe(32); + }); + + it('previously configured min-pixel-width is retained when switching from `default` to `iconSize` and back to `default`', async () => { + model.col1.widthMode = TableColumnMappingWidthMode.default; + model.col1.minPixelWidth = 500; + await waitForUpdatesAsync(); + expect(model.col1.columnInternals.minPixelWidth).toBe(500); + + model.col1.widthMode = TableColumnMappingWidthMode.iconSize; + await waitForUpdatesAsync(); + expect(model.col1.columnInternals.minPixelWidth).toBe(32); + + model.col1.widthMode = TableColumnMappingWidthMode.default; + await waitForUpdatesAsync(); + expect(model.col1.columnInternals.minPixelWidth).toBe(500); + }); + + it('min-pixel-width applied with mode of `iconSize` is used when width-mode changes to `default`', async () => { + model.col1.widthMode = TableColumnMappingWidthMode.iconSize; + await waitForUpdatesAsync(); + expect(model.col1.columnInternals.minPixelWidth).toBe(32); + + model.col1.minPixelWidth = 500; + await waitForUpdatesAsync(); + expect(model.col1.columnInternals.minPixelWidth).toBe(32); + + model.col1.widthMode = TableColumnMappingWidthMode.default; + await waitForUpdatesAsync(); + expect(model.col1.columnInternals.minPixelWidth).toBe(500); + }); + + it('clearing min-pixel-width while in `iconSize` mode resets the minimum width to default', async () => { + model.col1.widthMode = TableColumnMappingWidthMode.default; + model.col1.minPixelWidth = 500; + await waitForUpdatesAsync(); + expect(model.col1.columnInternals.minPixelWidth).toBe(500); + + model.col1.widthMode = TableColumnMappingWidthMode.iconSize; + await waitForUpdatesAsync(); + model.col1.minPixelWidth = undefined; + await waitForUpdatesAsync(); + expect(model.col1.columnInternals.minPixelWidth).toBe(32); + + model.col1.widthMode = TableColumnMappingWidthMode.default; + await waitForUpdatesAsync(); + expect(model.col1.columnInternals.minPixelWidth).toBe( + defaultMinPixelWidth + ); + }); + }); }); diff --git a/packages/nimble-components/src/table-column/icon/tests/table-column-icon.stories.ts b/packages/nimble-components/src/table-column/icon/tests/table-column-icon.stories.ts index 0f5255ed8d..216fe47e12 100644 --- a/packages/nimble-components/src/table-column/icon/tests/table-column-icon.stories.ts +++ b/packages/nimble-components/src/table-column/icon/tests/table-column-icon.stories.ts @@ -16,6 +16,8 @@ import { mappingSpinnerTag } from '../../../mapping/spinner'; import { sharedMappingValidityDescription } from '../../enum-base/tests/shared-storybook-docs'; import { isChromatic } from '../../../utilities/tests/isChromatic'; import { mappingTextTag } from '../../../mapping/text'; +import { TableColumnMappingWidthMode } from '../types'; +import { iconChartDiagramChildFocusTag } from '../../../icons/chart-diagram-child-focus'; const simpleData = [ { @@ -65,10 +67,16 @@ export default metadata; interface IconColumnTableArgs extends SharedTableArgs { fieldName: string; keyType: string; + widthMode: keyof typeof TableColumnMappingWidthMode; checkValidity: () => void; validity: () => void; } +const widthModeDescription = `When set to \`iconSize\`, the column will have a fixed width that makes the column the appropriate width to render only a single icon in the cell. +This should only be set when the header contains a single icon (no text) and none of the child mapping elements will result in text being rendered in a cell. When unset or set +to \`default\`, the column will be resizable and be sized based on its fractional-width and min-pixel-width values. A column with its \`width-mode\` set to \`iconSize\` should +should not be the right-most column in the table.`; + const validityDescription = `${sharedMappingValidityDescription} - \`invalidIconName\`: \`true\` when a mapping's \`icon\` value is not the tag name of a valid, loaded Nimble icon (e.g. \`nimble-icon-check\`) `; @@ -91,10 +99,11 @@ export const iconColumn: StoryObj = { <${mappingSpinnerTag} key="calculating" text="Calculating" text-hidden> <${mappingIconTag} key="unknown" text="Unknown" text-hidden> - <${tableColumnIconTag} field-name="isChild" key-type="boolean"> - Is Child - <${mappingIconTag} key="false" icon="${iconXmarkTag}" severity="error" text="Not a child"> - <${mappingIconTag} key="true" icon="${iconCheckLargeTag}" severity="success" text="Is a child"> + <${tableColumnIconTag} field-name="isChild" key-type="boolean" width-mode="${x => TableColumnMappingWidthMode[x.widthMode]}"> + <${iconChartDiagramChildFocusTag} title="Is child"> + + <${mappingIconTag} key="false" icon="${iconXmarkTag}" severity="error" text="Not a child" text-hidden> + <${mappingIconTag} key="true" icon="${iconCheckLargeTag}" severity="success" text="Is a child" text-hidden> <${tableColumnIconTag} field-name="gender" key-type="string"> Gender @@ -123,6 +132,12 @@ export const iconColumn: StoryObj = { description: 'The data type of the key values used for this column. Must be one of `"string"`, `"number"`, or `"boolean"`. Defaults to `"string"` if unspecified.' }, + widthMode: { + name: 'width-mode', + options: Object.keys(TableColumnMappingWidthMode), + control: { type: 'radio' }, + description: widthModeDescription + }, checkValidity: { name: 'checkValidity()', description: @@ -136,6 +151,7 @@ export const iconColumn: StoryObj = { ...sharedTableArgs(simpleData), fieldName: 'firstName', keyType: 'string', + widthMode: 'iconSize', checkValidity: () => {}, validity: () => {} } diff --git a/packages/nimble-components/src/table-column/icon/tests/types.spec.ts b/packages/nimble-components/src/table-column/icon/tests/types.spec.ts new file mode 100644 index 0000000000..bc51341459 --- /dev/null +++ b/packages/nimble-components/src/table-column/icon/tests/types.spec.ts @@ -0,0 +1,9 @@ +import type { TableColumnMappingWidthMode } from '../types'; + +describe('Icon column type', () => { + it('TableColumnMappingWidthMode fails compile if assigning arbitrary string values', () => { + // @ts-expect-error This expect will fail if the enum-like type is missing "as const" + const widthMode: TableColumnMappingWidthMode = 'hello'; + expect(widthMode!).toEqual('hello'); + }); +}); diff --git a/packages/nimble-components/src/table-column/icon/types.ts b/packages/nimble-components/src/table-column/icon/types.ts new file mode 100644 index 0000000000..ae19b32139 --- /dev/null +++ b/packages/nimble-components/src/table-column/icon/types.ts @@ -0,0 +1,9 @@ +/** + * Width mode for the icon column + */ +export const TableColumnMappingWidthMode = { + default: undefined, + iconSize: 'icon-size' +} as const; +export type TableColumnMappingWidthMode = + (typeof TableColumnMappingWidthMode)[keyof typeof TableColumnMappingWidthMode]; diff --git a/packages/nimble-components/src/table/components/header/index.ts b/packages/nimble-components/src/table/components/header/index.ts index 562df7edb2..5f35ec538a 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: 'indicators-hidden', mode: 'boolean' }) + public indicatorsHidden = 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..808f5a17e5 100644 --- a/packages/nimble-components/src/table/components/header/template.ts +++ b/packages/nimble-components/src/table/components/header/template.ts @@ -18,28 +18,31 @@ export const template = html` @mousedown="${(_x, c) => !((c.event as MouseEvent).detail > 1)}" > - ${'' /* Set aria-hidden="true" on sort indicators because aria-sort is set on the 1st sorted column */} - ${when(x => x.sortDirection === TableColumnSortDirection.ascending, html` - <${iconArrowUpTag} - class="sort-indicator" - title="${x => tableColumnHeaderSortedAscendingLabel.getValueFor(x)}" - aria-hidden="true" - > - `)} - ${when(x => x.sortDirection === TableColumnSortDirection.descending, html` - <${iconArrowDownTag} - class="sort-indicator" - title="${x => tableColumnHeaderSortedDescendingLabel.getValueFor(x)}" - aria-hidden="true" - > - `)} - ${when(x => x.isGrouped, html` - <${iconTwoSquaresInBracketsTag} - class="grouped-indicator" - title="${x => tableColumnHeaderGroupedLabel.getValueFor(x)}" - role="img" - aria-label="${x => tableColumnHeaderGroupedLabel.getValueFor(x)}" - > + + ${when(x => !x.indicatorsHidden, html` + ${'' /* Set aria-hidden="true" on sort indicators because aria-sort is set on the 1st sorted column */} + ${when(x => x.sortDirection === TableColumnSortDirection.ascending, html` + <${iconArrowUpTag} + class="sort-indicator" + title="${x => tableColumnHeaderSortedAscendingLabel.getValueFor(x)}" + aria-hidden="true" + > + `)} + ${when(x => x.sortDirection === TableColumnSortDirection.descending, html` + <${iconArrowDownTag} + class="sort-indicator" + title="${x => tableColumnHeaderSortedDescendingLabel.getValueFor(x)}" + aria-hidden="true" + > + `)} + ${when(x => x.isGrouped, html` + <${iconTwoSquaresInBracketsTag} + class="grouped-indicator" + title="${x => tableColumnHeaderGroupedLabel.getValueFor(x)}" + role="img" + aria-label="${x => tableColumnHeaderGroupedLabel.getValueFor(x)}" + > + `)} `)} `; diff --git a/packages/nimble-components/src/table/components/header/tests/table-header.spec.ts b/packages/nimble-components/src/table/components/header/tests/table-header.spec.ts index fd72467373..2dfd82e966 100644 --- a/packages/nimble-components/src/table/components/header/tests/table-header.spec.ts +++ b/packages/nimble-components/src/table/components/header/tests/table-header.spec.ts @@ -87,4 +87,64 @@ describe('TableHeader', () => { expect(element.isGrouped).toBeFalse(); expect(pageObject.isGroupIndicatorIconVisible()).toBeFalse(); }); + + it('sorting and grouping indicators are hidden when indicators-hidden is true', async () => { + element.isGrouped = true; + element.sortDirection = TableColumnSortDirection.ascending; + element.firstSortedColumn = true; + element.indicatorsHidden = true; + await waitForUpdatesAsync(); + + expect(pageObject.isSortAscendingIconVisible()).toBeFalse(); + expect(pageObject.isSortDescendingIconVisible()).toBeFalse(); + expect(pageObject.isGroupIndicatorIconVisible()).toBeFalse(); + }); + + it('sorting and grouping indicators become visible when indicators-hidden changes from true to false', async () => { + element.isGrouped = true; + element.sortDirection = TableColumnSortDirection.ascending; + element.firstSortedColumn = true; + element.indicatorsHidden = true; + await waitForUpdatesAsync(); + + expect(pageObject.isSortAscendingIconVisible()).toBeFalse(); + expect(pageObject.isSortDescendingIconVisible()).toBeFalse(); + expect(pageObject.isGroupIndicatorIconVisible()).toBeFalse(); + + element.indicatorsHidden = false; + await waitForUpdatesAsync(); + + expect(pageObject.isSortAscendingIconVisible()).toBeTrue(); + expect(pageObject.isSortDescendingIconVisible()).toBeFalse(); + expect(pageObject.isGroupIndicatorIconVisible()).toBeTrue(); + }); + + it('sorting and grouping indicators become hidden when indicators-hidden changes from false to true', async () => { + element.isGrouped = true; + element.sortDirection = TableColumnSortDirection.ascending; + element.firstSortedColumn = true; + await waitForUpdatesAsync(); + + expect(pageObject.isSortAscendingIconVisible()).toBeTrue(); + expect(pageObject.isSortDescendingIconVisible()).toBeFalse(); + expect(pageObject.isGroupIndicatorIconVisible()).toBeTrue(); + + element.indicatorsHidden = true; + await waitForUpdatesAsync(); + + expect(pageObject.isSortAscendingIconVisible()).toBeFalse(); + expect(pageObject.isSortDescendingIconVisible()).toBeFalse(); + expect(pageObject.isGroupIndicatorIconVisible()).toBeFalse(); + }); + + it('configures aria-sort when sorting indicator is hidden', async () => { + element.sortDirection = TableColumnSortDirection.descending; + element.firstSortedColumn = true; + element.indicatorsHidden = true; + await waitForUpdatesAsync(); + + expect(element.getAttribute('aria-sort')).toEqual('descending'); + expect(pageObject.isSortAscendingIconVisible()).toBeFalse(); + expect(pageObject.isSortDescendingIconVisible()).toBeFalse(); + }); }); 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 1f9da90fc6..6c20273494 100644 --- a/packages/nimble-components/src/table/models/table-layout-manager.ts +++ b/packages/nimble-components/src/table/models/table-layout-manager.ts @@ -28,8 +28,7 @@ export class TableLayoutManager { private rightColumnIndex?: number; private initialColumnWidths: { initalColumnFractionalWidth: number, - initialPixelWidth: number, - minPixelWidth: number + initialPixelWidth: number }[] = []; public constructor(private readonly table: Table) {} @@ -82,6 +81,20 @@ export class TableLayoutManager { document.addEventListener('mouseup', this.onDividerMouseUp); } + /** + * Determines if the specified column or any columns to the left are resizable. + */ + public hasResizableColumnToLeft(columnIndex: number): boolean { + return this.getFirstLeftResizableColumnIndex(columnIndex) !== -1; + } + + /** + * Determines if the specified column or any columns to the right are resizable. + */ + private hasResizableColumnToRight(columnIndex: number): boolean { + return this.getFirstRightResizableColumnIndex(columnIndex) !== -1; + } + private readonly onDividerMouseMove = (event: Event): void => { const mouseEvent = event as MouseEvent; for (let i = 0; i < this.visibleColumns.length; i++) { @@ -114,6 +127,7 @@ export class TableLayoutManager { this.isColumnBeingSized = false; this.activeColumnIndex = undefined; this.activeColumnDivider = undefined; + this.visibleColumns = []; }; private getTotalColumnFixedWidth(): number { @@ -138,41 +152,102 @@ 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; - currentIndex -= 1; + if (!this.hasResizableColumnToRight(this.rightColumnIndex!)) { + return 0; + } + + for (let i = this.leftColumnIndex!; i >= 0; i--) { + const columnInitialWidths = this.initialColumnWidths[i]!; + const column = this.visibleColumns[i]!; + if (!column.columnInternals.resizingDisabled) { + availableSpace + += columnInitialWidths.initialPixelWidth + - column.columnInternals.minPixelWidth; + } } return Math.max(requestedResizeAmount, -availableSpace); } + /** + * Gets the index of the first resizable column starting with + * `columnIndex` and moving to the left. If no resizable column + * is found, returns -1. + */ + private getFirstLeftResizableColumnIndex(columnIndex: number): number { + const visibleColumns = this.visibleColumns.length === 0 + ? this.getVisibleColumns() + : this.visibleColumns; + for (let i = columnIndex; i >= 0; i--) { + const column = visibleColumns[i]; + if (!column) { + return -1; + } + if (!column.columnInternals.resizingDisabled) { + return i; + } + } + return -1; + } + + /** + * Gets the index of the first resizable column starting with + * `columnIndex` and moving to the right. If no resizable column + * is found, returns -1. + */ + private getFirstRightResizableColumnIndex(columnIndex: number): number { + const visibleColumns = this.visibleColumns.length === 0 + ? this.getVisibleColumns() + : this.visibleColumns; + 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 leftColumn = this.visibleColumns[firstLeftResizableColumn]!; + const leftColumnInitialWidths = this.initialColumnWidths[firstLeftResizableColumn]!; const allowedDelta = delta < 0 ? Math.max( - leftColumnInitialWidths.minPixelWidth + leftColumn.columnInternals.minPixelWidth - leftColumnInitialWidths.initialPixelWidth, currentDelta ) : delta; const actualDelta = allowedDelta; - const leftColumn = this.visibleColumns[leftColumnIndex]!; 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 + ); } } @@ -180,26 +255,39 @@ 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 rightColumn = this.visibleColumns[firstRightResizableColumn]!; + const rightColumnInitialWidths = this.initialColumnWidths[firstRightResizableColumn]!; + let allowedDelta: number; + if (rightColumn.columnInternals.resizingDisabled) { + allowedDelta = 0; + } else if (delta > 0) { + allowedDelta = Math.min( rightColumnInitialWidths.initialPixelWidth - - rightColumnInitialWidths.minPixelWidth, + - rightColumn.columnInternals.minPixelWidth, currentDelta - ) - : delta; + ); + } else { + allowedDelta = delta; + } const actualDelta = allowedDelta; - const rightColumn = this.visibleColumns[rightColumnIndex]!; 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 + ); } } @@ -218,8 +306,7 @@ export class TableLayoutManager { this.initialColumnWidths.push({ initalColumnFractionalWidth: column.columnInternals.currentFractionalWidth, - initialPixelWidth: column.columnInternals.currentPixelWidth!, - minPixelWidth: column.columnInternals.minPixelWidth + initialPixelWidth: column.columnInternals.currentPixelWidth! }); } } diff --git a/packages/nimble-components/src/table/models/table-update-tracker.ts b/packages/nimble-components/src/table/models/table-update-tracker.ts index 635769a3ad..4f3baf07bb 100644 --- a/packages/nimble-components/src/table/models/table-update-tracker.ts +++ b/packages/nimble-components/src/table/models/table-update-tracker.ts @@ -132,7 +132,8 @@ export class TableUpdateTracker< changedColumnProperty, 'currentFractionalWidth', 'currentPixelWidth', - 'minPixelWidth' + 'minPixelWidth', + 'resizingDisabled' ) ) { this.track('columnWidths'); diff --git a/packages/nimble-components/src/table/specs/table-column-width-hld.md b/packages/nimble-components/src/table/specs/table-column-width-hld.md index 7fc2234251..e957eaea81 100644 --- a/packages/nimble-components/src/table/specs/table-column-width-hld.md +++ b/packages/nimble-components/src/table/specs/table-column-width-hld.md @@ -88,6 +88,13 @@ ColumnInternals { @observable public resizingDisabled = false; + /** + * Whether or not the grouping and sorting indicators should be hidden in the column header + * when the column is grouped or sorted. + */ + @observable + public hideHeaderIndicators = false; + ... } ``` @@ -169,7 +176,7 @@ export class MyPixelWidthColumn : TableColumn<...> { In some cases a column may not have space in its header for the sorting indicator or grouping indicator. For example, the icon column will be fixed width with enough space to render only a single icon. Therefore, there will not be space for a sorting indicator or grouping indicator next to the column's header icon. -In this case, the sorting indicator and grouping indicator will be hidden in the column header. This scenario will be determined by comparing the column's `minPixelWidth` with `defaultMinPixelWidth`. If the `minPixelWidth` of the column is less than `defaultMinPixelWidth`, then the sorting and grouping indicator will automatically be hidden in the header. +In this case, the sorting indicator and grouping indicator will be hidden in the column header. To hide the sorting indicator and grouping indicator in the column's header, the column can set `hideHeaderIndicators` to `true` on its `columnInternals` object. ### Implementation considerations diff --git a/packages/nimble-components/src/table/styles.ts b/packages/nimble-components/src/table/styles.ts index 870943fdf1..db6708e4ac 100644 --- a/packages/nimble-components/src/table/styles.ts +++ b/packages/nimble-components/src/table/styles.ts @@ -124,8 +124,8 @@ export const styles = css` border-color: ${borderHoverColor}; } - .column-divider.column-active, - .header-container:hover .column-divider { + .column-divider.column-active.draggable, + .header-container:hover .column-divider.draggable { display: block; } diff --git a/packages/nimble-components/src/table/template.ts b/packages/nimble-components/src/table/template.ts index 0481c90995..8353f3861f 100644 --- a/packages/nimble-components/src/table/template.ts +++ b/packages/nimble-components/src/table/template.ts @@ -89,6 +89,7 @@ export const template = html` left ${(_, c) => `${c.parent.layoutManager.activeColumnIndex === c.index ? 'column-active' : ''}`} ${(_, c) => `${c.parent.layoutManager.activeColumnDivider === c.parent.getLeftDividerIndex(c.index) ? 'divider-active' : ''}`} + ${(_, c) => `${c.parent.layoutManager.hasResizableColumnToLeft(c.index - 1) ? 'draggable' : ''}`} " @mousedown="${(_, c) => c.parent.onLeftDividerMouseDown(c.event as MouseEvent, c.index)}"> @@ -97,6 +98,7 @@ export const template = html
` class="header" sort-direction="${x => (typeof x.columnInternals.currentSortIndex === 'number' ? x.columnInternals.currentSortDirection : TableColumnSortDirection.none)}" ?first-sorted-column="${(x, c) => x === c.parent.firstSortedColumn}" + ?indicators-hidden="${x => x.columnInternals.hideHeaderIndicators}" @click="${(x, c) => c.parent.toggleColumnSort(x, (c.event as MouseEvent).shiftKey)}" :isGrouped=${x => (typeof x.columnInternals.groupIndex === 'number' && !x.columnInternals.groupingDisabled)} > @@ -109,6 +111,7 @@ export const template = html
` right ${(_, c) => `${c.parent.layoutManager.activeColumnIndex === c.index ? 'column-active' : ''}`} ${(_, c) => `${c.parent.layoutManager.activeColumnDivider === c.parent.getRightDividerIndex(c.index) ? 'divider-active' : ''}`} + ${(_, c) => `${c.parent.layoutManager.hasResizableColumnToLeft(c.index) ? 'draggable' : ''}`} " @mousedown="${(_, c) => c.parent.onRightDividerMouseDown(c.event as MouseEvent, c.index)}"> diff --git a/packages/nimble-components/src/table/tests/table-column-sizing.spec.ts b/packages/nimble-components/src/table/tests/table-column-sizing.spec.ts index 3b32cfa666..57a6fdcfd2 100644 --- a/packages/nimble-components/src/table/tests/table-column-sizing.spec.ts +++ b/packages/nimble-components/src/table/tests/table-column-sizing.spec.ts @@ -351,7 +351,7 @@ describe('Table Interactive Column Sizing', () => { await disconnect(); }); - describe('No hidden columns ', () => { + describe('No hidden columns', () => { const columnSizeTests = [ { name: 'sizing right only affects adjacent right column with delta less than min width', @@ -360,7 +360,8 @@ describe('Table Interactive Column Sizing', () => { fractionalWidths: [1, 1, 1, 1], pixelWidths: [undefined, undefined, undefined, undefined], minPixelWidths: [50, 50, 50, 50], - expectedColumnWidths: [101, 99, 100, 100] + expectedColumnWidths: [101, 99, 100, 100], + resizingDisabled: [false, false, false, false] }, { name: 'sizing right past the minimum size of adjacent right column cascades to next column', @@ -369,7 +370,8 @@ describe('Table Interactive Column Sizing', () => { fractionalWidths: [1, 1, 1, 1], pixelWidths: [undefined, undefined, undefined, undefined], minPixelWidths: [50, 50, 50, 50], - expectedColumnWidths: [151, 50, 99, 100] + expectedColumnWidths: [151, 50, 99, 100], + resizingDisabled: [false, false, false, false] }, { name: 'sizing right past the minimum size of all columns to right shrinks all columns to minimum size, but allows left column to keep growing', @@ -378,7 +380,8 @@ describe('Table Interactive Column Sizing', () => { fractionalWidths: [1, 1, 1, 1], pixelWidths: [undefined, undefined, undefined, undefined], minPixelWidths: [50, 50, 50, 50], - expectedColumnWidths: [251, 50, 50, 50] + expectedColumnWidths: [251, 50, 50, 50], + resizingDisabled: [false, false, false, false] }, { name: 'sizing left only affects adjacent left column with delta less than min width', @@ -387,7 +390,8 @@ describe('Table Interactive Column Sizing', () => { fractionalWidths: [1, 1, 1, 1], pixelWidths: [undefined, undefined, undefined, undefined], minPixelWidths: [50, 50, 50, 50], - expectedColumnWidths: [100, 100, 99, 101] + expectedColumnWidths: [100, 100, 99, 101], + resizingDisabled: [false, false, false, false] }, { name: 'sizing left past the minimum size of adjacent left column cascades to next column', @@ -396,7 +400,8 @@ describe('Table Interactive Column Sizing', () => { fractionalWidths: [1, 1, 1, 1], pixelWidths: [undefined, undefined, undefined, undefined], minPixelWidths: [50, 50, 50, 50], - expectedColumnWidths: [100, 99, 50, 151] + expectedColumnWidths: [100, 99, 50, 151], + resizingDisabled: [false, false, false, false] }, { name: 'sizing left past the minimum size of all columns to left shrinks all columns to minimum size, and stops growing right most column', @@ -405,7 +410,8 @@ describe('Table Interactive Column Sizing', () => { fractionalWidths: [1, 1, 1, 1], pixelWidths: [undefined, undefined, undefined, undefined], minPixelWidths: [50, 50, 50, 50], - expectedColumnWidths: [50, 50, 50, 250] + expectedColumnWidths: [50, 50, 50, 250], + resizingDisabled: [false, false, false, false] }, { name: `sizing left past the minimum size of all columns to left shrinks all columns to minimum size, and stops growing right most column, @@ -415,7 +421,8 @@ describe('Table Interactive Column Sizing', () => { fractionalWidths: [1, 1, 1, 1], pixelWidths: [undefined, undefined, undefined, undefined], minPixelWidths: [50, 50, 50, 50], - expectedColumnWidths: [50, 50, 50, 250] + expectedColumnWidths: [50, 50, 50, 250], + resizingDisabled: [false, false, false, false] }, { name: 'sizing right causing cascade and then sizing left in same interaction reverts cascade effect', @@ -424,7 +431,8 @@ describe('Table Interactive Column Sizing', () => { fractionalWidths: [1, 1, 1, 1], pixelWidths: [undefined, undefined, undefined, undefined], minPixelWidths: [50, 50, 50, 50], - expectedColumnWidths: [100, 100, 150, 50] + expectedColumnWidths: [100, 100, 150, 50], + resizingDisabled: [false, false, false, false] }, { name: 'sizing left causing cascade and then sizing right in same interaction reverts cascade effect', @@ -433,7 +441,8 @@ describe('Table Interactive Column Sizing', () => { fractionalWidths: [1, 1, 1, 1], pixelWidths: [undefined, undefined, undefined, undefined], minPixelWidths: [50, 50, 50, 50], - expectedColumnWidths: [75, 125, 100, 100] + expectedColumnWidths: [75, 125, 100, 100], + resizingDisabled: [false, false, false, false] }, { name: 'sizing a column with the same fractional width as other columns, but larger minimum size, does not result in different pixel widths for columns not resized', @@ -442,7 +451,8 @@ describe('Table Interactive Column Sizing', () => { fractionalWidths: [1, 1, 1, 1], pixelWidths: [undefined, undefined, undefined, undefined], minPixelWidths: [50, 50, 50, 175], - expectedColumnWidths: [75, 75, 65, 185] + expectedColumnWidths: [75, 75, 65, 185], + resizingDisabled: [false, false, false, false] }, { name: 'sizing a column with the same fractional width as other columns, but larger minimum size, with a fixed width column that is not interactively sized, does not result in different pixel widths for columns not resized', @@ -451,7 +461,8 @@ describe('Table Interactive Column Sizing', () => { fractionalWidths: [1, 1, 1, 1], pixelWidths: [85, undefined, undefined, undefined], minPixelWidths: [50, 50, 45, 175], - expectedColumnWidths: [85, 65, 45, 205] + expectedColumnWidths: [85, 65, 45, 205], + resizingDisabled: [false, false, false, false] }, { name: 'sizing a column with the same fractional width as other columns, but larger minimum size, along with a fixed width column, does not result in different pixel widths for columns not resized', @@ -460,7 +471,48 @@ describe('Table Interactive Column Sizing', () => { fractionalWidths: [1, 1, 1, 1], pixelWidths: [undefined, undefined, 75, undefined], minPixelWidths: [50, 50, 50, 175], - expectedColumnWidths: [75, 75, 50, 200] + expectedColumnWidths: [75, 75, 50, 200], + resizingDisabled: [false, false, false, false] + }, + { + name: 'dragging the divider between a resizable column and last column that is not resizable does not result in any column being resized', + dragDeltas: [-25], + columnDragIndex: 2, + fractionalWidths: [1, 1, 1, undefined], + pixelWidths: [undefined, undefined, undefined, 50], + minPixelWidths: [50, 50, 50, 50], + expectedColumnWidths: [116.7, 116.7, 116.7, 50], + resizingDisabled: [false, false, false, true] + }, + { + name: 'resizing to the left skips non-resizable columns', + dragDeltas: [-25], + columnDragIndex: 2, + fractionalWidths: [1, undefined, undefined, 1], + pixelWidths: [undefined, 50, 50, undefined], + minPixelWidths: [50, 50, 50, 50], + expectedColumnWidths: [125, 50, 50, 175], + resizingDisabled: [false, true, true, false] + }, + { + name: 'resizing to the right skips non-resizable columns', + dragDeltas: [25], + columnDragIndex: 0, + fractionalWidths: [1, undefined, undefined, 1], + pixelWidths: [undefined, 50, 50, undefined], + minPixelWidths: [50, 50, 50, 50], + expectedColumnWidths: [175, 50, 50, 125], + resizingDisabled: [false, true, true, false] + }, + { + name: 'dragging divider between two non-resizable columns resizes the surrounding columns', + dragDeltas: [25], + columnDragIndex: 1, + fractionalWidths: [1, undefined, undefined, 1], + pixelWidths: [undefined, 50, 50, undefined], + minPixelWidths: [50, 50, 50, 50], + expectedColumnWidths: [175, 50, 50, 125], + resizingDisabled: [false, true, true, false] } ] as const; parameterizeSpec(columnSizeTests, (spec, name, value) => { @@ -469,6 +521,7 @@ describe('Table Interactive Column Sizing', () => { column.columnInternals.fractionalWidth = value.fractionalWidths[i]!; column.columnInternals.pixelWidth = value.pixelWidths[i]!; column.columnInternals.minPixelWidth = value.minPixelWidths[i]!; + column.columnInternals.resizingDisabled = value.resizingDisabled[i]!; }); await waitForUpdatesAsync(); pageObject.dragSizeColumnByRightDivider( @@ -480,6 +533,68 @@ describe('Table Interactive Column Sizing', () => { }); }); + const resizingDisabledDividerDraggabilityTests = [ + { + name: 'all dividers are draggable when no columns have resizing disabled', + resizingDisabled: [false, false, false, false], + expectedDraggableDividers: [0, 1, 2, 3, 4, 5] + }, + { + name: 'no column dividers are draggable if no columns are resizable', + resizingDisabled: [true, true, true, true], + expectedDraggableDividers: [] + }, + { + name: 'right divider is not draggable on left most column if it is not resizable', + resizingDisabled: [true, false, false, false], + expectedDraggableDividers: [2, 3, 4, 5] + }, + { + name: 'dividers are not draggable on multiple non-resizable columns on the left of the table', + resizingDisabled: [true, true, false, false], + expectedDraggableDividers: [4, 5] + }, + { + name: 'can drag dividers for column surrounded by non-resizable columns if another column can be resized', + resizingDisabled: [true, false, true, false], + expectedDraggableDividers: [2, 3, 4, 5] + }, + { + name: 'can only drag the right divider for the only resizable column', + resizingDisabled: [true, true, false, true], + expectedDraggableDividers: [4, 5] + }, + { + name: 'can drag dividers for all columns to the right if one resizable column exists to the left', + resizingDisabled: [false, true, true, true], + expectedDraggableDividers: [0, 1, 2, 3, 4, 5] + } + ] as const; + parameterizeSpec( + resizingDisabledDividerDraggabilityTests, + (spec, name, value) => { + spec(name, async () => { + element.columns.forEach((column, i) => { + column.columnInternals.resizingDisabled = value.resizingDisabled[i]!; + }); + await waitForUpdatesAsync(); + + const dividers = Array.from( + element.shadowRoot!.querySelectorAll('.column-divider') + ); + const draggableDividers = []; + for (let i = 0; i < dividers.length; i++) { + if (dividers[i]!.classList.contains('draggable')) { + draggableDividers.push(i); + } + } + expect(draggableDividers).toEqual( + value.expectedDraggableDividers + ); + }); + } + ); + it('when table width is smaller than total column min width, dragging column still expands column', async () => { await pageObject.sizeTableToGivenRowWidth(100, element); await waitForUpdatesAsync(); @@ -566,11 +681,15 @@ describe('Table Interactive Column Sizing', () => { }); }); - describe('hidden column drag right divider tests ', () => { + describe('hidden column drag right divider tests', () => { const hiddenColumDragRightDividerTests = [ { name: 'first column hidden, drag first right divider to right results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [0], dragColumnIndex: 0, dragDeltas: [50], @@ -579,6 +698,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'first column hidden, drag second right divider to right results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [0], dragColumnIndex: 1, dragDeltas: [50], @@ -587,6 +710,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'second column hidden, drag first right divider to right results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [1], dragColumnIndex: 0, dragDeltas: [50], @@ -595,6 +722,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'second column hidden, drag second right divider to right results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [1], dragColumnIndex: 1, dragDeltas: [50], @@ -603,6 +734,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'first column hidden, drag first right divider to left results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [0], dragColumnIndex: 0, dragDeltas: [-50], @@ -611,6 +746,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'first column hidden, drag second right divider to left results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [0], dragColumnIndex: 1, dragDeltas: [-50], @@ -619,6 +758,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'second column hidden, drag first right divider to left results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [1], dragColumnIndex: 0, dragDeltas: [-50], @@ -627,10 +770,26 @@ describe('Table Interactive Column Sizing', () => { { name: 'second column hidden, drag second right divider to left results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [1], dragColumnIndex: 1, dragDeltas: [-50], expectedColumnWidths: [100, 50, 150] + }, + { + name: 'does not change size of non-resizable or hidden columns', + tableWidth: 300, + resizingDisabled: [false, true, false, false], + fractionalWidths: [1, undefined, 1, 1], + pixelWidths: [undefined, 50, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], + hiddenColumns: [2], + dragColumnIndex: 0, + dragDeltas: [50], + expectedColumnWidths: [175, 50, 75] } ] as const; parameterizeSpec( @@ -641,6 +800,12 @@ describe('Table Interactive Column Sizing', () => { value.tableWidth, element ); + element.columns.forEach((column, i) => { + column.columnInternals.fractionalWidth = value.fractionalWidths[i]!; + column.columnInternals.pixelWidth = value.pixelWidths[i]!; + column.columnInternals.minPixelWidth = value.minPixelWidths[i]!; + column.columnInternals.resizingDisabled = value.resizingDisabled[i]!; + }); value.hiddenColumns.forEach(columnIndex => { element.columns[columnIndex]!.columnHidden = true; }); @@ -663,6 +828,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'first column hidden, drag first left divider to right results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [0], dragColumnIndex: 1, dragDeltas: [50], @@ -671,6 +840,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'first column hidden, drag second left divider to right results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [0], dragColumnIndex: 2, dragDeltas: [50], @@ -679,6 +852,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'second column hidden, drag first left divider to right results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [1], dragColumnIndex: 1, dragDeltas: [50], @@ -687,6 +864,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'second column hidden, drag second left divider to right results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [1], dragColumnIndex: 2, dragDeltas: [50], @@ -695,6 +876,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'first column hidden, drag first left divider to left results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [0], dragColumnIndex: 1, dragDeltas: [-50], @@ -703,6 +888,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'first column hidden, drag second left divider to left results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [0], dragColumnIndex: 2, dragDeltas: [-50], @@ -711,6 +900,10 @@ describe('Table Interactive Column Sizing', () => { { name: 'second column hidden, drag first left divider to left results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [1], dragColumnIndex: 1, dragDeltas: [-50], @@ -719,10 +912,26 @@ describe('Table Interactive Column Sizing', () => { { name: 'second column hidden, drag second left divider to left results in correct columns widths', tableWidth: 300, + resizingDisabled: [false, false, false, false], + fractionalWidths: [1, 1, 1, 1], + pixelWidths: [undefined, undefined, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], hiddenColumns: [1], dragColumnIndex: 2, dragDeltas: [-50], expectedColumnWidths: [100, 50, 150] + }, + { + name: 'does not change size of non-resizable or hidden columns', + tableWidth: 300, + resizingDisabled: [false, true, false, false], + fractionalWidths: [1, undefined, 1, 1], + pixelWidths: [undefined, 50, undefined, undefined], + minPixelWidths: [25, 25, 25, 25], + hiddenColumns: [2], + dragColumnIndex: 2, + dragDeltas: [50], + expectedColumnWidths: [175, 50, 75] } ] as const; parameterizeSpec( @@ -733,6 +942,12 @@ describe('Table Interactive Column Sizing', () => { value.tableWidth, element ); + element.columns.forEach((column, i) => { + column.columnInternals.fractionalWidth = value.fractionalWidths[i]!; + column.columnInternals.pixelWidth = value.pixelWidths[i]!; + column.columnInternals.minPixelWidth = value.minPixelWidths[i]!; + column.columnInternals.resizingDisabled = value.resizingDisabled[i]!; + }); value.hiddenColumns.forEach(columnIndex => { element.columns[columnIndex]!.columnHidden = true; }); @@ -750,6 +965,50 @@ describe('Table Interactive Column Sizing', () => { ); }); + describe('divider draggability with hidden column', () => { + const resizingDisabledDividerDraggabilityTests = [ + { + name: 'all dividers are draggable when no visible columns have resizing disabled', + resizingDisabled: [true, false, false, false], + hiddenColumns: [0], + expectedDraggableDividers: [0, 1, 2, 3] + }, + { + name: 'no column dividers are draggable if no visible columns are resizable', + resizingDisabled: [true, true, false, true], + hiddenColumns: [2], + expectedDraggableDividers: [] + } + ] as const; + parameterizeSpec( + resizingDisabledDividerDraggabilityTests, + (spec, name, value) => { + spec(name, async () => { + element.columns.forEach((column, i) => { + column.columnInternals.resizingDisabled = value.resizingDisabled[i]!; + }); + value.hiddenColumns.forEach(columnIndex => { + element.columns[columnIndex]!.columnHidden = true; + }); + await waitForUpdatesAsync(); + + const dividers = Array.from( + element.shadowRoot!.querySelectorAll('.column-divider') + ); + const draggableDividers = []; + for (let i = 0; i < dividers.length; i++) { + if (dividers[i]!.classList.contains('draggable')) { + draggableDividers.push(i); + } + } + expect(draggableDividers).toEqual( + value.expectedDraggableDividers + ); + }); + } + ); + }); + describe('active divider tests', () => { const dividerActiveTests = [ { diff --git a/packages/nimble-components/src/table/tests/table.spec.ts b/packages/nimble-components/src/table/tests/table.spec.ts index ccc99ce621..db0c70bfa2 100644 --- a/packages/nimble-components/src/table/tests/table.spec.ts +++ b/packages/nimble-components/src/table/tests/table.spec.ts @@ -254,6 +254,25 @@ describe('Table', () => { expect(pageObject.getHeaderTitle(0)).toBe(''); }); + it('does not set indicators-hidden on a column header by default', async () => { + await connect(); + await waitForUpdatesAsync(); + + const header = pageObject.getHeaderElement(0); + expect(header.indicatorsHidden).toBeFalse(); + }); + + it('sets indicators-hidden to true on a column header when configured as hidden in columnInternals', async () => { + await connect(); + await waitForUpdatesAsync(); + + element.columns[0]!.columnInternals.hideHeaderIndicators = true; + await waitForUpdatesAsync(); + + const header = pageObject.getHeaderElement(0); + expect(header.indicatorsHidden).toBeTrue(); + }); + it('can set data before the element is connected', async () => { await element.setData(simpleTableData); await connect();