Skip to content

Commit a094ce2

Browse files
authored
chore: Applayout manages the global scroll-padding-top (#3691)
1 parent fbfdb12 commit a094ce2

File tree

6 files changed

+106
-0
lines changed

6 files changed

+106
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 AppLayout from '~components/app-layout';
6+
import Button from '~components/button';
7+
import Header from '~components/header';
8+
import SpaceBetween from '~components/space-between';
9+
10+
import ScreenshotArea from '../utils/screenshot-area';
11+
import ariaLabels from './utils/labels';
12+
13+
export default function () {
14+
return (
15+
<ScreenshotArea gutters={false}>
16+
<AppLayout
17+
ariaLabels={ariaLabels}
18+
navigationHide={true}
19+
content={
20+
<div style={{ height: '200vh', border: '1px solid red' }}>
21+
<div style={{ marginBlockEnd: '1rem' }}>
22+
<Header data-testid="header" variant="h1">
23+
Focusable components
24+
</Header>
25+
</div>
26+
<SpaceBetween direction="vertical" size="xxl">
27+
<Button data-testid="button-1" ariaLabel="Button 1">
28+
Button 1
29+
</Button>
30+
<Button data-testid="button-2" ariaLabel="Button 2">
31+
Button 2
32+
</Button>
33+
</SpaceBetween>
34+
</div>
35+
}
36+
/>
37+
</ScreenshotArea>
38+
);
39+
}

src/app-layout/__integ__/awsui-applayout.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,38 @@ describe.each(['classic', 'refresh', 'refresh-toolbar'] as Theme[])('%s', theme
199199
})
200200
);
201201

202+
(theme !== 'classic' ? test : test.skip)(
203+
'element should not be hidden under the sticky header when focused',
204+
setupTest({ pageName: 'global-scroll-padding' }, async page => {
205+
// Getting the header offset depending on the theme
206+
let headerOffset = (await page.getBoundingBox('#h')).height;
207+
if (theme === 'refresh-toolbar') {
208+
headerOffset += (await page.getBoundingBox(wrapper.findToolbar().toSelector())).height;
209+
}
210+
211+
// Set the focus to the second button
212+
await page.click(wrapper.findContentRegion().findHeader().toSelector());
213+
await page.keys('Tab');
214+
await page.keys('Tab');
215+
const secondButtonSelector = wrapper.findContentRegion().findButton('[data-testid="button-2"]').toSelector();
216+
await expect(page.isFocused(secondButtonSelector)).resolves.toBe(true);
217+
218+
// Scroll to a point where half of the first button is visible
219+
const firstButtonSelector = wrapper.findContentRegion().findButton('[data-testid="button-1"]').toSelector();
220+
const firstButtonBoundingBox = await page.getBoundingBox(firstButtonSelector);
221+
const firstButtonTopOffset = firstButtonBoundingBox.top - headerOffset;
222+
const firstButtonHalfOffset = firstButtonTopOffset + firstButtonBoundingBox.height / 2;
223+
await page.windowScrollTo({ top: firstButtonHalfOffset });
224+
225+
// Set the focus back to the first button
226+
await page.keys(['Shift', 'Tab']);
227+
await expect(page.isFocused(firstButtonSelector)).resolves.toBe(true);
228+
229+
// Assert that whole of the first button is visible and not only the half of it
230+
await expect(page.getWindowScroll()).resolves.toEqual({ top: firstButtonTopOffset, left: 0 });
231+
})
232+
);
233+
202234
testIf(theme === 'refresh-toolbar')(
203235
'should keep header visible and in position while scrolling',
204236
setupTest({ pageName: 'multi-layout-with-table-sticky-header' }, async page => {

src/app-layout/__tests__/main.test.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,21 @@ describeEachAppLayout({ themes: ['classic', 'refresh-toolbar'], sizes: ['desktop
117117
await waitFor(() => expect(wrapper.getElement()).toHaveStyle({ minBlockSize: 'calc(100vh - 75px)' }));
118118
});
119119

120+
(theme !== 'classic' ? test : test.skip)('should set the header height to the scrolling element', () => {
121+
Object.defineProperty(document, 'scrollingElement', { value: document.body });
122+
renderComponent(
123+
<div id="b">
124+
<div style={{ height: 40 }} id="h" />
125+
<AppLayout />
126+
</div>
127+
);
128+
129+
// Note: The toolbar height in jsdom environment is calculated as `0`.
130+
// For this reason both `refresh` and `refresh-toolbar` only consider the height of `#h` element which is `40px`.
131+
// We have covered this case with real values in integration tests.
132+
expect(document.scrollingElement).toHaveStyle('scroll-padding-block-start: 40px');
133+
});
134+
120135
test('should use alternative header and footer selector', async () => {
121136
const { wrapper } = renderComponent(
122137
<>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { useEffect } from 'react';
4+
5+
// Sets scroll padding to the scrolling element (`html` or `body`).
6+
// This prevents the focused element to be hidden under the sticky headers.
7+
export function useGlobalScrollPadding(headerHeight: number) {
8+
useEffect(() => {
9+
const scrollingElement = document.scrollingElement;
10+
if (scrollingElement instanceof HTMLElement) {
11+
scrollingElement.style.scrollPaddingBlockStart = `${headerHeight}px`;
12+
}
13+
}, [headerHeight]);
14+
}

src/app-layout/visual-refresh-toolbar/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { AppLayoutProps } from '../interfaces';
1717
import { SplitPanelProviderProps } from '../split-panel';
1818
import { MIN_DRAWER_SIZE, OnChangeParams, useDrawers } from '../utils/use-drawers';
1919
import { useFocusControl, useMultipleFocusControl } from '../utils/use-focus-control';
20+
import { useGlobalScrollPadding } from '../utils/use-global-scroll-padding';
2021
import { useSplitPanelFocusControl } from '../utils/use-split-panel-focus-control';
2122
import { ActiveDrawersContext } from '../utils/visibility-context';
2223
import {
@@ -313,6 +314,8 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef<AppLayoutProps.Ref, AppLa
313314
stickyNotifications: resolvedStickyNotifications,
314315
});
315316

317+
useGlobalScrollPadding(verticalOffsets.header);
318+
316319
const appLayoutInternals: AppLayoutInternals = {
317320
ariaLabels: ariaLabelsWithDrawers,
318321
headerVariant,

src/app-layout/visual-refresh/layout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from 'react';
44
import clsx from 'clsx';
55

66
import customCssProps from '../../internal/generated/custom-css-properties';
7+
import { useGlobalScrollPadding } from '../utils/use-global-scroll-padding';
78
import { useAppLayoutInternals } from './context';
89

910
import testutilStyles from '../test-classes/styles.css.js';
@@ -46,6 +47,8 @@ export default function Layout({ children }: LayoutProps) {
4647
splitPanelDisplayed,
4748
} = useAppLayoutInternals();
4849

50+
useGlobalScrollPadding(headerHeight);
51+
4952
// Determine the first content child so the gap will vertically align with the trigger buttons
5053
const contentFirstChild = getContentFirstChild(breadcrumbs, contentHeader, hasNotificationsContent, isMobile);
5154

0 commit comments

Comments
 (0)