allows to render any items not only strings

This commit is contained in:
sergeyteleshev
2025-09-18 11:13:26 +02:00
parent a59c7aff5d
commit a04a6a36ed
3 changed files with 11 additions and 55 deletions
@@ -7,7 +7,7 @@
*/
import clsx from 'clsx';
import { MenuProvider, Menu, MenuButton, MenuItem, MenuButtonArrow, useMenuStore, useStoreState, type MenuProps } from './Menu.js';
import { MenuProvider, Menu, MenuButton, MenuButtonArrow, useMenuStore, useStoreState, type MenuProps } from './Menu.js';
import './MenuField.css';
import { useLayoutEffect } from 'react';
@@ -23,25 +23,11 @@ export interface MenuFieldProps<T, ItemType = MenuItemData<T>> {
/** Options array - can be MenuItemData objects or arbitrary objects */
items: ItemType[];
/**
* Function to extract value from items
* Example: (item) => item.id
*/
itemValue?: PropertyGetter<ItemType, T>;
/**
* Function to extract label or render content from items
* Example: (item) => item.firstName + ' ' + item.lastName
*/
itemRender?: PropertyGetter<ItemType, React.ReactNode>;
/**
* Function to extract disabled state
* Example: (item) => !item.isActive
*/
itemDisabled?: PropertyGetter<ItemType, boolean>;
onChange?: (value: T) => void;
itemRender: PropertyGetter<ItemType, React.ReactNode>;
onSwitch?: (open: boolean) => void;
@@ -75,22 +61,10 @@ export interface MenuFieldProps<T, ItemType = MenuItemData<T>> {
id?: string;
}
// Utility function to get value by it's key or using getter function
function getValueByPath<Item, Value>(item: Item, getter: PropertyGetter<Item, Value> | undefined, defaultGetter: (item: Item) => Value): Value {
return getter ? getter(item) : defaultGetter(item);
}
function isNonNullObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
export function MenuField<T, ItemType extends {} = MenuItemData<T>>({
items,
onChange,
itemValue,
onSwitch,
itemRender,
itemDisabled,
getAnchorRect,
label,
noItemsPlaceholder = 'No items',
@@ -104,21 +78,6 @@ export function MenuField<T, ItemType extends {} = MenuItemData<T>>({
}: MenuFieldProps<T, ItemType>): React.ReactElement {
const storeState = useStoreState(store);
const getItemValue = (item: ItemType): T =>
getValueByPath<ItemType, T>(item, itemValue, i =>
isNonNullObject(i) && 'value' in i ? (i as unknown as MenuItemData<T>).value : (i as unknown as T),
);
const renderItem = (item: ItemType): React.ReactNode =>
getValueByPath<ItemType, React.ReactNode>(item, itemRender, i =>
isNonNullObject(i) && 'label' in i ? (i as unknown as MenuItemData<T>).label : String(i),
);
const isItemDisabled = (item: ItemType): boolean =>
getValueByPath<ItemType, boolean>(item, itemDisabled, i =>
isNonNullObject(i) && 'disabled' in i ? Boolean((i as unknown as MenuItemData<T>).disabled) : false,
);
const isOpen = storeState?.open ?? false;
useLayoutEffect(() => {
@@ -134,15 +93,7 @@ export function MenuField<T, ItemType extends {} = MenuItemData<T>>({
</MenuButton>
{description && <span className="dbv-kit-menu__description">{description}</span>}
<Menu getAnchorRect={getAnchorRect}>
{items.length === 0 ? (
<div className="dbv-kit-menu__empty">{noItemsPlaceholder}</div>
) : (
items.map(item => (
<MenuItem key={String(getItemValue(item))} disabled={isItemDisabled(item)} onClick={() => onChange?.(getItemValue(item))}>
{renderItem(item)}
</MenuItem>
))
)}
{items.length === 0 ? <div className="dbv-kit-menu__empty">{noItemsPlaceholder}</div> : items.map(itemRender)}
</Menu>
</MenuProvider>
</div>
@@ -20,13 +20,13 @@ import { ErrorBoundary } from '../../ErrorBoundary.js';
import type { IContextMenuPosition } from '../useContextMenuPosition.js';
import './MenuAriaKit.css';
interface IMenuProps<Value extends string> extends MenuFieldProps<Value, MenuItemData<Value>> {
export interface IMenuAriaKitProps<Value = string> extends MenuFieldProps<Value, MenuItemData<Value>> {
contextMenuPosition?: IContextMenuPosition;
modal?: boolean;
menuStoreProps?: MenuStoreProps;
}
export const MenuAriaKit = observer<IMenuProps<string>>(
export const MenuAriaKit = observer<IMenuAriaKitProps>(
forwardRef(function MenuAriaKit({ contextMenuPosition, items, menuStoreProps, ...props }) {
const store = useMenuStore(menuStoreProps);
const storeState = useStoreState(store);
@@ -19,6 +19,7 @@ import { useS } from '../useS.js';
import type { IProperty } from './IProperty.js';
import classes from './PropertyItem.module.css';
import { MenuAriaKit } from '../Menu/AriaKit/MenuAriaKit.js';
import { Menu } from '@dbeaver/ui-kit';
interface Props {
property: IProperty;
@@ -105,7 +106,11 @@ export const PropertyItem = observer<Props>(function PropertyItem({ property, va
buttonElement={<Icon className={s(styles, { icon: true, focus })} name="arrow" viewBox="0 0 16 16" />}
className={s(styles, { propertyValueSelector: true })}
items={property.validValues.map(value => ({ label: value, value }))}
onChange={handleValueChange}
itemRender={item => (
<Menu.Item key={item.value} disabled={item.disabled} onClick={() => handleValueChange(item.value)}>
{item.label}
</Menu.Item>
)}
onSwitch={setMenuOpen}
/>
</div>