panelui-native 0.62.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.
Files changed (198) hide show
  1. package/README.md +5 -0
  2. package/lib/module/components/area-chart/index.js +14 -4
  3. package/lib/module/components/area-chart/index.js.map +1 -1
  4. package/lib/module/components/badge/index.js +1 -1
  5. package/lib/module/components/badge/index.js.map +1 -1
  6. package/lib/module/components/bar-chart/index.js +53 -6
  7. package/lib/module/components/bar-chart/index.js.map +1 -1
  8. package/lib/module/components/bottom-sheet/index.js +2 -2
  9. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  10. package/lib/module/components/button/index.js +19 -9
  11. package/lib/module/components/button/index.js.map +1 -1
  12. package/lib/module/components/candlestick-chart/index.js +14 -4
  13. package/lib/module/components/candlestick-chart/index.js.map +1 -1
  14. package/lib/module/components/card/index.js +26 -1
  15. package/lib/module/components/card/index.js.map +1 -1
  16. package/lib/module/components/carousel/index.js +28 -14
  17. package/lib/module/components/carousel/index.js.map +1 -1
  18. package/lib/module/components/checkbox/index.js +2 -3
  19. package/lib/module/components/checkbox/index.js.map +1 -1
  20. package/lib/module/components/dialog/index.js +51 -5
  21. package/lib/module/components/dialog/index.js.map +1 -1
  22. package/lib/module/components/drawer/index.js +2 -2
  23. package/lib/module/components/drawer/index.js.map +1 -1
  24. package/lib/module/components/fab/index.js +4 -3
  25. package/lib/module/components/fab/index.js.map +1 -1
  26. package/lib/module/components/field/index.js +41 -6
  27. package/lib/module/components/field/index.js.map +1 -1
  28. package/lib/module/components/flow/flow-accessibility.js +84 -0
  29. package/lib/module/components/flow/flow-accessibility.js.map +1 -0
  30. package/lib/module/components/flow/index.js +100 -39
  31. package/lib/module/components/flow/index.js.map +1 -1
  32. package/lib/module/components/form/use-form.js +93 -66
  33. package/lib/module/components/form/use-form.js.map +1 -1
  34. package/lib/module/components/heatmap-chart/index.js +2 -2
  35. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  36. package/lib/module/components/hex-chart/index.js +4 -4
  37. package/lib/module/components/hex-chart/index.js.map +1 -1
  38. package/lib/module/components/kpi/index.js +3 -1
  39. package/lib/module/components/kpi/index.js.map +1 -1
  40. package/lib/module/components/line-chart/index.js +17 -7
  41. package/lib/module/components/line-chart/index.js.map +1 -1
  42. package/lib/module/components/map/index.js +71 -8
  43. package/lib/module/components/map/index.js.map +1 -1
  44. package/lib/module/components/map/map-accessibility.js +20 -0
  45. package/lib/module/components/map/map-accessibility.js.map +1 -0
  46. package/lib/module/components/map/maplibre.js.map +1 -1
  47. package/lib/module/components/menu/index.js +4 -1
  48. package/lib/module/components/menu/index.js.map +1 -1
  49. package/lib/module/components/number-input/index.js +1 -23
  50. package/lib/module/components/number-input/index.js.map +1 -1
  51. package/lib/module/components/number-input/number-input-math.js +44 -0
  52. package/lib/module/components/number-input/number-input-math.js.map +1 -0
  53. package/lib/module/components/pie-chart/index.js +2 -2
  54. package/lib/module/components/pie-chart/index.js.map +1 -1
  55. package/lib/module/components/plot/index.js +1329 -0
  56. package/lib/module/components/plot/index.js.map +1 -0
  57. package/lib/module/components/polar-area-chart/index.js +2 -2
  58. package/lib/module/components/polar-area-chart/index.js.map +1 -1
  59. package/lib/module/components/radar-chart/index.js +14 -4
  60. package/lib/module/components/radar-chart/index.js.map +1 -1
  61. package/lib/module/components/radio-group/index.js +1 -2
  62. package/lib/module/components/radio-group/index.js.map +1 -1
  63. package/lib/module/components/ring-chart/index.js +2 -2
  64. package/lib/module/components/ring-chart/index.js.map +1 -1
  65. package/lib/module/components/section-rail/index.js +120 -15
  66. package/lib/module/components/section-rail/index.js.map +1 -1
  67. package/lib/module/components/selection-mode/index.js +129 -18
  68. package/lib/module/components/selection-mode/index.js.map +1 -1
  69. package/lib/module/components/sortable/index.js +20 -60
  70. package/lib/module/components/sortable/index.js.map +1 -1
  71. package/lib/module/components/sortable/reorder.js +63 -0
  72. package/lib/module/components/sortable/reorder.js.map +1 -0
  73. package/lib/module/components/swipe/index.js +4 -4
  74. package/lib/module/components/swipe/index.js.map +1 -1
  75. package/lib/module/components/switch/index.js +36 -3
  76. package/lib/module/components/switch/index.js.map +1 -1
  77. package/lib/module/components/time-picker/accessibility.js +21 -0
  78. package/lib/module/components/time-picker/accessibility.js.map +1 -0
  79. package/lib/module/components/time-picker/index.js +34 -8
  80. package/lib/module/components/time-picker/index.js.map +1 -1
  81. package/lib/module/components/waterfall-chart/index.js +1325 -0
  82. package/lib/module/components/waterfall-chart/index.js.map +1 -0
  83. package/lib/module/index.js +8 -0
  84. package/lib/module/index.js.map +1 -1
  85. package/lib/module/primitives/modal-isolation-store.js +29 -0
  86. package/lib/module/primitives/modal-isolation-store.js.map +1 -0
  87. package/lib/module/primitives/portal.js +36 -4
  88. package/lib/module/primitives/portal.js.map +1 -1
  89. package/lib/module/providers/panel-ui-provider.js +27 -6
  90. package/lib/module/providers/panel-ui-provider.js.map +1 -1
  91. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  92. package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
  94. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  95. package/lib/typescript/src/components/candlestick-chart/index.d.ts +6 -3
  96. package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
  97. package/lib/typescript/src/components/card/index.d.ts +48 -1
  98. package/lib/typescript/src/components/card/index.d.ts.map +1 -1
  99. package/lib/typescript/src/components/carousel/index.d.ts +1 -1
  100. package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
  101. package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
  102. package/lib/typescript/src/components/dialog/index.d.ts +47 -2
  103. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  104. package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
  105. package/lib/typescript/src/components/field/index.d.ts +8 -1
  106. package/lib/typescript/src/components/field/index.d.ts.map +1 -1
  107. package/lib/typescript/src/components/flow/flow-accessibility.d.ts +49 -0
  108. package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -0
  109. package/lib/typescript/src/components/flow/index.d.ts +14 -2
  110. package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
  111. package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
  112. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  113. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
  114. package/lib/typescript/src/components/kpi/index.d.ts.map +1 -1
  115. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  116. package/lib/typescript/src/components/map/index.d.ts +13 -5
  117. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  118. package/lib/typescript/src/components/map/map-accessibility.d.ts +19 -0
  119. package/lib/typescript/src/components/map/map-accessibility.d.ts.map +1 -0
  120. package/lib/typescript/src/components/map/maplibre.d.ts +2 -0
  121. package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
  122. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  123. package/lib/typescript/src/components/number-input/index.d.ts.map +1 -1
  124. package/lib/typescript/src/components/number-input/number-input-math.d.ts +9 -0
  125. package/lib/typescript/src/components/number-input/number-input-math.d.ts.map +1 -0
  126. package/lib/typescript/src/components/plot/index.d.ts +471 -0
  127. package/lib/typescript/src/components/plot/index.d.ts.map +1 -0
  128. package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
  129. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  130. package/lib/typescript/src/components/section-rail/index.d.ts +8 -1
  131. package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
  132. package/lib/typescript/src/components/selection-mode/index.d.ts +82 -6
  133. package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
  134. package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
  135. package/lib/typescript/src/components/sortable/reorder.d.ts +14 -0
  136. package/lib/typescript/src/components/sortable/reorder.d.ts.map +1 -0
  137. package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
  138. package/lib/typescript/src/components/switch/index.d.ts +3 -3
  139. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  140. package/lib/typescript/src/components/time-picker/accessibility.d.ts +10 -0
  141. package/lib/typescript/src/components/time-picker/accessibility.d.ts.map +1 -0
  142. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  143. package/lib/typescript/src/components/waterfall-chart/index.d.ts +379 -0
  144. package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -0
  145. package/lib/typescript/src/index.d.ts +4 -1
  146. package/lib/typescript/src/index.d.ts.map +1 -1
  147. package/lib/typescript/src/primitives/modal-isolation-store.d.ts +10 -0
  148. package/lib/typescript/src/primitives/modal-isolation-store.d.ts.map +1 -0
  149. package/lib/typescript/src/primitives/portal.d.ts +6 -0
  150. package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
  151. package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
  152. package/package.json +5 -2
  153. package/src/components/area-chart/index.tsx +14 -4
  154. package/src/components/badge/index.tsx +4 -1
  155. package/src/components/bar-chart/index.tsx +54 -7
  156. package/src/components/bottom-sheet/index.tsx +3 -3
  157. package/src/components/button/index.tsx +15 -21
  158. package/src/components/candlestick-chart/index.tsx +20 -7
  159. package/src/components/card/index.tsx +32 -7
  160. package/src/components/carousel/index.tsx +42 -20
  161. package/src/components/checkbox/index.tsx +1 -2
  162. package/src/components/dialog/index.tsx +58 -5
  163. package/src/components/drawer/index.tsx +3 -3
  164. package/src/components/fab/index.tsx +8 -12
  165. package/src/components/field/index.tsx +52 -10
  166. package/src/components/flow/flow-accessibility.ts +109 -0
  167. package/src/components/flow/index.tsx +162 -50
  168. package/src/components/form/use-form.ts +101 -65
  169. package/src/components/heatmap-chart/index.tsx +2 -3
  170. package/src/components/hex-chart/index.tsx +4 -5
  171. package/src/components/kpi/index.tsx +3 -1
  172. package/src/components/line-chart/index.tsx +17 -6
  173. package/src/components/map/index.tsx +110 -7
  174. package/src/components/map/map-accessibility.ts +41 -0
  175. package/src/components/map/maplibre.ts +2 -0
  176. package/src/components/menu/index.tsx +4 -1
  177. package/src/components/number-input/index.tsx +1 -26
  178. package/src/components/number-input/number-input-math.ts +50 -0
  179. package/src/components/pie-chart/index.tsx +2 -2
  180. package/src/components/plot/index.tsx +1565 -0
  181. package/src/components/polar-area-chart/index.tsx +2 -2
  182. package/src/components/radar-chart/index.tsx +14 -4
  183. package/src/components/radio-group/index.tsx +1 -2
  184. package/src/components/ring-chart/index.tsx +2 -2
  185. package/src/components/section-rail/index.tsx +133 -14
  186. package/src/components/selection-mode/index.tsx +163 -26
  187. package/src/components/sortable/index.tsx +27 -69
  188. package/src/components/sortable/reorder.ts +88 -0
  189. package/src/components/swipe/index.tsx +7 -4
  190. package/src/components/switch/index.tsx +42 -6
  191. package/src/components/time-picker/accessibility.ts +24 -0
  192. package/src/components/time-picker/index.tsx +48 -2
  193. package/src/components/waterfall-chart/index.tsx +1589 -0
  194. package/src/index.ts +69 -0
  195. package/src/primitives/modal-isolation-store.ts +31 -0
  196. package/src/primitives/portal.tsx +34 -4
  197. package/src/providers/panel-ui-provider.tsx +41 -9
  198. package/theme.css +54 -3
