panelui-native 0.9.0 → 0.12.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 +5 -3
- package/lib/module/components/bottom-sheet/index.js +38 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +19 -1
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/frame/index.js +128 -15
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/input/index.js +107 -15
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/popover/index.js +22 -11
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/progress/index.js +52 -2
- package/lib/module/components/progress/index.js.map +1 -1
- package/lib/module/components/radio-group/index.js +34 -7
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/section-rail/index.js +355 -0
- package/lib/module/components/section-rail/index.js.map +1 -0
- package/lib/module/components/select/index.js +245 -56
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/slider/index.js +355 -0
- package/lib/module/components/slider/index.js.map +1 -0
- package/lib/module/components/switch/index.js +10 -1
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/tabs/index.js +186 -16
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/toggle-button/index.js +297 -0
- package/lib/module/components/toggle-button/index.js.map +1 -0
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +57 -8
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/hooks/use-scroll-sections.js +89 -0
- package/lib/module/hooks/use-scroll-sections.js.map +1 -0
- package/lib/module/index.js +4 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/primitives/keyboard-avoider.js +2 -0
- package/lib/module/primitives/keyboard-avoider.js.map +1 -1
- package/lib/module/utils/haptics.js +57 -0
- package/lib/module/utils/haptics.js.map +1 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +9 -4
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/frame/index.d.ts +47 -7
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input/index.d.ts +113 -2
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/popover/index.d.ts +7 -1
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress/index.d.ts +33 -0
- package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
- package/lib/typescript/src/components/radio-group/index.d.ts +7 -0
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/section-rail/index.d.ts +115 -0
- package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts +46 -7
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts +203 -0
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -0
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tabs/index.d.ts +46 -4
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/toggle-button/index.d.ts +244 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +1 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +10 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts +31 -0
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +16 -1
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts +8 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
- package/lib/typescript/src/utils/haptics.d.ts +12 -0
- package/lib/typescript/src/utils/haptics.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/components/bottom-sheet/index.tsx +38 -2
- package/src/components/button/index.tsx +26 -5
- package/src/components/frame/index.tsx +175 -29
- package/src/components/input/index.tsx +122 -16
- package/src/components/popover/index.tsx +49 -24
- package/src/components/progress/index.tsx +81 -1
- package/src/components/radio-group/index.tsx +55 -8
- package/src/components/section-rail/index.tsx +459 -0
- package/src/components/select/index.tsx +265 -52
- package/src/components/slider/index.tsx +371 -0
- package/src/components/switch/index.tsx +8 -1
- package/src/components/tabs/index.tsx +208 -28
- package/src/components/toggle-button/index.tsx +374 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/use-keyboard-avoidance.ts +73 -7
- package/src/hooks/use-scroll-sections.ts +127 -0
- package/src/index.ts +37 -6
- package/src/native/index.ts +13 -1
- package/src/primitives/keyboard-avoider.tsx +9 -1
- package/src/utils/haptics.ts +63 -0
- package/theme.css +6 -2
- package/lib/module/components/inline-select/index.js +0 -284
- package/lib/module/components/inline-select/index.js.map +0 -1
- package/lib/typescript/src/components/inline-select/index.d.ts +0 -48
- package/lib/typescript/src/components/inline-select/index.d.ts.map +0 -1
- package/src/components/inline-select/index.tsx +0 -332
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slider — a value picker driven by a single thumb.
|
|
3
|
+
*
|
|
4
|
+
* The thumb is a pill exactly as tall as the track rather than a disc floating
|
|
5
|
+
* over it. That is a deliberate shape choice and it is also the robust one: a
|
|
6
|
+
* knob taller than its track has to escape the track's own bounds to be drawn
|
|
7
|
+
* whole, which puts the visual design at the mercy of a clipping rule. Here the
|
|
8
|
+
* two are the same height, so there is nothing to clip and nothing to escape.
|
|
9
|
+
*
|
|
10
|
+
* Inside the pill sits a smaller knob that shrinks while dragging — the press
|
|
11
|
+
* feedback lives on the part you are looking at, not on the whole control.
|
|
12
|
+
*
|
|
13
|
+
* Position and fill width are animated on the UI thread; dragging never
|
|
14
|
+
* round-trips through React, and the picked value bridges back to JS on change
|
|
15
|
+
* and on release.
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <Slider label="Volume" showValue defaultValue={30} />
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Works controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
|
|
22
|
+
*/
|
|
23
|
+
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
|
24
|
+
import {
|
|
25
|
+
View,
|
|
26
|
+
type AccessibilityActionEvent,
|
|
27
|
+
type LayoutChangeEvent,
|
|
28
|
+
} from 'react-native';
|
|
29
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
30
|
+
import Animated, {
|
|
31
|
+
runOnJS,
|
|
32
|
+
useAnimatedStyle,
|
|
33
|
+
useSharedValue,
|
|
34
|
+
withSpring,
|
|
35
|
+
} from 'react-native-reanimated';
|
|
36
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
37
|
+
import { getNativeUI } from '../../native';
|
|
38
|
+
import { Text } from '../../primitives/text';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Springs the thumb onto its resting step after a drag or a track tap. Drags
|
|
42
|
+
* themselves are followed 1:1 with no spring, so the knob stays under the
|
|
43
|
+
* finger; the spring only settles the final snap.
|
|
44
|
+
*/
|
|
45
|
+
const SPRING = { damping: 18, stiffness: 220, mass: 0.6 } as const;
|
|
46
|
+
/** Settles the knob between its idle and dragging sizes. */
|
|
47
|
+
const KNOB_SPRING = { damping: 15, stiffness: 200, mass: 0.5 } as const;
|
|
48
|
+
/** How far the knob shrinks while the thumb is held. */
|
|
49
|
+
const KNOB_PRESSED_SCALE = 0.86;
|
|
50
|
+
/** Grab radius around the thumb — the pill is small, the target should not be. */
|
|
51
|
+
const HIT_SLOP = 16;
|
|
52
|
+
|
|
53
|
+
const sliderVariants = tv({
|
|
54
|
+
slots: {
|
|
55
|
+
root: 'w-full gap-2',
|
|
56
|
+
header: 'w-full flex-row items-center justify-between gap-3',
|
|
57
|
+
label: 'text-sm font-medium text-foreground',
|
|
58
|
+
value: 'text-sm text-muted-foreground',
|
|
59
|
+
// The track is the full height of the control, so the thumb has somewhere
|
|
60
|
+
// to sit rather than somewhere to stick out of.
|
|
61
|
+
track: 'w-full justify-center rounded-full bg-muted',
|
|
62
|
+
fill: 'absolute bottom-0 left-0 top-0 rounded-full',
|
|
63
|
+
thumb: 'absolute left-0 rounded-full p-0.5',
|
|
64
|
+
// `background`, not a per-colour on-token: the status foregrounds are the
|
|
65
|
+
// darker text hues meant for soft fills, so a green-700 knob on a green-500
|
|
66
|
+
// pill would barely show. The page background reads against every fill in
|
|
67
|
+
// both themes.
|
|
68
|
+
knob: 'flex-1 rounded-full bg-background shadow-sm',
|
|
69
|
+
},
|
|
70
|
+
variants: {
|
|
71
|
+
color: {
|
|
72
|
+
primary: { fill: 'bg-primary', thumb: 'bg-primary' },
|
|
73
|
+
success: { fill: 'bg-success', thumb: 'bg-success' },
|
|
74
|
+
warning: { fill: 'bg-warning', thumb: 'bg-warning' },
|
|
75
|
+
destructive: { fill: 'bg-destructive', thumb: 'bg-destructive' },
|
|
76
|
+
info: { fill: 'bg-info', thumb: 'bg-info' },
|
|
77
|
+
},
|
|
78
|
+
size: {
|
|
79
|
+
sm: { track: 'h-4', thumb: 'h-4' },
|
|
80
|
+
md: { track: 'h-5', thumb: 'h-5' },
|
|
81
|
+
lg: { track: 'h-6', thumb: 'h-6' },
|
|
82
|
+
},
|
|
83
|
+
disabled: {
|
|
84
|
+
true: { root: 'opacity-50' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
defaultVariants: {
|
|
88
|
+
color: 'primary',
|
|
89
|
+
size: 'md',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/** Thumb width per size. Wider than it is tall, so the pill reads as a grip. */
|
|
94
|
+
const THUMB_WIDTH: Record<'sm' | 'md' | 'lg', number> = { sm: 24, md: 28, lg: 32 };
|
|
95
|
+
|
|
96
|
+
/** Row height reserved for the platform's own slider. */
|
|
97
|
+
const NATIVE_HEIGHT = 32;
|
|
98
|
+
|
|
99
|
+
type SliderVariantProps = VariantProps<typeof sliderVariants>;
|
|
100
|
+
|
|
101
|
+
export interface SliderProps extends Omit<SliderVariantProps, 'disabled'> {
|
|
102
|
+
className?: string;
|
|
103
|
+
/** Controlled value. Leave unset and pass `defaultValue` to run uncontrolled. */
|
|
104
|
+
value?: number;
|
|
105
|
+
/** Starting value when uncontrolled. */
|
|
106
|
+
defaultValue?: number;
|
|
107
|
+
/** Lower bound. */
|
|
108
|
+
min?: number;
|
|
109
|
+
/** Upper bound. */
|
|
110
|
+
max?: number;
|
|
111
|
+
/** Snap granularity. The value is always a multiple of `step` from `min`. */
|
|
112
|
+
step?: number;
|
|
113
|
+
/** Fires on every change while dragging — cheap updates only. */
|
|
114
|
+
onValueChange?: (value: number) => void;
|
|
115
|
+
/** Fires once when the gesture ends — the place for expensive side effects. */
|
|
116
|
+
onValueCommit?: (value: number) => void;
|
|
117
|
+
disabled?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Render the platform's own slider instead of this one. Requires the
|
|
120
|
+
* optional `@expo/ui` package; without it this prop does nothing.
|
|
121
|
+
*
|
|
122
|
+
* **Theme tokens do not apply** — the platform draws the control, so
|
|
123
|
+
* `color`, `size` and the slot classNames are ignored. `label` and
|
|
124
|
+
* `showValue` still render the caption row above it, since that is ours.
|
|
125
|
+
*/
|
|
126
|
+
native?: boolean;
|
|
127
|
+
/** Caption above the track. Also becomes the accessibility label. */
|
|
128
|
+
label?: string;
|
|
129
|
+
/** Show the current value on the caption row, opposite the label. */
|
|
130
|
+
showValue?: boolean;
|
|
131
|
+
/** Format the shown value. Defaults to the rounded number. */
|
|
132
|
+
formatValue?: (value: number) => string;
|
|
133
|
+
/** Extra classes for the caption row. */
|
|
134
|
+
headerClassName?: string;
|
|
135
|
+
/** Extra classes for the unfilled track. */
|
|
136
|
+
trackClassName?: string;
|
|
137
|
+
/** Extra classes for the filled portion. */
|
|
138
|
+
fillClassName?: string;
|
|
139
|
+
/** Extra classes for the draggable thumb. */
|
|
140
|
+
thumbClassName?: string;
|
|
141
|
+
/** Extra classes for the knob inside the thumb. */
|
|
142
|
+
knobClassName?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function clampJS(value: number, min: number, max: number) {
|
|
146
|
+
return Math.min(Math.max(value, min), max);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function snap(value: number, min: number, max: number, step: number) {
|
|
150
|
+
if (step <= 0) return clampJS(value, min, max);
|
|
151
|
+
const stepped = Math.round((value - min) / step) * step + min;
|
|
152
|
+
return clampJS(stepped, min, max);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const Slider = forwardRef<View, SliderProps>(
|
|
156
|
+
(
|
|
157
|
+
{
|
|
158
|
+
className,
|
|
159
|
+
headerClassName,
|
|
160
|
+
trackClassName,
|
|
161
|
+
fillClassName,
|
|
162
|
+
thumbClassName,
|
|
163
|
+
knobClassName,
|
|
164
|
+
value: valueProp,
|
|
165
|
+
defaultValue = 0,
|
|
166
|
+
min = 0,
|
|
167
|
+
max = 100,
|
|
168
|
+
step = 1,
|
|
169
|
+
onValueChange,
|
|
170
|
+
onValueCommit,
|
|
171
|
+
disabled = false,
|
|
172
|
+
native,
|
|
173
|
+
label,
|
|
174
|
+
showValue = false,
|
|
175
|
+
formatValue,
|
|
176
|
+
color = 'primary',
|
|
177
|
+
size = 'md',
|
|
178
|
+
},
|
|
179
|
+
ref
|
|
180
|
+
) => {
|
|
181
|
+
const isControlled = valueProp !== undefined;
|
|
182
|
+
const [internal, setInternal] = useState(defaultValue);
|
|
183
|
+
const value = clampJS(isControlled ? valueProp! : internal, min, max);
|
|
184
|
+
|
|
185
|
+
const slots = sliderVariants({ color, size, disabled });
|
|
186
|
+
const thumbWidth = THUMB_WIDTH[size ?? 'md'];
|
|
187
|
+
const nativeUI = native ? getNativeUI() : null;
|
|
188
|
+
|
|
189
|
+
// Measured on layout; the thumb travels across (trackWidth - thumbWidth) so
|
|
190
|
+
// it stays inside the track at both ends.
|
|
191
|
+
const trackWidth = useSharedValue(0);
|
|
192
|
+
// Fraction 0..1 that drives the fill and the thumb, animated on the UI thread.
|
|
193
|
+
const progress = useSharedValue(max > min ? (value - min) / (max - min) : 0);
|
|
194
|
+
const startProgress = useSharedValue(0);
|
|
195
|
+
const pressed = useSharedValue(0);
|
|
196
|
+
|
|
197
|
+
// The change handler runs on JS; keep the latest props reachable from the
|
|
198
|
+
// worklet callback without re-creating the gesture on every render.
|
|
199
|
+
const changeRef = useRef(onValueChange);
|
|
200
|
+
changeRef.current = onValueChange;
|
|
201
|
+
const commitRef = useRef(onValueCommit);
|
|
202
|
+
commitRef.current = onValueCommit;
|
|
203
|
+
|
|
204
|
+
const emitChange = useCallback(
|
|
205
|
+
(next: number) => {
|
|
206
|
+
if (!isControlled) setInternal(next);
|
|
207
|
+
changeRef.current?.(next);
|
|
208
|
+
},
|
|
209
|
+
[isControlled]
|
|
210
|
+
);
|
|
211
|
+
const emitCommit = useCallback((next: number) => {
|
|
212
|
+
commitRef.current?.(next);
|
|
213
|
+
}, []);
|
|
214
|
+
|
|
215
|
+
// Keep the animation in step with a controlled value that changes elsewhere.
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
const next = max > min ? (value - min) / (max - min) : 0;
|
|
218
|
+
progress.value = withSpring(next, SPRING);
|
|
219
|
+
}, [value, min, max, progress]);
|
|
220
|
+
|
|
221
|
+
const onLayout = (event: LayoutChangeEvent) => {
|
|
222
|
+
trackWidth.value = event.nativeEvent.layout.width;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const commitFromProgress = useCallback(
|
|
226
|
+
(p: number, commit: boolean) => {
|
|
227
|
+
const raw = min + p * (max - min);
|
|
228
|
+
const snapped = snap(raw, min, max, step);
|
|
229
|
+
emitChange(snapped);
|
|
230
|
+
if (commit) emitCommit(snapped);
|
|
231
|
+
},
|
|
232
|
+
[min, max, step, emitChange, emitCommit]
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const pan = Gesture.Pan()
|
|
236
|
+
.enabled(!disabled)
|
|
237
|
+
.hitSlop(HIT_SLOP)
|
|
238
|
+
.onBegin(() => {
|
|
239
|
+
startProgress.value = progress.value;
|
|
240
|
+
pressed.value = withSpring(1, KNOB_SPRING);
|
|
241
|
+
})
|
|
242
|
+
.onUpdate((event) => {
|
|
243
|
+
const travel = Math.max(trackWidth.value - thumbWidth, 1);
|
|
244
|
+
const delta = event.translationX / travel;
|
|
245
|
+
const next = Math.min(Math.max(startProgress.value + delta, 0), 1);
|
|
246
|
+
progress.value = next;
|
|
247
|
+
runOnJS(commitFromProgress)(next, false);
|
|
248
|
+
})
|
|
249
|
+
.onFinalize(() => {
|
|
250
|
+
pressed.value = withSpring(0, KNOB_SPRING);
|
|
251
|
+
runOnJS(commitFromProgress)(progress.value, true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const tap = Gesture.Tap()
|
|
255
|
+
.enabled(!disabled)
|
|
256
|
+
.maxDuration(250)
|
|
257
|
+
.onEnd((event) => {
|
|
258
|
+
const travel = Math.max(trackWidth.value - thumbWidth, 1);
|
|
259
|
+
const next = Math.min(Math.max((event.x - thumbWidth / 2) / travel, 0), 1);
|
|
260
|
+
progress.value = withSpring(next, SPRING);
|
|
261
|
+
runOnJS(commitFromProgress)(next, true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const gesture = Gesture.Race(pan, tap);
|
|
265
|
+
|
|
266
|
+
// The fill runs under the thumb rather than up to it, so the two never show
|
|
267
|
+
// a seam between them as the thumb moves.
|
|
268
|
+
const fillStyle = useAnimatedStyle(() => {
|
|
269
|
+
const travel = Math.max(trackWidth.value - thumbWidth, 0);
|
|
270
|
+
return { width: progress.value * travel + thumbWidth };
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const thumbStyle = useAnimatedStyle(() => {
|
|
274
|
+
const travel = Math.max(trackWidth.value - thumbWidth, 0);
|
|
275
|
+
return { transform: [{ translateX: progress.value * travel }] };
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const knobStyle = useAnimatedStyle(() => ({
|
|
279
|
+
transform: [{ scale: 1 - pressed.value * (1 - KNOB_PRESSED_SCALE) }],
|
|
280
|
+
}));
|
|
281
|
+
|
|
282
|
+
// VoiceOver / TalkBack increment and decrement move by a single step.
|
|
283
|
+
const nudge = (dir: 1 | -1) => {
|
|
284
|
+
const next = snap(value + dir * (step || 1), min, max, step);
|
|
285
|
+
if (next === value) return;
|
|
286
|
+
progress.value = withSpring(max > min ? (next - min) / (max - min) : 0, SPRING);
|
|
287
|
+
emitChange(next);
|
|
288
|
+
emitCommit(next);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const onAccessibilityAction = (event: AccessibilityActionEvent) => {
|
|
292
|
+
if (event.nativeEvent.actionName === 'increment') nudge(1);
|
|
293
|
+
else if (event.nativeEvent.actionName === 'decrement') nudge(-1);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const shownValue = formatValue ? formatValue(value) : String(Math.round(value));
|
|
297
|
+
|
|
298
|
+
const header =
|
|
299
|
+
label || showValue ? (
|
|
300
|
+
<View className={slots.header({ className: headerClassName })}>
|
|
301
|
+
{label ? <Text className={slots.label()}>{label}</Text> : <View />}
|
|
302
|
+
{showValue ? <Text className={slots.value()}>{shownValue}</Text> : null}
|
|
303
|
+
</View>
|
|
304
|
+
) : null;
|
|
305
|
+
|
|
306
|
+
if (nativeUI) {
|
|
307
|
+
const { Host, Slider: NativeSlider } = nativeUI;
|
|
308
|
+
return (
|
|
309
|
+
<View ref={ref} className={slots.root({ className })}>
|
|
310
|
+
{header}
|
|
311
|
+
{/* Told its height rather than left to measure the platform's own
|
|
312
|
+
content, which arrives a frame late — a host that has to work its
|
|
313
|
+
size out renders at nothing and then jumps. */}
|
|
314
|
+
<Host style={{ height: NATIVE_HEIGHT }}>
|
|
315
|
+
<NativeSlider
|
|
316
|
+
value={value}
|
|
317
|
+
onValueChange={(next: number) => {
|
|
318
|
+
emitChange(snap(next, min, max, step));
|
|
319
|
+
}}
|
|
320
|
+
min={min}
|
|
321
|
+
max={max}
|
|
322
|
+
step={step}
|
|
323
|
+
disabled={disabled}
|
|
324
|
+
/>
|
|
325
|
+
</Host>
|
|
326
|
+
</View>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<View ref={ref} className={slots.root({ className })} collapsable={false}>
|
|
332
|
+
{header}
|
|
333
|
+
|
|
334
|
+
<GestureDetector gesture={gesture}>
|
|
335
|
+
<View
|
|
336
|
+
className={slots.track({ className: trackClassName })}
|
|
337
|
+
onLayout={onLayout}
|
|
338
|
+
>
|
|
339
|
+
<Animated.View
|
|
340
|
+
style={fillStyle}
|
|
341
|
+
className={slots.fill({ className: fillClassName })}
|
|
342
|
+
/>
|
|
343
|
+
<Animated.View
|
|
344
|
+
style={[thumbStyle, { width: thumbWidth }]}
|
|
345
|
+
className={slots.thumb({ className: thumbClassName })}
|
|
346
|
+
accessible
|
|
347
|
+
accessibilityRole="adjustable"
|
|
348
|
+
accessibilityLabel={label}
|
|
349
|
+
accessibilityState={{ disabled }}
|
|
350
|
+
accessibilityValue={{
|
|
351
|
+
min,
|
|
352
|
+
max,
|
|
353
|
+
now: Math.round(value),
|
|
354
|
+
text: shownValue,
|
|
355
|
+
}}
|
|
356
|
+
accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
|
|
357
|
+
onAccessibilityAction={onAccessibilityAction}
|
|
358
|
+
>
|
|
359
|
+
<Animated.View
|
|
360
|
+
style={knobStyle}
|
|
361
|
+
className={slots.knob({ className: knobClassName })}
|
|
362
|
+
/>
|
|
363
|
+
</Animated.View>
|
|
364
|
+
</View>
|
|
365
|
+
</GestureDetector>
|
|
366
|
+
</View>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
Slider.displayName = 'Slider';
|
|
@@ -33,6 +33,13 @@ const switchVariants = tv({
|
|
|
33
33
|
|
|
34
34
|
const TRAVEL: Record<'sm' | 'md', number> = { sm: 16, md: 20 };
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Row height given to the native host. It is told its size rather than asked
|
|
38
|
+
* to measure the platform's own content, which arrives a frame late and leaves
|
|
39
|
+
* the control collapsed until something forces a second pass.
|
|
40
|
+
*/
|
|
41
|
+
const NATIVE_HEIGHT = 32;
|
|
42
|
+
|
|
36
43
|
export interface SwitchProps extends VariantProps<typeof switchVariants> {
|
|
37
44
|
className?: string;
|
|
38
45
|
value: boolean;
|
|
@@ -77,7 +84,7 @@ export const Switch = forwardRef<View, SwitchProps>(
|
|
|
77
84
|
if (nativeUI) {
|
|
78
85
|
const { Host, Switch: NativeSwitch } = nativeUI;
|
|
79
86
|
return (
|
|
80
|
-
<Host
|
|
87
|
+
<Host style={{ height: NATIVE_HEIGHT }}>
|
|
81
88
|
<NativeSwitch
|
|
82
89
|
value={value}
|
|
83
90
|
onValueChange={(next: boolean) => onValueChange?.(next)}
|