@apostel/bedrock
0.2.1
Patch Changes
e14e4c2: Three fixes to press-and-hold on a touch screen, and one to
popover="hint"that reaches every engine but Chrome.A tooltip could not be dismissed on Safari, Firefox, or any iOS browser.
Tooltip.Contentwrotepopover="hint"unconditionally.popoveris an enumerated attribute whose invalid-value default ismanual, so on an engine withouthintthat is not a tooltip that layers badly — it is a tooltip that light dismiss and Escape never reach. It now asks first and writesautowherehintwould not be honoured, which is what the docs already claimed.Popover.Root'skindprop resolves the same way.A tooltip now goes when you lift your finger. It is a label held up while you press, not somewhere to go. A hover card still stays: it has content to reach into, and tapping elsewhere dismisses it.
The hold is shorter, and depends on the trigger. It was a flat 500ms. A trigger that does nothing when tapped — an info icon — has no tap to protect and opens after 150ms. One that does something (an
onClick, a link, a submit button) waits 250ms, just past the length of a tap. Neither isdelayDuration.701c675: Correct the documented behaviour of
/controlledfor<details>-backed primitives.CollapsibleandAccordionwere described as putting the disclosure back if you declined a toggle, "one frame of visible movement, then back". They do not, and never did:<details>fires nobeforetoggleand nocancel, soonOpenChangeis told about a move that has already happened and returning early from it changes nothing.No runtime behaviour changed. If you wrote a guard in
onOpenChangeon a Collapsible or Accordion expecting it to hold, it never did — move it topreventDefault()on the Trigger's click, which cancels the toggle before it happens, for pointer and keyboard alike. The other primitives are unaffected: Dialog, AlertDialog and everything popover-backed still refuse before anything moves.
0.2.0
Minor Changes
0d130b6: Tooltip and HoverCard now open on a long press, so both work on a touch screen.
Hold the trigger for half a second and the panel opens; lift and it stays, without following the link or firing the trigger's
onClick. A tap is still a tap, and a finger that drifts is scrolling rather than pressing. Dismissal is the popover's own light dismiss, so tapping anywhere else closes it.Where the browser runs
interestforitself (Chrome 142 and later) the press and the panel are its own, but the click the lift produces is not — without this, a long press on a hover card link previewed it and then followed it. That click is now swallowed on both paths.Two smaller behaviour changes come with it:
- The trigger carries
-webkit-touch-callout: noneinline, and turns selection off for the length of a press, so iOS offers its own callout for neither. Both are only applied where the JavaScript intent path is running. - Focus opens the panel where the browser calls that focus visible, rather than on any focus. Tapping a control focuses it on Android, and that focus should not open a tooltip.
- The trigger carries
0.1.1
Patch Changes
39a82f5: Fix popover content emptying itself a frame after it opens.
useOpenStateasked whether an element was open with:open, on the assumption that it covers dialog, popover, details and select alike. It does not::opendoes not match an open popover in Chrome. So a frame after opening, the DOM was read as closed and the content's children were unmounted while the popover stayed open — visibly a flicker open followed by a collapse to the smallest possible size, since the content is sized bymax-contentand there was no longer any content.Tooltip and HoverCard showed this every time. The read is shared by every popover-backed primitive, but whether it bit depended on a race between the queued
toggleevent, which set the flag back, and therequestAnimationFramethat cleared it — and those land in a different order whenshowPopover()comes from a timer rather than from a click.Fixing it exposed a second bug it had been masking. Menus focus their first item from an effect on the open state, which is set on
beforetoggle— while the popover is stilldisplay: none, wherefocus()does nothing. It only worked because the open state was briefly cleared and re-set, running the effect a second time, late enough to land. Focus now waits a frame, so it happens once, deliberately, after the menu is painted.
0.1.0
Minor Changes
7ac2ace: First release: 29 headless React primitives with Radix-shaped anatomy, built on the top layer, invoker commands, anchor positioning and native form controls.
Top layer — Dialog, AlertDialog, Popover, Tooltip, HoverCard, DropdownMenu, ContextMenu, Menubar, NavigationMenu, Toast.
The native element — Checkbox, Switch, RadioGroup, Select, Slider, Progress, Separator, Label, AspectRatio, ScrollArea, Collapsible, Accordion.
JavaScript, and honest about it — Tabs, Toolbar, ToggleGroup, Toggle, Avatar, AccessibleIcon, VisuallyHidden.
Every primitive with open state has two roots.
@apostel/bedrockis the DOM opening and closing itself;@apostel/bedrock/controlledgives React a veto. They are separate module graphs, checked in CI.Closed content unmounts, which is what makes the uncontrolled roots uncontrolled: closing discards what the content was holding, so a form resets itself with nothing wired to
onOpenChange.102 Playwright specs against real Chrome, including Radix's own Dialog suite ported verbatim — 30 pass, none fail, 13 test machinery this library exists to delete.
0.1.0rather than1.0.0deliberately: several questions indocs/gaps.mdhave answers that would change public API, and semver before 1.0 leaves room for them.
Patch Changes
671ecd2: Fix dropped DOM events caused by ref callbacks changing identity every render.
composeRefsreturns a new function on each call, and React treats a new ref callback as a different ref — so every composed ref was detached and reattached on every render. For a ref that stores a node that is merely wasteful; for one that attaches listeners it is a bug. An event arriving between the detach and the attach is dropped and nothing re-delivers it, so a popover that opened in that window stayed open with its content never mounted: thetogglethat would have told React went nowhere.useComposedRefsis the same thing with a stable identity, and every part now uses it.composeRefsis unchanged and still exported for use outside a component.useInteresthad the same shape of bug in its own effect — no dependency array, so it rebuilt its listeners on every render, and opening a hover card is itself a render. Listeners are now attached when a node registers.Report an open that happened before anything was listening.
An invoker needs no JavaScript, so a click landing before hydration finishes opens the element for real.
useOpenStatealready recovered the mount state when its listener attached to an element that was already open, so the content appeared — but it never made the matchingonOpenChangecall, leaving anyone watching that callback believing it was still closed until the next close.The recovery now reports as well. An element that is open because
defaultOpenasked for it still reports nothing, because nobody opened it.
Entries are generated by
changesets from the files in
.changeset/, newest first, and inserted directly under the heading above. To
add one, run npm run changeset and describe the change for someone consuming
the package — not for someone reading the diff.