panelui-native 0.46.0 → 0.51.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.
- package/README.md +12 -1
- package/lib/module/components/accordion/index.js +32 -4
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/button/index.js +83 -15
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/button-group/index.js +186 -0
- package/lib/module/components/button-group/index.js.map +1 -0
- package/lib/module/components/color-picker/index.js +110 -1
- package/lib/module/components/color-picker/index.js.map +1 -1
- package/lib/module/components/date-time-picker/index.js +23 -5
- package/lib/module/components/date-time-picker/index.js.map +1 -1
- package/lib/module/components/fab/index.js +514 -0
- package/lib/module/components/fab/index.js.map +1 -0
- package/lib/module/components/markdown-editor/index.js +406 -0
- package/lib/module/components/markdown-editor/index.js.map +1 -0
- package/lib/module/components/markdown-editor/markdown-transforms.js +243 -0
- package/lib/module/components/markdown-editor/markdown-transforms.js.map +1 -0
- package/lib/module/components/questionnaire/index.js +1312 -0
- package/lib/module/components/questionnaire/index.js.map +1 -0
- package/lib/module/components/sortable/index.js +806 -0
- package/lib/module/components/sortable/index.js.map +1 -0
- package/lib/module/components/swipe/index.js +140 -5
- package/lib/module/components/swipe/index.js.map +1 -1
- package/lib/module/components/tabs/index.js +94 -18
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/time-picker/index.js +34 -6
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/components/tree/index.js +500 -0
- package/lib/module/components/tree/index.js.map +1 -0
- package/lib/module/icons/index.js +246 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +8 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/haptics.js +19 -0
- package/lib/module/utils/haptics.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +21 -0
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +21 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button-group/index.d.ts +212 -0
- package/lib/typescript/src/components/button-group/index.d.ts.map +1 -0
- package/lib/typescript/src/components/color-picker/index.d.ts +82 -1
- package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/date-time-picker/index.d.ts +7 -1
- package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/fab/index.d.ts +285 -0
- package/lib/typescript/src/components/fab/index.d.ts.map +1 -0
- package/lib/typescript/src/components/markdown-editor/index.d.ts +102 -0
- package/lib/typescript/src/components/markdown-editor/index.d.ts.map +1 -0
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts +76 -0
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts.map +1 -0
- package/lib/typescript/src/components/questionnaire/index.d.ts +336 -0
- package/lib/typescript/src/components/questionnaire/index.d.ts.map +1 -0
- package/lib/typescript/src/components/sortable/index.d.ts +238 -0
- package/lib/typescript/src/components/sortable/index.d.ts.map +1 -0
- package/lib/typescript/src/components/swipe/index.d.ts +35 -0
- package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tabs/index.d.ts +9 -0
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/index.d.ts +19 -1
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tree/index.d.ts +125 -0
- package/lib/typescript/src/components/tree/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +30 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +10 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/haptics.d.ts +14 -0
- package/lib/typescript/src/utils/haptics.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/accordion/index.tsx +48 -6
- package/src/components/button/index.tsx +97 -15
- package/src/components/button-group/index.tsx +199 -0
- package/src/components/color-picker/index.tsx +140 -3
- package/src/components/date-time-picker/index.tsx +39 -3
- package/src/components/fab/index.tsx +583 -0
- package/src/components/markdown-editor/index.tsx +526 -0
- package/src/components/markdown-editor/markdown-transforms.ts +228 -0
- package/src/components/questionnaire/index.tsx +1615 -0
- package/src/components/sortable/index.tsx +1080 -0
- package/src/components/swipe/index.tsx +165 -3
- package/src/components/tabs/index.tsx +100 -28
- package/src/components/time-picker/index.tsx +42 -6
- package/src/components/tree/index.tsx +564 -0
- package/src/icons/index.tsx +176 -0
- package/src/index.ts +89 -1
- package/src/utils/haptics.ts +21 -0
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
*
|
|
28
28
|
* Everything that moves runs on the UI thread. A row being dragged does not
|
|
29
29
|
* re-render — the only React work in a swipe is the callback at the end of it.
|
|
30
|
+
*
|
|
31
|
+
* Rows in a `Swipe.Group` close each other, so only one of them is ever open.
|
|
32
|
+
* That is the behaviour of every list on the phone that has this gesture, and
|
|
33
|
+
* a row cannot arrange it alone: it knows when it opens and has no way to hear
|
|
34
|
+
* that a sibling did.
|
|
30
35
|
*/
|
|
31
36
|
import {
|
|
32
37
|
Children,
|
|
@@ -36,6 +41,7 @@ import {
|
|
|
36
41
|
isValidElement,
|
|
37
42
|
useCallback,
|
|
38
43
|
useContext,
|
|
44
|
+
useEffect,
|
|
39
45
|
useImperativeHandle,
|
|
40
46
|
useMemo,
|
|
41
47
|
useRef,
|
|
@@ -96,6 +102,131 @@ interface SwipeContextValue {
|
|
|
96
102
|
|
|
97
103
|
const SwipeContext = createContext<SwipeContextValue | null>(null);
|
|
98
104
|
|
|
105
|
+
/* -------------------------------------------------------------------------- */
|
|
106
|
+
/* Group */
|
|
107
|
+
/* -------------------------------------------------------------------------- */
|
|
108
|
+
|
|
109
|
+
interface SwipeGroupContextValue {
|
|
110
|
+
/** Take a slot in the group. Returns the function that gives it back. */
|
|
111
|
+
join: (row: SwipeGroupMember) => () => void;
|
|
112
|
+
/** Say that this row has opened, so every other one can put itself away. */
|
|
113
|
+
opened: (row: SwipeGroupMember) => void;
|
|
114
|
+
/** Shut every row in the group. */
|
|
115
|
+
closeAll: () => void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface SwipeGroupMember {
|
|
119
|
+
close: () => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const SwipeGroupContext = createContext<SwipeGroupContextValue | null>(null);
|
|
123
|
+
|
|
124
|
+
export interface SwipeGroupProps extends ViewProps {
|
|
125
|
+
className?: string;
|
|
126
|
+
children?: ReactNode;
|
|
127
|
+
/**
|
|
128
|
+
* Close the other rows when one opens. On by default — that is the whole
|
|
129
|
+
* reason to reach for a group. Turning it off keeps the container and the
|
|
130
|
+
* `useSwipeGroup` handle while letting several rows stand open at once.
|
|
131
|
+
*/
|
|
132
|
+
exclusive?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Several rows that agree only one of them is open at a time.
|
|
137
|
+
*
|
|
138
|
+
* Every list of swipeable rows wants this, and every list has to be told: a
|
|
139
|
+
* row knows when it opens but has no way to learn that a sibling did, so
|
|
140
|
+
* without something above them a list ends up with three rows standing open
|
|
141
|
+
* and a screen that reads as broken. The mail, message and reminder lists on
|
|
142
|
+
* a phone all behave this way, and a list that does not is the odd one out.
|
|
143
|
+
*
|
|
144
|
+
* ```tsx
|
|
145
|
+
* <Swipe.Group>
|
|
146
|
+
* {rows.map((row) => (
|
|
147
|
+
* <Swipe key={row.id}>
|
|
148
|
+
* <Swipe.End>
|
|
149
|
+
* <Swipe.Action label="Delete" color="destructive" onPress={…} />
|
|
150
|
+
* </Swipe.End>
|
|
151
|
+
* <Item>…</Item>
|
|
152
|
+
* </Swipe>
|
|
153
|
+
* ))}
|
|
154
|
+
* </Swipe.Group>
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* The rows register themselves rather than being found by inspecting children,
|
|
158
|
+
* so a row nested inside anything at all still belongs — wrapped in an
|
|
159
|
+
* `Item.Group`, produced by a `map`, rendered by a component of your own. The
|
|
160
|
+
* alternative, walking the tree for `Swipe` elements, only ever works for the
|
|
161
|
+
* one arrangement it was written against.
|
|
162
|
+
*
|
|
163
|
+
* Nothing here re-renders. The registry is a ref and closing a sibling writes
|
|
164
|
+
* to that row's shared value, so opening a row costs the springs it starts and
|
|
165
|
+
* no React work at all.
|
|
166
|
+
*/
|
|
167
|
+
const SwipeGroup = forwardRef<View, SwipeGroupProps>(
|
|
168
|
+
({ className, children, exclusive = true, ...props }, ref) => {
|
|
169
|
+
/*
|
|
170
|
+
* A ref rather than state: membership changes as rows mount and unmount,
|
|
171
|
+
* and rendering the whole list again because one row arrived would undo
|
|
172
|
+
* the point of a component that never re-renders while it is dragged.
|
|
173
|
+
*/
|
|
174
|
+
const members = useRef(new Set<SwipeGroupMember>());
|
|
175
|
+
|
|
176
|
+
const context = useMemo<SwipeGroupContextValue>(
|
|
177
|
+
() => ({
|
|
178
|
+
join: (row) => {
|
|
179
|
+
members.current.add(row);
|
|
180
|
+
return () => {
|
|
181
|
+
members.current.delete(row);
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
opened: (row) => {
|
|
185
|
+
if (!exclusive) return;
|
|
186
|
+
for (const other of members.current) {
|
|
187
|
+
if (other !== row) other.close();
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
closeAll: () => {
|
|
191
|
+
for (const row of members.current) row.close();
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
[exclusive]
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<SwipeGroupContext.Provider value={context}>
|
|
199
|
+
<View ref={ref} className={className} {...props}>
|
|
200
|
+
{children}
|
|
201
|
+
</View>
|
|
202
|
+
</SwipeGroupContext.Provider>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
SwipeGroup.displayName = 'Swipe.Group';
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Shut every row in the enclosing `Swipe.Group`.
|
|
210
|
+
*
|
|
211
|
+
* The one thing a group knows that a single row cannot: a list that scrolls,
|
|
212
|
+
* navigates away, or has just deleted the row that was open wants all of them
|
|
213
|
+
* put back, and holding a ref to each row to do it by hand is bookkeeping the
|
|
214
|
+
* group is already doing.
|
|
215
|
+
*
|
|
216
|
+
* ```tsx
|
|
217
|
+
* const { closeAll } = useSwipeGroup();
|
|
218
|
+
* <ScrollView onScrollBeginDrag={closeAll}>…</ScrollView>
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
221
|
+
* Outside a group it is inert rather than an error, so a row that is sometimes
|
|
222
|
+
* grouped and sometimes not does not need two versions of its parent.
|
|
223
|
+
*/
|
|
224
|
+
export function useSwipeGroup(): { closeAll: () => void } {
|
|
225
|
+
const group = useContext(SwipeGroupContext);
|
|
226
|
+
const closeAll = useCallback(() => group?.closeAll(), [group]);
|
|
227
|
+
return { closeAll };
|
|
228
|
+
}
|
|
229
|
+
|
|
99
230
|
/* -------------------------------------------------------------------------- */
|
|
100
231
|
/* Action */
|
|
101
232
|
/* -------------------------------------------------------------------------- */
|
|
@@ -440,6 +571,35 @@ const SwipeRoot = forwardRef<SwipeHandle, SwipeProps>(
|
|
|
440
571
|
reportOpen(null);
|
|
441
572
|
}, [offset, reportOpen]);
|
|
442
573
|
|
|
574
|
+
/*
|
|
575
|
+
* Membership in a `Swipe.Group`, if there is one above this row.
|
|
576
|
+
*
|
|
577
|
+
* The registered member is a stable object rather than the `close`
|
|
578
|
+
* function itself, because `close` changes identity whenever
|
|
579
|
+
* `onOpenChange` does and the group would then be holding a stale entry
|
|
580
|
+
* alongside a live one. The object never changes; what it closes is read
|
|
581
|
+
* off a ref at the moment it is asked.
|
|
582
|
+
*/
|
|
583
|
+
const group = useContext(SwipeGroupContext);
|
|
584
|
+
const closeRef = useRef(close);
|
|
585
|
+
closeRef.current = close;
|
|
586
|
+
const member = useRef<SwipeGroupMember>({ close: () => closeRef.current() }).current;
|
|
587
|
+
|
|
588
|
+
useEffect(() => group?.join(member), [group, member]);
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Told when this row settles open, so the group can put its siblings away.
|
|
592
|
+
* A row closing says nothing — the others are already closed, and telling
|
|
593
|
+
* them so would be a round of work per settle for no change.
|
|
594
|
+
*/
|
|
595
|
+
const announce = useCallback(
|
|
596
|
+
(side: SwipeOpenSide) => {
|
|
597
|
+
reportOpen(side);
|
|
598
|
+
if (side !== null) group?.opened(member);
|
|
599
|
+
},
|
|
600
|
+
[reportOpen, group, member]
|
|
601
|
+
);
|
|
602
|
+
|
|
443
603
|
useImperativeHandle(
|
|
444
604
|
ref,
|
|
445
605
|
() => ({
|
|
@@ -447,11 +607,11 @@ const SwipeRoot = forwardRef<SwipeHandle, SwipeProps>(
|
|
|
447
607
|
const width = side === 'start' ? startWidth.value : endWidth.value;
|
|
448
608
|
if (width === 0) return;
|
|
449
609
|
offset.value = withSpring(side === 'start' ? width : -width, SPRING);
|
|
450
|
-
|
|
610
|
+
announce(side);
|
|
451
611
|
},
|
|
452
612
|
close,
|
|
453
613
|
}),
|
|
454
|
-
[offset, startWidth, endWidth,
|
|
614
|
+
[offset, startWidth, endWidth, announce, close]
|
|
455
615
|
);
|
|
456
616
|
|
|
457
617
|
/**
|
|
@@ -541,7 +701,7 @@ const SwipeRoot = forwardRef<SwipeHandle, SwipeProps>(
|
|
|
541
701
|
|
|
542
702
|
if (projected > limit * OPEN_RATIO) {
|
|
543
703
|
offset.value = withSpring(toStart ? limit : -limit, SPRING);
|
|
544
|
-
runOnJS(
|
|
704
|
+
runOnJS(announce)(side);
|
|
545
705
|
} else {
|
|
546
706
|
offset.value = withSpring(0, SPRING);
|
|
547
707
|
runOnJS(reportOpen)(null);
|
|
@@ -562,6 +722,7 @@ const SwipeRoot = forwardRef<SwipeHandle, SwipeProps>(
|
|
|
562
722
|
fire,
|
|
563
723
|
tick,
|
|
564
724
|
reportOpen,
|
|
725
|
+
announce,
|
|
565
726
|
]
|
|
566
727
|
);
|
|
567
728
|
|
|
@@ -681,6 +842,7 @@ const SwipeRoot = forwardRef<SwipeHandle, SwipeProps>(
|
|
|
681
842
|
SwipeRoot.displayName = 'Swipe';
|
|
682
843
|
|
|
683
844
|
export const Swipe = Object.assign(SwipeRoot, {
|
|
845
|
+
Group: SwipeGroup,
|
|
684
846
|
Start: SwipeStart,
|
|
685
847
|
End: SwipeEnd,
|
|
686
848
|
Action: SwipeAction,
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
type ReactNode,
|
|
37
37
|
} from 'react';
|
|
38
38
|
import {
|
|
39
|
+
Platform,
|
|
39
40
|
Pressable,
|
|
40
41
|
ScrollView,
|
|
41
42
|
View,
|
|
@@ -92,8 +93,18 @@ const SWIPE_RESISTANCE_AT_END = 0.16;
|
|
|
92
93
|
* card: a panel on its way out is on its way out, and the one arriving comes
|
|
93
94
|
* back up through the same fade. The two together read as a dissolve carried
|
|
94
95
|
* by the movement, rather than as a gap between two slides.
|
|
96
|
+
*
|
|
97
|
+
* **iOS only, and that is not a compromise.** A transform is a property of the
|
|
98
|
+
* layer the panel is drawn into, so moving it costs nothing per frame however
|
|
99
|
+
* much is inside it. An opacity that changes every frame is not: on Android the
|
|
100
|
+
* panel's view group has no offscreen buffer to fade, so the alpha is pushed
|
|
101
|
+
* down into its children and the whole visible subtree is re-drawn on every
|
|
102
|
+
* frame of the drag. Behind a list that is thirty rows of text and images,
|
|
103
|
+
* which is what a tab panel usually is, that is the difference between a swipe
|
|
104
|
+
* that tracks the finger and one that stutters. iOS fades the layer itself and
|
|
105
|
+
* is unaffected.
|
|
95
106
|
*/
|
|
96
|
-
const SWIPE_FADE = 0.75;
|
|
107
|
+
const SWIPE_FADE = Platform.OS === 'ios' ? 0.75 : 0;
|
|
97
108
|
|
|
98
109
|
/**
|
|
99
110
|
* How far off its resting place a panel arriving by a *press* starts, as a
|
|
@@ -101,7 +112,7 @@ const SWIPE_FADE = 0.75;
|
|
|
101
112
|
*
|
|
102
113
|
* A press has no finger travel to continue from, so it is given a throw of its
|
|
103
114
|
* own. A swipe does not use this: the panel it hands over to picks up exactly
|
|
104
|
-
*
|
|
115
|
+
* one width from wherever the outgoing one has travelled to — see `setValue`.
|
|
105
116
|
*/
|
|
106
117
|
const SWIPE_ENTER = 0.3;
|
|
107
118
|
|
|
@@ -291,6 +302,15 @@ export interface TabsProps extends ViewProps {
|
|
|
291
302
|
* already wants a horizontal drag — a carousel, a slider, a row that swipes
|
|
292
303
|
* open — and the two cannot both have it. Turn it on for panels of ordinary
|
|
293
304
|
* scrolling content, where it is the gesture people try first.
|
|
305
|
+
*
|
|
306
|
+
* **It does not change what is mounted.** Only `keepMounted` decides that,
|
|
307
|
+
* with or without this — a swipe animates between two panels of which one is
|
|
308
|
+
* being unmounted and the other mounted for the first time, exactly as a
|
|
309
|
+
* press does. What it does change is that the mount now happens *while
|
|
310
|
+
* something is moving*, so a panel that is slow to build stops being a pause
|
|
311
|
+
* before it appears and starts being a stutter in the movement. If a swipe
|
|
312
|
+
* feels heavier than a press on the same tab set, the panel is expensive to
|
|
313
|
+
* mount; `keepMounted` is the answer, not turning this off.
|
|
294
314
|
*/
|
|
295
315
|
swipeable?: boolean;
|
|
296
316
|
children: ReactNode;
|
|
@@ -319,8 +339,26 @@ function TabsRoot({
|
|
|
319
339
|
const isControlled = value !== undefined;
|
|
320
340
|
const resolvedValue = isControlled ? value : internalValue;
|
|
321
341
|
|
|
342
|
+
/*
|
|
343
|
+
* The two things `setValue` reads that change on every switch, held where
|
|
344
|
+
* reading them does not make it a new function.
|
|
345
|
+
*
|
|
346
|
+
* With them in the dependency list, changing tab produced a new `setValue`,
|
|
347
|
+
* therefore a new context, therefore a new `step`, therefore a new pan
|
|
348
|
+
* gesture — which detaches and re-attaches the native recogniser on the same
|
|
349
|
+
* commit that mounts the arriving panel's contents. The busiest frame of the
|
|
350
|
+
* interaction was doing the one piece of work that had no reason to be there.
|
|
351
|
+
*/
|
|
352
|
+
const tabsRef = useRef(tabs);
|
|
353
|
+
tabsRef.current = tabs;
|
|
354
|
+
const valueRef = useRef(resolvedValue);
|
|
355
|
+
valueRef.current = resolvedValue;
|
|
356
|
+
|
|
322
357
|
const setValue = useCallback(
|
|
323
358
|
(next: string, handover?: SwipeHandover) => {
|
|
359
|
+
const tabs = tabsRef.current;
|
|
360
|
+
const resolvedValue = valueRef.current;
|
|
361
|
+
|
|
324
362
|
/*
|
|
325
363
|
* The arriving panel starts on the side it is arriving from, and travels
|
|
326
364
|
* in. Done here rather than in the panel because only the root knows
|
|
@@ -338,11 +376,13 @@ function TabsRoot({
|
|
|
338
376
|
if (handover) {
|
|
339
377
|
/*
|
|
340
378
|
* A swipe hands over mid-movement, so the arriving panel starts a
|
|
341
|
-
* whole panel's width from wherever the outgoing one
|
|
379
|
+
* whole panel's width from wherever the outgoing one has got to —
|
|
342
380
|
* which is where it *would* have been all along, had both been
|
|
343
|
-
* mounted.
|
|
344
|
-
*
|
|
345
|
-
*
|
|
381
|
+
* mounted. Read live rather than captured, because the outgoing
|
|
382
|
+
* panel is still travelling while this runs: adding to the current
|
|
383
|
+
* displacement rather than replacing it is the whole difference
|
|
384
|
+
* between one continuous movement and a jump at the moment the
|
|
385
|
+
* arriving panel finishes mounting.
|
|
346
386
|
*/
|
|
347
387
|
swipeOffset.value += direction * sign * panelWidth.value;
|
|
348
388
|
swipeOffset.value = withSpring(0, {
|
|
@@ -361,16 +401,7 @@ function TabsRoot({
|
|
|
361
401
|
if (!isControlled) setInternalValue(next);
|
|
362
402
|
onValueChange?.(next);
|
|
363
403
|
},
|
|
364
|
-
[
|
|
365
|
-
isControlled,
|
|
366
|
-
onValueChange,
|
|
367
|
-
swipeable,
|
|
368
|
-
tabs,
|
|
369
|
-
resolvedValue,
|
|
370
|
-
sign,
|
|
371
|
-
swipeOffset,
|
|
372
|
-
panelWidth,
|
|
373
|
-
]
|
|
404
|
+
[isControlled, onValueChange, swipeable, sign, swipeOffset, panelWidth]
|
|
374
405
|
);
|
|
375
406
|
|
|
376
407
|
const registerLayout = useCallback((tab: string, layout: TabLayout) => {
|
|
@@ -781,13 +812,25 @@ function TabsContent({ className, value, children, style, ...props }: TabsConten
|
|
|
781
812
|
count.value = tabs.length;
|
|
782
813
|
}, [position, tabs.length, index, count]);
|
|
783
814
|
|
|
815
|
+
/*
|
|
816
|
+
* Same reason as `setValue` on the root: `step` is reached through the pan
|
|
817
|
+
* gesture, and a `step` that changes identity on every switch rebuilds the
|
|
818
|
+
* gesture on every switch. The two facts it needs are read at call time
|
|
819
|
+
* instead, which is when they are wanted anyway.
|
|
820
|
+
*/
|
|
821
|
+
const tabsRef = useRef(tabs);
|
|
822
|
+
tabsRef.current = tabs;
|
|
823
|
+
const valueRef = useRef(value);
|
|
824
|
+
valueRef.current = value;
|
|
825
|
+
|
|
784
826
|
const step = useCallback(
|
|
785
827
|
(delta: number, velocity: number) => {
|
|
786
|
-
const
|
|
787
|
-
const
|
|
828
|
+
const list = tabsRef.current;
|
|
829
|
+
const from = list.indexOf(valueRef.current);
|
|
830
|
+
const next = list[from + delta];
|
|
788
831
|
if (next) setValue(next, { velocity });
|
|
789
832
|
},
|
|
790
|
-
[
|
|
833
|
+
[setValue]
|
|
791
834
|
);
|
|
792
835
|
|
|
793
836
|
const pan = useMemo(
|
|
@@ -828,14 +871,31 @@ function TabsContent({ className, value, children, style, ...props }: TabsConten
|
|
|
828
871
|
// Distance and speed can disagree — a flick back the way it came
|
|
829
872
|
// reads as a cancel — so the direction comes from whichever of the
|
|
830
873
|
// two crossed its threshold, speed first.
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
874
|
+
const delta = fast ? (speed < 0 ? 1 : -1) : travel < 0 ? 1 : -1;
|
|
875
|
+
|
|
876
|
+
/*
|
|
877
|
+
* The outgoing panel carries on off the edge on the UI thread,
|
|
878
|
+
* immediately, rather than waiting to be told what happened.
|
|
879
|
+
*
|
|
880
|
+
* It used to hold wherever the finger left it until `setValue`
|
|
881
|
+
* landed — which is fine when React commits in a frame, and is a
|
|
882
|
+
* visible freeze when the arriving panel is expensive to mount, a
|
|
883
|
+
* list of any size being the usual case. The panel appeared to
|
|
884
|
+
* stick to the screen for exactly as long as the JS thread was
|
|
885
|
+
* busy, which reads as the swipe having dropped the gesture.
|
|
886
|
+
*
|
|
887
|
+
* `setValue` still places the arriving panel *relative* to this
|
|
888
|
+
* one, reading the offset live at commit time, so continuing the
|
|
889
|
+
* movement here does not desynchronise the handover: the panel
|
|
890
|
+
* that arrives is one width from wherever this one has got to,
|
|
891
|
+
* whenever that turns out to be.
|
|
892
|
+
*/
|
|
893
|
+
offset.value = withSpring(-delta * sign * width.value, {
|
|
894
|
+
...ENTER_SPRING,
|
|
895
|
+
velocity: event.velocityX,
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
runOnJS(step)(delta, event.velocityX);
|
|
839
899
|
return;
|
|
840
900
|
}
|
|
841
901
|
|
|
@@ -853,6 +913,11 @@ function TabsContent({ className, value, children, style, ...props }: TabsConten
|
|
|
853
913
|
);
|
|
854
914
|
|
|
855
915
|
const followStyle = useAnimatedStyle(() => {
|
|
916
|
+
// No `opacity` key at all when there is no fade, rather than a constant 1.
|
|
917
|
+
// Declaring it would still mark the property as animated and hand the view
|
|
918
|
+
// an alpha to composite every frame — the cost this is avoiding.
|
|
919
|
+
if (!SWIPE_FADE) return { transform: [{ translateX: offset.value }] };
|
|
920
|
+
|
|
856
921
|
const span = width.value || 1;
|
|
857
922
|
const travelled = Math.min(1, Math.abs(offset.value) / span);
|
|
858
923
|
return {
|
|
@@ -893,7 +958,14 @@ function TabsContent({ className, value, children, style, ...props }: TabsConten
|
|
|
893
958
|
const measured = event.nativeEvent.layout.width;
|
|
894
959
|
if (measured > 0) width.value = measured;
|
|
895
960
|
}}
|
|
896
|
-
|
|
961
|
+
/*
|
|
962
|
+
* The follow style goes on the panel that is moving, and only that one.
|
|
963
|
+
* Under `keepMounted` every other panel is `display: none` and cannot be
|
|
964
|
+
* seen to move — but an animated style still subscribes them all to the
|
|
965
|
+
* offset, so a five-tab set ran five mappers per frame to reposition four
|
|
966
|
+
* views nobody was looking at.
|
|
967
|
+
*/
|
|
968
|
+
style={[!active && { display: 'none' }, swipeable && active && followStyle, style]}
|
|
897
969
|
pointerEvents={active ? 'auto' : 'none'}
|
|
898
970
|
accessibilityElementsHidden={!active}
|
|
899
971
|
importantForAccessibility={active ? 'auto' : 'no-hide-descendants'}
|
|
@@ -116,6 +116,15 @@ const TICK_SPACING = 12;
|
|
|
116
116
|
/** Settles the clock hands after a pick. Slow enough to be followed by eye. */
|
|
117
117
|
const HAND_SPRING = { damping: 16, stiffness: 140, mass: 0.7 } as const;
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* How loudly the ruler face states the time it is on.
|
|
121
|
+
*
|
|
122
|
+
* `default` is the big centred number, right when the scale is the only thing
|
|
123
|
+
* on the panel. `compact` steps it down to sit under something that outranks
|
|
124
|
+
* it, and `none` drops it for a caller that writes the time itself.
|
|
125
|
+
*/
|
|
126
|
+
export type TimePickerReadout = 'default' | 'compact' | 'none';
|
|
127
|
+
|
|
119
128
|
const timePickerVariants = tv({
|
|
120
129
|
slots: {
|
|
121
130
|
panel: 'gap-3',
|
|
@@ -128,10 +137,20 @@ const timePickerVariants = tv({
|
|
|
128
137
|
row: 'items-center justify-center',
|
|
129
138
|
rowLabel: 'text-lg tabular-nums text-foreground',
|
|
130
139
|
rowLabelSelected: 'font-semibold text-primary',
|
|
131
|
-
readout: 'text-center
|
|
140
|
+
readout: 'text-center font-semibold tabular-nums text-foreground',
|
|
132
141
|
clock: 'flex-row items-center gap-4',
|
|
133
142
|
footer: 'flex-row justify-end gap-2',
|
|
134
143
|
},
|
|
144
|
+
variants: {
|
|
145
|
+
readout: {
|
|
146
|
+
default: { readout: 'text-4xl' },
|
|
147
|
+
compact: { readout: 'text-xl' },
|
|
148
|
+
none: {},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
defaultVariants: {
|
|
152
|
+
readout: 'default',
|
|
153
|
+
},
|
|
135
154
|
});
|
|
136
155
|
|
|
137
156
|
/* -------------------------------------------------------------------------- */
|
|
@@ -296,6 +315,8 @@ interface FaceProps {
|
|
|
296
315
|
minuteStep: number;
|
|
297
316
|
locale?: string;
|
|
298
317
|
disabled?: boolean;
|
|
318
|
+
/** Ruler face only — the other two never draw a number of their own. */
|
|
319
|
+
readout?: TimePickerReadout;
|
|
299
320
|
}
|
|
300
321
|
|
|
301
322
|
function WheelFace({
|
|
@@ -573,8 +594,9 @@ function RulerFace({
|
|
|
573
594
|
minuteStep,
|
|
574
595
|
locale,
|
|
575
596
|
disabled,
|
|
597
|
+
readout = 'default',
|
|
576
598
|
}: FaceProps) {
|
|
577
|
-
const slots = timePickerVariants();
|
|
599
|
+
const slots = timePickerVariants({ readout });
|
|
578
600
|
const ref = useRef<ScrollView>(null);
|
|
579
601
|
const times = useMemo(() => timesOfDay(minuteStep), [minuteStep]);
|
|
580
602
|
const index = Math.min(
|
|
@@ -638,10 +660,12 @@ function RulerFace({
|
|
|
638
660
|
);
|
|
639
661
|
|
|
640
662
|
return (
|
|
641
|
-
<View className=
|
|
642
|
-
|
|
643
|
-
{
|
|
644
|
-
|
|
663
|
+
<View className={readout === 'none' ? undefined : 'gap-4'}>
|
|
664
|
+
{readout === 'none' ? null : (
|
|
665
|
+
<Text className={slots.readout()}>
|
|
666
|
+
{formatTime(value, { hourCycle, locale })}
|
|
667
|
+
</Text>
|
|
668
|
+
)}
|
|
645
669
|
|
|
646
670
|
<View className="relative h-14 justify-center" onLayout={onLayout}>
|
|
647
671
|
<ScrollView
|
|
@@ -768,6 +792,16 @@ export interface TimePickerProps {
|
|
|
768
792
|
format?: (value: TimeValue) => string;
|
|
769
793
|
/** BCP 47 tag for the time's text and the meridiem labels. */
|
|
770
794
|
locale?: string;
|
|
795
|
+
/**
|
|
796
|
+
* How loudly the `ruler` face states the time it is on. The other two faces
|
|
797
|
+
* spell the time out in their own columns and hands, and ignore this.
|
|
798
|
+
*
|
|
799
|
+
* The big centred number is right when the scale is the only thing on the
|
|
800
|
+
* panel. Under something that outranks it — a calendar, a form row — it is
|
|
801
|
+
* the largest text on screen for the smaller half of the answer, so step it
|
|
802
|
+
* down with `compact` or take it over yourself with `none`.
|
|
803
|
+
*/
|
|
804
|
+
readout?: TimePickerReadout;
|
|
771
805
|
/** Stop the trigger opening it, and the faces from being scrolled. */
|
|
772
806
|
disabled?: boolean;
|
|
773
807
|
className?: string;
|
|
@@ -803,6 +837,7 @@ function TimePickerRoot({
|
|
|
803
837
|
placeholder = DEFAULT_PLACEHOLDER,
|
|
804
838
|
format,
|
|
805
839
|
locale,
|
|
840
|
+
readout = 'default',
|
|
806
841
|
disabled = false,
|
|
807
842
|
className,
|
|
808
843
|
children,
|
|
@@ -867,6 +902,7 @@ function TimePickerRoot({
|
|
|
867
902
|
minuteStep: step,
|
|
868
903
|
locale,
|
|
869
904
|
disabled,
|
|
905
|
+
readout,
|
|
870
906
|
};
|
|
871
907
|
|
|
872
908
|
if (presentation === 'inline') {
|