This commit is contained in:
sergeyteleshev
2025-09-19 15:20:54 +02:00
parent ded20a5192
commit af9a13e70e
8 changed files with 95 additions and 96 deletions
@@ -86,8 +86,6 @@
}
.dbv-kit-menu__separator {
height: 1px;
margin: 0.25rem 0;
background-color: var(--dbv-kit-menu-separator-color);
}
@@ -20,8 +20,8 @@
--dbv-kit-menu-popover-shadow: var(--tw-shadow-md);
--dbv-kit-menu-popover-max-height: 18rem;
--dbv-kit-menu-item-padding-inline: calc(var(--tw-spacing) * 3);
--dbv-kit-menu-item-padding-block: var(--tw-spacing);
--dbv-kit-menu-item-padding-inline: calc(var(--tw-spacing) * 5);
--dbv-kit-menu-item-padding-block: calc(var(--tw-spacing) * 1.4);
--dbv-kit-menu-item-hover-background: var(--tw-color-gray-200);
--dbv-kit-menu-item-hover-foreground: var(--dbv-kit-menu-foreground);
--dbv-kit-menu-item-selected-background: var(--dbv-kit-color-primary-100);
@@ -5,7 +5,7 @@
}
.dbv-kit-menu__button {
--dbv-kit-menu-background: var(--theme-input-background);
--dbv-kit-menu-foreground: var(--theme-input-color);
--dbv-kit-menu-background: transparent;
--dbv-kit-menu-foreground: inherit;
}
}
@@ -13,7 +13,7 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { forwardRef, useEffect } from 'react';
import { useEffect } from 'react';
import { MenuField, useMenuStore, useStoreState, type MenuFieldProps, type MenuStoreProps } from '@dbeaver/ui-kit';
import { ErrorBoundary } from '../../ErrorBoundary.js';
@@ -25,33 +25,30 @@ export interface IMenuAriaKitProps<Item> extends Omit<MenuFieldProps<Item>, 'sto
menuStoreProps?: MenuStoreProps;
}
export const MenuAriaKit = observer(
forwardRef(function MenuAriaKit<Item>({ contextMenuPosition, menuStoreProps, ...props }: IMenuAriaKitProps<Item>) {
const store = useMenuStore(menuStoreProps);
const storeState = useStoreState(store);
const menuVisible = !!storeState.open;
export const MenuAriaKit = observer(function MenuAriaKit<Item>({ contextMenuPosition, menuStoreProps, ...props }: IMenuAriaKitProps<Item>) {
const store = useMenuStore(menuStoreProps);
const storeState = useStoreState(store);
const menuVisible = !!storeState.open;
function getAnchorRect() {
if (!contextMenuPosition?.position) {
return;
}
return () => contextMenuPosition.position;
function getAnchorRect() {
if (!contextMenuPosition?.position) {
return;
}
useEffect(() => {
if (!contextMenuPosition?.position || menuVisible) {
return;
}
return () => contextMenuPosition.position;
}
store.show();
contextMenuPosition.position = null;
}, [contextMenuPosition?.position]);
useEffect(() => {
if (!contextMenuPosition?.position || menuVisible) {
return;
}
return (
<ErrorBoundary>
<MenuField store={store} getAnchorRect={getAnchorRect()} {...props} />
</ErrorBoundary>
);
}),
) as <Item>(props: IMenuAriaKitProps<Item> & React.RefAttributes<HTMLButtonElement>) => React.ReactElement;
store.show();
}, [contextMenuPosition?.position]);
return (
<ErrorBoundary>
<MenuField store={store} getAnchorRect={getAnchorRect()} {...props} />
</ErrorBoundary>
);
}) as <Item>(props: IMenuAriaKitProps<Item> & React.RefAttributes<HTMLButtonElement>) => React.ReactElement;
@@ -7,7 +7,6 @@
*/
import { getComputed, MenuAriaKit, useAutoLoad, useObjectRef, useTranslate, type IMenuAriaKitProps } from '@cloudbeaver/core-blocks';
import { observer } from 'mobx-react-lite';
import { forwardRef } from 'react';
import { MenuActionItem, type IMenuData, type IMenuItem } from '@cloudbeaver/core-view';
import { MenuItemRendererAriaKit } from './MenuItemRendererAriaKit.js';
@@ -16,50 +15,52 @@ export interface IContextMenuAriaKitProps extends Omit<IMenuAriaKitProps<IMenuIt
menu: IMenuData;
}
export const ContextMenuAriaKit = observer<IContextMenuAriaKitProps, HTMLButtonElement>(
forwardRef(function ContextMenuAriaKit({ contextMenuPosition, menu: menuData, onSwitch, ...props }, ref) {
const translate = useTranslate();
export const ContextMenuAriaKit = observer<IContextMenuAriaKitProps>(function ContextMenuAriaKit({
contextMenuPosition,
menu: menuData,
onSwitch,
...props
}) {
const translate = useTranslate();
const handler = menuData.handler;
const hidden = getComputed(() => handler?.isHidden?.(menuData.context) || false);
const loading = getComputed(() => handler?.isLoading?.(menuData.context) || menuData.loaders.some(loader => loader.isLoading()) || false);
const disabled = getComputed(() => loading || handler?.isDisabled?.(menuData.context) || false);
const lazy = getComputed(() => !menuData.available || hidden);
const handler = menuData.handler;
const hidden = getComputed(() => handler?.isHidden?.(menuData.context) || false);
const loading = getComputed(() => handler?.isLoading?.(menuData.context) || menuData.loaders.some(loader => loader.isLoading()) || false);
const disabled = getComputed(() => loading || handler?.isDisabled?.(menuData.context) || false);
const lazy = getComputed(() => !menuData.available || hidden);
useAutoLoad({ name: `${ContextMenuAriaKit.name}(${menuData.menu.id})` }, menuData.loaders, !lazy, lazy, true);
useAutoLoad({ name: `${ContextMenuAriaKit.name}(${menuData.menu.id})` }, menuData.loaders, !lazy, lazy, true);
const handlers = useObjectRef(
() => ({
hasBindings() {
return this.menuData.items.some(item => item instanceof MenuActionItem && item.action.binding !== null);
},
handleVisibleSwitch(visible: boolean) {
this.onSwitch?.(visible);
const handlers = useObjectRef(
() => ({
hasBindings() {
return this.menuData.items.some(item => item instanceof MenuActionItem && item.action.binding !== null);
},
handleVisibleSwitch(visible: boolean) {
this.onSwitch?.(visible);
if (visible) {
this.handler?.handler?.(this.menuData.context);
}
},
}),
{ menuData, handler, onSwitch },
['hasBindings', 'handleVisibleSwitch'],
);
if (visible) {
this.handler?.handler?.(this.menuData.context);
}
},
}),
{ menuData, handler, onSwitch },
['hasBindings', 'handleVisibleSwitch'],
);
if (lazy) {
return null;
}
if (lazy) {
return null;
}
return (
<MenuAriaKit
{...props}
ref={ref}
aria-label={translate(menuData.menu.info.label)}
contextMenuPosition={contextMenuPosition}
disabled={disabled}
items={menuData.items}
itemRender={item => <MenuItemRendererAriaKit key={item.id} item={item} menuData={menuData} />}
onSwitch={handlers.handleVisibleSwitch}
/>
);
}),
);
return (
<MenuAriaKit
{...props}
aria-label={translate(menuData.menu.info.label)}
contextMenuPosition={contextMenuPosition}
disabled={disabled}
items={menuData.items}
itemRender={item => <MenuItemRendererAriaKit key={item.id} item={item} menuData={menuData} />}
onSwitch={handlers.handleVisibleSwitch}
/>
);
});
@@ -13,6 +13,7 @@ import {
type IMenuData,
type IMenuItem,
isMenuCustomItem,
MenuActionItem,
MenuBaseItem,
MenuCheckboxItem,
MenuRadioItem,
@@ -65,12 +66,28 @@ export const MenuItemRendererAriaKit = observer<IMenuItemRendererAriaKitProps>(f
return <AriaMenuSeparator />;
}
if (item instanceof MenuActionItem) {
return (
<AriaMenuItem hidden={item.hidden} disabled={item.disabled} title={item.action.actionInfo.tooltip} onClick={() => onClick()}>
{translate(item.action.actionInfo.label)}
</AriaMenuItem>
);
}
if (item instanceof MenuBaseItem) {
const IconComponent = item.iconComponent?.();
const extraProps = item.getExtraProps?.();
return (
<AriaMenuItem aria-label={translate(item.label)} hidden={item.hidden} disabled={item.disabled} onClick={() => onClick()}>
<AriaMenuItem
id={item.id}
aria-label={translate(item.label)}
hidden={item.hidden}
disabled={item.disabled}
style={{ pointerEvents: 'auto' }}
focusable
onClick={() => onClick()}
>
{IconComponent ? <IconComponent item={item} {...extraProps} /> : item.icon}
{translate(item.label)}
</AriaMenuItem>
@@ -81,6 +98,7 @@ export const MenuItemRendererAriaKit = observer<IMenuItemRendererAriaKitProps>(f
return (
<AriaMenuItemCheckbox
hidden={item.hidden}
id={item.id}
aria-label={translate(item.label)}
disabled={item.disabled}
name={item.id}
@@ -97,6 +115,7 @@ export const MenuItemRendererAriaKit = observer<IMenuItemRendererAriaKitProps>(f
return (
<AriaMenuItemRadio
hidden={item.hidden}
id={item.id}
aria-label={translate(item.label)}
disabled={item.disabled}
name={item.id}
@@ -109,17 +128,5 @@ export const MenuItemRendererAriaKit = observer<IMenuItemRendererAriaKitProps>(f
);
}
if (item instanceof MenuBaseItem) {
const IconComponent = item.iconComponent?.();
const extraProps = item.getExtraProps?.();
return (
<AriaMenuItem aria-label={translate(item.label)} hidden={item.hidden} disabled={item.disabled} onClick={() => onClick()}>
{IconComponent ? <IconComponent item={item} {...extraProps} /> : item.icon}
{translate(item.label)}
</AriaMenuItem>
);
}
return null;
});
@@ -7,8 +7,6 @@
*/
.contextMenu {
padding: 0;
height: 16px;
width: 16px;
&:before {
display: none;
@@ -12,7 +12,7 @@ import { ConnectionInfoResource, DATA_CONTEXT_CONNECTION } from '@cloudbeaver/co
import { useDataContextLink } from '@cloudbeaver/core-data-context';
import { useService } from '@cloudbeaver/core-di';
import { DATA_CONTEXT_NAV_NODE, type INodeActions, type NavNode } from '@cloudbeaver/core-navigation-tree';
import { ContextMenu } from '@cloudbeaver/core-ui';
import { ContextMenuAriaKit } from '@cloudbeaver/core-ui';
import { useMenu } from '@cloudbeaver/core-view';
import { MENU_NAV_TREE } from '../../MENU_NAV_TREE.js';
@@ -49,14 +49,12 @@ export const TreeNodeMenu = observer<TreeNodeMenuProps>(function TreeNodeMenu({
}
return (
<ContextMenu
<ContextMenuAriaKit
menu={menu}
className={s(styles, { contextMenu: true, selected })}
contextMenuPosition={contextMenuPosition}
modal
onVisibleSwitch={handleVisibleSwitch}
>
<Icon className={s(styles, { icon: true })} name="snack" viewBox="0 0 16 10" />
</ContextMenu>
buttonElement={<Icon className={s(styles, { icon: true })} name="snack" viewBox="0 0 16 10" />}
onSwitch={handleVisibleSwitch}
/>
);
});