Files
outline/app/components/primitives/Menu/index.tsx
T
Tom Moor adbffc0734 chore: clear mechanical lint warnings (Phase 1) (#12198)
* chore: clear mechanical lint warnings

Drops 44 oxlint warnings (559 → 515) by fixing easy mechanical rules
across the codebase: no-useless-escape, no-duplicate-type-constituents,
no-redundant-type-constituents, no-unused-expressions,
no-meaningless-void-operator, require-array-sort-compare, await-thenable.

* chore: drop callback parameter from useCallback deps

The `open` argument is a parameter of the callback, not a closed-over
variable, so it doesn't belong in the deps array.

* chore: promote cleared lint rules to errors

Promotes the rules cleared in this PR from warn to error so future
violations fail the lint:

- no-unused-expressions
- typescript/await-thenable
- typescript/no-duplicate-type-constituents
- typescript/no-meaningless-void-operator
- typescript/require-array-sort-compare

Removes the override that suppressed no-useless-escape on source
files (the global rule is already error) and fixes the 21 escape
violations that this exposed in regex character classes and template
literals.

* chore: address PR review feedback

- usePinnedDocuments: simplify UrlId to plain string instead of the
  intersection trick.
- PlantUML embed: move - to end of character class so it's a literal
  hyphen rather than a range operator.
- checkboxes: type token params as Token | undefined to match the
  actual call sites that pass tokens[index - 2] etc.
2026-04-28 20:00:03 -04:00

462 lines
12 KiB
TypeScript

import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import * as Components from "../components/Menu";
import type { LocationDescriptor } from "history";
import * as React from "react";
import Tooltip from "~/components/Tooltip";
import { CheckmarkIcon } from "outline-icons";
import { normalizeKeyDisplay, shortcutSeparator } from "@shared/utils/keyboard";
import { useMenuContext } from "./MenuContext";
type MenuProps = React.ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.Root
> &
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Root>;
const Menu = ({ children, ...rest }: MenuProps) => {
const { variant } = useMenuContext();
const Root =
variant === "dropdown"
? DropdownMenuPrimitive.Root
: ContextMenuPrimitive.Root;
return <Root {...rest}>{children}</Root>;
};
type SubMenuProps = React.ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.Sub
> &
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Sub>;
const SubMenu = ({ children, ...rest }: SubMenuProps) => {
const { variant } = useMenuContext();
const Sub =
variant === "dropdown"
? DropdownMenuPrimitive.Sub
: ContextMenuPrimitive.Sub;
return <Sub {...rest}>{children}</Sub>;
};
type TriggerProps = React.ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.Trigger
> &
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Trigger>;
const MenuTrigger = React.forwardRef<
| React.ElementRef<typeof DropdownMenuPrimitive.Trigger>
| React.ElementRef<typeof ContextMenuPrimitive.Trigger>,
TriggerProps
>((props, ref) => {
const { variant } = useMenuContext();
const { children, ...rest } = props;
const Trigger =
variant === "dropdown"
? DropdownMenuPrimitive.Trigger
: ContextMenuPrimitive.Trigger;
return (
<Trigger ref={ref} {...rest} asChild>
{children}
</Trigger>
);
});
MenuTrigger.displayName = "MenuTrigger";
type ContentProps = React.ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.Content
> &
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>;
const MenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
ContentProps
>((props, ref) => {
const { variant } = useMenuContext();
const { children, ...rest } = props;
const Portal =
variant === "dropdown"
? DropdownMenuPrimitive.Portal
: ContextMenuPrimitive.Portal;
const Content =
variant === "dropdown"
? DropdownMenuPrimitive.Content
: ContextMenuPrimitive.Content;
const offsetProp =
variant === "dropdown" ? { sideOffset: 4 } : { alignOffset: 4 };
const contentProps = {
maxHeightVar:
variant === "dropdown"
? "--radix-dropdown-menu-content-available-height"
: "--radix-context-menu-content-available-height",
transformOriginVar:
variant === "dropdown"
? "--radix-dropdown-menu-content-transform-origin"
: "--radix-context-menu-content-transform-origin",
};
return (
<Portal>
<Content ref={ref} {...offsetProp} {...rest} collisionPadding={6} asChild>
<Components.MenuContent {...contentProps} hiddenScrollbars>
{children}
</Components.MenuContent>
</Content>
</Portal>
);
});
MenuContent.displayName = "MenuContent";
type SubMenuTriggerProps = BaseItemProps &
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> &
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger>;
const SubMenuTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
SubMenuTriggerProps
>((props, ref) => {
const { variant } = useMenuContext();
const { label, icon, disabled, ...rest } = props;
const Trigger =
variant === "dropdown"
? DropdownMenuPrimitive.SubTrigger
: ContextMenuPrimitive.SubTrigger;
return (
<Trigger ref={ref} {...rest} asChild>
<Components.MenuSubTrigger disabled={disabled}>
{icon}
<Components.MenuLabel>{label}</Components.MenuLabel>
<Components.MenuDisclosure />
</Components.MenuSubTrigger>
</Trigger>
);
});
SubMenuTrigger.displayName = "SubMenuTrigger";
type SubMenuContentProps = React.ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.SubContent
> &
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>;
const SubMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
SubMenuContentProps
>((props, ref) => {
const { variant } = useMenuContext();
const { children, ...rest } = props;
const Portal =
variant === "dropdown"
? DropdownMenuPrimitive.Portal
: ContextMenuPrimitive.Portal;
const Content =
variant === "dropdown"
? DropdownMenuPrimitive.SubContent
: ContextMenuPrimitive.SubContent;
const contentProps = {
maxHeightVar:
variant === "dropdown"
? "--radix-dropdown-menu-content-available-height"
: "--radix-context-menu-content-available-height",
transformOriginVar:
variant === "dropdown"
? "--radix-dropdown-menu-content-transform-origin"
: "--radix-context-menu-content-transform-origin",
};
return (
<Portal>
<Content ref={ref} {...rest} collisionPadding={6} asChild>
<Components.MenuContent {...contentProps} hiddenScrollbars>
{children}
</Components.MenuContent>
</Content>
</Portal>
);
});
SubMenuContent.displayName = "SubMenuContent";
type MenuGroupProps = {
label: string;
items: React.ReactNode[];
} & Omit<
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Group>,
"children" | "asChild"
> &
Omit<
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Group>,
"children" | "asChild"
>;
const MenuGroup = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Group>,
MenuGroupProps
>((props, ref) => {
const { variant } = useMenuContext();
const { label, items, ...rest } = props;
const Group =
variant === "dropdown"
? DropdownMenuPrimitive.Group
: ContextMenuPrimitive.Group;
return (
<Group ref={ref} {...rest}>
<MenuLabel>{label}</MenuLabel>
{items}
</Group>
);
});
MenuGroup.displayName = "MenuGroup";
type BaseItemProps = {
label: string;
icon?: React.ReactElement;
disabled?: boolean;
shortcut?: string[];
};
/**
* Renders a keyboard shortcut as formatted key symbols.
*
* @param shortcut - array of key strings (e.g. ["Meta+Shift+l"]).
* @returns rendered shortcut element or null.
*/
function MenuItemShortcut({ shortcut }: { shortcut?: string[] }) {
if (!shortcut?.length) {
return null;
}
return (
<Components.MenuShortcut>
{shortcut.map((sc, scIndex) =>
sc.split("+").flatMap((key, keyIndex, arr) => {
const el = (
<span key={`${scIndex}-${keyIndex}`}>
{normalizeKeyDisplay(key, true)}
</span>
);
return keyIndex < arr.length - 1 && shortcutSeparator
? [el, shortcutSeparator]
: [el];
})
)}
</Components.MenuShortcut>
);
}
type MenuButtonProps = BaseItemProps & {
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
tooltip?: React.ReactChild;
selected?: boolean;
dangerous?: boolean;
} & Omit<
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>,
"children" | "asChild" | "onClick"
> &
Omit<
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item>,
"children" | "asChild" | "onClick"
>;
const MenuButton = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
MenuButtonProps
>((props, ref) => {
const { variant } = useMenuContext();
const {
label,
icon,
tooltip,
disabled,
selected,
dangerous,
shortcut,
onClick,
...rest
} = props;
const Item =
variant === "dropdown"
? DropdownMenuPrimitive.Item
: ContextMenuPrimitive.Item;
const button = (
<Item ref={ref} disabled={disabled} {...rest} asChild>
<Components.MenuButton
disabled={disabled}
$dangerous={dangerous}
onClick={onClick}
>
{icon}
<Components.MenuLabel>{label}</Components.MenuLabel>
{selected !== undefined && (
<Components.SelectedIconWrapper aria-hidden>
{selected ? <CheckmarkIcon size={18} /> : null}
</Components.SelectedIconWrapper>
)}
<MenuItemShortcut shortcut={shortcut} />
</Components.MenuButton>
</Item>
);
return tooltip ? (
<Tooltip content={tooltip} placement="bottom">
<div>{button}</div>
</Tooltip>
) : (
<>{button}</>
);
});
MenuButton.displayName = "MenuButton";
type MenuInternalLinkProps = BaseItemProps & {
to: LocationDescriptor;
} & Omit<
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>,
"children" | "asChild" | "onClick"
> &
Omit<
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item>,
"children" | "asChild" | "onClick"
>;
const MenuInternalLink = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
MenuInternalLinkProps
>((props, ref) => {
const { variant } = useMenuContext();
const { label, icon, disabled, shortcut, to, ...rest } = props;
const Item =
variant === "dropdown"
? DropdownMenuPrimitive.Item
: ContextMenuPrimitive.Item;
return (
<Item ref={ref} disabled={disabled} {...rest} asChild>
<Components.MenuInternalLink to={to} disabled={disabled}>
{icon}
<Components.MenuLabel>{label}</Components.MenuLabel>
<MenuItemShortcut shortcut={shortcut} />
</Components.MenuInternalLink>
</Item>
);
});
MenuInternalLink.displayName = "MenuInternalLink";
type MenuExternalLinkProps = BaseItemProps & {
href: string;
target?: string;
} & Omit<
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>,
"children" | "asChild" | "onClick"
> &
Omit<
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item>,
"children" | "asChild" | "onClick"
>;
const MenuExternalLink = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
MenuExternalLinkProps
>((props, ref) => {
const { variant } = useMenuContext();
const { label, icon, disabled, shortcut, href, target, ...rest } = props;
const Item =
variant === "dropdown"
? DropdownMenuPrimitive.Item
: ContextMenuPrimitive.Item;
return (
<Item ref={ref} disabled={disabled} {...rest} asChild>
<Components.MenuExternalLink
href={href}
target={target}
disabled={disabled}
>
{icon}
<Components.MenuLabel>{label}</Components.MenuLabel>
<MenuItemShortcut shortcut={shortcut} />
</Components.MenuExternalLink>
</Item>
);
});
MenuExternalLink.displayName = "MenuExternalLink";
type MenuSeparatorProps = React.ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.Separator
> &
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>;
const MenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
MenuSeparatorProps
>((props, ref) => {
const { variant } = useMenuContext();
const Separator =
variant === "dropdown"
? DropdownMenuPrimitive.Separator
: ContextMenuPrimitive.Separator;
return (
<Separator ref={ref} {...props} asChild>
<Components.MenuSeparator />
</Separator>
);
});
MenuSeparator.displayName = "MenuSeparator";
type MenuLabelProps = React.ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.Label
> &
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label>;
const MenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
MenuLabelProps
>((props, ref) => {
const { variant } = useMenuContext();
const { children, ...rest } = props;
const Label =
variant === "dropdown"
? DropdownMenuPrimitive.Label
: ContextMenuPrimitive.Label;
return (
<Label ref={ref} {...rest} asChild>
<Components.MenuHeader>{children}</Components.MenuHeader>
</Label>
);
});
MenuLabel.displayName = "MenuLabel";
export {
Menu,
MenuTrigger,
MenuContent,
MenuButton,
MenuInternalLink,
MenuExternalLink,
MenuSeparator,
MenuGroup,
MenuLabel,
SubMenu,
SubMenuTrigger,
SubMenuContent,
};