@primer/react
Version:
An implementation of GitHub's Primer Design System using React
259 lines (258 loc) • 11.3 kB
JavaScript
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 };