Skip to content

Commit

Permalink
revert: Revert "feat: Introduce scroll snapping to responsive tabs (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
jperals authored Dec 9, 2024
1 parent 3c11f26 commit 2049c4a
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 13 deletions.
1 change: 0 additions & 1 deletion src/tabs/__integ__/tabs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ 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: 30 additions & 0 deletions src/tabs/__tests__/smooth-scroll.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
// 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 @@ -26,17 +31,42 @@ 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: 7 additions & 0 deletions src/tabs/native-smooth-scroll-supported.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// This function is in a separate file to allow for mocking in unit tests
export default function () {
return 'scrollBehavior' in document.documentElement.style;
}
22 changes: 22 additions & 0 deletions src/tabs/scroll-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,25 @@ export const hasInlineStartOverflow = (headerBar: HTMLElement): boolean => {
export const hasInlineEndOverflow = (headerBar: HTMLElement): boolean => {
return Math.ceil(getScrollInlineStart(headerBar)) < headerBar.scrollWidth - headerBar.offsetWidth;
};

export const scrollIntoView = (tabHeader: HTMLElement, headerBar: HTMLElement, smooth = true): void => {
if (!tabHeader || !headerBar) {
return;
}
// Extra left and right margin to always make the focus ring visible
const margin = 2;
let updatedLeftScroll = headerBar.scrollLeft;

// Anchor tab to left of scroll parent
updatedLeftScroll = Math.min(updatedLeftScroll, tabHeader.offsetLeft - margin);
// Anchor tab to right of scroll parent
updatedLeftScroll = Math.max(
updatedLeftScroll,
tabHeader.offsetLeft + tabHeader.offsetWidth - headerBar.offsetWidth + margin
);
if (smooth) {
smoothScroll(headerBar, updatedLeftScroll);
} else {
headerBar.scrollLeft = updatedLeftScroll;
}
};
58 changes: 52 additions & 6 deletions src/tabs/smooth-scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,62 @@
// 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) || !element.scrollTo) {
if (isMotionDisabled(element)) {
element.scrollLeft = to;
return;
}
// istanbul ignore next: unit tests always have motion disabled
element.scrollTo({
left: to,
behavior: 'smooth',
});
if (isNativeSmoothScrollingSupported() && element.scrollTo) {
element.scrollTo({
left: to,
behavior: 'smooth',
});
return;
}
simulateSmoothScroll(element, to);
};

export default smoothScroll;
4 changes: 0 additions & 4 deletions src/tabs/tab-header-bar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ $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 @@ -75,7 +74,6 @@ $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 @@ -207,7 +205,6 @@ $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 @@ -216,7 +213,6 @@ $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
12 changes: 10 additions & 2 deletions src/tabs/tab-header-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ import {
GeneratedAnalyticsMetadataTabsSelect,
} from './analytics-metadata/interfaces';
import { TabsProps } from './interfaces';
import { hasHorizontalOverflow, hasInlineEndOverflow, hasInlineStartOverflow, onPaginationClick } from './scroll-utils';
import {
hasHorizontalOverflow,
hasInlineEndOverflow,
hasInlineStartOverflow,
onPaginationClick,
scrollIntoView,
} from './scroll-utils';

import analyticsSelectors from './analytics-metadata/styles.css.js';
import styles from './styles.css.js';
Expand Down Expand Up @@ -127,7 +133,9 @@ export function TabHeaderBar({
return;
}
const activeTabRef = tabRefs.current.get(activeTabId);
activeTabRef?.scrollIntoView?.({ behavior: smooth ? 'smooth' : 'auto', inline: 'center' });
if (activeTabRef && headerBarRef.current) {
scrollIntoView(activeTabRef, headerBarRef.current, smooth);
}
};

useEffect(() => {
Expand Down

0 comments on commit 2049c4a

Please sign in to comment.