From 533fa815de68a759164b1fc2bdec56e208f831e2 Mon Sep 17 00:00:00 2001 From: Artur Yorsh Date: Fri, 28 Feb 2020 14:57:38 +0300 Subject: [PATCH] BREAKING: refactor List to new api --- src/components/ui/list/list.component.tsx | 67 +--- src/components/ui/list/list.spec.tsx | 321 ++++++++---------- src/components/ui/list/list.spec.tsx.snap | 93 ----- src/components/ui/list/listItem.component.tsx | 262 +++++--------- 4 files changed, 245 insertions(+), 498 deletions(-) delete mode 100644 src/components/ui/list/list.spec.tsx.snap diff --git a/src/components/ui/list/list.component.tsx b/src/components/ui/list/list.component.tsx index 259fbe098..ae0ec8901 100644 --- a/src/components/ui/list/list.component.tsx +++ b/src/components/ui/list/list.component.tsx @@ -8,28 +8,19 @@ import React from 'react'; import { FlatList, FlatListProps, - ListRenderItemInfo, - StyleSheet, - ViewStyle, } from 'react-native'; +import { Overwrite } from 'utility-types'; import { styled, StyledComponentProps, - StyleType, -} from '@kitten/theme'; -import { ListItemProps } from './listItem.component'; +} from '../../theme'; -// this is basically needed to avoid generics in required props -type ItemType = any; -type ListItemElement = React.ReactElement; -type RenderItemProp = (info: ListRenderItemInfo, style: StyleType) => ListItemElement; +type ListStyledProps = Overwrite; -interface ComponentProps { - renderItem: RenderItemProp; -} - -export type ListProps = StyledComponentProps & FlatListProps & ComponentProps; -export type ListElement = React.ReactElement; +export type ListProps = FlatListProps & ListStyledProps; +export type ListElement = React.ReactElement>; export interface BaseScrollParams { animated?: boolean; @@ -66,11 +57,11 @@ export interface ScrollToOffsetParams extends BaseScrollParams { * @example ListInlineStyling * ``` */ -export class ListComponent extends React.Component { +export class ListComponent extends React.Component { static styledComponentName: string = 'List'; - private listRef: React.RefObject> = React.createRef(); + private listRef: React.RefObject> = React.createRef(); public scrollToEnd = (params?: BaseScrollParams): void => { this.listRef.current.scrollToEnd(params); @@ -84,52 +75,22 @@ export class ListComponent extends React.Component { this.listRef.current.scrollToOffset(params); } - private getComponentStyle = (source: StyleType): StyleType => { - return { - container: source, - item: {}, - }; - }; - - private getItemStyle = (source: StyleType, index: number): ViewStyle => { - const { item } = source; - - return item; - }; - - private keyExtractor = (item: ItemType, index: number): string => { + private keyExtractor = (item: ItemT, index: number): string => { return index.toString(); }; - private renderItem = (info: ListRenderItemInfo): ListItemElement => { - const itemStyle: StyleType = this.getItemStyle(this.props.eva.style, info.index); - const itemElement: React.ReactElement = this.props.renderItem(info, itemStyle); - - return React.cloneElement(itemElement, { - style: [itemStyle, styles.item, itemElement.props.style], - index: info.index, - }); - }; - - public render(): React.ReactElement> { - const { style, eva, ...derivedProps } = this.props; - const componentStyle: StyleType = this.getComponentStyle(eva.style); + public render(): React.ReactElement { + const { eva, style, ...flatListProps } = this.props; return ( ); } } -const styles = StyleSheet.create({ - container: {}, - item: {}, -}); - export const List = styled(ListComponent); diff --git a/src/components/ui/list/list.spec.tsx b/src/components/ui/list/list.spec.tsx index 6dd0ed7ad..dc5e434a2 100644 --- a/src/components/ui/list/list.spec.tsx +++ b/src/components/ui/list/list.spec.tsx @@ -1,248 +1,213 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + import React from 'react'; import { Image, ImageProps, - ImageSourcePropType, + Text, TouchableOpacity, } from 'react-native'; import { - render, fireEvent, - shallow, - RenderAPI, + render, } from 'react-native-testing-library'; -import { ReactTestInstance } from 'react-test-renderer'; import { - ApplicationProvider, - ApplicationProviderProps, - StyleType, -} from '@kitten/theme'; + light, + mapping, +} from '@eva-design/eva'; +import { ApplicationProvider } from '../../theme'; import { List, + ListComponent, ListProps, } from './list.component'; import { ListItem, ListItemProps, } from './listItem.component'; -import { - mapping, - theme, -} from '../support/tests'; -const data: any[] = Array(8); +describe('@list-item: component checks', () => { -const Mock = (props?: ListProps): React.ReactElement => { - return ( + const TestListItem = (props?: ListItemProps) => ( - + theme={light}> + ); -}; -const ItemMock = (props?: ListItemProps): React.ReactElement => { - return ( - - ); -}; + it('should render text passed to title prop', () => { + const component = render( + , + ); -describe('@list: component checks', () => { + const title = component.getByText('Test List Item Title'); - it('* renders proper amount of data', () => { - const item = () => { - return ( - - ); - }; - - const component: RenderAPI = render( - , + expect(title).toBeTruthy(); + }); + + it('should render component passed to title prop', () => { + const component = render( + Title as Component}/>, ); - const items: ReactTestInstance[] = component.getAllByType(ListItem); + const titleAsComponent = component.getByText('Title as Component'); - expect(items.length).toEqual(8); + expect(titleAsComponent).toBeTruthy(); }); -}); - -describe('@list-item: template matches snapshot', () => { + it('should render text passed to description prop', () => { + const component = render( + , + ); - const iconSource: ImageSourcePropType = { uri: 'https://akveo.github.io/eva-icons/fill/png/128/star.png' }; + const description = component.getByText('Test List Item Description'); - it('* title', () => { - const item = () => { - return ( - - ); - }; + expect(description).toBeTruthy(); + }); - const component: RenderAPI = render( - , + it('should render component passed to description prop', () => { + const component = render( + Description as Component}/>, ); - const items: ReactTestInstance[] = component.getAllByType(ListItem); - const { output } = shallow(items[0]); + const descriptionAsComponent = component.getByText('Description as Component'); - expect(output).toMatchSnapshot(); + expect(descriptionAsComponent).toBeTruthy(); }); - it('* description', () => { - const item = () => { - return ( - - ); - }; - - const component: RenderAPI = render( - { + const AccessoryLeft = (props): React.ReactElement => ( + + ); + + const AccessoryRight = (props): React.ReactElement => ( + + ); + + const component = render( + , ); - const items: ReactTestInstance[] = component.getAllByType(ListItem); - const { output } = shallow(items[0]); + const [accessoryLeft, accessoryRight] = component.getAllByType(Image); + + expect(accessoryLeft).toBeTruthy(); + expect(accessoryRight).toBeTruthy(); - expect(output).toMatchSnapshot(); + expect(accessoryLeft.props.source.uri).toEqual('https://akveo.github.io/eva-icons/fill/png/128/star.png'); + expect(accessoryRight.props.source.uri).toEqual('https://akveo.github.io/eva-icons/fill/png/128/home.png'); }); - it('* text styles', () => { - const item = () => { - return ( - - ); - }; - - const component: RenderAPI = render( - , + it('should call onPressIn', () => { + const onPressIn = jest.fn(); + + const component = render( + , ); - const items: ReactTestInstance[] = component.getAllByType(ListItem); - const { output } = shallow(items[0]); + fireEvent(component.getByType(TouchableOpacity), 'pressIn'); - expect(output).toMatchSnapshot(); + expect(onPressIn).toHaveBeenCalled(); }); - it('* with icon', () => { - const item = () => { - return ( - - ); - }; - - const icon = (style: StyleType): React.ReactElement => { - return ( - - ); - }; - - const component: RenderAPI = render( - , + it('should call onPressOut', () => { + const onPressOut = jest.fn(); + + const component = render( + , ); - const items: ReactTestInstance[] = component.getAllByType(ListItem); - const { output } = shallow(items[0]); + fireEvent(component.getByType(TouchableOpacity), 'pressOut'); - expect(output).toMatchSnapshot(); + expect(onPressOut).toHaveBeenCalled(); }); +}); - it('* with accessory', () => { - const item = () => { - return ( - - ); - }; - - const accessory = (style: StyleType): React.ReactElement => { - return ( - - ); - }; - - const component: RenderAPI = render( - , +describe('@list: component checks', () => { + + const TestList = React.forwardRef((props: Partial, ref: React.Ref) => + + } + {...props} + /> + , + ); + + it('should render 2 list items', () => { + const component = render( + , ); - const items: ReactTestInstance[] = component.getAllByType(ListItem); - const { output } = shallow(items[0]); + const items = component.getAllByType(ListItem); - expect(output).toMatchSnapshot(); + expect(items.length).toEqual(2); }); -}); + it('should call renderItem once per visible item', () => { + const renderItem = jest.fn(); -describe('@list-item: component checks', () => { + render( + , + ); - it('* emits onPress with correct args', async () => { - const pressIndex: number = 0; - - const onPress = jest.fn((index: number) => { - expect(index).toEqual(pressIndex); - }); - - const item = () => { - return ( - - ); - }; - - const component: RenderAPI = render( - { + const componentRef = React.createRef(); + + render( + , ); - const items: ReactTestInstance[] = component.getAllByType(ListItem); - const touchable: ReactTestInstance = items[pressIndex].findByType(TouchableOpacity); + expect(componentRef.current.scrollToEnd).toBeTruthy(); + componentRef.current.scrollToEnd({}); + }); + + it('should be able to call scrollToIndex with ref', () => { + const componentRef = React.createRef(); + + render( + , + ); + + expect(componentRef.current.scrollToIndex).toBeTruthy(); + componentRef.current.scrollToIndex({ index: 0 }); + }); + + it('should be able to call scrollToIndex with ref', () => { + const componentRef = React.createRef(); + + render( + , + ); - fireEvent.press(touchable); + expect(componentRef.current.scrollToOffset).toBeTruthy(); + componentRef.current.scrollToOffset({ offset: 0 }); }); }); diff --git a/src/components/ui/list/list.spec.tsx.snap b/src/components/ui/list/list.spec.tsx.snap deleted file mode 100644 index b6635abdd..000000000 --- a/src/components/ui/list/list.spec.tsx.snap +++ /dev/null @@ -1,93 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`@list-item: template matches snapshot * description 1`] = ` - -`; - -exports[`@list-item: template matches snapshot * text styles 1`] = ` - -`; - -exports[`@list-item: template matches snapshot * title 1`] = ` - -`; - -exports[`@list-item: template matches snapshot * with accessory 1`] = ` - -`; - -exports[`@list-item: template matches snapshot * with icon 1`] = ` - -`; diff --git a/src/components/ui/list/listItem.component.tsx b/src/components/ui/list/listItem.component.tsx index a156ca51c..5c87764be 100644 --- a/src/components/ui/list/listItem.component.tsx +++ b/src/components/ui/list/listItem.component.tsx @@ -4,65 +4,43 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ - import React from 'react'; import { GestureResponderEvent, - ImageStyle, + ImageProps, Platform, - StyleProp, StyleSheet, - TextStyle, - TouchableOpacity, TouchableOpacityProps, View, ViewProps, } from 'react-native'; +import { Overwrite } from 'utility-types'; +import { + FalsyFC, + FalsyText, + RenderProp, + TouchableWithoutFeedback, +} from '../../devsupport'; import { Interaction, styled, StyledComponentProps, StyleType, -} from '@kitten/theme'; -import { - Text, - TextElement, -} from '../text/text.component'; -import { IconElement } from '../icon/icon.component'; -import { TouchableIndexedProps } from '../support/typings'; -import { isValidString } from '../support/services'; - -type IconProp = (style: StyleType, index: number) => IconElement; -type AccessoryProp = (style: StyleType, index: number) => React.ReactElement; - -interface ListDerivedProps { - index?: number; -} - -interface TemplateBaseProps { - icon?: IconProp; - accessory?: AccessoryProp; -} - -interface TemplateTitleProps extends TemplateBaseProps { - title: string; - description?: string; - titleStyle?: StyleProp; -} - -interface TemplateDescriptionProps extends TemplateBaseProps { - title?: string; - description: string; - descriptionStyle?: StyleProp; -} - -interface CustomContentProps { +} from '../../theme'; +import { TextProps } from '../text/text.component'; + +type ListItemStyledProps = Overwrite; + +export interface ListItemProps extends TouchableOpacityProps, ListItemStyledProps { + title?: RenderProp | React.ReactText; + description?: RenderProp | React.ReactText; + accessoryLeft?: RenderProp>; + accessoryRight?: RenderProp; children?: React.ReactNode; } -type ComponentProps = (TemplateTitleProps | TemplateDescriptionProps | CustomContentProps) & ListDerivedProps; - -export type ListItemProps = StyledComponentProps & TouchableIndexedProps & ComponentProps; export type ListItemElement = React.ReactElement; /** @@ -72,24 +50,29 @@ export type ListItemElement = React.ReactElement; * * @extends React.Component * - * @property {string} title - Determines the title of the ListItem. - * - * @property {string} description - Determines the description of the ListItem's title. + * @property {string | (props: TextProps)} title - A string or a function component + * to render within the item. + * If it is a function, it will be called with props provided by Eva. + * Otherwise, renders a Text styled by Eva. * - * @property {StyleProp} titleStyle - Customizes title style. + * @property {string | (props: TextProps)} description - A string or a function component + * to render within the item. + * If it is a function, it will be called with props provided by Eva. + * Otherwise, renders a Text styled by Eva. * - * @property {StyleProp} descriptionStyle - Customizes description style. + * @property {ReactNode} children - A component to render within the item. + * If provided, `title` and other props will be ignored. * - * @property {ReactNode} children - Determines React Children of the component. + * @property {(props: ImageProps, index: number) => ReactElement} accessoryLeft - A function component + * to render to start of the text. + * Called with props provided by Eva. * - * @property {(style: StyleType, index: number) => ReactElement} accessory - Determines the accessory of the - * component. + * @property {(props: ViewProps, index: number) => ReactElement} accessoryRight - A function component + * to render to end of the text. + * Called with props provided by Eva. * - * @property {(style: ImageStyle, index: number) => ReactElement} icon - Determines the icon of the - * component. - * - * @property {(index: number, event: GestureResponderEvent) => ReactElement} onPress - Emits when - * component is pressed. + * @property {(index: number, event: GestureResponderEvent) => void} onPress - Called when component is pressed. + * Note that `index` is provided. * * @property {TouchableOpacityProps} ...TouchableOpacityProps - Any props applied to TouchableOpacity component. * @@ -103,21 +86,15 @@ export type ListItemElement = React.ReactElement; * * @example ListItemInlineStyling */ -export class ListItemComponent extends React.Component { +export class ListItemComponent extends React.Component { static styledComponentName: string = 'ListItem'; - private onPress = (event: GestureResponderEvent): void => { - if (this.props.onPress) { - this.props.onPress(this.props.index, event); - } - }; - private onPressIn = (event: GestureResponderEvent): void => { this.props.eva.dispatch([Interaction.ACTIVE]); if (this.props.onPressIn) { - this.props.onPressIn(this.props.index, event); + this.props.onPressIn(event); } }; @@ -125,17 +102,11 @@ export class ListItemComponent extends React.Component { this.props.eva.dispatch([]); if (this.props.onPressOut) { - this.props.onPressOut(this.props.index, event); + this.props.onPressOut(event); } }; - private onLongPress = (event: GestureResponderEvent): void => { - if (this.props.onLongPress) { - this.props.onLongPress(this.props.index, event); - } - }; - - private getComponentStyle = (source: StyleType): StyleType => { + private getComponentStyle = (source: StyleType) => { const { iconWidth, iconHeight, @@ -159,7 +130,6 @@ export class ListItemComponent extends React.Component { return { container: containerParameters, - contentContainer: {}, icon: { width: iconWidth, height: iconHeight, @@ -188,114 +158,60 @@ export class ListItemComponent extends React.Component { }; }; - private renderIconElement = (style: ImageStyle): IconElement => { - // @ts-ignore: will be not executed if `icon` prop is provided - const { index, icon } = this.props; - - const iconElement: IconElement = icon(style, index); - - return React.cloneElement(iconElement, { - key: 0, - style: [style, styles.icon, iconElement.props.style], - }); - }; - - private renderContentElement = (style: StyleType): React.ReactElement => { - const { contentContainer, ...contentStyles } = style; - const [titleElement, descriptionElement] = this.renderContentElementChildren(contentStyles); - + private renderTemplateChildren = (props: ListItemProps, evaStyle): React.ReactFragment => { return ( - - {titleElement} - {descriptionElement} - + + + + + + + + ); }; - private renderTitleElement = (style: StyleType): TextElement => { - // @ts-ignore: will be not executed if `title` property is provided - const { title, titleStyle } = this.props; - - return ( - - {title} - - ); - }; - - private renderDescriptionElement = (style: StyleType): TextElement => { - // @ts-ignore: will be not executed if `description` property is provided - const { description, descriptionStyle } = this.props; - - return ( - - {description} - - ); - }; - - private renderAccessoryElement = (style: StyleType): React.ReactElement => { - // @ts-ignore: will be not executed if `accessory` property is provided - const { index, accessory } = this.props; - - const accessoryElement: React.ReactElement = accessory(style, index); - - return React.cloneElement(accessoryElement, { - key: 4, - style: [style, styles.accessory, accessoryElement.props.style], - }); - }; - - private renderContentElementChildren = (style: StyleType): React.ReactNodeArray => { - // @ts-ignore: will be not executed if any of properties below is provided - const { title, description } = this.props; - - return [ - isValidString(title) && this.renderTitleElement(style.title), - isValidString(description) && this.renderDescriptionElement(style.description), - ]; - }; - - private renderTemplateChildren = (style: StyleType): React.ReactNodeArray => { - // @ts-ignore: following props could not be provided - const { icon, title, description, accessory } = this.props; - - return [ - icon && this.renderIconElement(style.icon), - (title || description) && this.renderContentElement(style), - accessory && this.renderAccessoryElement(style.accessory), - ]; - }; - - private renderComponentChildren = (style: StyleType): React.ReactNode => { - const { children } = this.props; - - return children ? children : this.renderTemplateChildren(style); - }; - public render(): React.ReactElement { - const { eva, style, ...derivedProps } = this.props; - const { container, ...componentStyles } = this.getComponentStyle(eva.style); - - const componentChildren: React.ReactNode = this.renderComponentChildren(componentStyles); + const { + eva, + style, + children, + title, + description, + accessoryLeft, + accessoryRight, + ...touchableProps + } = this.props; + + const evaStyle = this.getComponentStyle(eva.style); return ( - - {componentChildren} - + onPressOut={this.onPressOut}> + {children || this.renderTemplateChildren({ + title, + description, + accessoryLeft, + accessoryRight, + }, evaStyle)} + ); } } @@ -308,14 +224,12 @@ const styles = StyleSheet.create({ contentContainer: { flex: 1, }, - icon: {}, title: { textAlign: 'left', }, description: { textAlign: 'left', }, - accessory: {}, }); const webStyles = Platform.OS === 'web' && StyleSheet.create({