UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

259 lines (258 loc) 11.3 kB
import { useProvidedRefOrCreate } from "../hooks/useProvidedRefOrCreate.js"; import { useProvidedStateOrCreate } from "../hooks/useProvidedStateOrCreate.js"; import { useMenuKeyboardNavigation } from "../hooks/useMenuKeyboardNavigation.js"; import { useId as useId$1 } from "../hooks/useId.js"; import { Tooltip } from "../TooltipV2/Tooltip.js"; import { ButtonComponent } from "../Button/Button.js"; import { useResponsiveValue } from "../hooks/useResponsiveValue.js"; import { isSlot } from "../utils/is-slot.js"; import { DialogContext } from "../Dialog/DialogContext.js"; import { ActionListContainerContext } from "../ActionList/ActionListContainerContext.js"; import { Divider } from "../ActionList/Divider.js"; import { AnchoredOverlay } from "../AnchoredOverlay/AnchoredOverlay.js"; import ActionMenu_module_css_default from "./ActionMenu.module.css.js"; import { clsx } from "clsx"; import { jsx } from "react/jsx-runtime"; import React, { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { ChevronRightIcon, TriangleDownIcon } from "@primer/octicons-react"; //#region src/ActionMenu/ActionMenu.tsx const MenuContext = /*#__PURE__*/ React.createContext({ renderAnchor: null, open: false }); const mergeAnchorHandlers = (anchorProps, buttonProps) => { const mergedAnchorProps = { ...anchorProps }; if (typeof buttonProps.onClick === "function") { const anchorOnClick = anchorProps.onClick; const mergedOnClick = (event) => { var _buttonProps$onClick; (_buttonProps$onClick = buttonProps.onClick) === null || _buttonProps$onClick === void 0 || _buttonProps$onClick.call(buttonProps, event); anchorOnClick === null || anchorOnClick === void 0 || anchorOnClick(event); }; mergedAnchorProps.onClick = mergedOnClick; } if (typeof buttonProps.onKeyDown === "function") { const anchorOnKeyDown = anchorProps.onKeyDown; const mergedOnAnchorKeyDown = (event) => { var _buttonProps$onKeyDow; (_buttonProps$onKeyDow = buttonProps.onKeyDown) === null || _buttonProps$onKeyDow === void 0 || _buttonProps$onKeyDow.call(buttonProps, event); anchorOnKeyDown === null || anchorOnKeyDown === void 0 || anchorOnKeyDown(event); }; mergedAnchorProps.onKeyDown = mergedOnAnchorKeyDown; } if (buttonProps.className) mergedAnchorProps.className = clsx(anchorProps.className, buttonProps.className); return mergedAnchorProps; }; const Menu = ({ anchorRef: externalAnchorRef, open, onOpenChange, children }) => { const parentMenuContext = useContext(MenuContext); const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, onOpenChange, false); const onOpen = React.useCallback(() => setCombinedOpenState(true), [setCombinedOpenState]); const isNarrow = useResponsiveValue({ narrow: true }, false); const onClose = React.useCallback((gesture) => { var _parentMenuContext$on; if (isNarrow && open && gesture === "tab") return; setCombinedOpenState(false); switch (gesture) { case "tab": case "item-select": (_parentMenuContext$on = parentMenuContext.onClose) === null || _parentMenuContext$on === void 0 || _parentMenuContext$on.call(parentMenuContext, gesture); } }, [ setCombinedOpenState, parentMenuContext, open, isNarrow ]); const menuButtonChild = React.Children.toArray(children).find((child) => /*#__PURE__*/ React.isValidElement(child) && (child.type === MenuButton || child.type === Anchor || isSlot(child, Anchor) || isSlot(child, MenuButton))); const menuButtonChildId = /*#__PURE__*/ React.isValidElement(menuButtonChild) ? menuButtonChild.props.id : void 0; const anchorRef = useProvidedRefOrCreate(externalAnchorRef); const anchorId = useId$1(menuButtonChildId); let renderAnchor = null; const contents = React.Children.map(children, (child) => { if (child.type === Tooltip || isSlot(child, Tooltip)) { const anchorChildren = child.props.children; if (anchorChildren.type === MenuButton || isSlot(anchorChildren, MenuButton)) renderAnchor = (anchorProps) => { const triggerButton = /*#__PURE__*/ React.cloneElement(anchorChildren, mergeAnchorHandlers({ ...anchorProps }, anchorChildren.props)); return /*#__PURE__*/ React.cloneElement(child, { children: triggerButton, ref: anchorRef }); }; return null; } else if (child.type === Anchor || isSlot(child, Anchor)) { const anchorChildren = child.props.children; if (anchorChildren !== void 0 ? anchorChildren.type === Tooltip || isSlot(anchorChildren, Tooltip) : false) { if (anchorChildren.props.children !== null) renderAnchor = (anchorProps) => { const tooltipTrigger = anchorChildren.props.children; const tooltipTriggerEl = /*#__PURE__*/ React.cloneElement(tooltipTrigger, mergeAnchorHandlers({ ...anchorProps }, tooltipTrigger.props)); const tooltip = /*#__PURE__*/ React.cloneElement(anchorChildren, { children: tooltipTriggerEl }); return /*#__PURE__*/ React.cloneElement(child, { children: tooltip, ref: anchorRef }); }; } else renderAnchor = (anchorProps) => /*#__PURE__*/ React.cloneElement(child, { ...anchorProps, className: clsx(anchorProps.className, child.props.className) }); return null; } else if (child.type === MenuButton || isSlot(child, MenuButton)) { renderAnchor = (anchorProps) => /*#__PURE__*/ React.cloneElement(child, mergeAnchorHandlers(anchorProps, child.props)); return null; } else return child; }); const isSubmenu = parentMenuContext.isSubmenu !== void 0; const menuContextValue = useMemo(() => ({ anchorRef, renderAnchor, anchorId, open: combinedOpenState, onOpen, onClose, isSubmenu }), [ anchorRef, renderAnchor, anchorId, combinedOpenState, onOpen, onClose, isSubmenu ]); return /*#__PURE__*/ jsx(MenuContext.Provider, { value: menuContextValue, children: contents }); }; Menu.displayName = "Menu"; const Anchor = /*#__PURE__*/ React.forwardRef(({ children: child, ...anchorProps }, anchorRef) => { const { onOpen, isSubmenu } = React.useContext(MenuContext); const openSubmenuOnRightArrow = useCallback((event) => { if (isSubmenu && event.key === "ArrowRight" && !event.defaultPrevented) onOpen === null || onOpen === void 0 || onOpen("anchor-key-press"); }, [isSubmenu, onOpen]); const onButtonClick = (event) => { var _child$props$onClick, _child$props, _anchorProps$onClick; (_child$props$onClick = (_child$props = child.props).onClick) === null || _child$props$onClick === void 0 || _child$props$onClick.call(_child$props, event); (_anchorProps$onClick = anchorProps.onClick) === null || _anchorProps$onClick === void 0 || _anchorProps$onClick.call(anchorProps, event); }; const onButtonKeyDown = (event) => { var _child$props$onKeyDow, _child$props2, _anchorProps$onKeyDow; (_child$props$onKeyDow = (_child$props2 = child.props).onKeyDown) === null || _child$props$onKeyDow === void 0 || _child$props$onKeyDow.call(_child$props2, event); openSubmenuOnRightArrow(event); (_anchorProps$onKeyDow = anchorProps.onKeyDown) === null || _anchorProps$onKeyDow === void 0 || _anchorProps$onKeyDow.call(anchorProps, event); }; const parentActionListContext = useContext(ActionListContainerContext); const thisActionListContext = useMemo(() => isSubmenu ? { ...parentActionListContext, defaultTrailingVisual: /*#__PURE__*/ jsx(ChevronRightIcon, {}), afterSelect: () => onOpen === null || onOpen === void 0 ? void 0 : onOpen("anchor-click") } : parentActionListContext, [ isSubmenu, onOpen, parentActionListContext ]); return /*#__PURE__*/ jsx(ActionListContainerContext.Provider, { value: thisActionListContext, children: /*#__PURE__*/ React.cloneElement(child, { ...anchorProps, ref: anchorRef, className: clsx(anchorProps.className, child.props.className), onClick: onButtonClick, onKeyDown: onButtonKeyDown }) }); }); /** this component is syntactical sugar 🍭 */ const MenuButton = /*#__PURE__*/ React.forwardRef(({ ...props }, anchorRef) => { return /*#__PURE__*/ jsx(Anchor, { ref: anchorRef, children: /*#__PURE__*/ jsx(ButtonComponent, { "data-component": "ActionMenu.Button", type: "button", trailingAction: TriangleDownIcon, ...props }) }); }); const defaultVariant = { regular: "anchored", narrow: "anchored" }; const Overlay = ({ children, align = "start", side, onPositionChange, displayInViewport, "aria-labelledby": ariaLabelledby, variant = defaultVariant, ...overlayProps }) => { const { anchorRef, renderAnchor, anchorId, open, onOpen, onClose, isSubmenu = false } = React.useContext(MenuContext); const containerRef = React.useRef(null); const isNarrowFullscreen = !!useResponsiveValue({ narrow: true }, false) && variant.narrow === "fullscreen"; const handleClose = React.useCallback((gesture) => { if (isNarrowFullscreen && gesture === "tab") return; onClose === null || onClose === void 0 || onClose(gesture); }, [isNarrowFullscreen, onClose]); useMenuKeyboardNavigation(open, handleClose, containerRef, anchorRef, isSubmenu); const responsiveVariant = useResponsiveValue(variant, { regular: "anchored", narrow: "anchored" }); const [anchorAriaLabelledby, setAnchorAriaLabelledby] = useState(null); useEffect(() => { if (anchorRef !== null && anchorRef !== void 0 && anchorRef.current) { const ariaLabelledby = anchorRef.current.getAttribute("aria-labelledby"); if (ariaLabelledby) setAnchorAriaLabelledby(ariaLabelledby); } }, [anchorRef]); const afterSelect = useCallback(() => onClose === null || onClose === void 0 ? void 0 : onClose("item-select"), [onClose]); const overlayContextValue = useMemo(() => ({ container: "ActionMenu", listRole: "menu", listLabelledBy: ariaLabelledby || anchorAriaLabelledby || anchorId, selectionAttribute: "aria-checked", afterSelect, enableFocusZone: isNarrowFullscreen }), [ ariaLabelledby, anchorAriaLabelledby, anchorId, afterSelect, isNarrowFullscreen ]); const isInsideDialog = useContext(DialogContext) !== void 0; return /*#__PURE__*/ jsx(AnchoredOverlay, { anchorRef, renderAnchor, anchorId, open, onOpen, onClose: handleClose, align, side: side !== null && side !== void 0 ? side : isSubmenu ? "outside-right" : "outside-bottom", overlayProps: { ...overlayProps, "data-component": "ActionMenu.Overlay" }, focusZoneSettings: isNarrowFullscreen ? { disabled: true } : { focusOutBehavior: "wrap" }, onPositionChange, variant, displayInViewport: displayInViewport !== void 0 ? displayInViewport : isInsideDialog, children: /*#__PURE__*/ jsx("div", { ref: containerRef, className: ActionMenu_module_css_default.ActionMenuContainer, "data-variant": responsiveVariant, ...overlayProps.overflow ? { [`data-overflow-${overlayProps.overflow}`]: "" } : {}, ...overlayProps.maxHeight ? { [`data-max-height-${overlayProps.maxHeight}`]: "" } : {}, children: /*#__PURE__*/ jsx(ActionListContainerContext.Provider, { value: overlayContextValue, children }) }) }); }; Overlay.displayName = "Overlay"; Menu.displayName = "ActionMenu"; MenuButton.__SLOT__ = Symbol("ActionMenu.Button"); Anchor.__SLOT__ = Symbol("ActionMenu.Anchor"); Overlay.__SLOT__ = Symbol("ActionMenu.Overlay"); const ActionMenu = Object.assign(Menu, { Button: MenuButton, Anchor, Overlay, Divider }); //#endregion export { ActionMenu };