From 56cfbb6c0df29371858a25c8193048caff394ace Mon Sep 17 00:00:00 2001 From: zbeyens Date: Thu, 7 Jul 2022 12:38:17 +0200 Subject: [PATCH 01/21] feat --- examples/src/PlaygroundApp.tsx | 157 +++++++++++++++++- .../core/src/atoms/createAtomStore.spec.ts | 5 +- packages/core/src/atoms/createAtomStore.ts | 2 + packages/core/src/hooks/plate/index.ts | 1 + .../core/src/hooks/plate/useElementProps.ts | 39 +++++ .../plate/createPlateElementComponent.ts | 33 ++++ packages/core/src/utils/plate/index.ts | 1 + packages/floating/.npmignore | 3 + packages/floating/README.md | 15 ++ packages/floating/package.json | 42 +++++ packages/floating/src/Menu/Menu.styles.ts | 139 ++++++++++++++++ packages/floating/src/Menu/Menu.tsx | 20 +++ packages/floating/src/Menu/Menu.types.ts | 11 ++ packages/floating/src/Menu/index.ts | 7 + .../src/MenuButton/MenuButton.styles.ts | 17 ++ .../floating/src/MenuButton/MenuButton.tsx | 44 +++++ .../src/MenuButton/MenuButton.types.ts | 17 ++ packages/floating/src/MenuButton/index.ts | 7 + packages/floating/src/index.tsx | 16 ++ packages/floating/tsconfig.json | 7 + packages/nodes/image/src/components/Image.tsx | 32 +--- packages/nodes/link/src/components/Link.tsx | 13 ++ packages/nodes/link/src/components/index.ts | 5 + packages/nodes/link/src/createLinkPlugin.ts | 2 +- packages/nodes/link/src/index.ts | 1 + .../src/ImageElement/ImageElement.styles.ts | 5 +- .../src/ImageElement/ImageElement.types.ts | 7 +- .../link/src/LinkElement/LinkElement.tsx | 27 +-- packages/ui/popover/src/Popover/Popover.tsx | 14 ++ .../src/getSelectionBoundingClientRect.ts | 1 + 30 files changed, 630 insertions(+), 60 deletions(-) create mode 100644 packages/core/src/hooks/plate/useElementProps.ts create mode 100644 packages/core/src/utils/plate/createPlateElementComponent.ts create mode 100644 packages/floating/.npmignore create mode 100644 packages/floating/README.md create mode 100644 packages/floating/package.json create mode 100644 packages/floating/src/Menu/Menu.styles.ts create mode 100644 packages/floating/src/Menu/Menu.tsx create mode 100644 packages/floating/src/Menu/Menu.types.ts create mode 100644 packages/floating/src/Menu/index.ts create mode 100644 packages/floating/src/MenuButton/MenuButton.styles.ts create mode 100644 packages/floating/src/MenuButton/MenuButton.tsx create mode 100644 packages/floating/src/MenuButton/MenuButton.types.ts create mode 100644 packages/floating/src/MenuButton/index.ts create mode 100644 packages/floating/src/index.tsx create mode 100644 packages/floating/tsconfig.json create mode 100644 packages/nodes/link/src/components/Link.tsx create mode 100644 packages/nodes/link/src/components/index.ts diff --git a/examples/src/PlaygroundApp.tsx b/examples/src/PlaygroundApp.tsx index 72f140cb66..0b89508f13 100644 --- a/examples/src/PlaygroundApp.tsx +++ b/examples/src/PlaygroundApp.tsx @@ -1,8 +1,11 @@ -import React, { CSSProperties, useMemo, useRef } from 'react'; +import React, { CSSProperties, useEffect, useMemo, useRef } from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { AutoformatPlugin, + Combobox, + comboboxActions, + ComboboxItemProps, createAlignPlugin, createAutoformatPlugin, createBlockquotePlugin, @@ -44,9 +47,15 @@ import { createTrailingBlockPlugin, createUnderlinePlugin, ELEMENT_CODE_BLOCK, + getSelectionBoundingClientRect, + isDefined, MentionCombobox, Plate, StyledElement, + useComboboxSelectors, + useEditorState, + usePopperPosition, + UsePopperPositionOptions, } from '@udecode/plate'; import { createJuicePlugin } from '@udecode/plate-juice'; import { @@ -54,6 +63,7 @@ import { ELEMENT_EXCALIDRAW, ExcalidrawElement, } from '@udecode/plate-ui-excalidraw'; +import { useFocused } from 'slate-react'; import { alignPlugin } from './align/alignPlugin'; import { autoformatPlugin } from './autoformat/autoformatPlugin'; import { MarkBalloonToolbar } from './balloon-toolbar/MarkBalloonToolbar'; @@ -91,6 +101,148 @@ const styles: Record = { container: { position: 'relative' }, }; +export const COMBOBOX_TRIGGER_SLASH_COMMAND = '/'; +export const COMBOBOX_KEY_SLASH_COMMAND = 'slash_command'; + +const COMMAND_BLOCK_ADD_PARAGRAPH = { + id: 'block.add.paragraph', + label: 'Block: Add Paragraph', + inlineLabel: 'Paragraph', + description: 'Plain text', + keybinding: '⌘⏎', +}; + +const SlashCommandComboboxEffect = () => { + const search = useComboboxSelectors.text(); + + useEffect(() => { + const commands = [COMMAND_BLOCK_ADD_PARAGRAPH, COMMAND_BLOCK_ADD_PARAGRAPH]; + + const items = commands + .map((item) => ({ + key: item.id, + text: item.inlineLabel, + data: { + description: item.description, + keybinding: item.keybinding, + }, + })) + .filter( + (c) => + !isDefined(search) || + c.text?.toLowerCase().includes(search.toLowerCase()) + ); + + comboboxActions.items(items); + }, [search]); + + return null; +}; + +const SlashCommandComboboxItem = ({ item }: ComboboxItemProps<{}>) => { + const data = item.data as any; + + return ( +
+
+
+
{item.text}
+ {isDefined(data.description) && ( +
+ {data.description} +
+ )} +
+ {isDefined(data.keybinding) && ( +
+ {/* {data.keybinding} */} +
+ )} +
+
+ ); +}; + +const SlashCommandCombobox = () => ( + {}} + /> +); + +export const useBalloonToolbarPopper = (options: UsePopperPositionOptions) => { + const editor = useEditorState(); + const focused = useFocused(); + + // const [isHidden, setIsHidden] = useState(true); + + // useEffect(() => { + // if ( + // !selectionExpanded || + // !selectionText || + // !focused || + // editor.id !== focusedEditorId + // ) { + // setIsHidden(true); + // } else if (selectionText && selectionExpanded) { + // setIsHidden(false); + // } + // }, [ + // editor.id, + // editor.selection, + // focused, + // focusedEditorId, + // selectionExpanded, + // selectionText, + // ]); + + const popperResult = usePopperPosition({ + // isHidden, + getBoundingClientRect: getSelectionBoundingClientRect, + ...options, + }); + + // const selectionTextLength = selectionText?.length ?? 0; + const { update } = popperResult; + // + useEffect(() => { + update?.(); + }, [editor.selection, update]); + + return popperResult; +}; + +const A = () => { + const popperRef = useRef(null); + + const popperOptions: UsePopperPositionOptions = { + popperElement: popperRef.current, + placement: 'bottom-start', + offset: [0, 8], + }; + + const { styles: popperStyles, attributes } = useBalloonToolbarPopper( + popperOptions + ); + + return ( +
+ ); +}; + const App = () => { const containerRef = useRef(null); @@ -164,9 +316,12 @@ const App = () => { initialValue={playgroundValue} plugins={plugins} > + + + diff --git a/packages/core/src/atoms/createAtomStore.spec.ts b/packages/core/src/atoms/createAtomStore.spec.ts index 96d4f63e96..b8c8b314a6 100644 --- a/packages/core/src/atoms/createAtomStore.spec.ts +++ b/packages/core/src/atoms/createAtomStore.spec.ts @@ -23,10 +23,12 @@ describe('createAtomStore', () => { }, { name: 'named', + scope: 'named', } ); expect(name).toBe('named'); + expect(namedStore.scope).toBe('named'); expect(namedStore.atom.b).toBeDefined(); expect(useNamedStore().get.b).toBeDefined(); expect(useNamedStore().use.b).toBeDefined(); @@ -51,11 +53,12 @@ describe('createAtomStore', () => { }, { name: 'renamed', + scope: 'renamed', } ); expect(name).toBe('renamed'); - expect(renamedStore.atom.b).toBeDefined(); + expect(renamedStore.scope).toBe('renamed'); expect(useRenamedStore().get.b).toBeDefined(); expect(useRenamedStore().use.b).toBeDefined(); expect(useRenamedStore().set.b).toBeDefined(); diff --git a/packages/core/src/atoms/createAtomStore.ts b/packages/core/src/atoms/createAtomStore.ts index 1cd108c3e6..781cae0771 100644 --- a/packages/core/src/atoms/createAtomStore.ts +++ b/packages/core/src/atoms/createAtomStore.ts @@ -23,6 +23,7 @@ export type AtomStoreApi = { } & { [key in keyof Record, {}>]: { atom: AtomRecord; + scope?: Scope; extend: ( extendedState: ET, options?: Omit< @@ -121,6 +122,7 @@ export const createAtomStore = ( ...api, [storeIndex]: { ...api[storeIndex], + scope: storeScope, extend: (extendedState: any, options: any) => createAtomStore(extendedState, { scope: storeScope, diff --git a/packages/core/src/hooks/plate/index.ts b/packages/core/src/hooks/plate/index.ts index 4d8738a79d..f9b4723178 100644 --- a/packages/core/src/hooks/plate/index.ts +++ b/packages/core/src/hooks/plate/index.ts @@ -3,6 +3,7 @@ */ export * from './useEditableProps'; +export * from './useElementProps'; export * from './usePlate'; export * from './usePlateEffects'; export * from './usePlateStoreEffects'; diff --git a/packages/core/src/hooks/plate/useElementProps.ts b/packages/core/src/hooks/plate/useElementProps.ts new file mode 100644 index 0000000000..d215c81ebe --- /dev/null +++ b/packages/core/src/hooks/plate/useElementProps.ts @@ -0,0 +1,39 @@ +import { TElement } from '../../slate/index'; +import { As, HTMLPropsAs, PlateRenderElementProps } from '../../types/index'; +import { useComposedRef } from '../react/index'; + +export type UseElementPropsOptions< + T extends TElement = TElement, + A extends As = 'div' +> = { + /** + * Get HTML attributes from Slate element. Alternative to `PlatePlugin.props`. + */ + elementToAttributes?: (element: T) => Partial>; +} & PlateRenderElementProps & + HTMLPropsAs; + +/** + * Get root element props for Slate element. + */ +export const useElementProps = < + T extends TElement = TElement, + A extends As = 'div' +>({ + attributes, + nodeProps, + element, + editor, + elementToAttributes, + ...props +}: UseElementPropsOptions): HTMLPropsAs => { + const htmlProps: HTMLPropsAs<'div'> = { + ...attributes, + ...props, + ...nodeProps, + ...(elementToAttributes?.(element as T) ?? {}), + ref: useComposedRef(props.ref, attributes.ref), + }; + + return htmlProps as HTMLPropsAs; +}; diff --git a/packages/core/src/utils/plate/createPlateElementComponent.ts b/packages/core/src/utils/plate/createPlateElementComponent.ts new file mode 100644 index 0000000000..1de4da4bc0 --- /dev/null +++ b/packages/core/src/utils/plate/createPlateElementComponent.ts @@ -0,0 +1,33 @@ +import { ElementType } from 'react'; +import { useElementProps, UseElementPropsOptions } from '../../hooks/index'; +import { TElement, Value } from '../../slate/index'; +import { As, HTMLPropsAs, PlateRenderElementProps } from '../../types/index'; +import { createComponentAs, createElementAs } from '../react/index'; + +export type CreatePlateElementComponentOptions< + T extends TElement = TElement, + A extends As = 'div' +> = { + as?: ElementType; +} & Pick, 'elementToAttributes'>; + +/** + * Create the top-level React component for a Slate element. + */ +export const createPlateElementComponent = < + T extends TElement = TElement, + A extends As = 'div' +>({ + as = 'div', + elementToAttributes, +}: CreatePlateElementComponentOptions = {}) => + createComponentAs & HTMLPropsAs>( + (props) => { + const htmlProps = useElementProps({ + ...(props as any), + elementToAttributes, + }); + + return createElementAs(as, htmlProps); + } + ); diff --git a/packages/core/src/utils/plate/index.ts b/packages/core/src/utils/plate/index.ts index 3d6053e924..ee9ac45e00 100644 --- a/packages/core/src/utils/plate/index.ts +++ b/packages/core/src/utils/plate/index.ts @@ -3,6 +3,7 @@ */ export * from './createPlateEditor'; +export * from './createPlateElementComponent'; export * from './createPluginFactory'; export * from './createPlugins'; export * from './flattenDeepPlugins'; diff --git a/packages/floating/.npmignore b/packages/floating/.npmignore new file mode 100644 index 0000000000..7d3b305b17 --- /dev/null +++ b/packages/floating/.npmignore @@ -0,0 +1,3 @@ +__tests__ +__test-utils__ +__mocks__ diff --git a/packages/floating/README.md b/packages/floating/README.md new file mode 100644 index 0000000000..e60df064ca --- /dev/null +++ b/packages/floating/README.md @@ -0,0 +1,15 @@ +# Plate menu UI + +This package implements the menu UI for Plate. + +## Documentation + +See [@szhsin/react-menu](https://github.com/szhsin/react-menu). + +## API + +See the [API documentation](https://plate-api.udecode.io/globals.html). + +## License + +[MIT](../../../LICENSE) diff --git a/packages/floating/package.json b/packages/floating/package.json new file mode 100644 index 0000000000..28e9a341cc --- /dev/null +++ b/packages/floating/package.json @@ -0,0 +1,42 @@ +{ + "name": "@udecode/plate-floating", + "version": "6.1.0", + "description": "Floating UI for Plate", + "license": "MIT", + "homepage": "https://plate.udecode.io", + "repository": { + "type": "git", + "url": "https://github.com/udecode/plate.git", + "directory": "packages/ui/menu" + }, + "bugs": { + "url": "https://github.com/udecode/plate/issues" + }, + "main": "dist/index.js", + "module": "dist/index.es.js", + "files": [ + "dist" + ], + "types": "dist/index.d.ts", + "dependencies": { + "@szhsin/react-menu": "^2.1.0", + "@udecode/plate-core": "6.0.0", + "@udecode/plate-styled-components": "6.0.0", + "@udecode/plate-ui-button": "6.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.66.1", + "slate-history": ">=0.66.0", + "slate-react": ">=0.74.2" + }, + "keywords": [ + "plate", + "plugin", + "slate" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/packages/floating/src/Menu/Menu.styles.ts b/packages/floating/src/Menu/Menu.styles.ts new file mode 100644 index 0000000000..9f286eb765 --- /dev/null +++ b/packages/floating/src/Menu/Menu.styles.ts @@ -0,0 +1,139 @@ +import { createStyles } from '@udecode/plate-styled-components'; +import { css } from 'styled-components'; +import tw from 'twin.macro'; +import { MenuStyleProps } from './Menu.types'; + +export const getMenuStyles = (props: MenuStyleProps) => + createStyles( + { prefixClassNames: 'Menu', ...props }, + { + root: css` + .szh-menu-container { + ${tw`relative w-0 h-0`}; + } + + .szh-menu { + ${tw`absolute bg-white`} + list-style: none; + width: max-content; + z-index: 100; + border: 1px solid rgba(0, 0, 0, 0.1); + } + .szh-menu:focus { + ${tw`outline-none`}; + } + .szh-menu--state-closed { + ${tw`hidden`}; + } + .szh-menu__arrow { + ${tw`absolute bg-white`}; + width: 0.75rem; + height: 0.75rem; + border: 1px solid transparent; + border-left-color: rgba(0, 0, 0, 0.1); + border-top-color: rgba(0, 0, 0, 0.1); + z-index: -1; + } + .szh-menu__arrow--dir-left { + right: -0.375rem; + transform: translateY(-50%) rotate(135deg); + } + .szh-menu__arrow--dir-right { + left: -0.375rem; + transform: translateY(-50%) rotate(-45deg); + } + .szh-menu__arrow--dir-top { + bottom: -0.375rem; + transform: translateX(-50%) rotate(-135deg); + } + .szh-menu__arrow--dir-bottom { + top: -0.375rem; + transform: translateX(-50%) rotate(45deg); + } + .szh-menu__item { + ${tw`cursor-pointer`}; + } + .szh-menu__item:focus { + ${tw`outline-none`}; + } + .szh-menu__item--hover { + ${tw`bg-gray-100`}; + } + .szh-menu__item--focusable { + ${tw`cursor-default`}; + background-color: inherit; + } + .szh-menu__item--disabled { + ${tw`cursor-default`}; + //color: #aaa; + } + .szh-menu__submenu { + ${tw`relative m-0`}; + } + .szh-menu__group { + } + .szh-menu__radio-group { + ${tw`m-0 p-0`}; + list-style: none; + } + .szh-menu__divider { + ${tw`bg-gray-200 h-px`}; + margin: 0.5rem 0; + } + + .szh-menu { + ${tw`select-none border-none`}; + border-radius: 0.25rem; + min-width: 10rem; + padding: 0.5rem 0; + box-shadow: rgba(9, 30, 66, 0.31) 0 0 1px, + rgba(9, 30, 66, 0.25) 0 4px 8px -2px; + } + .szh-menu__item { + ${tw`relative flex items-center m-0`}; + padding: 0.375rem 1.5rem; + } + .szh-menu-container--itemTransition .szh-menu__item { + } + .szh-menu__item--active { + ${tw`bg-gray-200`}; + } + .szh-menu__item--type-radio { + padding-left: 2.2rem; + } + .szh-menu__item--type-radio::before { + ${tw`absolute`}; + content: '○'; + left: 0.8rem; + top: 0.55rem; + font-size: 0.8rem; + } + .szh-menu__item--type-radio.szh-menu__item--checked::before { + content: '●'; + } + .szh-menu__item--type-checkbox { + padding-left: 2.2rem; + } + .szh-menu__item--type-checkbox::before { + ${tw`absolute`}; + left: 0.8rem; + } + .szh-menu__item--type-checkbox.szh-menu__item--checked::before { + content: '✔'; + } + .szh-menu__submenu > .szh-menu__item { + padding-right: 2.5rem; + } + .szh-menu__submenu > .szh-menu__item::after { + ${tw`absolute`}; + content: '❯'; + right: 1rem; + } + .szh-menu__header { + font-size: 0.8em; + padding: 0.2rem 1.5rem; + text-transform: uppercase; + } + `, + } + ); diff --git a/packages/floating/src/Menu/Menu.tsx b/packages/floating/src/Menu/Menu.tsx new file mode 100644 index 0000000000..1a0822abd9 --- /dev/null +++ b/packages/floating/src/Menu/Menu.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { getMenuStyles } from './Menu.styles'; +import { MenuProps } from './Menu.types'; + +export const Menu = (props: MenuProps) => { + const { rootProps, ...menuProps } = props; + + const { root } = getMenuStyles(props); + + return ( +
+ {/* */} +
+ ); +}; diff --git a/packages/floating/src/Menu/Menu.types.ts b/packages/floating/src/Menu/Menu.types.ts new file mode 100644 index 0000000000..256b3f5ef8 --- /dev/null +++ b/packages/floating/src/Menu/Menu.types.ts @@ -0,0 +1,11 @@ +import { HTMLAttributes } from 'react'; +import { MenuProps as MenuBaseProps } from '@szhsin/react-menu'; +import { StyledProps } from '@udecode/plate-styled-components'; + +export interface MenuStyleProps extends MenuProps {} + +export interface MenuStyles {} + +export interface MenuProps extends StyledProps, MenuBaseProps { + rootProps?: HTMLAttributes; +} diff --git a/packages/floating/src/Menu/index.ts b/packages/floating/src/Menu/index.ts new file mode 100644 index 0000000000..a7b0ee345d --- /dev/null +++ b/packages/floating/src/Menu/index.ts @@ -0,0 +1,7 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './Menu.styles'; +export * from './Menu'; +export * from './Menu.types'; diff --git a/packages/floating/src/MenuButton/MenuButton.styles.ts b/packages/floating/src/MenuButton/MenuButton.styles.ts new file mode 100644 index 0000000000..b7c1909f1a --- /dev/null +++ b/packages/floating/src/MenuButton/MenuButton.styles.ts @@ -0,0 +1,17 @@ +import { createStyles, StyledProps } from '@udecode/plate-styled-components'; +// import { getButtonStyles } from '@udecode/plate-ui-button'; +import tw from 'twin.macro'; +import { MenuButtonStyles } from './MenuButton.types'; + +export const getMenuButtonStyles = (props: StyledProps) => { + // const { root } = getButtonStyles(props); + + return createStyles( + { prefixClassNames: 'MenuButton', ...props }, + { + // root: [...root.css], + root: [], + chevron: tw`ml-1`, + } + ); +}; diff --git a/packages/floating/src/MenuButton/MenuButton.tsx b/packages/floating/src/MenuButton/MenuButton.tsx new file mode 100644 index 0000000000..cbd0c3eaa5 --- /dev/null +++ b/packages/floating/src/MenuButton/MenuButton.tsx @@ -0,0 +1,44 @@ +import React, { forwardRef } from 'react'; +import { MenuButton as MenuButtonBase } from '@szhsin/react-menu'; +import { ChevronDownIcon } from '@udecode/plate-ui-button'; +import { getMenuButtonStyles } from './MenuButton.styles'; +import { MenuButtonProps } from './MenuButton.types'; + +export const MenuButton = forwardRef( + ( + { + children, + menuButtonStyles, + styles, + prefixClassNames, + chevronProps, + ...props + }: MenuButtonProps, + ref + ) => { + const { root, chevron } = getMenuButtonStyles({ + styles, + prefixClassNames, + }); + + return ( + + {children} + + + + ); + } +); diff --git a/packages/floating/src/MenuButton/MenuButton.types.ts b/packages/floating/src/MenuButton/MenuButton.types.ts new file mode 100644 index 0000000000..c61f7ba336 --- /dev/null +++ b/packages/floating/src/MenuButton/MenuButton.types.ts @@ -0,0 +1,17 @@ +import { SVGProps } from 'react'; +import { MenuButtonProps as MenuButtonBaseProps } from '@szhsin/react-menu'; +import { StyledProps } from '@udecode/plate-styled-components'; +import { CSSProp } from 'styled-components'; + +export interface MenuButtonStyleProps extends MenuButtonProps {} + +export interface MenuButtonStyles { + chevron: CSSProp; +} + +export interface MenuButtonProps + extends Omit, + StyledProps { + menuButtonStyles?: MenuButtonBaseProps['styles']; + chevronProps?: SVGProps; +} diff --git a/packages/floating/src/MenuButton/index.ts b/packages/floating/src/MenuButton/index.ts new file mode 100644 index 0000000000..7ddd2d4901 --- /dev/null +++ b/packages/floating/src/MenuButton/index.ts @@ -0,0 +1,7 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './MenuButton.styles'; +export * from './MenuButton'; +export * from './MenuButton.types'; diff --git a/packages/floating/src/index.tsx b/packages/floating/src/index.tsx new file mode 100644 index 0000000000..9484ecada3 --- /dev/null +++ b/packages/floating/src/index.tsx @@ -0,0 +1,16 @@ +// export { +// applyHOC, +// applyStatics, +// useMenuState, +// ControlledMenu, +// SubMenu, +// MenuItem, +// FocusableItem, +// MenuDivider, +// MenuHeader, +// MenuGroup, +// MenuRadioGroup, +// } from '@szhsin/react-menu'; + +export * from './Menu'; +export * from './MenuButton'; diff --git a/packages/floating/tsconfig.json b/packages/floating/tsconfig.json new file mode 100644 index 0000000000..f10b50a74c --- /dev/null +++ b/packages/floating/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../config/tsconfig.build.json", + "compilerOptions": { + "declarationDir": "./dist" + }, + "include": ["src"] +} diff --git a/packages/nodes/image/src/components/Image.tsx b/packages/nodes/image/src/components/Image.tsx index 5d8ff2eb4d..50a70ff152 100644 --- a/packages/nodes/image/src/components/Image.tsx +++ b/packages/nodes/image/src/components/Image.tsx @@ -1,14 +1,9 @@ import { CSSProperties } from 'react'; import { createAtomStore, - createComponentAs, - createElementAs, + createPlateElementComponent, createStore, - HTMLPropsAs, - PlateRenderElementProps, TPath, - useComposedRef, - Value, } from '@udecode/plate-core'; import { TImageElement } from '../types'; import { ImageCaption } from './ImageCaption'; @@ -20,6 +15,7 @@ export const { imageStore, useImageStore } = createAtomStore( { width: 0 as CSSProperties['width'], }, + // NOTE: scope: ELEMENT_IMAGE is undefined (bundle issue) { name: 'image' as const, scope: 'img' } ); @@ -35,29 +31,7 @@ export const imageGlobalStore = createStore('image')({ focusStartCaptionPath: null as TPath | null, }); -export type ImageProps = PlateRenderElementProps & - HTMLPropsAs<'div'>; - -export const useElementProps = ({ - attributes, - nodeProps, - element, - editor, - ...props -}: ImageProps): HTMLPropsAs<'div'> => { - return { - ...attributes, - ...props, - ...nodeProps, - ref: useComposedRef(props.ref, attributes.ref), - }; -}; - -export const ImageRoot = createComponentAs((props) => { - const htmlProps = useElementProps(props as any); - - return createElementAs('div', htmlProps); -}); +export const ImageRoot = createPlateElementComponent(); export const Image = { Root: ImageRoot, diff --git a/packages/nodes/link/src/components/Link.tsx b/packages/nodes/link/src/components/Link.tsx new file mode 100644 index 0000000000..3aac6a522f --- /dev/null +++ b/packages/nodes/link/src/components/Link.tsx @@ -0,0 +1,13 @@ +import { createPlateElementComponent } from '@udecode/plate-core'; +import { TLinkElement } from '../types'; + +export const LinkRoot = createPlateElementComponent({ + as: 'a', + elementToAttributes: (element) => ({ + href: element.url, + }), +}); + +export const Link = { + Root: LinkRoot, +}; diff --git a/packages/nodes/link/src/components/index.ts b/packages/nodes/link/src/components/index.ts new file mode 100644 index 0000000000..ee261355b3 --- /dev/null +++ b/packages/nodes/link/src/components/index.ts @@ -0,0 +1,5 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './Link'; diff --git a/packages/nodes/link/src/createLinkPlugin.ts b/packages/nodes/link/src/createLinkPlugin.ts index ea7bf9e07d..e49d8f6593 100644 --- a/packages/nodes/link/src/createLinkPlugin.ts +++ b/packages/nodes/link/src/createLinkPlugin.ts @@ -15,7 +15,7 @@ export const createLinkPlugin = createPluginFactory({ key: ELEMENT_LINK, isElement: true, isInline: true, - props: ({ element }) => ({ nodeProps: { url: element?.url } }), + props: ({ element }) => ({ nodeProps: { href: element?.url } }), handlers: { onKeyDown: onKeyDownLink, }, diff --git a/packages/nodes/link/src/index.ts b/packages/nodes/link/src/index.ts index d05fe64fc3..8e36fa2b1f 100644 --- a/packages/nodes/link/src/index.ts +++ b/packages/nodes/link/src/index.ts @@ -6,4 +6,5 @@ export * from './createLinkPlugin'; export * from './onKeyDownLink'; export * from './types'; export * from './withLink'; +export * from './components/index'; export * from './transforms/index'; diff --git a/packages/ui/nodes/image/src/ImageElement/ImageElement.styles.ts b/packages/ui/nodes/image/src/ImageElement/ImageElement.styles.ts index 326e2f2722..5e93e24c5e 100644 --- a/packages/ui/nodes/image/src/ImageElement/ImageElement.styles.ts +++ b/packages/ui/nodes/image/src/ImageElement/ImageElement.styles.ts @@ -1,12 +1,9 @@ -import { Value } from '@udecode/plate-core'; import { createStyles } from '@udecode/plate-styled-components'; import { css } from 'styled-components'; import tw from 'twin.macro'; import { ImageElementStyleProps } from './ImageElement.types'; -export const getImageElementStyles = ( - props: ImageElementStyleProps -) => { +export const getImageElementStyles = (props: ImageElementStyleProps) => { const { focused, selected, diff --git a/packages/ui/nodes/image/src/ImageElement/ImageElement.types.ts b/packages/ui/nodes/image/src/ImageElement/ImageElement.types.ts index fab7dd1673..f7230276ce 100644 --- a/packages/ui/nodes/image/src/ImageElement/ImageElement.types.ts +++ b/packages/ui/nodes/image/src/ImageElement/ImageElement.types.ts @@ -4,8 +4,7 @@ import { StyledElementProps } from '@udecode/plate-styled-components'; import { ResizableProps } from 're-resizable'; import { CSSProp } from 'styled-components'; -export interface ImageElementStyleProps - extends ImageElementProps { +export interface ImageElementStyleProps extends ImageElementProps { selected?: boolean; focused?: boolean; } @@ -40,8 +39,8 @@ export interface ImageElementPropsCaption { readOnly?: boolean; } -export interface ImageElementProps - extends StyledElementProps, +export interface ImageElementProps + extends StyledElementProps, Pick, 'align'> { resizableProps?: Omit; diff --git a/packages/ui/nodes/link/src/LinkElement/LinkElement.tsx b/packages/ui/nodes/link/src/LinkElement/LinkElement.tsx index 238161096f..95327c37de 100644 --- a/packages/ui/nodes/link/src/LinkElement/LinkElement.tsx +++ b/packages/ui/nodes/link/src/LinkElement/LinkElement.tsx @@ -1,30 +1,17 @@ import React from 'react'; import { Value } from '@udecode/plate-core'; -import { TLinkElement } from '@udecode/plate-link'; -import { - getRootProps, - StyledElementProps, -} from '@udecode/plate-styled-components'; +import { Link, TLinkElement } from '@udecode/plate-link'; +import { StyledElementProps } from '@udecode/plate-styled-components'; import { getLinkElementStyles } from './LinkElement.styles'; -export const LinkElement = ( - props: StyledElementProps -) => { - const { attributes, children, nodeProps, element } = props; +export const LinkElement = (props: StyledElementProps) => { + const { as, ...rootProps } = props; - const rootProps = getRootProps(props); const { root } = getLinkElementStyles(props); return ( -
- {children} - + + + ); }; diff --git a/packages/ui/popover/src/Popover/Popover.tsx b/packages/ui/popover/src/Popover/Popover.tsx index 29a7f44424..e93b8d27e0 100644 --- a/packages/ui/popover/src/Popover/Popover.tsx +++ b/packages/ui/popover/src/Popover/Popover.tsx @@ -1,5 +1,11 @@ import React, { HTMLAttributes } from 'react'; import Tippy, { TippyProps } from '@tippyjs/react'; +import { createComponentAs } from '@udecode/plate-core'; +import { + ImageCaptionTextareaProps, + TextareaAutosize, + useImageCaptionTextarea, +} from '@udecode/plate-image/src/index'; import { StyledProps } from '@udecode/plate-styled-components'; import { useReadOnly, useSelected } from 'slate-react'; import { getPopoverStyles } from './Popover.styles'; @@ -46,3 +52,11 @@ export const Popover = ({ rootProps, content, ...props }: PopoverProps) => { return ; }; + +export const ImageCaptionTextarea = createComponentAs( + ({ as, ...props }) => { + const htmlProps = useImageCaptionTextarea({ as: as as any, ...props }); + + return ; + } +); diff --git a/packages/ui/popper/src/getSelectionBoundingClientRect.ts b/packages/ui/popper/src/getSelectionBoundingClientRect.ts index 2daab6b7f6..a94ee3cb49 100644 --- a/packages/ui/popper/src/getSelectionBoundingClientRect.ts +++ b/packages/ui/popper/src/getSelectionBoundingClientRect.ts @@ -6,6 +6,7 @@ export const getSelectionBoundingClientRect = () => { if (!domSelection || domSelection.rangeCount < 1) return; const domRange = domSelection.getRangeAt(0); + console.log(domRange); return domRange.getBoundingClientRect(); }; From 8bbcf056e93aec034833a4015d51190a05f413bf Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 8 Jul 2022 01:50:54 +0200 Subject: [PATCH 02/21] feat --- examples/apps/next/next.config.js | 1 + examples/src/PlaygroundApp.tsx | 2 +- packages/core/src/utils/misc/index.ts | 1 + packages/core/src/utils/misc/mergeProps.ts | 55 +++++++ packages/floating/README.md | 6 +- packages/floating/package.json | 10 +- packages/floating/src/Menu/Menu.styles.ts | 139 ------------------ packages/floating/src/Menu/Menu.tsx | 20 --- packages/floating/src/Menu/Menu.types.ts | 11 -- packages/floating/src/Menu/index.ts | 7 - .../src/MenuButton/MenuButton.styles.ts | 17 --- .../floating/src/MenuButton/MenuButton.tsx | 44 ------ .../src/MenuButton/MenuButton.types.ts | 17 --- packages/floating/src/MenuButton/index.ts | 7 - packages/floating/src/createVirtualElement.ts | 16 ++ packages/floating/src/floating-ui.ts | 1 + packages/floating/src/hooks/index.ts | 5 + .../floating/src/hooks/useVirtualFloating.ts | 98 ++++++++++++ packages/floating/src/index.ts | 8 + packages/floating/src/index.tsx | 16 -- .../src/utils/getRangeBoundingClientRect.ts | 17 +++ .../utils/getSelectionBoundingClientRect.ts | 17 +++ packages/floating/src/utils/index.ts | 6 + packages/floating/tsconfig.json | 2 +- .../link/src/LinkElement/LinkElement.tsx | 6 +- packages/ui/popover/src/Popover/Popover.tsx | 14 -- .../src/getSelectionBoundingClientRect.ts | 1 - packages/ui/toolbar/package.json | 1 + .../BalloonToolbar/BalloonToolbar.styles.ts | 2 +- .../src/BalloonToolbar/BalloonToolbar.tsx | 31 ++-- .../BalloonToolbar/BalloonToolbar.types.ts | 8 +- .../ui/toolbar/src/BalloonToolbar/index.ts | 2 +- .../BalloonToolbar/useBalloonToolbarPopper.ts | 61 -------- .../src/BalloonToolbar/useFloatingToolbar.ts | 103 +++++++++++++ yarn.lock | 70 ++++++++- 35 files changed, 426 insertions(+), 396 deletions(-) create mode 100644 packages/core/src/utils/misc/mergeProps.ts delete mode 100644 packages/floating/src/Menu/Menu.styles.ts delete mode 100644 packages/floating/src/Menu/Menu.tsx delete mode 100644 packages/floating/src/Menu/Menu.types.ts delete mode 100644 packages/floating/src/Menu/index.ts delete mode 100644 packages/floating/src/MenuButton/MenuButton.styles.ts delete mode 100644 packages/floating/src/MenuButton/MenuButton.tsx delete mode 100644 packages/floating/src/MenuButton/MenuButton.types.ts delete mode 100644 packages/floating/src/MenuButton/index.ts create mode 100644 packages/floating/src/createVirtualElement.ts create mode 100644 packages/floating/src/floating-ui.ts create mode 100644 packages/floating/src/hooks/index.ts create mode 100644 packages/floating/src/hooks/useVirtualFloating.ts create mode 100644 packages/floating/src/index.ts delete mode 100644 packages/floating/src/index.tsx create mode 100644 packages/floating/src/utils/getRangeBoundingClientRect.ts create mode 100644 packages/floating/src/utils/getSelectionBoundingClientRect.ts create mode 100644 packages/floating/src/utils/index.ts delete mode 100644 packages/ui/toolbar/src/BalloonToolbar/useBalloonToolbarPopper.ts create mode 100644 packages/ui/toolbar/src/BalloonToolbar/useFloatingToolbar.ts diff --git a/examples/apps/next/next.config.js b/examples/apps/next/next.config.js index 892793237b..6c73b1ca33 100644 --- a/examples/apps/next/next.config.js +++ b/examples/apps/next/next.config.js @@ -15,6 +15,7 @@ const alias = { '@udecode/plate-serializer-docx': 'serializers/docx', '@udecode/plate-excalidraw': 'ui/nodes/excalidraw', '@udecode/plate-find-replace': 'decorators/find-replace', + '@udecode/plate-floating': 'floating', '@udecode/plate-font': 'nodes/font', '@udecode/plate-headless': 'headless', '@udecode/plate-heading': 'nodes/heading', diff --git a/examples/src/PlaygroundApp.tsx b/examples/src/PlaygroundApp.tsx index 0b89508f13..b3cc5001bb 100644 --- a/examples/src/PlaygroundApp.tsx +++ b/examples/src/PlaygroundApp.tsx @@ -316,7 +316,7 @@ const App = () => { initialValue={playgroundValue} plugins={plugins} > - + {/* */} diff --git a/packages/core/src/utils/misc/index.ts b/packages/core/src/utils/misc/index.ts index 99f4e32f10..d04cd7cbc7 100644 --- a/packages/core/src/utils/misc/index.ts +++ b/packages/core/src/utils/misc/index.ts @@ -2,6 +2,7 @@ * @file Automatically generated by barrelsby. */ +export * from './mergeProps'; export * from './dom-attributes'; export * from './environment'; export * from './escapeRegexp'; diff --git a/packages/core/src/utils/misc/mergeProps.ts b/packages/core/src/utils/misc/mergeProps.ts new file mode 100644 index 0000000000..8beea54964 --- /dev/null +++ b/packages/core/src/utils/misc/mergeProps.ts @@ -0,0 +1,55 @@ +/** + * Merge props by composing handlers. + */ +export const mergeProps = ( + props?: T, + overrideProps?: T, + { + handlerKeys, + handlerQuery = (key) => key.indexOf('on') === 0, + }: { + /** + * The keys of the handlers to merge. + */ + handlerKeys?: string[]; + /** + * A function that returns true if it's a handler to merge. + * + * Default: keys having `on` prefix. + */ + handlerQuery?: ((key: string) => boolean) | null; + } = {} +): T => { + const map = new Map void>>(); + + const acc: any = {}; + + const mapProps = (_props?: T) => { + if (!_props) return; + + Object.entries(_props).forEach(([key, value]) => { + if ( + (!handlerKeys || handlerKeys.includes(key)) && + (!handlerQuery || handlerQuery(key)) && + typeof value === 'function' + ) { + if (!map.has(key)) { + map.set(key, []); + } + + map.get(key)?.push(value); + + acc[key] = (...args: unknown[]) => { + map.get(key)?.forEach((fn) => fn(...args)); + }; + } else { + acc[key] = value; + } + }); + }; + + mapProps(props); + mapProps(overrideProps); + + return acc; +}; diff --git a/packages/floating/README.md b/packages/floating/README.md index e60df064ca..65688e21d1 100644 --- a/packages/floating/README.md +++ b/packages/floating/README.md @@ -1,10 +1,10 @@ -# Plate menu UI +# Plate floating UI -This package implements the menu UI for Plate. +This package implements the floating UI for Plate. ## Documentation -See [@szhsin/react-menu](https://github.com/szhsin/react-menu). +WIP ## API diff --git a/packages/floating/package.json b/packages/floating/package.json index 28e9a341cc..4039672d4b 100644 --- a/packages/floating/package.json +++ b/packages/floating/package.json @@ -1,13 +1,13 @@ { "name": "@udecode/plate-floating", - "version": "6.1.0", + "version": "14.0.2", "description": "Floating UI for Plate", "license": "MIT", "homepage": "https://plate.udecode.io", "repository": { "type": "git", "url": "https://github.com/udecode/plate.git", - "directory": "packages/ui/menu" + "directory": "packages/floating" }, "bugs": { "url": "https://github.com/udecode/plate/issues" @@ -19,10 +19,8 @@ ], "types": "dist/index.d.ts", "dependencies": { - "@szhsin/react-menu": "^2.1.0", - "@udecode/plate-core": "6.0.0", - "@udecode/plate-styled-components": "6.0.0", - "@udecode/plate-ui-button": "6.1.0" + "@floating-ui/react-dom-interactions": "^0.6.6", + "@udecode/plate-core": "14.0.2" }, "peerDependencies": { "react": ">=16.8.0", diff --git a/packages/floating/src/Menu/Menu.styles.ts b/packages/floating/src/Menu/Menu.styles.ts deleted file mode 100644 index 9f286eb765..0000000000 --- a/packages/floating/src/Menu/Menu.styles.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { createStyles } from '@udecode/plate-styled-components'; -import { css } from 'styled-components'; -import tw from 'twin.macro'; -import { MenuStyleProps } from './Menu.types'; - -export const getMenuStyles = (props: MenuStyleProps) => - createStyles( - { prefixClassNames: 'Menu', ...props }, - { - root: css` - .szh-menu-container { - ${tw`relative w-0 h-0`}; - } - - .szh-menu { - ${tw`absolute bg-white`} - list-style: none; - width: max-content; - z-index: 100; - border: 1px solid rgba(0, 0, 0, 0.1); - } - .szh-menu:focus { - ${tw`outline-none`}; - } - .szh-menu--state-closed { - ${tw`hidden`}; - } - .szh-menu__arrow { - ${tw`absolute bg-white`}; - width: 0.75rem; - height: 0.75rem; - border: 1px solid transparent; - border-left-color: rgba(0, 0, 0, 0.1); - border-top-color: rgba(0, 0, 0, 0.1); - z-index: -1; - } - .szh-menu__arrow--dir-left { - right: -0.375rem; - transform: translateY(-50%) rotate(135deg); - } - .szh-menu__arrow--dir-right { - left: -0.375rem; - transform: translateY(-50%) rotate(-45deg); - } - .szh-menu__arrow--dir-top { - bottom: -0.375rem; - transform: translateX(-50%) rotate(-135deg); - } - .szh-menu__arrow--dir-bottom { - top: -0.375rem; - transform: translateX(-50%) rotate(45deg); - } - .szh-menu__item { - ${tw`cursor-pointer`}; - } - .szh-menu__item:focus { - ${tw`outline-none`}; - } - .szh-menu__item--hover { - ${tw`bg-gray-100`}; - } - .szh-menu__item--focusable { - ${tw`cursor-default`}; - background-color: inherit; - } - .szh-menu__item--disabled { - ${tw`cursor-default`}; - //color: #aaa; - } - .szh-menu__submenu { - ${tw`relative m-0`}; - } - .szh-menu__group { - } - .szh-menu__radio-group { - ${tw`m-0 p-0`}; - list-style: none; - } - .szh-menu__divider { - ${tw`bg-gray-200 h-px`}; - margin: 0.5rem 0; - } - - .szh-menu { - ${tw`select-none border-none`}; - border-radius: 0.25rem; - min-width: 10rem; - padding: 0.5rem 0; - box-shadow: rgba(9, 30, 66, 0.31) 0 0 1px, - rgba(9, 30, 66, 0.25) 0 4px 8px -2px; - } - .szh-menu__item { - ${tw`relative flex items-center m-0`}; - padding: 0.375rem 1.5rem; - } - .szh-menu-container--itemTransition .szh-menu__item { - } - .szh-menu__item--active { - ${tw`bg-gray-200`}; - } - .szh-menu__item--type-radio { - padding-left: 2.2rem; - } - .szh-menu__item--type-radio::before { - ${tw`absolute`}; - content: '○'; - left: 0.8rem; - top: 0.55rem; - font-size: 0.8rem; - } - .szh-menu__item--type-radio.szh-menu__item--checked::before { - content: '●'; - } - .szh-menu__item--type-checkbox { - padding-left: 2.2rem; - } - .szh-menu__item--type-checkbox::before { - ${tw`absolute`}; - left: 0.8rem; - } - .szh-menu__item--type-checkbox.szh-menu__item--checked::before { - content: '✔'; - } - .szh-menu__submenu > .szh-menu__item { - padding-right: 2.5rem; - } - .szh-menu__submenu > .szh-menu__item::after { - ${tw`absolute`}; - content: '❯'; - right: 1rem; - } - .szh-menu__header { - font-size: 0.8em; - padding: 0.2rem 1.5rem; - text-transform: uppercase; - } - `, - } - ); diff --git a/packages/floating/src/Menu/Menu.tsx b/packages/floating/src/Menu/Menu.tsx deleted file mode 100644 index 1a0822abd9..0000000000 --- a/packages/floating/src/Menu/Menu.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { getMenuStyles } from './Menu.styles'; -import { MenuProps } from './Menu.types'; - -export const Menu = (props: MenuProps) => { - const { rootProps, ...menuProps } = props; - - const { root } = getMenuStyles(props); - - return ( -
- {/* */} -
- ); -}; diff --git a/packages/floating/src/Menu/Menu.types.ts b/packages/floating/src/Menu/Menu.types.ts deleted file mode 100644 index 256b3f5ef8..0000000000 --- a/packages/floating/src/Menu/Menu.types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { HTMLAttributes } from 'react'; -import { MenuProps as MenuBaseProps } from '@szhsin/react-menu'; -import { StyledProps } from '@udecode/plate-styled-components'; - -export interface MenuStyleProps extends MenuProps {} - -export interface MenuStyles {} - -export interface MenuProps extends StyledProps, MenuBaseProps { - rootProps?: HTMLAttributes; -} diff --git a/packages/floating/src/Menu/index.ts b/packages/floating/src/Menu/index.ts deleted file mode 100644 index a7b0ee345d..0000000000 --- a/packages/floating/src/Menu/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @file Automatically generated by barrelsby. - */ - -export * from './Menu.styles'; -export * from './Menu'; -export * from './Menu.types'; diff --git a/packages/floating/src/MenuButton/MenuButton.styles.ts b/packages/floating/src/MenuButton/MenuButton.styles.ts deleted file mode 100644 index b7c1909f1a..0000000000 --- a/packages/floating/src/MenuButton/MenuButton.styles.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createStyles, StyledProps } from '@udecode/plate-styled-components'; -// import { getButtonStyles } from '@udecode/plate-ui-button'; -import tw from 'twin.macro'; -import { MenuButtonStyles } from './MenuButton.types'; - -export const getMenuButtonStyles = (props: StyledProps) => { - // const { root } = getButtonStyles(props); - - return createStyles( - { prefixClassNames: 'MenuButton', ...props }, - { - // root: [...root.css], - root: [], - chevron: tw`ml-1`, - } - ); -}; diff --git a/packages/floating/src/MenuButton/MenuButton.tsx b/packages/floating/src/MenuButton/MenuButton.tsx deleted file mode 100644 index cbd0c3eaa5..0000000000 --- a/packages/floating/src/MenuButton/MenuButton.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { forwardRef } from 'react'; -import { MenuButton as MenuButtonBase } from '@szhsin/react-menu'; -import { ChevronDownIcon } from '@udecode/plate-ui-button'; -import { getMenuButtonStyles } from './MenuButton.styles'; -import { MenuButtonProps } from './MenuButton.types'; - -export const MenuButton = forwardRef( - ( - { - children, - menuButtonStyles, - styles, - prefixClassNames, - chevronProps, - ...props - }: MenuButtonProps, - ref - ) => { - const { root, chevron } = getMenuButtonStyles({ - styles, - prefixClassNames, - }); - - return ( - - {children} - - - - ); - } -); diff --git a/packages/floating/src/MenuButton/MenuButton.types.ts b/packages/floating/src/MenuButton/MenuButton.types.ts deleted file mode 100644 index c61f7ba336..0000000000 --- a/packages/floating/src/MenuButton/MenuButton.types.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { SVGProps } from 'react'; -import { MenuButtonProps as MenuButtonBaseProps } from '@szhsin/react-menu'; -import { StyledProps } from '@udecode/plate-styled-components'; -import { CSSProp } from 'styled-components'; - -export interface MenuButtonStyleProps extends MenuButtonProps {} - -export interface MenuButtonStyles { - chevron: CSSProp; -} - -export interface MenuButtonProps - extends Omit, - StyledProps { - menuButtonStyles?: MenuButtonBaseProps['styles']; - chevronProps?: SVGProps; -} diff --git a/packages/floating/src/MenuButton/index.ts b/packages/floating/src/MenuButton/index.ts deleted file mode 100644 index 7ddd2d4901..0000000000 --- a/packages/floating/src/MenuButton/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @file Automatically generated by barrelsby. - */ - -export * from './MenuButton.styles'; -export * from './MenuButton'; -export * from './MenuButton.types'; diff --git a/packages/floating/src/createVirtualElement.ts b/packages/floating/src/createVirtualElement.ts new file mode 100644 index 0000000000..940e483011 --- /dev/null +++ b/packages/floating/src/createVirtualElement.ts @@ -0,0 +1,16 @@ +import { VirtualElement } from '@floating-ui/react-dom-interactions'; + +export const getDefaultBoundingClientRect = () => ({ + width: 0, + height: 0, + x: 0, + y: 0, + top: -9999, + left: -9999, + right: 9999, + bottom: 9999, +}); + +export const createVirtualElement = (): VirtualElement => ({ + getBoundingClientRect: getDefaultBoundingClientRect, +}); diff --git a/packages/floating/src/floating-ui.ts b/packages/floating/src/floating-ui.ts new file mode 100644 index 0000000000..358efc1789 --- /dev/null +++ b/packages/floating/src/floating-ui.ts @@ -0,0 +1 @@ +export * from '@floating-ui/react-dom-interactions'; diff --git a/packages/floating/src/hooks/index.ts b/packages/floating/src/hooks/index.ts new file mode 100644 index 0000000000..f6e2d563fa --- /dev/null +++ b/packages/floating/src/hooks/index.ts @@ -0,0 +1,5 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './useVirtualFloating'; diff --git a/packages/floating/src/hooks/useVirtualFloating.ts b/packages/floating/src/hooks/useVirtualFloating.ts new file mode 100644 index 0000000000..9e56bfc935 --- /dev/null +++ b/packages/floating/src/hooks/useVirtualFloating.ts @@ -0,0 +1,98 @@ +import * as React from 'react'; +import { + CSSProperties, + MutableRefObject, + useLayoutEffect, + useRef, + useState, +} from 'react'; +import { ClientRectObject } from '@floating-ui/core'; +import { + autoUpdate, + ReferenceType, + useFloating, + UseFloatingProps, + UseFloatingReturn, + VirtualElement, +} from '@floating-ui/react-dom-interactions'; +import { createVirtualElement } from '../createVirtualElement'; +import { getSelectionBoundingClientRect } from '../utils/index'; + +export interface ExtendedRefs { + reference: React.MutableRefObject; + floating: React.MutableRefObject; + domReference: React.MutableRefObject; +} + +export interface UseVirtualFloatingOptions extends Partial { + getBoundingClientRect?: () => ClientRectObject; + open?: boolean; +} + +export interface UseVirtualFloatingReturn< + RT extends ReferenceType = ReferenceType +> extends Omit, 'refs'> { + refs: ExtendedRefs; + virtualElementRef: MutableRefObject; + style: CSSProperties; +} + +/** + * `useFloating` with a controlled virtual element. Used to follow cursor position. + * + * Default options: + * - `whileElementsMounted: autoUpdate` + * + * Additional options: + * - `getBoundingClientRect` to get the bounding client rect. + * - `hidden` to hide the floating element + * + * Additional returns: + * - `style` to apply to the floating element + * - `virtualElementRef` + * + * @see useFloating + * @see https://floating-ui.com/docs/react-dom#virtual-element + */ +export const useVirtualFloating = ({ + getBoundingClientRect = getSelectionBoundingClientRect, + ...floatingOptions +}: UseVirtualFloatingOptions): UseVirtualFloatingReturn => { + const virtualElementRef = useRef(createVirtualElement() as RT); + const [visible, setVisible] = useState(true); + + const floatingResult = useFloating({ + // update on scroll and resize + whileElementsMounted: autoUpdate, + ...floatingOptions, + }); + + const { reference, middlewareData, strategy, x, y } = floatingResult; + + useLayoutEffect(() => { + virtualElementRef.current.getBoundingClientRect = getBoundingClientRect; + }, [getBoundingClientRect]); + + useLayoutEffect(() => { + reference(virtualElementRef.current); + }, [reference]); + + useLayoutEffect(() => { + if (!middlewareData?.hide) return; + + const { referenceHidden } = middlewareData.hide; + + setVisible(!referenceHidden); + }, [middlewareData.hide]); + + return { + ...floatingResult, + virtualElementRef, + style: { + position: strategy, + top: y ?? 0, + left: x ?? 0, + visibility: !visible ? 'hidden' : undefined, + }, + }; +}; diff --git a/packages/floating/src/index.ts b/packages/floating/src/index.ts new file mode 100644 index 0000000000..092b5b63dc --- /dev/null +++ b/packages/floating/src/index.ts @@ -0,0 +1,8 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './createVirtualElement'; +export * from './floating-ui'; +export * from './hooks/index'; +export * from './utils/index'; diff --git a/packages/floating/src/index.tsx b/packages/floating/src/index.tsx deleted file mode 100644 index 9484ecada3..0000000000 --- a/packages/floating/src/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// export { -// applyHOC, -// applyStatics, -// useMenuState, -// ControlledMenu, -// SubMenu, -// MenuItem, -// FocusableItem, -// MenuDivider, -// MenuHeader, -// MenuGroup, -// MenuRadioGroup, -// } from '@szhsin/react-menu'; - -export * from './Menu'; -export * from './MenuButton'; diff --git a/packages/floating/src/utils/getRangeBoundingClientRect.ts b/packages/floating/src/utils/getRangeBoundingClientRect.ts new file mode 100644 index 0000000000..214ec12cc9 --- /dev/null +++ b/packages/floating/src/utils/getRangeBoundingClientRect.ts @@ -0,0 +1,17 @@ +import { toDOMRange, TReactEditor, Value } from '@udecode/plate-core'; +import { Range } from 'slate'; + +/** + * Get bounding client rect by slate range + */ +export const getRangeBoundingClientRect = ( + editor: TReactEditor, + at: Range | null +) => { + if (!at) return; + + const domRange = toDOMRange(editor, at); + if (!domRange) return; + + return domRange.getBoundingClientRect(); +}; diff --git a/packages/floating/src/utils/getSelectionBoundingClientRect.ts b/packages/floating/src/utils/getSelectionBoundingClientRect.ts new file mode 100644 index 0000000000..668b5dac71 --- /dev/null +++ b/packages/floating/src/utils/getSelectionBoundingClientRect.ts @@ -0,0 +1,17 @@ +import { ClientRectObject } from '@floating-ui/core'; +import { getDefaultBoundingClientRect } from '../createVirtualElement'; + +/** + * Get bounding client rect of the window selection + */ +export const getSelectionBoundingClientRect = (): ClientRectObject => { + const domSelection = window.getSelection(); + + if (!domSelection || domSelection.rangeCount < 1) { + return getDefaultBoundingClientRect(); + } + + const domRange = domSelection.getRangeAt(0); + + return domRange.getBoundingClientRect(); +}; diff --git a/packages/floating/src/utils/index.ts b/packages/floating/src/utils/index.ts new file mode 100644 index 0000000000..bc3b6955e8 --- /dev/null +++ b/packages/floating/src/utils/index.ts @@ -0,0 +1,6 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './getRangeBoundingClientRect'; +export * from './getSelectionBoundingClientRect'; diff --git a/packages/floating/tsconfig.json b/packages/floating/tsconfig.json index f10b50a74c..9f68b4628c 100644 --- a/packages/floating/tsconfig.json +++ b/packages/floating/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../config/tsconfig.build.json", + "extends": "../../config/tsconfig.build.json", "compilerOptions": { "declarationDir": "./dist" }, diff --git a/packages/ui/nodes/link/src/LinkElement/LinkElement.tsx b/packages/ui/nodes/link/src/LinkElement/LinkElement.tsx index 95327c37de..df63497054 100644 --- a/packages/ui/nodes/link/src/LinkElement/LinkElement.tsx +++ b/packages/ui/nodes/link/src/LinkElement/LinkElement.tsx @@ -9,9 +9,5 @@ export const LinkElement = (props: StyledElementProps) => { const { root } = getLinkElementStyles(props); - return ( - - - - ); + return ; }; diff --git a/packages/ui/popover/src/Popover/Popover.tsx b/packages/ui/popover/src/Popover/Popover.tsx index e93b8d27e0..29a7f44424 100644 --- a/packages/ui/popover/src/Popover/Popover.tsx +++ b/packages/ui/popover/src/Popover/Popover.tsx @@ -1,11 +1,5 @@ import React, { HTMLAttributes } from 'react'; import Tippy, { TippyProps } from '@tippyjs/react'; -import { createComponentAs } from '@udecode/plate-core'; -import { - ImageCaptionTextareaProps, - TextareaAutosize, - useImageCaptionTextarea, -} from '@udecode/plate-image/src/index'; import { StyledProps } from '@udecode/plate-styled-components'; import { useReadOnly, useSelected } from 'slate-react'; import { getPopoverStyles } from './Popover.styles'; @@ -52,11 +46,3 @@ export const Popover = ({ rootProps, content, ...props }: PopoverProps) => { return ; }; - -export const ImageCaptionTextarea = createComponentAs( - ({ as, ...props }) => { - const htmlProps = useImageCaptionTextarea({ as: as as any, ...props }); - - return ; - } -); diff --git a/packages/ui/popper/src/getSelectionBoundingClientRect.ts b/packages/ui/popper/src/getSelectionBoundingClientRect.ts index a94ee3cb49..2daab6b7f6 100644 --- a/packages/ui/popper/src/getSelectionBoundingClientRect.ts +++ b/packages/ui/popper/src/getSelectionBoundingClientRect.ts @@ -6,7 +6,6 @@ export const getSelectionBoundingClientRect = () => { if (!domSelection || domSelection.rangeCount < 1) return; const domRange = domSelection.getRangeAt(0); - console.log(domRange); return domRange.getBoundingClientRect(); }; diff --git a/packages/ui/toolbar/package.json b/packages/ui/toolbar/package.json index 5c491ecd4e..d8fbb4fcf4 100644 --- a/packages/ui/toolbar/package.json +++ b/packages/ui/toolbar/package.json @@ -21,6 +21,7 @@ "dependencies": { "@tippyjs/react": "^4.2.6", "@udecode/plate-core": "14.0.2", + "@udecode/plate-floating": "14.0.2", "@udecode/plate-styled-components": "14.0.2", "@udecode/plate-ui-popper": "14.0.2", "react-popper": "^2.3.0", diff --git a/packages/ui/toolbar/src/BalloonToolbar/BalloonToolbar.styles.ts b/packages/ui/toolbar/src/BalloonToolbar/BalloonToolbar.styles.ts index de8d727527..071c27977a 100644 --- a/packages/ui/toolbar/src/BalloonToolbar/BalloonToolbar.styles.ts +++ b/packages/ui/toolbar/src/BalloonToolbar/BalloonToolbar.styles.ts @@ -17,7 +17,7 @@ export const getBalloonToolbarStyles = (props: BalloonToolbarStyleProps) => { borderColor = 'rgb(196, 196, 196)'; } - const { placement = 'top' } = props.popperOptions ?? {}; + const { placement = 'top' } = props; const arrowStyle: CSSProp = [ props.arrow && diff --git a/packages/ui/toolbar/src/BalloonToolbar/BalloonToolbar.tsx b/packages/ui/toolbar/src/BalloonToolbar/BalloonToolbar.tsx index a6dda04be6..881173a67d 100644 --- a/packages/ui/toolbar/src/BalloonToolbar/BalloonToolbar.tsx +++ b/packages/ui/toolbar/src/BalloonToolbar/BalloonToolbar.tsx @@ -1,11 +1,10 @@ -import React, { useRef } from 'react'; +import React from 'react'; import { withPlateEventProvider } from '@udecode/plate-core'; import { PortalBody } from '@udecode/plate-styled-components'; -import { UsePopperPositionOptions } from '@udecode/plate-ui-popper'; import { ToolbarBase } from '../Toolbar/Toolbar'; import { getBalloonToolbarStyles } from './BalloonToolbar.styles'; import { BalloonToolbarProps } from './BalloonToolbar.types'; -import { useBalloonToolbarPopper } from './useBalloonToolbarPopper'; +import { useFloatingToolbar } from './useFloatingToolbar'; export const BalloonToolbar = withPlateEventProvider( (props: BalloonToolbarProps) => { @@ -14,37 +13,29 @@ export const BalloonToolbar = withPlateEventProvider( theme = 'dark', arrow = false, portalElement, - popperOptions: _popperOptions = {}, + floatingOptions, } = props; - const popperRef = useRef(null); - - const popperOptions: UsePopperPositionOptions = { - popperElement: popperRef.current, - placement: 'top' as any, - offset: [0, 8], - ..._popperOptions, - }; - - const { styles: popperStyles, attributes } = useBalloonToolbarPopper( - popperOptions - ); + const { floating, style, placement, open } = useFloatingToolbar({ + floatingOptions, + }); const styles = getBalloonToolbarStyles({ - popperOptions, + placement, theme, arrow, ...props, }); + if (!open) return null; + return ( {children} diff --git a/packages/ui/toolbar/src/BalloonToolbar/BalloonToolbar.types.ts b/packages/ui/toolbar/src/BalloonToolbar/BalloonToolbar.types.ts index 9dd55ddb28..40c61c4661 100644 --- a/packages/ui/toolbar/src/BalloonToolbar/BalloonToolbar.types.ts +++ b/packages/ui/toolbar/src/BalloonToolbar/BalloonToolbar.types.ts @@ -1,9 +1,11 @@ import { ReactNode } from 'react'; +import { UseVirtualFloatingOptions } from '@udecode/plate-floating'; import { StyledProps } from '@udecode/plate-styled-components'; -import { UsePopperPositionOptions } from '@udecode/plate-ui-popper'; import { ToolbarProps } from '../Toolbar/Toolbar.types'; -export interface BalloonToolbarStyleProps extends BalloonToolbarProps {} +export interface BalloonToolbarStyleProps extends BalloonToolbarProps { + placement?: string; +} export interface BalloonToolbarProps extends StyledProps { children: ReactNode; @@ -20,5 +22,5 @@ export interface BalloonToolbarProps extends StyledProps { portalElement?: Element; - popperOptions?: Partial; + floatingOptions?: UseVirtualFloatingOptions; } diff --git a/packages/ui/toolbar/src/BalloonToolbar/index.ts b/packages/ui/toolbar/src/BalloonToolbar/index.ts index 692e06a2e4..f04e6a2e56 100644 --- a/packages/ui/toolbar/src/BalloonToolbar/index.ts +++ b/packages/ui/toolbar/src/BalloonToolbar/index.ts @@ -5,4 +5,4 @@ export * from './BalloonToolbar.styles'; export * from './BalloonToolbar'; export * from './BalloonToolbar.types'; -export * from './useBalloonToolbarPopper'; +export * from './useFloatingToolbar'; diff --git a/packages/ui/toolbar/src/BalloonToolbar/useBalloonToolbarPopper.ts b/packages/ui/toolbar/src/BalloonToolbar/useBalloonToolbarPopper.ts deleted file mode 100644 index 02a01f5e4b..0000000000 --- a/packages/ui/toolbar/src/BalloonToolbar/useBalloonToolbarPopper.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useEffect, useState } from 'react'; -import { - getSelectionText, - isSelectionExpanded, - useEditorState, - useEventEditorSelectors, -} from '@udecode/plate-core'; -import { - getSelectionBoundingClientRect, - usePopperPosition, - UsePopperPositionOptions, -} from '@udecode/plate-ui-popper'; -import { useFocused } from 'slate-react'; - -export const useBalloonToolbarPopper = (options: UsePopperPositionOptions) => { - const focusedEditorId = useEventEditorSelectors.focus(); - const editor = useEditorState(); - const focused = useFocused(); - - const [isHidden, setIsHidden] = useState(true); - - const selectionExpanded = editor && isSelectionExpanded(editor); - const selectionText = editor && getSelectionText(editor); - - useEffect(() => { - if ( - !selectionExpanded || - !selectionText || - !focused || - editor.id !== focusedEditorId - ) { - setIsHidden(true); - } else if (selectionText && selectionExpanded) { - setIsHidden(false); - } - }, [ - editor.id, - editor.selection, - focused, - focusedEditorId, - selectionExpanded, - selectionText, - ]); - - const popperResult = usePopperPosition({ - isHidden, - getBoundingClientRect: getSelectionBoundingClientRect, - ...options, - }); - - const selectionTextLength = selectionText?.length ?? 0; - const { update } = popperResult; - - useEffect(() => { - if (selectionTextLength > 0) { - update?.(); - } - }, [selectionTextLength, update]); - - return popperResult; -}; diff --git a/packages/ui/toolbar/src/BalloonToolbar/useFloatingToolbar.ts b/packages/ui/toolbar/src/BalloonToolbar/useFloatingToolbar.ts new file mode 100644 index 0000000000..f59e606669 --- /dev/null +++ b/packages/ui/toolbar/src/BalloonToolbar/useFloatingToolbar.ts @@ -0,0 +1,103 @@ +import { useEffect, useState } from 'react'; +import { + getSelectionText, + isSelectionExpanded, + mergeProps, + useEditorState, + useEventEditorSelectors, +} from '@udecode/plate-core'; +import { + flip, + getSelectionBoundingClientRect, + hide, + offset, + useVirtualFloating, + UseVirtualFloatingOptions, + UseVirtualFloatingReturn, +} from '@udecode/plate-floating'; +import { useFocused } from 'slate-react'; + +export const useFloatingToolbar = ({ + floatingOptions, +}: { + floatingOptions?: UseVirtualFloatingOptions; +} = {}): UseVirtualFloatingReturn & { + open: boolean; +} => { + const focusedEditorId = useEventEditorSelectors.focus(); + const editor = useEditorState(); + const focused = useFocused(); + + const [waitForCollapsedSelection, setWaitForCollapsedSelection] = useState( + false + ); + + const [open, setOpen] = useState(false); + + const selectionExpanded = editor && isSelectionExpanded(editor); + const selectionText = editor && getSelectionText(editor); + + // On refocus, the editor keeps the previous selection, + // so we need to wait it's collapsed at the new position before displaying the floating toolbar. + useEffect(() => { + if (!focused) { + setWaitForCollapsedSelection(true); + } + + if (!selectionExpanded) { + setWaitForCollapsedSelection(false); + } + }, [focused, selectionExpanded]); + + useEffect(() => { + if (!selectionExpanded || !selectionText || editor.id !== focusedEditorId) { + setOpen(false); + } else if ( + selectionText && + selectionExpanded && + !waitForCollapsedSelection + ) { + setOpen(true); + } + }, [ + editor.id, + editor.selection, + focusedEditorId, + selectionExpanded, + selectionText, + waitForCollapsedSelection, + ]); + + const floatingResult = useVirtualFloating( + mergeProps( + { + middleware: [ + flip({ + padding: 8, + }), + offset({ + mainAxis: 12, + }), + hide(), + ], + placement: 'top', + getBoundingClientRect: getSelectionBoundingClientRect, + open, + onOpenChange: setOpen, + }, + floatingOptions + ) + ); + + const { update } = floatingResult; + + const selectionTextLength = selectionText?.length ?? 0; + + useEffect(() => { + if (selectionTextLength > 0) { + update?.(); + } + }, [selectionTextLength, update]); + + return { ...floatingResult, open }; +}; diff --git a/yarn.lock b/yarn.lock index 57896f21d4..ce3699e419 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,6 +1972,49 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^0.7.3": + version: 0.7.3 + resolution: "@floating-ui/core@npm:0.7.3" + checksum: 8679a95a08393f8fcb7c7dfa6600b742e34b2795650316b55aa6f9c3d9298564166f133e138a8a224b102853e346a9cb8bf843ba839abf470e52d0d91fea1a54 + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^0.5.3": + version: 0.5.4 + resolution: "@floating-ui/dom@npm:0.5.4" + dependencies: + "@floating-ui/core": "npm:^0.7.3" + checksum: 55e225e10a351de10553321b6581ebd63e8cd5f050fe3bc73ab7085ec966799dab55ba10a218d43206703ae5397fc4b11082aa620fad9de563a2b6b679455ea9 + languageName: node + linkType: hard + +"@floating-ui/react-dom-interactions@npm:^0.6.6": + version: 0.6.6 + resolution: "@floating-ui/react-dom-interactions@npm:0.6.6" + dependencies: + "@floating-ui/react-dom": "npm:^0.7.2" + aria-hidden: "npm:^1.1.3" + use-isomorphic-layout-effect: "npm:^1.1.1" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 92638badb2d9039d2fd8ada001303539bb49a06664cb5067d9384334ea27b47a929db490640fc8693574fdb92fecd52c6c1f3b4061900b595561b8861ec24443 + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^0.7.2": + version: 0.7.2 + resolution: "@floating-ui/react-dom@npm:0.7.2" + dependencies: + "@floating-ui/dom": "npm:^0.5.3" + use-isomorphic-layout-effect: "npm:^1.1.1" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: e60cb41affea77347c37515d02ced104a69b2bc69621d92a17b0526084b89c10a21a6617f0dd482aa4c6613fed81222794756225f7fb54e3c409716191290c08 + languageName: node + linkType: hard + "@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -5002,6 +5045,21 @@ __metadata: languageName: unknown linkType: soft +"@udecode/plate-floating@npm:14.0.2, @udecode/plate-floating@workspace:packages/floating": + version: 0.0.0-use.local + resolution: "@udecode/plate-floating@workspace:packages/floating" + dependencies: + "@floating-ui/react-dom-interactions": "npm:^0.6.6" + "@udecode/plate-core": "npm:14.0.2" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + slate: ">=0.66.1" + slate-history: ">=0.66.0" + slate-react: ">=0.74.2" + languageName: unknown + linkType: soft + "@udecode/plate-font@npm:14.0.2, @udecode/plate-font@workspace:packages/nodes/font": version: 0.0.0-use.local resolution: "@udecode/plate-font@workspace:packages/nodes/font" @@ -5836,6 +5894,7 @@ __metadata: dependencies: "@tippyjs/react": "npm:^4.2.6" "@udecode/plate-core": "npm:14.0.2" + "@udecode/plate-floating": "npm:14.0.2" "@udecode/plate-styled-components": "npm:14.0.2" "@udecode/plate-ui-popper": "npm:14.0.2" react-popper: "npm:^2.3.0" @@ -6542,6 +6601,15 @@ __metadata: languageName: node linkType: hard +"aria-hidden@npm:^1.1.3": + version: 1.1.3 + resolution: "aria-hidden@npm:1.1.3" + dependencies: + tslib: "npm:^1.0.0" + checksum: 5cfe132b55ef86adf4e7e66e23cb3f096cdf37e6c714724f1770a0a8734690211ac4cf12228de902aefdef15916a0d477373d25fae79de0c060c8eef8bab3ed1 + languageName: node + linkType: hard + "aria-query@npm:^4.2.2": version: 4.2.2 resolution: "aria-query@npm:4.2.2" @@ -23537,7 +23605,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.8.1, tslib@npm:^1.9.0": +"tslib@npm:^1.0.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 441af59dc42ad4ae57140e62cb362369620c6076845c2c2b0ecc863c1d719ce24fdbc301e9053433fef43075e061bf84b702318ff1204b496a5bba10baf9eb9f From 8882b39b6cc65d82082c5185e9d4a3a9a6aa2691 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Sun, 10 Jul 2022 18:26:49 +0200 Subject: [PATCH 03/21] feat --- docs/docs/plugins/combobox.mdx | 3 +- docs/docusaurus.config.js | 72 +--------- examples/apps/next/README.md | 2 - examples/apps/next/next.config.js | 1 - examples/src/PlaygroundApp.tsx | 77 ---------- packages/core/src/components/plate/Plate.tsx | 62 ++++++++- packages/core/src/types/plugin/PlatePlugin.ts | 26 ++++ .../editor/combobox/src/combobox.store.ts | 11 +- packages/floating/src/createVirtualElement.ts | 3 +- packages/floating/src/floating-ui.ts | 86 +++++++++++- .../src/utils/getRangeBoundingClientRect.ts | 8 +- packages/headless/package.json | 1 + packages/headless/src/index.tsx | 1 + packages/ui/combobox/package.json | 4 +- packages/ui/combobox/src/Combobox.tsx | 28 ++-- packages/ui/plate/package.json | 1 - packages/ui/plate/src/index.tsx | 1 - packages/ui/popper/package.json | 4 +- .../popper/src/getRangeBoundingClientRect.ts | 17 --- .../src/getSelectionBoundingClientRect.ts | 11 -- packages/ui/popper/src/index.ts | 4 +- packages/ui/popper/src/usePopperPosition.ts | 131 ------------------ packages/ui/toolbar/package.json | 2 - .../src/BalloonToolbar/useFloatingToolbar.ts | 6 +- yarn.lock | 44 +----- 25 files changed, 204 insertions(+), 402 deletions(-) delete mode 100644 packages/ui/popper/src/getRangeBoundingClientRect.ts delete mode 100644 packages/ui/popper/src/getSelectionBoundingClientRect.ts delete mode 100644 packages/ui/popper/src/usePopperPosition.ts diff --git a/docs/docs/plugins/combobox.mdx b/docs/docs/plugins/combobox.mdx index 167323e661..42ef39616c 100644 --- a/docs/docs/plugins/combobox.mdx +++ b/docs/docs/plugins/combobox.mdx @@ -22,8 +22,7 @@ The combobox state is stored in a [zustood store](https://github.com/udecode/zus - `items` – Unfiltered items - `filteredItems` – Filtered items - `highlightedIndex` – Highlighted index -- `popperContainer` – Parent element of the popper element (the one that has the scroll) -- `popperOptions` – Overrides `usePopper` options +- `floatingOptions` – Overrides `useFloating` options - `targetRange` – Range from the trigger to the cursor - `text` – Text after the trigger diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index e8021a8088..c1e95c840b 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -20,76 +20,6 @@ const customFields = { announcementBarContent: `If you like plate, give it a star on
GitHub 🎉 !️`, }; -const alias = { - '@udecode/plate': 'plate', - '@udecode/plate-alignment': 'nodes/alignment', - '@udecode/plate-autoformat': 'editor/autoformat', - '@udecode/plate-basic-elements': 'nodes/basic-elements', - '@udecode/plate-basic-marks': 'nodes/basic-marks', - '@udecode/plate-block-quote': 'nodes/block-quote', - '@udecode/plate-break': 'editor/break', - '@udecode/plate-code-block': 'nodes/code-block', - '@udecode/plate-combobox': 'editor/combobox', - '@udecode/plate-core': 'core', - '@udecode/plate-serializer-csv': 'serializers/csv', - '@udecode/plate-serializer-docx': 'serializers/docx', - '@udecode/plate-excalidraw': 'ui/nodes/excalidraw', - '@udecode/plate-find-replace': 'decorators/find-replace', - '@udecode/plate-font': 'nodes/font', - '@udecode/plate-headless': 'headless', - '@udecode/plate-heading': 'nodes/heading', - '@udecode/plate-highlight': 'nodes/highlight', - '@udecode/plate-horizontal-rule': 'nodes/horizontal-rule', - '@udecode/plate-image': 'nodes/image', - '@udecode/plate-indent': 'nodes/indent', - '@udecode/plate-indent-list': 'nodes/indent-list', - '@udecode/plate-juice': 'serializers/juice', - '@udecode/plate-kbd': 'nodes/kbd', - '@udecode/plate-line-height': 'nodes/line-height', - '@udecode/plate-link': 'nodes/link', - '@udecode/plate-list': 'nodes/list', - '@udecode/plate-serializer-md': 'serializers/md', - '@udecode/plate-media-embed': 'nodes/media-embed', - '@udecode/plate-mention': 'nodes/mention', - '@udecode/plate-node-id': 'editor/node-id', - '@udecode/plate-normalizers': 'editor/normalizers', - '@udecode/plate-paragraph': 'nodes/paragraph', - '@udecode/plate-reset-node': 'editor/reset-node', - '@udecode/plate-select': 'editor/select', - '@udecode/plate-styled-components': 'ui/styled-components', - '@udecode/plate-table': 'nodes/table', - '@udecode/plate-test-utils': 'test-utils', - '@udecode/plate-trailing-block': 'editor/trailing-block', - '@udecode/plate-ui': 'ui/plate', - '@udecode/plate-ui-alignment': 'ui/nodes/alignment', - '@udecode/plate-ui-block-quote': 'ui/nodes/block-quote', - '@udecode/plate-ui-button': 'ui/button', - '@udecode/plate-ui-code-block': 'ui/nodes/code-block', - '@udecode/plate-ui-combobox': 'ui/combobox', - '@udecode/plate-ui-cursor': 'ui/cursor', - '@udecode/plate-ui-dnd': 'ui/dnd', - '@udecode/plate-ui-find-replace': 'ui/find-replace', - '@udecode/plate-ui-font': 'ui/nodes/font', - '@udecode/plate-ui-horizontal-rule': 'ui/nodes/horizontal-rule', - '@udecode/plate-ui-image': 'ui/nodes/image', - '@udecode/plate-ui-line-height': 'ui/nodes/line-height', - '@udecode/plate-ui-link': 'ui/nodes/link', - '@udecode/plate-ui-list': 'ui/nodes/list', - '@udecode/plate-ui-media-embed': 'ui/nodes/media-embed', - '@udecode/plate-ui-mention': 'ui/nodes/mention', - '@udecode/plate-ui-placeholder': 'ui/placeholder', - '@udecode/plate-ui-popover': 'ui/popover', - '@udecode/plate-ui-popper': 'ui/popper', - '@udecode/plate-ui-table': 'ui/nodes/table', - '@udecode/plate-ui-toolbar': 'ui/toolbar', - 'react/jsx-runtime': 'react/jsx-runtime.js', - 'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js', -}; - -Object.keys(alias).forEach((key) => { - alias[key] = path.resolve(__dirname, `../../packages/${alias[key]}/src`); -}); - module.exports = { title: customFields.title, tagline: customFields.tagline, @@ -253,7 +183,7 @@ module.exports = { [ path.resolve(__dirname, 'plugins/module-alias'), { - alias, + alias: {}, }, ], path.resolve(__dirname, 'plugins/source-maps'), diff --git a/examples/apps/next/README.md b/examples/apps/next/README.md index ae3da8498b..8ba4c9a838 100644 --- a/examples/apps/next/README.md +++ b/examples/apps/next/README.md @@ -133,8 +133,6 @@ yarn dev [//]: # ( "@udecode/plate-ui-popover": ["../../packages/ui/popover"],) -[//]: # ( "@udecode/plate-ui-popper": ["../../packages/ui/popper"],) - [//]: # ( "@udecode/plate-ui-table": ["../../packages/ui/nodes/table"],) [//]: # ( "@udecode/plate-ui-toolbar": ["../../packages/ui/toolbar"])) \ No newline at end of file diff --git a/examples/apps/next/next.config.js b/examples/apps/next/next.config.js index 6c73b1ca33..1867d767a2 100644 --- a/examples/apps/next/next.config.js +++ b/examples/apps/next/next.config.js @@ -60,7 +60,6 @@ const alias = { '@udecode/plate-ui-mention': 'ui/nodes/mention', '@udecode/plate-ui-placeholder': 'ui/placeholder', '@udecode/plate-ui-popover': 'ui/popover', - '@udecode/plate-ui-popper': 'ui/popper', '@udecode/plate-ui-table': 'ui/nodes/table', '@udecode/plate-ui-toolbar': 'ui/toolbar', }; diff --git a/examples/src/PlaygroundApp.tsx b/examples/src/PlaygroundApp.tsx index b3cc5001bb..7224b12f06 100644 --- a/examples/src/PlaygroundApp.tsx +++ b/examples/src/PlaygroundApp.tsx @@ -47,15 +47,11 @@ import { createTrailingBlockPlugin, createUnderlinePlugin, ELEMENT_CODE_BLOCK, - getSelectionBoundingClientRect, isDefined, MentionCombobox, Plate, StyledElement, useComboboxSelectors, - useEditorState, - usePopperPosition, - UsePopperPositionOptions, } from '@udecode/plate'; import { createJuicePlugin } from '@udecode/plate-juice'; import { @@ -63,7 +59,6 @@ import { ELEMENT_EXCALIDRAW, ExcalidrawElement, } from '@udecode/plate-ui-excalidraw'; -import { useFocused } from 'slate-react'; import { alignPlugin } from './align/alignPlugin'; import { autoformatPlugin } from './autoformat/autoformatPlugin'; import { MarkBalloonToolbar } from './balloon-toolbar/MarkBalloonToolbar'; @@ -174,75 +169,6 @@ const SlashCommandCombobox = () => ( /> ); -export const useBalloonToolbarPopper = (options: UsePopperPositionOptions) => { - const editor = useEditorState(); - const focused = useFocused(); - - // const [isHidden, setIsHidden] = useState(true); - - // useEffect(() => { - // if ( - // !selectionExpanded || - // !selectionText || - // !focused || - // editor.id !== focusedEditorId - // ) { - // setIsHidden(true); - // } else if (selectionText && selectionExpanded) { - // setIsHidden(false); - // } - // }, [ - // editor.id, - // editor.selection, - // focused, - // focusedEditorId, - // selectionExpanded, - // selectionText, - // ]); - - const popperResult = usePopperPosition({ - // isHidden, - getBoundingClientRect: getSelectionBoundingClientRect, - ...options, - }); - - // const selectionTextLength = selectionText?.length ?? 0; - const { update } = popperResult; - // - useEffect(() => { - update?.(); - }, [editor.selection, update]); - - return popperResult; -}; - -const A = () => { - const popperRef = useRef(null); - - const popperOptions: UsePopperPositionOptions = { - popperElement: popperRef.current, - placement: 'bottom-start', - offset: [0, 8], - }; - - const { styles: popperStyles, attributes } = useBalloonToolbarPopper( - popperOptions - ); - - return ( -
- ); -}; - const App = () => { const containerRef = useRef(null); @@ -316,12 +242,9 @@ const App = () => { initialValue={playgroundValue} plugins={plugins} > - {/* */} - - diff --git a/packages/core/src/components/plate/Plate.tsx b/packages/core/src/components/plate/Plate.tsx index 71cbeef40b..d5a6efe2be 100644 --- a/packages/core/src/components/plate/Plate.tsx +++ b/packages/core/src/components/plate/Plate.tsx @@ -1,4 +1,4 @@ -import React, { Ref, useEffect } from 'react'; +import React, { ReactNode, Ref, useEffect } from 'react'; import { Editable, Slate } from 'slate-react'; import { plateIdAtom, SCOPE_PLATE } from '../../atoms/plateIdAtom'; import { usePlate } from '../../hooks/plate/usePlate'; @@ -85,22 +85,74 @@ export const PlateContent = < }: PlateProps) => { const { slateProps, editableProps } = usePlate(options); - if (!slateProps.editor) return null; + const editor = slateProps.editor as E | undefined; + + if (!editor) return null; + + const { plugins } = editor; const editable = ; - return ( - + let afterEditable: ReactNode = null; + let beforeEditable: ReactNode = null; + + plugins.forEach((plugin) => { + const { renderBeforeEditable, renderAfterEditable } = plugin; + + if (renderAfterEditable) { + afterEditable = ( + <> + {afterEditable} + {renderAfterEditable({ + editor, + plugin, + })} + + ); + } + + if (renderBeforeEditable) { + beforeEditable = ( + <> + {beforeEditable} + {renderBeforeEditable({ + editor, + plugin, + })} + + ); + } + }); + + let aboveEditable: ReactNode = ( + <> {firstChildren} + {beforeEditable} + {renderEditable ? renderEditable(editable) : editable} + {afterEditable} + {children} - + ); + + plugins.forEach((plugin) => { + const { renderAboveEditable } = plugin; + + if (renderAboveEditable) + aboveEditable = renderAboveEditable({ + editor, + plugin, + children: aboveEditable, + }); + }); + + return {aboveEditable}; }; export const Plate = < diff --git a/packages/core/src/types/plugin/PlatePlugin.ts b/packages/core/src/types/plugin/PlatePlugin.ts index 0f473ad958..9ccaa7bcf9 100644 --- a/packages/core/src/types/plugin/PlatePlugin.ts +++ b/packages/core/src/types/plugin/PlatePlugin.ts @@ -1,3 +1,4 @@ +import { ReactNode } from 'react'; import { Value } from '../../slate/editor/TEditor'; import { AnyObject } from '../misc/AnyObject'; import { Nullable } from '../misc/Nullable'; @@ -146,6 +147,31 @@ export type PlatePlugin< */ props?: PlatePluginProps; + /** + * Render a component above `Editable`. + */ + renderAboveEditable?: (props: { + editor: E; + plugin: WithPlatePlugin; + children: ReactNode; + }) => ReactNode; + + /** + * Render a component after `Editable`. + */ + renderAfterEditable?: (props: { + editor: E; + plugin: WithPlatePlugin; + }) => ReactNode; + + /** + * Render a component before `Editable`. + */ + renderBeforeEditable?: (props: { + editor: E; + plugin: WithPlatePlugin; + }) => ReactNode; + /** * Property used by `serializeHtml` util to replace `renderElement` and `renderLeaf` when serializing a node of this `type`. */ diff --git a/packages/editor/combobox/src/combobox.store.ts b/packages/editor/combobox/src/combobox.store.ts index 939e9ccebb..a13109e526 100644 --- a/packages/editor/combobox/src/combobox.store.ts +++ b/packages/editor/combobox/src/combobox.store.ts @@ -1,4 +1,5 @@ import { createStore, StateActions, StoreApi } from '@udecode/plate-core'; +import { UseVirtualFloatingOptions } from '@udecode/plate-floating/src/index'; import { Range } from 'slate'; import { ComboboxOnSelectItem, NoData, TComboboxItem } from './types'; @@ -82,15 +83,9 @@ export type ComboboxState = { highlightedIndex: number; /** - * Parent element of the popper element (the one that has the scroll). - * @default document + * Overrides `useFloating` options. */ - popperContainer?: Document | HTMLElement; - - /** - * Overrides `usePopper` options. - */ - popperOptions?: any; + floatingOptions?: Partial; /** * Range from the trigger to the cursor. diff --git a/packages/floating/src/createVirtualElement.ts b/packages/floating/src/createVirtualElement.ts index 940e483011..344eb8bef1 100644 --- a/packages/floating/src/createVirtualElement.ts +++ b/packages/floating/src/createVirtualElement.ts @@ -1,6 +1,7 @@ +import { ClientRectObject } from '@floating-ui/core'; import { VirtualElement } from '@floating-ui/react-dom-interactions'; -export const getDefaultBoundingClientRect = () => ({ +export const getDefaultBoundingClientRect = (): ClientRectObject => ({ width: 0, height: 0, x: 0, diff --git a/packages/floating/src/floating-ui.ts b/packages/floating/src/floating-ui.ts index 358efc1789..02ca5ad045 100644 --- a/packages/floating/src/floating-ui.ts +++ b/packages/floating/src/floating-ui.ts @@ -1 +1,85 @@ -export * from '@floating-ui/react-dom-interactions'; +export { + // core + inline, + limitShift, + offset, + // dom + arrow, + autoPlacement, + autoUpdate, + computePosition, + detectOverflow, + flip, + getOverflowAncestors, + hide, + shift, + size, + // react-dom-interactions + safePolygon, + useClick, + useDelayGroup, + useDelayGroupContext, + useDismiss, + useFloating, + useFloatingNodeId, + useFloatingParentNodeId, + useFloatingPortalNode, + useFloatingTree, + useFocus, + useHover, + useId, + useInteractions, + useListNavigation, + useRole, + useTypeahead, +} from '@floating-ui/react-dom-interactions'; + +export type { + AlignedPlacement, + AutoUpdateOptions, + Axis, + Boundary, + ClientRectObject, + ComputePositionConfig, + ComputePositionReturn, + ContextData, + Coords, + DetectOverflowOptions, + Dimensions, + ElementContext, + ElementProps, + ElementRects, + Elements, + FloatingContext, + FloatingDelayGroup, + FloatingElement, + FloatingEvents, + FloatingFocusManager, + FloatingNode, + FloatingNodeType, + FloatingOverlay, + FloatingPortal, + FloatingTree, + FloatingTreeType, + Length, + Middleware, + MiddlewareArguments, + MiddlewareData, + MiddlewareReturn, + NodeScroll, + Padding, + Placement, + Platform, + Rect, + ReferenceElement, + ReferenceType, + RootBoundary, + Side, + SideObject, + SizeOptions, + Strategy, + UseFloatingData, + UseFloatingProps, + UseFloatingReturn, + VirtualElement, +} from '@floating-ui/react-dom-interactions'; diff --git a/packages/floating/src/utils/getRangeBoundingClientRect.ts b/packages/floating/src/utils/getRangeBoundingClientRect.ts index 214ec12cc9..b6fb54b94a 100644 --- a/packages/floating/src/utils/getRangeBoundingClientRect.ts +++ b/packages/floating/src/utils/getRangeBoundingClientRect.ts @@ -1,5 +1,7 @@ +import { ClientRectObject } from '@floating-ui/core'; import { toDOMRange, TReactEditor, Value } from '@udecode/plate-core'; import { Range } from 'slate'; +import { getDefaultBoundingClientRect } from '../createVirtualElement'; /** * Get bounding client rect by slate range @@ -7,11 +9,11 @@ import { Range } from 'slate'; export const getRangeBoundingClientRect = ( editor: TReactEditor, at: Range | null -) => { - if (!at) return; +): ClientRectObject => { + if (!at) return getDefaultBoundingClientRect(); const domRange = toDOMRange(editor, at); - if (!domRange) return; + if (!domRange) return getDefaultBoundingClientRect(); return domRange.getBoundingClientRect(); }; diff --git a/packages/headless/package.json b/packages/headless/package.json index e1716d5076..0e816ba7a3 100644 --- a/packages/headless/package.json +++ b/packages/headless/package.json @@ -29,6 +29,7 @@ "@udecode/plate-combobox": "14.0.2", "@udecode/plate-core": "14.0.2", "@udecode/plate-find-replace": "14.0.2", + "@udecode/plate-floating": "14.0.2", "@udecode/plate-font": "14.0.2", "@udecode/plate-heading": "14.0.2", "@udecode/plate-highlight": "14.0.2", diff --git a/packages/headless/src/index.tsx b/packages/headless/src/index.tsx index 71f9f152d0..4da7b5c497 100644 --- a/packages/headless/src/index.tsx +++ b/packages/headless/src/index.tsx @@ -12,6 +12,7 @@ export * from '@udecode/plate-code-block'; export * from '@udecode/plate-combobox'; export * from '@udecode/plate-core'; export * from '@udecode/plate-find-replace'; +export * from '@udecode/plate-floating'; export * from '@udecode/plate-font'; export * from '@udecode/plate-heading'; export * from '@udecode/plate-highlight'; diff --git a/packages/ui/combobox/package.json b/packages/ui/combobox/package.json index 42d2ef414f..3511fcf53f 100644 --- a/packages/ui/combobox/package.json +++ b/packages/ui/combobox/package.json @@ -21,8 +21,8 @@ "dependencies": { "@udecode/plate-combobox": "14.0.2", "@udecode/plate-core": "14.0.2", - "@udecode/plate-styled-components": "14.0.2", - "@udecode/plate-ui-popper": "14.0.2" + "@udecode/plate-floating": "14.0.2", + "@udecode/plate-styled-components": "14.0.2" }, "peerDependencies": { "react": ">=16.8.0", diff --git a/packages/ui/combobox/src/Combobox.tsx b/packages/ui/combobox/src/Combobox.tsx index 30ac5a7b25..df76ba37ee 100644 --- a/packages/ui/combobox/src/Combobox.tsx +++ b/packages/ui/combobox/src/Combobox.tsx @@ -11,12 +11,12 @@ import { useComboboxSelectors, } from '@udecode/plate-combobox'; import { useEditorState, useEventEditorSelectors } from '@udecode/plate-core'; -import { PortalBody } from '@udecode/plate-styled-components'; import { getRangeBoundingClientRect, - usePopperPosition, - virtualReference, -} from '@udecode/plate-ui-popper'; + offset, + useVirtualFloating, +} from '@udecode/plate-floating'; +import { PortalBody } from '@udecode/plate-styled-components'; import { getComboboxStyles } from './Combobox.styles'; import { ComboboxProps } from './Combobox.types'; @@ -38,8 +38,7 @@ const ComboboxContent = ( const targetRange = useComboboxSelectors.targetRange(); const filteredItems = useComboboxSelectors.filteredItems(); const highlightedIndex = useComboboxSelectors.highlightedIndex(); - const popperContainer = useComboboxSelectors.popperContainer?.(); - const popperOptions = useComboboxSelectors.popperOptions?.(); + const floatingOptions = useComboboxSelectors.floatingOptions?.(); const editor = useEditorState(); const combobox = useComboboxControls(); const activeComboboxStore = useActiveComboboxStore()!; @@ -50,8 +49,6 @@ const ComboboxContent = ( const maxSuggestions = activeComboboxStore.use.maxSuggestions?.() ?? storeItems.length; - const popperRef = React.useRef(null); - // Update items useEffect(() => { items && comboboxActions.items(items); @@ -73,18 +70,16 @@ const ComboboxContent = ( // Get target range rect const getBoundingClientRect = useCallback( - () => getRangeBoundingClientRect(editor, targetRange) ?? virtualReference, + () => getRangeBoundingClientRect(editor, targetRange), [editor, targetRange] ); // Update popper position - const { styles: popperStyles, attributes } = usePopperPosition({ - popperElement: popperRef.current, - popperContainer, - popperOptions, + const { style, floating } = useVirtualFloating({ placement: 'bottom-start', getBoundingClientRect, - offset: [0, 4], + middleware: [offset(4)], + ...floatingOptions, }); const menuProps = combobox @@ -99,11 +94,10 @@ const ComboboxContent = (