Skip to content

Commit

Permalink
feat(list): add list component
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur Bien committed Dec 6, 2020
1 parent fa8b762 commit b2cae6b
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 0 deletions.
65 changes: 65 additions & 0 deletions example/src/examples/ListExample.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Panel style={{ flex: 1, padding: 8, marginBottom: 44 }}>
<Cutout style={[{ flex: 1, backgroundColor: 'white' }]}>
<View style={[{ padding: 20 }]}>
<List.Accordion
title='Uncontrolled'
subtitle='subtitle'
defaultExpanded
>
<List.Section title='Groceries'>
<List.Item title='Vegetables' />
<List.Item title='Bread' />
<List.Item title='Meat' />
</List.Section>
<List.Divider />
<List.Section title='Other'>
<List.Item title='Accesories' />
<List.Item title='Electronics' />
<List.Item title='Art' />
</List.Section>
</List.Accordion>
</View>
<View style={[{ padding: 20 }]}>
<List.Accordion
title='Controlled'
expanded={expanded}
onPress={handleExpand}
>
<List.Section title='Groceries'>
{items.map(item => (
<List.Item
title={item}
onPress={() => console.warn(item)}
left={<Hourglass />}
key={item}
/>
))}
</List.Section>
</List.Accordion>
</View>
</Cutout>
</Panel>
);
};

export default HourglassExample;
2 changes: 2 additions & 0 deletions example/src/examples/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import {
StyleSheet,
// TODO: use Pressable instead of TouchableHighlight?
TouchableHighlight,
Text,
View,
Expand Down
3 changes: 3 additions & 0 deletions src/List/List.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as Section } from './ListSection';
export { default as Accordion, Divider } from './ListAccordion';
export { default as Item } from './ListItem';
140 changes: 140 additions & 0 deletions src/List/ListAccordion.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof View> & {
expanded?: boolean;
defaultExpanded?: boolean;
onPress?: () => void;
title?: string;
subtitle?: string;
children: React.ReactNode;
titleStyle?: StyleProp<TextStyle>;
subtitleStyle?: StyleProp<TextStyle>;
style?: StyleProp<ViewStyle>;
};

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 (
<View {...rest} style={[styles.wrapper, style]}>
<TouchableHighlight onPress={handlePress} accessibilityRole='button'>
<View style={[styles.header]} pointerEvents='none'>
<View>
{title && (
<Text bold style={[styles.title, titleStyle]}>
{title}
</Text>
)}
{subtitle && (
<Text secondary style={[styles.subtitle, subtitleStyle]}>
{subtitle}
</Text>
)}
</View>
<Image
style={[
styles.expandIcon,
{
transform: [
{
rotate: expanded ? '0deg' : '180deg',
},
{
translateY: -2,
},
],
},
]}
source={{
uri:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAM0lEQVQoU2NkIAIwoqthY2j5/4uhBkUchQNSANOErBCuCKYAJInMBmkCK0IXRBcbjG4CANI8JAqzEEN5AAAAAElFTkSuQmCC',
}}
/>
</View>
</TouchableHighlight>
{expanded && <View style={[styles.body]}>{children}</View>}
</View>
);
};

// 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 = () => <View style={[styles.divider]} />;

export default ListAccordion;
64 changes: 64 additions & 0 deletions src/List/ListItem.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof View> & {
left?: React.ReactNode;
right?: React.ReactNode;
titleStyle?: StyleProp<TextStyle>;
title?: string;
onPress?: () => void;
style?: StyleProp<ViewStyle>;
};

const ListItem = ({
left,
right,
title,
titleStyle,
style,
onPress,
...rest
}: Props) => (
<View style={[styles.wrapper, style]} {...rest}>
<TouchableOpacity onPress={onPress} accessibilityRole='button'>
<View style={[styles.content]} pointerEvents='none'>
{left && <View style={[styles.left]}>{left}</View>}
{title && <Text style={[styles.title, titleStyle]}>{title}</Text>}
{right && <View style={[styles.right]}>{right}</View>}
</View>
</TouchableOpacity>
</View>
);

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;
46 changes: 46 additions & 0 deletions src/List/ListSection.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof View> & {
title?: string;
children: React.ReactNode;
titleStyle?: StyleProp<TextStyle>;
style?: StyleProp<ViewStyle>;
};

const ListSection = ({
children,
title,
titleStyle,
style,
...rest
}: Props) => (
<View {...rest} style={[styles.container, style]}>
{title && (
<Text bold secondary style={[styles.title, titleStyle]}>
{title}
</Text>
)}
{children}
</View>
);

const styles = StyleSheet.create({
container: {
padding: 8,
// marginVertical: 8,
},
title: {
fontSize: 13,
marginVertical: 8,
},
});

export default ListSection;
3 changes: 3 additions & 0 deletions src/List/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as List from './List';

export default List;
5 changes: 5 additions & 0 deletions src/Text/Text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Props = React.ComponentProps<typeof NativeText> & {
linkUrl?: string | null;
disabled?: boolean;
secondary?: boolean;
bold?: boolean;
};

const Text = ({
Expand All @@ -24,6 +25,7 @@ const Text = ({
linkUrl = null,
disabled = false,
secondary = false,
bold = false,
...rest
}: Props) => {
const onLinkPress = () => {
Expand All @@ -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}
Expand Down
17 changes: 17 additions & 0 deletions src/common/hooks/useControlledOrUncontrolled.ts
Original file line number Diff line number Diff line change
@@ -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];
};
Loading

0 comments on commit b2cae6b

Please sign in to comment.