/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {LexicalEditor} from 'lexical';
import * as React from 'react';
import {
type ForwardedRef,
forwardRef,
type JSX,
type RefCallback,
useCallback,
useMemo,
useState,
} from 'react';
import {mergeRefs} from './mergeRefs';
import useLayoutEffect from './useLayoutEffect';
/**
* Props for the {@link ContentEditableElement} component. In addition to an
* `editor`, it accepts the standard `
` HTML attributes (except
* `placeholder`), including the hyphenated `aria-*` attributes, which are the
* preferred way to set ARIA properties. The camelCase `aria*` props (such as
* `ariaLabel`) are also accepted but are retained only for backwards
* compatibility.
*/
export type ContentEditableElementProps = {
editor: LexicalEditor;
ariaActiveDescendant?: React.AriaAttributes['aria-activedescendant'];
ariaAutoComplete?: React.AriaAttributes['aria-autocomplete'];
ariaControls?: React.AriaAttributes['aria-controls'];
ariaDescribedBy?: React.AriaAttributes['aria-describedby'];
ariaErrorMessage?: React.AriaAttributes['aria-errormessage'];
ariaExpanded?: React.AriaAttributes['aria-expanded'];
ariaInvalid?: React.AriaAttributes['aria-invalid'];
ariaLabel?: React.AriaAttributes['aria-label'];
ariaLabelledBy?: React.AriaAttributes['aria-labelledby'];
ariaMultiline?: React.AriaAttributes['aria-multiline'];
ariaOwns?: React.AriaAttributes['aria-owns'];
ariaRequired?: React.AriaAttributes['aria-required'];
autoCapitalize?: HTMLDivElement['autocapitalize'];
'data-testid'?: string | null | undefined;
} & Omit
, 'placeholder'>;
function ContentEditableElementImpl(
{
editor,
ariaActiveDescendant,
ariaAutoComplete,
ariaControls,
ariaDescribedBy,
ariaErrorMessage,
ariaExpanded,
ariaInvalid,
ariaLabel,
ariaLabelledBy,
ariaMultiline,
ariaOwns,
ariaRequired,
autoCapitalize,
className,
id,
role = 'textbox',
spellCheck = true,
style,
tabIndex,
'data-testid': testid,
...rest
}: ContentEditableElementProps,
ref: ForwardedRef,
): JSX.Element {
const [isEditable, setEditable] = useState(editor.isEditable());
const handleRef = useCallback>(
rootElement => {
// defaultView is required for a root element.
// In multi-window setups, the defaultView may not exist at certain points.
if (
rootElement &&
rootElement.ownerDocument &&
rootElement.ownerDocument.defaultView
) {
editor.setRootElement(rootElement);
} else {
editor.setRootElement(null);
}
},
[editor],
);
const mergedRefs = useMemo(() => mergeRefs(ref, handleRef), [handleRef, ref]);
useLayoutEffect(() => {
setEditable(editor.isEditable());
return editor.registerEditableListener(currentIsEditable => {
setEditable(currentIsEditable);
});
}, [editor]);
return (
);
}
/**
* A lower-level building block for the editor's editable ``. It binds the
* given `editor` to the rendered element via
* {@link LexicalEditor.setRootElement}, reflects the editor's editable state on
* the `contentEditable` attribute, and applies the provided ARIA and HTML
* attributes. Prefer {@link ContentEditable}, which reads the editor from
* context and adds placeholder support, unless you need this extra control.
*/
// Annotated by hand: React's forwardRef is not a Lexical factory, so the build
// does not annotate it, and an unannotated module-scope call pins the module
// into every bundle that imports it.
export const ContentEditableElement = /* @__PURE__ */ forwardRef(
ContentEditableElementImpl,
);