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
@@ -89,6 +89,7 @@ import { useCSSVariable } from 'uniwind';
89
89
  import { GripVerticalIcon, IconColorProvider } from '../../icons';
90
90
  import { cn } from '../../utils/cn';
91
91
  import { impactKnock, selectionTick } from '../../utils/haptics';
92
+ import { moveWithPinned, stepWithPinned } from './reorder';
92
93
 
93
94
  /**
94
95
  * Rows getting out of the way of the one being carried. Quick, because they
@@ -228,66 +229,6 @@ function slotCenter(
228
229
  return offset + (self === undefined ? 0 : (heights[self] ?? 0)) / 2;
229
230
  }
230
231
 
231
- /**
232
- * The order after a row has moved, with pinned rows left where they were.
233
- *
234
- * The move is applied first, to the whole list, so a carried row can be dragged
235
- * *past* a pinned one — refusing the move instead would make a pinned row a
236
- * wall, and a row that holds its place is not the same as a row nothing may
237
- * cross. The pinned ids are then put back at the indices they occupy in the
238
- * laid-out order, and everything else falls into the slots that are left, in
239
- * the order the move produced.
240
- *
241
- * `laid` rather than `list` is what the fixed indices are read from, because
242
- * that is the one order a pinned row is guaranteed to be correctly placed in:
243
- * it is where it was rendered, and holding its slot is the whole point.
244
- */
245
- function moveWithPinned(
246
- list: readonly string[],
247
- laid: readonly string[],
248
- pinned: Record<string, boolean>,
249
- id: string,
250
- from: number,
251
- to: number
252
- ): string[] {
253
- 'worklet';
254
- const moved = [...list];
255
- moved.splice(from, 1);
256
- moved.splice(to, 0, id);
257
-
258
- const next: (string | undefined)[] = [];
259
- let anyPinned = false;
260
- for (let i = 0; i < moved.length; i += 1) next.push(undefined);
261
- for (let i = 0; i < laid.length && i < next.length; i += 1) {
262
- const at = laid[i];
263
- if (at !== undefined && pinned[at]) {
264
- next[i] = at;
265
- anyPinned = true;
266
- }
267
- }
268
- if (!anyPinned) return moved;
269
-
270
- const free: string[] = [];
271
- for (let i = 0; i < moved.length; i += 1) {
272
- const at = moved[i];
273
- if (at !== undefined && !pinned[at]) free.push(at);
274
- }
275
-
276
- const result: string[] = [];
277
- let f = 0;
278
- for (let i = 0; i < next.length; i += 1) {
279
- const held = next[i];
280
- if (held !== undefined) {
281
- result.push(held);
282
- continue;
283
- }
284
- const take = free[f];
285
- f += 1;
286
- if (take !== undefined) result.push(take);
287
- }
288
- return result;
289
- }
290
-
291
232
  /**
292
233
  * Where the dragged row belongs now, given where its edges have reached.
293
234
  *
@@ -563,16 +504,26 @@ function SortableRoot({
563
504
  * the transforms that were holding rows in their new slots are no longer
564
505
  * needed — the layout puts them there now. Both orders are reset together,
565
506
  * which is what makes the re-render after a drop invisible.
507
+ *
508
+ * During render, and not in an effect. An effect runs *after* the commit that
509
+ * moved the rows, so for the frame in between, the dropped row sat in its new
510
+ * slot still carrying the transform that had carried it there — drawn a whole
511
+ * row's height away from where it belongs, which is what the flash after a
512
+ * drop was. Written here it lands on the same commit as the reorder, and there
513
+ * is no frame in which the two disagree.
514
+ *
515
+ * Writing a shared value is not React state, so this is not a render with a
516
+ * side effect on the tree; the ref is only there to make it happen once per
517
+ * change of order rather than on every render of the screen around it.
566
518
  */
