Skip to content

Releases: limonify/zest-ui

v0.12.6

Choose a tag to compare

@balcieren balcieren released this 05 Nov 00:59
5ba1622

Fixed

  • A Combobox popup no longer opens underneath the keyboard. The positioner only knew the size of the window, so a popup opened from low on the screen sat behind the on-screen keyboard, along with the input inside it. Combobox.Positioner and Autocomplete.Positioner now raise the bottom of the collision boundary by the keyboard's height, so the popup moves above its anchor.
  • autoFocus opens the keyboard inside a Modal on Android. A Combobox.Input in the popup got the caret but no keyboard, because the Modal's window does not have input focus yet when it appears, and the user had to tap the input. Combobox.Portal now applies autoFocus itself, shortly after the Modal has shown. modalProps.onShow is chained rather than replaced.
  • A query no longer shows on Combobox.Trigger. Combobox.Value rendered the input text, so what the user typed into the popup replaced the selection on the trigger, and stayed there if the list was dismissed. As in Base UI, a single-selection combobox with a trigger now shows the selected label, and its input is cleared whenever the list opens or closes, so every open starts blank. In that shape onInputValueChange no longer reports the chosen label, and clearing a query reports the input-clear reason. A combobox without a trigger, a multiple one and Autocomplete are unchanged, and state.value is still the input text.

Added

  • A development warning for an input the user cannot type into. A Combobox.Input outside Combobox.Portal loses focus when the list opens, because a Modal is a separate window. The portal now says so on iOS and Android, and names the two ways out: a Combobox.Trigger with the input in the popup, or modal={false}.

Documentation

  • "Keeping the input focused" on the Combobox page, with a warning under the first example, whose shape has this problem. It also covers keyboardShouldPersistTaps="handled", which every ScrollView above a combobox needs or the first tap on a row only dismisses the keyboard, and autoCorrect={false}, since an iOS autocorrect suggestion takes that tap as well.

v0.12.5

Choose a tag to compare

@balcieren balcieren released this 21 Oct 22:51

