: null;
321 | }
322 |
323 | renderFakePlaceholder = () => {
324 | return this.shouldShowFakePlaceholder()
325 | ? (
328 | {this.props.placeholder}
329 |
)
330 | : null;
331 | }
332 |
333 | renderInput = () => {
334 | return this.shouldShowInput()
335 | ? (
)
342 | : this.renderFakePlaceholder();
343 | }
344 |
345 | renderDropdownIndicator = () => {
346 | return this.props.simulateSelect
347 | ?
348 | : null;
349 | }
350 |
351 | render() {
352 | return (
353 |
354 |
355 | {this.renderTokens()}
356 | {this.renderInput()}
357 | {this.renderProcessing()}
358 | {this.renderDropdownIndicator()}
359 |
360 | {this.renderOptionsDropdown()}
361 |
362 | );
363 |
364 | }
365 |
366 | }
367 |
--------------------------------------------------------------------------------
/src/TokenAutocomplete/options/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import radium from 'radium';
3 | import styles from './styles';
4 | import {noop, map} from 'lodash';
5 | import keyCodes from 'utils/keyCodes';
6 | import Option from './option';
7 | import {decorators} from 'peters-toolbelt';
8 | const {StyleDefaults} = decorators;
9 |
10 | @radium
11 | @StyleDefaults(styles)
12 | export default class OptionList extends React.Component {
13 |
14 | static displayName = 'Option List';
15 |
16 | static propTypes = {
17 | options: React.PropTypes.array,
18 | alreadySelected: React.PropTypes.array,
19 | term: React.PropTypes.string
20 | }
21 |
22 | static defaultProps = {
23 | options: [],
24 | term: '',
25 | emptyInfo: 'no suggestions',
26 | handleAddSelected: noop
27 | }
28 |
29 | state = {
30 | selected: 0,
31 | suggestions: []
32 | }
33 |
34 | componentDidMount() {
35 | if (window) {
36 | window.addEventListener('keydown', this.onKeyDown);
37 | }
38 | }
39 |
40 | componentWillUnmount() {
41 | if (window) {
42 | window.removeEventListener('keydown', this.onKeyDown);
43 | }
44 | }
45 |
46 | componentWillReceiveProps(newProps) {
47 | if (newProps.options.length <= this.state.selected) {
48 | this.setState({selected: newProps.options.length - 1});
49 | }
50 |
51 | if (!newProps.options.length) {
52 | this.setState({selected: 0});
53 | }
54 | }
55 |
56 | onKeyDown = e => {
57 | switch (e.keyCode) {
58 | case keyCodes.UP :
59 | this.selectPrev();
60 | e.preventDefault();
61 | break;
62 | case keyCodes.DOWN :
63 | this.selectNext();
64 | e.preventDefault();
65 | break;
66 | }
67 | }
68 |
69 | renderOption = (option, index) => {
70 | return (
71 |
80 | );
81 | }
82 |
83 | renderOptions() {
84 | return map(this.props.options, (option, index) => {
85 | return this.renderOption(option, index);
86 | });
87 | }
88 |
89 | selectNext = () => {
90 |
91 | this.setState({
92 | selected: this.state.selected === this.props.options.length - 1
93 | ? 0
94 | : this.state.selected + 1
95 | });
96 | }
97 |
98 | selectPrev = () => {
99 |
100 | this.setState({
101 | selected: !this.state.selected
102 | ? this.props.options.length - 1
103 | : this.state.selected - 1
104 | });
105 | }
106 |
107 | handleSelect = index => {
108 | this.setState({
109 | selected: index,
110 | a: '123'
111 | });
112 | }
113 |
114 | getSelected = () => {
115 | return this.props.options[this.state.selected];
116 | }
117 |
118 | renderEmptyInfo() {
119 | return
{this.props.emptyInfo}
;
120 | }
121 |
122 | render() {
123 | const displayEmptyInfo = !this.props.options.length;
124 |
125 | return (
126 |
127 | {displayEmptyInfo ? this.renderEmptyInfo() : this.renderOptions() }
128 |
129 |
130 | );
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/TokenAutocomplete/options/option/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import radium from 'radium';
3 | import styles from './styles';
4 | import {noop, identity} from 'lodash';
5 | import {decorators} from 'peters-toolbelt';
6 | const {StyleDefaults} = decorators;
7 |
8 | @radium
9 | @StyleDefaults(styles)
10 | export default class Option extends React.Component {
11 |
12 | static displayName = 'Option';
13 |
14 | static propTypes = {
15 | selected: React.PropTypes.bool,
16 | index: React.PropTypes.number,
17 | handleSelect: React.PropTypes.func,
18 | handleClick: React.PropTypes.func,
19 | parse: React.PropTypes.func
20 | }
21 |
22 | static defaultProps = {
23 | handleSelect: noop,
24 | handleClick: noop,
25 | selected: false,
26 | index: 0,
27 | parse: identity
28 | }
29 |
30 | onMouseEnter = () => {
31 | this.props.handleSelect(this.props.index);
32 | }
33 |
34 | onClick = () => {
35 | this.props.handleClick(this.props.index);
36 | }
37 |
38 | render() {
39 | return (
40 |
45 | {this.props.parse(this.props.value)}
46 |
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/TokenAutocomplete/options/option/spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react/addons';
2 | import Option from './';
3 | import TestUtils from 'TestUtils';
4 |
5 | let component;
6 |
7 | describe('Single option', () => {
8 |
9 | afterEach(done => {
10 | React.unmountComponentAtNode(document.body);
11 | document.body.innerHTML = '';
12 | done();
13 | });
14 |
15 |
16 | //INITIAL STATE
17 |
18 | describe('by default', () => {
19 |
20 | beforeEach(() => {
21 | component = TestUtils.renderComponent(Option);
22 | });
23 |
24 | it('is not selected', () => {
25 | expect(component.props.selected).to.be.false;
26 | });
27 |
28 | });
29 |
30 | //UNIT
31 |
32 | it('accepts passed style objects', () => {
33 | component = TestUtils.renderComponent(Option, {
34 | style: {
35 | wrapper: { backgroundColor: '#000' }
36 | }
37 | });
38 | const wrapperNode = React.findDOMNode(component.refs.wrapper);
39 | expect(wrapperNode.style.backgroundColor).to.equal('rgb(0, 0, 0)');
40 | });
41 |
42 | it('calls handleSelect onMouseEnter', () => {
43 | let spy = sinon.spy();
44 |
45 | component = TestUtils.renderComponent(Option, {
46 | handleSelect: spy
47 | });
48 |
49 | TestUtils.SimulateNative.mouseOver(React.findDOMNode(component.refs.wrapper));
50 | expect(spy.called).to.be.true;
51 |
52 | });
53 |
54 | it('calls handleClick when clicked', () => {
55 | let spy = sinon.spy();
56 |
57 | component = TestUtils.renderComponent(Option, {
58 | handleClick: spy
59 | });
60 |
61 | TestUtils.Simulate.click(React.findDOMNode(component.refs.wrapper));
62 | expect(spy.called).to.be.true;
63 |
64 | });
65 |
66 |
67 | //FUNCTIONAL
68 |
69 | it('displays passed value', () => {
70 | component = TestUtils.renderComponent(Option, {value: 'someValue'});
71 | const wrapperNode = React.findDOMNode(component.refs.wrapper);
72 | expect(wrapperNode.textContent).to.equal('someValue');
73 | });
74 |
75 |
76 | it('displays proper style if selected', () => {
77 | component = TestUtils.renderComponent(Option, {
78 | selected: true,
79 | style: {
80 | wrapper: { backgroundColor: '#ddd' },
81 | selected: { backgroundColor: '#fff' }
82 | }
83 | });
84 | const wrapperNode = React.findDOMNode(component.refs.wrapper);
85 | expect(wrapperNode.style.backgroundColor).to.equal('rgb(255, 255, 255)');
86 | });
87 |
88 | });
89 |
--------------------------------------------------------------------------------
/src/TokenAutocomplete/options/option/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | wrapper: {
3 | padding: 5,
4 | cursor: 'default'
5 | },
6 | selected: {
7 | background: '#d4d4d4'
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/TokenAutocomplete/options/spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react/addons';
2 | import Options from './';
3 | import TestUtils from 'TestUtils';
4 |
5 | let component;
6 |
7 | describe('Option list', () => {
8 |
9 | afterEach(done => {
10 | React.unmountComponentAtNode(document.body);
11 | document.body.innerHTML = '';
12 | done();
13 | });
14 |
15 |
16 | //INITIAL STATE
17 |
18 | describe('by default', () => {
19 |
20 | beforeEach(() => {
21 | component = TestUtils.renderComponent(Options);
22 | });
23 |
24 | it('has empty options array', () => {
25 | expect(component.props.options).to.be.instanceof(Array);
26 | expect(component.props.options).to.be.empty;
27 | });
28 |
29 | it('has empty term', () => {
30 | expect(component.props.term).to.equal('');
31 | });
32 |
33 | it('has default empty info', () => {
34 | expect(component.props.emptyInfo).to.equal('no suggestions');
35 | });
36 |
37 | });
38 |
39 | //UNIT
40 |
41 | //FUNCTIONAL
42 | it('displays options', () => {
43 |
44 | const component = TestUtils.renderComponent(Options, {
45 | options: ['a', 'a', 'a', 'a', 'a', 'a']
46 | });
47 |
48 | const options = React.findDOMNode(component.refs.wrapper).querySelectorAll('div');
49 |
50 | expect(options.length).to.equal(6);
51 |
52 | });
53 |
54 | it('selects first option by default', () => {
55 |
56 | const component = TestUtils.renderComponent(Options, {
57 | options: ['a', 'a', 'a', 'a', 'a', 'a']
58 | });
59 |
60 | expect(component.state.selected).to.equal(0);
61 |
62 | });
63 |
64 | it('cycles through list', () => {
65 |
66 | const component = TestUtils.renderComponent(Options, {
67 | options: ['a', 'a', 'a']
68 | });
69 |
70 | component.selectNext();
71 | expect(component.state.selected).to.equal(1);
72 | component.selectNext();
73 | component.selectNext();
74 | expect(component.state.selected).to.equal(0);
75 | component.selectPrev();
76 | expect(component.state.selected).to.equal(2);
77 | component.selectPrev();
78 | expect(component.state.selected).to.equal(1);
79 |
80 | });
81 |
82 | it('displays empty info if options list is empty', () => {
83 |
84 | const component1 = TestUtils.renderComponent(Options, {
85 | options: ['a']
86 | });
87 |
88 | const component2 = TestUtils.renderComponent(Options, {
89 | options: []
90 | });
91 |
92 | const emptyInfoNode = React.findDOMNode(component2.refs.emptyInfo);
93 |
94 | expect(component1.refs.emptyInfo).not.to.exist;
95 | expect(component2.refs.emptyInfo).to.exist;
96 | expect(emptyInfoNode.textContent).to.equal('no suggestions');
97 |
98 | });
99 |
100 | });
101 |
--------------------------------------------------------------------------------
/src/TokenAutocomplete/options/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | wrapper: {
3 | position: 'absolute',
4 | width: '100%',
5 | border: '1px solid #aaa',
6 | marginTop: 2,
7 | maxHeight: 275,
8 | overflowX: 'hidden',
9 | overflowY: 'auto',
10 | backgroundColor: '#fff'
11 | },
12 | emptyInfo: {
13 | padding: 10
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/src/TokenAutocomplete/spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react/addons';
2 | import TokenAutocomplete from './';
3 | import Token from './token';
4 | import TestUtils from 'TestUtils';
5 | import {noop} from 'lodash';
6 |
7 | let component;
8 |
9 | const consoleWarnSpy = sinon.spy(console, 'warn');
10 |
11 |
12 | describe('TokenAutocomplete', () => {
13 |
14 | afterEach(done => {
15 | consoleWarnSpy.reset();
16 | React.unmountComponentAtNode(document.body);
17 | document.body.innerHTML = '';
18 | done();
19 | });
20 |
21 |
22 | //INITIAL STATE
23 |
24 | describe('by default', () => {
25 |
26 | beforeEach(() => {
27 | component = TestUtils.renderComponent(TokenAutocomplete);
28 | });
29 |
30 | //props
31 | it('has empty options array', () => {
32 | expect(component.props.options).to.be.instanceof(Array);
33 | expect(component.props.options).to.be.empty;
34 | });
35 |
36 | it('has empty value array by default', () => {
37 | expect(component.props.defaultValues).to.be.instanceof(Array);
38 | expect(component.props.defaultValues).to.be.empty;
39 | });
40 |
41 | it('has treshold of 0', () => {
42 | expect(component.props.treshold).to.be.equal(0);
43 | });
44 |
45 | it('has predefined placeholder', () => {
46 | expect(component.props.placeholder).to.be.equal('add new tag');
47 | expect(component.refs.input.props.placeholder).to.equal('add new tag');
48 | });
49 |
50 | it('is not processing', () => {
51 | expect(component.props.processing).to.be.false;
52 | });
53 |
54 | it('is not limiting tags to options', () => {
55 | expect(component.props.limitToOptions).to.be.false;
56 | });
57 |
58 | it('handle on input change with noop', () => {
59 | expect(component.props.onInputChange).to.equal(noop);
60 | });
61 |
62 | it('handle onAdd add with noop', () => {
63 | expect(component.props.onAdd).to.equal(noop);
64 | });
65 |
66 | it('handle onRemove add with noop', () => {
67 | expect(component.props.onRemove).to.equal(noop);
68 | });
69 |
70 | it('is not simulating select', () => {
71 | expect(component.props.simulateSelect).to.be.false;
72 | });
73 |
74 | it('allows filtering options', () => {
75 | expect(component.props.filterOptions).to.be.true;
76 | });
77 |
78 | //state
79 | it('is unfocused by default', () => {
80 | expect(component.state.focused).to.be.false;
81 | });
82 |
83 | });
84 |
85 | describe('contains', () => {
86 |
87 | it('input with placeholder', () => {
88 | const component = TestUtils.renderComponent(TokenAutocomplete);
89 | expect(component.refs.input).to.exist;
90 | });
91 |
92 | });
93 |
94 | //API
95 | describe('provides API', () => {
96 |
97 | it('to set initial focus', () => {
98 |
99 | const component1 = TestUtils.renderComponent(TokenAutocomplete, {focus: false});
100 | const component2 = TestUtils.renderComponent(TokenAutocomplete, {focus: true});
101 |
102 | expect(component1.state.focused).to.be.false;
103 | expect(component2.state.focused).to.be.true;
104 | });
105 |
106 | it('to limit tags to options', () => {
107 | const component = TestUtils.renderComponent(TokenAutocomplete, {
108 | options: ['aaa', 'ccc'],
109 | defaultValues: ['bbb'],
110 | limitToOptions: true
111 | });
112 |
113 |
114 | expect(component.state.values.size).to.equal(1);
115 |
116 | TestUtils.changeInputValue(component, 'ddd');
117 | TestUtils.hitEnter(component);
118 |
119 | expect(component.state.values.size).to.equal(1);
120 |
121 | });
122 |
123 | it('to allow custom tags', () => {
124 |
125 | const component = TestUtils.renderComponent(TokenAutocomplete, {
126 | options: ['aaa', 'ccc'],
127 | defaultValues: ['bbb'],
128 | limitToOptions: false
129 | });
130 |
131 |
132 | expect(component.state.values.size).to.equal(1);
133 |
134 | TestUtils.changeInputValue(component, 'ddd');
135 | TestUtils.hitEnter(component);
136 |
137 | expect(component.state.values.size).to.equal(2);
138 |
139 | });
140 |
141 | it('to display processing status', () => {
142 | const component1 = TestUtils.renderComponent(TokenAutocomplete, {
143 | processing: true
144 | });
145 |
146 | const component2 = TestUtils.renderComponent(TokenAutocomplete, {
147 | processing: false
148 | });
149 | expect(component1.refs.processing).to.exist;
150 | expect(component2.refs.processing).not.to.exist;
151 |
152 | });
153 |
154 | it('to handle input value change ', () => {
155 |
156 | var spy = sinon.spy();
157 | const component = TestUtils.renderComponent(TokenAutocomplete, {
158 | onInputChange: spy
159 | });
160 |
161 | TestUtils.changeInputValue(component, 'aaaaa');
162 | expect(spy.calledWith('aaaaa')).to.be.true;
163 | });
164 |
165 | it('to handle value addition', () => {
166 |
167 | var spy = sinon.spy();
168 | const component = TestUtils.renderComponent(TokenAutocomplete, {
169 | onAdd: spy,
170 | defaultValues: ['bbb'],
171 | limitToOptions: false
172 | });
173 |
174 | TestUtils.changeInputValue(component, 'aaaaa');
175 | TestUtils.hitEnter(component);
176 | expect(spy.calledWith('aaaaa', component.state.values)).to.be.true;
177 | });
178 |
179 | it('to handle value deletion', () => {
180 |
181 | var spy = sinon.spy();
182 | const component = TestUtils.renderComponent(TokenAutocomplete, {
183 | onRemove: spy,
184 | defaultValues: ['bbb'],
185 | limitToOptions: false
186 | });
187 |
188 | TestUtils.changeInputValue(component, 'bbbb');
189 | TestUtils.hitEnter(component);
190 | TestUtils.hitBackspace(component);
191 |
192 | expect(spy.calledWith('bbbb', component.state.values)).to.be.true;
193 | });
194 |
195 | it('to parse user entered values', () => {
196 |
197 | let spy = sinon.spy(value => '1' + value);
198 | const component = TestUtils.renderComponent(TokenAutocomplete, {
199 | parseCustom: spy,
200 | defaultValues: [],
201 | limitToOptions: false
202 | });
203 |
204 | TestUtils.changeInputValue(component, 'aaaaa');
205 | TestUtils.hitEnter(component);
206 | expect(spy.calledWith('aaaaa')).to.be.true;
207 | expect(component.state.values.get(0)).to.equal('1aaaaa');
208 | });
209 |
210 | it('to block filtering options', () => {
211 |
212 | const component = TestUtils.renderComponent(TokenAutocomplete, {
213 | filterOptions: false
214 | });
215 |
216 | expect(component.refs.input).not.to.exist;
217 |
218 | });
219 |
220 | });
221 |
222 |
223 | //UNIT
224 | it('stores input value in state.inputValue', () => {
225 |
226 | const component = TestUtils.renderComponent(TokenAutocomplete);
227 |
228 | expect(component.state.inputValue).to.equal('');
229 |
230 | TestUtils.changeInputValue(component, 'abc');
231 |
232 | expect(component.state.inputValue).to.equal('abc');
233 |
234 | });
235 |
236 | it('throws warning when more than one default value is passed when simulating select', () => {
237 |
238 | const WARN_MSG = 'Warning: Failed propType: when props.simulateSelect is set to TRUE, you should pass more than a single value in props.defaultValues';
239 |
240 | TestUtils.renderComponent(TokenAutocomplete, {
241 | simulateSelect: true,
242 | defaultValues: ['bbb', 'ccc']
243 | });
244 |
245 | expect(consoleWarnSpy.getCall(0).args).to.include(WARN_MSG);
246 |
247 | });
248 |
249 | it('throws warning when non-zero treshold is defined when simulating select', () => {
250 |
251 | const WARN_MSG = 'Warning: Failed propType: when props.simulateSelect is set to TRUE, you should not pass non-zero treshold';
252 |
253 | TestUtils.renderComponent(TokenAutocomplete, {
254 | simulateSelect: true,
255 | treshold: 3
256 | });
257 |
258 | expect(consoleWarnSpy.getCall(0).args).to.include(WARN_MSG);
259 |
260 | });
261 |
262 | describe('passes', () => {
263 |
264 | describe('to options list', () => {
265 |
266 | beforeEach(() => {
267 | component = TestUtils.renderComponent(TokenAutocomplete, {
268 | defaultValues: ['a', 'b']
269 | });
270 | TestUtils.changeInputValue(component, 'def');
271 | });
272 |
273 | it('inputValue as term props', () => {
274 | expect(component.refs.options.props.term).to.equal('def');
275 | });
276 |
277 | });
278 |
279 | describe('to tokens', () => {
280 |
281 | it('fullWidth setting based on the simulateSelect props', () => {
282 |
283 | const component1 = TestUtils.renderComponent(TokenAutocomplete, {
284 | simulateSelect: true,
285 | defaultValues: ['bbb']
286 | });
287 | const component2 = TestUtils.renderComponent(TokenAutocomplete, {
288 | defaultValues: ['bbb']
289 | });
290 |
291 | expect(component1.refs['token0'].props.fullWidth).to.be.true;
292 | expect(component2.refs['token0'].props.fullWidth).to.be.false;
293 |
294 | });
295 |
296 | });
297 |
298 |
299 | });
300 |
301 | //FUNCTIONAL
302 |
303 | it('displays a list when options are provided and treshold is achieved', () => {
304 |
305 | const component = TestUtils.renderComponent(TokenAutocomplete);
306 | expect(component.refs.options).not.to.exist;
307 |
308 | TestUtils.changeInputValue(component, 'abc');
309 |
310 | expect(component.refs.options).to.exist;
311 |
312 | });
313 |
314 | it('displays a token for each passed value', () => {
315 |
316 | const component = TestUtils.renderComponent(TokenAutocomplete, {
317 | defaultValues: ['a', 'b', 'c', 'd']
318 | });
319 |
320 | let tokens = TestUtils.scryRenderedComponentsWithType(component.refs.wrapper, Token);
321 | expect(tokens.length).to.equal(4);
322 | });
323 |
324 | it('dont show already selected options', () => {
325 |
326 | const component = TestUtils.renderComponent(TokenAutocomplete, {
327 | options: ['aaa1', 'aaa2', 'aaa3', 'aaa4'],
328 | defaultValues: ['aaa1', 'aaa2', 'aaa3']
329 | });
330 |
331 |
332 | TestUtils.changeInputValue(component, 'aaa');
333 |
334 | expect(component.refs.options.props.options).to.include('aaa4');
335 |
336 | });
337 |
338 | it('dont show options not matching currently typed value', () => {
339 |
340 | const component = TestUtils.renderComponent(TokenAutocomplete, {
341 | limitToOptions: true,
342 | options: ['aaa1', 'aaa2', 'aaa3', 'aaa4', 'ddd1'],
343 | defaultValues: ['aaa1', 'aaa2', 'aaa3']
344 | });
345 |
346 | TestUtils.changeInputValue(component, 'aaa');
347 |
348 | expect(component.refs.options.props.options.length).to.equal(1);
349 | expect(component.refs.options.props.options).to.include('aaa4');
350 |
351 | });
352 |
353 | describe('on enter', () => {
354 |
355 | it('adds currently selected element on enter', () => {
356 |
357 | const component = TestUtils.renderComponent(TokenAutocomplete, {
358 | options: ['aaa', 'ccc'],
359 | defaultValues: ['bbb'],
360 | limitToOptions: true
361 | });
362 |
363 | TestUtils.changeInputValue(component, 'aaa');
364 | TestUtils.hitEnter(component);
365 |
366 | expect(component.state.values.size).to.equal(2);
367 |
368 | TestUtils.changeInputValue(component, 'aaa');
369 | expect(component.refs.options.props.options).to.be.empty;
370 |
371 | });
372 |
373 | it('clears inputValue', () => {
374 |
375 | const component = TestUtils.renderComponent(TokenAutocomplete, {
376 | options: ['aaa'],
377 | defaultValues: ['bbb']
378 | });
379 |
380 | TestUtils.changeInputValue(component, 'aaa');
381 | TestUtils.hitEnter(component);
382 |
383 | expect(component.state.inputValue).to.be.empty;
384 |
385 | });
386 |
387 | });
388 |
389 | it('deletes the last value on backspace when input is empty ', () => {
390 |
391 | const component = TestUtils.renderComponent(TokenAutocomplete, {
392 | defaultValues: ['aaa1', 'aaa2', 'aaa3']
393 | });
394 | TestUtils.focus(component);
395 |
396 | TestUtils.hitBackspace(component);
397 | expect(component.state.values.size).to.equal(2);
398 | TestUtils.hitBackspace(component);
399 | expect(component.state.values.size).to.equal(1);
400 | });
401 |
402 | it('blurs on escape', () => {
403 | const component = TestUtils.renderComponent(TokenAutocomplete);
404 | TestUtils.focus(component);
405 |
406 | expect(component.state.focused).to.be.true;
407 | TestUtils.hitEscape(component);
408 | expect(component.state.focused).to.be.false;
409 |
410 | });
411 |
412 | it('handles token removal', () => {
413 |
414 | const component = TestUtils.renderComponent(TokenAutocomplete, {
415 | defaultValues: ['aaa1']
416 | });
417 |
418 | const removeBtnNode = React.findDOMNode(component.refs.inputWrapper);
419 | const TokenNode = removeBtnNode.querySelectorAll('div')[0];
420 | const TokenRemoveBtnNode = TokenNode.querySelectorAll('.token-remove-btn')[0];
421 |
422 | expect(component.state.values.size).to.equal(1);
423 | TestUtils.Simulate.click(TokenRemoveBtnNode);
424 |
425 | expect(component.state.values.size).to.equal(0);
426 |
427 | });
428 |
429 | it('refocuses after token removal', () => {
430 |
431 | let component = TestUtils.renderComponent(TokenAutocomplete, {
432 | defaultValues: ['aaa1'],
433 | focus: false
434 | });
435 |
436 |
437 | expect(component.state.focused).to.be.false;
438 | TestUtils.focus(component);
439 | expect(component.state.focused).to.be.true;
440 | TestUtils.hitBackspace(component);
441 | expect(component.state.focused).to.be.true;
442 |
443 |
444 | });
445 |
446 | it('toggles options list on focus/blur', () => {
447 |
448 | const component = TestUtils.renderComponent(TokenAutocomplete);
449 |
450 | TestUtils.changeInputValue(component, 'aaa');
451 |
452 | TestUtils.blur(component);
453 | expect(component.refs.options).not.to.exist;
454 |
455 | TestUtils.focus(component);
456 | expect(component.refs.options).to.exist;
457 |
458 |
459 | });
460 |
461 | it('doesn\'t allow duplicates', () => {
462 |
463 | const component = TestUtils.renderComponent(TokenAutocomplete, {
464 | focus: true,
465 | limitToOptions: false
466 | });
467 |
468 | TestUtils.changeInputValue(component, 'aaa');
469 | TestUtils.hitEnter(component);
470 | expect(component.state.values.size).to.equal(1);
471 |
472 | TestUtils.changeInputValue(component, 'aaa');
473 | TestUtils.hitEnter(component);
474 | expect(component.state.values.size).to.equal(1);
475 |
476 |
477 | });
478 |
479 | describe('when simulating select', () => {
480 |
481 | beforeEach(() => {
482 |
483 | component = TestUtils.renderComponent(TokenAutocomplete, {
484 | options: ['bbb', 'ccc'],
485 | defaultValues: ['bbb'],
486 | simulateSelect: true
487 | });
488 |
489 | });
490 |
491 | it('input is not displayed when value is provided', () => {
492 | expect(component.refs.input).not.to.exist;
493 | });
494 |
495 | it('current value is replaced when new one is selected', () => {
496 |
497 | TestUtils.focus(component);
498 | let firstOption = component.refs.options.refs.option1;
499 | TestUtils.SimulateNative.mouseOver(React.findDOMNode(firstOption));
500 | TestUtils.Simulate.click(React.findDOMNode(firstOption));
501 | expect(component.refs.token0.props.value).to.equal('ccc');
502 |
503 | });
504 |
505 | it('contains dropdownIndicator', () => {
506 | expect(component.refs.dropdownIndicator).to.exist;
507 | });
508 |
509 | });
510 |
511 |
512 | });
513 |
--------------------------------------------------------------------------------
/src/TokenAutocomplete/styles.js:
--------------------------------------------------------------------------------
1 | import radium from 'radium';
2 |
3 | const rotateKeyframe = radium.keyframes({
4 | 'from': {transform: 'rotate(0deg)'},
5 | 'to': {transform: 'rotate(360deg)'}
6 | });
7 |
8 | export default {
9 | wrapper: {
10 | fontFamily: '"Helvetica Neue", "Helvetica", "Arial", sans-serif',
11 | fontSize: 13,
12 | position: 'relative'
13 | },
14 | input: {
15 | verticalAlign: 'middle',
16 | border: 'none',
17 | cursor: 'default',
18 | flexGrow: 1,
19 | display: 'inline-block',
20 | fontSize: 13,
21 | minHeight: 25,
22 | fontFamily: '"Helvetica Neue", "Helvetica", "Arial", sans-serif',
23 | ':focus': {
24 | outline: 0
25 | }
26 | },
27 | inputWrapper: {
28 | border: '1px solid #999',
29 | borderRadius: 2,
30 | padding: '1px 4px',
31 | display: 'flex',
32 | fontWeight: 400,
33 | overflow: 'hidden',
34 | flexWrap: 'wrap',
35 | minHeight: 26
36 | },
37 | processing: {
38 | width: 18,
39 | height: 18,
40 | marginTop: 3,
41 | position: 'relative',
42 | animation: `${rotateKeyframe} .75s infinite linear`,
43 | borderRight: '2px solid #ddd',
44 | borderLeft: '2px solid #ddd',
45 | borderBottom: '2px solid #ddd',
46 | borderTop: '2px solid #888',
47 | borderRadius: '100%'
48 | },
49 | dropdownIndicator: {
50 | position: 'absolute',
51 | height: 0,
52 | width: 0,
53 | borderTop: '7px solid #999',
54 | borderBottom: '7px solid transparent',
55 | borderRight: '5px solid transparent',
56 | borderLeft: '5px solid transparent',
57 | top: 12,
58 | right: 10
59 | },
60 | fakePlaceholder: {
61 | fontSize: 13,
62 | paddingLeft: 6,
63 | paddingTop: 5,
64 | color: '#aaa',
65 | cursor: 'default'
66 | }
67 | };
68 |
--------------------------------------------------------------------------------
/src/TokenAutocomplete/token/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import radium from 'radium';
3 | import styles from './styles';
4 | import {identity, noop} from 'lodash';
5 | import {decorators} from 'peters-toolbelt';
6 | const {StyleDefaults} = decorators;
7 |
8 | @radium
9 | @StyleDefaults(styles)
10 | export default class Token extends React.Component {
11 |
12 | static displayName = 'Token';
13 |
14 | static propTypes = {
15 | handleRemove: React.PropTypes.func,
16 | index: React.PropTypes.number,
17 | parse: React.PropTypes.func
18 | }
19 |
20 | static defaultProps = {
21 | handleRemove: noop,
22 | parse: identity,
23 | index: 0,
24 | fullWidth: false
25 | }
26 |
27 | state = {
28 | }
29 |
30 | onRemoveBtnClick = () => {
31 | this.props.handleRemove(this.props.index);
32 | }
33 |
34 | parseLabel = value => {
35 |
36 | }
37 |
38 | renderRemoveBtn = () => {
39 | return (
40 |
45 | x
46 |
47 | );
48 | }
49 |
50 | render() {
51 |
52 | const {style} = this.props;
53 |
54 | return (
55 |
56 |
57 | { this.props.parse(this.props.value) }
58 |
59 | { !this.props.fullWidth ? this.renderRemoveBtn() : null}
60 |
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/TokenAutocomplete/token/spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react/addons';
2 | import Token from './';
3 | import TestUtils from 'TestUtils';
4 | import _ from 'lodash';
5 | let component;
6 |
7 | describe('Option list', () => {
8 |
9 | afterEach(done => {
10 | React.unmountComponentAtNode(document.body);
11 | document.body.innerHTML = '';
12 | done();
13 | });
14 |
15 | //INITIAL STATE
16 |
17 | describe('by default', () => {
18 |
19 | beforeEach(() => {
20 | component = TestUtils.renderComponent(Token);
21 | });
22 |
23 | it('has noop remove handler', () => {
24 | expect(component.props.handleRemove).to.equal(_.noop);
25 | });
26 |
27 | it('displays passed value without parsing', () => {
28 | component = TestUtils.renderComponent(Token, {value: 'someValue'});
29 | const wrapperNode = React.findDOMNode(component.refs.value);
30 | expect(wrapperNode.textContent).to.equal('someValue');
31 | });
32 |
33 | it('is not fullwidth', () => {
34 | expect(component.props.fullWidth).to.be.false;
35 | });
36 |
37 |
38 | });
39 |
40 | //STRUCTURE
41 |
42 | describe('contains', () => {
43 |
44 | beforeEach(() => {
45 | component = TestUtils.renderComponent(Token);
46 | });
47 |
48 | it('value wrapper', () => {
49 | component = TestUtils.renderComponent(Token);
50 | expect(component.refs.value).to.exist;
51 | });
52 |
53 | it('remove btn', () => {
54 | component = TestUtils.renderComponent(Token);
55 | expect(component.refs.removeBtn).to.exist;
56 | });
57 |
58 | });
59 |
60 |
61 | //FUNCTIONAL
62 |
63 | it('displays passed value after parsing', () => {
64 |
65 | function parser(value) {
66 | return 'sth ' + value;
67 | }
68 |
69 | component = TestUtils.renderComponent(Token,
70 | { parse: parser, value: 'someValue'});
71 | const wrapperNode = React.findDOMNode(component.refs.value);
72 | expect(wrapperNode.textContent).to.equal('sth someValue');
73 | });
74 |
75 | describe('when fullWidth prop is passed', () => {
76 |
77 | beforeEach(() => {
78 | component = TestUtils.renderComponent(Token, {fullWidth: true});
79 | });
80 |
81 | it('applies proper styles', () => {
82 | expect(component.refs.wrapper.props.style.border).to.equal('none');
83 | });
84 |
85 | it('won\'t show remove btn', () => {
86 | expect(component.refs.removeBtn).not.to.exist;
87 | });
88 |
89 | });
90 |
91 |
92 | });
93 |
--------------------------------------------------------------------------------
/src/TokenAutocomplete/token/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | wrapper: {
3 | borderRadius: 2,
4 | background: '#fff',
5 | marginRight: 3,
6 | marginTop: 3,
7 | marginBottom: 3,
8 | padding: 0,
9 | overflow: 'hidden',
10 | position: 'relative',
11 | border: '1px solid #bbb',
12 | width: 'auto'
13 | },
14 |
15 | wrapperFullWidth: {
16 | flexGrow: 1,
17 | border: 'none'
18 | },
19 |
20 | removeBtn: {
21 | marginLeft: 2,
22 | top: 0,
23 | right: 0,
24 | bottom: 0,
25 | padding: '4px 6px',
26 | color: '#888',
27 | fontSize: 10,
28 | borderLeft: '1px solid #bbb',
29 | lineHeight: '100%',
30 | position: 'absolute',
31 | verticalAlign: 'middle',
32 | cursor: 'pointer',
33 | ':hover': {
34 | color: '#777',
35 | background: '#bbb'
36 | }
37 | },
38 | value: {
39 | padding: '2px 5px',
40 | marginRight: 20,
41 | color: '#555',
42 | fontSize: 13,
43 | verticalAlign: 'middle'
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/src/_tests/utils.js:
--------------------------------------------------------------------------------
1 | import React from 'react/addons';
2 | import {noop} from 'lodash';
3 | const {TestUtils} = React.addons;
4 |
5 | function renderComponent(Component, props = {}) {
6 | return TestUtils.renderIntoDocument(
);
7 | }
8 |
9 | function changeInputValue(component, value) {
10 | focus(component);
11 | var inputNode = React.findDOMNode(component.refs.input);
12 | inputNode.value = value;
13 | TestUtils.Simulate.change(inputNode);
14 | }
15 |
16 | function blur(component) {
17 | component.blur();
18 | }
19 |
20 | function focus(component) {
21 | component.focus();
22 | }
23 |
24 | function hitEnter(component) {
25 | component.onKeyDown({keyCode: 13, preventDefault: noop});
26 | }
27 |
28 | function hitBackspace(component) {
29 | component.onKeyDown({keyCode: 8, preventDefault: noop});
30 | }
31 |
32 | function hitEscape(component) {
33 | component.onKeyDown({keyCode: 27, preventDefault: noop});
34 | }
35 |
36 | export default {
37 | renderComponent,
38 | changeInputValue,
39 | hitEnter,
40 | hitBackspace,
41 | blur,
42 | focus,
43 | hitEscape,
44 | ...TestUtils
45 | };
46 |
--------------------------------------------------------------------------------
/src/_utils/keyCodes.js:
--------------------------------------------------------------------------------
1 | export default {
2 | ENTER: 13,
3 | UP: 38,
4 | DOWN: 40,
5 | BACKSPACE: 8,
6 | ESC: 27
7 | };
8 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import TokenAutocomplete from './TokenAutocomplete';
2 |
3 | export default TokenAutocomplete;
4 |
--------------------------------------------------------------------------------
/tests/karma.conf.js:
--------------------------------------------------------------------------------
1 | var RewirePlugin = require('rewire-webpack');
2 | var webpack = require('webpack');
3 | var path = require('path');
4 |
5 | var CI = process.env.NODE_ENV !== 'development';
6 | var WATCH = !!process.env.WATCH_TESTS;
7 | var conf = {
8 | cache: true,
9 | devtool: 'inline-source-map',
10 | resolve: {
11 | modulesDirectories: ['node_modules'],
12 | extensions: ['', '.jsx', '.js'],
13 | alias: {
14 | TestUtils: path.join(__dirname, '../src/_tests/utils.js'),
15 | utils: path.join(__dirname, '../src/_utils/')
16 | }
17 | },
18 | module: {
19 | loaders: [
20 | { test: /\.(js?|jsx?)$/, exclude: /node_modules|(spec\.js)$/, loader: 'isparta?{babel: { stage: 0, plugins: ["babel-plugin-rewire"] } }' },
21 | { test: /spec\.js$/, exclude: /node_modules/, loader: 'babel-loader?stage=0&plugins=babel-plugin-rewire' }
22 | ]
23 | },
24 | plugins: [
25 | new RewirePlugin(),
26 | new webpack.PrefetchPlugin('react'),
27 | new webpack.PrefetchPlugin('radium'),
28 | new webpack.PrefetchPlugin('lodash')
29 | ],
30 | node: {
31 | net: 'empty',
32 | tls: 'empty',
33 | dns: 'empty',
34 | fs: 'empty'
35 | }
36 | };
37 |
38 | module.exports = function(config) {
39 | config.set({
40 | singleRun: !WATCH,
41 | autoWatch: true,
42 | browsers: [ CI ? 'Firefox' : 'PhantomJS2'],
43 | browserNoActivityTimeout: 60000,
44 | browserDisconnectTimeout: 10000,
45 | frameworks: ['mocha', 'chai', 'sinon'],
46 | reporters: ['mocha', 'coverage'],
47 | client: { mocha: { timeout: 5000 } },
48 | files: [ { pattern: '../tests/webpack.tests.js', watched: false } ],
49 | preprocessors: { '../tests/webpack.tests.js': ['webpack', 'sourcemap'] },
50 | webpack: conf,
51 | webpackServer: { noInfo: true },
52 | webpackMiddleware: { noInfo: true },
53 | coverageReporter: {
54 | dir: 'coverage/',
55 | reporters: [
56 | { type: 'lcov', subdir: 'report-lcov' },
57 | { type: 'text-summary' },
58 | { type: 'cobertura', subdir: 'cobertura'}
59 |
60 | ]
61 | }
62 | });
63 | };
64 |
--------------------------------------------------------------------------------
/tests/webpack.tests.js:
--------------------------------------------------------------------------------
1 |
2 | var context = require.context('../src', true, /spec\.js$/);
3 | context.keys().forEach(context);
4 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpackConf = require('peters-toolbelt').webpack;
3 |
4 | var plugins = [];
5 |
6 | var conf = new webpackConf({
7 | entry: './src',
8 | output: {
9 | path: path.join(__dirname, '/dist'),
10 | filename: 'index.js',
11 | library: 'TokenAutocomplete',
12 | libraryTarget: 'umd'
13 | },
14 | resolve: {
15 | alias: {
16 | utils: path.join(__dirname, 'src/_utils')
17 | },
18 | modulesDirectories: ['node_modules']
19 | },
20 | plugins: plugins
21 | })
22 | .iNeedReact()
23 | .iNeedWebFonts()
24 | .iNeedSCSS()
25 | .iNeedHotDevServer()
26 | .getConfig();
27 |
28 | /*RegExp.prototype.toJSON = RegExp.prototype.toString;
29 | console.log('CURRENT CONFIG', JSON.stringify(conf, null, 4));*/
30 |
31 | module.exports = conf;
32 |
--------------------------------------------------------------------------------