panelui-native 0.59.0 → 0.60.0
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.
- package/lib/module/components/fab/index.js +63 -30
- package/lib/module/components/fab/index.js.map +1 -1
- package/lib/module/components/selection-mode/index.js +805 -0
- package/lib/module/components/selection-mode/index.js.map +1 -0
- package/lib/module/components/tabs/index.js +384 -341
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/fab/index.d.ts +11 -5
- package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
- package/lib/typescript/src/components/selection-mode/index.d.ts +498 -0
- package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tabs/index.d.ts +64 -46
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/fab/index.tsx +66 -31
- package/src/components/selection-mode/index.tsx +1027 -0
- package/src/components/tabs/index.tsx +452 -413
- package/src/index.ts +13 -0
|
@@ -18,17 +18,25 @@
|
|
|
18
18
|
* </Tabs>
|
|
19
19
|
* ```
|
|
20
20
|
*
|
|
21
|
-
* **Swiping
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
21
|
+
* **Swiping puts the panels in a row.** With `swipeable`, the panels are laid
|
|
22
|
+
* out side by side in a strip as wide as all of them, inside a viewport that
|
|
23
|
+
* shows one at a time, and moving between tabs is that strip translating. The
|
|
24
|
+
* neighbours are therefore already built and already the right size before the
|
|
25
|
+
* finger arrives at them, which is the whole point: a panel that has to be
|
|
26
|
+
* mounted and measured at the moment it becomes visible is a panel that stalls
|
|
27
|
+
* there, and it stalls for exactly as long as it takes to build.
|
|
28
|
+
*
|
|
29
|
+
* One shared value carries the strip's position, in panels rather than points,
|
|
30
|
+
* and it is the only thing that decides where the strip is. A press springs it,
|
|
31
|
+
* a drag sets it, and neither waits for React: the value the tab set reports is
|
|
32
|
+
* updated alongside the movement, not ahead of it.
|
|
33
|
+
*
|
|
34
|
+
* A swipeable tab set therefore needs a height to fill, the same as any pager.
|
|
35
|
+
* Give it one — `flex-1` on the tab set, or a fixed height — or the strip has
|
|
36
|
+
* nothing to lay its panels out in.
|
|
29
37
|
*/
|
|
30
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
31
|
-
import {
|
|
38
|
+
import { Children, createContext, Fragment, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
39
|
+
import { Pressable, ScrollView, View } from 'react-native';
|
|
32
40
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
33
41
|
import Animated, { Easing, Extrapolation, FadeIn, interpolate, runOnJS, useAnimatedStyle, useReducedMotion, useSharedValue, withSpring, withTiming } from 'react-native-reanimated';
|
|
34
42
|
import { tv } from 'tailwind-variants';
|
|
@@ -51,50 +59,19 @@ const SPRING = {
|
|
|
51
59
|
const SWIPE_ACTIVATE_X = 12;
|
|
52
60
|
const SWIPE_FAIL_Y = 8;
|
|
53
61
|
|
|
54
|
-
/** A swipe past this share of
|
|
62
|
+
/** A swipe past this share of a panel's width changes tab on release. */
|
|
55
63
|
const SWIPE_DISTANCE_RATIO = 0.25;
|
|
56
|
-
/** …or past this speed, however short it was. */
|
|
64
|
+
/** …or past this speed, however short it was, in points per second. */
|
|
57
65
|
const SWIPE_VELOCITY = 500;
|
|
58
66
|
|
|
59
67
|
/**
|
|
60
|
-
* How much of the finger's travel the
|
|
61
|
-
* where there is
|
|
68
|
+
* How much of the finger's travel the strip follows at the ends of the row,
|
|
69
|
+
* where there is no further panel to bring on. Everywhere else it follows all
|
|
70
|
+
* of it.
|
|
62
71
|
*/
|
|
63
72
|
const SWIPE_RESISTANCE_AT_END = 0.16;
|
|
64
73
|
|
|
65
|
-
/**
|
|
66
|
-
* How far the panel fades as it is dragged, at a full panel's width.
|
|
67
|
-
*
|
|
68
|
-
* Tracking the finger one to one leaves the space behind the panel empty,
|
|
69
|
-
* because the panels are separate views and only the visible one is mounted.
|
|
70
|
-
* Dimming as it goes is what keeps that from reading as a hole punched in the
|
|
71
|
-
* card: a panel on its way out is on its way out, and the one arriving comes
|
|
72
|
-
* back up through the same fade. The two together read as a dissolve carried
|
|
73
|
-
* by the movement, rather than as a gap between two slides.
|
|
74
|
-
*
|
|
75
|
-
* **iOS only, and that is not a compromise.** A transform is a property of the
|
|
76
|
-
* layer the panel is drawn into, so moving it costs nothing per frame however
|
|
77
|
-
* much is inside it. An opacity that changes every frame is not: on Android the
|
|
78
|
-
* panel's view group has no offscreen buffer to fade, so the alpha is pushed
|
|
79
|
-
* down into its children and the whole visible subtree is re-drawn on every
|
|
80
|
-
* frame of the drag. Behind a list that is thirty rows of text and images,
|
|
81
|
-
* which is what a tab panel usually is, that is the difference between a swipe
|
|
82
|
-
* that tracks the finger and one that stutters. iOS fades the layer itself and
|
|
83
|
-
* is unaffected.
|
|
84
|
-
*/
|
|
85
|
-
const SWIPE_FADE = Platform.OS === 'ios' ? 0.75 : 0;
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* How far off its resting place a panel arriving by a *press* starts, as a
|
|
89
|
-
* share of the panel's width.
|
|
90
|
-
*
|
|
91
|
-
* A press has no finger travel to continue from, so it is given a throw of its
|
|
92
|
-
* own. A swipe does not use this: the panel it hands over to picks up exactly
|
|
93
|
-
* one width from wherever the outgoing one has travelled to — see `setValue`.
|
|
94
|
-
*/
|
|
95
|
-
const SWIPE_ENTER = 0.3;
|
|
96
|
-
|
|
97
|
-
/** The arriving panel is springier than the one being dragged back into place. */
|
|
74
|
+
/** The spring the strip settles on, whether it was thrown or pressed. */
|
|
98
75
|
const ENTER_SPRING = {
|
|
99
76
|
damping: 22,
|
|
100
77
|
stiffness: 240,
|
|
@@ -123,10 +100,21 @@ const MEASURE_WIDTH = 400;
|
|
|
123
100
|
/**
|
|
124
101
|
* How much of an inactive panel survives a switch away from it.
|
|
125
102
|
*
|
|
126
|
-
* `false` unmounts it. `true` keeps it mounted
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
103
|
+
* `false` unmounts it. `true` keeps it mounted.
|
|
104
|
+
*
|
|
105
|
+
* `'measured'` meant "keep it mounted *and* laid out at a real size", which was
|
|
106
|
+
* a distinction only a tab set of separately hidden panels had to make. In a
|
|
107
|
+
* swipeable tab set every panel in the strip is laid out at a real size
|
|
108
|
+
* already, so it is the same as `true` there and is kept only so that passing
|
|
109
|
+
* it does not break.
|
|
110
|
+
*
|
|
111
|
+
* @see TabsProps.keepMounted
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* `'disable-all'` turns off every animation in the tab set — the indicator, the
|
|
116
|
+
* strip, and an expanding tab's reveal — including the ones its parts run
|
|
117
|
+
* themselves.
|
|
130
118
|
*/
|
|
131
119
|
|
|
132
120
|
const tabsVariants = tv({
|
|
@@ -225,13 +213,6 @@ const tabsVariants = tv({
|
|
|
225
213
|
scrollable: false
|
|
226
214
|
}
|
|
227
215
|
});
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* What a swipe hands over to the tab it commits to: where the finger left the
|
|
231
|
-
* outgoing panel, and how fast it was still going. Absent when the tab was
|
|
232
|
-
* changed by pressing a trigger, which has neither.
|
|
233
|
-
*/
|
|
234
|
-
|
|
235
216
|
const TabsContext = /*#__PURE__*/createContext(null);
|
|
236
217
|
function useTabs(component) {
|
|
237
218
|
const context = useContext(TabsContext);
|
|
@@ -240,6 +221,45 @@ function useTabs(component) {
|
|
|
240
221
|
}
|
|
241
222
|
return context;
|
|
242
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Pulls the panels out of the children, keeping everything else where it was.
|
|
226
|
+
*
|
|
227
|
+
* A pager has to lay its panels out together, and they are written wherever
|
|
228
|
+
* they read best — usually after the list, sometimes inside a fragment from a
|
|
229
|
+
* `map`. So they are found rather than required to be somewhere: fragments are
|
|
230
|
+
* flattened through, `Tabs.Content` elements are collected in the order they
|
|
231
|
+
* appear, and every other child is left exactly where it was written.
|
|
232
|
+
*
|
|
233
|
+
* A panel that is *not* reachable this way — wrapped in a component of your own
|
|
234
|
+
* — is not found, and the tab set falls back to showing one panel at a time.
|
|
235
|
+
* Silently losing it would be worse than not paging it.
|
|
236
|
+
*/
|
|
237
|
+
function collectPanels(children) {
|
|
238
|
+
const rest = [];
|
|
239
|
+
const panels = [];
|
|
240
|
+
const walk = nodes => {
|
|
241
|
+
Children.forEach(nodes, child => {
|
|
242
|
+
if (! /*#__PURE__*/isValidElement(child)) {
|
|
243
|
+
if (child !== null && child !== undefined && child !== false) rest.push(child);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (child.type === Fragment) {
|
|
247
|
+
walk(child.props.children);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (child.type === TabsContent) {
|
|
251
|
+
panels.push(child);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
rest.push(child);
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
walk(children);
|
|
258
|
+
return {
|
|
259
|
+
rest,
|
|
260
|
+
panels
|
|
261
|
+
};
|
|
262
|
+
}
|
|
243
263
|
function TabsRoot({
|
|
244
264
|
className,
|
|
245
265
|
value,
|
|
@@ -248,79 +268,22 @@ function TabsRoot({
|
|
|
248
268
|
variant = 'segmented',
|
|
249
269
|
keepMounted = false,
|
|
250
270
|
swipeable = false,
|
|
271
|
+
animation,
|
|
251
272
|
children,
|
|
252
273
|
...props
|
|
253
274
|
}) {
|
|
254
275
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
255
276
|
const [layouts, setLayouts] = useState({});
|
|
256
|
-
const [tabs, setTabs] = useState([]);
|
|
257
|
-
const swipeOffset = useSharedValue(0);
|
|
258
|
-
const panelWidth = useSharedValue(0);
|
|
259
|
-
const sign = useDirectionSign();
|
|
260
277
|
// Published by the List rather than the root, because it is the List that
|
|
261
278
|
// decides whether it scrolls — but the Triggers below it need to know.
|
|
262
279
|
const [scrollable, setScrollable] = useState(false);
|
|
263
280
|
const isControlled = value !== undefined;
|
|
264
281
|
const resolvedValue = isControlled ? value : internalValue;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
* The two things `setValue` reads that change on every switch, held where
|
|
268
|
-
* reading them does not make it a new function.
|
|
269
|
-
*
|
|
270
|
-
* With them in the dependency list, changing tab produced a new `setValue`,
|
|
271
|
-
* therefore a new context, therefore a new `step`, therefore a new pan
|
|
272
|
-
* gesture — which detaches and re-attaches the native recogniser on the same
|
|
273
|
-
* commit that mounts the arriving panel's contents. The busiest frame of the
|
|
274
|
-
* interaction was doing the one piece of work that had no reason to be there.
|
|
275
|
-
*/
|
|
276
|
-
const tabsRef = useRef(tabs);
|
|
277
|
-
tabsRef.current = tabs;
|
|
278
|
-
const valueRef = useRef(resolvedValue);
|
|
279
|
-
valueRef.current = resolvedValue;
|
|
280
|
-
const setValue = useCallback((next, handover) => {
|
|
281
|
-
const tabs = tabsRef.current;
|
|
282
|
-
const resolvedValue = valueRef.current;
|
|
283
|
-
|
|
284
|
-
/*
|
|
285
|
-
* The arriving panel starts on the side it is arriving from, and travels
|
|
286
|
-
* in. Done here rather than in the panel because only the root knows
|
|
287
|
-
* both the tab being left and the tab being gone to — a panel knows
|
|
288
|
-
* which one it is, not which one it replaced — and because a tab changed
|
|
289
|
-
* by pressing a trigger deserves the same movement as one changed by
|
|
290
|
-
* swiping to it. Without it the two read as different features.
|
|
291
|
-
*/
|
|
292
|
-
if (swipeable) {
|
|
293
|
-
const from = tabs.indexOf(resolvedValue);
|
|
294
|
-
const to = tabs.indexOf(next);
|
|
295
|
-
if (from !== -1 && to !== -1 && from !== to) {
|
|
296
|
-
const direction = to > from ? 1 : -1;
|
|
297
|
-
if (handover) {
|
|
298
|
-
/*
|
|
299
|
-
* A swipe hands over mid-movement, so the arriving panel starts a
|
|
300
|
-
* whole panel's width from wherever the outgoing one has got to —
|
|
301
|
-
* which is where it *would* have been all along, had both been
|
|
302
|
-
* mounted. Read live rather than captured, because the outgoing
|
|
303
|
-
* panel is still travelling while this runs: adding to the current
|
|
304
|
-
* displacement rather than replacing it is the whole difference
|
|
305
|
-
* between one continuous movement and a jump at the moment the
|
|
306
|
-
* arriving panel finishes mounting.
|
|
307
|
-
*/
|
|
308
|
-
swipeOffset.value += direction * sign * panelWidth.value;
|
|
309
|
-
swipeOffset.value = withSpring(0, {
|
|
310
|
-
...ENTER_SPRING,
|
|
311
|
-
// Carried across too, so a flick keeps its speed instead of
|
|
312
|
-
// stopping dead and starting again from rest.
|
|
313
|
-
velocity: handover.velocity
|
|
314
|
-
});
|
|
315
|
-
} else {
|
|
316
|
-
swipeOffset.value = direction * sign * panelWidth.value * SWIPE_ENTER;
|
|
317
|
-
swipeOffset.value = withSpring(0, ENTER_SPRING);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
282
|
+
const animationDisabled = animation === 'disable-all';
|
|
283
|
+
const setValue = useCallback(next => {
|
|
321
284
|
if (!isControlled) setInternalValue(next);
|
|
322
285
|
onValueChange?.(next);
|
|
323
|
-
}, [isControlled, onValueChange
|
|
286
|
+
}, [isControlled, onValueChange]);
|
|
324
287
|
const registerLayout = useCallback((tab, layout) => {
|
|
325
288
|
setLayouts(current => {
|
|
326
289
|
const existing = current[tab];
|
|
@@ -335,45 +298,286 @@ function TabsRoot({
|
|
|
335
298
|
}, []);
|
|
336
299
|
|
|
337
300
|
/*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
301
|
+
* The panels are the sequence.
|
|
302
|
+
*
|
|
303
|
+
* They are read straight out of the children, in the order they are written,
|
|
304
|
+
* which is available on the first render and cannot disagree with what is on
|
|
305
|
+
* screen. The triggers used to register themselves to build this, which meant
|
|
306
|
+
* the order arrived a commit late and every mount and unmount of a trigger
|
|
307
|
+
* re-rendered every panel.
|
|
340
308
|
*/
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}, []);
|
|
309
|
+
const {
|
|
310
|
+
rest,
|
|
311
|
+
panels
|
|
312
|
+
} = useMemo(() => collectPanels(children), [children]);
|
|
313
|
+
const paged = swipeable && panels.length > 0;
|
|
347
314
|
const context = useMemo(() => ({
|
|
348
315
|
value: resolvedValue,
|
|
349
316
|
setValue,
|
|
350
317
|
registerLayout,
|
|
351
318
|
layouts,
|
|
352
|
-
tabs,
|
|
353
|
-
registerTab,
|
|
354
|
-
unregisterTab,
|
|
355
319
|
variant,
|
|
356
320
|
scrollable,
|
|
357
321
|
setScrollable,
|
|
358
322
|
keepMounted,
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}), [resolvedValue, setValue, registerLayout, layouts, tabs, registerTab, unregisterTab, variant, scrollable, keepMounted, swipeable, swipeOffset, panelWidth]);
|
|
323
|
+
pager: paged,
|
|
324
|
+
animationDisabled
|
|
325
|
+
}), [resolvedValue, setValue, registerLayout, layouts, variant, scrollable, keepMounted, paged, animationDisabled]);
|
|
363
326
|
return /*#__PURE__*/_jsx(TabsContext.Provider, {
|
|
364
327
|
value: context,
|
|
365
|
-
children: /*#__PURE__*/
|
|
328
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
366
329
|
className: cn('gap-3', className),
|
|
367
330
|
...props,
|
|
368
|
-
children: textChildren(children)
|
|
331
|
+
children: [textChildren(paged ? rest : children), paged ? /*#__PURE__*/_jsx(TabsPager, {
|
|
332
|
+
panels: panels,
|
|
333
|
+
value: resolvedValue,
|
|
334
|
+
setValue: setValue,
|
|
335
|
+
keepMounted: !!keepMounted,
|
|
336
|
+
animationDisabled: animationDisabled
|
|
337
|
+
}) : null]
|
|
369
338
|
})
|
|
370
339
|
});
|
|
371
340
|
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The panels, side by side, behind a window one panel wide.
|
|
344
|
+
*
|
|
345
|
+
* Everything about the movement lives on `position`, measured in panels rather
|
|
346
|
+
* than points: the strip is at `-position × width`, a drag sets it, a press
|
|
347
|
+
* springs it, and it is the only thing that says where the strip is. Nothing
|
|
348
|
+
* here waits for React to commit before moving, which is what the old
|
|
349
|
+
* arrangement did — it mounted the arriving panel and animated it in on the
|
|
350
|
+
* same frame, so the movement was only as smooth as the mount was quick.
|
|
351
|
+
*/
|
|
352
|
+
function TabsPager({
|
|
353
|
+
panels,
|
|
354
|
+
value,
|
|
355
|
+
setValue,
|
|
356
|
+
keepMounted,
|
|
357
|
+
animationDisabled
|
|
358
|
+
}) {
|
|
359
|
+
const sign = useDirectionSign();
|
|
360
|
+
const reducedMotion = useReducedMotion();
|
|
361
|
+
const still = animationDisabled || reducedMotion;
|
|
362
|
+
const order = useMemo(() => panels.map(panel => panel.props.value), [panels]);
|
|
363
|
+
const count = panels.length;
|
|
364
|
+
// An unknown value shows the first panel rather than none of them: a tab set
|
|
365
|
+
// with nothing in it is a harder thing to debug than one showing the wrong tab.
|
|
366
|
+
const active = Math.max(0, order.indexOf(value));
|
|
367
|
+
const [width, setWidth] = useState(0);
|
|
368
|
+
const position = useSharedValue(active);
|
|
369
|
+
const widthValue = useSharedValue(0);
|
|
370
|
+
const countValue = useSharedValue(count);
|
|
371
|
+
const start = useSharedValue(active);
|
|
372
|
+
/** 1 between a drag activating and it being finalised, 0 otherwise. */
|
|
373
|
+
const dragging = useSharedValue(0);
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
widthValue.value = width;
|
|
376
|
+
countValue.value = count;
|
|
377
|
+
}, [width, count, widthValue, countValue]);
|
|
378
|
+
|
|
379
|
+
/*
|
|
380
|
+
* Which panels have been built, and it only ever grows.
|
|
381
|
+
*
|
|
382
|
+
* A panel that has been reached stays mounted for the life of the tab set,
|
|
383
|
+
* so a tab is slow at most once. That is what `keepMounted` was reached for
|
|
384
|
+
* and could not deliver, because it decided mounting and hiding together and
|
|
385
|
+
* the hiding took the panel's size away.
|
|
386
|
+
*/
|
|
387
|
+
const [reached, setReached] = useState(() => [active]);
|
|
388
|
+
if (!reached.includes(active)) {
|
|
389
|
+
// During render, not in an effect: a press on a far tab has to have its
|
|
390
|
+
// panel in this commit, or the strip travels to an empty box.
|
|
391
|
+
setReached(current => current.includes(active) ? current : [...current, active]);
|
|
392
|
+
}
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
const wanted = keepMounted ? Array.from({
|
|
395
|
+
length: count
|
|
396
|
+
}, (_, index) => index) : [active - 1, active + 1].filter(index => index >= 0 && index < count);
|
|
397
|
+
|
|
398
|
+
/*
|
|
399
|
+
* The neighbours arrive a tick late, on purpose.
|
|
400
|
+
*
|
|
401
|
+
* They are what makes a swipe cost nothing — the panel you are swiping
|
|
402
|
+
* towards is already built — but mounting them in the same commit as the
|
|
403
|
+
* active one puts three panels' worth of work on the frame the tab set
|
|
404
|
+
* first appears. A timeout of zero is enough to let that frame out.
|
|
405
|
+
*/
|
|
406
|
+
const timer = setTimeout(() => {
|
|
407
|
+
setReached(current => {
|
|
408
|
+
const missing = wanted.filter(index => !current.includes(index));
|
|
409
|
+
return missing.length > 0 ? [...current, ...missing] : current;
|
|
410
|
+
});
|
|
411
|
+
}, 0);
|
|
412
|
+
return () => clearTimeout(timer);
|
|
413
|
+
}, [active, count, keepMounted]);
|
|
414
|
+
|
|
415
|
+
/*
|
|
416
|
+
* Which tab the strip has already been sprung to by a swipe.
|
|
417
|
+
*
|
|
418
|
+
* A swipe moves the strip and *then* reports the change, so by the time the
|
|
419
|
+
* value arrives the movement is under way with the flick's speed in it.
|
|
420
|
+
* Springing again from the effect below would restart it from rest, which is
|
|
421
|
+
* the flick visibly losing its throw halfway across.
|
|
422
|
+
*
|
|
423
|
+
* The index rather than a flag, so a change that never came back — a
|
|
424
|
+
* controlled parent that ignored the swipe — cannot swallow the next press.
|
|
425
|
+
*/
|
|
426
|
+
const sprungTo = useRef(null);
|
|
427
|
+
const orderRef = useRef(order);
|
|
428
|
+
orderRef.current = order;
|
|
429
|
+
const valueRef = useRef(value);
|
|
430
|
+
valueRef.current = value;
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
const already = sprungTo.current;
|
|
433
|
+
sprungTo.current = null;
|
|
434
|
+
if (already === active) return;
|
|
435
|
+
if (still || width === 0) {
|
|
436
|
+
position.value = active;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
position.value = withSpring(active, ENTER_SPRING);
|
|
440
|
+
}, [active, still, width, position]);
|
|
441
|
+
const commit = useCallback(index => {
|
|
442
|
+
const next = orderRef.current[index];
|
|
443
|
+
if (next === undefined || next === valueRef.current) return;
|
|
444
|
+
sprungTo.current = index;
|
|
445
|
+
setValue(next);
|
|
446
|
+
}, [setValue]);
|
|
447
|
+
const pan = useMemo(() => Gesture.Pan()
|
|
448
|
+
// Sideways past the threshold takes the gesture; any real vertical
|
|
449
|
+
// travel hands it back, so a panel that scrolls still scrolls.
|
|
450
|
+
.activeOffsetX([-SWIPE_ACTIVATE_X, SWIPE_ACTIVATE_X]).failOffsetY([-SWIPE_FAIL_Y, SWIPE_FAIL_Y]).onStart(() => {
|
|
451
|
+
// On activation rather than on touch-down, so a tap that never
|
|
452
|
+
// becomes a drag never claims a starting point. Rounded, so a drag
|
|
453
|
+
// begun while the last one is still settling starts from the tab it
|
|
454
|
+
// is settling on.
|
|
455
|
+
dragging.value = 1;
|
|
456
|
+
start.value = Math.round(position.value);
|
|
457
|
+
}).onUpdate(event => {
|
|
458
|
+
const span = widthValue.value;
|
|
459
|
+
if (span === 0) return;
|
|
460
|
+
const last = countValue.value - 1;
|
|
461
|
+
const raw = start.value - event.translationX * sign / span;
|
|
462
|
+
|
|
463
|
+
// Past either end there is no panel to bring on, so the strip gives
|
|
464
|
+
// a little and then stops, rather than pulling a blank into view.
|
|
465
|
+
if (raw < 0) position.value = raw * SWIPE_RESISTANCE_AT_END;else if (raw > last) position.value = last + (raw - last) * SWIPE_RESISTANCE_AT_END;else position.value = raw;
|
|
466
|
+
}).onEnd(event => {
|
|
467
|
+
const span = widthValue.value;
|
|
468
|
+
if (span === 0) return;
|
|
469
|
+
const last = countValue.value - 1;
|
|
470
|
+
const from = start.value;
|
|
471
|
+
const moved = position.value - from;
|
|
472
|
+
// Points per second becomes panels per second, which is the unit the
|
|
473
|
+
// spring that finishes the movement is working in.
|
|
474
|
+
const speed = -event.velocityX * sign / span;
|
|
475
|
+
let target = from;
|
|
476
|
+
// Speed first: distance and speed can disagree, and a flick back the
|
|
477
|
+
// way it came reads as a cancel however far it had already got.
|
|
478
|
+
if (Math.abs(event.velocityX) > SWIPE_VELOCITY) {
|
|
479
|
+
target = from + (speed > 0 ? 1 : -1);
|
|
480
|
+
} else if (Math.abs(moved) > SWIPE_DISTANCE_RATIO) {
|
|
481
|
+
target = from + (moved > 0 ? 1 : -1);
|
|
482
|
+
}
|
|
483
|
+
if (target < 0) target = 0;
|
|
484
|
+
if (target > last) target = last;
|
|
485
|
+
position.value = withSpring(target, {
|
|
486
|
+
...ENTER_SPRING,
|
|
487
|
+
velocity: speed
|
|
488
|
+
});
|
|
489
|
+
if (target !== from) runOnJS(commit)(target);
|
|
490
|
+
}).onFinalize((_event, success) => {
|
|
491
|
+
// A cancelled gesture never reaches `onEnd`, and would otherwise
|
|
492
|
+
// leave the strip wherever the finger abandoned it. Only for a drag
|
|
493
|
+
// that actually started: this also runs for every touch that never
|
|
494
|
+
// became one, and springing to the rounded position there would
|
|
495
|
+
// interrupt a press's own movement with a tap on the panel.
|
|
496
|
+
if (dragging.value === 1 && !success) {
|
|
497
|
+
position.value = withSpring(start.value, ENTER_SPRING);
|
|
498
|
+
}
|
|
499
|
+
dragging.value = 0;
|
|
500
|
+
}), [sign, commit, position, start, dragging, widthValue, countValue]);
|
|
501
|
+
const strip = useAnimatedStyle(() => ({
|
|
502
|
+
transform: [{
|
|
503
|
+
translateX: -position.value * widthValue.value * sign
|
|
504
|
+
}]
|
|
505
|
+
}));
|
|
506
|
+
const onLayout = useCallback(event => {
|
|
507
|
+
const measured = event.nativeEvent.layout;
|
|
508
|
+
if (measured.width > 0) setWidth(measured.width);
|
|
509
|
+
if (__DEV__ && measured.width > 0 && measured.height === 0) {
|
|
510
|
+
console.warn('[PanelUI] <Tabs swipeable> has no height to fill, so its panels have nowhere ' + 'to be laid out. Give the tab set a height — `className="flex-1"` on <Tabs>, ' + 'or a fixed height — the same as any pager needs.');
|
|
511
|
+
}
|
|
512
|
+
}, []);
|
|
513
|
+
|
|
514
|
+
/*
|
|
515
|
+
* A strip cannot be laid out before the width of one panel is known, so the
|
|
516
|
+
* first render is the active panel on its own, filling the window. The strip
|
|
517
|
+
* takes over on the next frame, and every frame after it.
|
|
518
|
+
*/
|
|
519
|
+
if (width === 0) {
|
|
520
|
+
return /*#__PURE__*/_jsx(View, {
|
|
521
|
+
style: PAGER_VIEWPORT,
|
|
522
|
+
onLayout: onLayout,
|
|
523
|
+
children: panels[active]
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return /*#__PURE__*/_jsx(View, {
|
|
527
|
+
style: PAGER_VIEWPORT,
|
|
528
|
+
onLayout: onLayout,
|
|
529
|
+
children: /*#__PURE__*/_jsx(GestureDetector, {
|
|
530
|
+
gesture: pan,
|
|
531
|
+
children: /*#__PURE__*/_jsx(Animated.View, {
|
|
532
|
+
style: [FILL, {
|
|
533
|
+
flexDirection: 'row',
|
|
534
|
+
width: width * count
|
|
535
|
+
}, strip],
|
|
536
|
+
children: panels.map((panel, index) => /*#__PURE__*/_jsx(View, {
|
|
537
|
+
style: [FILL, {
|
|
538
|
+
width
|
|
539
|
+
}],
|
|
540
|
+
children: reached.includes(index) ? panel : null
|
|
541
|
+
}, order[index]))
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Fill a height that is offered, and take the content's own when none is.
|
|
549
|
+
*
|
|
550
|
+
* `flexBasis: 'auto'` rather than the `0` that `flex: 1` sets, and it is on
|
|
551
|
+
* every box between the tab set and a panel — viewport, strip, panel — because
|
|
552
|
+
* one `flex: 1` anywhere in that chain breaks both cases at once. A `flex: 1`
|
|
553
|
+
* box inside a parent of indefinite height resolves to *nothing*: its basis is
|
|
554
|
+
* zero and there is no free space to grow into. So the tab set that had not
|
|
555
|
+
* been given a height would collapse, and take every panel with it — which is
|
|
556
|
+
* the same zero-height failure that made a kept panel useless to a list, one
|
|
557
|
+
* level up.
|
|
558
|
+
*
|
|
559
|
+
* With `auto` the chain resolves both ways. Given a height, the panels fill it
|
|
560
|
+
* and a virtualised list inside one has a real size to build against. Given
|
|
561
|
+
* none, the strip is as tall as its tallest panel and every panel stretches to
|
|
562
|
+
* match, so switching tabs does not change the tab set's height either.
|
|
563
|
+
*/
|
|
564
|
+
const FILL = {
|
|
565
|
+
flexGrow: 1,
|
|
566
|
+
flexShrink: 1,
|
|
567
|
+
flexBasis: 'auto'
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
/** The window the strip moves behind, one panel wide. */
|
|
571
|
+
const PAGER_VIEWPORT = {
|
|
572
|
+
...FILL,
|
|
573
|
+
overflow: 'hidden'
|
|
574
|
+
};
|
|
372
575
|
function TabsIndicator() {
|
|
373
576
|
const {
|
|
374
577
|
value,
|
|
375
578
|
layouts,
|
|
376
|
-
variant
|
|
579
|
+
variant,
|
|
580
|
+
animationDisabled
|
|
377
581
|
} = useTabs('Tabs.List');
|
|
378
582
|
const x = useSharedValue(0);
|
|
379
583
|
const width = useSharedValue(0);
|
|
@@ -390,7 +594,7 @@ function TabsIndicator() {
|
|
|
390
594
|
// duplicated when React re-renders.
|
|
391
595
|
useEffect(() => {
|
|
392
596
|
if (!layout) return;
|
|
393
|
-
if (initialized.value === 0) {
|
|
597
|
+
if (initialized.value === 0 || animationDisabled) {
|
|
394
598
|
// First measurement snaps into place; there is nothing to animate from.
|
|
395
599
|
x.value = layout.x;
|
|
396
600
|
width.value = layout.width;
|
|
@@ -399,7 +603,7 @@ function TabsIndicator() {
|
|
|
399
603
|
x.value = withSpring(layout.x, SPRING);
|
|
400
604
|
width.value = withSpring(layout.width, SPRING);
|
|
401
605
|
}
|
|
402
|
-
}, [layout?.x, layout?.width, x, width, initialized, layout]);
|
|
606
|
+
}, [layout?.x, layout?.width, x, width, initialized, animationDisabled, layout]);
|
|
403
607
|
const style = useAnimatedStyle(() => ({
|
|
404
608
|
opacity: initialized.value,
|
|
405
609
|
transform: [{
|
|
@@ -492,24 +696,13 @@ function TabsTrigger({
|
|
|
492
696
|
width
|
|
493
697
|
});
|
|
494
698
|
}, [context, value]);
|
|
495
|
-
|
|
496
|
-
// Separate from the layout registration above, and earlier than it: a swipe
|
|
497
|
-
// needs to know the sequence, which is known at mount, not the positions,
|
|
498
|
-
// which are not known until the row has been laid out.
|
|
499
|
-
const {
|
|
500
|
-
registerTab,
|
|
501
|
-
unregisterTab
|
|
502
|
-
} = context;
|
|
503
|
-
useEffect(() => {
|
|
504
|
-
registerTab(value);
|
|
505
|
-
return () => unregisterTab(value);
|
|
506
|
-
}, [value, registerTab, unregisterTab]);
|
|
507
699
|
if (context.variant === 'expanding') {
|
|
508
700
|
return /*#__PURE__*/_jsx(ExpandingTrigger, {
|
|
509
701
|
active: active,
|
|
510
702
|
disabled: disabled,
|
|
511
703
|
icon: icon,
|
|
512
704
|
badge: badge,
|
|
705
|
+
still: context.animationDisabled,
|
|
513
706
|
labelClassName: slots.label(),
|
|
514
707
|
className: cn(slots.trigger(), className),
|
|
515
708
|
onLayout: handleLayout,
|
|
@@ -554,6 +747,7 @@ function ExpandingTrigger({
|
|
|
554
747
|
disabled,
|
|
555
748
|
icon,
|
|
556
749
|
badge,
|
|
750
|
+
still,
|
|
557
751
|
className,
|
|
558
752
|
labelClassName,
|
|
559
753
|
onLayout,
|
|
@@ -564,7 +758,7 @@ function ExpandingTrigger({
|
|
|
564
758
|
const reducedMotion = useReducedMotion();
|
|
565
759
|
const open = useSharedValue(active ? 1 : 0);
|
|
566
760
|
useEffect(() => {
|
|
567
|
-
if (reducedMotion) {
|
|
761
|
+
if (reducedMotion || still) {
|
|
568
762
|
open.value = active ? 1 : 0;
|
|
569
763
|
return;
|
|
570
764
|
}
|
|
@@ -582,7 +776,7 @@ function ExpandingTrigger({
|
|
|
582
776
|
duration: EXPAND_DURATION,
|
|
583
777
|
easing: Easing.bezier(0.2, 0, 0, 1)
|
|
584
778
|
});
|
|
585
|
-
}, [active, reducedMotion, open]);
|
|
779
|
+
}, [active, reducedMotion, still, open]);
|
|
586
780
|
const reveal = useAnimatedStyle(() => ({
|
|
587
781
|
width: (labelWidth + LABEL_GAP) * open.value,
|
|
588
782
|
/*
|
|
@@ -664,196 +858,44 @@ function TabsContent({
|
|
|
664
858
|
}) {
|
|
665
859
|
const context = useTabs('Tabs.Content');
|
|
666
860
|
const active = context.value === value;
|
|
667
|
-
const {
|
|
668
|
-
tabs,
|
|
669
|
-
setValue,
|
|
670
|
-
swipeable,
|
|
671
|
-
swipeOffset: offset,
|
|
672
|
-
panelWidth: width
|
|
673
|
-
} = context;
|
|
674
|
-
const sign = useDirectionSign();
|
|
675
|
-
// Read on the UI thread while the finger is down, so the resistance at the
|
|
676
|
-
// ends is known without a round trip to JavaScript.
|
|
677
|
-
const index = useSharedValue(0);
|
|
678
|
-
const count = useSharedValue(0);
|
|
679
|
-
const position = tabs.indexOf(value);
|
|
680
|
-
useEffect(() => {
|
|
681
|
-
index.value = position;
|
|
682
|
-
count.value = tabs.length;
|
|
683
|
-
}, [position, tabs.length, index, count]);
|
|
684
861
|
|
|
685
862
|
/*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
863
|
+
* In a strip, a panel does no hiding and no moving.
|
|
864
|
+
*
|
|
865
|
+
* It is positioned by the strip and sized by the box it was put in, so all
|
|
866
|
+
* that is left to it is to fill that box and to stay out of the screen
|
|
867
|
+
* reader's way while it is off screen. Everything else this used to do — the
|
|
868
|
+
* displacement, the fade, the gesture, the two ways of being hidden — was
|
|
869
|
+
* work to make one panel stand in for a row of them, and the row is real now.
|
|
690
870
|
*/
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (next) setValue(next, {
|
|
700
|
-
velocity
|
|
701
|
-
});
|
|
702
|
-
}, [setValue]);
|
|
703
|
-
const pan = useMemo(() => Gesture.Pan()
|
|
704
|
-
// Sideways past the threshold takes the gesture; any real vertical
|
|
705
|
-
// travel hands it back, so a panel that scrolls still scrolls.
|
|
706
|
-
.activeOffsetX([-SWIPE_ACTIVATE_X, SWIPE_ACTIVATE_X]).failOffsetY([-SWIPE_FAIL_Y, SWIPE_FAIL_Y]).onUpdate(event => {
|
|
707
|
-
// Positive is "towards the previous tab" in reading order, which is
|
|
708
|
-
// rightwards under LTR and leftwards under RTL.
|
|
709
|
-
const travel = event.translationX * sign;
|
|
710
|
-
const atEnd = travel > 0 && index.value === 0 || travel < 0 && index.value >= count.value - 1;
|
|
711
|
-
/*
|
|
712
|
-
* One to one, except at the ends. The panel is under the finger for
|
|
713
|
-
* the whole of the drag, which is what lets the handover on release
|
|
714
|
-
* continue the movement rather than restart it — and a panel that
|
|
715
|
-
* moved a third as far as the finger never felt attached to it in
|
|
716
|
-
* the first place.
|
|
717
|
-
*/
|
|
718
|
-
offset.value = atEnd ? event.translationX * SWIPE_RESISTANCE_AT_END : event.translationX;
|
|
719
|
-
}).onEnd(event => {
|
|
720
|
-
const travel = event.translationX * sign;
|
|
721
|
-
const speed = event.velocityX * sign;
|
|
722
|
-
const far = Math.abs(travel) > width.value * SWIPE_DISTANCE_RATIO;
|
|
723
|
-
const fast = Math.abs(speed) > SWIPE_VELOCITY;
|
|
724
|
-
const atEnd = travel > 0 && index.value === 0 || travel < 0 && index.value >= count.value - 1;
|
|
725
|
-
if ((far || fast) && !atEnd) {
|
|
726
|
-
// Distance and speed can disagree — a flick back the way it came
|
|
727
|
-
// reads as a cancel — so the direction comes from whichever of the
|
|
728
|
-
// two crossed its threshold, speed first.
|
|
729
|
-
const delta = fast ? speed < 0 ? 1 : -1 : travel < 0 ? 1 : -1;
|
|
730
|
-
|
|
731
|
-
/*
|
|
732
|
-
* The outgoing panel carries on off the edge on the UI thread,
|
|
733
|
-
* immediately, rather than waiting to be told what happened.
|
|
734
|
-
*
|
|
735
|
-
* It used to hold wherever the finger left it until `setValue`
|
|
736
|
-
* landed — which is fine when React commits in a frame, and is a
|
|
737
|
-
* visible freeze when the arriving panel is expensive to mount, a
|
|
738
|
-
* list of any size being the usual case. The panel appeared to
|
|
739
|
-
* stick to the screen for exactly as long as the JS thread was
|
|
740
|
-
* busy, which reads as the swipe having dropped the gesture.
|
|
741
|
-
*
|
|
742
|
-
* `setValue` still places the arriving panel *relative* to this
|
|
743
|
-
* one, reading the offset live at commit time, so continuing the
|
|
744
|
-
* movement here does not desynchronise the handover: the panel
|
|
745
|
-
* that arrives is one width from wherever this one has got to,
|
|
746
|
-
* whenever that turns out to be.
|
|
747
|
-
*/
|
|
748
|
-
offset.value = withSpring(-delta * sign * width.value, {
|
|
749
|
-
...ENTER_SPRING,
|
|
750
|
-
velocity: event.velocityX
|
|
751
|
-
});
|
|
752
|
-
runOnJS(step)(delta, event.velocityX);
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Nothing changed hands, so this panel simply comes back — with the
|
|
757
|
-
// speed it was let go at, so a cancelled flick decelerates rather
|
|
758
|
-
// than stopping and being pulled.
|
|
759
|
-
offset.value = withSpring(0, {
|
|
760
|
-
...SPRING,
|
|
761
|
-
velocity: event.velocityX
|
|
871
|
+
if (context.pager) {
|
|
872
|
+
return /*#__PURE__*/_jsx(View, {
|
|
873
|
+
style: [PAGER_PANEL, style],
|
|
874
|
+
accessibilityElementsHidden: !active,
|
|
875
|
+
importantForAccessibility: active ? 'auto' : 'no-hide-descendants',
|
|
876
|
+
className: className,
|
|
877
|
+
...props,
|
|
878
|
+
children: textChildren(children)
|
|
762
879
|
});
|
|
763
|
-
}
|
|
764
|
-
// A cancelled gesture never reaches `onEnd`, and would otherwise
|
|
765
|
-
// leave the panel wherever the finger abandoned it.
|
|
766
|
-
if (!success) offset.value = withSpring(0, SPRING);
|
|
767
|
-
}), [sign, step, offset, width, index, count]);
|
|
768
|
-
const followStyle = useAnimatedStyle(() => {
|
|
769
|
-
// No `opacity` key at all when there is no fade, rather than a constant 1.
|
|
770
|
-
// Declaring it would still mark the property as animated and hand the view
|
|
771
|
-
// an alpha to composite every frame — the cost this is avoiding.
|
|
772
|
-
if (!SWIPE_FADE) return {
|
|
773
|
-
transform: [{
|
|
774
|
-
translateX: offset.value
|
|
775
|
-
}]
|
|
776
|
-
};
|
|
777
|
-
const span = width.value || 1;
|
|
778
|
-
const travelled = Math.min(1, Math.abs(offset.value) / span);
|
|
779
|
-
return {
|
|
780
|
-
transform: [{
|
|
781
|
-
translateX: offset.value
|
|
782
|
-
}],
|
|
783
|
-
opacity: 1 - SWIPE_FADE * travelled
|
|
784
|
-
};
|
|
785
|
-
});
|
|
880
|
+
}
|
|
786
881
|
if (!active && !context.keepMounted) return null;
|
|
787
882
|
|
|
788
|
-
/*
|
|
789
|
-
* Under `keepMounted='measured'` a hidden panel keeps its size instead of
|
|
790
|
-
* losing it. It is taken out of the flow and stretched over the tab set, so
|
|
791
|
-
* it is laid out at the same size as the visible panel without contributing
|
|
792
|
-
* its height to the parent, and it is hidden by being transparent rather
|
|
793
|
-
* than by `display: none`.
|
|
794
|
-
*
|
|
795
|
-
* That distinction is the whole point: `display: none` lays a panel out at
|
|
796
|
-
* zero size, and a child that sizes itself from its parent — a virtualised
|
|
797
|
-
* list deciding how many rows to render — renders nothing at all inside one.
|
|
798
|
-
* Given a real size while still hidden, it builds its rows now rather than on
|
|
799
|
-
* the frame the tab is switched to.
|
|
800
|
-
*
|
|
801
|
-
* `opacity: 0` is set once and never animated, so it costs a composite and
|
|
802
|
-
* not a per-frame redraw; the negative `zIndex` keeps it behind the visible
|
|
803
|
-
* panel rather than over it, whatever order the panels are written in.
|
|
804
|
-
*/
|
|
805
|
-
const measured = context.keepMounted === 'measured';
|
|
806
|
-
const hiddenStyle = measured ? {
|
|
807
|
-
position: 'absolute',
|
|
808
|
-
top: 0,
|
|
809
|
-
left: 0,
|
|
810
|
-
right: 0,
|
|
811
|
-
bottom: 0,
|
|
812
|
-
opacity: 0,
|
|
813
|
-
zIndex: -1
|
|
814
|
-
} : {
|
|
815
|
-
display: 'none'
|
|
816
|
-
};
|
|
817
|
-
|
|
818
883
|
/*
|
|
819
884
|
* Hidden rather than unmounted under `keepMounted`, and hidden thoroughly:
|
|
820
885
|
* it is not drawn, it takes no touches, and the accessibility props take it
|
|
821
886
|
* out of the reading order too. A screen reader walking through three panels
|
|
822
887
|
* of a tab set it cannot see is worse than no tabs at all.
|
|
888
|
+
*
|
|
889
|
+
* `display: none` takes it out of layout as well, so a kept panel costs
|
|
890
|
+
* nothing to have around — and can hold nothing that needs a size while it is
|
|
891
|
+
* hidden. That is what `swipeable` is for: in a strip every panel has one.
|
|
823
892
|
*/
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
* And never alongside the swipe: an entering layout animation owns the
|
|
831
|
-
* view's style for the length of it and will overwrite an animated style
|
|
832
|
-
* touching the same view, so the fade would eat the slide. When panels
|
|
833
|
-
* move, the movement is the entrance.
|
|
834
|
-
*/
|
|
835
|
-
entering: context.keepMounted || swipeable ? undefined : FadeIn.duration(150),
|
|
836
|
-
onLayout: event => {
|
|
837
|
-
// The thresholds are a share of the panel, not of the screen: a tab
|
|
838
|
-
// set inside a card is narrower than the window, and a quarter of the
|
|
839
|
-
// window would be most of the way across it.
|
|
840
|
-
//
|
|
841
|
-
// Zero is ignored. Every panel reports into the same value, and a
|
|
842
|
-
// panel kept with `display: none` measures as nothing — letting it say
|
|
843
|
-
// so would leave the visible panel with no width. A `'measured'` one
|
|
844
|
-
// is stretched over the tab set and reports the same width as the
|
|
845
|
-
// visible panel, so it is agreeing rather than overwriting.
|
|
846
|
-
const measured = event.nativeEvent.layout.width;
|
|
847
|
-
if (measured > 0) width.value = measured;
|
|
848
|
-
}
|
|
849
|
-
/*
|
|
850
|
-
* The follow style goes on the panel that is moving, and only that one.
|
|
851
|
-
* Under `keepMounted` every other panel is hidden and cannot be seen to
|
|
852
|
-
* move — but an animated style still subscribes them all to the offset,
|
|
853
|
-
* so a five-tab set ran five mappers per frame to reposition four views
|
|
854
|
-
* nobody was looking at.
|
|
855
|
-
*/,
|
|
856
|
-
style: [!active && hiddenStyle, swipeable && active && followStyle, style],
|
|
893
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
894
|
+
// Only worth animating when the panel is genuinely arriving. A kept panel
|
|
895
|
+
// is already there; fading it in every time it is revealed would undo the
|
|
896
|
+
// point of keeping it.
|
|
897
|
+
entering: context.keepMounted || context.animationDisabled ? undefined : FadeIn.duration(150),
|
|
898
|
+
style: [!active && HIDDEN_PANEL, style],
|
|
857
899
|
pointerEvents: active ? 'auto' : 'none',
|
|
858
900
|
accessibilityElementsHidden: !active,
|
|
859
901
|
importantForAccessibility: active ? 'auto' : 'no-hide-descendants',
|
|
@@ -861,17 +903,18 @@ function TabsContent({
|
|
|
861
903
|
...props,
|
|
862
904
|
children: textChildren(children)
|
|
863
905
|
});
|
|
864
|
-
|
|
865
|
-
// Only the visible panel carries the gesture. A kept-mounted panel takes no
|
|
866
|
-
// touches anyway — `pointerEvents` is off on it whichever way it is hidden —
|
|
867
|
-
// but attaching a detector to each of them would put several competing
|
|
868
|
-
// recognisers in the same tree.
|
|
869
|
-
if (!swipeable || !active) return panel;
|
|
870
|
-
return /*#__PURE__*/_jsx(GestureDetector, {
|
|
871
|
-
gesture: pan,
|
|
872
|
-
children: panel
|
|
873
|
-
});
|
|
874
906
|
}
|
|
907
|
+
|
|
908
|
+
/** A panel in the strip fills the box the strip put it in. */
|
|
909
|
+
const PAGER_PANEL = {
|
|
910
|
+
...FILL,
|
|
911
|
+
width: '100%'
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
/** …and one that is kept without a strip is mounted, but takes up no room. */
|
|
915
|
+
const HIDDEN_PANEL = {
|
|
916
|
+
display: 'none'
|
|
917
|
+
};
|
|
875
918
|
export const Tabs = Object.assign(TabsRoot, {
|
|
876
919
|
List: TabsList,
|
|
877
920
|
Trigger: TabsTrigger,
|