UNPKG

@carbon/ibm-products

Version:
273 lines (271 loc) 9.22 kB
/** * 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 "./utils/enums.js"; import { useClickOutsideElement, useWindowEvent } from "./utils/hooks.js"; import { CoachmarkContext } from "./utils/context.js"; import { CoachmarkOverlay } from "./CoachmarkOverlay.js"; import { throttle } from "../../global/js/utils/throttle.js"; import React, { forwardRef, useCallback, useEffect, useRef, useState } from "react"; import PropTypes from "prop-types"; import { Popover, PopoverContent } from "@carbon/react"; import { createPortal } from "react-dom"; //#region src/components/Coachmark/Coachmark.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`; const overlayBlockClass = `${blockClass}-overlay`; const componentName = "Coachmark"; const defaults = { align: "bottom", onClose: () => {}, overlayKind: "tooltip", theme: "light", isOpenByDefault: false, closeIconDescription: "" }; /** * 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. * @deprecated This component is deprecated. */ const Coachmark = forwardRef(({ align = defaults.align, autoAlign, children, className, onClose = defaults.onClose, overlayClassName, overlayKind = defaults.overlayKind, overlayRef, positionTune, portalTarget, target, theme = defaults.theme, isOpenByDefault = defaults.isOpenByDefault, closeIconDescription = defaults.closeIconDescription, ...rest }, ref) => { const isBeacon = overlayKind === "tooltip"; const isStacked = overlayKind === "stacked"; const [isOpen, setIsOpen] = useState(isStacked || isOpenByDefault); const [shouldResetPosition, setShouldResetPosition] = useState(false); const [targetRect, setTargetRect] = useState(); const [targetOffset, setTargetOffset] = useState({ x: 0, y: 0 }); const overlayBackupRef = useRef(void 0); const backupRef = useRef(void 0); const _coachmarkRef = ref || backupRef; const _overlayRef = overlayRef || overlayBackupRef; const portalNode = useRef(null); const popoverRef = useRef(null); let targetName; if (React.isValidElement(target) && typeof target.type !== "string") targetName = target.type; useIsomorphicEffect(() => { portalNode.current = portalTarget ? document?.querySelector(portalTarget) ?? document?.querySelector("body") : document?.querySelector("body"); }, [portalTarget]); const closeOverlay = () => { setIsOpen(false); }; const handleClose = useCallback(() => { if (isStacked) onClose(); else { setIsOpen(false); onClose(); } }, [isStacked, onClose]); const escFunction = useCallback((event) => { if (event.key === "Escape") handleClose(); }, [handleClose]); useEffect(() => { document.addEventListener("keydown", escFunction, false); return () => { document.removeEventListener("keydown", escFunction, false); }; }, [escFunction]); const handleTargetClick = (e) => { setTargetRect(e.target.getBoundingClientRect()); setTargetOffset({ x: e.target.offsetLeft, y: e.target.offsetTop }); if (isBeacon) setIsOpen((prevIsOpen) => !prevIsOpen); else { setIsOpen(false); setShouldResetPosition(true); } }; const contextValue = { buttonProps: { "aria-expanded": isOpen, tabIndex: 0, onClick: handleTargetClick, onDoubleClick: handleTargetClick }, closeButtonProps: { onClick: handleClose }, targetRect, targetOffset, align, positionTune, isOpen, closeIconDescription }; const handleResize = throttle(() => { closeOverlay(); }, 2e3); useEffect(() => { if (shouldResetPosition) { setShouldResetPosition(false); setIsOpen(true); } }, [shouldResetPosition]); useIsomorphicEffect(() => { const overlayPositionStyle = { top: `${(positionTune?.y ?? 0) - 16}px`, left: `${(positionTune?.x ?? 0) - 16}px` }; if (popoverRef.current && popoverRef.current.style && overlayPositionStyle) { const combinedStyle = { position: "absolute", ...overlayPositionStyle }; Object.assign(popoverRef.current.style, combinedStyle); } }, [popoverRef, positionTune]); useEffect(() => { return () => setIsOpen(false); }, []); useClickOutsideElement(_coachmarkRef, _overlayRef, overlayKind, closeOverlay); useWindowEvent("resize", handleResize); return /* @__PURE__ */ React.createElement(CoachmarkContext.Provider, { value: contextValue }, /* @__PURE__ */ React.createElement("div", { className: (0, import_classnames.default)(blockClass, `${blockClass}__${theme}`, className), ref: _coachmarkRef, ...rest, ...getDevtoolsProps(componentName) }, overlayKind !== "tooltip" ? /* @__PURE__ */ React.createElement(React.Fragment, null, targetName?.displayName === "CoachmarkBeacon" ? React.cloneElement(target, { buttonProps: contextValue.buttonProps }) : target, isOpen && portalNode?.current && createPortal(/* @__PURE__ */ React.createElement(CoachmarkOverlay, { ref: _overlayRef, fixedIsVisible: false, kind: overlayKind, onClose: handleClose, theme, className: (0, import_classnames.default)(overlayClassName, `${overlayBlockClass}--is-visible`) }, children), portalNode?.current)) : /* @__PURE__ */ React.createElement(Popover, { highContrast: true, caret: true, ref: popoverRef, align, autoAlign, open: isOpen }, targetName?.displayName === "CoachmarkBeacon" ? React.cloneElement(target, { buttonProps: contextValue.buttonProps }) : target, /* @__PURE__ */ React.createElement(PopoverContent, null, isOpen && /* @__PURE__ */ React.createElement(CoachmarkOverlay, { ref: _overlayRef, fixedIsVisible: false, kind: overlayKind, onClose: handleClose, theme, className: (0, import_classnames.default)(overlayClassName, { [`${overlayBlockClass}--is-visible`]: isOpen }) }, children))))); }); const overlayRefType = typeof HTMLElement === "undefined" ? PropTypes.object : PropTypes.instanceOf(HTMLElement); /**@ts-ignore*/ Coachmark.deprecated = { level: "warn", details: `${componentName} is deprecated.` }; Coachmark.displayName = componentName; const deprecatedProps = { /** * **Deprecated** * Optional class name for the Coachmark Overlay component. */ overlayClassName: PropTypes.string, /** * **Deprecated** * What kind or style of Coachmark to render. */ overlayKind: PropTypes.oneOf([ "tooltip", "floating", "stacked" ]), overlayRef: PropTypes.shape({ current: overlayRefType }), /** * **Deprecated** * By default, the Coachmark will be appended to the end of `document.body`. * The Coachmark will remain persistent as the user navigates the app until * the user closes the Coachmark. * * Alternatively, the app developer can tightly couple the Coachmark to a DOM * element or other component by specifying a CSS selector. The Coachmark will * remain visible as long as that element remains visible or mounted. When the * element is hidden or component is unmounted, the Coachmark will disappear. */ portalTarget: PropTypes.string, /** * **Deprecated** * The optional button or beacon that the user will click to show the Coachmark. */ target: PropTypes.node, /** * **Deprecated** * Determines the theme of the component. */ theme: PropTypes.oneOf(["light", "dark"]) }; Coachmark.propTypes = { /** * Where to render the Coachmark relative to its target. * Applies only to Floating and Tooltip Coachmarks. * @see COACHMARK_ALIGNMENT */ align: PropTypes.oneOf([ "bottom", "bottom-left", "bottom-right", "left", "left-top", "left-bottom", "right", "right-top", "right-bottom", "top", "top-left", "top-right" ]), /** * Auto aligns the coachmark based on screen boundaries * Applies only to Tooltip Coachmarks. */ autoAlign: PropTypes.bool, /** * Coachmark should use a single CoachmarkOverlayElements component as a child. * @see CoachmarkOverlayElements */ children: PropTypes.node.isRequired, /** * Optional class name for this component. */ className: PropTypes.string, /** * Tooltip text and aria label for the Close button icon. */ closeIconDescription: PropTypes.string, /** * Determines if the coachmark is open by default. * Does nothing if `overlayKind=stacked`. */ isOpenByDefault: PropTypes.bool, /** * Function to call when the Coachmark closes. */ onClose: PropTypes.func, /** * Fine tune the position of the target in pixels. Applies only to Beacons. */ positionTune: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }), ...deprecatedProps }; //#endregion export { Coachmark };