Skip to content

[lexical-html][lexical-playground] Feature: Conditional DOM render overrides (disabledForEditor/disabledForSession)#8575

Merged
etrepum merged 23 commits into
facebook:mainfrom
etrepum:claude/vigilant-cerf-AY7id
May 28, 2026
Merged

[lexical-html][lexical-playground] Feature: Conditional DOM render overrides (disabledForEditor/disabledForSession)#8575
etrepum merged 23 commits into
facebook:mainfrom
etrepum:claude/vigilant-cerf-AY7id

Conversation

@etrepum

@etrepum etrepum commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Description

domOverride overrides registered with DOMRenderExtension are currently compiled once into the editor's render config and stay active for the editor's lifetime. Extensions that want to toggle behavior have to keep the
override permanently installed and no-op per node — e.g. VisibleLineBreakExtension checked $isDisabled(editor) inside every $createDOM/$updateDOM and forced a re-render via a no-op
registerNodeTransform, and TerseExportExtension ran a wildcard $exportDOM on every node of every export and bailed after a context check.

This PR adds an optional third options argument to domOverride that gates whether an override is installed, based only on render context:

  • disabledForEditor(ctx) — gates residency in the editor's render config (reconciliation + the base for export). It is driven imperatively via the new $setRenderContextValue, which recompiles the resident config and re-renders the affected nodes, recreating DOM for structural overrides ($createDOM/$getDOMSlot).
  • disabledForSession(ctx) — gates participation in a single export/generate session, evaluated once against the active session context. It has no effect on live reconciliation (which is not a session).

Both predicates default to "not disabled", so existing domOverride calls are
unaffected.

New @lexical/html API:

  • domOverride(nodes, config, options?) with DOMOverrideOptions (disabledForEditor, disabledForSession) and the RenderContextReader passed to them.
  • $setRenderContextValue / $updateRenderContextValue — imperative, persistent writes to the editor render context (vs. the scoped $withRenderContext).
  • $getSessionDOMRenderConfig — resolves the per-session render config.

VisibleLineBreakExtension now uses disabledForEditor (dropping the per-node $isDisabled checks and the no-op-transform re-render hack) and TerseExportExtension uses disabledForSession (dropping the per-call terse check, so non-terse exports never enter the terse middleware).

Implementation note: this is contained entirely within @lexical/html — no core reconciler change was needed. Structural reverts on disable are driven through the existing $updateDOM → true recreate path via a transient force-recreate wrapper on the recompiled config, committed with a discrete update so the toggle re-renders synchronously.

lexical

Adds a new internal $fullReconcile() API to cause a reconciliation without having to mark nodes as dirty and cause NodeMap/history churn

Other:

Includes some drive-by fixes to:

Closes #8567

Test plan

New unit tests

claude added 2 commits May 27, 2026 23:29
…errides via disabledForEditor/disabledForSession

Adds an options argument to domOverride that gates whether an override is
installed, based only on render context:

- disabledForEditor: gates residency in the editor's render config
  (reconciliation + export base). Mirrored imperatively via
  $setRenderContextValue, which recompiles the resident config and
  re-renders affected nodes, recreating DOM for structural overrides.
- disabledForSession: gates participation in a single export/generate
  session, evaluated against the active session context. No effect on
  live reconciliation.

VisibleLineBreakExtension now removes its wrap from the pipeline entirely
when disabled (no per-node checks, no manual re-render hack), and
TerseExportExtension is installed only for terse export sessions.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
@vercel

