panelui-native 0.63.0 → 0.65.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 +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/index.js +28 -14
- 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/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 +84 -0
- package/lib/module/components/flow/flow-accessibility.js.map +1 -0
- package/lib/module/components/flow/index.js +100 -39
- 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/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/radio-group/index.js +1 -2
- package/lib/module/components/radio-group/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/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/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/carousel/index.d.ts +1 -1
- 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/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 +49 -0
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/index.d.ts +14 -2
- 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/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/radio-group/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 +2 -1
- 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/providers/panel-ui-provider.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/components/badge/index.tsx +4 -1
- package/src/components/bar-chart/index.tsx +40 -3
- package/src/components/bottom-sheet/index.tsx +3 -3
- package/src/components/button/index.tsx +15 -21
- package/src/components/carousel/index.tsx +42 -20
- package/src/components/checkbox/index.tsx +1 -2
- package/src/components/dialog/index.tsx +3 -3
- package/src/components/drawer/index.tsx +3 -3
- package/src/components/fab/index.tsx +8 -12
- package/src/components/field/index.tsx +52 -10
- package/src/components/flow/flow-accessibility.ts +109 -0
- package/src/components/flow/index.tsx +162 -50
- 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/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/radio-group/index.tsx +1 -2
- 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 +23 -0
- package/src/primitives/modal-isolation-store.ts +31 -0
- package/src/primitives/portal.tsx +34 -4
- package/src/providers/panel-ui-provider.tsx +41 -9
- package/theme.css +30 -3
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
useCallback,
|
|
49
49
|
useContext,
|
|
50
50
|
useEffect,
|
|
51
|
+
useId,
|
|
51
52
|
useMemo,
|
|
52
53
|
useRef,
|
|
53
54
|
useState,
|
|
@@ -97,6 +98,15 @@ import {
|
|
|
97
98
|
type FlowRect,
|
|
98
99
|
type FlowSide,
|
|
99
100
|
} from './flow-paths';
|
|
101
|
+
import {
|
|
102
|
+
clampNodePosition,
|
|
103
|
+
FLOW_DELETE_ACTION,
|
|
104
|
+
FLOW_MOVE_ACTIONS,
|
|
105
|
+
getFlowConnectionActions,
|
|
106
|
+
moveNodePosition,
|
|
107
|
+
type FlowAccessibilityHandle,
|
|
108
|
+
type FlowAccessibilityNode,
|
|
109
|
+
} from './flow-accessibility';
|
|
100
110
|
|
|
101
111
|
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
102
112
|
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
@@ -167,13 +177,9 @@ export interface FlowConnection {
|
|
|
167
177
|
targetHandle?: string;
|
|
168
178
|
}
|
|
169
179
|
|
|
170
|
-
interface HandleEntry {
|
|
171
|
-
key: string;
|
|
172
|
-
node: string;
|
|
173
|
-
id: string;
|
|
180
|
+
interface HandleEntry extends FlowAccessibilityHandle {
|
|
174
181
|
side: FlowSide;
|
|
175
182
|
offset: number;
|
|
176
|
-
type: 'source' | 'target' | 'both';
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
interface FlowContextValue {
|
|
@@ -215,7 +221,7 @@ interface FlowContextValue {
|
|
|
215
221
|
maxZoom: number;
|
|
216
222
|
locked: boolean;
|
|
217
223
|
setLocked: (locked: boolean) => void;
|
|
218
|
-
|
|
224
|
+
nodes: FlowAccessibilityNode[];
|
|
219
225
|
/**
|
|
220
226
|
* Which of `nodeIds` are containers rather than nodes. A group writes its box
|
|
221
227
|
* into the same registry — that is what lets an edge name one and the minimap
|
|
@@ -224,7 +230,7 @@ interface FlowContextValue {
|
|
|
224
230
|
groupIds: string[];
|
|
225
231
|
handles: HandleEntry[];
|
|
226
232
|
edges: { key: string; props: FlowEdgeProps }[];
|
|
227
|
-
registerNode: (id: string) => void;
|
|
233
|
+
registerNode: (id: string, label?: string) => void;
|
|
228
234
|
unregisterNode: (id: string) => void;
|
|
229
235
|
registerGroup: (id: string) => void;
|
|
230
236
|
unregisterGroup: (id: string) => void;
|
|
@@ -232,8 +238,8 @@ interface FlowContextValue {
|
|
|
232
238
|
unregisterHandle: (key: string) => void;
|
|
233
239
|
registerEdge: (key: string, props: FlowEdgeProps) => void;
|
|
234
240
|
unregisterEdge: (key: string) => void;
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
connectNodes: (connection: FlowConnection) => void;
|
|
242
|
+
canConnect: boolean;
|
|
237
243
|
onNodeDragEnd?: (id: string, position: FlowNodePosition) => void;
|
|
238
244
|
fitView: () => void;
|
|
239
245
|
zoomBy: (factor: number) => void;
|
|
@@ -332,7 +338,7 @@ function FlowRoot({
|
|
|
332
338
|
const size = useSharedValue({ width: 0, height: 0 });
|
|
333
339
|
|
|
334
340
|
const [locked, setLocked] = useState(false);
|
|
335
|
-
const [
|
|
341
|
+
const [nodes, setNodes] = useState<FlowAccessibilityNode[]>([]);
|
|
336
342
|
const [groupIds, setGroupIds] = useState<string[]>([]);
|
|
337
343
|
const [handles, setHandles] = useState<HandleEntry[]>([]);
|
|
338
344
|
const [edges, setEdges] = useState<{ key: string; props: FlowEdgeProps }[]>([]);
|
|
@@ -441,12 +447,12 @@ function FlowRoot({
|
|
|
441
447
|
viewportChangeRef.current?.({ x, y, zoom: z });
|
|
442
448
|
}, []);
|
|
443
449
|
|
|
444
|
-
const registerNode = useCallback((id: string) => {
|
|
445
|
-
|
|
450
|
+
const registerNode = useCallback((id: string, label = id) => {
|
|
451
|
+
setNodes((current) => [...current.filter((entry) => entry.id !== id), { id, label }]);
|
|
446
452
|
}, []);
|
|
447
453
|
|
|
448
454
|
const unregisterNode = useCallback((id: string) => {
|
|
449
|
-
|
|
455
|
+
setNodes((current) => current.filter((entry) => entry.id !== id));
|
|
450
456
|
}, []);
|
|
451
457
|
|
|
452
458
|
const registerGroup = useCallback((id: string) => {
|
|
@@ -476,6 +482,15 @@ function FlowRoot({
|
|
|
476
482
|
setEdges((current) => current.filter((edge) => edge.key !== key));
|
|
477
483
|
}, []);
|
|
478
484
|
|
|
485
|
+
const connectRef = useRef(onConnect);
|
|
486
|
+
connectRef.current = onConnect;
|
|
487
|
+
const validConnectionRef = useRef(isValidConnection);
|
|
488
|
+
validConnectionRef.current = isValidConnection;
|
|
489
|
+
const connectNodes = useCallback((next: FlowConnection) => {
|
|
490
|
+
if (validConnectionRef.current && !validConnectionRef.current(next)) return;
|
|
491
|
+
connectRef.current?.(next);
|
|
492
|
+
}, []);
|
|
493
|
+
|
|
479
494
|
const fitView = useCallback(() => {
|
|
480
495
|
const { width, height } = size.value;
|
|
481
496
|
const all = Object.values(rects.value);
|
|
@@ -549,10 +564,10 @@ function FlowRoot({
|
|
|
549
564
|
// mount has nothing to fit to yet. One frame later they do.
|
|
550
565
|
const fitOnMount = useRef(fitViewOnMount);
|
|
551
566
|
useEffect(() => {
|
|
552
|
-
if (!fitOnMount.current ||
|
|
567
|
+
if (!fitOnMount.current || nodes.length === 0) return;
|
|
553
568
|
const timer = setTimeout(fitView, 32);
|
|
554
569
|
return () => clearTimeout(timer);
|
|
555
|
-
}, [fitView,
|
|
570
|
+
}, [fitView, nodes.length]);
|
|
556
571
|
|
|
557
572
|
const panStart = useSharedValue({ x: 0, y: 0 });
|
|
558
573
|
const pinchStart = useSharedValue({ zoom: 1, x: 0, y: 0 });
|
|
@@ -647,7 +662,7 @@ function FlowRoot({
|
|
|
647
662
|
maxZoom,
|
|
648
663
|
locked,
|
|
649
664
|
setLocked,
|
|
650
|
-
|
|
665
|
+
nodes,
|
|
651
666
|
groupIds,
|
|
652
667
|
handles,
|
|
653
668
|
edges,
|
|
@@ -659,8 +674,8 @@ function FlowRoot({
|
|
|
659
674
|
unregisterHandle,
|
|
660
675
|
registerEdge,
|
|
661
676
|
unregisterEdge,
|
|
662
|
-
|
|
663
|
-
|
|
677
|
+
connectNodes,
|
|
678
|
+
canConnect: Boolean(onConnect),
|
|
664
679
|
onNodeDragEnd,
|
|
665
680
|
fitView,
|
|
666
681
|
zoomBy,
|
|
@@ -678,11 +693,11 @@ function FlowRoot({
|
|
|
678
693
|
handles,
|
|
679
694
|
registerEdge,
|
|
680
695
|
unregisterEdge,
|
|
681
|
-
|
|
696
|
+
connectNodes,
|
|
682
697
|
locked,
|
|
683
698
|
maxZoom,
|
|
684
699
|
minZoom,
|
|
685
|
-
|
|
700
|
+
nodes,
|
|
686
701
|
groupIds,
|
|
687
702
|
onConnect,
|
|
688
703
|
onNodeDragEnd,
|
|
@@ -856,7 +871,7 @@ function FlowBackground({
|
|
|
856
871
|
const token = useCSSVariable('--color-muted-foreground');
|
|
857
872
|
const tint = color ?? (typeof token === 'string' ? token : '#737373');
|
|
858
873
|
// A pattern is referenced by id, and two canvases on one screen would collide.
|
|
859
|
-
const id =
|
|
874
|
+
const id = `panelui-flow-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
860
875
|
|
|
861
876
|
/*
|
|
862
877
|
* The grid is a tile that follows the viewport in whole-cell steps.
|
|
@@ -972,6 +987,13 @@ export interface FlowNodeProps extends Omit<ViewProps, 'children'> {
|
|
|
972
987
|
selected?: boolean;
|
|
973
988
|
/** Tapping the node — separate from dragging it. */
|
|
974
989
|
onPress?: () => void;
|
|
990
|
+
/**
|
|
991
|
+
* Delete the node when assistive technology requests the advertised Delete
|
|
992
|
+
* node action. No delete action is exposed when this is omitted.
|
|
993
|
+
*/
|
|
994
|
+
onDelete?: () => void;
|
|
995
|
+
/** Graph points covered by each Move up, right, down or left accessibility action. */
|
|
996
|
+
accessibilityMoveStep?: number;
|
|
975
997
|
/** Spoken name. Defaults to the node's id. */
|
|
976
998
|
accessibilityLabel?: string;
|
|
977
999
|
children?: ReactNode;
|
|
@@ -991,7 +1013,11 @@ function FlowNode({
|
|
|
991
1013
|
pinned = false,
|
|
992
1014
|
selected = false,
|
|
993
1015
|
onPress,
|
|
1016
|
+
onDelete,
|
|
1017
|
+
accessibilityMoveStep = 24,
|
|
994
1018
|
accessibilityLabel,
|
|
1019
|
+
accessibilityActions,
|
|
1020
|
+
onAccessibilityAction,
|
|
995
1021
|
children,
|
|
996
1022
|
...props
|
|
997
1023
|
}: FlowNodeProps) {
|
|
@@ -1009,6 +1035,10 @@ function FlowNode({
|
|
|
1009
1035
|
registerGroup,
|
|
1010
1036
|
unregisterGroup,
|
|
1011
1037
|
onNodeDragEnd,
|
|
1038
|
+
nodes,
|
|
1039
|
+
handles,
|
|
1040
|
+
connectNodes,
|
|
1041
|
+
canConnect,
|
|
1012
1042
|
} = flow;
|
|
1013
1043
|
|
|
1014
1044
|
/*
|
|
@@ -1018,13 +1048,13 @@ function FlowNode({
|
|
|
1018
1048
|
* layer both need to know it is a container and not a card.
|
|
1019
1049
|
*/
|
|
1020
1050
|
useEffect(() => {
|
|
1021
|
-
registerNode(id);
|
|
1051
|
+
registerNode(id, accessibilityLabel ?? id);
|
|
1022
1052
|
registerGroup(id);
|
|
1023
1053
|
return () => {
|
|
1024
1054
|
unregisterNode(id);
|
|
1025
1055
|
unregisterGroup(id);
|
|
1026
1056
|
};
|
|
1027
|
-
}, [id, registerGroup, registerNode, unregisterGroup, unregisterNode]);
|
|
1057
|
+
}, [accessibilityLabel, id, registerGroup, registerNode, unregisterGroup, unregisterNode]);
|
|
1028
1058
|
|
|
1029
1059
|
// Writing on every render would fight the drag, snapping a node back to the
|
|
1030
1060
|
// position it was first given. Only an actual change to the prop moves it.
|
|
@@ -1065,11 +1095,70 @@ function FlowNode({
|
|
|
1065
1095
|
);
|
|
1066
1096
|
|
|
1067
1097
|
const start = useSharedValue({ x: 0, y: 0 });
|
|
1068
|
-
|
|
1069
1098
|
const pressRef = useRef(onPress);
|
|
1070
1099
|
pressRef.current = onPress;
|
|
1071
1100
|
const press = useCallback(() => pressRef.current?.(), []);
|
|
1072
1101
|
|
|
1102
|
+
const moveNode = useCallback(
|
|
1103
|
+
(actionName: string) => {
|
|
1104
|
+
const rect = rects.value[id];
|
|
1105
|
+
if (!rect) return;
|
|
1106
|
+
const bounds = confine && group ? rects.value[group] : undefined;
|
|
1107
|
+
const next = moveNodePosition(rect, bounds, actionName, accessibilityMoveStep);
|
|
1108
|
+
if (!next) return;
|
|
1109
|
+
setNodeRect(id, next);
|
|
1110
|
+
dragEnd(next.x, next.y);
|
|
1111
|
+
},
|
|
1112
|
+
[accessibilityMoveStep, confine, dragEnd, group, id, rects, setNodeRect]
|
|
1113
|
+
);
|
|
1114
|
+
|
|
1115
|
+
const connectionActions = useMemo(
|
|
1116
|
+
() => (canConnect && !locked ? getFlowConnectionActions(id, nodes, handles) : []),
|
|
1117
|
+
[canConnect, handles, id, locked, nodes]
|
|
1118
|
+
);
|
|
1119
|
+
const connectionByAction = useMemo(
|
|
1120
|
+
() => new Map(connectionActions.map((entry) => [entry.name, entry.connection])),
|
|
1121
|
+
[connectionActions]
|
|
1122
|
+
);
|
|
1123
|
+
const movable = draggable && !pinned && !locked;
|
|
1124
|
+
const nodeActions = useMemo(
|
|
1125
|
+
() => [
|
|
1126
|
+
...(onPress ? [{ name: 'activate' }] : []),
|
|
1127
|
+
...(movable ? FLOW_MOVE_ACTIONS : []),
|
|
1128
|
+
...connectionActions.map(({ name, label }) => ({ name, label })),
|
|
1129
|
+
...(onDelete ? [FLOW_DELETE_ACTION] : []),
|
|
1130
|
+
...(accessibilityActions ?? []),
|
|
1131
|
+
],
|
|
1132
|
+
[accessibilityActions, connectionActions, movable, onDelete, onPress]
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
const handleAccessibilityAction = useCallback<
|
|
1136
|
+
NonNullable<ViewProps['onAccessibilityAction']>
|
|
1137
|
+
>(
|
|
1138
|
+
(event) => {
|
|
1139
|
+
const action = event.nativeEvent.actionName;
|
|
1140
|
+
if (action === 'activate' && onPress) {
|
|
1141
|
+
press();
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (movable && FLOW_MOVE_ACTIONS.some((entry) => entry.name === action)) {
|
|
1145
|
+
moveNode(action);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const nextConnection = connectionByAction.get(action);
|
|
1149
|
+
if (nextConnection) {
|
|
1150
|
+
connectNodes(nextConnection);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
if (action === FLOW_DELETE_ACTION.name && onDelete) {
|
|
1154
|
+
onDelete();
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
onAccessibilityAction?.(event);
|
|
1158
|
+
},
|
|
1159
|
+
[connectNodes, connectionByAction, movable, moveNode, onAccessibilityAction, onDelete, onPress, press]
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1073
1162
|
const drag = useMemo(
|
|
1074
1163
|
() =>
|
|
1075
1164
|
Gesture.Pan()
|
|
@@ -1096,13 +1185,9 @@ function FlowNode({
|
|
|
1096
1185
|
* would leave the edges drawn to a position the node never reached.
|
|
1097
1186
|
*/
|
|
1098
1187
|
if (confine && group) {
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
const maxY = box.y + box.height - rect.height;
|
|
1103
|
-
x = x < box.x ? box.x : x > maxX ? maxX : x;
|
|
1104
|
-
y = y < box.y ? box.y : y > maxY ? maxY : y;
|
|
1105
|
-
}
|
|
1188
|
+
const next = clampNodePosition(rect, rects.value[group], { x, y });
|
|
1189
|
+
x = next.x;
|
|
1190
|
+
y = next.y;
|
|
1106
1191
|
}
|
|
1107
1192
|
|
|
1108
1193
|
rects.value = { ...rects.value, [id]: { ...rect, x, y } };
|
|
@@ -1158,6 +1243,8 @@ function FlowNode({
|
|
|
1158
1243
|
accessibilityRole={onPress ? 'button' : 'none'}
|
|
1159
1244
|
accessibilityLabel={accessibilityLabel ?? id}
|
|
1160
1245
|
accessibilityState={{ selected }}
|
|
1246
|
+
accessibilityActions={nodeActions}
|
|
1247
|
+
onAccessibilityAction={handleAccessibilityAction}
|
|
1161
1248
|
{...props}
|
|
1162
1249
|
>
|
|
1163
1250
|
{textChildren(children)}
|
|
@@ -1185,6 +1272,11 @@ export interface FlowHandleProps {
|
|
|
1185
1272
|
/** Where along the face, 0–1. For more than one handle on a side. */
|
|
1186
1273
|
offset?: number;
|
|
1187
1274
|
className?: string;
|
|
1275
|
+
/**
|
|
1276
|
+
* Spoken handle name used in the parent node's connection actions. Defaults
|
|
1277
|
+
* to the handle's id.
|
|
1278
|
+
*/
|
|
1279
|
+
accessibilityLabel?: string;
|
|
1188
1280
|
/** Draw nothing. The handle still anchors edges and still accepts a drop. */
|
|
1189
1281
|
hidden?: boolean;
|
|
1190
1282
|
}
|
|
@@ -1203,6 +1295,7 @@ function FlowHandle({
|
|
|
1203
1295
|
type = 'both',
|
|
1204
1296
|
offset = 0.5,
|
|
1205
1297
|
className,
|
|
1298
|
+
accessibilityLabel,
|
|
1206
1299
|
hidden = false,
|
|
1207
1300
|
}: FlowHandleProps) {
|
|
1208
1301
|
const flow = useFlow('Flow.Handle');
|
|
@@ -1211,31 +1304,51 @@ function FlowHandle({
|
|
|
1211
1304
|
throw new Error('Flow.Handle must be used within a <Flow.Node>');
|
|
1212
1305
|
}
|
|
1213
1306
|
|
|
1214
|
-
const {
|
|
1307
|
+
const {
|
|
1308
|
+
rects,
|
|
1309
|
+
zoom,
|
|
1310
|
+
connection,
|
|
1311
|
+
handles,
|
|
1312
|
+
locked,
|
|
1313
|
+
registerHandle,
|
|
1314
|
+
unregisterHandle,
|
|
1315
|
+
connectNodes,
|
|
1316
|
+
} = flow;
|
|
1215
1317
|
const key = `${node}.${id}`;
|
|
1216
1318
|
|
|
1217
1319
|
useEffect(() => {
|
|
1218
|
-
registerHandle({
|
|
1320
|
+
registerHandle({
|
|
1321
|
+
key,
|
|
1322
|
+
node,
|
|
1323
|
+
id,
|
|
1324
|
+
label: accessibilityLabel ?? id,
|
|
1325
|
+
side: position,
|
|
1326
|
+
offset,
|
|
1327
|
+
type,
|
|
1328
|
+
});
|
|
1219
1329
|
return () => unregisterHandle(key);
|
|
1220
|
-
}, [
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1330
|
+
}, [
|
|
1331
|
+
accessibilityLabel,
|
|
1332
|
+
id,
|
|
1333
|
+
key,
|
|
1334
|
+
node,
|
|
1335
|
+
offset,
|
|
1336
|
+
position,
|
|
1337
|
+
registerHandle,
|
|
1338
|
+
type,
|
|
1339
|
+
unregisterHandle,
|
|
1340
|
+
]);
|
|
1226
1341
|
|
|
1227
1342
|
const land = useCallback(
|
|
1228
1343
|
(target: string, targetHandle: string | undefined) => {
|
|
1229
|
-
|
|
1344
|
+
connectNodes({
|
|
1230
1345
|
source: node,
|
|
1231
1346
|
sourceHandle: id,
|
|
1232
1347
|
target,
|
|
1233
1348
|
targetHandle,
|
|
1234
|
-
};
|
|
1235
|
-
if (validRef.current && !validRef.current(payload)) return;
|
|
1236
|
-
connectRef.current?.(payload);
|
|
1349
|
+
});
|
|
1237
1350
|
},
|
|
1238
|
-
[id, node]
|
|
1351
|
+
[connectNodes, id, node]
|
|
1239
1352
|
);
|
|
1240
1353
|
|
|
1241
1354
|
// A snapshot of what a drag can land on, rebuilt only when handles come and
|
|
@@ -1354,8 +1467,7 @@ function FlowHandle({
|
|
|
1354
1467
|
<View
|
|
1355
1468
|
collapsable={false}
|
|
1356
1469
|
hitSlop={12}
|
|
1357
|
-
|
|
1358
|
-
accessibilityLabel={`${position} connection point`}
|
|
1470
|
+
accessible={false}
|
|
1359
1471
|
style={placement}
|
|
1360
1472
|
className={flowVariants().handle({ className })}
|
|
1361
1473
|
/>
|
|
@@ -1645,9 +1757,9 @@ function FlowGroup({
|
|
|
1645
1757
|
} = flow;
|
|
1646
1758
|
|
|
1647
1759
|
useEffect(() => {
|
|
1648
|
-
registerNode(id);
|
|
1760
|
+
registerNode(id, label ?? id);
|
|
1649
1761
|
return () => unregisterNode(id);
|
|
1650
|
-
}, [id, registerNode, unregisterNode]);
|
|
1762
|
+
}, [id, label, registerNode, unregisterNode]);
|
|
1651
1763
|
|
|
1652
1764
|
useEffect(() => {
|
|
1653
1765
|
setNodeRect(id, {
|
|
@@ -1919,7 +2031,7 @@ function FlowMiniMap({
|
|
|
1919
2031
|
height = 84,
|
|
1920
2032
|
nodeColor,
|
|
1921
2033
|
}: FlowMiniMapProps) {
|
|
1922
|
-
const { rects,
|
|
2034
|
+
const { rects, nodes, groupIds, translateX, translateY, zoom, size } =
|
|
1923
2035
|
useFlow('Flow.MiniMap');
|
|
1924
2036
|
const token = useCSSVariable('--color-muted-foreground');
|
|
1925
2037
|
const tint = nodeColor ?? (typeof token === 'string' ? token : '#737373');
|
|
@@ -1999,7 +2111,7 @@ function FlowMiniMap({
|
|
|
1999
2111
|
{/* Containers first and drawn as outlines, so the map reads the way
|
|
2000
2112
|
the canvas does — boxes with things in them, rather than a wash of
|
|
2001
2113
|
identical rectangles where the largest happens to be a group. */}
|
|
2002
|
-
{
|
|
2114
|
+
{nodes.map(({ id }) => {
|
|
2003
2115
|
const group = groups.has(id);
|
|
2004
2116
|
return (
|
|
2005
2117
|
<MiniMapNode
|
|
@@ -174,6 +174,8 @@ export function useForm<T extends Record<string, any>>({
|
|
|
174
174
|
const validatorsRef = useRef<
|
|
175
175
|
Partial<Record<keyof T, (value: any, values: T) => string | undefined | Promise<string | undefined>>>
|
|
176
176
|
>({});
|
|
177
|
+
const validationOwnersRef = useRef<Partial<Record<keyof T, symbol>>>({});
|
|
178
|
+
const submissionLockedRef = useRef(false);
|
|
177
179
|
|
|
178
180
|
const [state, dispatch] = useReducer(reducer<T>, {
|
|
179
181
|
values: defaultValues,
|
|
@@ -196,31 +198,46 @@ export function useForm<T extends Record<string, any>>({
|
|
|
196
198
|
const valuesRef = useRef(state.values);
|
|
197
199
|
valuesRef.current = state.values;
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
/* Only the latest owner of a field may commit validation state. A value,
|
|
202
|
+
* validator or manual-error change claims ownership too, invalidating any
|
|
203
|
+
* result that was calculated from the state before that change. */
|
|
204
|
+
const claimFieldValidation = useCallback(<K extends keyof T>(name: K) => {
|
|
205
|
+
const owner = Symbol();
|
|
206
|
+
validationOwnersRef.current[name] = owner;
|
|
207
|
+
return owner;
|
|
202
208
|
}, []);
|
|
203
209
|
|
|
210
|
+
const setFieldValue = useCallback(
|
|
211
|
+
<K extends keyof T>(name: K, value: T[K]) => {
|
|
212
|
+
claimFieldValidation(name);
|
|
213
|
+
valuesRef.current = { ...valuesRef.current, [name]: value } as T;
|
|
214
|
+
dispatch({ type: 'SET_VALUE', name, value });
|
|
215
|
+
},
|
|
216
|
+
[claimFieldValidation]
|
|
217
|
+
);
|
|
218
|
+
|
|
204
219
|
const setFieldTouched = useCallback(<K extends keyof T>(name: K, touched = true) => {
|
|
205
220
|
dispatch({ type: 'SET_TOUCHED', name, touched });
|
|
206
221
|
}, []);
|
|
207
222
|
|
|
208
223
|
const setFieldError = useCallback(
|
|
209
224
|
<K extends keyof T>(name: K, error: string | undefined) => {
|
|
225
|
+
claimFieldValidation(name);
|
|
210
226
|
dispatch({ type: 'SET_ERROR', name, error });
|
|
211
227
|
},
|
|
212
|
-
[]
|
|
228
|
+
[claimFieldValidation]
|
|
213
229
|
);
|
|
214
230
|
|
|
215
231
|
const registerValidator = useCallback(
|
|
216
232
|
<K extends keyof T>(name: K, validator?: Validator<T, K>) => {
|
|
233
|
+
claimFieldValidation(name);
|
|
217
234
|
if (validator) {
|
|
218
235
|
validatorsRef.current[name] = validator;
|
|
219
236
|
} else {
|
|
220
237
|
delete validatorsRef.current[name];
|
|
221
238
|
}
|
|
222
239
|
},
|
|
223
|
-
[]
|
|
240
|
+
[claimFieldValidation]
|
|
224
241
|
);
|
|
225
242
|
|
|
226
243
|
const getFieldState = useCallback(
|
|
@@ -235,81 +252,100 @@ export function useForm<T extends Record<string, any>>({
|
|
|
235
252
|
const validateField = useCallback(async <K extends keyof T>(name: K) => {
|
|
236
253
|
const validator = validatorsRef.current[name];
|
|
237
254
|
if (!validator) return true;
|
|
255
|
+
const owner = claimFieldValidation(name);
|
|
238
256
|
const values = valuesRef.current;
|
|
239
257
|
const error = await runValidator(name, validator, values);
|
|
240
|
-
|
|
258
|
+
if (validationOwnersRef.current[name] === owner) {
|
|
259
|
+
dispatch({ type: 'SET_ERROR', name, error });
|
|
260
|
+
}
|
|
241
261
|
return !error;
|
|
242
|
-
}, []);
|
|
262
|
+
}, [claimFieldValidation]);
|
|
243
263
|
|
|
244
264
|
const handleSubmit = useCallback(async () => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
265
|
+
// Reducer state cannot guard two calls made before React renders again.
|
|
266
|
+
if (submissionLockedRef.current) return;
|
|
267
|
+
submissionLockedRef.current = true;
|
|
268
|
+
try {
|
|
269
|
+
const values = valuesRef.current;
|
|
270
|
+
/*
|
|
271
|
+
* Every field the form knows of, which is not the same as every key of
|
|
272
|
+
* `defaultValues`: a field can register a validator under a name that was
|
|
273
|
+
* never declared there. Submitting only the declared ones meant such a
|
|
274
|
+
* field was never validated and never blocked anything, so a form of
|
|
275
|
+
* required-but-undeclared fields submitted itself while still empty.
|
|
276
|
+
*
|
|
277
|
+
* It is still a mistake to name a field that `defaultValues` does not
|
|
278
|
+
* declare — its value stays undefined and never reaches `onSubmit` —
|
|
279
|
+
* which is why `useField` says so. Validating it anyway is what turns
|
|
280
|
+
* that mistake into a visible error instead of a silent submit.
|
|
281
|
+
*/
|
|
282
|
+
const names = Array.from(
|
|
283
|
+
new Set([...Object.keys(values), ...Object.keys(validatorsRef.current)])
|
|
284
|
+
) as (keyof T)[];
|
|
285
|
+
const validationOwners = new Map(
|
|
286
|
+
names.map((name) => [name, claimFieldValidation(name)] as const)
|
|
287
|
+
);
|
|
288
|
+
dispatch({ type: 'TOUCH_ALL', names });
|
|
262
289
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
290
|
+
const fieldErrorEntries = await Promise.all(
|
|
291
|
+
names.map(async (name) => {
|
|
292
|
+
const validator = validatorsRef.current[name];
|
|
293
|
+
const error = validator
|
|
294
|
+
? await runValidator(name, validator, values)
|
|
295
|
+
: undefined;
|
|
296
|
+
return [name, error] as const;
|
|
297
|
+
})
|
|
298
|
+
);
|
|
272
299
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
300
|
+
/*
|
|
301
|
+
* Same reasoning as a field's rule, one level up: a cross-field check that
|
|
302
|
+
* throws must not take the screen, and must not let the submit through.
|
|
303
|
+
* It belongs to no single field, so it blocks without marking one — there
|
|
304
|
+
* is no field whose error line would be the honest place to put it.
|
|
305
|
+
*/
|
|
306
|
+
let formErrors: FieldErrors<T> = {};
|
|
307
|
+
let formValidateThrew = false;
|
|
308
|
+
if (validate) {
|
|
309
|
+
try {
|
|
310
|
+
formErrors = await validate(values);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error(
|
|
313
|
+
'[PanelUI] The form-level validate() threw, so the form is being ' +
|
|
314
|
+
'treated as invalid and the submit was not run.',
|
|
315
|
+
error
|
|
316
|
+
);
|
|
317
|
+
formValidateThrew = true;
|
|
318
|
+
}
|
|
291
319
|
}
|
|
292
|
-
}
|
|
293
320
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
321
|
+
const cleared = Object.fromEntries(names.map((name) => [name, undefined]));
|
|
322
|
+
const nextErrors: FieldErrors<T> = {
|
|
323
|
+
...cleared,
|
|
324
|
+
...Object.fromEntries(fieldErrorEntries.filter(([, error]) => error !== undefined)),
|
|
325
|
+
...formErrors,
|
|
326
|
+
};
|
|
327
|
+
const ownedErrors = Object.fromEntries(
|
|
328
|
+
names
|
|
329
|
+
.filter((name) => validationOwnersRef.current[name] === validationOwners.get(name))
|
|
330
|
+
.map((name) => [name, nextErrors[name]])
|
|
331
|
+
) as FieldErrors<T>;
|
|
332
|
+
dispatch({ type: 'SET_ERRORS', errors: ownedErrors });
|
|
301
333
|
|
|
302
|
-
|
|
334
|
+
if (formValidateThrew || Object.values(nextErrors).some(Boolean)) return;
|
|
303
335
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
336
|
+
dispatch({ type: 'SUBMIT_START' });
|
|
337
|
+
try {
|
|
338
|
+
await onSubmit(values);
|
|
339
|
+
} finally {
|
|
340
|
+
dispatch({ type: 'SUBMIT_END' });
|
|
341
|
+
}
|
|
307
342
|
} finally {
|
|
308
|
-
|
|
343
|
+
submissionLockedRef.current = false;
|
|
309
344
|
}
|
|
310
|
-
}, [validate, onSubmit]);
|
|
345
|
+
}, [claimFieldValidation, validate, onSubmit]);
|
|
311
346
|
|
|
312
347
|
const reset = useCallback((values: T = defaultsRef.current) => {
|
|
348
|
+
validationOwnersRef.current = {};
|
|
313
349
|
valuesRef.current = values;
|
|
314
350
|
dispatch({ type: 'RESET', values });
|
|
315
351
|
}, []);
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
isValidElement,
|
|
43
43
|
useContext,
|
|
44
44
|
useEffect,
|
|
45
|
+
useId,
|
|
45
46
|
useMemo,
|
|
46
47
|
useRef,
|
|
47
48
|
useState,
|
|
@@ -402,9 +403,7 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
|
|
|
402
403
|
) {
|
|
403
404
|
const [width, setWidth] = useState(0);
|
|
404
405
|
const [activeCell, setActiveCellState] = useState<HeatmapCell | null>(null);
|
|
405
|
-
const clipId =
|
|
406
|
-
`panelui-heat-${Math.random().toString(36).slice(2, 9)}`
|
|
407
|
-
).current;
|
|
406
|
+
const clipId = `panelui-heat-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
408
407
|
|
|
409
408
|
const reveal = useSharedValue(0);
|
|
410
409
|
const reducedMotion = useReducedMotion();
|