Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
by facebookTypeScript
Last 12 weeks · 298 commits
5 of 6 standards met
Description PR #8591 converted the shared icon glyph classes (, , etc.) from to for forced-colors support. The floating toolbar's fill was restored in 3bcb93ce8, but didn't have a corresponding rule in , so it rendered as a solid rectangle. This adds the missing rule for , matching the pattern used by every other icon glyph. Test plan Before The "Insert comment" button (rightmost in the floating toolbar) shows as a black square. After The button shows the speech bubble icon. [x] Manual playground: select text, floating toolbar appears, comment icon renders correctly [x] prettier clean
Description normalizes selection points at inline element boundaries. For inline elements that return (like ), it pushes the cursor outside at the right edge. When the inline element contains only a single child, offset 0 is the left edge and offset 1 is the right edge — both get normalized outward, making it impossible to place the cursor inside. This adds a guard to the right-edge normalization branch, so single-child inline elements keep the cursor inside at the right edge. Left-edge normalization (which keeps left bias) is unchanged. Closes #7551 Test plan [x] — 69 tests pass Single-child link: cursor stays at offset 1 inside the link Multi-child link: cursor normalizes to the next sibling (existing behavior preserved) [x] Manual playground: Type "hello", select "hello", insert link, click at end of link text: cursor stays inside the link and further typing extends the link Multi-word link: cursor at end of last child still normalizes outward [x] tsc / eslint / prettier clean
Lexical version: 0.38.2 Steps To Reproduce 1. In Firefox, go tothe official Lexical Playground 2. Select some text in the editor 3. Drag and drop it somewhere else in the editor Link to code example: No code example needed, as it can be easily reproduced in the official Playground. The current behavior Drag-and-drop of text does not work in Firefox. The text is not moved to the drop location. The expected behavior Drag-and-drop should work in Firefox (like it does in Chrome). Impact of fix The bug does affect all Firefox users.
In general using "Enter" on a toolbar that makes changes to lexical is problematic. Lets say I have a button that toggles "bold" and it's triggered using the "Enter" key on the mouse-down event which calls dispatchCommand(FORMAT_TEXT_COMMAND...) - this causes lexical to get focus and then handle the Enter key which can replace the selected text with a new line. adding the e.preventDefault to the event solves this problem but the focus still moves from the toolbar to the editor. I'm not sure how to handle this...
Description New package shipping framework-agnostic accessibility as extensions, plus a small set of React adapter hooks in , a shared ref-counted-registry primitive added to core , and reference adoption across the playground that takes the editor to WCAG 2.1 AA on the keyboard and with a screen reader. Builds on the keyboard accessibility RFC #6006 + KaiPrince's PR #7804 (focus indicator + dropdown focus return — already merged). The accessibility behavior ships as six platform-independent extensions. They are framework-agnostic because is: any host that builds an editor from extensions gets them, React hosts get thin adapter hooks, and non-React hosts (Svelte / Vue / Solid / vanilla) drive the same extension outputs from their own lifecycle. No framework type leaks into . Closes #6006. What ships (new package) — framework-agnostic, depends only on + + . Six extensions: — owns a single visually hidden region for the editor and exposes a stable sink as its output, plus runtime-tunable / signals. The region is bound to the editor's root element (via ), so it is created in the editor's own document — e.g. an iframe-portaled editor — not the top-level one, and follows the root across remounts. re-fires on a duplicate message via a trailing zero-width space. WAI-ARIA status message pattern (WCAG 4.1.3). A no-op until a region is mounted (a root element exists, or an is configured). — depends on ; announces undo / redo ( / , configurable) at , returning to keep the command chain intact. A signal toggles it at runtime. — same shape, for transitions ( / ). Transition-based (silent on mount); also runtime-. — output is a reference-counted registry: . Tab / Shift+Tab fully managed inside the container; a document-level listener pulls escaping focus back; restores the previously-focused element on dispose. . — registry . WAI-ARIA roving-tabindex: arrow keys + Home / End rove among items, Tab leaves the group as a unit. Items are queried lazily per interaction, so additions / removals are picked up automatically. — registry . APG editor menubar pattern: Alt+F10 inside the editor focuses the toolbar's first roving item, Escape inside the toolbar returns focus to the editor; the editor's selection is preserved across the jump. The three focus extensions share one core primitive: their output is a (reference-counted by container element), so the same container can be driven by more than one caller (or survive a re-entrant registration) without double-wiring, and the activation is torn down only when the last registration is released. (adapters) — four thin hooks; each requires the matching extension in the editor's tree. New in this PR: — a stable . — attach with ; registers the node while , releases on detach / deactivate. (held in a ref, so an inline lambda doesn't re-create the trap) lets portaled panels keep focus. . . The Ref hooks return a that registers / releases the attached node through the extension's reference-counted registry — changing the options re-registers with the new config without leaking the old one. (core) — adds / (a small first-activate / last-release reference-counting map keyed by object identity). now uses it to share a single document listener across every editor / root element registered against a document (replacing the hand-rolled count), which also makes the per-document keying correct for shadow-DOM / iframe hosts. Behavior-preserving. — adds , exposing as a reactive (consumed by , and by the playground autocomplete that previously hand-rolled the same ). (reference adoption) — wires the adapters across the main toolbar, floating text-format toolbar, modal, and equation node (see "Eleven areas"). (docs) — documents the contract per surface; the new extensions and are listed on the Included Extensions page. Using With directly (any host): From React ( / extension host): Non-React hosts (Svelte / Vue / Solid / vanilla) read the same extension output and call / the returned disposer from their own mount / cleanup hooks — no framework-specific adapter package ships in this PR. Eleven areas covered Grouped by WAI-ARIA APG pattern. Operate (keyboard) 1. Main toolbar + — screen readers group the toolbar buttons. 2. Main toolbar roving tabindex — ArrowRight / ArrowLeft / Home / End move between buttons; Tab leaves the toolbar in one step. . 3. FloatingTextFormatToolbar roving tabindex + — mirrors the main toolbar. 4. Alt+F10 editor ↔ toolbar jump with Escape return + selection restore — . 5. Modal focus trap — . Tab / Shift+Tab fully managed; document-level recovery. lands initial focus on the dialog container () so screen readers announce the dialog body via before any control. Perceive (visual + announce) 6. + dynamic — cached, re-applied on . 7. Editor mode announce — announces editable ↔ read-only transitions via aria-live. 8. Undo / Redo announce — announces the action via aria-live. 9. Forced-colors () — toolbar buttons, editor border, and modal outline pick up system / ; toolbar icons and the logo switch to with so they render as system foreground. Color-meaning icons (font-color, bg-color) keep so the picker color is preserved. 10. — collapses transitions / animations to . Understand (page contract) 11. Keyboard accessibility concepts page — documents the contract per surface (toolbar / modal / editor) in one place. Considered but dropped opt-in — verified Lexical's default on Escape already moves Tab focus past the editor on Chrome and Safari (macOS), so the opt-in was redundant for WCAG 2.1.2. / core API — only one consumer (); the existing direct pattern already covers it. — Tab inside is consumed by , and the Tab outline overlapped the outline. Equation stays announced via + ; reach it by caret traversal. Backwards compatibility Everything in and the four adapters is new and unreleased — no existing signatures change. The only core change is internal: now shares its listener through the new . The observable behavior (one shared document listener, attached on first root, removed after the last) is unchanged. The focus trap installs a -level listener: focus landing outside an active trap is pulled back inside (use , or portal panels inside the container). Only one trap should be active at a time. initial focus is the dialog container, not the first focusable — no outline ring on open (the inner outline stays); screen reader users hear the dialog title first. Test plan Before The playground had no keyboard-accessibility wiring for the toolbar / modal / equation surfaces; no package. After Automated [x] / — clean. [x] — passes. New unit tests for the six extensions (incl. shadow-DOM variants), the four React adapter hooks, the core (ref-counting / idempotent dispose / re-registration), and the ref-count. [x] prettier + eslint + the build-export audit — clean. Chrome / Safari (macOS) — keyboard + DOM [x] Toolbar + ; roving tabindex (Arrow / Home / End, single-step Tab out, 0 on active / -1 on rest) on both the main and floating toolbars. [x] Alt+F10 editor → toolbar; Escape returns focus to the editor with the pre-jump selection restored; default Escape blurs the editor and Tab leaves it. [x] Modal focus trap: initial focus on the dialog container; Tab / Shift+Tab cycle within; never escapes; close returns focus to the opener. Safari no longer flashes the URL bar between trap boundaries. [x] DOM has + , updated on edit. Screen readers [x] VoiceOver (macOS) and Narrator (Windows 11): toolbar grouping, dialog title announced before controls, equation , editable ↔ read-only announcements, Undo / Redo announcements. Visual preferences [x] Windows High Contrast / forced-colors: buttons, border, modal use system colors; toolbar icons + logo render as system foreground via ; color-picker icons keep their SVG colors. [x] (macOS + DevTools emulation): transitions collapse to ~. Mobile — physical keyboard** [x] iOS Safari / Android Chrome + bluetooth keyboard: Escape blurs the editor; Tab moves to the next focusable.
Repository: facebook/lexical. Description: Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance. Stars: 23592, Forks: 2193. Primary language: TypeScript. Languages: TypeScript (78.3%), JavaScript (19.2%), CSS (1.5%), MDX (0.5%), HTML (0.3%). License: MIT. Homepage: https://lexical.dev Latest release: v0.46.0 (6d ago). Open PRs: 31, open issues: 360. Last activity: 7h ago. Community health: 87%. Top contributors: trueadm, zurfyx, etrepum, acywatson, fantactuka, thegreatercurve, potatowagon, ivailop7, tylerjbainbridge, Sahejkm and others.