diff --git a/pages/app-layout/multi-layout-with-table-sticky-header.page.tsx b/pages/app-layout/multi-layout-with-table-sticky-header.page.tsx new file mode 100644 index 0000000000..a3a518777f --- /dev/null +++ b/pages/app-layout/multi-layout-with-table-sticky-header.page.tsx @@ -0,0 +1,58 @@ +// 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 from '~components/table'; + +import { generateItems, Instance } from '../table/generate-data'; +import { columnsConfig } from '../table/shared-configs'; +import ScreenshotArea from '../utils/screenshot-area'; +import { Breadcrumbs, Navigation } from './utils/content-blocks'; +import { Tools } from './utils/content-blocks'; +import labels from './utils/labels'; +import * as toolsContent from './utils/tools-content'; + +const items = generateItems(100); + +export default function () { + return ( + + } + toolsHide={true} + disableContentPaddings={true} + content={ + } + navigationHide={true} + content={ + + header={ +
Create} + > + Sticky Scrollbar Example +
+ } + stickyHeader={true} + variant="full-page" + columnDefinitions={columnsConfig} + items={items} + /> + } + tools={{toolsContent.long}} + /> + } + /> +
+ ); +} diff --git a/src/app-layout/__integ__/awsui-applayout.test.ts b/src/app-layout/__integ__/awsui-applayout.test.ts index 6b5ec24dcf..dbe635c0cd 100644 --- a/src/app-layout/__integ__/awsui-applayout.test.ts +++ b/src/app-layout/__integ__/awsui-applayout.test.ts @@ -5,7 +5,7 @@ import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; import createWrapper from '../../../lib/components/test-utils/selectors'; import { viewports } from './constants'; -import { getUrlParams, Theme } from './utils'; +import { getUrlParams, testIf, Theme } from './utils'; import testutilStyles from '../../../lib/components/app-layout/test-classes/styles.selectors.js'; @@ -197,4 +197,15 @@ describe.each(['classic', 'refresh', 'refresh-toolbar'] as Theme[])('%s', theme await expect(page.getNavPosition()).resolves.toEqual(navBefore); }) ); + + testIf(theme === 'refresh-toolbar')( + 'should keep header visible and in position while scrolling', + setupTest({ pageName: 'multi-layout-with-table-sticky-header' }, async page => { + const tableWrapper = createWrapper().findTable(); + const { top: headerOldOffset } = await page.getBoundingBox(tableWrapper.findHeaderSlot().toSelector()); + await page.windowScrollTo({ top: 200 }); + const { top: headerNewOffset } = await page.getBoundingBox(tableWrapper.findHeaderSlot().toSelector()); + expect(headerOldOffset).toEqual(headerNewOffset); + }) + ); }); diff --git a/src/app-layout/visual-refresh-toolbar/index.tsx b/src/app-layout/visual-refresh-toolbar/index.tsx index 05ddef1d02..be9da70044 100644 --- a/src/app-layout/visual-refresh-toolbar/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/index.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useEffect, useImperativeHandle, useState } from 'react'; +import React, { useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react'; import { useStableCallback } from '@cloudscape-design/component-toolkit/internal'; @@ -9,6 +9,7 @@ import { SplitPanelSideToggleProps } from '../../internal/context/split-panel-co import { fireNonCancelableEvent } from '../../internal/events'; import { useControllable } from '../../internal/hooks/use-controllable'; import { useIntersectionObserver } from '../../internal/hooks/use-intersection-observer'; +import { useMergeRefs } from '../../internal/hooks/use-merge-refs'; import { useMobile } from '../../internal/hooks/use-mobile'; import { useUniqueId } from '../../internal/hooks/use-unique-id'; import { useGetGlobalBreadcrumbs } from '../../internal/plugins/helpers/use-global-breadcrumbs'; @@ -78,6 +79,8 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef(null); const [toolsOpen = false, setToolsOpen] = useControllable(controlledToolsOpen, onToolsChange, false, { componentName: 'AppLayout', @@ -432,16 +435,45 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef { + let currentElement: Element | null = element?.parentElement ?? null; + + // this traverse is needed only for JSDOM + // in real browsers the globalVar will be propagated to all descendants and this loops exits after initial iteration + while (currentElement) { + if (getComputedStyle(currentElement).getPropertyValue(globalVars.stickyVerticalTopOffset)) { + return true; + } + currentElement = currentElement.parentElement; + } + + return false; + }; + + useLayoutEffect(() => { + if (!hasToolbar) { + setIsNested(getIsNestedInAppLayout(rootRef.current)); + } + }, [hasToolbar]); + return ( <> {/* Rendering a hidden copy of breadcrumbs to trigger their deduplication */} {!hasToolbar && breadcrumbs ? {breadcrumbs} : null}