diff --git a/pages/tabs/responsive-integ.page.tsx b/pages/tabs/responsive-integ.page.tsx index 04f47b6d14..aeb0c96c86 100644 --- a/pages/tabs/responsive-integ.page.tsx +++ b/pages/tabs/responsive-integ.page.tsx @@ -22,7 +22,7 @@ export default function TabsDemoPage() { 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', }, { - label: 'Third tab', + label: 'Third tab with longer title', id: 'third', content: 'Diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', @@ -30,7 +30,7 @@ export default function TabsDemoPage() { dismissLabel: 'Dismiss third tab', }, { - label: 'Fourth tab', + label: 'Fourth tab with some', id: 'fourth', content: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', @@ -43,7 +43,7 @@ export default function TabsDemoPage() { 'Diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', }, { - label: 'Sixth tab', + label: 'Sixth tab with actions', id: 'sixth', content: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', @@ -123,7 +123,7 @@ export default function TabsDemoPage() { ]); const extraTab: TabsProps.Tab = { - label: 'Seventh tab', + label: 'New Seventh tab', id: 'seventh', content: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', diff --git a/src/tabs/__tests__/smooth-scroll.test.tsx b/src/tabs/__tests__/smooth-scroll.test.tsx index 268d028fb7..104a85ce76 100644 --- a/src/tabs/__tests__/smooth-scroll.test.tsx +++ b/src/tabs/__tests__/smooth-scroll.test.tsx @@ -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(), @@ -31,31 +26,12 @@ 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(); @@ -63,10 +39,4 @@ describe('Smooth scroll', () => { 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); - }); }); diff --git a/src/tabs/__tests__/tabs.test.tsx b/src/tabs/__tests__/tabs.test.tsx index 775782d3c0..deed3f3549 100644 --- a/src/tabs/__tests__/tabs.test.tsx +++ b/src/tabs/__tests__/tabs.test.tsx @@ -771,23 +771,6 @@ describe('Tabs', () => { }); describe('Dismissible', () => { - test('scalls requestAnimationFrame for focus updates', () => { - const time = 0; - const requestAnimationFrameSpy: jest.SpyInstance = jest - .spyOn(window, 'requestAnimationFrame') - .mockImplementation(callback => { - callback(time); - return time; - }); - - const wrapper = renderTabs().wrapper; - wrapper.findActiveTab()!.getElement().focus(); - pressRight(wrapper); - - expect(requestAnimationFrameSpy).not.toBeCalledTimes(0); - requestAnimationFrameSpy.mockRestore(); - }); - test('renders the correct dismiss label', () => { const dismissibleButton = renderTabs( diff --git a/src/tabs/native-smooth-scroll-supported.ts b/src/tabs/native-smooth-scroll-supported.ts deleted file mode 100644 index ed85293e31..0000000000 --- a/src/tabs/native-smooth-scroll-supported.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 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; -} diff --git a/src/tabs/smooth-scroll.ts b/src/tabs/smooth-scroll.ts index c90a9caff5..3a4ff5e07b 100644 --- a/src/tabs/smooth-scroll.ts +++ b/src/tabs/smooth-scroll.ts @@ -2,62 +2,15 @@ // 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 { element.scrollTo({ left: to, behavior: 'smooth', }); - return; } - simulateSmoothScroll(element, to); }; export default smoothScroll; diff --git a/src/tabs/tab-header-bar.scss b/src/tabs/tab-header-bar.scss index ba74b04900..d6d0ef9810 100644 --- a/src/tabs/tab-header-bar.scss +++ b/src/tabs/tab-header-bar.scss @@ -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 @@ -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 { diff --git a/src/tabs/tab-header-bar.tsx b/src/tabs/tab-header-bar.tsx index 8e116c66ea..ec54d81935 100644 --- a/src/tabs/tab-header-bar.tsx +++ b/src/tabs/tab-header-bar.tsx @@ -29,13 +29,7 @@ import { GeneratedAnalyticsMetadataTabsSelect, } from './analytics-metadata/interfaces'; import { TabsProps } from './interfaces'; -import { - hasHorizontalOverflow, - hasInlineEndOverflow, - hasInlineStartOverflow, - onPaginationClick, - scrollIntoView, -} from './scroll-utils'; +import { hasHorizontalOverflow, hasInlineEndOverflow, hasInlineStartOverflow, onPaginationClick } from './scroll-utils'; import analyticsSelectors from './analytics-metadata/styles.css.js'; import styles from './styles.css.js'; @@ -128,33 +122,6 @@ export function TabHeaderBar({ } }, [widthChange, tabs]); - const scrollIntoViewIfPossible = (smooth: boolean) => { - if (!activeTabId) { - return; - } - const activeTabRef = tabRefs.current.get(activeTabId); - if (activeTabRef && headerBarRef.current) { - scrollIntoView(activeTabRef, headerBarRef.current, smooth); - } - }; - - useEffect(() => { - // Delay scrollIntoView as the position is depending on parent elements - // (effects are called inside-out in the component tree). - // Wait one frame to allow parents to complete it's calculation. - requestAnimationFrame(() => { - scrollIntoViewIfPossible(false); - }); - // Non-smooth scrolling should not be called upon activeId change - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [horizontalOverflow, widthChange, tabs.length]); - - useEffect(() => { - scrollIntoViewIfPossible(true); - // Smooth scrolling should only be called upon activeId change - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeTabId]); - useEffect(() => { /* When the selected tab changes and we are currently already focused on a tab, @@ -162,7 +129,7 @@ export function TabHeaderBar({ */ if (headerBarRef.current?.contains(document.activeElement)) { if (document.activeElement !== activeTabHeaderRef.current) { - activeTabHeaderRef.current?.focus({ preventScroll: true }); + activeTabHeaderRef.current?.focus(); } } }, [activeTabId]);