Skip to content

Commit e7341be

Browse files
stelselimMxKevinBeqo
authored andcommitted
feat: migrate to a new slider library
1 parent eea5707 commit e7341be

12 files changed

Lines changed: 469 additions & 816 deletions

File tree

packages/pluggableWidgets/range-slider-native/package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,9 @@
2121
"dependencies": {
2222
"@mendix/piw-native-utils-internal": "*",
2323
"@mendix/piw-utils-internal": "*",
24-
"@ptomasroos/react-native-multi-slider": "^1.0.0",
25-
"prop-types": "^15.7.2"
24+
"@miblanchard/react-native-slider": "^2.6.0"
2625
},
2726
"devDependencies": {
28-
"@mendix/pluggable-widgets-tools": "*",
29-
"@types/ptomasroos__react-native-multi-slider": "^0.0.1"
27+
"@mendix/pluggable-widgets-tools": "*"
3028
}
3129
}

packages/pluggableWidgets/range-slider-native/src/Marker.tsx

Lines changed: 0 additions & 63 deletions
This file was deleted.

packages/pluggableWidgets/range-slider-native/src/RangeSlider.tsx

Lines changed: 24 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import { available, flattenStyles, toNumber, unavailable } from "@mendix/piw-native-utils-internal";
2-
import MultiSlider, { MarkerProps } from "@ptomasroos/react-native-multi-slider";
3-
import { ReactElement, useCallback, useRef, useState, JSX } from "react";
4-
import { LayoutChangeEvent, Text, View } from "react-native";
2+
import { Slider } from "@miblanchard/react-native-slider";
3+
import { ReactElement, useCallback, useRef } from "react";
4+
import { Text, View } from "react-native";
55
import { Big } from "big.js";
66

77
import { RangeSliderProps } from "../typings/RangeSliderProps";
8-
import { Marker } from "./Marker";
98
import { defaultRangeSliderStyle, RangeSliderStyle } from "./ui/Styles";
109
import { executeAction } from "@mendix/piw-utils-internal";
1110

1211
export type Props = RangeSliderProps<RangeSliderStyle>;
1312

