panelui-native 0.63.0 → 0.66.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/lib/module/components/badge/index.js +1 -1
- package/lib/module/components/badge/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +39 -2
- package/lib/module/components/bar-chart/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +2 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +19 -9
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/carousel/carousel-lifecycle.js +51 -0
- package/lib/module/components/carousel/carousel-lifecycle.js.map +1 -0
- package/lib/module/components/carousel/index.js +86 -35
- package/lib/module/components/carousel/index.js.map +1 -1
- package/lib/module/components/checkbox/index.js +2 -3
- package/lib/module/components/checkbox/index.js.map +1 -1
- package/lib/module/components/context-menu/context-menu-invocation.js +23 -0
- package/lib/module/components/context-menu/context-menu-invocation.js.map +1 -0
- package/lib/module/components/context-menu/index.js +78 -9
- package/lib/module/components/context-menu/index.js.map +1 -1
- package/lib/module/components/dialog/index.js +2 -2
- package/lib/module/components/dialog/index.js.map +1 -1
- package/lib/module/components/drawer/index.js +2 -2
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/fab/index.js +4 -3
- package/lib/module/components/fab/index.js.map +1 -1
- package/lib/module/components/field/index.js +41 -6
- package/lib/module/components/field/index.js.map +1 -1
- package/lib/module/components/flow/flow-accessibility.js +100 -0
- package/lib/module/components/flow/flow-accessibility.js.map +1 -0
- package/lib/module/components/flow/flow-identifiers.js +81 -0
- package/lib/module/components/flow/flow-identifiers.js.map +1 -0
- package/lib/module/components/flow/index.js +112 -57
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/form/use-form.js +93 -66
- package/lib/module/components/form/use-form.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +2 -2
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +2 -2
- package/lib/module/components/hex-chart/index.js.map +1 -1
- package/lib/module/components/kpi/index.js +3 -1
- package/lib/module/components/kpi/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +3 -3
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +71 -8
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/map/map-accessibility.js +20 -0
- package/lib/module/components/map/map-accessibility.js.map +1 -0
- package/lib/module/components/map/maplibre.js.map +1 -1
- package/lib/module/components/menu/index.js +4 -1
- package/lib/module/components/menu/index.js.map +1 -1
- package/lib/module/components/message-scroller/index.js +17 -9
- package/lib/module/components/message-scroller/index.js.map +1 -1
- package/lib/module/components/message-scroller/message-scroller-math.js +20 -0
- package/lib/module/components/message-scroller/message-scroller-math.js.map +1 -0
- package/lib/module/components/number-input/index.js +1 -23
- package/lib/module/components/number-input/index.js.map +1 -1
- package/lib/module/components/number-input/number-input-math.js +44 -0
- package/lib/module/components/number-input/number-input-math.js.map +1 -0
- package/lib/module/components/plot/index.js +2 -2
- package/lib/module/components/plot/index.js.map +1 -1
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/radio-group/index.js +1 -2
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/selection-mode/index.js +26 -31
- package/lib/module/components/selection-mode/index.js.map +1 -1
- package/lib/module/components/selection-mode/selection-mode-contracts.js +48 -0
- package/lib/module/components/selection-mode/selection-mode-contracts.js.map +1 -0
- package/lib/module/components/signature/index.js +62 -20
- package/lib/module/components/signature/index.js.map +1 -1
- package/lib/module/components/signature/signature-accessibility.js +58 -0
- package/lib/module/components/signature/signature-accessibility.js.map +1 -0
- package/lib/module/components/slider/index.js +22 -18
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/sortable/index.js +5 -55
- package/lib/module/components/sortable/index.js.map +1 -1
- package/lib/module/components/sortable/reorder.js +63 -0
- package/lib/module/components/sortable/reorder.js.map +1 -0
- package/lib/module/components/swipe/index.js +4 -4
- package/lib/module/components/swipe/index.js.map +1 -1
- package/lib/module/components/switch/index.js +36 -3
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/time-picker/accessibility.js +21 -0
- package/lib/module/components/time-picker/accessibility.js.map +1 -0
- package/lib/module/components/time-picker/index.js +34 -8
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/components/waterfall-chart/index.js +1325 -0
- package/lib/module/components/waterfall-chart/index.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/modal-isolation-store.js +29 -0
- package/lib/module/primitives/modal-isolation-store.js.map +1 -0
- package/lib/module/primitives/portal.js +36 -4
- package/lib/module/primitives/portal.js.map +1 -1
- package/lib/module/primitives/scrim.js +94 -3
- package/lib/module/primitives/scrim.js.map +1 -1
- package/lib/module/providers/panel-ui-provider.js +27 -6
- package/lib/module/providers/panel-ui-provider.js.map +1 -1
- package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +3 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/carousel/carousel-lifecycle.d.ts +23 -0
- package/lib/typescript/src/components/carousel/carousel-lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/components/carousel/index.d.ts +5 -2
- package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
- package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/context-menu/context-menu-invocation.d.ts +20 -0
- package/lib/typescript/src/components/context-menu/context-menu-invocation.d.ts.map +1 -0
- package/lib/typescript/src/components/context-menu/index.d.ts +18 -8
- package/lib/typescript/src/components/context-menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/dialog/index.d.ts +3 -0
- package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
- package/lib/typescript/src/components/drawer/index.d.ts +3 -0
- package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
- package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
- package/lib/typescript/src/components/field/index.d.ts +8 -1
- package/lib/typescript/src/components/field/index.d.ts.map +1 -1
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts +57 -0
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/flow-identifiers.d.ts +24 -0
- package/lib/typescript/src/components/flow/flow-identifiers.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/index.d.ts +20 -7
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
- package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/kpi/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +13 -5
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/map-accessibility.d.ts +19 -0
- package/lib/typescript/src/components/map/map-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +2 -0
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts +8 -0
- package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts.map +1 -0
- package/lib/typescript/src/components/number-input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/number-input/number-input-math.d.ts +9 -0
- package/lib/typescript/src/components/number-input/number-input-math.d.ts.map +1 -0
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
- package/lib/typescript/src/components/popover/index.d.ts +3 -0
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/selection-mode/index.d.ts +6 -4
- package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
- package/lib/typescript/src/components/selection-mode/selection-mode-contracts.d.ts +18 -0
- package/lib/typescript/src/components/selection-mode/selection-mode-contracts.d.ts.map +1 -0
- package/lib/typescript/src/components/signature/index.d.ts +6 -0
- package/lib/typescript/src/components/signature/index.d.ts.map +1 -1
- package/lib/typescript/src/components/signature/signature-accessibility.d.ts +27 -0
- package/lib/typescript/src/components/signature/signature-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sortable/reorder.d.ts +14 -0
- package/lib/typescript/src/components/sortable/reorder.d.ts.map +1 -0
- package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts +3 -3
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/accessibility.d.ts +10 -0
- package/lib/typescript/src/components/time-picker/accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts +379 -0
- package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/modal-isolation-store.d.ts +10 -0
- package/lib/typescript/src/primitives/modal-isolation-store.d.ts.map +1 -0
- package/lib/typescript/src/primitives/portal.d.ts +6 -0
- package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
- package/lib/typescript/src/primitives/scrim.d.ts +5 -3
- package/lib/typescript/src/primitives/scrim.d.ts.map +1 -1
- package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
- package/package.json +11 -5
- package/src/components/badge/index.tsx +4 -1
- package/src/components/bar-chart/index.tsx +40 -3
- package/src/components/bottom-sheet/index.tsx +6 -3
- package/src/components/button/index.tsx +15 -21
- package/src/components/carousel/carousel-lifecycle.ts +74 -0
- package/src/components/carousel/index.tsx +118 -41
- package/src/components/checkbox/index.tsx +1 -2
- package/src/components/context-menu/context-menu-invocation.ts +44 -0
- package/src/components/context-menu/index.tsx +113 -13
- package/src/components/dialog/index.tsx +6 -3
- package/src/components/drawer/index.tsx +6 -3
- package/src/components/fab/index.tsx +8 -12
- package/src/components/field/index.tsx +52 -10
- package/src/components/flow/flow-accessibility.ts +123 -0
- package/src/components/flow/flow-identifiers.ts +98 -0
- package/src/components/flow/index.tsx +191 -69
- package/src/components/form/use-form.ts +101 -65
- package/src/components/heatmap-chart/index.tsx +2 -3
- package/src/components/hex-chart/index.tsx +2 -3
- package/src/components/kpi/index.tsx +3 -1
- package/src/components/line-chart/index.tsx +3 -2
- package/src/components/map/index.tsx +110 -7
- package/src/components/map/map-accessibility.ts +41 -0
- package/src/components/map/maplibre.ts +2 -0
- package/src/components/menu/index.tsx +4 -1
- package/src/components/message-scroller/index.tsx +39 -7
- package/src/components/message-scroller/message-scroller-math.ts +24 -0
- package/src/components/number-input/index.tsx +1 -26
- package/src/components/number-input/number-input-math.ts +50 -0
- package/src/components/plot/index.tsx +2 -1
- package/src/components/popover/index.tsx +3 -0
- package/src/components/radio-group/index.tsx +1 -2
- package/src/components/selection-mode/index.tsx +37 -30
- package/src/components/selection-mode/selection-mode-contracts.ts +53 -0
- package/src/components/signature/index.tsx +107 -20
- package/src/components/signature/signature-accessibility.ts +67 -0
- package/src/components/slider/index.tsx +28 -18
- package/src/components/sortable/index.tsx +12 -64
- package/src/components/sortable/reorder.ts +88 -0
- package/src/components/swipe/index.tsx +7 -4
- package/src/components/switch/index.tsx +42 -6
- package/src/components/time-picker/accessibility.ts +24 -0
- package/src/components/time-picker/index.tsx +48 -2
- package/src/components/waterfall-chart/index.tsx +1589 -0
- package/src/index.ts +25 -0
- package/src/primitives/modal-isolation-store.ts +31 -0
- package/src/primitives/portal.tsx +34 -4
- package/src/primitives/scrim.tsx +127 -7
- package/src/providers/panel-ui-provider.tsx +41 -9
- package/theme.css +30 -3
|
@@ -57,9 +57,12 @@ import { ChevronDownIcon } from '../../icons';
|
|
|
57
57
|
import { AnimatedPressable } from '../../primitives/animated-pressable';
|
|
58
58
|
import { cn } from '../../utils/cn';
|
|
59
59
|
import { textChildren } from '../../primitives/text';
|
|
60
|
+
import {
|
|
61
|
+
distanceFromMessageScrollerTarget,
|
|
62
|
+
isMessageScrollerTargetVisible,
|
|
63
|
+
MESSAGE_SCROLLER_EDGE_THRESHOLD,
|
|
64
|
+
} from './message-scroller-math';
|
|
60
65
|
|
|
61
|
-
/** Within this many pixels of the bottom still counts as being at the bottom. */
|
|
62
|
-
const END_THRESHOLD = 24;
|
|
63
66
|
/**
|
|
64
67
|
* How much of the previous turn is left showing above an anchored one. Scrolling
|
|
65
68
|
* a turn flush to the top reads as content having been cut off; a sliver of the
|
|
@@ -71,8 +74,11 @@ export type MessageScrollerPosition = 'start' | 'end' | 'last-anchor';
|
|
|
71
74
|
|
|
72
75
|
interface MessageScrollerContextValue {
|
|
73
76
|
scrollRef: ReturnType<typeof useAnimatedRef<Animated.ScrollView>>;
|
|
74
|
-
/** 1 while the reader is at the live edge, 0 otherwise. Drives
|
|
77
|
+
/** 1 while the reader is at the live edge, 0 otherwise. Drives follow state. */
|
|
75
78
|
atEnd: ReturnType<typeof useSharedValue<number>>;
|
|
79
|
+
/** Live distance from each edge, used by target-specific jump controls. */
|
|
80
|
+
distanceFromStart: ReturnType<typeof useSharedValue<number>>;
|
|
81
|
+
distanceFromEnd: ReturnType<typeof useSharedValue<number>>;
|
|
76
82
|
atEndJS: boolean;
|
|
77
83
|
setAtEndJS: (atEnd: boolean) => void;
|
|
78
84
|
autoScroll: boolean;
|
|
@@ -150,6 +156,11 @@ function MessageScrollerRoot({
|
|
|
150
156
|
}: MessageScrollerProps) {
|
|
151
157
|
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
|
152
158
|
const atEnd = useSharedValue(1);
|
|
159
|
+
const awayFromEdge = MESSAGE_SCROLLER_EDGE_THRESHOLD + 1;
|
|
160
|
+
const distanceFromStart = useSharedValue(
|
|
161
|
+
defaultScrollPosition === 'start' ? 0 : awayFromEdge
|
|
162
|
+
);
|
|
163
|
+
const distanceFromEnd = useSharedValue(defaultScrollPosition === 'end' ? 0 : awayFromEdge);
|
|
153
164
|
const [atEndJS, setAtEndJS] = useState(true);
|
|
154
165
|
const [currentAnchorId, setCurrentAnchorId] = useState<string | null>(null);
|
|
155
166
|
|
|
@@ -199,6 +210,8 @@ function MessageScrollerRoot({
|
|
|
199
210
|
() => ({
|
|
200
211
|
scrollRef,
|
|
201
212
|
atEnd,
|
|
213
|
+
distanceFromStart,
|
|
214
|
+
distanceFromEnd,
|
|
202
215
|
atEndJS,
|
|
203
216
|
setAtEndJS,
|
|
204
217
|
autoScroll,
|
|
@@ -218,6 +231,8 @@ function MessageScrollerRoot({
|
|
|
218
231
|
[
|
|
219
232
|
scrollRef,
|
|
220
233
|
atEnd,
|
|
234
|
+
distanceFromStart,
|
|
235
|
+
distanceFromEnd,
|
|
221
236
|
atEndJS,
|
|
222
237
|
autoScroll,
|
|
223
238
|
preserveScrollOnPrepend,
|
|
@@ -268,6 +283,8 @@ function MessageScrollerViewport({
|
|
|
268
283
|
const {
|
|
269
284
|
scrollRef,
|
|
270
285
|
atEnd,
|
|
286
|
+
distanceFromStart,
|
|
287
|
+
distanceFromEnd,
|
|
271
288
|
setAtEndJS,
|
|
272
289
|
autoScroll,
|
|
273
290
|
preserveScrollOnPrepend,
|
|
@@ -310,8 +327,19 @@ function MessageScrollerViewport({
|
|
|
310
327
|
const { contentOffset, contentSize, layoutMeasurement } = event;
|
|
311
328
|
offsetY.value = contentOffset.y;
|
|
312
329
|
|
|
313
|
-
|
|
314
|
-
|
|
330
|
+
distanceFromStart.value = distanceFromMessageScrollerTarget(
|
|
331
|
+
'start',
|
|
332
|
+
contentOffset.y,
|
|
333
|
+
contentSize.height,
|
|
334
|
+
layoutMeasurement.height
|
|
335
|
+
);
|
|
336
|
+
distanceFromEnd.value = distanceFromMessageScrollerTarget(
|
|
337
|
+
'end',
|
|
338
|
+
contentOffset.y,
|
|
339
|
+
contentSize.height,
|
|
340
|
+
layoutMeasurement.height
|
|
341
|
+
);
|
|
342
|
+
const next = isMessageScrollerTargetVisible(distanceFromEnd.value) ? 0 : 1;
|
|
315
343
|
if (next !== atEnd.value) {
|
|
316
344
|
atEnd.value = next;
|
|
317
345
|
runOnJS(publishAtEnd)(next === 1);
|
|
@@ -515,12 +543,16 @@ function MessageScrollerButton({
|
|
|
515
543
|
children,
|
|
516
544
|
...props
|
|
517
545
|
}: MessageScrollerButtonProps) {
|
|
518
|
-
const {
|
|
546
|
+
const { distanceFromStart, distanceFromEnd, scrollToEnd, scrollToStart } =
|
|
547
|
+
useScroller('MessageScroller.Button');
|
|
519
548
|
const tint = useCSSVariable('--color-foreground');
|
|
520
549
|
|
|
550
|
+
const targetDistance = target === 'end' ? distanceFromEnd : distanceFromStart;
|
|
521
551
|
// Shown when away from the edge the button points at.
|
|
522
552
|
const shown = useDerivedValue(() =>
|
|
523
|
-
withTiming(
|
|
553
|
+
withTiming(isMessageScrollerTargetVisible(targetDistance.value) ? 1 : 0, {
|
|
554
|
+
duration: 160,
|
|
555
|
+
})
|
|
524
556
|
);
|
|
525
557
|
|
|
526
558
|
const style = useAnimatedStyle(() => ({
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type MessageScrollerTarget = 'start' | 'end';
|
|
2
|
+
|
|
3
|
+
export const MESSAGE_SCROLLER_EDGE_THRESHOLD = 24;
|
|
4
|
+
|
|
5
|
+
/** Distance from the viewport to the edge a jump button targets. */
|
|
6
|
+
export function distanceFromMessageScrollerTarget(
|
|
7
|
+
target: MessageScrollerTarget,
|
|
8
|
+
contentOffsetY: number,
|
|
9
|
+
contentHeight: number,
|
|
10
|
+
viewportHeight: number
|
|
11
|
+
): number {
|
|
12
|
+
'worklet';
|
|
13
|
+
|
|
14
|
+
const maximumOffset = Math.max(0, contentHeight - viewportHeight);
|
|
15
|
+
const boundedOffset = Math.min(maximumOffset, Math.max(0, contentOffsetY));
|
|
16
|
+
return target === 'start' ? boundedOffset : maximumOffset - boundedOffset;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Whether the control has enough distance to its target edge to be useful. */
|
|
20
|
+
export function isMessageScrollerTargetVisible(distance: number): boolean {
|
|
21
|
+
'worklet';
|
|
22
|
+
|
|
23
|
+
return distance > MESSAGE_SCROLLER_EDGE_THRESHOLD;
|
|
24
|
+
}
|
|
@@ -34,6 +34,7 @@ import { AnimatedPressable } from '../../primitives/animated-pressable';
|
|
|
34
34
|
import { Text } from '../../primitives/text';
|
|
35
35
|
import { selectionTick } from '../../utils/haptics';
|
|
36
36
|
import { Label } from '../label';
|
|
37
|
+
import { normalize, precisionOf } from './number-input-math';
|
|
37
38
|
|
|
38
39
|
/** Wait before a held button starts repeating — a tap must not trip it. */
|
|
39
40
|
const REPEAT_DELAY = 400;
|
|
@@ -156,32 +157,6 @@ export interface NumberInputProps
|
|
|
156
157
|
haptics?: boolean;
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
/** Decimals implied by `step`, so display and math round to the same place. */
|
|
160
|
-
function precisionOf(step: number): number {
|
|
161
|
-
if (!isFinite(step)) return 0;
|
|
162
|
-
const s = String(step);
|
|
163
|
-
const dot = s.indexOf('.');
|
|
164
|
-
return dot === -1 ? 0 : s.length - dot - 1;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function clamp(value: number, min: number, max: number): number {
|
|
168
|
-
return Math.min(Math.max(value, min), max);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Snaps to the nearest step from a finite base (or 0 when `min` is open), then
|
|
173
|
-
* rounds to the step's precision so 0.1 + 0.2 does not drift to 0.30000004.
|
|
174
|
-
*/
|
|
175
|
-
function normalize(value: number, min: number, max: number, step: number): number {
|
|
176
|
-
const base = isFinite(min) ? min : 0;
|
|
177
|
-
const snapped = isFinite(step) && step > 0
|
|
178
|
-
? Math.round((value - base) / step) * step + base
|
|
179
|
-
: value;
|
|
180
|
-
const p = precisionOf(step);
|
|
181
|
-
const factor = 10 ** p;
|
|
182
|
-
return clamp(Math.round(snapped * factor) / factor, min, max);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
160
|
/**
|
|
186
161
|
* A number stepped by buttons or typed by hand, reconciled to one clamped,
|
|
187
162
|
* step-snapped value. See the file header for why it keeps both affordances.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** JavaScript numbers carry about fifteen reliable decimal digits. */
|
|
2
|
+
const MAX_DECIMAL_PRECISION = 15;
|
|
3
|
+
|
|
4
|
+
/** Decimals `step` implies, before the reliable-digits ceiling is applied. */
|
|
5
|
+
function requestedPrecision(step: number): number {
|
|
6
|
+
if (!Number.isFinite(step)) return 0;
|
|
7
|
+
|
|
8
|
+
const [coefficient = '', exponentText] = Math.abs(step).toString().toLowerCase().split('e');
|
|
9
|
+
const decimals = coefficient.split('.')[1]?.length ?? 0;
|
|
10
|
+
const exponent = exponentText === undefined ? 0 : Number(exponentText);
|
|
11
|
+
|
|
12
|
+
return Math.max(0, decimals - exponent);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Decimals implied by `step`, including numbers written with an exponent. */
|
|
16
|
+
export function precisionOf(step: number): number {
|
|
17
|
+
return Math.min(MAX_DECIMAL_PRECISION, requestedPrecision(step));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
21
|
+
return Math.min(Math.max(value, min), max);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Snaps to the nearest step from a finite base, then clears binary drift at
|
|
26
|
+
* the step's decimal precision.
|
|
27
|
+
*/
|
|
28
|
+
export function normalize(value: number, min: number, max: number, step: number): number {
|
|
29
|
+
const base = Number.isFinite(min) ? min : 0;
|
|
30
|
+
const offset = (value - base) / step;
|
|
31
|
+
const snapped =
|
|
32
|
+
Number.isFinite(step) && step > 0 && Number.isFinite(offset)
|
|
33
|
+
? Math.round(offset) * step + base
|
|
34
|
+
: value;
|
|
35
|
+
|
|
36
|
+
// A step finer than the reliable-digit ceiling cannot be tidied at its own
|
|
37
|
+
// precision: rounding at the ceiling instead lands on zero and takes the
|
|
38
|
+
// value with it. The snap already placed it on the step, so leave it there.
|
|
39
|
+
if (requestedPrecision(step) > MAX_DECIMAL_PRECISION) return clamp(snapped, min, max);
|
|
40
|
+
|
|
41
|
+
const precision = precisionOf(step);
|
|
42
|
+
const factor = 10 ** precision;
|
|
43
|
+
const scaled = snapped * factor;
|
|
44
|
+
// Keep the established rounding path while it is safe. toFixed avoids
|
|
45
|
+
// turning a finite large value into Infinity when scaling would overflow.
|
|
46
|
+
const rounded = Number.isFinite(scaled)
|
|
47
|
+
? Math.round(scaled) / factor
|
|
48
|
+
: Number(snapped.toFixed(precision));
|
|
49
|
+
return clamp(rounded, min, max);
|
|
50
|
+
}
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
isValidElement,
|
|
69
69
|
useContext,
|
|
70
70
|
useEffect,
|
|
71
|
+
useId,
|
|
71
72
|
useImperativeHandle,
|
|
72
73
|
useMemo,
|
|
73
74
|
useRef,
|
|
@@ -328,7 +329,7 @@ const PlotRoot = forwardRef<PlotHandle, PlotProps>(function PlotRoot(
|
|
|
328
329
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
329
330
|
const [series, setSeries] = useState<[string, string][]>([]);
|
|
330
331
|
const [activeIndexJS, setActiveIndexJS] = useState(-1);
|
|
331
|
-
const clipId =
|
|
332
|
+
const clipId = `panelui-plot-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
332
333
|
|
|
333
334
|
const reveal = useSharedValue(0);
|
|
334
335
|
const domainMin = useSharedValue(0);
|
|
@@ -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
|
/**
|
|
@@ -45,7 +45,7 @@ const itemVariants = tv({
|
|
|
45
45
|
// `self-start` matters: the group is a column, so without it Yoga stretches
|
|
46
46
|
// the row to the full width and the dead space to the right of the label
|
|
47
47
|
// becomes part of the target. Tapping nothing would select the option.
|
|
48
|
-
row: 'flex-row items-center gap-2.5 self-start',
|
|
48
|
+
row: 'min-h-12 min-w-12 flex-row items-center gap-2.5 self-start',
|
|
49
49
|
indicator:
|
|
50
50
|
'h-5 w-5 items-center justify-center rounded-full border border-input bg-background',
|
|
51
51
|
label: 'text-sm text-foreground',
|
|
@@ -248,7 +248,6 @@ const RadioGroupItem = forwardRef<View, RadioGroupItemProps>(
|
|
|
248
248
|
accessibilityLabel={label}
|
|
249
249
|
disabled={disabled}
|
|
250
250
|
onPress={() => context.onValueChange(value)}
|
|
251
|
-
hitSlop={variant === 'card' ? undefined : 8}
|
|
252
251
|
className={slots.row({ className })}
|
|
253
252
|
>
|
|
254
253
|
{body}
|
|
@@ -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
|
|
297
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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(
|
|
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
|
|
347
|
-
|
|
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
|
|
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.
|
|
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
|
|
491
|
-
* one gesture is exactly why the mode has to be
|
|
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={
|
|
541
|
-
accessibilityState={
|
|
542
|
-
//
|
|
543
|
-
//
|
|
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
|
-
|
|
556
|
+
active || disabled ? undefined : 'Long press to select'
|
|
546
557
|
}
|
|
547
|
-
disabled={disabled && showing}
|
|
548
558
|
onPress={() => {
|
|
549
|
-
|
|
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 {
|
|
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
|
-
|
|
430
|
-
|
|
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,
|
|
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=
|
|
545
|
-
accessibilityHint=
|
|
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
|