From d87aed4cad4a503554b674da9dda584ce8ebbcfe Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Fri, 24 May 2024 15:14:24 +0100 Subject: [PATCH 1/2] Make placeholder accessible --- examples/react-rich/src/App.tsx | 15 +- packages/lexical-playground/src/Editor.tsx | 10 +- .../src/nodes/ImageComponent.tsx | 12 +- .../InlineImageNode/InlineImageComponent.tsx | 12 +- .../src/nodes/StickyComponent.tsx | 12 +- .../src/plugins/CommentPlugin/index.tsx | 6 +- .../src/ui/ContentEditable.css | 21 +++ .../src/ui/ContentEditable.tsx | 24 ++- .../lexical-playground/src/ui/Placeholder.css | 28 ---- .../lexical-playground/src/ui/Placeholder.tsx | 22 --- .../flow/LexicalContentEditable.js.flow | 8 +- .../src/LexicalContentEditable.tsx | 138 ++++++------------ .../src/LexicalPlainTextPlugin.tsx | 6 +- .../src/LexicalRichTextPlugin.tsx | 6 +- .../shared/LexicalContentEditableElement.tsx | 107 ++++++++++++++ 15 files changed, 241 insertions(+), 186 deletions(-) delete mode 100644 packages/lexical-playground/src/ui/Placeholder.css delete mode 100644 packages/lexical-playground/src/ui/Placeholder.tsx create mode 100644 packages/lexical-react/src/shared/LexicalContentEditableElement.tsx diff --git a/examples/react-rich/src/App.tsx b/examples/react-rich/src/App.tsx index 665d65405..de1b5d4e1 100644 --- a/examples/react-rich/src/App.tsx +++ b/examples/react-rich/src/App.tsx @@ -16,9 +16,7 @@ import ExampleTheme from './ExampleTheme'; import ToolbarPlugin from './plugins/ToolbarPlugin'; import TreeViewPlugin from './plugins/TreeViewPlugin'; -function Placeholder() { - return
Enter some rich text...
; -} +const placeholder = 'Enter some rich text...'; const editorConfig = { namespace: 'React.js Demo', @@ -38,8 +36,15 @@ export default function App() {
} - placeholder={} + contentEditable={ + {placeholder}
+ } + aria-placeholder={placeholder} + /> + } ErrorBoundary={LexicalErrorBoundary} /> diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx index 096770f18..af2b07c74 100644 --- a/packages/lexical-playground/src/Editor.tsx +++ b/packages/lexical-playground/src/Editor.tsx @@ -69,7 +69,6 @@ import TreeViewPlugin from './plugins/TreeViewPlugin'; import TwitterPlugin from './plugins/TwitterPlugin'; import YouTubePlugin from './plugins/YouTubePlugin'; import ContentEditable from './ui/ContentEditable'; -import Placeholder from './ui/Placeholder'; const skipCollaborationInit = // @ts-expect-error @@ -94,12 +93,11 @@ export default function Editor(): JSX.Element { }, } = useSettings(); const isEditable = useLexicalEditable(); - const text = isCollab + const placeholder = isCollab ? 'Enter some collaborative rich text...' : isRichText ? 'Enter some rich text...' : 'Enter some plain text...'; - const placeholder = {text}; const [floatingAnchorElem, setFloatingAnchorElem] = useState(null); const [isSmallWidthViewport, setIsSmallWidthViewport] = @@ -168,11 +166,10 @@ export default function Editor(): JSX.Element { contentEditable={
- +
} - placeholder={placeholder} ErrorBoundary={LexicalErrorBoundary} /> @@ -224,8 +221,7 @@ export default function Editor(): JSX.Element { ) : ( <> } - placeholder={placeholder} + contentEditable={} ErrorBoundary={LexicalErrorBoundary} /> diff --git a/packages/lexical-playground/src/nodes/ImageComponent.tsx b/packages/lexical-playground/src/nodes/ImageComponent.tsx index 23c891e12..d53eb30db 100644 --- a/packages/lexical-playground/src/nodes/ImageComponent.tsx +++ b/packages/lexical-playground/src/nodes/ImageComponent.tsx @@ -62,7 +62,6 @@ import MentionsPlugin from '../plugins/MentionsPlugin'; import TreeViewPlugin from '../plugins/TreeViewPlugin'; import ContentEditable from '../ui/ContentEditable'; import ImageResizer from '../ui/ImageResizer'; -import Placeholder from '../ui/Placeholder'; import {EmojiNode} from './EmojiNode'; import {$isImageNode} from './ImageNode'; import {KeywordNode} from './KeywordNode'; @@ -452,12 +451,11 @@ export default function ImageComponent({ )} - } - placeholder={ - - Enter a caption... - + } ErrorBoundary={LexicalErrorBoundary} /> diff --git a/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageComponent.tsx b/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageComponent.tsx index 7cc9428f4..ad2861ba3 100644 --- a/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageComponent.tsx +++ b/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageComponent.tsx @@ -39,7 +39,6 @@ import LinkPlugin from '../../plugins/LinkPlugin'; import Button from '../../ui/Button'; import ContentEditable from '../../ui/ContentEditable'; import {DialogActions} from '../../ui/Dialog'; -import Placeholder from '../../ui/Placeholder'; import Select from '../../ui/Select'; import TextInput from '../../ui/TextInput'; import {$isInlineImageNode, InlineImageNode} from './InlineImageNode'; @@ -388,12 +387,11 @@ export default function InlineImageComponent({ - } - placeholder={ - - Enter a caption... - + } ErrorBoundary={LexicalErrorBoundary} /> diff --git a/packages/lexical-playground/src/nodes/StickyComponent.tsx b/packages/lexical-playground/src/nodes/StickyComponent.tsx index ec3ebf6fa..469ac8694 100644 --- a/packages/lexical-playground/src/nodes/StickyComponent.tsx +++ b/packages/lexical-playground/src/nodes/StickyComponent.tsx @@ -27,7 +27,6 @@ import {createWebsocketProvider} from '../collaboration'; import {useSharedHistoryContext} from '../context/SharedHistoryContext'; import StickyEditorTheme from '../themes/StickyEditorTheme'; import ContentEditable from '../ui/ContentEditable'; -import Placeholder from '../ui/Placeholder'; import {$isStickyNode} from './StickyNode'; type Positioning = { @@ -254,12 +253,11 @@ export default function StickyComponent({ )} - } - placeholder={ - - What's up? - + } ErrorBoundary={LexicalErrorBoundary} /> diff --git a/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx b/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx index 1d446889f..66915aacf 100644 --- a/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx @@ -67,7 +67,6 @@ import useModal from '../../hooks/useModal'; import CommentEditorTheme from '../../themes/CommentEditorTheme'; import Button from '../../ui/Button'; import ContentEditable from '../../ui/ContentEditable'; -import Placeholder from '../../ui/Placeholder'; export const INSERT_INLINE_COMMAND: LexicalCommand = createCommand( 'INSERT_INLINE_COMMAND', @@ -168,8 +167,9 @@ function PlainTextEditor({
} - placeholder={{placeholder}} + contentEditable={ + + } ErrorBoundary={LexicalErrorBoundary} /> diff --git a/packages/lexical-playground/src/ui/ContentEditable.css b/packages/lexical-playground/src/ui/ContentEditable.css index 319b2ca18..66ec2b92a 100644 --- a/packages/lexical-playground/src/ui/ContentEditable.css +++ b/packages/lexical-playground/src/ui/ContentEditable.css @@ -21,3 +21,24 @@ padding-right: 8px; } } + +.ContentEditable__placeholder { + font-size: 15px; + color: #999; + overflow: hidden; + position: absolute; + text-overflow: ellipsis; + top: 8px; + left: 8px; + right: 8px; + user-select: none; + white-space: nowrap; + display: inline-block; + pointer-events: none; +} +@media (max-width: 1025px) { + .Placeholder__root { + left: 8px; + right: 8px; + } +} diff --git a/packages/lexical-playground/src/ui/ContentEditable.tsx b/packages/lexical-playground/src/ui/ContentEditable.tsx index 9301f5f0c..b1ee20b21 100644 --- a/packages/lexical-playground/src/ui/ContentEditable.tsx +++ b/packages/lexical-playground/src/ui/ContentEditable.tsx @@ -11,10 +11,26 @@ import './ContentEditable.css'; import {ContentEditable} from '@lexical/react/LexicalContentEditable'; import * as React from 'react'; +type Props = { + className?: string; + placeholderClassName?: string; + placeholder: string; +}; + export default function LexicalContentEditable({ className, -}: { - className?: string; -}): JSX.Element { - return ; + placeholder, + placeholderClassName, +}: Props): JSX.Element { + return ( + + {placeholder} +
+ } + /> + ); } diff --git a/packages/lexical-playground/src/ui/Placeholder.css b/packages/lexical-playground/src/ui/Placeholder.css deleted file mode 100644 index 61e3df765..000000000 --- a/packages/lexical-playground/src/ui/Placeholder.css +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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. - * - * - */ - -.Placeholder__root { - font-size: 15px; - color: #999; - overflow: hidden; - position: absolute; - text-overflow: ellipsis; - top: 8px; - left: 28px; - right: 28px; - user-select: none; - white-space: nowrap; - display: inline-block; - pointer-events: none; -} -@media (max-width: 1025px) { - .Placeholder__root { - left: 8px; - } -} diff --git a/packages/lexical-playground/src/ui/Placeholder.tsx b/packages/lexical-playground/src/ui/Placeholder.tsx deleted file mode 100644 index 676776886..000000000 --- a/packages/lexical-playground/src/ui/Placeholder.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * 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 './Placeholder.css'; - -import * as React from 'react'; -import {ReactNode} from 'react'; - -export default function Placeholder({ - children, - className, -}: { - children: ReactNode; - className?: string; -}): JSX.Element { - return
{children}
; -} diff --git a/packages/lexical-react/flow/LexicalContentEditable.js.flow b/packages/lexical-react/flow/LexicalContentEditable.js.flow index b6b63a4f2..ff45922df 100644 --- a/packages/lexical-react/flow/LexicalContentEditable.js.flow +++ b/packages/lexical-react/flow/LexicalContentEditable.js.flow @@ -9,7 +9,13 @@ import * as React from 'react'; -export type Props = $ReadOnly<{ +export type Props = ({} | $ReadOnly<{ + 'aria-placeholder': string; + placeholder: + | ((isEditable: boolean) => null | React$Node) + | null + | React$Node; +}>) & $ReadOnly<{ ...Partial, ariaActiveDescendant?: string, ariaAutoComplete?: string, diff --git a/packages/lexical-react/src/LexicalContentEditable.tsx b/packages/lexical-react/src/LexicalContentEditable.tsx index 1dbfe1201..a7097a0e1 100644 --- a/packages/lexical-react/src/LexicalContentEditable.tsx +++ b/packages/lexical-react/src/LexicalContentEditable.tsx @@ -6,102 +6,58 @@ * */ -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import * as React from 'react'; -import {useCallback, useState} from 'react'; -import useLayoutEffect from 'shared/useLayoutEffect'; +import type {Props as ElementProps} from './shared/LexicalContentEditableElement'; -export type Props = { - ariaActiveDescendant?: React.AriaAttributes['aria-activedescendant']; - ariaAutoComplete?: React.AriaAttributes['aria-autocomplete']; - ariaControls?: React.AriaAttributes['aria-controls']; - ariaDescribedBy?: React.AriaAttributes['aria-describedby']; - ariaExpanded?: React.AriaAttributes['aria-expanded']; - 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; -} & React.AllHTMLAttributes; +import {useLexicalEditable} from '@lexical/react/useLexicalEditable'; -export function ContentEditable({ - 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 { - const [editor] = useLexicalComposerContext(); - const [isEditable, setEditable] = useState(false); +import {useLexicalComposerContext} from './LexicalComposerContext'; +import {ContentEditableElement} from './shared/LexicalContentEditableElement'; +import {useCanShowPlaceholder} from './shared/useCanShowPlaceholder'; - 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); - } - }, - [editor], - ); +/* eslint-disable @typescript-eslint/ban-types */ +export type Props = ( + | {} + | { + 'aria-placeholder': string; + placeholder: + | ((isEditable: boolean) => null | JSX.Element) + | null + | JSX.Element; + } +) & + ElementProps; +/* eslint-enable @typescript-eslint/ban-types */ - useLayoutEffect(() => { - setEditable(editor.isEditable()); - return editor.registerEditableListener((currentIsEditable) => { - setEditable(currentIsEditable); - }); - }, [editor]); +export function ContentEditable(props: Props): JSX.Element { + let placeholder = null; + if ('placeholder' in props) { + placeholder = props.placeholder; + } return ( -
+ <> + + + ); } + +function Placeholder({ + content, +}: { + content: ((isEditable: boolean) => null | JSX.Element) | null | JSX.Element; +}): null | JSX.Element { + const [editor] = useLexicalComposerContext(); + const showPlaceholder = useCanShowPlaceholder(editor); + const editable = useLexicalEditable(); + + if (!showPlaceholder) { + return null; + } + + if (typeof content === 'function') { + return content(editable); + } else { + return content; + } +} diff --git a/packages/lexical-react/src/LexicalPlainTextPlugin.tsx b/packages/lexical-react/src/LexicalPlainTextPlugin.tsx index 3a13ca672..108e6aa19 100644 --- a/packages/lexical-react/src/LexicalPlainTextPlugin.tsx +++ b/packages/lexical-react/src/LexicalPlainTextPlugin.tsx @@ -16,11 +16,12 @@ import {usePlainTextSetup} from './shared/usePlainTextSetup'; export function PlainTextPlugin({ contentEditable, - placeholder, + // TODO Remove. This property is now part of ContentEditable + placeholder = null, ErrorBoundary, }: { contentEditable: JSX.Element; - placeholder: + placeholder?: | ((isEditable: boolean) => null | JSX.Element) | null | JSX.Element; @@ -39,6 +40,7 @@ export function PlainTextPlugin({ ); } +// TODO Remove function Placeholder({ content, }: { diff --git a/packages/lexical-react/src/LexicalRichTextPlugin.tsx b/packages/lexical-react/src/LexicalRichTextPlugin.tsx index abc465557..40ce57544 100644 --- a/packages/lexical-react/src/LexicalRichTextPlugin.tsx +++ b/packages/lexical-react/src/LexicalRichTextPlugin.tsx @@ -16,11 +16,12 @@ import {useRichTextSetup} from './shared/useRichTextSetup'; export function RichTextPlugin({ contentEditable, - placeholder, + // TODO Remove. This property is now part of ContentEditable + placeholder = null, ErrorBoundary, }: { contentEditable: JSX.Element; - placeholder: + placeholder?: | ((isEditable: boolean) => null | JSX.Element) | null | JSX.Element; @@ -39,6 +40,7 @@ export function RichTextPlugin({ ); } +// TODO remove function Placeholder({ content, }: { diff --git a/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx new file mode 100644 index 000000000..25ab75abc --- /dev/null +++ b/packages/lexical-react/src/shared/LexicalContentEditableElement.tsx @@ -0,0 +1,107 @@ +/** + * 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 {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import * as React from 'react'; +import {useCallback, useState} from 'react'; +import useLayoutEffect from 'shared/useLayoutEffect'; + +export type Props = { + ariaActiveDescendant?: React.AriaAttributes['aria-activedescendant']; + ariaAutoComplete?: React.AriaAttributes['aria-autocomplete']; + ariaControls?: React.AriaAttributes['aria-controls']; + ariaDescribedBy?: React.AriaAttributes['aria-describedby']; + ariaExpanded?: React.AriaAttributes['aria-expanded']; + 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'>; + +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 { + 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); + } + }, + [editor], + ); + + useLayoutEffect(() => { + setEditable(editor.isEditable()); + return editor.registerEditableListener((currentIsEditable) => { + setEditable(currentIsEditable); + }); + }, [editor]); + + return ( +
+ ); +} From e94e1e9d5a8a6f0d18ac804a8d53c2a3e2db384a Mon Sep 17 00:00:00 2001 From: "korbit-ai[bot]" <131444098+korbit-ai[bot]@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:20:20 +0000 Subject: [PATCH 2/2] [skip ci]