567
- useEffect(() => {
519
+ const applied = useRef(key);
520
+ if (applied.current !== key) {
521
+ applied.current = key;
568
522
  order.value = value;
569
523
  rendered.value = value;
570
524
  translate.value = 0;
571
525
  lift.value = 0;
572
- // `value` is covered by `key`; depending on the array itself would fire
573
- // this on every render of the screen around it.
574
- // eslint-disable-next-line react-hooks/exhaustive-deps
575
- }, [key, order, rendered, translate, lift]);
526
+ }
576
527
 
577
528
  const indices = useMemo(() => {
578
529
  const map = new Map<string, number>();
@@ -750,15 +701,22 @@ function SortableRoot({
750
701
  const current = rendered.value.indexOf(id);
751
702
  if (current < 0) return;
752
703
 
753
- const to = current + delta;
754
- if (to < 0 || to >= rendered.value.length) return;
704
+ const next = stepWithPinned(
705
+ rendered.value,
706
+ rendered.value,
707
+ pinned.value,
708
+ id,
709
+ current,
710
+ delta
711
+ );
712
+ const to = next.indexOf(id);
713
+ if (to === current) return;
755
714
 
756
- const next = reorderItems(rendered.value, current, to);
757
715
  order.value = next;
758
716
  if (haptics) selectionTick();
759
717
  onReorder?.(next, { id, from: current, to });
760
718
  },
761
- [haptics, onReorder, order, rendered]
719
+ [haptics, onReorder, order, pinned, rendered]
762
720
  );
763
721
 
764
722
  const context = useMemo<SortableContextValue>(
@@ -0,0 +1,88 @@
1
+ type PinnedItems = Readonly<Record<string, boolean>>;
2
+
3
+ /**
4
+ * Move one item, then restore pinned items to the slots in `laid`.
5
+ *
6
+ * Restoring after the move lets an item cross a pin instead of turning that
7
+ * slot into a wall. The laid-out order is the source of truth because it is
8
+ * the last arrangement the caller rendered and therefore the one whose pins
9
+ * are known to be in their promised positions.
10
+ */
11
+ export function moveWithPinned(
12
+ list: readonly string[],
13
+ laid: readonly string[],
14
+ pinned: PinnedItems,
15
+ id: string,
16
+ from: number,
17
+ to: number
18
+ ): string[] {
19
+ 'worklet';
20
+ if (
21
+ from === to ||
22
+ from < 0 ||
23
+ from >= list.length ||
24
+ to < 0 ||
25
+ to >= list.length ||
26
+ list[from] !== id ||
27
+ pinned[id]
28
+ ) {
29
+ return [...list];
30
+ }
31
+
32
+ const moved = [...list];
33
+ moved.splice(from, 1);
34
+ moved.splice(to, 0, id);
35
+
36
+ const next: (string | undefined)[] = [];
37
+ let anyPinned = false;
38
+ for (let i = 0; i < moved.length; i += 1) next.push(undefined);
39
+ for (let i = 0; i < laid.length && i < next.length; i += 1) {
40
+ const at = laid[i];
41
+ if (at !== undefined && pinned[at]) {
42
+ next[i] = at;
43
+ anyPinned = true;
44
+ }
45
+ }
46
+ if (!anyPinned) return moved;
47
+
48
+ const free: string[] = [];
49
+ for (let i = 0; i < moved.length; i += 1) {
50
+ const at = moved[i];
51
+ if (at !== undefined && !pinned[at]) free.push(at);
52
+ }
53
+
54
+ const result: string[] = [];
55
+ let freeIndex = 0;
56
+ for (let i = 0; i < next.length; i += 1) {
57
+ const held = next[i];
58
+ if (held !== undefined) {
59
+ result.push(held);
60
+ continue;
61
+ }
62
+ const take = free[freeIndex];
63
+ freeIndex += 1;
64
+ if (take !== undefined) result.push(take);
65
+ }
66
+ return result;
67
+ }
68
+
69
+ /** Move by one available slot, stepping past pinned slots that would be a no-op. */
70
+ export function stepWithPinned(
71
+ list: readonly string[],
72
+ laid: readonly string[],
73
+ pinned: PinnedItems,
74
+ id: string,
75
+ from: number,
76
+ delta: number
77
+ ): string[] {
78
+ 'worklet';
79
+ const direction = Math.sign(delta);
80
+ if (direction === 0) return [...list];
81
+
82
+ for (let to = from + direction; to >= 0 && to < list.length; to += direction) {
83
+ const next = moveWithPinned(list, laid, pinned, id, from, to);
84
+ if (next.indexOf(id) !== from) return next;
85
+ }
86
+
87
+ return [...list];
88
+ }
@@ -258,10 +258,13 @@ const actionVariants = tv({
258
258
  color: {
259
259
  default: { root: 'bg-muted-foreground', label: 'text-background' },
260
260
  primary: { root: 'bg-primary', label: 'text-primary-foreground' },
261
- success: { root: 'bg-success', label: 'text-white' },
262
- warning: { root: 'bg-warning', label: 'text-white' },
263
- info: { root: 'bg-info', label: 'text-white' },
264
- destructive: { root: 'bg-destructive', label: 'text-white' },
261
+ success: { root: 'bg-success', label: 'text-success-solid-foreground' },
262
+ warning: { root: 'bg-warning', label: 'text-warning-solid-foreground' },
263
+ info: { root: 'bg-info', label: 'text-info-solid-foreground' },
264
+ destructive: {
265
+ root: 'bg-destructive',
266
+ label: 'text-destructive-solid-foreground',
267
+ },
265
268
  },
266
269
  },
267
270
  defaultVariants: {
@@ -1,5 +1,5 @@
1
1
  import { forwardRef, useEffect } from 'react';
2
- import { Pressable, View } from 'react-native';
2
+ import { Pressable, View, type PressableProps } from 'react-native';
3
3
  import Animated, {
4
4
  interpolate,
5
5
  useAnimatedStyle,
@@ -10,6 +10,7 @@ import { tv, type VariantProps } from 'tailwind-variants';
10
10
  import { useDirectionSign } from '../../hooks/use-direction';
11
11
  import { getNativeUI } from '../../native';
12
12
  import { selectionTick } from '../../utils/haptics';
13
+ import { useFieldLabelledBy } from '../field';
13
14
 
14
15
  const SPRING = { damping: 18, stiffness: 250, mass: 0.5 } as const;
15
16
 
@@ -35,7 +36,22 @@ const switchVariants = tv({
35
36
 
36
37
  const TRAVEL: Record<'sm' | 'md', number> = { sm: 16, md: 20 };
37
38
 
38
- export interface SwitchProps extends VariantProps<typeof switchVariants> {
39
+ /**
40
+ * Room around each track so the target reaches 48dp on both axes.
41
+ *
42
+ * Slop rather than a bigger box, because a box is layout: a switch sized to
43
+ * its target would stand 48dp tall in every row it has ever been placed in,
44
+ * and the tracks are only 24 and 28 tall. The `md` track is already 48 wide,
45
+ * which is why the horizontal figures differ between the two sizes.
46
+ */
47
+ const SWITCH_HIT_SLOP: Record<'sm' | 'md', { top: number; bottom: number; left: number; right: number }> = {
48
+ sm: { top: 12, bottom: 12, left: 4, right: 4 },
49
+ md: { top: 10, bottom: 10, left: 0, right: 0 },
50
+ };
51
+
52
+ export interface SwitchProps
53
+ extends VariantProps<typeof switchVariants>,
54
+ Pick<PressableProps, 'accessibilityLabel' | 'accessibilityHint' | 'accessibilityLabelledBy'> {
39
55
  className?: string;
40
56
  value: boolean;
41
57
  onValueChange?: (value: boolean) => void;
@@ -48,7 +64,7 @@ export interface SwitchProps extends VariantProps<typeof switchVariants> {
48
64
  * `className` and `size` are ignored.
49
65
  */
50
66
  native?: boolean;
51
- /** Text label drawn beside the control. Native mode only. */
67
+ /** Names the control to assistive technology; native mode also draws it. */
52
68
  label?: string;
53
69
  /**
54
70
  * Tick the haptic engine each time the switch is flipped — a toggle you feel
@@ -65,7 +81,19 @@ export interface SwitchProps extends VariantProps<typeof switchVariants> {
65
81
  */
66
82
  export const Switch = forwardRef<View, SwitchProps>(
67
83
  (
68
- { className, value, onValueChange, disabled, size = 'md', native, label, haptics },
84
+ {
85
+ className,
86
+ value,
87
+ onValueChange,
88
+ disabled,
89
+ size = 'md',
90
+ native,
91
+ label,
92
+ haptics,
93
+ accessibilityLabel,
94
+ accessibilityHint,
95
+ accessibilityLabelledBy,
96
+ },
69
97
  ref
70
98
  ) => {
71
99
  const progress = useSharedValue(value ? 1 : 0);
@@ -75,6 +103,9 @@ export const Switch = forwardRef<View, SwitchProps>(
75
103
  const sign = useDirectionSign();
76
104
  const nativeUI = native ? getNativeUI() : null;
77
105
  const slots = switchVariants({ size, disabled: !!disabled });
106
+ // React Native's labelled-by relationship is Android-only. `label` or an
107
+ // explicit accessibilityLabel remains the portable name on iOS.
108
+ const fieldLabelledBy = useFieldLabelledBy();
78
109
 
79
110
  useEffect(() => {
80
111
  progress.value = withSpring(value ? 1 : 0, SPRING);
@@ -101,7 +132,7 @@ export const Switch = forwardRef<View, SwitchProps>(
101
132
  <NativeSwitch
102
133
  value={value}
103
134
  onValueChange={(next: boolean) => onValueChange?.(next)}
104
- label={label}
135
+ label={label ?? accessibilityLabel}
105
136
  disabled={disabled}
106
137
  />
107
138
  </Host>
@@ -113,12 +144,17 @@ export const Switch = forwardRef<View, SwitchProps>(
113
144
  ref={ref}
114
145
  accessibilityRole="switch"
115
146
  accessibilityState={{ checked: value, disabled: !!disabled }}
147
+ accessibilityLabel={accessibilityLabel ?? label}
148
+ accessibilityHint={accessibilityHint}
149
+ accessibilityLabelledBy={
150
+ accessibilityLabelledBy ?? (accessibilityLabel || label ? undefined : fieldLabelledBy)
151
+ }
116
152
  disabled={disabled}
117
153
  onPress={() => {
118
154
  if (haptics) selectionTick();
119
155
  onValueChange?.(!value);
120
156
  }}
121
- hitSlop={8}
157
+ hitSlop={SWITCH_HIT_SLOP[size]}
122
158
  >
123
159
  <View className={slots.track({ className })}>
124
160
  <Animated.View style={activeTrackStyle} className={slots.activeTrack()} />
@@ -0,0 +1,24 @@
1
+ /** Resolve a standard adjustable action without wrapping past either bound. */
2
+ export function indexForAccessibilityAction(
3
+ index: number,
4
+ length: number,
5
+ actionName: string,
6
+ disabled = false
7
+ ): number | undefined {
8
+ const direction = actionName === 'increment' ? 1 : actionName === 'decrement' ? -1 : 0;
9
+ if (disabled || direction === 0 || length <= 0) return undefined;
10
+
11
+ const next = Math.min(Math.max(index + direction, 0), length - 1);
12
+ return next === index ? undefined : next;
13
+ }
14
+
15
+ /** Keep the numeric adjustable range aligned with the values the face offers. */
16
+ export function accessibilityValueForIndex(index: number, length: number, text: string) {
17
+ const max = Math.max(0, length - 1);
18
+ return {
19
+ min: 0,
20
+ max,
21
+ now: Math.min(Math.max(index, 0), max),
22
+ text,
23
+ };
24
+ }
@@ -55,6 +55,7 @@ import {
55
55
  import {
56
56
  ScrollView,
57
57
  View,
58
+ type AccessibilityActionEvent,
58
59
  type NativeScrollEvent,
59
60
  type NativeSyntheticEvent,
60
61
  } from 'react-native';
@@ -74,6 +75,10 @@ import { ClockIcon } from '../../icons';
74
75
  import { Text } from '../../primitives/text';
75
76
  import { cn } from '../../utils/cn';
76
77
  import { selectionTick } from '../../utils/haptics';
78
+ import {
79
+ accessibilityValueForIndex,
80
+ indexForAccessibilityAction,
81
+ } from './accessibility';
77
82
  import {
78
83
  clampTime,
79
84
  displayHour,
@@ -398,6 +403,20 @@ function Column<T>({
398
403
 
399
404
  const selectedItem = items[index];
400
405
 
406
+ const onAccessibilityAction = useCallback(
407
+ (event: AccessibilityActionEvent) => {
408
+ const next = indexForAccessibilityAction(
409
+ index,
410
+ items.length,
411
+ event.nativeEvent.actionName,
412
+ disabled
413
+ );
414
+ if (next === undefined) return;
415
+ onIndexChange(next);
416
+ },
417
+ [disabled, index, items.length, onIndexChange]
418
+ );
419
+
401
420
  return (
402
421
  <Animated.ScrollView
403
422
  ref={ref}
@@ -426,7 +445,12 @@ function Column<T>({
426
445
  removeClippedSubviews={false}
427
446
  accessibilityRole="adjustable"
428
447
  accessibilityLabel={accessibilityLabel}
429
- accessibilityValue={selectedItem ? { text: label(selectedItem) } : undefined}
448
+ accessibilityState={{ disabled }}
449
+ accessibilityValue={
450
+ selectedItem ? accessibilityValueForIndex(index, items.length, label(selectedItem)) : undefined
451
+ }
452
+ accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
453
+ onAccessibilityAction={onAccessibilityAction}
430
454
  >
431
455
  {items.map((item, i) => (
432
456
  <ColumnRow key={i} offset={offset} index={i} selected={i === index}>
@@ -930,6 +954,21 @@ function RulerFace({
930
954
  [width]
931
955
  );
932
956
 
957
+ const onAccessibilityAction = useCallback(
958
+ (event: AccessibilityActionEvent) => {
959
+ const next = indexForAccessibilityAction(
960
+ index,
961
+ times.length,
962
+ event.nativeEvent.actionName,
963
+ disabled
964
+ );
965
+ if (next === undefined) return;
966
+ const time = times[next];
967
+ if (time) onValueChange(time);
968
+ },
969
+ [disabled, index, onValueChange, times]
970
+ );
971
+
933
972
  return (
934
973
  <View className={readout === 'none' ? undefined : 'gap-4'}>
935
974
  {readout === 'none' ? null : (
@@ -955,7 +994,14 @@ function RulerFace({
955
994
  onMomentumScrollEnd={onMomentumEnd}
956
995
  accessibilityRole="adjustable"
957
996
  accessibilityLabel="Time"
958
- accessibilityValue={{ text: formatTime(value, { hourCycle, locale }) }}
997
+ accessibilityState={{ disabled }}
998
+ accessibilityValue={accessibilityValueForIndex(
999
+ index,
1000
+ times.length,
1001
+ formatTime(value, { hourCycle, locale })
1002
+ )}
1003
+ accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
1004
+ onAccessibilityAction={onAccessibilityAction}
959
1005
  >
960
1006
  {/*
961
1007
  No numbers under the ticks. The readout above already says the