Skip to content

Commit

Permalink
feat(ContextMenu): support switch and selection mode in context menu …
Browse files Browse the repository at this point in the history
…section (#1051)
  • Loading branch information
Lisa18289 authored Dec 18, 2024
1 parent e015b19 commit 42c750e
Show file tree
Hide file tree
Showing 12 changed files with 194 additions and 38 deletions.
34 changes: 20 additions & 14 deletions packages/components/src/components/ContextMenu/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import type { MenuItemProps } from "@/components/MenuItem";
import { useOverlayController } from "@/lib/controller";
import OverlayContextProvider from "@/lib/controller/overlay/OverlayContextProvider";
import { Action } from "@/components/Action";
import type { ContextMenuSelectionMode } from "@/components/ContextMenu/lib";
import {
getAriaSelectionMode,
getCloseOverlayType,
getMenuItemSelectionVariant,
} from "@/components/ContextMenu/lib";

export interface ContextMenuProps
extends Omit<PopoverProps, "withTip">,
Expand All @@ -24,7 +30,7 @@ export interface ContextMenuProps
>,
FlowComponentProps {
/** The type of selection that is allowed in the context menu. */
selectionMode?: "single" | "multiple" | "navigation";
selectionMode?: ContextMenuSelectionMode;
/** Sets the context menu to a fixed width. */
width?: string | number;
}
Expand All @@ -50,11 +56,7 @@ export const ContextMenu = flowComponent("ContextMenu", (props) => {
const overlayController =
overlayControllerFromProps ?? overlayControllerFromContext;

const ariaSelectionMode =
selectionMode === "navigation" ? "none" : selectionMode;

const selectionVariant =
selectionMode === "navigation" ? "navigation" : "control";
const selectionVariant = getMenuItemSelectionVariant(selectionMode);

const propsContext: PropsContext = {
MenuItem: {
Expand All @@ -66,19 +68,21 @@ export const ContextMenu = flowComponent("ContextMenu", (props) => {

Section: {
MenuItem: {
selectionVariant,
Avatar: {
size: "l",
},
},
renderContextMenuSection: true,
},
};

const closeOverlayType =
selectionMode === "single" || selectionMode === "navigation"
? "ContextMenu"
: undefined;
ContextMenuSection: {
MenuItem: {
Avatar: {
size: "l",
},
},
},
};

return (
<ClearPropsContext>
Expand All @@ -95,15 +99,17 @@ export const ContextMenu = flowComponent("ContextMenu", (props) => {
<Aria.Menu
className={styles.contextMenu}
onAction={onAction}
selectionMode={ariaSelectionMode}
selectionMode={getAriaSelectionMode(selectionMode)}
selectedKeys={selectedKeys}
defaultSelectedKeys={defaultSelectedKeys}
disabledKeys={disabledKeys}
onSelectionChange={onSelectionChange}
ref={ref}
>
<PropsContextProvider props={propsContext}>
<Action closeOverlay={closeOverlayType}>{children}</Action>
<Action closeOverlay={getCloseOverlayType(selectionMode)}>
{children}
</Action>
</PropsContextProvider>
</Aria.Menu>
</OverlayContextProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,49 @@ import { PropsContextProvider } from "@/lib/propsContext";
import type { FlowComponentProps } from "@/lib/componentFactory/flowComponent";
import { flowComponent } from "@/lib/componentFactory/flowComponent";
import styles from "../../ContextMenu.module.scss";
import type { ContextMenuSelectionMode } from "@/components/ContextMenu/lib";
import {
getAriaSelectionMode,
getCloseOverlayType,
getMenuItemSelectionVariant,
} from "@/components/ContextMenu/lib";
import { Action } from "@/components/Action";

export type ContextMenuSectionProps = PropsWithChildren & FlowComponentProps;
export type ContextMenuSectionProps = PropsWithChildren &
FlowComponentProps & {
selectionMode?: ContextMenuSelectionMode;
};
export const ContextMenuSection = flowComponent(
"ContextMenuSection",
(props) => {
const { children } = props;
const { children, selectionMode, ...rest } = props;

const selectionVariant = getMenuItemSelectionVariant(selectionMode);

const propsContext: PropsContext = {
Heading: {
level: 5,
wrapWith: <Aria.Header />,
},
MenuItem: {
selectionVariant,
},
};

return (
<Aria.Section className={styles.section}>
<Aria.MenuSection
{...rest}
selectionMode={getAriaSelectionMode(selectionMode)}
className={styles.section}
>
<PropsContextProvider props={propsContext} mergeInParentContext>
{children}
<Action skip>
<Action closeOverlay={getCloseOverlayType(selectionMode)}>
{children}
</Action>
</Action>
</PropsContextProvider>
</Aria.Section>
</Aria.MenuSection>
);
},
);
1 change: 1 addition & 0 deletions packages/components/src/components/ContextMenu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { ContextMenu } from "./ContextMenu";
export { type ContextMenuProps, ContextMenu } from "./ContextMenu";
export * from "../MenuItem";
export * from "./components/ContextMenuTrigger";
export * from "./components/ContextMenuSection";
export default ContextMenu;
31 changes: 31 additions & 0 deletions packages/components/src/components/ContextMenu/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export type ContextMenuSelectionMode =
| "single"
| "multiple"
| "navigation"
| "switch";

export const getAriaSelectionMode = (
selectionMode?: ContextMenuSelectionMode,
) => {
return selectionMode === "navigation"
? "none"
: selectionMode === "switch"
? "multiple"
: selectionMode;
};

export const getMenuItemSelectionVariant = (
selectionMode?: ContextMenuSelectionMode,
) => {
return selectionMode === "single" || selectionMode === "multiple"
? "control"
: selectionMode;
};

export const getCloseOverlayType = (
selectionMode?: ContextMenuSelectionMode,
) => {
return selectionMode === "single" || selectionMode === "navigation"
? "ContextMenu"
: undefined;
};
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import ContextMenu, {
ContextMenuSection,
ContextMenuTrigger,
MenuItem,
} from "@/components/ContextMenu";
import { Button } from "@/components/Button";
import { Separator } from "@/components/Separator";
import { Heading } from "@/components/Heading";
import { Section } from "@/components/Section";
import { IconCamera, IconServer } from "@/components/Icon/components/icons";
import {
IconCamera,
IconEmail,
IconServer,
} from "@/components/Icon/components/icons";
import { Text } from "@/components/Text";
import { Avatar } from "@/components/Avatar";
import { Initials } from "@/components/Initials";
Expand Down Expand Up @@ -104,14 +108,14 @@ export const WithContextMenuSection: Story = {
<ContextMenuTrigger>
<Button>Trigger</Button>
<ContextMenu selectionMode="navigation" {...props}>
<Section>
<ContextMenuSection>
<Heading>Websites</Heading>
<MenuItem href="https://www.mittwald.de" id="https://www.mittwald.de">
www.mittwald.de
</MenuItem>
<MenuItem href="https://www.google.de">www.google.de</MenuItem>
<MenuItem href="https://www.adobe.com">www.adobe.com</MenuItem>
</Section>
</ContextMenuSection>
</ContextMenu>
</ContextMenuTrigger>
),
Expand All @@ -125,7 +129,7 @@ export const WithIcon: Story = {
<ContextMenuTrigger>
<Button>Trigger</Button>
<ContextMenu selectionMode="navigation" {...props}>
<Section>
<ContextMenuSection>
<Heading>Websites</Heading>
<MenuItem href="https://www.mittwald.de" id="https://www.mittwald.de">
<IconServer />
Expand All @@ -139,7 +143,7 @@ export const WithIcon: Story = {
<IconServer />
<Text>www.adobe.com</Text>
</MenuItem>
</Section>
</ContextMenuSection>
</ContextMenu>
</ContextMenuTrigger>
),
Expand All @@ -155,24 +159,49 @@ export const WithAvatar: Story = {
<ContextMenuTrigger>
<Button>Trigger</Button>
<ContextMenu selectionMode="navigation" {...props}>
<Section>
<ContextMenuSection>
<MenuItem>
<Avatar>
<Initials>Max Mustermann</Initials>
</Avatar>
<IconCamera />
</MenuItem>
<Heading>Max Mustermann</Heading>
</Section>
</ContextMenuSection>
<Separator />
<Section>
<ContextMenuSection>
<MenuItem>
<Text>Settings</Text>
</MenuItem>
<MenuItem>
<Text>Logout</Text>
</MenuItem>
</Section>
</ContextMenuSection>
</ContextMenu>
</ContextMenuTrigger>
),
args: {
defaultOpen: true,
},
};

export const WithSectionSelectionMode: Story = {
render: (props) => (
<ContextMenuTrigger>
<Button>Trigger</Button>
<ContextMenu {...props}>
<ContextMenuSection selectionMode="switch">
<MenuItem>
<Text>Spam protection</Text>
</MenuItem>
</ContextMenuSection>
<Separator />
<ContextMenuSection selectionMode="navigation">
<MenuItem>
<IconEmail />
<Text>Update email address</Text>
</MenuItem>
</ContextMenuSection>
</ContextMenu>
</ContextMenuTrigger>
),
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/components/MenuItem/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface MenuItemProps
extends Omit<Aria.MenuItemProps, "children">,
PropsWithChildren,
FlowComponentProps {
selectionVariant?: "control" | "navigation";
selectionVariant?: "control" | "navigation" | "switch";
}

export const MenuItem = flowComponent("MenuItem", (props) => {
Expand Down
12 changes: 10 additions & 2 deletions packages/components/src/components/MenuItem/MenuItemContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import { deepHas } from "@/lib/react/deepHas";
import { Wrap } from "@/components/Wrap";
import clsx from "clsx";
import { Avatar } from "@/components/Avatar";
import { Switch } from "@/components/Switch";

interface Props extends Aria.MenuItemRenderProps, PropsWithChildren {
selectionVariant?: "control" | "navigation";
selectionVariant?: "control" | "navigation" | "switch";
}

export const MenuItemContent: FC<Props> = (props) => {
Expand All @@ -44,12 +45,19 @@ export const MenuItemContent: FC<Props> = (props) => {
Icon: {
className: clsx(styles.controlIcon, styles.icon),
},
Switch: {
className: clsx(styles.controlIcon, styles.switch),
},
};

const selectionIcon =
selectionMode === "none" ||
selectionVariant === "navigation" ? null : selectionMode === "single" &&
selectionVariant === "navigation" ? null : selectionVariant === "switch" &&
isSelected ? (
<Switch isReadOnly isSelected />
) : selectionVariant === "switch" && !isSelected ? (
<Switch isReadOnly />
) : selectionMode === "single" && isSelected ? (
<IconRadioOn />
) : selectionMode === "single" && !isSelected ? (
<IconRadioOff />
Expand Down
8 changes: 8 additions & 0 deletions packages/components/src/styles/mixins/menuItem.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
cursor: default;
}

&:has(.switch) {
justify-content: space-between;
}

@include focus.focus;

& {
Expand All @@ -34,6 +38,10 @@
margin-inline-start: var(--menu-item--spacing);
}

.switch {
order: 2;
}

&:hover {
background-color: var(--menu-item--background-color--hover);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import ContextMenu, {
ContextMenuSection,
ContextMenuTrigger,
} from "@mittwald/flow-react-components/ContextMenu";
import Button from "@mittwald/flow-react-components/Button";
import MenuItem from "@mittwald/flow-react-components/MenuItem";
import Heading from "@mittwald/flow-react-components/Heading";
import Separator from "@mittwald/flow-react-components/Separator";
import Section from "@mittwald/flow-react-components/Section";

<ContextMenuTrigger>
<Button>Trigger</Button>
<ContextMenu>
<Section>
<ContextMenuSection>
<Heading>Section 1</Heading>
<MenuItem id="1">Item 1</MenuItem>
<MenuItem id="2">Item 2</MenuItem>
<MenuItem id="3">Item 3</MenuItem>
</Section>
</ContextMenuSection>
<Separator />
<Section>
<ContextMenuSection>
<Heading>Section 2</Heading>
<MenuItem id="4">Item 4</MenuItem>
<MenuItem id="5">Item 5</MenuItem>
</Section>
</ContextMenuSection>
</ContextMenu>
</ContextMenuTrigger>;
Loading

0 comments on commit 42c750e

Please sign in to comment.