diff --git a/pages/table-root/header-truncation.page.tsx b/pages/table-root/header-truncation.page.tsx new file mode 100644 index 00000000000..ad23e8f4d27 --- /dev/null +++ b/pages/table-root/header-truncation.page.tsx @@ -0,0 +1,48 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import TableBody from '~components/table-body'; +import TableBodyCell from '~components/table-body-cell'; +import TableHead from '~components/table-head'; +import TableHeaderCell from '~components/table-header-cell'; +import TableRoot, { TableRootProps } from '~components/table-root'; +import TableRow from '~components/table-row'; + +import { SimplePage } from '../app/templates'; + +import styles from './styles.scss'; + +// Each column is narrower than its header label, so every header truncates. +const NARROW_COLUMNS: ReadonlyArray = [{ size: 120 }, { size: 100 }, { size: 100 }]; + +export default function TableHeaderTruncationPage() { + return ( + + + + + + + + @billedDuration + @memoryAllocated + + + + + 1,700,000 + 120 + 512 + + + + + ); +} diff --git a/src/table-header-cell/__integ__/truncation.test.ts b/src/table-header-cell/__integ__/truncation.test.ts new file mode 100644 index 00000000000..18a922241a2 --- /dev/null +++ b/src/table-header-cell/__integ__/truncation.test.ts @@ -0,0 +1,62 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects'; +import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; + +import bodyCellStyles from '../../../lib/components/table/body-cell/styles.selectors.js'; +import styles from '../../../lib/components/table-header-cell/styles.selectors.js'; + +const pageUrl = '#/light/table-root/header-truncation'; +const headerContent = `[data-testid="narrow-table"] .${styles['header-cell-content']}`; +const firstHeaderButton = '[data-testid="first-header-button"]'; + +test( + 'truncates header text that is wider than its column with an ellipsis', + useBrowser(async browser => { + await browser.url(pageUrl); + const page = new BasePageObject(browser); + await page.waitForVisible(headerContent); + + const contents = await browser.execute((selector: string) => { + return Array.from(document.querySelectorAll(selector)).map(element => ({ + overflowsContent: element.scrollWidth > element.clientWidth, + textOverflow: getComputedStyle(element).textOverflow, + rightOverhang: element.getBoundingClientRect().right - element.parentElement!.getBoundingClientRect().right, + })); + }, headerContent); + + expect(contents).toHaveLength(3); + for (const content of contents) { + expect(content.overflowsContent).toBe(true); + expect(content.textOverflow).toBe('ellipsis'); + expect(content.rightOverhang).toBeLessThanOrEqual(0); + } + }) +); + +test( + 'leaves room inside the clip for a focus ring on the first column, without moving its text', + useBrowser(async browser => { + await browser.url(pageUrl); + const page = new BasePageObject(browser); + await page.waitForVisible(firstHeaderButton); + + const { buttonLeft, clipLeft, bodyTextLeft } = await browser.execute( + (buttonSelector: string, bodyContentClass: string) => { + const button = document.querySelector(buttonSelector)!; + const bodyContent = document.querySelector(`.${bodyContentClass}`)!; + return { + buttonLeft: button.getBoundingClientRect().left, + clipLeft: button.parentElement!.getBoundingClientRect().left, + bodyTextLeft: + bodyContent.getBoundingClientRect().left + parseFloat(getComputedStyle(bodyContent).paddingInlineStart), + }; + }, + firstHeaderButton, + bodyCellStyles['body-cell-content'] + ); + + expect(buttonLeft - clipLeft).toBeGreaterThanOrEqual(4); + expect(buttonLeft).toBe(bodyTextLeft); + }) +); diff --git a/src/table-header-cell/styles.scss b/src/table-header-cell/styles.scss index dfae49be290..6c4c4b75483 100644 --- a/src/table-header-cell/styles.scss +++ b/src/table-header-cell/styles.scss @@ -43,9 +43,18 @@ padding-inline: 0; } +.header-cell:not(.disable-paddings) > .header-cell-content { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + // VR-gated: the substrate is shared with the existing Table, which still supports non-VR mode. -.header-cell.is-visual-refresh:first-child > .header-cell-content { - padding-inline-start: 0; +// The text sits flush with the cell's start edge, but the padding/margin pair keeps room inside the +// truncation clip so a focus ring on the first control isn't cut off. +.header-cell.is-visual-refresh:first-child:not(.disable-paddings) > .header-cell-content { + padding-inline-start: awsui.$space-button-focus-outline-gutter; + margin-inline-start: calc(-1 * #{awsui.$space-button-focus-outline-gutter}); } // Stretch to the row track, else an empty control header collapses to ~1px, leaving a stray divider stub.