From f8df70288f5f7a0a97cbac3a28c32cac24d297cd Mon Sep 17 00:00:00 2001 From: web-padawan Date: Fri, 24 Jul 2026 14:12:26 +0300 Subject: [PATCH] feat!: add ContextMenuItem and ContextMenuListBox components New public web-components entrypoints in 25.3.0-alpha7 (vaadin/web-components#12173). The React-flavored item type is renamed to ContextMenuItemData mirroring the web-components deprecation; ContextMenuItem is kept as a deprecated alias. The barrel resolves ContextMenuItem to the component via the explicit re-exports introduced in #395. BREAKING CHANGE: the TypeScript type `ContextMenuItem` is no longer exported from the package barrel; use `ContextMenuItemData` instead, or import the deprecated type from `@vaadin/react-components/ContextMenu.js`. `ContextMenuItem` now refers to the React component. Co-Authored-By: Claude Fable 5 --- dev/pages/ContextMenu.tsx | 8 +++--- packages/react-components/package.json | 10 ++++++++ packages/react-components/src/ContextMenu.tsx | 25 +++++++++++-------- .../react-components/src/ContextMenuItem.ts | 1 + .../src/ContextMenuListBox.ts | 1 + test/typings/ContextMenu.tsx | 4 +-- test/typings/ContextMenuItem.tsx | 21 ++++++++++++++++ test/typings/api.ts | 8 +++--- 8 files changed, 58 insertions(+), 20 deletions(-) create mode 100644 packages/react-components/src/ContextMenuItem.ts create mode 100644 packages/react-components/src/ContextMenuListBox.ts create mode 100644 test/typings/ContextMenuItem.tsx diff --git a/dev/pages/ContextMenu.tsx b/dev/pages/ContextMenu.tsx index 1a411d22..95bafc56 100644 --- a/dev/pages/ContextMenu.tsx +++ b/dev/pages/ContextMenu.tsx @@ -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'; @@ -25,7 +25,7 @@ function createItem(iconName: string, text: string) { ); } -const initialItemSets: Record = { +const initialItemSets: Record = { basic: [{ text: 'View' }, { text: 'Edit' }, { text: 'Delete' }], dividers: [{ text: 'View' }, { component: 'hr' }, { text: 'Edit' }, { text: 'Delete' }], checkable: [ @@ -74,7 +74,7 @@ type OpenOnType = 'contextmenu' | 'click'; export default function ContextMenuPage() { const [itemSetType, setItemSetType] = useState('basic'); - const [items, setItems] = useState(initialItemSets.basic); + const [items, setItems] = useState(initialItemSets.basic); const [openOn, setOpenOn] = useState('contextmenu'); const [eventLog, setEventLog] = useState([]); @@ -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) { diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 730eb460..6fabe30b 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -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" @@ -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", diff --git a/packages/react-components/src/ContextMenu.tsx b/packages/react-components/src/ContextMenu.tsx index 00c4f626..de659421 100644 --- a/packages/react-components/src/ContextMenu.tsx +++ b/packages/react-components/src/ContextMenu.tsx @@ -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'; @@ -13,23 +13,28 @@ export * from './generated/ContextMenu.js'; export type ContextMenuReactRendererProps = ReactContextRendererProps; -export type ContextMenuItem = Omit< - _ContextMenuItem, +export type ContextMenuItemData = Omit< + _ContextMenuItemData, 'component' | 'children' > & { component?: ReactElement | string; - children?: Array>; + children?: Array>; }; -export type ContextMenuItemSelectedEvent = CustomEvent<{ - value: ContextMenuItem; +/** + * @deprecated Use `ContextMenuItemData` instead. + */ +export type ContextMenuItem = ContextMenuItemData; + +export type ContextMenuItemSelectedEvent = CustomEvent<{ + value: ContextMenuItemData; }>; // 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 = Partial< +export type ContextMenuProps = Partial< Omit<_ContextMenuProps, 'opened' | 'renderer' | 'items' | 'onItemSelected'> > & Readonly<{ @@ -40,7 +45,7 @@ export type ContextMenuProps = onItemSelected?: (event: ContextMenuItemSelectedEvent) => void; }>; -function ContextMenu( +function ContextMenu( props: ContextMenuProps, ref: ForwardedRef, ): ReactElement | null { @@ -49,7 +54,7 @@ function ContextMenu( 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), @@ -73,7 +78,7 @@ function ContextMenu( ); } -const ForwardedContextMenu = forwardRef(ContextMenu) as ( +const ForwardedContextMenu = forwardRef(ContextMenu) as ( props: ContextMenuProps & RefAttributes, ) => ReactElement | null; diff --git a/packages/react-components/src/ContextMenuItem.ts b/packages/react-components/src/ContextMenuItem.ts new file mode 100644 index 00000000..59f1193c --- /dev/null +++ b/packages/react-components/src/ContextMenuItem.ts @@ -0,0 +1 @@ +export * from './generated/ContextMenuItem.js'; diff --git a/packages/react-components/src/ContextMenuListBox.ts b/packages/react-components/src/ContextMenuListBox.ts new file mode 100644 index 00000000..9c1097ae --- /dev/null +++ b/packages/react-components/src/ContextMenuListBox.ts @@ -0,0 +1 @@ +export * from './generated/ContextMenuListBox.js'; diff --git a/test/typings/ContextMenu.tsx b/test/typings/ContextMenu.tsx index 91938f23..a8a400ec 100644 --- a/test/typings/ContextMenu.tsx +++ b/test/typings/ContextMenu.tsx @@ -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 (value: TExpected) { return value; }; -type CustomContextMenuItem = ContextMenuItem<{ value: string }>; +type CustomContextMenuItem = ContextMenuItemData<{ value: string }>; const items: CustomContextMenuItem[] = [{ text: 'View', value: 'view' }]; diff --git a/test/typings/ContextMenuItem.tsx b/test/typings/ContextMenuItem.tsx new file mode 100644 index 00000000..1744b919 --- /dev/null +++ b/test/typings/ContextMenuItem.tsx @@ -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 = ( + + Open + +); + +// `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: View }]; + +// 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[] = []; diff --git a/test/typings/api.ts b/test/typings/api.ts index 0c38fc7c..8992bf6a 100644 --- a/test/typings/api.ts +++ b/test/typings/api.ts @@ -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 { @@ -216,14 +216,14 @@ assertOmitted, LoginOverlayProps>('onClick') const contextMenuProps = React.createElement(ContextMenu, {}).props; assertType(contextMenuProps.items![0].component); -assertType(contextMenuProps.items!); +assertType(contextMenuProps.items!); const contextMenuOnItemSelected: typeof contextMenuProps.onItemSelected = (event) => { assertType(event); - assertType(event.detail.value); + assertType(event.detail.value); }; assertType(contextMenuOnItemSelected); -type CustomContextMenuItem = ContextMenuItem<{ value: string }>; +type CustomContextMenuItem = ContextMenuItemData<{ value: string }>; const narrowedContextMenuProps = React.createElement(ContextMenu, {}).props; assertType(narrowedContextMenuProps.items);