Skip to content

fix(ui): harden the Astryx floating kernel (PR 5 review round 2) - #1672

Merged
Astro-Han merged 5 commits into
mainfrom
fix/1565-floating-kernel-hardening
Jul 30, 2026
Merged

fix(ui): harden the Astryx floating kernel (PR 5 review round 2)#1672
Astro-Han merged 5 commits into
mainfrom
fix/1565-floating-kernel-hardening

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

Refs #1565. PR #1665 merged while its second review round was still landing; these are the adjudicated round-2 fixes (Codex fix-verification pass + an independent fresh-eye pass, both PASS with no P0/P1), cherry-picked onto current main content-identical to the validated tree.

  • Aborted press swallowed the next keyboard activation (both reviewers independently): the popover trigger's per-gesture guard cleared its flag only inside a completed click, so a press that light-dismissed the popover and ended off the trigger left the flag set — the next Enter/Space was silently swallowed. The guard now judges only pointer-sourced clicks (event.detail > 0) and clears before the defaultPrevented early return.
  • Tooltip no longer lingered after activating its trigger: Base UI's trigger closed the tooltip on activation (closeOnClick default); the Astryx adapter dropped that. Restored through Astryx's own seams — hidePopover() on the layer element (its toggle listener syncs state back) plus a synthetic mouseleave that cancels a show still pending its delay.
  • Controlled reconcile echoed onOpenChange: with open held and the layer moving natively (Escape), the reconcile effect re-reported the parent's own instruction as a new request. Prop-driven reconcile is now silent on onOpenChange; onOpenChangeComplete still fires (lifecycle, and TimePicker keys its settled state on it).
  • Popover focus fallback could pick a disabled control; the selector now mirrors Astryx's own exclusion.
  • Comment truth-ups: light dismiss lands on pointerup per the HTML spec (not pointerdown), and focus return on close is native-first with useFocusTrap as backstop.

Verification

  • @maka/ui: typecheck clean, unit 280/280, Biome format:check clean (all on this branch, post-refactor(ui): migrate Markdown core to Astryx #1668 base).
  • floating-layers e2e on this branch: 2/2, including the two new behavior pins (aborted-press → Enter reopens; one-shot tooltip-hidden right after trigger click). Both pins were red-verified against a pre-fix build (each fails at exactly the guarded assertion) before landing.
  • Pre-merge-race, on the content-identical tree: visual contract 20/20 zero-diff, hit-test 5/5, tooltip-converge + z-index contracts 10/10, floating-layers 6/6 across 3 repeats.
  • Not run here: the full desktop e2e suite (a run on the old worktree was interrupted at 2/37 by the PR refactor(ui): move the floating kernel (Tooltip, Popover) onto Astryx #1665 merge cleanup); CI should cover it.

Review focus

The tooltip dismissal goes through native hidePopover() + a synthetic mouseleave because Astryx's useTooltip exposes no hide(); if upstream grows one, the adapter should switch to it.

…ard activation

The per-gesture guard cleared its flag only inside a completed click, so a
press that light-dismissed the popover and then ended off the trigger (drag
off, pointercancel) left the flag set — and the next keyboard Enter/Space,
which has no paired pointerdown, was silently swallowed. Judge only
pointer-sourced clicks (event.detail > 0) and clear the flag before the
defaultPrevented early return. Found independently by both round-2 reviews.

The e2e journey pins it: aborted press dismisses, then Enter (detail 0)
must still open. Red-verified against the pre-fix build.
The Base UI trigger closed the tooltip on activation (closeOnClick default);
the Astryx adapter dropped that, so clicking a chrome icon button left its
hint floating under the pointer. Astryx's useTooltip exposes no hide(), so
the trigger goes through the primitive's own seams: hidePopover() on the
layer element (its toggle listener syncs React state back) plus a synthetic
mouseleave that cancels a show still pending its delay. The trigger's click
handler now also composes render-element and call-site onClick, matching the
Base UI mergeProps behavior.

Known residual, recorded: Astryx exposes no way to also suppress the
follow-up focus-show path a keyboard activation could schedule; hover paths
are fully covered. E2E pins the synchronous dismissal with a one-shot check
(every other hide path is delayed >=100ms); red-verified pre-fix.
When the open prop contradicts the layer (open held true + Escape), the
reconcile effect's show()/hide() re-fired onOpenChange — reporting the
parent's own instruction back as if it were a second user request. Silence
the request callback during prop-driven reconcile (useLayer invokes it
synchronously inside show()/hide(), so a flag around the calls is exact);
onOpenChangeComplete keeps firing because it reports lifecycle, not intent,
and TimePicker keys its settled state on it.
The initialFocus fallback selector could pick a disabled control, making
focus() a silent no-op. Mirror the exclusion Astryx's own FOCUSABLE_SELECTOR
applies (not exported from the package, hence the local declaration).
The header claimed useFocusTrap's restore effect was the sole focus-return
authority; per the HTML spec the show-popover algorithm records the
previously focused element even for imperative showPopover(), and
hidePopover() returns focus natively. The trap restore is the backstop for
the paths the browser skips (light dismiss). Behavior unchanged.
@Astro-Han
Astro-Han merged commit 3829e56 into main Jul 30, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the fix/1565-floating-kernel-hardening branch July 30, 2026 18:38
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