1 import { KEY_DOWN_COMMAND, PASTE_COMMAND, type LexicalEditor } from 'lexical'
3 import type { Ref } from 'react'
4 import { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
5 import { mergeRefs } from '../Shared/mergeRefs'
6 import { BEFOREINPUT_EVENT_COMMAND, COMPOSITION_START_EVENT_COMMAND, INPUT_EVENT_COMMAND } from '../Commands/Events'
7 import { eventFiles } from '@lexical/rich-text'
11 isSuggestionMode: boolean
12 ariaActiveDescendant?: React.AriaAttributes['aria-activedescendant']
13 ariaAutoComplete?: React.AriaAttributes['aria-autocomplete']
14 ariaControls?: React.AriaAttributes['aria-controls']
15 ariaDescribedBy?: React.AriaAttributes['aria-describedby']
16 ariaExpanded?: React.AriaAttributes['aria-expanded']
17 ariaLabel?: React.AriaAttributes['aria-label']
18 ariaLabelledBy?: React.AriaAttributes['aria-labelledby']
19 ariaMultiline?: React.AriaAttributes['aria-multiline']
20 ariaOwns?: React.AriaAttributes['aria-owns']
21 ariaRequired?: React.AriaAttributes['aria-required']
22 autoCapitalize?: HTMLDivElement['autocapitalize']
23 'data-testid'?: string | null | undefined
24 } & Omit<React.AllHTMLAttributes<HTMLDivElement>, 'placeholder'>
26 function ContentEditableElementImpl(
47 'data-testid': testid,
50 ref: Ref<HTMLDivElement>,
52 const [isEditable, setEditable] = useState(editor.isEditable())
54 const isSuggestionModeRef = useRef(isSuggestionMode)
56 isSuggestionModeRef.current = isSuggestionMode
57 }, [isSuggestionMode])
59 const handleRef = useCallback(
60 (rootElement: null | HTMLElement) => {
62 rootElement.addEventListener('compositionstart', (event) => {
63 editor.dispatchCommand(COMPOSITION_START_EVENT_COMMAND, event)
65 rootElement.addEventListener('keydown', (event) => {
66 if (isSuggestionModeRef.current) {
67 // We don't want to preventDefault keydown
68 // as that will also stop beforeinput events
69 // from being triggered.
70 event.stopImmediatePropagation()
71 editor.dispatchCommand(KEY_DOWN_COMMAND, event)
74 rootElement.addEventListener('input', (event) => {
75 if (isSuggestionModeRef.current) {
76 event.preventDefault()
77 event.stopImmediatePropagation()
78 editor.dispatchCommand(INPUT_EVENT_COMMAND, event)
81 rootElement.addEventListener('beforeinput', (event) => {
82 if (isSuggestionModeRef.current) {
83 event.preventDefault()
84 event.stopImmediatePropagation()
85 editor.dispatchCommand(BEFOREINPUT_EVENT_COMMAND, event)
88 rootElement.addEventListener('paste', (event) => {
89 const [, files, hasTextContent] = eventFiles(event)
90 if (isSuggestionModeRef.current && files.length > 0 && !hasTextContent) {
91 event.preventDefault()
92 event.stopImmediatePropagation()
93 editor.dispatchCommand(PASTE_COMMAND, event)
97 // defaultView is required for a root element.
98 // In multi-window setups, the defaultView may not exist at certain points.
99 if (rootElement && rootElement.ownerDocument && rootElement.ownerDocument.defaultView) {
100 editor.setRootElement(rootElement)
102 editor.setRootElement(null)
107 const mergedRefs = useMemo(() => mergeRefs(ref, handleRef), [handleRef, ref])
109 useLayoutEffect(() => {
110 setEditable(editor.isEditable())
111 return editor.registerEditableListener((currentIsEditable) => {
112 setEditable(currentIsEditable)
117 // eslint-disable-next-line jsx-a11y/aria-activedescendant-has-tabindex
120 aria-activedescendant={isEditable ? ariaActiveDescendant : undefined}
121 aria-autocomplete={isEditable ? ariaAutoComplete : 'none'}
122 aria-controls={isEditable ? ariaControls : undefined}
123 aria-describedby={ariaDescribedBy}
124 aria-expanded={isEditable && role === 'combobox' ? !!ariaExpanded : undefined}
125 aria-label={ariaLabel}
126 aria-labelledby={ariaLabelledBy}
127 aria-multiline={ariaMultiline}
128 aria-owns={isEditable ? ariaOwns : undefined}
129 aria-readonly={isEditable ? undefined : true}
130 aria-required={ariaRequired}
131 autoCapitalize={autoCapitalize}
132 className={className}
133 contentEditable={isEditable}
137 role={isEditable ? role : undefined}
138 spellCheck={spellCheck}
145 export const ContentEditableElement = forwardRef(ContentEditableElementImpl)