orcs-design-system
Version:
TeamForm's Design System, aka: ORCS
450 lines (447 loc) • 19.6 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
const _excluded = ["children", "direction", "text", "textAlign", "width", "enableSelectAll", "variant", "ariaLabel", "inlineBlock", "withFocusControl", "offset", "headerAvatarSizing", "disabled"];
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import React, { useState, useMemo, useEffect } from "react";
import { useFloating, autoUpdate, offset, flip, shift, useHover, useFocus, useDismiss, useRole, useInteractions, FloatingPortal, safePolygon, FloatingFocusManager } from "@floating-ui/react";
import themeGet from "@styled-system/theme-get";
import styled from "styled-components";
import Icon from "../Icon";
import { PropTypes } from "prop-types";
import { getFloatingUiRootElement, getFloatingUiZIndex, isRenderedInReactSelectMenu } from "../../utils/floatingUiHelpers";
import { layout, space } from "styled-system";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const DIRECTIONS_MAP = {
topLeft: "top-start",
top: "top",
topRight: "top-end",
left: "left",
right: "right",
bottomLeft: "bottom-start",
bottom: "bottom",
bottomRight: "bottom-end"
};
const Container = /*#__PURE__*/styled.div.withConfig({
displayName: "Container",
componentId: "sc-1bwoak-0"
})(["", " ", " display:", ";position:relative;overflow:visible;"], space, layout, props => {
if (props.display) {
return props.display;
}
if (props.inlineBlock) {
return "inline-block";
}
return "block";
});
const TooltipControl = /*#__PURE__*/styled.div.withConfig({
displayName: "TooltipControl",
componentId: "sc-1bwoak-1"
})(["border:none;background:none;padding:0;cursor:help;font-size:1em;color:", ";transition:", ";&:hover,&:focus{outline:0;color:", ";}"], props => props.active ? themeGet("colors.primary")(props) : themeGet("colors.black")(props), themeGet("transition.transitionDefault"), themeGet("colors.primary"));
const StyledPopover = /*#__PURE__*/styled.div.withConfig({
displayName: "StyledPopover",
componentId: "sc-1bwoak-2"
})(["font-size:", ";line-height:", ";font-weight:", ";text-align:", ";word-break:break-word;color:", ";outline:0;padding:", ";border-radius:", ";width:", ";background:", ";border:1px solid ", ";box-shadow:", ";user-select:", ";&.hack-for-legacy-tests{position:absolute;pointer-events:none;opacity:0;visibility:hidden;height:0;width:0;padding:0;overflow:hidden;}&.visible{opacity:1;pointer-events:auto;visibility:visible;}& a{font-size:", ";}&::before{content:\"\";z-index:2;height:0;width:0;border-style:solid;border-width:6px 6px 6px 0;border-color:transparent;border-right-color:", ";left:-6px;top:50%;margin-top:-6px;position:absolute;}&::after{content:\"\";z-index:1;position:absolute;border-color:transparent;border-right-color:", ";height:0;width:0;border-style:solid;border-width:6px 6px 6px 0;left:-7px;top:50%;margin-top:-6px;}&.top{&::before{left:50%;top:auto;margin-top:0;bottom:-9px;margin-left:-3px;transform:rotate(-90deg);}&::after{left:50%;top:auto;margin-top:0;bottom:-10px;margin-left:-3px;transform:rotate(-90deg);}}&.topRight,&.top-end{&::before{left:1px;top:auto;margin-top:0;bottom:-5px;margin-left:-6px;transform:rotate(-45deg);border-width:5px 10px 5px 0;}&::after{left:1px;top:auto;margin-top:0;bottom:-6px;margin-left:-7px;transform:rotate(-45deg);border-width:5px 10px 5px 0;}}&.bottomRight,&.bottom-end{&::before{left:1px;bottom:auto;margin-top:0;top:-5px;margin-left:-6px;transform:rotate(45deg);border-width:5px 10px 5px 0;}&::after{left:1px;bottom:auto;margin-top:0;top:-6px;margin-left:-7px;transform:rotate(45deg);border-width:5px 10px 5px 0;}}&.bottom{&::before{left:50%;top:-9px;margin-top:0;margin-left:-3px;transform:rotate(90deg);}&::after{left:50%;top:-10px;margin-top:0;margin-left:-3px;transform:rotate(90deg);}}&.bottomLeft,&.bottom-start{&::before{right:1px;left:auto;bottom:auto;margin-top:0;top:-5px;margin-right:-6px;transform:rotate(135deg);border-width:5px 10px 5px 0;}&::after{right:1px;left:auto;bottom:auto;margin-top:0;top:-6px;margin-right:-7px;transform:rotate(135deg);border-width:5px 10px 5px 0;}}&.left{&::before{left:auto;right:-6px;transform:rotate(180deg);}&::after{left:auto;right:-7px;transform:rotate(180deg);top:50%;margin-top:-6px;}}&.topLeft,&.top-start{&::before{right:1px;left:auto;top:auto;margin-top:0;bottom:-5px;margin-right:-6px;transform:rotate(225deg);border-width:5px 10px 5px 0;}&::after{right:1px;left:auto;top:auto;margin-top:0;bottom:-6px;margin-right:-7px;transform:rotate(225deg);border-width:5px 10px 5px 0;}}"], themeGet("fontSizes.0"), themeGet("fontSizes.1"), themeGet("fontWeights.1"), props => props.textAlign ? props.textAlign : "left", themeGet("colors.greyDarkest"), themeGet("space.3"), themeGet("radii.1"), props => props.width ? props.width : "200px", themeGet("colors.white"), themeGet("colors.greyLight"), themeGet("shadows.boxDefault"), props => props.enableSelectAll ? "all" : "auto", themeGet("fontSizes.0"), themeGet("colors.white"), themeGet("colors.greyMid"));
/**
* Prevents the browser from scrolling to the previously focused element
* when a popover or tooltip closes — and skips focus entirely if it's offscreen.
*/
export function usePreventScrollOnRestoreFocus(enabled) {
useEffect(() => {
if (!enabled) {
return;
}
const previouslyFocused = document.activeElement;
if (!(previouslyFocused instanceof HTMLElement)) {
return;
}
const originalFocus = previouslyFocused.focus;
// Check if element is in the viewport
const isInViewport = isElementInViewport(previouslyFocused);
if (!isInViewport) {
// Skip restoring focus entirely
previouslyFocused.focus = () => {};
} else {
// Patch focus to use preventScroll
previouslyFocused.focus = function () {
try {
originalFocus.call(this, {
preventScroll: true
});
} catch (_unused) {
// fallback if preventScroll isn't accepted
originalFocus.call(this);
}
};
}
return () => {
previouslyFocused.focus = originalFocus;
};
}, [enabled]);
}
function isElementInViewport(el) {
const rect = el.getBoundingClientRect();
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth);
}
export default function Popover(_ref) {
var _refs$floating;
let {
children,
direction = "right",
text,
textAlign,
width,
enableSelectAll,
variant,
ariaLabel,
inlineBlock,
withFocusControl = true,
offset: customOffset,
headerAvatarSizing,
disabled
} = _ref,
props = _objectWithoutProperties(_ref, _excluded);
const [visible, setVisible] = useState(false);
const middleware = useMemo(() => [offset(_ref2 => {
let {
rects
} = _ref2;
const defaultOffset = {
mainAxis: 10,
alignmentAxis: -rects.floating.width
};
if (customOffset) {
// For simple placements (right, left, top, bottom), use crossAxis instead of alignmentAxis
const placement = DIRECTIONS_MAP[direction] || direction || "right";
const isSimplePlacement = !placement.includes("-");
if (isSimplePlacement && customOffset.alignmentAxis !== undefined) {
return _objectSpread(_objectSpread({}, defaultOffset), {}, {
mainAxis: customOffset.mainAxis !== undefined ? customOffset.mainAxis : defaultOffset.mainAxis,
crossAxis: customOffset.alignmentAxis
});
}
return _objectSpread(_objectSpread({}, defaultOffset), customOffset);
}
return defaultOffset;
}), flip({
fallbackAxisSideDirection: "start"
}), shift()], [direction, customOffset]);
const {
refs,
floatingStyles,
context
} = useFloating({
open: visible,
onOpenChange: disabled ? () => {} : setVisible,
placement: DIRECTIONS_MAP[direction] || direction || "right",
whileElementsMounted: (reference, floating, update) => autoUpdate(reference, floating, update, {
ancestorScroll: true,
ancestorResize: true,
elementResize: true,
layoutShift: true,
animationFrame: false
}),
middleware
});
const hover = useHover(context, {
move: false,
handleClose: safePolygon(),
delay: {
open: 400,
close: 0
}
});
const focus = useFocus(context);
const dismiss = useDismiss(context);
const role = useRole(context, {
role: "tooltip"
});
const {
getReferenceProps,
getFloatingProps
} = useInteractions([hover, focus, dismiss, role]);
const triggerProps = useMemo(() => _objectSpread(_objectSpread({}, getReferenceProps({
ref: refs.setReference
})), {}, {
tabIndex: props.tabIndex !== undefined ? props.tabIndex : "0"
}), [getReferenceProps, refs.setReference, props.tabIndex]);
const directionClass = useMemo(() => context.placement === DIRECTIONS_MAP[direction] ? direction : context.placement, [context.placement, direction]);
const style = useMemo(() => {
const baseStyle = _objectSpread(_objectSpread({}, floatingStyles), {}, {
zIndex: getFloatingUiZIndex(context.refs.reference)
});
// Add CSS-based positioning for Header Avatar sizing changes
if (headerAvatarSizing) {
const translateY = headerAvatarSizing === "large" ? -56 : -3;
baseStyle.transform = "".concat(baseStyle.transform || "", " translateY(").concat(translateY, "px)");
}
return baseStyle;
}, [floatingStyles, context.refs.reference, headerAvatarSizing]);
const containsLinks = withFocusControl && ((_refs$floating = refs.floating) === null || _refs$floating === void 0 || (_refs$floating = _refs$floating.current) === null || _refs$floating === void 0 ? void 0 : _refs$floating.querySelectorAll("a").length);
const visiblePopoverClassName = useMemo(() => "Tooltip popover visible ".concat(directionClass), [directionClass]);
const floatingProps = useMemo(() => getFloatingProps(_objectSpread(_objectSpread({}, props), {}, {
className: "".concat(props.className, " ").concat(visiblePopoverClassName)
})), [getFloatingProps, props, visiblePopoverClassName]);
usePreventScrollOnRestoreFocus(!visible);
return /*#__PURE__*/_jsxs(Container, _objectSpread(_objectSpread(_objectSpread({
inlineBlock: inlineBlock
}, props), triggerProps), {}, {
"aria-describedby": context.floatingId,
children: [variant === "tooltip" && /*#__PURE__*/_jsx(TooltipControl, {
active: visible,
tabIndex: "0",
children: /*#__PURE__*/_jsx(Icon, {
transform: "grow-4",
icon: ["fas", "question-circle"],
fontSize: "2"
})
}), text && (visible ? /*#__PURE__*/_jsx(FloatingPortal, {
root: getFloatingUiRootElement(context.refs.reference),
preserveTabOrder: true,
children: containsLinks ? /*#__PURE__*/_jsx(FloatingFocusManager, {
context: context,
modal: false,
restoreFocus: false,
initialFocus:
// If the popover is rendered in a React Select menu, don't focus the first element. Keep focus on select input else it will close the popover.
// Default to 0 to focus the first element if not rendered in a React Select menu.
isRenderedInReactSelectMenu(context.refs.reference) ? -1 : 0,
children: /*#__PURE__*/_jsx(StyledPopover, _objectSpread(_objectSpread({
className: visiblePopoverClassName,
ref: refs.setFloating,
textAlign: textAlign,
width: width,
enableSelectAll: enableSelectAll,
ariaLabel: ariaLabel
}, floatingProps), {}, {
style: style,
children: text
}))
}) : /*#__PURE__*/_jsx(StyledPopover, _objectSpread(_objectSpread({
className: visiblePopoverClassName,
ref: refs.setFloating,
textAlign: textAlign,
width: width,
enableSelectAll: enableSelectAll,
ariaLabel: ariaLabel
}, floatingProps), {}, {
style: style,
children: text
}))
}) :
/*#__PURE__*/
/*
* HACK: Fixing all the broken tests in teamform-app-ui is too time consuming
* right this moment with a lot of the tests asserting against Popover items.
* Rendering the markup even when closed but in a hidden state ensures that tests pass.
* Ideally, we would update all the tests in teamform-app-ui to open the Popover
* before assertion.
**/
_jsx(StyledPopover, {
ariaLabel: ariaLabel,
className: "Tooltip popover hack-for-legacy-tests",
children: text
})), children]
}));
}
Popover.propTypes = {
/** The element that requires the popover helper text. */
children: PropTypes.element,
/** Specifies the direction of the popover. Defaults to right if not specified */
direction: PropTypes.oneOf([...Object.keys(DIRECTIONS_MAP), ...Object.values(DIRECTIONS_MAP)]),
/** The text contained in the popover element */
text: PropTypes.node,
/** Specifies the alignment of the text inside the popover */
textAlign: PropTypes.oneOf(["left", "right", "center"]),
/** Specifies the width of the popover (you need to specify units, e.g. pixels, %). If you use % it will be a percentage of the width of the Popover container */
width: PropTypes.string,
/** Sets display property of popover tooltip to inline-block */
inlineBlock: PropTypes.bool,
/** Specifies the variant of the popover. */
variant: PropTypes.oneOf(["tooltip"]),
/** Specifies the system design theme. */
theme: PropTypes.object,
/** Specifies whether enable select all behaviour */
enableSelectAll: PropTypes.bool,
/** Provide an aria-label if text is not a string */
ariaLabel: PropTypes.string,
/** Provide a tab index for accessibilty, defaults to 0 */
tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
className: PropTypes.string,
/** Render tooltip with focus control when there is link inside, defaults to true */
withFocusControl: PropTypes.bool,
/** Custom offset configuration for the popover positioning. Can be an object with mainAxis and/or alignmentAxis properties, or a function that returns an offset object. */
offset: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
/** Special prop for Header Avatar to apply CSS-based positioning adjustments when sizing changes dynamically */
headerAvatarSizing: PropTypes.oneOf(["large", "default", "small"]),
/** Disable the popover */
disabled: PropTypes.bool
};
Popover.__docgenInfo = {
"description": "",
"methods": [],
"displayName": "Popover",
"props": {
"direction": {
"defaultValue": {
"value": "\"right\"",
"computed": false
},
"description": "Specifies the direction of the popover. Defaults to right if not specified",
"type": {
"name": "enum",
"value": [{
"value": "...Object.keys(DIRECTIONS_MAP)",
"computed": true
}, {
"value": "...Object.values(DIRECTIONS_MAP)",
"computed": true
}]
},
"required": false
},
"withFocusControl": {
"defaultValue": {
"value": "true",
"computed": false
},
"description": "Render tooltip with focus control when there is link inside, defaults to true",
"type": {
"name": "bool"
},
"required": false
},
"children": {
"description": "The element that requires the popover helper text.",
"type": {
"name": "element"
},
"required": false
},
"text": {
"description": "The text contained in the popover element",
"type": {
"name": "node"
},
"required": false
},
"textAlign": {
"description": "Specifies the alignment of the text inside the popover",
"type": {
"name": "enum",
"value": [{
"value": "\"left\"",
"computed": false
}, {
"value": "\"right\"",
"computed": false
}, {
"value": "\"center\"",
"computed": false
}]
},
"required": false
},
"width": {
"description": "Specifies the width of the popover (you need to specify units, e.g. pixels, %). If you use % it will be a percentage of the width of the Popover container",
"type": {
"name": "string"
},
"required": false
},
"inlineBlock": {
"description": "Sets display property of popover tooltip to inline-block",
"type": {
"name": "bool"
},
"required": false
},
"variant": {
"description": "Specifies the variant of the popover.",
"type": {
"name": "enum",
"value": [{
"value": "\"tooltip\"",
"computed": false
}]
},
"required": false
},
"theme": {
"description": "Specifies the system design theme.",
"type": {
"name": "object"
},
"required": false
},
"enableSelectAll": {
"description": "Specifies whether enable select all behaviour",
"type": {
"name": "bool"
},
"required": false
},
"ariaLabel": {
"description": "Provide an aria-label if text is not a string",
"type": {
"name": "string"
},
"required": false
},
"tabIndex": {
"description": "Provide a tab index for accessibilty, defaults to 0",
"type": {
"name": "union",
"value": [{
"name": "number"
}, {
"name": "string"
}]
},
"required": false
},
"className": {
"description": "",
"type": {
"name": "string"
},
"required": false
},
"offset": {
"description": "Custom offset configuration for the popover positioning. Can be an object with mainAxis and/or alignmentAxis properties, or a function that returns an offset object.",
"type": {
"name": "union",
"value": [{
"name": "object"
}, {
"name": "func"
}]
},
"required": false
},
"headerAvatarSizing": {
"description": "Special prop for Header Avatar to apply CSS-based positioning adjustments when sizing changes dynamically",
"type": {
"name": "enum",
"value": [{
"value": "\"large\"",
"computed": false
}, {
"value": "\"default\"",
"computed": false
}, {
"value": "\"small\"",
"computed": false
}]
},
"required": false
},
"disabled": {
"description": "Disable the popover",
"type": {
"name": "bool"
},
"required": false
}
}
};