Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions pages/app-layout/auto-skeleton-table.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import AppLayout from '~components/app-layout';
import Button from '~components/button';
import Header from '~components/header';
import Table, { TableProps } from '~components/table';

import ScreenshotArea from '../utils/screenshot-area';
import { Breadcrumbs, Footer, Navigation, Notifications } from './utils/content-blocks';
import labels from './utils/labels';

interface Item {
id: string;
name: string;
owner: string;
}

const columnDefinitions: TableProps.ColumnDefinition<Item>[] = [
{
id: 'id',
header: 'Resource identifier used to locate the item in the inventory',
cell: item => item.id,
},
{
id: 'name',
header: 'Resource name shown to customers in the management console',
cell: item => item.name,
},
{
id: 'owner',
header: 'Owning team responsible for operating this resource',
cell: item => item.owner,
},
];

const scenarioId = 'auto-skeleton-app-layout';

export default function AutoSkeletonTablePage() {
return (
<ScreenshotArea gutters={false}>
<div id={scenarioId}>
<AppLayout
ariaLabels={labels}
breadcrumbs={<Breadcrumbs />}
contentType="table"
footerSelector="#f"
headerSelector="#h"
navigation={<Navigation />}
notifications={<Notifications />}
content={
<Table<Item>
columnDefinitions={columnDefinitions}
footer={<div id={`${scenarioId}-table-footer`}>Table footer</div>}
header={
<Header
actions={<Button variant="primary">Create resource</Button>}
description="Automatic skeleton rows fill the AppLayout content viewport."
variant="awsui-h1-sticky"
>
Resources
</Header>
}
items={[]}
loading={true}
loadingText="Loading resources"
skeleton={{ totalRows: 'auto' }}
stickyHeader={true}
variant="full-page"
wrapLines={true}
/>
}
/>
<Footer legacyConsoleNav={false} />
</div>
</ScreenshotArea>
);
}
124 changes: 124 additions & 0 deletions pages/table/auto-skeleton-rows.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import Header from '~components/header';
import SpaceBetween from '~components/space-between';
import Table, { TableProps } from '~components/table';

interface Item {
id: string;
name: string;
owner: string;
}

const columnDefinitions: TableProps.ColumnDefinition<Item>[] = [
{ id: 'id', header: 'ID', cell: item => item.id },
{ id: 'name', header: 'Name', cell: item => item.name },
];

const longHeaderColumnDefinitions: TableProps.ColumnDefinition<Item>[] = [
{
id: 'id',
header: 'Resource identifier used to locate the item in the inventory',
cell: item => item.id,
},
{
id: 'name',
header: 'Resource name shown to customers in the management console',
cell: item => item.name,
},
{
id: 'owner',
header: 'Owning team responsible for operating this resource',
cell: item => item.owner,
},
];

const partialItems: Item[] = [
{ id: '1', name: 'First resource', owner: 'Team A' },
{ id: '2', name: 'Second resource', owner: 'Team B' },
{ id: '3', name: 'Third resource', owner: 'Team C' },
];

interface SkeletonScenarioProps {
footer?: boolean;
id: string;
items?: readonly Item[];
longHeaders?: boolean;
stickyHeader?: boolean;
title: string;
wrapLines?: boolean;
}

function SkeletonScenario({
footer,
id,
items = [],
longHeaders,
stickyHeader,
title,
wrapLines,
}: SkeletonScenarioProps) {
return (
<div id={id} style={{ blockSize: '320px', inlineSize: longHeaders ? '360px' : undefined, overflowY: 'auto' }}>
<Table
columnDefinitions={longHeaders ? longHeaderColumnDefinitions : columnDefinitions}
items={items}
loading={true}
loadingText="Loading items"
skeleton={{ totalRows: 'auto' }}
stickyHeader={stickyHeader}
wrapLines={wrapLines}
footer={footer ? <div id={`${id}-footer`}>Footer content</div> : undefined}
header={<Header description="Skeleton rows fill the visible scroll viewport.">{title}</Header>}
/>
</div>
);
}

