panelui-native 0.65.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/lib/module/components/bottom-sheet/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 +61 -24
- package/lib/module/components/carousel/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.map +1 -1
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/flow/flow-accessibility.js +18 -2
- package/lib/module/components/flow/flow-accessibility.js.map +1 -1
- 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 +12 -18
- package/lib/module/components/flow/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/popover/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/index.js.map +1 -1
- package/lib/module/primitives/scrim.js +94 -3
- package/lib/module/primitives/scrim.js.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/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/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/flow/flow-accessibility.d.ts +8 -0
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -1
- 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 +6 -5
- package/lib/typescript/src/components/flow/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/popover/index.d.ts +3 -0
- package/lib/typescript/src/components/popover/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/index.d.ts +1 -1
- package/lib/typescript/src/index.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/package.json +7 -4
- package/src/components/bottom-sheet/index.tsx +3 -0
- package/src/components/carousel/carousel-lifecycle.ts +74 -0
- package/src/components/carousel/index.tsx +80 -25
- 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 +3 -0
- package/src/components/drawer/index.tsx +3 -0
- package/src/components/flow/flow-accessibility.ts +16 -2
- package/src/components/flow/flow-identifiers.ts +98 -0
- package/src/components/flow/index.tsx +29 -19
- package/src/components/message-scroller/index.tsx +39 -7
- package/src/components/message-scroller/message-scroller-math.ts +24 -0
- package/src/components/popover/index.tsx +3 -0
- 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/index.ts +2 -0
- package/src/primitives/scrim.tsx +127 -7
|
@@ -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
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type SignatureChangeKind = 'draw' | 'undo' | 'redo' | 'clear';
|
|
2
|
+
|
|
3
|
+
export interface SignatureAccessibilityAction {
|
|
4
|
+
name: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What the pad reads as when it is focused.
|
|
10
|
+
*
|
|
11
|
+
* It counts strokes and says nothing about whether the signature is finished,
|
|
12
|
+
* because the component cannot know that — one mark may be the whole signature
|
|
13
|
+
* or the first letter of it. Announcing "complete" after the first stroke tells
|
|
14
|
+
* somebody who cannot see the pad that they are done when they may not be.
|
|
15
|
+
*/
|
|
16
|
+
export function signatureAccessibilityValue(strokeCount: number) {
|
|
17
|
+
return {
|
|
18
|
+
text:
|
|
19
|
+
strokeCount === 0
|
|
20
|
+
? 'Empty signature'
|
|
21
|
+
: `Signed, ${strokeCount} ${strokeCount === 1 ? 'stroke' : 'strokes'}`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function signatureAccessibilityActions(
|
|
26
|
+
strokeCount: number,
|
|
27
|
+
undoneCount: number,
|
|
28
|
+
alternative: boolean,
|
|
29
|
+
disabled: boolean
|
|
30
|
+
): SignatureAccessibilityAction[] {
|
|
31
|
+
if (disabled) return [];
|
|
32
|
+
|
|
33
|
+
const actions: SignatureAccessibilityAction[] = [];
|
|
34
|
+
if (strokeCount > 0) {
|
|
35
|
+
actions.push(
|
|
36
|
+
{ name: 'signature-undo', label: 'Undo last stroke' },
|
|
37
|
+
{ name: 'signature-clear', label: 'Clear signature' }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (undoneCount > 0) {
|
|
41
|
+
actions.push({ name: 'signature-redo', label: 'Redo last stroke' });
|
|
42
|
+
}
|
|
43
|
+
if (alternative) {
|
|
44
|
+
actions.push({ name: 'signature-alternative', label: 'Use another signature method' });
|
|
45
|
+
}
|
|
46
|
+
return actions;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Spoken when the pad crosses between empty and not, and nowhere else.
|
|
51
|
+
*
|
|
52
|
+
* Every stroke is silent on purpose: a running commentary during drawing is
|
|
53
|
+
* noise over the one gesture the reader is concentrating on. The two crossings
|
|
54
|
+
* are the ones that cannot be felt — that the pad now holds something, and that
|
|
55
|
+
* it no longer does.
|
|
56
|
+
*/
|
|
57
|
+
export function signatureAnnouncement(
|
|
58
|
+
previousCount: number,
|
|
59
|
+
strokeCount: number,
|
|
60
|
+
change: SignatureChangeKind
|
|
61
|
+
): string | null {
|
|
62
|
+
if (previousCount === strokeCount) return null;
|
|
63
|
+
if (change === 'clear') return 'Signature cleared. Signature is empty.';
|
|
64
|
+
if (strokeCount === 0) return 'Signature is empty.';
|
|
65
|
+
if (previousCount === 0) return 'Signature started.';
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
@@ -186,6 +186,12 @@ function snap(value: number, min: number, max: number, step: number) {
|
|
|
186
186
|
/** Which thumb a drag has hold of. A one-thumb slider only ever has `high`. */
|
|
187
187
|
type Thumb = 'low' | 'high';
|
|
188
188
|
|
|
189
|
+
function recordStep(last: Record<Thumb, number>, thumb: Thumb, next: number) {
|
|
190
|
+
if (next === last[thumb]) return false;
|
|
191
|
+
last[thumb] = next;
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
189
195
|
export const Slider = forwardRef<View, SliderProps>(
|
|
190
196
|
(
|
|
191
197
|
{
|
|
@@ -278,9 +284,12 @@ export const Slider = forwardRef<View, SliderProps>(
|
|
|
278
284
|
rangeChangeRef.current = onRangeChange;
|
|
279
285
|
const rangeCommitRef = useRef(onRangeCommit);
|
|
280
286
|
rangeCommitRef.current = onRangeCommit;
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
const lastTick = useRef(
|
|
287
|
+
// Each thumb owns its last tick. Sharing one scalar lets the stationary
|
|
288
|
+
// endpoint suppress the thumb under the finger on a range slider.
|
|
289
|
+
const lastTick = useRef<Record<Thumb, number>>({
|
|
290
|
+
low,
|
|
291
|
+
high: isRange ? high : value,
|
|
292
|
+
});
|
|
284
293
|
/*
|
|
285
294
|
* True from the moment a finger lands until it lifts. A controlled parent
|
|
286
295
|
* echoes every change straight back as a new prop, and springing the thumb
|
|
@@ -329,8 +338,7 @@ export const Slider = forwardRef<View, SliderProps>(
|
|
|
329
338
|
(p: number, commit: boolean) => {
|
|
330
339
|
const raw = min + p * span;
|
|
331
340
|
const snapped = snap(raw, min, max, step);
|
|
332
|
-
if (haptics &&
|
|
333
|
-
lastTick.current = snapped;
|
|
341
|
+
if (haptics && recordStep(lastTick.current, 'high', snapped)) {
|
|
334
342
|
selectionTick();
|
|
335
343
|
}
|
|
336
344
|
emitChange(snapped);
|
|
@@ -340,16 +348,13 @@ export const Slider = forwardRef<View, SliderProps>(
|
|
|
340
348
|
);
|
|
341
349
|
|
|
342
350
|
const commitRangeFromProgress = useCallback(
|
|
343
|
-
(pLow: number, pHigh: number, commit: boolean) => {
|
|
351
|
+
(pLow: number, pHigh: number, commit: boolean, thumb: Thumb) => {
|
|
344
352
|
const next: [number, number] = [
|
|
345
353
|
snap(min + pLow * span, min, max, step),
|
|
346
354
|
snap(min + pHigh * span, min, max, step),
|
|
347
355
|
];
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const moved = next[0] !== lastTick.current && next[1] !== lastTick.current;
|
|
351
|
-
if (haptics && moved) {
|
|
352
|
-
lastTick.current = next[0];
|
|
356
|
+
const nextStep = thumb === 'low' ? next[0] : next[1];
|
|
357
|
+
if (haptics && recordStep(lastTick.current, thumb, nextStep)) {
|
|
353
358
|
selectionTick();
|
|
354
359
|
}
|
|
355
360
|
emitRangeChange(next);
|
|
@@ -418,12 +423,12 @@ export const Slider = forwardRef<View, SliderProps>(
|
|
|
418
423
|
const ceiling = Math.max(highProgress.value - gap, 0);
|
|
419
424
|
const next = Math.min(Math.max(dragStartLow.value + delta, 0), ceiling);
|
|
420
425
|
lowProgress.value = next;
|
|
421
|
-
runOnJS(commitRangeFromProgress)(next, highProgress.value, false);
|
|
426
|
+
runOnJS(commitRangeFromProgress)(next, highProgress.value, false, 'low');
|
|
422
427
|
} else {
|
|
423
428
|
const floor = Math.min(lowProgress.value + gap, 1);
|
|
424
429
|
const next = Math.min(Math.max(dragStartHigh.value + delta, floor), 1);
|
|
425
430
|
highProgress.value = next;
|
|
426
|
-
runOnJS(commitRangeFromProgress)(lowProgress.value, next, false);
|
|
431
|
+
runOnJS(commitRangeFromProgress)(lowProgress.value, next, false, 'high');
|
|
427
432
|
}
|
|
428
433
|
})
|
|
429
434
|
.onFinalize(() => {
|
|
@@ -431,7 +436,12 @@ export const Slider = forwardRef<View, SliderProps>(
|
|
|
431
436
|
pressedLow.value = withSpring(0, KNOB_SPRING);
|
|
432
437
|
runOnJS(setDragging)(false);
|
|
433
438
|
if (isRange) {
|
|
434
|
-
runOnJS(commitRangeFromProgress)(
|
|
439
|
+
runOnJS(commitRangeFromProgress)(
|
|
440
|
+
lowProgress.value,
|
|
441
|
+
highProgress.value,
|
|
442
|
+
true,
|
|
443
|
+
activeThumb.value
|
|
444
|
+
);
|
|
435
445
|
} else {
|
|
436
446
|
runOnJS(commitFromProgress)(highProgress.value, true);
|
|
437
447
|
}
|
|
@@ -456,11 +466,11 @@ export const Slider = forwardRef<View, SliderProps>(
|
|
|
456
466
|
if (pickThumb(event.x) === 'low') {
|
|
457
467
|
const bounded = Math.min(next, Math.max(highProgress.value - gap, 0));
|
|
458
468
|
lowProgress.value = withSpring(bounded, SPRING);
|
|
459
|
-
runOnJS(commitRangeFromProgress)(bounded, highProgress.value, true);
|
|
469
|
+
runOnJS(commitRangeFromProgress)(bounded, highProgress.value, true, 'low');
|
|
460
470
|
} else {
|
|
461
471
|
const bounded = Math.max(next, Math.min(lowProgress.value + gap, 1));
|
|
462
472
|
highProgress.value = withSpring(bounded, SPRING);
|
|
463
|
-
runOnJS(commitRangeFromProgress)(lowProgress.value, bounded, true);
|
|
473
|
+
runOnJS(commitRangeFromProgress)(lowProgress.value, bounded, true, 'high');
|
|
464
474
|
}
|
|
465
475
|
});
|
|
466
476
|
|
|
@@ -503,7 +513,7 @@ export const Slider = forwardRef<View, SliderProps>(
|
|
|
503
513
|
const next = snap(value + dir * (step || 1), min, max, step);
|
|
504
514
|
if (next === value) return;
|
|
505
515
|
if (haptics) {
|
|
506
|
-
lastTick.current
|
|
516
|
+
recordStep(lastTick.current, 'high', next);
|
|
507
517
|
selectionTick();
|
|
508
518
|
}
|
|
509
519
|
highProgress.value = withSpring(toFraction(next), SPRING);
|
|
@@ -521,7 +531,7 @@ export const Slider = forwardRef<View, SliderProps>(
|
|
|
521
531
|
const next = snap(bounded, min, max, step);
|
|
522
532
|
if (next === current) return;
|
|
523
533
|
if (haptics) {
|
|
524
|
-
lastTick.current
|
|
534
|
+
recordStep(lastTick.current, thumb, next);
|
|
525
535
|
selectionTick();
|
|
526
536
|
}
|
|
527
537
|
const pair: [number, number] = thumb === 'low' ? [next, high] : [low, next];
|
package/src/index.ts
CHANGED
package/src/primitives/scrim.tsx
CHANGED
|
@@ -10,17 +10,20 @@
|
|
|
10
10
|
* that never blurs anything installs nothing. When it is asked for and the
|
|
11
11
|
* package is missing, this falls back to a dim rather than failing: a blur you
|
|
12
12
|
* cannot draw is better shown as a darkened screen than as a crash.
|
|
13
|
+
* Reduce Transparency replaces the blur with an opaque, tint-aware surface.
|
|
13
14
|
*
|
|
14
15
|
* It fills its parent and does not intercept touches itself — the overlay layers
|
|
15
16
|
* its own dismiss `Pressable` over it — so it is purely the visual backdrop.
|
|
16
17
|
*/
|
|
17
|
-
import { type ComponentType } from 'react';
|
|
18
|
-
import { StyleSheet, View, type ViewProps } from 'react-native';
|
|
18
|
+
import { useEffect, useState, type ComponentType } from 'react';
|
|
19
|
+
import { AccessibilityInfo, StyleSheet, View, type ViewProps } from 'react-native';
|
|
19
20
|
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
|
20
21
|
|
|
22
|
+
type BlurTint = 'light' | 'dark' | 'default' | 'systemMaterial';
|
|
23
|
+
|
|
21
24
|
interface BlurViewProps {
|
|
22
25
|
intensity?: number;
|
|
23
|
-
tint?:
|
|
26
|
+
tint?: BlurTint;
|
|
24
27
|
style?: unknown;
|
|
25
28
|
children?: React.ReactNode;
|
|
26
29
|
}
|
|
@@ -43,16 +46,112 @@ const BlurView: ComponentType<BlurViewProps> | null = (() => {
|
|
|
43
46
|
/** True when a real blur can be drawn — for a caller that wants to know. */
|
|
44
47
|
export const hasBlur = BlurView !== null;
|
|
45
48
|
|
|
49
|
+
type ReduceTransparencySource = {
|
|
50
|
+
isReduceTransparencyEnabled?: () => Promise<boolean>;
|
|
51
|
+
addEventListener?: (
|
|
52
|
+
event: 'reduceTransparencyChanged',
|
|
53
|
+
listener: (enabled: boolean) => void
|
|
54
|
+
) => { remove?: () => void } | undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function observeReduceTransparency(
|
|
58
|
+
source: ReduceTransparencySource,
|
|
59
|
+
onChange: (enabled: boolean) => void
|
|
60
|
+
) {
|
|
61
|
+
let active = true;
|
|
62
|
+
let changed = false;
|
|
63
|
+
const update = (enabled: boolean) => {
|
|
64
|
+
changed = true;
|
|
65
|
+
if (active) onChange(Boolean(enabled));
|
|
66
|
+
};
|
|
67
|
+
/*
|
|
68
|
+
* A platform that cannot report the preference has no preference to respect,
|
|
69
|
+
* so blur stays available. A platform that has the API and failed to answer
|
|
70
|
+
* might have it switched on, and that is the case worth being careful about.
|
|
71
|
+
*/
|
|
72
|
+
const fallback = (queryable: boolean) => () => {
|
|
73
|
+
if (active && !changed) onChange(queryable);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
let subscription: { remove?: () => void } | undefined;
|
|
77
|
+
try {
|
|
78
|
+
subscription = source.addEventListener?.('reduceTransparencyChanged', update);
|
|
79
|
+
} catch {
|
|
80
|
+
// Some web and test shims expose AccessibilityInfo without every event.
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
if (typeof source.isReduceTransparencyEnabled !== 'function') fallback(false)();
|
|
85
|
+
else {
|
|
86
|
+
void source.isReduceTransparencyEnabled().then((enabled) => {
|
|
87
|
+
// A preference event that arrived while the query was pending is newer.
|
|
88
|
+
if (active && !changed) update(enabled);
|
|
89
|
+
}, fallback(true));
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
fallback(true)();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return () => {
|
|
96
|
+
active = false;
|
|
97
|
+
try {
|
|
98
|
+
subscription?.remove?.();
|
|
99
|
+
} catch {
|
|
100
|
+
// Cleanup stays safe for partial platform shims.
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The last answer the platform gave, shared by every scrim in the process.
|
|
107
|
+
*
|
|
108
|
+
* The query is asynchronous and the preference belongs to the device, not to
|
|
109
|
+
* any one overlay. Without this, each overlay opens not knowing it and draws
|
|
110
|
+
* opaque until the answer arrives — a flash on every dialog for the many people
|
|
111
|
+
* who have the preference off. Remembered here, only the first overlay of a
|
|
112
|
+
* launch can see it.
|
|
113
|
+
*/
|
|
114
|
+
let knownReduceTransparency: boolean | null = null;
|
|
115
|
+
|
|
116
|
+
function useReduceTransparency() {
|
|
117
|
+
// Not knowing is deliberately conservative: draw opaque until the platform
|
|
118
|
+
// says blur is allowed, rather than flashing blur at someone who opted out.
|
|
119
|
+
const [enabled, setEnabled] = useState<boolean | null>(knownReduceTransparency);
|
|
120
|
+
useEffect(
|
|
121
|
+
() =>
|
|
122
|
+
observeReduceTransparency(AccessibilityInfo, (next) => {
|
|
123
|
+
knownReduceTransparency = next;
|
|
124
|
+
setEnabled(next);
|
|
125
|
+
}),
|
|
126
|
+
[]
|
|
127
|
+
);
|
|
128
|
+
return enabled;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type ScrimMode = 'blur' | 'dim' | 'opaque';
|
|
132
|
+
|
|
133
|
+
function scrimMode(blur: boolean, canBlur: boolean, reduceTransparency: boolean | null): ScrimMode {
|
|
134
|
+
if (!blur) return 'dim';
|
|
135
|
+
if (reduceTransparency !== false) return 'opaque';
|
|
136
|
+
return canBlur ? 'blur' : 'dim';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function opaqueClassName(tint: BlurTint) {
|
|
140
|
+
if (tint === 'light') return 'bg-white';
|
|
141
|
+
if (tint === 'dark') return 'bg-black';
|
|
142
|
+
return 'bg-background';
|
|
143
|
+
}
|
|
144
|
+
|
|
46
145
|
export interface ScrimProps extends Omit<ViewProps, 'children'> {
|
|
47
146
|
/** Frost the backdrop instead of dimming it. Falls back to a dim if it can't. */
|
|
48
147
|
blur?: boolean;
|
|
49
148
|
/** Blur strength, 0–100. */
|
|
50
149
|
intensity?: number;
|
|
51
150
|
/** Which way the blur tints. */
|
|
52
|
-
tint?:
|
|
151
|
+
tint?: BlurTint;
|
|
53
152
|
/**
|
|
54
|
-
* The dim used when not blurring
|
|
55
|
-
*
|
|
153
|
+
* The dim used when not blurring, when blur is unavailable, and over the
|
|
154
|
+
* opaque Reduce Transparency fallback. A popover passes a lighter one than a dialog.
|
|
56
155
|
*/
|
|
57
156
|
dimClassName?: string;
|
|
58
157
|
}
|
|
@@ -65,7 +164,10 @@ export function Scrim({
|
|
|
65
164
|
style,
|
|
66
165
|
...props
|
|
67
166
|
}: ScrimProps) {
|
|
68
|
-
|
|
167
|
+
const reduceTransparency = useReduceTransparency();
|
|
168
|
+
const mode = scrimMode(blur, BlurView !== null, reduceTransparency);
|
|
169
|
+
|
|
170
|
+
if (mode === 'blur' && BlurView) {
|
|
69
171
|
return (
|
|
70
172
|
<Animated.View
|
|
71
173
|
entering={FadeIn.duration(180)}
|
|
@@ -81,6 +183,24 @@ export function Scrim({
|
|
|
81
183
|
);
|
|
82
184
|
}
|
|
83
185
|
|
|
186
|
+
if (mode === 'opaque') {
|
|
187
|
+
return (
|
|
188
|
+
<Animated.View
|
|
189
|
+
entering={FadeIn.duration(180)}
|
|
190
|
+
exiting={FadeOut.duration(150)}
|
|
191
|
+
style={[StyleSheet.absoluteFill, style]}
|
|
192
|
+
{...props}
|
|
193
|
+
>
|
|
194
|
+
<View
|
|
195
|
+
pointerEvents="none"
|
|
196
|
+
style={StyleSheet.absoluteFill}
|
|
197
|
+
className={opaqueClassName(tint)}
|
|
198
|
+
/>
|
|
199
|
+
<View pointerEvents="none" style={StyleSheet.absoluteFill} className={dimClassName} />
|
|
200
|
+
</Animated.View>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
84
204
|
return (
|
|
85
205
|
<Animated.View
|
|
86
206
|
entering={FadeIn.duration(150)}
|