Skip to content

Commit bc5589e

Browse files
authored
Fix: #144 Web support implementation (#165)
* Web support implementation Signed-off-by: François BILLIOUD <[email protected]> * Fix eslint configuration * Fix : Cannot resolve module `@react-native-community/slider` from flow * Set cursor to pointer on hover * Handle step = 0 * Add accessibility support * Fix event firing Signed-off-by: François BILLIOUD <[email protected]> * Fix accessibility with arrow keys Signed-off-by: François BILLIOUD <[email protected]> Co-authored-by: François BILLIOUD <[email protected]>
1 parent 124ddfa commit bc5589e

File tree

7 files changed

+325
-5
lines changed

7 files changed

+325
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ buck-out/
5555
# Bundle artifact
5656
*.jsbundle
5757
.tmp/
58+
src/dist
5859

5960
*/ios/Pods

babel.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
module.exports = {
2-
presets: ['module:metro-react-native-babel-preset'],
2+
presets: [
3+
"@babel/preset-flow",
4+
'module:metro-react-native-babel-preset'
5+
],
36
};

example/SliderExample.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @format
8-
* @flow
97
*/
108

119
'use strict';

src/babel.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
presets: ['@babel/preset-flow', 'module:metro-react-native-babel-preset'],
3+
};
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import React from 'react';
10+
import {View, StyleSheet} from 'react-native';
11+
12+
// import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet';
13+
// import type {ColorValue} from 'react-native/Libraries/StyleSheet/StyleSheetTypes';
14+
// import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes';
15+
// import type {SyntheticEvent} from 'react-native/Libraries/Types/CoreEventTypes';
16+
17+
// type Props = $ReadOnly<{|
18+
// ...ViewProps,
19+
20+
// /**
21+
// * Set to true to animate values with default 'timing' animation type
22+
// */
23+
// animateTransitions?: ?boolean,
24+
25+
// /**
26+
// * Used to configure the animation parameters. These are the same parameters in the Animated library.
27+
// */
28+
// animationConfig?: ?any,
29+
30+
// /**
31+
// * Custom Animation type. 'spring' or 'timing'.
32+
// */
33+
// animationType?: 'spring' | 'timing' | undefined,
34+
35+
// /**
36+
// * If true the user won't be able to move the slider.
37+
// * Default value is false.
38+
// */
39+
// disabled?: ?boolean,
40+
41+
// /**
42+
// * Used to style and layout the `Slider`. See `StyleSheet.js` and
43+
// * `DeprecatedViewStylePropTypes.js` for more info.
44+
// */
45+
// style?: ?ViewStyleProp,
46+
47+
// /**
48+
// * Initial value of the slider. The value should be between minimumValue
49+
// * and maximumValue, which default to 0 and 1 respectively.
50+
// * Default value is 0.
51+
// *
52+
// * *This is not a controlled component*, you don't need to update the
53+
// * value during dragging.
54+
// */
55+
// value?: ?number,
56+
57+
// /**
58+
// * Step value of the slider. The value should be
59+
// * between 0 and (maximumValue - minimumValue).
60+
// * Default value is 0.
61+
// */
62+
// step?: ?number,
63+
64+
// /**
65+
// * Initial minimum value of the slider. Default value is 0.
66+
// */
67+
// minimumValue?: ?number,
68+
69+
// /**
70+
// * Initial maximum value of the slider. Default value is 1.
71+
// */
72+
// maximumValue?: ?number,
73+
74+
// /**
75+
// * The color used for the track to the left of the button.
76+
// * Overrides the default blue gradient image on iOS.
77+
// */
78+
// minimumTrackTintColor?: ?ColorValue,
79+
80+
// /**
81+
// * The color used for the track to the right of the button.
82+
// * Overrides the default blue gradient image on iOS.
83+
// */
84+
// maximumTrackTintColor?: ?ColorValue,
85+
// /**
86+
// * The color used to tint the default thumb images on iOS, or the
87+
// * color of the foreground switch grip on Android.
88+
// */
89+
// thumbTintColor?: ?ColorValue,
90+
91+
// /**
92+
// * If true the user won't be able to move the slider.
93+
// * Default value is false.
94+
// */
95+
// disabled?: ?boolean,
96+
97+
// /**
98+
// * Callback continuously called while the user is dragging the slider.
99+
// */
100+
// onValueChange?: ?(value: number) => void,
101+
102+
// /**
103+
// * Callback that is called when the user touches the slider,
104+
// * regardless if the value has changed. The current value is passed
105+
// * as an argument to the callback handler.
106+
// */
107+
108+
// onSlidingStart?: ?(value: number) => void,
109+
110+
// /**
111+
// * Callback that is called when the user releases the slider,
112+
// * regardless if the value has changed. The current value is passed
113+
// * as an argument to the callback handler.
114+
// */
115+
// onSlidingComplete?: ?(value: number) => void,
116+
117+
// /**
118+
// * Used to locate this view in UI automation tests.
119+
// */
120+
// testID?: ?string,
121+
122+
// /**
123+
// * Sets an image for the thumb. Only static images are supported.
124+
// */
125+
// thumbImage?: ?ImageSource,
126+
127+
// /**
128+
// * If true the slider will be inverted.
129+
// * Default value is false.
130+
// */
131+
// inverted?: ?boolean,
132+
// |}>;
133+
134+
const RCTSliderWebComponent = React.forwardRef(
135+
(
136+
{
137+
value: initialValue,
138+
minimumValue = 0,
139+
maximumValue = 0,
140+
step = 1,
141+
minimumTrackTintColor = '#009688',
142+
maximumTrackTintColor = '#939393',
143+
thumbTintColor = '#009688',
144+
thumbStyle = {},
145+
style = [],
146+
inverted = false,
147+
enabled = true,
148+
trackHeight = 4,
149+
thumbSize = 20,
150+
onRNCSliderSlidingStart = () => {},
151+
onRNCSliderSlidingComplete = () => {},
152+
onRNCSliderValueChange = () => {},
153+
...others
154+
},
155+
forwardedRef,
156+
) => {
157+
const onValueChange = value =>
158+
onRNCSliderValueChange &&
159+
onRNCSliderValueChange({nativeEvent: {fromUser: true, value}});
160+
const onSlidingStart = value =>
161+
onRNCSliderSlidingStart &&
162+
onRNCSliderSlidingStart({nativeEvent: {fromUser: true, value}});
163+
const onSlidingComplete = value =>
164+
onRNCSliderSlidingComplete &&
165+
onRNCSliderSlidingComplete({nativeEvent: {fromUser: true, value}});
166+
167+
const containerSize = React.useRef({width: 0, height: 0});
168+
const containerRef = forwardedRef || React.createRef();
169+
const [value, setValue] = React.useState(initialValue || minimumValue);
170+
171+
const percentageValue =
172+
(value - minimumValue) / (maximumValue - minimumValue);
173+
const minPercent = percentageValue;
174+
const maxPercent = 1 - percentageValue;
175+
176+
const containerStyle = StyleSheet.compose(
177+
{
178+
width: '100%',
179+
flexDirection: 'row',
180+
userSelect: 'none',
181+
alignItems: 'center',
182+
cursor: 'pointer',
183+
},
184+
style,
185+
);
186+
187+
const trackStyle = {
188+
height: trackHeight,
189+
borderRadius: trackHeight / 2,
190+
userSelect: 'none',
191+
flexGrow: 1,
192+
};
193+
194+
const minimumTrackStyle = {
195+
...trackStyle,
196+
backgroundColor: minimumTrackTintColor,
197+
flexBasis: minPercent * 100 + '%',
198+
};
199+
200+
const maximumTrackStyle = {
201+
...trackStyle,
202+
backgroundColor: maximumTrackTintColor,
203+
flexBasis: maxPercent * 100 + '%',
204+
};
205+
206+
// const width = (containerSize.current ? containerSize.current.width : 0)
207+
// const valueOffset = (inverted ? (1 - percentageValue) : percentageValue) * width
208+
209+
const thumbViewStyle = StyleSheet.compose(
210+
{
211+
width: thumbSize,
212+
height: thumbSize,
213+
// left: valueOffset - thumbSize / 2,
214+
// top: trackHeight / 2 - thumbSize / 2,
215+
// position: absolute,
216+
backgroundColor: thumbTintColor,
217+
zIndex: 1,
218+
borderRadius: thumbSize / 2,
219+
overflow: 'hidden',
220+
userSelect: 'none',
221+
},
222+
thumbStyle,
223+
);
224+
225+
const updateValue = newValue => {
226+
// Ensure that the new value is still between the bounds
227+
const withinBounds = Math.max(
228+
minimumValue,
229+
Math.min(newValue, maximumValue),
230+
);
231+
if (value !== withinBounds) {
232+
setValue(withinBounds);
233+
onValueChange(withinBounds);
234+
}
235+
};
236+
237+
const onTouchEnd = () => {
238+
onSlidingComplete(value);
239+
};
240+
241+
const onMove = event => {
242+
const {locationX: x} = event.nativeEvent;
243+
const width = containerSize.current ? containerSize.current.width : 1;
244+
const newValue = inverted
245+
? maximumValue - ((maximumValue - minimumValue) * x) / width
246+
: minimumValue + ((maximumValue - minimumValue) * x) / width;
247+
const roundedValue = step ? Math.round(newValue / step) * step : newValue;
248+
updateValue(roundedValue);
249+
};
250+
const accessibilityActions = event => {
251+
const tenth = (maximumValue - minimumValue) / 10;
252+
switch (event.nativeEvent.actionName) {
253+
case 'increment':
254+
updateValue(value + (step || tenth));
255+
break;
256+
case 'decrement':
257+
updateValue(value - (step || tenth));
258+
break;
259+
}
260+
};
261+
const handleAccessibilityKeys = key => {
262+
switch (key) {
263+
case 'ArrowUp':
264+
case 'ArrowRight':
265+
accessibilityActions({nativeEvent: {actionName: 'increment'}});
266+
break;
267+
case 'ArrowDown':
268+
case 'ArrowLeft':
269+
accessibilityActions({nativeEvent: {actionName: 'decrement'}});
270+
break;
271+
}
272+
};
273+
274+
return (
275+
<View
276+
ref={containerRef}
277+
onLayout={({nativeEvent}) =>
278+
(containerSize.current = nativeEvent.layout)
279+
}
280+
accessibilityActions={[
281+
{name: 'increment', label: 'increment'},
282+
{name: 'decrement', label: 'decrement'},
283+
]}
284+
onAccessibilityAction={accessibilityActions}
285+
accessible={true}
286+
accessibleValue={value}
287+
accessibilityRole={'adjustable'}
288+
style={containerStyle}
289+
onStartShouldSetResponder={() => enabled}
290+
onMoveShouldSetResponder={() => enabled}
291+
onResponderGrant={() => onSlidingStart(value)}
292+
onResponderRelease={onTouchEnd}
293+
onResponderMove={onMove}
294+
onKeyDown={({nativeEvent: {key}}) => handleAccessibilityKeys(key)}
295+
{...others}>
296+
<View pointerEvents="none" style={minimumTrackStyle} />
297+
<View pointerEvents="none" style={thumbViewStyle} />
298+
<View pointerEvents="none" style={maximumTrackStyle} />
299+
</View>
300+
);
301+
},
302+
);
303+
304+
RCTSliderWebComponent.displayName = 'RTCSliderWebComponent';
305+
306+
export default RCTSliderWebComponent;

