Skip to content

Commit

Permalink
chore: Move resize observers down to the observed nodes (#2404)
Browse files Browse the repository at this point in the history
  • Loading branch information
just-boris authored Jun 24, 2024
1 parent f0ac2a1 commit f680654
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 39 deletions.
11 changes: 4 additions & 7 deletions src/app-layout/visual-refresh-toolbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useImperativeHandle, useState } from 'react';
import { useContainerQuery } from '@cloudscape-design/component-toolkit';
import { useControllable } from '../../internal/hooks/use-controllable';
import { fireNonCancelableEvent } from '../../internal/events';
import { useFocusControl } from '../utils/use-focus-control';
Expand Down Expand Up @@ -66,6 +65,8 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef(
const embeddedViewMode = (rest as any).__embeddedViewMode;
const splitPanelControlId = useUniqueId('split-panel');
const [toolbarState, setToolbarState] = useState<'show' | 'hide'>('show');
const [toolbarHeight, setToolbarHeight] = useState(0);
const [notificationsHeight, setNotificationsHeight] = useState(0);

const onNavigationToggle = (open: boolean) => {
fireNonCancelableEvent(onNavigationChange, { open });
Expand Down Expand Up @@ -163,10 +164,6 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef(
focusSplitPanel: () => splitPanelFocusControl.refs.slider.current?.focus(),
}));

// TODO move into respective components
const [notificationsHeight, notificationsRef] = useContainerQuery(rect => rect.borderBoxHeight);
const [toolbarHeight, toolbarRef] = useContainerQuery(rect => rect.borderBoxHeight);

const resolvedNavigation = navigationHide ? null : navigation ?? <></>;
const { maxDrawerSize, maxSplitPanelSize, splitPanelForcedPosition, splitPanelPosition } = computeHorizontalLayout({
activeDrawerSize,
Expand Down Expand Up @@ -215,8 +212,8 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef(
toolbarState,
setToolbarState,
verticalOffsets,
notificationsRef,
toolbarRef,
setToolbarHeight,
setNotificationsHeight,
onSplitPanelToggle: onSplitPanelToggleHandler,
onNavigationToggle,
onActiveDrawerChange,
Expand Down
4 changes: 2 additions & 2 deletions src/app-layout/visual-refresh-toolbar/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export interface AppLayoutInternals {
toolbarState: 'show' | 'hide';
setToolbarState: (state: 'show' | 'hide') => void;
verticalOffsets: VerticalLayoutOutput;
notificationsRef: React.Ref<HTMLElement>;
toolbarRef: React.Ref<HTMLElement>;
setNotificationsHeight: (height: number) => void;
setToolbarHeight: (height: number) => void;
onSplitPanelToggle: () => void;
onNavigationToggle: (open: boolean) => void;
onActiveDrawerChange: (newDrawerId: string | null) => void;
Expand Down
16 changes: 13 additions & 3 deletions src/app-layout/visual-refresh-toolbar/notifications/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import React, { useEffect, useRef } from 'react';
import clsx from 'clsx';
import { useResizeObserver } from '@cloudscape-design/component-toolkit/internal';
import { NotificationsSlot } from '../skeleton/slot-wrappers';
import { createWidgetizedComponent } from '../../../internal/widgets';
import { AppLayoutInternals } from '../interfaces';
Expand All @@ -17,10 +18,19 @@ export function AppLayoutNotificationsImplementation({
appLayoutInternals,
children,
}: AppLayoutNotificationsImplementationProps) {
const { ariaLabels, stickyNotifications, notificationsRef, verticalOffsets } = appLayoutInternals;
const { ariaLabels, stickyNotifications, setNotificationsHeight, verticalOffsets } = appLayoutInternals;
const ref = useRef<HTMLElement>(null);
useResizeObserver(ref, entry => setNotificationsHeight(entry.borderBoxHeight));
useEffect(() => {
return () => {
setNotificationsHeight(0);
};
// unmount effect only
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<NotificationsSlot
ref={notificationsRef}
ref={ref}
className={clsx(stickyNotifications && styles['sticky-notifications'])}
style={{
insetBlockStart: stickyNotifications ? verticalOffsets.notifications : undefined,
Expand Down
16 changes: 13 additions & 3 deletions src/app-layout/visual-refresh-toolbar/toolbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import clsx from 'clsx';
import styles from './styles.css.js';
import testutilStyles from '../../test-classes/styles.css.js';
Expand All @@ -9,6 +9,7 @@ import TriggerButton from './trigger-button';
import { ToolbarSlot } from '../skeleton/slot-wrappers';
import { createWidgetizedComponent } from '../../../internal/widgets';
import { AppLayoutInternals } from '../interfaces';
import { useResizeObserver } from '@cloudscape-design/component-toolkit/internal';

interface AppLayoutToolbarImplementationProps {
appLayoutInternals: AppLayoutInternals;
Expand All @@ -19,7 +20,7 @@ export function AppLayoutToolbarImplementation({ appLayoutInternals }: AppLayout
ariaLabels,
breadcrumbs,
drawers,
toolbarRef,
setToolbarHeight,
verticalOffsets,
onNavigationToggle,
isMobile,
Expand All @@ -32,6 +33,15 @@ export function AppLayoutToolbarImplementation({ appLayoutInternals }: AppLayout
} = appLayoutInternals;
// TODO: expose configuration property

Check warning on line 34 in src/app-layout/visual-refresh-toolbar/toolbar/index.tsx

View workflow job for this annotation

GitHub Actions / build / build

Unexpected 'todo' comment: 'TODO: expose configuration property'
const pinnedToolbar = false;
const ref = useRef<HTMLElement>(null);
useResizeObserver(ref, entry => setToolbarHeight(entry.borderBoxHeight));
useEffect(() => {
return () => {
setToolbarHeight(0);
};
// unmount effect only
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
let lastScrollY = window.scrollY;
Expand Down Expand Up @@ -62,7 +72,7 @@ export function AppLayoutToolbarImplementation({ appLayoutInternals }: AppLayout

return (
<ToolbarSlot
ref={toolbarRef}
ref={ref}
className={clsx(styles['universal-toolbar'], {
[testutilStyles['mobile-bar']]: isMobile,
[styles['toolbar-hidden']]: toolbarHidden,
Expand Down
21 changes: 3 additions & 18 deletions src/app-layout/visual-refresh/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { useMobile } from '../../internal/hooks/use-mobile';
import { useStableCallback } from '@cloudscape-design/component-toolkit/internal';
import useResize from '../utils/use-resize';
import styles from './styles.css.js';
import { useContainerQuery } from '@cloudscape-design/component-toolkit';
import useBackgroundOverlap from './use-background-overlap';
import { useDrawers } from '../utils/use-drawers';
import { useUniqueId } from '../../internal/hooks/use-unique-id';
Expand Down Expand Up @@ -65,8 +64,8 @@ interface AppLayoutInternals extends AppLayoutPropsWithDefaults {
mainElement: React.Ref<HTMLDivElement>;
mainOffsetLeft: number;
navigationRefs: FocusControlRefs;
notificationsElement: React.Ref<HTMLDivElement>;
notificationsHeight: number;
setNotificationsHeight: (height: number) => void;
offsetBottom: number;
setSplitPanelReportedSize: (value: number) => void;
setSplitPanelReportedHeaderHeight: (value: number) => void;
Expand Down Expand Up @@ -385,21 +384,7 @@ export const AppLayoutInternalsProvider = React.forwardRef(
[isMobile, navigationOpen, isToolsOpen, activeDrawer]
);

/**
* Because the notifications slot does not give us any direction insight into
* what the state of the child content is we need to have a mechanism for
* tracking the height of the notifications and whether or not it has content.
* The height of the notifications is an integer that will be used as a custom
* property on the Layout component to determine what the sticky offset should
* be if there are sticky notifications. This could be any number including
* zero based on how the child content renders. The hasNotificationsContent boolean
* is simply centralizing the logic of the notifications height being > 0 such
* that it is not repeated in various components (such as MobileToolbar) that need to
* know if the notifications slot is empty.
*/
const [notificationsContainerQuery, notificationsElement] = useContainerQuery(rect => rect.contentBoxHeight);

const notificationsHeight = notificationsContainerQuery ?? 0;
const [notificationsHeight, setNotificationsHeight] = useState(0);
const hasNotificationsContent = notificationsHeight > 0;
/**
* Determine the offsetBottom value based on the presence of a footer element and
Expand Down Expand Up @@ -558,8 +543,8 @@ export const AppLayoutInternalsProvider = React.forwardRef(
minContentWidth,
navigationHide,
navigationRefs,
notificationsElement,
notificationsHeight,
setNotificationsHeight,
offsetBottom,
setSplitPanelReportedSize,
setSplitPanelReportedHeaderHeight,
Expand Down
27 changes: 21 additions & 6 deletions src/app-layout/visual-refresh/notifications.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import React, { useEffect, useRef } from 'react';
import clsx from 'clsx';
import { highContrastHeaderClassName } from '../../internal/utils/content-header-utils';
import { useAppLayoutInternals } from './context';
import styles from './styles.css.js';
import testutilStyles from '../test-classes/styles.css.js';
import { useResizeObserver } from '@cloudscape-design/component-toolkit/internal';

export default function Notifications() {
const { notifications } = useAppLayoutInternals();
if (!notifications) {
return null;
}
return <NotificationsImplementation />;
}

function NotificationsImplementation() {
const {
ariaLabels,
hasDrawerViewportOverlay,
notifications,
notificationsElement,
setNotificationsHeight,
stickyNotifications,
headerVariant,
hasNotificationsContent,
} = useAppLayoutInternals();
const ref = useRef<HTMLDivElement>(null);

if (!notifications) {
return null;
}
useResizeObserver(ref, entry => setNotificationsHeight(entry.contentBoxHeight));
useEffect(() => {
return () => {
setNotificationsHeight(0);
};
// unmount effect only
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

/**
* The notificationsElement ref is assigned to an inner div to prevent internal bottom margin
Expand All @@ -42,7 +57,7 @@ export default function Notifications() {
testutilStyles.notifications
)}
>
<div ref={notificationsElement}>{notifications}</div>
<div ref={ref}>{notifications}</div>
</div>
);
}

0 comments on commit f680654

Please sign in to comment.