@symbiote-native/slider 0.1.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/LICENSE +21 -0
- package/README.md +90 -0
- 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 +648 -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 +11 -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 +6 -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 +175 -0
- package/build/register.d.ts +1 -0
- package/build/register.js +48 -0
- package/build/vue/index.d.ts +3 -0
- package/build/vue/index.js +8 -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 +254 -0
- package/build-ngc/angular/index.d.ts +3 -0
- package/build-ngc/angular/index.js +3 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/slider/index.android.d.ts +12 -0
- package/build-ngc/angular/slider/index.android.js +28 -0
- package/build-ngc/angular/slider/index.android.js.map +1 -0
- package/build-ngc/angular/slider/index.d.ts +2 -0
- package/build-ngc/angular/slider/index.ios.d.ts +14 -0
- package/build-ngc/angular/slider/index.ios.js +29 -0
- package/build-ngc/angular/slider/index.ios.js.map +1 -0
- package/build-ngc/angular/slider/index.js +4 -0
- package/build-ngc/angular/slider/index.js.map +1 -0
- package/build-ngc/angular/slider/shared.d.ts +87 -0
- package/build-ngc/angular/slider/shared.js +427 -0
- package/build-ngc/angular/slider/shared.js.map +1 -0
- package/build-ngc/core/constants.d.ts +20 -0
- package/build-ngc/core/constants.js +41 -0
- package/build-ngc/core/constants.js.map +1 -0
- package/build-ngc/core/index.d.ts +7 -0
- package/build-ngc/core/index.js +12 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/render-slider.d.ts +11 -0
- package/build-ngc/core/render-slider.js +67 -0
- package/build-ngc/core/render-slider.js.map +1 -0
- package/build-ngc/core/render-steps-indicator.d.ts +28 -0
- package/build-ngc/core/render-steps-indicator.js +92 -0
- package/build-ngc/core/render-steps-indicator.js.map +1 -0
- package/build-ngc/core/slider-props.d.ts +52 -0
- package/build-ngc/core/slider-props.js +7 -0
- package/build-ngc/core/slider-props.js.map +1 -0
- package/build-ngc/core/slider-state.d.ts +15 -0
- package/build-ngc/core/slider-state.js +74 -0
- package/build-ngc/core/slider-state.js.map +1 -0
- package/build-ngc/register.d.ts +1 -0
- package/build-ngc/register.js +49 -0
- package/build-ngc/register.js.map +1 -0
- package/package.json +116 -0
- package/src/angular/index.ts +4 -0
- package/src/angular/slider/index.android.ts +25 -0
- package/src/angular/slider/index.ios.ts +26 -0
- package/src/angular/slider/index.ts +5 -0
- package/src/angular/slider/shared.ts +364 -0
- package/src/angular/slider/slider.test.ts +260 -0
- package/src/core/constants.ts +47 -0
- package/src/core/index.ts +46 -0
- package/src/core/render-slider.ts +89 -0
- package/src/core/render-steps-indicator.ts +154 -0
- package/src/core/slider-props.ts +83 -0
- package/src/core/slider-state.ts +112 -0
- package/src/react/index.ts +11 -0
- package/src/react/slider/index.android.ts +13 -0
- package/src/react/slider/index.ios.ts +14 -0
- package/src/react/slider/index.ts +4 -0
- package/src/react/slider/shared.ts +332 -0
- package/src/react/slider/slider.test.tsx +203 -0
- package/src/register.test.tsx +89 -0
- package/src/register.ts +59 -0
- package/src/vue/index.ts +11 -0
- package/src/vue/slider/index.android.ts +13 -0
- package/src/vue/slider/index.ios.ts +14 -0
- package/src/vue/slider/index.ts +5 -0
- package/src/vue/slider/shared.ts +383 -0
- package/src/vue/slider/slider.test.ts +184 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Slider: the logic half (framework-agnostic, zero render). Unlike Switch, the slider is NOT
|
|
2
|
+
// controlled during a drag — the native view owns the thumb position and reports values up, so
|
|
3
|
+
// there is no snap-back reducer. The shared logic is the pure prop folding the library's React
|
|
4
|
+
// wrapper does before handing props to the native view: value sanitation, disabled/accessibility
|
|
5
|
+
// resolution, limit defaulting, and the step-indicator option layout. Every adapter reuses these.
|
|
6
|
+
|
|
7
|
+
import type { ISymbioteEvent } from '@symbiote-native/engine';
|
|
8
|
+
import { SLIDER_LIMIT_MAX_VALUE, SLIDER_LIMIT_MIN_VALUE } from './constants';
|
|
9
|
+
|
|
10
|
+
// The accessibilityState slice the disabled fold reads/writes. Only `disabled` is typed (the only
|
|
11
|
+
// field the fold inspects) and NO index signature — so RN's IAccessibilityStateValue (which has no
|
|
12
|
+
// index signature) assigns cleanly. Other state fields (selected/checked/…) are preserved at
|
|
13
|
+
// RUNTIME by the resolve spread; the static type just doesn't track them.
|
|
14
|
+
export type ISliderAccessibilityState = {
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Mirror the library's `passedValue`: NaN or a falsy value (incl. 0) becomes undefined, so the
|
|
19
|
+
// native view falls back to its own default initial value rather than receiving NaN/0 as a
|
|
20
|
+
// write. Entered once it still acts as the initial value (the slider is uncontrolled after).
|
|
21
|
+
export function sanitizeSliderValue(value: number | undefined): number | undefined {
|
|
22
|
+
if (value === undefined) return undefined;
|
|
23
|
+
return Number.isNaN(value) || !value ? undefined : value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Resolve the effective disabled flag exactly as the wrapper does: an explicit boolean wins,
|
|
27
|
+
// else it falls back to accessibilityState.disabled.
|
|
28
|
+
export function resolveSliderDisabled(
|
|
29
|
+
disabled: boolean | undefined,
|
|
30
|
+
accessibilityState: ISliderAccessibilityState | undefined,
|
|
31
|
+
): boolean {
|
|
32
|
+
if (typeof disabled === 'boolean') return disabled;
|
|
33
|
+
return accessibilityState?.disabled === true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// When `disabled` is given explicitly, fold it into accessibilityState so the native view and
|
|
37
|
+
// the a11y tree agree; otherwise pass the caller's accessibilityState through untouched.
|
|
38
|
+
export function resolveSliderAccessibilityState(
|
|
39
|
+
disabled: boolean | undefined,
|
|
40
|
+
accessibilityState: ISliderAccessibilityState | undefined,
|
|
41
|
+
): ISliderAccessibilityState | undefined {
|
|
42
|
+
if (typeof disabled === 'boolean') return { ...accessibilityState, disabled };
|
|
43
|
+
return accessibilityState;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Default the slide limits to the unbounded sentinels when unset (the native, non-web case).
|
|
47
|
+
export function resolveSliderLowerLimit(lowerLimit: number | undefined): number {
|
|
48
|
+
return lowerLimit ?? SLIDER_LIMIT_MIN_VALUE;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveSliderUpperLimit(upperLimit: number | undefined): number {
|
|
52
|
+
return upperLimit ?? SLIDER_LIMIT_MAX_VALUE;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The value the native value/sliding events carry as nativeEvent.value. nativeEvent is an
|
|
56
|
+
// untyped Record, so narrow; a non-number payload yields undefined (no callback fired upstream).
|
|
57
|
+
export function valueFromSliderEvent(event: ISymbioteEvent): number | undefined {
|
|
58
|
+
const value = event.nativeEvent.value;
|
|
59
|
+
return typeof value === 'number' ? value : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A step overlay is rendered when the caller supplies a custom StepMarker OR opts into step
|
|
63
|
+
// numbers, mirroring `props.StepMarker || !!props.renderStepNumber`.
|
|
64
|
+
export function shouldRenderStepsIndicator(
|
|
65
|
+
hasStepMarker: boolean,
|
|
66
|
+
renderStepNumber: boolean | undefined,
|
|
67
|
+
): boolean {
|
|
68
|
+
return hasStepMarker || renderStepNumber === true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// With a thumbImage AND a custom StepMarker, the native thumb tint goes transparent so the
|
|
72
|
+
// custom mark shows through; otherwise the caller's thumbTintColor stands. Kept `unknown` (not
|
|
73
|
+
// IColorValue) since a color is an opaque value the engine's derived processor handles — the
|
|
74
|
+
// fold only chooses between the input and the 'transparent' sentinel, never inspects the color.
|
|
75
|
+
export function resolveThumbTintColor(
|
|
76
|
+
thumbTintColor: unknown,
|
|
77
|
+
hasStepMarker: boolean,
|
|
78
|
+
hasThumbImage: boolean,
|
|
79
|
+
): unknown {
|
|
80
|
+
return hasThumbImage && hasStepMarker ? 'transparent' : thumbTintColor;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The native view gets a thumbImage only when there is one AND no custom StepMarker (the marker
|
|
84
|
+
// draws its own thumb); matches `props.StepMarker || !props.thumbImage ? undefined : <image>`.
|
|
85
|
+
// The adapter still resolves the asset source (RN's Image.resolveAssetSource) when this is true.
|
|
86
|
+
export function shouldPassNativeThumbImage(
|
|
87
|
+
hasStepMarker: boolean,
|
|
88
|
+
hasThumbImage: boolean,
|
|
89
|
+
): boolean {
|
|
90
|
+
return hasThumbImage && !hasStepMarker;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The library warns when the limits cross; surfaced as a pure predicate so the adapter logs it.
|
|
94
|
+
export function isInvalidLimitConfig(lowerLimit: number, upperLimit: number): boolean {
|
|
95
|
+
return lowerLimit >= upperLimit;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Lay out the step-indicator option values, mirroring the library's options array. With an
|
|
99
|
+
// explicit step the points run min→max by step; with step 0 they spread `resolution`+1 points
|
|
100
|
+
// evenly across the range. `resolution` is the platform-resolved fallback the adapter supplies
|
|
101
|
+
// (ios 1000 / android 128), keeping this fn platform-invariant. Array.from floors a fractional
|
|
102
|
+
// length, matching the library.
|
|
103
|
+
export function computeStepOptions(
|
|
104
|
+
min: number,
|
|
105
|
+
max: number,
|
|
106
|
+
step: number,
|
|
107
|
+
resolution: number,
|
|
108
|
+
): number[] {
|
|
109
|
+
const stepLength = step !== 0 ? step : (max - min) / resolution;
|
|
110
|
+
const count = (step !== 0 ? (max - min) / step : resolution) + 1;
|
|
111
|
+
return Array.from({ length: count }, (_, index) => min + index * stepLength);
|
|
112
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @symbiote-native/slider/react: the React 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 Slider component), then exposes the
|
|
4
|
+
// platform-split Slider. The public prop type is the agnostic base plus React's StepMarker render
|
|
5
|
+
// component (Vue takes the same marker as a scoped slot). See ADR 0027.
|
|
6
|
+
|
|
7
|
+
import '../register';
|
|
8
|
+
|
|
9
|
+
export { Slider } from './slider';
|
|
10
|
+
export type { ISliderProps } from './slider/shared';
|
|
11
|
+
export type { IStepMarkerProps } from '../core';
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
|
|
4
|
+
import { createSlider } from './shared';
|
|
5
|
+
import { SLIDER_STEP_RESOLUTION_ANDROID } from '../../core';
|
|
6
|
+
|
|
7
|
+
const ANDROID_STEPS_CONTAINER_TOP = 0;
|
|
8
|
+
|
|
9
|
+
export const Slider = createSlider({
|
|
10
|
+
defaultStyle: {},
|
|
11
|
+
stepResolution: SLIDER_STEP_RESOLUTION_ANDROID,
|
|
12
|
+
stepsContainerTop: ANDROID_STEPS_CONTAINER_TOP,
|
|
13
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
|
|
5
|
+
import { createSlider } from './shared';
|
|
6
|
+
import { SLIDER_IOS_DEFAULT_HEIGHT, SLIDER_STEP_RESOLUTION_IOS } from '../../core';
|
|
7
|
+
|
|
8
|
+
const IOS_STEPS_CONTAINER_TOP = 10;
|
|
9
|
+
|
|
10
|
+
export const Slider = createSlider({
|
|
11
|
+
defaultStyle: { height: SLIDER_IOS_DEFAULT_HEIGHT },
|
|
12
|
+
stepResolution: SLIDER_STEP_RESOLUTION_IOS,
|
|
13
|
+
stepsContainerTop: IOS_STEPS_CONTAINER_TOP,
|
|
14
|
+
});
|
|
@@ -0,0 +1,332 @@
|
|
|
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. See CLAUDE.md <third_party_rn_packages_are_react_only>, ADR 0027.
|
|
10
|
+
|
|
11
|
+
import { createElement, forwardRef, useCallback, useEffect, useState } from 'react';
|
|
12
|
+
import type {
|
|
13
|
+
FC,
|
|
14
|
+
ForwardRefExoticComponent,
|
|
15
|
+
PropsWithoutRef,
|
|
16
|
+
ReactElement,
|
|
17
|
+
RefAttributes,
|
|
18
|
+
} from 'react';
|
|
19
|
+
import { descriptorToReact, Image } from '@symbiote-native/react';
|
|
20
|
+
import { resolveAccessibilityProps } from '@symbiote-native/components';
|
|
21
|
+
import type { IDescriptor, IDescriptorChild, IImageSourceProp } from '@symbiote-native/components';
|
|
22
|
+
import { dlog, type ISymbioteEvent, type ISymbioteNode } from '@symbiote-native/engine';
|
|
23
|
+
import {
|
|
24
|
+
sanitizeSliderValue,
|
|
25
|
+
resolveSliderDisabled,
|
|
26
|
+
resolveSliderAccessibilityState,
|
|
27
|
+
resolveSliderLowerLimit,
|
|
28
|
+
resolveSliderUpperLimit,
|
|
29
|
+
valueFromSliderEvent,
|
|
30
|
+
shouldRenderStepsIndicator,
|
|
31
|
+
resolveThumbTintColor,
|
|
32
|
+
shouldPassNativeThumbImage,
|
|
33
|
+
isInvalidLimitConfig,
|
|
34
|
+
computeStepOptions,
|
|
35
|
+
orderStepOptions,
|
|
36
|
+
stepNumberFontSize,
|
|
37
|
+
renderSlider,
|
|
38
|
+
renderSliderNative,
|
|
39
|
+
resolveStepsContainerStyle,
|
|
40
|
+
renderStepsIndicator,
|
|
41
|
+
STEP_INDICATOR_ELEMENT_STYLE,
|
|
42
|
+
TRACK_MARK_CONTAINER_STYLE,
|
|
43
|
+
THUMB_IMAGE_CONTAINER_STYLE,
|
|
44
|
+
THUMB_IMAGE_STYLE,
|
|
45
|
+
STEP_NUMBER_CONTAINER_STYLE,
|
|
46
|
+
SLIDER_DEFAULT_MINIMUM_VALUE,
|
|
47
|
+
SLIDER_DEFAULT_MAXIMUM_VALUE,
|
|
48
|
+
SLIDER_DEFAULT_STEP,
|
|
49
|
+
SLIDER_ON_CHANGE,
|
|
50
|
+
SLIDER_ON_VALUE_CHANGE,
|
|
51
|
+
SLIDER_ON_SLIDING_START,
|
|
52
|
+
SLIDER_ON_SLIDING_COMPLETE,
|
|
53
|
+
SLIDER_ON_ACCESSIBILITY_ACTION,
|
|
54
|
+
type ISliderPlatform,
|
|
55
|
+
type ISliderProps as ISliderBaseProps,
|
|
56
|
+
type ISliderViewProps,
|
|
57
|
+
type IStepMarkerProps,
|
|
58
|
+
} from '../../core';
|
|
59
|
+
|
|
60
|
+
// React's flavored prop type: the agnostic base plus the per-adapter StepMarker — a render
|
|
61
|
+
// component returning a React element, which (per CLAUDE.md <prop_types_split_agnostic_vs_per_adapter>)
|
|
62
|
+
// cannot live in the shared layer. Same name as the agnostic base by the split convention (cf.
|
|
63
|
+
// IPressableProps declared per-adapter); Vue takes the same marker as a `#stepMarker` scoped slot.
|
|
64
|
+
export interface ISliderProps extends ISliderBaseProps {
|
|
65
|
+
StepMarker?: FC<IStepMarkerProps>;
|
|
66
|
+
// Forwarded onto the OUTER wrapper View, like `style` (resolveSliderWrapperStyle) — resolves
|
|
67
|
+
// through the shared style registry. Explicitly destructured below, not left in `...passthrough`,
|
|
68
|
+
// which lands on the INNER native RNCSlider leaf (renderSliderNative) — the same wrapper/inner
|
|
69
|
+
// routing bug ImageBackground was fixed for.
|
|
70
|
+
className?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type ISliderComponent = ForwardRefExoticComponent<
|
|
74
|
+
PropsWithoutRef<ISliderProps> & RefAttributes<ISymbioteNode>
|
|
75
|
+
>;
|
|
76
|
+
|
|
77
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
78
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toChild(child: IDescriptorChild): ReactElement | string {
|
|
82
|
+
return typeof child === 'string' ? child : descriptorToReact(child);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// renderSlider's Descriptor IS the outer wrapper (symbiote-view hosting the native leaf +
|
|
86
|
+
// optional steps overlay); className is reapplied on it here, exactly like ImageBackground's
|
|
87
|
+
// wrapper.
|
|
88
|
+
function withClassName(descriptor: IDescriptor, className: string | undefined): ReactElement {
|
|
89
|
+
return createElement(
|
|
90
|
+
descriptor.type,
|
|
91
|
+
{ key: descriptor.key, ...descriptor.props, className },
|
|
92
|
+
...descriptor.children.map(toChild),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createSlider(platform: ISliderPlatform): ISliderComponent {
|
|
97
|
+
return forwardRef<ISymbioteNode, ISliderProps>((rawProps, forwardedRef) => {
|
|
98
|
+
// Slider owns its host wrapper rather than rendering through a symbiote View, so it folds
|
|
99
|
+
// aria/role into accessibility* here; the rest (testID, accessibilityLabel, accessibility-
|
|
100
|
+
// Actions, …) rides to the native node via `passthrough`.
|
|
101
|
+
const props = resolveAccessibilityProps(rawProps);
|
|
102
|
+
const {
|
|
103
|
+
value,
|
|
104
|
+
minimumValue,
|
|
105
|
+
maximumValue,
|
|
106
|
+
step,
|
|
107
|
+
lowerLimit,
|
|
108
|
+
upperLimit,
|
|
109
|
+
disabled,
|
|
110
|
+
inverted,
|
|
111
|
+
thumbTintColor,
|
|
112
|
+
thumbImage,
|
|
113
|
+
accessibilityState,
|
|
114
|
+
renderStepNumber,
|
|
115
|
+
style,
|
|
116
|
+
className,
|
|
117
|
+
StepMarker,
|
|
118
|
+
onValueChange,
|
|
119
|
+
onSlidingStart,
|
|
120
|
+
onSlidingComplete,
|
|
121
|
+
onAccessibilityAction,
|
|
122
|
+
...passthrough
|
|
123
|
+
} = props;
|
|
124
|
+
|
|
125
|
+
const [reportedValue, setReportedValue] = useState<number | undefined>(undefined);
|
|
126
|
+
const [width, setWidth] = useState(0);
|
|
127
|
+
|
|
128
|
+
const handleValueChange = useCallback(
|
|
129
|
+
(event: ISymbioteEvent): void => {
|
|
130
|
+
const next = valueFromSliderEvent(event);
|
|
131
|
+
if (next === undefined) return;
|
|
132
|
+
setReportedValue(next);
|
|
133
|
+
onValueChange?.(next);
|
|
134
|
+
},
|
|
135
|
+
[onValueChange],
|
|
136
|
+
);
|
|
137
|
+
const handleSlidingStart = useCallback(
|
|
138
|
+
(event: ISymbioteEvent): void => {
|
|
139
|
+
const next = valueFromSliderEvent(event);
|
|
140
|
+
if (next !== undefined) onSlidingStart?.(next);
|
|
141
|
+
},
|
|
142
|
+
[onSlidingStart],
|
|
143
|
+
);
|
|
144
|
+
const handleSlidingComplete = useCallback(
|
|
145
|
+
(event: ISymbioteEvent): void => {
|
|
146
|
+
const next = valueFromSliderEvent(event);
|
|
147
|
+
if (next !== undefined) onSlidingComplete?.(next);
|
|
148
|
+
},
|
|
149
|
+
[onSlidingComplete],
|
|
150
|
+
);
|
|
151
|
+
const handleAccessibilityAction = useCallback(
|
|
152
|
+
(event: ISymbioteEvent): void => {
|
|
153
|
+
onAccessibilityAction?.(event);
|
|
154
|
+
},
|
|
155
|
+
[onAccessibilityAction],
|
|
156
|
+
);
|
|
157
|
+
const handleLayout = useCallback((event: ISymbioteEvent): void => {
|
|
158
|
+
const layout = event.nativeEvent.layout;
|
|
159
|
+
if (isRecord(layout) && typeof layout.width === 'number') setWidth(layout.width);
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
const minimum = minimumValue ?? SLIDER_DEFAULT_MINIMUM_VALUE;
|
|
163
|
+
const maximum = maximumValue ?? SLIDER_DEFAULT_MAXIMUM_VALUE;
|
|
164
|
+
const stepValue = step ?? SLIDER_DEFAULT_STEP;
|
|
165
|
+
const lower = resolveSliderLowerLimit(lowerLimit);
|
|
166
|
+
const upper = resolveSliderUpperLimit(upperLimit);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (isInvalidLimitConfig(lower, upper))
|
|
170
|
+
dlog('Slider: lowerLimit must be smaller than upperLimit');
|
|
171
|
+
}, [lower, upper]);
|
|
172
|
+
|
|
173
|
+
const hasStepMarker = StepMarker !== undefined;
|
|
174
|
+
const hasThumbImage = thumbImage !== undefined;
|
|
175
|
+
const showSteps = shouldRenderStepsIndicator(hasStepMarker, renderStepNumber);
|
|
176
|
+
// Passed to the native view raw — the engine runs the image processor derived from RNCSlider's
|
|
177
|
+
// ViewConfig — and only when no custom marker draws its own thumb (matches the library).
|
|
178
|
+
const nativeThumbImage = shouldPassNativeThumbImage(hasStepMarker, hasThumbImage)
|
|
179
|
+
? thumbImage
|
|
180
|
+
: undefined;
|
|
181
|
+
|
|
182
|
+
const view: ISliderViewProps = {
|
|
183
|
+
value: sanitizeSliderValue(value),
|
|
184
|
+
minimumValue: minimum,
|
|
185
|
+
maximumValue: maximum,
|
|
186
|
+
step: stepValue,
|
|
187
|
+
lowerLimit: lower,
|
|
188
|
+
upperLimit: upper,
|
|
189
|
+
disabled: resolveSliderDisabled(disabled, accessibilityState),
|
|
190
|
+
inverted: inverted ?? false,
|
|
191
|
+
thumbTintColor: resolveThumbTintColor(thumbTintColor, hasStepMarker, hasThumbImage),
|
|
192
|
+
thumbImage: nativeThumbImage,
|
|
193
|
+
accessibilityState: resolveSliderAccessibilityState(disabled, accessibilityState),
|
|
194
|
+
width,
|
|
195
|
+
style,
|
|
196
|
+
passthrough: {
|
|
197
|
+
...passthrough,
|
|
198
|
+
ref: forwardedRef,
|
|
199
|
+
[SLIDER_ON_CHANGE]: handleValueChange,
|
|
200
|
+
[SLIDER_ON_VALUE_CHANGE]: handleValueChange,
|
|
201
|
+
[SLIDER_ON_SLIDING_START]: handleSlidingStart,
|
|
202
|
+
[SLIDER_ON_SLIDING_COMPLETE]: handleSlidingComplete,
|
|
203
|
+
[SLIDER_ON_ACCESSIBILITY_ACTION]: handleAccessibilityAction,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
if (!showSteps) {
|
|
208
|
+
return withClassName(renderSlider(view, platform, { onLayout: handleLayout }), className);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const options = computeStepOptions(minimum, maximum, stepValue, platform.stepResolution);
|
|
212
|
+
const currentValue = reportedValue ?? view.value ?? minimum;
|
|
213
|
+
|
|
214
|
+
if (StepMarker !== undefined) {
|
|
215
|
+
const overlay = renderCustomStepsOverlay({
|
|
216
|
+
options,
|
|
217
|
+
currentValue,
|
|
218
|
+
inverted: view.inverted,
|
|
219
|
+
renderStepNumber: renderStepNumber === true,
|
|
220
|
+
thumbImage,
|
|
221
|
+
width,
|
|
222
|
+
platform,
|
|
223
|
+
StepMarker,
|
|
224
|
+
});
|
|
225
|
+
return createElement(
|
|
226
|
+
'symbiote-view',
|
|
227
|
+
{ style: resolveStepsWrapperStyle(style, platform), onLayout: handleLayout, className },
|
|
228
|
+
overlay,
|
|
229
|
+
descriptorToReact(renderSliderNative(view, platform)),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const steps = renderStepsIndicator({
|
|
234
|
+
options,
|
|
235
|
+
currentValue,
|
|
236
|
+
width,
|
|
237
|
+
renderStepNumber: renderStepNumber === true,
|
|
238
|
+
thumbImage,
|
|
239
|
+
inverted: view.inverted,
|
|
240
|
+
platform,
|
|
241
|
+
});
|
|
242
|
+
return withClassName(
|
|
243
|
+
renderSlider(view, platform, { steps, onLayout: handleLayout }),
|
|
244
|
+
className,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// The wrapper style for the custom-marker steps path mirrors renderSlider's wrapper.
|
|
250
|
+
function resolveStepsWrapperStyle(style: unknown, platform: ISliderPlatform): unknown {
|
|
251
|
+
return [platform.defaultStyle, style, { justifyContent: 'center' }];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
type ICustomStepsParams = {
|
|
255
|
+
options: readonly number[];
|
|
256
|
+
currentValue: number;
|
|
257
|
+
inverted: boolean;
|
|
258
|
+
renderStepNumber: boolean;
|
|
259
|
+
thumbImage: IImageSourceProp | undefined;
|
|
260
|
+
width: number;
|
|
261
|
+
platform: ISliderPlatform;
|
|
262
|
+
StepMarker: FC<IStepMarkerProps>;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// The custom-marker overlay: the same flex row as the agnostic default, each mark hosting the
|
|
266
|
+
// user's StepMarker element (plus the thumbImage on the current step and the optional step
|
|
267
|
+
// number), mirroring the library's SliderTrackMark composition.
|
|
268
|
+
function renderCustomStepsOverlay(params: ICustomStepsParams): ReactElement {
|
|
269
|
+
const fontSize = stepNumberFontSize(params.options.length);
|
|
270
|
+
const ordered = orderStepOptions(params.options, params.inverted);
|
|
271
|
+
const min = params.options[0];
|
|
272
|
+
const max = params.options[params.options.length - 1];
|
|
273
|
+
const cells = ordered.map((value, index) => {
|
|
274
|
+
const trackChildren: ReactElement[] = [
|
|
275
|
+
createElement(params.StepMarker, {
|
|
276
|
+
key: 'marker',
|
|
277
|
+
stepMarked: value === params.currentValue,
|
|
278
|
+
currentValue: params.currentValue,
|
|
279
|
+
index,
|
|
280
|
+
min,
|
|
281
|
+
max,
|
|
282
|
+
}),
|
|
283
|
+
];
|
|
284
|
+
if (params.thumbImage !== undefined && value === params.currentValue) {
|
|
285
|
+
trackChildren.push(
|
|
286
|
+
createElement(
|
|
287
|
+
'symbiote-view',
|
|
288
|
+
{
|
|
289
|
+
key: 'thumb',
|
|
290
|
+
style: THUMB_IMAGE_CONTAINER_STYLE,
|
|
291
|
+
testID: 'sliderTrackMark-thumbImage',
|
|
292
|
+
},
|
|
293
|
+
createElement(Image, { source: params.thumbImage, style: THUMB_IMAGE_STYLE }),
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const cellChildren: ReactElement[] = [
|
|
298
|
+
createElement(
|
|
299
|
+
'symbiote-view',
|
|
300
|
+
{ key: 'track', style: TRACK_MARK_CONTAINER_STYLE },
|
|
301
|
+
...trackChildren,
|
|
302
|
+
),
|
|
303
|
+
];
|
|
304
|
+
if (params.renderStepNumber) {
|
|
305
|
+
cellChildren.push(
|
|
306
|
+
createElement(
|
|
307
|
+
'symbiote-view',
|
|
308
|
+
{ key: 'number', style: STEP_NUMBER_CONTAINER_STYLE },
|
|
309
|
+
createElement(
|
|
310
|
+
'symbiote-text',
|
|
311
|
+
{ testID: `${index}th-step`, style: { fontSize } },
|
|
312
|
+
String(value),
|
|
313
|
+
),
|
|
314
|
+
),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return createElement(
|
|
318
|
+
'symbiote-view',
|
|
319
|
+
{ key: index, style: STEP_INDICATOR_ELEMENT_STYLE },
|
|
320
|
+
...cellChildren,
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
return createElement(
|
|
324
|
+
'symbiote-view',
|
|
325
|
+
{
|
|
326
|
+
pointerEvents: 'none',
|
|
327
|
+
testID: 'StepsIndicator-Container',
|
|
328
|
+
style: resolveStepsContainerStyle(params.width, params.platform),
|
|
329
|
+
},
|
|
330
|
+
...cells,
|
|
331
|
+
);
|
|
332
|
+
}
|