Skip to content

Commit 671a072

Browse files
Timeline - new component (#2027)
* first commit * point styles and props * renderPointContent * Dashed line and ref alignment * testID, fix Android measure * clean-up * Rename 'Timeline' (remove as 'TimelineItem' in src/index and component alias in demo screen) * adding comment link * Fix error 'SyntaxError: Cannot use import statement outside a module' poiting to react-native-dash util.js file * fix ts errors * pr comments - renames, remove TimelineItem, no default line * move to types file * Removing 'renderContent', 'height' and 'targetContainerRef' props * Moving to Point component * Moving to Line component * fix types * fix ts error - ref * Wrapping types with propWithChildren * error * support dynamic target ref * Exporting types * exporting props 2 * change alignment to anchor * animate children update * Demo - timeline under "layouts" * remove animation * remove MeasureMeHOC from Dash. Adjusting Dash defaults * Fix layout issues * Dash - cleaning code * rename const Co-authored-by: Ethan Sharabi <ethans@wix.com>
1 parent 30c7193 commit 671a072

File tree

11 files changed

+620
-0
lines changed

11 files changed

+620
-0
lines changed

demo/src/screens/MenuStructure.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export const navigationData = {
9797
{title: 'Modal', tags: 'modal topbar screen', screen: 'unicorn.screens.ModalScreen'},
9898
{title: 'StateScreen', tags: 'empty state screen', screen: 'unicorn.screens.EmptyStateScreen'},
9999
{title: 'TabController', tags: 'tabbar controller native', screen: 'unicorn.components.TabControllerScreen'},
100+
{title: 'Timeline', tags: 'timeline', screen: 'unicorn.components.TimelineScreen'},
100101
{
101102
title: 'withScrollEnabler',
102103
tags: 'scroll enabled withScrollEnabler',
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import React, {useCallback, useRef, useState} from 'react';
2+
import {StyleSheet, ScrollView} from 'react-native';
3+
import {Assets, Colors, Timeline, View, Card, Text, Button} from 'react-native-ui-lib';
4+
5+
const contents = [
6+
'CURRENT (default) state with dashed line.\nAligned to title',
7+
'SUCCESS state with label.',
8+
'ERROR state with icon.',
9+
'Custom color with icon and outline.\nAligned to title',
10+
'NEXT state with outline.',
11+
'NEXT state with circle point and entry point.'
12+
];
13+
14+
const TimelineScreen = () => {
15+
const [anchorIndex, setAnchorIndex] = useState(0);
16+
const [expand, setExpand] = useState(false);
17+
const anchor = useRef();
18+
19+
const onPress = useCallback(() => {
20+
setAnchorIndex(anchorIndex === 0 ? 1 : 0);
21+
}, [anchorIndex]);
22+
23+
const onPressExpand = useCallback(() => {
24+
setExpand(!expand);
25+
}, [expand]);
26+
27+
const renderContent = (index = 0, anchorRef?: any) => {
28+
return (
29+
<Card padding-page>
30+
<Text text70BO ref={anchorRef}>
31+
Step {index + 1}
32+
</Text>
33+
<View marginT-5 padding-8 bg-grey70 br30>
34+
<Text>{contents[index]}</Text>
35+
<Button marginT-10 size={'small'} label={!expand ? 'Expand' : 'Close'} onPress={onPressExpand}/>
36+
{expand && <View style={{height: 100, marginTop: 10, backgroundColor: 'red'}}/>}
37+
</View>
38+
</Card>
39+
);
40+
};
41+
42+
return (
43+
<>
44+
<View row margin-20 spread>
45+
<Text h1 $textDefault margin-20>
46+
Timeline
47+
</Text>
48+
<Button margin-20 link size={'small'} label={'Change Points Anchor'} onPress={onPress}/>
49+
</View>
50+
<ScrollView contentContainerStyle={styles.container}>
51+
<Timeline
52+
// key={String(expand)}
53+
// topLine={{
54+
// type: Timeline.lineTypes.DASHED,
55+
// entry: true
56+
// }}
57+
bottomLine={{type: Timeline.lineTypes.DASHED}}
58+
// bottomLine={{state: Timeline.states.SUCCESS}}
59+
point={{anchorRef: anchorIndex === 0 ? anchor : undefined}}
60+
>
61+
{renderContent(0, anchorIndex === 0 ? anchor : undefined)}
62+
</Timeline>
63+
<Timeline
64+
topLine={{type: Timeline.lineTypes.DASHED}}
65+
bottomLine={{state: Timeline.states.SUCCESS}}
66+
point={{
67+
state: Timeline.states.SUCCESS,
68+
label: 2,
69+
anchorRef: anchorIndex === 1 ? anchor : undefined
70+
}}
71+
>
72+
{renderContent(1, anchorIndex === 1 ? anchor : undefined)}
73+
</Timeline>
74+
75+
<Timeline
76+
topLine={{state: Timeline.states.SUCCESS}}
77+
bottomLine={{state: Timeline.states.ERROR}}
78+
point={{
79+
state: Timeline.states.ERROR,
80+
icon: Assets.icons.demo.settings
81+
}}
82+
>
83+
{renderContent(2)}
84+
</Timeline>
85+
<Timeline
86+
topLine={{state: Timeline.states.ERROR}}
87+
bottomLine={{
88+
type: Timeline.lineTypes.DASHED,
89+
color: Colors.orange40
90+
}}
91+
point={{
92+
type: Timeline.pointTypes.OUTLINE,
93+
color: Colors.orange40,
94+
icon: Assets.icons.demo.camera
95+
}}
96+
>
97+
{renderContent(3)}
98+
</Timeline>
99+
<Timeline
100+
topLine={{
101+
type: Timeline.lineTypes.DASHED,
102+
color: Colors.orange40
103+
}}
104+
bottomLine={{
105+
state: Timeline.states.NEXT,
106+
type: Timeline.lineTypes.DASHED
107+
}}
108+
point={{
109+
state: Timeline.states.NEXT,
110+
type: Timeline.pointTypes.OUTLINE
111+
}}
112+
>
113+
{renderContent(4)}
114+
</Timeline>
115+
116+
<Timeline
117+
topLine={{
118+
state: Timeline.states.NEXT,
119+
type: Timeline.lineTypes.DASHED
120+
}}
121+
bottomLine={{
122+
state: Timeline.states.NEXT,
123+
entry: true
124+
}}
125+
point={{
126+
state: Timeline.states.NEXT,
127+
type: Timeline.pointTypes.CIRCLE
128+
}}
129+
>
130+
{renderContent(5)}
131+
</Timeline>
132+
</ScrollView>
133+
</>
134+
);
135+
};
136+
137+
export default TimelineScreen;
138+
139+
const styles = StyleSheet.create({
140+
container: {
141+
paddingBottom: 20
142+
}
143+
});

demo/src/screens/componentScreens/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export function registerScreens(registrar) {
6363
registrar('unicorn.lists.BasicListScreen', () => require('./BasicListScreen').default);
6464
registrar('unicorn.lists.ContactsListScreen', () => require('./ContactsListScreen').default);
6565
registrar('unicorn.lists.ConversationListScreen', () => require('./ConversationListScreen').default);
66+
registrar('unicorn.components.TimelineScreen', () => require('./TimelineScreen').default);
6667
// Full Screen components
6768
registrar('unicorn.screens.EmptyStateScreen', () => require('./EmptyStateScreen').default);
6869
registrar('unicorn.components.FaderScreen', () => require('./FaderScreen').default);

src/components/timeline/Dash.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React, {useState, useCallback, useMemo} from 'react';
2+
import {StyleSheet, StyleProp, ViewProps, ViewStyle, LayoutChangeEvent} from 'react-native';
3+
import View from '../view';
4+
import {Colors} from '../../style';
5+
import {Layout} from './types';
6+
7+
interface DashProps extends ViewProps {
8+
vertical?: boolean;
9+
dashGap: number;
10+
dashLength: number;
11+
dashThickness: number;
12+
dashColor?: string;
13+
style?: StyleProp<ViewStyle>;
14+
}
15+
16+
const Dash = (props: DashProps) => {
17+
const {style, vertical, dashGap, dashLength, dashThickness, dashColor} = props;
18+
const [measurements, setMeasurements] = useState<Layout | undefined>();
19+
20+
const onDashLayout = useCallback((event: LayoutChangeEvent) => {
21+
const {x, y, width, height} = event.nativeEvent.layout;
22+
setMeasurements({x, y, width, height});
23+
}, []);
24+
25+
const dashStyle = useMemo(() => {
26+
return {
27+
width: vertical ? dashThickness : dashLength,
28+
height: vertical ? dashLength : dashThickness,
29+
marginRight: vertical ? 0 : dashGap,
30+
marginBottom: vertical ? dashGap : 0,
31+
backgroundColor: dashColor
32+
};
33+
}, [vertical, dashLength, dashThickness, dashGap, dashColor]);
34+
35+
const lineStyle = useMemo(() => {
36+
const directionStyle = vertical ? styles.column : styles.row;
37+
const sizeStyle = {
38+
width: vertical ? dashThickness : dashLength,
39+
height: vertical ? dashLength : dashThickness
40+
};
41+
return [style, directionStyle, sizeStyle];
42+
}, [style, vertical, dashThickness, dashLength]);
43+
44+
const renderDash = () => {
45+
const length = (vertical ? measurements?.height : measurements?.width) || 0;
46+
const n = Math.ceil(length / (dashGap + dashLength));
47+
const dash = [];
48+
49+
for (let i = 0; i < n; i++) {
50+
dash.push(<View key={i} style={dashStyle}/>);
51+
}
52+
53+
return dash;
54+
};
55+
56+
return (
57+
<View onLayout={onDashLayout} style={lineStyle}>
58+
{renderDash()}
59+
</View>
60+
);
61+
};
62+
63+
export default Dash;
64+
Dash.defaultProps = {
65+
dashGap: 6,
66+
dashLength: 6,
67+
dashThickness: 2,
68+
dashColor: Colors.black
69+
};
70+
71+
const styles = StyleSheet.create({
72+
row: {
73+
flexDirection: 'row'
74+
},
75+
column: {
76+
flexDirection: 'column'
77+
}
78+
});

src/components/timeline/Line.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React, {useMemo} from 'react';
2+
import {StyleSheet, ViewStyle} from 'react-native';
3+
import View from '../view';
4+
import Dash from './Dash';
5+
import {LineProps, LineTypes} from './types';
6+
7+
const LINE_WIDTH = 2;
8+
const ENTRY_POINT_HEIGHT = 2;
9+
10+
type LinePropsInternal = LineProps & {
11+
top?: boolean;
12+
style?: ViewStyle;
13+
};
14+
15+
const Line = React.memo((props: LinePropsInternal) => {
16+
const {type, color = 'transparent', entry, top, style} = props;
17+
18+
const solidLineStyle = useMemo(() => {
19+
return [style, styles.solidLine, {backgroundColor: color}];
20+
}, [color, style]);
21+
22+
const dashedLineStyle = useMemo(() => {
23+
return [style, styles.dashedLine];
24+
}, [style]);
25+
26+
const renderStartPoint = () => {
27+
if (entry) {
28+
return <View style={[styles.entryPoint, {backgroundColor: color}]}/>;
29+
}
30+
};
31+
32+
const renderLine = () => {
33+
if (type === LineTypes.DASHED) {
34+
return <Dash vertical dashColor={color} style={dashedLineStyle}/>;
35+
}
36+
return <View style={solidLineStyle}/>;
37+
};
38+
39+
return (
40+
<>
41+
{top && renderStartPoint()}
42+
{renderLine()}
43+
{!top && renderStartPoint()}
44+
</>
45+
);
46+
});
47+
48+
export default Line;
49+
50+
const styles = StyleSheet.create({
51+
entryPoint: {
52+
width: 12,
53+
height: ENTRY_POINT_HEIGHT
54+
},
55+
solidLine: {
56+
width: LINE_WIDTH,
57+
overflow: 'hidden'
58+
},
59+
dashedLine: {
60+
overflow: 'hidden'
61+
}
62+
});

src/components/timeline/Point.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
2+
import React, {useMemo} from 'react';
3+
import {StyleSheet, LayoutChangeEvent} from 'react-native';
4+
import {Colors, Spacings} from '../../style';
5+
import View from '../view';
6+
import Icon from '../icon';
7+
import Text from '../text';
8+
import {PointProps, PointTypes} from './types';
9+
10+
const POINT_SIZE = 12;
11+
const CONTENT_POINT_SIZE = 20;
12+
const POINT_MARGINS = Spacings.s1;
13+
const CIRCLE_WIDTH = 2;
14+
const OUTLINE_WIDTH = 4;
15+
const OUTLINE_TINT = 70;
16+
const ICON_SIZE = 12;
17+
18+
type PointPropsInternal = PointProps & {
19+
onLayout?: (event: LayoutChangeEvent) => void;
20+
};
21+
22+
const Point = (props: PointPropsInternal) => {
23+
const {icon, label, type, color, onLayout} = props;
24+
25+
const pointStyle = useMemo(() => {
26+
const hasOutline = type === PointTypes.OUTLINE;
27+
const isCircle = type === PointTypes.CIRCLE;
28+
const hasContent = label || icon;
29+
30+
const size = hasContent ? CONTENT_POINT_SIZE : POINT_SIZE;
31+
const pointSize = hasOutline ? size + OUTLINE_WIDTH * 2 : size;
32+
const pointSizeStyle = {width: pointSize, height: pointSize, borderRadius: pointSize / 2};
33+
34+
const pointColorStyle = {backgroundColor: color};
35+
36+
const outlineStyle = hasOutline &&
37+
{borderWidth: OUTLINE_WIDTH, borderColor: color && Colors.getColorTint(color, OUTLINE_TINT)};
38+
const circleStyle = !hasContent && isCircle &&
39+
{backgroundColor: Colors.white, borderWidth: CIRCLE_WIDTH, borderColor: color};
40+
41+
return [styles.point, pointSizeStyle, pointColorStyle, outlineStyle, circleStyle];
42+
}, [type, color, label, icon]);
43+
44+
const renderPointContent = () => {
45+
if (icon) {
46+
return <Icon source={icon} size={ICON_SIZE} tintColor={Colors.white}/>;
47+
} else if (label) {
48+
return <Text white subtext>{label}</Text>;
49+
}
50+
};
51+
52+
return (
53+
<View center style={pointStyle} onLayout={onLayout}>
54+
{renderPointContent()}
55+
</View>
56+
);
57+
};
58+
59+
export default Point;
60+
61+
const styles = StyleSheet.create({
62+
point: {
63+
marginVertical: POINT_MARGINS
64+
}
65+
});

0 commit comments

Comments
 (0)