-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Bug: NodeContextMenuPlugin key Prop Issue #7801
Copy link
Copy link
Closed
Description
I used the ContextMenu example from Playground in my own project. But I am getting the errors below. I tried a lot, but I couldn’t fix them.
Lexical version: 0.35
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type { JSX } from "react";
import { useMemo } from "react";
import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { NodeContextMenuOption, NodeContextMenuPlugin, NodeContextMenuSeparator } from "@lexical/react/LexicalNodeContextMenuPlugin";
import {
$getSelection,
$isDecoratorNode,
$isNodeSelection,
$isRangeSelection,
COPY_COMMAND,
CUT_COMMAND,
PASTE_COMMAND,
type LexicalNode,
} from "lexical";
import { Clipboard, ClipboardType, Copy, Link2Off, Scissors, Trash2 } from "lucide-react";
export function ContextMenuPlugin(): JSX.Element {
const [editor] = useLexicalComposerContext();
const items = useMemo(() => {
return [
new NodeContextMenuOption(`Remove Link`, {
$onSelect: () => {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
},
$showOn: (node: LexicalNode) => $isLinkNode(node.getParent()),
disabled: false,
icon: <Link2Off className="h-4 w-4" />,
}),
new NodeContextMenuSeparator({
$showOn: (node: LexicalNode) => $isLinkNode(node.getParent()),
}),
new NodeContextMenuOption(`Cut`, {
$onSelect: () => {
editor.dispatchCommand(CUT_COMMAND, null);
},
disabled: false,
icon: <Scissors className="h-4 w-4" />,
}),
new NodeContextMenuOption(`Copy`, {
$onSelect: () => {
editor.dispatchCommand(COPY_COMMAND, null);
},
disabled: false,
icon: <Copy className="h-4 w-4" />,
}),
new NodeContextMenuOption(`Paste`, {
$onSelect: () => {
navigator.clipboard.read().then(async function (...args) {
const data = new DataTransfer();
const readClipboardItems = await navigator.clipboard.read();
const item = readClipboardItems[0];
const permission = await navigator.permissions.query({
// @ts-expect-error These types are incorrect.
name: "clipboard-read",
});
if (permission.state === "denied") {
alert("Not allowed to paste from clipboard.");
return;
}
for (const type of item.types) {
const dataString = await (await item.getType(type)).text();
data.setData(type, dataString);
}
const event = new ClipboardEvent("paste", {
clipboardData: data,
});
editor.dispatchCommand(PASTE_COMMAND, event);
});
},
disabled: false,
icon: <Clipboard className="h-4 w-4" />,
}),
new NodeContextMenuOption(`Paste as Plain Text`, {
$onSelect: () => {
navigator.clipboard.read().then(async function (...args) {
const permission = await navigator.permissions.query({
// @ts-expect-error These types are incorrect.
name: "clipboard-read",
});
if (permission.state === "denied") {
alert("Not allowed to paste from clipboard.");
return;
}
const data = new DataTransfer();
const clipboardText = await navigator.clipboard.readText();
data.setData("text/plain", clipboardText);
const event = new ClipboardEvent("paste", {
clipboardData: data,
});
editor.dispatchCommand(PASTE_COMMAND, event);
});
},
disabled: false,
icon: <ClipboardType className="h-4 w-4" />,
}),
new NodeContextMenuSeparator(),
new NodeContextMenuOption(`Delete Node`, {
$onSelect: () => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const currentNode = selection.anchor.getNode();
const ancestorNodeWithRootAsParent = currentNode.getParents().at(-2);
ancestorNodeWithRootAsParent?.remove();
} else if ($isNodeSelection(selection)) {
const selectedNodes = selection.getNodes();
selectedNodes.forEach((node) => {
if ($isDecoratorNode(node)) {
node.remove();
}
});
}
},
disabled: false,
icon: <Trash2 className="h-4 w-4" />,
}),
];
}, [editor]);
return (
<NodeContextMenuPlugin
className="bg-popover text-popover-foreground !z-50 overflow-hidden rounded-md border shadow-md outline-none [&:has(*)]:!z-10"
itemClassName="relative w-full flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50"
separatorClassName="bg-border -mx-1 h-px"
items={items}
/>
);
}
A props object containing a "key" prop is being spread into JSX:
let props = {key: someKey, className: ..., disabled: ..., icon: ..., label: ..., onClick: ..., title: ..., type: ..., onMouseUp: ..., ref: ..., tabIndex: ..., onFocus: ..., onMouseMove: ..., onPointerLeave: ...};
<ForwardRef {...props} />
React keys must be passed directly to JSX without using spread:
let props = {className: ..., disabled: ..., icon: ..., label: ..., onClick: ..., title: ..., type: ..., onMouseUp: ..., ref: ..., tabIndex: ..., onFocus: ..., onMouseMove: ..., onPointerLeave: ...};
<ForwardRef key={someKey} {...props} />
components\editor\plugins\context-menu-plugin.tsx (139:5) @ ContextMenuPlugin
137 |
138 | return (
> 139 | <NodeContextMenuPlugin
| ^
140 | className="bg-popover text-popover-foreground !z-50 overflow-hidden rounded-md border shadow-md outline-none [&:has(*)]:!z-10"
141 | itemClassName="relative w-full flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50"
142 | separatorClassName="bg-border -mx-1 h-px"
A props object containing a "key" prop is being spread into JSX:
let props = {key: someKey, className: ..., type: ..., ref: ..., tabIndex: ..., onFocus: ..., onClick: ..., onMouseMove: ..., onPointerLeave: ...};
<ForwardRef {...props} />
React keys must be passed directly to JSX without using spread:
let props = {className: ..., type: ..., ref: ..., tabIndex: ..., onFocus: ..., onClick: ..., onMouseMove: ..., onPointerLeave: ...};
<ForwardRef key={someKey} {...props} />
components\editor\plugins\context-menu-plugin.tsx (139:5) @ ContextMenuPlugin
137 |
138 | return (
> 139 | <NodeContextMenuPlugin
| ^
140 | className="bg-popover text-popover-foreground !z-50 overflow-hidden rounded-md border shadow-md outline-none [&:has(*)]:!z-10"
141 | itemClassName="relative w-full flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50"
142 | separatorClassName="bg-border -mx-1 h-px"
package.json
{
"name": "agencive-blog-main",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3120 -H blog.agencive.l",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@excalidraw/excalidraw": "^0.18.0",
"@hookform/resolvers": "^5.2.1",
"@lexical/code": "^0.35.0",
"@lexical/file": "^0.35.0",
"@lexical/hashtag": "^0.35.0",
"@lexical/link": "^0.35.0",
"@lexical/list": "^0.35.0",
"@lexical/markdown": "^0.35.0",
"@lexical/overflow": "^0.35.0",
"@lexical/react": "^0.35.0",
"@lexical/rich-text": "^0.35.0",
"@lexical/selection": "^0.35.0",
"@lexical/table": "^0.35.0",
"@lexical/text": "^0.35.0",
"@lexical/utils": "^0.35.0",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-visually-hidden": "^1.2.3",
"@tabler/icons-react": "^3.34.1",
"@tanstack/react-query": "^5.85.3",
"@tanstack/react-table": "^8.21.3",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"cookies-next": "^6.1.0",
"date-fns": "^4.1.0",
"dayjs": "^1.11.13",
"immer": "^10.1.1",
"jose": "^6.0.12",
"juice": "^11.0.1",
"katex": "^0.16.22",
"lexical": "^0.35.0",
"lodash-es": "^4.17.21",
"lucide-react": "^0.539.0",
"next": "15.4.6",
"next-themes": "^0.4.6",
"notistack": "^3.0.2",
"radix-ui": "^1.4.3",
"react": "^19.1.1",
"react-colorful": "^5.6.1",
"react-day-picker": "^9.9.0",
"react-dom": "^19.1.1",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.62.0",
"shadcn": "^2.10.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"yup": "^1.7.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@faker-js/faker": "^9.9.0",
"@tailwindcss/postcss": "^4",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"tw-animate-css": "^1.3.6",
"typescript": "^5"
}
}tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"react": ["./node_modules/@types/react"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"main/types/*.d.ts",
"scripts/build-registry.mts",
"next.config.ts"
],
"exclude": ["node_modules"]
}Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels