Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ exports[`ColorPicker > should render correctly 1`] = `
"
>
<a
aria-controls="radix-:r0:"
aria-controls="radix-«r0»"
aria-expanded="false"
aria-haspopup="dialog"
class="
Expand Down Expand Up @@ -432,7 +432,7 @@ exports[`ColorPicker > should render with showAlpha 1`] = `
"
>
<a
aria-controls="radix-:r2:"
aria-controls="radix-«r2»"
aria-expanded="false"
aria-haspopup="dialog"
class="
Expand Down Expand Up @@ -809,7 +809,7 @@ exports[`ColorPicker > should render with value 1`] = `
"
>
<a
aria-controls="radix-:r1:"
aria-controls="radix-«r1»"
aria-expanded="false"
aria-haspopup="dialog"
class="
Expand Down
30 changes: 28 additions & 2 deletions packages/design/src/components/dropdown-menu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ import {
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuPrimitive,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from './DropdownMenuPrimitive';

Expand All @@ -35,6 +39,15 @@ interface IDropdownMenuNormalItem {
onSelect?: (item: DropdownMenuType) => void;
}

interface IDropdownMenuNormalSubItem {
type: 'subItem';
className?: string;
children: ReactNode;
options?: DropdownMenuType[];
disabled?: boolean;
onSelect?: (item: DropdownMenuType) => void;
}

interface IDropdownMenuSeparatorItem {
type: 'separator';
className?: string;
Expand Down Expand Up @@ -65,7 +78,7 @@ interface IDropdownMenuCheckItem {
onSelect?: (item: string) => void;
}

type DropdownMenuType = IDropdownMenuNormalItem | IDropdownMenuSeparatorItem | IDropdownMenuRadioItem | IDropdownMenuCheckItem;
type DropdownMenuType = IDropdownMenuNormalItem | IDropdownMenuNormalSubItem | IDropdownMenuSeparatorItem | IDropdownMenuRadioItem | IDropdownMenuCheckItem;

export interface IDropdownMenuProps extends ComponentProps<typeof DropdownMenuContent> {
children: ReactNode;
Expand Down Expand Up @@ -164,6 +177,19 @@ export function DropdownMenu(props: IDropdownMenuProps) {
{item.children}
</DropdownMenuItem>
);
} else if (type === 'subItem') {
return (
<DropdownMenuSub key={index}>
<DropdownMenuSubTrigger>{item.children}</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent sideOffset={12}>
{item.options?.map((subItem, subIndex) => (
renderMenuItem(subItem, subIndex)
))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
);
}
}

Expand All @@ -172,7 +198,7 @@ export function DropdownMenu(props: IDropdownMenuProps) {
<DropdownMenuTrigger asChild>
{children}
</DropdownMenuTrigger>
<DropdownMenuContent {...restProps}>
<DropdownMenuContent {...restProps} onContextMenu={(e) => e.preventDefault()}>
{items.map((item, index) => renderMenuItem(item, index))}
</DropdownMenuContent>
</DropdownMenuPrimitive>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,14 @@ function DropdownMenuSubContent({

function DropdownMenuContent({
className,
alignOffset = 0,
sideOffset = 4,
...props
}: ComponentProps<typeof Content>) {
return (
<Portal>
<Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={clsx(
`
univer-z-[1080] univer-box-border univer-max-h-[var(--radix-popper-available-height)]
Expand All @@ -175,6 +175,8 @@ function DropdownMenuContent({
scrollbarClassName,
className
)}
alignOffset={alignOffset}
sideOffset={sideOffset}
{...props}
/>
</Portal>
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/components/menu/desktop/TinyMenuGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ import { combineLatest, of } from 'rxjs';
import { ComponentManager } from '../../../common';
import { useDependency } from '../../../utils/di';

/** @deprecated */
interface IUITinyMenuGroupProps {
item: IMenuSchema;
onOptionSelect?: (option: IValueOption) => void;
}

