Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@
},
"patchedDependencies": {
"@mendix/pluggable-widgets-tools@11.8.0": "patches/@mendix+pluggable-widgets-tools+11.8.0.patch",
"@ptomasroos/react-native-multi-slider@1.0.0": "patches/@ptomasroos+react-native-multi-slider+1.0.0.patch",
"react-native-action-button@2.8.5": "patches/react-native-action-button+2.8.5.patch",
"react-native-gesture-handler@2.30.0": "patches/react-native-gesture-handler+2.30.0.patch",
"react-native-slider@0.11.0": "patches/react-native-slider+0.11.0.patch",
Expand Down
6 changes: 2 additions & 4 deletions packages/pluggableWidgets/range-slider-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@
"dependencies": {
"@mendix/piw-native-utils-internal": "*",
"@mendix/piw-utils-internal": "*",
"@ptomasroos/react-native-multi-slider": "^1.0.0",
"prop-types": "^15.7.2"
"@miblanchard/react-native-slider": "^2.6.0"
},
"devDependencies": {
"@mendix/pluggable-widgets-tools": "*",
"@types/ptomasroos__react-native-multi-slider": "^0.0.1"
"@mendix/pluggable-widgets-tools": "*"
}
}
63 changes: 0 additions & 63 deletions packages/pluggableWidgets/range-slider-native/src/Marker.tsx

This file was deleted.

67 changes: 24 additions & 43 deletions packages/pluggableWidgets/range-slider-native/src/RangeSlider.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { available, flattenStyles, toNumber, unavailable } from "@mendix/piw-native-utils-internal";
import MultiSlider, { MarkerProps } from "@ptomasroos/react-native-multi-slider";
import { ReactElement, useCallback, useRef, useState, JSX } from "react";
import { LayoutChangeEvent, Text, View } from "react-native";
import { Slider } from "@miblanchard/react-native-slider";
import { ReactElement, useCallback, useRef } from "react";
import { Text, View } from "react-native";
import { Big } from "big.js";

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

export type Props = RangeSliderProps<RangeSliderStyle>;

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

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

const customMarker =
(markerEnabled: boolean, testID: string) =>
(markerProps: MarkerProps): JSX.Element =>
(
<Marker
{...markerProps}
markerStyle={markerEnabled ? markerProps.markerStyle : styles.markerDisabled}
testID={`${props.name}$${testID}`}
/>
);

const onLayout = useCallback((event: LayoutChangeEvent): void => {
setWidth(event.nativeEvent.layout.width);
}, []);

