@symbiote-native/slider 5.0.1 → 5.0.2
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/build/angular/index.d.ts +3 -0
- package/build/angular/index.js +2 -0
- package/build/angular/slider/index.android.d.ts +9 -0
- package/build/angular/slider/index.android.js +71 -0
- package/build/angular/slider/index.d.ts +2 -0
- package/build/angular/slider/index.ios.d.ts +11 -0
- package/build/angular/slider/index.ios.js +72 -0
- package/build/angular/slider/index.js +3 -0
- package/build/angular/slider/shared.d.ts +84 -0
- package/build/angular/slider/shared.js +649 -0
- package/build/core/constants.d.ts +20 -0
- package/build/core/constants.js +40 -0
- package/build/core/index.d.ts +7 -0
- package/build/core/index.js +10 -0
- package/build/core/render-slider.d.ts +11 -0
- package/build/core/render-slider.js +66 -0
- package/build/core/render-steps-indicator.d.ts +28 -0
- package/build/core/render-steps-indicator.js +91 -0
- package/build/core/slider-props.d.ts +52 -0
- package/build/core/slider-props.js +7 -0
- package/build/core/slider-state.d.ts +15 -0
- package/build/core/slider-state.js +73 -0
- package/build/react/index.d.ts +4 -0
- package/build/react/index.js +7 -0
- package/build/react/slider/index.android.d.ts +1 -0
- package/build/react/slider/index.android.js +10 -0
- package/build/react/slider/index.d.ts +1 -0
- package/build/react/slider/index.ios.d.ts +1 -0
- package/build/react/slider/index.ios.js +11 -0
- package/build/react/slider/index.js +3 -0
- package/build/react/slider/shared.d.ts +10 -0
- package/build/react/slider/shared.js +176 -0
- package/build/register/index.d.ts +1 -0
- package/build/register/index.js +47 -0
- package/build/vue/index.d.ts +3 -0
- package/build/vue/index.js +7 -0
- package/build/vue/slider/index.android.d.ts +18 -0
- package/build/vue/slider/index.android.js +10 -0
- package/build/vue/slider/index.d.ts +2 -0
- package/build/vue/slider/index.ios.d.ts +18 -0
- package/build/vue/slider/index.ios.js +11 -0
- package/build/vue/slider/index.js +3 -0
- package/build/vue/slider/shared.d.ts +26 -0
- package/build/vue/slider/shared.js +253 -0
- package/package.json +5 -5
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Slider, the React 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 the Vue adapter; here
|
|
3
|
+
// React supplies the lifecycle — useState for the last-reported value (to mark the active step)
|
|
4
|
+
// and the measured width (for the step indicator), useEffect for the limit warning — plus the
|
|
5
|
+
// descriptor bridge and forwardRef to the native view (matching the library's forwardRef). The
|
|
6
|
+
// native RNCSlider carries no symbiote metadata: the engine derives its events and color/image
|
|
7
|
+
// processors from the library's ViewConfig, registered by ../register (the package barrel pulls
|
|
8
|
+
// it). This is the React twin of the Vue wrapper; both drive the engine, NEITHER imports the
|
|
9
|
+
// library's React Slider component — that component internally calls React hooks off the React
|
|
10
|
+
// dispatcher, so a non-React adapter rendering it directly would crash on a null dispatcher.
|
|
11
|
+
import { createElement, forwardRef, useCallback, useEffect, useState } from 'react';
|
|
12
|
+
import { descriptorToReact, Image } from '@symbiote-native/react';
|
|
13
|
+
import { resolveAccessibilityProps } from '@symbiote-native/components';
|
|
14
|
+
import { dlog } from '@symbiote-native/engine';
|
|
15
|
+
import { sanitizeSliderValue, resolveSliderDisabled, resolveSliderAccessibilityState, resolveSliderLowerLimit, resolveSliderUpperLimit, valueFromSliderEvent, shouldRenderStepsIndicator, resolveThumbTintColor, shouldPassNativeThumbImage, isInvalidLimitConfig, computeStepOptions, orderStepOptions, stepNumberFontSize, renderSlider, renderSliderNative, 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';
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function toChild(child) {
|
|
20
|
+
return typeof child === 'string' ? child : descriptorToReact(child);
|
|
21
|
+
}
|
|
22
|
+
// renderSlider's Descriptor IS the outer wrapper (symbiote-view hosting the native leaf +
|
|
23
|
+
// optional steps overlay); className is reapplied on it here, exactly like ImageBackground's
|
|
24
|
+
// wrapper.
|
|
25
|
+
function withClassName(descriptor, className) {
|
|
26
|
+
return createElement(descriptor.type, { key: descriptor.key, ...descriptor.props, className }, ...descriptor.children.map(toChild));
|
|
27
|
+
}
|
|
28
|
+
export function createSlider(platform) {
|
|
29
|
+
return forwardRef((rawProps, forwardedRef) => {
|
|
30
|
+
// Slider owns its host wrapper rather than rendering through a symbiote View, so it folds
|
|
31
|
+
// aria/role into accessibility* here; the rest (testID, accessibilityLabel, accessibility-
|
|
32
|
+
// Actions, …) rides to the native node via `passthrough`.
|
|
33
|
+
const props = resolveAccessibilityProps(rawProps);
|
|
34
|
+
const { value, minimumValue, maximumValue, step, lowerLimit, upperLimit, disabled, inverted, thumbTintColor, thumbImage, accessibilityState, renderStepNumber, style, className, StepMarker, onValueChange, onSlidingStart, onSlidingComplete, onAccessibilityAction, ...passthrough } = props;
|
|
35
|
+
const [reportedValue, setReportedValue] = useState(undefined);
|
|
36
|
+
const [width, setWidth] = useState(0);
|
|
37
|
+
const handleValueChange = useCallback((event) => {
|
|
38
|
+
const next = valueFromSliderEvent(event);
|
|
39
|
+
if (next === undefined)
|
|
40
|
+
return;
|
|
41
|
+
setReportedValue(next);
|
|
42
|
+
onValueChange?.(next);
|
|
43
|
+
}, [onValueChange]);
|
|
44
|
+
const handleSlidingStart = useCallback((event) => {
|
|
45
|
+
const next = valueFromSliderEvent(event);
|
|
46
|
+
if (next !== undefined)
|
|
47
|
+
onSlidingStart?.(next);
|
|
48
|
+
}, [onSlidingStart]);
|
|
49
|
+
const handleSlidingComplete = useCallback((event) => {
|
|
50
|
+
const next = valueFromSliderEvent(event);
|
|
51
|
+
if (next !== undefined)
|
|
52
|
+
onSlidingComplete?.(next);
|
|
53
|
+
}, [onSlidingComplete]);
|
|
54
|
+
const handleAccessibilityAction = useCallback((event) => {
|
|
55
|
+
onAccessibilityAction?.(event);
|
|
56
|
+
}, [onAccessibilityAction]);
|
|
57
|
+
const handleLayout = useCallback((event) => {
|
|
58
|
+
const layout = event.nativeEvent.layout;
|
|
59
|
+
if (isRecord(layout) && typeof layout.width === 'number')
|
|
60
|
+
setWidth(layout.width);
|
|
61
|
+
}, []);
|
|
62
|
+
const minimum = minimumValue ?? SLIDER_DEFAULT_MINIMUM_VALUE;
|
|
63
|
+
const maximum = maximumValue ?? SLIDER_DEFAULT_MAXIMUM_VALUE;
|
|
64
|
+
const stepValue = step ?? SLIDER_DEFAULT_STEP;
|
|
65
|
+
const lower = resolveSliderLowerLimit(lowerLimit);
|
|
66
|
+
const upper = resolveSliderUpperLimit(upperLimit);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (isInvalidLimitConfig(lower, upper))
|
|
69
|
+
dlog('Slider: lowerLimit must be smaller than upperLimit');
|
|
70
|
+
}, [lower, upper]);
|
|
71
|
+
const hasStepMarker = StepMarker !== undefined;
|
|
72
|
+
const hasThumbImage = thumbImage !== undefined;
|
|
73
|
+
const showSteps = shouldRenderStepsIndicator(hasStepMarker, renderStepNumber);
|
|
74
|
+
// Passed to the native view raw — the engine runs the image processor derived from RNCSlider's
|
|
75
|
+
// ViewConfig — and only when no custom marker draws its own thumb (matches the library).
|
|
76
|
+
const nativeThumbImage = shouldPassNativeThumbImage(hasStepMarker, hasThumbImage)
|
|
77
|
+
? thumbImage
|
|
78
|
+
: undefined;
|
|
79
|
+
const view = {
|
|
80
|
+
value: sanitizeSliderValue(value),
|
|
81
|
+
minimumValue: minimum,
|
|
82
|
+
maximumValue: maximum,
|
|
83
|
+
step: stepValue,
|
|
84
|
+
lowerLimit: lower,
|
|
85
|
+
upperLimit: upper,
|
|
86
|
+
disabled: resolveSliderDisabled(disabled, accessibilityState),
|
|
87
|
+
inverted: inverted ?? false,
|
|
88
|
+
thumbTintColor: resolveThumbTintColor(thumbTintColor, hasStepMarker, hasThumbImage),
|
|
89
|
+
thumbImage: nativeThumbImage,
|
|
90
|
+
accessibilityState: resolveSliderAccessibilityState(disabled, accessibilityState),
|
|
91
|
+
width,
|
|
92
|
+
style,
|
|
93
|
+
passthrough: {
|
|
94
|
+
...passthrough,
|
|
95
|
+
ref: forwardedRef,
|
|
96
|
+
[SLIDER_ON_CHANGE]: handleValueChange,
|
|
97
|
+
[SLIDER_ON_VALUE_CHANGE]: handleValueChange,
|
|
98
|
+
[SLIDER_ON_SLIDING_START]: handleSlidingStart,
|
|
99
|
+
[SLIDER_ON_SLIDING_COMPLETE]: handleSlidingComplete,
|
|
100
|
+
[SLIDER_ON_ACCESSIBILITY_ACTION]: handleAccessibilityAction,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
if (!showSteps) {
|
|
104
|
+
return withClassName(renderSlider(view, platform, { onLayout: handleLayout }), className);
|
|
105
|
+
}
|
|
106
|
+
const options = computeStepOptions(minimum, maximum, stepValue, platform.stepResolution);
|
|
107
|
+
const currentValue = reportedValue ?? view.value ?? minimum;
|
|
108
|
+
if (StepMarker !== undefined) {
|
|
109
|
+
const overlay = renderCustomStepsOverlay({
|
|
110
|
+
options,
|
|
111
|
+
currentValue,
|
|
112
|
+
inverted: view.inverted,
|
|
113
|
+
renderStepNumber: renderStepNumber === true,
|
|
114
|
+
thumbImage,
|
|
115
|
+
width,
|
|
116
|
+
platform,
|
|
117
|
+
StepMarker,
|
|
118
|
+
});
|
|
119
|
+
return createElement('symbiote-view', { style: resolveStepsWrapperStyle(style, platform), onLayout: handleLayout, className }, overlay, descriptorToReact(renderSliderNative(view, platform)));
|
|
120
|
+
}
|
|
121
|
+
const steps = renderStepsIndicator({
|
|
122
|
+
options,
|
|
123
|
+
currentValue,
|
|
124
|
+
width,
|
|
125
|
+
renderStepNumber: renderStepNumber === true,
|
|
126
|
+
thumbImage,
|
|
127
|
+
inverted: view.inverted,
|
|
128
|
+
platform,
|
|
129
|
+
});
|
|
130
|
+
return withClassName(renderSlider(view, platform, { steps, onLayout: handleLayout }), className);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// The wrapper style for the custom-marker steps path mirrors renderSlider's wrapper.
|
|
134
|
+
function resolveStepsWrapperStyle(style, platform) {
|
|
135
|
+
return [platform.defaultStyle, style, { justifyContent: 'center' }];
|
|
136
|
+
}
|
|
137
|
+
// The custom-marker overlay: the same flex row as the agnostic default, each mark hosting the
|
|
138
|
+
// user's StepMarker element (plus the thumbImage on the current step and the optional step
|
|
139
|
+
// number), mirroring the library's SliderTrackMark composition.
|
|
140
|
+
function renderCustomStepsOverlay(params) {
|
|
141
|
+
const fontSize = stepNumberFontSize(params.options.length);
|
|
142
|
+
const ordered = orderStepOptions(params.options, params.inverted);
|
|
143
|
+
const min = params.options[0];
|
|
144
|
+
const max = params.options[params.options.length - 1];
|
|
145
|
+
const cells = ordered.map((value, index) => {
|
|
146
|
+
const trackChildren = [
|
|
147
|
+
createElement(params.StepMarker, {
|
|
148
|
+
key: 'marker',
|
|
149
|
+
stepMarked: value === params.currentValue,
|
|
150
|
+
currentValue: params.currentValue,
|
|
151
|
+
index,
|
|
152
|
+
min,
|
|
153
|
+
max,
|
|
154
|
+
}),
|
|
155
|
+
];
|
|
156
|
+
if (params.thumbImage !== undefined && value === params.currentValue) {
|
|
157
|
+
trackChildren.push(createElement('symbiote-view', {
|
|
158
|
+
key: 'thumb',
|
|
159
|
+
style: THUMB_IMAGE_CONTAINER_STYLE,
|
|
160
|
+
testID: 'sliderTrackMark-thumbImage',
|
|
161
|
+
}, createElement(Image, { source: params.thumbImage, style: THUMB_IMAGE_STYLE })));
|
|
162
|
+
}
|
|
163
|
+
const cellChildren = [
|
|
164
|
+
createElement('symbiote-view', { key: 'track', style: TRACK_MARK_CONTAINER_STYLE }, ...trackChildren),
|
|
165
|
+
];
|
|
166
|
+
if (params.renderStepNumber) {
|
|
167
|
+
cellChildren.push(createElement('symbiote-view', { key: 'number', style: STEP_NUMBER_CONTAINER_STYLE }, createElement('symbiote-text', { testID: `${index}th-step`, style: { fontSize } }, String(value))));
|
|
168
|
+
}
|
|
169
|
+
return createElement('symbiote-view', { key: index, style: STEP_INDICATOR_ELEMENT_STYLE }, ...cellChildren);
|
|
170
|
+
});
|
|
171
|
+
return createElement('symbiote-view', {
|
|
172
|
+
pointerEvents: 'none',
|
|
173
|
+
testID: 'StepsIndicator-Container',
|
|
174
|
+
style: resolveStepsContainerStyle(params.width, params.platform),
|
|
175
|
+
}, ...cells);
|
|
176
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@react-native-community/slider/dist/RNCSliderNativeComponent';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Side-effect: register RNCSlider's ViewConfig in RN's ReactNativeViewConfigRegistry. Evaluating
|
|
2
|
+
// the codegen native-component spec (codegenNativeComponent('RNCSlider', …)) registers the
|
|
3
|
+
// ViewConfig getter the engine reads via setNativeViewConfigSource, so the engine can derive
|
|
4
|
+
// RNCSlider's events and color/image processors on first commit. We pull the codegen SPEC, never
|
|
5
|
+
// the library's default export (Slider.tsx, a React component using hooks) — this module has no
|
|
6
|
+
// hooks, so it is framework-agnostic and shared by every adapter entry (vue / react). Each adapter
|
|
7
|
+
// barrel imports it; the component modules and their headless tests stay free of the spec.
|
|
8
|
+
import '@react-native-community/slider/dist/RNCSliderNativeComponent';
|
|
9
|
+
import { processColor, registerComponent } from '@symbiote-native/engine';
|
|
10
|
+
import { RNC_SLIDER_VIEW_NAME, SLIDER_ON_CHANGE, SLIDER_ON_VALUE_CHANGE, SLIDER_ON_SLIDING_START, SLIDER_ON_SLIDING_COMPLETE, SLIDER_ON_ACCESSIBILITY_ACTION, } from '../core/index.js';
|
|
11
|
+
function listenerName(prop) {
|
|
12
|
+
return prop.charAt(2).toLowerCase() + prop.slice(3);
|
|
13
|
+
}
|
|
14
|
+
function processSliderColor(value) {
|
|
15
|
+
return processColor(value);
|
|
16
|
+
}
|
|
17
|
+
// Fallback metadata for the native slider. RN's codegen module above normally registers the same
|
|
18
|
+
// ViewConfig into ReactNativeViewConfigRegistry, but relying only on that side effect is brittle on
|
|
19
|
+
// Metro/RN version boundaries: if the registry lookup misses, the slider still mounts, but tint
|
|
20
|
+
// props stay raw and native value events never route back to JS. Keep the explicit fallback here,
|
|
21
|
+
// in the wrapper package that owns RNCSlider, so React/Vue/Angular all inherit the same behavior.
|
|
22
|
+
registerComponent(RNC_SLIDER_VIEW_NAME, {
|
|
23
|
+
events: [
|
|
24
|
+
{ raw: 'topChange', listener: listenerName(SLIDER_ON_CHANGE) },
|
|
25
|
+
{ raw: 'topRNCSliderValueChange', listener: listenerName(SLIDER_ON_VALUE_CHANGE) },
|
|
26
|
+
{
|
|
27
|
+
raw: 'topRNCSliderSlidingStart',
|
|
28
|
+
listener: listenerName(SLIDER_ON_SLIDING_START),
|
|
29
|
+
direct: true,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
raw: 'topRNCSliderSlidingComplete',
|
|
33
|
+
listener: listenerName(SLIDER_ON_SLIDING_COMPLETE),
|
|
34
|
+
direct: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
raw: 'topRNCSliderAccessibilityAction',
|
|
38
|
+
listener: listenerName(SLIDER_ON_ACCESSIBILITY_ACTION),
|
|
39
|
+
direct: true,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
processors: {
|
|
43
|
+
minimumTrackTintColor: processSliderColor,
|
|
44
|
+
maximumTrackTintColor: processSliderColor,
|
|
45
|
+
thumbTintColor: processSliderColor,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @symbiote-native/slider/vue: the Vue wrapper over the @react-native-community/slider native view.
|
|
2
|
+
// Importing this barrel first registers the native view's ViewConfig (./register, a side-effect
|
|
3
|
+
// import of the codegen spec — never the library's React component), then exposes the platform-
|
|
4
|
+
// split Slider. The public prop type is the agnostic core ISliderProps plus a Vue-only `class`
|
|
5
|
+
// field (see slider/shared.ts); the custom StepMarker is a Vue scoped slot, not a prop.
|
|
6
|
+
import '../register';
|
|
7
|
+
export { Slider } from './slider';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const Slider: import("vue").DefineSetupFnComponent<import("./shared").ISliderProps, {
|
|
2
|
+
valueChange: (value: number) => boolean;
|
|
3
|
+
slidingStart: (value: number) => boolean;
|
|
4
|
+
slidingComplete: (value: number) => boolean;
|
|
5
|
+
accessibilityAction: (event: import("@symbiote-native/engine").ISymbioteEvent) => boolean;
|
|
6
|
+
'update:modelValue': (value: number) => boolean;
|
|
7
|
+
'update:value': (value: number) => boolean;
|
|
8
|
+
}, {}, Omit<import("../../core").ISliderProps, "onValueChange" | "onSlidingStart" | "onSlidingComplete" | "onAccessibilityAction"> & {
|
|
9
|
+
modelValue?: number;
|
|
10
|
+
class?: import("@symbiote-native/engine").IClassNameValue;
|
|
11
|
+
} & {
|
|
12
|
+
onValueChange?: ((value: number) => any) | undefined;
|
|
13
|
+
onSlidingStart?: ((value: number) => any) | undefined;
|
|
14
|
+
onSlidingComplete?: ((value: number) => any) | undefined;
|
|
15
|
+
onAccessibilityAction?: ((event: import("@symbiote-native/engine").ISymbioteEvent) => any) | undefined;
|
|
16
|
+
"onUpdate:modelValue"?: ((value: number) => any) | undefined;
|
|
17
|
+
"onUpdate:value"?: ((value: number) => any) | undefined;
|
|
18
|
+
}, import("vue").PublicProps>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Slider on Android: the native view sizes itself (no default height), lays out implicit steps at
|
|
2
|
+
// 128-point resolution, and keeps the step row at the top. Mirrors the library's Android branches.
|
|
3
|
+
import { createSlider } from './shared.js';
|
|
4
|
+
import { SLIDER_STEP_RESOLUTION_ANDROID } from '../../core/index.js';
|
|
5
|
+
const ANDROID_STEPS_CONTAINER_TOP = 0;
|
|
6
|
+
export const Slider = createSlider({
|
|
7
|
+
defaultStyle: {},
|
|
8
|
+
stepResolution: SLIDER_STEP_RESOLUTION_ANDROID,
|
|
9
|
+
stepsContainerTop: ANDROID_STEPS_CONTAINER_TOP,
|
|
10
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const Slider: import("vue").DefineSetupFnComponent<import("./shared").ISliderProps, {
|
|
2
|
+
valueChange: (value: number) => boolean;
|
|
3
|
+
slidingStart: (value: number) => boolean;
|
|
4
|
+
slidingComplete: (value: number) => boolean;
|
|
5
|
+
accessibilityAction: (event: import("@symbiote-native/engine").ISymbioteEvent) => boolean;
|
|
6
|
+
'update:modelValue': (value: number) => boolean;
|
|
7
|
+
'update:value': (value: number) => boolean;
|
|
8
|
+
}, {}, Omit<import("../../core").ISliderProps, "onValueChange" | "onSlidingStart" | "onSlidingComplete" | "onAccessibilityAction"> & {
|
|
9
|
+
modelValue?: number;
|
|
10
|
+
class?: import("@symbiote-native/engine").IClassNameValue;
|
|
11
|
+
} & {
|
|
12
|
+
onValueChange?: ((value: number) => any) | undefined;
|
|
13
|
+
onSlidingStart?: ((value: number) => any) | undefined;
|
|
14
|
+
onSlidingComplete?: ((value: number) => any) | undefined;
|
|
15
|
+
onAccessibilityAction?: ((event: import("@symbiote-native/engine").ISymbioteEvent) => any) | undefined;
|
|
16
|
+
"onUpdate:modelValue"?: ((value: number) => any) | undefined;
|
|
17
|
+
"onUpdate:value"?: ((value: number) => any) | undefined;
|
|
18
|
+
}, import("vue").PublicProps>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Slider on iOS: the native view has an intrinsic 40pt height (the wrapper applies it as the
|
|
2
|
+
// default), lays out implicit steps at 1000-point resolution, and nudges the step row down 10pt.
|
|
3
|
+
// Also the base (./index re-exports it) for headless. Mirrors the library's iOS branches.
|
|
4
|
+
import { createSlider } from './shared.js';
|
|
5
|
+
import { SLIDER_IOS_DEFAULT_HEIGHT, SLIDER_STEP_RESOLUTION_IOS } from '../../core/index.js';
|
|
6
|
+
const IOS_STEPS_CONTAINER_TOP = 10;
|
|
7
|
+
export const Slider = createSlider({
|
|
8
|
+
defaultStyle: { height: SLIDER_IOS_DEFAULT_HEIGHT },
|
|
9
|
+
stepResolution: SLIDER_STEP_RESOLUTION_IOS,
|
|
10
|
+
stepsContainerTop: IOS_STEPS_CONTAINER_TOP,
|
|
11
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type IClassNameValue, type ISymbioteEvent } from '@symbiote-native/engine';
|
|
2
|
+
import { type ISliderPlatform, type ISliderProps as ISliderBaseProps } from '../../core';
|
|
3
|
+
export type ISliderProps = Omit<ISliderBaseProps, 'onValueChange' | 'onSlidingStart' | 'onSlidingComplete' | 'onAccessibilityAction'> & {
|
|
4
|
+
modelValue?: number;
|
|
5
|
+
class?: IClassNameValue;
|
|
6
|
+
};
|
|
7
|
+
type ISliderEmits = {
|
|
8
|
+
valueChange: (value: number) => boolean;
|
|
9
|
+
slidingStart: (value: number) => boolean;
|
|
10
|
+
slidingComplete: (value: number) => boolean;
|
|
11
|
+
accessibilityAction: (event: ISymbioteEvent) => boolean;
|
|
12
|
+
'update:modelValue': (value: number) => boolean;
|
|
13
|
+
'update:value': (value: number) => boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function createSlider(platform: ISliderPlatform): import("vue").DefineSetupFnComponent<ISliderProps, ISliderEmits, {}, Omit<ISliderBaseProps, "onValueChange" | "onSlidingStart" | "onSlidingComplete" | "onAccessibilityAction"> & {
|
|
16
|
+
modelValue?: number;
|
|
17
|
+
class?: IClassNameValue;
|
|
18
|
+
} & {
|
|
19
|
+
onValueChange?: ((value: number) => any) | undefined;
|
|
20
|
+
onSlidingStart?: ((value: number) => any) | undefined;
|
|
21
|
+
onSlidingComplete?: ((value: number) => any) | undefined;
|
|
22
|
+
onAccessibilityAction?: ((event: ISymbioteEvent) => any) | undefined;
|
|
23
|
+
"onUpdate:modelValue"?: ((value: number) => any) | undefined;
|
|
24
|
+
"onUpdate:value"?: ((value: number) => any) | undefined;
|
|
25
|
+
}, import("vue").PublicProps>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Slider, the Vue 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 the future Angular
|
|
3
|
+
// adapter; here Vue supplies the reactivity — a ref tracks the value native last reported (to mark
|
|
4
|
+
// the active step) and a ref the measured width (for the step indicator) — plus the descriptor
|
|
5
|
+
// bridge. The native RNCSlider view carries no symbiote metadata: the engine derives its events
|
|
6
|
+
// and color processors from the library's ViewConfig at runtime. That ViewConfig is registered by
|
|
7
|
+
// the side-effect import in ../register (pulled in by the package barrel, NOT here, so this module
|
|
8
|
+
// and its tests stay free of the third-party spec).
|
|
9
|
+
import { defineComponent, h, ref } from '@vue/runtime-core';
|
|
10
|
+
import { descriptorToVue, normalizeVueAttrs, resolveModelValue, emitModelUpdate, Image, } from '@symbiote-native/vue';
|
|
11
|
+
import { dlog } from '@symbiote-native/engine';
|
|
12
|
+
import { sanitizeSliderValue, resolveSliderDisabled, resolveSliderAccessibilityState, resolveSliderLowerLimit, resolveSliderUpperLimit, valueFromSliderEvent, shouldRenderStepsIndicator, resolveThumbTintColor, shouldPassNativeThumbImage, isInvalidLimitConfig, computeStepOptions, orderStepOptions, stepNumberFontSize, renderSlider, renderSliderNative, 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';
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function asNumber(value) {
|
|
17
|
+
return typeof value === 'number' ? value : undefined;
|
|
18
|
+
}
|
|
19
|
+
function isNumber(value) {
|
|
20
|
+
return typeof value === 'number';
|
|
21
|
+
}
|
|
22
|
+
function asBoolean(value) {
|
|
23
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
function isImageSourceProp(value) {
|
|
26
|
+
return typeof value === 'number' || (typeof value === 'object' && value !== null);
|
|
27
|
+
}
|
|
28
|
+
// accessibilityState arrives untyped; narrow its disabled flag for the fold while preserving the
|
|
29
|
+
// other entries at runtime (the resolve spread carries them onto the native node).
|
|
30
|
+
function asAccessibilityState(value) {
|
|
31
|
+
if (!isRecord(value))
|
|
32
|
+
return undefined;
|
|
33
|
+
return { ...value, disabled: typeof value.disabled === 'boolean' ? value.disabled : undefined };
|
|
34
|
+
}
|
|
35
|
+
// The props/handlers the lifecycle consumes itself; everything else (the track tints + images,
|
|
36
|
+
// thumbSize, tapToSeek, vertical, accessibility*/testID/aria-*) forwards onto the native node.
|
|
37
|
+
// The four callbacks are JS-only and must NEVER reach Fabric, so they are stripped here and
|
|
38
|
+
// re-supplied as the native event handlers in `passthrough`. `class` is handled too: like
|
|
39
|
+
// `style`, it targets the WRAPPER `symbiote-view` (renderSlider's outer element), never the
|
|
40
|
+
// inner native slider leaf `passthrough` reaches — applied explicitly by each render path below.
|
|
41
|
+
const HANDLED_ATTRS = [
|
|
42
|
+
'value',
|
|
43
|
+
'modelValue',
|
|
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
|
+
'onValueChange',
|
|
58
|
+
'onSlidingStart',
|
|
59
|
+
'onSlidingComplete',
|
|
60
|
+
'onAccessibilityAction',
|
|
61
|
+
];
|
|
62
|
+
function forwardAttrs(attrs) {
|
|
63
|
+
const result = {};
|
|
64
|
+
for (const key of Object.keys(attrs)) {
|
|
65
|
+
if (!HANDLED_ATTRS.includes(key))
|
|
66
|
+
result[key] = attrs[key];
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
// renderSlider builds the wrapper `symbiote-view` descriptor itself, with no `class` field of
|
|
71
|
+
// its own (packages/slider/src/core stays framework-agnostic). `class` is stamped onto that
|
|
72
|
+
// returned descriptor's props here, right before the Vue bridge, so it lands on the SAME
|
|
73
|
+
// element `style` targets rather than the inner native leaf `passthrough` would otherwise
|
|
74
|
+
// carry it to (the ImageBackground-style wrapper/inner split).
|
|
75
|
+
function applyWrapperClass(descriptor, classValue) {
|
|
76
|
+
descriptor.props = { ...descriptor.props, class: classValue };
|
|
77
|
+
return descriptor;
|
|
78
|
+
}
|
|
79
|
+
export function createSlider(platform) {
|
|
80
|
+
return defineComponent((_props, { attrs: rawAttrs, slots, emit }) => {
|
|
81
|
+
// The value native last reported, kept only to mark the active step in the indicator. Not
|
|
82
|
+
// the controlled value — the slider is uncontrolled during a drag (no snap-back).
|
|
83
|
+
const reportedValue = ref(undefined);
|
|
84
|
+
// The measured wrapper width the step indicator lays out against; 0 until the first layout.
|
|
85
|
+
const width = ref(0);
|
|
86
|
+
const handleValueChange = (event) => {
|
|
87
|
+
const value = valueFromSliderEvent(event);
|
|
88
|
+
if (value === undefined)
|
|
89
|
+
return;
|
|
90
|
+
reportedValue.value = value;
|
|
91
|
+
emit('valueChange', value);
|
|
92
|
+
emitModelUpdate(emit, value);
|
|
93
|
+
};
|
|
94
|
+
const handleSlidingStart = (event) => {
|
|
95
|
+
const value = valueFromSliderEvent(event);
|
|
96
|
+
if (value !== undefined)
|
|
97
|
+
emit('slidingStart', value);
|
|
98
|
+
};
|
|
99
|
+
const handleSlidingComplete = (event) => {
|
|
100
|
+
const value = valueFromSliderEvent(event);
|
|
101
|
+
if (value !== undefined)
|
|
102
|
+
emit('slidingComplete', value);
|
|
103
|
+
};
|
|
104
|
+
const handleAccessibilityAction = (event) => {
|
|
105
|
+
emit('accessibilityAction', event);
|
|
106
|
+
};
|
|
107
|
+
const handleLayout = (event) => {
|
|
108
|
+
const layout = event.nativeEvent.layout;
|
|
109
|
+
if (isRecord(layout) && typeof layout.width === 'number')
|
|
110
|
+
width.value = layout.width;
|
|
111
|
+
};
|
|
112
|
+
return () => {
|
|
113
|
+
const attrs = normalizeVueAttrs(rawAttrs);
|
|
114
|
+
const minimumValue = asNumber(attrs.minimumValue) ?? SLIDER_DEFAULT_MINIMUM_VALUE;
|
|
115
|
+
const maximumValue = asNumber(attrs.maximumValue) ?? SLIDER_DEFAULT_MAXIMUM_VALUE;
|
|
116
|
+
const step = asNumber(attrs.step) ?? SLIDER_DEFAULT_STEP;
|
|
117
|
+
const lowerLimit = resolveSliderLowerLimit(asNumber(attrs.lowerLimit));
|
|
118
|
+
const upperLimit = resolveSliderUpperLimit(asNumber(attrs.upperLimit));
|
|
119
|
+
if (isInvalidLimitConfig(lowerLimit, upperLimit)) {
|
|
120
|
+
dlog('Slider: lowerLimit must be smaller than upperLimit');
|
|
121
|
+
}
|
|
122
|
+
const disabledAttr = asBoolean(attrs.disabled);
|
|
123
|
+
const accessibilityState = asAccessibilityState(attrs.accessibilityState);
|
|
124
|
+
const inverted = asBoolean(attrs.inverted) ?? false;
|
|
125
|
+
const hasStepMarker = slots.stepMarker !== undefined;
|
|
126
|
+
const renderStepNumber = asBoolean(attrs.renderStepNumber);
|
|
127
|
+
const hasThumbImage = attrs.thumbImage !== undefined;
|
|
128
|
+
const showSteps = shouldRenderStepsIndicator(hasStepMarker, renderStepNumber);
|
|
129
|
+
// The native view gets a thumbImage only when there is one AND no custom marker (which
|
|
130
|
+
// draws its own thumb), matching the library. We pass the source raw — the engine runs the
|
|
131
|
+
// image processor derived from RNCSlider's ViewConfig (same path as the color tints), so
|
|
132
|
+
// unlike the plain-RN library wrapper we do NOT pre-call Image.resolveAssetSource here.
|
|
133
|
+
const nativeThumbImage = shouldPassNativeThumbImage(hasStepMarker, hasThumbImage)
|
|
134
|
+
? attrs.thumbImage
|
|
135
|
+
: undefined;
|
|
136
|
+
const view = {
|
|
137
|
+
value: sanitizeSliderValue(resolveModelValue(attrs, isNumber)),
|
|
138
|
+
minimumValue,
|
|
139
|
+
maximumValue,
|
|
140
|
+
step,
|
|
141
|
+
lowerLimit,
|
|
142
|
+
upperLimit,
|
|
143
|
+
disabled: resolveSliderDisabled(disabledAttr, accessibilityState),
|
|
144
|
+
inverted,
|
|
145
|
+
thumbTintColor: resolveThumbTintColor(attrs.thumbTintColor, hasStepMarker, hasThumbImage),
|
|
146
|
+
thumbImage: nativeThumbImage,
|
|
147
|
+
accessibilityState: resolveSliderAccessibilityState(disabledAttr, accessibilityState),
|
|
148
|
+
width: width.value,
|
|
149
|
+
style: attrs.style,
|
|
150
|
+
passthrough: {
|
|
151
|
+
...forwardAttrs(attrs),
|
|
152
|
+
[SLIDER_ON_CHANGE]: handleValueChange,
|
|
153
|
+
[SLIDER_ON_VALUE_CHANGE]: handleValueChange,
|
|
154
|
+
[SLIDER_ON_SLIDING_START]: handleSlidingStart,
|
|
155
|
+
[SLIDER_ON_SLIDING_COMPLETE]: handleSlidingComplete,
|
|
156
|
+
[SLIDER_ON_ACCESSIBILITY_ACTION]: handleAccessibilityAction,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
// Common case: no step overlay. The full slider (wrapper + native leaf) renders agnostic.
|
|
160
|
+
if (!showSteps) {
|
|
161
|
+
return descriptorToVue(applyWrapperClass(renderSlider(view, platform, { onLayout: handleLayout }), attrs.class));
|
|
162
|
+
}
|
|
163
|
+
const options = computeStepOptions(minimumValue, maximumValue, step, platform.stepResolution);
|
|
164
|
+
const currentValue = reportedValue.value ?? view.value ?? minimumValue;
|
|
165
|
+
// A custom StepMarker is a per-adapter element (a Vue slot), so its overlay is assembled
|
|
166
|
+
// here at the VNode level; the default overlay (numbers / thumbImage only) is the shared
|
|
167
|
+
// agnostic render. Either way the wrapper measures width via onLayout.
|
|
168
|
+
if (hasStepMarker) {
|
|
169
|
+
const overlay = renderCustomStepsOverlay({
|
|
170
|
+
options,
|
|
171
|
+
currentValue,
|
|
172
|
+
minimumValue,
|
|
173
|
+
maximumValue,
|
|
174
|
+
inverted,
|
|
175
|
+
renderStepNumber: renderStepNumber === true,
|
|
176
|
+
thumbImage: attrs.thumbImage,
|
|
177
|
+
width: width.value,
|
|
178
|
+
platform,
|
|
179
|
+
stepMarker: slots.stepMarker,
|
|
180
|
+
});
|
|
181
|
+
return h('symbiote-view', {
|
|
182
|
+
style: resolveStepsWrapperStyle(view.style, platform),
|
|
183
|
+
class: attrs.class,
|
|
184
|
+
onLayout: handleLayout,
|
|
185
|
+
}, [overlay, descriptorToVue(renderSliderNative(view, platform))]);
|
|
186
|
+
}
|
|
187
|
+
const steps = renderStepsIndicator({
|
|
188
|
+
options,
|
|
189
|
+
currentValue,
|
|
190
|
+
width: width.value,
|
|
191
|
+
renderStepNumber: renderStepNumber === true,
|
|
192
|
+
thumbImage: attrs.thumbImage,
|
|
193
|
+
inverted,
|
|
194
|
+
platform,
|
|
195
|
+
});
|
|
196
|
+
return descriptorToVue(applyWrapperClass(renderSlider(view, platform, { steps, onLayout: handleLayout }), attrs.class));
|
|
197
|
+
};
|
|
198
|
+
}, {
|
|
199
|
+
name: 'Slider',
|
|
200
|
+
inheritAttrs: false,
|
|
201
|
+
emits: {
|
|
202
|
+
valueChange: (_value) => true,
|
|
203
|
+
slidingStart: (_value) => true,
|
|
204
|
+
slidingComplete: (_value) => true,
|
|
205
|
+
accessibilityAction: (_event) => true,
|
|
206
|
+
'update:modelValue': (_value) => true,
|
|
207
|
+
'update:value': (_value) => true,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// The wrapper style for the steps path mirrors renderSlider's wrapper (kept here because the
|
|
212
|
+
// custom-marker path builds the wrapper itself rather than going through renderSlider).
|
|
213
|
+
function resolveStepsWrapperStyle(style, platform) {
|
|
214
|
+
return [platform.defaultStyle, style, { justifyContent: 'center' }];
|
|
215
|
+
}
|
|
216
|
+
// The custom-marker overlay: the same flex row as the agnostic default, but each mark hosts the
|
|
217
|
+
// user's `#stepMarker` scoped slot (plus the thumbImage on the current step and the optional step
|
|
218
|
+
// number), exactly as the library's SliderTrackMark composes a StepMarker + Image.
|
|
219
|
+
function renderCustomStepsOverlay(params) {
|
|
220
|
+
const fontSize = stepNumberFontSize(params.options.length);
|
|
221
|
+
const ordered = orderStepOptions(params.options, params.inverted);
|
|
222
|
+
const min = params.options[0];
|
|
223
|
+
const max = params.options[params.options.length - 1];
|
|
224
|
+
const cells = ordered.map((value, index) => {
|
|
225
|
+
const trackChildren = [];
|
|
226
|
+
const marker = params.stepMarker?.({
|
|
227
|
+
stepMarked: value === params.currentValue,
|
|
228
|
+
currentValue: params.currentValue,
|
|
229
|
+
index,
|
|
230
|
+
min,
|
|
231
|
+
max,
|
|
232
|
+
});
|
|
233
|
+
if (marker !== undefined)
|
|
234
|
+
trackChildren.push(h('symbiote-view', {}, marker));
|
|
235
|
+
if (isImageSourceProp(params.thumbImage) && value === params.currentValue) {
|
|
236
|
+
trackChildren.push(h('symbiote-view', { style: THUMB_IMAGE_CONTAINER_STYLE, testID: 'sliderTrackMark-thumbImage' }, [h(Image, { source: params.thumbImage, style: THUMB_IMAGE_STYLE })]));
|
|
237
|
+
}
|
|
238
|
+
const cellChildren = [
|
|
239
|
+
h('symbiote-view', { style: TRACK_MARK_CONTAINER_STYLE }, trackChildren),
|
|
240
|
+
];
|
|
241
|
+
if (params.renderStepNumber) {
|
|
242
|
+
cellChildren.push(h('symbiote-view', { style: STEP_NUMBER_CONTAINER_STYLE }, [
|
|
243
|
+
h('symbiote-text', { testID: `${index}th-step`, style: { fontSize } }, String(value)),
|
|
244
|
+
]));
|
|
245
|
+
}
|
|
246
|
+
return h('symbiote-view', { style: STEP_INDICATOR_ELEMENT_STYLE }, cellChildren);
|
|
247
|
+
});
|
|
248
|
+
return h('symbiote-view', {
|
|
249
|
+
pointerEvents: 'none',
|
|
250
|
+
testID: 'StepsIndicator-Container',
|
|
251
|
+
style: resolveStepsContainerStyle(params.width, params.platform),
|
|
252
|
+
}, cells);
|
|
253
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/slider",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
4
4
|
"description": "Third-party native Slider view wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
"react": ">=19.0.0",
|
|
72
72
|
"react-native": ">=0.86",
|
|
73
73
|
"vue": ">=3.5.0",
|
|
74
|
+
"@symbiote-native/angular": "0.6.1",
|
|
74
75
|
"@symbiote-native/react": "0.2.8",
|
|
75
|
-
"@symbiote-native/vue": "0.3.8"
|
|
76
|
-
"@symbiote-native/angular": "0.6.1"
|
|
76
|
+
"@symbiote-native/vue": "0.3.8"
|
|
77
77
|
},
|
|
78
78
|
"peerDependenciesMeta": {
|
|
79
79
|
"@symbiote-native/angular": {
|
|
@@ -109,13 +109,13 @@
|
|
|
109
109
|
"typescript": "~6.0.0",
|
|
110
110
|
"@symbiote-native/angular": "0.6.1",
|
|
111
111
|
"@symbiote-native/engine": "0.1.7",
|
|
112
|
-
"@symbiote-native/react": "0.2.8",
|
|
113
112
|
"@symbiote-native/test-utils": "0.1.6",
|
|
113
|
+
"@symbiote-native/react": "0.2.8",
|
|
114
114
|
"@symbiote-native/vue": "0.3.8"
|
|
115
115
|
},
|
|
116
116
|
"scripts": {
|
|
117
117
|
"typecheck": "tsc --build",
|
|
118
|
-
"clean": "rm -rf build",
|
|
118
|
+
"clean": "rm -rf build-ngc",
|
|
119
119
|
"ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
|
|
120
120
|
"format": "prettier --write \"src/**/*.{ts,tsx}\""
|
|
121
121
|
}
|