Conversation
r1chm8
force-pushed
the
laz-860
branch
2 times, most recently
from
August 6, 2026 13:55
a228471 to
c394df9
Compare
Bumps @headlessui/react from 1.7.19 to 2.2.10, now that the workspace is on React 18 and its useId requirement is satisfiable. Dot-notation subcomponents (Menu.Button and friends) are deprecated in v2, so every call site moves to the flat exports; Dialog.Overlay is the one API actually removed and becomes DialogBackdrop. Dropdown, listbox and popover panels now take their placement from v2's anchor prop, which positions them with Floating UI and portals them out of any clipping ancestor, replacing the hand-written absolute positioning in the stylesheets and the right-0/left-auto overrides at the call sites. Panels flip and shift to stay on screen, and the dropdown height cap moves to --anchor-max-height so it survives the inline style anchor sets. Closes LAZ-860.
Puts the tri-state switch onto a Headless UI primitive without losing the semi-on state. It's `Checkbox` rather than `Switch` because ARIA only allows aria-checked to be true or false on role="switch", and Headless UI controls that attribute, so mixed can't be expressed there — a tri-state master control is the checkbox pattern, which is what this component already was underneath. The track and thumb now style themselves off the data attributes Headless UI sets rather than a data-state we derived ourselves, the visually-hidden native input is gone, and onChange hands over the new checked value instead of a change event.
Resizing a toolbar with its overflow menu open makes chromium emit
"ResizeObserver loop completed with undelivered notifications." as an
ErrorEvent whose `error` is null, so errorHandler's fallback chain
resolved it to `evt.message` - a plain string.
Two things then went wrong. The string branch only matched chromium's
pre-92 wording ("loop limit exceeded"), so the modern message fell
through; and the packery check a few lines later read `error.stack`
unguarded, which on a string is undefined. The handler threw
"Cannot read properties of undefined (reading 'includes')", and that
exception - itself uncaught - was what reached the log, reported as a
bug in Vortex rather than the benign warning behind it.
Match the warning by prefix so both wordings are covered, and
preventDefault it so chromium stops printing it on its own account.
Read the stack once as a string for the library/extension sniffing, so
any ErrorEvent without an error object (a cross-origin "Script error."
too, not just this one) can no longer take the handler down.
The ResizeObserver loop itself is unchanged: floating-ui's autoUpdate
and the toolbar's own observer feed each other while the menu is open,
which chromium handles by deferring a frame.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why now
v2 requires React 18's
useId, which is why this was deferred. The workspace is onreact: 18.3.1(verified: the renderer resolves 18.3.1 and React DevTools reports 18.3.1 inthe running app), so the blocker is gone. There was a stale
react@17.0.2plus a Headless UI built against it innode_modules/.pnpm, but the lockfile has zero references toeither — orphaned artifacts, not real dependencies.
Commit 1 —
@headlessui/react1.7.19 → 2.2.10The migration was far smaller than expected, for two reasons: v2 keeps dot notation as deprecated aliases, so nothing was forced to change to compile, and there is no
Transitionusage anywhere in Vortex, which is normally the bulk of a v1→v2 migration.Dialog.Overlayis the only genuinely removed API →DialogBackdrop. One site, inModal.tsx.Menu.Button,Popover.Panel,Listbox.Options, …) to the flat exports. Several of our own components share names with v2'sexports, so those imports are aliased (
PopoverButton as HeadlessPopoverButton).anchorpositioningDropdownItems,PopoverPanelandListboxOptionsnow take placement fromanchor={{ gap: 4, to: "bottom end" }}, replacing hand-written absolute positioning. Three thingsworth knowing, all confirmed from the compiled source rather than assumed:
anchorforcesportal: true(_&&(s=!0)inmenu.js). Panels move to#headlessui-portal-rootat body level, so they escape clipping ancestors — but z-index becomesthe only thing keeping them above the page.
position: absolute; right: 0; margin-tophad to go or they fight it. The 4px margin becamegap: 4.max-height: min(var(--anchor-max-height, 100vh), …), and inline beats the stylesheet — so our designed 512px dropdown cap was silently becoming856px. Moving it to
--anchor-max-heightrestores the cap and keeps the viewport-awareness.Also removed the now-obsolete
right-0 left-auto/absolute … mt-2.5overrides at three call sites, since they'd fight the inline styles, and convertedPicker'splacementprop to drive
anchorrather than CSS classes. I kept the prop rather than deleting it as its TODO suggested — v2 removes the class hack, not the start/end choice the propexpresses.
Commit 2 —
Switchon Headless UI'sCheckboxNot HUI's
Switch: ARIA only allowsaria-checkedto be true/false onrole="switch", and HUI controls that attribute (SwitchPropsWeControlomits it from the publicprops), so
mixedcannot be expressed there. A tri-state master control is the checkbox pattern — which is what this component already was underneath (role="checkbox"+native
indeterminate). HUI'sCheckboxtakesindeterminateand emitsaria-checked": l?"mixed":a?"true":"false", so the tri-state is preserved exactly.A three-way radio group was considered and rejected:
semi-onis never user-selectable (clicking only flips on↔off), so radios would expose a state the user can't legitimatelychoose and announce it as "1 of 3".
useEffectthat imperatively setindeterminateare both gone; the component is 67 lines lighter.data-state="on|semi-on|off"onto HUI's owndata-checked,data-indeterminate,data-disabled,data-hover,data-active,data-focus—so hover/active/focus now come from HUI's state and get proper pointer-vs-keyboard semantics.
onChange(event)→onChange(checked: boolean). Both real consumers (theDisplayOptionswrappers) pass() => void, so were unaffected; five demo call sites thatdestructured
event.target.checkedwere updated.Testing
DialogBackdropdims correctly behind the panelplacement="left"Switch verified across all six states — off / on / semi-on, each enabled and disabled — with semi-on reporting
aria-checked="mixed". The "select all" master switch goesmixed→ all on → all off, never landing on mixed.
Switch tests went 6 → 8, adding explicit coverage for
role="checkbox"+aria-checked="mixed"and for semi-on holding whenchecked={false}, since that pairing is the wholereason for the
Checkboxchoice.