Skip to content

Fix console errors in rich-text-mention-listbox#2919

Draft
vivinkrishna-ni wants to merge 1 commit intomainfrom
users/vivin/fix-console-errors-rich-text-editor
Draft

Fix console errors in rich-text-mention-listbox#2919
vivinkrishna-ni wants to merge 1 commit intomainfrom
users/vivin/fix-console-errors-rich-text-editor

Conversation

@vivinkrishna-ni
Copy link
Copy Markdown
Contributor

@vivinkrishna-ni vivinkrishna-ni commented Apr 6, 2026

Pull Request

🤨 Rationale

#2744

Rich text editor mention listbox throws multiple TypeError console errors in Firefox (and occasionally Chrome) when using the @mention feature:

  • "can't access property 'insertBefore', node.parentNode is null"
  • "can't access property 'nextSibling', current is null"
  • "can't access property 'hasChildNodes', this.fragment is undefined"

These errors break the UI — the mention popup fails to render or behaves incorrectly.

👩‍💻 Implementation

FAST Element's @observable system uses DOM.queueUpdate (requestAnimationFrame-based) to execute deferred binding updates. When the mention listbox closes or repositions, the anchored-region's requestReset() sets initialLayoutComplete = false, causing the when(initialLayoutComplete) directive to tear down its <slot> and detach DOM marker nodes. When the deferred binding update fires (next rAF), it tries to insertBefore on a detached marker whose parentNode is null, causing the crash.

Solution

Applied multiple targeted workarounds in the RichTextMentionListbox component to prevent FAST's deferred binding updates from encountering detached DOM markers:

  1. slottedOptionsChanged — conditional base class call: When the listbox is closed, skip super.slottedOptionsChanged() which triggers observable notifications (options, selectedIndex, selectedOptions) that queue deferred DOM updates. Instead, silently update the internal _options list directly.
  2. filterOptions — skip redundant empty-array assignment: When both old and new filtered options are empty (different array references), skip the @observable assignment to avoid queuing a deferred binding update for the when-directive that can crash with detached markers.
  3. anchorElementChanged — bypass requestReset() on re-anchoring: When the anchored region already has an anchor element, write to the backing field _anchorElement directly and call update() instead of going through the observable setter. This avoids requestReset()initialLayoutComplete = false → DOM marker teardown cycle.
  4. setOpen — clear filteredOptions via backing field: When closing the listbox, write to _filteredOptions backing field directly instead of using the observable setter, to avoid queuing a deferred binding update that can crash during teardown.
  5. Replace when directive with ?hidden attribute for no-results label: The when directive creates/destroys DOM fragments, which involves marker node manipulation vulnerable to the timing issue. Using ?hidden keeps the element in the DOM and toggles visibility, avoiding the marker lifecycle entirely.

Known Limitations

  • No-results label not displayed on second attempt: When typing an invalid name the second time (e.g. type @invalid, clear, type @invalid again), the no-results label may not appear in Firefox. This is because the filterOptions optimization skips the @observable assignment when both old and new arrays are empty.
  • Popup flicker on @ tag: When inserting a mention via the @ trigger, the popup briefly appears at the top-left corner of the page before repositioning to the correct anchor location. This is caused by the anchorElementChanged workaround that bypasses requestReset() — the initial positioning is delayed until update() completes.

🧪 Testing

✅ Checklist

  • I have updated the project documentation to reflect my changes or determined no changes are needed.

…t deferred binding updates

- Guard slottedOptionsChanged to skip base class observable notifications when closed
- Replace when-directive with hidden attribute for no-results label to avoid DOM marker teardown
- Skip observable assignment in filterOptions when both old and new arrays are empty
- Use backing field write for anchorElement to bypass requestReset() on subsequent anchoring
- Clear filteredOptions via backing field in setOpen to avoid deferred binding crash

Closes #2744
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