Lexical: Fix autofocus
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
176a3b5ece
commit
b3f9edd41e
|
@ -6,7 +6,6 @@ Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
This source code is licensed under the MIT license found in the
|
This source code is licensed under the MIT license found in the
|
||||||
LICENSE file in the /app/soapbox/features/compose/editor directory.
|
LICENSE file in the /app/soapbox/features/compose/editor directory.
|
||||||
*/
|
*/
|
||||||
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
|
|
||||||
import { AutoLinkPlugin, createLinkMatcherWithRegExp } from '@lexical/react/LexicalAutoLinkPlugin';
|
import { AutoLinkPlugin, createLinkMatcherWithRegExp } from '@lexical/react/LexicalAutoLinkPlugin';
|
||||||
import { LexicalComposer, InitialConfigType } from '@lexical/react/LexicalComposer';
|
import { LexicalComposer, InitialConfigType } from '@lexical/react/LexicalComposer';
|
||||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||||
|
@ -25,6 +24,15 @@ import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import { useNodes } from './nodes';
|
||||||
|
import AutosuggestPlugin from './plugins/autosuggest-plugin';
|
||||||
|
import FloatingBlockTypeToolbarPlugin from './plugins/floating-block-type-toolbar-plugin';
|
||||||
|
import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
|
||||||
|
import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin';
|
||||||
|
import FocusPlugin from './plugins/focus-plugin';
|
||||||
|
import MentionPlugin from './plugins/mention-plugin';
|
||||||
|
import StatePlugin from './plugins/state-plugin';
|
||||||
|
|
||||||
const LINK_MATCHERS = [
|
const LINK_MATCHERS = [
|
||||||
createLinkMatcherWithRegExp(
|
createLinkMatcherWithRegExp(
|
||||||
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/,
|
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/,
|
||||||
|
@ -32,14 +40,6 @@ const LINK_MATCHERS = [
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
import { useNodes } from './nodes';
|
|
||||||
import AutosuggestPlugin from './plugins/autosuggest-plugin';
|
|
||||||
import FloatingBlockTypeToolbarPlugin from './plugins/floating-block-type-toolbar-plugin';
|
|
||||||
import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
|
|
||||||
import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin';
|
|
||||||
import MentionPlugin from './plugins/mention-plugin';
|
|
||||||
import StatePlugin from './plugins/state-plugin';
|
|
||||||
|
|
||||||
interface IComposeEditor {
|
interface IComposeEditor {
|
||||||
className?: string
|
className?: string
|
||||||
placeholderClassName?: string
|
placeholderClassName?: string
|
||||||
|
@ -104,7 +104,7 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
||||||
return compose.editorState;
|
return compose.editorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
return function() {
|
return () => {
|
||||||
if (compose.content_type === 'text/markdown') {
|
if (compose.content_type === 'text/markdown') {
|
||||||
$createRemarkImport({})(compose.text);
|
$createRemarkImport({})(compose.text);
|
||||||
} else {
|
} else {
|
||||||
|
@ -152,11 +152,10 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
||||||
contentEditable={
|
contentEditable={
|
||||||
<div className='editor' ref={onRef} onFocus={onFocus} onPaste={handlePaste}>
|
<div className='editor' ref={onRef} onFocus={onFocus} onPaste={handlePaste}>
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
className={clsx('mr-4 pb-8 outline-none transition-[min-height] motion-reduce:transition-none', {
|
className={clsx('outline-none transition-[min-height] motion-reduce:transition-none', {
|
||||||
'min-h-[40px]': condensed,
|
'min-h-[40px] pb-4': condensed,
|
||||||
'min-h-[100px]': !condensed,
|
'min-h-[100px] pb-8': !condensed,
|
||||||
})}
|
})}
|
||||||
autoFocus={autoFocus}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -172,7 +171,6 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
||||||
)}
|
)}
|
||||||
ErrorBoundary={LexicalErrorBoundary}
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
/>
|
/>
|
||||||
{autoFocus && <AutoFocusPlugin />}
|
|
||||||
<OnChangePlugin onChange={(_, editor) => {
|
<OnChangePlugin onChange={(_, editor) => {
|
||||||
if (editorStateRef) (editorStateRef as any).current = editor.getEditorState().read($createRemarkExport());
|
if (editorStateRef) (editorStateRef as any).current = editor.getEditorState().read($createRemarkExport());
|
||||||
}}
|
}}
|
||||||
|
@ -192,6 +190,7 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<StatePlugin composeId={composeId} handleSubmit={handleSubmit} />
|
<StatePlugin composeId={composeId} handleSubmit={handleSubmit} />
|
||||||
|
<FocusPlugin autoFocus={autoFocus} />
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -42,7 +42,7 @@ import type {
|
||||||
|
|
||||||
const imageCache = new Set();
|
const imageCache = new Set();
|
||||||
|
|
||||||
function useSuspenseImage(src: string) {
|
const useSuspenseImage = (src: string) => {
|
||||||
if (!imageCache.has(src)) {
|
if (!imageCache.has(src)) {
|
||||||
throw new Promise((resolve) => {
|
throw new Promise((resolve) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
@ -53,9 +53,9 @@ function useSuspenseImage(src: string) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function LazyImage({
|
const LazyImage = ({
|
||||||
altText,
|
altText,
|
||||||
className,
|
className,
|
||||||
imageRef,
|
imageRef,
|
||||||
|
@ -65,7 +65,7 @@ function LazyImage({
|
||||||
className: string | null
|
className: string | null
|
||||||
imageRef: {current: null | HTMLImageElement}
|
imageRef: {current: null | HTMLImageElement}
|
||||||
src: string
|
src: string
|
||||||
}): JSX.Element {
|
}): JSX.Element => {
|
||||||
useSuspenseImage(src);
|
useSuspenseImage(src);
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
|
@ -76,9 +76,9 @@ function LazyImage({
|
||||||
draggable='false'
|
draggable='false'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function ImageComponent({
|
const ImageComponent = ({
|
||||||
src,
|
src,
|
||||||
altText,
|
altText,
|
||||||
nodeKey,
|
nodeKey,
|
||||||
|
@ -86,7 +86,7 @@ export default function ImageComponent({
|
||||||
altText: string
|
altText: string
|
||||||
nodeKey: NodeKey
|
nodeKey: NodeKey
|
||||||
src: string
|
src: string
|
||||||
}): JSX.Element {
|
}): JSX.Element => {
|
||||||
const imageRef = useRef<null | HTMLImageElement>(null);
|
const imageRef = useRef<null | HTMLImageElement>(null);
|
||||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const [isSelected, setSelected, clearSelection] =
|
const [isSelected, setSelected, clearSelection] =
|
||||||
|
@ -270,4 +270,6 @@ export default function ImageComponent({
|
||||||
</>
|
</>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default ImageComponent;
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
interface IFocusPlugin {
|
||||||
|
autoFocus?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const FocusPlugin: React.FC<IFocusPlugin> = ({ autoFocus }) => {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
|
const focus = () => {
|
||||||
|
editor.focus(
|
||||||
|
() => {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
const rootElement = editor.getRootElement();
|
||||||
|
if (
|
||||||
|
rootElement !== null &&
|
||||||
|
(activeElement === null || !rootElement.contains(activeElement))
|
||||||
|
) {
|
||||||
|
rootElement.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
}, { defaultSelection: 'rootEnd' },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFocus) focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FocusPlugin;
|
|
@ -86,6 +86,7 @@ const ComposeModal: React.FC<IComposeModal> = ({ onClose, composeId = 'compose-m
|
||||||
<ComposeForm
|
<ComposeForm
|
||||||
id={composeId}
|
id={composeId}
|
||||||
extra={<ComposeFormGroupToggle composeId={composeId} groupId={groupId} />}
|
extra={<ComposeFormGroupToggle composeId={composeId} groupId={groupId} />}
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue