panelui-native 0.63.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/README.md +5 -0
- package/lib/module/components/badge/index.js +1 -1
- package/lib/module/components/badge/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +39 -2
- package/lib/module/components/bar-chart/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +2 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +19 -9
- package/lib/module/components/button/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 +86 -35
- package/lib/module/components/carousel/index.js.map +1 -1
- package/lib/module/components/checkbox/index.js +2 -3
- package/lib/module/components/checkbox/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 +2 -2
- package/lib/module/components/dialog/index.js.map +1 -1
- package/lib/module/components/drawer/index.js +2 -2
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/fab/index.js +4 -3
- package/lib/module/components/fab/index.js.map +1 -1
- package/lib/module/components/field/index.js +41 -6
- package/lib/module/components/field/index.js.map +1 -1
- package/lib/module/components/flow/flow-accessibility.js +100 -0
- package/lib/module/components/flow/flow-accessibility.js.map +1 -0
- 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 +112 -57
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/form/use-form.js +93 -66
- package/lib/module/components/form/use-form.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +2 -2
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +2 -2
- package/lib/module/components/hex-chart/index.js.map +1 -1
- package/lib/module/components/kpi/index.js +3 -1
- package/lib/module/components/kpi/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +3 -3
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +71 -8
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/map/map-accessibility.js +20 -0
- package/lib/module/components/map/map-accessibility.js.map +1 -0
- package/lib/module/components/map/maplibre.js.map +1 -1
- package/lib/module/components/menu/index.js +4 -1
- package/lib/module/components/menu/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/number-input/index.js +1 -23
- package/lib/module/components/number-input/index.js.map +1 -1
- package/lib/module/components/number-input/number-input-math.js +44 -0
- package/lib/module/components/number-input/number-input-math.js.map +1 -0
- package/lib/module/components/plot/index.js +2 -2
- package/lib/module/components/plot/index.js.map +1 -1
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/radio-group/index.js +1 -2
- package/lib/module/components/radio-group/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/components/sortable/index.js +5 -55
- package/lib/module/components/sortable/index.js.map +1 -1
- package/lib/module/components/sortable/reorder.js +63 -0
- package/lib/module/components/sortable/reorder.js.map +1 -0
- package/lib/module/components/swipe/index.js +4 -4
- package/lib/module/components/swipe/index.js.map +1 -1
- package/lib/module/components/switch/index.js +36 -3
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/time-picker/accessibility.js +21 -0
- package/lib/module/components/time-picker/accessibility.js.map +1 -0
- package/lib/module/components/time-picker/index.js +34 -8
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/components/waterfall-chart/index.js +1325 -0
- package/lib/module/components/waterfall-chart/index.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/modal-isolation-store.js +29 -0
- package/lib/module/primitives/modal-isolation-store.js.map +1 -0
- package/lib/module/primitives/portal.js +36 -4
- package/lib/module/primitives/portal.js.map +1 -1
- package/lib/module/primitives/scrim.js +94 -3
- package/lib/module/primitives/scrim.js.map +1 -1
- package/lib/module/providers/panel-ui-provider.js +27 -6
- package/lib/module/providers/panel-ui-provider.js.map +1 -1
- package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts.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/button/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/checkbox/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/fab/index.d.ts.map +1 -1
- package/lib/typescript/src/components/field/index.d.ts +8 -1
- package/lib/typescript/src/components/field/index.d.ts.map +1 -1
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts +57 -0
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -0
- 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 +20 -7
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
- package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/kpi/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +13 -5
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/map-accessibility.d.ts +19 -0
- package/lib/typescript/src/components/map/map-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +2 -0
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
- package/lib/typescript/src/components/menu/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/number-input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/number-input/number-input-math.d.ts +9 -0
- package/lib/typescript/src/components/number-input/number-input-math.d.ts.map +1 -0
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
- 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/radio-group/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/components/sortable/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sortable/reorder.d.ts +14 -0
- package/lib/typescript/src/components/sortable/reorder.d.ts.map +1 -0
- package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts +3 -3
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/accessibility.d.ts +10 -0
- package/lib/typescript/src/components/time-picker/accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts +379 -0
- package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/modal-isolation-store.d.ts +10 -0
- package/lib/typescript/src/primitives/modal-isolation-store.d.ts.map +1 -0
- package/lib/typescript/src/primitives/portal.d.ts +6 -0
- package/lib/typescript/src/primitives/portal.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/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
- package/package.json +11 -5
- package/src/components/badge/index.tsx +4 -1
- package/src/components/bar-chart/index.tsx +40 -3
- package/src/components/bottom-sheet/index.tsx +6 -3
- package/src/components/button/index.tsx +15 -21
- package/src/components/carousel/carousel-lifecycle.ts +74 -0
- package/src/components/carousel/index.tsx +118 -41
- package/src/components/checkbox/index.tsx +1 -2
- 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 +6 -3
- package/src/components/drawer/index.tsx +6 -3
- package/src/components/fab/index.tsx +8 -12
- package/src/components/field/index.tsx +52 -10
- package/src/components/flow/flow-accessibility.ts +123 -0
- package/src/components/flow/flow-identifiers.ts +98 -0
- package/src/components/flow/index.tsx +191 -69
- package/src/components/form/use-form.ts +101 -65
- package/src/components/heatmap-chart/index.tsx +2 -3
- package/src/components/hex-chart/index.tsx +2 -3
- package/src/components/kpi/index.tsx +3 -1
- package/src/components/line-chart/index.tsx +3 -2
- package/src/components/map/index.tsx +110 -7
- package/src/components/map/map-accessibility.ts +41 -0
- package/src/components/map/maplibre.ts +2 -0
- package/src/components/menu/index.tsx +4 -1
- package/src/components/message-scroller/index.tsx +39 -7
- package/src/components/message-scroller/message-scroller-math.ts +24 -0
- package/src/components/number-input/index.tsx +1 -26
- package/src/components/number-input/number-input-math.ts +50 -0
- package/src/components/plot/index.tsx +2 -1
- package/src/components/popover/index.tsx +3 -0
- package/src/components/radio-group/index.tsx +1 -2
- 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/components/sortable/index.tsx +12 -64
- package/src/components/sortable/reorder.ts +88 -0
- package/src/components/swipe/index.tsx +7 -4
- package/src/components/switch/index.tsx +42 -6
- package/src/components/time-picker/accessibility.ts +24 -0
- package/src/components/time-picker/index.tsx +48 -2
- package/src/components/waterfall-chart/index.tsx +1589 -0
- package/src/index.ts +25 -0
- package/src/primitives/modal-isolation-store.ts +31 -0
- package/src/primitives/portal.tsx +34 -4
- package/src/primitives/scrim.tsx +127 -7
- package/src/providers/panel-ui-provider.tsx +41 -9
- package/theme.css +30 -3
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* A `Menu` hangs off a control that exists to be pressed: a ⋯ button, a toolbar
|
|
5
5
|
* item, something whose whole job is to open the menu. A context menu has no
|
|
6
6
|
* such control. The target is the content itself — a message, a note, a photo,
|
|
7
|
-
* a row — and the actions are reached by
|
|
7
|
+
* a row — and the actions are reached by holding it, by a named accessibility
|
|
8
|
+
* action, or from the keyboard.
|
|
8
9
|
*
|
|
9
10
|
* ```tsx
|
|
10
11
|
* <ContextMenu>
|
|
@@ -31,9 +32,9 @@
|
|
|
31
32
|
* them. The panel is `Menu`'s panel, which is `Popover`'s, so `presentation`,
|
|
32
33
|
* submenus and edge-flipping all arrive already working.
|
|
33
34
|
*
|
|
34
|
-
* What this component owns is
|
|
35
|
-
*
|
|
36
|
-
* goes.
|
|
35
|
+
* What this component owns is what a menu opened on content needs and a menu
|
|
36
|
+
* opened from a button does not: alternate invocation paths, and where the
|
|
37
|
+
* panel goes.
|
|
37
38
|
*
|
|
38
39
|
* ## Anchored to the finger, not to the target
|
|
39
40
|
*
|
|
@@ -73,7 +74,11 @@ import {
|
|
|
73
74
|
type ReactElement,
|
|
74
75
|
type ReactNode,
|
|
75
76
|
} from 'react';
|
|
76
|
-
import {
|
|
77
|
+
import {
|
|
78
|
+
View,
|
|
79
|
+
type AccessibilityActionInfo,
|
|
80
|
+
type ViewProps,
|
|
81
|
+
} from 'react-native';
|
|
77
82
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
78
83
|
import Animated, {
|
|
79
84
|
FadeOut,
|
|
@@ -95,6 +100,12 @@ import {
|
|
|
95
100
|
import { usePopoverAnchor } from '../popover';
|
|
96
101
|
import { cn } from '../../utils/cn';
|
|
97
102
|
import { selectionTick } from '../../utils/haptics';
|
|
103
|
+
import {
|
|
104
|
+
contextMenuAccessibilityInvocation,
|
|
105
|
+
contextMenuKeyInvocation,
|
|
106
|
+
type ContextMenuInvocation,
|
|
107
|
+
type ContextMenuKeyDownEvent,
|
|
108
|
+
} from './context-menu-invocation';
|
|
98
109
|
|
|
99
110
|
/**
|
|
100
111
|
* How long the target is held before the menu opens.
|
|
@@ -260,7 +271,7 @@ function ContextMenuRoot({ children, ...props }: ContextMenuProps) {
|
|
|
260
271
|
/** Which rectangle the panel is placed against. */
|
|
261
272
|
export type ContextMenuAnchor = 'point' | 'target';
|
|
262
273
|
|
|
263
|
-
export interface ContextMenuTriggerProps extends Omit<ViewProps, 'children'> {
|
|
274
|
+
export interface ContextMenuTriggerProps extends Omit<ViewProps, 'children' | 'onKeyDown'> {
|
|
264
275
|
/**
|
|
265
276
|
* Classes on the wrapper the content sits in, which lays out like any other
|
|
266
277
|
* view — it does not shrink to its child, because the things held are usually
|
|
@@ -280,7 +291,8 @@ export interface ContextMenuTriggerProps extends Omit<ViewProps, 'children'> {
|
|
|
280
291
|
* Point is the default because a context menu's target is usually large, and
|
|
281
292
|
* the middle of a whole message is not where the press was. Reach for
|
|
282
293
|
* `target` when the target is small and list-shaped and the panel should read
|
|
283
|
-
* as lining up with it.
|
|
294
|
+
* as lining up with it. Keyboard and accessibility opens always use the target
|
|
295
|
+
* bounds, because those modalities have no pointer coordinate.
|
|
284
296
|
*/
|
|
285
297
|
anchor?: ContextMenuAnchor;
|
|
286
298
|
/** How long the hold has to last, in milliseconds. 350 by default. */
|
|
@@ -310,10 +322,17 @@ export interface ContextMenuTriggerProps extends Omit<ViewProps, 'children'> {
|
|
|
310
322
|
haptics?: boolean;
|
|
311
323
|
/** Nothing opens the menu, and the short press stops firing too. */
|
|
312
324
|
disabled?: boolean;
|
|
325
|
+
/**
|
|
326
|
+
* Called first for keyboard events. Prevent the event to keep ContextMenu
|
|
327
|
+
* from handling it. Context Menu and Shift+F10 open the menu; Enter and Space
|
|
328
|
+
* mirror the trigger's accessible activation.
|
|
329
|
+
*/
|
|
330
|
+
onKeyDown?: (event: ContextMenuKeyDownEvent) => void;
|
|
313
331
|
}
|
|
314
332
|
|
|
315
333
|
/**
|
|
316
|
-
* Wraps the content and opens the menu when
|
|
334
|
+
* Wraps the content and opens the menu when held, through accessibility
|
|
335
|
+
* actions, or from the keyboard.
|
|
317
336
|
*
|
|
318
337
|
* The wrapper is a plain view and lays out like one, stretching as a view does
|
|
319
338
|
* rather than shrinking to its child. That is the opposite of what a tooltip's
|
|
@@ -334,6 +353,14 @@ function ContextMenuTrigger({
|
|
|
334
353
|
onPress,
|
|
335
354
|
haptics = false,
|
|
336
355
|
disabled = false,
|
|
356
|
+
accessible,
|
|
357
|
+
accessibilityActions,
|
|
358
|
+
onAccessibilityAction,
|
|
359
|
+
accessibilityRole,
|
|
360
|
+
accessibilityState,
|
|
361
|
+
focusable,
|
|
362
|
+
tabIndex,
|
|
363
|
+
onKeyDown,
|
|
337
364
|
...props
|
|
338
365
|
}: ContextMenuTriggerProps) {
|
|
339
366
|
const { setOpen, anchorTo } = usePopoverAnchor('ContextMenu.Trigger');
|
|
@@ -359,8 +386,8 @@ function ContextMenuTrigger({
|
|
|
359
386
|
* The target is measured either way, because the preview draws at that rect
|
|
360
387
|
* whatever the panel is placed against.
|
|
361
388
|
*/
|
|
362
|
-
const
|
|
363
|
-
(x: number
|
|
389
|
+
const open = useCallback(
|
|
390
|
+
(point?: { x: number; y: number }) => {
|
|
364
391
|
// Ticked here rather than in the gesture callback so it fires once the
|
|
365
392
|
// hold has been accepted, which is the moment there is something to
|
|
366
393
|
// confirm — and on the same side of the bridge as the opening.
|
|
@@ -382,8 +409,8 @@ function ContextMenuTrigger({
|
|
|
382
409
|
*/
|
|
383
410
|
const grown = hasPreview ? (PREVIEW_SCALE - 1) / 2 : 0;
|
|
384
411
|
anchorTo(
|
|
385
|
-
against === 'point'
|
|
386
|
-
? {
|
|
412
|
+
against === 'point' && point
|
|
413
|
+
? { ...point, width: 0, height: 0 }
|
|
387
414
|
: {
|
|
388
415
|
x: mx - width * grown,
|
|
389
416
|
y: my - height * grown,
|
|
@@ -397,6 +424,63 @@ function ContextMenuTrigger({
|
|
|
397
424
|
[against, anchorTo, setOpen, haptics, setTarget, setContent, children, hasPreview]
|
|
398
425
|
);
|
|
399
426
|
|
|
427
|
+
const openAt = useCallback((x: number, y: number) => open({ x, y }), [open]);
|
|
428
|
+
// Keyboard and accessibility actions have no pointer coordinate to honour.
|
|
429
|
+
// The measured target is the only real location they can anchor against.
|
|
430
|
+
const openFromTarget = useCallback(() => open(), [open]);
|
|
431
|
+
|
|
432
|
+
const invoke = useCallback(
|
|
433
|
+
(invocation: ContextMenuInvocation) => {
|
|
434
|
+
if (invocation === 'menu') openFromTarget();
|
|
435
|
+
else onPress?.();
|
|
436
|
+
},
|
|
437
|
+
[onPress, openFromTarget]
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const triggerActions = useMemo<readonly AccessibilityActionInfo[] | undefined>(() => {
|
|
441
|
+
if (disabled) return undefined;
|
|
442
|
+
const reserved = new Set(['activate', 'showMenu']);
|
|
443
|
+
return [
|
|
444
|
+
{ name: 'activate' },
|
|
445
|
+
{ name: 'showMenu', label: 'Show menu' },
|
|
446
|
+
...(accessibilityActions ?? []).filter(({ name }) => !reserved.has(name)),
|
|
447
|
+
];
|
|
448
|
+
}, [accessibilityActions, disabled]);
|
|
449
|
+
|
|
450
|
+
const handleAccessibilityAction = useCallback<
|
|
451
|
+
NonNullable<ViewProps['onAccessibilityAction']>
|
|
452
|
+
>(
|
|
453
|
+
(event) => {
|
|
454
|
+
const actionName = event.nativeEvent.actionName;
|
|
455
|
+
const invocation = contextMenuAccessibilityInvocation(actionName, !!onPress, disabled);
|
|
456
|
+
if (invocation) {
|
|
457
|
+
invoke(invocation);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
// Reserved actions cannot bypass a disabled trigger through a consumer
|
|
461
|
+
// handler, while unrelated custom actions still compose normally.
|
|
462
|
+
if (actionName === 'activate' || actionName === 'showMenu') return;
|
|
463
|
+
onAccessibilityAction?.(event);
|
|
464
|
+
},
|
|
465
|
+
[disabled, invoke, onAccessibilityAction, onPress]
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
const handleKeyDown = useCallback(
|
|
469
|
+
(event: ContextMenuKeyDownEvent) => {
|
|
470
|
+
onKeyDown?.(event);
|
|
471
|
+
if (event.isDefaultPrevented()) return;
|
|
472
|
+
const invocation = contextMenuKeyInvocation(event.nativeEvent, !!onPress, disabled);
|
|
473
|
+
if (!invocation) return;
|
|
474
|
+
event.preventDefault();
|
|
475
|
+
invoke(invocation);
|
|
476
|
+
},
|
|
477
|
+
[disabled, invoke, onKeyDown, onPress]
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
// React Native exposes key events at runtime (and in its generated types),
|
|
481
|
+
// while its compatibility ViewProps declaration does not yet list them.
|
|
482
|
+
const keyboardProps = { onKeyDown: handleKeyDown } as ViewProps;
|
|
483
|
+
|
|
400
484
|
const gesture = useMemo(() => {
|
|
401
485
|
/*
|
|
402
486
|
* The hold and the tap are given to the recogniser as alternatives, so it
|
|
@@ -432,7 +516,23 @@ function ContextMenuTrigger({
|
|
|
432
516
|
a view that only groups children is otherwise flattened away — and a
|
|
433
517
|
flattened view cannot be measured, which `anchor="target"` needs.
|
|
434
518
|
*/}
|
|
435
|
-
<View
|
|
519
|
+
<View
|
|
520
|
+
ref={ref}
|
|
521
|
+
collapsable={false}
|
|
522
|
+
accessible={accessible ?? true}
|
|
523
|
+
accessibilityRole={accessibilityRole ?? 'button'}
|
|
524
|
+
accessibilityState={{
|
|
525
|
+
...accessibilityState,
|
|
526
|
+
disabled: disabled || accessibilityState?.disabled,
|
|
527
|
+
}}
|
|
528
|
+
accessibilityActions={triggerActions}
|
|
529
|
+
onAccessibilityAction={handleAccessibilityAction}
|
|
530
|
+
focusable={focusable ?? !disabled}
|
|
531
|
+
tabIndex={tabIndex ?? (disabled ? -1 : 0)}
|
|
532
|
+
className={className}
|
|
533
|
+
{...keyboardProps}
|
|
534
|
+
{...props}
|
|
535
|
+
>
|
|
436
536
|
{children}
|
|
437
537
|
</View>
|
|
438
538
|
</GestureDetector>
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { Pressable, View, type ViewProps } from 'react-native';
|
|
14
14
|
import Animated, { FadeIn, FadeOut, ZoomIn } from 'react-native-reanimated';
|
|
15
15
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
16
|
-
import {
|
|
16
|
+
import { ModalPortal } from '../../primitives/portal';
|
|
17
17
|
import { Scrim } from '../../primitives/scrim';
|
|
18
18
|
import { Text, type TextProps, textChildren } from '../../primitives/text';
|
|
19
19
|
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
@@ -89,6 +89,9 @@ export interface DialogContentProps extends ViewProps {
|
|
|
89
89
|
* Frost the screen behind the dialog instead of dimming it. Uses `expo-blur`
|
|
90
90
|
* when installed and falls back to the dim when it is not, so it is safe to
|
|
91
91
|
* pass either way.
|
|
92
|
+
*
|
|
93
|
+
* Someone who has Reduce Transparency switched on gets an opaque
|
|
94
|
+
* backdrop instead, which is the whole point of the setting.
|
|
92
95
|
*/
|
|
93
96
|
blur?: boolean;
|
|
94
97
|
children?: ReactNode;
|
|
@@ -111,7 +114,7 @@ function DialogContent({
|
|
|
111
114
|
if (!open) return null;
|
|
112
115
|
|
|
113
116
|
return (
|
|
114
|
-
<
|
|
117
|
+
<ModalPortal>
|
|
115
118
|
{/* Portal content mounts under PortalHost, outside this provider's
|
|
116
119
|
subtree — re-provide the context so Dialog.Close etc. keep working. */}
|
|
117
120
|
<DialogContext.Provider value={context}>
|
|
@@ -142,7 +145,7 @@ function DialogContent({
|
|
|
142
145
|
</Animated.View>
|
|
143
146
|
</Animated.View>
|
|
144
147
|
</DialogContext.Provider>
|
|
145
|
-
</
|
|
148
|
+
</ModalPortal>
|
|
146
149
|
);
|
|
147
150
|
}
|
|
148
151
|
|
|
@@ -84,7 +84,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
84
84
|
import { tv } from 'tailwind-variants';
|
|
85
85
|
import { useCSSVariable } from 'uniwind';
|
|
86
86
|
import { XIcon } from '../../icons';
|
|
87
|
-
import {
|
|
87
|
+
import { ModalPortal } from '../../primitives/portal';
|
|
88
88
|
import { Scrim } from '../../primitives/scrim';
|
|
89
89
|
import { Text, textChildren } from '../../primitives/text';
|
|
90
90
|
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
@@ -285,6 +285,9 @@ export interface DrawerContentProps extends ViewProps {
|
|
|
285
285
|
/**
|
|
286
286
|
* Frost the screen behind the drawer instead of dimming it. Needs the
|
|
287
287
|
* optional `expo-blur`; without it this dims, rather than failing.
|
|
288
|
+
*
|
|
289
|
+
* Someone who has Reduce Transparency switched on gets an opaque
|
|
290
|
+
* backdrop instead, which is the whole point of the setting.
|
|
288
291
|
*/
|
|
289
292
|
blur?: boolean;
|
|
290
293
|
children?: ReactNode;
|
|
@@ -458,7 +461,7 @@ function DrawerContent({
|
|
|
458
461
|
};
|
|
459
462
|
|
|
460
463
|
return (
|
|
461
|
-
<
|
|
464
|
+
<ModalPortal>
|
|
462
465
|
{/* Portal content mounts under PortalHost, outside this provider's
|
|
463
466
|
subtree — re-provide the context so Drawer.Close keeps working. */}
|
|
464
467
|
<DrawerContext.Provider value={{ open, setOpen }}>
|
|
@@ -561,7 +564,7 @@ function DrawerContent({
|
|
|
561
564
|
</View>
|
|
562
565
|
</DirectionContext.Provider>
|
|
563
566
|
</DrawerContext.Provider>
|
|
564
|
-
</
|
|
567
|
+
</ModalPortal>
|
|
565
568
|
);
|
|
566
569
|
}
|
|
567
570
|
|
|
@@ -129,7 +129,10 @@ const fabVariants = tv({
|
|
|
129
129
|
primary: { root: 'bg-primary', label: 'text-primary-foreground' },
|
|
130
130
|
secondary: { root: 'bg-secondary', label: 'text-secondary-foreground' },
|
|
131
131
|
surface: { root: 'border border-border bg-popover', label: 'text-foreground' },
|
|
132
|
-
destructive: {
|
|
132
|
+
destructive: {
|
|
133
|
+
root: 'bg-destructive',
|
|
134
|
+
label: 'text-destructive-solid-foreground',
|
|
135
|
+
},
|
|
133
136
|
},
|
|
134
137
|
disabled: {
|
|
135
138
|
true: { root: 'opacity-[0.64] shadow-none' },
|
|
@@ -158,8 +161,9 @@ export type FabSize = NonNullable<FabVariantProps['size']>;
|
|
|
158
161
|
export type FabVariant = NonNullable<FabVariantProps['variant']>;
|
|
159
162
|
|
|
160
163
|
/** The theme token each variant's icon reads against. */
|
|
161
|
-
const CONTENT_COLOR_VAR: Record<
|
|
164
|
+
const CONTENT_COLOR_VAR: Record<FabVariant, string> = {
|
|
162
165
|
primary: '--color-primary-foreground',
|
|
166
|
+
destructive: '--color-destructive-solid-foreground',
|
|
163
167
|
secondary: '--color-secondary-foreground',
|
|
164
168
|
surface: '--color-foreground',
|
|
165
169
|
};
|
|
@@ -234,17 +238,9 @@ const FabRoot = forwardRef<View, FabProps>(
|
|
|
234
238
|
disabled,
|
|
235
239
|
});
|
|
236
240
|
|
|
237
|
-
const themed = useCSSVariable(
|
|
238
|
-
CONTENT_COLOR_VAR[
|
|
239
|
-
variant === 'destructive' ? 'primary' : (variant ?? 'primary')
|
|
240
|
-
]
|
|
241
|
-
);
|
|
241
|
+
const themed = useCSSVariable(CONTENT_COLOR_VAR[variant ?? 'primary']);
|
|
242
242
|
const contentColor =
|
|
243
|
-
|
|
244
|
-
? '#ffffff'
|
|
245
|
-
: typeof themed === 'string'
|
|
246
|
-
? themed
|
|
247
|
-
: undefined;
|
|
243
|
+
typeof themed === 'string' ? themed : undefined;
|
|
248
244
|
|
|
249
245
|
const handlePress = useCallback(() => {
|
|
250
246
|
if (haptics) selectionTick();
|
|
@@ -18,7 +18,12 @@
|
|
|
18
18
|
* <Field.Title>Marketing emails</Field.Title>
|
|
19
19
|
* <Field.Description>Product updates, at most weekly.</Field.Description>
|
|
20
20
|
* </Field.Content>
|
|
21
|
-
* <Switch
|
|
21
|
+
* <Switch
|
|
22
|
+
* value={subscribed}
|
|
23
|
+
* onValueChange={setSubscribed}
|
|
24
|
+
* accessibilityLabel="Marketing emails"
|
|
25
|
+
* accessibilityHint="Product updates, at most weekly."
|
|
26
|
+
* />
|
|
22
27
|
* </Field>
|
|
23
28
|
* ```
|
|
24
29
|
*/
|
|
@@ -26,6 +31,7 @@ import {
|
|
|
26
31
|
createContext,
|
|
27
32
|
forwardRef,
|
|
28
33
|
useContext,
|
|
34
|
+
useId,
|
|
29
35
|
useMemo,
|
|
30
36
|
type ReactNode,
|
|
31
37
|
} from 'react';
|
|
@@ -73,14 +79,22 @@ interface FieldState {
|
|
|
73
79
|
invalid: boolean;
|
|
74
80
|
disabled: boolean;
|
|
75
81
|
required: boolean;
|
|
82
|
+
relationshipIds: string[];
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
const FieldContext = createContext<FieldState>({
|
|
79
86
|
invalid: false,
|
|
80
87
|
disabled: false,
|
|
81
88
|
required: false,
|
|
89
|
+
relationshipIds: [],
|
|
82
90
|
});
|
|
83
91
|
|
|
92
|
+
/** Android can announce the label, help and error text rendered beside a control. */
|
|
93
|
+
export function useFieldLabelledBy(): string[] | undefined {
|
|
94
|
+
const { relationshipIds } = useContext(FieldContext);
|
|
95
|
+
return relationshipIds.length > 0 ? relationshipIds : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
export interface FieldProps extends ViewProps {
|
|
85
99
|
className?: string;
|
|
86
100
|
/** `horizontal` puts a label beside the control instead of above it. */
|
|
@@ -107,9 +121,19 @@ const FieldRoot = forwardRef<View, FieldProps>(
|
|
|
107
121
|
ref
|
|
108
122
|
) => {
|
|
109
123
|
const { root } = fieldVariants({ orientation, disabled });
|
|
124
|
+
const id = useId().replace(/:/g, '');
|
|
125
|
+
const labelId = `panelui-field-${id}-label`;
|
|
126
|
+
const titleId = `panelui-field-${id}-title`;
|
|
127
|
+
const descriptionId = `panelui-field-${id}-description`;
|
|
128
|
+
const errorId = `panelui-field-${id}-error`;
|
|
110
129
|
const state = useMemo(
|
|
111
|
-
() => ({
|
|
112
|
-
|
|
130
|
+
() => ({
|
|
131
|
+
invalid,
|
|
132
|
+
disabled,
|
|
133
|
+
required,
|
|
134
|
+
relationshipIds: [labelId, titleId, descriptionId, errorId],
|
|
135
|
+
}),
|
|
136
|
+
[invalid, disabled, required, labelId, titleId, descriptionId, errorId]
|
|
113
137
|
);
|
|
114
138
|
|
|
115
139
|
return (
|
|
@@ -145,7 +169,7 @@ FieldContent.displayName = 'Field.Content';
|
|
|
145
169
|
export interface FieldLabelProps extends LabelProps {}
|
|
146
170
|
|
|
147
171
|
const FieldLabel = forwardRef<View, FieldLabelProps>(
|
|
148
|
-
({ isRequired, isInvalid, isDisabled, ...props }, ref) => {
|
|
172
|
+
({ isRequired, isInvalid, isDisabled, nativeID, ...props }, ref) => {
|
|
149
173
|
const ctx = useContext(FieldContext);
|
|
150
174
|
return (
|
|
151
175
|
<Label
|
|
@@ -153,6 +177,7 @@ const FieldLabel = forwardRef<View, FieldLabelProps>(
|
|
|
153
177
|
isRequired={isRequired ?? ctx.required}
|
|
154
178
|
isInvalid={isInvalid ?? ctx.invalid}
|
|
155
179
|
isDisabled={isDisabled ?? ctx.disabled}
|
|
180
|
+
nativeID={nativeID ?? ctx.relationshipIds[0]}
|
|
156
181
|
{...props}
|
|
157
182
|
/>
|
|
158
183
|
);
|
|
@@ -165,10 +190,17 @@ export interface FieldDescriptionProps extends TextProps {
|
|
|
165
190
|
}
|
|
166
191
|
|
|
167
192
|
const FieldDescription = forwardRef<RNText, FieldDescriptionProps>(
|
|
168
|
-
({ className, ...props }, ref) => {
|
|
169
|
-
const { disabled } = useContext(FieldContext);
|
|
193
|
+
({ className, nativeID, ...props }, ref) => {
|
|
194
|
+
const { disabled, relationshipIds } = useContext(FieldContext);
|
|
170
195
|
const { description } = fieldVariants({ disabled });
|
|
171
|
-
return
|
|
196
|
+
return (
|
|
197
|
+
<Text
|
|
198
|
+
ref={ref}
|
|
199
|
+
nativeID={nativeID ?? relationshipIds[2]}
|
|
200
|
+
className={description({ className })}
|
|
201
|
+
{...props}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
172
204
|
}
|
|
173
205
|
);
|
|
174
206
|
FieldDescription.displayName = 'Field.Description';
|
|
@@ -186,7 +218,8 @@ export interface FieldErrorProps extends Omit<ViewProps, 'children'> {
|
|
|
186
218
|
}
|
|
187
219
|
|
|
188
220
|
const FieldError = forwardRef<View, FieldErrorProps>(
|
|
189
|
-
({ className, errors, children, ...props }, ref) => {
|
|
221
|
+
({ className, errors, children, nativeID, ...props }, ref) => {
|
|
222
|
+
const { relationshipIds } = useContext(FieldContext);
|
|
190
223
|
const messages = useMemo(() => {
|
|
191
224
|
if (!errors) return null;
|
|
192
225
|
const seen = new Set<string>();
|
|
@@ -213,6 +246,7 @@ const FieldError = forwardRef<View, FieldErrorProps>(
|
|
|
213
246
|
// accessibilityRole list has no alert, the ARIA-aligned one does.
|
|
214
247
|
role="alert"
|
|
215
248
|
accessibilityLiveRegion="polite"
|
|
249
|
+
nativeID={nativeID ?? relationshipIds[3]}
|
|
216
250
|
className={error({ className })}
|
|
217
251
|
{...props}
|
|
218
252
|
>
|
|
@@ -307,9 +341,17 @@ export interface FieldTitleProps extends TextProps {
|
|
|
307
341
|
}
|
|
308
342
|
|
|
309
343
|
const FieldTitle = forwardRef<RNText, FieldTitleProps>(
|
|
310
|
-
({ className, ...props }, ref) => {
|
|
344
|
+
({ className, nativeID, ...props }, ref) => {
|
|
311
345
|
const { title } = fieldVariants();
|
|
312
|
-
|
|
346
|
+
const { relationshipIds } = useContext(FieldContext);
|
|
347
|
+
return (
|
|
348
|
+
<Text
|
|
349
|
+
ref={ref}
|
|
350
|
+
nativeID={nativeID ?? relationshipIds[1]}
|
|
351
|
+
className={title({ className })}
|
|
352
|
+
{...props}
|
|
353
|
+
/>
|
|
354
|
+
);
|
|
313
355
|
}
|
|
314
356
|
);
|
|
315
357
|
FieldTitle.displayName = 'Field.Title';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { FlowConnection, FlowNodePosition } from '.';
|
|
2
|
+
import type { FlowRect } from './flow-paths';
|
|
3
|
+
|
|
4
|
+
export const FLOW_MOVE_ACTIONS = [
|
|
5
|
+
{ name: 'flow-move-up', label: 'Move up', dx: 0, dy: -1 },
|
|
6
|
+
{ name: 'flow-move-right', label: 'Move right', dx: 1, dy: 0 },
|
|
7
|
+
{ name: 'flow-move-down', label: 'Move down', dx: 0, dy: 1 },
|
|
8
|
+
{ name: 'flow-move-left', label: 'Move left', dx: -1, dy: 0 },
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export const FLOW_DELETE_ACTION = { name: 'flow-delete', label: 'Delete node' } as const;
|
|
12
|
+
|
|
13
|
+
export interface FlowAccessibilityNode {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FlowAccessibilityHandle {
|
|
19
|
+
key: string;
|
|
20
|
+
node: string;
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
type: 'source' | 'target' | 'both';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FlowConnectionAction {
|
|
27
|
+
name: string;
|
|
28
|
+
label: string;
|
|
29
|
+
connection: FlowConnection;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Opaque, collision-free name for the native accessibility action map. */
|
|
33
|
+
export function flowConnectionActionName(
|
|
34
|
+
source: { node: string; handle?: string },
|
|
35
|
+
target: { node: string; handle?: string }
|
|
36
|
+
): string {
|
|
37
|
+
return `flow-connect:${JSON.stringify([source, target])}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Keep pointer and assistive-technology movement inside the same group bounds. */
|
|
41
|
+
export function clampNodePosition(
|
|
42
|
+
rect: FlowRect,
|
|
43
|
+
bounds: FlowRect | undefined,
|
|
44
|
+
position: FlowNodePosition
|
|
45
|
+
): FlowNodePosition {
|
|
46
|
+
'worklet';
|
|
47
|
+
if (!bounds) return position;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
x: Math.min(Math.max(position.x, bounds.x), bounds.x + bounds.width - rect.width),
|
|
51
|
+
y: Math.min(Math.max(position.y, bounds.y), bounds.y + bounds.height - rect.height),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function moveNodePosition(
|
|
56
|
+
rect: FlowRect,
|
|
57
|
+
bounds: FlowRect | undefined,
|
|
58
|
+
actionName: string,
|
|
59
|
+
step: number
|
|
60
|
+
): FlowNodePosition | undefined {
|
|
61
|
+
const action = FLOW_MOVE_ACTIONS.find((entry) => entry.name === actionName);
|
|
62
|
+
if (!action) return undefined;
|
|
63
|
+
|
|
64
|
+
return clampNodePosition(rect, bounds, {
|
|
65
|
+
x: rect.x + action.dx * Math.max(1, step),
|
|
66
|
+
y: rect.y + action.dy * Math.max(1, step),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Build connection choices from the same registered nodes and handles used by drag hit-testing. */
|
|
71
|
+
export function getFlowConnectionActions(
|
|
72
|
+
sourceNode: string,
|
|
73
|
+
nodes: FlowAccessibilityNode[],
|
|
74
|
+
handles: FlowAccessibilityHandle[]
|
|
75
|
+
): FlowConnectionAction[] {
|
|
76
|
+
const sources = handles.filter(
|
|
77
|
+
(handle) => handle.node === sourceNode && handle.type !== 'target'
|
|
78
|
+
);
|
|
79
|
+
if (sources.length === 0) return [];
|
|
80
|
+
|
|
81
|
+
const actions: FlowConnectionAction[] = [];
|
|
82
|
+
for (const source of sources) {
|
|
83
|
+
for (const targetNode of nodes) {
|
|
84
|
+
if (targetNode.id === sourceNode) continue;
|
|
85
|
+
const targets = handles.filter(
|
|
86
|
+
(handle) => handle.node === targetNode.id && handle.type !== 'source'
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (targets.length === 0) {
|
|
90
|
+
actions.push({
|
|
91
|
+
name: flowConnectionActionName(
|
|
92
|
+
{ node: source.node, handle: source.id },
|
|
93
|
+
{ node: targetNode.id }
|
|
94
|
+
),
|
|
95
|
+
label: `Connect ${source.label} to ${targetNode.label}`,
|
|
96
|
+
connection: {
|
|
97
|
+
source: sourceNode,
|
|
98
|
+
sourceHandle: source.id,
|
|
99
|
+
target: targetNode.id,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const target of targets) {
|
|
106
|
+
actions.push({
|
|
107
|
+
name: flowConnectionActionName(
|
|
108
|
+
{ node: source.node, handle: source.id },
|
|
109
|
+
{ node: target.node, handle: target.id }
|
|
110
|
+
),
|
|
111
|
+
label: `Connect ${source.label} to ${targetNode.label}, ${target.label}`,
|
|
112
|
+
connection: {
|
|
113
|
+
source: sourceNode,
|
|
114
|
+
sourceHandle: source.id,
|
|
115
|
+
target: target.node,
|
|
116
|
+
targetHandle: target.id,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return actions;
|
|
123
|
+
}
|
|
@@ -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
|
+
}
|