Skip to content

Commit

Permalink
No references (#25)
Browse files Browse the repository at this point in the history
* No more references between components

Parent components are no longer explicitly calling methods of their
sub-components. This requires an API change, where the `onSelect` moves
up from the TypeaheadResult to the Typeahead.

* Test update
  • Loading branch information
smfoote authored Feb 21, 2018
1 parent e6830cf commit 4b256dc
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 94 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ The `Typeahead` component wraps the other, more functional component. It's purpo

|Name|Required|Type|Default Value|Description|
|----|--------|----|-----------|
|onSelect|required|function|N/A|This function is called when a TypeaheadResult is selected. It has one argument, which is the `value` prop of the selected TypeaheadResult|
|onDismiss|optional|function|No-op function|This function is called when the typeahead is dismissed when the user presses the escape key|
|onBlur|optional|function|No-op function|This function is called when the entire typeahead component is blurred. If you want to listen to blurs on the input, this is the place to do it|

Expand Down Expand Up @@ -65,7 +66,7 @@ This component, which must be a direct child of the `TypeaheadResultsList` compo

|Name|Required|Type|Default Value|Description|
|----|--------|----|-----------|
|onSelect|required|function|N/A|This function will be called when the typeahead is selected, whether by click or by keyboard interaction.|
|value|required|any|N/A|The value of the result item. This value will be passed to the `Typeahead`'s `onSelect` prop|
|onHighlight|optional|function|No-op function|This function will be called when the typeahead is highlighted through keyboard interaction|

## Examples
Expand Down Expand Up @@ -113,15 +114,15 @@ class Demo extends Component {
const { selectedResult } = this.state;

return [
<Typeahead key="typeahead">
<Typeahead onSelect={result => this.resultSelected(result)} key="typeahead">
<TypeaheadInput
value={this.state.taValue}
onChange={(evt)=> this.typeaheadInputChange(evt)}
/>
<TypeaheadResultsList>
{this.state.results.map(result => (
<TypeaheadResult
onSelect={() => this.resultSelected(result)}
value={result}
>
{result.name}
</TypeaheadResult>
Expand Down Expand Up @@ -193,7 +194,10 @@ class Demo extends Component {
}, {});

return [
<Typeahead key="typeahead">
<Typeahead
onSelect={result => this.resultSelected(result)}
key="typeahead"
>
<TypeaheadInput
value={this.state.taValue}
onChange={(evt)=> this.typeaheadInputChange(evt)}
Expand All @@ -205,7 +209,7 @@ class Demo extends Component {
// Display county name at the top of each county group
(<h3 key={county}>{county}</h3>),
...(counties[county].map(city => (
<TypeaheadResult onSelect={() => this.resultSelected(city)}>{city.name}</TypeaheadResult>
<TypeaheadResult value={city}>{city.name}</TypeaheadResult>
)))
]
}, [])}
Expand Down
56 changes: 40 additions & 16 deletions __tests__/Typeahead-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import Adapter from 'enzyme-adapter-react-16';
import Typeahead from '../src/Typeahead';
import TypeaheadInput from '../src/TypeaheadInput';
import TypeaheadResultsList from '../src/TypeaheadResultsList';
import TypeaheadResult from '../src/TypeaheadResult';

Enzyme.configure({ adapter: new Adapter() });

const RESULT_VALUE = { some: 'value' };
const shallowSetup = () => {
const props = {
onDismiss: jest.fn()
onDismiss: jest.fn(),
onSelect: jest.fn()
};
return {
wrapper: shallow(<Typeahead {...props}/>),
Expand All @@ -21,14 +24,18 @@ const shallowSetup = () => {

const mountSetup = () => {
const props = {
onDismiss: jest.fn()
onDismiss: jest.fn(),
onSelect: jest.fn()
};
return {
wrapper: mount(<Typeahead {...props}>
hi
<TypeaheadInput value="hello" onChange={jest.fn()}/>
<TypeaheadResultsList>RESULTS</TypeaheadResultsList>
{null}
<TypeaheadResultsList>
<TypeaheadResult value={RESULT_VALUE}>RESULTS</TypeaheadResult>
<TypeaheadResult value={'hello'}>hello</TypeaheadResult>
<TypeaheadResult value={'world'}>world</TypeaheadResult>
</TypeaheadResultsList>
</Typeahead>),
props
};
Expand All @@ -45,11 +52,31 @@ describe('Typeahead', () => {
expect(toJson(wrapper)).toMatchSnapshot();
});

it('calls resultsList.navigateList when an arrow key is pressed', () => {
it('updates the highlighted index when an arrow key is pressed', () => {
const { wrapper } = mountSetup();
wrapper.instance().resultsList.navigateList = jest.fn();
wrapper.setState({highlightedIndex: -1});
wrapper.find('TypeaheadInput').props().arrowKeyPressed('ArrowDown');
expect(wrapper.instance().resultsList.navigateList).toHaveBeenCalled();
expect(wrapper.state().highlightedIndex).toBe(0);
});

it('should navigate the list, go around the horn', () => {
const { wrapper } = mountSetup();
wrapper.find('TypeaheadInput').props().arrowKeyPressed('ArrowDown');
expect(wrapper.state().highlightedIndex).toBe(0);
wrapper.find('TypeaheadInput').props().arrowKeyPressed('ArrowDown');
expect(wrapper.state().highlightedIndex).toBe(1);
wrapper.find('TypeaheadInput').props().arrowKeyPressed('ArrowDown');
expect(wrapper.state().highlightedIndex).toBe(2);
wrapper.find('TypeaheadInput').props().arrowKeyPressed('ArrowDown');
expect(wrapper.state().highlightedIndex).toBe(0);
wrapper.find('TypeaheadInput').props().arrowKeyPressed('ArrowUp');
expect(wrapper.state().highlightedIndex).toBe(2);
wrapper.find('TypeaheadInput').props().arrowKeyPressed('ArrowUp');
expect(wrapper.state().highlightedIndex).toBe(1);
wrapper.find('TypeaheadInput').props().arrowKeyPressed('ArrowUp');
expect(wrapper.state().highlightedIndex).toBe(0);
wrapper.find('TypeaheadInput').props().arrowKeyPressed('ArrowUp');
expect(wrapper.state().highlightedIndex).toBe(2);
});

it('should call props.onDismiss when escapeKeyPressed is called', () => {
Expand All @@ -64,27 +91,24 @@ describe('Typeahead', () => {
expect(wrapper.state().highlightedIndex).toBe(1);
});

it('should select the highlightedIndex by calling resultsList.selectResult with the state\'s highlightedIndex', () => {
const { wrapper } = mountSetup();
const highlightedIndex = 3;
it('should select the highlighted result', () => {
const { wrapper, props } = mountSetup();
const highlightedIndex = 0;
const evt = { preventDefault: jest.fn() };
wrapper.instance().resultsList.selectResult = jest.fn();
wrapper.setState({highlightedIndex});
wrapper.find('TypeaheadInput').props().enterKeyPressed(evt);
expect(wrapper.instance().resultsList.selectResult).toHaveBeenCalled();
expect(wrapper.instance().resultsList.selectResult).toHaveBeenCalledWith(highlightedIndex);
expect(props.onSelect).toHaveBeenCalledWith(RESULT_VALUE);
expect(evt.preventDefault).toHaveBeenCalledTimes(1);
expect(wrapper.state('highlightedIndex')).toBe(-1);
});

it('should not select a result or preventDefault if highlightedindex is -1 state\'s highlightedIndex', () => {
const { wrapper } = mountSetup();
const { wrapper, props } = mountSetup();
const highlightedIndex = -1;
const evt = { preventDefault: jest.fn() };
wrapper.instance().resultsList.selectResult = jest.fn();
wrapper.setState({highlightedIndex});
wrapper.find('TypeaheadInput').props().enterKeyPressed(evt);
expect(wrapper.instance().resultsList.selectResult).toHaveBeenCalledTimes(0);
expect(props.onSelect).toHaveBeenCalledTimes(0);
expect(evt.preventDefault).toHaveBeenCalledTimes(0);
expect(wrapper.state('highlightedIndex')).toBe(-1);
});
Expand Down
9 changes: 6 additions & 3 deletions __tests__/TypeaheadResult-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import TypeaheadResult from '../src/TypeaheadResult';

Enzyme.configure({ adapter: new Adapter() });

const RESULT_VALUE = 'RESULT_VALUE';
const shallowSetup = () => {
const props = {
onSelect: jest.fn(),
_onSelect: jest.fn(),
onHighlight: jest.fn(),
isHighlighted: false,
value: RESULT_VALUE
};
return {
wrapper: shallow(<TypeaheadResult {...props}>Las Vegas</TypeaheadResult>),
Expand All @@ -32,10 +34,11 @@ describe('TypeaheadResult', () => {
expect(toJson(wrapper)).toMatchSnapshot();
});

it('should call onSelect when clicked', () => {
it('should call _onSelect when clicked', () => {
const { props, wrapper } = shallowSetup();
wrapper.find('typeahead-result').simulate('click');
expect(props.onSelect).toHaveBeenCalled();
expect(props._onSelect).toHaveBeenCalled();
expect(props._onSelect).toHaveBeenCalledWith(props.value);
});

it('should call onHighlight when result becomes highlighted', () => {
Expand Down
35 changes: 1 addition & 34 deletions __tests__/TypeaheadResultsList-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Enzyme.configure({ adapter: new Adapter() });
const setup = () => {
const props = {
updateHighlightedIndex: jest.fn(),
onResultsUpdate: jest.fn(),
highlightedIndex: -1,
};
return {
Expand All @@ -29,38 +30,4 @@ describe('TypeaheadResultsList', () => {
const { wrapper } = setup();
expect(toJson(wrapper)).toMatchSnapshot();
});

it('should navigate the list, go around the horn', () => {
const { wrapper } = setup();
wrapper.setProps({
updateHighlightedIndex: jest.fn().mockImplementation(idx => {
wrapper.setProps({highlightedIndex: idx});
})
});
const updateHighlightedIndex = wrapper.props().updateHighlightedIndex;
wrapper.instance().navigateList('ArrowDown');
expect(updateHighlightedIndex).toHaveBeenLastCalledWith(0);
wrapper.instance().navigateList('ArrowDown');
expect(updateHighlightedIndex).toHaveBeenLastCalledWith(1);
wrapper.instance().navigateList('ArrowDown');
expect(updateHighlightedIndex).toHaveBeenLastCalledWith(2);
wrapper.instance().navigateList('ArrowDown');
expect(updateHighlightedIndex).toHaveBeenLastCalledWith(0);
wrapper.instance().navigateList('ArrowUp');
expect(updateHighlightedIndex).toHaveBeenLastCalledWith(2);
wrapper.instance().navigateList('ArrowUp');
expect(updateHighlightedIndex).toHaveBeenLastCalledWith(1);
wrapper.instance().navigateList('ArrowUp');
expect(updateHighlightedIndex).toHaveBeenLastCalledWith(0);
wrapper.instance().navigateList('ArrowUp');
expect(updateHighlightedIndex).toHaveBeenLastCalledWith(2);
});

it('should call select on the correct result', () => {
const { wrapper } = setup();

wrapper.instance().result1.select = jest.fn();
wrapper.instance().selectResult(1);
expect(wrapper.instance().result1.select).toHaveBeenCalled();
});
});
50 changes: 49 additions & 1 deletion __tests__/__snapshots__/Typeahead-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ exports[`Typeahead renders the children 1`] = `
<Typeahead
onBlur={[Function]}
onDismiss={[Function]}
onSelect={[Function]}
>
<pure-typeahead
onBlur={[Function]}
Expand All @@ -36,12 +37,59 @@ exports[`Typeahead renders the children 1`] = `
/>
</TypeaheadInput>
<TypeaheadResultsList
_onSelect={[Function]}
highlightedIndex={-1}
key=".2"
onResultsUpdate={[Function]}
updateHighlightedIndex={[Function]}
>
<typeahead-results-list>
RESULTS
<TypeaheadResult
_onSelect={[Function]}
isHighlighted={false}
key=".0"
value={
Object {
"some": "value",
}
}
>
<typeahead-result
class=""
onClick={[Function]}
tabindex="-1"
>
RESULTS
</typeahead-result>
</TypeaheadResult>
<TypeaheadResult
_onSelect={[Function]}
isHighlighted={false}
key=".1"
value="hello"
>
<typeahead-result
class=""
onClick={[Function]}
tabindex="-1"
>
hello
</typeahead-result>
</TypeaheadResult>
<TypeaheadResult
_onSelect={[Function]}
isHighlighted={false}
key=".2"
value="world"
>
<typeahead-result
class=""
onClick={[Function]}
tabindex="-1"
>
world
</typeahead-result>
</TypeaheadResult>
</typeahead-results-list>
</TypeaheadResultsList>
</pure-typeahead>
Expand Down
4 changes: 4 additions & 0 deletions __tests__/__snapshots__/TypeaheadResultsList-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
exports[`TypeaheadResultsList renders self and subcomponents 1`] = `
<TypeaheadResultsList
highlightedIndex={-1}
onResultsUpdate={[Function]}
updateHighlightedIndex={[Function]}
>
<typeahead-results-list>
Expand All @@ -12,6 +13,7 @@ exports[`TypeaheadResultsList renders self and subcomponents 1`] = `
Cities
</h3>
<TypeaheadResult
_onSelect={[Function]}
isHighlighted={false}
key=".1"
>
Expand All @@ -24,6 +26,7 @@ exports[`TypeaheadResultsList renders self and subcomponents 1`] = `
</typeahead-result>
</TypeaheadResult>
<TypeaheadResult
_onSelect={[Function]}
isHighlighted={false}
key=".2"
>
Expand All @@ -36,6 +39,7 @@ exports[`TypeaheadResultsList renders self and subcomponents 1`] = `
</typeahead-result>
</TypeaheadResult>
<TypeaheadResult
_onSelect={[Function]}
isHighlighted={false}
key=".3"
>
Expand Down
6 changes: 4 additions & 2 deletions demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class Demo extends Component {
...arr,
(<h3 key={county}>{county}</h3>),
...(counties[county].map(city => (
<TypeaheadResult onSelect={() => this.resultSelected(city)}>{city.name}</TypeaheadResult>
<TypeaheadResult value={city}>{city.name}</TypeaheadResult>
)))
]
}, []);
Expand All @@ -74,7 +74,9 @@ class Demo extends Component {
return <div>
<h1>Cities of Utah</h1>
<button>Focus</button>
<Typeahead onBlur={() => console.log('blurred')}>
<Typeahead
onSelect={(city) => this.resultSelected(city)}
onBlur={() => console.log('blurred')}>
<label htmlFor="input"><p>Search Cities</p></label>
<TypeaheadInput
id="input"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pure-typeahead",
"version": "0.2.2",
"version": "1.0.0",
"description": "pure-typeahead React component",
"main": "lib/index.js",
"module": "es/index.js",
Expand Down
Loading

0 comments on commit 4b256dc

Please sign in to comment.