Skip to content

Commit 51ed2e1

Browse files
feat: Sticky horizontal scrollbar for atomic table components (#5236)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent bee4ff3 commit 51ed2e1

18 files changed

Lines changed: 367 additions & 49 deletions

‎pages/table-fragments/sticky-scrollbar-custom.page.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import React, { useRef } from 'react';
44

55
import { Box, Link } from '~components';
6+
import { StickyScrollbar } from '~components/internal/components/sticky-scrollbar';
67
import { useScrollSync } from '~components/internal/hooks/use-scroll-sync';
7-
import { StickyScrollbar } from '~components/table/sticky-scrollbar';
88

99
import { generateItems, Instance } from '../table/generate-data';
1010

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React from 'react';
4+
5+
import SpaceBetween from '~components/space-between';
6+
import TableBody from '~components/table-body';
7+
import TableBodyCell from '~components/table-body-cell';
8+
import TableHead from '~components/table-head';
9+
import TableHeaderCell from '~components/table-header-cell';
10+
import TableRoot, { TableRootProps } from '~components/table-root';
11+
import TableRow from '~components/table-row';
12+
13+
import { SimplePage } from '../app/templates';
14+
15+
import styles from './styles.scss';
16+
17+
// Wide, tall tables whose horizontal scrollbar would otherwise sit below their last row: one scrolled by
18+
// the page, one inside a fixed-height container.
19+
20+
const COLUMN_COUNT = 8;
21+
const ROW_COUNT = 60;
22+
const COLUMNS: ReadonlyArray<TableRootProps.ColumnDefinition> = Array.from({ length: COLUMN_COUNT }, () => ({
23+
size: 240,
24+
}));
25+
26+
function WideTable({ label, testId }: { label: string; testId: string }) {
27+
return (
28+
<TableRoot columnLayout={{ type: 'grid', columns: COLUMNS }} ariaLabel={label} data-testid={testId}>
29+
<TableHead>
30+
<TableRow>
31+
{COLUMNS.map((_, column) => (
32+
<TableHeaderCell key={column}>Column {column + 1}</TableHeaderCell>
33+
))}
34+
</TableRow>
35+
</TableHead>
36+
<TableBody>
37+
{Array.from({ length: ROW_COUNT }, (_, row) => (
38+
<TableRow key={row}>
39+
{COLUMNS.map((_, column) => (
40+
<TableBodyCell key={column}>
41+
Row {row + 1}, column {column + 1}
42+
</TableBodyCell>
43+
))}
44+
</TableRow>
45+
))}
46+
</TableBody>
47+
</TableRoot>
48+
);
49+
}
50+
51+
export default function TableStickyScrollbarPage() {
52+
return (
53+
<SimplePage title="Table atomics — sticky scrollbar" screenshotArea={{}}>
54+
<SpaceBetween size="l">
55+
<h2>Inside a fixed-height container</h2>
56+
<div className={styles['scroll-container']} data-testid="scroll-container">
57+
<WideTable label="Resources in a container" testId="container-table" />
58+
</div>
59+
<h2>Scrolled with the page</h2>
60+
<WideTable label="Resources on the page" testId="page-table" />
61+
</SpaceBetween>
62+
</SimplePage>
63+
);
64+
}

‎pages/table-root/styles.scss‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,9 @@
4242
align-items: center;
4343
gap: tokens.$space-static-xxs;
4444
}
45+
46+
// A fixed-height region that scrolls its table in both directions.
47+
.scroll-container {
48+
block-size: 400px;
49+
overflow: auto;
50+
}

‎src/app-layout/__integ__/app-layout-split-panel.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { viewports } from './constants';
88
import { AppLayoutSplitViewPage, getUrlParams } from './utils';
99

1010
import mobileStyles from '../../../lib/components/app-layout/mobile-toolbar/styles.selectors.js';
11-
import tableScrollbarStyles from '../../../lib/components/table/sticky-scrollbar/styles.selectors.js';
11+
import tableScrollbarStyles from '../../../lib/components/internal/components/sticky-scrollbar/styles.selectors.js';
1212

1313
const scrollbarSelector = `.${tableScrollbarStyles['sticky-scrollbar-visible']}`;
1414

src/table/sticky-scrollbar/__tests__/sticky-scrollbar.test.tsx renamed to src/internal/components/sticky-scrollbar/__tests__/sticky-scrollbar.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
import React from 'react';
55
import { render } from '@testing-library/react';
66

7-
import { browserScrollbarSize } from '../../../../lib/components/internal/utils/browser-scrollbar-size';
8-
import { StickyScrollbar } from '../../../../lib/components/table/sticky-scrollbar';
7+
import { StickyScrollbar } from '../../../../../lib/components/internal/components/sticky-scrollbar';
8+
import { browserScrollbarSize } from '../../../../../lib/components/internal/utils/browser-scrollbar-size';
99

10-
import styles from '../../../../lib/components/table/sticky-scrollbar/styles.css.js';
10+
import styles from '../../../../../lib/components/internal/components/sticky-scrollbar/styles.css.js';
1111

