jfs-components 0.1.2 → 0.1.8
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/CHANGELOG.md +29 -0
- package/lib/commonjs/components/AmountInput/AmountInput.js +8 -5
- package/lib/commonjs/components/BenefitCard/BenefitCard.js +231 -0
- package/lib/commonjs/components/CcCard/CcCard.js +470 -0
- package/lib/commonjs/components/Checkbox/Checkbox.js +4 -3
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +4 -3
- package/lib/commonjs/components/CompareTable/CompareTable.js +372 -0
- package/lib/commonjs/components/ComparisonBar/ComparisonBar.js +266 -0
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +35 -3
- package/lib/commonjs/components/FormField/FormField.js +4 -3
- package/lib/commonjs/components/InputSearch/InputSearch.js +6 -4
- package/lib/commonjs/components/NoteInput/NoteInput.js +6 -5
- package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +273 -0
- package/lib/commonjs/components/ProductMerchandisingCard/GlassFill.js +263 -0
- package/lib/commonjs/components/ProductMerchandisingCard/GlassFill.web.js +116 -0
- package/lib/commonjs/components/ProductMerchandisingCard/ProductMerchandisingCard.js +353 -0
- package/lib/commonjs/components/ProjectionMarker/ProjectionMarker.js +161 -0
- package/lib/commonjs/components/Radio/Radio.js +5 -5
- package/lib/commonjs/components/Slider/Slider.js +473 -0
- package/lib/commonjs/components/TextInput/TextInput.js +13 -8
- package/lib/commonjs/components/TextSegment/TextSegment.js +118 -0
- package/lib/commonjs/components/index.js +63 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +38 -9
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/utils/react-utils.js +22 -0
- package/lib/module/components/AmountInput/AmountInput.js +6 -4
- package/lib/module/components/BenefitCard/BenefitCard.js +225 -0
- package/lib/module/components/CcCard/CcCard.js +464 -0
- package/lib/module/components/Checkbox/Checkbox.js +5 -4
- package/lib/module/components/CheckboxItem/CheckboxItem.js +5 -4
- package/lib/module/components/CompareTable/CompareTable.js +367 -0
- package/lib/module/components/ComparisonBar/ComparisonBar.js +260 -0
- package/lib/module/components/DropdownInput/DropdownInput.js +36 -4
- package/lib/module/components/FormField/FormField.js +5 -4
- package/lib/module/components/InputSearch/InputSearch.js +6 -4
- package/lib/module/components/NoteInput/NoteInput.js +7 -6
- package/lib/module/components/PdpCcCard/PdpCcCard.js +267 -0
- package/lib/module/components/ProductMerchandisingCard/GlassFill.js +257 -0
- package/lib/module/components/ProductMerchandisingCard/GlassFill.web.js +111 -0
- package/lib/module/components/ProductMerchandisingCard/ProductMerchandisingCard.js +347 -0
- package/lib/module/components/ProjectionMarker/ProjectionMarker.js +156 -0
- package/lib/module/components/Radio/Radio.js +5 -4
- package/lib/module/components/Slider/Slider.js +468 -0
- package/lib/module/components/TextInput/TextInput.js +15 -10
- package/lib/module/components/TextSegment/TextSegment.js +113 -0
- package/lib/module/components/index.js +9 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/design-tokens/figma-modes.generated.js +38 -9
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/utils/react-utils.js +21 -0
- package/lib/typescript/src/components/AmountInput/AmountInput.d.ts +3 -2
- package/lib/typescript/src/components/BenefitCard/BenefitCard.d.ts +93 -0
- package/lib/typescript/src/components/CcCard/CcCard.d.ts +137 -0
- package/lib/typescript/src/components/Checkbox/Checkbox.d.ts +3 -2
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +2 -2
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +88 -0
- package/lib/typescript/src/components/ComparisonBar/ComparisonBar.d.ts +118 -0
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +20 -1
- package/lib/typescript/src/components/FormField/FormField.d.ts +2 -2
- package/lib/typescript/src/components/InputSearch/InputSearch.d.ts +23 -2
- package/lib/typescript/src/components/NoteInput/NoteInput.d.ts +19 -2
- package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +84 -0
- package/lib/typescript/src/components/ProductMerchandisingCard/GlassFill.d.ts +56 -0
- package/lib/typescript/src/components/ProductMerchandisingCard/GlassFill.web.d.ts +27 -0
- package/lib/typescript/src/components/ProductMerchandisingCard/ProductMerchandisingCard.d.ts +81 -0
- package/lib/typescript/src/components/ProjectionMarker/ProjectionMarker.d.ts +82 -0
- package/lib/typescript/src/components/Radio/Radio.d.ts +3 -2
- package/lib/typescript/src/components/RadioButton/RadioButton.d.ts +2 -2
- package/lib/typescript/src/components/Slider/Slider.d.ts +99 -0
- package/lib/typescript/src/components/TextInput/TextInput.d.ts +9 -29
- package/lib/typescript/src/components/TextSegment/TextSegment.d.ts +100 -0
- package/lib/typescript/src/components/index.d.ts +10 -1
- package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +22 -2
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/utils/react-utils.d.ts +10 -0
- package/package.json +2 -1
- package/src/components/AmountInput/AmountInput.tsx +7 -5
- package/src/components/BenefitCard/BenefitCard.tsx +309 -0
- package/src/components/CcCard/CcCard.tsx +598 -0
- package/src/components/Checkbox/Checkbox.tsx +5 -4
- package/src/components/CheckboxItem/CheckboxItem.tsx +5 -4
- package/src/components/CompareTable/CompareTable.tsx +477 -0
- package/src/components/ComparisonBar/ComparisonBar.tsx +356 -0
- package/src/components/DropdownInput/DropdownInput.tsx +55 -3
- package/src/components/FormField/FormField.tsx +5 -4
- package/src/components/InputSearch/InputSearch.tsx +8 -5
- package/src/components/NoteInput/NoteInput.tsx +8 -6
- package/src/components/PdpCcCard/PdpCcCard.tsx +356 -0
- package/src/components/ProductMerchandisingCard/GlassFill.tsx +276 -0
- package/src/components/ProductMerchandisingCard/GlassFill.web.tsx +127 -0
- package/src/components/ProductMerchandisingCard/ProductMerchandisingCard.tsx +423 -0
- package/src/components/ProjectionMarker/ProjectionMarker.tsx +277 -0
- package/src/components/Radio/Radio.tsx +5 -4
- package/src/components/Slider/Slider.tsx +628 -0
- package/src/components/TextInput/TextInput.tsx +15 -11
- package/src/components/TextSegment/TextSegment.tsx +166 -0
- package/src/components/index.ts +10 -1
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/design-tokens/figma-modes.generated.ts +38 -9
- package/src/icons/registry.ts +1 -1
- package/src/utils/react-utils.ts +23 -0
- package/lib/typescript/scripts/extract-component-tokens.d.ts +0 -9
- package/lib/typescript/scripts/generate-component-docs.d.ts +0 -9
- package/lib/typescript/scripts/generate-icon-registry.d.ts +0 -3
- package/lib/typescript/scripts/generate-mode-types.d.ts +0 -2
- package/lib/typescript/scripts/retype-modes.d.cts +0 -2
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import {
|
|
10
|
+
View,
|
|
11
|
+
Text,
|
|
12
|
+
PanResponder,
|
|
13
|
+
Platform,
|
|
14
|
+
type LayoutChangeEvent,
|
|
15
|
+
type GestureResponderEvent,
|
|
16
|
+
type StyleProp,
|
|
17
|
+
type ViewStyle,
|
|
18
|
+
type TextStyle,
|
|
19
|
+
} from 'react-native'
|
|
20
|
+
import Svg, { Path } from 'react-native-svg'
|
|
21
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
22
|
+
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
23
|
+
import type { Modes } from '../../design-tokens'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Imperative handle exposed via `ref`. The primary way to read a slider's value
|
|
27
|
+
* is still the controlled `value` + `onChange` pair (each `<Slider />` reports
|
|
28
|
+
* its own value, so multiple sliders are disambiguated by their own handlers).
|
|
29
|
+
* The ref is a convenience for reading the latest value on demand (e.g. on a
|
|
30
|
+
* button press) or imperatively nudging an uncontrolled slider.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const slider = useRef<SliderHandle>(null)
|
|
34
|
+
* slider.current?.setValue(50)
|
|
35
|
+
* const v = slider.current?.getValue()
|
|
36
|
+
*/
|
|
37
|
+
export interface SliderHandle {
|
|
38
|
+
/** Returns the slider's current (clamped, step-snapped) value. */
|
|
39
|
+
getValue: () => number
|
|
40
|
+
/** Sets the value. Fires `onChange` + `onChangeEnd`. */
|
|
41
|
+
setValue: (value: number) => void
|
|
42
|
+
/** Steps the value up by one `step` (or `by`, snapped to `step`). */
|
|
43
|
+
increment: (by?: number) => void
|
|
44
|
+
/** Steps the value down by one `step` (or `by`, snapped to `step`). */
|
|
45
|
+
decrement: (by?: number) => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SliderProps {
|
|
49
|
+
/** Current value (controlled). Pair with `onChange`. */
|
|
50
|
+
value?: number
|
|
51
|
+
/** Initial value (uncontrolled). Defaults to `minValue`. */
|
|
52
|
+
defaultValue?: number
|
|
53
|
+
/** Called continuously while the value changes (drag / keypress / track press). */
|
|
54
|
+
onChange?: (value: number) => void
|
|
55
|
+
/** Called once when the interaction ends (drag release / keypress). */
|
|
56
|
+
onChangeEnd?: (value: number) => void
|
|
57
|
+
/** Minimum selectable value. Defaults to `0`. */
|
|
58
|
+
minValue?: number
|
|
59
|
+
/** Maximum selectable value. Defaults to `100`. */
|
|
60
|
+
maxValue?: number
|
|
61
|
+
/** Snap increment. Defaults to `1`. Use a smaller value for finer control. */
|
|
62
|
+
step?: number
|
|
63
|
+
/** Disables interaction and dims the control. */
|
|
64
|
+
isDisabled?: boolean
|
|
65
|
+
/**
|
|
66
|
+
* `Intl.NumberFormat` options used to format the value bubble and the
|
|
67
|
+
* min/max labels (e.g. `{ style: 'currency', currency: 'USD' }`).
|
|
68
|
+
*/
|
|
69
|
+
formatOptions?: Intl.NumberFormatOptions
|
|
70
|
+
/** BCP-47 locale used with `formatOptions`. Defaults to the runtime locale. */
|
|
71
|
+
locale?: string
|
|
72
|
+
/**
|
|
73
|
+
* Full override for value formatting. Takes precedence over `formatOptions`.
|
|
74
|
+
* Receives the raw numeric value, returns the string to display.
|
|
75
|
+
*/
|
|
76
|
+
formatValue?: (value: number) => string
|
|
77
|
+
/** Renders fully custom value-bubble content. Takes precedence over `formatValue`. */
|
|
78
|
+
renderTooltip?: (value: number) => React.ReactNode
|
|
79
|
+
/**
|
|
80
|
+
* When `true` (default, matches the Figma design) the value bubble is
|
|
81
|
+
* always visible. When `false` it behaves like a normal tooltip — hidden at
|
|
82
|
+
* rest and revealed only while the user interacts (dragging on touch, or
|
|
83
|
+
* hovering/dragging on web), then dismissed as soon as the finger lifts.
|
|
84
|
+
*
|
|
85
|
+
* Either way the bubble floats above the track and never occupies layout
|
|
86
|
+
* space, so it will overlap content above the slider — leave room for it.
|
|
87
|
+
*/
|
|
88
|
+
alwaysShowTooltip?: boolean
|
|
89
|
+
/** Toggles the min/max labels below the track. Defaults to `true`. */
|
|
90
|
+
showLabels?: boolean
|
|
91
|
+
/** Override for the left (min) label. Defaults to the formatted `minValue`. */
|
|
92
|
+
minLabel?: React.ReactNode
|
|
93
|
+
/** Override for the right (max) label. Defaults to the formatted `maxValue`. */
|
|
94
|
+
maxLabel?: React.ReactNode
|
|
95
|
+
/** Slider width. Defaults to `'100%'` so it fills its parent. */
|
|
96
|
+
width?: number | `${number}%`
|
|
97
|
+
/** Design-token modes for theming. */
|
|
98
|
+
modes?: Modes
|
|
99
|
+
/** Style override for the outer container. */
|
|
100
|
+
style?: StyleProp<ViewStyle>
|
|
101
|
+
/** Accessibility label for screen readers. */
|
|
102
|
+
accessibilityLabel?: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const clamp = (n: number, min: number, max: number) => Math.min(Math.max(n, min), max)
|
|
106
|
+
|
|
107
|
+
const asNum = (raw: unknown, fallback: number): number => {
|
|
108
|
+
const n = typeof raw === 'number' ? raw : parseFloat(raw as string)
|
|
109
|
+
return Number.isFinite(n) ? n : fallback
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const asStr = (raw: unknown, fallback: string): string => (raw != null ? String(raw) : fallback)
|
|
113
|
+
|
|
114
|
+
// The unfilled track is rendered at a lighter emphasis in the design.
|
|
115
|
+
const TRACK_EMPHASIS: Modes = { Emphasis: 'Low' }
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Slider — Figma node 5373:446 ("Slider").
|
|
119
|
+
*
|
|
120
|
+
* A horizontal, single-thumb slider driven entirely by design tokens. It maps a
|
|
121
|
+
* numeric range (`minValue`–`maxValue`) onto a track with a filled indicator, a
|
|
122
|
+
* draggable handle, a value bubble pinned above the handle, and optional min/max
|
|
123
|
+
* labels beneath the track. Built on `PanResponder` so the one code path works
|
|
124
|
+
* on iOS, Android and the web.
|
|
125
|
+
*
|
|
126
|
+
* The design only labels the range bounds, but a slider's purpose is to bind a
|
|
127
|
+
* value — so the live value is surfaced through the bubble (formatted via
|
|
128
|
+
* `formatOptions` / `formatValue`) and reported through `onChange` /
|
|
129
|
+
* `onChangeEnd`. Supports controlled and uncontrolled usage, web keyboard
|
|
130
|
+
* control, and an imperative {@link SliderHandle} `ref` for on-demand reads.
|
|
131
|
+
*/
|
|
132
|
+
const Slider = forwardRef<SliderHandle, SliderProps>(function Slider(
|
|
133
|
+
{
|
|
134
|
+
value: controlledValue,
|
|
135
|
+
defaultValue,
|
|
136
|
+
onChange,
|
|
137
|
+
onChangeEnd,
|
|
138
|
+
minValue = 0,
|
|
139
|
+
maxValue = 100,
|
|
140
|
+
step = 1,
|
|
141
|
+
isDisabled = false,
|
|
142
|
+
formatOptions,
|
|
143
|
+
locale,
|
|
144
|
+
formatValue,
|
|
145
|
+
renderTooltip,
|
|
146
|
+
alwaysShowTooltip = true,
|
|
147
|
+
showLabels = true,
|
|
148
|
+
minLabel,
|
|
149
|
+
maxLabel,
|
|
150
|
+
width = '100%',
|
|
151
|
+
modes = EMPTY_MODES,
|
|
152
|
+
style,
|
|
153
|
+
accessibilityLabel,
|
|
154
|
+
},
|
|
155
|
+
ref
|
|
156
|
+
) {
|
|
157
|
+
const tokens = useMemo(() => resolveTokens(modes), [modes])
|
|
158
|
+
|
|
159
|
+
const isControlled = controlledValue !== undefined
|
|
160
|
+
const [internalValue, setInternalValue] = useState(() =>
|
|
161
|
+
clamp(defaultValue ?? minValue, minValue, maxValue)
|
|
162
|
+
)
|
|
163
|
+
const rawValue = isControlled ? (controlledValue as number) : internalValue
|
|
164
|
+
const currentValue = clamp(rawValue, minValue, maxValue)
|
|
165
|
+
|
|
166
|
+
const [trackWidth, setTrackWidth] = useState(0)
|
|
167
|
+
const [tooltipWidth, setTooltipWidth] = useState(0)
|
|
168
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
169
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
170
|
+
|
|
171
|
+
const quantize = useCallback(
|
|
172
|
+
(v: number) => {
|
|
173
|
+
if (!step || step <= 0) return clamp(v, minValue, maxValue)
|
|
174
|
+
const stepped = Math.round((v - minValue) / step) * step + minValue
|
|
175
|
+
// Round away float dust introduced by the division (e.g. 0.30000000000004).
|
|
176
|
+
const decimals = (String(step).split('.')[1] || '').length
|
|
177
|
+
const fixed = decimals > 0 ? Number(stepped.toFixed(decimals)) : stepped
|
|
178
|
+
return clamp(fixed, minValue, maxValue)
|
|
179
|
+
},
|
|
180
|
+
[minValue, maxValue, step]
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
// Keep the latest interactive config in a ref so the PanResponder (created
|
|
184
|
+
// once) always reads fresh values without being recreated mid-gesture.
|
|
185
|
+
const interaction = useRef({
|
|
186
|
+
trackWidth,
|
|
187
|
+
minValue,
|
|
188
|
+
maxValue,
|
|
189
|
+
isDisabled,
|
|
190
|
+
isControlled,
|
|
191
|
+
quantize,
|
|
192
|
+
onChange,
|
|
193
|
+
onChangeEnd,
|
|
194
|
+
})
|
|
195
|
+
interaction.current = {
|
|
196
|
+
trackWidth,
|
|
197
|
+
minValue,
|
|
198
|
+
maxValue,
|
|
199
|
+
isDisabled,
|
|
200
|
+
isControlled,
|
|
201
|
+
quantize,
|
|
202
|
+
onChange,
|
|
203
|
+
onChangeEnd,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const lastValueRef = useRef(currentValue)
|
|
207
|
+
lastValueRef.current = currentValue
|
|
208
|
+
|
|
209
|
+
const commit = useCallback((next: number, isFinal: boolean) => {
|
|
210
|
+
const { isControlled: ctrl, onChange: change, onChangeEnd: changeEnd } = interaction.current
|
|
211
|
+
const changed = next !== lastValueRef.current
|
|
212
|
+
lastValueRef.current = next
|
|
213
|
+
if (changed) {
|
|
214
|
+
if (!ctrl) setInternalValue(next)
|
|
215
|
+
change?.(next)
|
|
216
|
+
}
|
|
217
|
+
if (isFinal) changeEnd?.(next)
|
|
218
|
+
}, [])
|
|
219
|
+
|
|
220
|
+
const valueFromX = useCallback((x: number): number => {
|
|
221
|
+
const { trackWidth: w, minValue: min, maxValue: max, quantize: q } = interaction.current
|
|
222
|
+
if (w <= 0) return lastValueRef.current
|
|
223
|
+
const ratio = clamp(x / w, 0, 1)
|
|
224
|
+
return q(min + ratio * (max - min))
|
|
225
|
+
}, [])
|
|
226
|
+
|
|
227
|
+
const panResponder = useRef(
|
|
228
|
+
PanResponder.create({
|
|
229
|
+
onStartShouldSetPanResponder: () => !interaction.current.isDisabled,
|
|
230
|
+
onMoveShouldSetPanResponder: () => !interaction.current.isDisabled,
|
|
231
|
+
onPanResponderGrant: (e: GestureResponderEvent) => {
|
|
232
|
+
if (interaction.current.isDisabled) return
|
|
233
|
+
setIsDragging(true)
|
|
234
|
+
commit(valueFromX(e.nativeEvent.locationX), false)
|
|
235
|
+
},
|
|
236
|
+
onPanResponderMove: (e: GestureResponderEvent) => {
|
|
237
|
+
if (interaction.current.isDisabled) return
|
|
238
|
+
commit(valueFromX(e.nativeEvent.locationX), false)
|
|
239
|
+
},
|
|
240
|
+
onPanResponderRelease: (e: GestureResponderEvent) => {
|
|
241
|
+
setIsDragging(false)
|
|
242
|
+
if (interaction.current.isDisabled) return
|
|
243
|
+
commit(valueFromX(e.nativeEvent.locationX), true)
|
|
244
|
+
},
|
|
245
|
+
onPanResponderTerminate: () => {
|
|
246
|
+
setIsDragging(false)
|
|
247
|
+
if (interaction.current.isDisabled) return
|
|
248
|
+
commit(lastValueRef.current, true)
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
).current
|
|
252
|
+
|
|
253
|
+
// Keyboard support (web). Arrow/Page keys nudge, Home/End jump to bounds.
|
|
254
|
+
const handleKeyDown = useCallback(
|
|
255
|
+
(e: any) => {
|
|
256
|
+
if (isDisabled) return
|
|
257
|
+
const big = Math.max(step, (maxValue - minValue) / 10)
|
|
258
|
+
let next: number | null = null
|
|
259
|
+
switch (e.key) {
|
|
260
|
+
case 'ArrowRight':
|
|
261
|
+
case 'ArrowUp':
|
|
262
|
+
next = quantize(currentValue + step)
|
|
263
|
+
break
|
|
264
|
+
case 'ArrowLeft':
|
|
265
|
+
case 'ArrowDown':
|
|
266
|
+
next = quantize(currentValue - step)
|
|
267
|
+
break
|
|
268
|
+
case 'PageUp':
|
|
269
|
+
next = quantize(currentValue + big)
|
|
270
|
+
break
|
|
271
|
+
case 'PageDown':
|
|
272
|
+
next = quantize(currentValue - big)
|
|
273
|
+
break
|
|
274
|
+
case 'Home':
|
|
275
|
+
next = minValue
|
|
276
|
+
break
|
|
277
|
+
case 'End':
|
|
278
|
+
next = maxValue
|
|
279
|
+
break
|
|
280
|
+
default:
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
e.preventDefault?.()
|
|
284
|
+
commit(next, true)
|
|
285
|
+
},
|
|
286
|
+
[isDisabled, step, minValue, maxValue, currentValue, quantize, commit]
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
const handleAccessibilityAction = useCallback(
|
|
290
|
+
(event: { nativeEvent: { actionName: string } }) => {
|
|
291
|
+
if (isDisabled) return
|
|
292
|
+
const name = event.nativeEvent.actionName
|
|
293
|
+
if (name === 'increment') commit(quantize(currentValue + step), true)
|
|
294
|
+
else if (name === 'decrement') commit(quantize(currentValue - step), true)
|
|
295
|
+
},
|
|
296
|
+
[isDisabled, step, currentValue, quantize, commit]
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
useImperativeHandle(
|
|
300
|
+
ref,
|
|
301
|
+
(): SliderHandle => ({
|
|
302
|
+
getValue: () => lastValueRef.current,
|
|
303
|
+
setValue: (v: number) => commit(quantize(v), true),
|
|
304
|
+
increment: (by?: number) => commit(quantize(lastValueRef.current + (by ?? step)), true),
|
|
305
|
+
decrement: (by?: number) => commit(quantize(lastValueRef.current - (by ?? step)), true),
|
|
306
|
+
}),
|
|
307
|
+
[quantize, commit, step]
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
const defaultFormat = useCallback(
|
|
311
|
+
(v: number) => {
|
|
312
|
+
try {
|
|
313
|
+
return new Intl.NumberFormat(locale, formatOptions).format(v)
|
|
314
|
+
} catch {
|
|
315
|
+
return String(v)
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
[locale, formatOptions]
|
|
319
|
+
)
|
|
320
|
+
const format = formatValue ?? defaultFormat
|
|
321
|
+
|
|
322
|
+
const ratio = maxValue > minValue ? (currentValue - minValue) / (maxValue - minValue) : 0
|
|
323
|
+
const percent = clamp(ratio, 0, 1) * 100
|
|
324
|
+
|
|
325
|
+
const handleSize = tokens.handleSize
|
|
326
|
+
// The bubble floats (absolutely positioned, no reserved layout space). When
|
|
327
|
+
// `alwaysShowTooltip` is false it only appears while interacting: dragging on
|
|
328
|
+
// any platform, or hovering on web. Lifting the finger clears `isDragging`,
|
|
329
|
+
// so the bubble disappears immediately on touch release.
|
|
330
|
+
const showTooltip = alwaysShowTooltip || isDragging || isHovered
|
|
331
|
+
|
|
332
|
+
const onTrackLayout = useCallback((e: LayoutChangeEvent) => {
|
|
333
|
+
setTrackWidth(e.nativeEvent.layout.width)
|
|
334
|
+
}, [])
|
|
335
|
+
|
|
336
|
+
const onTooltipLayout = useCallback((e: LayoutChangeEvent) => {
|
|
337
|
+
setTooltipWidth(e.nativeEvent.layout.width)
|
|
338
|
+
}, [])
|
|
339
|
+
|
|
340
|
+
const valueText = format(currentValue)
|
|
341
|
+
|
|
342
|
+
const tooltipNode = showTooltip ? (
|
|
343
|
+
<View
|
|
344
|
+
onLayout={onTooltipLayout}
|
|
345
|
+
style={[
|
|
346
|
+
styles.tooltip,
|
|
347
|
+
{
|
|
348
|
+
backgroundColor: tokens.tooltipBackground,
|
|
349
|
+
paddingHorizontal: tokens.tooltipPaddingH,
|
|
350
|
+
paddingVertical: tokens.tooltipPaddingV,
|
|
351
|
+
borderRadius: tokens.tooltipRadius,
|
|
352
|
+
maxWidth: tokens.tooltipMaxWidth,
|
|
353
|
+
bottom: handleSize + tokens.tooltipWrapGap,
|
|
354
|
+
// Pinned to the value point on the full-width track area (not
|
|
355
|
+
// the tiny handle-width thumb). An absolute child with only
|
|
356
|
+
// `left` set is clamped by Yoga to `parentWidth - left`, so
|
|
357
|
+
// parenting it to the 20px thumb collapsed the text to ~0px
|
|
358
|
+
// and rendered an empty bubble on device. Centre via translate.
|
|
359
|
+
left: `${percent}%`,
|
|
360
|
+
transform: [{ translateX: -tooltipWidth / 2 }],
|
|
361
|
+
},
|
|
362
|
+
]}
|
|
363
|
+
pointerEvents="none"
|
|
364
|
+
>
|
|
365
|
+
{renderTooltip ? (
|
|
366
|
+
renderTooltip(currentValue)
|
|
367
|
+
) : (
|
|
368
|
+
<Text style={tokens.tooltipLabel} numberOfLines={1}>
|
|
369
|
+
{valueText}
|
|
370
|
+
</Text>
|
|
371
|
+
)}
|
|
372
|
+
<View style={[styles.tip, { bottom: -tokens.tipHeight }]} pointerEvents="none">
|
|
373
|
+
<Svg
|
|
374
|
+
width={tokens.tipWidth}
|
|
375
|
+
height={tokens.tipHeight}
|
|
376
|
+
viewBox={`0 0 ${tokens.tipWidth} ${tokens.tipHeight}`}
|
|
377
|
+
>
|
|
378
|
+
<Path
|
|
379
|
+
d={`M0 0 L${tokens.tipWidth / 2} ${tokens.tipHeight} L${tokens.tipWidth} 0 Z`}
|
|
380
|
+
fill={tokens.tooltipBackground}
|
|
381
|
+
/>
|
|
382
|
+
</Svg>
|
|
383
|
+
</View>
|
|
384
|
+
</View>
|
|
385
|
+
) : null
|
|
386
|
+
|
|
387
|
+
return (
|
|
388
|
+
<View
|
|
389
|
+
style={[
|
|
390
|
+
{
|
|
391
|
+
width,
|
|
392
|
+
gap: tokens.gap,
|
|
393
|
+
paddingTop: tokens.paddingTop,
|
|
394
|
+
paddingBottom: tokens.paddingBottom,
|
|
395
|
+
paddingLeft: tokens.paddingLeft,
|
|
396
|
+
paddingRight: tokens.paddingRight,
|
|
397
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
398
|
+
},
|
|
399
|
+
style,
|
|
400
|
+
]}
|
|
401
|
+
accessibilityLabel={accessibilityLabel}
|
|
402
|
+
>
|
|
403
|
+
{/* Track area — the PanResponder target spans the full track width. */}
|
|
404
|
+
<View
|
|
405
|
+
{...panResponder.panHandlers}
|
|
406
|
+
onLayout={onTrackLayout}
|
|
407
|
+
style={[styles.trackArea, { height: handleSize }]}
|
|
408
|
+
accessible
|
|
409
|
+
accessibilityRole="adjustable"
|
|
410
|
+
accessibilityLabel={accessibilityLabel}
|
|
411
|
+
accessibilityValue={{ min: minValue, max: maxValue, now: currentValue, text: valueText }}
|
|
412
|
+
accessibilityState={{ disabled: isDisabled }}
|
|
413
|
+
accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
|
|
414
|
+
onAccessibilityAction={handleAccessibilityAction}
|
|
415
|
+
focusable={!isDisabled}
|
|
416
|
+
{...(Platform.OS === 'web'
|
|
417
|
+
? {
|
|
418
|
+
onKeyDown: handleKeyDown,
|
|
419
|
+
tabIndex: isDisabled ? -1 : 0,
|
|
420
|
+
onMouseEnter: () => !isDisabled && setIsHovered(true),
|
|
421
|
+
onMouseLeave: () => setIsHovered(false),
|
|
422
|
+
}
|
|
423
|
+
: {})}
|
|
424
|
+
>
|
|
425
|
+
{/* Track */}
|
|
426
|
+
<View
|
|
427
|
+
style={{
|
|
428
|
+
height: tokens.trackHeight,
|
|
429
|
+
borderRadius: tokens.trackRadius,
|
|
430
|
+
backgroundColor: tokens.trackBackground,
|
|
431
|
+
}}
|
|
432
|
+
/>
|
|
433
|
+
{/* Filled indicator */}
|
|
434
|
+
<View
|
|
435
|
+
pointerEvents="none"
|
|
436
|
+
style={{
|
|
437
|
+
position: 'absolute',
|
|
438
|
+
left: 0,
|
|
439
|
+
top: (handleSize - tokens.indicatorHeight) / 2,
|
|
440
|
+
height: tokens.indicatorHeight,
|
|
441
|
+
width: `${percent}%`,
|
|
442
|
+
borderRadius: tokens.indicatorRadius,
|
|
443
|
+
backgroundColor: tokens.indicatorBackground,
|
|
444
|
+
}}
|
|
445
|
+
/>
|
|
446
|
+
{/* Thumb (handle), centred on the value point */}
|
|
447
|
+
<View
|
|
448
|
+
pointerEvents="none"
|
|
449
|
+
style={{
|
|
450
|
+
position: 'absolute',
|
|
451
|
+
top: 0,
|
|
452
|
+
left: `${percent}%`,
|
|
453
|
+
width: handleSize,
|
|
454
|
+
height: handleSize,
|
|
455
|
+
transform: [{ translateX: -handleSize / 2 }],
|
|
456
|
+
}}
|
|
457
|
+
>
|
|
458
|
+
<View
|
|
459
|
+
style={{
|
|
460
|
+
width: handleSize,
|
|
461
|
+
height: handleSize,
|
|
462
|
+
borderRadius: tokens.handleRadius,
|
|
463
|
+
backgroundColor: tokens.handleBackground,
|
|
464
|
+
}}
|
|
465
|
+
/>
|
|
466
|
+
</View>
|
|
467
|
+
{/* Value bubble — parented to the full-width track area so it can
|
|
468
|
+
size to its content (see the note on the tooltip's `left`). */}
|
|
469
|
+
{tooltipNode}
|
|
470
|
+
</View>
|
|
471
|
+
|
|
472
|
+
{/* Min / max labels */}
|
|
473
|
+
{showLabels ? (
|
|
474
|
+
<View style={styles.labelWrap}>
|
|
475
|
+
<Text style={tokens.label}>{minLabel ?? format(minValue)}</Text>
|
|
476
|
+
<Text style={tokens.label}>{maxLabel ?? format(maxValue)}</Text>
|
|
477
|
+
</View>
|
|
478
|
+
) : null}
|
|
479
|
+
</View>
|
|
480
|
+
)
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
interface ResolvedTokens {
|
|
484
|
+
gap: number
|
|
485
|
+
paddingTop: number
|
|
486
|
+
paddingBottom: number
|
|
487
|
+
paddingLeft: number
|
|
488
|
+
paddingRight: number
|
|
489
|
+
trackHeight: number
|
|
490
|
+
trackRadius: number
|
|
491
|
+
trackBackground: string
|
|
492
|
+
indicatorHeight: number
|
|
493
|
+
indicatorRadius: number
|
|
494
|
+
indicatorBackground: string
|
|
495
|
+
handleSize: number
|
|
496
|
+
handleRadius: number
|
|
497
|
+
handleBackground: string
|
|
498
|
+
tooltipWrapGap: number
|
|
499
|
+
tooltipBackground: string
|
|
500
|
+
tooltipPaddingH: number
|
|
501
|
+
tooltipPaddingV: number
|
|
502
|
+
tooltipRadius: number
|
|
503
|
+
tooltipMaxWidth: number
|
|
504
|
+
tooltipLineHeight: number
|
|
505
|
+
tipWidth: number
|
|
506
|
+
tipHeight: number
|
|
507
|
+
tooltipLabel: TextStyle
|
|
508
|
+
label: TextStyle
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function resolveTokens(modes: Modes): ResolvedTokens {
|
|
512
|
+
// NOTE: token names are passed as string literals DIRECTLY to
|
|
513
|
+
// getVariableByName so the `extract-component-tokens` script can statically
|
|
514
|
+
// collect them for the generated docs. Do not refactor into a helper that
|
|
515
|
+
// receives the name as a variable.
|
|
516
|
+
const gap = asNum(getVariableByName('slider/gap', modes), 16)
|
|
517
|
+
const paddingTop = asNum(getVariableByName('slider/padding/top', modes), 8)
|
|
518
|
+
const paddingBottom = asNum(getVariableByName('slider/padding/bottom', modes), 0)
|
|
519
|
+
const paddingLeft = asNum(getVariableByName('slider/padding/left', modes), 0)
|
|
520
|
+
const paddingRight = asNum(getVariableByName('slider/padding/right', modes), 0)
|
|
521
|
+
|
|
522
|
+
const trackHeight = asNum(getVariableByName('slider/track/height', modes), 4)
|
|
523
|
+
const trackRadius = asNum(getVariableByName('slider/track/radius', modes), 999)
|
|
524
|
+
// The unfilled track uses a lighter emphasis in the design; consumer modes
|
|
525
|
+
// still win over the baked-in `Emphasis: Low`.
|
|
526
|
+
const trackBackground = asStr(
|
|
527
|
+
getVariableByName('slider/track/background', { ...TRACK_EMPHASIS, ...modes }),
|
|
528
|
+
'#fde8c9'
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
const indicatorHeight = asNum(getVariableByName('slider/indicator/height', modes), 4)
|
|
532
|
+
const indicatorRadius = asNum(getVariableByName('slider/indicator/radius', modes), 999)
|
|
533
|
+
const indicatorBackground = asStr(getVariableByName('slider/indicator/background', modes), '#f7ab21')
|
|
534
|
+
|
|
535
|
+
const handleSize = asNum(getVariableByName('slider/handle/size', modes), 20)
|
|
536
|
+
const handleRadius = asNum(getVariableByName('slider/handle/radius', modes), 999999)
|
|
537
|
+
const handleBackground = asStr(getVariableByName('slider/handle/background', modes), '#f7ab21')
|
|
538
|
+
|
|
539
|
+
const tooltipWrapGap = asNum(getVariableByName('slider/tooltipWrap/gap', modes), 12)
|
|
540
|
+
const tooltipBackground = asStr(getVariableByName('tooltip/background', modes), '#0f0d0a')
|
|
541
|
+
const tooltipPaddingH = asNum(getVariableByName('tooltip/padding/horizontal', modes), 12)
|
|
542
|
+
const tooltipPaddingV = asNum(getVariableByName('tooltip/padding/vertical', modes), 8)
|
|
543
|
+
const tooltipRadius = asNum(getVariableByName('radius', modes), 8)
|
|
544
|
+
const tooltipMaxWidth = asNum(getVariableByName('maxWidth', modes), 280)
|
|
545
|
+
const tipWidth = asNum(getVariableByName('tooltip/tipItem/width', modes), 16)
|
|
546
|
+
const tipHeight = asNum(getVariableByName('tooltip/tipItem/height', modes), 8)
|
|
547
|
+
|
|
548
|
+
const tooltipLabelColor = asStr(getVariableByName('tooltip/label/color', modes), '#ffffff')
|
|
549
|
+
const tooltipLabelSize = asNum(getVariableByName('tooltip/label/fontSize', modes), 14)
|
|
550
|
+
const tooltipLabelFamily = asStr(getVariableByName('tooltip/lable/fontFamily', modes), 'JioType Var')
|
|
551
|
+
const tooltipLabelLineHeight = asNum(getVariableByName('tooltip/label/lineHeight', modes), 18)
|
|
552
|
+
const tooltipLabelWeight = asStr(getVariableByName('tooltip/label/fontWeight', modes), '500')
|
|
553
|
+
|
|
554
|
+
const labelColor = asStr(getVariableByName('text/foreground', modes), '#000000')
|
|
555
|
+
const labelSize = asNum(getVariableByName('text/fontSize', modes), 14)
|
|
556
|
+
const labelFamily = asStr(getVariableByName('text/fontFamily', modes), 'JioType Var')
|
|
557
|
+
const labelLineHeight = asNum(getVariableByName('text/lineHeight', modes), 20)
|
|
558
|
+
const labelWeight = asStr(getVariableByName('text/fontWeight', modes), '500')
|
|
559
|
+
const labelLetterSpacing = asNum(getVariableByName('text/letterSpacing', modes), -0.5)
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
gap,
|
|
563
|
+
paddingTop,
|
|
564
|
+
paddingBottom,
|
|
565
|
+
paddingLeft,
|
|
566
|
+
paddingRight,
|
|
567
|
+
trackHeight,
|
|
568
|
+
trackRadius,
|
|
569
|
+
trackBackground,
|
|
570
|
+
indicatorHeight,
|
|
571
|
+
indicatorRadius,
|
|
572
|
+
indicatorBackground,
|
|
573
|
+
handleSize,
|
|
574
|
+
handleRadius,
|
|
575
|
+
handleBackground,
|
|
576
|
+
tooltipWrapGap,
|
|
577
|
+
tooltipBackground,
|
|
578
|
+
tooltipPaddingH,
|
|
579
|
+
tooltipPaddingV,
|
|
580
|
+
tooltipRadius,
|
|
581
|
+
tooltipMaxWidth,
|
|
582
|
+
tooltipLineHeight: tooltipLabelLineHeight,
|
|
583
|
+
tipWidth,
|
|
584
|
+
tipHeight,
|
|
585
|
+
tooltipLabel: {
|
|
586
|
+
color: tooltipLabelColor,
|
|
587
|
+
fontSize: tooltipLabelSize,
|
|
588
|
+
fontFamily: tooltipLabelFamily,
|
|
589
|
+
lineHeight: tooltipLabelLineHeight,
|
|
590
|
+
fontWeight: tooltipLabelWeight as TextStyle['fontWeight'],
|
|
591
|
+
includeFontPadding: false as any,
|
|
592
|
+
},
|
|
593
|
+
label: {
|
|
594
|
+
color: labelColor,
|
|
595
|
+
fontSize: labelSize,
|
|
596
|
+
fontFamily: labelFamily,
|
|
597
|
+
lineHeight: labelLineHeight,
|
|
598
|
+
fontWeight: labelWeight as TextStyle['fontWeight'],
|
|
599
|
+
letterSpacing: labelLetterSpacing,
|
|
600
|
+
includeFontPadding: false as any,
|
|
601
|
+
},
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const styles = {
|
|
606
|
+
trackArea: {
|
|
607
|
+
width: '100%',
|
|
608
|
+
justifyContent: 'center',
|
|
609
|
+
position: 'relative',
|
|
610
|
+
} as ViewStyle,
|
|
611
|
+
labelWrap: {
|
|
612
|
+
flexDirection: 'row',
|
|
613
|
+
justifyContent: 'space-between',
|
|
614
|
+
alignItems: 'flex-start',
|
|
615
|
+
width: '100%',
|
|
616
|
+
} as ViewStyle,
|
|
617
|
+
tooltip: {
|
|
618
|
+
position: 'absolute',
|
|
619
|
+
alignItems: 'center',
|
|
620
|
+
justifyContent: 'center',
|
|
621
|
+
} as ViewStyle,
|
|
622
|
+
tip: {
|
|
623
|
+
position: 'absolute',
|
|
624
|
+
alignSelf: 'center',
|
|
625
|
+
} as ViewStyle,
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export default Slider
|