From 55c41a7ab788714960e01a4ba498760ee183d2a2 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Mon, 8 Jul 2024 14:25:00 -0400 Subject: [PATCH 01/15] Placeholder type fixes --- packages/lexical-react/README.md | 2 +- .../flow/LexicalContentEditable.js.flow | 46 ++++++++++--------- .../flow/LexicalPlainTextPlugin.js.flow | 5 +- .../flow/LexicalRichTextPlugin.js.flow | 5 +- .../src/LexicalContentEditable.tsx | 8 ++-- .../docs/getting-started/react.md | 2 +- 6 files changed, 38 insertions(+), 30 deletions(-) diff --git a/packages/lexical-react/README.md b/packages/lexical-react/README.md index 922cef51f0c..b0507783cea 100644 --- a/packages/lexical-react/README.md +++ b/packages/lexical-react/README.md @@ -10,7 +10,7 @@ Install `lexical` and `@lexical/react`: npm install --save lexical @lexical/react ``` -Below is an example of a basic plain text editor using `lexical` and `@lexical/react` ([try it yourself](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-plain-text?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview)). +Below is an example of a basic plain text editor using `lexical` and `@lexical/react` ([try it yourself](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-plain-text?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=0&showSidebar=0&devtoolsheight=0&view=preview)). ```jsx import {$getRoot, $getSelection} from 'lexical'; diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 8f2752fe6d0..e3e6eefbd56 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -5,31 +5,33 @@ * LICENSE file in the root directory of this source tree. * * @flow strict + * @generated + * @oncall lexical_web_text_editor */ import * as React from 'react'; -export type Props = ({...Partial,...} | $ReadOnly<{ - 'aria-placeholder': string; - placeholder: - | ((isEditable: boolean) => null | React$Node) - | null - | React$Node; -}>) & $ReadOnly<{ - ...Partial, - ariaActiveDescendant?: string, - ariaAutoComplete?: string, - ariaControls?: string, - ariaDescribedBy?: string, - ariaExpanded?: boolean, - ariaLabel?: string, - ariaLabelledBy?: string, - ariaMultiline?: boolean, - ariaOwns?: string, - ariaRequired?: boolean, - autoCapitalize?: boolean, - 'data-testid'?: string | null, - ... -}>; +export type Props = + $ReadOnly<{ + ...Partial, + ariaActiveDescendant?: string, + ariaAutoComplete?: string, + ariaControls?: string, + ariaDescribedBy?: string, + ariaExpanded?: boolean, + ariaLabel?: string, + ariaLabelledBy?: string, + ariaMultiline?: boolean, + ariaOwns?: string, + ariaRequired?: boolean, + autoCapitalize?: boolean, + 'data-testid'?: string | null, + 'aria-placeholder': string, + placeholder: + | ((isEditable: boolean) => null | React$Node) + | null + | React$Node, + ... + }> declare export function ContentEditable(props: Props): React$Node; diff --git a/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow b/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow index be50e13f492..7a9e8b403e2 100644 --- a/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow +++ b/packages/lexical-react/flow/LexicalPlainTextPlugin.js.flow @@ -21,6 +21,9 @@ type InitialEditorStateType = declare export function PlainTextPlugin({ contentEditable: React$Node, - placeholder: ((isEditable: boolean) => React$Node) | React$Node, + placeholder?: + | ((isEditable: boolean) => null | React$Node) + | null + | React$Node; ErrorBoundary: LexicalErrorBoundary, }): React$Node; diff --git a/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow b/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow index a99380c5fe4..a07bf9e92f4 100644 --- a/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow +++ b/packages/lexical-react/flow/LexicalRichTextPlugin.js.flow @@ -21,6 +21,9 @@ type InitialEditorStateType = declare export function RichTextPlugin({ contentEditable: React$Node, - placeholder: ((isEditable: boolean) => React$Node) | React$Node, + placeholder?: + | ((isEditable: boolean) => null | React$Node) + | null + | React$Node; ErrorBoundary: LexicalErrorBoundary, }): React$Node; diff --git a/packages/lexical-react/src/LexicalContentEditable.tsx b/packages/lexical-react/src/LexicalContentEditable.tsx index 2ca5839087e..999db64f46e 100644 --- a/packages/lexical-react/src/LexicalContentEditable.tsx +++ b/packages/lexical-react/src/LexicalContentEditable.tsx @@ -17,12 +17,12 @@ import {useCanShowPlaceholder} from './shared/useCanShowPlaceholder'; /* eslint-disable @typescript-eslint/ban-types */ export type Props = ( | {} + | { + placeholder: null; + } | { 'aria-placeholder': string; - placeholder: - | ((isEditable: boolean) => null | JSX.Element) - | null - | JSX.Element; + placeholder: ((isEditable: boolean) => null | JSX.Element) | JSX.Element; } ) & ElementProps; diff --git a/packages/lexical-website/docs/getting-started/react.md b/packages/lexical-website/docs/getting-started/react.md index fd09e720ed8..a52ddd50fa0 100644 --- a/packages/lexical-website/docs/getting-started/react.md +++ b/packages/lexical-website/docs/getting-started/react.md @@ -81,7 +81,7 @@ Below you can find an example of the integration from the previous chapter that However no UI can be created w/o CSS and Lexical is not an exception here. Pay attention to `ExampleTheme.ts` and how it's used in this example, with corresponding styles defined in `styles.css`. - + ## Saving Lexical State From e58edc6e7ef7173d6635a1f28d8c98ddb50890d4 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Mon, 8 Jul 2024 16:56:20 -0400 Subject: [PATCH 02/15] more code --- .../flow/LexicalContentEditable.js.flow | 43 ++++---- .../src/LexicalContentEditable.tsx | 41 ++++--- .../shared/LexicalContentEditableElement.tsx | 100 +++++++++--------- .../lexical-react/src/shared/mergeRefs.ts | 24 +++++ 4 files changed, 124 insertions(+), 84 deletions(-) create mode 100644 packages/lexical-react/src/shared/mergeRefs.ts diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index e3e6eefbd56..40b20c93492 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -11,27 +11,34 @@ import * as React from 'react'; -export type Props = - $ReadOnly<{ - ...Partial, - ariaActiveDescendant?: string, - ariaAutoComplete?: string, - ariaControls?: string, - ariaDescribedBy?: string, - ariaExpanded?: boolean, - ariaLabel?: string, - ariaLabelledBy?: string, - ariaMultiline?: boolean, - ariaOwns?: string, - ariaRequired?: boolean, - autoCapitalize?: boolean, - 'data-testid'?: string | null, - 'aria-placeholder': string, +type PlaceholderProps = + | $ReadOnly<{ + 'aria-placeholder'?: void, + placeholder?: null, + }> + | $ReadOnly<{ + 'aria-placeholder': string, placeholder: | ((isEditable: boolean) => null | React$Node) | null | React$Node, - ... - }> + }>; + +export type Props = $ReadOnly<{ + ...Partial, + ariaActiveDescendant?: string, + ariaAutoComplete?: string, + ariaControls?: string, + ariaDescribedBy?: string, + ariaExpanded?: boolean, + ariaLabel?: string, + ariaLabelledBy?: string, + ariaMultiline?: boolean, + ariaOwns?: string, + ariaRequired?: boolean, + autoCapitalize?: boolean, + 'data-testid'?: string | null, + ... +}> & PlaceholderProps; declare export function ContentEditable(props: Props): React$Node; diff --git a/packages/lexical-react/src/LexicalContentEditable.tsx b/packages/lexical-react/src/LexicalContentEditable.tsx index 999db64f46e..c77f056dea3 100644 --- a/packages/lexical-react/src/LexicalContentEditable.tsx +++ b/packages/lexical-react/src/LexicalContentEditable.tsx @@ -10,43 +10,52 @@ import type {Props as ElementProps} from './shared/LexicalContentEditableElement import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {useLexicalEditable} from '@lexical/react/useLexicalEditable'; +import {forwardRef, Ref} from 'react'; import {ContentEditableElement} from './shared/LexicalContentEditableElement'; import {useCanShowPlaceholder} from './shared/useCanShowPlaceholder'; /* eslint-disable @typescript-eslint/ban-types */ -export type Props = ( - | {} - | { - placeholder: null; - } - | { - 'aria-placeholder': string; - placeholder: ((isEditable: boolean) => null | JSX.Element) | JSX.Element; - } -) & - ElementProps; +export type Props = ElementProps & + ( + | { + 'aria-placeholder'?: void; + placeholder?: null; + } + | { + 'aria-placeholder': string; + placeholder: + | ((isEditable: boolean) => null | JSX.Element) + | JSX.Element; + } + ); + /* eslint-enable @typescript-eslint/ban-types */ -export function ContentEditable(props: Props): JSX.Element { +function ContentEditableImpl( + props: Props, + ref: Ref, +): JSX.Element { let placeholder = null; - let rest = props; + let rest: Omit = props; if ('placeholder' in props) { ({placeholder, ...rest} = props); } return ( <> - - + + {placeholder != null && } ); } +export const ContentEditable = forwardRef(ContentEditableImpl); + function Placeholder({ content, }: { - content: ((isEditable: boolean) => null | JSX.Element) | null | JSX.Element; + content: ((isEditable: boolean) => null | JSX.Element) | JSX.Element; }): null | JSX.Element { const [editor] = useLexicalComposerContext(); const showPlaceholder = useCanShowPlaceholder(editor); diff --git a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx index 328da5d4028..55f9497b01c 100644 --- a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx +++ b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx @@ -8,9 +8,11 @@ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import * as React from 'react'; -import {useCallback, useState} from 'react'; +import {forwardRef, Ref, useState} from 'react'; import useLayoutEffect from 'shared/useLayoutEffect'; +import {mergeRefs} from './mergeRefs'; + export type Props = { ariaActiveDescendant?: React.AriaAttributes['aria-activedescendant']; ariaAutoComplete?: React.AriaAttributes['aria-autocomplete']; @@ -26,46 +28,46 @@ export type Props = { 'data-testid'?: string | null | undefined; } & Omit, 'placeholder'>; -export function ContentEditableElement({ - ariaActiveDescendant, - ariaAutoComplete, - ariaControls, - ariaDescribedBy, - ariaExpanded, - ariaLabel, - ariaLabelledBy, - ariaMultiline, - ariaOwns, - ariaRequired, - autoCapitalize, - className, - id, - role = 'textbox', - spellCheck = true, - style, - tabIndex, - 'data-testid': testid, - ...rest -}: Props): JSX.Element { +function ContentEditableElementImpl( + { + ariaActiveDescendant, + ariaAutoComplete, + ariaControls, + ariaDescribedBy, + ariaExpanded, + ariaLabel, + ariaLabelledBy, + ariaMultiline, + ariaOwns, + ariaRequired, + autoCapitalize, + className, + id, + role = 'textbox', + spellCheck = true, + style, + tabIndex, + 'data-testid': testid, + ...rest + }: Props, + ref: Ref, +): JSX.Element { const [editor] = useLexicalComposerContext(); const [isEditable, setEditable] = useState(false); - const ref = useCallback( - (rootElement: null | HTMLElement) => { - // 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 handleRef = (rootElement: null | HTMLElement) => { + // 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); + } + }; useLayoutEffect(() => { setEditable(editor.isEditable()); @@ -77,33 +79,31 @@ export function ContentEditableElement({ return (
); } + +export const ContentEditableElement = forwardRef(ContentEditableElementImpl); diff --git a/packages/lexical-react/src/shared/mergeRefs.ts b/packages/lexical-react/src/shared/mergeRefs.ts new file mode 100644 index 00000000000..23ddadd0620 --- /dev/null +++ b/packages/lexical-react/src/shared/mergeRefs.ts @@ -0,0 +1,24 @@ +/** + * 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. + * + */ +// Source: https://github.com/gregberge/react-merge-refs/blob/main/src/index.tsx + +export function mergeRefs( + ...refs: Array< + React.MutableRefObject | React.LegacyRef | undefined | null + > +): React.RefCallback { + return (value) => { + refs.forEach((ref) => { + if (typeof ref === 'function') { + ref(value); + } else if (ref != null) { + (ref as React.MutableRefObject).current = value; + } + }); + }; +} From 44068a6020569685368ec8390be76e312e1828ce Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Mon, 8 Jul 2024 16:57:28 -0400 Subject: [PATCH 03/15] revert some stuff --- packages/lexical-react/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-react/README.md b/packages/lexical-react/README.md index b0507783cea..922cef51f0c 100644 --- a/packages/lexical-react/README.md +++ b/packages/lexical-react/README.md @@ -10,7 +10,7 @@ Install `lexical` and `@lexical/react`: npm install --save lexical @lexical/react ``` -Below is an example of a basic plain text editor using `lexical` and `@lexical/react` ([try it yourself](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-plain-text?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=0&showSidebar=0&devtoolsheight=0&view=preview)). +Below is an example of a basic plain text editor using `lexical` and `@lexical/react` ([try it yourself](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-plain-text?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview)). ```jsx import {$getRoot, $getSelection} from 'lexical'; From 274971b767f93c92db4b77054f3539768fd91541 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Mon, 8 Jul 2024 16:58:33 -0400 Subject: [PATCH 04/15] cleanup some more --- packages/lexical-react/flow/LexicalContentEditable.js.flow | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 40b20c93492..f8366e30775 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -5,8 +5,6 @@ * LICENSE file in the root directory of this source tree. * * @flow strict - * @generated - * @oncall lexical_web_text_editor */ import * as React from 'react'; From d7137eb0a9fa8bf6e9b23275d16f7b4982aeda2a Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Mon, 8 Jul 2024 19:28:32 -0400 Subject: [PATCH 05/15] flow... --- .../flow/LexicalContentEditable.js.flow | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index f8366e30775..422938c7244 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -7,23 +7,41 @@ * @flow strict */ +// $FlowFixMe - Not able to type this with a flow extension +import type {TRefFor} from 'CoreTypes.flow'; + import * as React from 'react'; +// Due to Flow limitations, we prefer fixed types over the built-in inexact HTMLElement +type HTMLDivElementDOMProps = $ReadOnly<{ + autoCapitalize?: boolean, + autoComplete?: boolean, + autoCorrect?: boolean, + className?: string, + 'data-testid'?: string, + role?: string, + spellCheck?: boolean, + suppressContentEditableWarning?: boolean, + tabIndex?: number, + style?: CSSStyleDeclaration, + 'data-testid'?: string | null, +}> + type PlaceholderProps = | $ReadOnly<{ 'aria-placeholder'?: void, - placeholder?: null, + placeholderComponent?: null, }> | $ReadOnly<{ 'aria-placeholder': string, - placeholder: + placeholderComponent: | ((isEditable: boolean) => null | React$Node) | null | React$Node, }>; export type Props = $ReadOnly<{ - ...Partial, + ...HTMLDivElementDOMProps, ariaActiveDescendant?: string, ariaAutoComplete?: string, ariaControls?: string, @@ -33,10 +51,10 @@ export type Props = $ReadOnly<{ ariaLabelledBy?: string, ariaMultiline?: boolean, ariaOwns?: string, - ariaRequired?: boolean, + ariaRequired?: string, autoCapitalize?: boolean, - 'data-testid'?: string | null, - ... -}> & PlaceholderProps; + ref?: TRefFor, + ...PlaceholderProps +}> declare export function ContentEditable(props: Props): React$Node; From 2a3c8c7423bd7f29d6888b4b92dd16a1e9888a9d Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Tue, 9 Jul 2024 11:22:10 -0400 Subject: [PATCH 06/15] . --- packages/lexical-website/docs/getting-started/react.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-website/docs/getting-started/react.md b/packages/lexical-website/docs/getting-started/react.md index a52ddd50fa0..fd09e720ed8 100644 --- a/packages/lexical-website/docs/getting-started/react.md +++ b/packages/lexical-website/docs/getting-started/react.md @@ -81,7 +81,7 @@ Below you can find an example of the integration from the previous chapter that However no UI can be created w/o CSS and Lexical is not an exception here. Pay attention to `ExampleTheme.ts` and how it's used in this example, with corresponding styles defined in `styles.css`. - + ## Saving Lexical State From a93bd22f9ee88b0652b47828f463db695783fe4f Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Tue, 9 Jul 2024 13:15:16 -0400 Subject: [PATCH 07/15] . --- packages/lexical-react/flow/LexicalContentEditable.js.flow | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 422938c7244..31c44ee4823 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -30,11 +30,11 @@ type HTMLDivElementDOMProps = $ReadOnly<{ type PlaceholderProps = | $ReadOnly<{ 'aria-placeholder'?: void, - placeholderComponent?: null, + placeholder?: null, }> | $ReadOnly<{ 'aria-placeholder': string, - placeholderComponent: + placeholder: | ((isEditable: boolean) => null | React$Node) | null | React$Node, From dedbd50726740056d6199e93769153e110fcea5b Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Tue, 9 Jul 2024 14:45:42 -0400 Subject: [PATCH 08/15] missing prop --- packages/lexical-react/flow/LexicalContentEditable.js.flow | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 31c44ee4823..2eaf2fcc732 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -17,6 +17,7 @@ type HTMLDivElementDOMProps = $ReadOnly<{ autoCapitalize?: boolean, autoComplete?: boolean, autoCorrect?: boolean, + id?: string, className?: string, 'data-testid'?: string, role?: string, From 177a4b7326d60879b03eecbdd0f1a72a397321b7 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Tue, 9 Jul 2024 17:00:24 -0400 Subject: [PATCH 09/15] add editor deprecated --- .../flow/LexicalContentEditable.js.flow | 2 + .../src/LexicalContentEditable.tsx | 42 ++++++++++++------- .../shared/LexicalContentEditableElement.tsx | 6 ++- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 2eaf2fcc732..7f03a646a2c 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -7,6 +7,7 @@ * @flow strict */ +import type { LexicalEditor } from 'lexical'; // $FlowFixMe - Not able to type this with a flow extension import type {TRefFor} from 'CoreTypes.flow'; @@ -43,6 +44,7 @@ type PlaceholderProps = export type Props = $ReadOnly<{ ...HTMLDivElementDOMProps, + editor__DEPRECATED?: LexicalEditor; ariaActiveDescendant?: string, ariaAutoComplete?: string, ariaControls?: string, diff --git a/packages/lexical-react/src/LexicalContentEditable.tsx b/packages/lexical-react/src/LexicalContentEditable.tsx index c77f056dea3..7102df584f1 100644 --- a/packages/lexical-react/src/LexicalContentEditable.tsx +++ b/packages/lexical-react/src/LexicalContentEditable.tsx @@ -7,17 +7,18 @@ */ import type {Props as ElementProps} from './shared/LexicalContentEditableElement'; +import type {LexicalEditor} from 'lexical'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {useLexicalEditable} from '@lexical/react/useLexicalEditable'; -import {forwardRef, Ref} from 'react'; +import {forwardRef, Ref, useLayoutEffect, useState} from 'react'; import {ContentEditableElement} from './shared/LexicalContentEditableElement'; import {useCanShowPlaceholder} from './shared/useCanShowPlaceholder'; /* eslint-disable @typescript-eslint/ban-types */ -export type Props = ElementProps & - ( +export type Props = Omit & { + editor__DEPRECATED?: LexicalEditor; +} & ( | { 'aria-placeholder'?: void; placeholder?: null; @@ -32,34 +33,43 @@ export type Props = ElementProps & /* eslint-enable @typescript-eslint/ban-types */ +export const ContentEditable = forwardRef(ContentEditableImpl); + function ContentEditableImpl( props: Props, ref: Ref, ): JSX.Element { - let placeholder = null; - let rest: Omit = props; - if ('placeholder' in props) { - ({placeholder, ...rest} = props); - } + const {placeholder, editor__DEPRECATED, ...rest} = props; + // editor__DEPRECATED will always be defined for non MLC surfaces + // eslint-disable-next-line react-hooks/rules-of-hooks + const editor = editor__DEPRECATED || useLexicalComposerContext()[0]; return ( <> - - {placeholder != null && } + + {placeholder != null && ( + + )} ); } -export const ContentEditable = forwardRef(ContentEditableImpl); - function Placeholder({ content, + editor, }: { + editor: LexicalEditor; content: ((isEditable: boolean) => null | JSX.Element) | JSX.Element; }): null | JSX.Element { - const [editor] = useLexicalComposerContext(); const showPlaceholder = useCanShowPlaceholder(editor); - const editable = useLexicalEditable(); + + const [isEditable, setEditable] = useState(false); + useLayoutEffect(() => { + setEditable(editor.isEditable()); + return editor.registerEditableListener((currentIsEditable) => { + setEditable(currentIsEditable); + }); + }, [editor]); if (!showPlaceholder) { return null; @@ -67,7 +77,7 @@ function Placeholder({ let placeholder = null; if (typeof content === 'function') { - placeholder = content(editable); + placeholder = content(isEditable); } else if (content !== null) { placeholder = content; } diff --git a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx index 55f9497b01c..5d275479900 100644 --- a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx +++ b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx @@ -6,7 +6,8 @@ * */ -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import type {LexicalEditor} from 'lexical'; + import * as React from 'react'; import {forwardRef, Ref, useState} from 'react'; import useLayoutEffect from 'shared/useLayoutEffect'; @@ -14,6 +15,7 @@ import useLayoutEffect from 'shared/useLayoutEffect'; import {mergeRefs} from './mergeRefs'; export type Props = { + editor: LexicalEditor; ariaActiveDescendant?: React.AriaAttributes['aria-activedescendant']; ariaAutoComplete?: React.AriaAttributes['aria-autocomplete']; ariaControls?: React.AriaAttributes['aria-controls']; @@ -30,6 +32,7 @@ export type Props = { function ContentEditableElementImpl( { + editor, ariaActiveDescendant, ariaAutoComplete, ariaControls, @@ -52,7 +55,6 @@ function ContentEditableElementImpl( }: Props, ref: Ref, ): JSX.Element { - const [editor] = useLexicalComposerContext(); const [isEditable, setEditable] = useState(false); const handleRef = (rootElement: null | HTMLElement) => { From 07e704c198dda05b8657eb9387193ca95e77caed Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Tue, 9 Jul 2024 17:22:04 -0400 Subject: [PATCH 10/15] . --- packages/lexical-react/flow/LexicalContentEditable.js.flow | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 7f03a646a2c..35bd87bfe28 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -12,6 +12,7 @@ import type { LexicalEditor } from 'lexical'; import type {TRefFor} from 'CoreTypes.flow'; import * as React from 'react'; +import type { AbstractComponent } from "react"; // Due to Flow limitations, we prefer fixed types over the built-in inexact HTMLElement type HTMLDivElementDOMProps = $ReadOnly<{ @@ -60,4 +61,7 @@ export type Props = $ReadOnly<{ ...PlaceholderProps }> -declare export function ContentEditable(props: Props): React$Node; +declare export var ContentEditable: AbstractComponent< + Props, + HTMLDivElement, +>; From 3197abbce56a2acd432097dc91370e4e46a999ad Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Wed, 10 Jul 2024 15:36:06 -0400 Subject: [PATCH 11/15] export placeholder type --- packages/lexical-react/flow/LexicalContentEditable.js.flow | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 35bd87bfe28..2f65c3a4227 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -30,7 +30,7 @@ type HTMLDivElementDOMProps = $ReadOnly<{ 'data-testid'?: string | null, }> -type PlaceholderProps = +export type PlaceholderProps = | $ReadOnly<{ 'aria-placeholder'?: void, placeholder?: null, From 9b1c5697f04b63160de20b4174e7e486e8df47da Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Wed, 10 Jul 2024 17:19:37 -0400 Subject: [PATCH 12/15] revise flow types --- .../flow/LexicalContentEditable.js.flow | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index 2f65c3a4227..99004d8b137 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -14,21 +14,29 @@ import type {TRefFor} from 'CoreTypes.flow'; import * as React from 'react'; import type { AbstractComponent } from "react"; +type InlineStyle = { + [key: string]: mixed; +} + // Due to Flow limitations, we prefer fixed types over the built-in inexact HTMLElement type HTMLDivElementDOMProps = $ReadOnly<{ - autoCapitalize?: boolean, - autoComplete?: boolean, - autoCorrect?: boolean, - id?: string, - className?: string, - 'data-testid'?: string, - role?: string, - spellCheck?: boolean, - suppressContentEditableWarning?: boolean, - tabIndex?: number, - style?: CSSStyleDeclaration, - 'data-testid'?: string | null, -}> + 'aria-label'?: void | string, + 'aria-labeledby'?: void | string, + 'title'?: void | string, + onClick?: void | (e: SyntheticEvent) => mixed, + autoCapitalize?: void | boolean, + autoComplete?: void | boolean, + autoCorrect?: void | boolean, + id?: void | string, + className?: void | string, + 'data-testid'?: void | string, + role?: void | string, + spellCheck?: void | boolean, + suppressContentEditableWarning?: void | boolean, + tabIndex?: void | number, + style?: void | InlineStyle | CSSStyleDeclaration, + 'data-testid'?: void | string, +}>; export type PlaceholderProps = | $ReadOnly<{ From 76857d51ea36cfbaf66664c9320665f0184e6324 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Thu, 11 Jul 2024 11:23:32 -0400 Subject: [PATCH 13/15] improve editable init --- .../lexical-react/src/shared/LexicalContentEditableElement.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx index 5d275479900..239866ce9b3 100644 --- a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx +++ b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx @@ -55,7 +55,7 @@ function ContentEditableElementImpl( }: Props, ref: Ref, ): JSX.Element { - const [isEditable, setEditable] = useState(false); + const [isEditable, setEditable] = useState(editor.isEditable()); const handleRef = (rootElement: null | HTMLElement) => { // defaultView is required for a root element. From 4614e924b833eecbe09edb6c2d8e2dad0e2dd558 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Thu, 11 Jul 2024 14:26:07 -0400 Subject: [PATCH 14/15] . --- packages/lexical-react/src/LexicalContentEditable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lexical-react/src/LexicalContentEditable.tsx b/packages/lexical-react/src/LexicalContentEditable.tsx index 7102df584f1..30829f6fb40 100644 --- a/packages/lexical-react/src/LexicalContentEditable.tsx +++ b/packages/lexical-react/src/LexicalContentEditable.tsx @@ -63,7 +63,7 @@ function Placeholder({ }): null | JSX.Element { const showPlaceholder = useCanShowPlaceholder(editor); - const [isEditable, setEditable] = useState(false); + const [isEditable, setEditable] = useState(editor.isEditable()); useLayoutEffect(() => { setEditable(editor.isEditable()); return editor.registerEditableListener((currentIsEditable) => { From 164fc944e5e8d5b9559e027bef01653b0ac1d5d6 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Thu, 11 Jul 2024 19:30:14 -0400 Subject: [PATCH 15/15] memo --- .../shared/LexicalContentEditableElement.tsx | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx index 239866ce9b3..2e1208e0d64 100644 --- a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx +++ b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx @@ -9,7 +9,7 @@ import type {LexicalEditor} from 'lexical'; import * as React from 'react'; -import {forwardRef, Ref, useState} from 'react'; +import {forwardRef, Ref, useCallback, useMemo, useState} from 'react'; import useLayoutEffect from 'shared/useLayoutEffect'; import {mergeRefs} from './mergeRefs'; @@ -57,19 +57,23 @@ function ContentEditableElementImpl( ): JSX.Element { const [isEditable, setEditable] = useState(editor.isEditable()); - const handleRef = (rootElement: null | HTMLElement) => { - // 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); - } - }; + const handleRef = useCallback( + (rootElement: null | HTMLElement) => { + // 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()); @@ -99,7 +103,7 @@ function ContentEditableElementImpl( contentEditable={isEditable} data-testid={testid} id={id} - ref={mergeRefs(ref, handleRef)} + ref={mergedRefs} role={isEditable ? role : undefined} spellCheck={spellCheck} style={style}