export default function AutoSkeletonRowsPage() {
return (
<SpaceBetween size="l">
<Header variant="h1">Automatic skeleton rows</Header>
<div id="auto-skeleton-scroll-container" style={{ blockSize: '400px', overflowY: 'auto' }}>
<div style={{ blockSize: '64px' }} />
<div id="auto-skeleton-inner-scroll-container" style={{ blockSize: '320px', overflowY: 'auto' }}>
<Table
columnDefinitions={columnDefinitions}
items={[]}
loading={true}
loadingText="Loading items"
skeleton={{ totalRows: 'auto' }}
header={
<Header description="Skeleton rows fill the visible scroll viewport.">Nested scroll viewport</Header>
}
/>
</div>
</div>
<SkeletonScenario id="auto-skeleton-sticky-header" stickyHeader={true} title="Sticky header" />
<SkeletonScenario footer={true} id="auto-skeleton-footer" title="Footer" />
<SkeletonScenario
id="auto-skeleton-long-headers-nowrap"
longHeaders={true}
title="Long headers without wrapping"
wrapLines={false}
/>
<SkeletonScenario
id="auto-skeleton-long-headers-wrap"
longHeaders={true}
title="Long headers with wrapping"
wrapLines={true}
/>
<SkeletonScenario id="auto-skeleton-mixed-rows" items={partialItems} title="Mixed data and skeleton rows" />
<SkeletonScenario
footer={true}
id="auto-skeleton-all-features"
longHeaders={true}
stickyHeader={true}
title="Combined features"
wrapLines={true}
/>
</SpaceBetween>
);
}
17 changes: 15 additions & 2 deletions pages/table/skeleton-rows.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ type SelectionMode = 'none' | 'single' | 'multi';

