Skip to content

Commit

Permalink
feat: add emoji plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasSlama committed Jun 28, 2023
1 parent b43658d commit 6eaffc4
Show file tree
Hide file tree
Showing 18 changed files with 448 additions and 27 deletions.
31 changes: 31 additions & 0 deletions packages/picasso/src/LexicalEditor/LexicalEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import LexicalTextLengthPlugin from '../LexicalTextLengthPlugin'
import LexicalHeadingsReplacementPlugin from '../LexicalHeadingsReplacementPlugin'
import ToolbarPlugin from '../LexicalEditorToolbarPlugin'
import LexicalListPlugin from '../LexicalListPlugin'
import type { CustomEmojiGroup } from './types'

jest.mock('../LexicalEditorToolbarPlugin', () => ({
__esModule: true,
Expand All @@ -18,6 +19,10 @@ jest.mock('../LexicalListPlugin', () => ({
__esModule: true,
default: jest.fn(() => <div>LexicalListPlugin</div>),
}))
jest.mock('../LexicalEmojiPlugin', () => ({
__esModule: true,
default: jest.fn(() => <div>LexicalEmojiPlugin</div>),
}))

jest.mock('@lexical/react/LexicalComposerContext', () => ({
__esModule: true,
Expand Down Expand Up @@ -136,6 +141,8 @@ describe('LexicalEditor', () => {
expect(mockedToolbarPlugin).toHaveBeenCalledWith(
{
disabled: true,
customEmojis: undefined,
plugins: [],
toolbarRef: {
current: null,
},
Expand All @@ -152,6 +159,30 @@ describe('LexicalEditor', () => {
expect(mockedToolbarPlugin).toHaveBeenCalledWith(
{
disabled: true,
customEmojis: undefined,
plugins: [],
toolbarRef: {
current: null,
},
},
{}
)
})
})

describe('when customEmojis and plugins prop is passed', () => {
it('renders ToolbarPlugin with correct props', () => {
renderLexicalEditor({
disabled: true,
customEmojis: ['foo' as unknown as CustomEmojiGroup],
plugins: ['link'],
})

expect(mockedToolbarPlugin).toHaveBeenCalledWith(
{
disabled: true,
customEmojis: ['foo'],
plugins: ['link'],
toolbarRef: {
current: null,
},
Expand Down
42 changes: 27 additions & 15 deletions packages/picasso/src/LexicalEditor/LexicalEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,29 @@ import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin'
import { ContentEditable } from '@lexical/react/LexicalContentEditable'
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'
import { HeadingNode } from '@lexical/rich-text'
import { $generateHtmlFromNodes } from '@lexical/html'
import { ListItemNode, ListNode } from '@lexical/list'
import { $isRootTextContentEmpty } from '@lexical/text'
import { ListItemNode, ListNode } from '@lexical/list'
import { HeadingNode } from '@lexical/rich-text'

import { createLexicalTheme } from './utils'
import noop from '../utils/noop'
import Container from '../Container'
import Typography from '../Typography'
import { useTypographyClasses, useOnFocus } from './hooks'
import styles from './styles'
import type { ChangeHandler, TextLengthChangeHandler } from './types'
import type {
ChangeHandler,
EditorPlugin,
TextLengthChangeHandler,
CustomEmojiGroup,
} from './types'
import ToolbarPlugin from '../LexicalEditorToolbarPlugin'
import LexicalTextLengthPlugin from '../LexicalTextLengthPlugin'
import LexicalListPlugin from '../LexicalListPlugin'
import LexicalHeadingsReplacementPlugin from '../LexicalHeadingsReplacementPlugin'
import { CustomEmojiNode } from '../LexicalEmojiPlugin/nodes/CustomEmojiNode'
import LexicalEmojiPlugin from '../LexicalEmojiPlugin'

const useStyles = makeStyles<Theme>(styles, {
name: 'LexicalEditor',
Expand Down Expand Up @@ -65,8 +72,7 @@ export type Props = BaseProps & {
onTextLengthChange: TextLengthChangeHandler
/** The placeholder attribute specifies a short hint that describes the expected value of a text editor. */
placeholder?: string
/** List of plugins to enable on the editor */
// plugins?: EditorPlugin[]

testIds?: {
editor?: string
// headerSelect?: string
Expand All @@ -75,15 +81,17 @@ export type Props = BaseProps & {
// unorderedListButton?: string
// orderedListButton?: string
}
// customEmojis?: CustomEmojiGroup[]
customEmojis?: CustomEmojiGroup[]
/** List of plugins to enable on the editor */
plugins?: EditorPlugin[]
}

const LexicalEditor = forwardRef<HTMLDivElement, Props>(function LexicalEditor(
props,
ref
) {
const {
// plugins,
plugins = [],
autoFocus = false,
// defaultValue,
disabled = false,
Expand All @@ -105,7 +113,7 @@ const LexicalEditor = forwardRef<HTMLDivElement, Props>(function LexicalEditor(
// @todo don't know what to do with NAME prop
// name,
// highlight,
// customEmojis,
customEmojis,
} = props

const classes = useStyles()
Expand All @@ -125,21 +133,22 @@ const LexicalEditor = forwardRef<HTMLDivElement, Props>(function LexicalEditor(
typographyClassNames,
classes,
}),
[typographyClassNames, classes.paragraph]
[typographyClassNames, classes]
)

const editorConfig: InitialConfigType = useMemo(
() => ({
const editorConfig = useMemo(() => {
const config: InitialConfigType = {
theme,
onError(error: Error) {
throw error
},
namespace: 'editor',
nodes: [ListNode, ListItemNode, HeadingNode],
nodes: [CustomEmojiNode, ListNode, ListItemNode, HeadingNode],
editable: !disabled,
}),
[theme, disabled]
)
}

return config
}, [theme, disabled])

const handleChange = useCallback(
(editorState, editor) => {
Expand Down Expand Up @@ -170,13 +179,16 @@ const LexicalEditor = forwardRef<HTMLDivElement, Props>(function LexicalEditor(
toolbarRef={toolbarRef}
// remount Toolbar when disabled
key={`${disabled || !isFocused}`}
customEmojis={customEmojis}
plugins={plugins}
/>
<OnChangePlugin ignoreSelectionChange onChange={handleChange} />
{autoFocus && <AutoFocusPlugin />}

<LexicalHeadingsReplacementPlugin />
<LexicalTextLengthPlugin onTextLengthChange={onTextLengthChange} />
<LexicalListPlugin />
<LexicalEmojiPlugin />

<div className={classes.editorContainer} id={id} ref={ref}>
<RichTextPlugin
Expand Down
7 changes: 7 additions & 0 deletions packages/picasso/src/LexicalEditor/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,12 @@ export default ({ typography }: Theme) => {
italic: {
fontStyle: 'italic',
},
customEmoji: {
'& > img': {
verticalAlign: 'bottom',
width: '22px',
height: '22px',
},
},
})
}
32 changes: 32 additions & 0 deletions packages/picasso/src/LexicalEditor/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
export type SettingName = 'link' | 'emoji'

export type ChangeHandler = (html: string) => void

export type { TextLengthChangeHandler } from '../LexicalTextLengthPlugin'

export type EditorPlugin = SettingName

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
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const createLexicalTheme = ({
ul: classes.ul,
ol: classes.ol,
},
customEmoji: classes.customEmoji,
}

return theme
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { ToolbarActions } from './toolbarState'
import { synchronizeToolbarState } from './synchronizeToolbarState'
import { getLexicalNode } from './getLexicalNode'

// jest.mock('../nodes/CustomEmojiNode', () => ({
// default: jest.fn(),
// }))

jest.mock('lexical', () => ({
$getSelection: jest.fn(),
$isRangeSelection: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,32 @@ import {
toolbarStateReducer,
} from '../LexicalEditor/utils'
import { noop } from '../utils'
import type {
CustomEmojiGroup,
EditorPlugin,
Emoji,
} from '../LexicalEditor/types'
import {
INSERT_CUSTOM_EMOJI_COMMAND,
INSERT_EMOJI_COMMAND,
} from '../LexicalEmojiPlugin/commands'
import type { HeaderValue } from '../RichTextEditorToolbar'
import RichTextEditorToolbar, {
ALLOWED_HEADER_TYPE,
} from '../RichTextEditorToolbar'

type Props = {
customEmojis?: CustomEmojiGroup[]
disabled?: boolean
toolbarRef: React.RefObject<HTMLDivElement>
plugins?: EditorPlugin[]
}

const LexicalEditorToolbarPlugin = ({
disabled = false,
toolbarRef,
customEmojis,
plugins,
}: Props) => {
const [editor] = useLexicalComposerContext()
const [{ bold, italic, list, header }, dispatch] = useReducer(
Expand Down Expand Up @@ -73,6 +86,24 @@ 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<{
Expand Down Expand Up @@ -108,8 +139,10 @@ const LexicalEditorToolbarPlugin = ({
onLinkClick={noop}
onHeaderChange={handleHeaderClick}
disabled={disabled}
onInsertEmoji={noop}
onInsertEmoji={handleInsertEmoji}
ref={toolbarRef}
customEmojis={customEmojis}
plugins={plugins}
/>
)
}
Expand Down
77 changes: 77 additions & 0 deletions packages/picasso/src/LexicalEmojiPlugin/LexicalEmojiPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { renderHook } from '@testing-library/react-hooks'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $createTextNode, $insertNodes, COMMAND_PRIORITY_EDITOR } from 'lexical'

import LexicalEmojiPlugin, {
INSERT_CUSTOM_EMOJI_COMMAND,
INSERT_EMOJI_COMMAND,
} from './index'
import { $createCustomEmojiNode } from './nodes/CustomEmojiNode'

jest.mock('@lexical/react/LexicalComposerContext', () => ({
useLexicalComposerContext: jest.fn(() => [{}]),
}))

jest.mock('@lexical/utils', () => ({
mergeRegister: jest.fn(),
}))

jest.mock('lexical', () => ({
$createTextNode: jest.fn(),
$insertNodes: jest.fn(),
COMMAND_PRIORITY_EDITOR: jest.fn(),
createCommand: jest.fn(),
}))

jest.mock('./nodes/CustomEmojiNode', () => ({
$createCustomEmojiNode: jest.fn(),
}))

describe('LexicalEmojiPlugin', () => {
const mockEditor = {
registerCommand: jest.fn(),
}

beforeEach(() => {
jest.resetAllMocks()
;(useLexicalComposerContext as jest.Mock).mockReturnValue([mockEditor])
})

it('registers commands on mount', () => {
renderHook(() => LexicalEmojiPlugin())

expect(mockEditor.registerCommand).toHaveBeenCalledTimes(2)
expect(mockEditor.registerCommand).toHaveBeenCalledWith(
INSERT_EMOJI_COMMAND,
expect.any(Function),
COMMAND_PRIORITY_EDITOR
)
expect(mockEditor.registerCommand).toHaveBeenCalledWith(
INSERT_CUSTOM_EMOJI_COMMAND,
expect.any(Function),
COMMAND_PRIORITY_EDITOR
)
})

it('inserts a text node when the native emoji command is called', () => {
renderHook(() => LexicalEmojiPlugin())
const nativeEmojiCommand = mockEditor.registerCommand.mock.calls[0][1]

nativeEmojiCommand('😃')

expect($createTextNode).toHaveBeenCalledWith('😃')
expect($insertNodes).toHaveBeenCalledWith([$createTextNode()])
})

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(() => LexicalEmojiPlugin())
const customEmojiCommand = mockEditor.registerCommand.mock.calls[1][1]

customEmojiCommand(payload)

expect($createCustomEmojiNode).toHaveBeenCalledWith(payload)
expect($insertNodes).toHaveBeenCalledWith([$createCustomEmojiNode(payload)])
})
})
Loading

0 comments on commit 6eaffc4

Please sign in to comment.