Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions less/_autocomplete.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.autocomplete-dropdown {
display: block;
position: absolute;
left: inherit;
margin-top: 0;
top: auto;
}
1 change: 1 addition & 0 deletions less/patternfly-react.less
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/**
Patternfly React Specific Extensions
*/
@import 'autocomplete';
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dependencies": {
"breakjs": "^1.0.0",
"classnames": "^2.2.5",
"downshift": "^1.25.0",
"patternfly": "^3.38.0",
"react-bootstrap": "^0.31.5",
"react-bootstrap-switch": "^15.5.3",
Expand Down
7 changes: 7 additions & 0 deletions sass/patternfly-react/_autocomplete.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.autocomplete-dropdown {
display: block;
position: absolute;
left: inherit;
margin-top: 0;
top: auto;
}
1 change: 1 addition & 0 deletions sass/patternfly-react/_patternfly-react.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/**
Patternfly React Partials
*/
@import 'autocomplete';
2 changes: 2 additions & 0 deletions src/common/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ export const propsChanged = (propNames, oldProps, newProps) =>
export const nullValues = obj => selectKeys(obj, Object.keys(obj), () => null);

export const noop = Function.prototype;

export const KEY_CODES = { TAB_KEY: 9, ENTER_KEY: 13 };
65 changes: 65 additions & 0 deletions src/components/AutoComplete/AutoComplete.fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const colors = [
'Red',
'Orange',
'Yellow',
'Green',
'Cyan',
'Blue',
'Indigo',
'Violet'
];

const adjectives = [
'agreeable',
'ambitious',
'calm',
'delightful',
'interesting',
'pleasant',
'simple',
'wonderful'
];

const autoCompleteTerms = colors
.map(color => adjectives.map(adj => `${color} is ${adj}`))
.reduce((terms, colorItems) => terms.concat(colorItems));

const limitWordCount = (searchTerm, i) => {
const wordCount = searchTerm.split(' ').length;
return i
.split(' ')
.slice(0, wordCount)
.join(' ');
};

const removeDuplicates = (item, index, array) => array.indexOf(item) === index;

const includesTerm = (searchTerm, i) =>
!searchTerm || i.toLowerCase().includes(searchTerm.toLowerCase());

const limitResults = (i, index) => index < 10;

const stringsToConfig = item => ({
text: item,
disabled: item.includes('Red')
});

export const getSearchItems = (searchTerm = '') => {
const items = autoCompleteTerms
.map(limitWordCount.bind(null, searchTerm))
.filter(removeDuplicates)
.filter(includesTerm.bind(null, searchTerm))
.filter(limitResults)
.map(stringsToConfig);

if (items.length > 2) {
items.splice(
2,
0,
{ type: 'header', text: 'This is a Header' },
{ type: 'divider' }
);
}

return items;
};
134 changes: 134 additions & 0 deletions src/components/AutoComplete/AutoComplete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, { Component } from 'react';
import Downshift from 'downshift';
import PropTypes from 'prop-types';

import { InputGroup, Button } from '../../index';
import AutoCompleteInput from './AutoCompleteInput';
import AutoCompleteItems from './AutoCompleteItems';
import { KEY_CODES } from '../../common/helpers';
import { getActiveItems } from './helpers';