Fixed

  • A cached avatar hides its fallback on iOS. iOS delivers a cached image's onLoad before its onLoadStart, and Avatar.Image mapped every load start to 'loading', so the status went loaded → loading and stayed there: Avatar.Fallback never hid, and the initials showed beside the picture. A load that has settled (loaded or errored) now stays settled until the image's source changes, compared by what it points at (a uri, an asset number), so an equal source on a new render is the same load. Android, which keeps the order, is unchanged. (#10)

v0.12.4

Choose a tag to compare

@balcieren balcieren released this 17 Oct 23:41

[0.12.4] - 2026-10-18

Fixed

  • A swipe that dismisses a toast keeps the distance it reached. Toast.Root reset
    state.swipeMovement to 0 as the finger lifted, right after the same swipe had closed the toast,
    so an exit animation reading it started from the resting place: a thrown toast snapped back to the
    middle before it left. The distance is now kept on a toast a swipe closed, so the exit can carry on
    the way it was thrown, as the web's does with --toast-swipe-movement-*. A swipe released short of
    the threshold still springs back to 0.

v0.12.3

Choose a tag to compare

@balcieren balcieren released this 17 Oct 19:42

[0.12.3] - 2026-10-17

Fixed

  • Types hold on React Native 0.88. RN 0.88 made View and TextInput function components, so
    TextInput used as a type names the component, not its instance. The public
    Combobox store's inputRef and useFieldControl's inputRef were typed
    RefObject<TextInput | null>, and a consumer calling .focus() / .blur() on them failed to
    type-check. They are now typed from the component's own ref prop (React.ComponentRef). That
    resolves to the class instance on older RN, so the react-native >=0.79 peer range is unchanged.
    The source type-checks against RN 0.79, 0.83, 0.86 and 0.88. The handler event types RN 0.88 renamed
    (TextInputFocusEventData, TextInputKeyPressEventData, ImageErrorEventData) and the
    narrowed Modal onRequestClose event are derived from the props in the same way.

  • preventZestUIHandler works in a menu item's onPress. Menu.Item, Menu.LinkItem,
    Menu.CheckboxItem and Menu.RadioItem call the consumer's onPress themselves rather than
    through mergeProps, so the event never got the method its type promised, and calling it threw.
    The event is now preventable, and preventing it skips the item's own close, as mergeProps skips
    Zest's handler everywhere else.

v0.12.2

Choose a tag to compare

@balcieren balcieren released this 16 Oct 02:47

[0.12.2] - 2026-10-16

Fixed

  • Popups lay themselves out in the provided direction. A Modal is a new layout root, and a
    new root takes its direction from I18nManager rather than from the tree that opened it. Under a
    DirectionProvider that disagreed with the device, every portalled popup — Dialog,
    AlertDialog, Drawer, Popover, Menu, Select, Combobox, Tooltip — kept the device's
    direction: rows, padding and text alignment stayed LTR in an RTL app while the parts that read the
    context flipped. The portal's gesture root now carries useDirection() as its direction style.

v0.12.1

Choose a tag to compare

@balcieren balcieren released this 15 Oct 23:40

[0.12.1] - 2026-10-16

Fixed

  • A popup with no room on either side of its axis drops below (or above) its anchor instead of
    running off the screen.
    The clearest case is a Menu.SubmenuRoot on a phone: the submenu opens
    inline-end of a row in a menu that already spans most of a 393pt screen, so neither the right nor
    the left has room, and it stayed on the side it overflowed least — half of it past the edge, since
    shift only moves a popup along its anchor. flip now takes fallbackAxisSideDirection: 'end', as
    Base UI's collisionAvoidance default does, so floating-ui tries the perpendicular sides too.

  • Cross-axis overflow no longer flips a popup. flip ran with floating-ui's default
    crossAxis: true, under which a popover that was only too wide for the screen counted as
    overflowing its side and could be moved to the opposite one. It is crossAxis: 'alignment' now,
    as in Base UI: a popup leaves its side only when that side has no room along the main axis, and
    overflow across the axis is shift's to correct.

v0.12.0

Choose a tag to compare

@balcieren balcieren released this 25 Sep 02:12

[0.12.0] - 2026-09-25

Fixed

  • Opening a Tooltip no longer crashes on Android. Tooltip.Popup set both role and
    accessibilityRole to 'tooltip', and 'tooltip' is not an accessibilityRole Android's
    accessibility delegate knows, so the app went down the moment a tooltip opened. The popup now sets
    role alone, which React Native maps onto each platform itself; the tooltip semantics are
    unchanged on iOS.

  • CheckboxGroup inside a Field validates the array it submits. Every Checkbox.Root in the
    group registered its own boolean with the surrounding Field, so a field's validate saw
    true/false per box instead of the group's value, and a Form submit validated the field once
    per checkbox. The group now registers with the field and reports the array, marking it changed and
    touched on every press, and it inherits the field's disabled. A checkbox inside a group leaves
    the field alone; a standalone Checkbox is unaffected.

    <Field.Root name="colors" validate={(value) => (value.length >= 2 ? null : 'Pick two')}>
      <CheckboxGroup>…</CheckboxGroup>  {/* validate receives ['red', 'blue'] */}
      <Field.Error />
    </Field.Root>

Changed

  • A read-only Select opens for browsing. readOnly now locks the value, not the interaction,
    as Base UI 1.8 does: pressing Select.Trigger opens the list so the options and the current choice
    can be seen, and pressing a Select.Item does nothing. Until now the trigger ignored the press, so
    a read-only select showed its value and nothing else, while the same prop on the web opened.
    aria-readonly still marks the trigger. If a read-only field should not open at all, that is
    disabled.

v0.11.0

Choose a tag to compare

@balcieren balcieren released this 15 Sep 16:34

[0.11.0] - 2026-09-15

0.10.0 moved every gesture to gesture-handler's hook API and raised the peer to >=3.1.0. That put
this package outside every stock Expo project, and this release undoes it. The peer is >=2.32.0
again and the gestures are back on the Gesture.*() builder — which, it turns out, works on both
lines, so nothing has to be given up to get there.

This release is breaking, which is why it is a minor bump. The break is narrow: only
simultaneousGesture changes shape, back to what it was in 0.9.x.

Fixed

  • zest installs and runs in a stock Expo project again. Expo SDK 57 pins
    react-native-gesture-handler to ~2.32.0 in its bundledNativeModules.json, and the hook API
    does not exist in 2.x, so 0.10.0's >=3.1.0 peer was disjoint from the version Expo hands you.
    Three things followed from that, all of them now gone: npm install failed outright with
    ERESOLVE for anyone who had installed gesture-handler the way the docs say to; expo-doctor
    reported the project off-SDK on every run and in every EAS build log; and Expo Go could not
    load the app at all
    , because its binary embeds native 2.32 and installing JS 3.3 against it
    crashes at gesture attach rather than at build. Trying zest now needs no development build.

  • The simultaneousGesture "is it actually attached?" tests assert something again. Under the
    hook API a gesture registers with gesture-handler's jest registry when it is created, so
    getByGestureTestId found gestures that had never reached a GestureDetector and the check
    passed with the composition deleted — 0.10.0 removed the Slider's as worthless rather than
    fixing it. The builder registers at attach time, so the assertion is real, and both
    Slider.Control and Drawer.Popup now have one.

Breaking

  • react-native-gesture-handler is a >=2.32.0 peer, down from >=3.1.0. No one needs to
    change a version to take this: 2.32 satisfies it, and so does 3.x.

  • simultaneousGesture takes a builder gesture again, on both Slider.Control and
    Drawer.Popup.
    The prop is GestureType, not SingleGesture | ComposedGesture:

    - const gesture = usePanGesture({ onUpdate: … })
    + const gesture = Gesture.Pan().onUpdate(…)
    
      <Slider.Control simultaneousGesture={gesture} />

    This only affects code written against 0.10.0, which was current for one day. Gesture.Pan() is
    available on 2.x and 3.x alike, so the snippet above compiles on whichever line you are on, and
    it no longer has to be built inside a component.

Changed

  • The five gesture parts are back on the builder. Slider.Control, Drawer.Popup,
    Drawer.SwipeArea, Toast.Root and NumberField.ScrubArea build their pan with
    Gesture.Pan() and compose with Gesture.Simultaneous. Behaviour is unchanged, and
    Drawer.Popup's simultaneousGesture — added in 0.10.0 — is kept, just expressed in builder
    form.

    The builder is deprecated in 3.x but not removed, and it emits no runtime warning: 3.x still
    exports Gesture.Pan() and Gesture.Simultaneous(), and its GestureDetector branches to the
    legacy detector when it is handed one (props.gesture instanceof BaseGesture). One code path
    therefore covers 2.32 and 3.3, which is what makes the wide peer range honest rather than
    optimistic. The one thing it costs is that a single GestureDetector is locked to one gesture
    family, so a consumer's simultaneousGesture has to be a builder gesture too — available to
    them on either line.

    This is a deliberate hold, not an oversight: the hooks come back when Expo ships an SDK whose
    pinned gesture-handler is 3.x, and doing it before that buys nothing but costs everyone Expo Go.

  • The example app runs in Expo Go again, and the docs site is back on gesture-handler 2.x with
    it. Both pin ~2.32.0, the version SDK 57 bundles, so npx expo start is enough — no
    npx expo run:ios.

  • The README documents the gesture-handler peer. It listed only React and React Native, and
    told Expo users to match their SDK's version, which 0.10.0 had quietly made impossible.

v0.10.0

Choose a tag to compare

@balcieren balcieren released this 14 Sep 00:09

[0.10.0]

gesture-handler deprecated the Gesture.*() builder in v3 in favour of a hook API, and every
gesture in this package has moved to it. Drawer.Popup gains the UI-thread escape hatch
Slider.Control already had, which is the one place the JS-thread round trip is most visible.

This release is breaking, which is why it is a minor bump rather than a patch. Both breaks
follow from the same fact: the builder and the hooks produce different gesture shapes, and
gesture-handler cannot compose one with the other. There is therefore no version of this package
that can support both.

Added

  • Drawer.Popup takes a simultaneousGesture, so a consumer can move the sheet on the UI
    thread.
    The same arrangement Slider.Control has had since 0.9.0, and it matters more here:
    the popup's own handlers touch React state, so they run on JS (runOnJS: true) and the sheet's
    position reaches the consumer as state.swipeMovement — a render sits between the finger and the
    sheet, once per frame of a drag. A thumb travels twenty points and a sheet travels half a screen,
    so the same lag is far more visible on a drawer. zest neither animates nor takes an animation
    dependency, so instead it now runs a gesture of the consumer's own alongside its own: both see the
    same touch, state.swipeMovement and dismissal at swipeThreshold are unchanged, and the sheet
    can follow the finger from a shared value with no render in the path. Nothing changes for anyone
    who does not pass the prop.

Breaking

  • react-native-gesture-handler is now a >=3.1.0 peer, up from >=2.32.0. The hooks do not
    exist in 2.x. An app still on 2.x stays on 0.9.x.

  • Slider.Control's simultaneousGesture takes a hook gesture, not a builder one. It was typed
    as the deprecated GestureType and composed with Gesture.Simultaneous, which meant a consumer
    passing it was pinned to a deprecated API with no way out — a hook gesture did not fit the slot,
    and casting one in would have handed Gesture.Simultaneous an object it calls builder methods on.
    The prop is now SingleGesture | ComposedGesture:

    - const gesture = Gesture.Pan().onUpdate(…)
    + const gesture = usePanGesture({ onUpdate: … })
    
      <Slider.Control simultaneousGesture={gesture} />

    Note the gesture must now be built inside a component, because usePanGesture is a hook.

Changed

  • The five parts that use gesture-handler are on the hook API. Slider.Control,
    Drawer.Popup, Drawer.SwipeArea, Toast.Root and NumberField.ScrubArea all build their pan
    with usePanGesture({ … }). Behaviour is unchanged: the same testID reaches gesture-handler's
    registry, so existing tests that find a gesture through it keep working, and the handlers still
    run on the JS thread (runOnJS: true) because they touch React state. The callback renames are
    gesture-handler's: .onStart is onActivate and .onEnd is onDeactivate.

  • The example app needs a development build. Expo Go ships gesture-handler 2.x, so it can no
    longer load the example; npx expo run:ios replaces expo start for it. The docs site moved to
    3.x for the same reason — its Next build resolves this package's source directly and failed on
    Export usePanGesture doesn't exist in target module.

v0.9.2

Choose a tag to compare

@balcieren balcieren released this 28 Aug 03:25
b8b1149

[0.9.2]

Added

  • The combobox root's four contexts are exported, so a consumer can re-provide them around a
    part rendered outside the root's own tree.
    ComboboxRootContext, ComboboxItemsContext,
    ComboboxSelectionContext and ComboboxTransitionContext, together with their
    use…Context hooks, are now named exports. They exist for the same reason as
    Combobox.Portal modal={false}: hoisting a list into a root-level overlay moves it above the
    <Combobox.Root> that supplies these contexts, so the consumer re-provides them around the
    hoisted part.