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
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The parts of a planner that are arithmetic rather than rendering: which
3
+ * entries fall on which day, which category an entry belongs to, what a day
4
+ * says to a screen reader, and what the month adds up to.
5
+ *
6
+ * Kept out of the component so they can be tested without a renderer, and so
7
+ * the day cell stays a function of its inputs rather than of when it ran.
8
+ *
9
+ * Nothing here imports anything. The tests run this file through `node --test`
10
+ * with type stripping rather than a bundler, so an extensionless import of a
11
+ * sibling module — which is how the rest of the library is written — does not
12
+ * resolve. That is why the one line of date arithmetic below is here instead
13
+ * of coming from `utils/date`.
14
+ */
15
+
16
+ /** The minimum an entry has to be for the grid to place it. */
17
+ export interface PlannerDatedEntry {
18
+ date: Date;
19
+ /** Matches a `PlannerCategory` id. An entry without one is uncategorised. */
20
+ category?: string;
21
+ }
22
+
23
+ export interface PlannerCountedCategory {
24
+ id: string;
25
+ label: string;
26
+ count: number;
27
+ }
28
+
29
+ /** A day's key: local midnight, so two times on the same day agree. */
30
+ export function dayKey(date: Date): number {
31
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
32
+ }
33
+
34
+ /**
35
+ * Entries grouped by the day they fall on, each day in the order it was given.
36
+ *
37
+ * A map rather than a lookup per cell: a month draws 42 cells, and asking each
38
+ * of them to filter the whole list is 42 passes over it every render.
39
+ */
40
+ export function bucketByDay<T extends PlannerDatedEntry>(
41
+ entries: readonly T[]
42
+ ): Map<number, T[]> {
43
+ const days = new Map<number, T[]>();
44
+ for (const entry of entries) {
45
+ const key = dayKey(entry.date);
46
+ const bucket = days.get(key);
47
+ if (bucket) bucket.push(entry);
48
+ else days.set(key, [entry]);
49
+ }
50
+ return days;
51
+ }
52
+
53
+ /** What falls on one day. Always an array, so a cell has nothing to guard. */
54
+ export function entriesOn<T extends PlannerDatedEntry>(
55
+ days: Map<number, T[]>,
56
+ date: Date
57
+ ): T[] {
58
+ return days.get(dayKey(date)) ?? EMPTY;
59
+ }
60
+
61
+ /*
62
+ * One frozen array for every empty day, rather than a new `[]` each call. A
63
+ * month is mostly empty days, and a fresh array per cell per render defeats
64
+ * every memo below it for a day where nothing changed.
65
+ */
66
+ const EMPTY: never[] = Object.freeze([]) as never[];
67
+
68
+ /**
69
+ * How many entries a cell draws before it stops and says how many are left.
70
+ *
71
+ * `limit` of 0 means draw none of them — the day still carries its marker and
72
+ * its spoken label, which is the whole content of a compact planner.
73
+ */
74
+ export function visibleEntries<T>(entries: readonly T[], limit: number): {
75
+ shown: T[];
76
+ overflow: number;
77
+ } {
78
+ const cap = Math.max(0, Math.floor(limit));
79
+ if (entries.length <= cap) return { shown: [...entries], overflow: 0 };
80
+ return { shown: entries.slice(0, cap), overflow: entries.length - cap };
81
+ }
82
+
83
+ /**
84
+ * What a day says when it is read out.
85
+ *
86
+ * The date on its own is what a date picker says, and it is not enough here: a
87
+ * day in a planner differs from the one beside it by what is on it, and a
88
+ * marker that carries that difference in colour alone carries it to nobody who
89
+ * cannot see the colour. So the count and the categories are spoken.
90
+ *
91
+ * `dateLabel` is passed in rather than formatted here, so the grid and the
92
+ * label agree about the calendar system and the locale.
93
+ */
94
+ export function dayAccessibilityLabel<T extends PlannerDatedEntry>(
95
+ dateLabel: string,
96
+ entries: readonly T[],
97
+ categoryLabels: ReadonlyMap<string, string>
98
+ ): string {
99
+ if (entries.length === 0) return `${dateLabel}, nothing planned`;
100
+
101
+ const named: string[] = [];
102
+ for (const entry of entries) {
103
+ const label = entry.category ? categoryLabels.get(entry.category) : undefined;
104
+ if (label && !named.includes(label)) named.push(label);
105
+ }
106
+
107
+ const count = `${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}`;
108
+ return named.length > 0
109
+ ? `${dateLabel}, ${count}: ${named.join(', ')}`
110
+ : `${dateLabel}, ${count}`;
111
+ }
112
+
113
+ /**
114
+ * What the month adds up to, per category, in the order the categories were
115
+ * declared — so the legend and the totals beside it never disagree about which
116
+ * one comes first.
117
+ *
118
+ * Entries outside `month` are left out. The grid draws the days either side of
119
+ * the month it is showing, and counting those would make a total that changes
120
+ * with the week the month happens to start on.
121
+ */
122
+ export function summariseMonth<T extends PlannerDatedEntry>(
123
+ entries: readonly T[],
124
+ categories: readonly { id: string; label: string }[],
125
+ isInMonth: (date: Date) => boolean
126
+ ): { total: number; categories: PlannerCountedCategory[] } {
127
+ const counts = new Map<string, number>();
128
+ let total = 0;
129
+
130
+ for (const entry of entries) {
131
+ if (!isInMonth(entry.date)) continue;
132
+ total += 1;
133
+ if (entry.category) {
134
+ counts.set(entry.category, (counts.get(entry.category) ?? 0) + 1);
135
+ }
136
+ }
137
+
138
+ return {
139
+ total,
140
+ categories: categories.map(({ id, label }) => ({
141
+ id,
142
+ label,
143
+ count: counts.get(id) ?? 0,
144
+ })),
145
+ };
146
+ }
@@ -321,6 +321,9 @@ export interface PopoverContentProps extends ViewProps {
321
321
  * Frost the background behind the panel instead of dimming it. Uses
322
322
  * `expo-blur` when installed and falls back to the dimmed scrim when it is
323
323
  * not, so it is safe to pass either way.
324
+ *
325
+ * Someone who has Reduce Transparency switched on gets an opaque
326
+ * backdrop instead, which is the whole point of the setting.
324
327
  */
325
328
  blur?: boolean;
326
329
  /**
@@ -59,6 +59,7 @@ import Animated, {
59
59
  } from 'react-native-reanimated';
60
60
  import { useCSSVariable } from 'uniwind';
61
61
  import { Text } from '../../primitives/text';
62
+ import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
62
63
  import { cn } from '../../utils/cn';
63
64
  import {
64
65
  polarPoint,
@@ -212,6 +213,9 @@ export interface RadarChartProps extends Omit<ViewProps, 'children'> {
212
213
  /** Drop the room reserved for axis labels, for a radar with none. */
213
214
  compact?: boolean;
214
215
  children?: ReactNode;
216
+ /** Accessible data labels and optional activation for each axis row. */
217
+ accessibilityLabelForDatum?: ChartAccessibilityProps<RadarChartDatum>['accessibilityLabelForDatum'];
218
+ onAccessibilityDatumPress?: ChartAccessibilityProps<RadarChartDatum>['onAccessibilityDatumPress'];
215
219
  }
