From 4f90e00919f4897a9a1320f74c8203ba7ec9e9f2 Mon Sep 17 00:00:00 2001 From: Jeremy Hamilton Date: Tue, 2 Mar 2021 16:19:29 -0800 Subject: [PATCH] Typescript work (#2781) * convert RNE to typescript (#2766) * convert Avatar to typescript * convert the rest of the files into tsx, still rough * move tests back to being js * fix more type errors * more ts updates Co-authored-by: Jeremy Hamilton * typescript migration (#2779) * fix: typescript errors in SearchBar-default * refactor: all styles use StyleSheet.create * feat: add header Placement type * style: all react components use React.FunctionComponent * fix: add missing InputProps to SearchBarAndroidProps * fix(Tooltip): typescript errors * fix(Input): Animated.timing easing typo * fix: small fixes * fix(SearchBarAndroidProps): add missing InputProps * fix typing with static members of functions * ignore ts in slider; needs redone * finish up cleaning up the typescript conversion * trying out a different wrapped component * add dist folder to publish * a few more tsconfig changes * fix withTheme typing * Typescript work (#2792) * trying out a different wrapped component * add dist folder to publish * a few more tsconfig changes * fix withTheme typing Co-authored-by: Jeremy Hamilton * forwardRef in withTheme Co-authored-by: Jeremy Hamilton Co-authored-by: Mohammed Faragallah --- .eslintignore | 1 + .eslintrc | 3 +- .gitignore | 1 + package.json | 57 +- src/avatar/{Accessory.js => Accessory.tsx} | 60 +- src/avatar/{Avatar.js => Avatar.tsx} | 110 +- src/avatar/__tests__/Accessory.js | 2 - src/avatar/__tests__/Avatar.js | 27 +- .../__tests__/__snapshots__/Avatar.js.snap | 104 +- src/badge/{Badge.js => Badge.tsx} | 74 +- src/badge/__tests__/Badge.js | 23 +- .../__tests__/__snapshots__/Badge.js.snap | 6 +- src/badge/__tests__/withBadge.js | 5 - src/badge/{withBadge.js => withBadge.tsx} | 25 +- .../{BottomSheet.js => BottomSheet.tsx} | 40 +- src/bottomSheet/__tests__/BottomSheet.js | 1 - .../__snapshots__/BottomSheet.js.snap | 28 +- src/buttons/{Button.js => Button.tsx} | 195 +- src/buttons/ButtonGroup.js | 262 -- src/buttons/ButtonGroup.tsx | 239 ++ src/buttons/__tests__/Button.js | 15 - src/buttons/__tests__/ButtonGroup.js | 27 - .../__tests__/__snapshots__/Button.js.snap | 197 +- src/card/Card.js | 81 - src/card/Card.tsx | 81 + src/card/{CardDivider.js => CardDivider.tsx} | 12 +- src/card/CardFeaturedSubtitle.js | 32 - src/card/CardFeaturedSubtitle.tsx | 36 + src/card/CardFeaturedTitle.js | 32 - src/card/CardFeaturedTitle.tsx | 36 + src/card/{CardImage.js => CardImage.tsx} | 11 +- src/card/CardTitle.js | 34 - src/card/CardTitle.tsx | 38 + src/card/__tests__/Card.js | 6 - src/checkbox/{CheckBox.js => CheckBox.tsx} | 107 +- .../{CheckBoxIcon.js => CheckBoxIcon.tsx} | 42 +- src/checkbox/__tests__/CheckBox.js | 13 - .../__tests__/__snapshots__/CheckBox.js.snap | 122 +- ...BackgroundImage.js => BackgroundImage.tsx} | 0 .../{ThemeProvider.js => ThemeProvider.tsx} | 52 +- src/config/__tests__/ThemeProvider.js | 17 - .../__snapshots__/ThemeProvider.js.snap | 1 + src/config/__tests__/colors.js | 1 + src/config/__tests__/withTheme.js | 17 +- src/config/{colors.js => colors.tsx} | 41 +- src/config/{colorsDark.js => colorsDark.tsx} | 15 +- src/config/{fonts.js => fonts.tsx} | 0 src/config/{index.js => index.tsx} | 0 src/config/theme.js | 5 - src/config/theme.tsx | 73 + src/config/{withTheme.js => withTheme.tsx} | 58 +- src/divider/Divider.js | 27 - src/divider/Divider.tsx | 29 + src/divider/__tests__/Divider.js | 6 - src/header/{Header.js => Header.tsx} | 124 +- src/header/__tests__/Header.js | 19 - .../{getIconStyle.js => getIconStyle.tsx} | 4 +- .../{getIconType.js => getIconType.tsx} | 8 +- src/helpers/{index.js => index.tsx} | 11 +- src/helpers/nodeType.js | 8 - .../{normalizeText.js => normalizeText.tsx} | 2 +- src/helpers/{renderNode.js => renderNode.tsx} | 3 +- src/helpers/types.js | 18 - src/icons/{Icon.js => Icon.tsx} | 105 +- src/icons/__tests__/Icon.js | 8 - src/image/{Image.js => Image.tsx} | 85 +- src/image/__tests__/Image.js | 9 - .../__tests__/__snapshots__/Image.js.snap | 1 + src/index.d.ts | 2113 ----------------- src/index.js | 5 - src/input/{Input.js => Input.tsx} | 154 +- src/input/__tests__/Input.js | 23 - src/list/{ListItem.js => ListItem.tsx} | 124 +- ...ButtonGroup.js => ListItemButtonGroup.tsx} | 14 +- ...stItemCheckBox.js => ListItemCheckBox.tsx} | 9 +- ...ListItemChevron.js => ListItemChevron.tsx} | 5 +- ...ListItemContent.js => ListItemContent.tsx} | 19 +- .../{ListItemInput.js => ListItemInput.tsx} | 13 +- ...stItemSubtitle.js => ListItemSubtitle.tsx} | 19 +- .../{ListItemTitle.js => ListItemTitle.tsx} | 18 +- src/list/__tests__/ListItem.js | 14 +- .../__tests__/__snapshots__/ListItem.js.snap | 4 +- src/overlay/{Overlay.js => Overlay.tsx} | 26 +- src/overlay/__tests__/Overlay.js | 9 - .../__tests__/__snapshots__/Overlay.js.snap | 1 + .../{PricingCard.js => PricingCard.tsx} | 136 +- src/pricing/__tests__/PricingCard.js | 13 +- .../__snapshots__/PricingCard.js.snap | 15 +- ...chBar-android.js => SearchBar-android.tsx} | 120 +- ...chBar-default.js => SearchBar-default.tsx} | 157 +- .../{SearchBar-ios.js => SearchBar-ios.tsx} | 146 +- src/searchbar/SearchBar.js | 57 - src/searchbar/SearchBar.tsx | 85 + src/searchbar/__tests__/SearchBar.js | 33 - .../__snapshots__/SearchBar-android.js.snap | 12 - .../__snapshots__/SearchBar-ios.js.snap | 2 - .../__tests__/__snapshots__/SearchBar.js.snap | 34 - src/searchbar/__tests__/common.js | 27 - src/slider/{Slider.js => Slider.tsx} | 233 +- src/slider/__tests__/Slider.js | 30 - .../__tests__/__snapshots__/Slider.js.snap | 6 + src/social/{SocialIcon.js => SocialIcon.tsx} | 105 +- src/social/__tests__/SocialIcon.js | 14 - .../__snapshots__/SocialIcon.js.snap | 1 + src/text/{Text.js => Text.tsx} | 68 +- src/text/__tests__/Text.js | 11 - .../{FeaturedTile.js => FeaturedTile.tsx} | 47 +- src/tile/{Tile.js => Tile.tsx} | 97 +- src/tile/__tests__/FeaturedTile.js | 11 - src/tile/__tests__/Tile.js | 10 - src/tile/__tests__/__snapshots__/Tile.js.snap | 5 +- src/tooltip/{Tooltip.js => Tooltip.tsx} | 189 +- src/tooltip/{Triangle.js => Triangle.tsx} | 18 +- src/tooltip/__tests__/Tooltip.android.js | 4 - src/tooltip/__tests__/Tooltip.js | 27 +- .../__tests__/__snapshots__/Tooltip.js.snap | 16 +- ...Coordinate.js => getTooltipCoordinate.tsx} | 48 +- tsconfig.json | 51 + yarn.lock | 86 +- 119 files changed, 2500 insertions(+), 4916 deletions(-) rename src/avatar/{Accessory.js => Accessory.tsx} (62%) rename src/avatar/{Avatar.js => Avatar.tsx} (64%) rename src/badge/{Badge.js => Badge.tsx} (52%) rename src/badge/{withBadge.js => withBadge.tsx} (68%) rename src/bottomSheet/{BottomSheet.js => BottomSheet.tsx} (69%) rename src/buttons/{Button.js => Button.tsx} (57%) delete mode 100644 src/buttons/ButtonGroup.js create mode 100644 src/buttons/ButtonGroup.tsx delete mode 100644 src/card/Card.js create mode 100644 src/card/Card.tsx rename src/card/{CardDivider.js => CardDivider.tsx} (61%) delete mode 100644 src/card/CardFeaturedSubtitle.js create mode 100644 src/card/CardFeaturedSubtitle.tsx delete mode 100644 src/card/CardFeaturedTitle.js create mode 100644 src/card/CardFeaturedTitle.tsx rename src/card/{CardImage.js => CardImage.tsx} (62%) delete mode 100644 src/card/CardTitle.js create mode 100644 src/card/CardTitle.tsx rename src/checkbox/{CheckBox.js => CheckBox.tsx} (60%) rename src/checkbox/{CheckBoxIcon.js => CheckBoxIcon.tsx} (55%) rename src/config/{BackgroundImage.js => BackgroundImage.tsx} (100%) rename src/config/{ThemeProvider.js => ThemeProvider.tsx} (63%) rename src/config/{colors.js => colors.tsx} (60%) rename src/config/{colorsDark.js => colorsDark.tsx} (77%) rename src/config/{fonts.js => fonts.tsx} (100%) rename src/config/{index.js => index.tsx} (100%) delete mode 100644 src/config/theme.js create mode 100644 src/config/theme.tsx rename src/config/{withTheme.js => withTheme.tsx} (50%) delete mode 100644 src/divider/Divider.js create mode 100644 src/divider/Divider.tsx rename src/header/{Header.js => Header.tsx} (66%) rename src/helpers/{getIconStyle.js => getIconStyle.tsx} (88%) rename src/helpers/{getIconType.js => getIconType.tsx} (89%) rename src/helpers/{index.js => index.tsx} (68%) delete mode 100644 src/helpers/nodeType.js rename src/helpers/{normalizeText.js => normalizeText.tsx} (71%) rename src/helpers/{renderNode.js => renderNode.tsx} (89%) delete mode 100644 src/helpers/types.js rename src/icons/{Icon.js => Icon.tsx} (70%) rename src/image/{Image.js => Image.tsx} (68%) delete mode 100644 src/index.d.ts rename src/input/{Input.js => Input.tsx} (63%) rename src/list/{ListItem.js => ListItem.tsx} (53%) rename src/list/{ListItemButtonGroup.js => ListItemButtonGroup.tsx} (64%) rename src/list/{ListItemCheckBox.js => ListItemCheckBox.tsx} (69%) rename src/list/{ListItemChevron.js => ListItemChevron.tsx} (81%) rename src/list/{ListItemContent.js => ListItemContent.tsx} (61%) rename src/list/{ListItemInput.js => ListItemInput.tsx} (83%) rename src/list/{ListItemSubtitle.js => ListItemSubtitle.tsx} (72%) rename src/list/{ListItemTitle.js => ListItemTitle.tsx} (71%) rename src/overlay/{Overlay.js => Overlay.tsx} (81%) rename src/pricing/{PricingCard.js => PricingCard.tsx} (59%) rename src/searchbar/{SearchBar-android.js => SearchBar-android.tsx} (67%) rename src/searchbar/{SearchBar-default.js => SearchBar-default.tsx} (57%) rename src/searchbar/{SearchBar-ios.js => SearchBar-ios.tsx} (74%) delete mode 100644 src/searchbar/SearchBar.js create mode 100644 src/searchbar/SearchBar.tsx rename src/slider/{Slider.js => Slider.tsx} (77%) rename src/social/{SocialIcon.js => SocialIcon.tsx} (77%) rename src/text/{Text.js => Text.tsx} (53%) rename src/tile/{FeaturedTile.js => FeaturedTile.tsx} (70%) rename src/tile/{Tile.js => Tile.tsx} (63%) rename src/tooltip/{Tooltip.js => Tooltip.tsx} (75%) rename src/tooltip/{Triangle.js => Triangle.tsx} (70%) rename src/tooltip/{getTooltipCoordinate.js => getTooltipCoordinate.tsx} (87%) create mode 100644 tsconfig.json diff --git a/.eslintignore b/.eslintignore index c007627028..9115b5a564 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ website coverage +dist \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 45b8942f9d..c830cac279 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,7 @@ { "extends": "@react-native-community", "rules": { - "react-native/no-inline-styles": 0 + "react-native/no-inline-styles": 0, + "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }] } } diff --git a/.gitignore b/.gitignore index 2a38ec94f6..e6b0c370ce 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ website/build package-lock.json build *.orig +dist/ diff --git a/package.json b/package.json index aa767ad265..d6fafa3d0c 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,12 @@ "name": "react-native-elements", "version": "3.2.0", "description": "React Native Elements & UI Toolkit", - "main": "src/index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "prepublish": "tsc", "files": [ - "src", - "!**/__tests__" + "dist" ], - "types": "src/index.d.ts", "keywords": [ "react-native", "reactjs", @@ -15,12 +15,13 @@ "bootstrap" ], "scripts": { + "build": "tsc", "test": "jest", "test:update": "jest -u", "test:ci": "jest --runInBand", "test:watch": "jest --watch", "postinstall": "opencollective-postinstall || exit 0", - "lint": "eslint --ext js,ts .", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "prettify": "prettier --single-quote --trailing-comma=es5 --write './**/*.md'", "clean-install": "rimraf node_modules && yarn", "changelog": "auto-changelog -p" @@ -43,16 +44,18 @@ "hoist-non-react-statics": "^3.3.2", "lodash.isequal": "^4.5.0", "opencollective-postinstall": "^2.0.3", - "prop-types": "^15.7.2", "react-native-ratings": "^7.3.0", "react-native-size-matters": "^0.3.1" }, "devDependencies": { "@react-native-community/eslint-config": "^2.0.0", "@testing-library/react-native": "^7.0.2", - "@types/prop-types": "^15.7.3", - "@types/react": "^16.9.0", - "@types/react-native": "^0.63.2", + "@types/color": "^3.0.1", + "@types/enzyme": "^3.10.8", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/lodash.isequal": "^4.5.5", + "@types/react-native": "^0.63.48", + "@types/react-test-renderer": "^17.0.0", "auto-changelog": "^2.2.1", "babel-jest": "^26.3.0", "enzyme": "^3.11.0", @@ -67,15 +70,16 @@ "react": "16.13.1", "react-dom": "16.13.1", "react-native": "0.63.2", - "react-native-vector-icons": "^7.0.0", "react-native-safe-area-context": "^3.1.9", + "react-native-vector-icons": "^7.0.0", "react-test-renderer": "^16.13.1", "rimraf": "^3.0.2", - "typescript": "^3.9.5" + "typescript": "^4.1.3", + "utility-types": "^3.10.0" }, "peerDependencies": { - "react-native-vector-icons": ">7.0.0", - "react-native-safe-area-context": "^3.1.9" + "react-native-safe-area-context": "^3.1.9", + "react-native-vector-icons": ">7.0.0" }, "jest": { "preset": "react-native", @@ -83,15 +87,16 @@ "coverageDirectory": "./coverage/", "testPathIgnorePatterns": [ "./src/searchbar/__tests__/common.js", - "/node_modules" + "/node_modules", + "/dist" ], "coveragePathIgnorePatterns": [ "./src/searchbar/__tests__/common.js" ], "collectCoverageFrom": [ - "src/**/*.js", - "!src/index.js", - "!src/helpers/*.js" + "src/**/*.tsx", + "!src/index.tsx", + "!src/helpers/*.tsx" ], "collectCoverage": true, "globals": { @@ -102,15 +107,25 @@ ], "transform": { ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub" - } + }, + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json", + "node" + ] }, "lint-staged": { - "src/**/*.js": [ + "src/**/*.{ts,tsx}": [ "eslint --fix", + "bash -c tsc", "jest --bail --findRelatedTests" ], - "src/index.d.ts": [ - "eslint --fix" + "src/**/*.{js,jsx}": [ + "eslint --fix", + "jest --bail --findRelatedTests" ], "**/*.md": [ "prettier --single-quote --trailing-comma=es5 --write" diff --git a/src/avatar/Accessory.js b/src/avatar/Accessory.tsx similarity index 62% rename from src/avatar/Accessory.js rename to src/avatar/Accessory.tsx index ec376404bd..e8dcc912ba 100644 --- a/src/avatar/Accessory.js +++ b/src/avatar/Accessory.tsx @@ -1,22 +1,35 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { TouchableHighlight, View, Platform, StyleSheet } from 'react-native'; +import { + TouchableHighlight, + View, + Platform, + StyleSheet, + StyleProp, + ViewStyle, + ColorValue, +} from 'react-native'; import { withTheme } from '../config'; -import Image from '../image/Image'; -import Icon from '../icons/Icon'; +import Image, { ImageProps } from '../image/Image'; +import Icon, { IconProps } from '../icons/Icon'; -function Accessory({ - size, +export type AccessoryProps = Partial & + Partial & { + underlayColor?: ColorValue; + style?: StyleProp; + }; + +const Accessory: React.FunctionComponent = ({ + size = 10, style, - underlayColor, + underlayColor = '#000', onPress, onLongPress, source, ...props -}) { +}: AccessoryProps) => { return ( {source ? ( + //@ts-ignore ) : ( - + )} ); -} - -Accessory.defaultProps = { - size: 10, - name: 'mode-edit', - type: 'material', - color: '#fff', - underlayColor: '#000', -}; - -Accessory.propTypes = { - size: PropTypes.number, - name: PropTypes.string, - type: PropTypes.string, - color: PropTypes.string, - underlayColor: PropTypes.string, - style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - onPress: PropTypes.func, }; const styles = StyleSheet.create({ diff --git a/src/avatar/Avatar.js b/src/avatar/Avatar.tsx similarity index 64% rename from src/avatar/Avatar.js rename to src/avatar/Avatar.tsx index 612e32795a..298f1038c1 100644 --- a/src/avatar/Avatar.js +++ b/src/avatar/Avatar.tsx @@ -1,22 +1,21 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { View, Text, Image as RNImage, StyleSheet, TouchableOpacity, - TouchableHighlight, - TouchableNativeFeedback, - TouchableWithoutFeedback, + StyleProp, + ViewStyle, + TextStyle, + ImageSourcePropType, + ImageStyle, } from 'react-native'; import isEqual from 'lodash.isequal'; - import { withTheme } from '../config'; -import { renderNode, nodeType, ImageSourceType } from '../helpers'; - -import Icon from '../icons/Icon'; -import Image from '../image/Image'; +import { renderNode } from '../helpers'; +import Icon, { IconObject } from '../icons/Icon'; +import Image, { ImageProps } from '../image/Image'; import Accessory from './Accessory'; const avatarSizes = { @@ -26,7 +25,34 @@ const avatarSizes = { xlarge: 150, }; -const AvatarComponent = ({ +interface AvatarIcon extends IconObject { + iconStyle?: StyleProp; +} + +export type AvatarProps = { + Component?: typeof React.Component; + onPress?(): void; + onLongPress?(): void; + containerStyle?: StyleProp; + source?: ImageSourcePropType; + avatarStyle?: ImageStyle; + rounded?: boolean; + title?: string; + titleStyle?: StyleProp; + overlayContainerStyle?: StyleProp; + activeOpacity?: number; + icon?: AvatarIcon; + iconStyle?: StyleProp; + size?: ('small' | 'medium' | 'large' | 'xlarge') | number; + placeholderStyle?: StyleProp; + renderPlaceholderContent?: React.ReactElement<{}>; + imageProps?: Partial; + ImageComponent?: React.ComponentClass; +}; + +interface Avatar extends React.FunctionComponent {} + +const AvatarComponent: Avatar = ({ onPress, onLongPress, Component = onPress || onLongPress ? TouchableOpacity : View, @@ -34,7 +60,7 @@ const AvatarComponent = ({ icon, iconStyle, source, - size, + size = 'small', avatarStyle, rounded, title, @@ -43,16 +69,15 @@ const AvatarComponent = ({ imageProps, placeholderStyle, renderPlaceholderContent, - ImageComponent, + ImageComponent = RNImage, children, ...attributes -}) => { +}: React.PropsWithChildren) => { const width = typeof size === 'number' ? size : avatarSizes[size] || avatarSizes.small; const height = width; const titleSize = width / 2; const iconSize = width / 2; - const PlaceholderContent = (renderPlaceholderContent && renderNode(undefined, renderPlaceholderContent)) || @@ -69,6 +94,7 @@ const AvatarComponent = ({ )) || (icon && ( )); - - // Remove placeholder styling if we're not using image + // @ts-ignore const hidePlaceholder = !(source && source.uri); - // Merge image container style const imageContainerStyle = StyleSheet.flatten([ styles.overlayContainer, @@ -87,11 +111,9 @@ const AvatarComponent = ({ overlayContainerStyle, imageProps && imageProps.containerStyle, ]); - if (imageProps && imageProps.containerStyle) { delete imageProps.containerStyle; } - return ( { jest.useFakeTimers(); - it('uses Icon', () => { const component = shallow(); expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); - it('uses Image', () => { const component = shallow( diff --git a/src/avatar/__tests__/Avatar.js b/src/avatar/__tests__/Avatar.js index 12b8a43c8d..7ec50a3849 100644 --- a/src/avatar/__tests__/Avatar.js +++ b/src/avatar/__tests__/Avatar.js @@ -3,9 +3,9 @@ import { Text, TouchableOpacity } from 'react-native'; import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { create } from 'react-test-renderer'; - import { ThemeProvider } from '../../config'; import ThemedAvatar, { Avatar } from '../Avatar'; +import { Image } from '../../image/Image'; describe('Avatar Component', () => { jest.useFakeTimers(); @@ -14,7 +14,6 @@ describe('Avatar Component', () => { const component = shallow( ); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); @@ -45,7 +44,6 @@ describe('Avatar Component', () => { onPress={() => null} /> ); - expect(component.find(TouchableOpacity)).toBeTruthy(); expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); @@ -57,14 +55,12 @@ describe('Avatar Component', () => { source: { uri: 'https://i.imgur.com/0y8Ftya.jpg' }, }, }; - const component = create( ); - - expect(component.root.findByType('Image').props.source.uri).toBe( + expect(component.root.findByType(Image).props.source.uri).toBe( 'https://i.imgur.com/0y8Ftya.jpg' ); expect(component.toJSON()).toMatchSnapshot(); @@ -115,21 +111,6 @@ describe('Avatar Component', () => { expect(toJson(component)).toMatchSnapshot(); }); - it('defaults to small if invalid string given', () => { - const error = jest.fn(); - global.console.error = error; - - const component = shallow( - - ); - - expect(component.length).toBe(1); - expect(toJson(component)).toMatchSnapshot(); - }); - it('accepts a number', () => { const component = shallow( @@ -147,7 +128,6 @@ describe('Avatar Component', () => { title="MH" /> ); - jest.advanceTimersByTime(200); done(); }); @@ -174,7 +154,6 @@ describe('Avatar Component', () => { }} /> ); - expect(toJson(component)).toMatchSnapshot(); }); @@ -188,7 +167,6 @@ describe('Avatar Component', () => { icon={{}} /> ); - expect(toJson(component)).toMatchSnapshot(); }); @@ -200,7 +178,6 @@ describe('Avatar Component', () => { title="MD" /> ); - expect(component.props().style.backgroundColor).toBe('transparent'); expect(toJson(component)).toMatchSnapshot(); }); diff --git a/src/avatar/__tests__/__snapshots__/Avatar.js.snap b/src/avatar/__tests__/__snapshots__/Avatar.js.snap index aed9a969f3..8512b52a5b 100644 --- a/src/avatar/__tests__/__snapshots__/Avatar.js.snap +++ b/src/avatar/__tests__/__snapshots__/Avatar.js.snap @@ -39,8 +39,8 @@ exports[`Avatar Component Placeholders renders using icon prop 1`] = ` style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -85,8 +85,8 @@ exports[`Avatar Component Placeholders renders using icon with defaults 1`] = ` style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -133,8 +133,8 @@ exports[`Avatar Component Placeholders shouldn't show placeholder if not using s style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -182,8 +182,8 @@ exports[`Avatar Component Placeholders shouldn't show placeholder if source does style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -235,8 +235,8 @@ exports[`Avatar Component Placeholders shouldn't show placeholder if source exis style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -269,8 +269,8 @@ exports[`Avatar Component Sizes accepts a number 1`] = ` style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -303,8 +303,8 @@ exports[`Avatar Component Sizes accepts large 1`] = ` style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -337,8 +337,8 @@ exports[`Avatar Component Sizes accepts medium 1`] = ` style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -371,8 +371,8 @@ exports[`Avatar Component Sizes accepts small 1`] = ` style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -405,42 +405,8 @@ exports[`Avatar Component Sizes accepts xlarge 1`] = ` style={ Object { "flex": 1, - "height": null, - "width": null, - } - } - /> - -`; - -exports[`Avatar Component Sizes defaults to small if invalid string given 1`] = ` - - @@ -474,8 +440,8 @@ exports[`Avatar Component allows custom imageProps 1`] = ` style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -512,8 +478,8 @@ exports[`Avatar Component renders rounded 1`] = ` style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -547,8 +513,8 @@ exports[`Avatar Component renders touchable if onPress given 1`] = ` style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -589,6 +555,7 @@ exports[`Avatar Component should apply values from theme 1`] = ` "error": "#f44336", "grey": "rgba(0, 0, 0, 0.54)", "primary": "#2196f3", + "searchBg": "#dcdce1", "secondary": "#9C27B0", "success": "#4caf50", "warning": "#ffeb3b", @@ -655,12 +622,12 @@ exports[`Avatar Component should apply values from theme 1`] = ` Object { "bottom": 0, "flex": 1, - "height": null, + "height": undefined, "left": 0, "position": "absolute", "right": 0, "top": 0, - "width": null, + "width": undefined, } } testID="RNE__Image" @@ -688,6 +655,7 @@ exports[`Avatar Component should apply values from theme 1`] = ` "error": "#f44336", "grey": "rgba(0, 0, 0, 0.54)", "primary": "#2196f3", + "searchBg": "#dcdce1", "secondary": "#9C27B0", "success": "#4caf50", "warning": "#ffeb3b", @@ -754,9 +722,9 @@ exports[`Avatar Component should apply values from theme 1`] = ` "alignItems": "center", "backgroundColor": "#bdbdbd", "flex": 1, - "height": null, + "height": undefined, "justifyContent": "center", - "width": null, + "width": undefined, } } testID="RNE__Image__placeholder" @@ -766,8 +734,8 @@ exports[`Avatar Component should apply values from theme 1`] = ` style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> @@ -801,8 +769,8 @@ exports[`Avatar Component should render without issues 1`] = ` style={ Object { "flex": 1, - "height": null, - "width": null, + "height": undefined, + "width": undefined, } } /> diff --git a/src/badge/Badge.js b/src/badge/Badge.tsx similarity index 52% rename from src/badge/Badge.js rename to src/badge/Badge.tsx index 4cf9798e4f..e65a0c1376 100644 --- a/src/badge/Badge.js +++ b/src/badge/Badge.tsx @@ -1,11 +1,30 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { StyleSheet, Text, View, TouchableOpacity } from 'react-native'; - +import { + StyleSheet, + Text, + View, + TouchableOpacity, + TextProps, + StyleProp, + ViewStyle, +} from 'react-native'; import { withTheme } from '../config'; +import { Theme } from '../config/theme'; import { renderNode } from '../helpers'; -const Badge = (props) => { +export type BadgeProps = { + containerStyle?: StyleProp; + badgeStyle?: StyleProp; + textProps?: TextProps; + textStyle?: StyleProp; + value?: React.ReactNode; + onPress?: (...args: any[]) => any; + Component?: typeof React.Component; + theme?: Theme; + status?: 'primary' | 'success' | 'warning' | 'error'; +}; + +const Badge: React.FunctionComponent = (props) => { const { containerStyle, textStyle, @@ -15,21 +34,29 @@ const Badge = (props) => { Component = onPress ? TouchableOpacity : View, value, theme, - status, + status = 'primary', ...attributes } = props; - const element = renderNode(Text, value, { style: StyleSheet.flatten([styles.text, textStyle && textStyle]), ...textProps, }); - return ( { ); }; -Badge.propTypes = { - containerStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - badgeStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - textProps: PropTypes.object, - textStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - value: PropTypes.node, - onPress: PropTypes.func, - Component: PropTypes.elementType, - theme: PropTypes.object, - status: PropTypes.oneOf(['primary', 'success', 'warning', 'error']), -}; - -Badge.defaultProps = { - status: 'primary', -}; - const size = 18; const miniSize = 8; -const styles = { - badge: (theme, status) => ({ - alignSelf: 'center', - minWidth: size, - height: size, - borderRadius: size / 2, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: theme.colors[status], - borderWidth: StyleSheet.hairlineWidth, - borderColor: '#fff', - }), +const styles = StyleSheet.create({ miniBadge: { paddingHorizontal: 0, paddingVertical: 0, @@ -84,7 +84,7 @@ const styles = { color: 'white', paddingHorizontal: 4, }, -}; +}); export { Badge }; export default withTheme(Badge, 'Badge'); diff --git a/src/badge/__tests__/Badge.js b/src/badge/__tests__/Badge.js index 2ad31e9e34..14782c7dd3 100644 --- a/src/badge/__tests__/Badge.js +++ b/src/badge/__tests__/Badge.js @@ -4,33 +4,28 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { render } from '@testing-library/react-native'; import renderer from 'react-test-renderer'; - import { ThemeProvider } from '../../config'; import theme from '../../config/theme'; - import ThemedBadge, { Badge } from '../Badge'; describe('Badge Component', () => { it('should render without issue', () => { const component = shallow(); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); it('should render if element included', () => { - const component = shallow( - } /> - ); - + const component = shallow(foo} />); expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); - expect(component.props().children.props.children.props.title).toBe('foo'); + expect(component.props().children.props.children).toStrictEqual( + foo + ); }); it('should pass value props should still work', () => { const component = shallow(); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); @@ -43,7 +38,6 @@ describe('Badge Component', () => { containerStyle={{ backgroundColor: 'orange' }} /> ); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); @@ -56,7 +50,6 @@ describe('Badge Component', () => { badgeStyle={{ backgroundColor: 'pink' }} /> ); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); @@ -65,7 +58,6 @@ describe('Badge Component', () => { const component = shallow( ); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); @@ -74,7 +66,6 @@ describe('Badge Component', () => { const component = shallow( ); - expect(component.length).toBe(1); expect(component.find(TouchableOpacity)).toBeTruthy(); expect(toJson(component)).toMatchSnapshot(); @@ -91,28 +82,24 @@ describe('Badge Component', () => { describe('Mini badge', () => { it('primary', () => { const component = shallow(); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); it('success', () => { const component = shallow(); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); it('warning', () => { const component = shallow(); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); it('error', () => { const component = shallow(); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); @@ -124,13 +111,11 @@ describe('Badge Component', () => { textStyle: { color: 'red' }, }, }; - const component = renderer.create( ); - expect(component.root.findByType(Text).props.style).toMatchObject({ color: 'red', }); diff --git a/src/badge/__tests__/__snapshots__/Badge.js.snap b/src/badge/__tests__/__snapshots__/Badge.js.snap index 3fc0fffdbd..fde4fff97f 100644 --- a/src/badge/__tests__/__snapshots__/Badge.js.snap +++ b/src/badge/__tests__/__snapshots__/Badge.js.snap @@ -290,9 +290,9 @@ exports[`Badge Component should render if element included 1`] = ` } } > - + + foo + `; diff --git a/src/badge/__tests__/withBadge.js b/src/badge/__tests__/withBadge.js index b74eb26e7c..e186cda964 100644 --- a/src/badge/__tests__/withBadge.js +++ b/src/badge/__tests__/withBadge.js @@ -2,7 +2,6 @@ import React from 'react'; import { TouchableOpacity } from 'react-native'; import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; - import withBadge from '../withBadge'; describe('withBadge HOC', () => { @@ -10,7 +9,6 @@ describe('withBadge HOC', () => { it('should render with just a value', () => { const BadgedComponent = withBadge(1)(TouchableOpacity); const component = shallow(); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); @@ -18,7 +16,6 @@ describe('withBadge HOC', () => { it('should render when given a function as value', () => { const BadgedComponent = withBadge(() => 1)(TouchableOpacity); const component = shallow(); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); @@ -33,7 +30,6 @@ describe('withBadge HOC', () => { }; const BadgedComponent = withBadge(1, options)(TouchableOpacity); const component = shallow(); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); @@ -44,7 +40,6 @@ describe('withBadge HOC', () => { }; const BadgedComponent = withBadge(1, options)(TouchableOpacity); const component = shallow(); - expect(component.length).toBe(1); expect(toJson(component)).toMatchSnapshot(); }); diff --git a/src/badge/withBadge.js b/src/badge/withBadge.tsx similarity index 68% rename from src/badge/withBadge.js rename to src/badge/withBadge.tsx index 577e70e94c..4ffdd4e01f 100644 --- a/src/badge/withBadge.js +++ b/src/badge/withBadge.tsx @@ -1,10 +1,21 @@ import React from 'react'; -import { StyleSheet, View } from 'react-native'; +import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; +import Badge, { BadgeProps } from './Badge'; -import Badge from './Badge'; +type withBadgeOptions = { + bottom?: number; + left?: number; + right?: number; + top?: number; + hidden?: boolean; + containerStyle?: StyleProp; +} & BadgeProps; -const withBadge = (value, options = {}) => (WrappedComponent) => { - const WithBadge = (props) => { +const withBadge = ( + value: React.ReactNode | ((props: any) => React.ReactNode), + options: withBadgeOptions = {} +) => (WrappedComponent: React.ComponentType): React.ComponentType => { + const WithBadge = (props: any) => { const { bottom, hidden = false, @@ -12,16 +23,12 @@ const withBadge = (value, options = {}) => (WrappedComponent) => { containerStyle, ...badgeProps } = options; - let { right = -16, top = -1 } = options; - if (!value) { right = -3; top = 3; } - const badgeValue = typeof value === 'function' ? value(props) : value; - return ( @@ -40,11 +47,9 @@ const withBadge = (value, options = {}) => (WrappedComponent) => { ); }; - WithBadge.displayName = `WithBadge(${ WrappedComponent.displayName || WrappedComponent.name || 'Component' })`; - return WithBadge; }; diff --git a/src/bottomSheet/BottomSheet.js b/src/bottomSheet/BottomSheet.tsx similarity index 69% rename from src/bottomSheet/BottomSheet.js rename to src/bottomSheet/BottomSheet.tsx index 01949920b1..6c847ffbd5 100644 --- a/src/bottomSheet/BottomSheet.js +++ b/src/bottomSheet/BottomSheet.tsx @@ -1,16 +1,34 @@ import React from 'react'; -import { Modal, View, StyleSheet, ScrollView } from 'react-native'; +import { + Modal, + View, + StyleSheet, + ScrollView, + StyleProp, + ViewStyle, + ModalProps, +} from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { withTheme } from '../config'; -import PropTypes from 'prop-types'; -function BottomSheet({ +export type BottomSheetProps = { + containerStyle?: StyleProp; + modalProps?: ModalProps; + isVisible?: boolean; +} & typeof defaultProps; + +const defaultProps = { + modalProps: {}, + isVisible: false, +}; + +const BottomSheet: React.FunctionComponent = ({ containerStyle, isVisible, modalProps, children, ...props -}) { +}) => { return ( ); -} +}; const styles = StyleSheet.create({ safeAreaView: { @@ -42,17 +60,7 @@ const styles = StyleSheet.create({ listContainer: { backgroundColor: 'white' }, }); -BottomSheet.defaultProps = { - modalProps: {}, - isVisible: false, -}; - -BottomSheet.propTypes = { - containerStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - modalProps: PropTypes.object, - isVisible: PropTypes.bool, -}; +BottomSheet.defaultProps = defaultProps; export { BottomSheet }; - export default withTheme(BottomSheet, 'BottomSheet'); diff --git a/src/bottomSheet/__tests__/BottomSheet.js b/src/bottomSheet/__tests__/BottomSheet.js index af29c27a3c..a6410a0bbd 100644 --- a/src/bottomSheet/__tests__/BottomSheet.js +++ b/src/bottomSheet/__tests__/BottomSheet.js @@ -3,7 +3,6 @@ import { BottomSheet } from '../BottomSheet'; import { Modal } from 'react-native'; import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; - import ListItem from '../../list/ListItem'; import toJson from 'enzyme-to-json'; diff --git a/src/bottomSheet/__tests__/__snapshots__/BottomSheet.js.snap b/src/bottomSheet/__tests__/__snapshots__/BottomSheet.js.snap index a9ef325e45..a3793d423a 100644 --- a/src/bottomSheet/__tests__/__snapshots__/BottomSheet.js.snap +++ b/src/bottomSheet/__tests__/__snapshots__/BottomSheet.js.snap @@ -34,11 +34,14 @@ exports[`BottomSheet Component renders correctly 1`] = ` > ({ +const defaultLoadingProps = ( + type: 'solid' | 'clear' | 'outline', + theme: Theme +): ActivityIndicatorProps => ({ color: type === 'solid' ? 'white' : theme.colors.primary, size: 'small', }); -class Button extends Component { +export type ButtonProps = TouchableOpacityProps & + TouchableNativeFeedbackProps & { + title?: string | React.ReactElement<{}>; + titleStyle?: StyleProp; + titleProps?: TextProps; + buttonStyle?: StyleProp; + type?: 'solid' | 'clear' | 'outline'; + loading?: boolean; + loadingStyle?: StyleProp; + loadingProps?: ActivityIndicatorProps; + containerStyle?: StyleProp; + icon?: IconNode; + iconContainerStyle?: StyleProp; + iconRight?: boolean; + linearGradientProps?: object; + TouchableComponent?: typeof React.Component; + ViewComponent?: typeof React.Component; + disabled?: boolean; + disabledStyle?: StyleProp; + disabledTitleStyle?: StyleProp; + raised?: boolean; + theme?: Theme; + }; + +class Button extends Component { componentDidMount() { const { linearGradientProps, ViewComponent } = this.props; if (linearGradientProps && !ViewComponent) { @@ -31,8 +64,10 @@ class Button extends Component { } handleOnPress = (evt) => { - const { loading, onPress } = this.props; - + const { + loading, + onPress = () => console.log('Please attach a method to this component'), + } = this.props; if (!loading) { onPress(evt); } @@ -44,20 +79,20 @@ class Button extends Component { containerStyle, onPress, buttonStyle, - type, - loading, + type = 'solid', + loading = false, loadingStyle, loadingProps: passedLoadingProps, - title, + title = '', titleProps, titleStyle: passedTitleStyle, icon, iconContainerStyle, - iconRight, - disabled, + iconRight = false, + disabled = false, disabledStyle, disabledTitleStyle, - raised, + raised = false, linearGradientProps, ViewComponent = View, theme, @@ -74,22 +109,24 @@ class Button extends Component { default: TouchableOpacity, }); - const titleStyle = StyleSheet.flatten([ - styles.title(type, theme), + const titleStyle: StyleProp = StyleSheet.flatten([ + { color: type === 'solid' ? 'white' : theme.colors.primary }, + styles.title, passedTitleStyle, - disabled && styles.disabledTitle(theme), + disabled && { color: color(theme.colors.disabled).darken(0.3).string() }, disabled && disabledTitleStyle, ]); const background = Platform.OS === 'android' && Platform.Version >= 21 ? TouchableNativeFeedback.Ripple( + // @ts-ignore Color(titleStyle.color).alpha(0.32).rgb().string(), true ) : undefined; - const loadingProps = { + const loadingProps: ActivityIndicatorProps = { ...defaultLoadingProps(type, theme), ...passedLoadingProps, }; @@ -101,15 +138,14 @@ class Button extends Component { return ( @@ -172,71 +221,19 @@ class Button extends Component { } } -Button.propTypes = { - title: PropTypes.string, - titleStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - titleProps: PropTypes.object, - buttonStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - type: PropTypes.oneOf(['solid', 'clear', 'outline']), - loading: PropTypes.bool, - loadingStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - loadingProps: PropTypes.object, - onPress: PropTypes.func, - containerStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - icon: nodeType, - iconContainerStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - iconRight: PropTypes.bool, - linearGradientProps: PropTypes.object, - TouchableComponent: PropTypes.elementType, - ViewComponent: PropTypes.elementType, - disabled: PropTypes.bool, - disabledStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - disabledTitleStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - raised: PropTypes.bool, - theme: PropTypes.object, -}; - -Button.defaultProps = { - title: '', - iconRight: false, - onPress: () => console.log('Please attach a method to this component'), - type: 'solid', - buttonStyle: { - borderRadius: 3, - }, - disabled: false, - raised: false, - loading: false, -}; - -const styles = { - button: (type, theme) => ({ +const styles = StyleSheet.create({ + button: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', borderRadius: 3, - backgroundColor: type === 'solid' ? theme.colors.primary : 'transparent', padding: 8, - borderWidth: type === 'outline' ? StyleSheet.hairlineWidth : 0, - borderColor: theme.colors.primary, - }), + }, container: { overflow: 'hidden', borderRadius: 3, }, - disabled: (type, theme) => ({ - ...conditionalStyle(type === 'solid', { - backgroundColor: theme.colors.disabled, - }), - ...conditionalStyle(type === 'outline', { - borderColor: color(theme.colors.disabled).darken(0.3).string(), - }), - }), - disabledTitle: (theme) => ({ - color: color(theme.colors.disabled).darken(0.3).string(), - }), - title: (type, theme) => ({ - color: type === 'solid' ? 'white' : theme.colors.primary, + title: { fontSize: 16, textAlign: 'center', paddingVertical: 1, @@ -248,30 +245,30 @@ const styles = { fontSize: 18, }, }), - }), + }, iconContainer: { marginHorizontal: 5, }, - raised: (type) => - type !== 'clear' && { - backgroundColor: '#fff', - overflow: 'visible', - ...Platform.select({ - android: { - elevation: 4, - }, - default: { - shadowColor: 'rgba(0,0,0, .4)', - shadowOffset: { height: 1, width: 1 }, - shadowOpacity: 1, - shadowRadius: 1, - }, - }), - }, + raised: { + backgroundColor: '#fff', + overflow: 'visible', + ...Platform.select({ + android: { + elevation: 4, + }, + default: { + shadowColor: 'rgba(0,0,0, .4)', + shadowOffset: { height: 1, width: 1 }, + shadowOpacity: 1, + shadowRadius: 1, + }, + }), + }, loading: { marginVertical: 2, }, -}; +}); export { Button }; +//@ts-ignore export default withTheme(Button, 'Button'); diff --git a/src/buttons/ButtonGroup.js b/src/buttons/ButtonGroup.js deleted file mode 100644 index ac8f7dd4e6..0000000000 --- a/src/buttons/ButtonGroup.js +++ /dev/null @@ -1,262 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { - View, - TouchableNativeFeedback, - TouchableOpacity, - Platform, - StyleSheet, -} from 'react-native'; - -import { withTheme } from '../config'; -import { normalizeText, color } from '../helpers'; - -import Text from '../text/Text'; - -const ButtonGroup = (props) => { - const { theme, ...rest } = props; - - const { - Component, - buttons, - onPress, - selectedIndex, - selectedIndexes, - selectMultiple, - containerStyle, - innerBorderStyle, - buttonStyle, - buttonContainerStyle, - textStyle, - selectedTextStyle, - selectedButtonStyle, - underlayColor = theme.colors.primary, - activeOpacity, - onHideUnderlay, - onShowUnderlay, - setOpacityTo, - disabled, - disabledStyle, - disabledTextStyle, - disabledSelectedStyle, - disabledSelectedTextStyle, - vertical, - ...attributes - } = rest; - - let innerBorderWidth = 1; - - if ( - innerBorderStyle && - Object.prototype.hasOwnProperty.call(innerBorderStyle, 'width') - ) { - innerBorderWidth = innerBorderStyle.width; - } - - return ( - - {buttons.map((button, i) => { - const isSelected = selectedIndex === i || selectedIndexes.includes(i); - const isDisabled = - disabled === true || - (Array.isArray(disabled) && disabled.includes(i)); - - return ( - - { - if (selectMultiple) { - if (selectedIndexes.includes(i)) { - onPress(selectedIndexes.filter((index) => index !== i)); - } else { - onPress([...selectedIndexes, i]); - } - } else { - onPress(i); - } - }} - style={styles.button} - > - - {button.element ? ( - - ) : ( - - {button} - - )} - - - - ); - })} - - ); -}; - -const styles = { - button: { - flex: 1, - }, - textContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - container: { - marginHorizontal: 10, - marginVertical: 5, - borderColor: '#e3e3e3', - borderWidth: 1, - flexDirection: 'row', - borderRadius: 3, - overflow: 'hidden', - backgroundColor: '#fff', - height: 40, - }, - verticalContainer: { - flexDirection: 'column', - height: null, - }, - verticalComponent: { - height: 40, - }, - buttonText: (theme) => ({ - fontSize: normalizeText(13), - color: theme.colors.grey2, - ...Platform.select({ - android: {}, - default: { - fontWeight: '500', - }, - }), - }), - disabled: { - backgroundColor: 'transparent', - }, - disabledText: (theme) => ({ - color: color(theme.colors.disabled).darken(0.3).toString(), - }), - disabledSelected: (theme) => ({ - backgroundColor: theme.colors.disabled, - }), -}; - -ButtonGroup.propTypes = { - button: PropTypes.object, - Component: PropTypes.elementType, - onPress: PropTypes.func, - buttons: PropTypes.array, - containerStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - textStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - selectedTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - selectedButtonStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - underlayColor: PropTypes.string, - selectedIndex: PropTypes.number, - selectedIndexes: PropTypes.arrayOf(PropTypes.number), - activeOpacity: PropTypes.number, - onHideUnderlay: PropTypes.func, - onShowUnderlay: PropTypes.func, - setOpacityTo: PropTypes.func, - innerBorderStyle: PropTypes.shape({ - color: PropTypes.string, - width: PropTypes.number, - }), - buttonStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - buttonContainerStyle: PropTypes.oneOfType([ - PropTypes.object, - PropTypes.array, - ]), - selectMultiple: PropTypes.bool, - theme: PropTypes.object, - disabled: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.arrayOf(PropTypes.number), - ]), - disabledStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - disabledTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - disabledSelectedStyle: PropTypes.oneOfType([ - PropTypes.object, - PropTypes.array, - ]), - disabledSelectedTextStyle: PropTypes.oneOfType([ - PropTypes.object, - PropTypes.array, - ]), - vertical: PropTypes.bool, -}; - -ButtonGroup.defaultProps = { - selectedIndex: null, - selectedIndexes: [], - selectMultiple: false, - disabled: false, - Component: Platform.select({ - android: TouchableNativeFeedback, - default: TouchableOpacity, - }), - onPress: () => null, - vertical: false, -}; - -export { ButtonGroup }; -export default withTheme(ButtonGroup, 'ButtonGroup'); diff --git a/src/buttons/ButtonGroup.tsx b/src/buttons/ButtonGroup.tsx new file mode 100644 index 0000000000..a3048918fc --- /dev/null +++ b/src/buttons/ButtonGroup.tsx @@ -0,0 +1,239 @@ +import React from 'react'; +import { + View, + TouchableNativeFeedback, + TouchableOpacity, + Platform, + StyleSheet, + ViewStyle, + StyleProp, + TextStyle, +} from 'react-native'; +import { withTheme } from '../config'; +import { Theme } from '../config/theme'; +import { normalizeText, color } from '../helpers'; +import Text from '../text/Text'; + +export type ButtonGroupProps = { + button?: object; + Component?: typeof React.Component; + onPress?(...args: any[]): void; + buttons?: string[] | React.ReactElement<{}>[]; + containerStyle?: StyleProp; + textStyle?: StyleProp; + selectedTextStyle?: StyleProp; + selectedButtonStyle?: StyleProp; + underlayColor?: string; + selectedIndex?: number | null; + selectedIndexes?: number[]; + activeOpacity?: number; + onHideUnderlay?(): void; + onShowUnderlay?(): void; + setOpacityTo?: (value: number) => void; + innerBorderStyle?: { + color?: string; + width?: number; + }; + buttonStyle?: StyleProp; + buttonContainerStyle?: StyleProp; + selectMultiple?: boolean; + theme?: Theme; + disabled?: boolean | number[]; + disabledStyle?: StyleProp; + disabledTextStyle?: StyleProp; + disabledSelectedStyle?: StyleProp; + disabledSelectedTextStyle?: StyleProp; + vertical?: boolean; +}; + +const ButtonGroup: React.FunctionComponent = (props) => { + const { theme, ...rest } = props; + const { + Component = Platform.select({ + android: TouchableNativeFeedback, + default: TouchableOpacity, + }), + buttons, + onPress = () => null, + selectedIndex = null, + selectedIndexes = [], + selectMultiple = false, + containerStyle, + innerBorderStyle, + buttonStyle, + buttonContainerStyle, + textStyle, + selectedTextStyle, + selectedButtonStyle, + underlayColor = theme.colors.primary, + activeOpacity, + onHideUnderlay, + onShowUnderlay, + setOpacityTo, + disabled = false, + disabledStyle, + disabledTextStyle, + disabledSelectedStyle, + disabledSelectedTextStyle, + vertical = false, + ...attributes + } = rest; + let innerBorderWidth = 1; + if ( + innerBorderStyle && + Object.prototype.hasOwnProperty.call(innerBorderStyle, 'width') + ) { + innerBorderWidth = innerBorderStyle.width; + } + return ( + + { + // @ts-ignore + buttons.map((button: any, i: number) => { + const isSelected = selectedIndex === i || selectedIndexes.includes(i); + const isDisabled = + disabled === true || + (Array.isArray(disabled) && disabled.includes(i)); + return ( + + { + if (selectMultiple) { + if (selectedIndexes.includes(i)) { + onPress(selectedIndexes.filter((index) => index !== i)); + } else { + onPress([...selectedIndexes, i]); + } + } else { + onPress(i); + } + }} + style={styles.button} + > + + {button.element ? ( + + ) : ( + + {button} + + )} + + + + ); + }) + } + + ); +}; + +const styles = StyleSheet.create({ + button: { + flex: 1, + }, + textContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + container: { + marginHorizontal: 10, + marginVertical: 5, + borderColor: '#e3e3e3', + borderWidth: 1, + flexDirection: 'row', + borderRadius: 3, + overflow: 'hidden', + backgroundColor: '#fff', + height: 40, + }, + verticalContainer: { + flexDirection: 'column', + height: null, + }, + verticalComponent: { + height: 40, + }, + disabled: { + backgroundColor: 'transparent', + }, +}); + +export { ButtonGroup }; +export default withTheme(ButtonGroup, 'ButtonGroup'); diff --git a/src/buttons/__tests__/Button.js b/src/buttons/__tests__/Button.js index fcb3861d06..3721601496 100644 --- a/src/buttons/__tests__/Button.js +++ b/src/buttons/__tests__/Button.js @@ -3,16 +3,13 @@ import { ActivityIndicator, Platform } from 'react-native'; import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { create } from 'react-test-renderer'; - import theme from '../../config/theme'; import { ThemeProvider } from '../../config'; - import ThemedButton, { Button } from '../Button'; describe('Button Component', () => { it('should render without issues', () => { const component = shallow(