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

fix: Render Visual Refresh background height in sync with content #1344

Closed
wants to merge 13 commits into from
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import AppLayout from '~components/app-layout';
import labels from './utils/labels';
import Table from '~components/table';
import { generateItems, Instance } from '../table/generate-data';
import { columnsConfig } from '../table/shared-configs';
import ExpandableSection from '~components/expandable-section';

const items = generateItems(20);

export default function () {
return (
<AppLayout
ariaLabels={labels}
contentType="table"
navigationHide={true}
content={
<Table<Instance>
header={
<ExpandableSection headerText="Click to expand header area" headingTagOverride="h1">
<div style={{ height: '300px' }}>Content</div>
</ExpandableSection>
}
stickyHeader={true}
variant="full-page"
columnDefinitions={columnsConfig}
items={items}
/>
}
/>
);
}
33 changes: 20 additions & 13 deletions src/internal/hooks/container-queries/use-resize-observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ResizeObserver, ResizeObserverEntry } from '@juggle/resize-observer';
import React, { useEffect, useLayoutEffect } from 'react';
import { useStableEventHandler } from '../use-stable-event-handler';
import { ContainerQueryEntry } from '@cloudscape-design/component-toolkit';
import { convertResizeObserverEntry } from './utils';
import { flushSync } from 'react-dom';

type ElementReference = (() => Element | null) | React.RefObject<Element>;

Expand All @@ -21,8 +23,13 @@ type ElementReference = (() => Element | null) | React.RefObject<Element>;
*
* @param elementRef React reference or memoized getter for the target element
* @param onObserve Function to fire when observation occurs
* @param sync Prevent batching onObserve with other state updates
*/
export function useResizeObserver(elementRef: ElementReference, onObserve: (entry: ContainerQueryEntry) => void) {
export function useResizeObserver(
elementRef: ElementReference,
onObserve: (entry: ContainerQueryEntry) => void,
sync = false
) {
const stableOnObserve = useStableEventHandler(onObserve);

// This effect provides a synchronous update required to prevent flakiness when initial state and first observed state are different.
Expand All @@ -47,7 +54,17 @@ export function useResizeObserver(elementRef: ElementReference, onObserve: (entr
const observer = new ResizeObserver(entries => {
// Prevent observe notifications on already unmounted component.
if (connected) {
stableOnObserve(convertResizeObserverEntry(entries[0]));
const callback = () => stableOnObserve(convertResizeObserverEntry(entries[0]));
if (sync) {
// Use flushSync to prevent state update batching.
// This will let all component renders have access to the resulting update state after running the callback.
// Because we do this is inside a useEffect hook, we also need to wrap the call inside queueMicrotask,
// to avoid possible interference with other render processes by deferring execution
// to the end of the current execution context.
queueMicrotask(() => flushSync(callback));
} else {
callback();
}
}
});
observer.observe(element);
Expand All @@ -56,15 +73,5 @@ export function useResizeObserver(elementRef: ElementReference, onObserve: (entr
observer.disconnect();
};
}
}, [elementRef, stableOnObserve]);
}

function convertResizeObserverEntry(entry: ResizeObserverEntry): ContainerQueryEntry {
return {
target: entry.target,
contentBoxWidth: entry.contentBoxSize[0].inlineSize,
contentBoxHeight: entry.contentBoxSize[0].blockSize,
borderBoxWidth: entry.borderBoxSize[0].inlineSize,
borderBoxHeight: entry.borderBoxSize[0].blockSize,
};
}, [elementRef, stableOnObserve, sync]);
}
14 changes: 14 additions & 0 deletions src/internal/hooks/container-queries/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { ResizeObserverEntry } from '@juggle/resize-observer';
import { ContainerQueryEntry } from '@cloudscape-design/component-toolkit';

export function convertResizeObserverEntry(entry: ResizeObserverEntry): ContainerQueryEntry {
return {
target: entry.target,
contentBoxWidth: entry.contentBoxSize[0].inlineSize,
contentBoxHeight: entry.contentBoxSize[0].blockSize,
borderBoxWidth: entry.borderBoxSize[0].inlineSize,
borderBoxHeight: entry.borderBoxSize[0].blockSize,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { render, screen } from '@testing-library/react';
import { useDynamicOverlap } from '../../../../../lib/components/internal/hooks/use-dynamic-overlap';
import { DynamicOverlapContext } from '../../../../../lib/components/internal/context/dynamic-overlap-context';

jest.mock('@cloudscape-design/component-toolkit', () => ({
...jest.requireActual('@cloudscape-design/component-toolkit'),
useContainerQuery: () => [800, () => {}],
jest.mock('../../../../../lib/components/internal/hooks/container-queries/utils', () => ({
...jest.requireActual('../../../../../lib/components/internal/hooks/container-queries/utils'),
convertResizeObserverEntry: () => ({ contentBoxHeight: 800 }),
}));

function renderApp(children: React.ReactNode) {
Expand Down
18 changes: 16 additions & 2 deletions src/internal/hooks/use-dynamic-overlap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import { useContext, useLayoutEffect } from 'react';

import { DynamicOverlapContext } from '../../context/dynamic-overlap-context';
import { useContainerQuery } from '@cloudscape-design/component-toolkit';
import { useRef, useState, useCallback } from 'react';
import { useResizeObserver } from '../container-queries';
import { ContainerQueryEntry } from '@cloudscape-design/component-toolkit';

export interface UseDynamicOverlapProps {
/**
Expand All @@ -21,7 +23,19 @@ export interface UseDynamicOverlapProps {
export function useDynamicOverlap(props?: UseDynamicOverlapProps) {
const disabled = props?.disabled ?? false;
const setDynamicOverlapHeight = useContext(DynamicOverlapContext);
const [overlapHeight, overlapElementRef] = useContainerQuery(rect => rect.contentBoxHeight);
const overlapElementRef = useRef(null);
const [overlapHeight, setOverlapHeight] = useState<null | number>(null);

const getElement = useCallback(() => overlapElementRef.current, [overlapElementRef]);
const updateState = useCallback(
(entry: ContainerQueryEntry) => setOverlapHeight(entry.contentBoxHeight),
[setOverlapHeight]
);

// Set sync to true in order to wrap the updateState callback inside flushSync
// and therefore work around React 18 triggering a paint before the state is consistently updated
// (see https://github.com/facebook/react/issues/24331)
useResizeObserver(getElement, updateState, true);

useLayoutEffect(
function handleDynamicOverlapHeight() {
Expand Down
Loading