UNPKG

@carbon/ibm-products

Version:

Carbon for IBM Products

211 lines (200 loc) 7.72 kB
/** * Copyright IBM Corp. 2020, 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. */ 'use strict'; var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var React = require('react'); var enums = require('../Coachmark/utils/enums.js'); var context = require('../Coachmark/utils/context.js'); var CoachmarkOverlay = require('../Coachmark/CoachmarkOverlay.js'); var CoachmarkTagline = require('../Coachmark/CoachmarkTagline.js'); var index = require('../../_virtual/index.js'); var reactDom = require('react-dom'); var cx = require('classnames'); var devtools = require('../../global/js/utils/devtools.js'); var settings = require('../../settings.js'); var usePrefersReducedMotion = require('../../global/js/hooks/usePrefersReducedMotion.js'); var useIsomorphicEffect = require('../../global/js/hooks/useIsomorphicEffect.js'); // Carbon and package components we use. /* TODO: @import(s) of carbon components and other package components. */ // The block part of our conventional BEM class names (blockClass__E--M). const coachmarkClass = `${settings.pkg.prefix}--coachmark`; const blockClass = `${coachmarkClass}-fixed`; const overlayBlockClass = `${coachmarkClass}-overlay`; const componentName = 'CoachmarkFixed'; const defaults = { onClose: () => {}, theme: 'light', tagline: '' }; /** * Fixed 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 a coachmark overlay to be displayed by interacting with the tagline. */ exports.CoachmarkFixed = /*#__PURE__*/React.forwardRef((_ref, ref) => { let { children, className, onClose = defaults.onClose, portalTarget, tagline = defaults.tagline, theme = defaults.theme, // Collect any other property values passed in. ...rest } = _ref; const overlayRef = React.useRef(null); const portalNode = React.useRef(null); const [isOpen, setIsOpen] = React.useState(false); const [shouldResetPosition, setShouldResetPosition] = React.useState(false); const [targetRect, setTargetRect] = React.useState(); const [targetOffset, setTargetOffset] = React.useState({ x: 0, y: 0 }); const [fixedIsVisible, setFixedIsVisible] = React.useState(false); const shouldReduceMotion = usePrefersReducedMotion.usePrefersReducedMotion(); useIsomorphicEffect.useIsomorphicEffect(() => { portalNode.current = portalTarget ? document?.querySelector(portalTarget) ?? document?.querySelector('body') : document?.querySelector('body'); }, [portalTarget]); const handleClose = React.useCallback(() => { if (shouldReduceMotion) { setIsOpen(false); } else { setFixedIsVisible(false); } }, [shouldReduceMotion]); const handleTransitionEnd = e => { if (e.propertyName === 'transform' && !fixedIsVisible) { setIsOpen(false); onClose(); } }; const handleTargetClick = e => { setTargetRect(e.target.getBoundingClientRect()); setTargetOffset({ x: e.target.offsetLeft, y: e.target.offsetTop }); // reset position for all other kinds setIsOpen(false); setShouldResetPosition(true); }; const escFunction = React.useCallback(event => { if (event.key === 'Escape') { handleClose(); } }, [handleClose]); React.useEffect(() => { document.addEventListener('keydown', escFunction, false); return () => { document.removeEventListener('keydown', escFunction, false); }; }, [escFunction]); const contextValue = { buttonProps: { 'aria-expanded': isOpen, tabIndex: 0, onClick: handleTargetClick, // Compensate for accidental open/close on double-click. // Only open on double-click. onDoubleClick: handleTargetClick }, closeButtonProps: { onClick: handleClose }, targetRect: targetRect, targetOffset: targetOffset, isOpen: isOpen, tacos: 'tacos' }; // instead of toggling on/off, // keep open and reset to original position React.useEffect(() => { if (shouldResetPosition) { setShouldResetPosition(false); setIsOpen(true); } }, [shouldResetPosition]); React.useEffect(() => { setFixedIsVisible(isOpen); }, [isOpen]); // On unmount: // - DO NOT "Close()" the coachmark. // - This triggers a "signal" to close it forever. // - "Closing" should only ever be a user-triggered event. // - DO "hide" the coachmark. // - The app is doing the action for the user. // - The user will have the opportunity to open it again. React.useEffect(() => { return () => setIsOpen(false); }, []); return /*#__PURE__*/React.createElement(context.CoachmarkContext.Provider, { value: contextValue }, /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({}, rest, { className: cx(blockClass, // Apply the block class to the main HTML element `${blockClass}__${theme}`, className // Apply any supplied class names to the main HTML element. ), ref: ref }, devtools.getDevtoolsProps(componentName)), /*#__PURE__*/React.createElement(CoachmarkTagline.CoachmarkTagline, { title: tagline, onClose: onClose }), isOpen && portalNode?.current && /*#__PURE__*/reactDom.createPortal(/*#__PURE__*/React.createElement(CoachmarkOverlay.CoachmarkOverlay, { ref: overlayRef, fixedIsVisible: fixedIsVisible, kind: enums.COACHMARK_OVERLAY_KIND.FIXED, onClose: handleClose, onTransitionEnd: handleTransitionEnd, theme: theme, className: cx(fixedIsVisible && `${overlayBlockClass}--is-visible`, overlayBlockClass) }, children), // Default to `document.body` when `portalNode` is `null` portalNode?.current))); }); // Return a placeholder if not released and not enabled by feature flag exports.CoachmarkFixed = settings.pkg.checkComponentEnabled(exports.CoachmarkFixed, componentName); // The display name of the component, used by React. Note that displayName // is used in preference to relying on function.name. exports.CoachmarkFixed.displayName = componentName; // The types and DocGen commentary for the component props, // in alphabetical order (for consistency). // See https://www.npmjs.com/package/prop-types#usage. exports.CoachmarkFixed.propTypes = { // TODO: Add this to MDX as will be done with Coachmark /** * CoachmarkFixed should use a single CoachmarkOverlayElements component as a child. */ children: index.default.node.isRequired, /** * Optional class name for this component. */ className: index.default.string, /** * Function to call when the Coachmark closes. */ onClose: index.default.func, /** * 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: index.default.string, /** * The tagline title which will be fixed to the bottom right of the window and will serve as the display trigger. */ tagline: index.default.string.isRequired, /** * Determines the theme of the component. */ theme: index.default.oneOf(['light', 'dark']) };