export default function TableSkeletonRowsPage() {
const { urlParams, setUrlParams } = useAppContext<
'loadingState' | 'skeletonRows' | 'dataRows' | 'stripedRows' | 'selectionMode'
'loadingState' | 'skeletonRows' | 'dataRows' | 'stripedRows' | 'selectionMode' | 'autoSkeletonRows'
>();

const loadingState = (urlParams.loadingState || 'skeleton') as LoadingState;
const skeletonRowsCount = String(urlParams.skeletonRows || '5');
const dataRowsCount = String(urlParams.dataRows || '10');
const stripedRows = urlParams.stripedRows !== 'false' && urlParams.stripedRows !== false;
const autoSkeletonRows = urlParams.autoSkeletonRows === true || urlParams.autoSkeletonRows === 'true';
const selectionMode = (urlParams.selectionMode || 'multi') as SelectionMode;

const [selectedItems, setSelectedItems] = useState<Item[]>([]);
Expand Down Expand Up @@ -135,6 +136,12 @@ export default function TableSkeletonRowsPage() {
>
Striped rows
</Checkbox>
<Checkbox
checked={autoSkeletonRows}
onChange={({ detail }) => setUrlParams({ autoSkeletonRows: detail.checked })}
>
Fill the viewport automatically
</Checkbox>
</FormField>
</ColumnLayout>
</SpaceBetween>
Expand All @@ -144,7 +151,13 @@ export default function TableSkeletonRowsPage() {
columnDefinitions={columnDefinitions}
items={items}
enableKeyboardNavigation={true}
skeleton={loadingState === 'skeleton' ? { totalRows: skeletonRows } : undefined}
skeleton={
loadingState === 'skeleton'
? autoSkeletonRows
? { totalRows: 'auto', maxAutoRows: 10 }
: { totalRows: skeletonRows }
: undefined
}
loading={loadingState !== 'data'}
loadingText="Loading items..."
empty="No items to display"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28964,17 +28964,19 @@ the table items array is empty.",
"description": "Renders skeleton placeholder rows to fill the table while data is loading. Accepts:
- \`totalRows\` (number) - The total number of rows that should be rendered. If \`items\`
are also provided, those items will be rendered first, and \`totalRows - items.length\`
additional skeleton rows rendered after.",
additional skeleton rows rendered after.
- \`totalRows\` ('auto') - The number of skeleton rows is calculated from the available viewport height.
- \`maxAutoRows\` (number) - Limits the number of skeleton rows rendered when \`totalRows\` is set to \`'auto'\`.
- \`minAutoRows\` (number) - Sets the minimum number of skeleton rows rendered when \`totalRows\` is set to \`'auto'\`.
Defaults to 1. Useful for tables rendered off-screen, where the calculated available height would
otherwise yield a single row.",
"inlineType": {
"name": "TableProps.SkeletonConfig",
"properties": [
{
"name": "totalRows",
"optional": false,
"type": "number",
},
"type": "union",
"values": [
"TableProps.FixedSkeletonConfig",
"TableProps.AutoSkeletonConfig",
],
"type": "object",
},
"name": "skeleton",
"optional": true,
Expand Down
52 changes: 51 additions & 1 deletion src/internal/utils/__tests__/scrollable-containers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import React from 'react';
import { render } from '@testing-library/react';

import { getFirstScrollableParent, scrollRectangleIntoView } from '../scrollable-containers';
import { getFirstScrollableParent, getScrollableParents, scrollRectangleIntoView } from '../scrollable-containers';

const originalScrollBy = window.scrollBy;

Expand Down Expand Up @@ -66,3 +66,53 @@ describe('getFirstScrollableParent', () => {
expect(getFirstScrollableParent(inner)).toBe(undefined);
});
});

describe('getScrollableParents', () => {
const originalGetComputedStyle = window.getComputedStyle;

afterEach(() => {
window.getComputedStyle = originalGetComputedStyle;
});

function mockOverflowByOverflowY(overflowByTestId: Record<string, string>) {
window.getComputedStyle = ((element: Element, pseudoElt?: string | null) => {
const result = originalGetComputedStyle(element as Element, pseudoElt);
const testId = (element as HTMLElement).dataset?.testid;
if (testId && overflowByTestId[testId]) {
result.overflowY = overflowByTestId[testId];
}
return result;
}) as Window['getComputedStyle'];
}

test('returns scrollable ancestors nearest-first, excluding non-scrollable ones', () => {
const { container } = render(
<div data-testid="outer">
<div data-testid="middle">
<div data-testid="scroll-parent">
<div data-testid="target" />
</div>
</div>
</div>
);
mockOverflowByOverflowY({ outer: 'scroll', middle: 'visible', 'scroll-parent': 'auto' });

const target = container.querySelector<HTMLElement>('[data-testid="target"]')!;
const outer = container.querySelector<HTMLElement>('[data-testid="outer"]')!;
const scrollParent = container.querySelector<HTMLElement>('[data-testid="scroll-parent"]')!;

expect(getScrollableParents(target)).toEqual([scrollParent, outer]);
});

test('returns an empty array when no ancestor is scrollable', () => {
const { container } = render(
<div data-testid="outer">
<div data-testid="target" />
</div>
);
mockOverflowByOverflowY({ outer: 'visible' });

const target = container.querySelector<HTMLElement>('[data-testid="target"]')!;
expect(getScrollableParents(target)).toEqual([]);
});
});
18 changes: 18 additions & 0 deletions src/internal/utils/scrollable-containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import { findUpUntil } from '@cloudscape-design/component-toolkit/dom';

import { isHTMLElement } from './dom';

export interface BoundingBox {
blockSize: number;
inlineSize: number;
Expand Down Expand Up @@ -117,6 +119,22 @@ export function scrollRectangleIntoView(box: BoundingBox, scrollableParent?: HTM
}
}

/**
* Returns the element's ancestors whose computed `overflow-y` is `auto` or `scroll`,
* ordered nearest-first. These are the ancestors capable of producing a vertical scrollbar.
*/
export function getScrollableParents(element: HTMLElement): HTMLElement[] {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a test for this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implicitly, but now added an explicit test too

const parents: HTMLElement[] = [];
let node = element.parentElement;
while (node) {
if (isHTMLElement(node) && ['auto', 'scroll'].includes(getComputedStyle(node).overflowY)) {
parents.push(node);
}
node = node.parentElement;
}
return parents;
}

export function getFirstScrollableParent(element: HTMLElement): HTMLElement | undefined {
return (
findUpUntil(element, el => {
Expand Down
Loading
Loading