class AutoComplete extends Component {
constructor(props) {
super(props);

this.state = {
inputValue: ''
};
}

handleStateChange = ({ inputValue, ...rest }) => {
if (typeof inputValue === 'string') {
this.props.onInputUpdate(inputValue);
this.setState({ inputValue });
}
};

render() {
const {
onSearch,
labelText,
onInputUpdate,
items,
actionText,
...rest
} = this.props;

const activeItems = getActiveItems(items);

return (
<Downshift
onStateChange={this.handleStateChange}
defaultHighlightedIndex={0}
selectedItem={this.state.inputValue}
{...rest}
render={({
getInputProps,
getItemProps,
getLabelProps,
isOpen,
inputValue,
highlightedIndex,
getRootProps,
selectedItem,
selectItem
}) => {
const shouldShowItems = isOpen && items.length > 0;
const autoCompleteItemsProps = {
items,
highlightedIndex,
selectedItem,
getItemProps,
activeItems
};

return (
<div>
{labelText && <label {...getLabelProps()}>{labelText}</label>}
<InputGroup>
<AutoCompleteInput
onKeyPress={e => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you move this function to be apart of the class like you did with handleStateChange?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately we don't have the render prop props (say that three times fast :) in a class function so we'd have to do some pass-throughs which I'm not a fan of.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cool, I understand your point now, it will actually be really messy...

switch (e.keyCode) {
case KEY_CODES.TAB_KEY:
if (isOpen && activeItems[highlightedIndex]) {
selectItem(activeItems[highlightedIndex]);
e.preventDefault();
}

break;

case KEY_CODES.ENTER_KEY:
if (!isOpen || !activeItems[highlightedIndex]) {
onSearch(this.state.inputValue);
e.preventDefault();
}

break;

default:
break;
}
}}
passedProps={getInputProps()}
/>
<InputGroup.Button>
<Button onClick={() => onSearch(inputValue)}>
{actionText}
</Button>
</InputGroup.Button>
</InputGroup>

{shouldShowItems && (
<AutoCompleteItems {...autoCompleteItemsProps} />
)}
</div>
);
}}
/>
);
}
}

AutoComplete.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
/* text to display in MenuItem */
text: PropTypes.string,
/* item can be a header or divider or undefined for regular item */
type: PropTypes.oneOf(['header', 'divider']),
/* optionally disable a regular item */
disabled: PropTypes.bool
})
).isRequired,
onInputUpdate: PropTypes.func.isRequired,
onSearch: PropTypes.func.isRequired,
labelText: PropTypes.string,
actionText: PropTypes.string
};

AutoComplete.defaultProps = {
labelText: '',
actionText: 'Search'
};

export default AutoComplete;
31 changes: 31 additions & 0 deletions src/components/AutoComplete/AutoComplete.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withInfo } from '@storybook/addon-info';
import { inlineTemplate } from '../../../storybook/decorators/storyTemplates';

import AutoComplete from './AutoComplete';
import {
MockAutoComplete,
basicExampleSource
} from './__mocks__/mockAutoComplete';

storiesOf('AutoComplete', module).add(
'AutoComplete',
withInfo({
source: false,
propTables: [AutoComplete],
propTablesExclude: [MockAutoComplete],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@priley86 This approach can fix some problems we are facing with the storybook?

@danseethaler 👍

@gilad215 /cc

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍 yes - that is the suggestion right now... nice job.

text: (
<div>
<h1>Story Source</h1>
<pre>{basicExampleSource}</pre>
</div>
)
})(() => {
let story = <MockAutoComplete />;
return inlineTemplate({
title: 'AutoComplete Example',
story
});
})
);
42 changes: 42 additions & 0 deletions src/components/AutoComplete/AutoComplete.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import renderer from 'react-test-renderer';

import AutoComplete from './AutoComplete';
import { getActiveItems } from './helpers';
import AutoCompleteItems from './AutoCompleteItems';
import { getSearchItems } from './AutoComplete.fixtures';

describe('Autocomplete component', () => {
it('should render input but not items on initial render', () => {
const component = renderer.create(
<AutoComplete
items={getSearchItems()}
onInputUpdate={() => {}}
onSearch={() => {}}
/>
);

const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

it('renderItems should render the items', () => {
const items = getSearchItems();
const activeItems = getActiveItems(items);

const component = renderer.create(
<AutoCompleteItems
{...{
items,
activeItems,
highlightedIndex: items.indexOf(activeItems[0]),
selectedItem: '',
getItemProps: () => {}
}}
/>
);

const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
36 changes: 36 additions & 0 deletions src/components/AutoComplete/AutoCompleteInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormControl } from '../../index';

class AutoCompleteInput extends Component {
componentDidMount() {
this.ref && this.ref.addEventListener('keydown', this.handleKeyPress);
}

componentWillUnmount() {
this.ref && this.ref.removeEventListener('keydown', this.handleKeyPress);
}

handleKeyPress = e => {
this.props.onKeyPress(e);
};

render() {
return (
<FormControl
inputRef={ref => {
this.ref = ref;
}}
type="text"
{...this.props.passedProps}
/>
);
}
}

AutoCompleteInput.propTypes = {
passedProps: PropTypes.object.isRequired,
onKeyPress: PropTypes.func.isRequired
};

export default AutoCompleteInput;
Loading