@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
173 lines • 10.9 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, useRef } from 'react';
import clsx from 'clsx';
import { useContainerQuery } from '@awsui/component-toolkit';
import { getAnalyticsMetadataAttribute } from '@awsui/component-toolkit/internal/analytics-metadata';
import { InternalButton } from '../button/internal';
import InternalHeader from '../header/internal';
import { useInternalI18n } from '../i18n/context';
import { PerformanceMetrics } from '../internal/analytics';
import { FunnelNameSelectorContext, } from '../internal/analytics/context/analytics-context';
import { useFunnel, useFunnelStep, useFunnelSubStep } from '../internal/analytics/hooks/use-funnel';
import { getBaseProps } from '../internal/base-component';
import FocusLock from '../internal/components/focus-lock';
import Portal from '../internal/components/portal';
import { ButtonContext } from '../internal/context/button-context';
import { ModalContext } from '../internal/context/modal-context';
import ResetContextsForModal from '../internal/context/reset-contexts-for-modal';
import { fireNonCancelableEvent } from '../internal/events';
import { useContainerBreakpoints } from '../internal/hooks/container-queries';
import { useIntersectionObserver } from '../internal/hooks/use-intersection-observer';
import { useMergeRefs } from '../internal/hooks/use-merge-refs';
import { useUniqueId } from '../internal/hooks/use-unique-id';
import { useVisualRefresh } from '../internal/hooks/use-visual-mode';
import { KeyCode } from '../internal/keycode';
import { disableBodyScrolling, enableBodyScrolling } from './body-scroll';
import analyticsSelectors from './analytics-metadata/styles.css.js';
import styles from './styles.css.js';
export function InternalModalAsFunnel(props) {
const { funnelProps, funnelSubmit, funnelNextOrSubmitAttempt } = useFunnel();
const { funnelStepProps } = useFunnelStep();
const { subStepRef, funnelSubStepProps } = useFunnelSubStep();
const onButtonClick = ({ variant }) => {
if (variant === 'primary') {
funnelNextOrSubmitAttempt();
funnelSubmit();
}
};
return (React.createElement(InternalModal, Object.assign({ __funnelProps: funnelProps, __funnelStepProps: funnelStepProps, __subStepRef: subStepRef, __subStepFunnelProps: funnelSubStepProps, onButtonClick: onButtonClick }, props)));
}
export default function InternalModal(_a) {
var { modalRoot, getModalRoot, removeModalRoot } = _a, rest = __rest(_a, ["modalRoot", "getModalRoot", "removeModalRoot"]);
return (React.createElement(Portal, { container: modalRoot, getContainer: getModalRoot, removeContainer: removeModalRoot },
React.createElement(PortaledModal, Object.assign({}, rest))));
}
// Separate component to prevent the Portal from getting in the way of refs, as it needs extra cycles to render the inner components.
// useContainerQuery needs its targeted element to exist on the first render in order to work properly.
function PortaledModal(_a) {
var _b;
var { size, visible, header, children, footer, disableContentPaddings, onButtonClick = () => { }, onDismiss, __internalRootRef = null, __injectAnalyticsComponentMetadata, __funnelProps, __funnelStepProps, __subStepRef, __subStepFunnelProps, referrerId } = _a, rest = __rest(_a, ["size", "visible", "header", "children", "footer", "disableContentPaddings", "onButtonClick", "onDismiss", "__internalRootRef", "__injectAnalyticsComponentMetadata", "__funnelProps", "__funnelStepProps", "__subStepRef", "__subStepFunnelProps", "referrerId"]);
const instanceUniqueId = useUniqueId();
const headerId = `${rest.id || instanceUniqueId}-header`;
const lastMouseDownElementRef = useRef(null);
const [breakpoint, breakpointsRef] = useContainerBreakpoints(['xs']);
const i18n = useInternalI18n('modal');
const closeAriaLabel = i18n('closeAriaLabel', rest.closeAriaLabel);
const refObject = useRef(null);
const mergedRef = useMergeRefs(breakpointsRef, refObject, __internalRootRef);
const isRefresh = useVisualRefresh();
const baseProps = getBaseProps(rest);
const analyticsComponentMetadata = {
name: 'awsui.Modal',
label: `.${analyticsSelectors.header} h2`,
};
const metadataAttribute = __injectAnalyticsComponentMetadata
? getAnalyticsMetadataAttribute({ component: analyticsComponentMetadata })
: {};
const loadStartTime = useRef(0);
const loadCompleteTime = useRef(0);
const componentLoadingCount = useRef(0);
const performanceMetricLogged = useRef(false);
// enable body scroll and restore focus if unmounting while visible
useEffect(() => {
return () => {
enableBodyScrolling();
};
}, []);
const resetModalPerformanceData = () => {
loadStartTime.current = performance.now();
loadCompleteTime.current = 0;
performanceMetricLogged.current = false;
};
const emitTimeToContentReadyInModal = (loadCompleteTime) => {
var _a;
if (componentLoadingCount.current === 0 &&
loadStartTime.current &&
loadStartTime.current !== 0 &&
!performanceMetricLogged.current) {
const timeToContentReadyInModal = loadCompleteTime - loadStartTime.current;
PerformanceMetrics.modalPerformanceData({
timeToContentReadyInModal,
instanceIdentifier: instanceUniqueId,
componentIdentifier: ((_a = headerRef.current) === null || _a === void 0 ? void 0 : _a.textContent) || '',
});
performanceMetricLogged.current = true;
}
};
const MODAL_READY_TIMEOUT = 100;
/**
* This useEffect is triggered when the visible attribute of modal changes.
* When modal becomes visible, modal performance metrics are reset marking the beginning loading process.
* To ensure that the modal component ready metric is always emitted, a setTimeout is implemented.
* This setTimeout automatically emits the component ready metric after a specified duration.
*/
useEffect(() => {
if (visible) {
disableBodyScrolling();
resetModalPerformanceData();
setTimeout(() => {
emitTimeToContentReadyInModal(loadStartTime.current);
}, MODAL_READY_TIMEOUT);
}
else {
enableBodyScrolling();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]);
// Because we hide the element with styles (and not actually detach it from DOM), we need to scroll to top
useEffect(() => {
if (visible && refObject.current) {
refObject.current.scrollTop = 0;
}
}, [visible]);
const dismiss = (reason) => fireNonCancelableEvent(onDismiss, { reason });
const onOverlayMouseDown = (event) => {
lastMouseDownElementRef.current = event.target;
};
const onOverlayClick = (event) => {
const overlay = refObject.current;
const lastClicked = lastMouseDownElementRef.current;
if (event.target === overlay && lastClicked === overlay) {
dismiss('overlay');
}
};
const onCloseButtonClick = () => dismiss('closeButton');
const escKeyHandler = (event) => {
if (event.keyCode === KeyCode.escape) {
dismiss('keyboard');
}
};
// We use an empty div element at the end of the content slot as a sentinel
// to detect when the user has scrolled to the bottom.
const { ref: stickySentinelRef, isIntersecting: footerStuck } = useIntersectionObserver();
// Add extra scroll padding to account for the height of the sticky footer,
// to prevent it from covering focused elements.
const [footerHeight, footerRef] = useContainerQuery(rect => rect.borderBoxHeight);
const headerRef = useRef(null);
const { subStepRef } = useFunnelSubStep();
return (React.createElement(FunnelNameSelectorContext.Provider, { value: `.${styles['header--text']}` },
React.createElement(ResetContextsForModal, null,
React.createElement(ModalContext.Provider, { value: {
isInModal: true,
componentLoadingCount,
emitTimeToContentReadyInModal,
} },
React.createElement("div", Object.assign({}, baseProps, __funnelProps, __funnelStepProps, { className: clsx(styles.root, { [styles.hidden]: !visible }, baseProps.className, isRefresh && styles.refresh), role: "dialog", "aria-labelledby": headerId, onMouseDown: onOverlayMouseDown, onClick: onOverlayClick, ref: mergedRef, style: footerHeight ? { scrollPaddingBottom: footerHeight } : undefined, "data-awsui-referrer-id": ((_b = subStepRef.current) === null || _b === void 0 ? void 0 : _b.id) || referrerId }),
React.createElement(FocusLock, { disabled: !visible, autoFocus: true, restoreFocus: true, className: styles['focus-lock'] },
React.createElement("div", Object.assign({ className: clsx(styles.dialog, styles[size], styles[`breakpoint-${breakpoint}`], isRefresh && styles.refresh), onKeyDown: escKeyHandler }, metadataAttribute),
React.createElement("div", { className: styles.container },
React.createElement("div", { className: clsx(styles.header, analyticsSelectors.header) },
React.createElement(InternalHeader, { variant: "h2", __disableActionsWrapping: true, actions: React.createElement("div", Object.assign({}, getAnalyticsMetadataAttribute({
action: 'dismiss',
})),
React.createElement(InternalButton, { ariaLabel: closeAriaLabel, className: styles['dismiss-control'], variant: "modal-dismiss", iconName: "close", formAction: "none", onClick: onCloseButtonClick })) },
React.createElement("span", { ref: headerRef, id: headerId, className: styles['header--text'] }, header))),
React.createElement("div", Object.assign({ ref: __subStepRef }, __subStepFunnelProps, { className: clsx(styles.content, { [styles['no-paddings']]: disableContentPaddings }) }),
children,
React.createElement("div", { ref: stickySentinelRef })),
footer && (React.createElement(ButtonContext.Provider, { value: { onClick: onButtonClick } },
React.createElement("div", { ref: footerRef, className: clsx(styles.footer, footerStuck && styles['footer--stuck']) }, footer)))))))))));
}
//# sourceMappingURL=internal.js.map