/** @deprecated */
export function UITinyMenuGroup(props: IUITinyMenuGroupProps) {
const { item, onOptionSelect } = props;
const [activeItems, setActiveItems] = useState<string[]>([]);
Expand Down
212 changes: 180 additions & 32 deletions packages/ui/src/views/components/context-menu/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,68 @@
* limitations under the License.
*/

import type { IDropdownMenuProps } from '@univerjs/design';
import type { IMouseEvent } from '@univerjs/engine-render';
import type { IMenuSchema } from '../../../services/menu/menu-manager.service';
import { ICommandService } from '@univerjs/core';
import { Popup } from '@univerjs/design';
import React, { useEffect, useRef, useState } from 'react';
import { Menu } from '../../../components/menu/desktop/Menu';

import { DropdownMenu } from '@univerjs/design';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { CustomLabel } from '../../../components/custom-label/CustomLabel';
import { IContextMenuService } from '../../../services/contextmenu/contextmenu.service';
import { ILayoutService } from '../../../services/layout/layout.service';
import { useDependency, useInjector } from '../../../utils/di';
import { MenuItemType } from '../../../services/menu/menu';
import { IMenuManagerService } from '../../../services/menu/menu-manager.service';
import { useDependency, useObservable } from '../../../utils/di';

function MenuItemButton(props: { menuItem: IMenuSchema; setVisible: (visible: boolean) => void }) {
const { menuItem, setVisible } = props;

const commandService = useDependency(ICommandService);
const layoutService = useDependency(ILayoutService);

const { title, commandId, value$, icon, label, id } = menuItem.item!;
const value = (menuItem.item as any)?.value;

const observableValue = useObservable(value$);

const [realValue, setRealValue] = useState<any>(observableValue ?? value);

return (
<div
className="univer-box-border univer-flex univer-w-full univer-items-center univer-gap-2"
onClick={() => {
if (commandService) {
commandService.executeCommand(commandId ?? id as string, { value: realValue });
}
layoutService.focus();
setVisible(false);
}}
>
<CustomLabel
icon={icon}
value$={value$ as any}
label={label}
title={title}
onChange={(value) => {
if (commandService) {
setRealValue(value);
commandService.executeCommand(commandId ?? id as string, { value: +value });
}
}}
/>
</div>
);
}

export function DesktopContextMenu() {
const contentRef = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
const [menuType, setMenuType] = useState('');
const [menu, setMenu] = useState<IDropdownMenuProps['items']>([]);
const [offset, setOffset] = useState<[number, number]>([0, 0]);
const visibleRef = useRef(visible);
const contextMenuService = useDependency(IContextMenuService);
const commandService = useDependency(ICommandService);
const injector = useInjector();
const menuManagerService = useDependency(IMenuManagerService);

visibleRef.current = visible;

useEffect(() => {
Expand All @@ -53,11 +96,9 @@
}

document.addEventListener('pointerdown', handleClickOutside);
document.addEventListener('wheel', handleClose);

return () => {
document.removeEventListener('pointerdown', handleClickOutside);
document.removeEventListener('wheel', handleClose);
disposables.dispose();
};
}, [contextMenuService]);
Expand All @@ -66,7 +107,105 @@
function handleContextMenu(event: IMouseEvent, menuType: string) {
setVisible(false);
requestAnimationFrame(() => {
setMenuType(menuType);
const menu = menuManagerService.getMenuByPositionKey(menuType);

if (!menu) {
return;
}

const dropdownMenu: IDropdownMenuProps['items'] = [];

for (let i = 0; i < menu.length; i++) {
const group = menu[i];

if (!group?.children?.length) continue;

for (const menuItem of group?.children) {

Check notice on line 123 in packages/ui/src/views/components/context-menu/ContextMenu.tsx

View check run for this annotation

codefactor.io / CodeFactor

packages/ui/src/views/components/context-menu/ContextMenu.tsx#L123

Unsafe usage of optional chaining. (eslint/no-unsafe-optional-chaining)
if (!menuItem.item) continue;

const { type, title, hidden$, disabled$, icon, label, id } = menuItem.item;

let hidden = false;
let disabled = false;
if (hidden$) {
hidden$.subscribe((v) => {
hidden = v;
}).unsubscribe();
}
if (disabled$) {
disabled$.subscribe((v) => {
disabled = v;
}).unsubscribe();
}

if (hidden) continue;

if (type === MenuItemType.BUTTON) {
dropdownMenu.push({
type: 'item',
children: (
<MenuItemButton menuItem={menuItem} setVisible={setVisible} />
),
disabled,
});
} else if (type === MenuItemType.SUBITEMS) {
const subMenu = menuManagerService.getMenuByPositionKey(id);

dropdownMenu.push({
type: 'subItem',
children: (
<div
className={`
univer-box-border univer-flex univer-w-full univer-items-center univer-gap-2
`}
>
<CustomLabel
icon={icon}
label={label}
title={title}
/>
</div>
),
disabled,
options: subMenu.map((item) => {
if (!item.item) return null;

const { hidden$, disabled$ } = item.item;

let hidden = false;
let disabled = false;

if (hidden$) {
hidden$.subscribe((v) => {
hidden = v;
}).unsubscribe();
}
if (disabled$) {
disabled$.subscribe((v) => {
disabled = v;
}).unsubscribe();
}

if (hidden) return null;

return {
type: 'item',
children: (
<MenuItemButton menuItem={item} setVisible={setVisible} />
),
disabled,
} as IDropdownMenuProps['items'][number];
}).filter((item) => item !== null),
});
}
}

if (i < menu.length - 1) {
dropdownMenu.push({ type: 'separator' });
}
}

setMenu(dropdownMenu);
setOffset([event.clientX, event.clientY]);
setVisible(true);
});
Expand All @@ -76,27 +215,36 @@
setVisible(false);
}

const contextMenuRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
requestAnimationFrame(() => {
if (!contextMenuRef.current) return;

const screenHeight = window.innerHeight;
const { height: contextMenuHeight } = contextMenuRef.current.getBoundingClientRect();

if (contextMenuHeight + offset[1] > screenHeight) {
if (offset[1] !== screenHeight - contextMenuHeight) {
setOffset([offset[0], screenHeight - contextMenuHeight]);
}
}
});
}, [visible, contextMenuRef, offset]);

return (
<Popup visible={visible} offset={offset}>
<section ref={contentRef}>
{menuType && (
<Menu
menuType={menuType}
onOptionSelect={(params) => {
const { label: id, commandId, value } = params;

if (commandService) {
commandService.executeCommand(commandId ?? id as string, { value });
}

const layoutService = injector.get(ILayoutService);
layoutService.focus();

setVisible(false);
}}
/>
)}
</section>
</Popup>
<DropdownMenu
ref={contextMenuRef}
className="!univer-max-h-screen univer-min-w-52 !univer-animate-none"
open={visible}
items={menu}
align="start"
alignOffset={offset[0]}
sideOffset={offset[1]}
sticky="always"
collisionBoundary={document.body}
onOpenChange={setVisible}
>
<div className="univer-fixed univer-left-0 univer-top-0 univer-hidden univer-rounded-md" />
</DropdownMenu>
);
}
Loading