12-
jest.mock('../../../../lib/components/internal/utils/browser-scrollbar-size');
12+
jest.mock('../../../../../lib/components/internal/utils/browser-scrollbar-size');
1313
const browserScrollbarSizeMock = jest.mocked(browserScrollbarSize);
1414

1515
it('adds an offset if the scrollbar is an overlay', () => {

src/table/sticky-scrollbar/__tests__/update-position.test.ts renamed to src/internal/components/sticky-scrollbar/__tests__/update-position.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import globalVars from '../../../../lib/components/internal/styles/global-vars';
5-
import { updatePosition } from '../../../../lib/components/table/sticky-scrollbar/use-sticky-scrollbar';
4+
import { updatePosition } from '../../../../../lib/components/internal/components/sticky-scrollbar/use-sticky-scrollbar';
5+
import globalVars from '../../../../../lib/components/internal/styles/global-vars';
66

77
describe('updatePosition', () => {
88
function setupElements() {
@@ -30,4 +30,11 @@ describe('updatePosition', () => {
3030
expect(scrollbar.style.inlineSize).toBe('600px');
3131
expect(scrollbarContent.style.inlineSize).toBe('800px');
3232
});
33+
34+
test('sizes the content from a custom measurement when one is given', () => {
35+
const { scrollbar, scrollbarContent, table, wrapper } = setupElements();
36+
updatePosition(table, wrapper, scrollbar, scrollbarContent, true, () => 1200);
37+
expect(scrollbar.style.inlineSize).toBe('600px');
38+
expect(scrollbarContent.style.inlineSize).toBe('1200px');
39+
});
3340
});
File renamed without changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// For a scroller whose horizontal scrollbar is replaced by the sticky scrollbar.
5+
@mixin hidden-scrollbar {
6+
scrollbar-width: none; /* Firefox */
7+
&::-webkit-scrollbar {
8+
display: none; /* Safari and Chrome */
9+
}
10+
}

src/table/sticky-scrollbar/sticky-scrollbar.tsx renamed to src/internal/components/sticky-scrollbar/sticky-scrollbar.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import clsx from 'clsx';
55

66
import { useMergeRefs } from '@cloudscape-design/component-toolkit/internal';
77

8-
import { useIntersectionObserver } from '../../internal/hooks/use-intersection-observer';
9-
import { useVisualRefresh } from '../../internal/hooks/use-visual-mode';
10-
import { browserScrollbarSize } from '../../internal/utils/browser-scrollbar-size';
11-
import { useStickyScrollbar } from './use-sticky-scrollbar';
8+
import { useIntersectionObserver } from '../../hooks/use-intersection-observer';
9+
import { useVisualRefresh } from '../../hooks/use-visual-mode';
10+
import { browserScrollbarSize } from '../../utils/browser-scrollbar-size';
11+
import { StickyScrollbarContentOptions, useStickyScrollbar } from './use-sticky-scrollbar';
1212

1313
import styles from './styles.css.js';
1414

15-
interface StickyScrollbarProps {
15+
interface StickyScrollbarProps extends StickyScrollbarContentOptions {
1616
wrapperRef: React.RefObject<HTMLDivElement>;
1717
tableRef: React.RefObject<HTMLTableElement>;
1818
onScroll?: React.UIEventHandler<HTMLDivElement>;
@@ -22,7 +22,7 @@ interface StickyScrollbarProps {
2222
export default forwardRef(StickyScrollbar);
2323

2424
function StickyScrollbar(
25-
{ wrapperRef, tableRef, onScroll, hasStickyColumns }: StickyScrollbarProps,
25+
{ wrapperRef, tableRef, onScroll, hasStickyColumns, getContentInlineSize, contentKey }: StickyScrollbarProps,
2626
ref: React.Ref<HTMLDivElement>
2727
) {
2828
const isVisualRefresh = useVisualRefresh();
@@ -36,7 +36,10 @@ function StickyScrollbar(
3636
*/
3737
const offsetScrollbar = hasStickyColumns || browserScrollbarSize().height === 0;
3838

39-
useStickyScrollbar(scrollbarRef, scrollbarContentRef, tableRef, wrapperRef, offsetScrollbar);
39+
useStickyScrollbar(scrollbarRef, scrollbarContentRef, tableRef, wrapperRef, offsetScrollbar, {
40+
getContentInlineSize,
41+
contentKey,
42+
});
4043

4144
const { ref: stickyDetectionRef, isIntersecting: isStickyDetectionVisible } = useIntersectionObserver();
4245

src/table/sticky-scrollbar/styles.scss renamed to src/internal/components/sticky-scrollbar/styles.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
SPDX-License-Identifier: Apache-2.0
44
*/
5-
@use '../../internal/styles/tokens' as awsui;
6-
@use '../../internal/styles' as styles;
5+
@use '../../styles/tokens' as awsui;
6+
@use '../../styles' as styles;
77

88
.sticky-scrollbar {
99
block-size: 15px;

0 commit comments

Comments
 (0)