1413
export function RangeSlider(props: Props): ReactElement {
15-
const [width, setWidth] = useState<number>();
1614
const styles = flattenStyles(defaultRangeSliderStyle, props.style);
1715

1816
const lastLowerValue = useRef<number | undefined>(toNumber(props.lowerValueAttribute));
@@ -23,27 +21,13 @@ export function RangeSlider(props: Props): ReactElement {
2321
const validationMessages = validate(props);
2422
const validProps = validationMessages.length === 0;
2523
const editable = props.editable !== "never" && validProps;
26-
const enabledOne = editable && lowerValue !== undefined && !props.lowerValueAttribute.readOnly;
27-
const enabledTwo = editable && upperValue !== undefined && !props.upperValueAttribute.readOnly;
24+
const enabledLower = editable && lowerValue !== undefined && !props.lowerValueAttribute.readOnly;
25+
const enabledUpper = editable && upperValue !== undefined && !props.upperValueAttribute.readOnly;
26+
const isEnabled = enabledLower || enabledUpper;
2827

29-
const customMarker =
30-
(markerEnabled: boolean, testID: string) =>
31-
(markerProps: MarkerProps): JSX.Element =>
32-
(
33-
<Marker
34-
{...markerProps}
35-
markerStyle={markerEnabled ? markerProps.markerStyle : styles.markerDisabled}
36-
testID={`${props.name}$${testID}`}
37-
/>
38-
);
39-
40-
const onLayout = useCallback((event: LayoutChangeEvent): void => {
41-
setWidth(event.nativeEvent.layout.width);
42-
}, []);
43-
44-
const onSlide = useCallback(
28+
const onValueChange = useCallback(
4529
(values: number[]): void => {
46-
if (values[0] === null || values[1] === null) {
30+
if (values[0] === null || values[0] === undefined || values[1] === null || values[1] === undefined) {
4731
return;
4832
}
4933
props.lowerValueAttribute.setValue(new Big(values[0]));
@@ -52,11 +36,13 @@ export function RangeSlider(props: Props): ReactElement {
5236
[props.lowerValueAttribute, props.upperValueAttribute]
5337
);
5438

55-
const onChange = useCallback(
39+
const onSlidingComplete = useCallback(
5640
(values: number[]): void => {
5741
if (
5842
values[0] === null ||
43+
values[0] === undefined ||
5944
values[1] === null ||
45+
values[1] === undefined ||
6046
(lastLowerValue.current === values[0] && lastUpperValue.current === values[1])
6147
) {
6248
return;
@@ -73,24 +59,19 @@ export function RangeSlider(props: Props): ReactElement {
7359
);
7460

7561
return (
76-
<View onLayout={onLayout} style={styles.container} testID={props.name}>
77-
<MultiSlider
78-
values={lowerValue != null && upperValue != null ? [lowerValue, upperValue] : undefined}
79-
min={validProps ? toNumber(props.minimumValue) : undefined}
80-
max={validProps ? toNumber(props.maximumValue) : undefined}
81-
step={validProps ? toNumber(props.stepSize) : undefined}
82-
enabledOne={enabledOne}
83-
enabledTwo={enabledTwo}
84-
markerStyle={styles.marker}
85-
trackStyle={enabledOne || enabledTwo ? styles.track : styles.trackDisabled}
86-
selectedStyle={enabledOne || enabledTwo ? styles.highlight : styles.highlightDisabled}
87-
pressedMarkerStyle={styles.markerActive}
88-
onValuesChange={onSlide}
89-
onValuesChangeFinish={onChange}
90-
sliderLength={width}
91-
isMarkersSeparated
92-
customMarkerLeft={customMarker(enabledOne, "leftMarker")}
93-
customMarkerRight={customMarker(enabledTwo, "rightMarker")}
62+
<View style={styles.container} testID={props.name}>
63+
<Slider
64+
value={lowerValue != null && upperValue != null ? [lowerValue, upperValue] : [0, 100]}
65+
minimumValue={validProps ? toNumber(props.minimumValue) ?? 0 : 0}
66+
maximumValue={validProps ? toNumber(props.maximumValue) ?? 100 : 100}
67+
step={validProps ? toNumber(props.stepSize) ?? 1 : 1}
68+
disabled={!isEnabled}
69+
trackStyle={isEnabled ? styles.track : styles.trackDisabled}
70+
minimumTrackStyle={isEnabled ? styles.minimumTrack : styles.minimumTrackDisabled}
71+
maximumTrackStyle={isEnabled ? styles.maximumTrack : styles.maximumTrackDisabled}
72+
thumbStyle={isEnabled ? styles.thumb : styles.thumbDisabled}
73+
onValueChange={onValueChange}
74+
onSlidingComplete={onSlidingComplete}
9475
/>
9576
{props.lowerValueAttribute.validation && (
9677
<Text style={styles.validationMessage}>{props.lowerValueAttribute.validation}</Text>

packages/pluggableWidgets/range-slider-native/src/__tests__/RangeSlider.spec.tsx

Lines changed: 78 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
import { actionValue, dynamicValue, EditableValueBuilder } from "@mendix/piw-utils-internal";
22
import { Big } from "big.js";
3-
import { View } from "react-native";
4-
import { fireEvent, render, RenderAPI } from "@testing-library/react-native";
5-
import { ReactTestInstance } from "react-test-renderer";
3+
import { fireEvent, render } from "@testing-library/react-native";
64
import { ValueStatus, DynamicValue } from "mendix";
75
import { Props, RangeSlider } from "../RangeSlider";
86

7+
jest.mock("@miblanchard/react-native-slider", () => {
8+
const { View } = require("react-native");
9+
return {
10+
Slider: (props: any) => (
11+
<View
12+
testID="mocked-slider"
13+
{...props}
14+
onValueChange={(values: number[]) => props.onValueChange?.(values)}
15+
onSlidingComplete={(values: number[]) => props.onSlidingComplete?.(values)}
16+
/>
17+
)
18+
};
19+
});
20+
921
describe("RangeSlider", () => {
1022
const noValue: DynamicValue<Big> = { status: ValueStatus.Unavailable, value: undefined };
1123
let defaultProps: Props;
@@ -32,7 +44,7 @@ describe("RangeSlider", () => {
3244
const component = render(
3345
<RangeSlider {...defaultProps} lowerValueAttribute={new EditableValueBuilder<Big>().isLoading().build()} />
3446
);
35-
expect(component.queryByTestId(`${defaultProps.name}-validation-message`)).toBeNull();
47+
expect(component.queryByTestId(`${defaultProps.name}-validation-messages`)).toBeNull();
3648
});
3749

3850
it("renders an error when no minimum value is provided", () => {
@@ -102,29 +114,6 @@ describe("RangeSlider", () => {
102114
expect(component.queryByText("The upper value must be equal or less than the maximum value.")).not.toBeNull();
103115
});
104116

105-
it("renders with the width of the parent view", () => {
106-
const component = render(
107-
<RangeSlider
108-
{...defaultProps}
109-
style={[
110-
{
111-
container: { width: 100 },
112-
highlight: {},
113-
highlightDisabled: {},
114-
marker: {},
115-
markerActive: {},
116-
markerDisabled: {},
117-
track: {},
118-
trackDisabled: {},
119-
validationMessage: {}
120-
}
121-
]}
122-
/>
123-
);
124-
fireEvent(component.getByTestId("range-slider-test"), "layout", { nativeEvent: { layout: { width: 100 } } });
125-
expect(component.getByTestId("range-slider-test").findByProps({ sliderLength: 100 })).not.toBeNull();
126-
});
127-
128117
it("renders a validation message", () => {
129118
const value = new EditableValueBuilder<Big>().withValidation("Invalid").build();
130119
const component = render(
@@ -134,71 +123,85 @@ describe("RangeSlider", () => {
134123
expect(component.getAllByText("Invalid")).toHaveLength(2);
135124
});
136125

137-
it("changes the lower value when swiping", () => {
138-
const onChangeAction = actionValue();
139-
const component = render(<RangeSlider {...defaultProps} onChange={onChangeAction} />);
126+
it("renders as disabled when editable is never", () => {
127+
const component = render(<RangeSlider {...defaultProps} editable={"never"} />);
128+
const slider = component.getByTestId("mocked-slider");
129+
expect(slider.props.disabled).toBe(true);
130+
});
140131

141-
fireEvent(getHandle(component), "responderGrant", { touchHistory: { touchBank: [] } });
142-
fireEvent(getHandle(component), "responderMove", responderMove(50));
132+
it("renders as enabled when editable is default", () => {
133+
const component = render(<RangeSlider {...defaultProps} />);
134+
const slider = component.getByTestId("mocked-slider");
135+
expect(slider.props.disabled).toBe(false);
136+
});
143137

144-
expect(onChangeAction.execute).not.toHaveBeenCalled();
138+
it("passes correct min/max/step to the slider", () => {
139+
const component = render(<RangeSlider {...defaultProps} />);
140+
const slider = component.getByTestId("mocked-slider");
141+
expect(slider.props.minimumValue).toBe(0);
142+
expect(slider.props.maximumValue).toBe(280);
143+
expect(slider.props.step).toBe(1);
144+
});
145145

146-
fireEvent(getHandle(component), "responderRelease", {});
146+
it("passes range values as array", () => {
147+
const component = render(<RangeSlider {...defaultProps} />);
148+
const slider = component.getByTestId("mocked-slider");
149+
expect(slider.props.value).toEqual([70, 210]);
150+
});
151+
152+
it("calls onValueChange when sliding", () => {
153+
const component = render(<RangeSlider {...defaultProps} />);
154+
const slider = component.getByTestId("mocked-slider");
155+
156+
fireEvent(slider, "onValueChange", [100, 210]);
147157

148-
expect(defaultProps.lowerValueAttribute.setValue).toHaveBeenCalledWith(new Big(120));
158+
expect(defaultProps.lowerValueAttribute.setValue).toHaveBeenCalledWith(new Big(100));
149159
expect(defaultProps.upperValueAttribute.setValue).toHaveBeenCalledWith(new Big(210));
150-
expect(onChangeAction.execute).toHaveBeenCalledTimes(1);
151160
});
152161

153-
it("changes the upper value when swiping", () => {
162+
it("calls onChange action on sliding complete", () => {
154163
const onChangeAction = actionValue();
155164
const component = render(<RangeSlider {...defaultProps} onChange={onChangeAction} />);
165+
const slider = component.getByTestId("mocked-slider");
156166

157-
fireEvent(getHandle(component, 1), "responderGrant", { touchHistory: { touchBank: [] } });
158-
fireEvent(getHandle(component, 1), "responderMove", responderMove(-50));
159-
160-
expect(onChangeAction.execute).not.toHaveBeenCalled();
161-
162-
fireEvent(getHandle(component, 1), "responderRelease", {});
167+
fireEvent(slider, "onSlidingComplete", [100, 250]);
163168

164-
expect(defaultProps.lowerValueAttribute.setValue).toHaveBeenCalledWith(new Big(70));
165-
expect(defaultProps.upperValueAttribute.setValue).toHaveBeenCalledWith(new Big(160));
169+
expect(defaultProps.lowerValueAttribute.setValue).toHaveBeenCalledWith(new Big(100));
170+
expect(defaultProps.upperValueAttribute.setValue).toHaveBeenCalledWith(new Big(250));
166171
expect(onChangeAction.execute).toHaveBeenCalledTimes(1);
167172
});
168173

169-
it("does not change the value when non editable", () => {
174+
it("does not call onChange when values haven't changed", () => {
170175
const onChangeAction = actionValue();
171-
const component = render(<RangeSlider {...defaultProps} editable={"never"} onChange={onChangeAction} />);
176+
const component = render(<RangeSlider {...defaultProps} onChange={onChangeAction} />);
177+
const slider = component.getByTestId("mocked-slider");
172178

173-
fireEvent(getHandle(component), "responderGrant", { touchHistory: { touchBank: [] } });
174-
fireEvent(getHandle(component), "responderMove", responderMove(50));
175-
fireEvent(getHandle(component), "responderRelease", {});
179+
fireEvent(slider, "onSlidingComplete", [70, 210]);
176180

177181
expect(onChangeAction.execute).not.toHaveBeenCalled();
178-
expect(defaultProps.lowerValueAttribute.setValue).not.toHaveBeenCalled();
179-
expect(defaultProps.upperValueAttribute.setValue).not.toHaveBeenCalled();
180182
});
181-
});
182-
183-
function getHandle(component: RenderAPI, index = 0): ReactTestInstance {
184-
return component.UNSAFE_getAllByType(View).filter(instance => instance.props.onMoveShouldSetResponder)[index];
185-
}
186183

187-
function responderMove(dx: number): any {
188-
return {
189-
touchHistory: {
190-
numberActiveTouches: 1,
191-
indexOfSingleActiveTouch: 0,
192-
touchBank: [
193-
{
194-
touchActive: true,
195-
currentTimeStamp: Date.now(),
196-
currentPageX: dx,
197-
currentPageY: 0,
198-
previousPageX: 0,
199-
previousPageY: 0
200-
}
201-
]
202-
}
203-
};
204-
}
184+
it("applies custom styles", () => {
185+
const component = render(
186+
<RangeSlider
187+
{...defaultProps}
188+
style={[
189+
{
190+
container: { width: 100 },
191+
track: {},
192+
trackDisabled: {},
193+
minimumTrack: {},
194+
minimumTrackDisabled: {},
195+
maximumTrack: {},
196+
maximumTrackDisabled: {},
197+
thumb: {},
198+
thumbActive: {},
199+
thumbDisabled: {},
200+
validationMessage: {}
201+
}
202+
]}
203+
/>
204+
);
205+
expect(component.toJSON()).toMatchSnapshot("with custom styles");
206+
});
207+
});

0 commit comments

Comments
 (0)