diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fe6ee80702..709abe6aaf 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,4 +2,4 @@ *Enter description to help the reviewer understand what's the change about...* ## Changelog -*Add a quick message for our users about this change (include Compoennt name, relevant props and general purpose of the PR)* +*Add a quick message for our users about this change (include Component name, relevant props and general purpose of the PR)* diff --git a/.npmignore b/.npmignore index 3934605551..3fc18293fb 100644 --- a/.npmignore +++ b/.npmignore @@ -1,10 +1,12 @@ example/ docs/ +docuilib/ +eslint-rules/ extensions/ node_modules/ .npmignore .watchmanconfig - +src/**/__tests__/*.* ################# # from .gitignore: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 31e383ef15..ec864c779a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ When contributing to this repository, please first discuss the change you wish t ## Pull Request Process 1. First make sure the environment is working and synced with master. For installation details go [here](https://github.com/wix/react-native-ui-lib/blob/master/markdowns/getting-started/setup.md#demo-app) -2. Before submitting a PR we suggest running `npm run prepush` command that verifies our lint, TS and tests were not broken. +2. Before submitting a PR we suggest running `npm run pre-push` command that verifies our lint, TS and tests were not broken. 3. Please use our PR prefixes accordingly. `fix` for bugfix, `feat` for feature, `infra` for infrastructure changes and `docs` for documentation related changes (e.g `fix/ButtonStyle`, `feat/Avatar_colorProp`) 4. Please don't change our PR template. - In the Description section add everything that can help the reviewer and the reviewing process, like a description of the issue and the way you decided to go about it, screenshots or gifs, etc. diff --git a/demo/src/demoApp.js b/demo/src/demoApp.js index c0fa880e8e..048952fed8 100644 --- a/demo/src/demoApp.js +++ b/demo/src/demoApp.js @@ -128,3 +128,30 @@ Navigation.events().registerAppLaunchedListener(() => { registerScreens(Navigation.registerComponent.bind(Navigation)); getDefaultScreenAndStartApp(); }); + + +/* Setting Intl Polyfills +This is due to lack of Intl support in Hermes engine + */ + +if (global.HermesInternal) { + if (Constants.isIOS) { + + // Polyfills required to use Intl with Hermes engine + require('@formatjs/intl-getcanonicallocales/polyfill').default; + require('@formatjs/intl-locale/polyfill').default; + require('@formatjs/intl-pluralrules/polyfill').default; + require('@formatjs/intl-pluralrules/locale-data/en').default; + require('@formatjs/intl-numberformat/polyfill').default; + require('@formatjs/intl-numberformat/locale-data/en').default; + require('@formatjs/intl-datetimeformat/polyfill').default; + require('@formatjs/intl-datetimeformat/locale-data/en').default; + require('@formatjs/intl-datetimeformat/add-all-tz').default; + } else { + require('@formatjs/intl-getcanonicallocales/polyfill'); + require('@formatjs/intl-locale/polyfill'); + require('@formatjs/intl-datetimeformat/polyfill'); + require('@formatjs/intl-datetimeformat/locale-data/en'); + require('@formatjs/intl-datetimeformat/add-all-tz'); + } +} diff --git a/demo/src/screens/MainScreen.js b/demo/src/screens/MainScreen.js index 7127f45f36..4c4e33c24c 100644 --- a/demo/src/screens/MainScreen.js +++ b/demo/src/screens/MainScreen.js @@ -2,7 +2,8 @@ import _ from 'lodash'; import React, {Component} from 'react'; import AsyncStorage from '@react-native-community/async-storage'; import PropTypes from 'prop-types'; -import {StyleSheet, FlatList, ViewPropTypes} from 'react-native'; +import {StyleSheet, FlatList} from 'react-native'; +import {ViewPropTypes} from 'deprecated-react-native-prop-types'; import {Navigation} from 'react-native-navigation'; import {gestureHandlerRootHOC} from 'react-native-gesture-handler'; import { diff --git a/demo/src/screens/__tests__/AvatarScreen.spec.js b/demo/src/screens/__tests__/AvatarScreen.spec.js new file mode 100644 index 0000000000..a4d3be50bb --- /dev/null +++ b/demo/src/screens/__tests__/AvatarScreen.spec.js @@ -0,0 +1,15 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; + +describe('AvatarScreen', () => { + let AvatarScreen; + + beforeEach(() => { + AvatarScreen = require('../componentScreens/AvatarsScreen').default; + }); + + it('renders screen', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/demo/src/screens/__tests__/__snapshots__/AvatarScreen.spec.js.snap b/demo/src/screens/__tests__/__snapshots__/AvatarScreen.spec.js.snap new file mode 100644 index 0000000000..3cd68781d4 --- /dev/null +++ b/demo/src/screens/__tests__/__snapshots__/AvatarScreen.spec.js.snap @@ -0,0 +1,2756 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AvatarScreen renders screen 1`] = ` + + + + + Custom Background + + + + + + + + Empty Avatar with ribbon + + + + + + New + + + + + + + Initials with Color + + + + + AD + + + + + New + + + + + + + Initials, badge ("online") + + + + + ES + + + + + + + + + + Image, badge ("away") + + + + + + + + + + + + Smaller size, Badge ("offline") + + + + + + + + + + + + Image with fade in animation + + + + + + + + + + + Big pimple + + + + + + + + + + + + Icon badge + + + + + + + + + + + + + + GIF + + + + + + + + + Invalid Gravatar (see logs) + + + + + 🤦 + + + + + + + + Monitored Avatar (see logs) + + + + + ?! + + + + + + + + Empty Gravatar + + + + + + + + + With custom badge label + + + + + LD + + + + + + +2 + + + + + + + +`; diff --git a/demo/src/screens/componentScreens/CheckboxScreen.tsx b/demo/src/screens/componentScreens/CheckboxScreen.tsx index 4ff8c9ee06..b07a9891a8 100644 --- a/demo/src/screens/componentScreens/CheckboxScreen.tsx +++ b/demo/src/screens/componentScreens/CheckboxScreen.tsx @@ -14,16 +14,34 @@ export default class CheckboxScreen extends Component { render() { return ( - - + + Checkbox - - this.setState({value1})} - style={styles.checkbox} - /> + + Customizable UI + + this.setState({value1})}/> + this.setState({value2})} + borderRadius={2} + size={30} + color={Colors.purple30} + selectedIcon={Assets.icons.x} + marginL-s5 + /> + + this.setState({value3})} + borderRadius={5} + size={18} + color={Colors.grey10} + iconColor={Colors.green10} + marginL-s5 + /> + this.setState({value6})} containerStyle={styles.checkbox} /> - this.setState({value2})} - borderRadius={2} - size={30} - color={Colors.purple30} - selectedIcon={Assets.icons.x} - style={styles.checkbox} - /> - this.setState({value3})} - borderRadius={5} - size={18} - color={Colors.grey10} - iconColor={Colors.green10} - style={styles.checkbox} - /> - + + - Disabled: + Disabled States { {_.times(3, this.renderItem)} {!this.props.scrollReachedProps.isScrollAtEnd && ( - + // @ts-expect-error + )} diff --git a/jest-setup.js b/jest-setup.js index f2635985c9..7066218e2a 100644 --- a/jest-setup.js +++ b/jest-setup.js @@ -10,6 +10,9 @@ jest.mock('@react-native-community/netinfo', () => {}); jest.mock('react-native-reanimated', () => { const reactNativeReanimated = require('react-native-reanimated/mock'); reactNativeReanimated.interpolateColor = jest.fn(v => v); // TODO: See this https://github.com/software-mansion/react-native-reanimated/issues/2749 + reactNativeReanimated.FadeIn = { + duration: jest.fn() + }; return reactNativeReanimated; }); global.__reanimatedWorkletInit = jest.fn(); diff --git a/lib/components/WheelPicker/index.tsx b/lib/components/WheelPicker/index.tsx index 430af68c34..75fe62ea76 100644 --- a/lib/components/WheelPicker/index.tsx +++ b/lib/components/WheelPicker/index.tsx @@ -10,14 +10,6 @@ import {Typography, Colors} from '../../../src/style'; import {PickerPackage, CommunityPickerPackage} from '../../../src/optionalDependencies'; const Picker = PickerPackage?.Picker || CommunityPickerPackage?.Picker || (() => null); -if (!PickerPackage) { - if (CommunityPickerPackage) { - console.warn(`RNUILib Picker will soon migrate to use "@react-native-picker/picker" package instead of '@react-native-community/picker'`); - } else { - console.error(`RNUILib Picker requires installing "@react-native-picker/picker" dependency`); - } -} - const WheelPickerNative = requireNativeComponent('WheelPicker'); export type WheelPickerProps = { diff --git a/package.json b/package.json index 5c9bf837d5..267f63b51c 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "babel-plugin-transform-inline-environment-variables": "^0.0.2", "color": "^3.1.0", "commons-validator-js": "^1.0.237", + "deprecated-react-native-prop-types": "^2.3.0", "hoist-non-react-statics": "^3.0.0", "lodash": "^4.17.21", "memoize-one": "^5.0.5", @@ -57,6 +58,11 @@ "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.10.1", "@babel/runtime": "^7.12.5", + "@formatjs/intl-datetimeformat": "^6.0.3", + "@formatjs/intl-getcanonicallocales": "^2.0.2", + "@formatjs/intl-locale": "^3.0.3", + "@formatjs/intl-numberformat": "^8.0.4", + "@formatjs/intl-pluralrules": "^5.0.3", "@react-native-community/async-storage": "^1.6.2", "@react-native-community/blur": "^3.4.1", "@react-native-community/datetimepicker": "^3.4.6", @@ -129,7 +135,9 @@ "setupFiles": [ "./jest-setup.js" ], - "testMatch": ["**/*.spec.(js|tsx)"], + "testMatch": [ + "**/*.spec.(js|tsx)" + ], "moduleNameMapper": { "^react-native-reanimated$": "/node_modules/react-native-reanimated/src/Animated.js" } diff --git a/src/components/animatedImage/index.js b/src/components/animatedImage/index.js deleted file mode 100644 index 34146d1ecc..0000000000 --- a/src/components/animatedImage/index.js +++ /dev/null @@ -1,73 +0,0 @@ -// TODO: consider unify this component functionality with our Image component -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {Animated, StyleSheet} from 'react-native'; -import View from '../../components/view'; -import Image from '../../components/image'; -import {BaseComponent} from '../../commons'; - -/** - * @description: Image component that fades-in the image with animation once it's loaded - * @extends: Animated.Image - * @gif: https://media.giphy.com/media/l0HU7jj0ivEFyZIA0/giphy.gif - * @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/AnimatedImageScreen.js - */ -class AnimatedImage extends BaseComponent { - static displayName = 'AnimatedImage'; - static propTypes = { - /** - * Image prop Types - */ - ...Image.propTypes, - /** - * Additional spacing styles for the container - */ - containerStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number]), - /** - * Duration for the fade animation when the image is loaded - */ - animationDuration: PropTypes.number, - /** - * A component to render while the image is loading - */ - loader: PropTypes.element - }; - - static defaultProps = { - animationDuration: 300 - }; - - constructor(props) { - super(props); - this.state = {opacity: new Animated.Value(0), isLoading: true}; - } - - onLoad = (...args) => { - this.setState({isLoading: false}, () => { - const animationParams = {toValue: 1, duration: this.props.animationDuration, useNativeDriver: true}; - Animated.timing(this.state.opacity, animationParams).start(); - }); - _.invoke(this.props, 'onLoad', ...args); - }; - - render() { - const {containerStyle, source, loader, style, testID, ...others} = this.props; - return ( - - - {this.state.isLoading && loader && ( - {loader} - )} - - ); - } -} - -export default AnimatedImage; diff --git a/src/components/animatedImage/index.tsx b/src/components/animatedImage/index.tsx new file mode 100644 index 0000000000..c2f2b93db7 --- /dev/null +++ b/src/components/animatedImage/index.tsx @@ -0,0 +1,74 @@ +// TODO: consider unify this component functionality with our Image component +import React, {useState, useCallback} from 'react'; +import {StyleSheet, StyleProp, ViewStyle, NativeSyntheticEvent, ImageLoadEventData} from 'react-native'; +import Animated, {FadeIn} from 'react-native-reanimated'; +import View from '../../components/view'; +import Image, {ImageProps} from '../../components/image'; + +const UIAnimatedImage = Animated.createAnimatedComponent(Image); + +export interface AnimatedImageProps extends ImageProps { + /** + * Additional spacing styles for the container + */ + containerStyle?: StyleProp; + /** + * Duration for the fade animation when the image is loaded + */ + animationDuration?: number; + /** + * A component to render while the image is loading + */ + loader?: React.ReactElement; +} + +/** + * @description: Image component that fades-in the image with animation once it's loaded + * @extends: Animated.Image + * @gif: https://media.giphy.com/media/l0HU7jj0ivEFyZIA0/giphy.gif + * @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/AnimatedImageScreen.js + */ +const AnimatedImage = (props: AnimatedImageProps) => { + const { + containerStyle, + source, + loader, + style, + onLoad: propsOnLoad, + animationDuration = 300, + testID, + ...others + } = props; + const [isLoading, setIsLoading] = useState(true); + + const onLoad = useCallback((event: NativeSyntheticEvent) => { + setIsLoading(false); + propsOnLoad?.(event); + }, + [setIsLoading, propsOnLoad]); + + return ( + + + {isLoading && loader && {loader}} + + ); +}; + +AnimatedImage.displayName = 'AnimatedImage'; + +export default AnimatedImage; + +const styles = StyleSheet.create({ + loader: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center' + } +}); diff --git a/src/components/animatedScanner/index.js b/src/components/animatedScanner/index.js index a5a7c10180..a6fe7116b2 100644 --- a/src/components/animatedScanner/index.js +++ b/src/components/animatedScanner/index.js @@ -1,7 +1,8 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React from 'react'; -import {ViewPropTypes, StyleSheet, Animated} from 'react-native'; +import {StyleSheet, Animated} from 'react-native'; +import {ViewPropTypes} from 'deprecated-react-native-prop-types'; import {Colors} from '../../style'; import {BaseComponent} from '../../commons'; import View from '../../components/view'; diff --git a/src/components/avatar/__tests__/index.spec.js b/src/components/avatar/__tests__/index.spec.js index ffc1226253..70c87dc3e1 100644 --- a/src/components/avatar/__tests__/index.spec.js +++ b/src/components/avatar/__tests__/index.spec.js @@ -1,16 +1,25 @@ +import React from 'react'; +import {StyleSheet} from 'react-native'; +import {render} from '@testing-library/react-native'; import {Avatar} from '../index'; +function verifyBadgeSize(renderTree, expectedSize) { + const badge = renderTree.getByTestId('avatar.onlineBadge'); + const style = StyleSheet.flatten(badge.props.style); + expect(style.width).toEqual(expectedSize); + expect(style.height).toEqual(expectedSize); +} describe('Avatar Badge', () => { describe('badgeProps.size supports number', () => { it('should return 99 as the size number given', () => { - const uut = new Avatar({badgeProps: {size: 99}}); - expect(uut.getBadgeSize()).toEqual(99); + const renderTree = render(); + verifyBadgeSize(renderTree, 99); }); it('should return default when passing 0 as size', () => { - const uut = new Avatar({badgeProps: {size: 0}}); - expect(uut.getBadgeSize()).toEqual(10); + const renderTree = render(); + verifyBadgeSize(renderTree, 10); }); }); }); diff --git a/src/components/avatar/index.tsx b/src/components/avatar/index.tsx index d539df86a7..03997c890b 100644 --- a/src/components/avatar/index.tsx +++ b/src/components/avatar/index.tsx @@ -1,5 +1,5 @@ import _ from 'lodash'; -import React, {PropsWithChildren, PureComponent} from 'react'; +import React, {PropsWithChildren, useEffect, useMemo, forwardRef} from 'react'; import { StyleSheet, ImageSourcePropType, @@ -11,10 +11,8 @@ import { TextStyle, AccessibilityProps } from 'react-native'; -import memoize from 'memoize-one'; import {LogService} from '../../services'; import {Colors, BorderRadiuses} from '../../style'; -import {forwardRef, asBaseComponent} from '../../commons/new'; import {extractAccessibilityProps} from '../../commons/modifiers'; import Badge, {BadgeProps} from '../badge'; import View from '../view'; @@ -23,6 +21,7 @@ import Image, {ImageProps} from '../image'; // @ts-ignore import AnimatedImage from '../animatedImage'; import * as AvatarHelper from '../../helpers/AvatarHelper'; +import {useThemeProps} from '../../hooks'; export enum BadgePosition { TOP_RIGHT = 'TOP_RIGHT', @@ -155,43 +154,59 @@ export type AvatarProps = Pick & testID?: string; }>; +interface Statics { + badgePosition: typeof BadgePosition; +} + /** * @description: Avatar component for displaying user profile images * @extends: TouchableOpacity, Image * @image: https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Avatar/Avarat_1.png?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Avatar/Avarat_2.png?raw=true * @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/AvatarsScreen.tsx */ -class Avatar extends PureComponent { - static displayName = 'Avatar'; - - styles: ReturnType; - - constructor(props: AvatarProps) { - super(props); - - this.styles = createStyles(props); - - if (props.imageSource) { +const Avatar = forwardRef((props: AvatarProps, ref: React.ForwardedRef) => { + const themeProps = useThemeProps(props, 'Avatar'); + const { + imageSource, + source, + size = 50, + labelColor = Colors.$textDefault, + badgeProps = {}, + badgePosition = BadgePosition.TOP_RIGHT, + testID, + ribbonLabel, + customRibbon, + ribbonStyle, + ribbonLabelStyle, + animate = false, + imageStyle, + onImageLoadStart, + onImageLoadEnd, + onImageLoadError, + imageProps, + label, + name, + backgroundColor, + useAutoColors, + autoColorsConfig, + containerStyle, + onPress, + children + } = themeProps; + const {size: _badgeSize, borderWidth: badgeBorderWidth = 0} = badgeProps; + const badgeSize = _badgeSize || DEFAULT_BADGE_SIZE; + + useEffect(() => { + if (imageSource) { LogService.warn('uilib: imageSource prop is deprecated, use source instead.'); } - } + }, [imageSource]); - static defaultProps = { - animate: false, - size: 50, - labelColor: Colors.$textDefault, - badgePosition: BadgePosition.TOP_RIGHT - }; - - static badgePosition = BadgePosition; - - get source() { - return this.props.source || this.props.imageSource; - } - - getContainerStyle(): StyleProp { - const {size} = this.props; + const _source = useMemo(() => { + return source || imageSource; + }, [source, imageSource]); + const _baseContainerStyle: StyleProp = useMemo(() => { return { width: size, height: size, @@ -199,132 +214,48 @@ class Avatar extends PureComponent { justifyContent: 'center', borderRadius: BorderRadiuses.br100 }; - } + }, [size]); - getInitialsContainer(): StyleProp { + const initialsStyle = useMemo(() => { return { - ...StyleSheet.absoluteFillObject, - alignItems: 'center', - justifyContent: 'center', - borderRadius: BorderRadiuses.br100 + color: labelColor, + backgroundColor: 'transparent', + lineHeight: undefined }; - } - - getRibbonStyle(): StyleProp { - const {size} = this.props; + }, [labelColor]); + const _baseRibbonStyle: StyleProp = useMemo(() => { return { position: 'absolute', top: '10%', left: size / 1.7, borderRadius: size / 2 }; - } - - getBadgeBorderWidth = () => _.get(this.props, 'badgeProps.borderWidth', 0); - - getBadgeColor() { - return _.get(this.props, 'badgeProps.backgroundColor'); - } + }, [size]); - getBadgeSize = () => { - return this.props?.badgeProps?.size || DEFAULT_BADGE_SIZE; - }; + const _ribbonStyle: StyleProp = useMemo(() => { + return [_baseRibbonStyle, styles.ribbon, ribbonStyle]; + }, [_baseRibbonStyle, ribbonStyle]); - getBadgePosition = (): object => { - const {size, badgePosition} = this.props; + const _badgePosition: StyleProp = useMemo(() => { const radius = size / 2; const x = Math.sqrt(radius ** 2 * 2); const y = x - radius; - const shift = Math.sqrt(y ** 2 / 2) - (this.getBadgeSize() + this.getBadgeBorderWidth() * 2) / 2; + const shift = Math.sqrt(y ** 2 / 2) - (badgeSize + badgeBorderWidth * 2) / 2; const badgeLocation = _.split(_.toLower(badgePosition), '_', 2); - const badgeAlignment = {position: 'absolute', [badgeLocation[0]]: shift, [badgeLocation[1]]: shift}; + return {position: 'absolute', [badgeLocation[0]]: shift, [badgeLocation[1]]: shift}; + }, [size, badgeBorderWidth, badgeSize, badgePosition]); - return badgeAlignment; - }; - - renderBadge() { - const {testID, badgeProps} = this.props; - - if (badgeProps || this.getBadgeColor()) { - return ( - - ); - } - } - - renderRibbon() { - const {ribbonLabel, ribbonStyle, ribbonLabelStyle, customRibbon} = this.props; - if (ribbonLabel) { - return customRibbon ? ( - {customRibbon} - ) : ( - - - {ribbonLabel} - - - ); - } - } - - renderImage() { - const { - animate, - // @ts-ignore - onImageLoadStart, - onImageLoadEnd, - onImageLoadError, - testID, - imageProps, - imageStyle - } = this.props; - const hasImage = !_.isUndefined(this.source); - const ImageContainer = animate ? AnimatedImage : Image; - - if (hasImage) { - return ( - - ); - } - } - - getText = memoize((label, name) => { + const text = useMemo(() => { let text = label; if (_.isNil(label) && !_.isNil(name)) { text = AvatarHelper.getInitials(name); } return text; - }); + }, [label, name]); - get text() { - const {label, name} = this.props; - return this.getText(label, name); - } - - getBackgroundColor = memoize((text, avatarColors, hashFunction, defaultColor) => { - return AvatarHelper.getBackgroundColor(text, avatarColors, hashFunction, defaultColor); - }); - - get backgroundColor() { - const {backgroundColor, useAutoColors, autoColorsConfig, name} = this.props; + const _backgroundColor = useMemo(() => { if (backgroundColor) { return backgroundColor; } @@ -335,87 +266,136 @@ class Avatar extends PureComponent { defaultColor = Colors.$backgroundNeutralLight } = autoColorsConfig || {}; if (useAutoColors) { - return this.getBackgroundColor(name, avatarColors, hashFunction, defaultColor); + return AvatarHelper.getBackgroundColor(name, avatarColors, hashFunction, defaultColor); } else { return defaultColor; } - } + }, [backgroundColor, autoColorsConfig, useAutoColors, name]); - render() { - const { - labelColor: color, - onPress, - containerStyle, - children, - size, - testID, - //@ts-ignore - forwardedRef - } = this.props; - const Container = onPress ? TouchableOpacity : View; - const hasImage = !_.isUndefined(this.source); + const _containerStyle: StyleProp = useMemo(() => { + return [_baseContainerStyle, containerStyle]; + }, [_baseContainerStyle, containerStyle]); + + const textStyle = useMemo(() => { const fontSizeToImageSizeRatio = 0.32; const fontSize = size * fontSizeToImageSizeRatio; - const text = this.text; - - return ( - - - {!_.isUndefined(text) && ( - - {text} - - )} + return [{fontSize}, initialsStyle, {color: labelColor}]; + }, [size, initialsStyle, labelColor]); + + const textContainerStyle = useMemo(() => { + const hasImage = !_.isUndefined(_source); + return [ + styles.initialsContainer, + {backgroundColor: _backgroundColor}, + hasImage && styles.initialsContainerWithInset + ]; + }, [_source, _backgroundColor]); + + const accessibilityProps = useMemo(() => { + return extractAccessibilityProps(props); + }, [props]); + + const _imageStyle = useMemo(() => { + return [_baseContainerStyle, StyleSheet.absoluteFillObject, imageStyle]; + }, [_baseContainerStyle, imageStyle]); + + const renderImage = () => { + const hasImage = !_.isUndefined(_source); + + if (hasImage) { + const ImageContainer = animate ? AnimatedImage : Image; + return ( + + ); + } + }; + + const renderBadge = () => { + if (!_.isEmpty(badgeProps)) { + return ( + + ); + } + }; + + const renderRibbon = () => { + if (ribbonLabel) { + return ( + + + {ribbonLabel} + - {this.renderImage()} - {this.renderBadge()} - {this.renderRibbon()} - {children} - - ); - } -} + ); + } + }; -function createStyles(props: AvatarProps) { - const {labelColor} = props; - const styles = StyleSheet.create({ - initialsContainerWithInset: { - top: 1, - right: 1, - bottom: 1, - left: 1 - }, - initials: { - color: labelColor, - backgroundColor: 'transparent', - lineHeight: undefined - }, - ribbon: { - backgroundColor: Colors.$backgroundPrimaryHeavy, - paddingHorizontal: 6, - paddingVertical: 3 + const renderCustomRibbon = () => { + if (customRibbon) { + return {customRibbon}; } - }); + }; - return styles; -} + const Container = onPress ? TouchableOpacity : View; + + return ( + + + {!_.isUndefined(text) && ( + + {text} + + )} + + {renderImage()} + {renderBadge()} + {renderCustomRibbon()} + {renderRibbon()} + {children} + + ); +}); + +const styles = StyleSheet.create({ + initialsContainer: { + ...StyleSheet.absoluteFillObject, + alignItems: 'center', + justifyContent: 'center', + borderRadius: BorderRadiuses.br100 + }, + initialsContainerWithInset: { + top: 1, + right: 1, + bottom: 1, + left: 1 + }, + ribbon: { + backgroundColor: Colors.$backgroundPrimaryHeavy, + paddingHorizontal: 6, + paddingVertical: 3 + } +}); +// @ts-expect-error +Avatar.badgePosition = BadgePosition; export {Avatar}; // For tests -export default asBaseComponent(forwardRef(Avatar)); +export default Avatar as typeof Avatar & Statics; diff --git a/src/components/baseInput/index.tsx b/src/components/baseInput/index.tsx index 5943bb566b..166a1e0bbe 100644 --- a/src/components/baseInput/index.tsx +++ b/src/components/baseInput/index.tsx @@ -2,7 +2,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import 'react'; -import {ViewPropTypes, TextInput as RNTextInput} from 'react-native'; +import {ViewPropTypes, TextInputPropTypes} from 'deprecated-react-native-prop-types'; import {Colors, Typography} from '../../style'; import {BaseComponent} from '../../commons'; import Validators from './Validators'; @@ -19,7 +19,7 @@ export default class BaseInput extends BaseComponent { static displayName = 'BaseInput'; static propTypes = { - ...RNTextInput.propTypes, + ...TextInputPropTypes, // ...BaseComponent.propTypes, /** * text color diff --git a/src/components/button/__tests__/__snapshots__/index.spec.js.snap b/src/components/button/__tests__/__snapshots__/index.spec.js.snap index f001c88ec9..380301ef0d 100644 --- a/src/components/button/__tests__/__snapshots__/index.spec.js.snap +++ b/src/components/button/__tests__/__snapshots__/index.spec.js.snap @@ -58,8 +58,8 @@ exports[`Button backgroundColor should return backgroundColor according to modif Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -129,8 +129,8 @@ exports[`Button backgroundColor should return backgroundColor according to prop Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -200,8 +200,8 @@ exports[`Button backgroundColor should return defined theme backgroundColor 1`] Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -276,8 +276,8 @@ exports[`Button backgroundColor should return theme disabled color if button is Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -347,8 +347,8 @@ exports[`Button backgroundColor should return undefined if this button is link 1 Object { "color": "#5A48F5", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -420,8 +420,8 @@ exports[`Button backgroundColor should return undefined if this button is outlin Object { "color": "#5A48F5", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -491,8 +491,8 @@ exports[`Button border radius should return 0 border radius when border radius p Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -562,8 +562,8 @@ exports[`Button border radius should return 0 border radius when button is full Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -633,8 +633,8 @@ exports[`Button border radius should return given border radius when use plain n Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -710,7 +710,7 @@ exports[`Button container size should avoid minWidth limitation if avoidMinWidth "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -786,7 +786,7 @@ exports[`Button container size should have no padding if avoidInnerPadding prop "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -862,7 +862,7 @@ exports[`Button container size should have no padding of button is a link nor mi "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -965,7 +965,7 @@ exports[`Button container size should have no padding of button is an icon butto "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -1037,8 +1037,8 @@ exports[`Button container size should reduce padding by outlineWidth in case of Object { "color": "#5A48F5", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -1108,8 +1108,8 @@ exports[`Button container size should return style for large button 1`] = ` Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -1181,8 +1181,8 @@ exports[`Button container size should return style for large button 2`] = ` Object { "color": "#5A48F5", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -1258,7 +1258,7 @@ exports[`Button container size should return style for medium button 1`] = ` "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -1336,7 +1336,7 @@ exports[`Button container size should return style for medium button 2`] = ` "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -1406,8 +1406,8 @@ exports[`Button container size should return style for round button 1`] = ` Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -1483,7 +1483,7 @@ exports[`Button container size should return style for small button 1`] = ` "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -1561,7 +1561,7 @@ exports[`Button container size should return style for small button 2`] = ` "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -1637,7 +1637,7 @@ exports[`Button container size should return style for xSmall button 1`] = ` "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -1715,7 +1715,7 @@ exports[`Button container size should return style for xSmall button 2`] = ` "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -1787,8 +1787,8 @@ exports[`Button hyperlink should render button as a hyperlink 1`] = ` Object { "color": "#5A48F5", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -2210,8 +2210,8 @@ exports[`Button icon should return the right spacing according to button size wh Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -2287,7 +2287,7 @@ exports[`Button icon should return the right spacing according to button size wh "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -2363,7 +2363,7 @@ exports[`Button icon should return the right spacing according to button size wh "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -2439,7 +2439,7 @@ exports[`Button icon should return the right spacing according to button size wh "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -2509,8 +2509,8 @@ exports[`Button label size should return style for large button 1`] = ` Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -2586,7 +2586,7 @@ exports[`Button label size should return style for medium button 1`] = ` "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -2662,7 +2662,7 @@ exports[`Button label size should return style for small button 1`] = ` "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -2738,7 +2738,7 @@ exports[`Button label size should return style for xSmall button 1`] = ` "fontWeight": "400", "lineHeight": 20, }, - Object {}, + undefined, undefined, ], ] @@ -2808,8 +2808,8 @@ exports[`Button labelColor should return Theme linkColor color for link 1`] = ` Object { "color": "#FFD54E", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -2879,8 +2879,8 @@ exports[`Button labelColor should return color according to color modifier 1`] = Object { "color": "#D52712", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -2950,8 +2950,8 @@ exports[`Button labelColor should return color according to color prop 1`] = ` Object { "color": "green", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -3026,8 +3026,8 @@ exports[`Button labelColor should return disabled text color according to theme Object { "color": "#D2D6D8", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -3097,8 +3097,8 @@ exports[`Button labelColor should return linkColor color for link 1`] = ` Object { "color": "#FDB893", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -3225,8 +3225,8 @@ exports[`Button link should render button as a link 1`] = ` Object { "color": "#5A48F5", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -3298,8 +3298,8 @@ exports[`Button outline should render button with an outline 1`] = ` Object { "color": "#5A48F5", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -3371,8 +3371,8 @@ exports[`Button outline should render button with an outlineColor 1`] = ` Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -3444,8 +3444,8 @@ exports[`Button outline should render button with outline and outlineColor 1`] = Object { "color": "blue", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -3517,8 +3517,8 @@ exports[`Button outline should return custom borderWidth according to outlineWid Object { "color": "#5A48F5", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -3595,8 +3595,8 @@ exports[`Button outline should return disabled color for outline if button is di Object { "color": "#D2D6D8", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -3666,8 +3666,8 @@ exports[`Button outline should return undefined when link is true, even when out Object { "color": "#5A48F5", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] @@ -3737,8 +3737,8 @@ exports[`Button should render default button 1`] = ` Object { "color": "#FFFFFF", }, - Object {}, - Object {}, + undefined, + undefined, undefined, ], ] diff --git a/src/components/button/index.tsx b/src/components/button/index.tsx index 47d1864c23..79cb43cfd7 100644 --- a/src/components/button/index.tsx +++ b/src/components/button/index.tsx @@ -1,10 +1,8 @@ import _ from 'lodash'; import React, {PureComponent} from 'react'; -import {Platform, StyleSheet, LayoutAnimation, LayoutChangeEvent, ImageStyle} from 'react-native'; +import {Platform, StyleSheet, LayoutAnimation, LayoutChangeEvent, ImageStyle, TextStyle} from 'react-native'; import {asBaseComponent, forwardRef, Constants} from '../../commons/new'; import {Colors, Typography, BorderRadiuses} from 'style'; -// @ts-ignore need to migrate to commonsNew -import {modifiers} from 'commons'; import TouchableOpacity from '../touchableOpacity'; import Text from '../text'; import Image from '../image'; @@ -14,8 +12,6 @@ export {ButtonSize, ButtonAnimationDirection, ButtonProps}; import {PADDINGS, HORIZONTAL_PADDINGS, MIN_WIDTH, DEFAULT_SIZE} from './ButtonConstants'; -const {extractColorValue, extractTypographyValue} = modifiers; - class Button extends PureComponent { static displayName = 'Button'; @@ -77,16 +73,15 @@ class Button extends PureComponent { } getBackgroundColor() { - const {backgroundColor: themeBackgroundColor, modifiers} = this.props; - const {disabled, outline, disabledBackgroundColor, backgroundColor: propsBackgroundColor} = this.props; - const {backgroundColor: stateBackgroundColor} = modifiers; + const {disabled, outline, disabledBackgroundColor, backgroundColor, modifiers} = this.props; + const {backgroundColor: modifiersBackgroundColor} = modifiers; if (!outline && !this.isLink) { if (disabled) { return disabledBackgroundColor || Colors.$backgroundDisabled; } - return propsBackgroundColor || stateBackgroundColor || themeBackgroundColor || Colors.$backgroundPrimaryHeavy; + return backgroundColor || modifiersBackgroundColor || Colors.$backgroundPrimaryHeavy; } return 'transparent'; } @@ -100,7 +95,8 @@ class Button extends PureComponent { } getLabelColor() { - const {linkColor, outline, outlineColor, disabled, color: propsColor, backgroundColor} = this.props; + const {linkColor, outline, outlineColor, disabled, color: propsColor, backgroundColor, modifiers} = this.props; + const {color: modifiersColor} = modifiers; const isLink = this.isLink; let color: string | undefined = Colors.$textDefaultLight; @@ -116,27 +112,34 @@ class Button extends PureComponent { return Colors.$textDisabled; } - color = propsColor || extractColorValue(this.props) || color; + color = propsColor || modifiersColor || color; return color; } getLabelSizeStyle() { const size = this.props.size || DEFAULT_SIZE; - const LABEL_STYLE_BY_SIZE: Dictionary = {}; - LABEL_STYLE_BY_SIZE[Button.sizes.xSmall] = {...Typography.text80}; - LABEL_STYLE_BY_SIZE[Button.sizes.small] = {...Typography.text80}; - LABEL_STYLE_BY_SIZE[Button.sizes.medium] = {...Typography.text80}; - LABEL_STYLE_BY_SIZE[Button.sizes.large] = {}; + const LABEL_STYLE_BY_SIZE: Dictionary = {}; + LABEL_STYLE_BY_SIZE[Button.sizes.xSmall] = Typography.text80; + LABEL_STYLE_BY_SIZE[Button.sizes.small] = Typography.text80; + LABEL_STYLE_BY_SIZE[Button.sizes.medium] = Typography.text80; + LABEL_STYLE_BY_SIZE[Button.sizes.large] = undefined; const labelSizeStyle = LABEL_STYLE_BY_SIZE[size]; return labelSizeStyle; } getContainerSizeStyle() { - const {outline, avoidMinWidth, avoidInnerPadding, round} = this.props; - const size = this.props.size || DEFAULT_SIZE; - const outlineWidth = this.props.outlineWidth || 1; + const { + outline, + avoidMinWidth, + avoidInnerPadding, + round, + size: propsSize, + outlineWidth: propsOutlineWidth + } = this.props; + const size = propsSize || DEFAULT_SIZE; + const outlineWidth = propsOutlineWidth || 1; const CONTAINER_STYLE_BY_SIZE: Dictionary = {}; CONTAINER_STYLE_BY_SIZE[Button.sizes.xSmall] = round @@ -217,14 +220,14 @@ class Button extends PureComponent { } getBorderRadiusStyle() { - const {fullWidth, borderRadius: borderRadiusFromProps, modifiers} = this.props; + const {fullWidth, borderRadius: propsBorderRadius, modifiers} = this.props; + const {borderRadius: modifiersBorderRadius} = modifiers; - if (this.isLink || fullWidth || borderRadiusFromProps === 0) { + if (this.isLink || fullWidth || propsBorderRadius === 0) { return {borderRadius: 0}; } - const {borderRadius: borderRadiusFromState} = modifiers; - const borderRadius = borderRadiusFromProps || borderRadiusFromState || BorderRadiuses.br100; + const borderRadius = propsBorderRadius || modifiersBorderRadius || BorderRadiuses.br100; return {borderRadius}; } @@ -238,8 +241,8 @@ class Button extends PureComponent { } getIconStyle() { - const {disabled, iconStyle: propsIconStyle, iconOnRight} = this.props; - const size = this.props.size || DEFAULT_SIZE; + const {disabled, iconStyle: propsIconStyle, iconOnRight, size: propsSize} = this.props; + const size = propsSize || DEFAULT_SIZE; const iconStyle: ImageStyle = { tintColor: this.getLabelColor() }; @@ -294,15 +297,15 @@ class Button extends PureComponent { } renderLabel() { - const {label, labelStyle, labelProps, hyperlink, testID} = this.props; - const typography = extractTypographyValue(this.props); + const {label, labelStyle, labelProps, hyperlink, testID, modifiers} = this.props; const color = this.getLabelColor(); const labelSizeStyle = this.getLabelSizeStyle(); + const {typography} = modifiers; if (label) { return ( (forwardRef(Button)); +const modifiersOptions = { + paddings: true, + margins: true, + borderRadius: true, + backgroundColor: true, + typography: true, + color: true +}; + +export default asBaseComponent(forwardRef(Button), {modifiersOptions}); diff --git a/src/components/carousel/types.tsx b/src/components/carousel/types.tsx index 452b8779ee..d600a8758a 100644 --- a/src/components/carousel/types.tsx +++ b/src/components/carousel/types.tsx @@ -1,5 +1,7 @@ import React from 'react'; -import {ScrollViewProps, StyleProp, ViewStyle, NativeSyntheticEvent, NativeScrollEvent, PointPropType, Animated} from 'react-native'; +import {ScrollViewProps, StyleProp, ViewStyle, NativeSyntheticEvent, NativeScrollEvent, Animated} from 'react-native'; +// @ts-expect-error No typings available for 'deprecated-react-native-prop-types' +import {PointPropType} from 'deprecated-react-native-prop-types'; import {PageControlProps} from '../pageControl'; export enum PageControlPosition { @@ -91,7 +93,7 @@ export interface CarouselProps extends ScrollViewProps { */ horizontal?: boolean | null; /** - * Pass to attach to ScrollView's Animated.event in order to animated elements base on + * Pass to attach to ScrollView's Animated.event in order to animated elements base on * Carousel scroll offset (pass new Animated.ValueXY()) */ animatedScrollOffset?: Animated.ValueXY; diff --git a/src/components/colorPalette/index.tsx b/src/components/colorPalette/index.tsx index 7412912237..6e0ef20679 100644 --- a/src/components/colorPalette/index.tsx +++ b/src/components/colorPalette/index.tsx @@ -1,15 +1,14 @@ import _ from 'lodash'; import memoize from 'memoize-one'; import React, {PureComponent} from 'react'; -import {StyleSheet, UIManager, findNodeHandle, StyleProp, ViewStyle} from 'react-native'; +import {StyleSheet, StyleProp, ViewStyle} from 'react-native'; import {Colors} from '../../style'; -import {Constants, asBaseComponent} from '../../commons/new'; +import {Constants} from '../../commons/new'; import View from '../view'; import Carousel from '../carousel'; import ScrollBar from '../scrollBar'; import PageControl from '../pageControl'; -import ColorSwatch, {SWATCH_SIZE} from '../colorSwatch'; - +import ColorSwatch, {SWATCH_SIZE, SWATCH_MARGIN} from '../colorSwatch'; interface Props { /** @@ -58,10 +57,10 @@ interface Props { export type ColorPaletteProps = Props; interface State { - currentPage: number, - scrollable: boolean, - orientation?: string, - contentWidth?: number + currentPage: number; + scrollable: boolean; + orientation?: string; + contentWidth?: number; } const VERTICAL_PADDING = 16; @@ -100,7 +99,7 @@ class ColorPalette extends PureComponent { carousel: React.RefObject = React.createRef(); scrollBar: React.RefObject = React.createRef(); - itemsRefs?: React.RefObject[] = undefined; + itemsRefs?: any = React.createRef(); selectedColorIndex?: number = undefined; selectedPage?: number = undefined; currentColorsCount?: number = undefined; @@ -113,6 +112,18 @@ class ColorPalette extends PureComponent { componentDidMount() { this.dimensionsChangeListener = Constants.addDimensionsEventListener(this.onOrientationChanged); + _.times(this.props.colors.length, i => { + this.itemsRefs.current[i] = React.createRef(); + }); + this.scrollToSelected(); + } + + componentDidUpdate(prevProps: Props) { + if (this.props.colors !== prevProps.colors) { + const newIndex = this.itemsRefs.current.length; + this.itemsRefs.current[newIndex] = React.createRef(); + this.scrollToSelected(); + } } componentWillUnmount() { @@ -127,7 +138,7 @@ class ColorPalette extends PureComponent { }; initLocalVariables() { - this.itemsRefs = undefined; + this.itemsRefs.current = []; this.selectedColorIndex = undefined; this.selectedPage = undefined; this.currentColorsCount = this.colors.length; @@ -208,38 +219,36 @@ class ColorPalette extends PureComponent { return (margin - 0.001) / 2; } - scrollToSelected() { + scrollToSelected = () => setTimeout(() => { const {scrollable, currentPage} = this.state; - if (scrollable && this.selectedColorIndex !== undefined && this.itemsRefs) { - const childRef = this.itemsRefs[this.selectedColorIndex]; + if (scrollable && this.selectedColorIndex !== undefined && this.itemsRefs.current) { + // The this.selectedColorIndex layout doesn't update on time + // so we use this.selectedColorIndex - 1 and add an offset of 1 Swatch + const childRef: any = this.itemsRefs.current[this.selectedColorIndex - 1]?.current; if (childRef) { - const handle = findNodeHandle(childRef.current); - if (handle) { - //@ts-ignore - UIManager.measureLayoutRelativeToParent(handle, e => { - console.warn(e); - }, - (x: number, _y: number, w: number, _h: number) => { - if (x + w > this.containerWidth) { - this.scrollBar?.current?.scrollTo({ - x: x + w + HORIZONTAL_PADDING - this.containerWidth, - y: 0, - animated: false - }); - } + const childLayout = childRef.getLayout(); + const leftMargins = this.getHorizontalMargins(this.selectedColorIndex).marginLeft; + const childX = childLayout.x + childLayout.width + SWATCH_MARGIN + leftMargins + SWATCH_SIZE; + if (childX > this.containerWidth) { + this.scrollBar?.current?.scrollTo({ + x: childX + HORIZONTAL_PADDING - this.containerWidth, + y: 0, + animated: false }); } + } else if (this.usePagination) { + this.carousel?.current?.goToPage(this.selectedPage || currentPage, false); } - } else if (this.usePagination) { - this.carousel?.current?.goToPage(this.selectedPage || currentPage, false); } - } + }, 100) onContentSizeChange = (contentWidth: number) => { this.setState({ - scrollable: contentWidth > this.containerWidth, contentWidth}); + scrollable: contentWidth > this.containerWidth, + contentWidth + }); }; onChangePage = (index: number) => { @@ -250,12 +259,6 @@ class ColorPalette extends PureComponent { this.props.onValueChange?.(value, options); }; - onLayout = () => { - setTimeout(() => { - this.scrollToSelected(); - }, 0); - }; - getHorizontalMargins = (index: number) => { const isFirst = index === 0; const isOnLeft = isFirst || index % this.itemsPerRow === 0; @@ -292,12 +295,6 @@ class ColorPalette extends PureComponent { } }; - addRefByIndex = (index: number, ref?: any) => { - if (this.itemsRefs && ref) { - this.itemsRefs[index] = ref; - } - } - renderColorSwatch(color: string, index: number) { const {animatedIndex, testID} = this.props; @@ -311,7 +308,7 @@ class ColorPalette extends PureComponent { selected={this.value === color} animated={index === animatedIndex} onPress={this.onValueChange} - ref={r => this.addRefByIndex(index, r)} + ref={this.itemsRefs.current[index]} testID={`${testID}-${color}`} /> ); @@ -319,10 +316,9 @@ class ColorPalette extends PureComponent { renderPalette(props: Props, contentStyle: StyleProp, colors: string[], pageIndex: number) { const {style, ...others} = props; - this.itemsRefs = []; return ( - + {_.map(colors, (color, i) => { if (color === this.value) { this.selectedColorIndex = i; @@ -383,7 +379,7 @@ class ColorPalette extends PureComponent { } } -export default asBaseComponent(ColorPalette); +export default ColorPalette; const styles = StyleSheet.create({ paletteContainer: { diff --git a/src/components/colorPicker/index.tsx b/src/components/colorPicker/index.tsx index 318e653bd7..a3d4d6accd 100644 --- a/src/components/colorPicker/index.tsx +++ b/src/components/colorPicker/index.tsx @@ -1,6 +1,5 @@ import React, {PureComponent} from 'react'; import {StyleSheet, StyleProp, ViewStyle} from 'react-native'; -import {asBaseComponent} from '../../commons/new'; import Assets from '../../assets'; import {Colors} from '../../style'; import View from '../view'; @@ -136,7 +135,7 @@ class ColorPicker extends PureComponent { }; } -export default asBaseComponent(ColorPicker); +export default ColorPicker; const plusButtonContainerWidth = SWATCH_SIZE + 20 + 12; diff --git a/src/components/colorSwatch/index.tsx b/src/components/colorSwatch/index.tsx index 56a31f8e5f..b49298c637 100644 --- a/src/components/colorSwatch/index.tsx +++ b/src/components/colorSwatch/index.tsx @@ -2,7 +2,7 @@ import React, {PureComponent} from 'react'; import {StyleSheet, Animated, Easing, LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native'; import Assets from '../../assets'; import {BorderRadiuses, Colors} from '../../style'; -import {Constants, asBaseComponent} from '../../commons/new'; +import {Constants} from '../../commons/new'; import View from '../view'; import TouchableOpacity from '../touchableOpacity'; import Image from '../image'; @@ -190,7 +190,7 @@ class ColorSwatch extends PureComponent { } } -export default asBaseComponent(ColorSwatch); +export default ColorSwatch; function createStyles({color = Colors.grey30}) { return StyleSheet.create({ diff --git a/src/components/icon/index.tsx b/src/components/icon/index.tsx index 7c96324094..ffc0d73a59 100644 --- a/src/components/icon/index.tsx +++ b/src/components/icon/index.tsx @@ -65,7 +65,7 @@ Icon.displayName = 'Icon'; Icon.defaultProps = { assetGroup: 'icons' }; -export default asBaseComponent(Icon); +export default asBaseComponent(Icon, {modifiersOptions: {margins: true}}); const styles = StyleSheet.create({ rtlFlipped: { diff --git a/src/components/image/image.api.json b/src/components/image/image.api.json index 64aaff104c..faa540bf95 100644 --- a/src/components/image/image.api.json +++ b/src/components/image/image.api.json @@ -42,7 +42,8 @@ {"name": "overlayColor", "type": "string", "description": "Pass a custom color for the overlay"}, {"name": "customOverlayContent", "type": "JSX.Element", "description": "Render an overlay with custom content"}, {"name": "errorSource", "type": "ImageSourcePropType", "description": "Default image source in case of an error"}, - {"name": "imageId", "type": "string", "description": "An imageId that can be used in sourceTransformer logic"} + {"name": "imageId", "type": "string", "description": "An imageId that can be used in sourceTransformer logic"}, + {"name": "useBackgroundContainer", "type": "boolean", "description": "Use a container for the Image, this can solve issues on Android when animation needs to be performed on the same view; i.e. animation related crashes on Android."} ], "snippet": [ "" diff --git a/src/components/image/index.tsx b/src/components/image/index.tsx index 0eb0b3ed6f..8a3741f7c6 100644 --- a/src/components/image/index.tsx +++ b/src/components/image/index.tsx @@ -6,10 +6,11 @@ import { Image as RNImage, ImageProps as RNImageProps, ImageBackground, - ImageSourcePropType, NativeSyntheticEvent, ImageErrorEventData } from 'react-native'; +// @ts-expect-error No typings available for 'deprecated-react-native-prop-types' +import {ImagePropTypes} from 'deprecated-react-native-prop-types'; import { Constants, asBaseComponent, @@ -28,7 +29,7 @@ export type ImageProps = RNImageProps & /** * custom source transform handler for manipulating the image source (great for size control) */ - sourceTransformer?: (props: any) => ImageSourcePropType; + sourceTransformer?: (props: any) => ImagePropTypes.source; /** * if provided image source will be driven from asset name */ @@ -73,7 +74,7 @@ export type ImageProps = RNImageProps & /** * Default image source in case of an error */ - errorSource?: ImageSourcePropType; + errorSource?: ImagePropTypes.source; /** * An imageId that can be used in sourceTransformer logic */ @@ -90,7 +91,7 @@ type Props = ImageProps & ForwardRefInjectedProps & BaseComponentInjectedProps; type State = { error: boolean; - prevSource: ImageSourcePropType; + prevSource: ImagePropTypes.source; }; /** @@ -111,7 +112,7 @@ class Image extends PureComponent { public static overlayTypes = Overlay.overlayTypes; public static overlayIntensityType = Overlay.intensityTypes; - sourceTransformer?: (props: any) => ImageSourcePropType; + sourceTransformer?: (props: any) => ImagePropTypes.source; constructor(props: Props) { super(props); @@ -149,7 +150,7 @@ class Image extends PureComponent { return !!overlayType || this.isGif() || !_.isUndefined(customOverlayContent); } - getVerifiedSource(source?: ImageSourcePropType) { + getVerifiedSource(source?: ImagePropTypes.source) { if (_.get(source, 'uri') === null || _.get(source, 'uri') === '') { // @ts-ignore return {...source, uri: undefined}; diff --git a/src/components/maskedInput/old.js b/src/components/maskedInput/old.js index 8130711654..9403813e35 100644 --- a/src/components/maskedInput/old.js +++ b/src/components/maskedInput/old.js @@ -1,7 +1,8 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React from 'react'; -import {StyleSheet, ViewPropTypes, Keyboard} from 'react-native'; +import {StyleSheet, Keyboard} from 'react-native'; +import {ViewPropTypes} from 'deprecated-react-native-prop-types'; import BaseInput from '../baseInput'; import TextField from '../textField'; import View from '../view'; diff --git a/src/components/picker/PickerDialog.android.js b/src/components/picker/PickerDialog.android.js index a229659a43..0ea1f25b45 100644 --- a/src/components/picker/PickerDialog.android.js +++ b/src/components/picker/PickerDialog.android.js @@ -1,5 +1,6 @@ import React, {Component} from 'react'; -import {StyleSheet, Text as RNText} from 'react-native'; +import {StyleSheet} from 'react-native'; +import {TextPropTypes} from 'deprecated-react-native-prop-types'; import PropTypes from 'prop-types'; import _ from 'lodash'; @@ -17,11 +18,11 @@ class PickerDialog extends Component { /** * select label style */ - selectLabelStyle: RNText.propTypes.style, + selectLabelStyle: TextPropTypes.style, /** * cancel label style */ - cancelLabelStyle: RNText.propTypes.style + cancelLabelStyle: TextPropTypes.style }; state = {}; @@ -59,8 +60,7 @@ class PickerDialog extends Component { ); } - - render() { + render() { const {panDirection, visible, pickerModalProps} = this.props; return ( diff --git a/src/components/skeletonView/index.tsx b/src/components/skeletonView/index.tsx index ff3bb2724c..2a16f7d19f 100644 --- a/src/components/skeletonView/index.tsx +++ b/src/components/skeletonView/index.tsx @@ -1,18 +1,11 @@ import _ from 'lodash'; import React, {Component} from 'react'; import {StyleSheet, Animated, Easing, StyleProp, ViewStyle, AccessibilityProps} from 'react-native'; +import memoize from 'memoize-one'; import {BorderRadiuses, Colors, Dividers, Spacings} from '../../style'; import {createShimmerPlaceholder, LinearGradientPackage} from 'optionalDeps'; import View from '../view'; -import { - Constants, - asBaseComponent, - BaseComponentInjectedProps, - AlignmentModifiers, - PaddingModifiers, - MarginModifiers -} from '../../commons/new'; -import {extractAccessibilityProps} from '../../commons/modifiers'; +import {Constants, AlignmentModifiers, PaddingModifiers, MarginModifiers} from '../../commons/new'; const LinearGradient = LinearGradientPackage?.default; @@ -174,10 +167,7 @@ interface SkeletonState { * @image: https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Skeleton/Skeleton.gif?raw=true * @notes: View requires installing the 'react-native-shimmer-placeholder' and 'react-native-linear-gradient' library */ - -type InternalSkeletonViewProps = SkeletonViewProps & BaseComponentInjectedProps; - -class SkeletonView extends Component { +class SkeletonView extends Component { static displayName = 'SkeletonView'; static defaultProps = { size: Size.SMALL, @@ -190,8 +180,24 @@ class SkeletonView extends Component { static contentTypes: typeof ContentType = ContentType; fadeInAnimation?: Animated.CompositeAnimation; + contentAccessibilityProps?: AccessibilityProps; + listItemAccessibilityProps?: AccessibilityProps; + + setAccessibilityProps(template?: Template) { + const isListItem = template === Template.LIST_ITEM; + const accessibilityProps = { + accessible: true, + accessibilityLabel: isListItem ? 'Loading list item' : 'Loading content' + }; + + if (isListItem) { + this.listItemAccessibilityProps = accessibilityProps; + } else { + this.contentAccessibilityProps = accessibilityProps; + } + } - constructor(props: InternalSkeletonViewProps) { + constructor(props: SkeletonViewProps) { super(props); this.state = { @@ -203,9 +209,11 @@ class SkeletonView extends Component { console.error(`RNUILib SkeletonView's requires installing "react-native-linear-gradient" dependency`); } else if (_.isUndefined(createShimmerPlaceholder)) { console.error(`RNUILib SkeletonView's requires installing "react-native-shimmer-placeholder" dependency`); - } else { + } else if (ShimmerPlaceholder === undefined) { ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient); } + + this.setAccessibilityProps(props.template); } componentDidMount() { @@ -214,7 +222,7 @@ class SkeletonView extends Component { } } - componentDidUpdate(prevProps: InternalSkeletonViewProps) { + componentDidUpdate(prevProps: SkeletonViewProps) { if (this.props.showContent && !prevProps.showContent) { this.fadeInAnimation?.stop(); this.fade(false, this.showChildren); @@ -237,14 +245,6 @@ class SkeletonView extends Component { this.setState({isAnimating: false}); }; - getAccessibilityProps = (accessibilityLabel: any) => { - return { - accessible: true, - accessibilityLabel, - ...extractAccessibilityProps(this.props) - }; - }; - getDefaultSkeletonProps = (input?: {circleOverride: boolean; style: StyleProp}) => { const {circleOverride, style} = input || {}; const {circle, colors, width, height = 0, shimmerStyle} = this.props; @@ -338,11 +338,15 @@ class SkeletonView extends Component { ); }; + getListItemStyle = memoize(style => { + return [styles.listItem, style]; + }); + renderListItemTemplate = () => { const {style, ...others} = this.props; return ( - + {this.renderListItemLeftContent()} {this.renderListItemContentStrips()} @@ -351,7 +355,7 @@ class SkeletonView extends Component { renderTextContentTemplate = () => { return ( - + {this.renderStrip(true, 235, 0)} {this.renderStrip(true, 260, 12)} {this.renderStrip(true, 190, 12)} @@ -377,7 +381,7 @@ class SkeletonView extends Component { const data = showContent && _.isFunction(renderContent) ? renderContent(this.props) : children; return ( - + {showContent && data} @@ -481,7 +485,6 @@ class SkeletonView extends Component { const key = timesKey ? `${timesKey}-${index}` : `${index}`; return ( { } } -export default asBaseComponent(SkeletonView); +export default SkeletonView; const styles = StyleSheet.create({ listItem: { diff --git a/src/components/stepper/index.tsx b/src/components/stepper/index.tsx index 99f3f1c09b..6a551e7f86 100644 --- a/src/components/stepper/index.tsx +++ b/src/components/stepper/index.tsx @@ -54,6 +54,10 @@ interface Props { * Test id for component */ testID?: string; + /** + * useCustomTheme for component. + */ + useCustomTheme?: boolean; } export type StepperProps = Props; @@ -169,7 +173,7 @@ class Stepper extends PureComponent { } renderButton(actionType: ActionType) { - const {disabled, small, testID} = this.props; + const {disabled, small, testID, useCustomTheme} = this.props; const allowStepChange = this.allowStepChange(actionType); const minusButton = small ? minusOutlineSmall : minusOutline; const plusButton = small ? plusOutlineSmall : plusOutline; @@ -182,7 +186,7 @@ class Stepper extends PureComponent { disabled={disabled || !allowStepChange} onPress={() => this.handleStepChange(actionType)} testID={actionType === ActionType.MINUS ? `${testID}.minusStep` : `${testID}.plusStep`} - useCustomTheme + useCustomTheme={useCustomTheme} /> ); } diff --git a/src/components/textArea/index.js b/src/components/textArea/index.js index 30a6eec7ee..54ef599d4d 100644 --- a/src/components/textArea/index.js +++ b/src/components/textArea/index.js @@ -1,5 +1,6 @@ import React from 'react'; import {View, TextInput as RNTextInput, StyleSheet} from 'react-native'; +import {TextInputPropTypes} from 'deprecated-react-native-prop-types'; import BaseInput from '../baseInput'; /** @@ -14,7 +15,7 @@ export default class TextArea extends BaseInput { static displayName = 'TextArea'; static propTypes = { - ...RNTextInput.propTypes, + ...TextInputPropTypes, ...BaseInput.propTypes }; diff --git a/src/components/textField/index.tsx b/src/components/textField/index.tsx index 9983486fc4..246d4f5fc9 100644 --- a/src/components/textField/index.tsx +++ b/src/components/textField/index.tsx @@ -8,7 +8,8 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React from 'react'; -import {StyleSheet, Animated, TextInput as RNTextInput, Image as RNImage} from 'react-native'; +import {StyleSheet, Animated, TextInput as RNTextInput} from 'react-native'; +import {TextInputPropTypes, ImagePropTypes} from 'deprecated-react-native-prop-types'; import memoize from 'memoize-one'; import {Constants} from '../../commons/new'; import {Colors, Typography, Spacings} from '../../style'; @@ -57,7 +58,7 @@ export default class TextField extends BaseInput { static displayName = 'TextField'; static propTypes = { - ...RNTextInput.propTypes, + ...TextInputPropTypes, ...BaseInput.propTypes, /** * should placeholder have floating behavior @@ -174,7 +175,7 @@ export default class TextField extends BaseInput { * Props for the right button {iconSource, onPress, style} */ rightButtonProps: PropTypes.shape({ - iconSource: RNImage.propTypes.source, + iconSource: ImagePropTypes.source, iconColor: PropTypes.string, onPress: PropTypes.func, style: PropTypes.oneOfType([PropTypes.object, PropTypes.number]), @@ -183,7 +184,7 @@ export default class TextField extends BaseInput { /** * Pass to render a leading icon to the TextInput value. Accepts Image props (doesn't work with floatingPlaceholder) */ - leadingIcon: PropTypes.shape(Image.propTypes) + leadingIcon: PropTypes.shape(ImagePropTypes) }; static defaultProps = { diff --git a/src/components/timeline/timeline.api.json b/src/components/timeline/timeline.api.json index 92d5ecaec9..013ee8052e 100644 --- a/src/components/timeline/timeline.api.json +++ b/src/components/timeline/timeline.api.json @@ -4,7 +4,7 @@ "description": "A timeline item to render as part of a timeline list", "extends": ["View"], "extendsLink": ["https://reactnative.dev/docs/view"], - "example": "https://github.com/wix/react-native-ui-lib/blob/master/src/components/timeline/index.tsx", + "example": "https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/TimelineScreen.tsx", "images": [], "props": [ {"name": "backgroundColor", "type": "string", "description": "Background color for the item"}, diff --git a/src/components/touchableOpacity/index.tsx b/src/components/touchableOpacity/index.tsx index 8f8899b073..7e78c3dedd 100644 --- a/src/components/touchableOpacity/index.tsx +++ b/src/components/touchableOpacity/index.tsx @@ -11,7 +11,9 @@ import { import IncubatorTouchableOpacity from '../../incubator/TouchableOpacity'; import {ViewProps} from '../view'; -export interface TouchableOpacityProps extends Omit, ContainerModifiers { +export interface TouchableOpacityProps + extends Omit, + ContainerModifiers { /** * background color for TouchableOpacity */ @@ -42,6 +44,9 @@ export interface TouchableOpacityProps extends Omit void; + onPressIn?: (props?: TouchableOpacityProps) => void | RNTouchableOpacityProps['onPressIn']; + onPressOut?: (props?: TouchableOpacityProps) => void | RNTouchableOpacityProps['onPressOut']; + onLongPress?: (props?: TouchableOpacityProps) => void | RNTouchableOpacityProps['onLongPress']; } type Props = BaseComponentInjectedProps & ForwardRefInjectedProps & TouchableOpacityProps; @@ -78,15 +83,25 @@ class TouchableOpacity extends PureComponent { } onPressIn = (...args: any) => { - this.setState({active: true}); - //@ts-expect-error - this.props.onPressIn?.(...args); + if (this.props.activeBackgroundColor) { + this.setState({active: true}); + } + if (this.props?.customValue) { + this.props.onPressIn?.(this.props); + } else { + this.props.onPressIn?.(...args); + } }; onPressOut = (...args: any) => { - this.setState({active: false}); - //@ts-expect-error - this.props.onPressOut?.(...args); + if (this.props.activeBackgroundColor) { + this.setState({active: false}); + } + if (this.props?.customValue) { + this.props.onPressOut?.(this.props); + } else { + this.props.onPressOut?.(...args); + } }; get backgroundColorStyle() { @@ -124,6 +139,7 @@ class TouchableOpacity extends PureComponent { onPress={this.onPress} onPressIn={this.onPressIn} onPressOut={this.onPressOut} + onLongPress={this.onLongPress} style={[ this.backgroundColorStyle, borderRadius && {borderRadius}, @@ -142,6 +158,10 @@ class TouchableOpacity extends PureComponent { onPress() { this.props.onPress?.(this.props); } + + onLongPress = () => { + this.props.onLongPress?.(this.props); + }; } const modifiersOptions = { diff --git a/src/components/view/index.tsx b/src/components/view/index.tsx index ab249dfb1d..42ef25a50c 100644 --- a/src/components/view/index.tsx +++ b/src/components/view/index.tsx @@ -1,7 +1,6 @@ import {useModifiers, useThemeProps} from 'hooks'; import React, {useEffect, useMemo, useState} from 'react'; import {View as RNView, SafeAreaView, Animated, ViewProps as RNViewProps, StyleProp, ViewStyle} from 'react-native'; -import Reanimated from 'react-native-reanimated'; import {Constants, ContainerModifiers} from '../../commons/new'; export interface ViewProps extends Omit, ThemeComponent, ContainerModifiers { @@ -92,6 +91,7 @@ function View(props: ViewProps, ref: any) { const container = useSafeArea && Constants.isIOS ? SafeAreaView : RNView; if (reanimated) { + const {default: Reanimated}: typeof import('react-native-reanimated') = require('react-native-reanimated'); return Reanimated.createAnimatedComponent(container); } else if (animated) { return Animated.createAnimatedComponent(container); diff --git a/src/components/wheelPickerDialog/index.js b/src/components/wheelPickerDialog/index.js index e43df1235b..caf7b33772 100644 --- a/src/components/wheelPickerDialog/index.js +++ b/src/components/wheelPickerDialog/index.js @@ -1,12 +1,13 @@ import PropTypes from 'prop-types'; import React, {Component} from 'react'; -import {StyleSheet, TouchableOpacity, Text as RNText} from 'react-native'; +import {StyleSheet, TouchableOpacity} from 'react-native'; +import {TextPropTypes} from 'deprecated-react-native-prop-types'; import Colors from '../../style/colors'; import Typography from '../../style/typography'; import View from '../view'; import Text from '../text'; import {WheelPicker} from '../../nativeComponents'; - +import {PickerPackage, CommunityPickerPackage} from '../../../src/optionalDependencies'; export default class WheelPickerDialog extends Component { static displayName = 'IGNORE'; @@ -31,11 +32,11 @@ export default class WheelPickerDialog extends Component { /** * select label style */ - selectLabelStyle: RNText.propTypes.style, + selectLabelStyle: TextPropTypes.style, /** * cancel label style */ - cancelLabelStyle: RNText.propTypes.style, + cancelLabelStyle: TextPropTypes.style, /** * onCancel callback invoked when 'Cancel' button is pressed */ @@ -50,10 +51,22 @@ export default class WheelPickerDialog extends Component { onValueChange: PropTypes.func } - state = { - initialSelectedValue: this.props.selectedValue, - currentValue: false - }; + constructor(props) { + super(props); + this.state = { + initialSelectedValue: props.selectedValue, + currentValue: false + }; + + if (!PickerPackage) { + if (CommunityPickerPackage) { + // eslint-disable-next-line max-len + console.warn(`RNUILib Picker will soon migrate to use "@react-native-picker/picker" package instead of '@react-native-community/picker'`); + } else { + console.error(`RNUILib Picker requires installing "@react-native-picker/picker" dependency`); + } + } + } onValueChange = (value, index) => { if (this.props.onValueChange) { diff --git a/src/helpers/AvatarHelper.ts b/src/helpers/AvatarHelper.ts index 5c8306e832..96b893477d 100644 --- a/src/helpers/AvatarHelper.ts +++ b/src/helpers/AvatarHelper.ts @@ -2,11 +2,13 @@ import _ from 'lodash'; import URL from 'url-parse'; import Colors from '../style/colors'; -export function hashStringToNumber(str: string) { +export function hashStringToNumber(str?: string) { let hash = 5381; - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i); - hash = (hash << 5) + hash + char; /* hash * 33 + c */ // eslint-disable-line + if (str) { + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) + hash + char; /* hash * 33 + c */ // eslint-disable-line + } } return hash; } diff --git a/src/incubator/ChipsInput/index.tsx b/src/incubator/ChipsInput/index.tsx index a5abb37582..7ef79f6f53 100644 --- a/src/incubator/ChipsInput/index.tsx +++ b/src/incubator/ChipsInput/index.tsx @@ -142,6 +142,7 @@ const ChipsInput = forwardRef((props: ChipsInputProps, refToForward: React.Ref { + const toValue = floatOnFocus ? context.isFocused || context.hasValue : context.hasValue; + Animated.timing(animation, { + toValue: Number(toValue), + duration: 200, + useNativeDriver: true + }).start(); + }, [floatOnFocus, context.isFocused, context.hasValue]); + const animatedStyle = useMemo(() => { return { transform: [ @@ -46,14 +56,8 @@ const FloatingPlaceholder = ({ }; }, [placeholderOffset, extraOffset]); - useEffect(() => { - const toValue = floatOnFocus ? context.isFocused || context.hasValue : context.hasValue; - Animated.timing(animation, { - toValue: Number(toValue), - duration: 200, - useNativeDriver: true - }).start(); - }, [floatOnFocus, context.isFocused, context.hasValue]); + const style = useMemo(() => [styles.placeholder, floatingPlaceholderStyle, animatedStyle], + [floatingPlaceholderStyle, animatedStyle]); const onPlaceholderLayout = useCallback((event: LayoutChangeEvent) => { const {width, height} = event.nativeEvent.layout; @@ -70,7 +74,7 @@ const FloatingPlaceholder = ({ diff --git a/src/incubator/TextField/Input.tsx b/src/incubator/TextField/Input.tsx index 2e32f211c9..2551b6abb4 100644 --- a/src/incubator/TextField/Input.tsx +++ b/src/incubator/TextField/Input.tsx @@ -25,7 +25,6 @@ const Input = ({ const placeholder = !context.isFocused ? props.placeholder : hint || props.placeholder; const inputColor = getColorByState(color, context); const placeholderTextColor = getColorByState(props.placeholderTextColor, context); - const value = formatter && !context.isFocused ? formatter(props.value) : props.value; return ( diff --git a/src/incubator/TextField/Label.tsx b/src/incubator/TextField/Label.tsx index b477a027d8..23b4bcd74a 100644 --- a/src/incubator/TextField/Label.tsx +++ b/src/incubator/TextField/Label.tsx @@ -1,4 +1,4 @@ -import React, {useContext} from 'react'; +import React, {useContext, useMemo} from 'react'; import {StyleSheet} from 'react-native'; import {Colors} from '../../style'; import Text from '../../components/text'; @@ -20,12 +20,16 @@ const Label = ({ const forceHidingLabel = !context.isValid && validationMessagePosition === ValidationMessagePosition.TOP; + const style = useMemo(() => { + return [styles.label, labelStyle, floatingPlaceholder && styles.dummyPlaceholder]; + }, [labelStyle, floatingPlaceholder]); + if ((label || floatingPlaceholder) && !forceHidingLabel) { return ( {label} diff --git a/src/incubator/TextField/ValidationMessage.tsx b/src/incubator/TextField/ValidationMessage.tsx index 401c197467..b226878f8d 100644 --- a/src/incubator/TextField/ValidationMessage.tsx +++ b/src/incubator/TextField/ValidationMessage.tsx @@ -1,4 +1,4 @@ -import React, {useContext} from 'react'; +import React, {useContext, useMemo} from 'react'; import {StyleSheet} from 'react-native'; import Text from '../../components/text'; import FieldContext from './FieldContext'; @@ -15,6 +15,8 @@ const ValidationMessage = ({ }: ValidationMessageProps) => { const context = useContext(FieldContext); + const style = useMemo(() => [styles.validationMessage, validationMessageStyle], [validationMessageStyle]); + if (!enableErrors || (!retainSpace && context.isValid)) { return null; } @@ -23,7 +25,7 @@ const ValidationMessage = ({ const showValidationMessage = !context.isValid || (!validate && !!validationMessage); return ( - + {showValidationMessage ? relevantValidationMessage : ''} ); diff --git a/src/incubator/TextField/index.tsx b/src/incubator/TextField/index.tsx index d6950240e5..336bc0003e 100644 --- a/src/incubator/TextField/index.tsx +++ b/src/incubator/TextField/index.tsx @@ -84,6 +84,7 @@ const TextField = (props: InternalTextFieldProps) => { const {margins, paddings, typography, color} = modifiers; const typographyStyle = useMemo(() => omit(typography, 'lineHeight'), [typography]); const colorStyle = useMemo(() => color && {color}, [color]); + const _floatingPlaceholderStyle = useMemo(() => [typographyStyle, floatingPlaceholderStyle], [typographyStyle, floatingPlaceholderStyle]); const fieldStyle = [fieldStyleProp, dynamicFieldStyle?.(context, {preset: props.preset})]; const hidePlaceholder = shouldHidePlaceholder(props, fieldState.isFocused); @@ -118,7 +119,7 @@ const TextField = (props: InternalTextFieldProps) => { {floatingPlaceholder && (