diff --git a/packages/richtext-lexical/src/features/indent/client/index.tsx b/packages/richtext-lexical/src/features/indent/client/index.tsx index 66d6f23d13..94cdb8d480 100644 --- a/packages/richtext-lexical/src/features/indent/client/index.tsx +++ b/packages/richtext-lexical/src/features/indent/client/index.tsx @@ -7,6 +7,7 @@ import type { ToolbarGroup } from '../../toolbars/types.js' import { IndentDecreaseIcon } from '../../../lexical/ui/icons/IndentDecrease/index.js' import { IndentIncreaseIcon } from '../../../lexical/ui/icons/IndentIncrease/index.js' import { createClientFeature } from '../../../utilities/createClientFeature.js' +import { IndentPlugin } from './plugins/index.js' import { toolbarIndentGroupWithItems } from './toolbarIndentGroup.js' const toolbarGroups: ToolbarGroup[] = [ @@ -55,6 +56,12 @@ const toolbarGroups: ToolbarGroup[] = [ ] export const IndentFeatureClient = createClientFeature({ + plugins: [ + { + Component: IndentPlugin, + position: 'normal', + }, + ], toolbarFixed: { groups: toolbarGroups, }, diff --git a/packages/richtext-lexical/src/features/indent/client/plugins/index.tsx b/packages/richtext-lexical/src/features/indent/client/plugins/index.tsx new file mode 100644 index 0000000000..a64884ceea --- /dev/null +++ b/packages/richtext-lexical/src/features/indent/client/plugins/index.tsx @@ -0,0 +1,61 @@ +'use client' + +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js' +import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin' +import { mergeRegister } from '@lexical/utils' +import { COMMAND_PRIORITY_NORMAL, FOCUS_COMMAND, KEY_ESCAPE_COMMAND } from 'lexical' +import { useEffect, useState } from 'react' + +import type { PluginComponent } from '../../../typesClient.js' + +export const IndentPlugin: PluginComponent = () => { + const [editor] = useLexicalComposerContext() + + const [firefoxFlag, setFirefoxFlag] = useState(false) + + useEffect(() => { + return mergeRegister( + editor.registerCommand( + FOCUS_COMMAND, + () => { + setFirefoxFlag(false) + return true + }, + COMMAND_PRIORITY_NORMAL, + ), + editor.registerCommand( + KEY_ESCAPE_COMMAND, + () => { + setFirefoxFlag(true) + editor.getRootElement()?.blur() + return true + }, + COMMAND_PRIORITY_NORMAL, + ), + ) + }, [editor, setFirefoxFlag]) + + useEffect(() => { + if (!firefoxFlag) { + return + } + + const handleKeyDown = (e: KeyboardEvent) => { + if (!['Escape', 'Shift'].includes(e.key)) { + setFirefoxFlag(false) + } + // Pressing Shift+Tab after blurring refocuses the editor in Firefox + // we focus parent to allow exiting the editor + if (e.shiftKey && e.key === 'Tab') { + editor.getRootElement()?.parentElement?.focus() + } + } + + document.addEventListener('keydown', handleKeyDown) + return () => { + document.removeEventListener('keydown', handleKeyDown) + } + }, [editor, firefoxFlag]) + + return +} diff --git a/packages/richtext-lexical/src/lexical/LexicalEditor.tsx b/packages/richtext-lexical/src/lexical/LexicalEditor.tsx index 537ae24a0b..723d4b679e 100644 --- a/packages/richtext-lexical/src/lexical/LexicalEditor.tsx +++ b/packages/richtext-lexical/src/lexical/LexicalEditor.tsx @@ -4,7 +4,6 @@ import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary.js' import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin.js' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin.js' import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin.js' -import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin.js' import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, FOCUS_COMMAND } from 'lexical' import * as React from 'react' import { useEffect, useState } from 'react' @@ -115,7 +114,7 @@ export const LexicalEditor: React.FC< -
+
@@ -173,8 +172,6 @@ export const LexicalEditor: React.FC< {editorConfig?.features?.markdownTransformers?.length > 0 && } )} - - {editorConfig.features.plugins?.map((plugin) => { if (plugin.position === 'normal') { return ( diff --git a/test/fields/collections/Lexical/e2e/main/e2e.spec.ts b/test/fields/collections/Lexical/e2e/main/e2e.spec.ts index 7d98c382b5..482df08f32 100644 --- a/test/fields/collections/Lexical/e2e/main/e2e.spec.ts +++ b/test/fields/collections/Lexical/e2e/main/e2e.spec.ts @@ -784,6 +784,54 @@ describe('lexicalMain', () => { }) }) + /** + * When the escape key is pressed, Firefox resets the active element to the beginning of the page instead of staying with the editor. + * By applying a keydown listener when the escape key is pressed, we can programatically focus the previous element if shift+tab is pressed. + */ + test('ensure escape key can be used to move focus away from editor', async () => { + await navigateToLexicalFields() + + const richTextField = page.locator('.rich-text-lexical').first() + await richTextField.scrollIntoViewIfNeeded() + await expect(richTextField).toBeVisible() + // Wait until there at least 10 blocks visible in that richtext field - thus wait for it to be fully loaded + await expect(page.locator('.rich-text-lexical').nth(2).locator('.lexical-block')).toHaveCount( + 10, + ) + await expect(page.locator('.shimmer-effect')).toHaveCount(0) + + const paragraph = richTextField.locator('.LexicalEditorTheme__paragraph').first() + await paragraph.scrollIntoViewIfNeeded() + await expect(paragraph).toBeVisible() + + const textField = page.locator('#field-title') + const addBlockButton = page.locator('.add-block-menu').first() + + // Pressing 'Escape' allows focus to be moved to the previous element + await paragraph.click() + await page.keyboard.press('Tab') + await page.keyboard.press('Escape') + await page.keyboard.press('Shift+Tab') + await expect(textField).toBeFocused() + + // Pressing 'Escape' allows focus to be moved to the next element + await paragraph.click() + await page.keyboard.press('Tab') + await page.keyboard.press('Escape') + await page.keyboard.press('Tab') + await expect(addBlockButton).toBeFocused() + + // Focus is not moved to the previous element if 'Escape' is not pressed + await paragraph.click() + await page.keyboard.press('Shift+Tab') + await expect(textField).not.toBeFocused() + + // Focus is not moved to the next element if 'Escape' is not pressed + await paragraph.click() + await page.keyboard.press('Tab') + await expect(addBlockButton).not.toBeFocused() + }) + test('creating a link, then clicking in the link drawer, then saving the link, should preserve cursor position and not move cursor to beginning of richtext field', async () => { await navigateToLexicalFields() const richTextField = page.locator('.rich-text-lexical').first() @@ -798,7 +846,6 @@ describe('lexicalMain', () => { const paragraph = richTextField.locator('.LexicalEditorTheme__paragraph').first() await paragraph.scrollIntoViewIfNeeded() await expect(paragraph).toBeVisible() - /** * Type some text */ @@ -1129,7 +1176,7 @@ describe('lexicalMain', () => { const lexicalField: SerializedEditorState = lexicalDoc.lexicalRootEditor // @ts-expect-error no need to type this - await expect(lexicalField?.root?.children[1].fields.someTextRequired).toEqual('test') + expect(lexicalField?.root?.children[1].fields.someTextRequired).toEqual('test') }).toPass({ timeout: POLL_TOPASS_TIMEOUT, })