Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add scroll snapping to responsive tabs #2899

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions src/tabs/__integ__/tabs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ test(
await page.setWindowSize({ width: 500, height: 1000 });
await page.click('#add-tab');
await page.click(page.paginationButton('right', true));
await page.click(page.paginationButton('right', true));
await page.click(wrapper.findTabLinkByIndex(7).toSelector());
await page.waitForAssertion(async () =>
expect(await page.isExisting(page.paginationButton('right', true))).toBe(false)
Expand Down
30 changes: 0 additions & 30 deletions src/tabs/__tests__/smooth-scroll.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { render } from '@testing-library/react';
import { waitFor } from '@testing-library/react';

import { isMotionDisabled } from '@cloudscape-design/component-toolkit/internal';

import nativeSupport from '../../../lib/components/tabs/native-smooth-scroll-supported';
import smoothScroll from '../../../lib/components/tabs/smooth-scroll';
import createWrapper from '../../../lib/components/test-utils/dom';

jest.mock('../../../lib/components/tabs/native-smooth-scroll-supported', () => {
return jest.fn();
});
jest.mock('@cloudscape-design/component-toolkit/internal', () => ({
...jest.requireActual('@cloudscape-design/component-toolkit/internal'),
isMotionDisabled: jest.fn(),
Expand All @@ -31,42 +26,17 @@ function renderScrollableElement(): HTMLElement {
return createWrapper(renderResult.container).findByClassName('scrollable')!.getElement();
}

async function usesCustomScrollingFunction(element: HTMLElement, scrollLeft: number) {
expect(nativeScrollMock).not.toHaveBeenCalled();
await waitFor(() => {
expect(element.scrollLeft).toEqual(scrollLeft);
});
}

beforeEach(() => {
(nativeSupport as jest.Mock).mockReturnValue(false);
(isMotionDisabled as jest.Mock).mockReturnValue(false);
nativeScrollMock.mockClear();
});

describe('Smooth scroll', () => {
test('uses native scrollTo function if the browser supports it', () => {
(nativeSupport as jest.Mock).mockReturnValue(true);
const element = renderScrollableElement();
smoothScroll(element, 100);
expect(nativeScrollMock).toHaveBeenCalled();
});
test('relies on custom function when browsers do not support it', async () => {
const element = renderScrollableElement();
smoothScroll(element, 100);
await usesCustomScrollingFunction(element, 100);
});
test('does not animate when motion is disabled', () => {
(isMotionDisabled as jest.Mock).mockReturnValue(true);
const element = renderScrollableElement();
smoothScroll(element, 100);
expect(nativeScrollMock).not.toHaveBeenCalled();
expect(element.scrollLeft).toEqual(100);
});
test('animates left with custom function', async () => {
const element = renderScrollableElement();
element.scrollLeft = 500;
smoothScroll(element, 100);
await usesCustomScrollingFunction(element, 100);
});
});
7 changes: 0 additions & 7 deletions src/tabs/native-smooth-scroll-supported.ts

This file was deleted.

52 changes: 3 additions & 49 deletions src/tabs/smooth-scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,16 @@
// SPDX-License-Identifier: Apache-2.0
import { isMotionDisabled } from '@cloudscape-design/component-toolkit/internal';

import isNativeSmoothScrollingSupported from './native-smooth-scroll-supported';

interface ScrollContext {
scrollable: HTMLElement;
startX: number;
endX: number;
startTime: number;
scrollTime: number;
}

// The scroll speed depends on the scrolling distance. The equation below is an interpolation of measurements in Chrome.
const getScrollSpeed = (pixels: number) => 0.0015 * Math.abs(pixels) + 0.558;
const getScrollTime = (pixels: number) => Math.round(Math.abs(pixels) / getScrollSpeed(pixels));

const now = () => (window.performance ? window.performance.now() : Date.now());

const ease = (k: number): number => {
return 0.5 * (1 - Math.cos(Math.PI * k));
};

const step = (context: ScrollContext): void => {
const time = now();
const elapsed = Math.min((time - context.startTime) / context.scrollTime, 1);
const value = ease(elapsed);
const currentX = context.startX + (context.endX - context.startX) * value;
context.scrollable.scrollLeft = currentX;
// scroll more if we have not reached our destination
if (currentX !== context.endX) {
requestAnimationFrame(() => step(context));
}
};

const simulateSmoothScroll = (element: HTMLElement, endX: number): void => {
const startX = element.scrollLeft;
step({
scrollable: element,
startX,
endX,
startTime: now(),
scrollTime: getScrollTime(endX - startX),
});
};

const smoothScroll = (element: HTMLElement, to: number) => {
if (isMotionDisabled(element)) {
if (isMotionDisabled(element) || !element.scrollTo) {
element.scrollLeft = to;
return;
}
if (isNativeSmoothScrollingSupported() && element.scrollTo) {
} else {

Check warning on line 8 in src/tabs/smooth-scroll.ts

View check run for this annotation

Codecov / codecov/patch

src/tabs/smooth-scroll.ts#L8

Added line #L8 was not covered by tests
// istanbul ignore next: unit tests always have motion disabled
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't understand why this is not working properly. If I move the ignore comment above line 8, it still complains. I think we might just need to override the coverage check for this PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

(if this becomes a blocker, I'm happy to separate this out into a second PR. I just really need to deliver the scrolling update)

element.scrollTo({
left: to,
behavior: 'smooth',
});
return;
}
simulateSmoothScroll(element, to);
};

export default smoothScroll;
4 changes: 4 additions & 0 deletions src/tabs/tab-header-bar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ $label-horizontal-spacing: awsui.$space-xs;
overflow-y: hidden;
position: relative;
inline-size: 100%;
scroll-snap-type: inline proximity;
// do not use pointer-events none because it disables scroll by sliding

// Hide scrollbar in all browsers
Expand Down Expand Up @@ -74,6 +75,7 @@ $label-horizontal-spacing: awsui.$space-xs;
flex-shrink: 0;
display: flex;
max-inline-size: calc(90% - awsui.$space-l);
scroll-snap-align: start;
}

.tabs-tab-label {
Expand Down Expand Up @@ -205,6 +207,7 @@ $label-horizontal-spacing: awsui.$space-xs;
// Remediate focus shadow
.tabs-tab:first-child {
margin-inline-start: 1px;
scroll-margin-inline-start: 1px;
& > .tabs-tab-header-container {
padding-inline-start: calc(#{$label-horizontal-spacing} - 1px);
}
Expand All @@ -213,6 +216,7 @@ $label-horizontal-spacing: awsui.$space-xs;
// Remediate focus shadow
.tabs-tab:last-child {
margin-inline-end: 1px;
scroll-margin-inline-end: 1px;
& > .tabs-tab-header-container {
padding-inline-end: calc(#{$label-horizontal-spacing} - 1px);
}
Expand Down
Loading