@@ -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 value={subscribed} onValueChange={setSubscribed} />
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
- () => ({ invalid, disabled, required }),
112
- [invalid, disabled, required]
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 <Text ref={ref} className={description({ className })} {...props} />;
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
- return <Text ref={ref} className={title({ className })} {...props} />;
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,109 @@
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
+ /** Keep pointer and assistive-technology movement inside the same group bounds. */
33
+ export function clampNodePosition(
34
+ rect: FlowRect,
35
+ bounds: FlowRect | undefined,
36
+ position: FlowNodePosition
37
+ ): FlowNodePosition {
38
+ 'worklet';
39
+ if (!bounds) return position;
40
+
41
+ return {
42
+ x: Math.min(Math.max(position.x, bounds.x), bounds.x + bounds.width - rect.width),
43
+ y: Math.min(Math.max(position.y, bounds.y), bounds.y + bounds.height - rect.height),
44
+ };
45
+ }
46
+
47
+ export function moveNodePosition(
48
+ rect: FlowRect,
49
+ bounds: FlowRect | undefined,
50
+ actionName: string,
51
+ step: number
52
+ ): FlowNodePosition | undefined {
53
+ const action = FLOW_MOVE_ACTIONS.find((entry) => entry.name === actionName);
54
+ if (!action) return undefined;
55
+
56
+ return clampNodePosition(rect, bounds, {
57
+ x: rect.x + action.dx * Math.max(1, step),
58
+ y: rect.y + action.dy * Math.max(1, step),
59
+ });
60
+ }
61
+
62
+ /** Build connection choices from the same registered nodes and handles used by drag hit-testing. */
63
+ export function getFlowConnectionActions(
64
+ sourceNode: string,
65
+ nodes: FlowAccessibilityNode[],
66
+ handles: FlowAccessibilityHandle[]
67
+ ): FlowConnectionAction[] {
68
+ const sources = handles.filter(
69
+ (handle) => handle.node === sourceNode && handle.type !== 'target'
70
+ );
71
+ if (sources.length === 0) return [];
72
+
73
+ const actions: FlowConnectionAction[] = [];
74
+ for (const source of sources) {
75
+ for (const targetNode of nodes) {
76
+ if (targetNode.id === sourceNode) continue;
77
+ const targets = handles.filter(
78
+ (handle) => handle.node === targetNode.id && handle.type !== 'source'
79
+ );
80
+
81
+ if (targets.length === 0) {
82
+ actions.push({
83
+ name: `flow-connect:${source.key}->${targetNode.id}`,
84
+ label: `Connect ${source.label} to ${targetNode.label}`,
85
+ connection: {
86
+ source: sourceNode,
87
+ sourceHandle: source.id,
88
+ target: targetNode.id,
89
+ },
90
+ });
91
+ continue;
92
+ }
93
+
94
+ for (const target of targets) {
95
+ actions.push({
96
+ name: `flow-connect:${source.key}->${target.key}`,
97
+ label: `Connect ${source.label} to ${targetNode.label}, ${target.label}`,
98
+ connection: {
99
+ source: sourceNode,
100
+ sourceHandle: source.id,
101
+ target: target.node,
102
+ targetHandle: target.id,
103
+ },
104
+ });
105
+ }
106
+ }
107
+ }
108
+ return actions;
109
+ }
@@ -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
- nodeIds: string[];
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
- onConnect?: (connection: FlowConnection) => void;
236
- isValidConnection?: (connection: FlowConnection) => boolean;
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 [nodeIds, setNodeIds] = useState<string[]>([]);
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
- setNodeIds((current) => (current.includes(id) ? current : [...current, id]));
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
- setNodeIds((current) => current.filter((entry) => entry !== id));
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 || nodeIds.length === 0) return;
567
+ if (!fitOnMount.current || nodes.length === 0) return;
553
568
  const timer = setTimeout(fitView, 32);
