diff --git a/change/@ni-nimble-components-a5fd15e6-373b-4e3c-b562-fc2e440df26c.json b/change/@ni-nimble-components-a5fd15e6-373b-4e3c-b562-fc2e440df26c.json new file mode 100644 index 0000000000..cf6adffc95 --- /dev/null +++ b/change/@ni-nimble-components-a5fd15e6-373b-4e3c-b562-fc2e440df26c.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "Create token representing the table's height with all rows visible; **Breaking change:** the table now specifies a max-height; if a different max-height is required, it needs to be configured.", + "packageName": "@ni/nimble-components", + "email": "20542556+mollykreis@users.noreply.github.com", + "dependentChangeType": "major" +} diff --git a/change/@ni-nimble-tokens-2857f29e-c895-406b-a302-78a6ca683d9d.json b/change/@ni-nimble-tokens-2857f29e-c895-406b-a302-78a6ca683d9d.json new file mode 100644 index 0000000000..6e88592f4b --- /dev/null +++ b/change/@ni-nimble-tokens-2857f29e-c895-406b-a302-78a6ca683d9d.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Create token representing the table's height with all rows visible", + "packageName": "@ni/nimble-tokens", + "email": "20542556+mollykreis@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/nimble-components/src/table/styles.ts b/packages/nimble-components/src/table/styles.ts index 4fc0dec644..6f8b73e4eb 100644 --- a/packages/nimble-components/src/table/styles.ts +++ b/packages/nimble-components/src/table/styles.ts @@ -10,7 +10,10 @@ import { mediumPadding, standardPadding, tableRowBorderColor, - borderHoverColor + borderHoverColor, + controlHeight, + tableFitRowsHeight, + borderWidth } from '../theme-provider/design-tokens'; import { Theme } from '../theme-provider/types'; import { hexToRgbaCssColor } from '../utilities/style/colors'; @@ -25,6 +28,16 @@ export const styles = css` :host { height: 480px; + ${tableFitRowsHeight.cssCustomProperty}: calc(var(--ni-private-table-scroll-height) + ${controlHeight}); + ${ + /** + * Set a default maximum height for the table of 40.5 rows plus the header row so + * that clients don't accidentally create a table that tries to render too many rows at once. + * If needed, the max-height can be overridden by the client, but setting a default ensures + * that the max-height is considered if a larger one is needed rather than being overlooked. + */ '' + } + max-height: calc(${controlHeight} + (40.5 * (2 * ${borderWidth} + ${controlHeight}))); --ni-private-column-divider-width: 2px; --ni-private-column-divider-padding: 3px; } diff --git a/packages/nimble-components/src/table/template.ts b/packages/nimble-components/src/table/template.ts index 32ae5addd7..ef635115fa 100644 --- a/packages/nimble-components/src/table/template.ts +++ b/packages/nimble-components/src/table/template.ts @@ -41,11 +41,11 @@ export const template = html` aria-multiselectable="${x => x.ariaMultiSelectable}" ${children({ property: 'childItems', filter: elements() })} > +
+ ${tableTag} { + height: var(${tableFitRowsHeight.cssCustomProperty}); + max-height: none; + } + <${tableTag} ${ref('tableRef')} data-unused="${x => x.updateData(x)}" - ${/* Make the table big enough to remove vertical scrollbar */ ''} - style="height: calc((34px * var(--data-length)) + 32px);" > <${tableColumnAnchorTag} target="_top" column-id="component-name-column" @@ -650,10 +655,6 @@ const metadata: Meta = { const data = components.filter(component => (x.status === 'future' ? isFuture(component) : !isFuture(component))); - x.tableRef.style.setProperty( - '--data-length', - data.length.toString() - ); await x.tableRef.setData(data); })(); }, diff --git a/packages/storybook/src/nimble/icon-base/icons.stories.ts b/packages/storybook/src/nimble/icon-base/icons.stories.ts index 6809da619f..3786a2e938 100644 --- a/packages/storybook/src/nimble/icon-base/icons.stories.ts +++ b/packages/storybook/src/nimble/icon-base/icons.stories.ts @@ -12,6 +12,7 @@ import { mappingIconTag } from '../../../../nimble-components/src/mapping/icon'; import { tableColumnTextTag } from '../../../../nimble-components/src/table-column/text'; import { IconSeverity } from '../../../../nimble-components/src/icon-base/types'; import { iconMetadata } from '../../../../nimble-components/src/icon-base/tests/icon-metadata'; +import { tableFitRowsHeight } from '../../../../nimble-components/src/theme-provider/design-tokens'; import { apiCategory, createUserSelectedThemeStory, @@ -57,7 +58,6 @@ const updateData = (tableRef: Table): void => { // Safari workaround: the table element instance is made at this point // but doesn't seem to be upgraded to a custom element yet await customElements.whenDefined('nimble-table'); - tableRef.style.setProperty('--data-length', data.length.toString()); await tableRef.setData(data); })(); }; @@ -82,10 +82,14 @@ export const icons: StoryObj = { } }, render: createUserSelectedThemeStory(html` + <${tableTag} ${ref('tableRef')} - ${/* Make the table big enough to remove vertical scrollbar */ ''} - style="height: calc((34px * var(--data-length)) + 32px);" data-unused="${x => updateData(x.tableRef)}" > <${tableColumnMappingTag} field-name="tag" key-type="string" fractional-width="0.2" > diff --git a/packages/storybook/src/nimble/table/table-fit-rows-height-matrix.stories.ts b/packages/storybook/src/nimble/table/table-fit-rows-height-matrix.stories.ts new file mode 100644 index 0000000000..c6f5402650 --- /dev/null +++ b/packages/storybook/src/nimble/table/table-fit-rows-height-matrix.stories.ts @@ -0,0 +1,86 @@ +import type { Meta, StoryFn } from '@storybook/html'; +import { html, ViewTemplate } from '@microsoft/fast-element'; +import { tableColumnTextTag } from '../../../../nimble-components/src/table-column/text'; +import { Table, tableTag } from '../../../../nimble-components/src/table'; +import type { TableRecord } from '../../../../nimble-components/src/table/types'; +import { tableFitRowsHeight } from '../../../../nimble-components/src/theme-provider/design-tokens'; +import { createFixedThemeStory } from '../../utilities/storybook'; +import { createMatrix, sharedMatrixParameters } from '../../utilities/matrix'; +import { backgroundStates } from '../../utilities/states'; + +interface SimpleData extends TableRecord { + firstName: string; + lastName: string; + favoriteColor: string; +} + +const data: SimpleData[] = []; +for (let i = 0; i < 50; i++) { + data.push({ + firstName: `First Name ${i}`, + lastName: `Last Name ${i}`, + favoriteColor: `Favorite Color ${i}` + }); +} + +const groupingStates = [ + ['Not Grouped', undefined], + ['Grouped', 0] +] as const; +type GroupingState = (typeof groupingStates)[number]; + +const metadata: Meta = { + title: 'Tests/Table', + parameters: { + ...sharedMatrixParameters() + } +}; + +export default metadata; + +// prettier-ignore +const component = ( + [_groupingName, groupIndex]: GroupingState +): ViewTemplate => html` + + <${tableTag}> + <${tableColumnTextTag} field-name="firstName" group-index="${() => groupIndex}">First Name + <${tableColumnTextTag} field-name="lastName">Last Name + <${tableColumnTextTag} field-name="favoriteColor">Favorite Color + +`; + +const playFunction = async (rowCount: number): Promise => { + const tableData = data.slice(0, rowCount); + await Promise.all( + Array.from(document.querySelectorAll
(tableTag)).map( + async table => { + await table.setData(tableData); + } + ) + ); +}; + +export const tableFitRowsHeightWith5Rows: StoryFn = createFixedThemeStory( + createMatrix(component, [groupingStates]), + backgroundStates[0] +); + +tableFitRowsHeightWith5Rows.play = async () => playFunction(5); + +export const tableFitRowsHeightWith10Rows: StoryFn = createFixedThemeStory( + createMatrix(component, [groupingStates]), + backgroundStates[0] +); +tableFitRowsHeightWith10Rows.play = async () => playFunction(10); + +export const tableFitRowsHeightWith50Rows: StoryFn = createFixedThemeStory( + createMatrix(component, [groupingStates]), + backgroundStates[0] +); +tableFitRowsHeightWith50Rows.play = async () => playFunction(50); diff --git a/packages/storybook/src/nimble/table/table.mdx b/packages/storybook/src/nimble/table/table.mdx index a51cb7022b..2de5143dde 100644 --- a/packages/storybook/src/nimble/table/table.mdx +++ b/packages/storybook/src/nimble/table/table.mdx @@ -2,6 +2,7 @@ import { Controls, Canvas, Meta, Title } from '@storybook/blocks'; import * as tableStories from './table.stories'; import ComponentApisLink from '../../docs/component-apis-link.mdx'; import { tableTag } from '../../../../nimble-components/src/table'; +import { tableFitRowsHeight } from '../../../../nimble-components/src/theme-provider/design-tokens'; @@ -23,10 +24,13 @@ and the **Table Column** pages for individual table column types. ### Sizing -The <Tag name={tableTag}/> should be sized explicitly or sized to fill the space -of a parent container. The <Tag name={tableTag}/> does not currently support -being styled with `height: auto`. The ability to auto size the table is tracked -with [issue 1624](https://github.com/ni/nimble/issues/1624). +The table's height can be configured to grow to fit all rows without a scrollbar +by styling the table's height with the <code>{tableFitRowsHeight.name}</code> +token. + +The table has a default maximum height that will render approximately 40 rows to +avoid performance problems. Use caution when overriding the maximum height of +the table because this may lead to performance issues. ### Full bleed diff --git a/packages/storybook/src/nimble/table/table.stories.ts b/packages/storybook/src/nimble/table/table.stories.ts index 93b98982fa..c6e759796e 100644 --- a/packages/storybook/src/nimble/table/table.stories.ts +++ b/packages/storybook/src/nimble/table/table.stories.ts @@ -1,6 +1,7 @@ import { html, ref } from '@microsoft/fast-element'; import { withActions } from '@storybook/addon-actions/decorator'; import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html'; +import { tableFitRowsHeight } from '../../../../nimble-components/src/theme-provider/design-tokens'; import { iconUserTag } from '../../../../nimble-components/src/icons/user'; import { menuTag } from '../../../../nimble-components/src/menu'; import { menuItemTag } from '../../../../nimble-components/src/menu-item'; @@ -13,6 +14,10 @@ import { TableRowSelectionMode } from '../../../../nimble-components/src/table/types'; import { ExampleDataType } from '../../../../nimble-components/src/table/tests/types'; +import { + scssPropertySetterMarkdown, + tokenNames +} from '../../../../nimble-components/src/theme-provider/design-token-names'; import { addLabelUseMetadata, type LabelUserArgs @@ -78,6 +83,7 @@ interface TableArgs extends BaseTableArgs { selectionChange: undefined; columnConfigurationChange: undefined; rowExpandToggle: undefined; + fitRowsHeight: boolean; } const simpleData = [ @@ -263,9 +269,18 @@ const setSelectedRecordIdsDescription = `A function that makes the rows associat If a record does not exist in the table's data, it will not be selected. If multiple record IDs are specified when the table's selection mode is \`single\`, only the first record that exists in the table's data will become selected.`; +const fitRowsHeightDescription = `Style the table with ${scssPropertySetterMarkdown(tokenNames.tableFitRowsHeight, 'height')} to make the table's height grow to fit all rows. + +See the **Sizing** section for information on sizing the table.`; + export const table: StoryObj<TableArgs> = { // prettier-ignore render: createUserSelectedThemeStory(html<TableArgs>` + <style class="code-hide"> + nimble-table { + ${x => (x.fitRowsHeight ? `height: var(${tableFitRowsHeight.cssCustomProperty})` : '')} + } + </style> <${tableTag} ${ref('tableRef')} selection-mode="${x => TableRowSelectionMode[x.selectionMode]}" @@ -485,6 +500,11 @@ export const table: StoryObj<TableArgs> = { 'Event emitted when the user expands or collapses a row in a table with hierarchy. This does not emit when group rows are expanded or collapsed.', control: false, table: { category: apiCategory.events } + }, + fitRowsHeight: { + name: 'Fit rows height', + description: fitRowsHeightDescription, + table: { category: apiCategory.styles } } }, args: { @@ -494,6 +514,7 @@ export const table: StoryObj<TableArgs> = { validity: undefined, checkValidity: undefined, tableRef: undefined, + fitRowsHeight: false, updateData: x => { void (async () => { // Safari workaround: the table element instance is made at this point