From b2cae6b40e348ca87c2399401536d2994c8216ad Mon Sep 17 00:00:00 2001 From: Artur Bien Date: Sun, 6 Dec 2020 19:04:32 +0100 Subject: [PATCH] feat(list): add list component --- example/src/examples/ListExample.tsx | 65 ++++++++ example/src/examples/index.tsx | 2 + src/Button/Button.tsx | 1 + src/List/List.tsx | 3 + src/List/ListAccordion.tsx | 140 ++++++++++++++++++ src/List/ListItem.tsx | 64 ++++++++ src/List/ListSection.tsx | 46 ++++++ src/List/index.ts | 3 + src/Text/Text.tsx | 5 + .../hooks/useControlledOrUncontrolled.ts | 17 +++ src/index.ts | 1 + 11 files changed, 347 insertions(+) create mode 100644 example/src/examples/ListExample.tsx create mode 100644 src/List/List.tsx create mode 100644 src/List/ListAccordion.tsx create mode 100644 src/List/ListItem.tsx create mode 100644 src/List/ListSection.tsx create mode 100644 src/List/index.ts create mode 100644 src/common/hooks/useControlledOrUncontrolled.ts diff --git a/example/src/examples/ListExample.tsx b/example/src/examples/ListExample.tsx new file mode 100644 index 0000000..d500c1e --- /dev/null +++ b/example/src/examples/ListExample.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { View } from 'react-native'; +import { Panel, List, Hourglass, Cutout } from 'react95-native'; + +const items = [ + 'Control Panel', + 'My Documents', + 'Shared Documents', + 'My Computer', + 'My Network Places', +]; + +const HourglassExample = () => { + const [expanded, setExpanded] = React.useState(true); + + const handleExpand = () => { + setExpanded(currentExpanded => !currentExpanded); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + {items.map(item => ( + console.warn(item)} + left={} + key={item} + /> + ))} + + + + + + ); +}; + +export default HourglassExample; diff --git a/example/src/examples/index.tsx b/example/src/examples/index.tsx index 537c8ba..5bc629c 100644 --- a/example/src/examples/index.tsx +++ b/example/src/examples/index.tsx @@ -16,6 +16,7 @@ import SelectBoxExample from './SelectBoxExample'; import DesktopExample from './DesktopExample'; import TabsExample from './TabsExample'; import HourglassExample from './HourglassExample'; +import ListExample from './ListExample'; export default [ { name: 'ButtonExample', component: ButtonExample, title: 'Button' }, @@ -36,6 +37,7 @@ export default [ { name: 'DesktopExample', component: DesktopExample, title: 'Desktop' }, { name: 'TabsExample', component: TabsExample, title: 'Tabs' }, { name: 'HourglassExample', component: HourglassExample, title: 'Hourglass' }, + { name: 'ListExample', component: ListExample, title: 'List' }, ].sort((a, b) => { /* Sort screens alphabetically */ if (a.title < b.title) return -1; diff --git a/src/Button/Button.tsx b/src/Button/Button.tsx index 2949b28..c5f9845 100644 --- a/src/Button/Button.tsx +++ b/src/Button/Button.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { StyleSheet, + // TODO: use Pressable instead of TouchableHighlight? TouchableHighlight, Text, View, diff --git a/src/List/List.tsx b/src/List/List.tsx new file mode 100644 index 0000000..684f858 --- /dev/null +++ b/src/List/List.tsx @@ -0,0 +1,3 @@ +export { default as Section } from './ListSection'; +export { default as Accordion, Divider } from './ListAccordion'; +export { default as Item } from './ListItem'; diff --git a/src/List/ListAccordion.tsx b/src/List/ListAccordion.tsx new file mode 100644 index 0000000..51ec421 --- /dev/null +++ b/src/List/ListAccordion.tsx @@ -0,0 +1,140 @@ +import * as React from 'react'; +import { + View, + ViewStyle, + StyleSheet, + StyleProp, + TextStyle, + Image, + TouchableHighlight, +} from 'react-native'; +import { Text } from '..'; +import { original as theme } from '../common/themes'; +import { blockSizes } from '../common/styles'; +import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled'; + +// TODO: create LinkButton component that will have link colour that will serve as a clickable List.Item + +type Props = React.ComponentPropsWithRef & { + expanded?: boolean; + defaultExpanded?: boolean; + onPress?: () => void; + title?: string; + subtitle?: string; + children: React.ReactNode; + titleStyle?: StyleProp; + subtitleStyle?: StyleProp; + style?: StyleProp; +}; + +const ListAccordion = ({ + expanded: expandedProp, + defaultExpanded, + onPress, + children, + title, + subtitle, + titleStyle, + subtitleStyle, + style, + ...rest +}: Props) => { + const [expanded, setExpanded] = useControlledOrUncontrolled({ + value: expandedProp, + defaultValue: defaultExpanded, + }); + + const handlePress = () => { + onPress?.(); + + if (expandedProp === undefined) { + setExpanded(currentExpanded => !currentExpanded); + } + }; + + return ( + + + + + {title && ( + + {title} + + )} + {subtitle && ( + + {subtitle} + + )} + + + + + {expanded && {children}} + + ); +}; + +// TODO: do we need 'displayName' ? +// ListAccordion.displayName = 'List.Accordion'; + +const styles = StyleSheet.create({ + wrapper: { + borderWidth: 2, + borderColor: theme.flatLight, + }, + header: { + paddingVertical: 4, + paddingHorizontal: 8, + minHeight: blockSizes.md, + justifyContent: 'space-between', + backgroundColor: theme.flatLight, + alignItems: 'center', + display: 'flex', + flexDirection: 'row', + }, + title: { + // TODO: create separate color variable for this? or should we use theme.materialColor instead? + // color: theme.progress, + color: theme.progress, + }, + subtitle: { + // TODO: make a Text component with standarized font sizes where normal is 16 / small 13 ...etc + fontSize: 13, + }, + expandIcon: { + width: 14, + height: 14, + marginRight: 2, + }, + body: {}, + + divider: { + height: 2, + width: 'auto', + backgroundColor: theme.flatLight, + }, +}); + +export const Divider = () => ; + +export default ListAccordion; diff --git a/src/List/ListItem.tsx b/src/List/ListItem.tsx new file mode 100644 index 0000000..26ff8b7 --- /dev/null +++ b/src/List/ListItem.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { + View, + ViewStyle, + StyleSheet, + StyleProp, + TextStyle, + TouchableOpacity, +} from 'react-native'; +import { Text } from '..'; +import { original as theme } from '../common/themes'; +import { blockSizes } from '../common/styles'; + +type Props = React.ComponentPropsWithRef & { + left?: React.ReactNode; + right?: React.ReactNode; + titleStyle?: StyleProp; + title?: string; + onPress?: () => void; + style?: StyleProp; +}; + +const ListItem = ({ + left, + right, + title, + titleStyle, + style, + onPress, + ...rest +}: Props) => ( + + + + {left && {left}} + {title && {title}} + {right && {right}} + + + +); + +const styles = StyleSheet.create({ + wrapper: {}, + content: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + minHeight: blockSizes.md, + paddingVertical: 4, + }, + title: { + color: theme.progress, + fontSize: 16, + }, + left: { + marginRight: 8, + }, + right: { + marginLeft: 8, + }, +}); + +export default ListItem; diff --git a/src/List/ListSection.tsx b/src/List/ListSection.tsx new file mode 100644 index 0000000..7b0a96e --- /dev/null +++ b/src/List/ListSection.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { + View, + ViewStyle, + StyleSheet, + StyleProp, + TextStyle, +} from 'react-native'; +import { Text } from '..'; + +type Props = React.ComponentPropsWithRef & { + title?: string; + children: React.ReactNode; + titleStyle?: StyleProp; + style?: StyleProp; +}; + +const ListSection = ({ + children, + title, + titleStyle, + style, + ...rest +}: Props) => ( + + {title && ( + + {title} + + )} + {children} + +); + +const styles = StyleSheet.create({ + container: { + padding: 8, + // marginVertical: 8, + }, + title: { + fontSize: 13, + marginVertical: 8, + }, +}); + +export default ListSection; diff --git a/src/List/index.ts b/src/List/index.ts new file mode 100644 index 0000000..d12cafa --- /dev/null +++ b/src/List/index.ts @@ -0,0 +1,3 @@ +import * as List from './List'; + +export default List; diff --git a/src/Text/Text.tsx b/src/Text/Text.tsx index 7a44186..d440e83 100644 --- a/src/Text/Text.tsx +++ b/src/Text/Text.tsx @@ -16,6 +16,7 @@ type Props = React.ComponentProps & { linkUrl?: string | null; disabled?: boolean; secondary?: boolean; + bold?: boolean; }; const Text = ({ @@ -24,6 +25,7 @@ const Text = ({ linkUrl = null, disabled = false, secondary = false, + bold = false, ...rest }: Props) => { const onLinkPress = () => { @@ -37,6 +39,9 @@ const Text = ({ style={[ disabled ? text.disabled : secondary ? text.secondary : text.default, linkUrl ? styles.link : {}, + { + fontWeight: bold ? 'bold' : 'normal', + }, style, ]} onPress={onLinkPress} diff --git a/src/common/hooks/useControlledOrUncontrolled.ts b/src/common/hooks/useControlledOrUncontrolled.ts new file mode 100644 index 0000000..47db90d --- /dev/null +++ b/src/common/hooks/useControlledOrUncontrolled.ts @@ -0,0 +1,17 @@ +import { useState, useCallback } from 'react'; + +type Props = { + value: any; + defaultValue: any; +}; + +export default ({ value, defaultValue }: Props) => { + const isControlled = value !== undefined; + const [controlledValue, setControlledValue] = useState(defaultValue); + const handleChangeIfUncontrolled = useCallback(newValue => { + if (!isControlled) { + setControlledValue(newValue); + } + }, []); + return [isControlled ? value : controlledValue, handleChangeIfUncontrolled]; +}; diff --git a/src/index.ts b/src/index.ts index bf92f37..67f431a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,5 +16,6 @@ export { default as Menu } from './Menu'; export { default as Tabs } from './Tabs'; export { default as ScrollView } from './ScrollView'; export { default as Hourglass } from './Hourglass'; +export { default as List } from './List'; export * from './common/themes';