fix
This commit is contained in:
@@ -6,6 +6,13 @@ import {
|
|||||||
|
|
||||||
import { Icon } from "../Icon/Icon";
|
import { Icon } from "../Icon/Icon";
|
||||||
import { SpinnerSvg } from "../../assets/svg/SpinnerSvg";
|
import { SpinnerSvg } from "../../assets/svg/SpinnerSvg";
|
||||||
|
import {
|
||||||
|
brandRecipe,
|
||||||
|
cpm,
|
||||||
|
inlineSizeRecipe,
|
||||||
|
itemSizeRecipe,
|
||||||
|
itemVariantRecipe,
|
||||||
|
} from "@dg/css";
|
||||||
|
|
||||||
export type ButtonSize = "xs" | "sm" | "md" | "lg";
|
export type ButtonSize = "xs" | "sm" | "md" | "lg";
|
||||||
|
|
||||||
@@ -46,7 +53,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
|
|
||||||
const mergedClass = cpm(
|
const mergedClass = cpm(
|
||||||
itemSizeRecipe({ size, shape, iconOnly, longPadding: false }),
|
itemSizeRecipe({ size, shape, iconOnly, longPadding: false }),
|
||||||
variantRecipe({ variant, disabled }),
|
itemVariantRecipe({ variant, disabled }),
|
||||||
brandRecipe({ brand }),
|
brandRecipe({ brand }),
|
||||||
className,
|
className,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,50 +1,149 @@
|
|||||||
import {
|
import {
|
||||||
autoUpdate,
|
autoUpdate,
|
||||||
flip,
|
flip,
|
||||||
|
FloatingNode,
|
||||||
|
offset,
|
||||||
|
safePolygon,
|
||||||
|
shift,
|
||||||
useClick,
|
useClick,
|
||||||
useDismiss,
|
useDismiss,
|
||||||
useFloating,
|
useFloating,
|
||||||
|
useFloatingNodeId,
|
||||||
|
useFloatingParentNodeId,
|
||||||
|
useFloatingTree,
|
||||||
|
useHover,
|
||||||
useInteractions,
|
useInteractions,
|
||||||
|
useListItem,
|
||||||
useListNavigation,
|
useListNavigation,
|
||||||
|
useMergeRefs,
|
||||||
useRole,
|
useRole,
|
||||||
useTypeahead,
|
|
||||||
type Placement,
|
|
||||||
} from "@floating-ui/react";
|
} from "@floating-ui/react";
|
||||||
import { useRef, useState } from "react";
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type ComponentPropsWithoutRef,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
import type { CommonProps } from "../../utils/CommonProps";
|
import type { CommonProps } from "../../utils/CommonProps";
|
||||||
|
import { MenuContext } from "./MenuContext";
|
||||||
|
import { Button } from "../Button/Button";
|
||||||
|
import { MenuItem } from "./MenuItem";
|
||||||
|
|
||||||
export type MenuProps = CommonProps;
|
export type MenuProps = ComponentPropsWithoutRef<"button"> & {
|
||||||
|
size?: "sm" | "md";
|
||||||
|
iconSvg?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
export const Menu = (props: MenuProps) => {
|
export const Menu = forwardRef<HTMLElement, MenuProps>(
|
||||||
const { children } = props;
|
(props: MenuProps, ref) => {
|
||||||
|
const { children, size = "sm", iconSvg, ...rest } = props;
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||||
const [hasFocusInside, setHasFocusInside] = useState(false);
|
const [hasFocusInside, setHasFocusInside] = useState(false);
|
||||||
|
|
||||||
|
const elementsRef = useRef([]);
|
||||||
|
const parent = useContext(MenuContext);
|
||||||
|
const tree = useFloatingTree();
|
||||||
|
const nodeId = useFloatingNodeId();
|
||||||
|
const parentId = useFloatingParentNodeId();
|
||||||
|
const item = useListItem();
|
||||||
|
|
||||||
|
const isNested = parentId != null;
|
||||||
|
|
||||||
const { refs, floatingStyles, context } = useFloating({
|
const { refs, floatingStyles, context } = useFloating({
|
||||||
placement: "bottom-start",
|
placement: isNested ? "right-start" : "bottom-start",
|
||||||
open: isOpen,
|
open: isOpen,
|
||||||
onOpenChange: setIsOpen,
|
onOpenChange: setIsOpen,
|
||||||
whileElementsMounted: autoUpdate,
|
whileElementsMounted: autoUpdate,
|
||||||
middleware: [flip()],
|
middleware: [
|
||||||
|
offset({
|
||||||
|
mainAxis: isNested ? 0 : 4,
|
||||||
|
alignmentAxis: isNested ? -4 : 0,
|
||||||
|
}),
|
||||||
|
flip(),
|
||||||
|
shift(),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const elementsRef = useRef([]);
|
const hover = useHover(context, {
|
||||||
|
enabled: isNested,
|
||||||
|
delay: { open: 75 },
|
||||||
|
handleClose: safePolygon({ blockPointerEvents: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const click = useClick(context, {
|
||||||
|
event: "mousedown",
|
||||||
|
toggle: !isNested,
|
||||||
|
ignoreMouse: isNested,
|
||||||
|
});
|
||||||
|
|
||||||
|
const role = useRole(context, { role: "menu" });
|
||||||
|
|
||||||
|
const dismiss = useDismiss(context, { bubbles: true });
|
||||||
|
|
||||||
const listNav = useListNavigation(context, {
|
const listNav = useListNavigation(context, {
|
||||||
listRef: elementsRef,
|
listRef: elementsRef,
|
||||||
activeIndex,
|
activeIndex,
|
||||||
|
nested: isNested,
|
||||||
onNavigate: setActiveIndex,
|
onNavigate: setActiveIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
const click = useClick(context);
|
const { getReferenceProps, getFloatingProps, getItemProps } =
|
||||||
const dismiss = useDismiss(context);
|
useInteractions([hover, click, dismiss, role, listNav]);
|
||||||
const role = useRole(context, { role: "listbox" });
|
|
||||||
|
|
||||||
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
|
// Event emitter allows you to communicate across tree components.
|
||||||
[listNav, click, dismiss, role],
|
// This effect closes all menus when an item gets clicked anywhere
|
||||||
);
|
// in the tree.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tree) return;
|
||||||
|
|
||||||
return;
|
function handleTreeClick() {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubMenuOpen(event: { nodeId: string; parentId: string }) {
|
||||||
|
if (event.nodeId !== nodeId && event.parentId === parentId) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.events.on("click", handleTreeClick);
|
||||||
|
tree.events.on("menuopen", onSubMenuOpen);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tree.events.off("click", handleTreeClick);
|
||||||
|
tree.events.off("menuopen", onSubMenuOpen);
|
||||||
};
|
};
|
||||||
|
}, [tree, nodeId, parentId]);
|
||||||
|
|
||||||
|
const Trigger = isNested ? MenuItem : Button;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FloatingNode id={nodeId}>
|
||||||
|
<Trigger
|
||||||
|
ref={useMergeRefs([refs.setReference, item.ref, ref])}
|
||||||
|
tabIndex={
|
||||||
|
!isNested ? undefined : parent.activeIndex === item.index ? 0 : -1
|
||||||
|
}
|
||||||
|
role={isNested ? "menuitem" : undefined}
|
||||||
|
iconSvg={iconSvg}
|
||||||
|
size={size}
|
||||||
|
{...getReferenceProps(
|
||||||
|
parent.getItemProps({
|
||||||
|
...rest,
|
||||||
|
onFocus(event: React.FocusEvent<HTMLButtonElement>) {
|
||||||
|
rest.onFocus?.(event);
|
||||||
|
setHasFocusInside(false);
|
||||||
|
parent.setHasFocusInside(true);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
></Trigger>
|
||||||
|
</FloatingNode>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { MenuContext } from "./MenuContext";
|
||||||
|
|
||||||
|
export const MenuItem = () => {
|
||||||
|
const menuContext = useContext(MenuContext);
|
||||||
|
const isRoot = menuContext ?? false;
|
||||||
|
return <div></div>;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user