src/js/Slider.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,11 @@ const SliderComponent = (
253253
return (
254254
<RCTSliderNativeComponent
255255
{...localProps}
256-
thumbImage={Image.resolveAssetSource(props.thumbImage)}
256+
thumbImage={
257+
Platform.OS === 'web'
258+
? props.thumbImage
259+
: Image.resolveAssetSource(props.thumbImage)
260+
}
257261
ref={forwardedRef}
258262
style={style}
259263
onChange={onChangeEvent}

src/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,24 @@
88
"publishConfig": {
99
"access": "public"
1010
},
11-
"main": "js/Slider.js",
11+
"main": "dist/Slider.js",
1212
"types": "typings/index.d.ts",
1313
"keywords": [
1414
"react-native",
1515
"react native",
1616
"slider"
1717
],
18+
"scripts": {
19+
"prepare": "babel --out-dir dist js"
20+
},
1821
"peerDependencies": {
1922
"react": "*",
2023
"react-native": "*"
2124
},
2225
"devDependencies": {
26+
"@babel/cli": "^7.8.4",
2327
"@babel/core": "^7.6.2",
28+
"@babel/preset-flow": "^7.9.0",
2429
"@babel/runtime": "^7.6.2",
2530
"@react-native-community/eslint-config": "0.0.5",
2631
"babel-jest": "^24.9.0",

0 commit comments

Comments
 (0)