@carbon/ibm-products
Version:
Carbon for IBM Products
233 lines (231 loc) • 8.45 kB
JavaScript
/**
* Copyright IBM Corp. 2020, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { __toESM } from "../../_virtual/_rolldown/runtime.js";
import { require_classnames } from "../../node_modules/classnames/index.js";
import { pkg } from "../../settings.js";
import { useIsomorphicEffect } from "../../global/js/hooks/useIsomorphicEffect.js";
import { getDevtoolsProps } from "../../global/js/utils/devtools.js";
import "../Coachmark/utils/enums.js";
import { CoachmarkContext } from "../Coachmark/utils/context.js";
import { CoachmarkOverlay } from "../Coachmark/CoachmarkOverlay.js";
import { CoachmarkTagline } from "../Coachmark/CoachmarkTagline.js";
import { CoachmarkStackHome } from "./CoachmarkStackHome.js";
import React, { Children, useCallback, useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
import { createPortal } from "react-dom";
//#region src/components/CoachmarkStack/CoachmarkStack.tsx
/**
* Copyright IBM Corp. 2023, 2025
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
var import_classnames = /* @__PURE__ */ __toESM(require_classnames());
const blockClass = `${pkg.prefix}--coachmark-stack`;
const componentName = "CoachmarkStack";
const elementBlockClass = `${pkg.prefix}--coachmark-stack-element`;
const defaults = {
onClose: () => {},
theme: "light",
portalTarget: "body"
};
/**
* Stacked coachmarks are used to call out specific functionality or concepts
* within the UI that may not be intuitive but are important for the
* user to gain understanding of the product's main value and discover new use cases.
* This variant allows the stacking of multiple coachmark overlays to be displayed by interacting with the tagline.
* @deprecated This component is deprecated.
*/
const CoachmarkStack = React.forwardRef(({ children, className, onClose = defaults.onClose, description, renderMedia, navLinkLabels, portalTarget = defaults.portalTarget, closeButtonLabel, tagline, theme = defaults.theme, title, tooltipAlign, closeIconDescription, ...rest }, ref) => {
const portalNode = useRef(null);
useIsomorphicEffect(() => {
portalNode.current = portalTarget ? document?.querySelector(portalTarget) ?? document?.querySelector("body") : document?.querySelector("body");
}, [portalTarget]);
const stackHomeRef = useRef(null);
const stackedCoachmarkRefs = useRef([]);
const [isOpen, setIsOpen] = useState(false);
const [selectedItemNumber, setSelectedItemNumber] = useState(0);
const [parentHeight, setParentHeight] = useState();
const childArray = Children.toArray(children);
const mountedRef = useRef(void 0);
const delayMs = 240;
const handleClickNavItem = (itemNumber) => {
setSelectedItemNumber(itemNumber);
};
const handleClose = useCallback((isParentCloseButton) => {
if (isParentCloseButton) {
setSelectedItemNumber(-1);
const timer = setTimeout(() => {
setIsOpen(false);
onClose();
}, delayMs);
return () => clearTimeout(timer);
} else setSelectedItemNumber(0);
}, [onClose]);
const escFunction = useCallback((event) => {
if (event.key === "Escape") if (selectedItemNumber === 0) handleClose(true);
else handleClose(false);
}, [handleClose, selectedItemNumber]);
useEffect(() => {
document.addEventListener("keydown", escFunction, false);
return () => {
document.removeEventListener("keydown", escFunction, false);
};
}, [escFunction]);
const contextValue = {
buttonProps: {
tabIndex: 0,
"aria-expanded": isOpen,
onClick: () => {
setIsOpen(true);
},
onDoubleClick: () => {
setIsOpen(true);
}
},
closeButtonProps: { onClick: () => handleClose(false) },
isOpen,
closeIconDescription
};
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
useEffect(() => {
const targetSelectedItem = selectedItemNumber - 1;
if (!parentHeight) {
if (stackHomeRef.current) {
const height = stackHomeRef.current.clientHeight;
if (height > 0) setParentHeight(height);
}
return;
}
if (stackHomeRef.current) stackHomeRef.current.style.height = `${parentHeight}px`;
if (!isOpen || targetSelectedItem < 0) {
if (stackHomeRef.current) {
stackHomeRef.current.classList.remove(`${blockClass}--scaled-home`);
stackHomeRef.current.classList.add(`${blockClass}--unscaled-home`);
stackHomeRef.current.focus();
}
return;
}
const targetHomeHeight = stackedCoachmarkRefs.current[targetSelectedItem].clientHeight;
if (stackHomeRef.current) {
stackHomeRef.current.style.height = `calc(${targetHomeHeight}px + 3rem)`;
stackedCoachmarkRefs.current[targetSelectedItem].focus();
stackHomeRef.current.classList.remove(`${blockClass}--unscaled-home`);
stackHomeRef.current.classList.add(`${blockClass}--scaled-home`);
}
}, [
selectedItemNumber,
isOpen,
parentHeight
]);
const wrappedChildren = Children.map(childArray, (child, idx) => {
const mountedClass = mountedRef.current ? `${elementBlockClass}--is-mounted` : "";
return /* @__PURE__ */ React.createElement(CoachmarkOverlay, {
key: idx,
ref: (ref) => {
stackedCoachmarkRefs.current[idx] = ref;
},
kind: "stacked",
onClose: () => handleClose(false),
theme,
fixedIsVisible: false,
className: (0, import_classnames.default)(elementBlockClass, mountedClass, idx === selectedItemNumber - 1 && `${elementBlockClass}--is-visible`, mountedRef.current && `${elementBlockClass}--is-mounted`)
}, child);
});
return /* @__PURE__ */ React.createElement(CoachmarkContext.Provider, { value: contextValue }, /* @__PURE__ */ React.createElement("div", {
...rest,
className: (0, import_classnames.default)(blockClass, `${pkg.prefix}--coachmark-overlay--stack`, className),
ref,
...getDevtoolsProps(componentName)
}, /* @__PURE__ */ React.createElement(CoachmarkTagline, {
title: tagline,
onClose
}), /* @__PURE__ */ React.createElement(CoachmarkStackHome, {
ref: stackHomeRef,
className: (0, import_classnames.default)(`${pkg.prefix}--coachmark-overlay`, `${pkg.prefix}--coachmark-overlay__${theme}`, elementBlockClass, selectedItemNumber > 0 && `${elementBlockClass}--is-stacked`, selectedItemNumber > 0 && `${elementBlockClass}--is-stacked__${theme}`, isOpen && `${elementBlockClass}--is-visible`, mountedRef.current && `${elementBlockClass}--is-mounted`),
isOpen: isOpen && selectedItemNumber < 1,
description,
renderMedia,
navLinkLabels,
onClickNavItem: handleClickNavItem,
onClose: () => {
handleClose(true);
},
portalTarget,
closeButtonLabel,
title,
tooltipAlign
}), portalNode?.current ? createPortal(wrappedChildren, portalNode?.current) : null));
});
/**@ts-ignore*/
CoachmarkStack.deprecated = {
level: "warn",
details: `${componentName} is deprecated.`
};
CoachmarkStack.displayName = componentName;
CoachmarkStack.propTypes = {
/**
* CoachmarkStack should use a single CoachmarkOverlayElements component as a child.
*/
children: PropTypes.node.isRequired,
/**
* Provide an optional class to be applied to the containing node.
*/
className: PropTypes.string,
/**
* The label for the button that will close the Stack
*/
closeButtonLabel: PropTypes.string,
/**
* Tooltip text and aria label for the Close button icon.
*/
closeIconDescription: PropTypes.string,
/**
* The description of the Coachmark.
*/
description: PropTypes.node.isRequired,
/**
* The labels used to link to the stackable Coachmarks.
*/
navLinkLabels: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
/**
* Function to call when the CoachmarkStack closes.
*/
onClose: PropTypes.func,
/**
* Where in the DOM to render the stack.
* The default is `document.body`.
*/
portalTarget: PropTypes.string,
/**
* Optional prop to render any media like images or animated media.
*/
renderMedia: PropTypes.func,
/**
* The tagline title which will be fixed to the bottom right of the window and will serve as the display trigger.
*/
tagline: PropTypes.string.isRequired,
/**
* Determines the theme of the component.
*/
theme: PropTypes.oneOf(["light", "dark"]),
/**
* The title of the Coachmark.
*/
title: PropTypes.string.isRequired,
/**
* Label's tooltip position
*/
tooltipAlign: PropTypes.oneOf(["top", "bottom"])
};
//#endregion
export { CoachmarkStack };