From a0cc5d712f8fcfd1cd9c0e569424c128a43775c1 Mon Sep 17 00:00:00 2001 From: Augusto Moura Date: Fri, 7 Jul 2023 03:29:12 -0300 Subject: [PATCH 1/5] feat(rich-text-editor): emoji plugin as a component --- .../src/LexicalEditor/LexicalEditor.test.tsx | 3 +- .../src/LexicalEditor/LexicalEditor.tsx | 22 +++---- .../useComponentPlugins.tsx | 46 +++++++++++---- .../src/LexicalEditor/types.ts | 28 --------- .../LexicalEditorToolbarPlugin.tsx | 45 ++------------- .../src/RichTextEditor/RichTextEditor.tsx | 22 +++---- .../src/RichTextEditor/index.ts | 4 +- .../RichTextEditor/story/Emoji.example.tsx | 7 ++- .../src/RichTextEditor/types.ts | 2 - .../RichTextEditorEmojiPicker.tsx | 6 +- .../RichTextEditorToolbar.tsx | 35 +++--------- .../plugins/EmojiPlugin/EmojiPlugin.test.ts | 6 +- .../src/plugins/EmojiPlugin/EmojiPlugin.tsx | 57 +++++++++++++++++-- .../src/plugins/EmojiPlugin/index.ts | 1 + .../src/plugins/EmojiPlugin/types.ts | 26 +++++++++ .../src/plugins/LinkPlugin/LinkPlugin.tsx | 2 +- .../src/plugins/api.tsx | 4 +- 17 files changed, 160 insertions(+), 156 deletions(-) diff --git a/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.test.tsx b/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.test.tsx index 4e5d637aa5..1fc6277798 100644 --- a/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.test.tsx +++ b/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.test.tsx @@ -13,7 +13,6 @@ import { LinkPlugin, } from '../plugins' import ToolbarPlugin from '../LexicalEditorToolbarPlugin' -import type { CustomEmojiGroup } from './types' jest.mock('../LexicalEditorToolbarPlugin', () => ({ __esModule: true, @@ -189,7 +188,7 @@ describe('LexicalEditor', () => { it('renders ToolbarPlugin with correct props', () => { renderLexicalEditor({ disabled: true, - customEmojis: ['foo' as unknown as CustomEmojiGroup], + customEmojis: ['foo' as any], plugins: ['link'], }) diff --git a/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.tsx b/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.tsx index 9838f12628..5a44af53c8 100644 --- a/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.tsx +++ b/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.tsx @@ -20,9 +20,7 @@ import { $getRoot } from 'lexical' import ToolbarPlugin from '../LexicalEditorToolbarPlugin' import { RTEPluginContextProvider } from '../plugins/api' -import { CustomEmojiNode } from '../plugins/EmojiPlugin/nodes/CustomEmojiNode' import { - EmojiPlugin, ListPlugin, TextLengthPlugin, HeadingsReplacementPlugin, @@ -35,11 +33,11 @@ import { useComponentPlugins } from './hooks/useComponentPlugins/useComponentPlu import styles from './styles' import type { ChangeHandler, - CustomEmojiGroup, EditorPlugin, TextLengthChangeHandler, } from './types' import { cleanupHtmlOutput, createLexicalTheme, setEditorValue } from './utils' +import type { CustomEmojiGroup } from '../plugins/EmojiPlugin' const useStyles = makeStyles(styles, { name: 'LexicalEditor', @@ -96,7 +94,7 @@ export type Props = BaseProps & { } const LexicalEditor = forwardRef(function LexicalEditor( - props, + props: Props, ref ) { const { @@ -133,7 +131,10 @@ const LexicalEditor = forwardRef(function LexicalEditor( [typographyClassNames, classes] ) - const { componentPlugins, lexicalNodes } = useComponentPlugins(plugins) + const { componentPlugins, lexicalNodes } = useComponentPlugins( + plugins, + customEmojis + ) const editorConfig: InitialConfigType = useMemo( () => ({ @@ -144,13 +145,7 @@ const LexicalEditor = forwardRef(function LexicalEditor( throw error }, namespace: 'editor', - nodes: [ - CustomEmojiNode, - ListNode, - ListItemNode, - HeadingNode, - ...lexicalNodes, - ], + nodes: [ListNode, ListItemNode, HeadingNode, ...lexicalNodes], editable: !disabled, }), [defaultValue, theme, disabled, lexicalNodes] @@ -198,8 +193,6 @@ const LexicalEditor = forwardRef(function LexicalEditor( toolbarRef={toolbarRef} // remount Toolbar when disabled key={`${disabled || !focused}`} - customEmojis={customEmojis} - plugins={plugins} testIds={testIds} /> @@ -213,7 +206,6 @@ const LexicalEditor = forwardRef(function LexicalEditor( - {hiddenInputId && ( diff --git a/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useComponentPlugins/useComponentPlugins.tsx b/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useComponentPlugins/useComponentPlugins.tsx index 3cf29e448e..c2b4a98f69 100644 --- a/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useComponentPlugins/useComponentPlugins.tsx +++ b/packages/picasso-rich-text-editor/src/LexicalEditor/hooks/useComponentPlugins/useComponentPlugins.tsx @@ -1,19 +1,45 @@ +import type { ReactElement } from 'react' import React, { cloneElement } from 'react' -import { isRTEPluginElement, RTEPluginMeta } from '../../../plugins/api' -import { LinkPlugin } from '../../../plugins' import type { EditorPlugin } from '../..' +import { LinkPlugin } from '../../../plugins' +import type { RTEPlugin } from '../../../plugins/api' +import { isRTEPluginElement, RTEPluginMeta } from '../../../plugins/api' +import type { CustomEmojiGroup } from '../../../plugins/EmojiPlugin' +import EmojiPlugin from '../../../plugins/EmojiPlugin' -export const useComponentPlugins = (plugins: EditorPlugin[]) => { - const mappedPlugins: EditorPlugin[] = plugins.map(plugin => { - switch (plugin) { - case 'link': - return +const uniquePlugins = () => { + const plugins = new Set() - default: - return plugin + return ({ type }: ReactElement>): boolean => { + if (plugins.has(type)) { + return false } - }) + + plugins.add(type) + + return true + } +} + +export const useComponentPlugins = ( + plugins: EditorPlugin[], + customEmojis: CustomEmojiGroup[] | undefined +) => { + const mappedPlugins: EditorPlugin[] = plugins + .map(plugin => { + switch (plugin) { + case 'link': + return + + case 'emoji': + return + + default: + return plugin + } + }) + .filter(uniquePlugins()) const componentPlugins = mappedPlugins.filter(isRTEPluginElement) diff --git a/packages/picasso-rich-text-editor/src/LexicalEditor/types.ts b/packages/picasso-rich-text-editor/src/LexicalEditor/types.ts index a00a45dada..acb4e8a2cc 100644 --- a/packages/picasso-rich-text-editor/src/LexicalEditor/types.ts +++ b/packages/picasso-rich-text-editor/src/LexicalEditor/types.ts @@ -10,31 +10,3 @@ export type EditorPlugin = | 'link' | 'emoji' | ReactElement> - -export type CustomEmoji = { - id: string - name: string - keywords: string[] - skins: [ - { - src: string - } - ] -} - -export type CustomEmojiGroup = { - id: string - name: string - emojis: CustomEmoji[] -} - -export type Emoji = { - id: string - name: string - native?: string - unified?: string - keywords: string[] - shortcodes: string - emoticons?: string[] - src?: string -} diff --git a/packages/picasso-rich-text-editor/src/LexicalEditorToolbarPlugin/LexicalEditorToolbarPlugin.tsx b/packages/picasso-rich-text-editor/src/LexicalEditorToolbarPlugin/LexicalEditorToolbarPlugin.tsx index ca30d8f706..df4c54e304 100644 --- a/packages/picasso-rich-text-editor/src/LexicalEditorToolbarPlugin/LexicalEditorToolbarPlugin.tsx +++ b/packages/picasso-rich-text-editor/src/LexicalEditorToolbarPlugin/LexicalEditorToolbarPlugin.tsx @@ -1,44 +1,33 @@ -import React, { useEffect, useReducer } from 'react' -import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND, } from '@lexical/list' -import type { ChangeEvent } from 'react' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { $createHeadingNode } from '@lexical/rich-text' +import { $setBlocksType } from '@lexical/selection' import { $createParagraphNode, $getSelection, $isRangeSelection, FORMAT_TEXT_COMMAND, } from 'lexical' -import { $createHeadingNode } from '@lexical/rich-text' -import { $setBlocksType } from '@lexical/selection' +import type { ChangeEvent } from 'react' +import React, { useEffect, useReducer } from 'react' import { registerLexicalEvents, synchronizeToolbarState, toolbarStateReducer, } from '../LexicalEditor/utils' -import type { - CustomEmojiGroup, - EditorPlugin, - Emoji, -} from '../LexicalEditor/types' -import { - INSERT_CUSTOM_EMOJI_COMMAND, - INSERT_EMOJI_COMMAND, -} from '../plugins/EmojiPlugin/commands' import type { HeaderValue } from '../RichTextEditorToolbar' import RichTextEditorToolbar, { ALLOWED_HEADER_TYPE, } from '../RichTextEditorToolbar' type Props = { - customEmojis?: CustomEmojiGroup[] disabled?: boolean toolbarRef: React.RefObject - plugins?: EditorPlugin[] testIds?: { wrapper?: string editor?: string @@ -53,8 +42,6 @@ type Props = { const LexicalEditorToolbarPlugin = ({ disabled = false, toolbarRef, - customEmojis, - plugins, testIds, }: Props) => { const [editor] = useLexicalComposerContext() @@ -94,24 +81,6 @@ const LexicalEditorToolbarPlugin = ({ ) } - const handleInsertEmoji = (emoji: Emoji) => { - const isNativeEmoji = emoji.native - const isCustomEmoji = emoji.src - - if (isNativeEmoji) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - editor.dispatchCommand(INSERT_EMOJI_COMMAND, emoji.native!) - } - - if (isCustomEmoji) { - editor.dispatchCommand(INSERT_CUSTOM_EMOJI_COMMAND, { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - src: emoji.src!, - id: emoji.id, - }) - } - } - const handleHeaderClick = ({ target: { value }, }: ChangeEvent<{ @@ -138,17 +107,13 @@ const LexicalEditorToolbarPlugin = ({ list, header, }} - id='toolbar' onUnorderedClick={handleUnorderedClick} onOrderedClick={handleOrderedClick} onBoldClick={handleBoldClick} onItalicClick={handleItalicClick} onHeaderChange={handleHeaderClick} disabled={disabled} - onInsertEmoji={handleInsertEmoji} ref={toolbarRef} - customEmojis={customEmojis} - plugins={plugins} testIds={testIds} /> ) diff --git a/packages/picasso-rich-text-editor/src/RichTextEditor/RichTextEditor.tsx b/packages/picasso-rich-text-editor/src/RichTextEditor/RichTextEditor.tsx index a680ff7693..747ad8bdd1 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditor/RichTextEditor.tsx +++ b/packages/picasso-rich-text-editor/src/RichTextEditor/RichTextEditor.tsx @@ -1,23 +1,23 @@ -import React, { forwardRef, useRef, useState, useCallback } from 'react' import type { Theme } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles' +import type { OutlinedInputStatus } from '@toptal/picasso' +import { InputMultilineAdornment } from '@toptal/picasso' import type { BaseProps } from '@toptal/picasso-shared' import { useHasMultilineCounter } from '@toptal/picasso-shared' -import cx from 'classnames' import { noop, usePropDeprecationWarning } from '@toptal/picasso/utils' -import { InputMultilineAdornment } from '@toptal/picasso' -import type { OutlinedInputStatus } from '@toptal/picasso' +import cx from 'classnames' +import React, { forwardRef, useCallback, useRef, useState } from 'react' -import styles from './styles' -import { useCounter } from './hooks' -import type { ASTType } from '../RichText' -import type { CounterMessageSetter } from './types' -import LexicalEditor from '../LexicalEditor' import type { ChangeHandler, - CustomEmojiGroup, EditorPlugin, + LexicalEditorProps, } from '../LexicalEditor' +import LexicalEditor from '../LexicalEditor' +import type { ASTType } from '../RichText' +import { useCounter } from './hooks' +import styles from './styles' +import type { CounterMessageSetter } from './types' export interface Props extends BaseProps { /** Indicates that an element is to be focused on page load */ @@ -86,7 +86,7 @@ export interface Props extends BaseProps { orderedListButton?: string } highlight?: 'autofill' - customEmojis?: CustomEmojiGroup[] + customEmojis?: LexicalEditorProps['customEmojis'] } const useStyles = makeStyles(styles, { diff --git a/packages/picasso-rich-text-editor/src/RichTextEditor/index.ts b/packages/picasso-rich-text-editor/src/RichTextEditor/index.ts index f813768044..000ee2f18f 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditor/index.ts +++ b/packages/picasso-rich-text-editor/src/RichTextEditor/index.ts @@ -1,7 +1,9 @@ import type { OmitInternalProps } from '@toptal/picasso-shared' import type { Props } from './RichTextEditor' -export type { CustomEmojiGroup, RichTextEditorChangeHandler } from './types' +export type { RichTextEditorChangeHandler } from './types' + +export type CustomEmojiGroup = Exclude[0] export { default } from './RichTextEditor' export type RichTextEditorProps = OmitInternalProps diff --git a/packages/picasso-rich-text-editor/src/RichTextEditor/story/Emoji.example.tsx b/packages/picasso-rich-text-editor/src/RichTextEditor/story/Emoji.example.tsx index 9fd91db987..a59ffb8c0e 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditor/story/Emoji.example.tsx +++ b/packages/picasso-rich-text-editor/src/RichTextEditor/story/Emoji.example.tsx @@ -2,9 +2,10 @@ import React, { useState } from 'react' import { Container } from '@toptal/picasso' import { RichTextEditor } from '@toptal/picasso-rich-text-editor' -import type { RichTextEditorChangeHandler, CustomEmojiGroup } from '../types' +import type { RichTextEditorChangeHandler } from '../types' +import type { RichTextEditorProps } from '..' -const customEmojis = [ +const customEmojis: RichTextEditorProps['customEmojis'] = [ { id: 'talent-community', name: 'Talent Community', @@ -21,7 +22,7 @@ const customEmojis = [ }, ], }, -] as CustomEmojiGroup[] +] const Example = () => { const [value, setValue] = useState() diff --git a/packages/picasso-rich-text-editor/src/RichTextEditor/types.ts b/packages/picasso-rich-text-editor/src/RichTextEditor/types.ts index c23309ceca..9354927792 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditor/types.ts +++ b/packages/picasso-rich-text-editor/src/RichTextEditor/types.ts @@ -9,5 +9,3 @@ export type { ChangeHandler as RichTextEditorChangeHandler, TextLengthChangeHandler, } from '../LexicalEditor' - -export type { CustomEmojiGroup, CustomEmoji } from '../LexicalEditor' diff --git a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx index 0066f8cdb3..df7db8ae47 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx +++ b/packages/picasso-rich-text-editor/src/RichTextEditorEmojiPicker/RichTextEditorEmojiPicker.tsx @@ -8,10 +8,9 @@ import cx from 'classnames' import { Container } from '@toptal/picasso' import TextEditorButton from '../RichTextEditorButton' -import type { CustomEmojiGroup, Emoji } from '../LexicalEditor' +import type { CustomEmojiGroup, Emoji } from '../plugins/EmojiPlugin' interface Props { - richEditorId: string customEmojis?: CustomEmojiGroup[] onInsertEmoji: (emoji: Emoji) => void disabled?: boolean @@ -46,7 +45,6 @@ const handleEmojiPickerEscBehaviour = ( } export const RichTextEditorEmojiPicker = ({ - richEditorId, customEmojis, onInsertEmoji, disabled, @@ -100,8 +98,6 @@ export const RichTextEditorEmojiPicker = ({ )} > void onHeaderChange: SelectOnChangeHandler onUnorderedClick: ButtonHandlerType onOrderedClick: ButtonHandlerType - plugins?: EditorPlugin[] - customEmojis?: CustomEmojiGroup[] } const useStyles = makeStyles(styles, { @@ -52,21 +46,16 @@ const useStyles = makeStyles(styles, { export const ALLOWED_HEADER_TYPE = '3' export const RichTextEditorToolbar = forwardRef( - // eslint-disable-next-line complexity function RichTextEditorToolbar(props: Props, ref) { const { disabled, - id, format, onBoldClick, onItalicClick, - onInsertEmoji, onHeaderChange, onUnorderedClick, onOrderedClick, testIds, - plugins, - customEmojis, } = props const { setToolbarPortalEl } = useToolbarPortalRegister() @@ -74,10 +63,8 @@ export const RichTextEditorToolbar = forwardRef( const classes = useStyles(props) const isHeadingFormat = format.header === ALLOWED_HEADER_TYPE - const allowEmojis = plugins?.includes('emoji') - return ( - + ( /> - {allowEmojis && ( - - )} ) } diff --git a/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/EmojiPlugin.test.ts b/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/EmojiPlugin.test.ts index 6b4bf6de5a..12781716b6 100644 --- a/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/EmojiPlugin.test.ts +++ b/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/EmojiPlugin.test.ts @@ -38,7 +38,7 @@ describe('LexicalEmojiPlugin', () => { }) it('registers commands on mount', () => { - renderHook(() => EmojiPlugin()) + renderHook(() => EmojiPlugin({})) expect(mockEditor.registerCommand).toHaveBeenCalledTimes(2) expect(mockEditor.registerCommand).toHaveBeenCalledWith( @@ -54,7 +54,7 @@ describe('LexicalEmojiPlugin', () => { }) it('inserts a text node when the native emoji command is called', () => { - renderHook(() => EmojiPlugin()) + renderHook(() => EmojiPlugin({})) const nativeEmojiCommand = mockEditor.registerCommand.mock.calls[0][1] nativeEmojiCommand('😃') @@ -66,7 +66,7 @@ describe('LexicalEmojiPlugin', () => { it('inserts a custom emoji node when the custom emoji command is called', () => { const payload = { id: 'custom emoji', src: 'https://example.com/emoji.png' } - renderHook(() => EmojiPlugin()) + renderHook(() => EmojiPlugin({})) const customEmojiCommand = mockEditor.registerCommand.mock.calls[1][1] customEmojiCommand(payload) diff --git a/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/EmojiPlugin.tsx b/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/EmojiPlugin.tsx index ab477125cc..3da9d7fe9e 100644 --- a/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/EmojiPlugin.tsx +++ b/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/EmojiPlugin.tsx @@ -1,14 +1,46 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' -import { useEffect } from 'react' import { $createTextNode, $insertNodes, COMMAND_PRIORITY_EDITOR } from 'lexical' +import React, { useCallback, useEffect } from 'react' +import { RichTextEditorEmojiPicker } from '../../RichTextEditorEmojiPicker/RichTextEditorEmojiPicker' +import type { RTEPlugin } from '../api' +import { RTEPluginMeta, Toolbar, useRTEPluginContext } from '../api' import { INSERT_CUSTOM_EMOJI_COMMAND, INSERT_EMOJI_COMMAND } from './commands' -import { $createCustomEmojiNode } from './nodes/CustomEmojiNode' -import type { CustomEmojiPayload } from './types' +import { + $createCustomEmojiNode, + CustomEmojiNode, +} from './nodes/CustomEmojiNode' +import type { CustomEmojiGroup, CustomEmojiPayload, Emoji } from './types' -const EmojiPlugin = () => { +const PLUGIN_NAME = 'emoji' + +export type Props = { + customEmojis?: CustomEmojiGroup[] +} + +const EmojiPlugin: RTEPlugin = ({ customEmojis }: Props) => { const [editor] = useLexicalComposerContext() + const { disabled } = useRTEPluginContext() + + const handleInsertEmoji = useCallback( + (emoji: Emoji) => { + const isNativeEmoji = 'native' in emoji + const isCustomEmoji = 'src' in emoji + + if (isNativeEmoji) { + editor.dispatchCommand(INSERT_EMOJI_COMMAND, emoji.native) + } + + if (isCustomEmoji) { + editor.dispatchCommand(INSERT_CUSTOM_EMOJI_COMMAND, { + src: emoji.src, + id: emoji.id, + }) + } + }, + [editor] + ) useEffect(() => { return mergeRegister( @@ -35,7 +67,22 @@ const EmojiPlugin = () => { ) }, [editor]) - return null + return ( + + + + ) +} + +EmojiPlugin[RTEPluginMeta] = { + name: PLUGIN_NAME, + lexical: { + nodes: [CustomEmojiNode], + }, } export default EmojiPlugin diff --git a/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/index.ts b/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/index.ts index 42a76062a3..b6e8ddfd4c 100644 --- a/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/index.ts +++ b/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/index.ts @@ -1,3 +1,4 @@ export { default } from './EmojiPlugin' export * from './commands' export * from './nodes' +export type { CustomEmojiGroup, Emoji, CustomEmoji } from './types' diff --git a/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/types.ts b/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/types.ts index 8f3ff95a4f..10f78c07f6 100644 --- a/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/types.ts +++ b/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/types.ts @@ -1 +1,27 @@ export type { CustomEmojiPayload } from './nodes/CustomEmojiNode' + +export type CustomEmoji = { + id: string + name: string + keywords: string[] + skins: [ + { + src: string + } + ] +} + +export type CustomEmojiGroup = { + id: string + name: string + emojis: CustomEmoji[] +} + +export type Emoji = { + id: string + name: string + unified?: string + keywords: string[] + shortcodes: string + emoticons?: string[] +} & ({ native: string } | { src: string }) diff --git a/packages/picasso-rich-text-editor/src/plugins/LinkPlugin/LinkPlugin.tsx b/packages/picasso-rich-text-editor/src/plugins/LinkPlugin/LinkPlugin.tsx index beb6da18df..0236ba1438 100644 --- a/packages/picasso-rich-text-editor/src/plugins/LinkPlugin/LinkPlugin.tsx +++ b/packages/picasso-rich-text-editor/src/plugins/LinkPlugin/LinkPlugin.tsx @@ -12,7 +12,7 @@ export type Props = { 'data-testid'?: string } -const LinkPlugin: RTEPlugin = ({ 'data-testid': testId }: Props) => { +const LinkPlugin: RTEPlugin = ({ 'data-testid': testId }: Props) => { return ( <> diff --git a/packages/picasso-rich-text-editor/src/plugins/api.tsx b/packages/picasso-rich-text-editor/src/plugins/api.tsx index e4507d6d47..fb81f99ef1 100644 --- a/packages/picasso-rich-text-editor/src/plugins/api.tsx +++ b/packages/picasso-rich-text-editor/src/plugins/api.tsx @@ -16,14 +16,14 @@ export type RTEPluginMeta = { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface RTEPlugin

{ +export interface RTEPlugin

{ (props: P): ReactElement | null [RTEPluginMeta]?: RTEPluginMeta } export const isRTEPluginElement = (plugin: {}): plugin is ReactElement< unknown, - RTEPlugin + RTEPlugin > => { return ( React.isValidElement(plugin) && From 46daa6d56eb546f12997f8e975381874897f1297 Mon Sep 17 00:00:00 2001 From: Augusto Moura Date: Fri, 7 Jul 2023 03:44:57 -0300 Subject: [PATCH 2/5] test(rich-text-editor): fix tests for emoji plugin --- .../src/LexicalEditor/LexicalEditor.test.tsx | 6 ------ packages/picasso-rich-text-editor/src/plugins/api.tsx | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.test.tsx b/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.test.tsx index 1fc6277798..e6498b9114 100644 --- a/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.test.tsx +++ b/packages/picasso-rich-text-editor/src/LexicalEditor/LexicalEditor.test.tsx @@ -155,8 +155,6 @@ describe('LexicalEditor', () => { expect(mockedToolbarPlugin).toHaveBeenCalledWith( { disabled: true, - customEmojis: undefined, - plugins: [], toolbarRef: { current: null, }, @@ -173,8 +171,6 @@ describe('LexicalEditor', () => { expect(mockedToolbarPlugin).toHaveBeenCalledWith( { disabled: true, - customEmojis: undefined, - plugins: [], toolbarRef: { current: null, }, @@ -195,8 +191,6 @@ describe('LexicalEditor', () => { expect(mockedToolbarPlugin).toHaveBeenCalledWith( { disabled: true, - customEmojis: ['foo'], - plugins: ['link'], toolbarRef: { current: null, }, diff --git a/packages/picasso-rich-text-editor/src/plugins/api.tsx b/packages/picasso-rich-text-editor/src/plugins/api.tsx index fb81f99ef1..cf874e9472 100644 --- a/packages/picasso-rich-text-editor/src/plugins/api.tsx +++ b/packages/picasso-rich-text-editor/src/plugins/api.tsx @@ -4,7 +4,7 @@ import { createPortal } from 'react-dom' import type { LexicalNode, Klass } from 'lexical' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' -import { registerLexicalEvents } from '../LexicalEditor/utils' +import { registerLexicalEvents } from '../LexicalEditor/utils/registerLexicalEvents' export const RTEPluginMeta = Symbol('PicassoRTEPluginMeta') From 8524662fa799c1fd30adda637fc38a4907b6073e Mon Sep 17 00:00:00 2001 From: Augusto Moura Date: Thu, 13 Jul 2023 15:54:36 -0300 Subject: [PATCH 3/5] feat(rich-text-editor): move toolbar plugin groups to portal (#3714) * feat(rich-text-editor): move toolbar plugin groups to portal * chore: add changeset * feat: simplify toolbar portal --- .changeset/good-actors-glow.md | 7 +++ .changeset/tender-kangaroos-shout.md | 7 +++ .../RichTextEditorToolbar.tsx | 6 +- .../src/plugins/Toolbar/Toolbar.tsx | 61 +++++++++++++++++++ .../src/plugins/Toolbar/index.ts | 1 + .../src/plugins/Toolbar/styles.ts | 22 +++++++ .../src/plugins/api.tsx | 46 +++++--------- packages/picasso/src/utils/index.ts | 1 + .../src/utils/use-multiple-forward-refs.ts | 37 +++++++++++ 9 files changed, 154 insertions(+), 34 deletions(-) create mode 100644 .changeset/good-actors-glow.md create mode 100644 .changeset/tender-kangaroos-shout.md create mode 100644 packages/picasso-rich-text-editor/src/plugins/Toolbar/Toolbar.tsx create mode 100644 packages/picasso-rich-text-editor/src/plugins/Toolbar/index.ts create mode 100644 packages/picasso-rich-text-editor/src/plugins/Toolbar/styles.ts create mode 100644 packages/picasso/src/utils/use-multiple-forward-refs.ts diff --git a/.changeset/good-actors-glow.md b/.changeset/good-actors-glow.md new file mode 100644 index 0000000000..48c0375281 --- /dev/null +++ b/.changeset/good-actors-glow.md @@ -0,0 +1,7 @@ +--- +'@toptal/picasso': minor +--- + +### utils + +- add utility for forwarding a ref to multiple holders diff --git a/.changeset/tender-kangaroos-shout.md b/.changeset/tender-kangaroos-shout.md new file mode 100644 index 0000000000..a69d96540b --- /dev/null +++ b/.changeset/tender-kangaroos-shout.md @@ -0,0 +1,7 @@ +--- +'@toptal/picasso-rich-text-editor': patch +--- + +### RichTextEditorToolbar + +- add groups for component plugins using the Toolbar portal automatically diff --git a/packages/picasso-rich-text-editor/src/RichTextEditorToolbar/RichTextEditorToolbar.tsx b/packages/picasso-rich-text-editor/src/RichTextEditorToolbar/RichTextEditorToolbar.tsx index 78b50be033..a08cd9e283 100644 --- a/packages/picasso-rich-text-editor/src/RichTextEditorToolbar/RichTextEditorToolbar.tsx +++ b/packages/picasso-rich-text-editor/src/RichTextEditorToolbar/RichTextEditorToolbar.tsx @@ -8,6 +8,7 @@ import { ListUnordered16, Select, } from '@toptal/picasso' +import { useMultipleForwardRefs } from '@toptal/picasso/utils' import cx from 'classnames' import React, { forwardRef } from 'react' @@ -60,11 +61,13 @@ export const RichTextEditorToolbar = forwardRef( const { setToolbarPortalEl } = useToolbarPortalRegister() + const toolbarRef = useMultipleForwardRefs([ref, setToolbarPortalEl]) + const classes = useStyles(props) const isHeadingFormat = format.header === ALLOWED_HEADER_TYPE return ( - + ( data-testid={testIds?.orderedListButton} /> - ) } diff --git a/packages/picasso-rich-text-editor/src/plugins/Toolbar/Toolbar.tsx b/packages/picasso-rich-text-editor/src/plugins/Toolbar/Toolbar.tsx new file mode 100644 index 0000000000..888dddeb03 --- /dev/null +++ b/packages/picasso-rich-text-editor/src/plugins/Toolbar/Toolbar.tsx @@ -0,0 +1,61 @@ +import type { Theme } from '@material-ui/core' +import { makeStyles } from '@material-ui/core' +import { Container } from '@toptal/picasso' +import type { ReactNode } from 'react' +import React, { createContext, useContext, useState } from 'react' +import { createPortal } from 'react-dom' + +import styles from './styles' + +type ContextValue = { + portalEl?: HTMLElement + setPortalEl: (element: HTMLElement | null) => void +} + +const Context = createContext({ + setPortalEl: () => {}, +}) + +export const ToolbarProvider = ({ children }: { children: ReactNode }) => { + const [portalEl, setPortalEl] = useState(null) + + return ( + + {children} + + ) +} + +export const useToolbarPortalRegister = () => { + const { setPortalEl } = useContext(Context) + + return { + setToolbarPortalEl: setPortalEl, + } +} + +export type Props = { + children: ReactNode + keyName: string +} + +const useStyles = makeStyles(styles, { + name: 'RichTextEditorToolbar', +}) + +export const Toolbar = (props: Props) => { + const { children, keyName } = props + const { portalEl } = useContext(Context) + + const classes = useStyles(props) + + if (!portalEl) { + return null + } + + return createPortal( + {children}, + portalEl, + keyName + ) +} diff --git a/packages/picasso-rich-text-editor/src/plugins/Toolbar/index.ts b/packages/picasso-rich-text-editor/src/plugins/Toolbar/index.ts new file mode 100644 index 0000000000..d24a005a3d --- /dev/null +++ b/packages/picasso-rich-text-editor/src/plugins/Toolbar/index.ts @@ -0,0 +1 @@ +export * from './Toolbar' diff --git a/packages/picasso-rich-text-editor/src/plugins/Toolbar/styles.ts b/packages/picasso-rich-text-editor/src/plugins/Toolbar/styles.ts new file mode 100644 index 0000000000..8e7f716f7b --- /dev/null +++ b/packages/picasso-rich-text-editor/src/plugins/Toolbar/styles.ts @@ -0,0 +1,22 @@ +import type { Theme } from '@material-ui/core/styles' +import { createStyles } from '@material-ui/core/styles' + +export default ({ palette }: Theme) => + createStyles({ + group: { + display: 'flex', + alignItems: 'center', + position: 'relative', + pointerEvents: 'unset', + + '&:not(:last-child):not(:empty)::after': { + content: '""', + height: '1em', + width: '1px', + position: 'relative', + marginLeft: '0.5em', + marginRight: '0.5em', + backgroundColor: palette.grey.lighter2, + }, + }, + }) diff --git a/packages/picasso-rich-text-editor/src/plugins/api.tsx b/packages/picasso-rich-text-editor/src/plugins/api.tsx index cf874e9472..3c857b8b55 100644 --- a/packages/picasso-rich-text-editor/src/plugins/api.tsx +++ b/packages/picasso-rich-text-editor/src/plugins/api.tsx @@ -1,10 +1,10 @@ -import React, { createContext, useState, useContext, useEffect } from 'react' -import type { ReactElement, ReactNode } from 'react' -import { createPortal } from 'react-dom' -import type { LexicalNode, Klass } from 'lexical' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { Klass, LexicalNode } from 'lexical' +import type { ReactElement, ReactNode } from 'react' +import React, { createContext, useContext, useEffect } from 'react' import { registerLexicalEvents } from '../LexicalEditor/utils/registerLexicalEvents' +import { ToolbarProvider } from './Toolbar/Toolbar' export const RTEPluginMeta = Symbol('PicassoRTEPluginMeta') @@ -35,8 +35,6 @@ export const isRTEPluginElement = (plugin: {}): plugin is ReactElement< type RTEPluginContextValue = { disabled: boolean focused: boolean - toolbarPortalEl?: HTMLElement - setToolbarPortalEl: (element: HTMLElement | null) => void } export const useRTEUpdate = (callback: () => void) => { @@ -55,7 +53,6 @@ export const useRTEUpdate = (callback: () => void) => { const RTEPluginContext = createContext({ disabled: false, focused: false, - setToolbarPortalEl: () => {}, }) export type ToolbarPortalProviderProps = { @@ -69,30 +66,20 @@ export const RTEPluginContextProvider = ({ disabled, focused, }: ToolbarPortalProviderProps) => { - const [element, setElement] = useState(null) - const value: RTEPluginContextValue = { disabled, focused, - toolbarPortalEl: element ?? undefined, - setToolbarPortalEl: setElement, } return ( - - {children} - + + + {children} + + ) } -export const useToolbarPortalRegister = () => { - const { setToolbarPortalEl } = useContext(RTEPluginContext) - - return { - setToolbarPortalEl, - } -} - export const useRTEPluginContext = () => { const { disabled, focused } = useContext(RTEPluginContext) @@ -102,13 +89,8 @@ export const useRTEPluginContext = () => { } } -export type ToolbarProps = { - children: ReactNode - keyName: string -} - -export const Toolbar = ({ children, keyName }: ToolbarProps) => { - const { toolbarPortalEl: element } = useContext(RTEPluginContext) - - return <>{element && createPortal(children, element, keyName)} -} +export { + Props as ToolbarProps, + Toolbar, + useToolbarPortalRegister, +} from './Toolbar/Toolbar' diff --git a/packages/picasso/src/utils/index.ts b/packages/picasso/src/utils/index.ts index 4614ed59ad..05bf1f4376 100644 --- a/packages/picasso/src/utils/index.ts +++ b/packages/picasso/src/utils/index.ts @@ -49,6 +49,7 @@ export { default as unsafeErrorLog } from './unsafe-error-log' export { default as useBoolean } from './useBoolean/use-boolean' export { default as sum } from './sum' export type { ReferenceObject } from './use-width-of' +export { default as useMultipleForwardRefs } from './use-multiple-forward-refs' export const Transitions = TransitionUtils diff --git a/packages/picasso/src/utils/use-multiple-forward-refs.ts b/packages/picasso/src/utils/use-multiple-forward-refs.ts new file mode 100644 index 0000000000..9adcdb6a90 --- /dev/null +++ b/packages/picasso/src/utils/use-multiple-forward-refs.ts @@ -0,0 +1,37 @@ +import type { ForwardedRef } from 'react' +import { useCallback } from 'react' + +const forwardRef = (ref: ForwardedRef, value: T) => { + if (typeof ref === 'function') { + ref(value) + } else if (ref) { + ref.current = value + } +} + +/** + * This hook allows to forward ref to multiple holders. + * + * @example + * + * const ref1 = useRef(null) + * const ref2 = useRef(null) + * + * const ref = useMultipleForwardRefs([ref1, ref2]) + * + *

+ * + * console.log(ref1.current) //
+ * console.log(ref2.current) //
+ */ +const useMultipleForwardRefs = (refs: ForwardedRef[]) => + useCallback( + (refValue: T) => { + for (const ref of refs) { + forwardRef(ref, refValue) + } + }, + [...refs] + ) + +export default useMultipleForwardRefs From dc6f8e47282cf7dc1caf2a60d99505c054e85d03 Mon Sep 17 00:00:00 2001 From: Augusto Moura Date: Thu, 13 Jul 2023 15:57:25 -0300 Subject: [PATCH 4/5] chore: add changeset --- .changeset/fifty-pugs-travel.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/fifty-pugs-travel.md diff --git a/.changeset/fifty-pugs-travel.md b/.changeset/fifty-pugs-travel.md new file mode 100644 index 0000000000..87a9d5929e --- /dev/null +++ b/.changeset/fifty-pugs-travel.md @@ -0,0 +1,7 @@ +--- +'@toptal/picasso-rich-text-editor': minor +--- + +### EmojiPlugin + +- implement emoji plugin as a component From b6b0fc3a5e5aaca110119bb500b8d1404142ea97 Mon Sep 17 00:00:00 2001 From: Augusto Moura Date: Thu, 13 Jul 2023 16:56:28 -0300 Subject: [PATCH 5/5] fix(rich-text-editor): disable emoji on not focused --- .../src/plugins/EmojiPlugin/EmojiPlugin.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/EmojiPlugin.tsx b/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/EmojiPlugin.tsx index 3da9d7fe9e..7f066f533e 100644 --- a/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/EmojiPlugin.tsx +++ b/packages/picasso-rich-text-editor/src/plugins/EmojiPlugin/EmojiPlugin.tsx @@ -21,7 +21,7 @@ export type Props = { const EmojiPlugin: RTEPlugin = ({ customEmojis }: Props) => { const [editor] = useLexicalComposerContext() - const { disabled } = useRTEPluginContext() + const { disabled, focused } = useRTEPluginContext() const handleInsertEmoji = useCallback( (emoji: Emoji) => { @@ -72,7 +72,7 @@ const EmojiPlugin: RTEPlugin = ({ customEmojis }: Props) => { )