Skip to content
This repository has been archived by the owner on Mar 4, 2019. It is now read-only.

Commit

Permalink
Feat: Added TagsInput component
Browse files Browse the repository at this point in the history
Summary: Created TagsInput component. Added tests and Storybook's stories for the TagsInput.
  • Loading branch information
Filip Messa committed Jan 9, 2019
1 parent 0e448b0 commit 33c3f73
Show file tree
Hide file tree
Showing 13 changed files with 585 additions and 2 deletions.
28 changes: 28 additions & 0 deletions ios/storybooknative.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@
5C04BEA7D09C4DC4B05BB661 /* Roboto-MediumItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D3368E9B6BC24CE8AFE7E255 /* Roboto-MediumItalic.ttf */; };
90151CF9607749A28BB60FDD /* Roboto-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0808ED4D12B4404482E493B9 /* Roboto-Regular.ttf */; };
F9A904A78F284EE6989903AF /* orbit-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4EF6E07BA1764ABC8E910490 /* orbit-icons.ttf */; };
D88FDFB084E3435482711AB1 /* orbit-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F88BF6F8550C40ECB6E1F669 /* orbit-icons.ttf */; };
B4921AE35D5A438D987F3FC5 /* Roboto-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7631BF3BAE99477E9CF71881 /* Roboto-Bold.ttf */; };
3F30DC3532D94F7BA82C0D87 /* Roboto-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2C0592B1ED294CE4BD29107A /* Roboto-BoldItalic.ttf */; };
142BF99E75174B93B9550702 /* Roboto-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3842E694390E4196A171A621 /* Roboto-Italic.ttf */; };
BAB939A95F654D879A59C232 /* Roboto-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 8E656E6C94884B0C95E06CF8 /* Roboto-Medium.ttf */; };
0503E680E838498897E40D77 /* Roboto-MediumItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2F13D900B58D425287B594CC /* Roboto-MediumItalic.ttf */; };
76738EB1DFB842C4B0793D06 /* Roboto-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 14819B31DE9A47359FA106A0 /* Roboto-Regular.ttf */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -356,6 +363,13 @@
D3368E9B6BC24CE8AFE7E255 /* Roboto-MediumItalic.ttf */ = {isa = PBXFileReference; name = "Roboto-MediumItalic.ttf"; path = "../fonts/Roboto/Roboto-MediumItalic.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
0808ED4D12B4404482E493B9 /* Roboto-Regular.ttf */ = {isa = PBXFileReference; name = "Roboto-Regular.ttf"; path = "../fonts/Roboto/Roboto-Regular.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
4EF6E07BA1764ABC8E910490 /* orbit-icons.ttf */ = {isa = PBXFileReference; name = "orbit-icons.ttf"; path = "../fonts/orbit-icons.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
F88BF6F8550C40ECB6E1F669 /* orbit-icons.ttf */ = {isa = PBXFileReference; name = "orbit-icons.ttf"; path = "../lib/fonts/orbit-icons.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
7631BF3BAE99477E9CF71881 /* Roboto-Bold.ttf */ = {isa = PBXFileReference; name = "Roboto-Bold.ttf"; path = "../lib/fonts/Roboto/Roboto-Bold.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
2C0592B1ED294CE4BD29107A /* Roboto-BoldItalic.ttf */ = {isa = PBXFileReference; name = "Roboto-BoldItalic.ttf"; path = "../lib/fonts/Roboto/Roboto-BoldItalic.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
3842E694390E4196A171A621 /* Roboto-Italic.ttf */ = {isa = PBXFileReference; name = "Roboto-Italic.ttf"; path = "../lib/fonts/Roboto/Roboto-Italic.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
8E656E6C94884B0C95E06CF8 /* Roboto-Medium.ttf */ = {isa = PBXFileReference; name = "Roboto-Medium.ttf"; path = "../lib/fonts/Roboto/Roboto-Medium.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
2F13D900B58D425287B594CC /* Roboto-MediumItalic.ttf */ = {isa = PBXFileReference; name = "Roboto-MediumItalic.ttf"; path = "../lib/fonts/Roboto/Roboto-MediumItalic.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
14819B31DE9A47359FA106A0 /* Roboto-Regular.ttf */ = {isa = PBXFileReference; name = "Roboto-Regular.ttf"; path = "../lib/fonts/Roboto/Roboto-Regular.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -627,6 +641,13 @@
D3368E9B6BC24CE8AFE7E255 /* Roboto-MediumItalic.ttf */,
0808ED4D12B4404482E493B9 /* Roboto-Regular.ttf */,
4EF6E07BA1764ABC8E910490 /* orbit-icons.ttf */,
F88BF6F8550C40ECB6E1F669 /* orbit-icons.ttf */,
7631BF3BAE99477E9CF71881 /* Roboto-Bold.ttf */,
2C0592B1ED294CE4BD29107A /* Roboto-BoldItalic.ttf */,
3842E694390E4196A171A621 /* Roboto-Italic.ttf */,
8E656E6C94884B0C95E06CF8 /* Roboto-Medium.ttf */,
2F13D900B58D425287B594CC /* Roboto-MediumItalic.ttf */,
14819B31DE9A47359FA106A0 /* Roboto-Regular.ttf */,
);
name = Resources;
sourceTree = "<group>";
Expand Down Expand Up @@ -1085,6 +1106,13 @@
5C04BEA7D09C4DC4B05BB661 /* Roboto-MediumItalic.ttf in Resources */,
90151CF9607749A28BB60FDD /* Roboto-Regular.ttf in Resources */,
F9A904A78F284EE6989903AF /* orbit-icons.ttf in Resources */,
D88FDFB084E3435482711AB1 /* orbit-icons.ttf in Resources */,
B4921AE35D5A438D987F3FC5 /* Roboto-Bold.ttf in Resources */,
3F30DC3532D94F7BA82C0D87 /* Roboto-BoldItalic.ttf in Resources */,
142BF99E75174B93B9550702 /* Roboto-Italic.ttf in Resources */,
BAB939A95F654D879A59C232 /* Roboto-Medium.ttf in Resources */,
0503E680E838498897E40D77 /* Roboto-MediumItalic.ttf in Resources */,
76738EB1DFB842C4B0793D06 /* Roboto-Regular.ttf in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
7 changes: 6 additions & 1 deletion src/Badge/Badge.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ export default function Badge({
children,
type = 'primary',
style,
fontSize,
}: BadgeProps) {
return (
<View style={[styles.wrapper, theme(type).wrapper, style]}>
<Text style={[styles.text, theme(type).text]}>{children}</Text>
<Text
style={[styles.text, theme(type).text, fontSize ? { fontSize } : {}]}
>
{children}
</Text>
</View>
);
}
Expand Down
1 change: 1 addition & 0 deletions src/Badge/BadgeTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type BadgeProps = {|
+children: React.Node,
+type?: BadgeType,
+style?: StylePropType,
+fontSize?: number,
|};

export type BadgeType =
Expand Down
160 changes: 160 additions & 0 deletions src/TagsInput/TagsInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// @flow

import * as React from 'react';
import { View } from 'react-native';
import type { ViewLayoutEvent } from 'react-native/Libraries/Components/View/ViewPropTypes';
import { defaultTokens } from '@kiwicom/orbit-design-tokens';

import IconButton from './components/IconButton';
import TagsContainer from './components/TagsContainer';
import InputField from './components/InputField';
import { StyleSheet } from '../PlatformStyleSheet';
import { Text } from '../Text';

import { MIN_WIDTH } from './constants';

type Props = {|
+label: string,
+onChangeText: (value: string) => void,
+placeholder: string,
+selected?: string[],
+disabled?: boolean,
+fontSize: number,
+onClickDelete?: () => void,
+value?: string,
|};

type State = {
value: string,
containerWidth: ?number,
inputWidth: ?number,
};

class TagInput extends React.Component<Props, State> {
inputRef: any; // @TODO find a way how to type this properly, the issue with mixing react and react-native
static defaultProps = {
fontSize: 16,
};

constructor(props: Props) {
super(props);

this.state = {
value: props.value ?? '',
containerWidth: null,
inputWidth: null,
};
}

componentDidUpdate = (prevProps: Props) => {
const { value } = this.props;
if (value !== prevProps.value) {
this.setState({ value });
}
};

setContainerWidth = (event: ViewLayoutEvent) => {
this.setState({ containerWidth: event.nativeEvent.layout.width });
};

setInputWidth = (event: ViewLayoutEvent) => {
this.setState({ inputWidth: event.nativeEvent.layout.width });
};

setRef = (input: React.Element<any>) => {
this.inputRef = input;
};

getWidth = (width: ?number) => {
const { containerWidth } = this.state;
if (containerWidth && width) {
return containerWidth - width;
}
return undefined;
};

handleChange = (value: string) => {
const { value: oldValue } = this.state;
const { onChangeText } = this.props;
if (value !== oldValue) {
this.setState({ value });
onChangeText(value);
}
};

handleClear = () => {
const { onClickDelete } = this.props;
this.setState({ value: '' });
this.inputRef.focus();

if (onClickDelete) {
onClickDelete();
}
};

renderClearButton = () => {
const { value } = this.state;
const { disabled } = this.props;

return (
<IconButton
style={{ opacity: value.length > 0 ? 1 : 0 }}
onPress={this.handleClear}
disabled={!value.length ?? disabled}
/>
);
};

render() {
const { selected, fontSize, label, placeholder, disabled } = this.props;
const { value, inputWidth } = this.state;

return (
<View style={styles.container}>
<Text weight="bold" style={(styles.label, { fontSize })}>
{label}
</Text>
<View style={styles.fieldContainer} onLayout={this.setContainerWidth}>
<TagsContainer
maxWidth={this.getWidth(inputWidth)}
tags={selected}
fontSize={fontSize}
/>
<InputField
setRef={this.setRef}
fontSize={fontSize}
disabled={disabled}
minWidth={MIN_WIDTH}
maxWidth={this.getWidth(MIN_WIDTH)}
onLayout={this.setInputWidth}
value={value}
placeholder={
selected?.length === 0 && placeholder ? placeholder : null
}
onChangeText={this.handleChange}
/>
</View>
{this.renderClearButton()}
</View>
);
}
}

const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
},
fieldContainer: {
flexDirection: 'row',
flex: 1,
marginStart: 4,
overflow: 'hidden',
},

label: {
color: defaultTokens.paletteInkDark,
},
});

export default TagInput;
55 changes: 55 additions & 0 deletions src/TagsInput/TagsInput.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// @flow

import React from 'react';
import { View } from 'react-native';
import { storiesOf } from '@storybook/react-native';
import { text, withKnobs, array, number } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';

import TagsInput from './TagsInput';

const selectedMock = ['Prague', 'Bratislava', 'London'];

storiesOf('TagsInput', module)
.addDecorator(withKnobs)
.add('Default', () => (
<View style={{ margin: 20 }}>
<TagsInput
onChangeText={action('onChangeText')}
label="From:"
placeholder="Departure point"
/>
</View>
))
.add('with Tags', () => (
<View style={{ margin: 20 }}>
<TagsInput
selected={selectedMock}
onChangeText={action('onChangeText')}
label="From:"
placeholder="Departure point"
/>
</View>
))
.add('Playground', () => {
const tags = array('Tags', ['Prague']);
const fontSize = number('fontSize', 16, {
range: true,
min: 14,
max: 25,
step: 1,
});
const label = text('Label', 'From:');
const placeholder = text('Placeholder', 'Departure point');
return (
<View style={{ margin: 20 }}>
<TagsInput
fontSize={fontSize}
selected={tags}
onChangeText={action('onChangeText')}
label={label}
placeholder={placeholder}
/>
</View>
);
});
66 changes: 66 additions & 0 deletions src/TagsInput/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// @flow

import * as React from 'react';
import { TextInput } from 'react-native';
import { render, fireEvent } from 'react-native-testing-library';
import { TagsInput } from '../index';

describe('TagsInput', () => {
const fontSize = 15;
const label = 'label';
const onChangeText = jest.fn();
const onClickDelete = jest.fn();
const placeholder = 'placeholder';
const tags = ['Prague', 'London'];
const value = 'value';

const { getByType, getByText, getAllByProps, getByTestId } = render(
<TagsInput
selected={tags}
onChangeText={onChangeText}
onClickDelete={onClickDelete}
label={label}
placeholder={placeholder}
fontSize={fontSize}
value={value}
/>
);

it('should contain an input', () => {
expect(getByType(TextInput)).toBeDefined();
});

it('should contain a label', () => {
expect(getByText(label)).toBeDefined();
});

it('should have passed props', () => {
expect(
getAllByProps({
selected: tags,
label,
placeholder,
fontSize,
onChangeText,
value,
})
).toBeDefined();
});

it('should execute onChangeText method', () => {
const input = 'content';
fireEvent.changeText(getByType(TextInput), input);
expect(onChangeText).toHaveBeenCalledWith(input);
expect(onChangeText).toHaveBeenCalledTimes(1);
expect(getByText(input)).toBeDefined();
});

it('should execute onClickDelete method', () => {
fireEvent.press(getByTestId('clear-button'));
expect(onClickDelete).toHaveBeenCalledTimes(1);
});

it('should render Tags', () => {
tags.map(tag => expect(getByText(tag)).toBeDefined());
});
});
Loading

0 comments on commit 33c3f73

Please sign in to comment.