diff --git a/src/flashbar/collapsible-flashbar.tsx b/src/flashbar/collapsible-flashbar.tsx index e6f0906ea5..60d10a51e6 100644 --- a/src/flashbar/collapsible-flashbar.tsx +++ b/src/flashbar/collapsible-flashbar.tsx @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { ReactNode, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; + +import React, { ReactNode, useCallback, useLayoutEffect, useRef, useState } from 'react'; import { TransitionGroup } from 'react-transition-group'; import clsx from 'clsx'; @@ -17,8 +18,8 @@ import { getVisualContextClassname } from '../internal/components/visual-context import customCssProps from '../internal/generated/custom-css-properties'; import { useDebounceCallback } from '../internal/hooks/use-debounce-callback'; import { useEffectOnUpdate } from '../internal/hooks/use-effect-on-update'; +import { useThrottleCallback } from '../internal/hooks/use-throttle-callback'; import { scrollElementIntoView } from '../internal/utils/scrollable-containers'; -import { throttle } from '../internal/utils/throttle'; import { GeneratedAnalyticsMetadataFlashbarCollapse, GeneratedAnalyticsMetadataFlashbarExpand, @@ -121,28 +122,28 @@ export default function CollapsibleFlashbar({ items, style, ...restProps }: Inte } }, [isFlashbarStackExpanded]); - const updateBottomSpacing = useMemo( - () => - throttle(() => { - // Allow vertical space between Flashbar and page bottom only when the Flashbar is reaching the end of the page, - // otherwise avoid spacing with eventual sticky elements below. - const listElement = listElementRef?.current; - const flashbar = listElement?.parentElement; - if (listElement && flashbar) { - // Make sure the bottom padding is present when we make the calculations, - // then we might decide to remove it or not. - flashbar.classList.remove(styles.floating); - const windowHeight = window.innerHeight; - // Take the parent region into account if using the App Layout, because it might have additional margins. - // Otherwise we use the Flashbar component for this calculation. - const outerElement = findUpUntil(flashbar, element => element.getAttribute('role') === 'region') || flashbar; - const applySpacing = - isFlashbarStackExpanded && Math.ceil(outerElement.getBoundingClientRect().bottom) >= windowHeight; - if (!applySpacing) { - flashbar.classList.add(styles.floating); - } + const updateBottomSpacing = useThrottleCallback( + () => { + // Allow vertical space between Flashbar and page bottom only when the Flashbar is reaching the end of the page, + // otherwise avoid spacing with eventual sticky elements below. + const listElement = listElementRef?.current; + const flashbar = listElement?.parentElement; + if (listElement && flashbar) { + // Make sure the bottom padding is present when we make the calculations, + // then we might decide to remove it or not. + flashbar.classList.remove(styles.floating); + const windowHeight = window.innerHeight; + // Take the parent region into account if using the App Layout, because it might have additional margins. + // Otherwise we use the Flashbar component for this calculation. + const outerElement = findUpUntil(flashbar, element => element.getAttribute('role') === 'region') || flashbar; + const applySpacing = + isFlashbarStackExpanded && Math.ceil(outerElement.getBoundingClientRect().bottom) >= windowHeight; + if (!applySpacing) { + flashbar.classList.add(styles.floating); } - }, resizeListenerThrottleDelay), + } + }, + resizeListenerThrottleDelay, [isFlashbarStackExpanded] ); diff --git a/src/internal/hooks/use-throttle-callback/index.ts b/src/internal/hooks/use-throttle-callback/index.ts new file mode 100644 index 0000000000..3cea6fd5af --- /dev/null +++ b/src/internal/hooks/use-throttle-callback/index.ts @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useMemo } from 'react'; + +import { throttle, ThrottledFunction } from '../../utils/throttle'; + +export function useThrottleCallback any>( + func: F, + delay: number, + deps: React.DependencyList +): ThrottledFunction { + // eslint-disable-next-line react-hooks/exhaustive-deps + return useMemo(() => throttle(func, delay), deps); +} diff --git a/src/progress-bar/index.tsx b/src/progress-bar/index.tsx index 0d5228371a..1c854d12d5 100644 --- a/src/progress-bar/index.tsx +++ b/src/progress-bar/index.tsx @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 'use client'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import clsx from 'clsx'; import { useUniqueId, warnOnce } from '@cloudscape-design/component-toolkit/internal'; @@ -9,9 +9,9 @@ import { useUniqueId, warnOnce } from '@cloudscape-design/component-toolkit/inte import { getBaseProps } from '../internal/base-component'; import { fireNonCancelableEvent } from '../internal/events'; import useBaseComponent from '../internal/hooks/use-base-component'; +import { useThrottleCallback } from '../internal/hooks/use-throttle-callback'; import { applyDisplayName } from '../internal/utils/apply-display-name'; import { joinStrings } from '../internal/utils/strings'; -import { throttle } from '../internal/utils/throttle'; import InternalLiveRegion from '../live-region/internal'; import { ProgressBarProps } from './interfaces'; import { Progress, ResultState, SmallText } from './internal'; @@ -52,11 +52,13 @@ export default function ProgressBar({ const additionalInfoId = useUniqueId('progressbar-additional-info-'); const [announcedValue, setAnnouncedValue] = useState(''); - const throttledAssertion = useMemo(() => { - return throttle((value: ProgressBarProps['value']) => { + const throttledAssertion = useThrottleCallback( + (value: ProgressBarProps['value']) => { setAnnouncedValue(`${value}%`); - }, ASSERTION_FREQUENCY); - }, []); + }, + ASSERTION_FREQUENCY, + [] + ); useEffect(() => { throttledAssertion(value); diff --git a/src/table/__tests__/resizable-columns.test.tsx b/src/table/__tests__/resizable-columns.test.tsx index 3b69dfd1a2..4813e8d1b4 100644 --- a/src/table/__tests__/resizable-columns.test.tsx +++ b/src/table/__tests__/resizable-columns.test.tsx @@ -66,6 +66,10 @@ function findDragHandle() { return new InternalDragHandleWrapper(document.body); } +async function waitForResizeThrottle() { + await new Promise(resolve => setTimeout(resolve, 50)); +} + afterEach(() => { jest.restoreAllMocks(); }); @@ -235,18 +239,25 @@ test('takes width as min width if it is less than 120px and min width is not set expect(wrapper.findColumnHeaders()[0].getElement()).toHaveStyle({ width: '100px' }); }); -test('should follow along each mouse move event', () => { +test('should follow along each mouse move event', async () => { const { wrapper } = renderTable(); expect(wrapper.findColumnHeaders()[0].getElement()).toHaveStyle({ width: '150px' }); firePointerdown(wrapper.findColumnResizer(1)!); firePointermove(200); + await waitForResizeThrottle(); expect(wrapper.findColumnHeaders()[0].getElement()).toHaveStyle({ width: '200px' }); + firePointermove(250); + await waitForResizeThrottle(); expect(wrapper.findColumnHeaders()[0].getElement()).toHaveStyle({ width: '250px' }); + firePointermove(200); + await waitForResizeThrottle(); expect(wrapper.findColumnHeaders()[0].getElement()).toHaveStyle({ width: '200px' }); + firePointerup(200); + await waitForResizeThrottle(); expect(wrapper.findColumnHeaders()[0].getElement()).toHaveStyle({ width: '200px' }); }); diff --git a/src/table/resizer/index.tsx b/src/table/resizer/index.tsx index 1476c98ce0..30b67278a4 100644 --- a/src/table/resizer/index.tsx +++ b/src/table/resizer/index.tsx @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + import React, { useCallback, useEffect, useRef, useState } from 'react'; import clsx from 'clsx'; @@ -8,6 +9,7 @@ import { getIsRtl, getLogicalBoundingClientRect, getLogicalPageX } from '@clouds import { useSingleTabStopNavigation } from '@cloudscape-design/component-toolkit/internal'; import DragHandleWrapper from '../../internal/components/drag-handle-wrapper'; +import { useThrottleCallback } from '../../internal/hooks/use-throttle-callback'; import { useVisualRefresh } from '../../internal/hooks/use-visual-mode'; import { KeyCode } from '../../internal/keycode'; import handleKey, { isEventLike } from '../../internal/utils/handle-key'; @@ -30,6 +32,7 @@ interface ResizerProps { isBorderless: boolean; } +const RESIZE_THROTTLE = 25; const AUTO_GROW_START_TIME = 10; const AUTO_GROW_INTERVAL = 10; const AUTO_GROW_INCREMENT = 5; @@ -168,7 +171,7 @@ export function Resizer({ [minWidth, onWidthUpdate, updateTrackerPosition] ); - const resizeColumn = useCallback( + const resizeColumn = useThrottleCallback( (offset: number) => { const elements = getResizerElements(resizerToggleRef.current); if (!elements) { @@ -183,6 +186,7 @@ export function Resizer({ updateColumnWidth(newWidth); } }, + RESIZE_THROTTLE, [updateColumnWidth] ); diff --git a/src/table/use-sticky-header.ts b/src/table/use-sticky-header.ts index ddb29e7021..952ccb6b49 100644 --- a/src/table/use-sticky-header.ts +++ b/src/table/use-sticky-header.ts @@ -24,11 +24,6 @@ export const useStickyHeader = ( secondaryTableRef.current && tableWrapperRef.current ) { - // Using the tableRef getBoundingClientRect().width instead of the theadRef because in VR - // the tableRef adds extra padding to the table and by default the theadRef will have a width - // without the padding and will make the sticky header width incorrect. - secondaryTableRef.current.style.inlineSize = `${tableRef.current.getBoundingClientRect().width}px`; - tableWrapperRef.current.style.marginBlockStart = `-${theadRef.current.getBoundingClientRect().height}px`; } }, [theadRef, secondaryTheadRef, secondaryTableRef, tableWrapperRef, tableRef]);