Skip to content

Fix native toolbar to reflect formatting state after kill/resume#1024

Draft
samuelpecher wants to merge 2 commits into
mainfrom
fix-native-adapter-attributes-dispatch
Draft

Fix native toolbar to reflect formatting state after kill/resume#1024
samuelpecher wants to merge 2 commits into
mainfrom
fix-native-adapter-attributes-dispatch

Conversation

@samuelpecher
Copy link
Copy Markdown
Collaborator

@samuelpecher samuelpecher commented Apr 28, 2026

Summary

Fixes Basecamp card #9828466892 — on iOS/Android, after the host app is killed and the composer reopened, the native toolbar buttons (bold, italic, etc.) no longer reflected the active formatting state.

Two compounding root causes:

  • Cause B (lifecycle): #reset left currentlyFocused set to true across disconnect/reconnect, so the next focusin short-circuited at the !this.currentlyFocused guard and never re-synced the toolbar.
  • Cause A (dispatch): the registration-time attributes-change ran synchronously against an editor whose state hadn't settled (no RangeSelection yet, or a stale post-setContent selection), so iOS cached an empty/wrong payload.

The fix:

  • Closure-injection adapter contract. dispatchAttributesChange(attributesFn: () => Attributes)BrowserAdapter doesn't call the closure (free); NativeAdapter calls it, merges with module-level DEFAULT_ATTRIBUTES, and dispatches the lexxy:attributes-change event.
  • Always emit a coherent payload. When there's no RangeSelection, the editor returns {} and the adapter merges it with DEFAULT_ATTRIBUTES — native consumers always receive a renderable payload from the first event onward.
  • Lifecycle hygiene. currentlyFocused is reset in #registerFocusEvents (close to the focus-tracking logic) on every connect.

Public contract change for native consumers

The lexxy:attributes-change event detail now carries headingTag (string "h2"/"h3"/"h4" or null) at top-level and at attributes.heading.active. Both fields hold the same value — top-level headingTag is preserved for backward compat with consumers who read it there. New consumers can read either; attributes.heading.active is the canonical internal location.

Test plan

  • Reproducing test (test/javascript/native/state_restoration.test.js) fails on main, passes on this branch.
  • Structural-parity tests for both active-selection and empty-payload branches.
  • New Playwright regression for toolbar active state across cursor moves (test/browser/tests/formatting/toolbar_active_state.test.js).
  • Vitest unit suite green (96+ native tests).
  • Playwright chromium suite green; flaky retries are pre-existing and unrelated.

@samuelpecher samuelpecher changed the base branch from improve-native-adapter-test-coverage to main April 29, 2026 08:27
NativeAdapter would not dispatch attributes consistently on editor
load/reset and editor operations, leaving the toolbar out of sync. This
streamlines the attributes code between the native and browser bar, both
pulling all format information from Selection.getFormat, which itself
has been streamlined and ligned.

The toolbars will now correctly report status for non-RangeSelections,
namely that all attributes are disabled.

- Initialize currentlyFocused with focus listeners so that an
inconsistent state is not kept on reset
- Move attributes calcs into NativeAdapter so that the BrowserAdapter is
a true noop
- Simplify toolbar refresh code
- Deduplicate selection guards
- Boolean() isPressed for undefined-safety
@samuelpecher samuelpecher force-pushed the fix-native-adapter-attributes-dispatch branch from ed55392 to 8cf2e30 Compare April 29, 2026 17:18
Splits coverage along the seam where the test belongs:

- vitest (`test/javascript/native/`): adapter lifecycle, registration,
dispatch
  shape, freeze/thaw — anything that can be asserted without exercising
real
  Lexical selection behavior. Shared `helpers/attributes_capture.js`
hard-codes
  the public contract (10 attribute keys, top-level
link/highlight/headingTag,
  type invariants per field) and throws on every violation.
- playwright (`test/browser/tests/native/`): everything that needs a
real
  Lexical instance — format toggles, format detail correctness
(link.href,
  highlight color, heading.tag), multi-format spans, selection-driven
  dispatch, history. Shared `helpers/native_capture.js` registers a real
  NativeAdapter (exposed on `window.LexxyNativeAdapter` from the
fixture)
  and asserts the same contract.

Removes three vitest files (`adapter_registration`, `attributes_change`,
`selection_freeze`) whose coverage is redistributed across the new
files.
@samuelpecher samuelpecher force-pushed the fix-native-adapter-attributes-dispatch branch from 8cf2e30 to 7e5304a Compare April 29, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant