diff --git a/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs b/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs
index 1f1cf21bcae..39d3ef16e50 100644
--- a/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs
+++ b/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs
@@ -6,6 +6,14 @@
*
*/
+import {
+ decreaseFontSize,
+ increaseFontSize,
+ toggleBold,
+ toggleItalic,
+ toggleStrikethrough,
+ toggleUnderline,
+} from '../keyboardShortcuts/index.mjs';
import {
assertHTML,
focusEditor,
@@ -30,14 +38,12 @@ test.describe('Autocomplete', () => {
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
Sort by alpha
-
-
- betical (TAB)
-
+
+ betical (TAB)
-
`,
html`
@@ -45,8 +51,7 @@ test.describe('Autocomplete', () => {
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr"
dir="ltr">
Sort by alpha
-
-
+
`,
);
@@ -58,7 +63,94 @@ test.describe('Autocomplete', () => {
- Sort by alphabetical order:
+ Sort by alpha
+
+ betical order:
+
+
+ `,
+ );
+ });
+
+ test('Can autocomplete in the same format as the original text', async ({
+ page,
+ isPlainText,
+ }) => {
+ test.skip(isPlainText);
+ await focusEditor(page);
+ await toggleBold(page);
+ await toggleItalic(page);
+ await toggleUnderline(page);
+ await toggleStrikethrough(page);
+ await increaseFontSize(page);
+
+ await page.keyboard.type('Test');
+ await sleep(500);
+
+ await assertHTML(
+ page,
+ html`
+
+
+ Test
+
+
+ imonials (TAB)
+
+
+ `,
+ html`
+
+
+ Test
+
+
+
+ `,
+ );
+
+ await page.keyboard.press('Tab');
+
+ await toggleBold(page);
+ await toggleItalic(page);
+ await toggleUnderline(page);
+ await toggleStrikethrough(page);
+ await decreaseFontSize(page);
+
+ await page.keyboard.type(' 2024');
+
+ await assertHTML(
+ page,
+ html`
+
+
+ Test
+
+
+ imonials
+
+ 2024
`,
);
diff --git a/packages/lexical-playground/src/App.tsx b/packages/lexical-playground/src/App.tsx
index 6f20d8c3841..aa4852cf87e 100644
--- a/packages/lexical-playground/src/App.tsx
+++ b/packages/lexical-playground/src/App.tsx
@@ -22,7 +22,6 @@ import {
import {isDevPlayground} from './appSettings';
import {FlashMessageContext} from './context/FlashMessageContext';
import {SettingsContext, useSettings} from './context/SettingsContext';
-import {SharedAutocompleteContext} from './context/SharedAutocompleteContext';
import {SharedHistoryContext} from './context/SharedHistoryContext';
import {ToolbarContext} from './context/ToolbarContext';
import Editor from './Editor';
@@ -211,24 +210,22 @@ function App(): JSX.Element {
-
-
-
-
-
-
-
- {isDevPlayground ? : null}
- {isDevPlayground ? : null}
- {isDevPlayground ? : null}
+
+
+
+
+
+
+ {isDevPlayground ? : null}
+ {isDevPlayground ? : null}
+ {isDevPlayground ? : null}
- {measureTypingPerf ? : null}
-
-
+ {measureTypingPerf ? : null}
+
diff --git a/packages/lexical-playground/src/context/SharedAutocompleteContext.tsx b/packages/lexical-playground/src/context/SharedAutocompleteContext.tsx
deleted file mode 100644
index 4f282709eea..00000000000
--- a/packages/lexical-playground/src/context/SharedAutocompleteContext.tsx
+++ /dev/null
@@ -1,71 +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 * as React from 'react';
-import {
- createContext,
- ReactNode,
- useContext,
- useEffect,
- useMemo,
- useState,
-} from 'react';
-
-type Suggestion = null | string;
-type CallbackFn = (newSuggestion: Suggestion) => void;
-type SubscribeFn = (callbackFn: CallbackFn) => () => void;
-type PublishFn = (newSuggestion: Suggestion) => void;
-type ContextShape = [SubscribeFn, PublishFn];
-type HookShape = [suggestion: Suggestion, setSuggestion: PublishFn];
-
-const Context: React.Context = createContext([
- (_cb) => () => {
- return;
- },
- (_newSuggestion: Suggestion) => {
- return;
- },
-]);
-
-export const SharedAutocompleteContext = ({
- children,
-}: {
- children: ReactNode;
-}): JSX.Element => {
- const context: ContextShape = useMemo(() => {
- let suggestion: Suggestion | null = null;
- const listeners: Set = new Set();
- return [
- (cb: (newSuggestion: Suggestion) => void) => {
- cb(suggestion);
- listeners.add(cb);
- return () => {
- listeners.delete(cb);
- };
- },
- (newSuggestion: Suggestion) => {
- suggestion = newSuggestion;
- for (const listener of listeners) {
- listener(newSuggestion);
- }
- },
- ];
- }, []);
- return {children};
-};
-
-export const useSharedAutocompleteContext = (): HookShape => {
- const [subscribe, publish]: ContextShape = useContext(Context);
- const [suggestion, setSuggestion] = useState(null);
- useEffect(() => {
- return subscribe((newSuggestion: Suggestion) => {
- setSuggestion(newSuggestion);
- });
- }, [subscribe]);
- return [suggestion, publish];
-};
diff --git a/packages/lexical-playground/src/nodes/AutocompleteNode.tsx b/packages/lexical-playground/src/nodes/AutocompleteNode.tsx
index f3eb6bd715a..220add6396c 100644
--- a/packages/lexical-playground/src/nodes/AutocompleteNode.tsx
+++ b/packages/lexical-playground/src/nodes/AutocompleteNode.tsx
@@ -7,36 +7,26 @@
*/
import type {
+ DOMExportOutput,
EditorConfig,
- EditorThemeClassName,
LexicalEditor,
NodeKey,
- SerializedLexicalNode,
+ SerializedTextNode,
Spread,
} from 'lexical';
-import {DecoratorNode} from 'lexical';
-import * as React from 'react';
+import {TextNode} from 'lexical';
-import {useSharedAutocompleteContext} from '../context/SharedAutocompleteContext';
import {uuid as UUID} from '../plugins/AutocompletePlugin';
-declare global {
- interface Navigator {
- userAgentData?: {
- mobile: boolean;
- };
- }
-}
-
export type SerializedAutocompleteNode = Spread<
{
uuid: string;
},
- SerializedLexicalNode
+ SerializedTextNode
>;
-export class AutocompleteNode extends DecoratorNode {
+export class AutocompleteNode extends TextNode {
/**
* A unique uuid is generated for each session and assigned to the instance.
* This helps to:
@@ -48,7 +38,7 @@ export class AutocompleteNode extends DecoratorNode {
__uuid: string;
static clone(node: AutocompleteNode): AutocompleteNode {
- return new AutocompleteNode(node.__uuid, node.__key);
+ return new AutocompleteNode(node.__text, node.__uuid, node.__key);
}
static getType(): 'autocomplete' {
@@ -58,7 +48,14 @@ export class AutocompleteNode extends DecoratorNode {
static importJSON(
serializedNode: SerializedAutocompleteNode,
): AutocompleteNode {
- const node = $createAutocompleteNode(serializedNode.uuid);
+ const node = $createAutocompleteNode(
+ serializedNode.text,
+ serializedNode.uuid,
+ );
+ node.setFormat(serializedNode.format);
+ node.setDetail(serializedNode.detail);
+ node.setMode(serializedNode.mode);
+ node.setStyle(serializedNode.style);
return node;
}
@@ -71,8 +68,8 @@ export class AutocompleteNode extends DecoratorNode {
};
}
- constructor(uuid: string, key?: NodeKey) {
- super(key);
+ constructor(text: string, uuid: string, key?: NodeKey) {
+ super(text, key);
this.__uuid = uuid;
}
@@ -84,36 +81,23 @@ export class AutocompleteNode extends DecoratorNode {
return false;
}
- createDOM(config: EditorConfig): HTMLElement {
- return document.createElement('span');
+ exportDOM(_: LexicalEditor): DOMExportOutput {
+ return {element: null};
}
- decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element | null {
+ createDOM(config: EditorConfig): HTMLElement {
if (this.__uuid !== UUID) {
- return null;
+ return document.createElement('span');
}
- return ;
+ const dom = super.createDOM(config);
+ dom.classList.add(config.theme.autocomplete);
+ return dom;
}
}
-export function $createAutocompleteNode(uuid: string): AutocompleteNode {
- return new AutocompleteNode(uuid);
-}
-
-function AutocompleteComponent({
- className,
-}: {
- className: EditorThemeClassName;
-}): JSX.Element {
- const [suggestion] = useSharedAutocompleteContext();
- const userAgentData = window.navigator.userAgentData;
- const isMobile =
- userAgentData !== undefined
- ? userAgentData.mobile
- : window.innerWidth <= 800 && window.innerHeight <= 600;
- return (
-
- {suggestion} {isMobile ? '(SWIPE \u2B95)' : '(TAB)'}
-
- );
+export function $createAutocompleteNode(
+ text: string,
+ uuid: string,
+): AutocompleteNode {
+ return new AutocompleteNode(text, uuid);
}
diff --git a/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx b/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx
index 7e32e1f4fb1..fa7d5fe5690 100644
--- a/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx
+++ b/packages/lexical-playground/src/plugins/AutocompletePlugin/index.tsx
@@ -6,7 +6,7 @@
*
*/
-import type {BaseSelection, NodeKey} from 'lexical';
+import type {BaseSelection, NodeKey, TextNode} from 'lexical';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$isAtNodeEnd} from '@lexical/selection';
@@ -24,13 +24,21 @@ import {
} from 'lexical';
import {useCallback, useEffect} from 'react';
-import {useSharedAutocompleteContext} from '../../context/SharedAutocompleteContext';
+import {useToolbarState} from '../../context/ToolbarContext';
import {
$createAutocompleteNode,
AutocompleteNode,
} from '../../nodes/AutocompleteNode';
import {addSwipeRightListener} from '../../utils/swipe';
+declare global {
+ interface Navigator {
+ userAgentData?: {
+ mobile: boolean;
+ };
+ }
+}
+
type SearchPromise = {
dismiss: () => void;
promise: Promise;
@@ -76,16 +84,27 @@ function useQuery(): (searchText: string) => SearchPromise {
}, []);
}
+function formatSuggestionText(suggestion: string): string {
+ const userAgentData = window.navigator.userAgentData;
+ const isMobile =
+ userAgentData !== undefined
+ ? userAgentData.mobile
+ : window.innerWidth <= 800 && window.innerHeight <= 600;
+
+ return `${suggestion} ${isMobile ? '(SWIPE \u2B95)' : '(TAB)'}`;
+}
+
export default function AutocompletePlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
- const [, setSuggestion] = useSharedAutocompleteContext();
const query = useQuery();
+ const {toolbarState} = useToolbarState();
useEffect(() => {
let autocompleteNodeKey: null | NodeKey = null;
let lastMatch: null | string = null;
let lastSuggestion: null | string = null;
let searchPromise: null | SearchPromise = null;
+ let prevNodeFormat: number = 0;
function $clearSuggestion() {
const autocompleteNode =
autocompleteNodeKey !== null
@@ -101,7 +120,7 @@ export default function AutocompletePlugin(): JSX.Element | null {
}
lastMatch = null;
lastSuggestion = null;
- setSuggestion(null);
+ prevNodeFormat = 0;
}
function updateAsyncSuggestion(
refSearchPromise: SearchPromise,
@@ -124,12 +143,18 @@ export default function AutocompletePlugin(): JSX.Element | null {
return;
}
const selectionCopy = selection.clone();
- const node = $createAutocompleteNode(uuid);
+ const prevNode = selection.getNodes()[0] as TextNode;
+ prevNodeFormat = prevNode.getFormat();
+ const node = $createAutocompleteNode(
+ formatSuggestionText(newSuggestion),
+ uuid,
+ )
+ .setFormat(prevNodeFormat)
+ .setStyle(`font-size: ${toolbarState.fontSize}`);
autocompleteNodeKey = node.getKey();
selection.insertNodes([node]);
$setSelection(selectionCopy);
lastSuggestion = newSuggestion;
- setSuggestion(newSuggestion);
},
{tag: 'history-merge'},
);
@@ -175,7 +200,9 @@ export default function AutocompletePlugin(): JSX.Element | null {
if (autocompleteNode === null) {
return false;
}
- const textNode = $createTextNode(lastSuggestion);
+ const textNode = $createTextNode(lastSuggestion)
+ .setFormat(prevNodeFormat)
+ .setStyle(`font-size: ${toolbarState.fontSize}`);
autocompleteNode.replace(textNode);
textNode.selectNext();
$clearSuggestion();
@@ -224,7 +251,7 @@ export default function AutocompletePlugin(): JSX.Element | null {
: []),
unmountSuggestion,
);
- }, [editor, query, setSuggestion]);
+ }, [editor, query, toolbarState.fontSize]);
return null;
}