216
220
 
217
221
  /** Imperative handle: re-run the reveal on demand, for a "replay" control. */
@@ -231,6 +235,11 @@ const RadarChartRoot = forwardRef<RadarChartHandle, RadarChartProps>(
231
235
  domain,
232
236
  animationDuration = REVEAL_DURATION,
233
237
  compact = false,
238
+ accessible,
239
+ accessibilityLabel,
240
+ accessibilityHint,
241
+ accessibilityLabelForDatum,
242
+ onAccessibilityDatumPress,
234
243
  children,
235
244
  ...props
236
245
  },
@@ -393,6 +402,19 @@ const RadarChartRoot = forwardRef<RadarChartHandle, RadarChartProps>(
393
402
  <RadarChartContext.Provider value={context}>
394
403
  <View {...props} style={props.style} className={cn('w-full', className)}>
395
404
  {header}
405
+ <ChartAccessibilityData
406
+ chart="Radar chart"
407
+ data={data}
408
+ disabled={accessible === false || status === 'loading'}
409
+ accessibilityLabel={accessibilityLabel}
410
+ accessibilityHint={accessibilityHint}
411
+ accessibilityLabelForDatum={accessibilityLabelForDatum}
412
+ onAccessibilityDatumPress={onAccessibilityDatumPress}
413
+ valueOf={(datum) => [
414
+ [axisKey, datum[axisKey]],
415
+ ...series.map(([key]) => [key, datum[key]] as [string, unknown]),
416
+ ]}
417
+ />
396
418
  <View
397
419
  onLayout={onLayout}
398
420
  style={
@@ -407,6 +429,8 @@ const RadarChartRoot = forwardRef<RadarChartHandle, RadarChartProps>(
407
429
  : { aspectRatio }
408
430
  }
409
431
  className={fixedSize ? 'self-center' : 'w-full'}
432
+ accessibilityElementsHidden
433
+ importantForAccessibility="no-hide-descendants"
410
434
  >
411
435
  {radius > 0 ? (
412
436
  <>
@@ -67,6 +67,7 @@ import Animated, {
67
67
  import Svg, { Circle, G, Line as SvgLine } from 'react-native-svg';
68
68
  import { useCSSVariable } from 'uniwind';
69
69
  import { Text } from '../../primitives/text';
70
+ import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
70
71
  import { compactNumber, useSeriesColor, xAt, yOf, type Plot } from '../../utils/chart';
71
72
  import { cn } from '../../utils/cn';
72
73
 
@@ -191,7 +192,7 @@ export function useScatterChart() {
191
192
  return { activePoint, xDataKey };
192
193
  }
193
194
 
194
- export interface ScatterChartProps extends ViewProps {
195
+ export interface ScatterChartProps extends ViewProps, ChartAccessibilityProps<ScatterChartDatum> {
195
196
  className?: string;
196
197
  /** The rows. Each one is a point, placed by two of its values. */
197
198
  data: ScatterChartDatum[];
@@ -244,6 +245,11 @@ const ScatterChartRoot = forwardRef<ScatterChartHandle, ScatterChartProps>(
244
245
  xDomain,
245
246
  yDomain,
246
247
  onActivePointChange,
248
+ accessible,
249
+ accessibilityLabel,
250
+ accessibilityHint,
251
+ accessibilityLabelForDatum,
252
+ onAccessibilityDatumPress,
247
253
  compact = false,
248
254
  children,
249
255
  ...props
@@ -476,7 +482,26 @@ const ScatterChartRoot = forwardRef<ScatterChartHandle, ScatterChartProps>(
476
482
  <ScatterChartContext.Provider value={context}>
477
483
  <View {...props} style={props.style} className={cn('w-full', className)}>
478
484
  {header}
479
- <View onLayout={onLayout} style={{ aspectRatio }} className="w-full">
485
+ <ChartAccessibilityData
486
+ chart="Scatter chart"
487
+ data={data}
488
+ disabled={accessible === false || status === 'loading'}
489
+ accessibilityLabel={accessibilityLabel}
490
+ accessibilityHint={accessibilityHint}
491
+ accessibilityLabelForDatum={accessibilityLabelForDatum}
492
+ onAccessibilityDatumPress={onAccessibilityDatumPress}
493
+ valueOf={(datum) => [
494
+ [xDataKey, datum[xDataKey]],
495
+ ...series.map(([key]) => [key, datum[key]] as [string, unknown]),
496
+ ]}
497
+ />
498
+ <View
499
+ onLayout={onLayout}
500
+ style={{ aspectRatio }}
501
+ className="w-full"
502
+ accessibilityElementsHidden
503
+ importantForAccessibility="no-hide-descendants"
504
+ >
480
505
  {plot.width > 0 ? (
481
506
  <>
482
507
  <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
@@ -88,6 +88,15 @@ import { Text, textChildren } from '../../primitives/text';
88
88
  import { cn } from '../../utils/cn';
89
89
  import { selectionTick } from '../../utils/haptics';
90
90
  import { BottomSheet } from '../bottom-sheet';
91
+ import {
92
+ canEnterSelection,
93
+ handleSelectionItemPress,
94
+ selectAllValues,
95
+ selectionOwnsPress,
96
+ selectionTarget,
97
+ selectValue,
98
+ toggleValue,
99
+ } from './selection-mode-contracts';
91
100
 
92
101
  /** How long the circle takes to come and go, in milliseconds. */
93
102
  const REVEAL_DURATION = 180;
@@ -293,11 +302,12 @@ function SelectionModeRoot({
293
302
  // Entering with the row that was pressed already picked. Entering with
294
303
  // nothing picked leaves the reader in a list that has changed under them
295
304
  // with nothing to show for it.
296
- if (value !== undefined && !selectedRef.current.includes(value)) {
297
- setSelected([...selectedRef.current, value]);
305
+ if (value !== undefined) {
306
+ const next = selectValue(selectedRef.current, value, max);
307
+ if (next !== selectedRef.current) setSelected(next);
298
308
  }
299
309
  },
300
- [haptics, setActive, setSelected]
310
+ [haptics, max, setActive, setSelected]
301
311
  );
302
312
 
303
313
  /*
@@ -321,13 +331,10 @@ function SelectionModeRoot({
321
331
  const toggle = useCallback(
322
332
  (value: string) => {
323
333
  const current = selectedRef.current;
324
- if (current.includes(value)) {
325
- setSelected(current.filter((entry) => entry !== value));
326
- } else {
327
- if (max !== undefined && current.length >= max) return;
328
- if (haptics) selectionTick();
329
- setSelected([...current, value]);
330
- }
334
+ const next = toggleValue(current, value, max);
335
+ if (next === current) return;
336
+ if (!current.includes(value) && haptics) selectionTick();
337
+ setSelected(next);
331
338
  },
332
339
  [max, haptics, setSelected]
333
340
  );
@@ -336,15 +343,15 @@ function SelectionModeRoot({
336
343
  if (!values) return;
337
344
  // At the limit rather than refusing: somebody who asked for all of them and
338
345
  // can only have twenty wants the twenty, not an error.
339
- setSelected(max === undefined ? [...values] : values.slice(0, max));
346
+ setSelected(selectAllValues(values, max));
340
347
  }, [values, max, setSelected]);
341
348
 
342
349
  const clear = useCallback(() => setSelected([]), [setSelected]);
343
350
 
344
351
  const total = values?.length ?? 0;
345
352
  const count = selected.length;
346
- const allSelected =
347
- total > 0 && count >= (max === undefined ? total : Math.min(total, max));
353
+ const target = selectionTarget(total, max);
354
+ const allSelected = target > 0 && count >= target;
348
355
 
349
356
  // An open mode owns the back button: back should leave the mode, not the
350
357
  // screen the list is on.
@@ -459,10 +466,11 @@ export interface SelectionModeItemProps extends Omit<ViewProps, 'children'> {
459
466
  className?: string;
460
467
  /** This row's id. What ends up in `selected`. */
461
468
  value: string;
462
- /** What the row does when it is pressed and the mode is off. */
469
+ /** What the row does whenever selection does not own its press. */
463
470
  onPress?: () => void;
464
471
  /**
465
- * Stop this row entering selection mode, and being picked once in it. For a
472
+ * Stop this row entering selection mode, and being picked once in it. Its
473
+ * ordinary `onPress` still runs, including while selection is active. For a
466
474
  * header row, an advert, a "load more" — anything in the list that is not one
467
475
  * of the things being chosen between.
468
476
  */
@@ -487,8 +495,9 @@ export interface SelectionModeItemProps extends Omit<ViewProps, 'children'> {
487
495
  *
488
496
  * The press behaviour is the whole component: off mode, a press is the row's
489
497
  * own and a long press turns the mode on with this row picked; in it, a press
490
- * picks and unpicks and the row's own press is unreachable. Two meanings for
491
- * one gesture is exactly why the mode has to be visible from the header.
498
+ * picks and unpicks. A row excluded from selection keeps its ordinary press in
499
+ * both states. Two meanings for one gesture is exactly why the mode has to be
500
+ * visible from the header.
492
501
  */
493
502
  function SelectionModeItem({
494
503
  className,
@@ -503,6 +512,7 @@ function SelectionModeItem({
503
512
  const { active, enter, toggle, isSelected, sheet } = useSelectionMode();
504
513
  const selected = isSelected(value);
505
514
  const showing = alwaysShowIndicator || active;
515
+ const selecting = selectionOwnsPress(active, disabled);
506
516
  const reducedMotion = useReducedMotion();
507
517
 
508
518
  const context = useMemo(() => ({ value }), [value]);
@@ -537,23 +547,20 @@ function SelectionModeItem({
537
547
  return (
538
548
  <SelectionModeItemContext.Provider value={context}>
539
549
  <Pressable
540
- accessibilityRole={showing ? 'checkbox' : 'button'}
541
- accessibilityState={showing ? { checked: selected, disabled } : { disabled }}
542
- // The row is unreachable in selection mode, so a screen reader is told
543
- // what pressing it does now rather than what it used to do.
550
+ accessibilityRole={selecting ? 'checkbox' : 'button'}
551
+ accessibilityState={selecting ? { checked: selected } : undefined}
552
+ // Eligible rows become checkboxes only while selection owns their
553
+ // press. Excluded rows remain ordinary buttons because their action is
554
+ // still available.
544
555
  accessibilityHint={
545
- showing ? undefined : disabled ? undefined : 'Long press to select'
556
+ active || disabled ? undefined : 'Long press to select'
546
557
  }
547
- disabled={disabled && showing}
548
558
  onPress={() => {
549
- if (disabled) return;
550
- if (showing) toggle(value);
551
- else onPress?.();
552
- }}
553
- onLongPress={sheet ? undefined : () => {
554
- if (disabled || showing) return;
555
- enter(value);
559
+ handleSelectionItemPress(active, disabled, () => toggle(value), onPress);
556
560
  }}
561
+ onLongPress={
562
+ canEnterSelection(active, disabled, sheet) ? () => enter(value) : undefined
563
+ }
557
564
  className={cn(
558
565
  indicator === 'leading' ? 'flex-row items-center gap-3 px-4 py-2.5' : '',
559
566
  className
@@ -0,0 +1,53 @@
1
+ /** The effective selection cap. Invalid and negative caps allow no additions. */
2
+ function selectionLimit(max?: number): number {
3
+ if (max === undefined || max === Infinity) return Infinity;
4
+ return Number.isFinite(max) ? Math.max(0, Math.floor(max)) : 0;
5
+ }
6
+
7
+ /** Add one value without duplicating it or crossing the configured cap. */
8
+ export function selectValue(selected: string[], value: string, max?: number): string[] {
9
+ if (selected.includes(value) || !(selected.length < selectionLimit(max))) return selected;
10
+ return [...selected, value];
11
+ }
12
+
13
+ /** Remove an existing value, or add it under the same contract used by entry. */
14
+ export function toggleValue(selected: string[], value: string, max?: number): string[] {
15
+ return selected.includes(value)
16
+ ? selected.filter((entry) => entry !== value)
17
+ : selectValue(selected, value, max);
18
+ }
19
+
20
+ /** The values produced by select-all, capped by the same normalized limit. */
21
+ export function selectAllValues(values: string[], max?: number): string[] {
22
+ const limit = selectionLimit(max);
23
+ return limit === Infinity ? [...values] : values.slice(0, limit);
24
+ }
25
+
26
+ /** How many selections constitute all selectable values. */
27
+ export function selectionTarget(total: number, max?: number): number {
28
+ return Math.min(total, selectionLimit(max));
29
+ }
30
+
31
+ /**
32
+ * A selection-ineligible row keeps its ordinary action in every mode. Only an
33
+ * active, eligible row gives its press to selection.
34
+ */
35
+ export function selectionOwnsPress(active: boolean, disabled: boolean): boolean {
36
+ return active && !disabled;
37
+ }
38
+
39
+ /** Execute the one action owned by a row press under the interaction contract. */
40
+ export function handleSelectionItemPress(
41
+ active: boolean,
42
+ disabled: boolean,
43
+ toggle: () => void,
44
+ action?: () => void
45
+ ): void {
46
+ if (selectionOwnsPress(active, disabled)) toggle();
47
+ else action?.();
48
+ }
49
+
50
+ /** Long press enters only from an inactive screen row that can be selected. */
51
+ export function canEnterSelection(active: boolean, disabled: boolean, sheet: boolean): boolean {
52
+ return !active && !disabled && !sheet;
53
+ }
@@ -58,7 +58,12 @@ import {
58
58
  type ReactNode,
59
59
  type Ref,
60
60
  } from 'react';
61
- import { View, type ViewProps } from 'react-native';
61
+ import {
62
+ AccessibilityInfo,
63
+ View,
64
+ type AccessibilityActionEvent,
65
+ type ViewProps,
66
+ } from 'react-native';
62
67
  import { useCSSVariable } from 'uniwind';
63
68
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
64
69
  import Animated, {
@@ -81,6 +86,12 @@ import {
81
86
  } from '../../primitives/animated-pressable';
82
87
  import { Text } from '../../primitives/text';
83
88
  import { cn } from '../../utils/cn';
89
+ import {
90
+ signatureAccessibilityActions,
91
+ signatureAccessibilityValue,
92
+ signatureAnnouncement,
93
+ type SignatureChangeKind,
94
+ } from './signature-accessibility';
84
95
 
85
96
  const AnimatedPath = Animated.createAnimatedComponent(Path);
86
97
 
@@ -175,6 +186,12 @@ export interface SignatureProps
175
186
  placeholder?: ReactNode;
176
187
  /** Take no input. The strokes already there stay visible. */
177
188
  disabled?: boolean;
189
+ /**
190
+ * Opens a product-provided non-drawing method, such as typing a legal name,
191
+ * uploading an image, or asking for assisted signing. Exposed as a screen
192
+ * reader action; provide the same choice as a visible control too.
193
+ */
194
+ onRequestAlternative?: () => void;
178
195
  /** A stroke has started. */
179
196
  onBegin?: () => void;
180
197
  /** A stroke has finished. */
@@ -292,12 +309,17 @@ function SignatureRoot(
292
309
  guidelineLabel,
293
310
  placeholder,
294
311
  disabled = false,
312
+ onRequestAlternative,
295
313
  onBegin,
296
314
  onEnd,
297
315
  onChange,
298
316
  padClassName,
299
317
  placeholderClassName,
300
318
  guideClassName,
319
+ accessibilityLabel,
320
+ accessibilityHint,
321
+ accessibilityActions,
322
+ onAccessibilityAction,
301
323
  ...props
302
324
  }: SignatureProps,
303
325
  ref: Ref<SignatureHandle>
@@ -325,19 +347,30 @@ function SignatureRoot(
325
347
  beginRef.current = onBegin;
326
348
  const endRef = useRef(onEnd);
327
349
  endRef.current = onEnd;
350
+ const alternativeRef = useRef(onRequestAlternative);
351
+ alternativeRef.current = onRequestAlternative;
328
352
 
329
353
  // `onChange` fires from an effect rather than from inside a state updater.
330
354
  // An updater can be replayed during a render, and calling a parent's setState
331
355
  // from there is the "cannot update a component while rendering a different
332
356
  // component" warning — earned, not spurious.
333
357
  const reported = useRef(0);
358
+ const changeKind = useRef<SignatureChangeKind>('draw');
334
359
  useEffect(() => {
335
360
  if (reported.current === strokes.length) return;
361
+ const previous = reported.current;
336
362
  reported.current = strokes.length;
337
363
  changeRef.current?.(strokes.length);
364
+ const announcement = signatureAnnouncement(
365
+ previous,
366
+ strokes.length,
367
+ changeKind.current
368
+ );
369
+ if (announcement) AccessibilityInfo.announceForAccessibility(announcement);
338
370
  }, [strokes.length]);
339
371
 
340
372
  const commit = useCallback((d: string) => {
373
+ changeKind.current = 'draw';
341
374
  setStrokes((current) => [...current, d]);
342
375
  // Drawing again is a new branch of history, so what was undone is gone.
343
376
  setUndone([]);
@@ -419,28 +452,37 @@ function SignatureRoot(
419
452
  [layout.height, layout.width]
420
453
  );
421
454
 
455
+ const clear = useCallback(() => {
456
+ changeKind.current = 'clear';
457
+ setStrokes([]);
458
+ setUndone([]);
459
+ }, []);
460
+
461
+ const undo = useCallback(() => {
462
+ const last = strokes[strokes.length - 1];
463
+ if (last === undefined) return;
464
+ changeKind.current = 'undo';
465
+ setUndone((stack) => [...stack, last]);
466
+ setStrokes(strokes.slice(0, -1));
467
+ }, [strokes]);
468
+
469
+ const redo = useCallback(() => {
470
+ const restored = undone[undone.length - 1];
471
+ if (restored === undefined) return;
472
+ changeKind.current = 'redo';
473
+ setUndone(undone.slice(0, -1));
474
+ setStrokes([...strokes, restored]);
475
+ }, [strokes, undone]);
476
+
422
477
  useImperativeHandle(
423
478
  ref,
424
479
  (): SignatureHandle => ({
425
480
  // These read the current arrays and set flat values rather than nesting
426
481
  // one updater inside another — a nested updater runs during the render
427
482
  // pass, which is not a safe place to schedule another component's update.
428
- clear() {
429
- setStrokes([]);
430
- setUndone([]);
431
- },
432
- undo() {
433
- const last = strokes[strokes.length - 1];
434
- if (last === undefined) return;
435
- setUndone((stack) => [...stack, last]);
436
- setStrokes(strokes.slice(0, -1));
437
- },
438
- redo() {
439
- const restored = undone[undone.length - 1];
440
- if (restored === undefined) return;
441
- setUndone(undone.slice(0, -1));
442
- setStrokes([...strokes, restored]);
443
- },
483
+ clear,
484
+ undo,
485
+ redo,
444
486
  isEmpty: () => strokes.length === 0,
445
487
  strokeCount: () => strokes.length,
446
488
  toSVG: svgDocument,
@@ -524,10 +566,47 @@ function SignatureRoot(
524
566
  };
525
567
  },
526
568
  }),
527
- [layout.height, layout.width, rasterise, strokes, undone, svgDocument]
569
+ [clear, layout.height, layout.width, rasterise, redo, strokes, svgDocument, undo]
528
570
  );
529
571
 
530
572
  const empty = strokes.length === 0;
573
+ const padActions = useMemo(
574
+ () =>
575
+ disabled
576
+ ? []
577
+ : [
578
+ ...(accessibilityActions ?? []),
579
+ ...signatureAccessibilityActions(
580
+ strokes.length,
581
+ undone.length,
582
+ Boolean(onRequestAlternative),
583
+ false
584
+ ),
585
+ ],
586
+ [accessibilityActions, disabled, onRequestAlternative, strokes.length, undone.length]
587
+ );
588
+ const handleAccessibilityAction = useCallback(
589
+ (event: AccessibilityActionEvent) => {
590
+ if (disabled) return;
591
+ switch (event.nativeEvent.actionName) {
592
+ case 'signature-undo':
593
+ undo();
594
+ return;
595
+ case 'signature-redo':
596
+ redo();
597
+ return;
598
+ case 'signature-clear':
599
+ clear();
600
+ return;
601
+ case 'signature-alternative':
602
+ alternativeRef.current?.();
603
+ return;
604
+ default:
605
+ onAccessibilityAction?.(event);
606
+ }
607
+ },
608
+ [clear, disabled, onAccessibilityAction, redo, undo]
609
+ );
531
610
 
532
611
  return (
533
612
  <View className={slots.root({ className })} {...props}>
@@ -541,9 +620,17 @@ function SignatureRoot(
541
620
  onLayout={(event) => setLayout(event.nativeEvent.layout)}
542
621
  accessible
543
622
  accessibilityRole="image"
544
- accessibilityLabel="Signature pad"
545
- accessibilityHint="Draw your signature with your finger"
623
+ accessibilityLabel={accessibilityLabel ?? 'Signature pad'}
624
+ accessibilityHint={
625
+ accessibilityHint ??
626
+ (onRequestAlternative
627
+ ? 'Direct touch drawing requires tracing a path. Use actions to edit or choose another signature method.'
628
+ : 'Direct touch drawing requires tracing a path. Use actions to undo, redo, or clear strokes.')
629
+ }
546
630
  accessibilityState={{ disabled }}
631
+ accessibilityValue={signatureAccessibilityValue(strokes.length)}
632
+ accessibilityActions={padActions}
633
+ onAccessibilityAction={handleAccessibilityAction}
547
634
  >
548
635
  {guideline && layout.height > 0 ? (
549
636
  <View