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
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SelectionMode — turning a list into one you can pick several things out of.
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* <SelectionMode values={ids} onSelectedChange={setSelected}>
|
|
8
|
+
* <SelectionMode.Header title="Choose" />
|
|
9
|
+
* <FlashList
|
|
10
|
+
* data={threads}
|
|
11
|
+
* renderItem={({ item }) => (
|
|
12
|
+
* <SelectionMode.Item value={item.id} onPress={() => open(item)}>
|
|
13
|
+
* <Item>…</Item>
|
|
14
|
+
* </SelectionMode.Item>
|
|
15
|
+
* )}
|
|
16
|
+
* />
|
|
17
|
+
* <SelectionMode.Bar>
|
|
18
|
+
* <SelectionMode.Action icon={<TrashIcon size={20} />} destructive onPress={remove}>
|
|
19
|
+
* Delete
|
|
20
|
+
* </SelectionMode.Action>
|
|
21
|
+
* </SelectionMode.Bar>
|
|
22
|
+
* </SelectionMode>
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* ## Two ways to present it
|
|
26
|
+
*
|
|
27
|
+
* On a screen it is a *mode*: the list is there to be read, and a long press
|
|
28
|
+
* turns it into one you can pick from. In a sheet it is a *picker*:
|
|
29
|
+
* `SelectionMode.Sheet` was opened in order to choose something, so it is
|
|
30
|
+
* choosing from the moment it appears, with the actions in the sheet's footer.
|
|
31
|
+
*
|
|
32
|
+
* ## The items stay yours
|
|
33
|
+
*
|
|
34
|
+
* `SelectionMode.Item` wraps whatever you put in it rather than replacing it.
|
|
35
|
+
* It adds the circle and takes over what a press means; what the item looks
|
|
36
|
+
* like is yours. That is what lets one component hold a row of people, a grid
|
|
37
|
+
* of colours, a run of slides and a list of files without growing a prop for
|
|
38
|
+
* each of them.
|
|
39
|
+
*
|
|
40
|
+
* ## A mode has to be obvious
|
|
41
|
+
*
|
|
42
|
+
* There are two states and the list behaves differently in each: normally a tap
|
|
43
|
+
* opens a row, and in selection a tap picks it. That is only safe if leaving is
|
|
44
|
+
* always available and never hidden — hence a cancel in the header, the Android
|
|
45
|
+
* back button, and the count in front of the reader the whole time.
|
|
46
|
+
*
|
|
47
|
+
* Entering is a long press on a row, which is the gesture the platform has used
|
|
48
|
+
* for this for fifteen years, and the row you pressed is the first one picked.
|
|
49
|
+
* Entering with nothing selected leaves the reader in a changed list with no
|
|
50
|
+
* explanation of what changed.
|
|
51
|
+
*
|
|
52
|
+
* ## Selection is a set of values, not of rows
|
|
53
|
+
*
|
|
54
|
+
* The component holds ids, never indices or elements. A list that reorders,
|
|
55
|
+
* pages in more rows or drops one underneath the reader would invalidate
|
|
56
|
+
* anything positional; a set of ids survives all three, and is also the shape
|
|
57
|
+
* the action at the end needs — deleting takes ids.
|
|
58
|
+
*/
|
|
59
|
+
import { Children, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
60
|
+
import { Pressable, View } from 'react-native';
|
|
61
|
+
import Animated, { FadeIn, FadeOut, SlideInDown, SlideOutDown, useAnimatedStyle, useReducedMotion, useSharedValue, withSpring, withTiming } from 'react-native-reanimated';
|
|
62
|
+
import { tv } from 'tailwind-variants';
|
|
63
|
+
import { useCSSVariable } from 'uniwind';
|
|
64
|
+
import { useBackHandler } from "../../hooks/use-back-handler.js";
|
|
65
|
+
import { CheckIcon, IconColorProvider, XIcon } from "../../icons/index.js";
|
|
66
|
+
import { AnimatedPressable } from "../../primitives/animated-pressable.js";
|
|
67
|
+
import { Text, textChildren } from "../../primitives/text.js";
|
|
68
|
+
import { cn } from "../../utils/cn.js";
|
|
69
|
+
import { selectionTick } from "../../utils/haptics.js";
|
|
70
|
+
import { BottomSheet } from "../bottom-sheet/index.js";
|
|
71
|
+
|
|
72
|
+
/** How long the circle takes to come and go, in milliseconds. */
|
|
73
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
74
|
+
const REVEAL_DURATION = 180;
|
|
75
|
+
|
|
76
|
+
/** The spring the tick lands with — the same one the checkbox uses. */
|
|
77
|
+
const TICK_SPRING = {
|
|
78
|
+
damping: 15,
|
|
79
|
+
stiffness: 300,
|
|
80
|
+
mass: 0.5
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** How far a floating action bar sits from the edges, in points. */
|
|
84
|
+
const DEFAULT_BAR_OFFSET = 16;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Fill a height that is offered, and take the content's own when none is.
|
|
88
|
+
*
|
|
89
|
+
* `flexBasis: 'auto'` rather than the `0` that `flex: 1` sets. A `flex: 1` box
|
|
90
|
+
* inside a parent of indefinite height resolves to *nothing* — its basis is
|
|
91
|
+
* zero and there is no free space to grow into — so a selection list dropped
|
|
92
|
+
* into a scrolling page would collapse to a hairline instead of showing its
|
|
93
|
+
* rows.
|
|
94
|
+
*/
|
|
95
|
+
const FILL = {
|
|
96
|
+
flexGrow: 1,
|
|
97
|
+
flexShrink: 1,
|
|
98
|
+
flexBasis: 'auto'
|
|
99
|
+
};
|
|
100
|
+
const selectionVariants = tv({
|
|
101
|
+
slots: {
|
|
102
|
+
circle: 'h-6 w-6 items-center justify-center rounded-full border-2 border-muted-foreground',
|
|
103
|
+
fill: 'absolute inset-0 items-center justify-center rounded-full bg-primary',
|
|
104
|
+
header: 'h-14 flex-row items-center gap-3 border-b border-border px-4',
|
|
105
|
+
title: 'flex-1 text-center text-base font-semibold text-foreground',
|
|
106
|
+
close: 'h-10 w-10 items-center justify-center rounded-full bg-muted',
|
|
107
|
+
group: 'overflow-hidden rounded-2xl bg-card',
|
|
108
|
+
ring: 'rounded-full border-2 border-transparent p-0.5',
|
|
109
|
+
bar: 'flex-row items-stretch',
|
|
110
|
+
action: 'flex-1 items-center justify-center gap-1.5 px-2 py-3',
|
|
111
|
+
actionLabel: 'text-xs font-medium text-foreground'
|
|
112
|
+
},
|
|
113
|
+
variants: {
|
|
114
|
+
selected: {
|
|
115
|
+
true: {
|
|
116
|
+
circle: 'border-primary',
|
|
117
|
+
ring: 'border-foreground'
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
destructive: {
|
|
121
|
+
true: {
|
|
122
|
+
actionLabel: 'text-destructive'
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
disabled: {
|
|
126
|
+
true: {
|
|
127
|
+
action: 'opacity-[0.44]'
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
/**
|
|
131
|
+
* Flush to the bottom edge, or lifted off it.
|
|
132
|
+
*
|
|
133
|
+
* `bar` is the platform shape — full width against the edge, a hairline
|
|
134
|
+
* along the top, and the same background as the screen's own chrome. It is
|
|
135
|
+
* the default because it is what a list with a selection in it does on both
|
|
136
|
+
* platforms, and because it does not take width away from the list.
|
|
137
|
+
*/
|
|
138
|
+
placement: {
|
|
139
|
+
bar: {
|
|
140
|
+
bar: 'border-t border-border bg-popover'
|
|
141
|
+
},
|
|
142
|
+
floating: {
|
|
143
|
+
bar: 'rounded-2xl border border-border bg-popover shadow-lg'
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
defaultVariants: {
|
|
148
|
+
placement: 'bar'
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
const SelectionModeContext = /*#__PURE__*/createContext(null);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Read the selection from anywhere inside a `SelectionMode` — for a header of
|
|
155
|
+
* your own, a count somewhere else on the screen, or an action that has to know
|
|
156
|
+
* what is picked.
|
|
157
|
+
*/
|
|
158
|
+
export function useSelectionMode() {
|
|
159
|
+
const context = useContext(SelectionModeContext);
|
|
160
|
+
if (!context) {
|
|
161
|
+
throw new Error('useSelectionMode must be used within a <SelectionMode>');
|
|
162
|
+
}
|
|
163
|
+
return context;
|
|
164
|
+
}
|
|
165
|
+
function SelectionModeRoot({
|
|
166
|
+
className,
|
|
167
|
+
values,
|
|
168
|
+
active: activeProp,
|
|
169
|
+
defaultActive = false,
|
|
170
|
+
onActiveChange,
|
|
171
|
+
selected: selectedProp,
|
|
172
|
+
defaultSelected,
|
|
173
|
+
onSelectedChange,
|
|
174
|
+
max,
|
|
175
|
+
haptics = false,
|
|
176
|
+
children,
|
|
177
|
+
...props
|
|
178
|
+
}) {
|
|
179
|
+
const [internalActive, setInternalActive] = useState(defaultActive);
|
|
180
|
+
const [internalSelected, setInternalSelected] = useState(defaultSelected ?? []);
|
|
181
|
+
const active = activeProp ?? internalActive;
|
|
182
|
+
const selected = selectedProp ?? internalSelected;
|
|
183
|
+
const selectedRef = useRef(selected);
|
|
184
|
+
selectedRef.current = selected;
|
|
185
|
+
const setSelected = useCallback(next => {
|
|
186
|
+
if (selectedProp === undefined) setInternalSelected(next);
|
|
187
|
+
onSelectedChange?.(next);
|
|
188
|
+
}, [selectedProp, onSelectedChange]);
|
|
189
|
+
const setActive = useCallback(next => {
|
|
190
|
+
if (activeProp === undefined) setInternalActive(next);
|
|
191
|
+
onActiveChange?.(next);
|
|
192
|
+
}, [activeProp, onActiveChange]);
|
|
193
|
+
const enter = useCallback(value => {
|
|
194
|
+
if (haptics) selectionTick();
|
|
195
|
+
setActive(true);
|
|
196
|
+
// Entering with the row that was pressed already picked. Entering with
|
|
197
|
+
// nothing picked leaves the reader in a list that has changed under them
|
|
198
|
+
// with nothing to show for it.
|
|
199
|
+
if (value !== undefined && !selectedRef.current.includes(value)) {
|
|
200
|
+
setSelected([...selectedRef.current, value]);
|
|
201
|
+
}
|
|
202
|
+
}, [haptics, setActive, setSelected]);
|
|
203
|
+
|
|
204
|
+
/*
|
|
205
|
+
* Leaving clears the selection.
|
|
206
|
+
*
|
|
207
|
+
* A selection that outlived the mode would come back the next time it was
|
|
208
|
+
* entered, and the reader who left by pressing cancel is exactly the reader
|
|
209
|
+
* who meant "not those". Keep it across a mode change by controlling
|
|
210
|
+
* `selected` yourself.
|
|
211
|
+
*/
|
|
212
|
+
const exit = useCallback(() => {
|
|
213
|
+
setActive(false);
|
|
214
|
+
setSelected([]);
|
|
215
|
+
}, [setActive, setSelected]);
|
|
216
|
+
const isSelected = useCallback(value => selectedRef.current.includes(value), []);
|
|
217
|
+
const toggle = useCallback(value => {
|
|
218
|
+
const current = selectedRef.current;
|
|
219
|
+
if (current.includes(value)) {
|
|
220
|
+
setSelected(current.filter(entry => entry !== value));
|
|
221
|
+
} else {
|
|
222
|
+
if (max !== undefined && current.length >= max) return;
|
|
223
|
+
if (haptics) selectionTick();
|
|
224
|
+
setSelected([...current, value]);
|
|
225
|
+
}
|
|
226
|
+
}, [max, haptics, setSelected]);
|
|
227
|
+
const selectAll = useCallback(() => {
|
|
228
|
+
if (!values) return;
|
|
229
|
+
// At the limit rather than refusing: somebody who asked for all of them and
|
|
230
|
+
// can only have twenty wants the twenty, not an error.
|
|
231
|
+
setSelected(max === undefined ? [...values] : values.slice(0, max));
|
|
232
|
+
}, [values, max, setSelected]);
|
|
233
|
+
const clear = useCallback(() => setSelected([]), [setSelected]);
|
|
234
|
+
const total = values?.length ?? 0;
|
|
235
|
+
const count = selected.length;
|
|
236
|
+
const allSelected = total > 0 && count >= (max === undefined ? total : Math.min(total, max));
|
|
237
|
+
|
|
238
|
+
// An open mode owns the back button: back should leave the mode, not the
|
|
239
|
+
// screen the list is on.
|
|
240
|
+
useBackHandler(active, exit);
|
|
241
|
+
const context = useMemo(() => ({
|
|
242
|
+
active,
|
|
243
|
+
enter,
|
|
244
|
+
exit,
|
|
245
|
+
selected,
|
|
246
|
+
isSelected,
|
|
247
|
+
toggle,
|
|
248
|
+
selectAll,
|
|
249
|
+
clear,
|
|
250
|
+
allSelected,
|
|
251
|
+
count,
|
|
252
|
+
total,
|
|
253
|
+
max,
|
|
254
|
+
haptics,
|
|
255
|
+
sheet: false
|
|
256
|
+
}), [active, enter, exit, selected, isSelected, toggle, selectAll, clear, allSelected, count, total, max, haptics]);
|
|
257
|
+
return /*#__PURE__*/_jsx(SelectionModeContext.Provider, {
|
|
258
|
+
value: context,
|
|
259
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
260
|
+
style: FILL,
|
|
261
|
+
className: className,
|
|
262
|
+
...props,
|
|
263
|
+
children: textChildren(children)
|
|
264
|
+
})
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* -------------------------------------------------------------------------- *
|
|
269
|
+
* Indicator
|
|
270
|
+
* -------------------------------------------------------------------------- */
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The circle at the left of a row.
|
|
274
|
+
*
|
|
275
|
+
* Round rather than square, and that is the convention doing real work: a
|
|
276
|
+
* square box is a form control the reader is filling in, a round one is a thing
|
|
277
|
+
* they are picking out of a list. `Checkbox` is the former and stays that way.
|
|
278
|
+
*
|
|
279
|
+
* `Item` draws one for you. This is exported for a row that wants it somewhere
|
|
280
|
+
* else — over a photo's corner, at the end instead of the start.
|
|
281
|
+
*/
|
|
282
|
+
function SelectionModeIndicator({
|
|
283
|
+
className,
|
|
284
|
+
value
|
|
285
|
+
}) {
|
|
286
|
+
const {
|
|
287
|
+
isSelected
|
|
288
|
+
} = useSelectionMode();
|
|
289
|
+
const row = useContext(SelectionModeItemContext);
|
|
290
|
+
const target = value ?? row?.value;
|
|
291
|
+
const selected = target !== undefined && isSelected(target);
|
|
292
|
+
const reducedMotion = useReducedMotion();
|
|
293
|
+
const progress = useSharedValue(selected ? 1 : 0);
|
|
294
|
+
const tickColor = useCSSVariable('--color-primary-foreground');
|
|
295
|
+
const slots = selectionVariants({
|
|
296
|
+
selected
|
|
297
|
+
});
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
if (reducedMotion) {
|
|
300
|
+
progress.value = selected ? 1 : 0;
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
progress.value = selected ? withSpring(1, TICK_SPRING) : withTiming(0, {
|
|
304
|
+
duration: 120
|
|
305
|
+
});
|
|
306
|
+
}, [selected, reducedMotion, progress]);
|
|
307
|
+
const fillStyle = useAnimatedStyle(() => ({
|
|
308
|
+
opacity: progress.value,
|
|
309
|
+
transform: [{
|
|
310
|
+
scale: 0.6 + progress.value * 0.4
|
|
311
|
+
}]
|
|
312
|
+
}));
|
|
313
|
+
return /*#__PURE__*/_jsx(View, {
|
|
314
|
+
className: cn(slots.circle(), className),
|
|
315
|
+
children: /*#__PURE__*/_jsx(Animated.View, {
|
|
316
|
+
style: fillStyle,
|
|
317
|
+
className: slots.fill(),
|
|
318
|
+
children: /*#__PURE__*/_jsx(CheckIcon, {
|
|
319
|
+
size: 14,
|
|
320
|
+
color: typeof tickColor === 'string' ? tickColor : '#fff'
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* -------------------------------------------------------------------------- *
|
|
327
|
+
* Item
|
|
328
|
+
* -------------------------------------------------------------------------- */
|
|
329
|
+
|
|
330
|
+
/** What an indicator inside a row needs to know, without being told twice. */
|
|
331
|
+
const SelectionModeItemContext = /*#__PURE__*/createContext(null);
|
|
332
|
+
/**
|
|
333
|
+
* One row, with the circle in front of it.
|
|
334
|
+
*
|
|
335
|
+
* The press behaviour is the whole component: off mode, a press is the row's
|
|
336
|
+
* own and a long press turns the mode on with this row picked; in it, a press
|
|
337
|
+
* picks and unpicks and the row's own press is unreachable. Two meanings for
|
|
338
|
+
* one gesture is exactly why the mode has to be visible from the header.
|
|
339
|
+
*/
|
|
340
|
+
function SelectionModeItem({
|
|
341
|
+
className,
|
|
342
|
+
value,
|
|
343
|
+
onPress,
|
|
344
|
+
disabled = false,
|
|
345
|
+
alwaysShowIndicator = false,
|
|
346
|
+
indicator = 'leading',
|
|
347
|
+
children,
|
|
348
|
+
...props
|
|
349
|
+
}) {
|
|
350
|
+
const {
|
|
351
|
+
active,
|
|
352
|
+
enter,
|
|
353
|
+
toggle,
|
|
354
|
+
isSelected,
|
|
355
|
+
sheet
|
|
356
|
+
} = useSelectionMode();
|
|
357
|
+
const selected = isSelected(value);
|
|
358
|
+
const showing = alwaysShowIndicator || active;
|
|
359
|
+
const reducedMotion = useReducedMotion();
|
|
360
|
+
const context = useMemo(() => ({
|
|
361
|
+
value
|
|
362
|
+
}), [value]);
|
|
363
|
+
return /*#__PURE__*/_jsx(SelectionModeItemContext.Provider, {
|
|
364
|
+
value: context,
|
|
365
|
+
children: /*#__PURE__*/_jsx(Pressable, {
|
|
366
|
+
accessibilityRole: showing ? 'checkbox' : 'button',
|
|
367
|
+
accessibilityState: showing ? {
|
|
368
|
+
checked: selected,
|
|
369
|
+
disabled
|
|
370
|
+
} : {
|
|
371
|
+
disabled
|
|
372
|
+
}
|
|
373
|
+
// The row is unreachable in selection mode, so a screen reader is told
|
|
374
|
+
// what pressing it does now rather than what it used to do.
|
|
375
|
+
,
|
|
376
|
+
accessibilityHint: showing ? undefined : disabled ? undefined : 'Long press to select',
|
|
377
|
+
disabled: disabled && showing,
|
|
378
|
+
onPress: () => {
|
|
379
|
+
if (disabled) return;
|
|
380
|
+
if (showing) toggle(value);else onPress?.();
|
|
381
|
+
},
|
|
382
|
+
onLongPress: sheet ? undefined : () => {
|
|
383
|
+
if (disabled || showing) return;
|
|
384
|
+
enter(value);
|
|
385
|
+
},
|
|
386
|
+
className: cn(indicator === 'leading' ? 'flex-row items-center gap-3 px-4 py-2.5' : '', className),
|
|
387
|
+
...props,
|
|
388
|
+
children: indicator === 'leading' ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
389
|
+
children: [showing ? /*#__PURE__*/_jsx(Animated.View, {
|
|
390
|
+
entering: reducedMotion ? undefined : FadeIn.duration(REVEAL_DURATION),
|
|
391
|
+
exiting: reducedMotion ? undefined : FadeOut.duration(REVEAL_DURATION),
|
|
392
|
+
children: /*#__PURE__*/_jsx(SelectionModeIndicator, {
|
|
393
|
+
value: value
|
|
394
|
+
})
|
|
395
|
+
}) : null, /*#__PURE__*/_jsx(View, {
|
|
396
|
+
style: {
|
|
397
|
+
flexGrow: 1,
|
|
398
|
+
flexShrink: 1,
|
|
399
|
+
minWidth: 0
|
|
400
|
+
},
|
|
401
|
+
children: textChildren(children)
|
|
402
|
+
})]
|
|
403
|
+
}) : indicator === 'ring' && showing ? /*#__PURE__*/_jsx(View, {
|
|
404
|
+
className: selectionVariants({
|
|
405
|
+
selected
|
|
406
|
+
}).ring(),
|
|
407
|
+
children: textChildren(children)
|
|
408
|
+
}) : textChildren(children)
|
|
409
|
+
})
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* -------------------------------------------------------------------------- *
|
|
414
|
+
* Group
|
|
415
|
+
* -------------------------------------------------------------------------- */
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* A rounded card holding a run of items.
|
|
419
|
+
*
|
|
420
|
+
* Grouping is what makes a sheet of choices readable: one card of options with
|
|
421
|
+
* hairlines between them reads as a set, and the same rows loose on the sheet's
|
|
422
|
+
* background read as a list that has not finished loading. It is also what the
|
|
423
|
+
* platform's own sheets do.
|
|
424
|
+
*
|
|
425
|
+
* Stacked by default, with a rule between each item. Pass `columns` for a grid.
|
|
426
|
+
*/
|
|
427
|
+
function SelectionModeGroup({
|
|
428
|
+
className,
|
|
429
|
+
columns,
|
|
430
|
+
gap = 12,
|
|
431
|
+
separators = true,
|
|
432
|
+
children,
|
|
433
|
+
style,
|
|
434
|
+
...props
|
|
435
|
+
}) {
|
|
436
|
+
const items = Children.toArray(children).filter(Boolean);
|
|
437
|
+
const slots = selectionVariants({});
|
|
438
|
+
if (columns && columns > 0) {
|
|
439
|
+
/*
|
|
440
|
+
* The gap is padding inside each cell, not `gap` on the row.
|
|
441
|
+
*
|
|
442
|
+
* A row of cells `100 / columns` wide with a gap between them is wider than
|
|
443
|
+
* the row by the gaps, so the last column wraps and the grid loses a
|
|
444
|
+
* column. Padding inside the cell keeps every cell an exact share of the
|
|
445
|
+
* width, and the negative margin cancels the outer half so the grid still
|
|
446
|
+
* sits flush against whatever it is in.
|
|
447
|
+
*/
|
|
448
|
+
const half = gap / 2;
|
|
449
|
+
return /*#__PURE__*/_jsx(View, {
|
|
450
|
+
style: [{
|
|
451
|
+
flexDirection: 'row',
|
|
452
|
+
flexWrap: 'wrap',
|
|
453
|
+
margin: -half
|
|
454
|
+
}, style],
|
|
455
|
+
className: className,
|
|
456
|
+
...props,
|
|
457
|
+
children: items.map((item, index) => /*#__PURE__*/_jsx(View, {
|
|
458
|
+
style: {
|
|
459
|
+
width: `${100 / columns}%`,
|
|
460
|
+
padding: half
|
|
461
|
+
},
|
|
462
|
+
children: item
|
|
463
|
+
}, index))
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return /*#__PURE__*/_jsx(View, {
|
|
467
|
+
className: cn(slots.group(), className),
|
|
468
|
+
style: style,
|
|
469
|
+
...props,
|
|
470
|
+
children: items.map((item, index) => /*#__PURE__*/_jsxs(View, {
|
|
471
|
+
children: [separators && index > 0 ?
|
|
472
|
+
/*#__PURE__*/
|
|
473
|
+
// Inset from the left so the rule starts under the text rather than
|
|
474
|
+
// under the circle, which is what a grouped list does.
|
|
475
|
+
_jsx(View, {
|
|
476
|
+
className: "ml-14 h-px bg-border"
|
|
477
|
+
}) : null, item]
|
|
478
|
+
}, index))
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* -------------------------------------------------------------------------- *
|
|
483
|
+
* Header
|
|
484
|
+
* -------------------------------------------------------------------------- */
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* The bar that says the mode is on: a way out, how many are picked, and all
|
|
488
|
+
* of them at once.
|
|
489
|
+
*
|
|
490
|
+
* Rendered only while the mode is on, and it is the thing that makes the mode
|
|
491
|
+
* legible — a list whose rows have quietly changed what a tap does, with no
|
|
492
|
+
* banner saying so, is a list that loses somebody's work.
|
|
493
|
+
*/
|
|
494
|
+
function SelectionModeHeader({
|
|
495
|
+
className,
|
|
496
|
+
title = 'Select',
|
|
497
|
+
hideSelectAll = false,
|
|
498
|
+
children,
|
|
499
|
+
...props
|
|
500
|
+
}) {
|
|
501
|
+
const {
|
|
502
|
+
active,
|
|
503
|
+
exit,
|
|
504
|
+
count,
|
|
505
|
+
total,
|
|
506
|
+
allSelected,
|
|
507
|
+
selectAll,
|
|
508
|
+
clear,
|
|
509
|
+
sheet
|
|
510
|
+
} = useSelectionMode();
|
|
511
|
+
const reducedMotion = useReducedMotion();
|
|
512
|
+
const slots = selectionVariants({});
|
|
513
|
+
if (!active) return null;
|
|
514
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
515
|
+
entering: reducedMotion ? undefined : FadeIn.duration(REVEAL_DURATION),
|
|
516
|
+
exiting: reducedMotion ? undefined : FadeOut.duration(REVEAL_DURATION),
|
|
517
|
+
className: cn(slots.header(), className),
|
|
518
|
+
...props,
|
|
519
|
+
children: children ? textChildren(children) : /*#__PURE__*/_jsxs(_Fragment, {
|
|
520
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
521
|
+
className: "w-20 items-start",
|
|
522
|
+
children: sheet ? null : /*#__PURE__*/_jsx(Pressable, {
|
|
523
|
+
accessibilityRole: "button",
|
|
524
|
+
accessibilityLabel: "Cancel selection",
|
|
525
|
+
onPress: exit,
|
|
526
|
+
hitSlop: 12,
|
|
527
|
+
className: slots.close(),
|
|
528
|
+
children: /*#__PURE__*/_jsx(XIcon, {
|
|
529
|
+
size: 20
|
|
530
|
+
})
|
|
531
|
+
})
|
|
532
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
533
|
+
numberOfLines: 1,
|
|
534
|
+
className: slots.title(),
|
|
535
|
+
children: count > 0 ? `${title} (${count})` : title
|
|
536
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
537
|
+
className: "w-20 items-end",
|
|
538
|
+
children: hideSelectAll || total === 0 ? null : /*#__PURE__*/_jsx(Pressable, {
|
|
539
|
+
accessibilityRole: "button",
|
|
540
|
+
accessibilityLabel: allSelected ? 'Clear selection' : 'Select all',
|
|
541
|
+
accessibilityState: {
|
|
542
|
+
checked: allSelected
|
|
543
|
+
},
|
|
544
|
+
onPress: allSelected ? clear : selectAll,
|
|
545
|
+
hitSlop: 12,
|
|
546
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
547
|
+
size: "sm",
|
|
548
|
+
weight: "medium",
|
|
549
|
+
className: "text-primary",
|
|
550
|
+
children: allSelected ? 'Clear' : 'All'
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
})]
|
|
554
|
+
})
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/* -------------------------------------------------------------------------- *
|
|
559
|
+
* Bar
|
|
560
|
+
* -------------------------------------------------------------------------- */
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* The actions, across the bottom of the list.
|
|
564
|
+
*
|
|
565
|
+
* Over the list rather than under it, because the list is as long as it is and
|
|
566
|
+
* a bar in the flow would be somewhere off the end of it. **Pad the bottom of
|
|
567
|
+
* your list so the last row can clear this** — nothing here can work out how
|
|
568
|
+
* tall the list is.
|
|
569
|
+
*
|
|
570
|
+
* Flush to the edge by default. A bar inset from the sides is a card floating
|
|
571
|
+
* over a list, which reads as something that arrived rather than as the mode
|
|
572
|
+
* the screen is in — and it takes width away from the actions, which are the
|
|
573
|
+
* one row of controls on screen that must not be cramped.
|
|
574
|
+
*/
|
|
575
|
+
function SelectionModeBar({
|
|
576
|
+
className,
|
|
577
|
+
placement,
|
|
578
|
+
inset = 0,
|
|
579
|
+
showWhenEmpty = false,
|
|
580
|
+
children,
|
|
581
|
+
style,
|
|
582
|
+
...props
|
|
583
|
+
}) {
|
|
584
|
+
const {
|
|
585
|
+
active,
|
|
586
|
+
count,
|
|
587
|
+
sheet
|
|
588
|
+
} = useSelectionMode();
|
|
589
|
+
const reducedMotion = useReducedMotion();
|
|
590
|
+
const floating = placement === 'floating';
|
|
591
|
+
const slots = selectionVariants({
|
|
592
|
+
placement
|
|
593
|
+
});
|
|
594
|
+
if (!active || count === 0 && !showWhenEmpty) return null;
|
|
595
|
+
|
|
596
|
+
/*
|
|
597
|
+
* In a sheet the bar is the sheet's footer: it is already at the bottom of
|
|
598
|
+
* something, already the width of it, and the footer draws the rule above it.
|
|
599
|
+
* Positioning it absolutely there would take it out of the sheet's layout and
|
|
600
|
+
* hang it over the content instead of under it.
|
|
601
|
+
*/
|
|
602
|
+
if (sheet) {
|
|
603
|
+
return /*#__PURE__*/_jsx(View, {
|
|
604
|
+
className: cn(slots.bar(), 'border-t-0', className),
|
|
605
|
+
...props,
|
|
606
|
+
children: textChildren(children)
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
const edge = floating ? inset || DEFAULT_BAR_OFFSET : 0;
|
|
610
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
611
|
+
entering: reducedMotion ? FadeIn : SlideInDown.duration(220),
|
|
612
|
+
exiting: reducedMotion ? FadeOut : SlideOutDown.duration(180),
|
|
613
|
+
style: [{
|
|
614
|
+
position: 'absolute',
|
|
615
|
+
left: edge,
|
|
616
|
+
right: edge,
|
|
617
|
+
bottom: edge
|
|
618
|
+
}, style],
|
|
619
|
+
...props,
|
|
620
|
+
children: /*#__PURE__*/_jsx(View
|
|
621
|
+
// Padding rather than a margin, so the bar's own background runs all
|
|
622
|
+
// the way to the edge and the safe area is filled rather than left as a
|
|
623
|
+
// stripe of whatever is behind it.
|
|
624
|
+
, {
|
|
625
|
+
style: floating ? undefined : {
|
|
626
|
+
paddingBottom: inset
|
|
627
|
+
},
|
|
628
|
+
className: cn(slots.bar(), className),
|
|
629
|
+
children: textChildren(children)
|
|
630
|
+
})
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/* -------------------------------------------------------------------------- *
|
|
635
|
+
* Action
|
|
636
|
+
* -------------------------------------------------------------------------- */
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* One action in the bar: a glyph with its name under it.
|
|
640
|
+
*
|
|
641
|
+
* Labelled, always. A row of bare glyphs at the bottom of a screen is a row of
|
|
642
|
+
* guesses, and one of them usually deletes something.
|
|
643
|
+
*/
|
|
644
|
+
function SelectionModeAction({
|
|
645
|
+
className,
|
|
646
|
+
icon,
|
|
647
|
+
onPress,
|
|
648
|
+
exitOnPress = false,
|
|
649
|
+
disabled = false,
|
|
650
|
+
destructive,
|
|
651
|
+
labelClassName,
|
|
652
|
+
children,
|
|
653
|
+
...props
|
|
654
|
+
}) {
|
|
655
|
+
const {
|
|
656
|
+
selected,
|
|
657
|
+
exit,
|
|
658
|
+
count
|
|
659
|
+
} = useSelectionMode();
|
|
660
|
+
const destructiveColor = useCSSVariable('--color-destructive');
|
|
661
|
+
// Nothing picked is nothing to act on, so the action is off rather than
|
|
662
|
+
// pressable-and-inert.
|
|
663
|
+
const off = disabled || count === 0;
|
|
664
|
+
const slots = selectionVariants({
|
|
665
|
+
destructive,
|
|
666
|
+
disabled: off
|
|
667
|
+
});
|
|
668
|
+
return /*#__PURE__*/_jsxs(AnimatedPressable, {
|
|
669
|
+
accessibilityRole: "button",
|
|
670
|
+
accessibilityState: {
|
|
671
|
+
disabled: off
|
|
672
|
+
},
|
|
673
|
+
disabled: off,
|
|
674
|
+
onPress: () => {
|
|
675
|
+
onPress?.(selected);
|
|
676
|
+
if (exitOnPress) exit();
|
|
677
|
+
},
|
|
678
|
+
className: cn(slots.action(), className),
|
|
679
|
+
...props,
|
|
680
|
+
children: [/*#__PURE__*/_jsx(IconColorProvider, {
|
|
681
|
+
color: destructive && typeof destructiveColor === 'string' ? destructiveColor : undefined,
|
|
682
|
+
children: icon
|
|
683
|
+
}), textChildren(children, text => /*#__PURE__*/_jsx(Text, {
|
|
684
|
+
className: cn(slots.actionLabel(), labelClassName),
|
|
685
|
+
children: text
|
|
686
|
+
}))]
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* -------------------------------------------------------------------------- *
|
|
691
|
+
* Sheet
|
|
692
|
+
* -------------------------------------------------------------------------- */
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* The whole selection, presented in a bottom sheet.
|
|
696
|
+
*
|
|
697
|
+
* A picker rather than a mode. The list on a screen has to be *turned into* one
|
|
698
|
+
* you can pick from — hence the long press, the cancel and the count — but a
|
|
699
|
+
* sheet was opened in order to pick something, so it is picking from the moment
|
|
700
|
+
* it appears and there is nothing to enter or leave.
|
|
701
|
+
*
|
|
702
|
+
* What goes in it is anything: a column of friends, a grid of colours, a run of
|
|
703
|
+
* slides. `SelectionMode.Item` wraps whatever you give it, so the sheet does
|
|
704
|
+
* not need to know what it is holding.
|
|
705
|
+
*
|
|
706
|
+
* ```tsx
|
|
707
|
+
* <SelectionMode values={ids} selected={selected} onSelectedChange={setSelected}>
|
|
708
|
+
* <SelectionMode.Sheet open={open} onOpenChange={setOpen} title="Share with">
|
|
709
|
+
* {people.map((person) => (
|
|
710
|
+
* <SelectionMode.Item key={person.id} value={person.id}>
|
|
711
|
+
* <Item>…</Item>
|
|
712
|
+
* </SelectionMode.Item>
|
|
713
|
+
* ))}
|
|
714
|
+
* <SelectionMode.Bar>
|
|
715
|
+
* <SelectionMode.Action icon={<SendIcon size={20} />} onPress={share}>Send</SelectionMode.Action>
|
|
716
|
+
* </SelectionMode.Bar>
|
|
717
|
+
* </SelectionMode.Sheet>
|
|
718
|
+
* </SelectionMode>
|
|
719
|
+
* ```
|
|
720
|
+
*/
|
|
721
|
+
function SelectionModeSheet({
|
|
722
|
+
className,
|
|
723
|
+
open,
|
|
724
|
+
defaultOpen,
|
|
725
|
+
onOpenChange,
|
|
726
|
+
title = 'Select',
|
|
727
|
+
hideSelectAll = false,
|
|
728
|
+
size = 'half',
|
|
729
|
+
children
|
|
730
|
+
}) {
|
|
731
|
+
const parent = useSelectionMode();
|
|
732
|
+
|
|
733
|
+
/*
|
|
734
|
+
* The bar is pulled out of the children and put in the sheet's footer,
|
|
735
|
+
* wherever it was written. A footer is a place in the sheet's layout rather
|
|
736
|
+
* than a thing you can position into from the middle of the body — and
|
|
737
|
+
* writing the bar last, after the items, is how it reads.
|
|
738
|
+
*/
|
|
739
|
+
const {
|
|
740
|
+
bar,
|
|
741
|
+
rest
|
|
742
|
+
} = useMemo(() => {
|
|
743
|
+
const others = [];
|
|
744
|
+
let found = null;
|
|
745
|
+
Children.forEach(children, child => {
|
|
746
|
+
if (/*#__PURE__*/isValidElement(child) && child.type === SelectionModeBar) found = child;else others.push(child);
|
|
747
|
+
});
|
|
748
|
+
return {
|
|
749
|
+
bar: found,
|
|
750
|
+
rest: others
|
|
751
|
+
};
|
|
752
|
+
}, [children]);
|
|
753
|
+
|
|
754
|
+
// Picking from the moment it opens: a sheet is not a mode to be entered.
|
|
755
|
+
const context = useMemo(() => ({
|
|
756
|
+
...parent,
|
|
757
|
+
active: true,
|
|
758
|
+
sheet: true
|
|
759
|
+
}), [parent]);
|
|
760
|
+
return /*#__PURE__*/_jsx(BottomSheet, {
|
|
761
|
+
open: open,
|
|
762
|
+
defaultOpen: defaultOpen,
|
|
763
|
+
onOpenChange: onOpenChange,
|
|
764
|
+
children: /*#__PURE__*/_jsx(BottomSheet.Content, {
|
|
765
|
+
size: size,
|
|
766
|
+
className: className,
|
|
767
|
+
children: /*#__PURE__*/_jsxs(SelectionModeContext.Provider, {
|
|
768
|
+
value: context,
|
|
769
|
+
children: [/*#__PURE__*/_jsx(BottomSheet.Header, {
|
|
770
|
+
children: /*#__PURE__*/_jsx(SelectionModeHeader, {
|
|
771
|
+
title: title,
|
|
772
|
+
hideSelectAll: hideSelectAll,
|
|
773
|
+
className: "h-auto border-b-0 px-0"
|
|
774
|
+
})
|
|
775
|
+
}), /*#__PURE__*/_jsx(BottomSheet.Body, {
|
|
776
|
+
contentContainerStyle: {
|
|
777
|
+
gap: 16,
|
|
778
|
+
paddingBottom: 8
|
|
779
|
+
},
|
|
780
|
+
children: textChildren(rest)
|
|
781
|
+
}), bar ? /*#__PURE__*/_jsx(BottomSheet.Footer, {
|
|
782
|
+
children: bar
|
|
783
|
+
}) : null]
|
|
784
|
+
})
|
|
785
|
+
})
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
SelectionModeRoot.displayName = 'SelectionMode';
|
|
789
|
+
SelectionModeSheet.displayName = 'SelectionMode.Sheet';
|
|
790
|
+
SelectionModeItem.displayName = 'SelectionMode.Item';
|
|
791
|
+
SelectionModeGroup.displayName = 'SelectionMode.Group';
|
|
792
|
+
SelectionModeIndicator.displayName = 'SelectionMode.Indicator';
|
|
793
|
+
SelectionModeHeader.displayName = 'SelectionMode.Header';
|
|
794
|
+
SelectionModeBar.displayName = 'SelectionMode.Bar';
|
|
795
|
+
SelectionModeAction.displayName = 'SelectionMode.Action';
|
|
796
|
+
export const SelectionMode = Object.assign(SelectionModeRoot, {
|
|
797
|
+
Sheet: SelectionModeSheet,
|
|
798
|
+
Group: SelectionModeGroup,
|
|
799
|
+
Item: SelectionModeItem,
|
|
800
|
+
Indicator: SelectionModeIndicator,
|
|
801
|
+
Header: SelectionModeHeader,
|
|
802
|
+
Bar: SelectionModeBar,
|
|
803
|
+
Action: SelectionModeAction
|
|
804
|
+
});
|
|
805
|
+
//# sourceMappingURL=index.js.map
|