Skip to content
Open
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
8 changes: 4 additions & 4 deletions dev/pages/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import {
ContextMenu,
type ContextMenuItem,
type ContextMenuItemData,
type ContextMenuItemSelectedEvent,
} from '../../packages/react-components/src/ContextMenu.js';
import { Icon } from '../../packages/react-components/src/Icon.js';
Expand All @@ -25,7 +25,7 @@ function createItem(iconName: string, text: string) {
);
}

const initialItemSets: Record<string, ContextMenuItem[]> = {
const initialItemSets: Record<string, ContextMenuItemData[]> = {
basic: [{ text: 'View' }, { text: 'Edit' }, { text: 'Delete' }],
dividers: [{ text: 'View' }, { component: 'hr' }, { text: 'Edit' }, { text: 'Delete' }],
checkable: [
Expand Down Expand Up @@ -74,7 +74,7 @@ type OpenOnType = 'contextmenu' | 'click';

export default function ContextMenuPage() {
const [itemSetType, setItemSetType] = useState<ItemSetType>('basic');
const [items, setItems] = useState<ContextMenuItem[]>(initialItemSets.basic);
const [items, setItems] = useState<ContextMenuItemData[]>(initialItemSets.basic);
const [openOn, setOpenOn] = useState<OpenOnType>('contextmenu');
const [eventLog, setEventLog] = useState<string[]>([]);

Expand All @@ -92,7 +92,7 @@ export default function ContextMenuPage() {

if (itemSetType === 'checkable') {
// Radio-button like behavior: only one item can be checked.
const updateCheckedState = (currentItems: ContextMenuItem[]): ContextMenuItem[] => {
const updateCheckedState = (currentItems: ContextMenuItemData[]): ContextMenuItemData[] => {
return currentItems.map((item) => {
const newItem = { ...item };
if (item === selectedItem) {
Expand Down
10 changes: 10 additions & 0 deletions packages/react-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,14 @@
"types": "./ContextMenu.d.ts",
"default": "./ContextMenu.js"
},
"./ContextMenuItem.js": {
"types": "./ContextMenuItem.d.ts",
"default": "./ContextMenuItem.js"
},
"./ContextMenuListBox.js": {
"types": "./ContextMenuListBox.d.ts",
"default": "./ContextMenuListBox.js"
},
"./CustomField.js": {
"types": "./CustomField.d.ts",
"default": "./CustomField.js"
Expand Down Expand Up @@ -469,6 +477,8 @@
"./ComboBox": "./ComboBox.js",
"./ConfirmDialog": "./ConfirmDialog.js",
"./ContextMenu": "./ContextMenu.js",
"./ContextMenuItem": "./ContextMenuItem.js",
"./ContextMenuListBox": "./ContextMenuListBox.js",
"./CustomField": "./CustomField.js",
"./DatePicker": "./DatePicker.js",
"./DateTimePicker": "./DateTimePicker.js",
Expand Down
25 changes: 15 additions & 10 deletions packages/react-components/src/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
type ContextMenuRendererContext,
type ContextMenuElement,
type ContextMenuProps as _ContextMenuProps,
type ContextMenuItem as _ContextMenuItem,
type ContextMenuItemData as _ContextMenuItemData,
} from './generated/ContextMenu.js';
import { type ReactContextRendererProps, useContextRenderer } from './renderers/useContextRenderer.js';
import { getOriginalItem, mapItemsWithComponents } from './utils/mapItemsWithComponents.js';
Expand All @@ -13,23 +13,28 @@ export * from './generated/ContextMenu.js';

export type ContextMenuReactRendererProps = ReactContextRendererProps<ContextMenuRendererContext, ContextMenuElement>;

export type ContextMenuItem<TItemData extends object = object> = Omit<
_ContextMenuItem<TItemData>,
export type ContextMenuItemData<TItemData extends object = object> = Omit<
_ContextMenuItemData<TItemData>,
'component' | 'children'
> & {
component?: ReactElement | string;

children?: Array<ContextMenuItem<TItemData>>;
children?: Array<ContextMenuItemData<TItemData>>;
};

export type ContextMenuItemSelectedEvent<TItem extends ContextMenuItem = ContextMenuItem> = CustomEvent<{
value: ContextMenuItem<TItem>;
/**
* @deprecated Use `ContextMenuItemData` instead.
*/
export type ContextMenuItem<TItemData extends object = object> = ContextMenuItemData<TItemData>;

export type ContextMenuItemSelectedEvent<TItem extends ContextMenuItemData = ContextMenuItemData> = CustomEvent<{
value: ContextMenuItemData<TItem>;
}>;

// The 'opened' property is omitted because it is readonly in the web component.
// So you cannot set it up manually, only read from the component.
// For changing the property, use specific methods of the component.
export type ContextMenuProps<TItem extends ContextMenuItem = ContextMenuItem> = Partial<
export type ContextMenuProps<TItem extends ContextMenuItemData = ContextMenuItemData> = Partial<
Omit<_ContextMenuProps, 'opened' | 'renderer' | 'items' | 'onItemSelected'>
> &
Readonly<{
Expand All @@ -40,7 +45,7 @@ export type ContextMenuProps<TItem extends ContextMenuItem = ContextMenuItem> =
onItemSelected?: (event: ContextMenuItemSelectedEvent<TItem>) => void;
}>;

function ContextMenu<TItem extends ContextMenuItem = ContextMenuItem>(
function ContextMenu<TItem extends ContextMenuItemData = ContextMenuItemData>(
props: ContextMenuProps<TItem>,
ref: ForwardedRef<ContextMenuElement>,
): ReactElement | null {
Expand All @@ -49,7 +54,7 @@ function ContextMenu<TItem extends ContextMenuItem = ContextMenuItem>(

const onItemSelected = props.onItemSelected;
const mappedOnItemSelected = onItemSelected
? (event: CustomEvent<{ value: _ContextMenuItem }>) => {
? (event: CustomEvent<{ value: _ContextMenuItemData }>) => {
// Replace the mapped web component item with the original item
Object.assign(event.detail, {
value: getOriginalItem(event.detail.value),
Expand All @@ -73,7 +78,7 @@ function ContextMenu<TItem extends ContextMenuItem = ContextMenuItem>(
);
}

const ForwardedContextMenu = forwardRef(ContextMenu) as <TItem extends ContextMenuItem = ContextMenuItem>(
const ForwardedContextMenu = forwardRef(ContextMenu) as <TItem extends ContextMenuItemData = ContextMenuItemData>(
props: ContextMenuProps<TItem> & RefAttributes<ContextMenuElement>,
) => ReactElement | null;

Expand Down
1 change: 1 addition & 0 deletions packages/react-components/src/ContextMenuItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './generated/ContextMenuItem.js';
1 change: 1 addition & 0 deletions packages/react-components/src/ContextMenuListBox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './generated/ContextMenuListBox.js';
4 changes: 2 additions & 2 deletions test/typings/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ContextMenu, type ContextMenuItem } from '../../packages/react-components/src/ContextMenu.js';
import { ContextMenu, type ContextMenuItemData } from '../../packages/react-components/src/ContextMenu.js';

const assertType = function <TExpected>(value: TExpected) {
return value;
};

type CustomContextMenuItem = ContextMenuItem<{ value: string }>;
type CustomContextMenuItem = ContextMenuItemData<{ value: string }>;

const items: CustomContextMenuItem[] = [{ text: 'View', value: 'view' }];

Expand Down
21 changes: 21 additions & 0 deletions test/typings/ContextMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ContextMenuItem, ContextMenuListBox, type ContextMenuItemData } from '@vaadin/react-components';
import type { ContextMenuItem as ContextMenuItemType } from '@vaadin/react-components/ContextMenu.js';

// `ContextMenuItem` and `ContextMenuListBox` must resolve to the React components and work in JSX position.
const element = (
<ContextMenuListBox>
<ContextMenuItem>Open</ContextMenuItem>
</ContextMenuListBox>
);

// `ContextMenuItemData` must still work in type position and refer to the React-flavored variant,
// whose `component` property accepts a `ReactElement` (the raw web component type only allows `Node`).
const items: ContextMenuItemData[] = [{ text: 'View', component: <b>View</b> }];

// The deprecated `ContextMenuItem` type is intentionally no longer exported from the barrel;
// `ContextMenuItem` now refers only to the component, so using it as a type must fail to compile.
// @ts-expect-error — `ContextMenuItem` is a value (the component) in the barrel, not a type.
let deprecated: ContextMenuItem[];

// Escape hatch: the deprecated type is still importable directly from the ContextMenu.js subpath.
let stillWorks: ContextMenuItemType[] = [];
8 changes: 4 additions & 4 deletions test/typings/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
import { ComboBox, type ComboBoxChangeEvent } from '../../packages/react-components/src/ComboBox.js';
import {
ContextMenu,
type ContextMenuItem,
type ContextMenuItemData,
type ContextMenuItemSelectedEvent,
} from '../../packages/react-components/src/ContextMenu.js';
import {
Expand Down Expand Up @@ -216,14 +216,14 @@ assertOmitted<HTMLAttributes<LoginOverlayElement>, LoginOverlayProps>('onClick')
const contextMenuProps = React.createElement(ContextMenu, {}).props;

assertType<ReactElement | string | undefined>(contextMenuProps.items![0].component);
assertType<ContextMenuItem[]>(contextMenuProps.items!);
assertType<ContextMenuItemData[]>(contextMenuProps.items!);
const contextMenuOnItemSelected: typeof contextMenuProps.onItemSelected = (event) => {
assertType<ContextMenuItemSelectedEvent>(event);
assertType<ContextMenuItem>(event.detail.value);
assertType<ContextMenuItemData>(event.detail.value);
};
assertType<typeof contextMenuProps.onItemSelected>(contextMenuOnItemSelected);

type CustomContextMenuItem = ContextMenuItem<{ value: string }>;
type CustomContextMenuItem = ContextMenuItemData<{ value: string }>;

const narrowedContextMenuProps = React.createElement(ContextMenu<CustomContextMenuItem>, {}).props;
assertType<CustomContextMenuItem[] | undefined>(narrowedContextMenuProps.items);
Expand Down
Loading