vercel Bot commented May 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment May 28, 2026 3:46am
lexical-playground Ready Ready Preview, Comment May 28, 2026 3:46am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 27, 2026
Clarifies that the captured config is the editor config from init (whose
`dom` is the user's pre-override base), not a recompile-specific structure.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
compileDOMRenderConfigOverrides only reads `nodes` (type tree) and `dom`
(base), so capture and pass just `Pick<InitialEditorConfig, 'nodes' | 'dom'>`
instead of the whole InitialEditorConfig.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…arkDirtyByType

We only need to assert the popped value is defined, not cast its type.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…d use it for conditional override re-renders

Adds an @internal $fullReconcile() to lexical that schedules a FULL_RECONCILE
of the active editor, re-rendering every node through the current config
without cloning the node map (so no spurious mutation/collaboration changes).

- lexical-utils $restoreEditorState drops its local `const FULL_RECONCILE = 2`
  workaround in favor of $fullReconcile().
- DOMRenderRuntime toggles now re-render via $fullReconcile() instead of
  marking/cloning nodes. Structural overrides force a recreate through a
  transient $updateDOM wrapper (a single (node) => boolean predicate) that is
  installed only for that reconcile and removed immediately after.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…er override test

Use null | LexicalNode instead of unknown for the before/after node captures
and clarify that identity equality proves the node was never made writable.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
… indexed access

Move DOMRenderMatchConfig to types.ts and export it, since it appears in the
public signature of domOverride. Type init's initialEditorConfig as
DOMRenderInitResult['initialEditorConfig'] to avoid duplicating the Pick.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…e toggle

Collapse the rerender/recreate split into a single recreatePredicate.
$createDOM/$getDOMSlot/$decorateDOM all recreate the affected nodes — including
$decorateDOM, whose additive DOM can only be reverted by a fresh $createDOM.
$updateDOM (diff-driven, applies on the next update) and export-only hooks need
no re-render. Recreating is the simple, always-correct choice; toggles are rare
so it can be optimized later.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
Use HISTORY_MERGE_TAG instead of the literal, pass $fullReconcile directly
(eta-reduce), and restore the transient $updateDOM on the next line — the
discrete update commits synchronously, so onUpdate is unnecessary.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…ust be discrete

The transient $updateDOM recreate wrapper mutates the shared active config, so
the reconcile must run and finish synchronously before the original is restored
(and the call must not be nested in an editor.update). Document that invariant
so the update isn't naively made non-discrete.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
… a theme load

$codeNodeTransform returned early while the default theme loaded asynchronously,
even when the tokenizer has defaultLanguage: null and the text is plainified
(no theme needed). That left such code blocks as an unsplit TextNode until the
theme happened to finish loading. Only gate the split on the theme load when a
language will actually consume it.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…es for VSCode

VSCode walks up from a test file to the nearest tsconfig.json (the root),
which excluded **/__tests__/** and lacked the test-only path aliases. That
left test files as loose files in the editor — every workspace import
("@lexical/*", "lexical", and the *src/__tests__/utils* subpaths) reported
"Cannot find module" even though the CLI tsc-test was fine.

Have update-tsconfig.mjs write the same generated test paths into
tsconfig.json so VSCode resolves them, and drop **/__tests__/** from its
exclude. Build output is unaffected (tsconfig.build.json has its own
**/__tests__/** exclude and emits no test files into the dist .d.ts —
verified). tsc-test is now equivalent to tsc, so drop the script.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…dirty in the same update

internalMarkNodeAsDirty (and the sibling clone path) unconditionally wrote
editor._dirtyType = HAS_DIRTY_NODES, which silently downgraded a previously
set FULL_RECONCILE — if a caller does
`editor.update(() => { $fullReconcile(); n.markDirty(); })`, the commit ends
up reconciling only the marked nodes instead of the full graph. Upgrade
the dirty type only when nothing has been marked yet (`=== NO_DIRTY_NODES`).
Also replaces a bare `editor._dirtyType = 0` reset with `NO_DIRTY_NODES`.

https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
@etrepum etrepum added this pull request to the merge queue May 28, 2026
Merged via the queue into facebook:main with commit 149c37d May 28, 2026
42 checks passed
@etrepum etrepum mentioned this pull request May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Add overlay support for DOMRenderExtension

3 participants