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} + /> + ))} +
    +
    +
    +
    +
    + )} +
    ); }