panelui-native 0.65.0 → 0.66.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/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/carousel/carousel-lifecycle.js +51 -0
- package/lib/module/components/carousel/carousel-lifecycle.js.map +1 -0
- package/lib/module/components/carousel/index.js +61 -24
- package/lib/module/components/carousel/index.js.map +1 -1
- package/lib/module/components/context-menu/context-menu-invocation.js +23 -0
- package/lib/module/components/context-menu/context-menu-invocation.js.map +1 -0
- package/lib/module/components/context-menu/index.js +78 -9
- package/lib/module/components/context-menu/index.js.map +1 -1
- package/lib/module/components/dialog/index.js.map +1 -1
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/flow/flow-accessibility.js +18 -2
- package/lib/module/components/flow/flow-accessibility.js.map +1 -1
- package/lib/module/components/flow/flow-identifiers.js +81 -0
- package/lib/module/components/flow/flow-identifiers.js.map +1 -0
- package/lib/module/components/flow/index.js +12 -18
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/message-scroller/index.js +17 -9
- package/lib/module/components/message-scroller/index.js.map +1 -1
- package/lib/module/components/message-scroller/message-scroller-math.js +20 -0
- package/lib/module/components/message-scroller/message-scroller-math.js.map +1 -0
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/selection-mode/index.js +26 -31
- package/lib/module/components/selection-mode/index.js.map +1 -1
- package/lib/module/components/selection-mode/selection-mode-contracts.js +48 -0
- package/lib/module/components/selection-mode/selection-mode-contracts.js.map +1 -0
- package/lib/module/components/signature/index.js +62 -20
- package/lib/module/components/signature/index.js.map +1 -1
- package/lib/module/components/signature/signature-accessibility.js +58 -0
- package/lib/module/components/signature/signature-accessibility.js.map +1 -0
- package/lib/module/components/slider/index.js +22 -18
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/scrim.js +94 -3
- package/lib/module/primitives/scrim.js.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +3 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/carousel/carousel-lifecycle.d.ts +23 -0
- package/lib/typescript/src/components/carousel/carousel-lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/components/carousel/index.d.ts +5 -2
- package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
- package/lib/typescript/src/components/context-menu/context-menu-invocation.d.ts +20 -0
- package/lib/typescript/src/components/context-menu/context-menu-invocation.d.ts.map +1 -0
- package/lib/typescript/src/components/context-menu/index.d.ts +18 -8
- package/lib/typescript/src/components/context-menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/dialog/index.d.ts +3 -0
- package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
- package/lib/typescript/src/components/drawer/index.d.ts +3 -0
- package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts +8 -0
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -1
- package/lib/typescript/src/components/flow/flow-identifiers.d.ts +24 -0
- package/lib/typescript/src/components/flow/flow-identifiers.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/index.d.ts +6 -5
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts +8 -0
- package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts.map +1 -0
- package/lib/typescript/src/components/popover/index.d.ts +3 -0
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/selection-mode/index.d.ts +6 -4
- package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
- package/lib/typescript/src/components/selection-mode/selection-mode-contracts.d.ts +18 -0
- package/lib/typescript/src/components/selection-mode/selection-mode-contracts.d.ts.map +1 -0
- package/lib/typescript/src/components/signature/index.d.ts +6 -0
- package/lib/typescript/src/components/signature/index.d.ts.map +1 -1
- package/lib/typescript/src/components/signature/signature-accessibility.d.ts +27 -0
- package/lib/typescript/src/components/signature/signature-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/scrim.d.ts +5 -3
- package/lib/typescript/src/primitives/scrim.d.ts.map +1 -1
- package/package.json +7 -4
- package/src/components/bottom-sheet/index.tsx +3 -0
- package/src/components/carousel/carousel-lifecycle.ts +74 -0
- package/src/components/carousel/index.tsx +80 -25
- package/src/components/context-menu/context-menu-invocation.ts +44 -0
- package/src/components/context-menu/index.tsx +113 -13
- package/src/components/dialog/index.tsx +3 -0
- package/src/components/drawer/index.tsx +3 -0
- package/src/components/flow/flow-accessibility.ts +16 -2
- package/src/components/flow/flow-identifiers.ts +98 -0
- package/src/components/flow/index.tsx +29 -19
- package/src/components/message-scroller/index.tsx +39 -7
- package/src/components/message-scroller/message-scroller-math.ts +24 -0
- package/src/components/popover/index.tsx +3 -0
- package/src/components/selection-mode/index.tsx +37 -30
- package/src/components/selection-mode/selection-mode-contracts.ts +53 -0
- package/src/components/signature/index.tsx +107 -20
- package/src/components/signature/signature-accessibility.ts +67 -0
- package/src/components/slider/index.tsx +28 -18
- package/src/index.ts +2 -0
- package/src/primitives/scrim.tsx +127 -7
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const FLOW_TOKEN_PREFIX = 'panelui-flow:';
|
|
2
|
+
|
|
3
|
+
export interface FlowEndpoint {
|
|
4
|
+
node: string;
|
|
5
|
+
handle?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type FlowEndpointReference = string | FlowEndpoint;
|
|
9
|
+
|
|
10
|
+
type EncodedReference = ['legacy', string] | ['endpoint', string] | ['endpoint', string, string];
|
|
11
|
+
|
|
12
|
+
function encodedReference(reference: FlowEndpointReference): EncodedReference {
|
|
13
|
+
if (typeof reference === 'string') return ['legacy', reference];
|
|
14
|
+
return reference.handle === undefined
|
|
15
|
+
? ['endpoint', reference.node]
|
|
16
|
+
: ['endpoint', reference.node, reference.handle];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function decodedReference(value: unknown): FlowEndpointReference | undefined {
|
|
20
|
+
if (!Array.isArray(value)) return undefined;
|
|
21
|
+
if (value.length === 2 && value[0] === 'legacy' && typeof value[1] === 'string') {
|
|
22
|
+
return value[1];
|
|
23
|
+
}
|
|
24
|
+
if (
|
|
25
|
+
(value.length === 2 || value.length === 3) &&
|
|
26
|
+
value[0] === 'endpoint' &&
|
|
27
|
+
typeof value[1] === 'string' &&
|
|
28
|
+
(value.length === 2 || typeof value[2] === 'string')
|
|
29
|
+
) {
|
|
30
|
+
return value.length === 2 ? { node: value[1] } : { node: value[1], handle: value[2] };
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function decodeToken(token: string): unknown[] | undefined {
|
|
36
|
+
if (!token.startsWith(FLOW_TOKEN_PREFIX)) return undefined;
|
|
37
|
+
try {
|
|
38
|
+
const value: unknown = JSON.parse(token.slice(FLOW_TOKEN_PREFIX.length));
|
|
39
|
+
return Array.isArray(value) ? value : undefined;
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Opaque identity for a registered handle. Node and handle text remain untouched. */
|
|
46
|
+
export function encodeFlowHandleKey(node: string, handle: string): string {
|
|
47
|
+
return FLOW_TOKEN_PREFIX + JSON.stringify(['handle', node, handle]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function decodeFlowHandleKey(token: string): FlowEndpoint | undefined {
|
|
51
|
+
const value = decodeToken(token);
|
|
52
|
+
if (
|
|
53
|
+
value?.length !== 3 ||
|
|
54
|
+
value[0] !== 'handle' ||
|
|
55
|
+
typeof value[1] !== 'string' ||
|
|
56
|
+
typeof value[2] !== 'string'
|
|
57
|
+
) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return { node: value[1], handle: value[2] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Opaque identity for an edge registration or accessibility connection action. */
|
|
64
|
+
export function encodeFlowEdgeKey(
|
|
65
|
+
from: FlowEndpointReference,
|
|
66
|
+
to: FlowEndpointReference
|
|
67
|
+
): string {
|
|
68
|
+
return FLOW_TOKEN_PREFIX + JSON.stringify(['edge', encodedReference(from), encodedReference(to)]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function decodeFlowEdgeKey(
|
|
72
|
+
token: string
|
|
73
|
+
): { from: FlowEndpointReference; to: FlowEndpointReference } | undefined {
|
|
74
|
+
const value = decodeToken(token);
|
|
75
|
+
if (value?.length !== 3 || value[0] !== 'edge') return undefined;
|
|
76
|
+
const from = decodedReference(value[1]);
|
|
77
|
+
const to = decodedReference(value[2]);
|
|
78
|
+
return from === undefined || to === undefined ? undefined : { from, to };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a public endpoint without splitting its identifiers. Exact node ids
|
|
83
|
+
* win; legacy `node.handle` text is accepted only when it names one registered
|
|
84
|
+
* handle. Structured endpoints cover the otherwise ambiguous case.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveFlowEndpoint(
|
|
87
|
+
reference: FlowEndpointReference,
|
|
88
|
+
nodes: readonly string[],
|
|
89
|
+
handles: readonly { node: string; id: string }[]
|
|
90
|
+
): FlowEndpoint {
|
|
91
|
+
if (typeof reference !== 'string') return reference;
|
|
92
|
+
if (nodes.includes(reference)) return { node: reference };
|
|
93
|
+
|
|
94
|
+
const matches = handles.filter((handle) => `${handle.node}.${handle.id}` === reference);
|
|
95
|
+
return matches.length === 1
|
|
96
|
+
? { node: matches[0]!.node, handle: matches[0]!.id }
|
|
97
|
+
: { node: reference };
|
|
98
|
+
}
|
|
@@ -107,6 +107,13 @@ import {
|
|
|
107
107
|
type FlowAccessibilityHandle,
|
|
108
108
|
type FlowAccessibilityNode,
|
|
109
109
|
} from './flow-accessibility';
|
|
110
|
+
import {
|
|
111
|
+
encodeFlowEdgeKey,
|
|
112
|
+
encodeFlowHandleKey,
|
|
113
|
+
resolveFlowEndpoint,
|
|
114
|
+
type FlowEndpoint,
|
|
115
|
+
type FlowEndpointReference,
|
|
116
|
+
} from './flow-identifiers';
|
|
110
117
|
|
|
111
118
|
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
112
119
|
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
@@ -1314,7 +1321,7 @@ function FlowHandle({
|
|
|
1314
1321
|
unregisterHandle,
|
|
1315
1322
|
connectNodes,
|
|
1316
1323
|
} = flow;
|
|
1317
|
-
const key =
|
|
1324
|
+
const key = encodeFlowHandleKey(node, id);
|
|
1318
1325
|
|
|
1319
1326
|
useEffect(() => {
|
|
1320
1327
|
registerHandle({
|
|
@@ -1481,10 +1488,10 @@ FlowHandle.displayName = 'Flow.Handle';
|
|
|
1481
1488
|
/* -------------------------------------------------------------------------- */
|
|
1482
1489
|
|
|
1483
1490
|
export interface FlowEdgeProps {
|
|
1484
|
-
/** Source
|
|
1485
|
-
from:
|
|
1486
|
-
/** Target, same shape. */
|
|
1487
|
-
to:
|
|
1491
|
+
/** Source node or handle. Strings retain the `"nodeId.handleId"` shorthand. */
|
|
1492
|
+
from: FlowEndpointReference;
|
|
1493
|
+
/** Target node or handle, in the same shape. */
|
|
1494
|
+
to: FlowEndpointReference;
|
|
1488
1495
|
/** How the edge is routed. */
|
|
1489
1496
|
variant?: FlowEdgeVariant;
|
|
1490
1497
|
/**
|
|
@@ -1530,7 +1537,7 @@ export interface FlowEdgeProps {
|
|
|
1530
1537
|
*/
|
|
1531
1538
|
function FlowEdge(props: FlowEdgeProps) {
|
|
1532
1539
|
const { registerEdge, unregisterEdge } = useFlow('Flow.Edge');
|
|
1533
|
-
const key =
|
|
1540
|
+
const key = encodeFlowEdgeKey(props.from, props.to);
|
|
1534
1541
|
|
|
1535
1542
|
// Re-registered whenever any prop changes, so `animated` toggling or a
|
|
1536
1543
|
// variant switch reaches the layer that draws it.
|
|
@@ -1571,8 +1578,9 @@ function FlowEdgePath({
|
|
|
1571
1578
|
tint: string;
|
|
1572
1579
|
origin: { x: number; y: number };
|
|
1573
1580
|
}) {
|
|
1574
|
-
const
|
|
1575
|
-
const
|
|
1581
|
+
const nodes = Object.keys(boxes);
|
|
1582
|
+
const source = resolveEnd(from, fromSide, nodes, handles);
|
|
1583
|
+
const target = resolveEnd(to, toSide, nodes, handles);
|
|
1576
1584
|
|
|
1577
1585
|
const fromRect = boxes[source.node];
|
|
1578
1586
|
const toRect = boxes[target.node];
|
|
@@ -1631,22 +1639,24 @@ function FlowEdgePath({
|
|
|
1631
1639
|
}
|
|
1632
1640
|
|
|
1633
1641
|
/**
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
1642
|
+
* Resolves a node or handle without parsing either identifier. Legacy
|
|
1643
|
+
* `"node.handle"` references are matched against the structured registry.
|
|
1636
1644
|
*/
|
|
1637
1645
|
function resolveEnd(
|
|
1638
|
-
reference:
|
|
1646
|
+
reference: FlowEndpointReference,
|
|
1639
1647
|
explicit: FlowSide | undefined,
|
|
1648
|
+
nodes: string[],
|
|
1640
1649
|
handles: HandleEntry[]
|
|
1641
1650
|
): { node: string; side: FlowSide | undefined; offset: number } {
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1651
|
+
const endpoint = resolveFlowEndpoint(reference, nodes, handles);
|
|
1652
|
+
const entry =
|
|
1653
|
+
endpoint.handle === undefined
|
|
1654
|
+
? undefined
|
|
1655
|
+
: handles.find(
|
|
1656
|
+
(handle) => handle.node === endpoint.node && handle.id === endpoint.handle
|
|
1657
|
+
);
|
|
1648
1658
|
return {
|
|
1649
|
-
node,
|
|
1659
|
+
node: endpoint.node,
|
|
1650
1660
|
side: explicit ?? entry?.side,
|
|
1651
1661
|
offset: entry?.offset ?? 0.5,
|
|
1652
1662
|
};
|
|
@@ -2199,4 +2209,4 @@ export const Flow = Object.assign(FlowRoot, {
|
|
|
2199
2209
|
MiniMap: FlowMiniMap,
|
|
2200
2210
|
});
|
|
2201
2211
|
|
|
2202
|
-
export type { FlowSide, FlowRect, FlowPoint };
|
|
2212
|
+
export type { FlowEndpoint, FlowEndpointReference, FlowSide, FlowRect, FlowPoint };
|
|
@@ -57,9 +57,12 @@ import { ChevronDownIcon } from '../../icons';
|
|
|
57
57
|
import { AnimatedPressable } from '../../primitives/animated-pressable';
|
|
58
58
|
import { cn } from '../../utils/cn';
|
|
59
59
|
import { textChildren } from '../../primitives/text';
|
|
60
|
+
import {
|
|
61
|
+
distanceFromMessageScrollerTarget,
|
|
62
|
+
isMessageScrollerTargetVisible,
|
|
63
|
+
MESSAGE_SCROLLER_EDGE_THRESHOLD,
|
|
64
|
+
} from './message-scroller-math';
|
|
60
65
|
|
|
61
|
-
/** Within this many pixels of the bottom still counts as being at the bottom. */
|
|
62
|
-
const END_THRESHOLD = 24;
|
|
63
66
|
/**
|
|
64
67
|
* How much of the previous turn is left showing above an anchored one. Scrolling
|
|
65
68
|
* a turn flush to the top reads as content having been cut off; a sliver of the
|
|
@@ -71,8 +74,11 @@ export type MessageScrollerPosition = 'start' | 'end' | 'last-anchor';
|
|
|
71
74
|
|
|
72
75
|
interface MessageScrollerContextValue {
|
|
73
76
|
scrollRef: ReturnType<typeof useAnimatedRef<Animated.ScrollView>>;
|
|
74
|
-
/** 1 while the reader is at the live edge, 0 otherwise. Drives
|
|
77
|
+
/** 1 while the reader is at the live edge, 0 otherwise. Drives follow state. */
|
|
75
78
|
atEnd: ReturnType<typeof useSharedValue<number>>;
|
|
79
|
+
/** Live distance from each edge, used by target-specific jump controls. */
|
|
80
|
+
distanceFromStart: ReturnType<typeof useSharedValue<number>>;
|
|
81
|
+
distanceFromEnd: ReturnType<typeof useSharedValue<number>>;
|
|
76
82
|
atEndJS: boolean;
|
|
77
83
|
setAtEndJS: (atEnd: boolean) => void;
|
|
78
84
|
autoScroll: boolean;
|
|
@@ -150,6 +156,11 @@ function MessageScrollerRoot({
|
|
|
150
156
|
}: MessageScrollerProps) {
|
|
151
157
|
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
|
152
158
|
const atEnd = useSharedValue(1);
|
|
159
|
+
const awayFromEdge = MESSAGE_SCROLLER_EDGE_THRESHOLD + 1;
|
|
160
|
+
const distanceFromStart = useSharedValue(
|
|
161
|
+
defaultScrollPosition === 'start' ? 0 : awayFromEdge
|
|
162
|
+
);
|
|
163
|
+
const distanceFromEnd = useSharedValue(defaultScrollPosition === 'end' ? 0 : awayFromEdge);
|
|
153
164
|
const [atEndJS, setAtEndJS] = useState(true);
|
|
154
165
|
const [currentAnchorId, setCurrentAnchorId] = useState<string | null>(null);
|
|
155
166
|
|
|
@@ -199,6 +210,8 @@ function MessageScrollerRoot({
|
|
|
199
210
|
() => ({
|
|
200
211
|
scrollRef,
|
|
201
212
|
atEnd,
|
|
213
|
+
distanceFromStart,
|
|
214
|
+
distanceFromEnd,
|
|
202
215
|
atEndJS,
|
|
203
216
|
setAtEndJS,
|
|
204
217
|
autoScroll,
|
|
@@ -218,6 +231,8 @@ function MessageScrollerRoot({
|
|
|
218
231
|
[
|
|
219
232
|
scrollRef,
|
|
220
233
|
atEnd,
|
|
234
|
+
distanceFromStart,
|
|
235
|
+
distanceFromEnd,
|
|
221
236
|
atEndJS,
|
|
222
237
|
autoScroll,
|
|
223
238
|
preserveScrollOnPrepend,
|
|
@@ -268,6 +283,8 @@ function MessageScrollerViewport({
|
|
|
268
283
|
const {
|
|
269
284
|
scrollRef,
|
|
270
285
|
atEnd,
|
|
286
|
+
distanceFromStart,
|
|
287
|
+
distanceFromEnd,
|
|
271
288
|
setAtEndJS,
|
|
272
289
|
autoScroll,
|
|
273
290
|
preserveScrollOnPrepend,
|
|
@@ -310,8 +327,19 @@ function MessageScrollerViewport({
|
|
|
310
327
|
const { contentOffset, contentSize, layoutMeasurement } = event;
|
|
311
328
|
offsetY.value = contentOffset.y;
|
|
312
329
|
|
|
313
|
-
|
|
314
|
-
|
|
330
|
+
distanceFromStart.value = distanceFromMessageScrollerTarget(
|
|
331
|
+
'start',
|
|
332
|
+
contentOffset.y,
|
|
333
|
+
contentSize.height,
|
|
334
|
+
layoutMeasurement.height
|
|
335
|
+
);
|
|
336
|
+
distanceFromEnd.value = distanceFromMessageScrollerTarget(
|
|
337
|
+
'end',
|
|
338
|
+
contentOffset.y,
|
|
339
|
+
contentSize.height,
|
|
340
|
+
layoutMeasurement.height
|
|
341
|
+
);
|
|
342
|
+
const next = isMessageScrollerTargetVisible(distanceFromEnd.value) ? 0 : 1;
|
|
315
343
|
if (next !== atEnd.value) {
|
|
316
344
|
atEnd.value = next;
|
|
317
345
|
runOnJS(publishAtEnd)(next === 1);
|
|
@@ -515,12 +543,16 @@ function MessageScrollerButton({
|
|
|
515
543
|
children,
|
|
516
544
|
...props
|
|
517
545
|
}: MessageScrollerButtonProps) {
|
|
518
|
-
const {
|
|
546
|
+
const { distanceFromStart, distanceFromEnd, scrollToEnd, scrollToStart } =
|
|
547
|
+
useScroller('MessageScroller.Button');
|
|
519
548
|
const tint = useCSSVariable('--color-foreground');
|
|
520
549
|
|
|
550
|
+
const targetDistance = target === 'end' ? distanceFromEnd : distanceFromStart;
|
|
521
551
|
// Shown when away from the edge the button points at.
|
|
522
552
|
const shown = useDerivedValue(() =>
|
|
523
|
-
withTiming(
|
|
553
|
+
withTiming(isMessageScrollerTargetVisible(targetDistance.value) ? 1 : 0, {
|
|
554
|
+
duration: 160,
|
|
555
|
+
})
|
|
524
556
|
);
|
|
525
557
|
|
|
526
558
|
const style = useAnimatedStyle(() => ({
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type MessageScrollerTarget = 'start' | 'end';
|
|
2
|
+
|
|
3
|
+
export const MESSAGE_SCROLLER_EDGE_THRESHOLD = 24;
|
|
4
|
+
|
|
5
|
+
/** Distance from the viewport to the edge a jump button targets. */
|
|
6
|
+
export function distanceFromMessageScrollerTarget(
|
|
7
|
+
target: MessageScrollerTarget,
|
|
8
|
+
contentOffsetY: number,
|
|
9
|
+
contentHeight: number,
|
|
10
|
+
viewportHeight: number
|
|
11
|
+
): number {
|
|
12
|
+
'worklet';
|
|
13
|
+
|
|
14
|
+
const maximumOffset = Math.max(0, contentHeight - viewportHeight);
|
|
15
|
+
const boundedOffset = Math.min(maximumOffset, Math.max(0, contentOffsetY));
|
|
16
|
+
return target === 'start' ? boundedOffset : maximumOffset - boundedOffset;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Whether the control has enough distance to its target edge to be useful. */
|
|
20
|
+
export function isMessageScrollerTargetVisible(distance: number): boolean {
|
|
21
|
+
'worklet';
|
|
22
|
+
|
|
23
|
+
return distance > MESSAGE_SCROLLER_EDGE_THRESHOLD;
|
|
24
|
+
}
|
|
@@ -321,6 +321,9 @@ export interface PopoverContentProps extends ViewProps {
|
|
|
321
321
|
* Frost the background behind the panel instead of dimming it. Uses
|
|
322
322
|
* `expo-blur` when installed and falls back to the dimmed scrim when it is
|
|
323
323
|
* not, so it is safe to pass either way.
|
|
324
|
+
*
|
|
325
|
+
* Someone who has Reduce Transparency switched on gets an opaque
|
|
326
|
+
* backdrop instead, which is the whole point of the setting.
|
|
324
327
|
*/
|
|
325
328
|
blur?: boolean;
|
|
326
329
|
/**
|
|
@@ -88,6 +88,15 @@ import { Text, textChildren } from '../../primitives/text';
|
|
|
88
88
|
import { cn } from '../../utils/cn';
|
|
89
89
|
import { selectionTick } from '../../utils/haptics';
|
|
90
90
|
import { BottomSheet } from '../bottom-sheet';
|
|
91
|
+
import {
|
|
92
|
+
canEnterSelection,
|
|
93
|
+
handleSelectionItemPress,
|
|
94
|
+
selectAllValues,
|
|
95
|
+
selectionOwnsPress,
|
|
96
|
+
selectionTarget,
|
|
97
|
+
selectValue,
|
|
98
|
+
toggleValue,
|
|
99
|
+
} from './selection-mode-contracts';
|
|
91
100
|
|
|
92
101
|
/** How long the circle takes to come and go, in milliseconds. */
|
|
93
102
|
const REVEAL_DURATION = 180;
|
|
@@ -293,11 +302,12 @@ function SelectionModeRoot({
|
|
|
293
302
|
// Entering with the row that was pressed already picked. Entering with
|
|
294
303
|
// nothing picked leaves the reader in a list that has changed under them
|
|
295
304
|
// with nothing to show for it.
|
|
296
|
-
if (value !== undefined
|
|
297
|
-
|
|
305
|
+
if (value !== undefined) {
|
|
306
|
+
const next = selectValue(selectedRef.current, value, max);
|
|
307
|
+
if (next !== selectedRef.current) setSelected(next);
|
|
298
308
|
}
|
|
299
309
|
},
|
|
300
|
-
[haptics, setActive, setSelected]
|
|
310
|
+
[haptics, max, setActive, setSelected]
|
|
301
311
|
);
|
|
302
312
|
|
|
303
313
|
/*
|
|
@@ -321,13 +331,10 @@ function SelectionModeRoot({
|
|
|
321
331
|
const toggle = useCallback(
|
|
322
332
|
(value: string) => {
|
|
323
333
|
const current = selectedRef.current;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (haptics) selectionTick();
|
|
329
|
-
setSelected([...current, value]);
|
|
330
|
-
}
|
|
334
|
+
const next = toggleValue(current, value, max);
|
|
335
|
+
if (next === current) return;
|
|
336
|
+
if (!current.includes(value) && haptics) selectionTick();
|
|
337
|
+
setSelected(next);
|
|
331
338
|
},
|
|
332
339
|
[max, haptics, setSelected]
|
|
333
340
|
);
|
|
@@ -336,15 +343,15 @@ function SelectionModeRoot({
|
|
|
336
343
|
if (!values) return;
|
|
337
344
|
// At the limit rather than refusing: somebody who asked for all of them and
|
|
338
345
|
// can only have twenty wants the twenty, not an error.
|
|
339
|
-
setSelected(
|
|
346
|
+
setSelected(selectAllValues(values, max));
|
|
340
347
|
}, [values, max, setSelected]);
|
|
341
348
|
|
|
342
349
|
const clear = useCallback(() => setSelected([]), [setSelected]);
|
|
343
350
|
|
|
344
351
|
const total = values?.length ?? 0;
|
|
345
352
|
const count = selected.length;
|
|
346
|
-
const
|
|
347
|
-
|
|
353
|
+
const target = selectionTarget(total, max);
|
|
354
|
+
const allSelected = target > 0 && count >= target;
|
|
348
355
|
|
|
349
356
|
// An open mode owns the back button: back should leave the mode, not the
|
|
350
357
|
// screen the list is on.
|
|
@@ -459,10 +466,11 @@ export interface SelectionModeItemProps extends Omit<ViewProps, 'children'> {
|
|
|
459
466
|
className?: string;
|
|
460
467
|
/** This row's id. What ends up in `selected`. */
|
|
461
468
|
value: string;
|
|
462
|
-
/** What the row does
|
|
469
|
+
/** What the row does whenever selection does not own its press. */
|
|
463
470
|
onPress?: () => void;
|
|
464
471
|
/**
|
|
465
|
-
* Stop this row entering selection mode, and being picked once in it.
|
|
472
|
+
* Stop this row entering selection mode, and being picked once in it. Its
|
|
473
|
+
* ordinary `onPress` still runs, including while selection is active. For a
|
|
466
474
|
* header row, an advert, a "load more" — anything in the list that is not one
|
|
467
475
|
* of the things being chosen between.
|
|
468
476
|
*/
|
|
@@ -487,8 +495,9 @@ export interface SelectionModeItemProps extends Omit<ViewProps, 'children'> {
|
|
|
487
495
|
*
|
|
488
496
|
* The press behaviour is the whole component: off mode, a press is the row's
|
|
489
497
|
* own and a long press turns the mode on with this row picked; in it, a press
|
|
490
|
-
* picks and unpicks
|
|
491
|
-
* one gesture is exactly why the mode has to be
|
|
498
|
+
* picks and unpicks. A row excluded from selection keeps its ordinary press in
|
|
499
|
+
* both states. Two meanings for one gesture is exactly why the mode has to be
|
|
500
|
+
* visible from the header.
|
|
492
501
|
*/
|
|
493
502
|
function SelectionModeItem({
|
|
494
503
|
className,
|
|
@@ -503,6 +512,7 @@ function SelectionModeItem({
|
|
|
503
512
|
const { active, enter, toggle, isSelected, sheet } = useSelectionMode();
|
|
504
513
|
const selected = isSelected(value);
|
|
505
514
|
const showing = alwaysShowIndicator || active;
|
|
515
|
+
const selecting = selectionOwnsPress(active, disabled);
|
|
506
516
|
const reducedMotion = useReducedMotion();
|
|
507
517
|
|
|
508
518
|
const context = useMemo(() => ({ value }), [value]);
|
|
@@ -537,23 +547,20 @@ function SelectionModeItem({
|
|
|
537
547
|
return (
|
|
538
548
|
<SelectionModeItemContext.Provider value={context}>
|
|
539
549
|
<Pressable
|
|
540
|
-
accessibilityRole={
|
|
541
|
-
accessibilityState={
|
|
542
|
-
//
|
|
543
|
-
//
|
|
550
|
+
accessibilityRole={selecting ? 'checkbox' : 'button'}
|
|
551
|
+
accessibilityState={selecting ? { checked: selected } : undefined}
|
|
552
|
+
// Eligible rows become checkboxes only while selection owns their
|
|
553
|
+
// press. Excluded rows remain ordinary buttons because their action is
|
|
554
|
+
// still available.
|
|
544
555
|
accessibilityHint={
|
|
545
|
-
|
|
556
|
+
active || disabled ? undefined : 'Long press to select'
|
|
546
557
|
}
|
|
547
|
-
disabled={disabled && showing}
|
|
548
558
|
onPress={() => {
|
|
549
|
-
|
|
550
|
-
if (showing) toggle(value);
|
|
551
|
-
else onPress?.();
|
|
552
|
-
}}
|
|
553
|
-
onLongPress={sheet ? undefined : () => {
|
|
554
|
-
if (disabled || showing) return;
|
|
555
|
-
enter(value);
|
|
559
|
+
handleSelectionItemPress(active, disabled, () => toggle(value), onPress);
|
|
556
560
|
}}
|
|
561
|
+
onLongPress={
|
|
562
|
+
canEnterSelection(active, disabled, sheet) ? () => enter(value) : undefined
|
|
563
|
+
}
|
|
557
564
|
className={cn(
|
|
558
565
|
indicator === 'leading' ? 'flex-row items-center gap-3 px-4 py-2.5' : '',
|
|
559
566
|
className
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** The effective selection cap. Invalid and negative caps allow no additions. */
|
|
2
|
+
function selectionLimit(max?: number): number {
|
|
3
|
+
if (max === undefined || max === Infinity) return Infinity;
|
|
4
|
+
return Number.isFinite(max) ? Math.max(0, Math.floor(max)) : 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Add one value without duplicating it or crossing the configured cap. */
|
|
8
|
+
export function selectValue(selected: string[], value: string, max?: number): string[] {
|
|
9
|
+
if (selected.includes(value) || !(selected.length < selectionLimit(max))) return selected;
|
|
10
|
+
return [...selected, value];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Remove an existing value, or add it under the same contract used by entry. */
|
|
14
|
+
export function toggleValue(selected: string[], value: string, max?: number): string[] {
|
|
15
|
+
return selected.includes(value)
|
|
16
|
+
? selected.filter((entry) => entry !== value)
|
|
17
|
+
: selectValue(selected, value, max);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The values produced by select-all, capped by the same normalized limit. */
|
|
21
|
+
export function selectAllValues(values: string[], max?: number): string[] {
|
|
22
|
+
const limit = selectionLimit(max);
|
|
23
|
+
return limit === Infinity ? [...values] : values.slice(0, limit);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** How many selections constitute all selectable values. */
|
|
27
|
+
export function selectionTarget(total: number, max?: number): number {
|
|
28
|
+
return Math.min(total, selectionLimit(max));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A selection-ineligible row keeps its ordinary action in every mode. Only an
|
|
33
|
+
* active, eligible row gives its press to selection.
|
|
34
|
+
*/
|
|
35
|
+
export function selectionOwnsPress(active: boolean, disabled: boolean): boolean {
|
|
36
|
+
return active && !disabled;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Execute the one action owned by a row press under the interaction contract. */
|
|
40
|
+
export function handleSelectionItemPress(
|
|
41
|
+
active: boolean,
|
|
42
|
+
disabled: boolean,
|
|
43
|
+
toggle: () => void,
|
|
44
|
+
action?: () => void
|
|
45
|
+
): void {
|
|
46
|
+
if (selectionOwnsPress(active, disabled)) toggle();
|
|
47
|
+
else action?.();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Long press enters only from an inactive screen row that can be selected. */
|
|
51
|
+
export function canEnterSelection(active: boolean, disabled: boolean, sheet: boolean): boolean {
|
|
52
|
+
return !active && !disabled && !sheet;
|
|
53
|
+
}
|