panelui-native 0.51.1 → 0.53.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/README.md +2 -1
- package/lib/module/components/candlestick-chart/index.js +1161 -0
- package/lib/module/components/candlestick-chart/index.js.map +1 -0
- package/lib/module/components/combobox/index.js +73 -7
- package/lib/module/components/combobox/index.js.map +1 -1
- package/lib/module/components/context-menu/index.js +529 -0
- package/lib/module/components/context-menu/index.js.map +1 -0
- package/lib/module/components/menu/index.js +18 -10
- package/lib/module/components/menu/index.js.map +1 -1
- package/lib/module/components/popover/index.js +71 -7
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/sortable/index.js +159 -23
- package/lib/module/components/sortable/index.js.map +1 -1
- package/lib/module/components/tabs/index.js +56 -15
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/time-picker/index.js +295 -31
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/index.js +3 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/candlestick-chart/index.d.ts +278 -0
- package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/context-menu/index.d.ts +270 -0
- package/lib/typescript/src/components/context-menu/index.d.ts.map +1 -0
- package/lib/typescript/src/components/menu/index.d.ts +20 -20
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/popover/index.d.ts +51 -1
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sortable/index.d.ts +13 -3
- package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tabs/index.d.ts +34 -2
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/candlestick-chart/index.tsx +1360 -0
- package/src/components/combobox/index.tsx +84 -6
- package/src/components/context-menu/index.tsx +658 -0
- package/src/components/menu/index.tsx +17 -10
- package/src/components/popover/index.tsx +94 -6
- package/src/components/sortable/index.tsx +208 -22
- package/src/components/tabs/index.tsx +82 -16
- package/src/components/time-picker/index.tsx +330 -35
- package/src/index.ts +27 -0
|
@@ -341,7 +341,7 @@ export interface MenuBackgroundProps extends ViewProps {
|
|
|
341
341
|
* `overflow-hidden` on the panel is what keeps whatever that is inside the
|
|
342
342
|
* corner radius.
|
|
343
343
|
*/
|
|
344
|
-
function MenuBackground({ className, children, ...props }: MenuBackgroundProps) {
|
|
344
|
+
export function MenuBackground({ className, children, ...props }: MenuBackgroundProps) {
|
|
345
345
|
const { background } = menuVariants();
|
|
346
346
|
|
|
347
347
|
return (
|
|
@@ -365,7 +365,7 @@ export interface MenuLabelProps extends ViewProps {
|
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
/** Non-interactive heading over a run of rows. */
|
|
368
|
-
function MenuLabel({ className, inset, children, ...props }: MenuLabelProps) {
|
|
368
|
+
export function MenuLabel({ className, inset, children, ...props }: MenuLabelProps) {
|
|
369
369
|
const { label } = menuVariants({ inset });
|
|
370
370
|
|
|
371
371
|
return (
|
|
@@ -384,7 +384,7 @@ export interface MenuSeparatorProps extends ViewProps {
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
/** Hairline between two runs of rows. */
|
|
387
|
-
function MenuSeparator({ className, ...props }: MenuSeparatorProps) {
|
|
387
|
+
export function MenuSeparator({ className, ...props }: MenuSeparatorProps) {
|
|
388
388
|
const { separator } = menuVariants();
|
|
389
389
|
|
|
390
390
|
return (
|
|
@@ -435,7 +435,7 @@ export interface MenuItemProps extends Omit<PressableProps, 'children' | 'style'
|
|
|
435
435
|
* that a row which is disabled or which opts out via `closeOnSelect` behaves
|
|
436
436
|
* the same either way.
|
|
437
437
|
*/
|
|
438
|
-
function MenuItem({
|
|
438
|
+
export function MenuItem({
|
|
439
439
|
className,
|
|
440
440
|
children,
|
|
441
441
|
icon,
|
|
@@ -540,7 +540,7 @@ export interface MenuCheckboxItemProps extends Omit<MenuItemProps, 'icon' | 'ins
|
|
|
540
540
|
* deliberately so: a set of toggles is nearly always set more than one at a
|
|
541
541
|
* time, and closing after each one turns three taps into six.
|
|
542
542
|
*/
|
|
543
|
-
function MenuCheckboxItem({
|
|
543
|
+
export function MenuCheckboxItem({
|
|
544
544
|
checked = false,
|
|
545
545
|
onCheckedChange,
|
|
546
546
|
onSelect,
|
|
@@ -592,7 +592,7 @@ export interface MenuRadioGroupProps extends ViewProps {
|
|
|
592
592
|
}
|
|
593
593
|
|
|
594
594
|
/** A run of rows of which exactly one is chosen. */
|
|
595
|
-
function MenuRadioGroup({ value, onValueChange, className, children, ...props }: MenuRadioGroupProps) {
|
|
595
|
+
export function MenuRadioGroup({ value, onValueChange, className, children, ...props }: MenuRadioGroupProps) {
|
|
596
596
|
const context = useMemo(
|
|
597
597
|
() => ({ value, select: (next: string) => onValueChange?.(next) }),
|
|
598
598
|
[value, onValueChange]
|
|
@@ -616,7 +616,7 @@ export interface MenuRadioItemProps extends Omit<MenuItemProps, 'icon' | 'inset'
|
|
|
616
616
|
}
|
|
617
617
|
|
|
618
618
|
/** One option inside a `Menu.RadioGroup`. */
|
|
619
|
-
function MenuRadioItem({
|
|
619
|
+
export function MenuRadioItem({
|
|
620
620
|
value,
|
|
621
621
|
indicator = 'check',
|
|
622
622
|
onSelect,
|
|
@@ -694,7 +694,7 @@ export interface MenuSubProps {
|
|
|
694
694
|
}
|
|
695
695
|
|
|
696
696
|
/** Groups a `Menu.SubTrigger` with the rows it reveals. */
|
|
697
|
-
function MenuSub({ children, defaultOpen = false, open, onOpenChange }: MenuSubProps) {
|
|
697
|
+
export function MenuSub({ children, defaultOpen = false, open, onOpenChange }: MenuSubProps) {
|
|
698
698
|
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
699
699
|
const isControlled = open !== undefined;
|
|
700
700
|
const resolved = isControlled ? open : internalOpen;
|
|
@@ -723,7 +723,7 @@ export interface MenuSubTriggerProps
|
|
|
723
723
|
* point down once open, so the row states which way its rows will appear
|
|
724
724
|
* rather than only that it has some.
|
|
725
725
|
*/
|
|
726
|
-
function MenuSubTrigger({ className, children, icon, onSelect, ...props }: MenuSubTriggerProps) {
|
|
726
|
+
export function MenuSubTrigger({ className, children, icon, onSelect, ...props }: MenuSubTriggerProps) {
|
|
727
727
|
const { open, toggle } = useMenuSub('Menu.SubTrigger');
|
|
728
728
|
const sign = useDirectionSign();
|
|
729
729
|
const direction = useDirection();
|
|
@@ -790,7 +790,7 @@ export interface MenuSubContentProps extends ViewProps {
|
|
|
790
790
|
* would otherwise report the animated height back, and the panel would settle
|
|
791
791
|
* at whatever it happened to measure on the first frame.
|
|
792
792
|
*/
|
|
793
|
-
function MenuSubContent({ className, children, ...props }: MenuSubContentProps) {
|
|
793
|
+
export function MenuSubContent({ className, children, ...props }: MenuSubContentProps) {
|
|
794
794
|
const { open } = useMenuSub('Menu.SubContent');
|
|
795
795
|
const reducedMotion = useReducedMotion();
|
|
796
796
|
const [height, setHeight] = useState(0);
|
|
@@ -841,6 +841,13 @@ MenuSub.displayName = 'Menu.Sub';
|
|
|
841
841
|
MenuSubTrigger.displayName = 'Menu.SubTrigger';
|
|
842
842
|
MenuSubContent.displayName = 'Menu.SubContent';
|
|
843
843
|
|
|
844
|
+
/*
|
|
845
|
+
* The parts above are exported individually as well as hung off `Menu`, and the
|
|
846
|
+
* reason is `ContextMenu`: it presents these same components as its own rows, so
|
|
847
|
+
* the emitted type declarations for it have to be able to name them. `Menu` is
|
|
848
|
+
* still the only thing the package exports, so nothing about how these are
|
|
849
|
+
* reached from outside has changed.
|
|
850
|
+
*/
|
|
844
851
|
export const Menu = Object.assign(MenuRoot, {
|
|
845
852
|
Trigger: MenuTrigger,
|
|
846
853
|
Content: MenuContent,
|
|
@@ -72,17 +72,37 @@ const DEFAULT_OFFSET = 8;
|
|
|
72
72
|
const SCREEN_MARGIN = 12;
|
|
73
73
|
/** Side of the arrow square before it is rotated 45°. */
|
|
74
74
|
const ARROW_SIZE = 12;
|
|
75
|
+
/**
|
|
76
|
+
* Headroom the sheet presentation leaves for the close button.
|
|
77
|
+
*
|
|
78
|
+
* That button floats over the sheet's top-end corner rather than sitting in the
|
|
79
|
+
* flow, so nothing below it is pushed out of its way. The sheet's own padding
|
|
80
|
+
* and grabber put the first child 24 points down; the button's lower edge is at
|
|
81
|
+
* 44. This is the difference, plus a little air — enough that a panel's first
|
|
82
|
+
* row clears the button instead of being drawn under it.
|
|
83
|
+
*/
|
|
84
|
+
const SHEET_CLOSE_CLEARANCE = 24;
|
|
75
85
|
|
|
76
86
|
export type PopoverPlacement = 'top' | 'bottom' | 'left' | 'right';
|
|
77
87
|
export type PopoverAlign = 'start' | 'center' | 'end';
|
|
78
88
|
|
|
79
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The rectangle the panel is placed against, in window coordinates.
|
|
91
|
+
*
|
|
92
|
+
* Normally the trigger's own bounds. A zero-sized rect is meaningful too: it
|
|
93
|
+
* anchors the panel to a single point, which is what a menu opened by a long
|
|
94
|
+
* press on arbitrary content needs — there the interesting position is where
|
|
95
|
+
* the finger landed, not the bounds of whatever it landed on.
|
|
96
|
+
*/
|
|
97
|
+
export interface PopoverAnchorRect {
|
|
80
98
|
x: number;
|
|
81
99
|
y: number;
|
|
82
100
|
width: number;
|
|
83
101
|
height: number;
|
|
84
102
|
}
|
|
85
103
|
|
|
104
|
+
type TriggerRect = PopoverAnchorRect;
|
|
105
|
+
|
|
86
106
|
interface PopoverContextValue {
|
|
87
107
|
open: boolean;
|
|
88
108
|
setOpen: (open: boolean) => void;
|
|
@@ -110,6 +130,38 @@ function usePopover(component: string): PopoverContextValue {
|
|
|
110
130
|
return context;
|
|
111
131
|
}
|
|
112
132
|
|
|
133
|
+
export interface PopoverAnchorControls {
|
|
134
|
+
open: boolean;
|
|
135
|
+
setOpen: (open: boolean) => void;
|
|
136
|
+
/**
|
|
137
|
+
* Place the panel against an explicit rect rather than against a measured
|
|
138
|
+
* trigger. Pass a zero-sized rect to anchor it to a point.
|
|
139
|
+
*/
|
|
140
|
+
anchorTo: (rect: PopoverAnchorRect) => void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* For a trigger that opens the panel on something other than a plain press, or
|
|
145
|
+
* anchors it to something other than its own bounds.
|
|
146
|
+
*
|
|
147
|
+
* `Popover.Trigger` covers the ordinary case — press the thing, measure the
|
|
148
|
+
* thing, open next to it. A component built on this one may need neither half
|
|
149
|
+
* of that: a context menu opens on a long press and belongs at the point the
|
|
150
|
+
* finger landed. Rather than have it own a second copy of the placing,
|
|
151
|
+
* flipping and edge-clamping this file already does, it borrows them by
|
|
152
|
+
* setting the anchor itself.
|
|
153
|
+
*
|
|
154
|
+
* Only useful inside a `Popover`, which is the same rule every other part here
|
|
155
|
+
* follows.
|
|
156
|
+
*/
|
|
157
|
+
export function usePopoverAnchor(component: string): PopoverAnchorControls {
|
|
158
|
+
const { open, setOpen, setTrigger } = usePopover(component);
|
|
159
|
+
return useMemo(
|
|
160
|
+
() => ({ open, setOpen, anchorTo: setTrigger }),
|
|
161
|
+
[open, setOpen, setTrigger]
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
113
165
|
export interface PopoverProps {
|
|
114
166
|
children: ReactNode;
|
|
115
167
|
/** Controlled open state. */
|
|
@@ -271,6 +323,18 @@ export interface PopoverContentProps extends ViewProps {
|
|
|
271
323
|
* not, so it is safe to pass either way.
|
|
272
324
|
*/
|
|
273
325
|
blur?: boolean;
|
|
326
|
+
/**
|
|
327
|
+
* Dim the screen behind the panel.
|
|
328
|
+
*
|
|
329
|
+
* Off by default: a popover is a panel *beside* something, and dimming the
|
|
330
|
+
* page says the thing behind it has stopped being available — which is a
|
|
331
|
+
* dialog's claim, not a popover's. Worth turning on when the panel is the
|
|
332
|
+
* only thing that matters while it is up, which is what a menu opened on the
|
|
333
|
+
* content itself is. Ignored under `blur`, which draws its own dim.
|
|
334
|
+
*/
|
|
335
|
+
scrim?: boolean;
|
|
336
|
+
/** The dim's classes, when `scrim` is set. */
|
|
337
|
+
scrimClassName?: string;
|
|
274
338
|
children?: ReactNode;
|
|
275
339
|
}
|
|
276
340
|
|
|
@@ -288,6 +352,8 @@ function PopoverContent({
|
|
|
288
352
|
background,
|
|
289
353
|
dismissible = true,
|
|
290
354
|
blur = false,
|
|
355
|
+
scrim = false,
|
|
356
|
+
scrimClassName = 'bg-black/30',
|
|
291
357
|
children,
|
|
292
358
|
onLayout: onLayoutProp,
|
|
293
359
|
style,
|
|
@@ -342,6 +408,23 @@ function PopoverContent({
|
|
|
342
408
|
? Math.min(Math.max(requestedWidth, minWidth), available)
|
|
343
409
|
: requestedWidth;
|
|
344
410
|
|
|
411
|
+
/*
|
|
412
|
+
* The floor also has to reach a panel sized to its own contents, which is the
|
|
413
|
+
* case it matters most in — a panel with a width already knows how wide it is.
|
|
414
|
+
*
|
|
415
|
+
* Folding it into `width` would be wrong: that pins the panel open at exactly
|
|
416
|
+
* the floor and stops it growing for content that needs more. It is a real
|
|
417
|
+
* minimum instead, left off when there is nothing to enforce so a content-fit
|
|
418
|
+
* panel keeps shrinking to fit.
|
|
419
|
+
*
|
|
420
|
+
* Without this a `content-fit` panel takes its width from whatever inside it
|
|
421
|
+
* is *not* flexible. A row of a flexible label and a fixed glyph collapses to
|
|
422
|
+
* the glyph, and the panel comes up as a strip of icons with the words
|
|
423
|
+
* squeezed out of it.
|
|
424
|
+
*/
|
|
425
|
+
const resolvedMinWidth =
|
|
426
|
+
minWidth === undefined ? undefined : Math.min(minWidth, available);
|
|
427
|
+
|
|
345
428
|
/*
|
|
346
429
|
* The cap is what keeps a tall panel reachable. `place` clamps the panel
|
|
347
430
|
* inside the bounds, but a panel taller than the bounds cannot be clamped
|
|
@@ -434,9 +517,7 @@ function PopoverContent({
|
|
|
434
517
|
width === 'full' || width === 'trigger' ? { width: '100%' } : null,
|
|
435
518
|
typeof width === 'number' ? { width } : null,
|
|
436
519
|
minWidth === undefined ? null : { minWidth },
|
|
437
|
-
|
|
438
|
-
// corner of the sheet rather than sitting in the flow.
|
|
439
|
-
{ paddingTop: 8 },
|
|
520
|
+
{ paddingTop: SHEET_CLOSE_CLEARANCE },
|
|
440
521
|
style,
|
|
441
522
|
]}
|
|
442
523
|
>
|
|
@@ -458,8 +539,14 @@ function PopoverContent({
|
|
|
458
539
|
<PopoverContext.Provider value={context}>
|
|
459
540
|
<View className="absolute inset-0">
|
|
460
541
|
{/* A popover does not dim the screen by default — the backdrop is
|
|
461
|
-
there only to catch the outside tap. `blur` opts into a frost
|
|
462
|
-
|
|
542
|
+
there only to catch the outside tap. `blur` opts into a frost,
|
|
543
|
+
`scrim` into a plain dim; the frost already draws one of its own,
|
|
544
|
+
so asking for both is not two dims. */}
|
|
545
|
+
{blur ? (
|
|
546
|
+
<Scrim blur intensity={20} dimClassName={scrimClassName} />
|
|
547
|
+
) : scrim ? (
|
|
548
|
+
<Scrim dimClassName={scrimClassName} />
|
|
549
|
+
) : null}
|
|
463
550
|
<Pressable
|
|
464
551
|
accessibilityLabel="Close"
|
|
465
552
|
className="absolute inset-0"
|
|
@@ -486,6 +573,7 @@ function PopoverContent({
|
|
|
486
573
|
maxWidth: bounds.right - bounds.left,
|
|
487
574
|
maxHeight: resolvedMaxHeight,
|
|
488
575
|
width: resolvedWidth,
|
|
576
|
+
minWidth: resolvedMinWidth,
|
|
489
577
|
}}
|
|
490
578
|
>
|
|
491
579
|
<Animated.View
|
|
@@ -76,6 +76,7 @@ import Animated, {
|
|
|
76
76
|
scrollTo,
|
|
77
77
|
useAnimatedRef,
|
|
78
78
|
useAnimatedStyle,
|
|
79
|
+
useDerivedValue,
|
|
79
80
|
useFrameCallback,
|
|
80
81
|
useReducedMotion,
|
|
81
82
|
useScrollViewOffset,
|
|
@@ -93,8 +94,14 @@ import { impactKnock, selectionTick } from '../../utils/haptics';
|
|
|
93
94
|
* Rows getting out of the way of the one being carried. Quick, because they
|
|
94
95
|
* are answering a finger that has already moved — a neighbour that ambles into
|
|
95
96
|
* its new slot reads as the list struggling to keep up with the drag.
|
|
97
|
+
*
|
|
98
|
+
* Critically damped, and stiff. Both were wrong before: the spring overshot its
|
|
99
|
+
* slot and spent the rest of a third of a second coming back, so a row the
|
|
100
|
+
* finger had already passed was still visibly moving. A row getting out of the
|
|
101
|
+
* way has nothing to express by bouncing — it is not the thing being carried,
|
|
102
|
+
* and the fastest way to say "your place is free" is to be out of it.
|
|
96
103
|
*/
|
|
97
|
-
const DISPLACE = { damping:
|
|
104
|
+
const DISPLACE = { damping: 28, stiffness: 400, mass: 0.5 } as const;
|
|
98
105
|
|
|
99
106
|
/**
|
|
100
107
|
* Settles a row into its slot. Stiffer and less bouncy than the library's
|
|
@@ -222,34 +229,114 @@ function slotCenter(
|
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
/**
|
|
225
|
-
*
|
|
232
|
+
* The order after a row has moved, with pinned rows left where they were.
|
|
233
|
+
*
|
|
234
|
+
* The move is applied first, to the whole list, so a carried row can be dragged
|
|
235
|
+
* *past* a pinned one — refusing the move instead would make a pinned row a
|
|
236
|
+
* wall, and a row that holds its place is not the same as a row nothing may
|
|
237
|
+
* cross. The pinned ids are then put back at the indices they occupy in the
|
|
238
|
+
* laid-out order, and everything else falls into the slots that are left, in
|
|
239
|
+
* the order the move produced.
|
|
240
|
+
*
|
|
241
|
+
* `laid` rather than `list` is what the fixed indices are read from, because
|
|
242
|
+
* that is the one order a pinned row is guaranteed to be correctly placed in:
|
|
243
|
+
* it is where it was rendered, and holding its slot is the whole point.
|
|
244
|
+
*/
|
|
245
|
+
function moveWithPinned(
|
|
246
|
+
list: readonly string[],
|
|
247
|
+
laid: readonly string[],
|
|
248
|
+
pinned: Record<string, boolean>,
|
|
249
|
+
id: string,
|
|
250
|
+
from: number,
|
|
251
|
+
to: number
|
|
252
|
+
): string[] {
|
|
253
|
+
'worklet';
|
|
254
|
+
const moved = [...list];
|
|
255
|
+
moved.splice(from, 1);
|
|
256
|
+
moved.splice(to, 0, id);
|
|
257
|
+
|
|
258
|
+
const next: (string | undefined)[] = [];
|
|
259
|
+
let anyPinned = false;
|
|
260
|
+
for (let i = 0; i < moved.length; i += 1) next.push(undefined);
|
|
261
|
+
for (let i = 0; i < laid.length && i < next.length; i += 1) {
|
|
262
|
+
const at = laid[i];
|
|
263
|
+
if (at !== undefined && pinned[at]) {
|
|
264
|
+
next[i] = at;
|
|
265
|
+
anyPinned = true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!anyPinned) return moved;
|
|
269
|
+
|
|
270
|
+
const free: string[] = [];
|
|
271
|
+
for (let i = 0; i < moved.length; i += 1) {
|
|
272
|
+
const at = moved[i];
|
|
273
|
+
if (at !== undefined && !pinned[at]) free.push(at);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const result: string[] = [];
|
|
277
|
+
let f = 0;
|
|
278
|
+
for (let i = 0; i < next.length; i += 1) {
|
|
279
|
+
const held = next[i];
|
|
280
|
+
if (held !== undefined) {
|
|
281
|
+
result.push(held);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const take = free[f];
|
|
285
|
+
f += 1;
|
|
286
|
+
if (take !== undefined) result.push(take);
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Where the dragged row belongs now, given where its edges have reached.
|
|
226
293
|
*
|
|
227
294
|
* It walks outwards from the row's current slot and stops at the first
|
|
228
|
-
* neighbour it has
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
295
|
+
* neighbour it has not reached, rather than scanning the whole list for the
|
|
296
|
+
* nearest slot. The difference shows up with rows of unequal height: scanning
|
|
297
|
+
* can hand back a slot two places away that happens to be closer, which reads
|
|
298
|
+
* as the row skipping one.
|
|
299
|
+
*
|
|
300
|
+
* What counts as reaching a neighbour is the *leading edge* of the carried row
|
|
301
|
+
* against that neighbour's middle — its bottom edge going down, its top edge
|
|
302
|
+
* going up. Comparing middle against middle, as this used to, means the finger
|
|
303
|
+
* has to travel a whole row before anything happens, because a row's middle
|
|
304
|
+
* starts a whole row away from its neighbour's: the list sat still through the
|
|
305
|
+
* first row of every drag and then moved all at once. Leading edge against
|
|
306
|
+
* middle halves that, and it is also the more natural reading — the rows get
|
|
307
|
+
* out of the way once the row being carried is over them, not once it is past
|
|
308
|
+
* them.
|
|
232
309
|
*/
|
|
233
310
|
function targetIndex(
|
|
234
311
|
order: readonly string[],
|
|
235
312
|
current: number,
|
|
236
|
-
|
|
313
|
+
top: number,
|
|
314
|
+
height: number,
|
|
237
315
|
heights: Record<string, number>,
|
|
238
316
|
gap: number
|
|
239
317
|
): number {
|
|
240
318
|
'worklet';
|
|
241
|
-
|
|
319
|
+
// Where the carried row's own slot begins, so the direction of travel is
|
|
320
|
+
// read from the row rather than from the sign of a gesture that may have
|
|
321
|
+
// changed its mind since.
|
|
322
|
+
const self = order[current];
|
|
323
|
+
const restingTop =
|
|
324
|
+
slotCenter(order, current, heights, gap) -
|
|
325
|
+
(self === undefined ? 0 : (heights[self] ?? 0)) / 2;
|
|
326
|
+
|
|
327
|
+
if (top < restingTop) {
|
|
242
328
|
let target = current;
|
|
243
329
|
for (let i = current - 1; i >= 0; i -= 1) {
|
|
244
|
-
if (
|
|
330
|
+
if (top >= slotCenter(order, i, heights, gap)) break;
|
|
245
331
|
target = i;
|
|
246
332
|
}
|
|
247
333
|
return target;
|
|
248
334
|
}
|
|
249
335
|
|
|
336
|
+
const bottom = top + height;
|
|
250
337
|
let target = current;
|
|
251
338
|
for (let i = current + 1; i < order.length; i += 1) {
|
|
252
|
-
if (
|
|
339
|
+
if (bottom <= slotCenter(order, i, heights, gap)) break;
|
|
253
340
|
target = i;
|
|
254
341
|
}
|
|
255
342
|
return target;
|
|
@@ -266,6 +353,20 @@ interface SortableContextValue {
|
|
|
266
353
|
rendered: SharedValue<string[]>;
|
|
267
354
|
/** Measured row heights, keyed by id. */
|
|
268
355
|
heights: SharedValue<Record<string, number>>;
|
|
356
|
+
/** Which ids hold their slot. Read on the UI thread while a drag resolves. */
|
|
357
|
+
pinned: SharedValue<Record<string, boolean>>;
|
|
358
|
+
/**
|
|
359
|
+
* How far each row is from where it was laid out, keyed by id.
|
|
360
|
+
*
|
|
361
|
+
* Derived once per rearrangement rather than worked out by each row for
|
|
362
|
+
* itself. Every row's style worklet re-runs on every frame of a drag — it
|
|
363
|
+
* closes over the value the carried row is riding on — so a row summing the
|
|
364
|
+
* heights above it twice per frame made the list cost the square of its
|
|
365
|
+
* length to drag, which is felt exactly when a list is long enough to be
|
|
366
|
+
* worth reordering by hand. This is invalidated by the same shared values it
|
|
367
|
+
* is built from, so a row changing height still puts it right.
|
|
368
|
+
*/
|
|
369
|
+
offsets: SharedValue<Record<string, number>>;
|
|
269
370
|
/** The row under the finger, or `null`. Also the settling row, until it lands. */
|
|
270
371
|
activeId: SharedValue<string | null>;
|
|
271
372
|
/** The active row's offset from where it was laid out. */
|
|
@@ -300,6 +401,8 @@ interface SortableContextValue {
|
|
|
300
401
|
/** Index of each id in the rendered order. */
|
|
301
402
|
indexOf: (id: string) => number;
|
|
302
403
|
measured: (id: string, height: number) => void;
|
|
404
|
+
/** Register or clear a row's hold on its slot. */
|
|
405
|
+
setPinned: (id: string, value: boolean) => void;
|
|
303
406
|
begin: (id: string) => void;
|
|
304
407
|
settled: (id: string) => void;
|
|
305
408
|
/** Move a row by whole slots — the path that is not a gesture. */
|
|
@@ -437,6 +540,7 @@ function SortableRoot({
|
|
|
437
540
|
const order = useSharedValue<string[]>(value);
|
|
438
541
|
const rendered = useSharedValue<string[]>(value);
|
|
439
542
|
const heights = useSharedValue<Record<string, number>>({});
|
|
543
|
+
const pinned = useSharedValue<Record<string, boolean>>({});
|
|
440
544
|
const activeId = useSharedValue<string | null>(null);
|
|
441
545
|
const translate = useSharedValue(0);
|
|
442
546
|
const lift = useSharedValue(0);
|
|
@@ -487,6 +591,47 @@ function SortableRoot({
|
|
|
487
591
|
[heights]
|
|
488
592
|
);
|
|
489
593
|
|
|
594
|
+
const setPinned = useCallback(
|
|
595
|
+
(id: string, next: boolean) => {
|
|
596
|
+
if (Boolean(pinned.value[id]) === next) return;
|
|
597
|
+
pinned.value = { ...pinned.value, [id]: next };
|
|
598
|
+
},
|
|
599
|
+
[pinned]
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
/*
|
|
603
|
+
* Every row's distance from where it was laid out, in one pass.
|
|
604
|
+
*
|
|
605
|
+
* Two prefix sums — one over the order being dragged, one over the order the
|
|
606
|
+
* children are actually in — and the difference between them per id. Rebuilt
|
|
607
|
+
* when a swap changes `order`, when a drop resets both, or when a row reports
|
|
608
|
+
* a new height, and at no other time; a drag that is only moving the carried
|
|
609
|
+
* row does not touch it at all.
|
|
610
|
+
*/
|
|
611
|
+
const offsets = useDerivedValue<Record<string, number>>(() => {
|
|
612
|
+
const map = heights.value;
|
|
613
|
+
const target: Record<string, number> = {};
|
|
614
|
+
const list = order.value;
|
|
615
|
+
let at = 0;
|
|
616
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
617
|
+
const id = list[i];
|
|
618
|
+
if (id === undefined) continue;
|
|
619
|
+
target[id] = at;
|
|
620
|
+
at += (map[id] ?? 0) + gap;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const result: Record<string, number> = {};
|
|
624
|
+
const laid = rendered.value;
|
|
625
|
+
at = 0;
|
|
626
|
+
for (let i = 0; i < laid.length; i += 1) {
|
|
627
|
+
const id = laid[i];
|
|
628
|
+
if (id === undefined) continue;
|
|
629
|
+
result[id] = (target[id] ?? at) - at;
|
|
630
|
+
at += (map[id] ?? 0) + gap;
|
|
631
|
+
}
|
|
632
|
+
return result;
|
|
633
|
+
}, [gap]);
|
|
634
|
+
|
|
490
635
|
/* ---------------------------------------------------------------------- */
|
|
491
636
|
/* Autoscroll */
|
|
492
637
|
/* ---------------------------------------------------------------------- */
|
|
@@ -607,6 +752,8 @@ function SortableRoot({
|
|
|
607
752
|
order,
|
|
608
753
|
rendered,
|
|
609
754
|
heights,
|
|
755
|
+
pinned,
|
|
756
|
+
offsets,
|
|
610
757
|
activeId,
|
|
611
758
|
translate,
|
|
612
759
|
lift,
|
|
@@ -623,6 +770,7 @@ function SortableRoot({
|
|
|
623
770
|
activeItem,
|
|
624
771
|
indexOf,
|
|
625
772
|
measured,
|
|
773
|
+
setPinned,
|
|
626
774
|
begin,
|
|
627
775
|
settled,
|
|
628
776
|
step,
|
|
@@ -632,6 +780,8 @@ function SortableRoot({
|
|
|
632
780
|
order,
|
|
633
781
|
rendered,
|
|
634
782
|
heights,
|
|
783
|
+
pinned,
|
|
784
|
+
offsets,
|
|
635
785
|
activeId,
|
|
636
786
|
translate,
|
|
637
787
|
lift,
|
|
@@ -648,6 +798,7 @@ function SortableRoot({
|
|
|
648
798
|
activeItem,
|
|
649
799
|
indexOf,
|
|
650
800
|
measured,
|
|
801
|
+
setPinned,
|
|
651
802
|
begin,
|
|
652
803
|
settled,
|
|
653
804
|
step,
|
|
@@ -686,10 +837,20 @@ export interface SortableItemProps extends Omit<ViewProps, 'children'> {
|
|
|
686
837
|
/**
|
|
687
838
|
* Stop this row being picked up. The others still move past it, because a
|
|
688
839
|
* row that cannot be dragged is not the same as a row that cannot be
|
|
689
|
-
* displaced —
|
|
690
|
-
*
|
|
840
|
+
* displaced — that is what `pinned` is for, and conflating the two would mean
|
|
841
|
+
* silently refusing drops that look like they worked.
|
|
691
842
|
*/
|
|
692
843
|
disabled?: boolean;
|
|
844
|
+
/**
|
|
845
|
+
* Hold this row's place in the list. It cannot be picked up, and — unlike a
|
|
846
|
+
* `disabled` row — nothing else can take its slot either: the rows being
|
|
847
|
+
* dragged reorder among the places left over, and one carried past this row
|
|
848
|
+
* goes around it rather than through it.
|
|
849
|
+
*
|
|
850
|
+
* For the row that means something by being where it is. A header, a total, a
|
|
851
|
+
* step that has to come first.
|
|
852
|
+
*/
|
|
853
|
+
pinned?: boolean;
|
|
693
854
|
/**
|
|
694
855
|
* Extra classes for the row while it is being carried, applied last. A
|
|
695
856
|
* lifted row is given an opaque surface and a shadow so it is never drawn
|
|
@@ -711,6 +872,7 @@ function SortableItem({
|
|
|
711
872
|
id,
|
|
712
873
|
children,
|
|
713
874
|
disabled = false,
|
|
875
|
+
pinned = false,
|
|
714
876
|
...props
|
|
715
877
|
}: SortableItemProps) {
|
|
716
878
|
const root = useSortableRoot('Sortable.Item');
|
|
@@ -718,6 +880,8 @@ function SortableItem({
|
|
|
718
880
|
order,
|
|
719
881
|
rendered,
|
|
720
882
|
heights,
|
|
883
|
+
pinned: pinnedIds,
|
|
884
|
+
offsets,
|
|
721
885
|
activeId,
|
|
722
886
|
translate,
|
|
723
887
|
lift,
|
|
@@ -733,13 +897,23 @@ function SortableItem({
|
|
|
733
897
|
activeItem,
|
|
734
898
|
indexOf,
|
|
735
899
|
measured,
|
|
900
|
+
setPinned,
|
|
736
901
|
begin,
|
|
737
902
|
settled,
|
|
738
903
|
step,
|
|
739
904
|
} = root;
|
|
740
905
|
|
|
741
|
-
/** A row is undraggable if
|
|
742
|
-
const locked = root.disabled || disabled;
|
|
906
|
+
/** A row is undraggable if it, the whole list, or its own pin says so. */
|
|
907
|
+
const locked = root.disabled || disabled || pinned;
|
|
908
|
+
|
|
909
|
+
/*
|
|
910
|
+
* Published to the root so the drag can read it on the UI thread. A pin is
|
|
911
|
+
* resolved while a finger is moving, where the props of a row two places away
|
|
912
|
+
* are not reachable.
|
|
913
|
+
*/
|
|
914
|
+
useEffect(() => {
|
|
915
|
+
setPinned(id, pinned);
|
|
916
|
+
}, [id, pinned, setPinned]);
|
|
743
917
|
const index = indexOf(id);
|
|
744
918
|
const isActive = activeItem === id;
|
|
745
919
|
|
|
@@ -811,13 +985,20 @@ function SortableItem({
|
|
|
811
985
|
if (current < 0) return;
|
|
812
986
|
|
|
813
987
|
const top = slotOffset(rendered.value, id, map, gap) + translate.value;
|
|
814
|
-
const
|
|
815
|
-
const to = targetIndex(list, current, center, map, gap);
|
|
988
|
+
const to = targetIndex(list, current, top, map[id] ?? 0, map, gap);
|
|
816
989
|
if (to === current) return;
|
|
817
990
|
|
|
818
|
-
const next =
|
|
819
|
-
|
|
820
|
-
|
|
991
|
+
const next = moveWithPinned(
|
|
992
|
+
list,
|
|
993
|
+
rendered.value,
|
|
994
|
+
pinnedIds.value,
|
|
995
|
+
id,
|
|
996
|
+
current,
|
|
997
|
+
to
|
|
998
|
+
);
|
|
999
|
+
// A move that only pinned rows could have absorbed leaves the order
|
|
1000
|
+
// exactly as it was, and there is nothing to feel or to redraw.
|
|
1001
|
+
if (next[current] === id) return;
|
|
821
1002
|
order.value = next;
|
|
822
1003
|
|
|
823
1004
|
if (haptics) runOnJS(notifyTick)();
|
|
@@ -914,9 +1095,14 @@ function SortableItem({
|
|
|
914
1095
|
};
|
|
915
1096
|
}
|
|
916
1097
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1098
|
+
/*
|
|
1099
|
+
* Read, not worked out. This worklet re-runs on every frame of a drag —
|
|
1100
|
+
* it closes over the value the carried row rides on — so summing the
|
|
1101
|
+
* heights above this row here, twice, made a list cost the square of its
|
|
1102
|
+
* length to drag. The root derives every row's offset in one pass instead,
|
|
1103
|
+
* and only when the arrangement actually changes.
|
|
1104
|
+
*/
|
|
1105
|
+
const offset = offsets.value[id] ?? 0;
|
|
920
1106
|
|
|
921
1107
|
/*
|
|
922
1108
|
* Only animated while a drag is in flight, and the difference is the whole
|