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.
- package/README.md +21 -1
- package/lib/module/components/area-chart/index.js +18 -1
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +18 -1
- package/lib/module/components/bar-chart/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/candlestick-chart/index.js +18 -1
- package/lib/module/components/candlestick-chart/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/form/index.js +7 -5
- package/lib/module/components/form/index.js.map +1 -1
- package/lib/module/components/form/typed-form.js +11 -0
- package/lib/module/components/form/typed-form.js.map +1 -0
- package/lib/module/components/form/use-form.js +4 -0
- package/lib/module/components/form/use-form.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +27 -1
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +18 -1
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/message-scroller/index.js +187 -17
- package/lib/module/components/message-scroller/index.js.map +1 -1
- package/lib/module/components/message-scroller/message-scroller-math.js +43 -0
- package/lib/module/components/message-scroller/message-scroller-math.js.map +1 -0
- package/lib/module/components/meter/index.js +250 -0
- package/lib/module/components/meter/index.js.map +1 -0
- package/lib/module/components/meter/meter-scale.js +88 -0
- package/lib/module/components/meter/meter-scale.js.map +1 -0
- package/lib/module/components/planner/index.js +714 -0
- package/lib/module/components/planner/index.js.map +1 -0
- package/lib/module/components/planner/planner-entries.js +124 -0
- package/lib/module/components/planner/planner-entries.js.map +1 -0
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/radar-chart/index.js +18 -1
- package/lib/module/components/radar-chart/index.js.map +1 -1
- package/lib/module/components/scatter-chart/index.js +18 -1
- package/lib/module/components/scatter-chart/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/skeleton/index.js +45 -7
- package/lib/module/components/skeleton/index.js.map +1 -1
- package/lib/module/components/slider/index.js +22 -18
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/spinner/index.js +63 -14
- package/lib/module/components/spinner/index.js.map +1 -1
- package/lib/module/components/time-picker/index.js +46 -21
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/components/time-picker/ruler-window.js +25 -0
- package/lib/module/components/time-picker/ruler-window.js.map +1 -0
- package/lib/module/hooks/breakpoint-contract.js +29 -0
- package/lib/module/hooks/breakpoint-contract.js.map +1 -0
- package/lib/module/hooks/index.js +1 -1
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-breakpoint.js +46 -1
- package/lib/module/hooks/use-breakpoint.js.map +1 -1
- package/lib/module/index.js +3 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/chart-accessibility.js +68 -0
- package/lib/module/primitives/chart-accessibility.js.map +1 -0
- package/lib/module/primitives/scrim.js +94 -3
- package/lib/module/primitives/scrim.js.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts +2 -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/candlestick-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/candlestick-chart/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/form/index.d.ts +18 -5
- package/lib/typescript/src/components/form/index.d.ts.map +1 -1
- package/lib/typescript/src/components/form/typed-form.d.ts +7 -0
- package/lib/typescript/src/components/form/typed-form.d.ts.map +1 -0
- package/lib/typescript/src/components/form/use-form.d.ts +4 -0
- package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message-scroller/index.d.ts +30 -2
- 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 +18 -0
- package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts.map +1 -0
- package/lib/typescript/src/components/meter/index.d.ts +270 -0
- package/lib/typescript/src/components/meter/index.d.ts.map +1 -0
- package/lib/typescript/src/components/meter/meter-scale.d.ts +57 -0
- package/lib/typescript/src/components/meter/meter-scale.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/index.d.ts +274 -0
- package/lib/typescript/src/components/planner/index.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/planner-entries.d.ts +75 -0
- package/lib/typescript/src/components/planner/planner-entries.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/radar-chart/index.d.ts +4 -0
- package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/scatter-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/scatter-chart/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/skeleton/index.d.ts +25 -2
- package/lib/typescript/src/components/skeleton/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/spinner/index.d.ts +25 -2
- package/lib/typescript/src/components/spinner/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/index.d.ts +5 -6
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/ruler-window.d.ts +12 -0
- package/lib/typescript/src/components/time-picker/ruler-window.d.ts.map +1 -0
- package/lib/typescript/src/hooks/breakpoint-contract.d.ts +6 -0
- package/lib/typescript/src/hooks/breakpoint-contract.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +1 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-breakpoint.d.ts +22 -0
- package/lib/typescript/src/hooks/use-breakpoint.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/chart-accessibility.d.ts +33 -0
- package/lib/typescript/src/primitives/chart-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/primitives/scrim.d.ts +5 -3
- package/lib/typescript/src/primitives/scrim.d.ts.map +1 -1
- package/lib/typescript/types/form-api.type-test.d.ts +2 -0
- package/lib/typescript/types/form-api.type-test.d.ts.map +1 -0
- package/package.json +34 -5
- package/src/components/area-chart/index.tsx +27 -2
- package/src/components/bar-chart/index.tsx +27 -2
- package/src/components/bottom-sheet/index.tsx +3 -0
- package/src/components/candlestick-chart/index.tsx +30 -2
- 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/form/index.tsx +32 -5
- package/src/components/form/typed-form.ts +13 -0
- package/src/components/form/use-form.ts +9 -0
- package/src/components/heatmap-chart/index.tsx +49 -3
- package/src/components/line-chart/index.tsx +27 -2
- package/src/components/message-scroller/index.tsx +273 -13
- package/src/components/message-scroller/message-scroller-math.ts +62 -0
- package/src/components/meter/index.tsx +318 -0
- package/src/components/meter/meter-scale.ts +109 -0
- package/src/components/planner/index.tsx +886 -0
- package/src/components/planner/planner-entries.ts +146 -0
- package/src/components/popover/index.tsx +3 -0
- package/src/components/radar-chart/index.tsx +24 -0
- package/src/components/scatter-chart/index.tsx +27 -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/skeleton/index.tsx +59 -5
- package/src/components/slider/index.tsx +28 -18
- package/src/components/spinner/index.tsx +80 -13
- package/src/components/time-picker/index.tsx +57 -29
- package/src/components/time-picker/ruler-window.ts +24 -0
- package/src/hooks/breakpoint-contract.ts +31 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/use-breakpoint.ts +61 -10
- package/src/index.ts +32 -0
- package/src/primitives/chart-accessibility.ts +102 -0
- package/src/primitives/scrim.tsx +127 -7
|
@@ -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
|
+
}
|
|
@@ -2,31 +2,85 @@ import { memo, useEffect } from 'react';
|
|
|
2
2
|
import Animated, {
|
|
3
3
|
cancelAnimation,
|
|
4
4
|
useAnimatedStyle,
|
|
5
|
+
useReducedMotion,
|
|
5
6
|
useSharedValue,
|
|
6
7
|
withRepeat,
|
|
7
8
|
withTiming,
|
|
8
9
|
} from 'react-native-reanimated';
|
|
9
10
|
import { cn } from '../../utils/cn';
|
|
10
11
|
|
|
12
|
+
/** Milliseconds for one half of the pulse. */
|
|
13
|
+
const PULSE_DURATION = 700;
|
|
14
|
+
/** How far down the pulse goes. */
|
|
15
|
+
const PULSE_FLOOR = 0.45;
|
|
16
|
+
/**
|
|
17
|
+
* Where the pulse rests when it is not running. Between the two ends of it, so
|
|
18
|
+
* a still skeleton looks like the same placeholder rather than a solid block.
|
|
19
|
+
*/
|
|
20
|
+
const RESTING_OPACITY = 0.7;
|
|
21
|
+
|
|
11
22
|
export interface SkeletonProps {
|
|
12
23
|
className?: string;
|
|
24
|
+
/**
|
|
25
|
+
* What is loading, for a screen reader. Setting it makes this skeleton
|
|
26
|
+
* announce as a busy status; leaving it unset keeps the placeholder out of
|
|
27
|
+
* the accessibility tree entirely.
|
|
28
|
+
*
|
|
29
|
+
* A screen full of placeholders needs **one** of them labelled, not all of
|
|
30
|
+
* them — put it on the skeleton standing for the region and leave the rest
|
|
31
|
+
* silent.
|
|
32
|
+
*/
|
|
33
|
+
label?: string;
|
|
13
34
|
}
|
|
14
35
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Pulsing placeholder for content that has not arrived. The opacity animation
|
|
38
|
+
* runs on the UI thread.
|
|
39
|
+
*
|
|
40
|
+
* A placeholder is a picture of absent content, so it is hidden from assistive
|
|
41
|
+
* technology by default — an unlabelled grey box announces nothing worth
|
|
42
|
+
* hearing, and a screen of them announces it many times over. Pass `label` on
|
|
43
|
+
* the one skeleton that stands for the region to have the wait announced once.
|
|
44
|
+
*
|
|
45
|
+
* Under the platform's reduce-motion setting the pulse stops and the
|
|
46
|
+
* placeholder holds at a middle opacity. Unlike a spinner, a skeleton that
|
|
47
|
+
* stops moving does not read as hung: it is a shape where content will be, and
|
|
48
|
+
* the shape says that on its own.
|
|
49
|
+
*/
|
|
50
|
+
export const Skeleton = memo(function Skeleton({
|
|
51
|
+
className,
|
|
52
|
+
label,
|
|
53
|
+
}: SkeletonProps) {
|
|
54
|
+
const reducedMotion = useReducedMotion();
|
|
55
|
+
const opacity = useSharedValue(reducedMotion ? RESTING_OPACITY : 1);
|
|
18
56
|
|
|
19
57
|
useEffect(() => {
|
|
20
|
-
|
|
58
|
+
if (reducedMotion) {
|
|
59
|
+
opacity.value = RESTING_OPACITY;
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
opacity.value = 1;
|
|
63
|
+
opacity.value = withRepeat(
|
|
64
|
+
withTiming(PULSE_FLOOR, { duration: PULSE_DURATION }),
|
|
65
|
+
-1,
|
|
66
|
+
true
|
|
67
|
+
);
|
|
21
68
|
return () => cancelAnimation(opacity);
|
|
22
|
-
}, [opacity]);
|
|
69
|
+
}, [opacity, reducedMotion]);
|
|
23
70
|
|
|
24
71
|
const animatedStyle = useAnimatedStyle(() => ({
|
|
25
72
|
opacity: opacity.value,
|
|
26
73
|
}));
|
|
27
74
|
|
|
75
|
+
const announced = label != null;
|
|
76
|
+
|
|
28
77
|
return (
|
|
29
78
|
<Animated.View
|
|
79
|
+
accessibilityRole={announced ? 'progressbar' : undefined}
|
|
80
|
+
accessibilityLabel={label}
|
|
81
|
+
accessibilityState={announced ? { busy: true } : undefined}
|
|
82
|
+
accessibilityElementsHidden={!announced}
|
|
83
|
+
importantForAccessibility={announced ? 'auto' : 'no-hide-descendants'}
|
|
30
84
|
style={animatedStyle}
|
|
31
85
|
className={cn('rounded-md bg-skeleton', className)}
|
|
32
86
|
/>
|
|
@@ -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];
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { memo, useEffect } from 'react';
|
|
2
2
|
import Animated, {
|
|
3
3
|
Easing,
|
|
4
|
+
cancelAnimation,
|
|
5
|
+
interpolate,
|
|
4
6
|
useAnimatedStyle,
|
|
7
|
+
useReducedMotion,
|
|
5
8
|
useSharedValue,
|
|
6
9
|
withRepeat,
|
|
7
10
|
withTiming,
|
|
8
|
-
cancelAnimation,
|
|
9
11
|
} from 'react-native-reanimated';
|
|
10
12
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
11
13
|
|
|
14
|
+
/** Milliseconds for one full turn. */
|
|
15
|
+
const SPIN_DURATION = 800;
|
|
16
|
+
/** Milliseconds for one half of the fade that stands in for the turn. */
|
|
17
|
+
const PULSE_DURATION = 900;
|
|
18
|
+
/** How far down the fade goes. Faint enough to read as a pulse, not a flicker. */
|
|
19
|
+
const PULSE_FLOOR = 0.35;
|
|
20
|
+
|
|
12
21
|
const spinnerVariants = tv({
|
|
13
22
|
base: 'rounded-full border-2 border-muted border-t-primary',
|
|
14
23
|
variants: {
|
|
@@ -25,27 +34,85 @@ const spinnerVariants = tv({
|
|
|
25
34
|
|
|
26
35
|
export interface SpinnerProps extends VariantProps<typeof spinnerVariants> {
|
|
27
36
|
className?: string;
|
|
37
|
+
/**
|
|
38
|
+
* What is loading, for a screen reader. Setting it makes the spinner
|
|
39
|
+
* announce as a busy status; leaving it unset keeps it out of the
|
|
40
|
+
* accessibility tree.
|
|
41
|
+
*
|
|
42
|
+
* Leave it unset wherever something around the spinner already says the
|
|
43
|
+
* wait is on — a loading button, a row with its own caption. Set it where
|
|
44
|
+
* the spinner is the only sign, or the wait passes in silence.
|
|
45
|
+
*/
|
|
46
|
+
label?: string;
|
|
28
47
|
}
|
|
29
48
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Rotating ring for work of unknown length. The animation runs entirely on the
|
|
51
|
+
* UI thread.
|
|
52
|
+
*
|
|
53
|
+
* A bare ring carries no words, so it is hidden from assistive technology
|
|
54
|
+
* unless `label` is given — an unnamed progress indicator announces its role
|
|
55
|
+
* and nothing else, which is noise standing in for information.
|
|
56
|
+
*
|
|
57
|
+
* Under the platform's reduce-motion setting the ring stops turning and fades
|
|
58
|
+
* in place instead. Stopping it dead is the other option and it is the wrong
|
|
59
|
+
* one: a spinner that holds still reads as one that has hung, which is the
|
|
60
|
+
* single thing a spinner exists to rule out. Reduce motion asks for less
|
|
61
|
+
* travel, not for less news.
|
|
62
|
+
*/
|
|
63
|
+
export const Spinner = memo(function Spinner({
|
|
64
|
+
className,
|
|
65
|
+
size,
|
|
66
|
+
label,
|
|
67
|
+
}: SpinnerProps) {
|
|
68
|
+
const reducedMotion = useReducedMotion();
|
|
69
|
+
const progress = useSharedValue(0);
|
|
33
70
|
|
|
34
71
|
useEffect(() => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
72
|
+
progress.value = 0;
|
|
73
|
+
progress.value = withRepeat(
|
|
74
|
+
withTiming(1, {
|
|
75
|
+
duration: reducedMotion ? PULSE_DURATION : SPIN_DURATION,
|
|
76
|
+
easing: reducedMotion ? Easing.inOut(Easing.ease) : Easing.linear,
|
|
77
|
+
}),
|
|
78
|
+
-1,
|
|
79
|
+
// The turn restarts where it began, because a full circle already ends
|
|
80
|
+
// where it started. The fade turns around, because a pulse that snapped
|
|
81
|
+
// back to full brightness would be the flicker the setting is there to
|
|
82
|
+
// avoid.
|
|
83
|
+
reducedMotion
|
|
38
84
|
);
|
|
39
|
-
return () => cancelAnimation(
|
|
40
|
-
}, [
|
|
85
|
+
return () => cancelAnimation(progress);
|
|
86
|
+
}, [progress, reducedMotion]);
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* One style sets both properties either case needs. The two are swapped on
|
|
90
|
+
* the same view when the setting changes under a mounted spinner, and an
|
|
91
|
+
* animated property that one case drops is not reset by the other — it is
|
|
92
|
+
* left at whatever it was last given, which would strand a half-faded ring
|
|
93
|
+
* at that opacity for the rest of its life.
|
|
94
|
+
*/
|
|
95
|
+
const animatedStyle = useAnimatedStyle(() =>
|
|
96
|
+
reducedMotion
|
|
97
|
+
? {
|
|
98
|
+
opacity: interpolate(progress.value, [0, 1], [1, PULSE_FLOOR]),
|
|
99
|
+
transform: [{ rotate: '0deg' }],
|
|
100
|
+
}
|
|
101
|
+
: {
|
|
102
|
+
opacity: 1,
|
|
103
|
+
transform: [{ rotate: `${progress.value * 360}deg` }],
|
|
104
|
+
}
|
|
105
|
+
);
|
|
41
106
|
|
|
42
|
-
const
|
|
43
|
-
transform: [{ rotate: `${rotation.value}deg` }],
|
|
44
|
-
}));
|
|
107
|
+
const announced = label != null;
|
|
45
108
|
|
|
46
109
|
return (
|
|
47
110
|
<Animated.View
|
|
48
|
-
accessibilityRole=
|
|
111
|
+
accessibilityRole={announced ? 'progressbar' : undefined}
|
|
112
|
+
accessibilityLabel={label}
|
|
113
|
+
accessibilityState={announced ? { busy: true } : undefined}
|
|
114
|
+
accessibilityElementsHidden={!announced}
|
|
115
|
+
importantForAccessibility={announced ? 'auto' : 'no-hide-descendants'}
|
|
49
116
|
style={animatedStyle}
|
|
50
117
|
className={spinnerVariants({ size, className })}
|
|
51
118
|
/>
|
|
@@ -36,12 +36,11 @@
|
|
|
36
36
|
*
|
|
37
37
|
* ## Scroll offset, not gesture maths
|
|
38
38
|
*
|
|
39
|
-
* The wheel, the clock's list and the ruler are
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* scrolling never round-trips through React — only the settled value does.
|
|
39
|
+
* The wheel, the clock's list and the ruler are snapping scroll views, and the
|
|
40
|
+
* selection is `Math.round(offset / itemSize)`. That buys momentum,
|
|
41
|
+
* deceleration, edge bounce and platform-correct fling physics for nothing,
|
|
42
|
+
* and none of it would be worth rebuilding on a pan gesture. The ruler keeps
|
|
43
|
+
* that full scroll range but mounts only an overscanned window of its ticks.
|
|
45
44
|
*/
|
|
46
45
|
import {
|
|
47
46
|
useCallback,
|
|
@@ -79,6 +78,7 @@ import {
|
|
|
79
78
|
accessibilityValueForIndex,
|
|
80
79
|
indexForAccessibilityAction,
|
|
81
80
|
} from './accessibility';
|
|
81
|
+
import { rulerWindow } from './ruler-window';
|
|
82
82
|
import {
|
|
83
83
|
clampTime,
|
|
84
84
|
displayHour,
|
|
@@ -848,10 +848,20 @@ function RulerFace({
|
|
|
848
848
|
*/
|
|
849
849
|
const [width, setWidth] = useState(0);
|
|
850
850
|
const pad = Math.max(0, width / 2 - TICK_SPACING / 2);
|
|
851
|
+
const [windowIndex, setWindowIndex] = useState(index);
|
|
852
|
+
// A controlled/accessibility jump renders its destination before the effect
|
|
853
|
+
// moves the scroll view there; an in-flight gesture follows the scroll window.
|
|
854
|
+
const windowAnchor = index === resting.current ? windowIndex : index;
|
|
855
|
+
const window = rulerWindow(times.length, windowAnchor);
|
|
851
856
|
|
|
852
|
-
const
|
|
853
|
-
(
|
|
854
|
-
|
|
857
|
+
const showIndex = useCallback(
|
|
858
|
+
(next: number) => {
|
|
859
|
+
const nextStart = rulerWindow(times.length, next).start;
|
|
860
|
+
setWindowIndex((current) =>
|
|
861
|
+
rulerWindow(times.length, current).start === nextStart ? current : next
|
|
862
|
+
);
|
|
863
|
+
},
|
|
864
|
+
[times.length]
|
|
855
865
|
);
|
|
856
866
|
|
|
857
867
|
// The scale runs the same rest-and-resync machinery the wheel's columns do,
|
|
@@ -872,10 +882,14 @@ function RulerFace({
|
|
|
872
882
|
|
|
873
883
|
// Unanimated, for the reason given on the wheel's columns: an animated
|
|
874
884
|
// programmatic scroll reports itself as a gesture and the correction loops.
|
|
875
|
-
const snapTo = useCallback(
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
885
|
+
const snapTo = useCallback(
|
|
886
|
+
(to: number) => {
|
|
887
|
+
resting.current = to;
|
|
888
|
+
showIndex(to);
|
|
889
|
+
ref.current?.scrollTo({ x: to * TICK_SPACING, animated: false });
|
|
890
|
+
},
|
|
891
|
+
[showIndex]
|
|
892
|
+
);
|
|
879
893
|
|
|
880
894
|
useEffect(() => {
|
|
881
895
|
if (index === resting.current) return;
|
|
@@ -934,6 +948,13 @@ function RulerFace({
|
|
|
934
948
|
[atRest, cancelTimer, settleAt]
|
|
935
949
|
);
|
|
936
950
|
|
|
951
|
+
const onScroll = useCallback(
|
|
952
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
953
|
+
showIndex(Math.round(event.nativeEvent.contentOffset.x / TICK_SPACING));
|
|
954
|
+
},
|
|
955
|
+
[showIndex]
|
|
956
|
+
);
|
|
957
|
+
|
|
937
958
|
useEffect(() => cancelTimer, [cancelTimer]);
|
|
938
959
|
|
|
939
960
|
const stepsPerHour = Math.max(1, Math.round(60 / minuteStep));
|
|
@@ -985,9 +1006,11 @@ function RulerFace({
|
|
|
985
1006
|
contentOffset={{ x: index * TICK_SPACING, y: 0 }}
|
|
986
1007
|
scrollEnabled={!disabled}
|
|
987
1008
|
showsHorizontalScrollIndicator={false}
|
|
988
|
-
|
|
1009
|
+
snapToInterval={TICK_SPACING}
|
|
989
1010
|
disableIntervalMomentum
|
|
990
1011
|
decelerationRate="fast"
|
|
1012
|
+
onScroll={onScroll}
|
|
1013
|
+
scrollEventThrottle={16}
|
|
991
1014
|
onScrollBeginDrag={startMoving}
|
|
992
1015
|
onScrollEndDrag={onDragEnd}
|
|
993
1016
|
onMomentumScrollBegin={startMoving}
|
|
@@ -1009,22 +1032,27 @@ function RulerFace({
|
|
|
1009
1032
|
at most — which is a legend, not a scale, and reads as clutter
|
|
1010
1033
|
beside a number set at 36px.
|
|
1011
1034
|
*/}
|
|
1012
|
-
{
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
className="items-center justify-center"
|
|
1017
|
-
>
|
|
1035
|
+
<View style={{ width: window.start * TICK_SPACING }} />
|
|
1036
|
+
{times.slice(window.start, window.end).map((_, offset) => {
|
|
1037
|
+
const i = window.start + offset;
|
|
1038
|
+
return (
|
|
1018
1039
|
<View
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1040
|
+
key={i}
|
|
1041
|
+
style={{ width: TICK_SPACING }}
|
|
1042
|
+
className="items-center justify-center"
|
|
1043
|
+
>
|
|
1044
|
+
<View
|
|
1045
|
+
className={cn(
|
|
1046
|
+
'w-0.5 rounded-full',
|
|
1047
|
+
// Taller and darker on the hour, so the scale reads as hours
|
|
1048
|
+
// rather than as an undifferentiated comb.
|
|
1049
|
+
i % stepsPerHour === 0 ? 'h-7 bg-muted-foreground' : 'h-4 bg-border'
|
|
1050
|
+
)}
|
|
1051
|
+
/>
|
|
1052
|
+
</View>
|
|
1053
|
+
);
|
|
1054
|
+
})}
|
|
1055
|
+
<View style={{ width: (times.length - window.end) * TICK_SPACING }} />
|
|
1028
1056
|
</ScrollView>
|
|
1029
1057
|
|
|
1030
1058
|
{/* Over the scale rather than in it: the indicator marks the centre of
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Maximum tick cells mounted by the ruler, regardless of the minute step. */
|
|
2
|
+
export const RULER_WINDOW_SIZE = 121;
|
|
3
|
+
|
|
4
|
+
const WINDOW_SHIFT = 40;
|
|
5
|
+
|
|
6
|
+
export interface RulerWindow {
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A chunked, overscanned slice around the visible tick. It moves only once per
|
|
13
|
+
* WINDOW_SHIFT ticks, leaving at least forty ticks ahead of a normal scroll.
|
|
14
|
+
*/
|
|
15
|
+
export function rulerWindow(itemCount: number, visibleIndex: number): RulerWindow {
|
|
16
|
+
const count = Math.max(0, Math.floor(itemCount));
|
|
17
|
+
if (count <= RULER_WINDOW_SIZE) return { start: 0, end: count };
|
|
18
|
+
|
|
19
|
+
const index = Math.min(Math.max(Math.round(visibleIndex), 0), count - 1);
|
|
20
|
+
const chunk = Math.floor(index / WINDOW_SHIFT) * WINDOW_SHIFT;
|
|
21
|
+
const before = Math.floor((RULER_WINDOW_SIZE - WINDOW_SHIFT) / 2);
|
|
22
|
+
const start = Math.min(Math.max(chunk - before, 0), count - RULER_WINDOW_SIZE);
|
|
23
|
+
return { start, end: start + RULER_WINDOW_SIZE };
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type BreakpointDefinition = Readonly<Record<string, number>>;
|
|
2
|
+
|
|
3
|
+
/** Validate declaration order once, when an app creates its responsive contract. */
|
|
4
|
+
export function breakpointEntries<const T extends BreakpointDefinition>(definition: T) {
|
|
5
|
+
const entries = Object.entries(definition) as [keyof T & string, number][];
|
|
6
|
+
if (!entries.length) throw new Error('Breakpoints must declare at least one threshold.');
|
|
7
|
+
let previous = -1;
|
|
8
|
+
for (const [name, threshold] of entries) {
|
|
9
|
+
if (!name || !Number.isFinite(threshold) || threshold < 0) {
|
|
10
|
+
throw new Error(`Breakpoint "${name}" must have a finite, non-negative threshold.`);
|
|
11
|
+
}
|
|
12
|
+
if (threshold <= previous) {
|
|
13
|
+
throw new Error('Breakpoint thresholds must be strictly ascending in declaration order.');
|
|
14
|
+
}
|
|
15
|
+
previous = threshold;
|
|
16
|
+
}
|
|
17
|
+
return entries;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Largest satisfied name, with `base` as the deterministic below-range fallback. */
|
|
21
|
+
export function breakpointAt<Name extends string>(
|
|
22
|
+
entries: readonly (readonly [Name, number])[],
|
|
23
|
+
width: number
|
|
24
|
+
): Name | 'base' {
|
|
25
|
+
let current: Name | 'base' = 'base';
|
|
26
|
+
for (const [name, threshold] of entries) {
|
|
27
|
+
if (width < threshold) break;
|
|
28
|
+
current = name;
|
|
29
|
+
}
|
|
30
|
+
return current;
|
|
31
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -10,8 +10,12 @@ export {
|
|
|
10
10
|
} from './use-disclosure';
|
|
11
11
|
export {
|
|
12
12
|
useBreakpoint,
|
|
13
|
+
createBreakpoints,
|
|
13
14
|
BREAKPOINTS,
|
|
14
15
|
type Breakpoint,
|
|
16
|
+
type BreakpointContract,
|
|
17
|
+
type BreakpointProviderProps,
|
|
18
|
+
type BreakpointResult,
|
|
15
19
|
type UseBreakpointResult,
|
|
16
20
|
} from './use-breakpoint';
|
|
17
21
|
export { useKeyboard, type UseKeyboardResult } from './use-keyboard';
|