From 7ff9080305eb477cbaa533e58b02be5c4d17b304 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Sun, 8 Dec 2024 19:32:38 +0000 Subject: [PATCH 1/3] Unused PUNCTUATION variable --- packages/lexical-react/src/shared/LexicalMenu.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/lexical-react/src/shared/LexicalMenu.ts b/packages/lexical-react/src/shared/LexicalMenu.ts index cda6ca3c5cb..764db4981e3 100644 --- a/packages/lexical-react/src/shared/LexicalMenu.ts +++ b/packages/lexical-react/src/shared/LexicalMenu.ts @@ -46,9 +46,6 @@ export type MenuResolution = { getRect: () => DOMRect; }; -export const PUNCTUATION = - '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;'; - export class MenuOption { key: string; ref?: MutableRefObject; From bcaf785608ba89fc5ae6e7e51459d4143bab2d82 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Mon, 9 Dec 2024 00:23:38 +0000 Subject: [PATCH 2/3] FloatingUI Menu Positioning --- packages/lexical-playground/src/index.css | 12 -- .../src/plugins/AutoEmbedPlugin/index.tsx | 95 ---------------- .../plugins/ComponentPickerPlugin/index.tsx | 65 ----------- .../src/plugins/ContextMenuPlugin/index.tsx | 104 +----------------- .../src/plugins/EmojiPickerPlugin/index.tsx | 71 +----------- .../src/plugins/MentionsPlugin/index.tsx | 71 +----------- 6 files changed, 9 insertions(+), 409 deletions(-) diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index 87fb8fdbbff..1730d317b55 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -355,18 +355,6 @@ pre::-webkit-scrollbar-thumb { width: 200px; } -.mentions-menu { - width: 250px; -} - -.auto-embed-menu { - width: 150px; -} - -.emoji-menu { - width: 200px; -} - i.palette { background-image: url(images/icons/palette.svg); } diff --git a/packages/lexical-playground/src/plugins/AutoEmbedPlugin/index.tsx b/packages/lexical-playground/src/plugins/AutoEmbedPlugin/index.tsx index 808967b3082..9f3df261f22 100644 --- a/packages/lexical-playground/src/plugins/AutoEmbedPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/AutoEmbedPlugin/index.tsx @@ -17,8 +17,6 @@ import { } from '@lexical/react/LexicalAutoEmbedPlugin'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {useMemo, useState} from 'react'; -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; import useModal from '../../hooks/useModal'; import Button from '../../ui/Button'; @@ -154,68 +152,6 @@ export const EmbedConfigs = [ FigmaEmbedConfig, ]; -function AutoEmbedMenuItem({ - index, - isSelected, - onClick, - onMouseEnter, - option, -}: { - index: number; - isSelected: boolean; - onClick: () => void; - onMouseEnter: () => void; - option: AutoEmbedOption; -}) { - let className = 'item'; - if (isSelected) { - className += ' selected'; - } - return ( -
  • - {option.title} -
  • - ); -} - -function AutoEmbedMenu({ - options, - selectedItemIndex, - onOptionClick, - onOptionMouseEnter, -}: { - selectedItemIndex: number | null; - onOptionClick: (option: AutoEmbedOption, index: number) => void; - onOptionMouseEnter: (index: number) => void; - options: Array; -}) { - return ( -
    -
      - {options.map((option: AutoEmbedOption, i: number) => ( - onOptionClick(option, i)} - onMouseEnter={() => onOptionMouseEnter(i)} - key={option.key} - option={option} - /> - ))} -
    -
    - ); -} - const debounce = (callback: (text: string) => void, delay: number) => { let timeoutId: number; return (text: string) => { @@ -320,37 +256,6 @@ export default function AutoEmbedPlugin(): JSX.Element { embedConfigs={EmbedConfigs} onOpenEmbedModalForConfig={openEmbedModal} getMenuOptions={getMenuOptions} - menuRenderFn={( - anchorElementRef, - {selectedIndex, options, selectOptionAndCleanUp, setHighlightedIndex}, - ) => - anchorElementRef.current - ? ReactDOM.createPortal( -
    - { - setHighlightedIndex(index); - selectOptionAndCleanUp(option); - }} - onOptionMouseEnter={(index: number) => { - setHighlightedIndex(index); - }} - /> -
    , - anchorElementRef.current, - ) - : null - } /> ); diff --git a/packages/lexical-playground/src/plugins/ComponentPickerPlugin/index.tsx b/packages/lexical-playground/src/plugins/ComponentPickerPlugin/index.tsx index 19406089056..c2dce1d0db5 100644 --- a/packages/lexical-playground/src/plugins/ComponentPickerPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ComponentPickerPlugin/index.tsx @@ -32,8 +32,6 @@ import { TextNode, } from 'lexical'; import {useCallback, useMemo, useState} from 'react'; -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; import useModal from '../../hooks/useModal'; import catTypingGif from '../../images/cat-typing.gif'; @@ -77,40 +75,6 @@ class ComponentPickerOption extends MenuOption { } } -function ComponentPickerMenuItem({ - index, - isSelected, - onClick, - onMouseEnter, - option, -}: { - index: number; - isSelected: boolean; - onClick: () => void; - onMouseEnter: () => void; - option: ComponentPickerOption; -}) { - let className = 'item'; - if (isSelected) { - className += ' selected'; - } - return ( -
  • - {option.icon} - {option.title} -
  • - ); -} - function getDynamicOptions(editor: LexicalEditor, queryString: string) { const options: Array = []; @@ -365,35 +329,6 @@ export default function ComponentPickerMenuPlugin(): JSX.Element { onSelectOption={onSelectOption} triggerFn={checkForTriggerMatch} options={options} - menuRenderFn={( - anchorElementRef, - {selectedIndex, selectOptionAndCleanUp, setHighlightedIndex}, - ) => - anchorElementRef.current && options.length - ? ReactDOM.createPortal( -
    -
      - {options.map((option, i: number) => ( - { - setHighlightedIndex(i); - selectOptionAndCleanUp(option); - }} - onMouseEnter={() => { - setHighlightedIndex(i); - }} - key={option.key} - option={option} - /> - ))} -
    -
    , - anchorElementRef.current, - ) - : null - } /> ); diff --git a/packages/lexical-playground/src/plugins/ContextMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/ContextMenuPlugin/index.tsx index 36f51ecb2ba..6185a5b1666 100644 --- a/packages/lexical-playground/src/plugins/ContextMenuPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ContextMenuPlugin/index.tsx @@ -21,71 +21,7 @@ import { type LexicalNode, PASTE_COMMAND, } from 'lexical'; -import {useCallback, useMemo} from 'react'; -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; - -function ContextMenuItem({ - index, - isSelected, - onClick, - onMouseEnter, - option, -}: { - index: number; - isSelected: boolean; - onClick: () => void; - onMouseEnter: () => void; - option: ContextMenuOption; -}) { - let className = 'item'; - if (isSelected) { - className += ' selected'; - } - return ( -
  • - {option.title} -
  • - ); -} - -function ContextMenu({ - options, - selectedItemIndex, - onOptionClick, - onOptionMouseEnter, -}: { - selectedItemIndex: number | null; - onOptionClick: (option: ContextMenuOption, index: number) => void; - onOptionMouseEnter: (index: number) => void; - options: Array; -}) { - return ( -
    -
      - {options.map((option: ContextMenuOption, i: number) => ( - onOptionClick(option, i)} - onMouseEnter={() => onOptionMouseEnter(i)} - key={option.key} - option={option} - /> - ))} -
    -
    - ); -} +import {useCallback, useMemo, useState} from 'react'; export class ContextMenuOption extends MenuOption { title: string; @@ -187,7 +123,7 @@ export default function ContextMenuPlugin(): JSX.Element { ]; }, [editor]); - const [options, setOptions] = React.useState(defaultOptions); + const [options, setOptions] = useState(defaultOptions); const onSelectOption = useCallback( ( @@ -229,42 +165,6 @@ export default function ContextMenuPlugin(): JSX.Element { options={options} onSelectOption={onSelectOption} onWillOpen={onWillOpen} - menuRenderFn={( - anchorElementRef, - { - selectedIndex, - options: _options, - selectOptionAndCleanUp, - setHighlightedIndex, - }, - {setMenuRef}, - ) => - anchorElementRef.current - ? ReactDOM.createPortal( -
    - { - setHighlightedIndex(index); - selectOptionAndCleanUp(option); - }} - onOptionMouseEnter={(index: number) => { - setHighlightedIndex(index); - }} - /> -
    , - anchorElementRef.current, - ) - : null - } /> ); } diff --git a/packages/lexical-playground/src/plugins/EmojiPickerPlugin/index.tsx b/packages/lexical-playground/src/plugins/EmojiPickerPlugin/index.tsx index b88c33fc5db..b3871cc66e9 100644 --- a/packages/lexical-playground/src/plugins/EmojiPickerPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/EmojiPickerPlugin/index.tsx @@ -18,9 +18,7 @@ import { $isRangeSelection, TextNode, } from 'lexical'; -import * as React from 'react'; import {useCallback, useEffect, useMemo, useState} from 'react'; -import * as ReactDOM from 'react-dom'; class EmojiOption extends MenuOption { title: string; @@ -40,40 +38,6 @@ class EmojiOption extends MenuOption { this.keywords = options.keywords || []; } } -function EmojiMenuItem({ - index, - isSelected, - onClick, - onMouseEnter, - option, -}: { - index: number; - isSelected: boolean; - onClick: () => void; - onMouseEnter: () => void; - option: EmojiOption; -}) { - let className = 'item'; - if (isSelected) { - className += ' selected'; - } - return ( -
  • - - {option.emoji} {option.title} - -
  • - ); -} type Emoji = { emoji: string; @@ -102,7 +66,7 @@ export default function EmojiPickerPlugin() { emojis != null ? emojis.map( ({emoji, aliases, tags}) => - new EmojiOption(aliases[0], emoji, { + new EmojiOption(`${emoji} ${aliases[0]}`, emoji, { keywords: [...aliases, ...tags], }), ) @@ -160,39 +124,6 @@ export default function EmojiPickerPlugin() { onSelectOption={onSelectOption} triggerFn={checkForTriggerMatch} options={options} - menuRenderFn={( - anchorElementRef, - {selectedIndex, selectOptionAndCleanUp, setHighlightedIndex}, - ) => { - if (anchorElementRef.current == null || options.length === 0) { - return null; - } - - return anchorElementRef.current && options.length - ? ReactDOM.createPortal( -
    -
      - {options.map((option: EmojiOption, index) => ( - { - setHighlightedIndex(index); - selectOptionAndCleanUp(option); - }} - onMouseEnter={() => { - setHighlightedIndex(index); - }} - option={option} - /> - ))} -
    -
    , - anchorElementRef.current, - ) - : null; - }} /> ); } diff --git a/packages/lexical-playground/src/plugins/MentionsPlugin/index.tsx b/packages/lexical-playground/src/plugins/MentionsPlugin/index.tsx index faaca3496a3..13cea68f357 100644 --- a/packages/lexical-playground/src/plugins/MentionsPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/MentionsPlugin/index.tsx @@ -15,8 +15,6 @@ import { } from '@lexical/react/LexicalTypeaheadMenuPlugin'; import {TextNode} from 'lexical'; import {useCallback, useEffect, useMemo, useState} from 'react'; -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; import {$createMentionNode} from '../../nodes/MentionNode'; @@ -563,48 +561,20 @@ function getPossibleQueryMatch(text: string): MenuTextMatch | null { class MentionTypeaheadOption extends MenuOption { name: string; picture: JSX.Element; + title: JSX.Element; constructor(name: string, picture: JSX.Element) { super(name); this.name = name; this.picture = picture; + this.title = ( + <> + {picture} {name} + + ); } } -function MentionsTypeaheadMenuItem({ - index, - isSelected, - onClick, - onMouseEnter, - option, -}: { - index: number; - isSelected: boolean; - onClick: () => void; - onMouseEnter: () => void; - option: MentionTypeaheadOption; -}) { - let className = 'item'; - if (isSelected) { - className += ' selected'; - } - return ( -
  • - {option.picture} - {option.name} -
  • - ); -} - export default function NewMentionsPlugin(): JSX.Element | null { const [editor] = useLexicalComposerContext(); @@ -662,35 +632,6 @@ export default function NewMentionsPlugin(): JSX.Element | null { onSelectOption={onSelectOption} triggerFn={checkForMentionMatch} options={options} - menuRenderFn={( - anchorElementRef, - {selectedIndex, selectOptionAndCleanUp, setHighlightedIndex}, - ) => - anchorElementRef.current && results.length - ? ReactDOM.createPortal( -
    -
      - {options.map((option, i: number) => ( - { - setHighlightedIndex(i); - selectOptionAndCleanUp(option); - }} - onMouseEnter={() => { - setHighlightedIndex(i); - }} - key={option.key} - option={option} - /> - ))} -
    -
    , - anchorElementRef.current, - ) - : null - } /> ); } From 9bfcfa3087a8a17be271495a49e046bdc17518b4 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Mon, 9 Dec 2024 00:28:13 +0000 Subject: [PATCH 3/3] FloatingUI Menu Positioning --- package-lock.json | 106 ++++++++++ packages/lexical-react/package.json | 1 + .../src/LexicalAutoEmbedPlugin.tsx | 4 - .../src/LexicalContextMenuPlugin.tsx | 30 +-- .../src/LexicalNodeMenuPlugin.tsx | 7 +- .../src/LexicalTypeaheadMenuPlugin.tsx | 16 +- .../{LexicalMenu.ts => LexicalMenu.tsx} | 183 +++++++++++++++--- 7 files changed, 271 insertions(+), 76 deletions(-) rename packages/lexical-react/src/shared/{LexicalMenu.ts => LexicalMenu.tsx} (79%) diff --git a/package-lock.json b/package-lock.json index e5387c0ffdc..d2c8ab5b196 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5922,6 +5922,59 @@ "react-dom": "^17.0.2 || ^18.2.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.0.tgz", + "integrity": "sha512-WLEksq7fJapXSJbmfiyq9pAW0a7ZFMEJToFE4oTDESxGjoa+nZu3YMjmZE2KvoUtQhqOK2yMMfWQFZyeWD0wGQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", + "license": "MIT" + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -34892,6 +34945,12 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", @@ -39178,6 +39237,7 @@ "version": "0.21.0", "license": "MIT", "dependencies": { + "@floating-ui/react": "^0.27.0", "@lexical/clipboard": "0.21.0", "@lexical/code": "0.21.0", "@lexical/devtools-core": "0.21.0", @@ -43287,6 +43347,46 @@ "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.17.6.tgz", "integrity": "sha512-fyCl+zG/Z5yhHDh5Fq2ZGmphcrALmuOdtITm8gN4d8w4ntnaopTXcTfnAAaU3VleDC6LhTkoLOTG6P5kgREiIg==" }, + "@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "requires": { + "@floating-ui/utils": "^0.2.8" + } + }, + "@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "requires": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "@floating-ui/react": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.0.tgz", + "integrity": "sha512-WLEksq7fJapXSJbmfiyq9pAW0a7ZFMEJToFE4oTDESxGjoa+nZu3YMjmZE2KvoUtQhqOK2yMMfWQFZyeWD0wGQ==", + "requires": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + } + }, + "@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "requires": { + "@floating-ui/dom": "^1.0.0" + } + }, + "@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" + }, "@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -44111,6 +44211,7 @@ "@lexical/react": { "version": "file:packages/lexical-react", "requires": { + "@floating-ui/react": "^0.27.0", "@lexical/clipboard": "0.21.0", "@lexical/code": "0.21.0", "@lexical/devtools-core": "0.21.0", @@ -63346,6 +63447,11 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "tailwindcss": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", diff --git a/packages/lexical-react/package.json b/packages/lexical-react/package.json index 79482eb0db9..78a25c01ede 100644 --- a/packages/lexical-react/package.json +++ b/packages/lexical-react/package.json @@ -10,6 +10,7 @@ "license": "MIT", "version": "0.21.0", "dependencies": { + "@floating-ui/react": "^0.27.0", "@lexical/clipboard": "0.21.0", "@lexical/code": "0.21.0", "@lexical/devtools-core": "0.21.0", diff --git a/packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx b/packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx index 49ebbb1a270..1a59ce21c39 100644 --- a/packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx +++ b/packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx @@ -16,7 +16,6 @@ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import { LexicalNodeMenuPlugin, MenuOption, - MenuRenderFn, } from '@lexical/react/LexicalNodeMenuPlugin'; import {mergeRegister} from '@lexical/utils'; import { @@ -82,7 +81,6 @@ type LexicalAutoEmbedPluginProps = { embedFn: () => void, dismissFn: () => void, ) => Array; - menuRenderFn: MenuRenderFn; menuCommandPriority?: CommandListenerPriority; }; @@ -90,7 +88,6 @@ export function LexicalAutoEmbedPlugin({ embedConfigs, onOpenEmbedModalForConfig, getMenuOptions, - menuRenderFn, menuCommandPriority = COMMAND_PRIORITY_LOW, }: LexicalAutoEmbedPluginProps): JSX.Element | null { const [editor] = useLexicalComposerContext(); @@ -233,7 +230,6 @@ export function LexicalAutoEmbedPlugin({ onClose={reset} onSelectOption={onSelectOption} options={options} - menuRenderFn={menuRenderFn} commandPriority={menuCommandPriority} /> ) : null; diff --git a/packages/lexical-react/src/LexicalContextMenuPlugin.tsx b/packages/lexical-react/src/LexicalContextMenuPlugin.tsx index c105d48b8f1..21397854612 100644 --- a/packages/lexical-react/src/LexicalContextMenuPlugin.tsx +++ b/packages/lexical-react/src/LexicalContextMenuPlugin.tsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * */ -import type {MenuRenderFn, MenuResolution} from './shared/LexicalMenu'; +import type {MenuResolution} from './shared/LexicalMenu'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {calculateZoomLevel} from '@lexical/utils'; @@ -15,8 +15,8 @@ import { LexicalNode, } from 'lexical'; import { - MutableRefObject, - ReactPortal, + // MutableRefObject, + // ReactPortal, useCallback, useEffect, useState, @@ -25,19 +25,6 @@ import * as React from 'react'; import {LexicalMenu, MenuOption, useMenuAnchorRef} from './shared/LexicalMenu'; -export type ContextMenuRenderFn = ( - anchorElementRef: MutableRefObject, - itemProps: { - selectedIndex: number | null; - selectOptionAndCleanUp: (option: TOption) => void; - setHighlightedIndex: (index: number) => void; - options: Array; - }, - menuProps: { - setMenuRef: (element: HTMLElement | null) => void; - }, -) => ReactPortal | JSX.Element | null; - export type LexicalContextMenuPluginProps = { onSelectOption: ( option: TOption, @@ -49,7 +36,6 @@ export type LexicalContextMenuPluginProps = { onClose?: () => void; onWillOpen?: (event: MouseEvent) => void; onOpen?: (resolution: MenuResolution) => void; - menuRenderFn: ContextMenuRenderFn; anchorClassName?: string; commandPriority?: CommandListenerPriority; parent?: HTMLElement; @@ -63,7 +49,6 @@ export function LexicalContextMenuPlugin({ onClose, onOpen, onSelectOption, - menuRenderFn: contextMenuRenderFn, anchorClassName, commandPriority = COMMAND_PRIORITY_LOW, parent, @@ -153,17 +138,10 @@ export function LexicalContextMenuPlugin({ editor={editor} anchorElementRef={anchorElementRef} options={options} - menuRenderFn={(anchorRef, itemProps) => - contextMenuRenderFn(anchorRef, itemProps, { - setMenuRef: (ref) => { - menuRef.current = ref; - }, - }) - } onSelectOption={onSelectOption} commandPriority={commandPriority} /> ); } -export {MenuOption, MenuRenderFn, MenuResolution}; +export {MenuOption, MenuResolution}; diff --git a/packages/lexical-react/src/LexicalNodeMenuPlugin.tsx b/packages/lexical-react/src/LexicalNodeMenuPlugin.tsx index b8f685ad990..5d1ba7f8df3 100644 --- a/packages/lexical-react/src/LexicalNodeMenuPlugin.tsx +++ b/packages/lexical-react/src/LexicalNodeMenuPlugin.tsx @@ -6,7 +6,7 @@ * */ -import type {MenuRenderFn, MenuResolution} from './shared/LexicalMenu'; +import type {MenuResolution} from './shared/LexicalMenu'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import { @@ -33,7 +33,6 @@ export type NodeMenuPluginProps = { nodeKey: NodeKey | null; onClose?: () => void; onOpen?: (resolution: MenuResolution) => void; - menuRenderFn: MenuRenderFn; anchorClassName?: string; commandPriority?: CommandListenerPriority; parent?: HTMLElement; @@ -45,7 +44,6 @@ export function LexicalNodeMenuPlugin({ onClose, onOpen, onSelectOption, - menuRenderFn, anchorClassName, commandPriority = COMMAND_PRIORITY_LOW, parent, @@ -119,11 +117,10 @@ export function LexicalNodeMenuPlugin({ editor={editor} anchorElementRef={anchorElementRef} options={options} - menuRenderFn={menuRenderFn} onSelectOption={onSelectOption} commandPriority={commandPriority} /> ); } -export {MenuOption, MenuRenderFn, MenuResolution}; +export {MenuOption, MenuResolution}; diff --git a/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx b/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx index da497dc9aad..9a48f872342 100644 --- a/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx +++ b/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx @@ -7,7 +7,6 @@ */ import type { - MenuRenderFn, MenuResolution, MenuTextMatch, TriggerFn, @@ -20,15 +19,14 @@ import { $isTextNode, COMMAND_PRIORITY_LOW, CommandListenerPriority, - createCommand, + // createCommand, getDOMSelection, - LexicalCommand, + // LexicalCommand, LexicalEditor, RangeSelection, TextNode, } from 'lexical'; import {useCallback, useEffect, useState} from 'react'; -import * as React from 'react'; import {startTransition} from 'shared/reactPatches'; import {LexicalMenu, MenuOption, useMenuAnchorRef} from './shared/LexicalMenu'; @@ -140,11 +138,6 @@ export function getScrollParent( export {useDynamicPositioning} from './shared/LexicalMenu'; -export const SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND: LexicalCommand<{ - index: number; - option: MenuOption; -}> = createCommand('SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND'); - export function useBasicTypeaheadTriggerMatch( trigger: string, {minLength = 1, maxLength = 75}: {minLength?: number; maxLength?: number}, @@ -191,7 +184,6 @@ export type TypeaheadMenuPluginProps = { matchingString: string, ) => void; options: Array; - menuRenderFn: MenuRenderFn; triggerFn: TriggerFn; onOpen?: (resolution: MenuResolution) => void; onClose?: () => void; @@ -206,7 +198,6 @@ export function LexicalTypeaheadMenuPlugin({ onSelectOption, onOpen, onClose, - menuRenderFn, triggerFn, anchorClassName, commandPriority = COMMAND_PRIORITY_LOW, @@ -305,7 +296,6 @@ export function LexicalTypeaheadMenuPlugin({ editor={editor} anchorElementRef={anchorElementRef} options={options} - menuRenderFn={menuRenderFn} shouldSplitNodeWithQuery={true} onSelectOption={onSelectOption} commandPriority={commandPriority} @@ -313,4 +303,4 @@ export function LexicalTypeaheadMenuPlugin({ ); } -export {MenuOption, MenuRenderFn, MenuResolution, MenuTextMatch, TriggerFn}; +export {MenuOption, MenuResolution, MenuTextMatch, TriggerFn}; diff --git a/packages/lexical-react/src/shared/LexicalMenu.ts b/packages/lexical-react/src/shared/LexicalMenu.tsx similarity index 79% rename from packages/lexical-react/src/shared/LexicalMenu.ts rename to packages/lexical-react/src/shared/LexicalMenu.tsx index 764db4981e3..816c54a8064 100644 --- a/packages/lexical-react/src/shared/LexicalMenu.ts +++ b/packages/lexical-react/src/shared/LexicalMenu.tsx @@ -6,6 +6,23 @@ * */ +import { + // autoPlacement, + // autoUpdate, + flip, + FloatingFocusManager, + FloatingList, + FloatingOverlay, + FloatingPortal, + // offset, + shift, + useDismiss, + useFloating, + useInteractions, + useListNavigation, + useRole, + // useTypeahead +} from '@floating-ui/react'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {mergeRegister} from '@lexical/utils'; import { @@ -24,11 +41,15 @@ import { TextNode, } from 'lexical'; import { + // Children, MutableRefObject, - ReactPortal, + // isValidElement, + // cloneElement, + // forwardRef, + // ReactPortal, useCallback, useEffect, - useMemo, + // useMemo, useRef, useState, } from 'react'; @@ -61,17 +82,6 @@ export class MenuOption { } } -export type MenuRenderFn = ( - anchorElementRef: MutableRefObject, - itemProps: { - selectedIndex: number | null; - selectOptionAndCleanUp: (option: TOption) => void; - setHighlightedIndex: (index: number) => void; - options: Array; - }, - matchingString: string | null, -) => ReactPortal | JSX.Element | null; - const scrollIntoViewIfNeeded = (target: HTMLElement) => { const typeaheadContainerNode = document.getElementById('typeahead-menu'); if (!typeaheadContainerNode) { @@ -252,13 +262,48 @@ export const SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND: LexicalCommand<{ option: MenuOption; }> = createCommand('SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND'); +function MenuItem({ + index, + isSelected, + onClick, + onMouseEnter, + option, +}: { + index: number; + isSelected: boolean; + onClick: () => void; + onMouseEnter: () => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + option: any; // ComponentPickerOption +}) { + let className = 'item'; + if (isSelected) { + className += ' selected'; + } + return ( +
  • + {option.icon} + {/* maybe support emoji and .name */} + {option.title} +
  • + ); +} + export function LexicalMenu({ close, editor, anchorElementRef, resolution, options, - menuRenderFn, onSelectOption, shouldSplitNodeWithQuery = false, commandPriority = COMMAND_PRIORITY_LOW, @@ -269,7 +314,6 @@ export function LexicalMenu({ resolution: MenuResolution; options: Array; shouldSplitNodeWithQuery?: boolean; - menuRenderFn: MenuRenderFn; onSelectOption: ( option: TOption, textNodeContainingQuery: TextNode | null, @@ -458,20 +502,103 @@ export function LexicalMenu({ commandPriority, ]); - const listItemProps = useMemo( - () => ({ - options, - selectOptionAndCleanUp, - selectedIndex, - setHighlightedIndex, - }), - [selectOptionAndCleanUp, selectedIndex, options], - ); + // const listItemProps = useMemo( + // () => ({ + // options, + // selectOptionAndCleanUp, + // selectedIndex, + // setHighlightedIndex, + // }), + // [selectOptionAndCleanUp, selectedIndex, options], + // ); + + // const [activeIndex, setActiveIndex] = useState(null); + const [isOpen, setIsOpen] = useState(Boolean(anchorElementRef.current)); + + const {refs, floatingStyles, context} = useFloating({ + elements: { + reference: anchorElementRef.current, + }, + open: isOpen, + // eslint-disable-next-line sort-keys-fix/sort-keys-fix + onOpenChange: setIsOpen, + // eslint-disable-next-line sort-keys-fix/sort-keys-fix + middleware: [ + // eslint-disable-next-line sort-keys-fix/sort-keys-fix + // offset({ mainAxis: 0, alignmentAxis: 0 }), + // autoPlacement(), + flip({ + fallbackPlacements: ['top-start'], + }), + shift({padding: 10}), + ], + placement: 'bottom-start', + strategy: 'fixed', + // whileElementsMounted: autoUpdate + }); + + const elementsRef = useRef>([]); + const labelsRef = useRef>([]); + + const role = useRole(context, {role: 'menu'}); + const dismiss = useDismiss(context); + const listNavigation = useListNavigation(context, { + activeIndex: selectedIndex, + listRef: elementsRef, + onNavigate: setHighlightedIndex, + }); + // console.log('context', context) + // const typeahead = useTypeahead(context, { + // enabled: isOpen, + // listRef: listContentRef, + // onMatch: setHighlightedIndex, + // // eslint-disable-next-line sort-keys-fix/sort-keys-fix + // activeIndex: selectedIndex + // }); + + // const { getFloatingProps, getItemProps } = useInteractions([ + const {getFloatingProps} = useInteractions([ + role, + dismiss, + listNavigation, + // typeahead + ]); - return menuRenderFn( - anchorElementRef, - listItemProps, - resolution.match ? resolution.match.matchingString : '', + return ( + + {isOpen && ( + + {/* */} + +
    + +
      + {options.map((option, index: number) => ( + { + setHighlightedIndex(index); + selectOptionAndCleanUp(option); + }} + onMouseEnter={() => { + setHighlightedIndex(index); + }} + key={option.key} + option={option} + /> + ))} +
    +
    +
    +
    +
    + )} +
    ); }