@awsui/components-react
Version:
On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en
351 lines • 21.6 kB
JavaScript
import { __rest } from "tslib";
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
import { useStableCallback } from '@awsui/component-toolkit/internal';
import ScreenreaderOnly from '../../internal/components/screenreader-only';
import { fireNonCancelableEvent } from '../../internal/events';
import { useControllable } from '../../internal/hooks/use-controllable';
import { useIntersectionObserver } from '../../internal/hooks/use-intersection-observer';
import { useMergeRefs } from '../../internal/hooks/use-merge-refs';
import { useMobile } from '../../internal/hooks/use-mobile';
import { useUniqueId } from '../../internal/hooks/use-unique-id';
import { useGetGlobalBreadcrumbs } from '../../internal/plugins/helpers/use-global-breadcrumbs';
import globalVars from '../../internal/styles/global-vars';
import { getSplitPanelDefaultSize } from '../../split-panel/utils/size-utils';
import { MIN_DRAWER_SIZE, useDrawers } from '../utils/use-drawers';
import { useFocusControl, useMultipleFocusControl } from '../utils/use-focus-control';
import { useSplitPanelFocusControl } from '../utils/use-split-panel-focus-control';
import { ActiveDrawersContext } from '../utils/visibility-context';
import { computeHorizontalLayout, computeSplitPanelOffsets, computeVerticalLayout, CONTENT_PADDING, } from './compute-layout';
import { AppLayoutVisibilityContext } from './contexts';
import { AppLayoutDrawer, AppLayoutGlobalDrawers, AppLayoutNavigation, AppLayoutNotifications, AppLayoutSplitPanelBottom, AppLayoutSplitPanelSide, AppLayoutToolbar, } from './internal';
import { useMultiAppLayout } from './multi-layout';
import { SkeletonLayout } from './skeleton';
const AppLayoutVisualRefreshToolbar = React.forwardRef((_a, forwardRef) => {
var _b, _c;
var { ariaLabels, contentHeader, content, navigationOpen, navigationWidth, navigation, navigationHide, onNavigationChange, tools, toolsOpen: controlledToolsOpen, onToolsChange, toolsHide, toolsWidth, contentType, headerVariant, breadcrumbs, notifications, stickyNotifications, splitPanelPreferences: controlledSplitPanelPreferences, splitPanelOpen: controlledSplitPanelOpen, splitPanel, splitPanelSize: controlledSplitPanelSize, onSplitPanelToggle, onSplitPanelResize, onSplitPanelPreferencesChange, disableContentPaddings, minContentWidth, maxContentWidth, placement, navigationTriggerHide } = _a, rest = __rest(_a, ["ariaLabels", "contentHeader", "content", "navigationOpen", "navigationWidth", "navigation", "navigationHide", "onNavigationChange", "tools", "toolsOpen", "onToolsChange", "toolsHide", "toolsWidth", "contentType", "headerVariant", "breadcrumbs", "notifications", "stickyNotifications", "splitPanelPreferences", "splitPanelOpen", "splitPanel", "splitPanelSize", "onSplitPanelToggle", "onSplitPanelResize", "onSplitPanelPreferencesChange", "disableContentPaddings", "minContentWidth", "maxContentWidth", "placement", "navigationTriggerHide"]);
const isMobile = useMobile();
const { __embeddedViewMode: embeddedViewMode, __forceDeduplicationType: forceDeduplicationType } = rest;
const splitPanelControlId = useUniqueId('split-panel');
const [toolbarState, setToolbarState] = useState('show');
const [toolbarHeight, setToolbarHeight] = useState(0);
const [notificationsHeight, setNotificationsHeight] = useState(0);
const [navigationAnimationDisabled, setNavigationAnimationDisabled] = useState(true);
const [splitPanelAnimationDisabled, setSplitPanelAnimationDisabled] = useState(true);
const [isNested, setIsNested] = useState(false);
const rootRef = useRef(null);
const [toolsOpen = false, setToolsOpen] = useControllable(controlledToolsOpen, onToolsChange, false, {
componentName: 'AppLayout',
controlledProp: 'toolsOpen',
changeHandler: 'onToolsChange',
});
const onToolsToggle = (open) => {
setToolsOpen(open);
drawersFocusControl.setFocus();
fireNonCancelableEvent(onToolsChange, { open });
};
const onGlobalDrawerFocus = (drawerId, open) => {
globalDrawersFocusControl.setFocus({ force: true, drawerId, open });
};
const onAddNewActiveDrawer = (drawerId) => {
var _a, _b;
// If a local drawer is already open, and we attempt to open a new one,
// it will replace the existing one instead of opening an additional drawer,
// since only one local drawer is supported. Therefore, layout calculations are not necessary.
if (activeDrawer && (drawers === null || drawers === void 0 ? void 0 : drawers.find(drawer => drawer.id === drawerId))) {
return;
}
// get the size of drawerId. it could be either local or global drawer
const combinedDrawers = [...(drawers || []), ...globalDrawers];
const newDrawer = combinedDrawers.find(drawer => drawer.id === drawerId);
if (!newDrawer) {
return;
}
const newDrawerSize = Math.min((_b = (_a = newDrawer.defaultSize) !== null && _a !== void 0 ? _a : drawerSizes[drawerId]) !== null && _b !== void 0 ? _b : MIN_DRAWER_SIZE, MIN_DRAWER_SIZE);
// check if the active drawers could be resized to fit the new drawers
// to do this, we need to take all active drawers, sum up their min sizes, truncate it from resizableSpaceAvailable
// and compare a given number with the new drawer id min size
// the total size of all global drawers resized to their min size
const availableSpaceForNewDrawer = resizableSpaceAvailable - totalActiveDrawersMinSize;
if (availableSpaceForNewDrawer >= newDrawerSize) {
return;
}
// now we made sure we cannot accommodate the new drawer with existing ones
closeFirstDrawer();
};
const { drawers, activeDrawer, minDrawerSize, minGlobalDrawersSizes, activeDrawerSize, ariaLabelsWithDrawers, globalDrawers, activeGlobalDrawers, activeGlobalDrawersIds, activeGlobalDrawersSizes, drawerSizes, drawersOpenQueue, onActiveDrawerChange, onActiveDrawerResize, onActiveGlobalDrawersChange, } = useDrawers(Object.assign(Object.assign({}, rest), { onGlobalDrawerFocus, onAddNewActiveDrawer }), ariaLabels, {
ariaLabels,
toolsHide,
toolsOpen,
tools,
toolsWidth,
onToolsToggle,
});
const onActiveDrawerChangeHandler = (drawerId, params = { initiatedByUserAction: true }) => {
onActiveDrawerChange(drawerId, params);
drawersFocusControl.setFocus();
};
const [splitPanelOpen = false, setSplitPanelOpen] = useControllable(controlledSplitPanelOpen, onSplitPanelToggle, false, {
componentName: 'AppLayout',
controlledProp: 'splitPanelOpen',
changeHandler: 'onSplitPanelToggle',
});
const onSplitPanelToggleHandler = () => {
setSplitPanelAnimationDisabled(false);
setSplitPanelOpen(!splitPanelOpen);
splitPanelFocusControl.setLastInteraction({ type: splitPanelOpen ? 'close' : 'open' });
fireNonCancelableEvent(onSplitPanelToggle, { open: !splitPanelOpen });
};
const [splitPanelPreferences, setSplitPanelPreferences] = useControllable(controlledSplitPanelPreferences, onSplitPanelPreferencesChange, undefined, {
componentName: 'AppLayout',
controlledProp: 'splitPanelPreferences',
changeHandler: 'onSplitPanelPreferencesChange',
});
const onSplitPanelPreferencesChangeHandler = (detail) => {
setSplitPanelPreferences(detail);
splitPanelFocusControl.setLastInteraction({ type: 'position' });
fireNonCancelableEvent(onSplitPanelPreferencesChange, detail);
};
const [splitPanelSize = 0, setSplitPanelSize] = useControllable(controlledSplitPanelSize, onSplitPanelResize, getSplitPanelDefaultSize((_b = splitPanelPreferences === null || splitPanelPreferences === void 0 ? void 0 : splitPanelPreferences.position) !== null && _b !== void 0 ? _b : 'bottom'), { componentName: 'AppLayout', controlledProp: 'splitPanelSize', changeHandler: 'onSplitPanelResize' });
const [splitPanelReportedSize, setSplitPanelReportedSize] = useState(0);
const [splitPanelHeaderBlockSize, setSplitPanelHeaderBlockSize] = useState(0);
const onSplitPanelResizeHandler = (size) => {
setSplitPanelSize(size);
fireNonCancelableEvent(onSplitPanelResize, { size });
};
const [splitPanelToggleConfig, setSplitPanelToggleConfig] = useState({
ariaLabel: undefined,
displayed: false,
});
const globalDrawersFocusControl = useMultipleFocusControl(true, activeGlobalDrawersIds);
const drawersFocusControl = useFocusControl(!!(activeDrawer === null || activeDrawer === void 0 ? void 0 : activeDrawer.id), true, activeDrawer === null || activeDrawer === void 0 ? void 0 : activeDrawer.id);
const navigationFocusControl = useFocusControl(navigationOpen, navigationTriggerHide);
const splitPanelFocusControl = useSplitPanelFocusControl([splitPanelPreferences, splitPanelOpen]);
const onNavigationToggle = useStableCallback((open) => {
setNavigationAnimationDisabled(false);
navigationFocusControl.setFocus();
fireNonCancelableEvent(onNavigationChange, { open });
});
useImperativeHandle(forwardRef, () => ({
closeNavigationIfNecessary: () => isMobile && onNavigationToggle(false),
openTools: () => onToolsToggle(true),
focusToolsClose: () => drawersFocusControl.setFocus(true),
focusActiveDrawer: () => drawersFocusControl.setFocus(true),
focusSplitPanel: () => { var _a; return (_a = splitPanelFocusControl.refs.slider.current) === null || _a === void 0 ? void 0 : _a.focus(); },
focusNavigation: () => navigationFocusControl.setFocus(true),
}));
const resolvedStickyNotifications = !!stickyNotifications && !isMobile;
//navigation must be null if hidden so toolbar knows to hide the toggle button
const resolvedNavigation = navigationHide ? null : navigation || React.createElement(React.Fragment, null);
//navigation must not be open if navigationHide is true
const resolvedNavigationOpen = !!resolvedNavigation && navigationOpen;
const { maxDrawerSize, maxSplitPanelSize, splitPanelForcedPosition, splitPanelPosition, maxGlobalDrawersSizes, resizableSpaceAvailable, } = computeHorizontalLayout({
activeDrawerSize: activeDrawer ? activeDrawerSize : 0,
splitPanelSize,
minContentWidth,
navigationOpen: resolvedNavigationOpen,
navigationWidth,
placement,
splitPanelOpen,
splitPanelPosition: splitPanelPreferences === null || splitPanelPreferences === void 0 ? void 0 : splitPanelPreferences.position,
isMobile,
activeGlobalDrawersSizes,
});
const { ref: intersectionObserverRef, isIntersecting } = useIntersectionObserver({ initialState: true });
const { registered, toolbarProps } = useMultiAppLayout({
forceDeduplicationType,
ariaLabels: ariaLabelsWithDrawers,
navigation: resolvedNavigation && !navigationTriggerHide,
navigationOpen: resolvedNavigationOpen,
onNavigationToggle,
navigationFocusRef: navigationFocusControl.refs.toggle,
breadcrumbs,
activeDrawerId: (_c = activeDrawer === null || activeDrawer === void 0 ? void 0 : activeDrawer.id) !== null && _c !== void 0 ? _c : null,
// only pass it down if there are non-empty drawers or tools
drawers: (drawers === null || drawers === void 0 ? void 0 : drawers.length) || !toolsHide ? drawers : undefined,
globalDrawersFocusControl,
globalDrawers: (globalDrawers === null || globalDrawers === void 0 ? void 0 : globalDrawers.length) ? globalDrawers : undefined,
activeGlobalDrawersIds,
onActiveGlobalDrawersChange,
onActiveDrawerChange: onActiveDrawerChangeHandler,
drawersFocusRef: drawersFocusControl.refs.toggle,
splitPanel,
splitPanelToggleProps: Object.assign(Object.assign({}, splitPanelToggleConfig), { active: splitPanelOpen, controlId: splitPanelControlId, position: splitPanelPosition }),
splitPanelFocusRef: splitPanelFocusControl.refs.toggle,
onSplitPanelToggle: onSplitPanelToggleHandler,
}, isIntersecting);
const hasToolbar = !embeddedViewMode && !!toolbarProps;
const discoveredBreadcrumbs = useGetGlobalBreadcrumbs(hasToolbar && !breadcrumbs);
const verticalOffsets = computeVerticalLayout({
topOffset: placement.insetBlockStart,
hasVisibleToolbar: hasToolbar && toolbarState !== 'hide',
notificationsHeight: notificationsHeight !== null && notificationsHeight !== void 0 ? notificationsHeight : 0,
toolbarHeight: toolbarHeight !== null && toolbarHeight !== void 0 ? toolbarHeight : 0,
stickyNotifications: resolvedStickyNotifications,
});
const appLayoutInternals = {
ariaLabels: ariaLabelsWithDrawers,
headerVariant,
isMobile,
breadcrumbs,
discoveredBreadcrumbs,
stickyNotifications: resolvedStickyNotifications,
navigationOpen: resolvedNavigationOpen,
navigation: resolvedNavigation,
navigationFocusControl,
activeDrawer,
activeDrawerSize,
minDrawerSize,
maxDrawerSize,
minGlobalDrawersSizes,
maxGlobalDrawersSizes,
drawers: drawers,
globalDrawers,
activeGlobalDrawers,
activeGlobalDrawersIds,
activeGlobalDrawersSizes,
onActiveGlobalDrawersChange,
drawersFocusControl,
globalDrawersFocusControl,
splitPanelPosition,
splitPanelToggleConfig,
splitPanelOpen,
splitPanelControlId,
splitPanelFocusControl,
placement,
toolbarState,
setToolbarState,
verticalOffsets,
drawersOpenQueue,
setToolbarHeight,
setNotificationsHeight,
onSplitPanelToggle: onSplitPanelToggleHandler,
onNavigationToggle,
onActiveDrawerChange: onActiveDrawerChangeHandler,
onActiveDrawerResize,
splitPanelAnimationDisabled,
};
const splitPanelInternals = {
bottomOffset: 0,
getMaxHeight: () => {
const availableHeight = document.documentElement.clientHeight - placement.insetBlockStart - placement.insetBlockEnd;
// If the page is likely zoomed in at 200%, allow the split panel to fill the content area.
return availableHeight < 400 ? availableHeight - 40 : availableHeight - 250;
},
maxWidth: maxSplitPanelSize,
isForcedPosition: splitPanelForcedPosition,
isOpen: splitPanelOpen,
leftOffset: 0,
onPreferencesChange: onSplitPanelPreferencesChangeHandler,
onResize: onSplitPanelResizeHandler,
onToggle: onSplitPanelToggleHandler,
position: splitPanelPosition,
reportSize: size => setSplitPanelReportedSize(size),
reportHeaderHeight: size => setSplitPanelHeaderBlockSize(size),
headerHeight: splitPanelHeaderBlockSize,
rightOffset: 0,
size: splitPanelSize,
topOffset: 0,
setSplitPanelToggle: setSplitPanelToggleConfig,
refs: splitPanelFocusControl.refs,
};
const closeFirstDrawer = useStableCallback(() => {
const drawerToClose = drawersOpenQueue[drawersOpenQueue.length - 1];
if (activeDrawer && (activeDrawer === null || activeDrawer === void 0 ? void 0 : activeDrawer.id) === drawerToClose) {
onActiveDrawerChange(null, { initiatedByUserAction: true });
}
else if (activeGlobalDrawersIds.includes(drawerToClose)) {
onActiveGlobalDrawersChange(drawerToClose, { initiatedByUserAction: true });
}
});
useEffect(() => {
// Close navigation drawer on mobile so that the main content is visible
if (isMobile) {
onNavigationToggle(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMobile]);
const getTotalActiveDrawersMinSize = () => {
var _a;
const combinedDrawers = [...(drawers || []), ...globalDrawers];
let result = activeGlobalDrawersIds
.map(activeDrawerId => {
var _a, _b;
return Math.min((_b = (_a = combinedDrawers.find(drawer => drawer.id === activeDrawerId)) === null || _a === void 0 ? void 0 : _a.defaultSize) !== null && _b !== void 0 ? _b : MIN_DRAWER_SIZE, MIN_DRAWER_SIZE);
})
.reduce((acc, curr) => acc + curr, 0);
if (activeDrawer) {
result += Math.min((_a = activeDrawer === null || activeDrawer === void 0 ? void 0 : activeDrawer.defaultSize) !== null && _a !== void 0 ? _a : MIN_DRAWER_SIZE, MIN_DRAWER_SIZE);
}
return result;
};
const totalActiveDrawersMinSize = getTotalActiveDrawersMinSize();
useEffect(() => {
if (isMobile) {
return;
}
const activeNavigationWidth = !navigationHide && navigationOpen ? navigationWidth : 0;
const scrollWidth = activeNavigationWidth + CONTENT_PADDING + totalActiveDrawersMinSize;
const hasHorizontalScroll = scrollWidth > placement.inlineSize;
if (hasHorizontalScroll) {
if (!navigationHide && navigationOpen) {
onNavigationToggle(false);
return;
}
closeFirstDrawer();
}
}, [
totalActiveDrawersMinSize,
closeFirstDrawer,
isMobile,
navigationHide,
navigationOpen,
navigationWidth,
onNavigationToggle,
placement.inlineSize,
]);
/**
* Returns true if the AppLayout is nested
* Does not apply to iframe
*/
const getIsNestedInAppLayout = (element) => {
var _a;
let currentElement = (_a = element === null || element === void 0 ? void 0 : element.parentElement) !== null && _a !== void 0 ? _a : null;
// this traverse is needed only for JSDOM
// in real browsers the globalVar will be propagated to all descendants and this loops exits after initial iteration
while (currentElement) {
if (getComputedStyle(currentElement).getPropertyValue(globalVars.stickyVerticalTopOffset)) {
return true;
}
currentElement = currentElement.parentElement;
}
return false;
};
useLayoutEffect(() => {
if (!hasToolbar) {
setIsNested(getIsNestedInAppLayout(rootRef.current));
}
}, [hasToolbar]);
const splitPanelOffsets = computeSplitPanelOffsets({
placement,
hasSplitPanel: !!splitPanel,
splitPanelOpen,
splitPanelPosition,
splitPanelFullHeight: splitPanelReportedSize,
splitPanelHeaderHeight: splitPanelHeaderBlockSize,
});
return (React.createElement(AppLayoutVisibilityContext.Provider, { value: isIntersecting },
!hasToolbar && breadcrumbs ? React.createElement(ScreenreaderOnly, null, breadcrumbs) : null,
React.createElement(SkeletonLayout, { ref: useMergeRefs(intersectionObserverRef, rootRef), isNested: isNested, style: Object.assign(Object.assign({ paddingBlockEnd: splitPanelOffsets.mainContentPaddingBlockEnd }, (hasToolbar || !isNested
? {
[globalVars.stickyVerticalTopOffset]: `${verticalOffsets.header}px`,
[globalVars.stickyVerticalBottomOffset]: `${splitPanelOffsets.stickyVerticalBottomOffset}px`,
}
: {})), (!isMobile ? { minWidth: `${minContentWidth}px` } : {})), toolbar: hasToolbar && React.createElement(AppLayoutToolbar, { appLayoutInternals: appLayoutInternals, toolbarProps: toolbarProps }), notifications: notifications && (React.createElement(AppLayoutNotifications, { appLayoutInternals: appLayoutInternals }, notifications)), headerVariant: headerVariant, contentHeader: contentHeader,
// delay rendering the content until registration of this instance is complete
content: registered ? content : null, navigation: resolvedNavigation && React.createElement(AppLayoutNavigation, { appLayoutInternals: appLayoutInternals }), navigationOpen: resolvedNavigationOpen, navigationWidth: navigationWidth, navigationAnimationDisabled: navigationAnimationDisabled, tools: drawers && drawers.length > 0 && React.createElement(AppLayoutDrawer, { appLayoutInternals: appLayoutInternals }), globalTools: React.createElement(ActiveDrawersContext.Provider, { value: activeGlobalDrawersIds },
React.createElement(AppLayoutGlobalDrawers, { appLayoutInternals: appLayoutInternals })), globalToolsOpen: !!activeGlobalDrawersIds.length, toolsOpen: !!activeDrawer, toolsWidth: activeDrawerSize, sideSplitPanel: splitPanelPosition === 'side' && (React.createElement(AppLayoutSplitPanelSide, { appLayoutInternals: appLayoutInternals, splitPanelInternals: splitPanelInternals }, splitPanel)), bottomSplitPanel: splitPanelPosition === 'bottom' && (React.createElement(AppLayoutSplitPanelBottom, { appLayoutInternals: appLayoutInternals, splitPanelInternals: splitPanelInternals }, splitPanel)), splitPanelOpen: splitPanelOpen, placement: placement, contentType: contentType, maxContentWidth: maxContentWidth, disableContentPaddings: disableContentPaddings })));
});
export default AppLayoutVisualRefreshToolbar;
//# sourceMappingURL=index.js.map