const onSlide = useCallback(
const onValueChange = useCallback(
(values: number[]): void => {
if (values[0] === null || values[1] === null) {
if (values[0] === null || values[0] === undefined || values[1] === null || values[1] === undefined) {
return;
}
props.lowerValueAttribute.setValue(new Big(values[0]));
Expand All @@ -52,11 +36,13 @@ export function RangeSlider(props: Props): ReactElement {
[props.lowerValueAttribute, props.upperValueAttribute]
);

const onChange = useCallback(
const onSlidingComplete = useCallback(
(values: number[]): void => {
if (
values[0] === null ||
values[0] === undefined ||
values[1] === null ||
values[1] === undefined ||
(lastLowerValue.current === values[0] && lastUpperValue.current === values[1])
) {
return;
Expand All @@ -73,24 +59,19 @@ export function RangeSlider(props: Props): ReactElement {
);

return (
<View onLayout={onLayout} style={styles.container} testID={props.name}>
<MultiSlider
values={lowerValue != null && upperValue != null ? [lowerValue, upperValue] : undefined}
min={validProps ? toNumber(props.minimumValue) : undefined}
max={validProps ? toNumber(props.maximumValue) : undefined}
step={validProps ? toNumber(props.stepSize) : undefined}
enabledOne={enabledOne}
enabledTwo={enabledTwo}
markerStyle={styles.marker}
trackStyle={enabledOne || enabledTwo ? styles.track : styles.trackDisabled}
selectedStyle={enabledOne || enabledTwo ? styles.highlight : styles.highlightDisabled}
pressedMarkerStyle={styles.markerActive}
onValuesChange={onSlide}
onValuesChangeFinish={onChange}
sliderLength={width}
isMarkersSeparated
customMarkerLeft={customMarker(enabledOne, "leftMarker")}
customMarkerRight={customMarker(enabledTwo, "rightMarker")}
<View style={styles.container} testID={props.name}>
<Slider
value={lowerValue != null && upperValue != null ? [lowerValue, upperValue] : [0, 100]}
minimumValue={validProps ? toNumber(props.minimumValue) ?? 0 : 0}
maximumValue={validProps ? toNumber(props.maximumValue) ?? 100 : 100}
step={validProps ? toNumber(props.stepSize) ?? 1 : 1}
disabled={!isEnabled}
trackStyle={isEnabled ? styles.track : styles.trackDisabled}
minimumTrackStyle={isEnabled ? styles.minimumTrack : styles.minimumTrackDisabled}
maximumTrackStyle={isEnabled ? styles.maximumTrack : styles.maximumTrackDisabled}
thumbStyle={isEnabled ? styles.thumb : styles.thumbDisabled}
onValueChange={onValueChange}
onSlidingComplete={onSlidingComplete}
/>
{props.lowerValueAttribute.validation && (
<Text style={styles.validationMessage}>{props.lowerValueAttribute.validation}</Text>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import { actionValue, dynamicValue, EditableValueBuilder } from "@mendix/piw-utils-internal";
import { Big } from "big.js";
import { View } from "react-native";
import { fireEvent, render, RenderAPI } from "@testing-library/react-native";
import { ReactTestInstance } from "react-test-renderer";
import { fireEvent, render } from "@testing-library/react-native";
import { ValueStatus, DynamicValue } from "mendix";
import { Props, RangeSlider } from "../RangeSlider";

jest.mock("@miblanchard/react-native-slider", () => {
const { View } = require("react-native");
return {
Slider: (props: any) => (
<View
testID="mocked-slider"
{...props}
onValueChange={(values: number[]) => props.onValueChange?.(values)}
onSlidingComplete={(values: number[]) => props.onSlidingComplete?.(values)}
/>
)
};
});

describe("RangeSlider", () => {
const noValue: DynamicValue<Big> = { status: ValueStatus.Unavailable, value: undefined };
let defaultProps: Props;
Expand All @@ -32,7 +44,7 @@ describe("RangeSlider", () => {
const component = render(
<RangeSlider {...defaultProps} lowerValueAttribute={new EditableValueBuilder<Big>().isLoading().build()} />
);
expect(component.queryByTestId(`${defaultProps.name}-validation-message`)).toBeNull();
expect(component.queryByTestId(`${defaultProps.name}-validation-messages`)).toBeNull();
});

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

it("renders with the width of the parent view", () => {
const component = render(
<RangeSlider
{...defaultProps}
style={[
{
container: { width: 100 },
highlight: {},
highlightDisabled: {},
marker: {},
markerActive: {},
markerDisabled: {},
track: {},
trackDisabled: {},
validationMessage: {}
}
]}
/>
);
fireEvent(component.getByTestId("range-slider-test"), "layout", { nativeEvent: { layout: { width: 100 } } });
expect(component.getByTestId("range-slider-test").findByProps({ sliderLength: 100 })).not.toBeNull();
});

it("renders a validation message", () => {
const value = new EditableValueBuilder<Big>().withValidation("Invalid").build();
const component = render(
Expand All @@ -134,71 +123,85 @@ describe("RangeSlider", () => {
expect(component.getAllByText("Invalid")).toHaveLength(2);
});

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

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

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

fireEvent(getHandle(component), "responderRelease", {});
it("passes range values as array", () => {
const component = render(<RangeSlider {...defaultProps} />);
const slider = component.getByTestId("mocked-slider");
expect(slider.props.value).toEqual([70, 210]);
});

it("calls onValueChange when sliding", () => {
const component = render(<RangeSlider {...defaultProps} />);
const slider = component.getByTestId("mocked-slider");

fireEvent(slider, "onValueChange", [100, 210]);

expect(defaultProps.lowerValueAttribute.setValue).toHaveBeenCalledWith(new Big(120));
expect(defaultProps.lowerValueAttribute.setValue).toHaveBeenCalledWith(new Big(100));
expect(defaultProps.upperValueAttribute.setValue).toHaveBeenCalledWith(new Big(210));
expect(onChangeAction.execute).toHaveBeenCalledTimes(1);
});

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

fireEvent(getHandle(component, 1), "responderGrant", { touchHistory: { touchBank: [] } });
fireEvent(getHandle(component, 1), "responderMove", responderMove(-50));

expect(onChangeAction.execute).not.toHaveBeenCalled();

fireEvent(getHandle(component, 1), "responderRelease", {});
fireEvent(slider, "onSlidingComplete", [100, 250]);

expect(defaultProps.lowerValueAttribute.setValue).toHaveBeenCalledWith(new Big(70));
expect(defaultProps.upperValueAttribute.setValue).toHaveBeenCalledWith(new Big(160));
expect(defaultProps.lowerValueAttribute.setValue).toHaveBeenCalledWith(new Big(100));
expect(defaultProps.upperValueAttribute.setValue).toHaveBeenCalledWith(new Big(250));
expect(onChangeAction.execute).toHaveBeenCalledTimes(1);
});

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

fireEvent(getHandle(component), "responderGrant", { touchHistory: { touchBank: [] } });
fireEvent(getHandle(component), "responderMove", responderMove(50));
fireEvent(getHandle(component), "responderRelease", {});
fireEvent(slider, "onSlidingComplete", [70, 210]);

expect(onChangeAction.execute).not.toHaveBeenCalled();
expect(defaultProps.lowerValueAttribute.setValue).not.toHaveBeenCalled();
expect(defaultProps.upperValueAttribute.setValue).not.toHaveBeenCalled();
});
});

function getHandle(component: RenderAPI, index = 0): ReactTestInstance {
return component.UNSAFE_getAllByType(View).filter(instance => instance.props.onMoveShouldSetResponder)[index];
}

function responderMove(dx: number): any {
return {
touchHistory: {
numberActiveTouches: 1,
indexOfSingleActiveTouch: 0,
touchBank: [
{
touchActive: true,
currentTimeStamp: Date.now(),
currentPageX: dx,
currentPageY: 0,
previousPageX: 0,
previousPageY: 0
}
]
}
};
}
it("applies custom styles", () => {
const component = render(
<RangeSlider
{...defaultProps}
style={[
{
container: { width: 100 },
track: {},
trackDisabled: {},
minimumTrack: {},
minimumTrackDisabled: {},
maximumTrack: {},
maximumTrackDisabled: {},
thumb: {},
thumbActive: {},
thumbDisabled: {},
validationMessage: {}
}
]}
/>
);
expect(component.toJSON()).toMatchSnapshot("with custom styles");
});
});
Loading
Loading