diff --git a/GestureDetectorMock.tsx b/GestureDetectorMock.tsx deleted file mode 100644 index d92a5f2802..0000000000 --- a/GestureDetectorMock.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import {TouchableOpacity} from 'react-native'; - -type Props = { - gesture: any; - children: any; -}; - -export class GestureDetectorMock extends React.Component { - render() { - switch (this.props.gesture.type) { - case 'tap': - return ( - { - this.props.gesture._handlers.onTouchesDown(); - this.props.gesture._handlers.onEnd(); - this.props.gesture._handlers.onFinalize(); - }} - > - {this.props.children} - - ); - case 'pan': - return ( - { - this.props.gesture._handlers.onStart({ - absoluteX: 0, - absoluteY: 0, - translationX: 0, - translationY: 0, - velocityX: 0, - velocityY: 0, - x: 0, - y: 0 - }); - this.props.gesture._handlers.onUpdate({ - absoluteX: 0, - absoluteY: 0, - translationX: 0, - translationY: 0, - velocityX: 0, - velocityY: 0, - x: 0, - y: 0 - }); - this.props.gesture._handlers.onEnd({ - absoluteX: 0, - absoluteY: 0, - translationX: 0, - translationY: 0, - velocityX: 0, - velocityY: 0, - x: 0, - y: 0 - }); - }} - > - {this.props.children} - - ); - default: - throw new Error(`Unhandled gesture of type: ${this.props.gesture.type}`); - } - } -} diff --git a/README.md b/README.md index 2d76333ed8..c7fef3aab4 100644 --- a/README.md +++ b/README.md @@ -127,3 +127,7 @@ class MyScreen extends Component { } } ``` + +## Contributing + See [Contribution Guide](https://github.com/wix/react-native-ui-lib/blob/master/CONTRIBUTING.md) + \ No newline at end of file diff --git a/demo/src/screens/componentScreens/HintsScreen.tsx b/demo/src/screens/componentScreens/HintsScreen.tsx index dd0e52e340..2b2885d13d 100644 --- a/demo/src/screens/componentScreens/HintsScreen.tsx +++ b/demo/src/screens/componentScreens/HintsScreen.tsx @@ -10,7 +10,6 @@ const reactions = ['❤️', '😮', '😔', '😂', '😡']; type HintScreenProps = {}; export default class HintsScreen extends Component { - state = { showHint: true, useShortMessage: false, @@ -134,6 +133,7 @@ export default class HintsScreen extends Component { const message = useShortMessage ? 'Add other cool and useful stuff.' : 'Add other cool and useful stuff through adding apps to your visitors to enjoy.'; + const color = !showCustomContent && showReactionStrip ? {color: Colors.$backgroundDefault} : undefined; return ( @@ -178,7 +178,7 @@ export default class HintsScreen extends Component { ? this.renderReactionStrip() : undefined } - color={!showCustomContent && showReactionStrip ? Colors.$backgroundDefault : undefined} + {...color} removePaddings={!showCustomContent && showReactionStrip} enableShadow={enableShadow} testID={'Hint'} diff --git a/demo/src/screens/componentScreens/ModalScreen.tsx b/demo/src/screens/componentScreens/ModalScreen.tsx index 07fcd68652..b077ebe7d5 100644 --- a/demo/src/screens/componentScreens/ModalScreen.tsx +++ b/demo/src/screens/componentScreens/ModalScreen.tsx @@ -63,6 +63,7 @@ export default class ModalScreen extends Component { Alert.alert('cancel')} onDone={() => Alert.alert('done')} cancelIcon={null} diff --git a/demo/src/screens/componentScreens/SegmentedControlScreen.tsx b/demo/src/screens/componentScreens/SegmentedControlScreen.tsx index 4e91fa21b0..7a9bb02f63 100644 --- a/demo/src/screens/componentScreens/SegmentedControlScreen.tsx +++ b/demo/src/screens/componentScreens/SegmentedControlScreen.tsx @@ -14,7 +14,7 @@ const segments = { }, {label: 'Short'} ], - forth: [{label: 'With'}, {label: 'Custom'}, {label: 'Colors'}], + forth: [{label: 'With'}, {label: 'Custom'}, {label: 'Style'}], fifth: [{label: 'Full'}, {label: 'Width'}], sixth: [{label: 'Full'}, {label: 'Width'}, {label: 'With'}, {label: 'A'}, {label: 'Very Long Segment'}] }; @@ -49,6 +49,8 @@ const SegmentedControlScreen = () => { backgroundColor={Colors.$backgroundInverted} activeBackgroundColor={Colors.$backgroundNeutralIdle} inactiveColor={Colors.$textDisabled} + style={styles.customStyle} + segmentsStyle={styles.customSegmentsStyle} /> { const styles = StyleSheet.create({ container: { marginTop: 20 + }, + customStyle: { + height: 50, + width: 300 + }, + customSegmentsStyle: { + height: 50 } }); diff --git a/demo/src/screens/componentScreens/SliderScreen.tsx b/demo/src/screens/componentScreens/SliderScreen.tsx index 402d4d4a53..7d5fe1494b 100644 --- a/demo/src/screens/componentScreens/SliderScreen.tsx +++ b/demo/src/screens/componentScreens/SliderScreen.tsx @@ -157,7 +157,13 @@ export default class SliderScreen extends Component + + ); + } + renderCustomSlider() { + return ( + <> Custom with Steps @@ -174,7 +180,7 @@ export default class SliderScreen extends Component - + ); } @@ -317,6 +323,7 @@ export default class SliderScreen extends Component { const Container = locked ? View : TouchableOpacity; return ( toggleItemSelection(item)} // overriding the BG color to anything other than white will cause Android's elevation to fail diff --git a/demo/src/screens/incubatorScreens/IncubatorCalendarScreen/MockServer.ts b/demo/src/screens/incubatorScreens/IncubatorCalendarScreen/MockServer.ts new file mode 100644 index 0000000000..b942d2885f --- /dev/null +++ b/demo/src/screens/incubatorScreens/IncubatorCalendarScreen/MockServer.ts @@ -0,0 +1,22 @@ +import _ from 'lodash'; +import {data} from './MockData'; + +const PAGE_SIZE = 100; +const FAKE_FETCH_TIME = 1500; + +class MockServer { + async getEvents(date: number): Promise { + return new Promise(resolve => { + const eventIndexByDate = _.findIndex(data, event => { + return event.start > date; + }); + + setTimeout(() => { + const newEvents = _.slice(data, eventIndexByDate - PAGE_SIZE / 2, eventIndexByDate + PAGE_SIZE / 2); + resolve(newEvents); + }, FAKE_FETCH_TIME); + }); + } +} + +export default new MockServer(); diff --git a/demo/src/screens/incubatorScreens/IncubatorCalendarScreen/index.tsx b/demo/src/screens/incubatorScreens/IncubatorCalendarScreen/index.tsx index 845048db69..61053eda58 100644 --- a/demo/src/screens/incubatorScreens/IncubatorCalendarScreen/index.tsx +++ b/demo/src/screens/incubatorScreens/IncubatorCalendarScreen/index.tsx @@ -1,27 +1,49 @@ +import _ from 'lodash'; import React, {Component} from 'react'; -import {View, Incubator} from 'react-native-ui-lib'; -import {data} from './MockData'; +import {Incubator} from 'react-native-ui-lib'; +import MockServer from './MockServer'; export default class CalendarScreen extends Component { - // constructor(props) { - // super(props); - - // setTimeout(() => { - // this.setState({date: 1676026748000}); - // }, 2000); - // } + pageIndex = 0; state = { - date: undefined + date: new Date().getTime(), + events: [] as any[], + showLoader: false }; - + + componentDidMount(): void { + this.loadEvents(this.state.date); + } + + loadEvents = async (date: number) => { + this.setState({showLoader: true}); + const {events} = this.state; + const newEvents = await MockServer.getEvents(date); + this.pageIndex++; + this.setState({events: _.uniqBy([...events, ...newEvents], e => e.id), showLoader: false}); + }; + + onChangeDate = (date: number) => { + console.log('Date change: ', date); + const {events} = this.state; + if (date < events[0]?.start || date > _.last(events)?.start) { + console.log('Load new events'); + this.loadEvents(date); + } + }; + + onEndReached = (date: number) => { + console.log('Reached End: ', date); + this.loadEvents(date); + }; + render() { + const {date, events, showLoader} = this.state; return ( - - - - - + + + ); } } diff --git a/demo/src/screens/incubatorScreens/IncubatorSliderScreen.tsx b/demo/src/screens/incubatorScreens/IncubatorSliderScreen.tsx index 2f0067ba05..f4765a8a8c 100644 --- a/demo/src/screens/incubatorScreens/IncubatorSliderScreen.tsx +++ b/demo/src/screens/incubatorScreens/IncubatorSliderScreen.tsx @@ -1,6 +1,6 @@ import React, {useState, useRef, useCallback} from 'react'; import {StyleSheet, ScrollView} from 'react-native'; -import {Constants, Colors, View, Text, Button, Incubator} from 'react-native-ui-lib'; //eslint-disable-line +import {Constants, Colors, View, Text, Button, Incubator, GradientSlider, ColorSliderGroup} from 'react-native-ui-lib'; //eslint-disable-line import {renderBooleanOption} from '../ExampleScreenPresenter'; const VALUE = 20; @@ -9,6 +9,7 @@ const MIN = 0; const MAX = Constants.screenWidth - 40; // horizontal margins 20 const INITIAL_MIN = 30; const INITIAL_MAX = 270; +const COLOR = Colors.blue30; const IncubatorSliderScreen = () => { const [disableRTL, setDisableRTL] = useState(false); @@ -19,6 +20,9 @@ const IncubatorSliderScreen = () => { const [sliderMinValue, setSliderMinValue] = useState(INITIAL_MIN); const [sliderMaxValue, setSliderMaxValue] = useState(INITIAL_MAX); + const [color, setColor] = useState(COLOR); + const [alpha, setAlpha] = useState(1); + const slider = useRef(); const customSlider = useRef(); const negativeSlider = useRef(); @@ -48,6 +52,15 @@ const IncubatorSliderScreen = () => { setSliderMinValue(value.min); }, []); + const onGradientValueChange = useCallback((value: string, alpha: number) => { + setColor(value); + setAlpha(alpha); + }, []); + + const onGroupValueChange = (value: string) => { + console.log('onGroupValueChange: ', value); + }; + const renderValuesBox = (min: number, max?: number) => { if (max !== undefined) { return ( @@ -179,6 +192,65 @@ const IncubatorSliderScreen = () => { ); }; + const renderGradientSlidersExample = () => { + return ( + + + Gradient Sliders + + + + DEFAULT + + + + + + + + + HUE + + + + + + + + ); + }; + + const renderColorSliderGroupExample = () => { + return ( + <> + + Color Slider Group + + + + ); + }; + return ( @@ -196,6 +268,8 @@ const IncubatorSliderScreen = () => { {renderCustomSliderExample()} {renderNegativeSliderExample()} {renderRangeSliderExample()} + {renderGradientSlidersExample()} + {renderColorSliderGroupExample()} ); }; @@ -221,5 +295,26 @@ const styles = StyleSheet.create({ backgroundColor: Colors.red30, borderWidth: 2, borderColor: Colors.red70 + }, + gradientSliderContainer: { + flex: 1, // NOTE: to place a slider in a row layout you must set flex in its 'containerStyle'!!! + marginHorizontal: 20, + marginVertical: 10 + }, + box: { + width: 20, + height: 20, + borderRadius: 4, + borderWidth: 1, + borderColor: Colors.$outlineDefault + }, + slider: { + marginVertical: 6 + }, + group: { + backgroundColor: Colors.$backgroundNeutralMedium, + padding: 20, + margin: 20, + borderRadius: 6 } }); diff --git a/ios/rnuilib.xcodeproj/project.pbxproj b/ios/rnuilib.xcodeproj/project.pbxproj index db28547d76..3f365b54c8 100644 --- a/ios/rnuilib.xcodeproj/project.pbxproj +++ b/ios/rnuilib.xcodeproj/project.pbxproj @@ -8,17 +8,17 @@ /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* rnuilibTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* rnuilibTests.m */; }; - 0A1C60508FA9D9CBB0208734 /* libPods-rnuilib-rnuilibTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8E874090C50DE6EC77F7F72 /* libPods-rnuilib-rnuilibTests.a */; }; 10A864C1285B5CB00011FF03 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 10A864C0285B5CB00011FF03 /* AppDelegate.mm */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 2D02E4BD1E0B4A84006451C7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 2D02E4BF1E0B4AB3006451C7 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 2DCD954D1E0B4F2C00145EB5 /* rnuilibTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* rnuilibTests.m */; }; + 56EBC4A1F72808C5B51F7BBF /* libPods-rnuilib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C5768CF5EB4CEC5AF24695D /* libPods-rnuilib.a */; }; 8E52CBDF2887DD21009D5EC5 /* DesignTokens.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8E52CBDE2887DD21009D5EC5 /* DesignTokens.xcassets */; }; 8E8B0D662744D9CD0026B520 /* void.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E8B0D652744D9CD0026B520 /* void.swift */; }; 8EA1FC8C2519E7F7008B4B36 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8EA1FC8B2519E7F7008B4B36 /* LaunchScreen.storyboard */; }; - AF7086C5E11D38CFBEB20C79 /* libPods-rnuilib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3637F09D204DA39F84CE0A /* libPods-rnuilib.a */; }; + C24F706C923507D9D0AFAF26 /* libPods-rnuilib-rnuilibTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB858F3A7230EAA142D23DA1 /* libPods-rnuilib-rnuilibTests.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -49,19 +49,19 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = rnuilib/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = rnuilib/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = rnuilib/main.m; sourceTree = ""; }; - 1A3637F09D204DA39F84CE0A /* libPods-rnuilib.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-rnuilib.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 2D02E47B1E0B4A5D006451C7 /* rnuilib-tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "rnuilib-tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 2D02E4901E0B4A5D006451C7 /* rnuilib-tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "rnuilib-tvOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3D3E2E98DFD30898C81A5293 /* Pods-rnuilib-rnuilibTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-rnuilib-rnuilibTests.release.xcconfig"; path = "Target Support Files/Pods-rnuilib-rnuilibTests/Pods-rnuilib-rnuilibTests.release.xcconfig"; sourceTree = ""; }; - 4F15E4C4D4931613EAD48585 /* Pods-rnuilib.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-rnuilib.release.xcconfig"; path = "Target Support Files/Pods-rnuilib/Pods-rnuilib.release.xcconfig"; sourceTree = ""; }; - 666D1B6A60C23741B20A5051 /* Pods-rnuilib-rnuilibTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-rnuilib-rnuilibTests.debug.xcconfig"; path = "Target Support Files/Pods-rnuilib-rnuilibTests/Pods-rnuilib-rnuilibTests.debug.xcconfig"; sourceTree = ""; }; - 71C796B95A6CB852F0146DEF /* Pods-rnuilib.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-rnuilib.debug.xcconfig"; path = "Target Support Files/Pods-rnuilib/Pods-rnuilib.debug.xcconfig"; sourceTree = ""; }; + 56523CB09C6A79FEBA0C210A /* Pods-rnuilib-rnuilibTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-rnuilib-rnuilibTests.release.xcconfig"; path = "Target Support Files/Pods-rnuilib-rnuilibTests/Pods-rnuilib-rnuilibTests.release.xcconfig"; sourceTree = ""; }; + 65B766BC0A8D1F830F3D2004 /* Pods-rnuilib.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-rnuilib.debug.xcconfig"; path = "Target Support Files/Pods-rnuilib/Pods-rnuilib.debug.xcconfig"; sourceTree = ""; }; + 66AD894C4A02A884861A27E2 /* Pods-rnuilib.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-rnuilib.release.xcconfig"; path = "Target Support Files/Pods-rnuilib/Pods-rnuilib.release.xcconfig"; sourceTree = ""; }; + 6C5768CF5EB4CEC5AF24695D /* libPods-rnuilib.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-rnuilib.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 86A4AD8C011363501515A0EC /* Pods-rnuilib-rnuilibTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-rnuilib-rnuilibTests.debug.xcconfig"; path = "Target Support Files/Pods-rnuilib-rnuilibTests/Pods-rnuilib-rnuilibTests.debug.xcconfig"; sourceTree = ""; }; 8E52CBDE2887DD21009D5EC5 /* DesignTokens.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DesignTokens.xcassets; sourceTree = ""; }; 8E8B0D652744D9CD0026B520 /* void.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = void.swift; sourceTree = ""; }; 8EA1FC8B2519E7F7008B4B36 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = rnuilib/LaunchScreen.storyboard; sourceTree = ""; }; - A8E874090C50DE6EC77F7F72 /* libPods-rnuilib-rnuilibTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-rnuilib-rnuilibTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; + FB858F3A7230EAA142D23DA1 /* libPods-rnuilib-rnuilibTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-rnuilib-rnuilibTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -69,7 +69,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0A1C60508FA9D9CBB0208734 /* libPods-rnuilib-rnuilibTests.a in Frameworks */, + C24F706C923507D9D0AFAF26 /* libPods-rnuilib-rnuilibTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -77,7 +77,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - AF7086C5E11D38CFBEB20C79 /* libPods-rnuilib.a in Frameworks */, + 56EBC4A1F72808C5B51F7BBF /* libPods-rnuilib.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -136,8 +136,8 @@ children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED2971642150620600B7C4FE /* JavaScriptCore.framework */, - 1A3637F09D204DA39F84CE0A /* libPods-rnuilib.a */, - A8E874090C50DE6EC77F7F72 /* libPods-rnuilib-rnuilibTests.a */, + 6C5768CF5EB4CEC5AF24695D /* libPods-rnuilib.a */, + FB858F3A7230EAA142D23DA1 /* libPods-rnuilib-rnuilibTests.a */, ); name = Frameworks; sourceTree = ""; @@ -178,10 +178,10 @@ D99C980AB24A05601E0007F9 /* Pods */ = { isa = PBXGroup; children = ( - 71C796B95A6CB852F0146DEF /* Pods-rnuilib.debug.xcconfig */, - 4F15E4C4D4931613EAD48585 /* Pods-rnuilib.release.xcconfig */, - 666D1B6A60C23741B20A5051 /* Pods-rnuilib-rnuilibTests.debug.xcconfig */, - 3D3E2E98DFD30898C81A5293 /* Pods-rnuilib-rnuilibTests.release.xcconfig */, + 65B766BC0A8D1F830F3D2004 /* Pods-rnuilib.debug.xcconfig */, + 66AD894C4A02A884861A27E2 /* Pods-rnuilib.release.xcconfig */, + 86A4AD8C011363501515A0EC /* Pods-rnuilib-rnuilibTests.debug.xcconfig */, + 56523CB09C6A79FEBA0C210A /* Pods-rnuilib-rnuilibTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -193,12 +193,12 @@ isa = PBXNativeTarget; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "rnuilibTests" */; buildPhases = ( - 03ACEC70B300DF14A2726B96 /* [CP] Check Pods Manifest.lock */, + 72FA426D5675971732169A33 /* [CP] Check Pods Manifest.lock */, 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, - F68A06EBFB89A15C94C6B5FF /* [CP] Copy Pods Resources */, - 2403068A2103B3AD40F9CB77 /* [CP] Embed Pods Frameworks */, + 4A2DFE2E4F276607CC9422BA /* [CP] Embed Pods Frameworks */, + 05F5C3E9528158446F3788A5 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -214,14 +214,14 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "rnuilib" */; buildPhases = ( - D7E6439E4D8846E5E7EBBA81 /* [CP] Check Pods Manifest.lock */, + 5C139D0477DFF8903C0C457A /* [CP] Check Pods Manifest.lock */, FD10A7F022414F080027D42C /* Start Packager */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - 6A01744B3971F15EA74E9EC1 /* [CP] Copy Pods Resources */, - B501C960470EF44E6FDFF61C /* [CP] Embed Pods Frameworks */, + A077BED00DEE8BB3D2E7978D /* [CP] Embed Pods Frameworks */, + 74E5355D055C34254F806DF4 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -368,29 +368,39 @@ shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; }; - 03ACEC70B300DF14A2726B96 /* [CP] Check Pods Manifest.lock */ = { + 05F5C3E9528158446F3788A5 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-rnuilib-rnuilibTests/Pods-rnuilib-rnuilibTests-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle", ); + name = "[CP] Copy Pods Resources"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-rnuilib-rnuilibTests-checkManifestLockResult.txt", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-rnuilib-rnuilibTests/Pods-rnuilib-rnuilibTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 2403068A2103B3AD40F9CB77 /* [CP] Embed Pods Frameworks */ = { + 2D02E4CB1E0B4B27006451C7 /* Bundle React Native Code And Images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native Code And Images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh"; + }; + 4A2DFE2E4F276607CC9422BA /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -414,21 +424,51 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-rnuilib-rnuilibTests/Pods-rnuilib-rnuilibTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 2D02E4CB1E0B4B27006451C7 /* Bundle React Native Code And Images */ = { + 5C139D0477DFF8903C0C457A /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Bundle React Native Code And Images"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-rnuilib-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 72FA426D5675971732169A33 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-rnuilib-rnuilibTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 6A01744B3971F15EA74E9EC1 /* [CP] Copy Pods Resources */ = { + 74E5355D055C34254F806DF4 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -446,7 +486,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-rnuilib/Pods-rnuilib-resources.sh\"\n"; showEnvVarsInLog = 0; }; - B501C960470EF44E6FDFF61C /* [CP] Embed Pods Frameworks */ = { + A077BED00DEE8BB3D2E7978D /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -470,46 +510,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-rnuilib/Pods-rnuilib-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - D7E6439E4D8846E5E7EBBA81 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-rnuilib-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - F68A06EBFB89A15C94C6B5FF /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-rnuilib-rnuilibTests/Pods-rnuilib-rnuilibTests-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle", - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-rnuilib-rnuilibTests/Pods-rnuilib-rnuilibTests-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; FD10A7F022414F080027D42C /* Start Packager */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -603,7 +603,7 @@ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 666D1B6A60C23741B20A5051 /* Pods-rnuilib-rnuilibTests.debug.xcconfig */; + baseConfigurationReference = 86A4AD8C011363501515A0EC /* Pods-rnuilib-rnuilibTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -627,7 +627,7 @@ }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 3D3E2E98DFD30898C81A5293 /* Pods-rnuilib-rnuilibTests.release.xcconfig */; + baseConfigurationReference = 56523CB09C6A79FEBA0C210A /* Pods-rnuilib-rnuilibTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -649,7 +649,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 71C796B95A6CB852F0146DEF /* Pods-rnuilib.debug.xcconfig */; + baseConfigurationReference = 65B766BC0A8D1F830F3D2004 /* Pods-rnuilib.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -660,6 +660,7 @@ "FB_SONARKIT_ENABLED=1", ); INFOPLIST_FILE = rnuilib/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 1.1.16; OTHER_LDFLAGS = ( @@ -668,6 +669,7 @@ "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = org.reactjs.native.example.wix.rnuilib; PRODUCT_NAME = rnuilib; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -678,12 +680,13 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4F15E4C4D4931613EAD48585 /* Pods-rnuilib.release.xcconfig */; + baseConfigurationReference = 66AD894C4A02A884861A27E2 /* Pods-rnuilib.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; INFOPLIST_FILE = rnuilib/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 1.1.16; OTHER_LDFLAGS = ( @@ -692,6 +695,7 @@ "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = org.reactjs.native.example.wix.rnuilib; PRODUCT_NAME = rnuilib; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/jestSetup/GestureDetectorMock.tsx b/jestSetup/GestureDetectorMock.tsx new file mode 100644 index 0000000000..57e2827a5c --- /dev/null +++ b/jestSetup/GestureDetectorMock.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import {TouchableOpacity} from 'react-native'; + +type Props = { + gesture: any; + children: any; +}; + +const DEFAULT_DATA = { + absoluteX: 0, + absoluteY: 0, + translationX: 0, + translationY: 0, + velocityX: 0, + velocityY: 0, + x: 0, + y: 0 +}; + +export class GestureDetectorMock extends React.Component { + render() { + switch (this.props.gesture.type) { + case 'tap': + return ( + { + this.props.gesture._handlers.onTouchesDown?.(); + this.props.gesture._handlers.onEnd?.(); + this.props.gesture._handlers.onFinalize?.(); + }} + > + {this.props.children} + + ); + case 'pan': + return ( + { + this.props.gesture._handlers.onStart?.(DEFAULT_DATA); + if (Array.isArray(data)) { + data.forEach(info => { + this.props.gesture._handlers.onUpdate?.({...DEFAULT_DATA, ...info}); + }); + } else { + this.props.gesture._handlers.onUpdate?.({...DEFAULT_DATA, ...data}); + } + this.props.gesture._handlers.onEnd?.(DEFAULT_DATA); + this.props.gesture._handlers.onFinalize?.(DEFAULT_DATA); + }} + > + {this.props.children} + + ); + default: + throw new Error(`Unhandled gesture of type: ${this.props.gesture.type}`); + } + } +} diff --git a/jest-setup.js b/jestSetup/jest-setup.js similarity index 83% rename from jest-setup.js rename to jestSetup/jest-setup.js index 37144457c5..0da842324c 100644 --- a/jest-setup.js +++ b/jestSetup/jest-setup.js @@ -74,6 +74,11 @@ jest.mock('react-native-gesture-handler', PanMock.onStart = getDefaultMockedHandler('onStart'); PanMock.onUpdate = getDefaultMockedHandler('onUpdate'); PanMock.onEnd = getDefaultMockedHandler('onEnd'); + PanMock.onFinalize = getDefaultMockedHandler('onFinalize'); + PanMock.activateAfterLongPress = getDefaultMockedHandler('activateAfterLongPress'); + PanMock.enabled = getDefaultMockedHandler('enabled'); + PanMock.onTouchesMove = getDefaultMockedHandler('onTouchesMove'); + PanMock.prepare = jest.fn(); PanMock.initialize = jest.fn(); PanMock.toGestureArray = jest.fn(() => { return [PanMock]; @@ -92,6 +97,20 @@ jest.mock('react-native-gesture-handler', jest.mock('react-native', () => { const reactNative = jest.requireActual('react-native'); reactNative.NativeModules.KeyboardTrackingViewTempManager = {}; + const OriginalModal = reactNative.Modal; + const React = jest.requireActual('react'); + const useDidUpdate = require('./useDidUpdate').default; + Object.defineProperty(reactNative, 'Modal', { + value: props => { + // eslint-disable-next-line react-hooks/rules-of-hooks + useDidUpdate(() => { + if (!props.visible) { + props.onDismiss?.(); + } + }, [props.visible]); + return ; + } + }); return reactNative; }); diff --git a/jestSetup/useDidUpdate.ts b/jestSetup/useDidUpdate.ts new file mode 100644 index 0000000000..654a79a9b2 --- /dev/null +++ b/jestSetup/useDidUpdate.ts @@ -0,0 +1,18 @@ +import {useEffect, useRef, DependencyList} from 'react'; + +/** + * This hook avoid calling useEffect on the initial value of his dependency array + */ +const useDidUpdate = (callback: () => void, dep: DependencyList) => { + const isMounted = useRef(false); + + useEffect(() => { + if (isMounted.current) { + callback(); + } else { + isMounted.current = true; + } + }, dep); +}; + +export default useDidUpdate; diff --git a/package.json b/package.json index 02a080f6d5..fc95ddd755 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "prop-types": "^15.5.10", "react-freeze": "^1.0.0", "react-native-redash": "^12.0.3", - "react-native-text-size": "4.0.0-rc.1", + "wix-react-native-text-size": "~1.0.0", "semver": "^5.5.0", "tinycolor2": "^1.4.2", "url-parse": "^1.2.0" @@ -139,7 +139,7 @@ "/uilib-docs/" ], "setupFiles": [ - "./jest-setup.js" + "./jestSetup/jest-setup.js" ], "testMatch": [ "**/*.spec.(js|ts|tsx)" diff --git a/src/commons/Constants.ts b/src/commons/Constants.ts index 43f06ac4d3..0b45facc5e 100644 --- a/src/commons/Constants.ts +++ b/src/commons/Constants.ts @@ -1,11 +1,27 @@ -import {Platform, Dimensions, NativeModules, I18nManager, AccessibilityInfo, AccessibilityChangeEvent} from 'react-native'; - +import { + Platform, + Dimensions, + NativeModules, + I18nManager, + AccessibilityInfo, + AccessibilityChangeEvent +} from 'react-native'; +import {LogService} from '../services'; export enum orientations { PORTRAIT = 'portrait', LANDSCAPE = 'landscape' } +export interface Breakpoint { + breakpoint: number; + pageMargin: number; +} + +function breakpointComparator(b1: Breakpoint, b2: Breakpoint) { + return b1.breakpoint - b2.breakpoint; +} + const isAndroid: boolean = Platform.OS === 'android'; const isIOS: boolean = Platform.OS === 'ios'; const isWeb: boolean = Platform.OS === 'web'; @@ -15,6 +31,8 @@ let screenHeight: number = Dimensions.get('screen').height; let screenWidth: number = Dimensions.get('screen').width; let windowHeight: number = Dimensions.get('window').height; let windowWidth: number = Dimensions.get('window').width; +let breakpoints: Breakpoint[]; +let defaultMargin = 0; //@ts-ignore isTablet = Platform.isPad || (getAspectRatio() < 1.6 && Math.max(screenWidth, screenHeight) >= 900); @@ -112,6 +130,26 @@ const constants = { set isTablet(value: boolean) { isTablet = value; }, + setBreakpoints(value: Breakpoint[], options?: {defaultMargin: number}) { + breakpoints = value.sort(breakpointComparator); + if (options) { + defaultMargin = options.defaultMargin; + } + }, + getPageMargins(): number { + if (!breakpoints) { + LogService.warn('UILib breakpoints must be set via setBreakpoints before using getPageMargins'); + return 0; + } + + for (let i = breakpoints.length - 1; i >= 0; --i) { + if (screenWidth > breakpoints[i].breakpoint) { + return breakpoints[i].pageMargin; + } + } + + return defaultMargin; + }, get isWideScreen() { return isTablet || this.isLandscape; }, @@ -123,12 +161,14 @@ const constants = { }, /* Devices */ get isIphoneX() { - return isIOS && + return ( + isIOS && //@ts-ignore !Platform.isPad && //@ts-ignore !Platform.isTVOS && - (screenHeight >= 812 || screenWidth >= 812); + (screenHeight >= 812 || screenWidth >= 812) + ); }, /* Orientation */ dimensionsEventListener: undefined, @@ -154,3 +194,10 @@ setStatusBarHeight(); Dimensions.addEventListener('change', updateConstants); export default constants; + +// For tests +export const _reset = () => { + // @ts-ignore + breakpoints = undefined; + defaultMargin = 0; +}; diff --git a/src/commons/__tests__/constants.spec.ts b/src/commons/__tests__/constants.spec.ts new file mode 100644 index 0000000000..5efc3424ed --- /dev/null +++ b/src/commons/__tests__/constants.spec.ts @@ -0,0 +1,94 @@ +import {default as Constants, updateConstants, _reset} from '../Constants'; + +describe('Constants', () => { + beforeEach(() => { + _reset(); + }); + + describe('Breakpoints and Page Margins', () => { + it('getPageMargins without init should return 0 and trigger a warn', () => { + const warn = console.warn; + console.warn = jest.fn(); + expect(Constants.getPageMargins()).toBe(0); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith('UILib breakpoints must be set via setBreakpoints before using getPageMargins'); + console.warn = warn; + }); + + it('getPageMargins with one breakpoint', () => { + const original = { + screen: {width: Constants.screenWidth, height: Constants.screenHeight}, + window: {width: Constants.windowWidth, height: Constants.windowHeight} + }; + updateConstants({screen: {width: 50, height: 50}, window: {width: 50, height: 50}}); + Constants.setBreakpoints([{breakpoint: 100, pageMargin: 5}]); + expect(Constants.getPageMargins()).toBe(0); + updateConstants(original); + expect(Constants.getPageMargins()).toBe(5); + }); + + it('getPageMargins with one breakpoint and a default', () => { + const original = { + screen: {width: Constants.screenWidth, height: Constants.screenHeight}, + window: {width: Constants.windowWidth, height: Constants.windowHeight} + }; + updateConstants({screen: {width: 50, height: 50}, window: {width: 50, height: 50}}); + Constants.setBreakpoints([{breakpoint: 100, pageMargin: 5}], {defaultMargin: 3}); + expect(Constants.getPageMargins()).toBe(3); + updateConstants(original); + expect(Constants.getPageMargins()).toBe(5); + }); + + it('getPageMargins with three breakpoints', () => { + const original = { + screen: {width: Constants.screenWidth, height: Constants.screenHeight}, + window: {width: Constants.windowWidth, height: Constants.windowHeight} + }; + updateConstants({screen: {width: 50, height: 50}, window: {width: 50, height: 50}}); + Constants.setBreakpoints([ + {breakpoint: 100, pageMargin: 5}, + {breakpoint: 1000, pageMargin: 10} + ]); + expect(Constants.getPageMargins()).toBe(0); + updateConstants({screen: {width: 1200, height: 1200}, window: {width: 1200, height: 1200}}); + expect(Constants.getPageMargins()).toBe(10); + updateConstants(original); + expect(Constants.getPageMargins()).toBe(5); + }); + + it('getPageMargins with three breakpoints and a default', () => { + const original = { + screen: {width: Constants.screenWidth, height: Constants.screenHeight}, + window: {width: Constants.windowWidth, height: Constants.windowHeight} + }; + updateConstants({screen: {width: 50, height: 50}, window: {width: 50, height: 50}}); + Constants.setBreakpoints([ + {breakpoint: 100, pageMargin: 5}, + {breakpoint: 1000, pageMargin: 10} + ], + {defaultMargin: 3}); + expect(Constants.getPageMargins()).toBe(3); + updateConstants({screen: {width: 1200, height: 1200}, window: {width: 1200, height: 1200}}); + expect(Constants.getPageMargins()).toBe(10); + updateConstants(original); + expect(Constants.getPageMargins()).toBe(5); + }); + + it('setBreakpoints should arrange input in order', () => { + const original = { + screen: {width: Constants.screenWidth, height: Constants.screenHeight}, + window: {width: Constants.windowWidth, height: Constants.windowHeight} + }; + updateConstants({screen: {width: 50, height: 50}, window: {width: 50, height: 50}}); + Constants.setBreakpoints([ + {breakpoint: 1000, pageMargin: 10}, + {breakpoint: 100, pageMargin: 5} + ]); + expect(Constants.getPageMargins()).toBe(0); + updateConstants({screen: {width: 1200, height: 1200}, window: {width: 1200, height: 1200}}); + expect(Constants.getPageMargins()).toBe(10); + updateConstants(original); + expect(Constants.getPageMargins()).toBe(5); + }); + }); +}); diff --git a/src/components/WheelPicker/index.tsx b/src/components/WheelPicker/index.tsx index 7c63fe7819..014ea40036 100644 --- a/src/components/WheelPicker/index.tsx +++ b/src/components/WheelPicker/index.tsx @@ -146,6 +146,13 @@ const WheelPicker = ({ const prevIndex = useRef(currentIndex); const [flatListWidth, setFlatListWidth] = useState(0); const keyExtractor = useCallback((item: ItemProps, index: number) => `${item}.${index}`, []); + const androidFlatListProps = useMemo(() => { + if (Constants.isAndroid) { + return { + maxToRenderPerBatch: items.length + }; + } + }, [items]); useEffect(() => { // This effect making sure to reset index if initialValue has changed @@ -323,6 +330,7 @@ const WheelPicker = ({ maxToRenderPerBatch prop set to items.length to solve FlatList bug on Android, you can override it by passing your own flatListProps with maxToRenderPerBatch prop.
See the RN FlatList issue for more info: https://github.com/facebook/react-native/issues/15990", "example": "https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/WheelPickerScreen.tsx", "images": [], "props": [ @@ -33,7 +34,12 @@ "default": "center" }, {"name": "separatorsStyle", "type": "ViewStyle", "description": "Extra style for the separators"}, - {"name": "testID", "type": "string", "description": "test identifier"} + {"name": "testID", "type": "string", "description": "test identifier"}, + { + "name": "flatListProps", + "type": "FlatListProps", + "description": "Props to be sent to the FlatList." + } ], "snippet": [ " { > {this.handleRenderIcon(option)} - + {option.label} diff --git a/src/components/button/Button.driver.ts b/src/components/button/Button.driver.ts index 914a353b42..f300d84a2b 100644 --- a/src/components/button/Button.driver.ts +++ b/src/components/button/Button.driver.ts @@ -4,7 +4,7 @@ import {TextDriver} from '../text/Text.driver'; /** * Please run clear after each test - * */ + */ export class ButtonDriver extends ComponentDriver { private readonly labelDriver: TextDriver; private readonly iconDriver: ImageDriver; diff --git a/src/components/colorPicker/ColorPickerDialog.tsx b/src/components/colorPicker/ColorPickerDialog.tsx index a15ce6cbb2..899c1cd4dd 100644 --- a/src/components/colorPicker/ColorPickerDialog.tsx +++ b/src/components/colorPicker/ColorPickerDialog.tsx @@ -51,6 +51,10 @@ interface Props extends DialogProps { doneButton?: string, input?: string }; + /** + * Whether to use the new Slider implementation using Reanimated + */ + migrate?: boolean; } export type ColorPickerDialogProps = Props; @@ -247,6 +251,7 @@ class ColorPickerDialog extends PureComponent { renderSliders() { const {keyboardHeight, color} = this.state; + const {migrate} = this.props; const colorValue = color.a === 0 ? Colors.$backgroundInverted : Colors.getHexString(color); return ( @@ -258,6 +263,7 @@ class ColorPickerDialog extends PureComponent { labelsStyle={styles.label} onValueChange={this.onSliderValueChange} accessible={false} + migrate={migrate} /> ); } diff --git a/src/components/dateTimePicker/index.tsx b/src/components/dateTimePicker/index.tsx index 6c182dd9d2..a97376d076 100644 --- a/src/components/dateTimePicker/index.tsx +++ b/src/components/dateTimePicker/index.tsx @@ -1,7 +1,16 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, + useImperativeHandle, + forwardRef, + ForwardedRef +} from 'react'; import {StyleProp, StyleSheet, ViewStyle} from 'react-native'; import {DateTimePickerPackage as RNDateTimePicker, MomentPackage as moment} from '../../optionalDependencies'; -import {useDidUpdate} from 'hooks'; +import {useDidUpdate} from '../../hooks'; import {Colors} from '../../style'; import Assets from '../../assets'; import {Constants, asBaseComponent, BaseComponentInjectedProps} from '../../commons/new'; @@ -10,7 +19,7 @@ import {DialogProps} from '../dialog'; import View from '../view'; import Button from '../button'; import ExpandableOverlay, {ExpandableOverlayMethods, RenderCustomOverlayProps} from '../../incubator/expandableOverlay'; -import type {TextFieldProps} from '../../incubator/TextField'; +import type {TextFieldProps, TextFieldMethods} from '../../incubator/TextField'; const MODES = { DATE: 'date', @@ -98,11 +107,10 @@ export type DateTimePickerProps = Omit & { * Should migrate to the new TextField implementation */ migrateTextField?: boolean; -} +}; type DateTimePickerPropsInternal = DateTimePickerProps & BaseComponentInjectedProps; - /*eslint-disable*/ /** * @description: Date and Time Picker Component that wraps RNDateTimePicker for date and time modes. @@ -113,7 +121,7 @@ type DateTimePickerPropsInternal = DateTimePickerProps & BaseComponentInjectedPr * @gif: https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/DateTimePicker/DateTimePicker_iOS.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/DateTimePicker/DateTimePicker_Android.gif?raw=true */ /*eslint-enable*/ -function DateTimePicker(props: DateTimePickerPropsInternal) { +const DateTimePicker = forwardRef((props: DateTimePickerPropsInternal, ref: ForwardedRef) => { const { value: propsValue, renderInput, @@ -143,6 +151,13 @@ function DateTimePicker(props: DateTimePickerPropsInternal) { const [value, setValue] = useState(propsValue); const chosenDate = useRef(propsValue); const expandable = useRef(); + const textField = useRef(); + + useImperativeHandle(ref, () => { + return { + validate: () => textField.current?.validate() + }; + }); useEffect(() => { if (!RNDateTimePicker) { @@ -322,6 +337,8 @@ function DateTimePicker(props: DateTimePickerPropsInternal) { ) : ( ); -} +}); DateTimePicker.displayName = 'DateTimePicker'; export {DateTimePicker}; // For tests diff --git a/src/components/hint/index.tsx b/src/components/hint/index.tsx index 5baba45250..b261977108 100644 --- a/src/components/hint/index.tsx +++ b/src/components/hint/index.tsx @@ -291,7 +291,7 @@ class Hint extends Component { } getTargetPositionOnScreen() { - if (this.targetLayout?.x && this.targetLayout?.width) { + if (this.targetLayout?.x !== undefined && this.targetLayout?.width) { const targetMidPosition = this.targetLayout.x + this.targetLayout.width / 2; if (targetMidPosition > this.containerWidth * (2 / 3)) { @@ -314,7 +314,7 @@ class Hint extends Component { const {position} = this.props; const hintPositionStyle: HintPositionStyle = {alignItems: 'center'}; - if (this.targetLayout?.x) { + if (this.targetLayout?.x !== undefined) { hintPositionStyle.left = -this.targetLayout.x; } @@ -337,7 +337,7 @@ class Hint extends Component { getHintPadding() { const paddings: Paddings = {paddingVertical: this.hintOffset, paddingHorizontal: this.edgeMargins}; - if (this.useSideTip && this.targetLayout?.x) { + if (this.useSideTip && this.targetLayout?.x !== undefined) { const targetPositionOnScreen = this.getTargetPositionOnScreen(); if (targetPositionOnScreen === TARGET_POSITIONS.LEFT) { paddings.paddingLeft = this.targetLayout.x; @@ -376,7 +376,7 @@ class Hint extends Component { const layoutWidth = this.targetLayout?.width || 0; - if (this.targetLayout?.x) { + if (this.targetLayout?.x !== undefined) { const targetMidWidth = layoutWidth / 2; const tipMidWidth = this.tipSize.width / 2; diff --git a/src/components/modal/TopBar.tsx b/src/components/modal/TopBar.tsx index f7f51c03a8..2f2a732cbe 100644 --- a/src/components/modal/TopBar.tsx +++ b/src/components/modal/TopBar.tsx @@ -17,6 +17,14 @@ export interface ModalTopBarProps { * title custom style */ titleStyle?: StyleProp; + /** + * subtitle to display below the top bar title + */ + subtitle?: string; + /** + * subtitle custom style + */ + subtitleStyle?: StyleProp; /** * done action props (Button props) */ @@ -162,22 +170,27 @@ class TopBar extends Component { }; render() { - const {title, titleStyle, includeStatusBar, containerStyle, useSafeArea} = this.props; + const {title, titleStyle, subtitle, subtitleStyle, includeStatusBar, containerStyle, useSafeArea} = this.props; return ( {includeStatusBar && } - + {this.renderCancel()} {this.renderLeftButtons()} - + {title} + {subtitle && ( + + {subtitle} + + )} - + {this.renderRightButtons()} {this.renderDone()} diff --git a/src/components/modal/api/modalTopBar.api.json b/src/components/modal/api/modalTopBar.api.json index 78f4b25c29..c2b1ecf555 100644 --- a/src/components/modal/api/modalTopBar.api.json +++ b/src/components/modal/api/modalTopBar.api.json @@ -7,6 +7,8 @@ "props": [ {"name": "title", "type": "string", "description": "Title to display in the center of the top bar"}, {"name": "titleStyle", "type": "TextStyle", "description": "Title custom style"}, + {"name": "subtitle", "type": "string", "description": "Subtitle to display below the top bar title"}, + {"name": "subtitleStyle", "type": "TextStyle", "description": "Subtitle custom style"}, { "name": "doneButtonProps", "type": "ButtonProps", diff --git a/src/components/picker/NativePicker.js b/src/components/picker/NativePicker.js index fd3aa2b618..02f4e1075c 100644 --- a/src/components/picker/NativePicker.js +++ b/src/components/picker/NativePicker.js @@ -7,6 +7,7 @@ import TouchableOpacity from '../touchableOpacity'; import {Colors} from '../../style'; import {WheelPicker} from '../../incubator'; +// TODO: remove this file? class NativePicker extends Component { static displayName = 'NativePicker'; @@ -65,10 +66,8 @@ class NativePicker extends Component { renderPicker = () => { const {selectedValue} = this.state; - const {children, /* renderNativePicker, */ pickerStyle, wheelPickerProps, testID} = this.props; - // if (_.isFunction(renderNativePicker)) { - // return renderNativePicker(this.props); - // } + const {children, pickerStyle, wheelPickerProps, testID} = this.props; + return ( & * Use wheel picker instead of a list picker */ useWheelPicker?: boolean; - // /** - // * Callback for rendering a custom native picker inside the dialog (relevant to native picker only) - // */ - // renderNativePicker?: () => React.ReactElement; /** * Pass props to the list component that wraps the picker options (allows to control FlatList behavior) */ - listProps?: FlatListProps; + listProps?: Partial>; /** * Pass props to the picker modal */ diff --git a/src/components/segmentedControl/index.tsx b/src/components/segmentedControl/index.tsx index 542a16ae95..580aba1559 100644 --- a/src/components/segmentedControl/index.tsx +++ b/src/components/segmentedControl/index.tsx @@ -13,7 +13,7 @@ import Reanimated, { import {Colors, BorderRadiuses, Spacings} from '../../style'; import {Constants, asBaseComponent} from '../../commons/new'; import View from '../view'; -import Segment, {SegmentedControlItemProps as SegmentProps} from './segment'; +import Segment, {SegmentedControlItemProps} from './segment'; const BORDER_WIDTH = 1; const TIMING_CONFIG: WithTimingConfig = { @@ -21,7 +21,7 @@ const TIMING_CONFIG: WithTimingConfig = { easing: Easing.bezier(0.33, 1, 0.68, 1) }; -export type SegmentedControlItemProps = SegmentProps; +export {SegmentedControlItemProps}; export type SegmentedControlProps = { /** * Array on segments. @@ -71,6 +71,10 @@ export type SegmentedControlProps = { * Trailing throttle time of changing index in ms. */ throttleTime?: number; + /** + * Additional style for the segment + */ + segmentsStyle?: StyleProp; /** * Additional spacing styles for the container */ @@ -98,6 +102,7 @@ const SegmentedControl = (props: SegmentedControlProps) => { outlineColor = activeColor, outlineWidth = BORDER_WIDTH, throttleTime = 0, + segmentsStyle: segmentsStyleProp, testID } = props; const animatedSelectedIndex = useSharedValue(initialIndex); @@ -164,6 +169,7 @@ const SegmentedControl = (props: SegmentedControlProps) => { selectedIndex={animatedSelectedIndex} activeColor={activeColor} inactiveColor={inactiveColor} + style={segmentsStyleProp} {...segments?.[index]} testID={testID} /> diff --git a/src/components/segmentedControl/segment.tsx b/src/components/segmentedControl/segment.tsx index 1354e0d1fe..3ad1d7f57f 100644 --- a/src/components/segmentedControl/segment.tsx +++ b/src/components/segmentedControl/segment.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useMemo} from 'react'; -import {LayoutChangeEvent, ImageSourcePropType, ImageStyle, StyleProp} from 'react-native'; +import {LayoutChangeEvent, ImageSourcePropType, ImageStyle, StyleProp, ViewStyle} from 'react-native'; import Reanimated, {useAnimatedStyle} from 'react-native-reanimated'; import {Spacings, Typography} from '../../style'; import {asBaseComponent} from '../../commons/new'; @@ -49,6 +49,10 @@ export type SegmentProps = SegmentedControlItemProps & { * onLayout function. */ onLayout?: (index: number, event: LayoutChangeEvent) => void; + /** + * Additional style for the segment. + */ + style?: StyleProp; testID?: string; }; @@ -67,6 +71,7 @@ const Segment = React.memo((props: SegmentProps) => { inactiveColor, index, iconOnRight, + style, testID } = props; @@ -80,7 +85,9 @@ const Segment = React.memo((props: SegmentProps) => { return {tintColor}; }); - const segmentStyle = useMemo(() => ({paddingHorizontal: Spacings.s3, paddingVertical: Spacings.s2}), []); + const segmentStyle = useMemo(() => { + return [{paddingHorizontal: Spacings.s3, paddingVertical: Spacings.s2}, style]; + }, [style]); const renderIcon = useCallback(() => { return iconSource && ; diff --git a/src/components/segmentedControl/segmentedControl.api.json b/src/components/segmentedControl/segmentedControl.api.json index 5a27e54cfa..faecdeb6f4 100644 --- a/src/components/segmentedControl/segmentedControl.api.json +++ b/src/components/segmentedControl/segmentedControl.api.json @@ -16,6 +16,7 @@ {"name": "outlineWidth", "type": "number", "description": "The width of the active segment outline"}, {"name": "iconOnRight", "type": "boolean", "description": "Should the icon be on right of the label"}, {"name": "throttleTime", "type": "number", "description": "Trailing throttle time of changing index in ms."}, + {"name": "segmentsStyle", "type": "ViewStyle", "description": "Additional style for the segments"}, {"name": "containerStyle", "type": "ViewStyle", "description": "Additional spacing styles for the container"}, {"name": "style", "type": "ViewStyle", "description": "Custom style to inner container"}, {"name": "testID", "type": "string", "description": "Component test id"} diff --git a/src/components/sharedTransition/SharedArea.js b/src/components/sharedTransition/SharedArea.js index dea77402ac..192751a829 100644 --- a/src/components/sharedTransition/SharedArea.js +++ b/src/components/sharedTransition/SharedArea.js @@ -11,6 +11,7 @@ const {interpolate: _interpolate, interpolateNode} = Animated; const Easing = EasingNode || _Easing; const interpolate = interpolateNode || _interpolate; +// TODO: 2.17 breaks Android (at list the screen, the image is not shown) - move to incubator? class SharedArea extends Component { displayName = 'IGNORE'; static propTypes = { diff --git a/src/components/slider/ColorSliderGroup.tsx b/src/components/slider/ColorSliderGroup.tsx index 7c85a5f259..151d6115e1 100644 --- a/src/components/slider/ColorSliderGroup.tsx +++ b/src/components/slider/ColorSliderGroup.tsx @@ -42,6 +42,10 @@ export type ColorSliderGroupProps = { * If true the component will have accessibility features enabled */ accessible?: boolean; + /** + * Whether to use the new Slider implementation using Reanimated + */ + migrate?: boolean; }; interface ColorSliderGroupState { @@ -79,7 +83,7 @@ class ColorSliderGroup extends PureComponent { - const {sliderContainerStyle, showLabels, labelsStyle, accessible, labels} = this.props; + const {sliderContainerStyle, showLabels, labelsStyle, accessible, labels, migrate} = this.props; return ( <> @@ -93,6 +97,7 @@ class ColorSliderGroup extends PureComponent ); diff --git a/src/components/slider/GradientSlider.tsx b/src/components/slider/GradientSlider.tsx index 1bcb741776..0c306156b5 100644 --- a/src/components/slider/GradientSlider.tsx +++ b/src/components/slider/GradientSlider.tsx @@ -5,6 +5,7 @@ import {StyleProp, ViewStyle} from 'react-native'; import {Colors} from '../../style'; import {asBaseComponent, forwardRef, ForwardRefInjectedProps} from '../../commons/new'; import Slider, {SliderProps} from './index'; +import {Slider as NewSlider} from '../../incubator'; import {SliderContextProps} from './context/SliderContext'; import asSliderGroupChild from './context/asSliderGroupChild'; import Gradient from '../gradient'; @@ -184,7 +185,7 @@ class GradientSlider extends Component { }; render() { - const {type, containerStyle, disabled, accessible, forwardedRef, ...others} = this.props; + const {type, containerStyle, disabled, accessible, forwardedRef, migrate, ...others} = this.props; const initialColor = this.state.initialColor; const color = this.getColor(); const thumbTintColor = Colors.getHexString(color); @@ -216,9 +217,12 @@ class GradientSlider extends Component { break; } + const SliderComponent = migrate ? NewSlider : Slider; + return ( - - {this.props.children} + + {this.props.children} + ); } diff --git a/src/components/slider/index.tsx b/src/components/slider/index.tsx index 0372670e54..3aa988bfd4 100644 --- a/src/components/slider/index.tsx +++ b/src/components/slider/index.tsx @@ -16,6 +16,7 @@ import { } from 'react-native'; import {Constants} from '../../commons/new'; import {Colors} from '../../style'; +import IncubatorSlider from '../../incubator/Slider'; import View from '../view'; import Thumb, {ThumbProps} from './Thumb'; import {extractAccessibilityProps} from '../../commons/modifiers'; @@ -120,6 +121,10 @@ export type SliderProps = Omit & { * The slider's test identifier */ testID?: string; + /** + * Whether to use the new Slider implementation using Reanimated + */ + migrate?: boolean; } & typeof defaultProps; interface State { @@ -739,7 +744,11 @@ export default class Slider extends PureComponent { } render() { - const {containerStyle, testID} = this.props; + const {containerStyle, testID, migrate} = this.props; + + if (migrate) { + return ; + } return ( { + const makeSUT = ({numOfColumns = DEFAULT_NUM_COLUMNS, itemSpacing = DEFAULT_ITEM_SPACINGS}) => { + return renderHook(() => usePresenter(numOfColumns, itemSpacing)); + }; + + describe('ltr', () => { + it('getTranslationByOrderChange', () => { + let sut = makeSUT({}); + expect(sut.result.current.getTranslationByOrderChange(0, 0)).toEqual({x: 0, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(5, 1)).toEqual({x: 16, y: 16}); + expect(sut.result.current.getTranslationByOrderChange(2, 1)).toEqual({x: 16, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(1, 2)).toEqual({x: -16, y: 0}); + + sut = makeSUT({numOfColumns: 5}); + expect(sut.result.current.getTranslationByOrderChange(0, 0)).toEqual({x: 0, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(5, 1)).toEqual({x: -16, y: 16}); + expect(sut.result.current.getTranslationByOrderChange(2, 1)).toEqual({x: 16, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(1, 2)).toEqual({x: -16, y: 0}); + + sut = makeSUT({itemSpacing: 30}); + expect(sut.result.current.getTranslationByOrderChange(0, 0)).toEqual({x: 0, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(5, 1)).toEqual({x: 30, y: 30}); + expect(sut.result.current.getTranslationByOrderChange(2, 1)).toEqual({x: 30, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(1, 2)).toEqual({x: -30, y: 0}); + + sut = makeSUT({numOfColumns: 5, itemSpacing: 30}); + expect(sut.result.current.getTranslationByOrderChange(0, 0)).toEqual({x: 0, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(5, 1)).toEqual({x: -30, y: 30}); + expect(sut.result.current.getTranslationByOrderChange(2, 1)).toEqual({x: 30, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(1, 2)).toEqual({x: -30, y: 0}); + }); + + it('getOrderByPosition', () => { + let sut = makeSUT({}); + expect(sut.result.current.getOrderByPosition(0, 0)).toEqual(0); + expect(sut.result.current.getOrderByPosition(200, 200)).toEqual(4); + expect(sut.result.current.getOrderByPosition(0, 200)).toEqual(3); + expect(sut.result.current.getOrderByPosition(200, 0)).toEqual(1); + + sut = makeSUT({numOfColumns: 5}); + expect(sut.result.current.getOrderByPosition(0, 0)).toEqual(0); + expect(sut.result.current.getOrderByPosition(200, 200)).toEqual(6); + expect(sut.result.current.getOrderByPosition(0, 200)).toEqual(5); + expect(sut.result.current.getOrderByPosition(200, 0)).toEqual(1); + + sut = makeSUT({itemSpacing: 30}); + expect(sut.result.current.getOrderByPosition(0, 0)).toEqual(0); + expect(sut.result.current.getOrderByPosition(200, 200)).toEqual(4); + expect(sut.result.current.getOrderByPosition(0, 200)).toEqual(3); + expect(sut.result.current.getOrderByPosition(200, 0)).toEqual(1); + + sut = makeSUT({numOfColumns: 5, itemSpacing: 30}); + expect(sut.result.current.getOrderByPosition(0, 0)).toEqual(0); + expect(sut.result.current.getOrderByPosition(200, 200)).toEqual(6); + expect(sut.result.current.getOrderByPosition(0, 200)).toEqual(5); + expect(sut.result.current.getOrderByPosition(200, 0)).toEqual(1); + }); + }); + + describe('rtl', () => { + beforeAll(() => (Constants.isRTL = true)); + afterAll(() => (Constants.isRTL = false)); + + it('getTranslationByOrderChange', () => { + let sut = makeSUT({}); + expect(sut.result.current.getTranslationByOrderChange(0, 0)).toEqual({x: -0, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(5, 1)).toEqual({x: -16, y: 16}); + expect(sut.result.current.getTranslationByOrderChange(2, 1)).toEqual({x: -16, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(1, 2)).toEqual({x: 16, y: 0}); + + sut = makeSUT({numOfColumns: 5}); + expect(sut.result.current.getTranslationByOrderChange(0, 0)).toEqual({x: -0, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(5, 1)).toEqual({x: 16, y: 16}); + expect(sut.result.current.getTranslationByOrderChange(2, 1)).toEqual({x: -16, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(1, 2)).toEqual({x: 16, y: 0}); + + sut = makeSUT({itemSpacing: 30}); + expect(sut.result.current.getTranslationByOrderChange(0, 0)).toEqual({x: -0, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(5, 1)).toEqual({x: -30, y: 30}); + expect(sut.result.current.getTranslationByOrderChange(2, 1)).toEqual({x: -30, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(1, 2)).toEqual({x: 30, y: 0}); + + sut = makeSUT({numOfColumns: 5, itemSpacing: 30}); + expect(sut.result.current.getTranslationByOrderChange(0, 0)).toEqual({x: -0, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(5, 1)).toEqual({x: 30, y: 30}); + expect(sut.result.current.getTranslationByOrderChange(2, 1)).toEqual({x: -30, y: 0}); + expect(sut.result.current.getTranslationByOrderChange(1, 2)).toEqual({x: 30, y: 0}); + }); + + it('getOrderByPosition', () => { + let sut = makeSUT({}); + expect(sut.result.current.getOrderByPosition(0, 0)).toEqual(0); + expect(sut.result.current.getOrderByPosition(-200, 200)).toEqual(4); + expect(sut.result.current.getOrderByPosition(0, 200)).toEqual(3); + expect(sut.result.current.getOrderByPosition(-200, 0)).toEqual(1); + + sut = makeSUT({numOfColumns: 5}); + expect(sut.result.current.getOrderByPosition(0, 0)).toEqual(0); + expect(sut.result.current.getOrderByPosition(-200, 200)).toEqual(6); + expect(sut.result.current.getOrderByPosition(0, 200)).toEqual(5); + expect(sut.result.current.getOrderByPosition(-200, 0)).toEqual(1); + + sut = makeSUT({itemSpacing: 30}); + expect(sut.result.current.getOrderByPosition(0, 0)).toEqual(0); + expect(sut.result.current.getOrderByPosition(-200, 200)).toEqual(4); + expect(sut.result.current.getOrderByPosition(0, 200)).toEqual(3); + expect(sut.result.current.getOrderByPosition(-200, 0)).toEqual(1); + + sut = makeSUT({numOfColumns: 5, itemSpacing: 30}); + expect(sut.result.current.getOrderByPosition(0, 0)).toEqual(0); + expect(sut.result.current.getOrderByPosition(-200, 200)).toEqual(6); + expect(sut.result.current.getOrderByPosition(0, 200)).toEqual(5); + expect(sut.result.current.getOrderByPosition(-200, 0)).toEqual(1); + }); + }); +}); diff --git a/src/components/sortableList/SortableList.api.json b/src/components/sortableList/SortableList.api.json index b3102dac4b..096f56a285 100644 --- a/src/components/sortableList/SortableList.api.json +++ b/src/components/sortableList/SortableList.api.json @@ -30,6 +30,11 @@ "type": "number", "default": "1", "description": "Scale the item once dragged." + }, + { + "name": "itemProps", + "type": "{margins?: {marginTop?: number; marginBottom?: number; marginLeft?: number; marginRight?: number}}", + "description": "Extra props for the item." } ], "snippet": [ diff --git a/src/components/sortableList/SortableListContext.ts b/src/components/sortableList/SortableListContext.ts index 61e87cd449..2fb5a00cd5 100644 --- a/src/components/sortableList/SortableListContext.ts +++ b/src/components/sortableList/SortableListContext.ts @@ -12,6 +12,7 @@ export interface SortableListContextType { onItemLayout: ViewProps['onLayout']; enableHaptic?: boolean; scale?: number; + itemProps?: {margins?: {marginTop?: number; marginBottom?: number; marginLeft?: number; marginRight?: number}}; } // @ts-ignore diff --git a/src/components/sortableList/SortableListItem.driver.ts b/src/components/sortableList/SortableListItem.driver.ts new file mode 100644 index 0000000000..88e7d9dfee --- /dev/null +++ b/src/components/sortableList/SortableListItem.driver.ts @@ -0,0 +1,35 @@ +import _ from 'lodash'; +import {ComponentDriver} from '../../testkit/Component.driver'; + +/** + * Please run clear after each test + */ +export class SortableListItemDriver extends ComponentDriver { + dragUp = async (indices: number) => { + this.validateIndices(indices); + const data = _.times(indices, index => { + return { + translationY: -52 * (index + 1) + }; + }); + + await this.uniDriver.selectorByTestId(this.testID).then(driver => driver.drag(data)); + }; + + dragDown = async (indices: number) => { + this.validateIndices(indices); + const data = _.times(indices, index => { + return { + translationY: 52 * (index + 1) + }; + }); + + await this.uniDriver.selectorByTestId(this.testID).then(driver => driver.drag(data)); + }; + + private validateIndices = (indices: number) => { + if (indices <= 0 || !Number.isInteger(indices)) { + throw Error('indices must be a positive integer'); + } + }; +} diff --git a/src/components/sortableList/SortableListItem.tsx b/src/components/sortableList/SortableListItem.tsx index da508ea765..1574f3b471 100644 --- a/src/components/sortableList/SortableListItem.tsx +++ b/src/components/sortableList/SortableListItem.tsx @@ -39,6 +39,7 @@ const SortableListItem = (props: Props) => { const { data, itemHeight, + itemProps, onItemLayout, itemsOrder, lockedIds, @@ -167,6 +168,7 @@ const SortableListItem = (props: Props) => { zIndex, transform: [{translateY: translateY.value}, {scale}], opacity, + ...itemProps?.margins, ...shadow }; }); diff --git a/src/components/sortableList/__tests__/index.spec.tsx b/src/components/sortableList/__tests__/index.spec.tsx new file mode 100644 index 0000000000..3c9a86607c --- /dev/null +++ b/src/components/sortableList/__tests__/index.spec.tsx @@ -0,0 +1,83 @@ +import _ from 'lodash'; +import React, {useCallback} from 'react'; +import Text from '../../text'; +import View from '../../view'; +import SortableList from '../index'; +import {ComponentDriver, SortableListItemDriver} from '../../../testkit'; + +const defaultProps = { + testID: 'sortableList' +}; + +const ITEMS = _.times(5, index => { + return { + text: `${index}`, + id: `${index}` + }; +}); + +const TestCase = props => { + const renderItem = useCallback(({item}) => { + return ( + + {item.text} + + ); + }, []); + + return ( + + + + + + ); +}; + +describe('SortableList', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + ComponentDriver.clear(); + SortableListItemDriver.clear(); + }); + + it('SortableList onOrderChange is called - down', async () => { + const onOrderChange = jest.fn(); + const component = ; + const sortableListDriver = new ComponentDriver({component, testID: 'sortableList'}); + expect(await sortableListDriver.exists()).toBeTruthy(); + expect(onOrderChange).toHaveBeenCalledTimes(0); + const item1Driver = new SortableListItemDriver({component, testID: 'item1'}); + expect(await item1Driver.exists()).toBeTruthy(); + await item1Driver.dragDown(1); + expect(onOrderChange).toHaveBeenCalledTimes(1); + expect(onOrderChange).toHaveBeenCalledWith([ + {id: '0', text: '0'}, + {id: '2', text: '2'}, + {id: '1', text: '1'}, + {id: '3', text: '3'}, + {id: '4', text: '4'} + ]); + }); + + it('SortableList onOrderChange is called - up', async () => { + const onOrderChange = jest.fn(); + const component = ; + const sortableListDriver = new ComponentDriver({component, testID: 'sortableList'}); + expect(await sortableListDriver.exists()).toBeTruthy(); + expect(onOrderChange).toHaveBeenCalledTimes(0); + const item4Driver = new SortableListItemDriver({component, testID: 'item4'}); + expect(await item4Driver.exists()).toBeTruthy(); + await item4Driver.dragUp(3); + expect(onOrderChange).toHaveBeenCalledTimes(1); + expect(onOrderChange).toHaveBeenCalledWith([ + {id: '0', text: '0'}, + {id: '4', text: '4'}, + {id: '1', text: '1'}, + {id: '2', text: '2'}, + {id: '3', text: '3'} + ]); + }); +}); diff --git a/src/components/sortableList/index.tsx b/src/components/sortableList/index.tsx index 3f6c4397df..aa9cf79104 100644 --- a/src/components/sortableList/index.tsx +++ b/src/components/sortableList/index.tsx @@ -22,7 +22,7 @@ function generateLockedIds(data: SortableLi const SortableList = (props: SortableListProps) => { const themeProps = useThemeProps(props, 'SortableList'); - const {data, onOrderChange, enableHaptic, scale, ...others} = themeProps; + const {data, onOrderChange, enableHaptic, scale, itemProps, ...others} = themeProps; const itemsOrder = useSharedValue(generateItemsOrder(data)); const lockedIds = useSharedValue>(generateLockedIds(data)); @@ -49,7 +49,7 @@ const SortableList = (props: SortableListPr const newHeight = Math.round(event.nativeEvent.layout.height); // Check validity for tests if (newHeight) { - itemHeight.value = newHeight; + itemHeight.value = newHeight + (itemProps?.margins?.marginTop ?? 0) + (itemProps?.margins?.marginBottom ?? 0); } }, []); @@ -60,6 +60,7 @@ const SortableList = (props: SortableListPr lockedIds, onChange, itemHeight, + itemProps, onItemLayout, enableHaptic, scale diff --git a/src/components/sortableList/types.ts b/src/components/sortableList/types.ts index 170860ff88..fcef0ca2bf 100644 --- a/src/components/sortableList/types.ts +++ b/src/components/sortableList/types.ts @@ -13,7 +13,8 @@ export interface SortableListProps extends Omit, 'extraData' | 'data'>, Pick, 'scale'> { /** - * The data of the list, do not update the data. + * The data of the list. + IMPORTANT: Do not update 'data' in 'onOrderChange' (i.e. for each order change); only update it when you change the items (i.g. adding and removing an item). */ data: Data; /** @@ -25,4 +26,8 @@ export interface SortableListProps * (please note that react-native-haptic-feedback does not support the specific haptic type on Android starting on an unknown version, you can use 1.8.2 for it to work properly) */ enableHaptic?: boolean; + /** + * Extra props for the item + */ + itemProps?: {margins?: {marginTop?: number; marginBottom?: number; marginLeft?: number; marginRight?: number}}; } diff --git a/src/incubator/Calendar/Agenda.tsx b/src/incubator/Calendar/Agenda.tsx index 31423cc3b3..c411804480 100644 --- a/src/incubator/Calendar/Agenda.tsx +++ b/src/incubator/Calendar/Agenda.tsx @@ -1,18 +1,20 @@ import React, {useContext, useCallback, useRef} from 'react'; +import {ActivityIndicator} from 'react-native'; import {runOnJS, useAnimatedReaction, useSharedValue} from 'react-native-reanimated'; import {FlashListPackage} from 'optionalDeps'; import type {FlashList as FlashListType, ViewToken} from '@shopify/flash-list'; -import {BorderRadiuses} from 'style'; +import {BorderRadiuses, Colors} from 'style'; import View from '../../components/view'; import Text from '../../components/text'; import {isSameDay, isSameMonth} from './helpers/DateUtils'; -import {InternalEvent, Event, DateSectionHeader, UpdateSource} from './types'; +import {AgendaProps, InternalEvent, Event, DateSectionHeader, UpdateSource} from './types'; import CalendarContext from './CalendarContext'; -const {FlashList} = FlashListPackage; +const FlashList = FlashListPackage?.FlashList; // TODO: Fix initial scrolling -function Agenda() { +function Agenda(props: AgendaProps) { + const {onEndReached, showLoader} = props; const {data, selectedDate, setDate, updateSource} = useContext(CalendarContext); const flashList = useRef>(null); const closestSectionHeader = useSharedValue(null); @@ -48,14 +50,7 @@ function Agenda() { const renderHeader = useCallback((item: DateSectionHeader) => { return ( - + {item.header} ); @@ -96,7 +91,7 @@ function Agenda() { return selectedDate.value; }, (selected, previous) => { - if (updateSource?.value !== UpdateSource.AGENDA_SCROLL) { + if (updateSource.value !== UpdateSource.AGENDA_SCROLL) { if ( selected !== previous && (closestSectionHeader.value?.date === undefined || !isSameDay(selected, closestSectionHeader.value?.date)) @@ -142,19 +137,32 @@ function Agenda() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const _onEndReached = useCallback(() => { + onEndReached?.(selectedDate.value); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [onEndReached]); + return ( - + + + {showLoader && ( + + + + )} + ); } diff --git a/src/incubator/Calendar/CalendarItem.tsx b/src/incubator/Calendar/CalendarItem.tsx index a571dd7615..90162fe5ed 100644 --- a/src/incubator/Calendar/CalendarItem.tsx +++ b/src/incubator/Calendar/CalendarItem.tsx @@ -7,7 +7,8 @@ import CalendarContext from './CalendarContext'; import Month from './Month'; import Header from './Header'; -const CALENDAR_HEIGHT = 250; + +const CALENDAR_HEIGHT = 270; function CalendarItem(props: CalendarItemProps) { const {year, month} = props; @@ -21,6 +22,7 @@ function CalendarItem(props: CalendarItemProps) { height: CALENDAR_HEIGHT - (staticHeader ? headerHeight.value : 0) } ]; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [staticHeader]); if (month !== undefined) { @@ -34,7 +36,7 @@ function CalendarItem(props: CalendarItemProps) { return null; } -export default CalendarItem; +export default React.memo(CalendarItem); const styles = StyleSheet.create({ container: { diff --git a/src/incubator/Calendar/Day.tsx b/src/incubator/Calendar/Day.tsx index 0c7e8aa252..8bace8c5f9 100644 --- a/src/incubator/Calendar/Day.tsx +++ b/src/incubator/Calendar/Day.tsx @@ -1,7 +1,7 @@ import isNull from 'lodash/isNull'; import React, {useContext, useCallback} from 'react'; import {StyleSheet} from 'react-native'; -import Reanimated, {useSharedValue, useAnimatedStyle, useAnimatedReaction} from 'react-native-reanimated'; +import Reanimated, {useSharedValue, useAnimatedStyle, useAnimatedReaction, withTiming} from 'react-native-reanimated'; import {Colors} from 'style'; import View from '../../components/view'; import TouchableOpacity from '../../components/touchableOpacity'; @@ -11,50 +11,61 @@ import {DayProps, UpdateSource} from './types'; import CalendarContext from './CalendarContext'; +const DAY_SIZE = 32; +const SELECTION_SIZE = 24; +const NO_COLOR = Colors.transparent; +const TEXT_COLOR = Colors.$textPrimary; +const TODAY_BACKGROUND_COLOR = Colors.$backgroundPrimaryLight; +const INACTIVE_TODAY_BACKGROUND_COLOR = Colors.$backgroundNeutral; +const SELECTED_BACKGROUND_COLOR = Colors.$backgroundPrimaryHeavy; +const SELECTED_TEXT_COLOR = Colors.$textDefaultLight; +const INACTIVE_TEXT_COLOR = Colors.$textNeutralLight; + const AnimatedText = Reanimated.createAnimatedComponent(Text); const Day = (props: DayProps) => { - const {date, onPress} = props; - const {selectedDate, setDate} = useContext(CalendarContext); + const {date, onPress, inactive} = props; + const {selectedDate, setDate, showExtraDays} = useContext(CalendarContext); - const shouldMarkSelected = !isNull(date) ? isSameDay(selectedDate.value, date) : false; - const isSelected = useSharedValue(shouldMarkSelected); + const isSelected = useSharedValue(!isNull(date) ? isSameDay(selectedDate.value, date) : false); + const backgroundColor = !isToday(date) ? NO_COLOR : + inactive ? INACTIVE_TODAY_BACKGROUND_COLOR : TODAY_BACKGROUND_COLOR; + const isHidden = !showExtraDays && inactive; - const backgroundColor = isToday(date) ? Colors.$backgroundSuccessHeavy : Colors.transparent; - const textColor = isToday(date) ? Colors.$textDefaultLight : Colors.$backgroundPrimaryHeavy; - - const animatedStyles = useAnimatedStyle(() => { + useAnimatedReaction(() => { + return selectedDate.value; + }, (selected) => { + isSelected.value = !inactive && isSameDay(selected, date!); + }, []); + + const animatedSelectionStyles = useAnimatedStyle(() => { return { - backgroundColor: isSelected.value ? Colors.$backgroundPrimaryHeavy : backgroundColor, - color: isSelected.value ? Colors.$textDefaultLight : textColor + backgroundColor: withTiming(isSelected.value ? SELECTED_BACKGROUND_COLOR : backgroundColor, {duration: 100}) }; }); const animatedTextStyles = useAnimatedStyle(() => { return { - color: isSelected.value ? Colors.$textDefaultLight : textColor + color: withTiming(isSelected.value ? + SELECTED_TEXT_COLOR : inactive ? + showExtraDays ? INACTIVE_TEXT_COLOR : NO_COLOR : TEXT_COLOR, {duration: 100}) }; }); - useAnimatedReaction(() => { - return selectedDate.value; - }, (selected) => { - isSelected.value = isSameDay(selected, date!); - }, []); - const _onPress = useCallback(() => { - if (date !== null) { + if (date !== null && !isHidden) { isSelected.value = true; setDate(date, UpdateSource.DAY_SELECT); onPress?.(date); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [date, setDate, onPress]); const renderDay = () => { const day = !isNull(date) ? getDayOfDate(date) : ''; return ( - + {day} ); @@ -71,13 +82,13 @@ export default Day; const styles = StyleSheet.create({ dayContainer: { - width: 32, - height: 32 + width: DAY_SIZE, + height: DAY_SIZE }, selection: { position: 'absolute', - width: 24, - height: 24, - borderRadius: 12 + width: SELECTION_SIZE, + height: SELECTION_SIZE, + borderRadius: SELECTION_SIZE / 2 } }); diff --git a/src/incubator/Calendar/Header.tsx b/src/incubator/Calendar/Header.tsx index 748fc852fb..310ef456e6 100644 --- a/src/incubator/Calendar/Header.tsx +++ b/src/incubator/Calendar/Header.tsx @@ -4,28 +4,36 @@ import Reanimated, {useAnimatedProps} from 'react-native-reanimated'; import {Colors, Typography} from 'style'; import View from '../../components/view'; import Button from '../../components/button'; -import {getDateObject, addMonths, getMonthForIndex} from './helpers/DateUtils'; +import {getDateObject, getMonthForIndex, addMonths, getTimestamp} from './helpers/DateUtils'; import {HeaderProps, DayNamesFormat, UpdateSource} from './types'; import CalendarContext from './CalendarContext'; import WeekDaysNames from './WeekDaysNames'; -const AnimatedTextInput = Reanimated.createAnimatedComponent(TextInput); -const WEEK_NUMBER_WIDTH = 30; +const WEEK_NUMBER_WIDTH = 32; const ARROW_NEXT = require('./assets/arrowNext.png'); const ARROW_BACK = require('./assets/arrowBack.png'); +const AnimatedTextInput = Reanimated.createAnimatedComponent(TextInput); + const Header = (props: HeaderProps) => { const {month, year} = props; const {selectedDate, setDate, showWeeksNumbers, staticHeader, setHeaderHeight} = useContext(CalendarContext); + const getNewDate = useCallback((count: number) => { + const newDate = addMonths(selectedDate.value, count); + const dateObject = getDateObject(newDate); + return getTimestamp({year: dateObject.year, month: dateObject.month, day: 1}); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const onLeftArrowPress = useCallback(() => { - setDate(addMonths(selectedDate.value, -1), UpdateSource.MONTH_ARROW); - }, [selectedDate.value, setDate]); + setDate(getNewDate(-1), UpdateSource.MONTH_ARROW); + }, [setDate, getNewDate]); const onRightArrowPress = useCallback(() => { - setDate(addMonths(selectedDate.value, 1), UpdateSource.MONTH_ARROW); - }, [selectedDate.value, setDate]); + setDate(getNewDate(1), UpdateSource.MONTH_ARROW); + }, [setDate, getNewDate]); const animatedProps = useAnimatedProps(() => { const dateObject = getDateObject(selectedDate.value); @@ -38,7 +46,7 @@ const Header = (props: HeaderProps) => { const onLayout = useCallback((event: LayoutChangeEvent) => { setHeaderHeight?.(event.nativeEvent.layout.height); - }, []); + }, [setHeaderHeight]); const renderTitle = () => { // @ts-expect-error diff --git a/src/incubator/Calendar/Month.tsx b/src/incubator/Calendar/Month.tsx index 001a26450e..5e439a366c 100644 --- a/src/incubator/Calendar/Month.tsx +++ b/src/incubator/Calendar/Month.tsx @@ -5,18 +5,19 @@ import {MonthProps} from './types'; import Week from './Week'; import CalendarContext from './CalendarContext'; + function Month(props: MonthProps) { const {year, month} = props; const {firstDayOfWeek} = useContext(CalendarContext); const weekNumbers = useMemo(() => { return getWeekNumbersOfMonth(year, month, firstDayOfWeek); - }, [year, month]); + }, [year, month, firstDayOfWeek]); return ( {weekNumbers.map(weekNumber => { - return ; + return ; })} ); diff --git a/src/incubator/Calendar/Week.tsx b/src/incubator/Calendar/Week.tsx index 3b1de13e2a..26a9063818 100644 --- a/src/incubator/Calendar/Week.tsx +++ b/src/incubator/Calendar/Week.tsx @@ -1,18 +1,18 @@ import _ from 'lodash'; -import React, {useContext, useMemo} from 'react'; +import React, {useContext, useMemo, useCallback} from 'react'; import {StyleSheet} from 'react-native'; import View from '../../components/view'; import Text from '../../components/text'; -import {getDaysOfWeekNumber} from './helpers/DateUtils'; +import {getDaysOfWeekNumber, getDateObject} from './helpers/DateUtils'; import {WeekProps} from './types'; import CalendarContext from './CalendarContext'; import Day from './Day'; -const WEEK_NUMBER_WIDTH = 18; +const WEEK_NUMBER_WIDTH = 20; const Week = (props: WeekProps) => { - const {weekNumber, year} = props; + const {weekNumber, year, month} = props; const {firstDayOfWeek, showWeeksNumbers} = useContext(CalendarContext); @@ -22,15 +22,20 @@ const Week = (props: WeekProps) => { const renderWeekNumbers = () => { if (showWeeksNumbers) { - return {weekNumber}; + return {weekNumber}; } }; + const isExtraDay = useCallback((day: number) => { + const dayMonth = getDateObject(day).month; + return dayMonth !== month; + }, [month]); + return ( {renderWeekNumbers()} {_.map(days, day => ( - + ))} ); diff --git a/src/incubator/Calendar/__tests__/DateUtils.spec.ts b/src/incubator/Calendar/__tests__/DateUtils.spec.ts index 2e3b556ead..aaf9f0144e 100644 --- a/src/incubator/Calendar/__tests__/DateUtils.spec.ts +++ b/src/incubator/Calendar/__tests__/DateUtils.spec.ts @@ -273,6 +273,24 @@ describe('Calendar/DateUtils', () => { }); }); + describe('addYears', () => { + it('should return the date timestamp for the next (1) year in the same month', () => { + const date = DateUtils.addYears(new Date(2022, 11, 26).getTime(), 1); + const dayObject = DateUtils.getDateObject(date); + expect(dayObject.day).toBe(26); + expect(dayObject.month).toBe(11); + expect(dayObject.year).toBe(2023); + }); + + it('should return the date timestamp for the previous (-1) year in the same month', () => { + const date = DateUtils.addYears(new Date(2022, 11, 26).getTime(), -1); + const dayObject = DateUtils.getDateObject(date); + expect(dayObject.day).toBe(26); + expect(dayObject.month).toBe(11); + expect(dayObject.year).toBe(2021); + }); + }); + describe('getWeekDayNames', () => { it('should return the week days names for first day = Sunday', () => { const weekDaysNames = DateUtils.getWeekDayNames(); diff --git a/src/incubator/Calendar/helpers/CalendarProcessor.ts b/src/incubator/Calendar/helpers/CalendarProcessor.ts index 2111a542ad..d6e8820cb3 100644 --- a/src/incubator/Calendar/helpers/CalendarProcessor.ts +++ b/src/incubator/Calendar/helpers/CalendarProcessor.ts @@ -1,9 +1,9 @@ -export function generateMonthItems(range: number) { - const today = new Date(); - const currentYear = today.getFullYear(); +export function generateMonthItems(date: number, pastRange: number, futureRange: number) { + const currentDate = new Date(date); + const currentYear = currentDate.getFullYear(); const monthItems = []; - for (let year = currentYear - range; year <= currentYear + range; year++) { + for (let year = currentYear - pastRange; year <= currentYear + futureRange; year++) { for (let month = 0; month < 12; month++) { monthItems.push({year, month}); } diff --git a/src/incubator/Calendar/helpers/DateUtils.ts b/src/incubator/Calendar/helpers/DateUtils.ts index 54ace9d738..a3b3ce0821 100644 --- a/src/incubator/Calendar/helpers/DateUtils.ts +++ b/src/incubator/Calendar/helpers/DateUtils.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import getWeek from 'date-fns/getWeek'; -import {FirstDayOfWeek, DayNamesFormat, DateObjectWithOptionalDay} from '../types'; +import {FirstDayOfWeek, DayNamesFormat, DateObjectWithOptionalDay, DateObject} from '../types'; export const HOUR_IN_MS = 60 * 60 * 1000; const DAY_IN_MS = 24 * HOUR_IN_MS; @@ -20,7 +20,7 @@ export function getWeekNumbersOfMonth(year: number, month: number, firstDayOfWee return weekNumbers; } -function getNumberOfWeeksInMonth(year: number, month: number, firstDayOfWeek: FirstDayOfWeek) { +export function getNumberOfWeeksInMonth(year: number, month: number, firstDayOfWeek: FirstDayOfWeek) { const numberOfDaysInMonth = new Date(year, month + 1, 0).getDate(); const dayOfTheWeek = new Date(year, month, 1).getDay(); // Modify day in the week based on the first day of the week @@ -78,6 +78,11 @@ export function getDateObject(date: number) { }; } +export function getTimestamp(date: DateObject) { + 'worklet'; + return new Date(date.year, date.month, date.day).getTime(); +} + export function addMonths(date: number, count: number) { 'worklet'; if (count === 0) { @@ -88,6 +93,16 @@ export function addMonths(date: number, count: number) { return new Date(date).setMonth(month + count); } +export function addYears(date: number, count: number) { + 'worklet'; + if (count === 0) { + return date; + } + + const year = getDateObject(date).year; + return new Date(date).setFullYear(year + count); +} + export function getMonthForIndex(index: number) { 'worklet'; const months = [ diff --git a/src/incubator/Calendar/index.tsx b/src/incubator/Calendar/index.tsx index 5d976933a9..c3b6ba7d95 100644 --- a/src/incubator/Calendar/index.tsx +++ b/src/incubator/Calendar/index.tsx @@ -1,33 +1,52 @@ -import findIndex from 'lodash/findIndex'; -import React, {PropsWithChildren, useCallback, useMemo, useRef, useEffect} from 'react'; +import React, {PropsWithChildren, useCallback, useMemo, useRef, useState} from 'react'; import {useSharedValue, useAnimatedReaction, runOnJS} from 'react-native-reanimated'; import {FlashListPackage} from 'optionalDeps'; import {Constants} from '../../commons/new'; import {generateMonthItems} from './helpers/CalendarProcessor'; import {addHeaders} from './helpers/DataProcessor'; -import {isSameMonth} from './helpers/DateUtils'; -import {CalendarContextProps, CalendarProps, FirstDayOfWeek, UpdateSource} from './types'; +import {isSameMonth, getTimestamp, addYears} from './helpers/DateUtils'; +import {CalendarContextProps, CalendarProps, FirstDayOfWeek, UpdateSource, DateObjectWithOptionalDay} from './types'; import CalendarContext from './CalendarContext'; import CalendarItem from './CalendarItem'; import Agenda from './Agenda'; import TodayButton from './TodayButton'; import Header from './Header'; +import {useDidUpdate} from 'hooks'; -const {FlashList} = FlashListPackage; +const FlashList = FlashListPackage?.FlashList; -// TODO: Move this logic elsewhere to pre-generate on install? -const MONTH_ITEMS = generateMonthItems(2); -const getIndex = (date: number) => { - return findIndex(MONTH_ITEMS, item => isSameMonth(item, date)); -}; +const VIEWABILITY_CONFIG = {itemVisiblePercentThreshold: 95, minimumViewTime: 200}; +const YEARS_RANGE = 1; +const PAGE_RELOAD_THRESHOLD = 3; +const NOW = Date.now(); // so the 'initialDate' effect won't get called since the now different on every rerender function Calendar(props: PropsWithChildren) { - const {data, children, initialDate = Date.now(), firstDayOfWeek = FirstDayOfWeek.MONDAY, staticHeader = false} = props; + const { + data, + children, + initialDate = NOW, + onChangeDate, + firstDayOfWeek = FirstDayOfWeek.MONDAY, + staticHeader = false, + showExtraDays = true + } = props; + + const initialItems = generateMonthItems(initialDate, YEARS_RANGE, YEARS_RANGE); + const [items, setItems] = useState(initialItems); + + const getItemIndex = useCallback((date: number) => { + 'worklet'; + for (let i = 0; i < items.length; i++) { + if (isSameMonth(items[i], date)) { + return i; + } + } + return -1; + }, [items]); const flashListRef = useRef(); - const calendarWidth = Constants.screenWidth; const current = useSharedValue(initialDate); - const initialMonthIndex = useRef(getIndex(current.value)); + const initialMonthIndex = useRef(getItemIndex(current.value)); const lastUpdateSource = useSharedValue(UpdateSource.INIT); const processedData = useMemo(() => addHeaders(data), [data]); const scrolledByUser = useSharedValue(false); @@ -36,15 +55,31 @@ function Calendar(props: PropsWithChildren) { const setDate = useCallback((date: number, updateSource: UpdateSource) => { current.value = date; lastUpdateSource.value = updateSource; + if (updateSource !== UpdateSource.PROP_UPDATE) { + onChangeDate?.(date); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { + const scrollToIndex = useCallback((index: number) => { + scrolledByUser.value = false; + // @ts-expect-error + flashListRef.current?.scrollToIndex({index, animated: true}); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getItemIndex]); + + useDidUpdate(() => { setDate(initialDate, UpdateSource.PROP_UPDATE); - }, [initialDate, setDate]); + }, [initialDate]); + + useDidUpdate(() => { + const index = getItemIndex(current.value); + scrollToIndex(index); + }, [items, getItemIndex]); const setHeaderHeight = useCallback((height: number) => { headerHeight.value = height; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const contextValue = useMemo(() => { @@ -54,39 +89,72 @@ function Calendar(props: PropsWithChildren) { selectedDate: current, setDate, showWeeksNumbers: true, + showExtraDays, updateSource: lastUpdateSource, staticHeader, setHeaderHeight, headerHeight }; - }, []); + }, [processedData, staticHeader, showExtraDays, firstDayOfWeek]); - const scrollToIndex = useCallback((date: number) => { - scrolledByUser.value = false; - // @ts-expect-error - flashListRef.current?.scrollToIndex({index: getIndex(date), animated: false}); + /** Pages reload */ + + const mergeArrays = (prepend: boolean, array: DateObjectWithOptionalDay[], newArray: DateObjectWithOptionalDay[]) => { + const arr: DateObjectWithOptionalDay[] = array.slice(); + if (prepend) { + arr.unshift(...newArray); + } else { + arr.push(...newArray); + } + return arr; + }; + + const addPages = useCallback((index: number) => { + const prepend = index < PAGE_RELOAD_THRESHOLD; + const append = index > items.length - PAGE_RELOAD_THRESHOLD; + const pastRange = prepend ? YEARS_RANGE : 0; + const futureRange = append ? YEARS_RANGE : 0; + const newDate = addYears(current.value, prepend ? -1 : 1); + const newItems = generateMonthItems(newDate, pastRange, futureRange); + const newArray = mergeArrays(prepend, items, newItems); + setItems(newArray); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [items]); + + const shouldAddPages = useCallback((index: number) => { + 'worklet'; + return index !== -1 && + (index < PAGE_RELOAD_THRESHOLD || index > items.length - PAGE_RELOAD_THRESHOLD); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [items]); useAnimatedReaction(() => { return current.value; }, (selected, previous) => { - if (lastUpdateSource.value !== UpdateSource.MONTH_SCROLL) { + const index = getItemIndex(selected); + + if (shouldAddPages(index)) { + console.log('Add new pages'); + runOnJS(addPages)(index); + } else if (lastUpdateSource.value !== UpdateSource.MONTH_SCROLL) { if (previous && !isSameMonth(selected, previous)) { - runOnJS(scrollToIndex)(selected); + runOnJS(scrollToIndex)(index); } } - }, []); + }, [getItemIndex]); + + /** Events */ const onViewableItemsChanged = useCallback(({viewableItems}: any) => { - if (scrolledByUser.value) { - const item = viewableItems?.[0]?.item; - if (item && !isSameMonth(item, current.value)) { - const newDate = new Date(item.year, item.month, 1); - setDate(newDate.getTime(), UpdateSource.MONTH_SCROLL); + const item = viewableItems?.[0]?.item; + if (item && scrolledByUser.value) { + if (!isSameMonth(item, current.value)) { + const newDate = getTimestamp({year: item.year, month: item.month, day: 1}); + setDate(newDate, UpdateSource.MONTH_SCROLL); } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onMomentumScrollBegin = useCallback(() => { @@ -108,8 +176,8 @@ function Calendar(props: PropsWithChildren) { {staticHeader &&
} ) { showsHorizontalScrollIndicator={false} // TODO: Consider moving this shared logic with Agenda to a hook onViewableItemsChanged={onViewableItemsChanged} + viewabilityConfig={VIEWABILITY_CONFIG} onMomentumScrollBegin={onMomentumScrollBegin} onScrollBeginDrag={onScrollBeginDrag} /> diff --git a/src/incubator/Calendar/types.ts b/src/incubator/Calendar/types.ts index c1d8ec75f1..9afac5d788 100644 --- a/src/incubator/Calendar/types.ts +++ b/src/incubator/Calendar/types.ts @@ -55,7 +55,8 @@ export interface CalendarContextProps { setDate: (date: number, updateSource: UpdateSource) => void; data: InternalData; showWeeksNumbers: boolean; - updateSource?: SharedValue; + showExtraDays: boolean; + updateSource: SharedValue; staticHeader?: boolean; setHeaderHeight?: (height: number) => void; headerHeight: SharedValue; @@ -64,11 +65,13 @@ export interface CalendarContextProps { export interface DayProps { date: number | null; onPress?: (date: number) => void; + inactive?: boolean; // inactive look but still pressable } export interface WeekProps { weekNumber: number; year: number; + month: number; } export interface MonthProps { @@ -107,12 +110,16 @@ export enum DayNamesFormat { export interface CalendarProps { data: Data; initialDate?: number; + onChangeDate: (date: number) => void; firstDayOfWeek?: /* `${FirstDayOfWeek}` & */ FirstDayOfWeek; // NOTE: template literals usage depends on ts min version ^4.3.2 staticHeader?: boolean; + showExtraDays?: boolean; } -// export interface AgendaProps { -// // Type: list(events)/timeline -// // layout: -// // scrollTo(date) -// } +export interface AgendaProps { + showLoader?: boolean; + onEndReached?: (date: number) => void; + // Type: list(events)/timeline + // layout: + // scrollTo(date) +} diff --git a/src/incubator/Dialog/__tests__/index.spec.tsx b/src/incubator/Dialog/__tests__/index.spec.tsx index ab9904a432..c8191d9fd4 100644 --- a/src/incubator/Dialog/__tests__/index.spec.tsx +++ b/src/incubator/Dialog/__tests__/index.spec.tsx @@ -41,12 +41,10 @@ const TestCase = props => { ); }; -const _TestCase = props => ; - describe('Incubator.Dialog', () => { it('Incubator.Dialog should exist only if visible', async () => { const onDismiss = jest.fn(); - const component = _TestCase({onDismiss}); + const component = ; const dialogDriver = new ComponentDriver({component, testID: 'dialog'}); expect(await dialogDriver.exists()).toBeFalsy(); const openButtonDriver = new ButtonDriver({component, testID: 'openButton'}); diff --git a/src/incubator/Slider/index.tsx b/src/incubator/Slider/index.tsx index b640b5158c..bbd51c2ed4 100644 --- a/src/incubator/Slider/index.tsx +++ b/src/incubator/Slider/index.tsx @@ -95,11 +95,17 @@ const Slider = React.memo((props: Props) => { const end = useSharedValue(0); const defaultThumbOffset = useSharedValue(0); const rangeThumbOffset = useSharedValue(0); - - const defaultThumbStyle: StyleProp = useMemo(() => [ - styles.thumb, {backgroundColor: disabled ? Colors.$backgroundDisabled : thumbTintColor} + + const thumbBackground: StyleProp = useMemo(() => [ + {backgroundColor: disabled ? Colors.$backgroundDisabled : thumbTintColor} ], [disabled, thumbTintColor]); - const _thumbStyle = useSharedValue(StyleUtils.unpackStyle(thumbStyle || defaultThumbStyle, {flatten: true})); + const defaultThumbStyle: StyleProp = useMemo(() => [ + styles.thumb, thumbBackground + ], [thumbBackground]); + const customThumbStyle: StyleProp = useMemo(() => [ + thumbStyle, thumbBackground + ], [thumbStyle, thumbBackground]); + const _thumbStyle = useSharedValue(StyleUtils.unpackStyle(customThumbStyle || defaultThumbStyle, {flatten: true})); const _activeThumbStyle = useSharedValue(StyleUtils.unpackStyle(activeThumbStyle, {flatten: true})); useEffect(() => { @@ -133,7 +139,7 @@ const Slider = React.memo((props: Props) => { const onValueChangeThrottled = useCallback(_.throttle(value => { onValueChange?.(value); - }, 100), [onValueChange]); + }, 200), [onValueChange]); const onRangeChangeThrottled = useCallback(_.throttle((min, max) => { onRangeChange?.({min, max}); @@ -142,17 +148,19 @@ const Slider = React.memo((props: Props) => { useAnimatedReaction(() => { return Math.round(defaultThumbOffset.value); }, - (offset, _prevOffset) => { - const value = getValueForOffset(offset, trackSize.value.width, minimumValue, maximumValue, stepXValue.value); - if (useRange) { - const maxValue = getValueForOffset(rangeThumbOffset.value, - trackSize.value.width, - minimumValue, - maximumValue, - stepXValue.value); - runOnJS(onRangeChangeThrottled)(value, maxValue); - } else { - runOnJS(onValueChangeThrottled)(value); + (offset, prevOffset) => { + if (offset !== prevOffset) { + const value = getValueForOffset(offset, trackSize.value.width, minimumValue, maximumValue, stepXValue.value); + if (useRange) { + const maxValue = getValueForOffset(rangeThumbOffset.value, + trackSize.value.width, + minimumValue, + maximumValue, + stepXValue.value); + runOnJS(onRangeChangeThrottled)(value, maxValue); + } else { + runOnJS(onValueChangeThrottled)(value); + } } }); diff --git a/src/incubator/TextField/CharCounter.tsx b/src/incubator/TextField/CharCounter.tsx index c226940f07..20d70fad3e 100644 --- a/src/incubator/TextField/CharCounter.tsx +++ b/src/incubator/TextField/CharCounter.tsx @@ -7,13 +7,14 @@ import {CharCounterProps} from './types'; const CharCounter = ({maxLength, charCounterStyle, testID}: CharCounterProps) => { const {value} = useContext(FieldContext); + const length = value?.length ?? 0; if (_.isUndefined(maxLength)) { return null; } return ( - {`${_.size(value)}/${maxLength}`} + {`${length}/${maxLength}`} ); }; diff --git a/src/incubator/TextField/FloatingPlaceholder.tsx b/src/incubator/TextField/FloatingPlaceholder.tsx index a72deeb64a..2c5d8dabee 100644 --- a/src/incubator/TextField/FloatingPlaceholder.tsx +++ b/src/incubator/TextField/FloatingPlaceholder.tsx @@ -39,21 +39,27 @@ const FloatingPlaceholder = (props: FloatingPlaceholderProps) => { }, [shouldFloat]); const animatedStyle = useMemo(() => { + const {left, top} = placeholderOffset; + if (left !== 0 && top !== 0) { + return { + transform: [ + { + scale: interpolateValue(animation, [1, FLOATING_PLACEHOLDER_SCALE]) + }, + { + translateX: interpolateValue(animation, [ + 0, + -placeholderOffset.left - extraOffset / FLOATING_PLACEHOLDER_SCALE + ]) + }, + { + translateY: interpolateValue(animation, [0, -placeholderOffset.top]) + } + ] + }; + } return { - transform: [ - { - scale: interpolateValue(animation, [1, FLOATING_PLACEHOLDER_SCALE]) - }, - { - translateX: interpolateValue(animation, [ - 0, - -placeholderOffset.left - extraOffset / FLOATING_PLACEHOLDER_SCALE - ]) - }, - { - translateY: interpolateValue(animation, [0, -placeholderOffset.top]) - } - ] + opacity: 0 }; }, [placeholderOffset, extraOffset]); diff --git a/src/incubator/index.ts b/src/incubator/index.ts index 56a47a4b62..f585a83f3c 100644 --- a/src/incubator/index.ts +++ b/src/incubator/index.ts @@ -1,4 +1,4 @@ -// export {default as Calendar} from './Calendar'; +export {default as Calendar} from './Calendar'; export {default as ExpandableOverlay, ExpandableOverlayProps, ExpandableOverlayMethods} from './expandableOverlay'; // @ts-ignore export { diff --git a/src/optionalDependencies/index.web.ts b/src/optionalDependencies/index.web.ts index 3890b17746..9c87fef755 100644 --- a/src/optionalDependencies/index.web.ts +++ b/src/optionalDependencies/index.web.ts @@ -4,4 +4,5 @@ export {default as SvgPackage} from './SvgPackage'; export {createShimmerPlaceholder} from './ShimmerPackage'; export {default as LinearGradientPackage} from './LinearGradientPackage'; export {default as PostCssPackage} from './PostCssPackage'; +export {default as FlashListPackage} from './FlashListPackage'; diff --git a/src/services/LogService.ts b/src/services/LogService.ts index c4974edb69..ca64d87fc9 100644 --- a/src/services/LogService.ts +++ b/src/services/LogService.ts @@ -12,15 +12,15 @@ class LogService { this.biLogger?.log(event); }; - warn = (message: string) => { + warn = (message?: any, ...optionalParams: any[]) => { if (__DEV__) { - console.warn(message); + console.warn(message, ...optionalParams); } }; - error = (message: string) => { + error = (message?: any, ...optionalParams: any[]) => { if (__DEV__) { - console.error(message); + console.error(message, ...optionalParams); } }; diff --git a/src/style/typography.ts b/src/style/typography.ts index c4e6ccb7ff..2cb72afcbb 100644 --- a/src/style/typography.ts +++ b/src/style/typography.ts @@ -45,7 +45,7 @@ export class Typography { async measureTextSize(text: string, typography: MeasureTextTypography = TypographyPresets.text70!, containerWidth = Constants.screenWidth) { - const rnTextSize = require('react-native-text-size').default; + const rnTextSize = require('wix-react-native-text-size').default; if (text) { const size = await rnTextSize.measure({ text, // text to measure, can include symbols diff --git a/src/testkit/Component.driver.ts b/src/testkit/Component.driver.ts index 8125c113f5..ddd98fee61 100644 --- a/src/testkit/Component.driver.ts +++ b/src/testkit/Component.driver.ts @@ -1,4 +1,4 @@ -import {UniDriver, UniDriverClass} from './UniDriver'; +import {DragData, UniDriver, UniDriverClass} from './UniDriver'; import {TestingLibraryDriver} from './drivers/TestingLibraryDriver'; export type ComponentDriverArgs = { @@ -9,7 +9,7 @@ export type ComponentDriverArgs = { /** * Please run clear after each test - * */ + */ export class ComponentDriver { protected readonly testID: string; protected readonly uniDriver: UniDriver; @@ -45,6 +45,12 @@ export class ComponentDriver { .then((driver) => driver.press()); }; + drag = async (data: DragData | DragData[]) => { + return this.uniDriver + .selectorByTestId(this.testID) + .then((driver) => driver.drag(data)); + }; + focus = async () => { return this.uniDriver .selectorByTestId(this.testID) diff --git a/src/testkit/UniDriver.ts b/src/testkit/UniDriver.ts index 12525caf5e..f2d1e2bf1d 100644 --- a/src/testkit/UniDriver.ts +++ b/src/testkit/UniDriver.ts @@ -1,3 +1,14 @@ +export type DragData = { + absoluteX?: number; + absoluteY?: number; + translationX?: number; + translationY?: number; + velocityX?: number; + velocityY?: number; + x?: number; + y?: number; +}; + export interface UniDriver { selectorByTestId(testId: string): Promise; selectorByText(text: string): Promise; @@ -7,6 +18,7 @@ export interface UniDriver { instance(): Promise; getInstanceProps(): Promise; press(): void; + drag(data: DragData | DragData[]): void; focus(): void; blur(): void; typeText(text: string): Promise; diff --git a/src/testkit/drivers/TestingLibraryDriver.tsx b/src/testkit/drivers/TestingLibraryDriver.tsx index 865e47044f..a51d5cb6dc 100644 --- a/src/testkit/drivers/TestingLibraryDriver.tsx +++ b/src/testkit/drivers/TestingLibraryDriver.tsx @@ -1,4 +1,4 @@ -import {UniDriver} from '../UniDriver'; +import {DragData, UniDriver} from '../UniDriver'; import {fireEvent, render, RenderAPI} from '@testing-library/react-native'; import {ReactTestInstance} from 'react-test-renderer'; import {act} from '@testing-library/react-hooks'; @@ -85,6 +85,15 @@ export class TestingLibraryDriver implements UniDriver { fireEvent.press(this.reactTestInstances[0]); }; + drag = (data: DragData | DragData[]): void => { + if (!this.reactTestInstances) { + throw new NoSelectorException(); + } + this.validateExplicitInstance(); + this.validateSingleInstance(); + fireEvent.press(this.reactTestInstances[0], data); + }; + focus = (): void => { if (!this.reactTestInstances) { throw new NoSelectorException(); diff --git a/src/testkit/index.ts b/src/testkit/index.ts index 790a2c91d0..e1c4c6cc07 100644 --- a/src/testkit/index.ts +++ b/src/testkit/index.ts @@ -1,6 +1,6 @@ export {ComponentDriver} from './Component.driver'; export {ImageDriver} from '../components/image/Image.driver'; -export {TextDriver} from '../components/Text/Text.driver'; +export {TextDriver} from '../components/text/Text.driver'; export {SwitchDriver} from '../components/switch/switch.driver'; export {ButtonDriver} from '../components/button/Button.driver'; export {TextFieldDriver} from '../incubator/TextField/TextField.driver'; @@ -10,3 +10,4 @@ export {CheckboxDriver} from '../components/checkbox/Checkbox.driver'; export {HintDriver} from '../components/hint/Hint.driver'; export {RadioButtonDriver} from '../components/radioButton/RadioButton.driver'; export {RadioGroupDriver} from '../components/radioGroup/RadioGroup.driver'; +export {SortableListItemDriver} from '../components/sortableList/SortableListItem.driver';