mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
refactor(core-ui): CB-2038 use menu blocks in context menu
This commit is contained in:
@@ -16,28 +16,36 @@ import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
import { useObjectRef } from '../useObjectRef';
|
||||
import { MenuPanel } from './MenuPanel';
|
||||
import { menuPanelStyles } from './menuPanelStyles';
|
||||
import { MenuStateContext } from './MenuStateContext';
|
||||
import { IMenuState, MenuStateContext } from './MenuStateContext';
|
||||
|
||||
interface IMenuProps extends Omit<React.ButtonHTMLAttributes<any>, 'style'> {
|
||||
label: string;
|
||||
items: React.ReactNode;
|
||||
items: React.ReactNode | (() => React.ReactNode);
|
||||
menuRef?: React.RefObject<IMenuState | undefined>;
|
||||
style?: ComponentStyle;
|
||||
disclosure?: boolean;
|
||||
placement?: MenuInitialState['placement'];
|
||||
modal?: boolean;
|
||||
visible?: boolean;
|
||||
rtl?: boolean;
|
||||
hasBindings?: boolean;
|
||||
panelAvailable?: boolean;
|
||||
getHasBindings?: () => boolean;
|
||||
onVisibleSwitch?: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export const Menu = observer<IMenuProps, React.ButtonHTMLAttributes<any>>(function Menu({
|
||||
export const Menu = observer<IMenuProps, HTMLButtonElement>(function Menu({
|
||||
label,
|
||||
items,
|
||||
menuRef,
|
||||
disclosure,
|
||||
children,
|
||||
style,
|
||||
placement,
|
||||
visible,
|
||||
hasBindings,
|
||||
panelAvailable,
|
||||
getHasBindings,
|
||||
onVisibleSwitch,
|
||||
modal,
|
||||
rtl,
|
||||
@@ -47,6 +55,11 @@ export const Menu = observer<IMenuProps, React.ButtonHTMLAttributes<any>>(functi
|
||||
const menu = useMenuState({ modal, placement, visible, rtl });
|
||||
const styles = useStyles(menuPanelStyles, style);
|
||||
|
||||
if (menuRef) {
|
||||
//@ts-expect-error Ref mutation
|
||||
menuRef.current = menu;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
propsRef.onVisibleSwitch?.(menu.visible);
|
||||
}, [menu.visible]);
|
||||
@@ -63,6 +76,9 @@ export const Menu = observer<IMenuProps, React.ButtonHTMLAttributes<any>>(functi
|
||||
menu={menu}
|
||||
style={style}
|
||||
rtl={rtl}
|
||||
panelAvailable={panelAvailable}
|
||||
hasBindings={hasBindings}
|
||||
getHasBindings={getHasBindings}
|
||||
>
|
||||
{items}
|
||||
</MenuPanel>
|
||||
@@ -72,7 +88,7 @@ export const Menu = observer<IMenuProps, React.ButtonHTMLAttributes<any>>(functi
|
||||
|
||||
return styled(styles)(
|
||||
<MenuStateContext.Provider value={menu}>
|
||||
<MenuButton {...menu} {...props}>
|
||||
<MenuButton ref={ref} {...menu} {...props}>
|
||||
<box>{children}</box>
|
||||
</MenuButton>
|
||||
<MenuPanel
|
||||
@@ -80,6 +96,9 @@ export const Menu = observer<IMenuProps, React.ButtonHTMLAttributes<any>>(functi
|
||||
menu={menu}
|
||||
style={style}
|
||||
rtl={rtl}
|
||||
panelAvailable={panelAvailable}
|
||||
hasBindings={hasBindings}
|
||||
getHasBindings={getHasBindings}
|
||||
>
|
||||
{items}
|
||||
</MenuPanel>
|
||||
|
||||
@@ -13,6 +13,7 @@ import styled, { use } from 'reshadow';
|
||||
|
||||
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { getComputed } from '../getComputed';
|
||||
import { MenuEmptyItem } from './MenuEmptyItem';
|
||||
import { menuPanelStyles } from './menuPanelStyles';
|
||||
|
||||
@@ -22,18 +23,23 @@ export interface IMenuPanelProps {
|
||||
menu: MenuStateReturn; // from reakit useMenuState
|
||||
panelAvailable?: boolean;
|
||||
hasBindings?: boolean;
|
||||
getHasBindings?: () => boolean;
|
||||
children: React.ReactNode | (() => React.ReactNode);
|
||||
rtl?: boolean;
|
||||
style?: ComponentStyle;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MenuPanel = observer<React.PropsWithChildren<IMenuPanelProps>>(function MenuPanel({
|
||||
export const MenuPanel = observer<IMenuPanelProps>(function MenuPanel({
|
||||
label,
|
||||
menu,
|
||||
panelAvailable = true,
|
||||
hasBindings,
|
||||
rtl,
|
||||
getHasBindings,
|
||||
hasBindings,
|
||||
children,
|
||||
style,
|
||||
className,
|
||||
}) {
|
||||
const styles = useStyles(menuPanelStyles, style);
|
||||
const visible = menu.visible;
|
||||
@@ -42,13 +48,23 @@ export const MenuPanel = observer<React.PropsWithChildren<IMenuPanelProps>>(func
|
||||
return null;
|
||||
}
|
||||
|
||||
hasBindings = panelAvailable && (hasBindings || getComputed(() => getHasBindings?.()));
|
||||
|
||||
let renderedChildren: React.ReactNode = <></>;
|
||||
|
||||
if (panelAvailable) {
|
||||
renderedChildren = typeof children === 'function'
|
||||
? children()
|
||||
: children;
|
||||
}
|
||||
|
||||
return styled(styles)(
|
||||
<Menu {...menu} aria-label={label} visible={panelAvailable}>
|
||||
<Menu {...menu} aria-label={label} className={className} visible={panelAvailable}>
|
||||
<menu-box dir={rtl ? 'rtl' : undefined} {...use({ hasBindings })}>
|
||||
{Children.count(children) === 0 && (
|
||||
{Children.count(renderedChildren) === 0 && (
|
||||
<MenuEmptyItem style={style} />
|
||||
)}
|
||||
{children}
|
||||
{renderedChildren}
|
||||
</menu-box>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -9,4 +9,6 @@
|
||||
import { createContext } from 'react';
|
||||
import type { MenuStateReturn } from 'reakit/ts';
|
||||
|
||||
export const MenuStateContext = createContext<MenuStateReturn | null>(null);
|
||||
export type IMenuState = MenuStateReturn;
|
||||
|
||||
export const MenuStateContext = createContext<IMenuState | null>(null);
|
||||
@@ -7,18 +7,16 @@
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { ButtonHTMLAttributes, useCallback, useEffect } from 'react';
|
||||
import { MenuButton, MenuInitialState, useMenuState } from 'reakit/Menu';
|
||||
import { ButtonHTMLAttributes, useRef } from 'react';
|
||||
import type { MenuInitialState } from 'reakit/Menu';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { getComputed, useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import { getComputed, IMenuState, Menu, menuPanelStyles, useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
import { IMenuData, MenuService } from '@cloudbeaver/core-view';
|
||||
import { IMenuData, MenuActionItem, MenuService } from '@cloudbeaver/core-view';
|
||||
|
||||
import { MenuItemRenderer } from './MenuItemRenderer';
|
||||
import { MenuPanel } from './MenuPanel';
|
||||
import { menuPanelStyles } from './menuPanelStyles';
|
||||
|
||||
interface IMenuProps extends React.PropsWithChildren {
|
||||
loading: boolean;
|
||||
@@ -39,7 +37,7 @@ interface IContextMenuProps extends Omit<ButtonHTMLAttributes<any>, 'style' | 'c
|
||||
onVisibleSwitch?: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export const ContextMenu = observer<IContextMenuProps, ButtonHTMLAttributes<any>>(function ContextMenu({
|
||||
export const ContextMenu = observer<IContextMenuProps, HTMLButtonElement>(function ContextMenu({
|
||||
menu: menuData,
|
||||
disclosure,
|
||||
children,
|
||||
@@ -59,80 +57,63 @@ export const ContextMenu = observer<IContextMenuProps, ButtonHTMLAttributes<any>
|
||||
const disabled = getComputed(() => loading || handler?.isDisabled?.(menuData.context) || false);
|
||||
const lazy = getComputed(() => !menuData.available || hidden);
|
||||
|
||||
const propsRef = useObjectRef({ onVisibleSwitch, visible });
|
||||
const menu = useMenuState({ modal, placement, visible, rtl });
|
||||
const menu = useRef<IMenuState>();
|
||||
const styles = useStyles(menuPanelStyles, style);
|
||||
|
||||
const handleItemClose = useCallback(() => {
|
||||
menu.hide();
|
||||
}, [menu]);
|
||||
const handlers = useObjectRef(() => ({
|
||||
handleItemClose() {
|
||||
menu.current?.hide();
|
||||
},
|
||||
hasBindings() {
|
||||
return this.menuData.items.some(
|
||||
item => item instanceof MenuActionItem && item.action.binding !== null
|
||||
);
|
||||
},
|
||||
handleVisibleSwitch(visible: boolean) {
|
||||
this.onVisibleSwitch?.(visible);
|
||||
|
||||
useEffect(() => {
|
||||
propsRef.onVisibleSwitch?.(menu.visible);
|
||||
|
||||
if (menu.visible) {
|
||||
handler?.handler?.(menuData.context);
|
||||
}
|
||||
}, [menu.visible]);
|
||||
if (visible) {
|
||||
this.handler?.handler?.(this.menuData.context);
|
||||
}
|
||||
},
|
||||
}), { menuData, handler, onVisibleSwitch }, ['handleItemClose', 'hasBindings', 'handleVisibleSwitch']);
|
||||
|
||||
if (lazy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: fix type error
|
||||
const renderingChildren: React.ReactNode = typeof children === 'function'
|
||||
? (children as any)({ loading, disabled })
|
||||
? children({ loading, disabled })
|
||||
: children;
|
||||
|
||||
if (React.isValidElement(renderingChildren) && disclosure) {
|
||||
return styled(styles)(
|
||||
<>
|
||||
<MenuButton ref={ref} {...menu} {...props} {...renderingChildren.props} disabled={disabled}>
|
||||
{disclosureProps => React.cloneElement(renderingChildren, { ...disclosureProps, ...renderingChildren.props })}
|
||||
</MenuButton>
|
||||
<MenuPanel
|
||||
menuData={menuData}
|
||||
menu={menu}
|
||||
style={style}
|
||||
rtl={rtl}
|
||||
>
|
||||
{item => (
|
||||
<MenuItemRenderer
|
||||
key={item.id}
|
||||
item={item}
|
||||
menuData={menuData}
|
||||
menu={menu}
|
||||
style={style}
|
||||
onItemClose={handleItemClose}
|
||||
/>
|
||||
)}
|
||||
</MenuPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return styled(styles)(
|
||||
<>
|
||||
<MenuButton {...menu} {...props} disabled={disabled}>
|
||||
<box>{renderingChildren}</box>
|
||||
</MenuButton>
|
||||
<MenuPanel
|
||||
menuData={menuData}
|
||||
menu={menu}
|
||||
style={style}
|
||||
rtl={rtl}
|
||||
>
|
||||
{item => (
|
||||
<MenuItemRenderer
|
||||
key={item.id}
|
||||
item={item}
|
||||
menuData={menuData}
|
||||
menu={menu}
|
||||
style={style}
|
||||
onItemClose={handleItemClose}
|
||||
/>
|
||||
)}
|
||||
</MenuPanel>
|
||||
</>
|
||||
<Menu
|
||||
ref={ref}
|
||||
label={menuData.menu.label}
|
||||
items={() => menuData.items.map(item => menu.current && (
|
||||
<MenuItemRenderer
|
||||
key={item.id}
|
||||
item={item}
|
||||
menuData={menuData}
|
||||
rtl={rtl}
|
||||
menu={menu.current}
|
||||
style={style}
|
||||
onItemClose={handlers.handleItemClose}
|
||||
/>
|
||||
))}
|
||||
rtl={rtl}
|
||||
menuRef={menu}
|
||||
modal={modal}
|
||||
visible={visible}
|
||||
placement={placement}
|
||||
disabled={disabled}
|
||||
disclosure={disclosure}
|
||||
style={style}
|
||||
getHasBindings={handlers.hasBindings}
|
||||
onVisibleSwitch={handlers.handleVisibleSwitch}
|
||||
{...props}
|
||||
>
|
||||
{renderingChildren}
|
||||
</Menu>
|
||||
);
|
||||
}, { forwardRef: true });
|
||||
|
||||
@@ -11,13 +11,10 @@ import React from 'react';
|
||||
import { MenuItem, MenuItemCheckbox, MenuItemRadio, MenuStateReturn } from 'reakit/Menu';
|
||||
import styled, { use } from 'reshadow';
|
||||
|
||||
import { Checkbox, Radio } from '@cloudbeaver/core-blocks';
|
||||
import { Checkbox, MenuItemElement, menuPanelStyles, Radio } from '@cloudbeaver/core-blocks';
|
||||
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
import type { IMenuActionItem } from '@cloudbeaver/core-view';
|
||||
|
||||
import { MenuItemElement } from './MenuItemElement';
|
||||
import { menuPanelStyles } from './menuPanelStyles';
|
||||
|
||||
interface IMenuActionElementProps {
|
||||
item: IMenuActionItem;
|
||||
menu: MenuStateReturn;
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2022 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { MenuItem, MenuStateReturn } from 'reakit/Menu';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { MenuItemElement } from './MenuItemElement';
|
||||
import { menuPanelStyles } from './menuPanelStyles';
|
||||
|
||||
|
||||
interface Props extends Omit<React.ButtonHTMLAttributes<any>, 'style'> {
|
||||
menu: MenuStateReturn; // from reakit useMenuState
|
||||
style?: ComponentStyle;
|
||||
}
|
||||
|
||||
export const MenuEmptyItem = observer<Props>(function MenuEmptyItem({
|
||||
menu, style,
|
||||
}) {
|
||||
const styles = useStyles(menuPanelStyles, style);
|
||||
const translate = useTranslate();
|
||||
|
||||
const label = translate('core_ui_empty');
|
||||
|
||||
return styled(styles)(
|
||||
<MenuItem
|
||||
{...menu}
|
||||
id='empty'
|
||||
aria-label={label}
|
||||
disabled
|
||||
>
|
||||
<MenuItemElement label={label} style={style} />
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2022 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { Icon, IconOrImage, Loader, useStateDelay } from '@cloudbeaver/core-blocks';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { useStyles, ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { menuPanelStyles } from './menuPanelStyles';
|
||||
|
||||
interface IMenuItemElementProps {
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
binding?: string;
|
||||
icon?: string | React.ReactNode;
|
||||
menu?: boolean;
|
||||
loading?: boolean;
|
||||
style?: ComponentStyle;
|
||||
}
|
||||
|
||||
export const MenuItemElement = observer<IMenuItemElementProps>(function MenuItemElement({
|
||||
label,
|
||||
tooltip,
|
||||
binding,
|
||||
icon,
|
||||
menu,
|
||||
loading = false,
|
||||
style = [],
|
||||
}) {
|
||||
const translate = useTranslate();
|
||||
|
||||
const title = translate(label);
|
||||
loading = useStateDelay(loading, 100);
|
||||
|
||||
return styled(useStyles(menuPanelStyles, style))(
|
||||
<menu-panel-item title={tooltip ? translate(tooltip) : title}>
|
||||
<menu-item-content>
|
||||
{typeof icon === 'string' ? <IconOrImage icon={icon} /> : icon}
|
||||
</menu-item-content>
|
||||
<menu-item-text title={title}>
|
||||
{title}
|
||||
</menu-item-text>
|
||||
<menu-item-binding title={binding}>
|
||||
{binding}
|
||||
</menu-item-binding>
|
||||
<menu-item-content>
|
||||
{loading && <Loader small fullSize />}
|
||||
{menu && !loading && <Icon name="arrow" viewBox="0 0 16 16" />}
|
||||
</menu-item-content>
|
||||
</menu-panel-item>
|
||||
|
||||
);
|
||||
});
|
||||
@@ -11,13 +11,11 @@ import React, { useCallback } from 'react';
|
||||
import { MenuItem, MenuSeparator, MenuStateReturn } from 'reakit/Menu';
|
||||
import styled, { use } from 'reshadow';
|
||||
|
||||
import { MenuItemElement, menuPanelStyles } from '@cloudbeaver/core-blocks';
|
||||
import { ComponentStyle, joinStyles, useStyles } from '@cloudbeaver/core-theming';
|
||||
import { IMenuItem, IMenuData, MenuSubMenuItem, MenuSeparatorItem, MenuActionItem, MenuBaseItem } from '@cloudbeaver/core-view';
|
||||
|
||||
import { MenuActionElement } from './MenuActionElement';
|
||||
import { MenuItemElement } from './MenuItemElement';
|
||||
import { MenuPanel } from './MenuPanel';
|
||||
import { menuPanelStyles } from './menuPanelStyles';
|
||||
import { SubMenuElement } from './SubMenuElement';
|
||||
|
||||
|
||||
@@ -25,12 +23,13 @@ export interface IMenuItemRendererProps extends Omit<React.ButtonHTMLAttributes<
|
||||
item: IMenuItem;
|
||||
menuData: IMenuData;
|
||||
menu: MenuStateReturn; // from reakit useMenuState
|
||||
rtl?: boolean;
|
||||
onItemClose?: () => void;
|
||||
style?: ComponentStyle;
|
||||
}
|
||||
|
||||
export const MenuItemRenderer = observer<IMenuItemRendererProps>(function MenuItemRenderer({
|
||||
item, menuData, menu, onItemClose, style,
|
||||
item, rtl, menuData, menu, onItemClose, style,
|
||||
}) {
|
||||
const styles = useStyles(menuPanelStyles, style);
|
||||
const onClick = useCallback(() => {
|
||||
@@ -49,7 +48,7 @@ export const MenuItemRenderer = observer<IMenuItemRendererProps>(function MenuIt
|
||||
id={item.id}
|
||||
aria-label={item.menu.label}
|
||||
itemRenderer={MenuItemRenderer}
|
||||
menuPanel={MenuPanel}
|
||||
rtl={rtl}
|
||||
menuData={menuData}
|
||||
subMenu={item}
|
||||
style={style}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2022 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Menu, MenuStateReturn } from 'reakit/Menu';
|
||||
import styled, { use } from 'reshadow';
|
||||
|
||||
import { getComputed } from '@cloudbeaver/core-blocks';
|
||||
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
import { IMenuData, IMenuItem, MenuActionItem } from '@cloudbeaver/core-view';
|
||||
|
||||
import { MenuEmptyItem } from './MenuEmptyItem';
|
||||
import { menuPanelStyles } from './menuPanelStyles';
|
||||
|
||||
|
||||
export interface IMenuPanelProps {
|
||||
menuData: IMenuData;
|
||||
menu: MenuStateReturn; // from reakit useMenuState
|
||||
children: (item: IMenuItem) => React.ReactNode;
|
||||
panelAvailable?: boolean;
|
||||
rtl?: boolean;
|
||||
style?: ComponentStyle;
|
||||
}
|
||||
|
||||
export const MenuPanel = observer<IMenuPanelProps>(function MenuPanel({
|
||||
menuData,
|
||||
menu,
|
||||
panelAvailable = true,
|
||||
rtl,
|
||||
children,
|
||||
style,
|
||||
}) {
|
||||
const styles = useStyles(menuPanelStyles, style);
|
||||
const visible = menu.visible;
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let items: IMenuItem[] = [];
|
||||
|
||||
if (panelAvailable) {
|
||||
items = menuData.items;
|
||||
}
|
||||
|
||||
const hasBindings = getComputed(() => items.some(
|
||||
item => item instanceof MenuActionItem && item.action.binding !== null
|
||||
));
|
||||
|
||||
return styled(styles)(
|
||||
<Menu {...menu} aria-label={menuData.menu.label} visible={panelAvailable}>
|
||||
<menu-box dir={rtl ? 'rtl' : undefined} {...use({ hasBindings })}>
|
||||
{items.length === 0 && (
|
||||
<MenuEmptyItem menu={menu} style={style} />
|
||||
)}
|
||||
{items.map(item => children(item))}
|
||||
</menu-box>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
@@ -7,25 +7,21 @@
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { MenuButton, useMenuState } from 'reakit/Menu';
|
||||
import { useRef } from 'react';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { getComputed } from '@cloudbeaver/core-blocks';
|
||||
import { getComputed, IMenuState, Menu, MenuItemElement, menuPanelStyles, useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
|
||||
import { DATA_CONTEXT_MENU_NESTED, IMenuData, IMenuSubMenuItem, MenuService, useMenu } from '@cloudbeaver/core-view';
|
||||
import { DATA_CONTEXT_MENU_NESTED, IMenuData, IMenuSubMenuItem, MenuActionItem, MenuService, useMenu } from '@cloudbeaver/core-view';
|
||||
|
||||
import { MenuItemElement } from './MenuItemElement';
|
||||
import type { IMenuItemRendererProps } from './MenuItemRenderer';
|
||||
import type { IMenuPanelProps } from './MenuPanel';
|
||||
import { menuPanelStyles } from './menuPanelStyles';
|
||||
|
||||
interface ISubMenuElementProps extends Omit<React.ButtonHTMLAttributes<any>, 'style'> {
|
||||
menuData: IMenuData;
|
||||
subMenu: IMenuSubMenuItem;
|
||||
itemRenderer: React.FC<IMenuItemRendererProps>;
|
||||
menuPanel: React.FC<IMenuPanelProps>;
|
||||
rtl?: boolean;
|
||||
onItemClose?: () => void;
|
||||
style?: ComponentStyle;
|
||||
}
|
||||
@@ -35,15 +31,15 @@ export const SubMenuElement = observer<ISubMenuElementProps, HTMLButtonElement>(
|
||||
menuData,
|
||||
subMenu,
|
||||
itemRenderer,
|
||||
menuPanel,
|
||||
style,
|
||||
rtl,
|
||||
onItemClose,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const menuService = useService(MenuService);
|
||||
const menu = useMenuState();
|
||||
const menu = useRef<IMenuState>();
|
||||
const styles = useStyles(menuPanelStyles, style);
|
||||
const subMenuData = useMenu({ menu: subMenu.menu, context: menuData.context });
|
||||
subMenuData.context.set(DATA_CONTEXT_MENU_NESTED, true);
|
||||
@@ -51,17 +47,23 @@ export const SubMenuElement = observer<ISubMenuElementProps, HTMLButtonElement>(
|
||||
const handler = menuService.getHandler(subMenuData.context);
|
||||
const hidden = getComputed(() => handler?.isHidden?.(subMenuData.context));
|
||||
|
||||
const handleItemClose = useCallback(() => {
|
||||
menu.hide();
|
||||
onItemClose?.();
|
||||
}, [menu, onItemClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (menu.visible) {
|
||||
subMenu.events?.onOpen?.();
|
||||
handler?.handler?.(subMenuData.context);
|
||||
}
|
||||
}, [menu.visible]);
|
||||
const handlers = useObjectRef(() => ({
|
||||
handleItemClose() {
|
||||
menu.current?.hide();
|
||||
this.onItemClose?.();
|
||||
},
|
||||
hasBindings() {
|
||||
return this.subMenuData.items.some(
|
||||
item => item instanceof MenuActionItem && item.action.binding !== null
|
||||
);
|
||||
},
|
||||
handleVisibleSwitch(visible: boolean) {
|
||||
if (visible) {
|
||||
this.subMenu.events?.onOpen?.();
|
||||
this.handler?.handler?.(this.subMenuData.context);
|
||||
}
|
||||
},
|
||||
}), { subMenuData, menuData, handler, subMenu, onItemClose }, ['handleItemClose', 'hasBindings', 'handleVisibleSwitch']);
|
||||
|
||||
if (hidden) {
|
||||
return null;
|
||||
@@ -69,40 +71,39 @@ export const SubMenuElement = observer<ISubMenuElementProps, HTMLButtonElement>(
|
||||
|
||||
const loading = getComputed(() => handler?.isLoading?.(subMenuData.context));
|
||||
const disabled = getComputed(() => handler?.isDisabled?.(subMenuData.context));
|
||||
const MenuPanel = menuPanel;
|
||||
const MenuItemRenderer = itemRenderer;
|
||||
|
||||
return styled(styles)(
|
||||
<>
|
||||
<MenuButton ref={ref} {...menu} {...rest} disabled={disabled}>
|
||||
<box>
|
||||
<MenuItemElement
|
||||
label={subMenu.menu.label}
|
||||
icon={subMenu.menu.icon}
|
||||
tooltip={subMenu.menu.tooltip}
|
||||
loading={loading}
|
||||
style={style}
|
||||
menu
|
||||
/>
|
||||
</box>
|
||||
</MenuButton>
|
||||
<MenuPanel
|
||||
menuData={subMenuData}
|
||||
menu={menu}
|
||||
<Menu
|
||||
ref={ref}
|
||||
menuRef={menu}
|
||||
label={subMenuData.menu.label}
|
||||
items={() => subMenuData.items.map(item => menu.current && (
|
||||
<MenuItemRenderer
|
||||
key={item.id}
|
||||
item={item}
|
||||
menuData={subMenuData}
|
||||
menu={menu.current}
|
||||
style={style}
|
||||
onItemClose={handlers.handleItemClose}
|
||||
/>
|
||||
))}
|
||||
rtl={rtl}
|
||||
style={style}
|
||||
panelAvailable={subMenuData.available && !loading}
|
||||
disabled={disabled}
|
||||
getHasBindings={handlers.hasBindings}
|
||||
onVisibleSwitch={handlers.handleVisibleSwitch}
|
||||
{...rest}
|
||||
>
|
||||
<MenuItemElement
|
||||
label={subMenu.menu.label}
|
||||
icon={subMenu.menu.icon}
|
||||
tooltip={subMenu.menu.tooltip}
|
||||
loading={loading}
|
||||
style={style}
|
||||
panelAvailable={subMenuData.available && !loading}
|
||||
>
|
||||
{item => (
|
||||
<MenuItemRenderer
|
||||
key={item.id}
|
||||
item={item}
|
||||
menuData={menuData}
|
||||
menu={menu}
|
||||
style={style}
|
||||
onItemClose={handleItemClose}
|
||||
/>
|
||||
)}
|
||||
</MenuPanel>
|
||||
</>
|
||||
menu
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
}, { forwardRef: true });
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2022 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { css } from 'reshadow';
|
||||
|
||||
export const menuPanelStyles = css`
|
||||
menu-box {
|
||||
composes: theme-background-surface theme-text-on-surface from global;
|
||||
}
|
||||
MenuItem,
|
||||
MenuItemCheckbox,
|
||||
MenuItemRadio,
|
||||
MenuButton,
|
||||
MenuItemElement {
|
||||
composes: theme-ripple from global;
|
||||
}
|
||||
menu-panel-item, MenuSeparator {
|
||||
composes: theme-border-color-background from global;
|
||||
}
|
||||
MenuButton {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none !important;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
& box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
Menu {
|
||||
outline: none;
|
||||
z-index: 999;
|
||||
}
|
||||
menu-box {
|
||||
composes: theme-typography--body2 theme-elevation-z5 from global;
|
||||
min-width: 140px;
|
||||
padding: 12px 0;
|
||||
|
||||
& menu-box {
|
||||
margin-top: -12px;
|
||||
}
|
||||
}
|
||||
|
||||
menu-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:not([|hasBindings]) menu-item-binding {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem, MenuItemCheckbox, MenuItemRadio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: none;
|
||||
text-align: left;
|
||||
outline: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&[use|hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover, &:global([aria-expanded="true"]) {
|
||||
& Icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin: 4px 0;
|
||||
border: none !important;
|
||||
border-bottom: 1px solid !important;
|
||||
}
|
||||
|
||||
menu-item-binding {
|
||||
composes: theme-typography--caption from global;
|
||||
}
|
||||
|
||||
menu-panel-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
padding: 0 4px;
|
||||
& menu-item-text, & menu-item-binding {
|
||||
display: block;
|
||||
padding: 0 4px;
|
||||
}
|
||||
& menu-item-binding {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
padding-left: 32px;
|
||||
width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
& menu-item-content {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
& Icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.5;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
& IconOrImage {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
& Loader {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,7 +1,6 @@
|
||||
export * from './Clipboard/ClipboardBootstrap';
|
||||
export * from './Clipboard/ClipboardService';
|
||||
|
||||
export * from './ContextMenu/MenuItemElement';
|
||||
export * from './ContextMenu/ContextMenu';
|
||||
export * from './ContextMenu/MenuBar/MenuBar';
|
||||
export * from './ContextMenu/MenuBar/MENU_BAR_DEFAULT_STYLES';
|
||||
|
||||
Reference in New Issue
Block a user