554
569
  return () => clearTimeout(timer);
555
- }, [fitView, nodeIds.length]);
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
- nodeIds,
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
- onConnect,
663
- isValidConnection,
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
- isValidConnection,
696
+ connectNodes,
682
697
  locked,
683
698
  maxZoom,
684
699
  minZoom,
685
- nodeIds,
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 = useRef(`panelui-flow-${Math.random().toString(36).slice(2, 7)}`).current;
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 box = rects.value[group];
1100
- if (box) {
1101
- const maxX = box.x + box.width - rect.width;
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 { rects, zoom, connection, handles, locked, registerHandle, unregisterHandle } = flow;
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({ key, node, id, side: position, offset, type });
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
- }, [id, key, node, offset, position, registerHandle, type, unregisterHandle]);
1221
-
1222
- const connectRef = useRef(flow.onConnect);
1223
- connectRef.current = flow.onConnect;
1224
- const validRef = useRef(flow.isValidConnection);
1225
- validRef.current = flow.isValidConnection;
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
- const payload: FlowConnection = {
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
- accessibilityRole="button"
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, nodeIds, groupIds, translateX, translateY, zoom, size } =
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
- {nodeIds.map((id) => {
2114
+ {nodes.map(({ id }) => {
2003
2115
  const group = groups.has(id);
2004
2116
  return (
2005
2117
  <MiniMapNode