@carbon/ibm-products
Version:
Carbon for IBM Products
240 lines (230 loc) • 7.46 kB
JavaScript
/**
* 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.
*/
import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
import React__default, { forwardRef, useState, useRef, useMemo, useEffect } from 'react';
import uuidv4 from '../../global/js/utils/uuidv4.js';
import PropTypes from '../../_virtual/index.js';
import cx from 'classnames';
import { getDevtoolsProps } from '../../global/js/utils/devtools.js';
import { pkg } from '../../settings.js';
import { CoachmarkDragbar } from './CoachmarkDragbar.js';
import { CoachmarkHeader } from './CoachmarkHeader.js';
import { getOffsetTune } from './utils/constants.js';
import { useCoachmark } from './utils/context.js';
import { COACHMARK_OVERLAY_KIND } from './utils/enums.js';
import { useIsomorphicEffect } from '../../global/js/hooks/useIsomorphicEffect.js';
// The block part of our conventional BEM class names (blockClass__E--M).
const blockClass = `${pkg.prefix}--coachmark-overlay`;
const componentName = 'CoachmarkOverlay';
// NOTE: the component SCSS is not imported here: it is rolled up separately.
const defaults = {
kind: COACHMARK_OVERLAY_KIND.FLOATING,
theme: 'light'
};
/**
* DO NOT USE. This component is for the exclusive use
* of other Onboarding components.
*/
let CoachmarkOverlay = /*#__PURE__*/forwardRef((_ref, ref) => {
let {
children,
onClose,
fixedIsVisible,
className,
kind = defaults.kind,
theme = defaults.theme,
...rest
} = _ref;
const {
winHeight,
winWidth
} = useWindowDimensions();
const [a11yDragMode, setA11yDragMode] = useState(false);
const overlayRef = useRef(null);
const coachmark = useCoachmark();
const isBeacon = kind === COACHMARK_OVERLAY_KIND.TOOLTIP;
const isDraggable = kind === COACHMARK_OVERLAY_KIND.FLOATING;
const isVisible = className?.includes('is-visible');
const handleKeyPress = event => {
const {
shiftKey,
key
} = event;
/* istanbul ignore next */
if (key === 'Enter' || key === ' ') {
setA11yDragMode(prevVal => !prevVal);
} else if (a11yDragMode) {
const distanceToMove = shiftKey ? 128 : 32;
switch (key) {
case 'ArrowLeft':
handleDrag(distanceToMove * -1, 0);
break;
case 'ArrowRight':
handleDrag(distanceToMove, 0);
break;
case 'ArrowUp':
handleDrag(0, distanceToMove * -1);
break;
case 'ArrowDown':
handleDrag(0, distanceToMove);
break;
}
}
};
const styledTune = useMemo(() => {
const style = {};
if (isBeacon || isDraggable) {
if (coachmark.targetRect) {
style.left = coachmark.targetRect.x + window.scrollX;
style.top = coachmark.targetRect.y + window.scrollY;
}
if (style.left && style.top) {
if (isBeacon) {
style.left = style.left + 16;
style.top = style.top + 16;
}
if (isDraggable) {
const offsetTune = getOffsetTune(coachmark, kind);
style.left = style.left + offsetTune.left;
style.top = style.top + offsetTune.top;
}
}
}
return style;
}, [isBeacon, isDraggable, coachmark, kind]);
/* istanbul ignore next */
function handleDragBounds(x, y) {
let xRes = x;
let yRes = y;
const xMax = winWidth - 288;
const yMax = winHeight - 150;
if (xRes < 0) {
xRes = 0;
} else if (xRes > xMax) {
xRes = xMax;
}
if (yRes < 0) {
yRes = 0;
} else if (yRes > yMax) {
yRes = yMax;
}
return {
targetX: xRes,
targetY: yRes
};
}
function handleDrag(movementX, movementY) {
const overlay = overlayRef.current;
if (!overlay) {
return;
}
const {
x,
y
} = overlay.getBoundingClientRect();
const {
targetX,
targetY
} = handleDragBounds(x + movementX, y + movementY);
overlay.style.transform = 'none';
overlay.style.position = 'fixed';
overlay.style.left = `${targetX}px`;
overlay.style.top = `${targetY}px`;
overlay.style.bottom = 'auto';
}
const contentId = uuidv4();
useIsomorphicEffect(() => {
if (overlayRef.current) {
const currentStyle = overlayRef.current?.style;
Object.keys(styledTune).forEach(key => {
const value = styledTune[key];
currentStyle.setProperty(key, `${value}px`);
});
}
}, [styledTune, overlayRef]);
return /*#__PURE__*/React__default.createElement("div", _extends({}, rest, {
className: cx(blockClass, `${blockClass}--${kind}`, `${blockClass}__${theme}`, (isBeacon || isDraggable) && `${blockClass}--${coachmark.align}`, fixedIsVisible && `${blockClass}--is-visible`, a11yDragMode && `${blockClass}--is-dragmode`, className),
ref: overlayRef,
"aria-labelledby": contentId,
tabIndex: -1
}, getDevtoolsProps(componentName)), isDraggable ? /*#__PURE__*/React__default.createElement(CoachmarkDragbar, {
a11yKeyboardHandler: handleKeyPress,
onBlur: () => setA11yDragMode(false),
onDrag: handleDrag,
theme: theme,
onClose: onClose
}) : /*#__PURE__*/React__default.createElement(CoachmarkHeader, {
onClose: onClose
}), /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__body`,
ref: ref,
id: contentId
}, React__default.Children.map(children, child => {
return /*#__PURE__*/React__default.cloneElement(child, {
isVisible
});
})));
});
function getWindowDimensions() {
const {
innerWidth: winWidth,
innerHeight: winHeight
} = window;
return {
winWidth,
winHeight
};
}
const useWindowDimensions = () => {
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
useEffect(() => {
/* istanbul ignore next */
function handleResize() {
setWindowDimensions(getWindowDimensions());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowDimensions;
};
// Return a placeholder if not released and not enabled by feature flag
CoachmarkOverlay = pkg.checkComponentEnabled(CoachmarkOverlay, componentName);
// The display name of the component, used by React. Note that displayName
// is used in preference to relying on function.name.
CoachmarkOverlay.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.
CoachmarkOverlay.propTypes = {
/**
* The CoachmarkOverlayElements child components.
* Validation is handled in the parent Coachmark component.
*/
children: PropTypes.node.isRequired,
/**
* Optional class name for this component.
*/
className: PropTypes.string,
/**
* The visibility of CoachmarkOverlay is
* managed in the parent Coachmark component.
*/
fixedIsVisible: PropTypes.bool.isRequired,
/**
* What kind or style of Coachmark to render.
*/
kind: PropTypes.oneOf(Object.values(COACHMARK_OVERLAY_KIND)),
/**
* Function to call when the Coachmark closes.
*/
onClose: PropTypes.func.isRequired,
/**
* Determines the theme of the component.
*/
theme: PropTypes.oneOf(['light', 'dark'])
};
export { CoachmarkOverlay };