panelui-native 0.65.0 → 0.68.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 (209) hide show
  1. package/README.md +21 -1
  2. package/lib/module/components/area-chart/index.js +18 -1
  3. package/lib/module/components/area-chart/index.js.map +1 -1
  4. package/lib/module/components/bar-chart/index.js +18 -1
  5. package/lib/module/components/bar-chart/index.js.map +1 -1
  6. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  7. package/lib/module/components/candlestick-chart/index.js +18 -1
  8. package/lib/module/components/candlestick-chart/index.js.map +1 -1
  9. package/lib/module/components/carousel/carousel-lifecycle.js +51 -0
  10. package/lib/module/components/carousel/carousel-lifecycle.js.map +1 -0
  11. package/lib/module/components/carousel/index.js +61 -24
  12. package/lib/module/components/carousel/index.js.map +1 -1
  13. package/lib/module/components/context-menu/context-menu-invocation.js +23 -0
  14. package/lib/module/components/context-menu/context-menu-invocation.js.map +1 -0
  15. package/lib/module/components/context-menu/index.js +78 -9
  16. package/lib/module/components/context-menu/index.js.map +1 -1
  17. package/lib/module/components/dialog/index.js.map +1 -1
  18. package/lib/module/components/drawer/index.js.map +1 -1
  19. package/lib/module/components/flow/flow-accessibility.js +18 -2
  20. package/lib/module/components/flow/flow-accessibility.js.map +1 -1
  21. package/lib/module/components/flow/flow-identifiers.js +81 -0
  22. package/lib/module/components/flow/flow-identifiers.js.map +1 -0
  23. package/lib/module/components/flow/index.js +12 -18
  24. package/lib/module/components/flow/index.js.map +1 -1
  25. package/lib/module/components/form/index.js +7 -5
  26. package/lib/module/components/form/index.js.map +1 -1
  27. package/lib/module/components/form/typed-form.js +11 -0
  28. package/lib/module/components/form/typed-form.js.map +1 -0
  29. package/lib/module/components/form/use-form.js +4 -0
  30. package/lib/module/components/form/use-form.js.map +1 -1
  31. package/lib/module/components/heatmap-chart/index.js +27 -1
  32. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  33. package/lib/module/components/line-chart/index.js +18 -1
  34. package/lib/module/components/line-chart/index.js.map +1 -1
  35. package/lib/module/components/message-scroller/index.js +187 -17
  36. package/lib/module/components/message-scroller/index.js.map +1 -1
  37. package/lib/module/components/message-scroller/message-scroller-math.js +43 -0
  38. package/lib/module/components/message-scroller/message-scroller-math.js.map +1 -0
  39. package/lib/module/components/meter/index.js +250 -0
  40. package/lib/module/components/meter/index.js.map +1 -0
  41. package/lib/module/components/meter/meter-scale.js +88 -0
  42. package/lib/module/components/meter/meter-scale.js.map +1 -0
  43. package/lib/module/components/planner/index.js +714 -0
  44. package/lib/module/components/planner/index.js.map +1 -0
  45. package/lib/module/components/planner/planner-entries.js +124 -0
  46. package/lib/module/components/planner/planner-entries.js.map +1 -0
  47. package/lib/module/components/popover/index.js.map +1 -1
  48. package/lib/module/components/radar-chart/index.js +18 -1
  49. package/lib/module/components/radar-chart/index.js.map +1 -1
  50. package/lib/module/components/scatter-chart/index.js +18 -1
  51. package/lib/module/components/scatter-chart/index.js.map +1 -1
  52. package/lib/module/components/selection-mode/index.js +26 -31
  53. package/lib/module/components/selection-mode/index.js.map +1 -1
  54. package/lib/module/components/selection-mode/selection-mode-contracts.js +48 -0
  55. package/lib/module/components/selection-mode/selection-mode-contracts.js.map +1 -0
  56. package/lib/module/components/signature/index.js +62 -20
  57. package/lib/module/components/signature/index.js.map +1 -1
  58. package/lib/module/components/signature/signature-accessibility.js +58 -0
  59. package/lib/module/components/signature/signature-accessibility.js.map +1 -0
  60. package/lib/module/components/skeleton/index.js +45 -7
  61. package/lib/module/components/skeleton/index.js.map +1 -1
  62. package/lib/module/components/slider/index.js +22 -18
  63. package/lib/module/components/slider/index.js.map +1 -1
  64. package/lib/module/components/spinner/index.js +63 -14
  65. package/lib/module/components/spinner/index.js.map +1 -1
  66. package/lib/module/components/time-picker/index.js +46 -21
  67. package/lib/module/components/time-picker/index.js.map +1 -1
  68. package/lib/module/components/time-picker/ruler-window.js +25 -0
  69. package/lib/module/components/time-picker/ruler-window.js.map +1 -0
  70. package/lib/module/hooks/breakpoint-contract.js +29 -0
  71. package/lib/module/hooks/breakpoint-contract.js.map +1 -0
  72. package/lib/module/hooks/index.js +1 -1
  73. package/lib/module/hooks/index.js.map +1 -1
  74. package/lib/module/hooks/use-breakpoint.js +46 -1
  75. package/lib/module/hooks/use-breakpoint.js.map +1 -1
  76. package/lib/module/index.js +3 -1
  77. package/lib/module/index.js.map +1 -1
  78. package/lib/module/primitives/chart-accessibility.js +68 -0
  79. package/lib/module/primitives/chart-accessibility.js.map +1 -0
  80. package/lib/module/primitives/scrim.js +94 -3
  81. package/lib/module/primitives/scrim.js.map +1 -1
  82. package/lib/typescript/src/components/area-chart/index.d.ts +2 -1
  83. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  84. package/lib/typescript/src/components/bar-chart/index.d.ts +2 -1
  85. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/bottom-sheet/index.d.ts +3 -0
  87. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  88. package/lib/typescript/src/components/candlestick-chart/index.d.ts +2 -1
  89. package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
  90. package/lib/typescript/src/components/carousel/carousel-lifecycle.d.ts +23 -0
  91. package/lib/typescript/src/components/carousel/carousel-lifecycle.d.ts.map +1 -0
  92. package/lib/typescript/src/components/carousel/index.d.ts +5 -2
  93. package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
  94. package/lib/typescript/src/components/context-menu/context-menu-invocation.d.ts +20 -0
  95. package/lib/typescript/src/components/context-menu/context-menu-invocation.d.ts.map +1 -0
  96. package/lib/typescript/src/components/context-menu/index.d.ts +18 -8
  97. package/lib/typescript/src/components/context-menu/index.d.ts.map +1 -1
  98. package/lib/typescript/src/components/dialog/index.d.ts +3 -0
  99. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  100. package/lib/typescript/src/components/drawer/index.d.ts +3 -0
  101. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
  102. package/lib/typescript/src/components/flow/flow-accessibility.d.ts +8 -0
  103. package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -1
  104. package/lib/typescript/src/components/flow/flow-identifiers.d.ts +24 -0
  105. package/lib/typescript/src/components/flow/flow-identifiers.d.ts.map +1 -0
  106. package/lib/typescript/src/components/flow/index.d.ts +6 -5
  107. package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
  108. package/lib/typescript/src/components/form/index.d.ts +18 -5
  109. package/lib/typescript/src/components/form/index.d.ts.map +1 -1
  110. package/lib/typescript/src/components/form/typed-form.d.ts +7 -0
  111. package/lib/typescript/src/components/form/typed-form.d.ts.map +1 -0
  112. package/lib/typescript/src/components/form/use-form.d.ts +4 -0
  113. package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
  114. package/lib/typescript/src/components/heatmap-chart/index.d.ts +2 -1
  115. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  116. package/lib/typescript/src/components/line-chart/index.d.ts +2 -1
  117. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  118. package/lib/typescript/src/components/message-scroller/index.d.ts +30 -2
  119. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
  120. package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts +18 -0
  121. package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts.map +1 -0
  122. package/lib/typescript/src/components/meter/index.d.ts +270 -0
  123. package/lib/typescript/src/components/meter/index.d.ts.map +1 -0
  124. package/lib/typescript/src/components/meter/meter-scale.d.ts +57 -0
  125. package/lib/typescript/src/components/meter/meter-scale.d.ts.map +1 -0
  126. package/lib/typescript/src/components/planner/index.d.ts +274 -0
  127. package/lib/typescript/src/components/planner/index.d.ts.map +1 -0
  128. package/lib/typescript/src/components/planner/planner-entries.d.ts +75 -0
  129. package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -0
  130. package/lib/typescript/src/components/popover/index.d.ts +3 -0
  131. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  132. package/lib/typescript/src/components/radar-chart/index.d.ts +4 -0
  133. package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
  134. package/lib/typescript/src/components/scatter-chart/index.d.ts +2 -1
  135. package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -1
  136. package/lib/typescript/src/components/selection-mode/index.d.ts +6 -4
  137. package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
  138. package/lib/typescript/src/components/selection-mode/selection-mode-contracts.d.ts +18 -0
  139. package/lib/typescript/src/components/selection-mode/selection-mode-contracts.d.ts.map +1 -0
  140. package/lib/typescript/src/components/signature/index.d.ts +6 -0
  141. package/lib/typescript/src/components/signature/index.d.ts.map +1 -1
  142. package/lib/typescript/src/components/signature/signature-accessibility.d.ts +27 -0
  143. package/lib/typescript/src/components/signature/signature-accessibility.d.ts.map +1 -0
  144. package/lib/typescript/src/components/skeleton/index.d.ts +25 -2
  145. package/lib/typescript/src/components/skeleton/index.d.ts.map +1 -1
  146. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  147. package/lib/typescript/src/components/spinner/index.d.ts +25 -2
  148. package/lib/typescript/src/components/spinner/index.d.ts.map +1 -1
  149. package/lib/typescript/src/components/time-picker/index.d.ts +5 -6
  150. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  151. package/lib/typescript/src/components/time-picker/ruler-window.d.ts +12 -0
  152. package/lib/typescript/src/components/time-picker/ruler-window.d.ts.map +1 -0
  153. package/lib/typescript/src/hooks/breakpoint-contract.d.ts +6 -0
  154. package/lib/typescript/src/hooks/breakpoint-contract.d.ts.map +1 -0
  155. package/lib/typescript/src/hooks/index.d.ts +1 -1
  156. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  157. package/lib/typescript/src/hooks/use-breakpoint.d.ts +22 -0
  158. package/lib/typescript/src/hooks/use-breakpoint.d.ts.map +1 -1
  159. package/lib/typescript/src/index.d.ts +4 -2
  160. package/lib/typescript/src/index.d.ts.map +1 -1
  161. package/lib/typescript/src/primitives/chart-accessibility.d.ts +33 -0
  162. package/lib/typescript/src/primitives/chart-accessibility.d.ts.map +1 -0
  163. package/lib/typescript/src/primitives/scrim.d.ts +5 -3
  164. package/lib/typescript/src/primitives/scrim.d.ts.map +1 -1
  165. package/lib/typescript/types/form-api.type-test.d.ts +2 -0
  166. package/lib/typescript/types/form-api.type-test.d.ts.map +1 -0
  167. package/package.json +34 -5
  168. package/src/components/area-chart/index.tsx +27 -2
  169. package/src/components/bar-chart/index.tsx +27 -2
  170. package/src/components/bottom-sheet/index.tsx +3 -0
  171. package/src/components/candlestick-chart/index.tsx +30 -2
  172. package/src/components/carousel/carousel-lifecycle.ts +74 -0
  173. package/src/components/carousel/index.tsx +80 -25
  174. package/src/components/context-menu/context-menu-invocation.ts +44 -0
  175. package/src/components/context-menu/index.tsx +113 -13
  176. package/src/components/dialog/index.tsx +3 -0
  177. package/src/components/drawer/index.tsx +3 -0
  178. package/src/components/flow/flow-accessibility.ts +16 -2
  179. package/src/components/flow/flow-identifiers.ts +98 -0
  180. package/src/components/flow/index.tsx +29 -19
  181. package/src/components/form/index.tsx +32 -5
  182. package/src/components/form/typed-form.ts +13 -0
  183. package/src/components/form/use-form.ts +9 -0
  184. package/src/components/heatmap-chart/index.tsx +49 -3
  185. package/src/components/line-chart/index.tsx +27 -2
  186. package/src/components/message-scroller/index.tsx +273 -13
  187. package/src/components/message-scroller/message-scroller-math.ts +62 -0
  188. package/src/components/meter/index.tsx +318 -0
  189. package/src/components/meter/meter-scale.ts +109 -0
  190. package/src/components/planner/index.tsx +886 -0
  191. package/src/components/planner/planner-entries.ts +146 -0
  192. package/src/components/popover/index.tsx +3 -0
  193. package/src/components/radar-chart/index.tsx +24 -0
  194. package/src/components/scatter-chart/index.tsx +27 -2
  195. package/src/components/selection-mode/index.tsx +37 -30
  196. package/src/components/selection-mode/selection-mode-contracts.ts +53 -0
  197. package/src/components/signature/index.tsx +107 -20
  198. package/src/components/signature/signature-accessibility.ts +67 -0
  199. package/src/components/skeleton/index.tsx +59 -5
  200. package/src/components/slider/index.tsx +28 -18
  201. package/src/components/spinner/index.tsx +80 -13
  202. package/src/components/time-picker/index.tsx +57 -29
  203. package/src/components/time-picker/ruler-window.ts +24 -0
  204. package/src/hooks/breakpoint-contract.ts +31 -0
  205. package/src/hooks/index.ts +4 -0
  206. package/src/hooks/use-breakpoint.ts +61 -10
  207. package/src/index.ts +32 -0
  208. package/src/primitives/chart-accessibility.ts +102 -0
  209. package/src/primitives/scrim.tsx +127 -7
@@ -70,6 +70,7 @@ import Animated, {
70
70
  import Svg, { G, Line as SvgLine, Path } from 'react-native-svg';
71
71
  import { useCSSVariable } from 'uniwind';
72
72
  import { Text } from '../../primitives/text';
73
+ import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
73
74
  import { bandOf, compactNumber, yOf, type Plot } from '../../utils/chart';
74
75
  import { cn } from '../../utils/cn';
75
76
 
@@ -234,7 +235,7 @@ function ohlcOf(
234
235
  return { open, high, low, close };
235
236
  }
236
237
 
237
- export interface CandlestickChartProps extends ViewProps {
238
+ export interface CandlestickChartProps extends ViewProps, ChartAccessibilityProps<CandlestickChartDatum> {
238
239
  className?: string;
239
240
  /** The rows. Each one is a period along the x-axis. */
240
241
  data: CandlestickChartDatum[];
@@ -333,6 +334,11 @@ const CandlestickChartRoot = forwardRef<CandlestickChartHandle, CandlestickChart
333
334
  candleWidth,
334
335
  fadedOpacity = 0.3,
335
336
  onActiveIndexChange,
337
+ accessible,
338
+ accessibilityLabel,
339
+ accessibilityHint,
340
+ accessibilityLabelForDatum,
341
+ onAccessibilityDatumPress,
336
342
  compact = false,
337
343
  children,
338
344
  ...props
@@ -532,7 +538,29 @@ const CandlestickChartRoot = forwardRef<CandlestickChartHandle, CandlestickChart
532
538
  <CandlestickChartContext.Provider value={context}>
533
539
  <View {...props} style={props.style} className={cn('w-full', className)}>
534
540
  {header}
535
- <View onLayout={onLayout} style={{ aspectRatio }} className="w-full">
541
+ <ChartAccessibilityData
542
+ chart="Candlestick chart"
543
+ data={data}
544
+ disabled={accessible === false || loading}
545
+ accessibilityLabel={accessibilityLabel}
546
+ accessibilityHint={accessibilityHint}
547
+ accessibilityLabelForDatum={accessibilityLabelForDatum}
548
+ onAccessibilityDatumPress={onAccessibilityDatumPress}
549
+ valueOf={(datum) => [
550
+ [xDataKey, datum[xDataKey]],
551
+ [openDataKey, datum[openDataKey]],
552
+ [highDataKey, datum[highDataKey]],
553
+ [lowDataKey, datum[lowDataKey]],
554
+ [closeDataKey, datum[closeDataKey]],
555
+ ]}
556
+ />
557
+ <View
558
+ onLayout={onLayout}
559
+ style={{ aspectRatio }}
560
+ className="w-full"
561
+ accessibilityElementsHidden
562
+ importantForAccessibility="no-hide-descendants"
563
+ >
536
564
  {plot.width > 0 ? (
537
565
  <>
538
566
  <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
@@ -0,0 +1,74 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ export function normalizeCarouselIndex(index: number, count: number, loop: boolean): number {
4
+ 'worklet';
5
+ if (count <= 0 || !Number.isFinite(index)) return 0;
6
+ if (loop) return ((index % count) + count) % count;
7
+ return Math.max(0, Math.min(index, count - 1));
8
+ }
9
+
10
+ interface CarouselIndexLifecycleOptions {
11
+ requestedIndex: number;
12
+ count: number;
13
+ countKnown: boolean;
14
+ loop: boolean;
15
+ onCorrection: (index: number) => void;
16
+ onSettledIndex: (index: number) => void;
17
+ }
18
+
19
+ /** Keeps the rendered position inside the current run and reports corrections once. */
20
+ export function useCarouselIndexLifecycle({
21
+ requestedIndex,
22
+ count,
23
+ countKnown,
24
+ loop,
25
+ onCorrection,
26
+ onSettledIndex,
27
+ }: CarouselIndexLifecycleOptions): number {
28
+ const index = countKnown
29
+ ? normalizeCarouselIndex(requestedIndex, count, loop)
30
+ : requestedIndex;
31
+ const reportedCorrection = useRef<string | null>(null);
32
+
33
+ useEffect(() => {
34
+ if (!countKnown) return;
35
+ onSettledIndex(index);
36
+
37
+ if (index === requestedIndex) {
38
+ reportedCorrection.current = null;
39
+ return;
40
+ }
41
+
42
+ const correction = `${requestedIndex}:${count}:${loop}`;
43
+ if (reportedCorrection.current === correction) return;
44
+ reportedCorrection.current = correction;
45
+ onCorrection(index);
46
+ }, [count, countKnown, index, loop, onCorrection, onSettledIndex, requestedIndex]);
47
+
48
+ return index;
49
+ }
50
+
51
+ interface CarouselAutoplayOptions {
52
+ enabled: boolean;
53
+ index: number;
54
+ count: number;
55
+ loop: boolean;
56
+ interval: number;
57
+ onAdvance: (index: number) => void;
58
+ }
59
+
60
+ /** One autoplay request per accepted index; configuration changes re-arm it. */
61
+ export function useCarouselAutoplay({
62
+ enabled,
63
+ index,
64
+ count,
65
+ loop,
66
+ interval,
67
+ onAdvance,
68
+ }: CarouselAutoplayOptions): void {
69
+ useEffect(() => {
70
+ if (!enabled || count <= 1 || (!loop && index >= count - 1)) return;
71
+ const timer = setTimeout(() => onAdvance(index + 1), interval);
72
+ return () => clearTimeout(timer);
73
+ }, [count, enabled, index, interval, loop, onAdvance]);
74
+ }
@@ -50,11 +50,13 @@ import {
50
50
  Children,
51
51
  createContext,
52
52
  forwardRef,
53
+ isValidElement,
53
54
  useCallback,
54
55
  useContext,
55
56
  useEffect,
56
57
  useImperativeHandle,
57
58
  useMemo,
59
+ useRef,
58
60
  useState,
59
61
  type ReactNode,
60
62
  } from 'react';
@@ -74,6 +76,11 @@ import Animated, {
74
76
  import { ChevronLeftIcon, ChevronRightIcon } from '../../icons';
75
77
  import { Text, textChildren } from '../../primitives/text';
76
78
  import { cn } from '../../utils/cn';
79
+ import {
80
+ normalizeCarouselIndex,
81
+ useCarouselAutoplay,
82
+ useCarouselIndexLifecycle,
83
+ } from './carousel-lifecycle';
77
84
 
78
85
  /** Settles the run onto a whole index. Tuned to stop rather than to bounce. */
79
86
  const SPRING = { damping: 22, stiffness: 190, mass: 0.55 } as const;
@@ -161,8 +168,7 @@ export function useCarouselState() {
161
168
  */
162
169
  function wrap(value: number, count: number) {
163
170
  'worklet';
164
- if (count <= 0) return 0;
165
- return ((value % count) + count) % count;
171
+ return normalizeCarouselIndex(value, count, true);
166
172
  }
167
173
 
168
174
  /**
@@ -183,6 +189,17 @@ function distance(index: number, progress: number, count: number, loop: boolean)
183
189
  return raw;
184
190
  }
185
191
 
192
+ /** Drop the empty nodes React omits before assigning slide positions. */
193
+ function renderableChildren(children: ReactNode) {
194
+ // React treats a Fragment as one child here. Keep that keyed boundary rather
195
+ // than recursively flattening it and changing React's reconciliation model.
196
+ return Children.toArray(children);
197
+ }
198
+
199
+ function renderableChildKey(child: ReactNode, index: number) {
200
+ return isValidElement(child) && child.key !== null ? child.key : index;
201
+ }
202
+
186
203
  export interface CarouselProps extends ViewProps {
187
204
  className?: string;
188
205
  /** How the slides are arranged, and how they move. */
@@ -203,11 +220,14 @@ export interface CarouselProps extends ViewProps {
203
220
  * wants; set it for a run that shows more than one at a time.
204
221
  */
205
222
  itemSize?: number;
206
- /** Advance on a timer. Stops for good the first time a finger lands. */
223
+ /** Advance on a timer. Stops at the non-looping end or after the first touch. */
207
224
  autoplay?: boolean;
208
225
  /** Milliseconds each slide is held when `autoplay` is set. */
209
226
  autoplayInterval?: number;
210
- /** Controlled active slide. Requests move visually only after this value changes. */
227
+ /**
228
+ * Controlled active slide. Requests move visually only after this value changes;
229
+ * an index invalidated by a child-count change is normalized and reported.
230
+ */
211
231
  index?: number;
212
232
  /** Starting slide when uncontrolled. */
213
233
  defaultIndex?: number;
@@ -245,14 +265,15 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
245
265
  ) {
246
266
  const [size, setSize] = useState({ width: 0, height: 0 });
247
267
  const [internalIndex, setInternalIndex] = useState(defaultIndex);
248
- const [count, setCount] = useState(0);
268
+ const [count, setCountValue] = useState(0);
269
+ const [countKnown, setCountKnown] = useState(false);
249
270
  const [touched, setTouched] = useState(false);
250
271
  const reducedMotion = useReducedMotion();
251
272
 
252
273
  const isControlled = indexProp !== undefined;
253
- const index = isControlled ? indexProp : internalIndex;
274
+ const requestedIndex = isControlled ? indexProp : internalIndex;
254
275
 
255
- const progress = useSharedValue(defaultIndex);
276
+ const progress = useSharedValue(indexProp ?? defaultIndex);
256
277
  const engaged = useSharedValue(0);
257
278
 
258
279
  // A deck is dealt from the top of a pile, so it is dragged sideways whatever
@@ -261,6 +282,11 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
261
282
  const along = axis === 'horizontal' ? size.width : size.height;
262
283
  const itemSize = itemSizeProp ?? along ?? 0;
263
284
 
285
+ const setCount = useCallback((next: number) => {
286
+ setCountValue(next);
287
+ setCountKnown(true);
288
+ }, []);
289
+
264
290
  const setIndex = useCallback(
265
291
  (next: number) => {
266
292
  if (!isControlled) setInternalIndex(next);
@@ -269,8 +295,22 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
269
295
  [isControlled, onIndexChange]
270
296
  );
271
297
 
298
+ /*
299
+ * The slide the run is currently travelling to.
300
+ *
301
+ * Moving the run and recording where it went are two steps: `scrollTo` starts
302
+ * the spring, then the new index arrives back through state and the lifecycle
303
+ * effect asks for it again. Without this the second ask restarts the spring a
304
+ * frame into the first — from a standstill, so a flick loses the momentum it
305
+ * was carrying. Remembering the target lets the echo be recognised and
306
+ * ignored, while a genuine request for the same slide still animates, because
307
+ * that one comes through `scrollTo` and sets this first.
308
+ */
309
+ const animatedTarget = useRef<number | null>(null);
310
+
272
311
  const animateTo = useCallback(
273
312
  (target: number) => {
313
+ animatedTarget.current = target;
274
314
  if (reducedMotion) {
275
315
  progress.value = target;
276
316
  } else if (loop) {
@@ -288,10 +328,27 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
288
328
  [count, loop, progress, reducedMotion]
289
329
  );
290
330
 
331
+ const settleIndex = useCallback(
332
+ (next: number) => {
333
+ if (animatedTarget.current === next) return;
334
+ if (Math.abs(progress.value - next) >= 0.001) animateTo(next);
335
+ },
336
+ [animateTo, progress]
337
+ );
338
+
339
+ const index = useCarouselIndexLifecycle({
340
+ requestedIndex,
341
+ count,
342
+ countKnown,
343
+ loop,
344
+ onCorrection: setIndex,
345
+ onSettledIndex: settleIndex,
346
+ });
347
+
291
348
  const scrollTo = useCallback(
292
349
  (target: number) => {
293
350
  if (count <= 0) return;
294
- const settled = loop ? wrap(target, count) : Math.max(0, Math.min(target, count - 1));
351
+ const settled = normalizeCarouselIndex(target, count, loop);
295
352
 
296
353
  // A controlled request belongs to its owner. The finger may move the run,
297
354
  // but after release it returns to the current prop until the owner accepts
@@ -307,23 +364,19 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
307
364
 
308
365
  useImperativeHandle(ref, () => ({ next, previous, scrollTo }), [next, previous, scrollTo]);
309
366
 
310
- // A controlled index moving underneath us animates rather than jumps.
311
- useEffect(() => {
312
- if (!isControlled || indexProp === undefined || count <= 0) return;
313
- if (Math.abs(progress.value - indexProp) < 0.001) return;
314
- animateTo(indexProp);
315
- }, [animateTo, isControlled, indexProp, count, progress]);
316
-
317
367
  /*
318
368
  * Autoplay stops for good the first time a finger lands, rather than pausing.
319
369
  * Someone who has taken hold of the run is reading it, and having it start
320
370
  * moving again a few seconds later is the behaviour everybody hates.
321
371
  */
322
- useEffect(() => {
323
- if (!autoplay || touched || count <= 1) return;
324
- const timer = setInterval(() => scrollTo(index + 1), autoplayInterval);
325
- return () => clearInterval(timer);
326
- }, [autoplay, touched, count, index, autoplayInterval, scrollTo]);
372
+ useCarouselAutoplay({
373
+ enabled: autoplay && !touched,
374
+ index,
375
+ count,
376
+ loop,
377
+ interval: autoplayInterval,
378
+ onAdvance: scrollTo,
379
+ });
327
380
 
328
381
  const settle = useCallback(
329
382
  (target: number) => {
@@ -447,13 +500,13 @@ export interface CarouselContentProps extends ViewProps {
447
500
  */
448
501
  function CarouselContent({ className, children, ...props }: CarouselContentProps) {
449
502
  const { setCount, variant, orientation, align } = useCarousel('Carousel.Content');
450
- const slides = Children.count(children);
503
+ const slides = renderableChildren(children);
451
504
  const horizontal = orientation === 'horizontal';
452
505
  const centred = align === 'center' || variant === 'coverflow' || variant === 'stack';
453
506
 
454
507
  useEffect(() => {
455
- setCount(slides);
456
- }, [setCount, slides]);
508
+ setCount(slides.length);
509
+ }, [setCount, slides.length]);
457
510
 
458
511
  return (
459
512
  <View
@@ -474,8 +527,10 @@ function CarouselContent({ className, children, ...props }: CarouselContentProps
474
527
  props.style,
475
528
  ]}
476
529
  >
477
- {Children.map(children, (child, index) => (
478
- <ItemIndexContext.Provider value={index}>{child}</ItemIndexContext.Provider>
530
+ {slides.map((child, index) => (
531
+ <ItemIndexContext.Provider key={renderableChildKey(child, index)} value={index}>
532
+ {child}
533
+ </ItemIndexContext.Provider>
479
534
  ))}
480
535
  </View>
481
536
  );
@@ -0,0 +1,44 @@
1
+ export type ContextMenuInvocation = 'menu' | 'press';
2
+
3
+ export interface ContextMenuKey {
4
+ key: string;
5
+ shiftKey?: boolean;
6
+ altKey?: boolean;
7
+ ctrlKey?: boolean;
8
+ metaKey?: boolean;
9
+ repeat?: boolean;
10
+ }
11
+
12
+ /** The cross-platform subset ContextMenu consumes from a key-down event. */
13
+ export interface ContextMenuKeyDownEvent {
14
+ nativeEvent: ContextMenuKey;
15
+ preventDefault: () => void;
16
+ isDefaultPrevented: () => boolean;
17
+ }
18
+
19
+ /** Resolve a screen-reader/switch action to the same paths touch users have. */
20
+ export function contextMenuAccessibilityInvocation(
21
+ actionName: string,
22
+ hasPrimaryAction: boolean,
23
+ disabled: boolean
24
+ ): ContextMenuInvocation | undefined {
25
+ if (disabled) return undefined;
26
+ if (actionName === 'showMenu') return 'menu';
27
+ if (actionName === 'activate') return hasPrimaryAction ? 'press' : 'menu';
28
+ return undefined;
29
+ }
30
+
31
+ /** Resolve web/hardware keys without stealing modified shortcuts or repeats. */
32
+ export function contextMenuKeyInvocation(
33
+ event: ContextMenuKey,
34
+ hasPrimaryAction: boolean,
35
+ disabled: boolean
36
+ ): ContextMenuInvocation | undefined {
37
+ if (disabled || event.repeat) return undefined;
38
+ if (event.key === 'ContextMenu' || (event.key === 'F10' && event.shiftKey)) return 'menu';
39
+ if (event.altKey || event.ctrlKey || event.metaKey) return undefined;
40
+ if (event.key === 'Enter' || event.key === ' ') {
41
+ return hasPrimaryAction ? 'press' : 'menu';
42
+ }
43
+ return undefined;
44
+ }
@@ -4,7 +4,8 @@
4
4
  * A `Menu` hangs off a control that exists to be pressed: a ⋯ button, a toolbar
5
5
  * item, something whose whole job is to open the menu. A context menu has no
6
6
  * such control. The target is the content itself — a message, a note, a photo,
7
- * a row — and the actions are reached by pressing and holding it.
7
+ * a row — and the actions are reached by holding it, by a named accessibility
8
+ * action, or from the keyboard.
8
9
  *
9
10
  * ```tsx
10
11
  * <ContextMenu>
@@ -31,9 +32,9 @@
31
32
  * them. The panel is `Menu`'s panel, which is `Popover`'s, so `presentation`,
32
33
  * submenus and edge-flipping all arrive already working.
33
34
  *
34
- * What this component owns is the two things a menu opened on content needs and
35
- * a menu opened from a button does not: the long press, and where the panel
36
- * goes.
35
+ * What this component owns is what a menu opened on content needs and a menu
36
+ * opened from a button does not: alternate invocation paths, and where the
37
+ * panel goes.
37
38
  *
38
39
  * ## Anchored to the finger, not to the target
39
40
  *
@@ -73,7 +74,11 @@ import {
73
74
  type ReactElement,
74
75
  type ReactNode,
75
76
  } from 'react';
76
- import { View, type ViewProps } from 'react-native';
77
+ import {
78
+ View,
79
+ type AccessibilityActionInfo,
80
+ type ViewProps,
81
+ } from 'react-native';
77
82
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
78
83
  import Animated, {
79
84
  FadeOut,
@@ -95,6 +100,12 @@ import {
95
100
  import { usePopoverAnchor } from '../popover';
96
101
  import { cn } from '../../utils/cn';
97
102
  import { selectionTick } from '../../utils/haptics';
103
+ import {
104
+ contextMenuAccessibilityInvocation,
105
+ contextMenuKeyInvocation,
106
+ type ContextMenuInvocation,
107
+ type ContextMenuKeyDownEvent,
108
+ } from './context-menu-invocation';
98
109
 
99
110
  /**
100
111
  * How long the target is held before the menu opens.
@@ -260,7 +271,7 @@ function ContextMenuRoot({ children, ...props }: ContextMenuProps) {
260
271
  /** Which rectangle the panel is placed against. */
261
272
  export type ContextMenuAnchor = 'point' | 'target';
262
273
 
263
- export interface ContextMenuTriggerProps extends Omit<ViewProps, 'children'> {
274
+ export interface ContextMenuTriggerProps extends Omit<ViewProps, 'children' | 'onKeyDown'> {
264
275
  /**
265
276
  * Classes on the wrapper the content sits in, which lays out like any other
266
277
  * view — it does not shrink to its child, because the things held are usually
@@ -280,7 +291,8 @@ export interface ContextMenuTriggerProps extends Omit<ViewProps, 'children'> {
280
291
  * Point is the default because a context menu's target is usually large, and
281
292
  * the middle of a whole message is not where the press was. Reach for
282
293
  * `target` when the target is small and list-shaped and the panel should read
283
- * as lining up with it.
294
+ * as lining up with it. Keyboard and accessibility opens always use the target
295
+ * bounds, because those modalities have no pointer coordinate.
284
296
  */
285
297
  anchor?: ContextMenuAnchor;
286
298
  /** How long the hold has to last, in milliseconds. 350 by default. */
@@ -310,10 +322,17 @@ export interface ContextMenuTriggerProps extends Omit<ViewProps, 'children'> {
310
322
  haptics?: boolean;
311
323
  /** Nothing opens the menu, and the short press stops firing too. */
312
324
  disabled?: boolean;
325
+ /**
326
+ * Called first for keyboard events. Prevent the event to keep ContextMenu
327
+ * from handling it. Context Menu and Shift+F10 open the menu; Enter and Space
328
+ * mirror the trigger's accessible activation.
329
+ */
330
+ onKeyDown?: (event: ContextMenuKeyDownEvent) => void;
313
331
  }
314
332
 
315
333
  /**
316
- * Wraps the content and opens the menu when it is held.
334
+ * Wraps the content and opens the menu when held, through accessibility
335
+ * actions, or from the keyboard.
317
336
  *
318
337
  * The wrapper is a plain view and lays out like one, stretching as a view does
319
338
  * rather than shrinking to its child. That is the opposite of what a tooltip's
@@ -334,6 +353,14 @@ function ContextMenuTrigger({
334
353
  onPress,
335
354
  haptics = false,
336
355
  disabled = false,
356
+ accessible,
357
+ accessibilityActions,
358
+ onAccessibilityAction,
359
+ accessibilityRole,
360
+ accessibilityState,
361
+ focusable,
362
+ tabIndex,
363
+ onKeyDown,
337
364
  ...props
338
365
  }: ContextMenuTriggerProps) {
339
366
  const { setOpen, anchorTo } = usePopoverAnchor('ContextMenu.Trigger');
@@ -359,8 +386,8 @@ function ContextMenuTrigger({
359
386
  * The target is measured either way, because the preview draws at that rect
360
387
  * whatever the panel is placed against.
361
388
  */
362
- const openAt = useCallback(
363
- (x: number, y: number) => {
389
+ const open = useCallback(
390
+ (point?: { x: number; y: number }) => {
364
391
  // Ticked here rather than in the gesture callback so it fires once the
365
392
  // hold has been accepted, which is the moment there is something to
366
393
  // confirm — and on the same side of the bridge as the opening.
@@ -382,8 +409,8 @@ function ContextMenuTrigger({
382
409
  */
383
410
  const grown = hasPreview ? (PREVIEW_SCALE - 1) / 2 : 0;
384
411
  anchorTo(
385
- against === 'point'
386
- ? { x, y, width: 0, height: 0 }
412
+ against === 'point' && point
413
+ ? { ...point, width: 0, height: 0 }
387
414
  : {
388
415
  x: mx - width * grown,
389
416
  y: my - height * grown,
@@ -397,6 +424,63 @@ function ContextMenuTrigger({
397
424
  [against, anchorTo, setOpen, haptics, setTarget, setContent, children, hasPreview]
398
425
  );
399
426
 
427
+ const openAt = useCallback((x: number, y: number) => open({ x, y }), [open]);
428
+ // Keyboard and accessibility actions have no pointer coordinate to honour.
429
+ // The measured target is the only real location they can anchor against.
430
+ const openFromTarget = useCallback(() => open(), [open]);
431
+
432
+ const invoke = useCallback(
433
+ (invocation: ContextMenuInvocation) => {
434
+ if (invocation === 'menu') openFromTarget();
435
+ else onPress?.();
436
+ },
437
+ [onPress, openFromTarget]
438
+ );
439
+
440
+ const triggerActions = useMemo<readonly AccessibilityActionInfo[] | undefined>(() => {
441
+ if (disabled) return undefined;
442
+ const reserved = new Set(['activate', 'showMenu']);
443
+ return [
444
+ { name: 'activate' },
445
+ { name: 'showMenu', label: 'Show menu' },
446
+ ...(accessibilityActions ?? []).filter(({ name }) => !reserved.has(name)),
447
+ ];
448
+ }, [accessibilityActions, disabled]);
449
+
450
+ const handleAccessibilityAction = useCallback<
451
+ NonNullable<ViewProps['onAccessibilityAction']>
452
+ >(
453
+ (event) => {
454
+ const actionName = event.nativeEvent.actionName;
455
+ const invocation = contextMenuAccessibilityInvocation(actionName, !!onPress, disabled);
456
+ if (invocation) {
457
+ invoke(invocation);
458
+ return;
459
+ }
460
+ // Reserved actions cannot bypass a disabled trigger through a consumer
461
+ // handler, while unrelated custom actions still compose normally.
462
+ if (actionName === 'activate' || actionName === 'showMenu') return;
463
+ onAccessibilityAction?.(event);
464
+ },
465
+ [disabled, invoke, onAccessibilityAction, onPress]
466
+ );
467
+
468
+ const handleKeyDown = useCallback(
469
+ (event: ContextMenuKeyDownEvent) => {
470
+ onKeyDown?.(event);
471
+ if (event.isDefaultPrevented()) return;
472
+ const invocation = contextMenuKeyInvocation(event.nativeEvent, !!onPress, disabled);
473
+ if (!invocation) return;
474
+ event.preventDefault();
475
+ invoke(invocation);
476
+ },
477
+ [disabled, invoke, onKeyDown, onPress]
478
+ );
479
+
480
+ // React Native exposes key events at runtime (and in its generated types),
481
+ // while its compatibility ViewProps declaration does not yet list them.
482
+ const keyboardProps = { onKeyDown: handleKeyDown } as ViewProps;
483
+
400
484
  const gesture = useMemo(() => {
401
485
  /*
402
486
  * The hold and the tap are given to the recogniser as alternatives, so it
@@ -432,7 +516,23 @@ function ContextMenuTrigger({
432
516
  a view that only groups children is otherwise flattened away — and a
433
517
  flattened view cannot be measured, which `anchor="target"` needs.
434
518
  */}
435
- <View ref={ref} collapsable={false} className={className} {...props}>
519
+ <View
520
+ ref={ref}
521
+ collapsable={false}
522
+ accessible={accessible ?? true}
523
+ accessibilityRole={accessibilityRole ?? 'button'}
524
+ accessibilityState={{
525
+ ...accessibilityState,
526
+ disabled: disabled || accessibilityState?.disabled,
527
+ }}
528
+ accessibilityActions={triggerActions}
529
+ onAccessibilityAction={handleAccessibilityAction}
530
+ focusable={focusable ?? !disabled}
531
+ tabIndex={tabIndex ?? (disabled ? -1 : 0)}
532
+ className={className}
533
+ {...keyboardProps}
534
+ {...props}
535
+ >
436
536
  {children}
437
537
  </View>
438
538
  </GestureDetector>
@@ -89,6 +89,9 @@ export interface DialogContentProps extends ViewProps {
89
89
  * Frost the screen behind the dialog instead of dimming it. Uses `expo-blur`
90
90
  * when installed and falls back to the dim when it is not, so it is safe to
91
91
  * pass either way.
92
+ *
93
+ * Someone who has Reduce Transparency switched on gets an opaque
94
+ * backdrop instead, which is the whole point of the setting.
92
95
  */
93
96
  blur?: boolean;
94
97
  children?: ReactNode;
@@ -285,6 +285,9 @@ export interface DrawerContentProps extends ViewProps {
285
285
  /**
286
286
  * Frost the screen behind the drawer instead of dimming it. Needs the
287
287
  * optional `expo-blur`; without it this dims, rather than failing.
288
+ *
289
+ * Someone who has Reduce Transparency switched on gets an opaque
290
+ * backdrop instead, which is the whole point of the setting.
288
291
  */
289
292
  blur?: boolean;
290
293
  children?: ReactNode;
@@ -29,6 +29,14 @@ export interface FlowConnectionAction {
29
29
  connection: FlowConnection;
30
30
  }
31
31
 
32
+ /** Opaque, collision-free name for the native accessibility action map. */
33
+ export function flowConnectionActionName(
34
+ source: { node: string; handle?: string },
35
+ target: { node: string; handle?: string }
36
+ ): string {
37
+ return `flow-connect:${JSON.stringify([source, target])}`;
38
+ }
39
+
32
40
  /** Keep pointer and assistive-technology movement inside the same group bounds. */
33
41
  export function clampNodePosition(
34
42
  rect: FlowRect,
@@ -80,7 +88,10 @@ export function getFlowConnectionActions(
80
88
 
81
89
  if (targets.length === 0) {
82
90
  actions.push({
83
- name: `flow-connect:${source.key}->${targetNode.id}`,
91
+ name: flowConnectionActionName(
92
+ { node: source.node, handle: source.id },
93
+ { node: targetNode.id }
94
+ ),
84
95
  label: `Connect ${source.label} to ${targetNode.label}`,
85
96
  connection: {
86
97
  source: sourceNode,
@@ -93,7 +104,10 @@ export function getFlowConnectionActions(
93
104
 
94
105
  for (const target of targets) {
95
106
  actions.push({
96
- name: `flow-connect:${source.key}->${target.key}`,
107
+ name: flowConnectionActionName(
108
+ { node: source.node, handle: source.id },
109
+ { node: target.node, handle: target.id }
110
+ ),
97
111
  label: `Connect ${source.label} to ${targetNode.label}, ${target.label}`,
98
112
  connection: {
99
113
  source: sourceNode,