@symbiote-native/slider 5.0.3 → 5.2.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 +24 -7
- package/build/angular/slider/index.android.js +9 -1
- package/build/angular/slider/index.ios.js +10 -2
- package/build/angular/slider/shared.d.ts +6 -1
- package/build/angular/slider/shared.js +23 -1
- package/build/core/index.d.ts +2 -2
- package/build/core/render-slider.js +5 -1
- package/build/core/render-steps-indicator.js +13 -4
- package/build/core/slider-props.js +4 -6
- package/build/react/slider/index.ios.js +1 -1
- package/build/react/slider/shared.js +11 -4
- package/build/register/index.js +5 -2
- package/build/solid/index.d.ts +4 -0
- package/build/solid/index.js +8 -0
- package/build/solid/slider/index.android.d.ts +2 -0
- package/build/solid/slider/index.android.js +10 -0
- package/build/solid/slider/index.d.ts +1 -0
- package/build/solid/slider/index.ios.d.ts +2 -0
- package/build/solid/slider/index.ios.js +11 -0
- package/build/solid/slider/index.js +4 -0
- package/build/solid/slider/shared.d.ts +12 -0
- package/build/solid/slider/shared.js +296 -0
- package/build/svelte/index.d.ts +3 -0
- package/build/svelte/index.js +9 -0
- package/build/svelte/slider/index.svelte +326 -0
- package/build/svelte/slider/index.svelte.d.ts +5 -0
- package/build/svelte/slider/slider-platform.android.d.ts +2 -0
- package/build/svelte/slider/slider-platform.android.js +11 -0
- package/build/svelte/slider/slider-platform.d.ts +1 -0
- package/build/svelte/slider/slider-platform.ios.d.ts +2 -0
- package/build/svelte/slider/slider-platform.ios.js +12 -0
- package/build/svelte/slider/slider-platform.js +5 -0
- package/build/svelte/slider/slider-props.d.ts +7 -0
- package/build/svelte/slider/slider-props.js +1 -0
- package/build/vue/slider/index.ios.js +1 -1
- package/build/vue/slider/shared.js +11 -5
- package/build-ngc/angular/slider/index.android.js +18 -4
- package/build-ngc/angular/slider/index.android.js.map +1 -1
- package/build-ngc/angular/slider/index.ios.js +19 -5
- package/build-ngc/angular/slider/index.ios.js.map +1 -1
- package/build-ngc/angular/slider/shared.d.ts +6 -1
- package/build-ngc/angular/slider/shared.js +26 -4
- package/build-ngc/angular/slider/shared.js.map +1 -1
- package/build-ngc/core/index.d.ts +2 -2
- package/build-ngc/core/index.js.map +1 -1
- package/build-ngc/core/render-slider.js +5 -1
- package/build-ngc/core/render-slider.js.map +1 -1
- package/build-ngc/core/render-steps-indicator.js +13 -4
- package/build-ngc/core/render-steps-indicator.js.map +1 -1
- package/build-ngc/core/slider-props.js +4 -6
- package/build-ngc/core/slider-props.js.map +1 -1
- package/build-ngc/core/slider-state.js.map +1 -1
- package/build-ngc/register/index.js +5 -2
- package/build-ngc/register/index.js.map +1 -1
- package/package.json +48 -15
- package/src/angular/slider/index.android.ts +9 -1
- package/src/angular/slider/index.ios.ts +13 -2
- package/src/angular/slider/shared.ts +55 -10
- package/src/core/index.ts +9 -2
- package/src/core/render-slider.ts +24 -6
- package/src/core/render-steps-indicator.ts +38 -10
- package/src/core/slider-props.ts +17 -17
- package/src/core/slider-state.ts +16 -5
- package/src/react/slider/index.ios.ts +4 -1
- package/src/react/slider/shared.ts +66 -15
- package/src/register/index.ts +9 -2
- package/src/solid/index.ts +12 -0
- package/src/solid/slider/index.android.ts +15 -0
- package/src/solid/slider/index.ios.ts +19 -0
- package/src/solid/slider/index.ts +5 -0
- package/src/solid/slider/shared.ts +468 -0
- package/src/svelte/index.ts +12 -0
- package/src/svelte/slider/index.svelte +326 -0
- package/src/svelte/slider/slider-platform.android.ts +14 -0
- package/src/svelte/slider/slider-platform.ios.ts +18 -0
- package/src/svelte/slider/slider-platform.ts +5 -0
- package/src/svelte/slider/slider-props.ts +17 -0
- package/src/vue/slider/index.ios.ts +4 -1
- package/src/vue/slider/shared.ts +72 -19
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// Slider, the Solid lifecycle half. The logic (value/limit/disabled folds, the step-option layout)
|
|
2
|
+
// and the native render live in @symbiote-native/slider core, shared verbatim with React, Vue,
|
|
3
|
+
// Svelte and Angular; Solid supplies only the reactivity — a signal for the value native last
|
|
4
|
+
// reported (to mark the active step), a signal for the measured width — plus the descriptor bridge.
|
|
5
|
+
// The native RNCSlider carries no symbiote metadata: the engine derives its events and color/image
|
|
6
|
+
// processors from the library's ViewConfig, registered by ../register (the package barrel pulls it,
|
|
7
|
+
// NOT this module, so the tests stay free of the third-party spec). Like every other adapter here,
|
|
8
|
+
// this NEVER imports the library's React Slider component — it calls hooks off the React dispatcher
|
|
9
|
+
// and would crash under a non-React adapter.
|
|
10
|
+
//
|
|
11
|
+
// NOTHING here destructures `props`. Solid props are getters and a component body runs ONCE, so a
|
|
12
|
+
// destructure would freeze the slider at its mount-time values; every read sits inside an accessor.
|
|
13
|
+
//
|
|
14
|
+
// WHY THE TREE IS ASSEMBLED BY HAND INSTEAD OF THROUGH renderSlider(). Solid has no reconciler
|
|
15
|
+
// between what this file returns and the host nodes, so a tree whose SHAPE changes has to be
|
|
16
|
+
// rebuilt, and rebuilding the native slider would throw away the node identity its own thumb grip
|
|
17
|
+
// and every imperative command key on. renderSlider's Descriptor carries the step overlay as a
|
|
18
|
+
// child, i.e. its child count flips with `renderStepNumber` — which descriptorToSolid refuses
|
|
19
|
+
// (.claude/rules/solid-descriptor-bridge.md §2). So the wrapper is built once here, the native leaf
|
|
20
|
+
// is bridged once via renderSliderNative (the split core exposes for exactly this: React takes it
|
|
21
|
+
// too on its custom-marker path), and only the OVERLAY sits behind a rebuild boundary. The result
|
|
22
|
+
// is a stable native node across every overlay change — where React remounts it, since the wrapper's
|
|
23
|
+
// unkeyed children shift by one.
|
|
24
|
+
//
|
|
25
|
+
// The hand-written createElement / spread / insert calls below are what compiled JSX emits; this
|
|
26
|
+
// package cannot use JSX at all, because its tsconfig is shared with the React/Vue/Angular entries
|
|
27
|
+
// and TypeScript has one `jsx` setting per program. descriptor-to-solid.ts drives the renderer the
|
|
28
|
+
// same way for the same reason.
|
|
29
|
+
// createComponent comes from solid-js, not the renderer's re-export: the renderer's is typed to
|
|
30
|
+
// return its own host-node union, which cannot hold what a component returns (an Image is
|
|
31
|
+
// JSX.Element). Same function either way.
|
|
32
|
+
import { createComponent, createEffect, createMemo, createSignal, untrack, } from 'solid-js';
|
|
33
|
+
import { descriptorToSolid, Image, } from '@symbiote-native/solid';
|
|
34
|
+
import { createElement, createTextNode, insert, insertNode, setProp, spread, } from '@symbiote-native/solid/renderer';
|
|
35
|
+
import { resolveAccessibilityProps } from '@symbiote-native/components';
|
|
36
|
+
import { dlog, isSymbioteNode, } from '@symbiote-native/engine';
|
|
37
|
+
import { sanitizeSliderValue, resolveSliderDisabled, resolveSliderAccessibilityState, resolveSliderLowerLimit, resolveSliderUpperLimit, valueFromSliderEvent, shouldRenderStepsIndicator, resolveThumbTintColor, shouldPassNativeThumbImage, isInvalidLimitConfig, computeStepOptions, orderStepOptions, stepNumberFontSize, renderSliderNative, resolveSliderWrapperStyle, resolveStepsContainerStyle, renderStepsIndicator, STEP_INDICATOR_ELEMENT_STYLE, TRACK_MARK_CONTAINER_STYLE, THUMB_IMAGE_CONTAINER_STYLE, THUMB_IMAGE_STYLE, STEP_NUMBER_CONTAINER_STYLE, SLIDER_DEFAULT_MINIMUM_VALUE, SLIDER_DEFAULT_MAXIMUM_VALUE, SLIDER_DEFAULT_STEP, SLIDER_ON_CHANGE, SLIDER_ON_VALUE_CHANGE, SLIDER_ON_SLIDING_START, SLIDER_ON_SLIDING_COMPLETE, SLIDER_ON_ACCESSIBILITY_ACTION, } from '../../core/index.js';
|
|
38
|
+
// Read by the component itself; everything else (the track tints and images, thumbSize, tapToSeek,
|
|
39
|
+
// vertical, testID, the accessibility surface, a caller's `ref`) forwards onto the native leaf. The
|
|
40
|
+
// four JS callbacks MUST be stripped — a function prop reaching Fabric crashes Android's
|
|
41
|
+
// folly::dynamic serializer — and are re-supplied as the native event handlers in `passthrough`.
|
|
42
|
+
const HANDLED_PROPS = [
|
|
43
|
+
'value',
|
|
44
|
+
'minimumValue',
|
|
45
|
+
'maximumValue',
|
|
46
|
+
'step',
|
|
47
|
+
'lowerLimit',
|
|
48
|
+
'upperLimit',
|
|
49
|
+
'disabled',
|
|
50
|
+
'inverted',
|
|
51
|
+
'thumbTintColor',
|
|
52
|
+
'thumbImage',
|
|
53
|
+
'accessibilityState',
|
|
54
|
+
'renderStepNumber',
|
|
55
|
+
'style',
|
|
56
|
+
'class',
|
|
57
|
+
'StepMarker',
|
|
58
|
+
'onValueChange',
|
|
59
|
+
'onSlidingStart',
|
|
60
|
+
'onSlidingComplete',
|
|
61
|
+
'onAccessibilityAction',
|
|
62
|
+
];
|
|
63
|
+
function forwardProps(resolved) {
|
|
64
|
+
const forwarded = { ...resolved };
|
|
65
|
+
for (const key of HANDLED_PROPS)
|
|
66
|
+
delete forwarded[key];
|
|
67
|
+
return forwarded;
|
|
68
|
+
}
|
|
69
|
+
function isRecord(value) {
|
|
70
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
71
|
+
}
|
|
72
|
+
// Narrowing, not defensive: the renderer's createElement is typed over its IHostNode union (which
|
|
73
|
+
// includes the surface), while everything below needs a real host node. Same guard descriptorToSolid
|
|
74
|
+
// opens with.
|
|
75
|
+
function hostElement(tag) {
|
|
76
|
+
const node = createElement(tag);
|
|
77
|
+
if (!isSymbioteNode(node)) {
|
|
78
|
+
throw new Error(`Slider: ${tag} did not create a host node`);
|
|
79
|
+
}
|
|
80
|
+
return node;
|
|
81
|
+
}
|
|
82
|
+
// Solid core's own render-prop shape, line for line with `<Show>`: untrack + accessor, with an arity
|
|
83
|
+
// guard because `typeof fn === 'function'` cannot separate a render prop from the bare zero-argument
|
|
84
|
+
// accessor JSX.Element also permits. A 0-arity marker is handed back unread so `insert` wraps it in
|
|
85
|
+
// its own render effect; calling it here would freeze it at its first value.
|
|
86
|
+
function callStepMarker(marker, props) {
|
|
87
|
+
if (marker.length === 0)
|
|
88
|
+
return () => marker(props);
|
|
89
|
+
return untrack(() => marker(props));
|
|
90
|
+
}
|
|
91
|
+
export function createSlider(platform) {
|
|
92
|
+
return function Slider(props) {
|
|
93
|
+
// What native last reported, kept only to mark the active step. NOT the controlled value — the
|
|
94
|
+
// slider is uncontrolled during a drag, native owns the thumb and reports up.
|
|
95
|
+
const [reportedValue, setReportedValue] = createSignal(undefined);
|
|
96
|
+
// The measured wrapper width the step indicator lays out against; 0 until the first layout.
|
|
97
|
+
const [width, setWidth] = createSignal(0);
|
|
98
|
+
const handleValueChange = (event) => {
|
|
99
|
+
const next = valueFromSliderEvent(event);
|
|
100
|
+
if (next === undefined)
|
|
101
|
+
return;
|
|
102
|
+
setReportedValue(next);
|
|
103
|
+
props.onValueChange?.(next);
|
|
104
|
+
};
|
|
105
|
+
const handleSlidingStart = (event) => {
|
|
106
|
+
const next = valueFromSliderEvent(event);
|
|
107
|
+
if (next !== undefined)
|
|
108
|
+
props.onSlidingStart?.(next);
|
|
109
|
+
};
|
|
110
|
+
const handleSlidingComplete = (event) => {
|
|
111
|
+
const next = valueFromSliderEvent(event);
|
|
112
|
+
if (next !== undefined)
|
|
113
|
+
props.onSlidingComplete?.(next);
|
|
114
|
+
};
|
|
115
|
+
const handleAccessibilityAction = (event) => {
|
|
116
|
+
props.onAccessibilityAction?.(event);
|
|
117
|
+
};
|
|
118
|
+
const handleLayout = (event) => {
|
|
119
|
+
const layout = event.nativeEvent.layout;
|
|
120
|
+
if (isRecord(layout) && typeof layout.width === 'number') {
|
|
121
|
+
setWidth(layout.width);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const minimumValue = () => props.minimumValue ?? SLIDER_DEFAULT_MINIMUM_VALUE;
|
|
125
|
+
const maximumValue = () => props.maximumValue ?? SLIDER_DEFAULT_MAXIMUM_VALUE;
|
|
126
|
+
const step = () => props.step ?? SLIDER_DEFAULT_STEP;
|
|
127
|
+
const lowerLimit = () => resolveSliderLowerLimit(props.lowerLimit);
|
|
128
|
+
const upperLimit = () => resolveSliderUpperLimit(props.upperLimit);
|
|
129
|
+
createEffect(() => {
|
|
130
|
+
if (isInvalidLimitConfig(lowerLimit(), upperLimit())) {
|
|
131
|
+
dlog('Slider: lowerLimit must be smaller than upperLimit');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
const hasStepMarker = () => props.StepMarker !== undefined;
|
|
135
|
+
const hasThumbImage = () => props.thumbImage !== undefined;
|
|
136
|
+
const showSteps = () => shouldRenderStepsIndicator(hasStepMarker(), props.renderStepNumber);
|
|
137
|
+
const view = () => {
|
|
138
|
+
// Folded BEFORE the handled props are read, exactly as React folds rawProps before it
|
|
139
|
+
// destructures: `aria-disabled` lands in accessibilityState, and reading the raw prop instead
|
|
140
|
+
// would hand the fold below an undefined and blank the a11y state the alias just set.
|
|
141
|
+
const resolved = resolveAccessibilityProps(props);
|
|
142
|
+
return {
|
|
143
|
+
value: sanitizeSliderValue(props.value),
|
|
144
|
+
minimumValue: minimumValue(),
|
|
145
|
+
maximumValue: maximumValue(),
|
|
146
|
+
step: step(),
|
|
147
|
+
lowerLimit: lowerLimit(),
|
|
148
|
+
upperLimit: upperLimit(),
|
|
149
|
+
disabled: resolveSliderDisabled(props.disabled, resolved.accessibilityState),
|
|
150
|
+
inverted: props.inverted ?? false,
|
|
151
|
+
thumbTintColor: resolveThumbTintColor(props.thumbTintColor, hasStepMarker(), hasThumbImage()),
|
|
152
|
+
// Passed to native raw — the engine runs the image processor derived from RNCSlider's
|
|
153
|
+
// ViewConfig — and only when no custom marker draws its own thumb (matches the library).
|
|
154
|
+
thumbImage: shouldPassNativeThumbImage(hasStepMarker(), hasThumbImage())
|
|
155
|
+
? props.thumbImage
|
|
156
|
+
: undefined,
|
|
157
|
+
accessibilityState: resolveSliderAccessibilityState(props.disabled, resolved.accessibilityState),
|
|
158
|
+
width: width(),
|
|
159
|
+
style: props.style,
|
|
160
|
+
passthrough: {
|
|
161
|
+
...forwardProps(resolved),
|
|
162
|
+
[SLIDER_ON_CHANGE]: handleValueChange,
|
|
163
|
+
[SLIDER_ON_VALUE_CHANGE]: handleValueChange,
|
|
164
|
+
[SLIDER_ON_SLIDING_START]: handleSlidingStart,
|
|
165
|
+
[SLIDER_ON_SLIDING_COMPLETE]: handleSlidingComplete,
|
|
166
|
+
[SLIDER_ON_ACCESSIBILITY_ACTION]: handleAccessibilityAction,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
const options = createMemo(() => computeStepOptions(minimumValue(), maximumValue(), step(), platform.stepResolution));
|
|
171
|
+
const currentValue = createMemo(() => reportedValue() ?? sanitizeSliderValue(props.value) ?? minimumValue());
|
|
172
|
+
// THE REBUILD BOUNDARY (.claude/rules/solid-descriptor-bridge.md §5). Everything read TRACKED
|
|
173
|
+
// here changes the overlay's SHAPE — which overlay kind, how many cells, whether each cell
|
|
174
|
+
// carries a number — and the build itself is untracked so it does not also subscribe to every
|
|
175
|
+
// value/width read inside it. Drop the untrack and one drag rebuilds the whole indicator.
|
|
176
|
+
const overlay = createMemo(() => {
|
|
177
|
+
if (!showSteps())
|
|
178
|
+
return null;
|
|
179
|
+
const marker = props.StepMarker;
|
|
180
|
+
const cells = options();
|
|
181
|
+
const withNumbers = props.renderStepNumber === true;
|
|
182
|
+
const inverted = props.inverted ?? false;
|
|
183
|
+
const thumbImage = props.thumbImage;
|
|
184
|
+
// Read for its dependency, not its value: the DEFAULT overlay hands the thumb image to
|
|
185
|
+
// whichever cell matches the current value, so the cell's child count moves with that value —
|
|
186
|
+
// a shape change descriptorToSolid refuses. With no thumb image the shape is fixed and the
|
|
187
|
+
// bridge just re-props the same nodes.
|
|
188
|
+
if (marker === undefined && thumbImage !== undefined)
|
|
189
|
+
currentValue();
|
|
190
|
+
return untrack(() => marker === undefined
|
|
191
|
+
? descriptorToSolid(() => renderStepsIndicator({
|
|
192
|
+
options: cells,
|
|
193
|
+
currentValue: currentValue(),
|
|
194
|
+
width: width(),
|
|
195
|
+
renderStepNumber: withNumbers,
|
|
196
|
+
thumbImage,
|
|
197
|
+
inverted,
|
|
198
|
+
platform,
|
|
199
|
+
}))
|
|
200
|
+
: buildCustomOverlay({
|
|
201
|
+
cells,
|
|
202
|
+
marker,
|
|
203
|
+
withNumbers,
|
|
204
|
+
inverted,
|
|
205
|
+
currentValue,
|
|
206
|
+
width,
|
|
207
|
+
thumbImage: () => props.thumbImage,
|
|
208
|
+
platform,
|
|
209
|
+
}));
|
|
210
|
+
});
|
|
211
|
+
// Built ONCE and kept by identity: descriptorToSolid wires every prop through a render effect on
|
|
212
|
+
// this same node, so a value/tint/limit change re-commits the existing native slider instead of
|
|
213
|
+
// replacing it — which would drop the thumb the user is dragging.
|
|
214
|
+
const leaf = descriptorToSolid(() => renderSliderNative(view(), platform));
|
|
215
|
+
const wrapper = hostElement('symbiote-view');
|
|
216
|
+
// A statically-present key set (all three are always emitted), so this needs no withStableKeys
|
|
217
|
+
// widening — `spread` alone has no removal pass for a key that vanishes.
|
|
218
|
+
spread(wrapper, () => ({
|
|
219
|
+
style: resolveSliderWrapperStyle(props.style, platform),
|
|
220
|
+
class: props.class,
|
|
221
|
+
onLayout: handleLayout,
|
|
222
|
+
}), true);
|
|
223
|
+
insertNode(wrapper, leaf);
|
|
224
|
+
// The overlay goes BEFORE the leaf, matching the z-order renderSlider composes; `leaf` is the
|
|
225
|
+
// marker `insert` positions against, which is also what keeps it from clearing the wrapper.
|
|
226
|
+
insert(wrapper, overlay, leaf);
|
|
227
|
+
return wrapper;
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// The custom-marker overlay: the same flex row the agnostic default paints, but each mark hosts the
|
|
231
|
+
// caller's own marker element, mirroring the library's SliderTrackMark composition. Built by hand
|
|
232
|
+
// rather than as a Descriptor because a Descriptor holds only VALUES, and a marker is a live Solid
|
|
233
|
+
// subtree — the same boundary View and Pressable sit on.
|
|
234
|
+
function buildCustomOverlay(params) {
|
|
235
|
+
const fontSize = stepNumberFontSize(params.cells.length);
|
|
236
|
+
const ordered = orderStepOptions(params.cells, params.inverted);
|
|
237
|
+
const min = params.cells[0];
|
|
238
|
+
const max = params.cells[params.cells.length - 1];
|
|
239
|
+
const container = hostElement('symbiote-view');
|
|
240
|
+
// pointerEvents none so the overlay never eats the drag. `style` tracks the measured width, hence
|
|
241
|
+
// an accessor rather than a one-shot setProp.
|
|
242
|
+
spread(container, () => ({
|
|
243
|
+
pointerEvents: 'none',
|
|
244
|
+
testID: 'StepsIndicator-Container',
|
|
245
|
+
style: resolveStepsContainerStyle(params.width(), params.platform),
|
|
246
|
+
}), true);
|
|
247
|
+
ordered.forEach((value, index) => {
|
|
248
|
+
insertNode(container, buildStepCell({ value, index, min, max, fontSize, params }));
|
|
249
|
+
});
|
|
250
|
+
return container;
|
|
251
|
+
}
|
|
252
|
+
function buildStepCell(cell) {
|
|
253
|
+
const { params } = cell;
|
|
254
|
+
const element = hostElement('symbiote-view');
|
|
255
|
+
setProp(element, 'style', STEP_INDICATOR_ELEMENT_STYLE);
|
|
256
|
+
const track = hostElement('symbiote-view');
|
|
257
|
+
setProp(track, 'style', TRACK_MARK_CONTAINER_STYLE);
|
|
258
|
+
insertNode(element, track);
|
|
259
|
+
// A memo, not a bare accessor: several leaves inside one marker must see the same object, and an
|
|
260
|
+
// unchanged value must not hand out a fresh one.
|
|
261
|
+
const markerProps = createMemo(() => ({
|
|
262
|
+
stepMarked: cell.value === params.currentValue(),
|
|
263
|
+
currentValue: params.currentValue(),
|
|
264
|
+
index: cell.index,
|
|
265
|
+
min: cell.min,
|
|
266
|
+
max: cell.max,
|
|
267
|
+
}));
|
|
268
|
+
const markerElement = callStepMarker(params.marker, markerProps);
|
|
269
|
+
// One `insert` owns the track's children: the marker (stable identity, reconciled in place) and
|
|
270
|
+
// the thumb image, which only the cell matching the current value carries.
|
|
271
|
+
insert(track, () => [markerElement, buildThumbImage(cell)]);
|
|
272
|
+
if (params.withNumbers) {
|
|
273
|
+
const numberBox = hostElement('symbiote-view');
|
|
274
|
+
setProp(numberBox, 'style', STEP_NUMBER_CONTAINER_STYLE);
|
|
275
|
+
const label = hostElement('symbiote-text');
|
|
276
|
+
setProp(label, 'testID', `${cell.index}th-step`);
|
|
277
|
+
setProp(label, 'style', { fontSize: cell.fontSize });
|
|
278
|
+
insertNode(label, createTextNode(String(cell.value)));
|
|
279
|
+
insertNode(numberBox, label);
|
|
280
|
+
insertNode(element, numberBox);
|
|
281
|
+
}
|
|
282
|
+
return element;
|
|
283
|
+
}
|
|
284
|
+
function buildThumbImage(cell) {
|
|
285
|
+
const source = cell.params.thumbImage();
|
|
286
|
+
if (source === undefined || cell.value !== cell.params.currentValue()) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const container = hostElement('symbiote-view');
|
|
290
|
+
setProp(container, 'style', THUMB_IMAGE_CONTAINER_STYLE);
|
|
291
|
+
setProp(container, 'testID', 'sliderTrackMark-thumbImage');
|
|
292
|
+
// The Image COMPONENT, not a raw symbiote-image: it resolves an asset id through RN's own
|
|
293
|
+
// resolveAssetSource, exactly as React's and Vue's custom overlays do.
|
|
294
|
+
insert(container, createComponent(Image, { source, style: THUMB_IMAGE_STYLE }));
|
|
295
|
+
return container;
|
|
296
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @symbiote-native/slider/svelte: Svelte wrapper over the @react-native-community/slider native
|
|
2
|
+
// view. Importing this barrel first registers the native view's ViewConfig (../register, a
|
|
3
|
+
// side-effect import of the codegen spec, never the library's React component), then exposes
|
|
4
|
+
// Slider. Mirrors vue/index.ts, except the platform split lives one level down
|
|
5
|
+
// (slider/slider-platform.ts, Metro-filename-selected) - matching this adapter's own
|
|
6
|
+
// Switch/ActivityIndicator convention, since Svelte components are compiled `.svelte` files, not
|
|
7
|
+
// factories like Vue's per-platform `createSlider(platform)`.
|
|
8
|
+
import '../register';
|
|
9
|
+
export { default as Slider } from './slider/index.svelte';
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
// Slider, the Svelte lifecycle half. Logic (value/limit/disabled folds, step-option layout) and
|
|
3
|
+
// the native render live in @symbiote-native/slider core, shared with every adapter; Svelte
|
|
4
|
+
// supplies reactivity (`$state`/`$derived`) and the descriptor bridge for the native
|
|
5
|
+
// `RNCSlider` leaf. The native view carries no symbiote metadata - the engine derives its
|
|
6
|
+
// events/color/image processors from the library's ViewConfig at runtime, registered by the
|
|
7
|
+
// side-effect import in ../index (not here, so this module and its tests stay free of the
|
|
8
|
+
// third-party spec).
|
|
9
|
+
//
|
|
10
|
+
// The leaf is bridged rather than written as `<RNCSlider>` because 'RNCSlider' is neither
|
|
11
|
+
// hyphenated nor lowercase-leading, so Svelte's compiler parses a literal tag as a COMPONENT
|
|
12
|
+
// reference, not an element (confirmed against the compiled output). The descriptor bridge
|
|
13
|
+
// (`native-view-bridge`'s `mountDescriptorChildren`, svelte-adapter-dom-shim skill §19)
|
|
14
|
+
// sidesteps this by calling `document.createElement(child.type)` programmatically, which the
|
|
15
|
+
// compiler never inspects. Imported from `native-view-bridge`, not the package's main barrel -
|
|
16
|
+
// the main barrel pulls in the whole `.svelte` module graph, fatal under vitest's plain
|
|
17
|
+
// svelte-plugin-free transform (this component's own smoke test).
|
|
18
|
+
//
|
|
19
|
+
// The custom-marker overlay's thumb-image cell uses the raw `symbiote-image` host tag rather
|
|
20
|
+
// than the framework's `Image` component: core's `render-steps-indicator.ts` paints the same
|
|
21
|
+
// default-overlay thumb with a bare `el('symbiote-image', {source, style})` Descriptor, and
|
|
22
|
+
// importing another `.svelte` component here would reintroduce the same main-barrel problem
|
|
23
|
+
// one level down.
|
|
24
|
+
import type { ISliderProps } from './slider-props';
|
|
25
|
+
|
|
26
|
+
export type { ISliderProps };
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<script lang="ts">
|
|
30
|
+
import { dlog, type ISymbioteEvent } from '@symbiote-native/engine';
|
|
31
|
+
import {
|
|
32
|
+
mountDescriptorChildren,
|
|
33
|
+
createDescriptorChildrenSync,
|
|
34
|
+
type IDescriptorChildrenMount,
|
|
35
|
+
type ShimElement,
|
|
36
|
+
} from '@symbiote-native/svelte/native-view-bridge';
|
|
37
|
+
import type { IDescriptorChild } from '@symbiote-native/components';
|
|
38
|
+
import {
|
|
39
|
+
sanitizeSliderValue,
|
|
40
|
+
resolveSliderDisabled,
|
|
41
|
+
resolveSliderAccessibilityState,
|
|
42
|
+
resolveSliderLowerLimit,
|
|
43
|
+
resolveSliderUpperLimit,
|
|
44
|
+
valueFromSliderEvent,
|
|
45
|
+
shouldRenderStepsIndicator,
|
|
46
|
+
resolveThumbTintColor,
|
|
47
|
+
shouldPassNativeThumbImage,
|
|
48
|
+
isInvalidLimitConfig,
|
|
49
|
+
computeStepOptions,
|
|
50
|
+
orderStepOptions,
|
|
51
|
+
stepNumberFontSize,
|
|
52
|
+
renderSlider,
|
|
53
|
+
renderSliderNative,
|
|
54
|
+
resolveSliderWrapperStyle,
|
|
55
|
+
resolveStepsContainerStyle,
|
|
56
|
+
renderStepsIndicator,
|
|
57
|
+
STEP_INDICATOR_ELEMENT_STYLE,
|
|
58
|
+
TRACK_MARK_CONTAINER_STYLE,
|
|
59
|
+
THUMB_IMAGE_CONTAINER_STYLE,
|
|
60
|
+
THUMB_IMAGE_STYLE,
|
|
61
|
+
STEP_NUMBER_CONTAINER_STYLE,
|
|
62
|
+
SLIDER_DEFAULT_MINIMUM_VALUE,
|
|
63
|
+
SLIDER_DEFAULT_MAXIMUM_VALUE,
|
|
64
|
+
SLIDER_DEFAULT_STEP,
|
|
65
|
+
SLIDER_ON_CHANGE,
|
|
66
|
+
SLIDER_ON_VALUE_CHANGE,
|
|
67
|
+
SLIDER_ON_SLIDING_START,
|
|
68
|
+
SLIDER_ON_SLIDING_COMPLETE,
|
|
69
|
+
SLIDER_ON_ACCESSIBILITY_ACTION,
|
|
70
|
+
type ISliderViewProps,
|
|
71
|
+
} from '../../core';
|
|
72
|
+
import { PLATFORM } from './slider-platform';
|
|
73
|
+
|
|
74
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
75
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let {
|
|
79
|
+
value = $bindable(),
|
|
80
|
+
minimumValue,
|
|
81
|
+
maximumValue,
|
|
82
|
+
step,
|
|
83
|
+
lowerLimit,
|
|
84
|
+
upperLimit,
|
|
85
|
+
disabled,
|
|
86
|
+
inverted = false,
|
|
87
|
+
thumbTintColor,
|
|
88
|
+
thumbImage,
|
|
89
|
+
accessibilityState,
|
|
90
|
+
renderStepNumber,
|
|
91
|
+
style,
|
|
92
|
+
class: className,
|
|
93
|
+
stepMarker,
|
|
94
|
+
onValueChange,
|
|
95
|
+
onSlidingStart,
|
|
96
|
+
onSlidingComplete,
|
|
97
|
+
onAccessibilityAction,
|
|
98
|
+
...passthrough
|
|
99
|
+
}: ISliderProps = $props();
|
|
100
|
+
|
|
101
|
+
// Last value native reported, kept only to mark the active step - not the controlled value (the
|
|
102
|
+
// slider is uncontrolled during a drag, unlike Switch: native owns the thumb position). A plain
|
|
103
|
+
// number, not an engine-node reference, so `$state` (not `.raw`) is fine here; only `hostShim*`
|
|
104
|
+
// below needs `.raw`'s no-deep-proxy guarantee for the engine's WeakMap-keyed identity lookups.
|
|
105
|
+
let reportedValue = $state<number | undefined>(undefined);
|
|
106
|
+
// The measured wrapper width the step indicator lays out against; 0 until the first layout.
|
|
107
|
+
let width = $state(0);
|
|
108
|
+
|
|
109
|
+
function handleValueChange(event: ISymbioteEvent): void {
|
|
110
|
+
const next = valueFromSliderEvent(event);
|
|
111
|
+
if (next === undefined) return;
|
|
112
|
+
reportedValue = next;
|
|
113
|
+
// Additive `bind:value` sugar (svelte-adapter-dom-shim skill): reassigning a `$bindable()`
|
|
114
|
+
// prop pushes it to the caller's bound variable, on the same report path as `onValueChange`
|
|
115
|
+
// below (matches the Vue adapter's `emitModelUpdate` timing) - never replacing the
|
|
116
|
+
// `value`/`onValueChange` contract.
|
|
117
|
+
value = next;
|
|
118
|
+
onValueChange?.(next);
|
|
119
|
+
}
|
|
120
|
+
function handleSlidingStart(event: ISymbioteEvent): void {
|
|
121
|
+
const next = valueFromSliderEvent(event);
|
|
122
|
+
if (next !== undefined) onSlidingStart?.(next);
|
|
123
|
+
}
|
|
124
|
+
function handleSlidingComplete(event: ISymbioteEvent): void {
|
|
125
|
+
const next = valueFromSliderEvent(event);
|
|
126
|
+
if (next !== undefined) onSlidingComplete?.(next);
|
|
127
|
+
}
|
|
128
|
+
function handleAccessibilityAction(event: ISymbioteEvent): void {
|
|
129
|
+
onAccessibilityAction?.(event);
|
|
130
|
+
}
|
|
131
|
+
function handleLayout(event: ISymbioteEvent): void {
|
|
132
|
+
const layout = event.nativeEvent.layout;
|
|
133
|
+
if (isRecord(layout) && typeof layout.width === 'number')
|
|
134
|
+
width = layout.width;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const minimum = $derived(minimumValue ?? SLIDER_DEFAULT_MINIMUM_VALUE);
|
|
138
|
+
const maximum = $derived(maximumValue ?? SLIDER_DEFAULT_MAXIMUM_VALUE);
|
|
139
|
+
const resolvedStep = $derived(step ?? SLIDER_DEFAULT_STEP);
|
|
140
|
+
const lower = $derived(resolveSliderLowerLimit(lowerLimit));
|
|
141
|
+
const upper = $derived(resolveSliderUpperLimit(upperLimit));
|
|
142
|
+
|
|
143
|
+
$effect(() => {
|
|
144
|
+
if (isInvalidLimitConfig(lower, upper)) {
|
|
145
|
+
dlog('Slider: lowerLimit must be smaller than upperLimit');
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const hasStepMarker = $derived(stepMarker !== undefined);
|
|
150
|
+
const hasThumbImage = $derived(thumbImage !== undefined);
|
|
151
|
+
const showSteps = $derived(
|
|
152
|
+
shouldRenderStepsIndicator(hasStepMarker, renderStepNumber),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// The native view gets a thumbImage only when there is one AND no custom marker (which draws
|
|
156
|
+
// its own thumb), matching the library. Passed raw — the engine runs the image processor
|
|
157
|
+
// derived from RNCSlider's ViewConfig (same path as the color tints).
|
|
158
|
+
const nativeThumbImage = $derived(
|
|
159
|
+
shouldPassNativeThumbImage(hasStepMarker, hasThumbImage)
|
|
160
|
+
? thumbImage
|
|
161
|
+
: undefined,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const view = $derived<ISliderViewProps>({
|
|
165
|
+
value: sanitizeSliderValue(value),
|
|
166
|
+
minimumValue: minimum,
|
|
167
|
+
maximumValue: maximum,
|
|
168
|
+
step: resolvedStep,
|
|
169
|
+
lowerLimit: lower,
|
|
170
|
+
upperLimit: upper,
|
|
171
|
+
disabled: resolveSliderDisabled(disabled, accessibilityState),
|
|
172
|
+
inverted,
|
|
173
|
+
thumbTintColor: resolveThumbTintColor(
|
|
174
|
+
thumbTintColor,
|
|
175
|
+
hasStepMarker,
|
|
176
|
+
hasThumbImage,
|
|
177
|
+
),
|
|
178
|
+
thumbImage: nativeThumbImage,
|
|
179
|
+
accessibilityState: resolveSliderAccessibilityState(
|
|
180
|
+
disabled,
|
|
181
|
+
accessibilityState,
|
|
182
|
+
),
|
|
183
|
+
width,
|
|
184
|
+
style,
|
|
185
|
+
passthrough: {
|
|
186
|
+
...passthrough,
|
|
187
|
+
[SLIDER_ON_CHANGE]: handleValueChange,
|
|
188
|
+
[SLIDER_ON_VALUE_CHANGE]: handleValueChange,
|
|
189
|
+
[SLIDER_ON_SLIDING_START]: handleSlidingStart,
|
|
190
|
+
[SLIDER_ON_SLIDING_COMPLETE]: handleSlidingComplete,
|
|
191
|
+
[SLIDER_ON_ACCESSIBILITY_ACTION]: handleAccessibilityAction,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const options = $derived(
|
|
196
|
+
computeStepOptions(minimum, maximum, resolvedStep, PLATFORM.stepResolution),
|
|
197
|
+
);
|
|
198
|
+
const currentValue = $derived(reportedValue ?? view.value ?? minimum);
|
|
199
|
+
|
|
200
|
+
// --- No custom marker: renderSlider() is the single source of truth (wrapper + optional
|
|
201
|
+
// default steps overlay + the native leaf), ALL pure Descriptor — mounted via the bridge.
|
|
202
|
+
// Unlike every fixed-shape component elsewhere in this adapter, this Descriptor's own shape
|
|
203
|
+
// genuinely varies with `renderStepNumber` (1 child with no overlay, 2 with one), so the sync
|
|
204
|
+
// below tolerates a child-count change with a clean rebuild instead of assuming constant shape.
|
|
205
|
+
const defaultDescriptor = $derived(
|
|
206
|
+
hasStepMarker
|
|
207
|
+
? undefined
|
|
208
|
+
: renderSlider(
|
|
209
|
+
view,
|
|
210
|
+
PLATFORM,
|
|
211
|
+
showSteps
|
|
212
|
+
? {
|
|
213
|
+
steps: renderStepsIndicator({
|
|
214
|
+
options,
|
|
215
|
+
currentValue,
|
|
216
|
+
width,
|
|
217
|
+
renderStepNumber: renderStepNumber === true,
|
|
218
|
+
thumbImage,
|
|
219
|
+
inverted,
|
|
220
|
+
platform: PLATFORM,
|
|
221
|
+
}),
|
|
222
|
+
onLayout: handleLayout,
|
|
223
|
+
}
|
|
224
|
+
: { onLayout: handleLayout },
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
let hostShimDefault = $state.raw<ShimElement | null>(null);
|
|
229
|
+
let mountedDefault: IDescriptorChildrenMount | undefined;
|
|
230
|
+
let mountedDefaultCount = -1;
|
|
231
|
+
|
|
232
|
+
function syncDefaultChildren(children: IDescriptorChild[]): void {
|
|
233
|
+
if (hostShimDefault === null) return;
|
|
234
|
+
if (
|
|
235
|
+
mountedDefault === undefined ||
|
|
236
|
+
mountedDefaultCount !== children.length
|
|
237
|
+
) {
|
|
238
|
+
for (const child of hostShimDefault.children.slice())
|
|
239
|
+
hostShimDefault.removeChild(child);
|
|
240
|
+
mountedDefault = mountDescriptorChildren(hostShimDefault, children);
|
|
241
|
+
mountedDefaultCount = children.length;
|
|
242
|
+
} else {
|
|
243
|
+
mountedDefault.update(children);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
$effect(() => {
|
|
248
|
+
const descriptor = defaultDescriptor;
|
|
249
|
+
if (descriptor === undefined) return;
|
|
250
|
+
syncDefaultChildren(descriptor.children);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// --- Custom marker: the overlay is hand-authored live template content below (it hosts the
|
|
254
|
+
// caller's `stepMarker` Snippet, a live child no Descriptor can carry — component-render-fn-
|
|
255
|
+
// boundary rule). Only the native leaf (always exactly one, constant shape) is bridged.
|
|
256
|
+
const fontSize = $derived(stepNumberFontSize(options.length));
|
|
257
|
+
const orderedOptions = $derived(orderStepOptions(options, inverted));
|
|
258
|
+
const minOption = $derived(options[0]);
|
|
259
|
+
const maxOption = $derived(options[options.length - 1]);
|
|
260
|
+
const nativeLeafDescriptor = $derived(renderSliderNative(view, PLATFORM));
|
|
261
|
+
const markerWrapperBag = $derived({
|
|
262
|
+
style: resolveSliderWrapperStyle(style, PLATFORM),
|
|
263
|
+
class: className,
|
|
264
|
+
onLayout: handleLayout,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
let hostShimMarker = $state.raw<ShimElement | null>(null);
|
|
268
|
+
const syncMarkerLeaf = createDescriptorChildrenSync();
|
|
269
|
+
|
|
270
|
+
$effect(() => {
|
|
271
|
+
if (!hasStepMarker) return;
|
|
272
|
+
syncMarkerLeaf(hostShimMarker, [nativeLeafDescriptor]);
|
|
273
|
+
});
|
|
274
|
+
</script>
|
|
275
|
+
|
|
276
|
+
{#if hasStepMarker}
|
|
277
|
+
<symbiote-view p={markerWrapperBag} bind:this={hostShimMarker}>
|
|
278
|
+
<symbiote-view
|
|
279
|
+
p={{
|
|
280
|
+
pointerEvents: 'none',
|
|
281
|
+
testID: 'StepsIndicator-Container',
|
|
282
|
+
style: resolveStepsContainerStyle(width, PLATFORM),
|
|
283
|
+
}}
|
|
284
|
+
>
|
|
285
|
+
{#each orderedOptions as optionValue, index (optionValue)}
|
|
286
|
+
<symbiote-view p={{ style: STEP_INDICATOR_ELEMENT_STYLE }}>
|
|
287
|
+
<symbiote-view p={{ style: TRACK_MARK_CONTAINER_STYLE }}>
|
|
288
|
+
{@render stepMarker?.({
|
|
289
|
+
stepMarked: optionValue === currentValue,
|
|
290
|
+
currentValue,
|
|
291
|
+
index,
|
|
292
|
+
min: minOption,
|
|
293
|
+
max: maxOption,
|
|
294
|
+
})}
|
|
295
|
+
{#if thumbImage !== undefined && optionValue === currentValue}
|
|
296
|
+
<symbiote-view
|
|
297
|
+
p={{
|
|
298
|
+
style: THUMB_IMAGE_CONTAINER_STYLE,
|
|
299
|
+
testID: 'sliderTrackMark-thumbImage',
|
|
300
|
+
}}
|
|
301
|
+
>
|
|
302
|
+
<symbiote-image
|
|
303
|
+
p={{ source: thumbImage, style: THUMB_IMAGE_STYLE }}
|
|
304
|
+
/>
|
|
305
|
+
</symbiote-view>
|
|
306
|
+
{/if}
|
|
307
|
+
</symbiote-view>
|
|
308
|
+
{#if renderStepNumber}
|
|
309
|
+
<symbiote-view p={{ style: STEP_NUMBER_CONTAINER_STYLE }}>
|
|
310
|
+
<symbiote-text
|
|
311
|
+
p={{ testID: `${index}th-step`, style: { fontSize } }}
|
|
312
|
+
>
|
|
313
|
+
{String(optionValue)}
|
|
314
|
+
</symbiote-text>
|
|
315
|
+
</symbiote-view>
|
|
316
|
+
{/if}
|
|
317
|
+
</symbiote-view>
|
|
318
|
+
{/each}
|
|
319
|
+
</symbiote-view>
|
|
320
|
+
</symbiote-view>
|
|
321
|
+
{:else}
|
|
322
|
+
<symbiote-view
|
|
323
|
+
p={{ ...defaultDescriptor?.props, class: className }}
|
|
324
|
+
bind:this={hostShimDefault}
|
|
325
|
+
/>
|
|
326
|
+
{/if}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Slider on Android: the native view sizes itself (no default height), lays out implicit steps
|
|
2
|
+
// at 128-point resolution, and keeps the step row at the top. Mirrors
|
|
3
|
+
// packages/slider/src/vue/slider/index.android.ts exactly, one level down (platform CONSTANT
|
|
4
|
+
// here — see index.svelte's header).
|
|
5
|
+
import { SLIDER_STEP_RESOLUTION_ANDROID } from '../../core/index.js';
|
|
6
|
+
const ANDROID_STEPS_CONTAINER_TOP = 0;
|
|
7
|
+
export const PLATFORM = {
|
|
8
|
+
defaultStyle: {},
|
|
9
|
+
stepResolution: SLIDER_STEP_RESOLUTION_ANDROID,
|
|
10
|
+
stepsContainerTop: ANDROID_STEPS_CONTAINER_TOP,
|
|
11
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './slider-platform.ios';
|