Skip to content

Commit

Permalink
feat(components): refactor AutoCompleteTags (#561)
Browse files Browse the repository at this point in the history
* Refactor AutoCompleteTags

* Update snapshot tests

* Make AutoCompleteTags class

* Update snapshots

* Update src/components/AutoCompleteTags/AutoCompleteTags.spec.js

Co-Authored-By: Connor Bär <connor-baer@users.noreply.github.com>

* Fix test

Co-authored-by: Connor Bär <connor-baer@users.noreply.github.com>
  • Loading branch information
marielakas and connor-baer authored Apr 7, 2020
1 parent 5fbe011 commit 9cbf2e2
Show file tree
Hide file tree
Showing 12 changed files with 449 additions and 68 deletions.
306 changes: 281 additions & 25 deletions src/__snapshots__/storyshots.spec.js.snap

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ label + .circuit-3 {
width: 100%;
font-size: 16px;
line-height: 24px;
margin: 0;
padding-left: calc( 12px + 16px + 12px );
padding-right: calc( 12px + 16px + 12px );
}
Expand Down
105 changes: 66 additions & 39 deletions src/components/AutoCompleteTags/AutoCompleteTags.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,72 +16,99 @@
import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import { remove, includes, isEmpty } from 'lodash/fp';
import { css } from '@emotion/core';
import { isEmpty, map, filter, difference } from 'lodash/fp';

import AutoCompleteInput from '../AutoCompleteInput';
import Tag from '../Tag';

const TagsWrapper = styled('div')`
margin-top: ${props => props.theme.spacings.kilo};
/* this *hack* is to not allow the tags to be visible below the overlay */
padding: 0 1px;
const TagsWrapper = styled('div')(
({ theme }) => css`
display: flex;
flex-direction: column;
align-items: flex-start;
margin-top: ${theme.spacings.kilo};
`
);

span {
display: inline-block;
margin-bottom: ${props => props.theme.spacings.byte};
}
`;
const StyledTag = styled(Tag)(
({ theme }) => css`
margin-top: ${theme.spacings.byte};
&:first-of-type: {
margin-top: 0;
}
`
);

class AutoCompleteTags extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
availableTags: PropTypes.arrayOf(PropTypes.string).isRequired
state = {
selected: this.props.selectedTags
};

state = { tags: [] };
handleAdd = tag => {
const { selected } = this.state;
const newSelected = [...selected, tag];

componentDidUpdate(prevProps, prevState) {
if (prevState.tags.length !== this.state.tags.length) {
this.props.onChange(this.state.tags);
}
}
this.setState({ selected: newSelected });
this.props.onChange(newSelected);
};

handleAddTag = option =>
this.setState(({ tags }) => ({ tags: [...tags, option] }));
handleRemove = tag => {
const { selected } = this.state;
const newSelected = filter(option => option !== tag, selected);

handleRemoveTag = newTag =>
this.setState(({ tags }) => ({
tags: remove(tag => tag === newTag)(tags)
}));
this.setState({ selected: newSelected });
this.props.onChange(newSelected);
};

render() {
const { availableTags } = this.props;
const { tags } = this.state;
const autoCompleteOptions = availableTags.filter(
option => !includes(option, tags)
);
const { availableTags, ...inputProps } = this.props;
const { selected } = this.state;

return (
<Fragment>
<AutoCompleteInput
onChange={this.handleAddTag}
options={autoCompleteOptions}
clearOnSelect
options={difference(availableTags, selected)}
{...inputProps}
onChange={this.handleAdd}
/>
{!isEmpty(tags) && (
<TagsWrapper>
{tags.map(tag => (
<Tag key={tag} onRemove={() => this.handleRemoveTag(tag)}>
{tag}
</Tag>
))}
{!isEmpty(selected) && (
<TagsWrapper data-testid="autocomplete-tags-selected">
{map(
tag => (
<StyledTag key={tag} onRemove={() => this.handleRemove(tag)}>
{tag}
</StyledTag>
),
selected
)}
</TagsWrapper>
)}
</Fragment>
);
}
}

AutoCompleteTags.propTypes = {
/**
* The available options to provided to the AutoCompleteInput.
*/
availableTags: PropTypes.arrayOf(PropTypes.string).isRequired,
/**
* The initially selected options.
*/
selectedTags: PropTypes.arrayOf(PropTypes.string),
/**
* Callback function used to handle adding and removing a tag.
*/
onChange: PropTypes.func.isRequired
};

AutoCompleteTags.defaultProps = {
selectedTags: []
};

/**
* @component
*/
Expand Down
34 changes: 34 additions & 0 deletions src/components/AutoCompleteTags/AutoCompleteTags.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,49 @@
*/

import React from 'react';
import { cleanup, fireEvent } from '@testing-library/react';

import AutoCompleteTags from '.';

const defaultProps = {
availableTags: [
'test1@sumup.com',
'test2@sumup.com',
'test3@sumup.com',
'test4@sumup.com'
],
placeholder: 'Search by email',
selectedTags: ['test1@sumup.com'],
onChange: jest.fn()
};

describe('AutoCompleteTags', () => {
afterEach(cleanup);
/**
* Style tests.
*/
it('should render with default styles', () => {
const actual = create(<AutoCompleteTags availableTags={[]} />);
expect(actual).toMatchSnapshot();
});

it('should display selected tags ', () => {
const { getByTestId, getByText } = render(
<AutoCompleteTags {...defaultProps} />
);
expect(getByTestId('autocomplete-tags-selected')).not.toBeNull();
expect(getByText('test1@sumup.com')).toBeVisible();
});

it('should handle changes in selected tags ', () => {
const { getByTestId, queryByTestId } = render(
<AutoCompleteTags {...defaultProps} />
);
const closeIcon = getByTestId('tag-close');

fireEvent.click(closeIcon);

expect(defaultProps.onChange).toHaveBeenCalledWith([]);
expect(queryByTestId('autocomplete-tags-selected')).toBeNull();
});
});
27 changes: 23 additions & 4 deletions src/components/AutoCompleteTags/AutoCompleteTags.story.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,27 @@ for (let i = 0; i < 10000; i += 1) {
}

export const base = () => (
<AutoCompleteTags
availableTags={randomItems}
onChange={action('handleChange')}
/>
<div style={{ height: '20vh' }}>
<AutoCompleteTags
placeholder="Search by email"
availableTags={randomItems}
onChange={action('handleChange')}
/>
</div>
);

export const selected = () => (
<div style={{ height: '20vh' }}>
<AutoCompleteTags
placeholder="Search by email"
availableTags={[
'test1@sumup.com',
'test2@sumup.com',
'test3@sumup.com',
'test4@sumup.com'
]}
selectedTags={['test4@sumup.com']}
onChange={action('handleChange')}
/>
</div>
);
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ label + .circuit-3 {
width: 100%;
font-size: 16px;
line-height: 24px;
margin: 0;
padding-left: calc( 12px + 16px + 12px );
padding-right: calc( 12px + 16px + 12px );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ label + .circuit-14 {
width: 100%;
font-size: 16px;
line-height: 24px;
margin: 0;
padding-right: calc( 12px + 16px + 12px );
}
Expand Down Expand Up @@ -330,6 +331,7 @@ label + .circuit-14 {
width: 100%;
font-size: 16px;
line-height: 24px;
margin: 0;
padding-right: calc( 12px + 16px + 12px );
}
Expand Down Expand Up @@ -546,6 +548,7 @@ HTMLCollection [
width: 100%;
font-size: 16px;
line-height: 24px;
margin: 0;
padding-right: calc( 12px + 16px + 12px );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ label + .circuit-4 {
width: 100%;
font-size: 16px;
line-height: 24px;
margin: 0;
padding-left: calc( 12px + 16px + 12px );
padding-right: calc( 12px + 16px + 12px );
text-align: right;
Expand Down Expand Up @@ -123,6 +124,7 @@ label + .circuit-4 {
width: 100%;
font-size: 16px;
line-height: 24px;
margin: 0;
padding-left: calc( 12px + 16px + 12px );
padding-right: calc( 12px + 16px + 12px );
text-align: right;
Expand Down Expand Up @@ -217,6 +219,7 @@ label + .circuit-4 {
width: 100%;
font-size: 16px;
line-height: 24px;
margin: 0;
padding-left: calc( 12px + 16px + 12px );
padding-right: calc( 12px + 16px + 12px );
text-align: right;
Expand Down Expand Up @@ -356,6 +359,7 @@ label + .circuit-4 {
width: 100%;
font-size: 16px;
line-height: 24px;
margin: 0;
padding-left: calc( 12px + 16px + 12px );
padding-right: calc( 12px + 16px + 12px );
text-align: right;
Expand Down Expand Up @@ -449,6 +453,7 @@ label + .circuit-4 {
width: 100%;
font-size: 16px;
line-height: 24px;
margin: 0;
padding-left: calc( 12px + 16px + 12px );
padding-right: calc( 12px + 16px + 12px );
text-align: right;
Expand Down Expand Up @@ -566,6 +571,7 @@ label + .circuit-4 {
width: 100%;
font-size: 16px;
line-height: 24px;
margin: 0;
padding-left: calc( 12px + 16px + 12px );
padding-right: calc( 12px + 16px + 12px );
text-align: right;
Expand Down Expand Up @@ -696,6 +702,7 @@ label + .circuit-4 {
width: 100%;
font-size: 16px;
line-height: 24px;
margin: 0;
padding-left: calc( 12px + 16px + 12px );
padding-right: calc( 12px + 16px + 12px );
text-align: right;
Expand Down
1 change: 1 addition & 0 deletions src/components/Input/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const inputBaseStyles = ({ theme }) => css`
transition: border-color ${theme.transitions.default};
width: 100%;
${textMega({ theme })};
margin: 0;
&:focus,
&:active {
Expand Down
Loading

1 comment on commit 9cbf2e2

@vercel
Copy link

@vercel vercel bot commented on 9cbf2e2 Apr 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.