@gryt/ui-native 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../../src/components/Drawer/Drawer.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../../src/components/Drawer/Drawer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AACf,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAYtB,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG/E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAgBrD,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,iBAAS,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,SAAS,EAAE,EAAE,eAAe,+BAKxD;AAED,iBAAS,OAAO,CAAC,EACf,QAAQ,EACR,KAAK,EACN,EAAE;IACD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,+BAWA;AAED,iBAAS,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,+BAErD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAgBD,iBAAS,KAAK,CAAC,EACb,QAAQ,EACR,IAAa,EACb,IAAU,EACV,WAAkB,EAClB,KAAK,EACN,EAAE,gBAAgB,+BA0UlB;AAED,iBAAS,KAAK,CAAC,EACb,QAAQ,EACR,KAAK,EACN,EAAE;IACD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,+BAWA;AAED,eAAO,MAAM,MAAM;;;;;;CAA0C,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useEffect,
|
|
3
|
-
import { Dimensions,
|
|
2
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { Dimensions, Modal, Pressable } from "react-native";
|
|
4
|
+
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
4
5
|
import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from "react-native-reanimated";
|
|
5
6
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
6
7
|
import { useReducedMotion } from "../../hooks/useReducedMotion.js";
|
|
@@ -79,7 +80,11 @@ function Popup({ children, side = "left", size = 0.8, dismissible = true, style
|
|
|
79
80
|
const panelExtent = extent + bleed;
|
|
80
81
|
// Measured against the bleed panel, so the panel's *visible* edge lands
|
|
81
82
|
// exactly off-screen rather than the overhang doing it.
|
|
82
|
-
const hidden = side === "right"
|
|
83
|
+
const hidden = side === "right"
|
|
84
|
+
? panelExtent
|
|
85
|
+
: side === "left"
|
|
86
|
+
? -panelExtent
|
|
87
|
+
: panelExtent;
|
|
83
88
|
/**
|
|
84
89
|
* 700ms on the *tight* curve, and this changed on both platforms at once.
|
|
85
90
|
*
|
|
@@ -150,141 +155,172 @@ function Popup({ children, side = "left", size = 0.8, dismissible = true, style
|
|
|
150
155
|
};
|
|
151
156
|
});
|
|
152
157
|
/**
|
|
153
|
-
* The scrim
|
|
158
|
+
* The scrim fades with the panel, and thins further as it is dragged away.
|
|
154
159
|
*
|
|
155
|
-
*
|
|
156
|
-
* distinction from a dialog, and a scrim that fades up alongside the panel
|
|
157
|
-
* puts a second, slower animation on top of the one that matters. It is up
|
|
158
|
-
* when the drawer is up.
|
|
160
|
+
* Two separate rules, both the web's:
|
|
159
161
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
162
|
+
* ```
|
|
163
|
+
* "[opacity:calc(1-var(--drawer-swipe-progress,0))]",
|
|
164
|
+
* "transition-opacity duration-(--gryt-dur-spring-soft) ease-spring-tight",
|
|
165
|
+
* "data-starting-style:opacity-0 data-ending-style:opacity-0",
|
|
166
|
+
* ```
|
|
163
167
|
*
|
|
164
|
-
* The
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* the
|
|
168
|
+
* The first is the drag term. The other two are the fade: the backdrop
|
|
169
|
+
* starts and ends at zero, over the same duration and easing as the panel's
|
|
170
|
+
* slide. `progress` already runs on `travel`, which is `easeSpringTight` at
|
|
171
|
+
* `springSoft` — so multiplying by it is the same curve, not a new one.
|
|
172
|
+
*
|
|
173
|
+
* GRYT-408 took the fade out, on the grounds that "the web's Popup declares
|
|
174
|
+
* `transition-transform` and nothing else". That is true, and it is about
|
|
175
|
+
* `Drawer.Popup` — the panel, which still only translates. The backdrop is a
|
|
176
|
+
* different element and says the opposite. Without the fade the scrim was at
|
|
177
|
+
* full strength before the panel was on screen and stayed there while it slid
|
|
178
|
+
* away, then blinked off with the Modal.
|
|
170
179
|
*/
|
|
171
180
|
const scrimStyle = useAnimatedStyle(() => {
|
|
172
181
|
const dragged = extent > 0 ? Math.min(1, Math.abs(drag.value) / extent) : 0;
|
|
173
|
-
return { opacity: 1 - dragged };
|
|
182
|
+
return { opacity: progress.value * (1 - dragged) };
|
|
174
183
|
});
|
|
175
184
|
/**
|
|
176
185
|
* Swipe to dismiss, which the web has had from Base UI's drawer primitive
|
|
177
186
|
* and this did not have at all (GRYT-395). A drawer you cannot push back is
|
|
178
187
|
* the thing a drawer is for.
|
|
179
188
|
*
|
|
180
|
-
* `
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
189
|
+
* `react-native-gesture-handler` rather than `PanResponder`. That was the
|
|
190
|
+
* other way round because gesture callbacks from this package's prebuilt
|
|
191
|
+
* output were measured doing nothing (GRYT-393) — the babel plugin was not
|
|
192
|
+
* reaching `node_modules`, so nothing became a worklet. Measured again on
|
|
193
|
+
* 2026-08-21 with a probe built inside this package: both worklet and
|
|
194
|
+
* `runOnJS` callbacks report correctly. Whatever it was, it is fixed, and
|
|
195
|
+
* `PanResponder` was only ever the fallback.
|
|
196
|
+
*
|
|
197
|
+
* The reason to move is what the axis constraints below buy. `PanResponder`
|
|
198
|
+
* cannot negotiate with a native scroll recogniser, so the old version kept
|
|
199
|
+
* the drag with `onPanResponderTerminationRequest: () => false` — which
|
|
200
|
+
* works by never handing the gesture back, and stops a `ScrollView` inside
|
|
201
|
+
* the drawer scrolling at all. Here the pan simply fails on a cross-axis
|
|
202
|
+
* drag and the scrollable gets it, which is the same rule the web relies on
|
|
203
|
+
* the browser for.
|
|
204
|
+
*
|
|
205
|
+
* The callbacks stay worklets. Running them on the JS thread would be
|
|
206
|
+
* simpler and would give up the whole point: the panel tracks the finger on
|
|
207
|
+
* the UI thread, with nothing to be blocked by a busy bridge.
|
|
184
208
|
*/
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
gesture
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
},
|
|
205
|
-
// Once it is ours it stays ours, for the same reason the Slider needs
|
|
206
|
-
// this: the default is to grant, and a ScrollView inside the drawer
|
|
207
|
-
// would take the drag back mid-swipe.
|
|
208
|
-
onPanResponderTerminationRequest: () => false,
|
|
209
|
-
onPanResponderMove: (_e, g) => {
|
|
210
|
-
const { vertical: v, side: sd } = gesture.current;
|
|
211
|
-
const raw = v ? g.dy : g.dx;
|
|
209
|
+
const pan = useMemo(() => {
|
|
210
|
+
const closingSign = side === "left" ? -1 : 1;
|
|
211
|
+
const gesture = Gesture.Pan().enabled(dismissible);
|
|
212
|
+
/* Claim only a drag heading the way the panel closes, and only once it is
|
|
213
|
+
* clearly a drag rather than a slow press — 8pt, the same threshold the
|
|
214
|
+
* old responder used. `failOffset` on the other axis is the half that
|
|
215
|
+
* `PanResponder` had no answer for: a finger moving across the panel is
|
|
216
|
+
* somebody scrolling its contents, and this hands that over rather than
|
|
217
|
+
* swallowing it. */
|
|
218
|
+
if (vertical) {
|
|
219
|
+
gesture.activeOffsetY(8).failOffsetX([-12, 12]);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
gesture.activeOffsetX(closingSign * 8).failOffsetY([-12, 12]);
|
|
223
|
+
}
|
|
224
|
+
return gesture
|
|
225
|
+
.onUpdate((event) => {
|
|
226
|
+
"worklet";
|
|
227
|
+
const raw = vertical ? event.translationY : event.translationX;
|
|
212
228
|
// Clamped to the closing direction. Dragging a left drawer rightwards
|
|
213
229
|
// should do nothing, not tear it off its edge.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
drag.value = closingSign < 0 ? Math.min(0, raw) : Math.max(0, raw);
|
|
231
|
+
})
|
|
232
|
+
.onEnd((event) => {
|
|
233
|
+
"worklet";
|
|
234
|
+
const moved = Math.abs(vertical ? event.translationY : event.translationX);
|
|
235
|
+
const speed = Math.abs(vertical ? event.velocityY : event.velocityX);
|
|
236
|
+
/* Half the panel, or a flick. The velocity term is what makes a short
|
|
237
|
+
* sharp swipe work — without it you have to drag the whole way, which
|
|
238
|
+
* is the difference between a drawer and a slow puzzle.
|
|
239
|
+
*
|
|
240
|
+
* Gesture-handler reports velocity in points per second where
|
|
241
|
+
* `PanResponder` reported points per millisecond, so the old `0.5`
|
|
242
|
+
* became 500. Same gesture, different unit — worth stating, because
|
|
243
|
+
* carrying the number across unchanged would have made a flick need to
|
|
244
|
+
* be a thousand times faster and looked like the velocity term simply
|
|
245
|
+
* not working.
|
|
246
|
+
*/
|
|
247
|
+
if (moved > extent / 2 || speed > 500) {
|
|
248
|
+
// Left where it is: the close animation runs from here, and snapping
|
|
249
|
+
// it back first would show the panel returning before it left.
|
|
250
|
+
runOnJS(setOpen)(false);
|
|
230
251
|
return;
|
|
231
252
|
}
|
|
232
|
-
// eslint-disable-next-line react-hooks/immutability
|
|
233
253
|
drag.value = travelTo(0, { duration: durations.springSoft });
|
|
234
|
-
}
|
|
235
|
-
|
|
254
|
+
})
|
|
255
|
+
.onFinalize((_event, success) => {
|
|
256
|
+
"worklet";
|
|
257
|
+
// A gesture cancelled by the system — an incoming call, a parent
|
|
258
|
+
// taking over — leaves the panel wherever the finger was.
|
|
259
|
+
if (!success)
|
|
260
|
+
drag.value = travelTo(0, { duration: durations.springSoft });
|
|
261
|
+
});
|
|
262
|
+
}, [dismissible, drag, extent, setOpen, side, vertical]);
|
|
263
|
+
/**
|
|
264
|
+
* Any leftover drag goes back as the panel opens, and is cleared once it is
|
|
265
|
+
* gone.
|
|
266
|
+
*
|
|
267
|
+
* Two moments, deliberately. Clearing on `open` going false was wrong: that
|
|
268
|
+
* happens the instant a swipe dismisses, so the panel jumped back to fully
|
|
269
|
+
* open and then slid out — exactly what the release handler above says must
|
|
270
|
+
* not happen (GRYT-429). `mounted` is the moment the panel is actually off
|
|
271
|
+
* screen, where a hard reset costs nothing to look at.
|
|
272
|
+
*/
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (open) {
|
|
236
275
|
// eslint-disable-next-line react-hooks/immutability
|
|
237
276
|
drag.value = travelTo(0, { duration: durations.springSoft });
|
|
238
277
|
}
|
|
239
|
-
})
|
|
240
|
-
/* eslint-enable react-hooks/refs */
|
|
241
|
-
// Cleared once the panel is closed, so the next open does not start from
|
|
242
|
-
// wherever the last swipe left it.
|
|
278
|
+
}, [open, drag]);
|
|
243
279
|
useEffect(() => {
|
|
244
|
-
if (!
|
|
280
|
+
if (!mounted) {
|
|
245
281
|
// eslint-disable-next-line react-hooks/immutability
|
|
246
282
|
drag.value = 0;
|
|
247
283
|
}
|
|
248
|
-
}, [
|
|
249
|
-
return (_jsx(Modal, { visible: mounted, transparent: true, animationType: "none", onRequestClose: dismissible ? () => setOpen(false) : undefined, children: _jsx(AnimatedPressable, { onPress: dismissible ? () => setOpen(false) : undefined, style: [{ flex: 1, backgroundColor: "rgba(0,0,0,0.6)" }, scrimStyle], children: _jsx(Animated.View, { accessibilityViewIsModal: true,
|
|
250
|
-
{
|
|
251
|
-
position: "absolute",
|
|
252
|
-
// The overhang hangs *off* the edge the panel comes from, which
|
|
253
|
-
// is the only direction that helps. It used to be added to the
|
|
254
|
-
// width instead, so the panel grew inwards — 64pt more of the
|
|
255
|
-
// screen covered, and the seam it was meant to hide still there,
|
|
256
|
-
// because the gap opens on the entering edge and that is the edge
|
|
257
|
-
// the extra material was not on.
|
|
258
|
-
top: side === "bottom" ? undefined : 0,
|
|
259
|
-
bottom: vertical ? -bleed : 0,
|
|
260
|
-
left: side === "right" ? undefined : side === "left" ? -bleed : 0,
|
|
261
|
-
right: side === "left" ? undefined : side === "right" ? -bleed : 0,
|
|
262
|
-
width: vertical ? "100%" : panelExtent,
|
|
263
|
-
height: vertical ? panelExtent : "100%",
|
|
264
|
-
// Puts the content back where it would have been without the
|
|
265
|
-
// overhang, so `size` still means what it says.
|
|
266
|
-
paddingLeft: side === "left" ? bleed : 0,
|
|
267
|
-
paddingRight: side === "right" ? bleed : 0,
|
|
268
|
-
paddingBottom: vertical ? bleed : 0,
|
|
269
|
-
backgroundColor: theme.color.surface,
|
|
270
|
-
borderColor: theme.color.border,
|
|
271
|
-
borderRightWidth: side === "left" ? 1 : 0,
|
|
272
|
-
borderLeftWidth: side === "right" ? 1 : 0,
|
|
273
|
-
borderTopWidth: vertical ? 1 : 0,
|
|
274
|
-
borderTopLeftRadius: vertical ? theme.radius.lg : 0,
|
|
275
|
-
borderTopRightRadius: vertical ? theme.radius.lg : 0
|
|
276
|
-
},
|
|
277
|
-
panelStyle
|
|
278
|
-
], children: _jsx(Pressable, { onPress: () => { }, style: [
|
|
284
|
+
}, [mounted, drag]);
|
|
285
|
+
return (_jsx(Modal, { visible: mounted, transparent: true, animationType: "none", onRequestClose: dismissible ? () => setOpen(false) : undefined, children: _jsx(AnimatedPressable, { onPress: dismissible ? () => setOpen(false) : undefined, style: [{ flex: 1, backgroundColor: "rgba(0,0,0,0.6)" }, scrimStyle], children: _jsx(GestureDetector, { gesture: pan, children: _jsx(Animated.View, { accessibilityViewIsModal: true, style: [
|
|
279
286
|
{
|
|
280
|
-
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
287
|
+
position: "absolute",
|
|
288
|
+
// The overhang hangs *off* the edge the panel comes from, which
|
|
289
|
+
// is the only direction that helps. It used to be added to the
|
|
290
|
+
// width instead, so the panel grew inwards — 64pt more of the
|
|
291
|
+
// screen covered, and the seam it was meant to hide still there,
|
|
292
|
+
// because the gap opens on the entering edge and that is the edge
|
|
293
|
+
// the extra material was not on.
|
|
294
|
+
top: side === "bottom" ? undefined : 0,
|
|
295
|
+
bottom: vertical ? -bleed : 0,
|
|
296
|
+
left: side === "right" ? undefined : side === "left" ? -bleed : 0,
|
|
297
|
+
right: side === "left" ? undefined : side === "right" ? -bleed : 0,
|
|
298
|
+
width: vertical ? "100%" : panelExtent,
|
|
299
|
+
height: vertical ? panelExtent : "100%",
|
|
300
|
+
// Puts the content back where it would have been without the
|
|
301
|
+
// overhang, so `size` still means what it says.
|
|
302
|
+
paddingLeft: side === "left" ? bleed : 0,
|
|
303
|
+
paddingRight: side === "right" ? bleed : 0,
|
|
304
|
+
paddingBottom: vertical ? bleed : 0,
|
|
305
|
+
backgroundColor: theme.color.surface,
|
|
306
|
+
borderColor: theme.color.border,
|
|
307
|
+
borderRightWidth: side === "left" ? 1 : 0,
|
|
308
|
+
borderLeftWidth: side === "right" ? 1 : 0,
|
|
309
|
+
borderTopWidth: vertical ? 1 : 0,
|
|
310
|
+
borderTopLeftRadius: vertical ? theme.radius.lg : 0,
|
|
311
|
+
borderTopRightRadius: vertical ? theme.radius.lg : 0
|
|
285
312
|
},
|
|
286
|
-
|
|
287
|
-
], children:
|
|
313
|
+
panelStyle
|
|
314
|
+
], children: _jsx(Pressable, { onPress: () => { }, style: [
|
|
315
|
+
{
|
|
316
|
+
flex: 1,
|
|
317
|
+
// Before the caller's style, so a drawer that wants to run
|
|
318
|
+
// under the island — a full-bleed image, say — still can.
|
|
319
|
+
paddingTop: vertical ? 0 : insets.top,
|
|
320
|
+
paddingBottom: insets.bottom
|
|
321
|
+
},
|
|
322
|
+
style
|
|
323
|
+
], children: children }) }) }) }) }));
|
|
288
324
|
}
|
|
289
325
|
function Close({ children, style }) {
|
|
290
326
|
const { setOpen } = useDrawer("Close");
|