Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ActionMenu] Removed defaultOpen-prop, moved rootElement, added Modal-support #3240

Merged
merged 9 commits into from
Oct 17, 2024
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Meta, StoryObj } from "@storybook/react";
import React, { useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { PencilIcon, StarIcon } from "@navikt/aksel-icons";
import { Button } from "../../button";
import { HStack, VStack } from "../../layout/stack";
Expand Down Expand Up @@ -435,11 +435,6 @@ export const TriggerWithTooltip: Story = {
decorators: [DemoDecorator],
};

/**
* TODO: Bugs
* - When keydown "space" on open modal button, the modal is closed instantly.
* Unsure if this is because of the keydown-event repeats or if its caused by eventbubbling
*/
export const ModalTrigger: Story = {
render: () => {
const ref = useRef<HTMLDialogElement>(null);
Expand Down Expand Up @@ -476,6 +471,62 @@ export const ModalTrigger: Story = {
decorators: [DemoDecorator],
};

export const OpenInsideModal: Story = {
render: () => {
const ref = useRef<HTMLDialogElement>(null);

useEffect(() => {
ref.current?.showModal();
}, []);

return (
<div>
<button onClick={() => ref.current?.showModal()}>Open modal</button>
<Modal ref={ref} header={{ heading: "Heading" }}>
<Modal.Body>
Culpa aliquip ut cupidatat laborum minim quis ex in aliqua.
<ActionMenu>
<ActionMenu.Trigger>
<button>Open action</button>
</ActionMenu.Trigger>
<ActionMenu.Content>
<ActionMenu.Item onSelect={() => console.log("Item 1 clicked")}>
Item 1
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 2 clicked")}>
Item 2
</ActionMenu.Item>
<ActionMenu.Item onSelect={() => console.log("Item 3 clicked")}>
Item 3
</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
<ActionMenu.Item>Item</ActionMenu.Item>
</ActionMenu.Content>
</ActionMenu>
</Modal.Body>
<Modal.Footer>
<Button type="button" onClick={() => ref.current?.close()}>
Close
</Button>
</Modal.Footer>
</Modal>
</div>
);
},
decorators: [DemoDecorator],
};

export const Links: Story = {
render: (props) => {
return (
Expand Down
189 changes: 82 additions & 107 deletions @navikt/core/react/src/overlays/action-menu/ActionMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import cl from "clsx";
import React, { forwardRef, useRef } from "react";
import { ChevronRightIcon } from "@navikt/aksel-icons";
import { useModalContext } from "../../modal/Modal.context";
import { Slot } from "../../slot/Slot";
import { OverridableComponent, useId } from "../../util";
import { composeEventHandlers } from "../../util/composeEventHandlers";
import { createContext } from "../../util/create-context";
import { useMergeRefs } from "../../util/hooks";
import { useControllableState } from "../../util/hooks/useControllableState";
import { requireReactElement } from "../../util/requireReactElement";
import { Menu } from "../floating-menu/Menu";
import { Menu, MenuPortalProps } from "../floating-menu/Menu";

/* -------------------------------------------------------------------------- */
/* ActionMenu */
Expand All @@ -20,6 +21,7 @@ type ActionMenuContextValue = {
open: boolean;
onOpenChange: (open: boolean) => void;
onOpenToggle: () => void;
rootElement: MenuPortalProps["rootElement"];
};

const [ActionMenuProvider, useActionMenuContext] =
Expand All @@ -29,22 +31,18 @@ const [ActionMenuProvider, useActionMenuContext] =
"ActionMenu sub-components cannot be rendered outside the ActionMenu component.",
});

interface ActionMenuProps {
type ActionMenuProps = {
HalvorHaugan marked this conversation as resolved.
Show resolved Hide resolved
children?: React.ReactNode;
/**
* Whether the menu is open or not.
* Only needed if you want manually control state.
*/
open?: boolean;
/**
* Whether the menu should be open by default.
*/
defaultOpen?: boolean;
/**
* Callback for when the menu is opened or closed.
*/
onOpenChange?: (open: boolean) => void;
}
} & Pick<MenuPortalProps, "rootElement">;

interface ActionMenuComponent extends React.FC<ActionMenuProps> {
/**
Expand Down Expand Up @@ -247,14 +245,17 @@ interface ActionMenuComponent extends React.FC<ActionMenuProps> {
const ActionMenuRoot = ({
children,
open: openProp,
defaultOpen = false,
onOpenChange,
rootElement: rootElementProp,
}: ActionMenuProps) => {
const triggerRef = useRef<HTMLButtonElement>(null);

const modalContext = useModalContext(false);
const rootElement = modalContext ? modalContext.ref.current : rootElementProp;

const [open = false, setOpen] = useControllableState({
value: openProp,
defaultValue: defaultOpen,
defaultValue: false,
onChange: onOpenChange,
});

Expand All @@ -266,6 +267,7 @@ const ActionMenuRoot = ({
open={open}
onOpenChange={setOpen}
onOpenToggle={() => setOpen((prevOpen) => !prevOpen)}
rootElement={rootElement}
>
<Menu open={open} onOpenChange={setOpen} modal>
{children}
Expand Down Expand Up @@ -328,58 +330,46 @@ export const ActionMenuTrigger = forwardRef<
/* ActionMenuContent */
/* -------------------------------------------------------------------------- */
interface ActionMenuContentProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "id">,
Pick<React.ComponentPropsWithoutRef<typeof Menu.Portal>, "rootElement"> {
extends Omit<React.HTMLAttributes<HTMLDivElement>, "id"> {
children?: React.ReactNode;
}

export const ActionMenuContent = forwardRef<
HTMLDivElement,
ActionMenuContentProps
>(
(
{
children,
className,
style,
rootElement,
...rest
}: ActionMenuContentProps,
ref,
) => {
const context = useActionMenuContext();
>(({ children, className, style, ...rest }: ActionMenuContentProps, ref) => {
const context = useActionMenuContext();

return (
<Menu.Portal rootElement={rootElement} asChild>
<Menu.Content
ref={ref}
id={context.contentId}
aria-labelledby={context.triggerId}
className={cl("navds-action-menu__content", className)}
{...rest}
align="start"
sideOffset={4}
collisionPadding={10}
onCloseAutoFocus={() => {
context.triggerRef.current?.focus();
}}
safeZone={{ anchor: context.triggerRef.current }}
style={{
...style,
...{
"--__ac-action-menu-content-transform-origin":
"var(--ac-floating-transform-origin)",
"--__ac-action-menu-content-available-height":
"var(--ac-floating-available-height)",
},
}}
>
<div className="navds-action-menu__content-inner">{children}</div>
</Menu.Content>
</Menu.Portal>
);
},
);
return (
<Menu.Portal rootElement={context.rootElement} asChild>
<Menu.Content
ref={ref}
id={context.contentId}
aria-labelledby={context.triggerId}
className={cl("navds-action-menu__content", className)}
{...rest}
align="start"
sideOffset={4}
collisionPadding={10}
onCloseAutoFocus={() => {
context.triggerRef.current?.focus();
}}
safeZone={{ anchor: context.triggerRef.current }}
style={{
...style,
...{
"--__ac-action-menu-content-transform-origin":
"var(--ac-floating-transform-origin)",
"--__ac-action-menu-content-available-height":
"var(--ac-floating-available-height)",
},
}}
>
<div className="navds-action-menu__content-inner">{children}</div>
</Menu.Content>
</Menu.Portal>
);
});

/* -------------------------------------------------------------------------- */
/* ActionMenuLabel */
Expand Down Expand Up @@ -839,22 +829,18 @@ interface ActionMenuSubProps {
* Whether the sub-menu is open or not. Only needed if you want to manually control state.
*/
open?: boolean;
/**
* Whether the sub-menu should be open by default.
*/
defaultOpen?: boolean;
/**
* Callback for when the sub-menu is opened or closed.
*/
onOpenChange?: (open: boolean) => void;
}

export const ActionMenuSub = (props: ActionMenuSubProps) => {
const { children, open: openProp, onOpenChange, defaultOpen = false } = props;
const { children, open: openProp, onOpenChange } = props;

const [open = false, setOpen] = useControllableState({
value: openProp,
defaultValue: defaultOpen,
defaultValue: false,
onChange: onOpenChange,
});

Expand Down Expand Up @@ -911,59 +897,48 @@ export const ActionMenuSubTrigger = forwardRef<
type ActionMenuSubContentElement = React.ElementRef<typeof Menu.Content>;

interface ActionMenuSubContentProps
extends React.HTMLAttributes<HTMLDivElement>,
Pick<React.ComponentPropsWithoutRef<typeof Menu.Portal>, "rootElement"> {
extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}

export const ActionMenuSubContent = forwardRef<
ActionMenuSubContentElement,
ActionMenuSubContentProps
>(
(
{
children,
className,
style,
rootElement,
...rest
}: ActionMenuSubContentProps,
ref,
) => {
return (
<Menu.Portal rootElement={rootElement}>
<Menu.SubContent
ref={ref}
alignOffset={-4}
sideOffset={1}
collisionPadding={10}
{...rest}
className={cl(
"navds-action-menu__content navds-action-menu__sub-content",
className,
)}
style={{
...style,
...{
"--ac-action-menu-content-transform-origin":
"var(--ac-floating-transform-origin)",
"--ac-action-menu-content-available-width":
"var(--ac-floating-available-width)",
"--ac-action-menu-content-available-height":
"var(--ac-floating-available-height)",
"--ac-action-menu-trigger-width":
"var(--ac-floating-anchor-width)",
"--ac-action-menu-trigger-height":
"var(--ac-floating-anchor-height)",
},
}}
>
<div className="navds-action-menu__content-inner">{children}</div>
</Menu.SubContent>
</Menu.Portal>
);
},
);
>(({ children, className, style, ...rest }: ActionMenuSubContentProps, ref) => {
const context = useActionMenuContext();

return (
<Menu.Portal rootElement={context.rootElement}>
<Menu.SubContent
ref={ref}
alignOffset={-4}
sideOffset={1}
collisionPadding={10}
{...rest}
className={cl(
"navds-action-menu__content navds-action-menu__sub-content",
className,
)}
style={{
...style,
...{
"--ac-action-menu-content-transform-origin":
"var(--ac-floating-transform-origin)",
"--ac-action-menu-content-available-width":
"var(--ac-floating-available-width)",
"--ac-action-menu-content-available-height":
"var(--ac-floating-available-height)",
"--ac-action-menu-trigger-width": "var(--ac-floating-anchor-width)",
"--ac-action-menu-trigger-height":
"var(--ac-floating-anchor-height)",
},
}}
>
<div className="navds-action-menu__content-inner">{children}</div>
</Menu.SubContent>
</Menu.Portal>
);
});

/* -------------------------------------------------------------------------- */
ActionMenu.Trigger = ActionMenuTrigger;
Expand Down
2 changes: 0 additions & 2 deletions @navikt/core/react/src/overlays/floating-menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@ const MenuRootContentNonModal = React.forwardRef<
ref={ref}
disableOutsidePointerEvents={false}
onDismiss={() => context.onOpenChange(false)}
flipAlignment={false}
/>
);
});
Expand All @@ -232,7 +231,6 @@ const MenuRootContentModal = forwardRef<
{ checkForDefaultPrevented: false },
)}
onDismiss={() => context.onOpenChange(false)}
flipAlignment={false}
/>
);
});
Expand Down
Loading
Loading