orcs-design-system
Version:
TeamForm's Design System, aka: ORCS
551 lines (550 loc) • 18.8 kB
JavaScript
import React, { useState, useImperativeHandle, createContext, useContext, useMemo, useId, useLayoutEffect } from "react";
import styled, { css, ThemeProvider } from "styled-components";
import PropTypes from "prop-types";
import { space, layout } from "styled-system";
import { themeGet } from "@styled-system/theme-get";
import { commonKeys } from "../../hooks/keypress";
import useActionMenu from "./useActionMenu";
import { crossFadeIn, beforeDotCollapsing, beforeDotExpanding, afterDotCollapsing, afterDotExpanding, beforeCrossExpanding, beforeCrossCollapsing, afterCrossExpanding, afterCrossCollapsing } from "./ActionsMenu.animations";
import { FloatingFocusManager, FloatingPortal, useMergeRefs } from "@floating-ui/react";
import { getFloatingUiRootElement, getFloatingUiZIndex } from "../../utils/floatingUiHelpers";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const ActionMenuContext = /*#__PURE__*/createContext({});
const StyledActionsMenuContainer = styled.div.withConfig({
displayName: "ActionsMenu__StyledActionsMenuContainer",
componentId: "sc-yvbni2-0"
})(["pointer-events:none;opacity:0;visibility:hidden;&.hack-for-legacy-tests{position:absolute;pointer-events:none;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border-width:0;}&.visible{visibility:visible;opacity:1;pointer-events:auto;}"]);
const Wrapper = styled.div.withConfig({
displayName: "ActionsMenu__Wrapper",
componentId: "sc-yvbni2-1"
})(["", " ", " position:relative;width:auto;"], space, layout);
const Control = styled.button.withConfig({
displayName: "ActionsMenu__Control",
componentId: "sc-yvbni2-2"
})(["position:relative;background-color:", ";border:solid 1px ", ";display:flex;align-items:center;justify-content:center;-moz-appearance:none;-webkit-appearance:none;appearance:none;box-shadow:none;text-decoration:none;text-align:center;border-radius:", ";transition:", ";cursor:pointer;width:30px;height:30px;&:hover,&:focus{outline:0;border-color:", ";}&[data-state=\"open\"] .actionsMenu__dots{&:before{animation:500ms ", " cubic-bezier(0.68,-0.6,0.32,1.6) forwards;}&:after{animation:500ms ", " cubic-bezier(0.68,-0.6,0.32,1.6) forwards;}}&[data-state=\"open\"] .actionsMenu__cross{&:before{animation:500ms ", " cubic-bezier(0.68,-0.6,0.32,1.6) forwards;}&:after{animation:500ms ", " cubic-bezier(0.68,-0.6,0.32,1.6) forwards;}}&[data-state=\"closed\"] .actionsMenu__dots{&:before{animation:500ms ", " cubic-bezier(0.68,-0.6,0.32,1.6) forwards;}&:after{animation:500ms ", " cubic-bezier(0.68,-0.6,0.32,1.6) forwards;}}&[data-state=\"closed\"] .actionsMenu__cross{&:before{animation:500ms ", " cubic-bezier(0.68,-0.6,0.32,1.6) forwards;}&:after{animation:500ms ", " cubic-bezier(0.68,-0.6,0.32,1.6) forwards;}}"], props => themeGet("colors.white")(props), props => themeGet("colors.greyLight")(props), props => themeGet("radii.2")(props), props => themeGet("transition.transitionDefault")(props), props => themeGet("colors.primary")(props), beforeDotCollapsing, afterDotCollapsing, beforeCrossExpanding, afterCrossExpanding, beforeDotExpanding, afterDotExpanding, beforeCrossCollapsing, afterCrossCollapsing);
const Dots = styled.div.withConfig({
displayName: "ActionsMenu__Dots",
componentId: "sc-yvbni2-3"
})(["border-radius:2px;height:4px;width:4px;background-color:", ";&:before,&:after{content:\"\";display:block;position:absolute;border-radius:2px;height:4px;width:4px;background-color:", ";}&:before{transform:translate(0,-6px);}&:after{transform:translate(0,6px);}"], props => themeGet("colors.greyDarker")(props), props => themeGet("colors.greyDarker")(props));
const Cross = styled.div.withConfig({
displayName: "ActionsMenu__Cross",
componentId: "sc-yvbni2-4"
})(["animation:1500ms ", " ease-in-out forwards;opacity:0;position:absolute;left:calc(50% - 2px);top:calc(50% - 2px);&:before,&:after{content:\"\";display:block;position:absolute;border-radius:2px;height:4px;width:4px;background-color:", ";}&:before{transform:rotate(-45deg);}&:after{transform:rotate(45deg);}"], crossFadeIn, props => themeGet("colors.greyDarker")(props));
const Menu = styled.div.withConfig({
displayName: "ActionsMenu__Menu",
componentId: "sc-yvbni2-5"
})(["display:block;width:", ";z-index:5;background-color:", ";border:1px solid ", ";box-shadow:", ";border-radius:", ";"], props => props.menuWidth ? props.menuWidth : "auto", themeGet("colors.white"), themeGet("colors.greyLight"), themeGet("shadows.boxDefault"), props => themeGet("radii.2")(props));
export const ActionsMenuHeading = styled(props => {
const {
actionMenu
} = useContext(ActionMenuContext);
const id = useId();
// // Only sets `aria-labelledby` on the Popover root element
// // if this component is mounted inside it.
useLayoutEffect(() => {
actionMenu?.setLabelId?.(id);
return () => actionMenu?.setLabelId?.(undefined);
}, [id, actionMenu]);
return /*#__PURE__*/_jsx("div", {
...props,
onKeyUp: e => {
if (e.key === commonKeys.ENTER && props?.canClick) {
props?.onClick();
}
}
});
}).attrs({
tabIndex: "0",
role: "button"
}).withConfig({
displayName: "ActionsMenu__ActionsMenuHeading",
componentId: "sc-yvbni2-6"
})(["color:", ";padding:8px;width:100%;font-size:", ";font-weight:", ";border-bottom:solid 1px ", ";white-space:nowrap;cursor:", ";"], props => themeGet("colors.greyDark")(props), props => themeGet("fontSizes.0")(props), props => themeGet("fontWeights.1")(props), props => themeGet("colors.greyLighter")(props), props => props.canClick ? "pointer" : "default");
export const ActionsMenuItem = styled(props => {
const {
id,
onItemClick,
actionMenu
} = useContext(ActionMenuContext);
const {
as,
...others
} = props;
const Component = as ? as : others.href ? "a" : "button";
const disabled = props.disabled;
let newProps = {
...others,
...(actionMenu?.getItemProps?.() || {})
};
const {
onClick: originalOnClick
} = newProps;
const onClick = useMemo(() => e => {
onItemClick?.(e);
originalOnClick?.(e);
}, [originalOnClick, onItemClick]);
if (Component === "button") {
newProps = {
...others,
type: "button",
["data-action-menu-id"]: id
};
}
if (props.Component) return /*#__PURE__*/_jsx(props.Component, {
...newProps,
onClick: onClick,
disabled: disabled
});
return /*#__PURE__*/_jsx(Component, {
...newProps,
onClick: onClick,
disabled: disabled
});
}).attrs({
role: "menuitem"
}).withConfig({
displayName: "ActionsMenu__ActionsMenuItem",
componentId: "sc-yvbni2-7"
})(["", ""], _ref => {
let {
Component
} = _ref;
return Component ? "" : css(["white-space:nowrap;display:block;width:100%;text-align:left;cursor:pointer;margin:0;padding:8px;appearance:none;background-color:", ";border:none;border-bottom:solid 1px ", ";border-radius:0;color:", ";font-size:", ";line-height:", ";font-family:", ";font-weight:", ";text-decoration:none;transition:", ";&:hover{background-color:", ";}&:first-child{border-radius:", " ", " 0 0;}&:last-child{border:0;border-radius:0 0 ", " ", ";}&:only-child{border-radius:", ";}&#other{padding:6px 8px;}"], props => props.selected ? themeGet("colors.success20")(props) : "transparent", props => themeGet("colors.white")(props), props => themeGet("colors.greyDarkest")(props), props => themeGet("fontSizes.0")(props), props => themeGet("fontSizes.0")(props), props => themeGet("fonts.main")(props), props => themeGet("fontWeights.2")(props), props => themeGet("transition.transitionDefault")(props), props => themeGet("colors.primaryLightest")(props), props => themeGet("radii.2")(props), props => themeGet("radii.2")(props), props => themeGet("radii.2")(props), props => themeGet("radii.2")(props), props => themeGet("radii.2")(props));
});
export const ActionsMenuBody = _ref2 => {
let {
theme,
onToggle,
toggleState,
// direction - Deprecated
direction = "right-start",
menuWidth,
customTriggerComponent,
renderTrigger,
children,
ariaLabel = "Options Menu",
onTriggerFocus,
closeMenu,
closeOnClick = false,
"data-testid": dataTestId = "ActionsMenu",
...props
} = _ref2;
const id = useId();
const actionMenu = useActionMenu({
placement: direction,
open: toggleState,
onOpenChange: (_, e) => {
if (e) {
onToggle?.(e);
}
}
});
const childrenRef = children.ref;
const triggerRef = useMergeRefs([actionMenu.refs.setReference, childrenRef]);
const ref = useMergeRefs([actionMenu.refs.setFloating]);
const triggerProps = useMemo(() => ({
"aria-label": ariaLabel,
onFocus: onTriggerFocus,
id,
...actionMenu.getReferenceProps({
...props,
onClick: onToggle,
ref: triggerRef,
"data-state": actionMenu.open ? "open" : "closed",
"data-testid": dataTestId
})
}), [ariaLabel, onTriggerFocus, id, actionMenu, onToggle, props, triggerRef, dataTestId]);
let triggerComponent = /*#__PURE__*/_jsxs(Control, {
...triggerProps,
children: [/*#__PURE__*/_jsx(Dots, {
className: "actionsMenu__dots"
}), /*#__PURE__*/_jsx(Cross, {
className: "actionsMenu__cross"
})]
});
if (renderTrigger) {
triggerComponent = renderTrigger(triggerProps);
}
if (customTriggerComponent) {
triggerComponent = /*#__PURE__*/_jsx("div", {
role: "button",
...triggerProps,
children: customTriggerComponent
});
}
const value = useMemo(() => ({
id,
onItemClick: e => {
if (closeOnClick) {
closeMenu(e);
}
},
actionMenu
}), [closeOnClick, closeMenu, id, actionMenu]);
const style = useMemo(() => ({
...actionMenu.floatingStyles,
zIndex: getFloatingUiZIndex(actionMenu.refs.reference)
}), [actionMenu.floatingStyles, actionMenu.refs.reference]);
const menuDataTestId = useMemo(() => `${dataTestId}__menu`, [dataTestId]);
const {
getFloatingProps
} = actionMenu;
const floatingProps = useMemo(() => getFloatingProps(props), [getFloatingProps, props]);
const component = /*#__PURE__*/_jsx(ActionMenuContext.Provider, {
value: value,
children: /*#__PURE__*/_jsxs(Wrapper, {
...props,
children: [triggerComponent, toggleState ? /*#__PURE__*/_jsx(FloatingPortal, {
root: getFloatingUiRootElement(actionMenu.refs.reference),
children: /*#__PURE__*/_jsx(FloatingFocusManager, {
context: actionMenu.context,
modal: true,
children: /*#__PURE__*/_jsx(StyledActionsMenuContainer, {
"aria-labelledby": actionMenu.labelId,
"data-testid": menuDataTestId,
...floatingProps,
style: style,
className: "actionMenu-content visible",
"aria-hidden": "false",
ref: ref,
children: /*#__PURE__*/_jsx(Menu, {
menuWidth: menuWidth,
isOpen: toggleState,
children: children
})
})
})
}) :
/*#__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 ActionsMenu 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 ActionsMenu *
before assertion. **/
_jsx(StyledActionsMenuContainer, {
"aria-labelledby": actionMenu.labelId,
"data-testid": menuDataTestId,
className: "actionMenu-content hack-for-legacy-tests",
children: /*#__PURE__*/_jsx(Menu, {
menuWidth: menuWidth,
isOpen: toggleState,
children: children
})
})]
})
});
return theme ? /*#__PURE__*/_jsx(ThemeProvider, {
theme: theme,
children: component
}) : component;
};
ActionsMenuBody.propTypes = {
onTriggerFocus: PropTypes.func,
onToggle: PropTypes.func.isRequired,
closeMenu: PropTypes.func.isRequired,
toggleState: PropTypes.bool.isRequired,
closeOnClick: PropTypes.bool,
direction: PropTypes.string,
placement: PropTypes.string,
menuTopPosition: PropTypes.string,
menuLeftPosition: PropTypes.string,
menuRightPosition: PropTypes.string,
menuWidth: PropTypes.string,
customTriggerComponent: PropTypes.node,
renderTrigger: PropTypes.func,
"data-testid": PropTypes.string,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
theme: PropTypes.object,
ariaLabel: PropTypes.string
};
const ActionsMenu = /*#__PURE__*/React.forwardRef((props, ref) => {
const [toggleState, setToggle] = useState(false);
const onToggle = e => {
e.stopPropagation();
setToggle(!toggleState);
};
useImperativeHandle(ref, () => ({
closeMenu: () => {
setToggle(false);
}
}));
return /*#__PURE__*/_jsx(ActionsMenuBody, {
...props,
closeMenu: () => setToggle(false),
toggleState: toggleState,
onToggle: onToggle,
children: props.children
});
});
ActionsMenu.propTypes = {
isOpen: PropTypes.bool,
direction: PropTypes.oneOf(["left", "right", "top", "bottom", "top-start", "top-end", "bottom-start", "bottom-end", "left-start", "left-end", "right-start", "right-end"]),
customTriggerComponent: PropTypes.node,
renderTrigger: PropTypes.func,
closeOnClick: PropTypes.bool,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
/** Specifies the colour theme */
theme: PropTypes.object,
/** Specifies the aria-label for the button */
ariaLabel: PropTypes.string
};
ActionsMenu.__docgenInfo = {
"description": "",
"methods": [],
"displayName": "ActionsMenu",
"props": {
"isOpen": {
"description": "",
"type": {
"name": "bool"
},
"required": false
},
"direction": {
"description": "",
"type": {
"name": "enum",
"value": [{
"value": "\"left\"",
"computed": false
}, {
"value": "\"right\"",
"computed": false
}, {
"value": "\"top\"",
"computed": false
}, {
"value": "\"bottom\"",
"computed": false
}, {
"value": "\"top-start\"",
"computed": false
}, {
"value": "\"top-end\"",
"computed": false
}, {
"value": "\"bottom-start\"",
"computed": false
}, {
"value": "\"bottom-end\"",
"computed": false
}, {
"value": "\"left-start\"",
"computed": false
}, {
"value": "\"left-end\"",
"computed": false
}, {
"value": "\"right-start\"",
"computed": false
}, {
"value": "\"right-end\"",
"computed": false
}]
},
"required": false
},
"customTriggerComponent": {
"description": "",
"type": {
"name": "node"
},
"required": false
},
"renderTrigger": {
"description": "",
"type": {
"name": "func"
},
"required": false
},
"closeOnClick": {
"description": "",
"type": {
"name": "bool"
},
"required": false
},
"children": {
"description": "",
"type": {
"name": "union",
"value": [{
"name": "node"
}, {
"name": "arrayOf",
"value": {
"name": "node"
}
}]
},
"required": false
},
"theme": {
"description": "Specifies the colour theme",
"type": {
"name": "object"
},
"required": false
},
"ariaLabel": {
"description": "Specifies the aria-label for the button",
"type": {
"name": "string"
},
"required": false
}
}
};
export default ActionsMenu;
ActionsMenuBody.__docgenInfo = {
"description": "",
"methods": [],
"displayName": "ActionsMenuBody",
"props": {
"direction": {
"defaultValue": {
"value": "\"right-start\"",
"computed": false
},
"description": "",
"type": {
"name": "string"
},
"required": false
},
"ariaLabel": {
"defaultValue": {
"value": "\"Options Menu\"",
"computed": false
},
"description": "",
"type": {
"name": "string"
},
"required": false
},
"closeOnClick": {
"defaultValue": {
"value": "false",
"computed": false
},
"description": "",
"type": {
"name": "bool"
},
"required": false
},
"data-testid": {
"defaultValue": {
"value": "\"ActionsMenu\"",
"computed": false
},
"description": "",
"type": {
"name": "string"
},
"required": false
},
"onTriggerFocus": {
"description": "",
"type": {
"name": "func"
},
"required": false
},
"onToggle": {
"description": "",
"type": {
"name": "func"
},
"required": true
},
"closeMenu": {
"description": "",
"type": {
"name": "func"
},
"required": true
},
"toggleState": {
"description": "",
"type": {
"name": "bool"
},
"required": true
},
"placement": {
"description": "",
"type": {
"name": "string"
},
"required": false
},
"menuTopPosition": {
"description": "",
"type": {
"name": "string"
},
"required": false
},
"menuLeftPosition": {
"description": "",
"type": {
"name": "string"
},
"required": false
},
"menuRightPosition": {
"description": "",
"type": {
"name": "string"
},
"required": false
},
"menuWidth": {
"description": "",
"type": {
"name": "string"
},
"required": false
},
"customTriggerComponent": {
"description": "",
"type": {
"name": "node"
},
"required": false
},
"renderTrigger": {
"description": "",
"type": {
"name": "func"
},
"required": false
},
"children": {
"description": "",
"type": {
"name": "union",
"value": [{
"name": "node"
}, {
"name": "arrayOf",
"value": {
"name": "node"
}
}]
},
"required": false
},
"theme": {
"description": "",
"type": {
"name": "object"
},
"required": false
}
}
};