mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
adbffc0734
* 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.
462 lines
12 KiB
TypeScript
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,
|
|
};
|