Merge pull request #690 from LautaroCesso/master

Add autocomplete component
This commit is contained in:
Guillermo Giuliana 2020-01-23 00:11:05 -03:00 committed by GitHub
commit e6466b76ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 989 additions and 76 deletions

View File

@ -59,10 +59,11 @@ class TicketViewer extends React.Component {
commentEdited: false,
commentPrivate: false,
edit: false,
editTitle: false,
editId: 0,
tagSelectorLoading: false,
editTitle: false,
newTitle: this.props.ticket.title,
editTitleError: false
editTitleError: false,
};
componentDidMount() {
@ -176,7 +177,14 @@ class TicketViewer extends React.Component {
onChange={this.onDepartmentDropdownChanged.bind(this)} />
</div>
<div className="col-md-4">{ticket.author.name}</div>
<div className="col-md-4"> <TagSelector items={this.props.tags} values={this.props.ticket.tags} onRemoveClick={this.removeTag.bind(this)} onTagSelected={this.addTag.bind(this)}/></div>
<div className="col-md-4">
<TagSelector
items={this.props.tags}
values={this.props.ticket.tags}
onRemoveClick={this.removeTag.bind(this)}
onTagSelected={this.addTag.bind(this)}
loading={this.state.tagSelectorLoading}/>
</div>
</div>
<div className="ticket-viewer__info-row-header row">
<div className="col-md-4">{i18n('PRIORITY')}</div>
@ -185,7 +193,11 @@ class TicketViewer extends React.Component {
</div>
<div className="ticket-viewer__info-row-values row">
<div className="col-md-4">
<DropDown className="ticket-viewer__editable-dropdown" items={priorityList} selectedIndex={priorities[ticket.priority]} onChange={this.onPriorityDropdownChanged.bind(this)} />
<DropDown
className="ticket-viewer__editable-dropdown"
items={priorityList}
selectedIndex={priorities[ticket.priority]}
onChange={this.onPriorityDropdownChanged.bind(this)} />
</div>
<div className="col-md-4">
{this.renderAssignStaffList()}
@ -510,23 +522,47 @@ class TicketViewer extends React.Component {
}
addTag(tag) {
this.setState({
tagSelectorLoading: true,
})
API.call({
path: '/ticket/add-tag',
data: {
ticketNumber: this.props.ticket.ticketNumber,
tagId: tag
}
}).then(this.onTicketModification.bind(this))
})
.then(() => {
this.setState({
tagSelectorLoading: false,
});
this.onTicketModification();
})
.catch(() => this.setState({
tagSelectorLoading: false,
}))
}
removeTag(tag) {
this.setState({
tagSelectorLoading: true,
});
API.call({
path: '/ticket/remove-tag',
data: {
ticketNumber: this.props.ticket.ticketNumber,
tagId: tag
}
}).then(this.onTicketModification.bind(this))
}).then(() => {
this.setState({
tagSelectorLoading: false,
});
this.onTicketModification();
}).catch(() => this.setState({
tagSelectorLoading: false,
}))
}
onCustomResponsesChanged({index}) {

View File

@ -265,4 +265,4 @@ let DemoPage = React.createClass({
}
});
export default DemoPage;
export default DemoPage;

View File

@ -0,0 +1,500 @@
// LIBS
const _ = require('lodash');
// MOCKS
const Tag = ReactMock();
const DropDown = ReactMock();
// COMPONENT
const Autocomplete = requireUnit('core-components/autocomplete', {
'core-components/drop-down': DropDown,
'core-components/tag': Tag,
});
const color = [
'red',
'cyan',
'blue',
'green',
];
let countries = ["Afghanistan","Åland Islands","Albania","Algeria","American Samoa","AndorrA","Angola","Anguilla","Antarctica","Antigua and Barbuda","Argentina","Armenia","Aruba","Australia","Austria","Azerbaijan","Bahamas","Bahrain","Bangladesh","Barbados","Belarus","Belgium","Belize","Benin","Bermuda","Bhutan","Bolivia","Bosnia and Herzegovina","Botswana","Bouvet Island","Brazil","British Indian Ocean Territory","Brunei Darussalam","Bulgaria","Burkina Faso","Burundi","Cambodia","Cameroon","Canada","Cape Verde","Cayman Islands","Central African Republic","Chad","Chile","China","Christmas Island","Cocos (Keeling) Islands","Colombia","Comoros","Congo","Congo, The Democratic Republic of the","Cook Islands","Costa Rica","Cote D'Ivoire","Croatia","Cuba","Cyprus","Czech Republic","Denmark","Djibouti","Dominica","Dominican Republic","Ecuador","Egypt","El Salvador","Equatorial Guinea","Eritrea","Estonia","Ethiopia","Falkland Islands (Malvinas)","Faroe Islands","Fiji","Finland","France","French Guiana","French Polynesia","French Southern Territories","Gabon","Gambia","Georgia","Germany","Ghana","Gibraltar","Greece","Greenland","Grenada","Guadeloupe","Guam","Guatemala","Guernsey","Guinea","Guinea-Bissau","Guyana","Haiti","Heard Island and Mcdonald Islands","Holy See (Vatican City State)","Honduras","Hong Kong","Hungary","Iceland","India","Indonesia","Iran, Islamic Republic Of","Iraq","Ireland","Isle of Man","Israel","Italy","Jamaica","Japan","Jersey","Jordan","Kazakhstan","Kenya","Kiribati","Korea, Democratic People'S Republic of","Korea, Republic of","Kuwait","Kyrgyzstan","Lao People'S Democratic Republic","Latvia","Lebanon","Lesotho","Liberia","Libyan Arab Jamahiriya","Liechtenstein","Lithuania","Luxembourg","Macao","Macedonia, The Former Yugoslav Republic of","Madagascar","Malawi","Malaysia","Maldives","Mali","Malta","Marshall Islands","Martinique","Mauritania","Mauritius","Mayotte","Mexico","Micronesia, Federated States of","Moldova, Republic of","Monaco","Mongolia","Montserrat","Morocco","Mozambique","Myanmar","Namibia","Nauru","Nepal","Netherlands","Netherlands Antilles","New Caledonia","New Zealand","Nicaragua","Niger","Nigeria","Niue","Norfolk Island","Northern Mariana Islands","Norway","Oman","Pakistan","Palau","Palestinian Territory, Occupied","Panama","Papua New Guinea","Paraguay","Peru","Philippines","Pitcairn","Poland","Portugal","Puerto Rico","Qatar","Reunion","Romania","Russian Federation","RWANDA","Saint Helena","Saint Kitts and Nevis","Saint Lucia","Saint Pierre and Miquelon","Saint Vincent and the Grenadines","Samoa","San Marino","Sao Tome and Principe","Saudi Arabia","Senegal","Serbia and Montenegro","Seychelles","Sierra Leone","Singapore","Slovakia","Slovenia","Solomon Islands","Somalia","South Africa","South Georgia and the South Sandwich Islands","Spain","Sri Lanka","Sudan","Suriname","Svalbard and Jan Mayen","Swaziland","Sweden","Switzerland","Syrian Arab Republic","Taiwan, Province of China","Tajikistan","Tanzania, United Republic of","Thailand","Timor-Leste","Togo","Tokelau","Tonga","Trinidad and Tobago","Tunisia","Turkey","Turkmenistan","Turks and Caicos Islands","Tuvalu","Uganda","Ukraine","United Arab Emirates","United Kingdom","United States","United States Minor Outlying Islands","Uruguay","Uzbekistan","Vanuatu","Venezuela","Viet Nam","Virgin Islands, British","Virgin Islands, U.S.","Wallis and Futuna","Western Sahara","Yemen","Zambia","Zimbabwe"];
countries = countries.map((name, index) => {
return {
id: index,
name: name.toLowerCase(),
content: name,
color: color[_.random(0, color.length-1)],
}
});
const timeout = function (f, t) {
return new Promise(function (res) {
setTimeout(function() {
res(f());
},t);
});
};
const searchApi = spy((query, blacklist = []) => {
const data = countries.filter(x => !_.includes(blacklist, x.id));
return new Promise((res,rej) => {
setTimeout(function () {
const result = data.filter(item => _.includes(item.name, query));
res(result.slice(0, 10));
}, query == 'brazilq' ? 100 : 50);
});
});
describe('Autocomplete component with external api', function () {
let selectedList = [], autocompleteInput, autocompleteDropdown, autocompleteWithExternalApi, tag;
function renderAutocomplete(props) {
selectedList = [];
autocompleteWithExternalApi = TestUtils.renderIntoDocument(
<Autocomplete
getItemListFromQuery={searchApi}
onChange={selectedListAutocomplete => selectedList = selectedListAutocomplete} />
);
autocompleteInput = TestUtils.scryRenderedDOMComponentsWithClass(autocompleteWithExternalApi, 'autocomplete__input')[0];
autocompleteDropdown = TestUtils.scryRenderedComponentsWithType(autocompleteWithExternalApi, DropDown)[0];
}
describe('writing in input', function() {
beforeEach(function() {
renderAutocomplete();
});
it('should open menu with list', function() {
expect(searchApi).to.have.been.calledWith("", selectedList.map(item => item.id));
expect(selectedList.length).to.equal(0);
autocompleteInput.value = "ho";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.loading).to.equal(true);
expect(selectedList.length).to.equal(0);
});
it('should select item if enter is pressed', function() {
expect(searchApi).to.have.been.calledWith("", selectedList.map(item => item.id));
expect(selectedList.length).to.equal(0);
autocompleteInput.value = "argentina";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.loading).to.equal(true);
return timeout(function() {
expect(autocompleteDropdown.props.loading).to.equal(false);
expect(searchApi).to.have.been.calledWith("argentina", selectedList.map(item => item.id));
expect(autocompleteDropdown.props.items.length).to.equal(1);
expect(autocompleteDropdown.props.items[0].name).to.equal("argentina");
expect(autocompleteDropdown.props.items[0].id).to.equal(10);
expect(selectedList.length).to.equal(0);
autocompleteDropdown.props.onChange({index: 0});
expect(autocompleteDropdown.props.opened).to.equal(false);
expect(searchApi).to.have.been.calledWith("", selectedList.map(item => item.id));
expect(selectedList.length).to.equal(1);
expect(selectedList[0].name).to.equal("argentina");
expect(selectedList[0].id).to.equal(10);
}, 360);
});
it('should sinc', function() {
expect(searchApi).to.have.been.calledWith("", selectedList.map(item => item.id));
expect(selectedList.length).to.equal(0);
autocompleteInput.value = "brazilq";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.loading).to.equal(true);
return timeout(function() {
expect(autocompleteDropdown.props.loading).to.equal(true);
expect(searchApi).to.have.not.been.calledWith("brazil", selectedList.map(item => item.id));
expect(selectedList.length).to.equal(0);
autocompleteInput.value = "brazil";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.loading).to.equal(true);
return timeout(function() {
expect(autocompleteDropdown.props.loading).to.equal(false);
expect(searchApi).to.have.been.calledWith("brazil", selectedList.map(item => item.id));
expect(autocompleteDropdown.props.items.length).to.equal(1);
expect(autocompleteDropdown.props.items[0].name).to.equal("brazil");
expect(autocompleteDropdown.props.items[0].id).to.equal(30);
expect(selectedList.length).to.equal(0);
autocompleteDropdown.props.onChange({index: 0});
expect(autocompleteDropdown.props.opened).to.equal(false);
expect(searchApi).to.have.been.calledWith("", selectedList.map(item => item.id));
expect(autocompleteDropdown.props.opened).to.equal(false);
expect(selectedList.length).to.equal(1);
expect(selectedList[0].name).to.equal("brazil");
expect(selectedList[0].id).to.equal(30);
autocompleteDropdown.props.onMenuToggle(true);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.items.length).to.equal(0);
}, 360);
}, 25);
});
it('should delete item if backspace is pressed and input value is ""', function() {
expect(searchApi).to.have.been.calledWith("", selectedList.map(item => item.id));
expect(selectedList.length).to.equal(0);
autocompleteInput.value = "ang";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.loading).to.equal(true);
return timeout(function() {
expect(autocompleteDropdown.props.loading).to.equal(false);
expect(searchApi).to.have.been.calledWith("ang", selectedList.map(item => item.id));
expect(autocompleteDropdown.props.items.length).to.equal(3);
expect(autocompleteDropdown.props.items[0].name).to.equal("angola");
expect(autocompleteDropdown.props.items[0].id).to.equal(6);
expect(selectedList.length).to.equal(0);
autocompleteDropdown.props.onChange({index: 0});
expect(autocompleteDropdown.props.opened).to.equal(false);
expect(searchApi).to.have.been.calledWith("", selectedList.map(item => item.id));
expect(selectedList.length).to.equal(1);
expect(selectedList[0].name).to.equal("angola");
expect(selectedList[0].id).to.equal(6);
autocompleteInput.value = "ang";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.loading).to.equal(true);
return timeout(function() {
expect(autocompleteDropdown.props.loading).to.equal(false);
expect(searchApi).to.have.been.calledWith("ang", selectedList.map(item => item.id));
expect(autocompleteDropdown.props.items.length).to.equal(2);
expect(autocompleteDropdown.props.items[0].name).to.equal("anguilla");
expect(autocompleteDropdown.props.items[0].id).to.equal(7);
expect(selectedList.length).to.equal(1);
autocompleteDropdown.props.onChange({index: 0});
expect(autocompleteDropdown.props.opened).to.equal(false);
expect(searchApi).to.have.been.calledWith("", selectedList.map(item => item.id));
expect(selectedList.length).to.equal(2);
expect(selectedList[0].name).to.equal("angola");
expect(selectedList[0].id).to.equal(6);
expect(selectedList[1].name).to.equal("anguilla");
expect(selectedList[1].id).to.equal(7);
expect(autocompleteDropdown.props.items.length).to.equal(1);
expect(autocompleteDropdown.props.items[0].name).to.equal("bangladesh");
expect(autocompleteDropdown.props.items[0].id).to.equal(18);
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
expect(searchApi).to.have.been.calledWith("", selectedList.map(item => item.id));
expect(selectedList.length).to.equal(1);
expect(selectedList[0].name).to.equal("angola");
expect(selectedList[0].id).to.equal(6);
autocompleteInput.value = "ang";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.loading).to.equal(true);
return timeout(function() {
expect(autocompleteDropdown.props.loading).to.equal(false);
expect(searchApi).to.have.been.calledWith("ang", selectedList.map(item => item.id));
expect(autocompleteDropdown.props.items.length).to.equal(2);
expect(autocompleteDropdown.props.items[0].name).to.equal("anguilla");
expect(autocompleteDropdown.props.items[0].id).to.equal(7);
expect(selectedList.length).to.equal(1);
}, 360);
},360);
}, 360);
});
it("should delete item if click is pressed", function() {
expect(searchApi).to.have.been.calledWith("", selectedList.map(item => item.id));
expect(selectedList.length).to.equal(0);
autocompleteInput.value = "ang";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.loading).to.equal(true);
return timeout(function () {
expect(autocompleteDropdown.props.loading).to.equal(false);
expect(searchApi).to.have.been.calledWith("ang", selectedList.map(item => item.id));
expect(autocompleteDropdown.props.items.length).to.equal(3);
expect(selectedList.length).to.equal(0);
autocompleteDropdown.props.onChange({index: 0});
expect(autocompleteDropdown.props.opened).to.equal(false);
expect(searchApi).to.have.been.calledWith("", selectedList.map(item => item.id));
expect(selectedList.length).to.equal(1);
expect(selectedList[0].name).to.equal("angola");
expect(selectedList[0].id).to.equal(6);
tag = TestUtils.scryRenderedComponentsWithType(autocompleteWithExternalApi, Tag)[0];
tag.props.onRemoveClick({ preventDefault: stub() });
expect(selectedList.length).to.equal(0);
autocompleteInput.value = "ang";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.loading).to.equal(true);
return timeout(function () {
expect(autocompleteDropdown.props.loading).to.equal(false);
expect(searchApi).to.have.been.calledWith("ang", selectedList.map(item => item.id));
expect(autocompleteDropdown.props.items.length).to.equal(3);
expect(autocompleteDropdown.props.items[0].name).to.equal("angola");
expect(autocompleteDropdown.props.items[0].id).to.equal(6);
expect(selectedList.length).to.equal(0);
}, 360);
}, 360);
});
});
describe('Autocomplete component with items', function() {
let selectedList = [], autocompleteInput, autocompleteDropdown, itemsList, autocomplete;
function renderAutocomplete(props) {
selectedList = [];
itemsList = [
{id: 45, name: 'lautaro', content: 'Lautaro.', color: 'gray'},
{id: 46, name: 'dsafa', content: 'dsafa', color: 'black'},
{id: 47, name: 'asdasdasd', content: 'asdasdasd.', color: 'red'},
{id: 48, name: '123123123', content: '123123123.', color: 'blue'},
{id: 49, name: 'hola', content: 'hola', color: 'green'},
];
autocomplete = TestUtils.renderIntoDocument(
<Autocomplete
items={itemsList}
onChange={selectedListAutocomplete => selectedList = selectedListAutocomplete} />
);
autocompleteInput = TestUtils.scryRenderedDOMComponentsWithClass(autocomplete, 'autocomplete__input')[0];
autocompleteDropdown = TestUtils.scryRenderedComponentsWithType(autocomplete, DropDown)[0];
}
beforeEach(function() {
renderAutocomplete();
});
it('should open menu with list', function() {
expect(selectedList.length).to.equal(0);
autocompleteInput.value = "la";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.items.length).to.equal(2);
});
it('should select item if enter is pressed', function() {
expect(selectedList.length).to.equal(0);
autocompleteInput.value = "la";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.items.length).to.equal(2);
expect(autocompleteDropdown.props.items[0].name).to.equal("lautaro");
expect(autocompleteDropdown.props.items[0].id).to.equal(45);
expect(autocompleteDropdown.props.items[1].name).to.equal("hola");
expect(autocompleteDropdown.props.items[1].id).to.equal(49);
autocompleteDropdown.props.onChange({index: 0});
expect(autocompleteDropdown.props.opened).to.equal(false);
expect(selectedList.length).to.equal(1);
expect(selectedList[0].name).to.equal("lautaro");
expect(selectedList[0].id).to.equal(45);
autocompleteInput.value = "";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.items.length).to.equal(4);
expect(autocompleteDropdown.props.items[0].name).to.equal("dsafa");
expect(autocompleteDropdown.props.items[0].id).to.equal(46);
expect(autocompleteDropdown.props.items[1].name).to.equal("asdasdasd");
expect(autocompleteDropdown.props.items[1].id).to.equal(47);
expect(autocompleteDropdown.props.items[2].name).to.equal("123123123");
expect(autocompleteDropdown.props.items[2].id).to.equal(48);
expect(autocompleteDropdown.props.items[3].name).to.equal("hola");
expect(autocompleteDropdown.props.items[3].id).to.equal(49);
autocompleteDropdown.props.onChange({index: 2});
expect(autocompleteDropdown.props.opened).to.equal(false);
expect(selectedList.length).to.equal(2);
expect(selectedList[0].name).to.equal("lautaro");
expect(selectedList[0].id).to.equal(45);
expect(selectedList[1].name).to.equal("123123123");
expect(selectedList[1].id).to.equal(48);
autocompleteInput.value = "";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.items.length).to.equal(3);
expect(autocompleteDropdown.props.items[0].name).to.equal("dsafa");
expect(autocompleteDropdown.props.items[0].id).to.equal(46);
expect(autocompleteDropdown.props.items[1].name).to.equal("asdasdasd");
expect(autocompleteDropdown.props.items[1].id).to.equal(47);
expect(autocompleteDropdown.props.items[2].name).to.equal("hola");
expect(autocompleteDropdown.props.items[2].id).to.equal(49);
autocompleteInput.value = "lau";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.items.length).to.equal(0);
autocompleteInput.value = "la";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.items.length).to.equal(1);
expect(autocompleteDropdown.props.items[0].name).to.equal("hola");
expect(autocompleteDropdown.props.items[0].id).to.equal(49);
autocompleteDropdown.props.onChange({index: 0});
expect(autocompleteDropdown.props.opened).to.equal(false);
expect(selectedList.length).to.equal(3);
expect(selectedList[0].name).to.equal("lautaro");
expect(selectedList[0].id).to.equal(45);
expect(selectedList[1].name).to.equal("123123123");
expect(selectedList[1].id).to.equal(48);
expect(selectedList[2].name).to.equal("hola");
expect(selectedList[2].id).to.equal(49);
});
it('should delete item if (backspace or click) is pressed and input value is "" ', function() {
expect(selectedList.length).to.equal(0);
autocompleteInput.value = "123";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.items.length).to.equal(1);
expect(autocompleteDropdown.props.items[0].name).to.equal("123123123");
expect(autocompleteDropdown.props.items[0].id).to.equal(48);
autocompleteDropdown.props.onChange({index: 0});
expect(autocompleteDropdown.props.opened).to.equal(false);
expect(selectedList.length).to.equal(1);
expect(selectedList[0].name).to.equal("123123123");
expect(selectedList[0].id).to.equal(48);
autocompleteInput.value = "la";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.items.length).to.equal(2);
expect(autocompleteDropdown.props.items[0].name).to.equal("lautaro");
expect(autocompleteDropdown.props.items[0].id).to.equal(45);
expect(autocompleteDropdown.props.items[1].name).to.equal("hola");
expect(autocompleteDropdown.props.items[1].id).to.equal(49);
autocompleteDropdown.props.onChange({index: 1});
expect(autocompleteDropdown.props.opened).to.equal(false);
expect(selectedList.length).to.equal(2);
expect(selectedList[0].name).to.equal("123123123");
expect(selectedList[0].id).to.equal(48);
expect(selectedList[1].name).to.equal("hola");
expect(selectedList[1].id).to.equal(49);
autocompleteInput.value = "l";
TestUtils.Simulate.change(autocompleteInput);
expect(autocompleteDropdown.props.opened).to.equal(true);
expect(autocompleteDropdown.props.items.length).to.equal(1);
expect(autocompleteDropdown.props.items[0].name).to.equal("lautaro");
expect(autocompleteDropdown.props.items[0].id).to.equal(45);
autocompleteDropdown.props.onChange({index: 0});
expect(autocompleteDropdown.props.opened).to.equal(false);
expect(selectedList.length).to.equal(3);
expect(selectedList[0].name).to.equal("123123123");
expect(selectedList[0].id).to.equal(48);
expect(selectedList[1].name).to.equal("hola");
expect(selectedList[1].id).to.equal(49);
expect(selectedList[2].name).to.equal("lautaro");
expect(selectedList[2].id).to.equal(45);
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
expect(selectedList.length).to.equal(2);
autocompleteInput.value = "asd";
TestUtils.Simulate.change(autocompleteInput);
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
expect(selectedList.length).to.equal(2);
expect(selectedList[0].name).to.equal("123123123");
expect(selectedList[0].id).to.equal(48);
expect(selectedList[1].name).to.equal("hola");
expect(selectedList[1].id).to.equal(49);
tag = TestUtils.scryRenderedComponentsWithType(autocomplete, Tag)[0];
tag.props.onRemoveClick({ preventDefault: stub() });
expect(selectedList.length).to.equal(1);
expect(selectedList[0].name).to.equal("hola");
expect(selectedList[0].id).to.equal(49);
autocompleteInput.value = "a";
TestUtils.Simulate.change(autocompleteInput);
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
expect(selectedList.length).to.equal(1);
autocompleteInput.value = "";
TestUtils.Simulate.change(autocompleteInput);
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
expect(selectedList.length).to.equal(0);
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
TestUtils.Simulate.keyDown(autocompleteInput, {key: 'backspace', keyCode: 8, which: 8});
expect(selectedList.length).to.equal(0);
});
});
});

View File

@ -0,0 +1,269 @@
import React from 'react';
import _ from 'lodash';
import keyCode from 'keycode';
import DropDown from 'core-components/drop-down';
import Tag from 'core-components/tag';
const ItemsSchema = React.PropTypes.arrayOf(React.PropTypes.shape({
id: React.PropTypes.number,
name: React.PropTypes.string,
content: React.PropTypes.node,
color: React.PropTypes.string,
}));
class Autocomplete extends React.Component {
static propTypes = {
items: ItemsSchema,
onChange: React.PropTypes.func,
values: ItemsSchema,
onRemoveClick: React.PropTypes.func,
onTagSelected: React.PropTypes.func,
getItemListFromQuery: React.PropTypes.func,
disabled: React.PropTypes.bool,
};
id = 1;
state = {
selectedItems: [],
inputValue: "",
opened: false,
highlightedIndex: 0,
itemsFromQuery: [],
loading: false,
};
componentDidMount() {
const { getItemListFromQuery, } = this.props;
this.setTimeout = _.throttle((query) => {
let id = ++this.id;
getItemListFromQuery(query, this.getSelectedItems().map(item => item.id))
.then(result => {
if(id === this.id)
this.setState({
itemsFromQuery: result,
loading: false,
});
})
.catch(() => this.setState({
loading: false,
}));
}, 300, {leading: false});
this.searchApi("");
}
render() {
let inputWidth = 0;
if(this.span) {
this.span.style.display = 'inline';
this.span.textContent = this.state.inputValue;
inputWidth = Math.ceil(this.span.getBoundingClientRect().width)-31;
this.span.style.display = 'none';
}
return (
<div className="autocomplete">
<label className="autocomplete__label" onClick={(e) => e.stopPropagation()}>
<DropDown
className="autocomplete__drop-down"
items={this.getDropdownList()}
size="large"
onChange={e => this.onChangeDropDown(e)}
onMenuToggle={e => this.onMenuToggle(e)}
opened={this.state.opened}
onHighlightedIndexChange={n => this.onHighlightedIndexChange(n)}
highlightedIndex={this.state.highlightedIndex}
loading={this.state.loading}
>
{this.renderSelectedItems()}
<input
className="autocomplete__input"
id="query"
ref={input => this.input = input}
value={this.state.inputValue}
onKeyDown={e => this.onKeyDown(e)}
onChange={e => this.onChangeInput(e.target.value)}
style={this.span ? {width: inputWidth} : {}} />
<span className="sizer" ref={span => this.span = span} />
</DropDown>
</label>
</div>
);
}
renderSelectedItems() {
return this.getSelectedItems().map(item => this.renderSelectedItem(item));
}
renderSelectedItem(item) {
return <Tag
name={item.name}
color={item.color}
showDeleteButton
onRemoveClick={this.onRemoveClick.bind(this,item.id)}
key={item.id} />
}
getDropdownList() {
const {
items,
} = this.props;
let dropdownList = [];
if(items !== undefined) {
const list = this.getUnselectedList(items, this.getSelectedItems());
dropdownList = list.filter(s => _.includes(s.name, this.state.inputValue));
} else {
dropdownList = this.getUnselectedList(this.state.itemsFromQuery, this.getSelectedItems());
}
return dropdownList;
}
getUnselectedList(list, selectedList) {
return list.filter(item => !_.some(selectedList, item));
}
getSelectedItems() {
const { values, } = this.props;
return (values !== undefined) ? values : this.state.selectedItems;
}
onRemoveClick(itemId, event) {
const {
onChange,
onRemoveClick,
} = this.props;
const newList = this.getSelectedItems().filter(item => item.id != itemId);
event.preventDefault();
this.setState({
selectedItems: newList,
opened: false,
highlightedIndex: 0,
});
onChange && onChange(newList);
onRemoveClick && onRemoveClick(itemId);
this.searchApi("", newList);
}
onChangeDropDown(e) {
const {
onChange,
onTagSelected,
} = this.props;
if(this.getDropdownList().length) {
const itemSelected = this.getDropdownList()[e.index];
const newList = [...this.getSelectedItems(), itemSelected];
this.setState({
selectedItems: newList,
inputValue: "",
highlightedIndex: 0,
opened: false,
});
onChange && onChange(newList);
onTagSelected && onTagSelected(itemSelected.id);
this.searchApi("", newList);
}
}
onChangeInput(str) {
const { getItemListFromQuery, } = this.props;
this.setState({
inputValue: str,
opened: true,
highlightedIndex: 0,
});
if(getItemListFromQuery !== undefined) {
this.setState({
loading: true,
});
this.setTimeout(str);
}
}
onMenuToggle(e) {
this.setState({
opened: e,
});
}
onHighlightedIndexChange(index) {
this.setState({
highlightedIndex: index,
});
}
onKeyDown(event) {
const {
onChange,
onRemoveClick,
} = this.props;
if(this.props.disabled) {
event.stopPropagation();
event.preventDefault();
return;
}
if(keyCode(event) === "space") {
event.stopPropagation();
}
if(keyCode(event) === "backspace" && this.state.inputValue === "") {
const lastSelectedItemsIndex = this.getSelectedItems().length-1;
const newList = this.getSelectedItems().slice(0, lastSelectedItemsIndex);
this.setState({
selectedItems: newList,
highlightedIndex: 0,
});
onChange && onChange(newList);
if(this.getSelectedItems().length) {
const itemId = this.getSelectedItems()[lastSelectedItemsIndex].id;
onRemoveClick && onRemoveClick(itemId);
}
this.searchApi("", newList);
}
}
searchApi(query, blacklist=this.getSelectedItems()) {
const { getItemListFromQuery, } = this.props;
if(getItemListFromQuery !== undefined) {
getItemListFromQuery(query, blacklist.map(item => item.id))
.then(result => {
this.setState({
itemsFromQuery: result,
loading: false,
});
})
.catch(() => {
this.setState({
loading: false,
});
});
}
}
}
export default Autocomplete;

View File

@ -0,0 +1,42 @@
@import "../scss/vars";
.autocomplete {
margin-bottom: 30px;
text-align: left;
&__drop-down {
.drop-down__current-item {
cursor: text;
background-color: $very-light-grey;
border: 1px solid $grey;
min-height: 38px;
&:focus {
outline: none;
border-color: $primary-blue;
}
}
}
&__label {
display: inline-block;
}
&__input {
display: inline-block;
border: 0;
background: transparent;
outline: none;
padding-left: 5px;
width: 10px;
max-width: 100%;
min-width: 10px;
}
.sizer {
line-height: 1.21428571em;
padding: .67857143em 2.1em .67857143em 1em;
display: none;
white-space: pre;
}
}

View File

@ -2,10 +2,11 @@ import React from 'react';
import _ from 'lodash';
import classNames from 'classnames';
import {Motion, spring} from 'react-motion';
import keyCode from 'keycode';
import keyCode from 'keycode';
import Menu from 'core-components/menu';
import Icon from 'core-components/icon';
import Loading from 'core-components/loading'
class DropDown extends React.Component {
@ -14,7 +15,11 @@ class DropDown extends React.Component {
selectedIndex: React.PropTypes.number,
items: Menu.propTypes.items,
onChange: React.PropTypes.func,
size: React.PropTypes.oneOf(['small', 'medium', 'large'])
size: React.PropTypes.oneOf(['small', 'medium', 'large']),
highlightedIndex: React.PropTypes.number,
onHighlightedIndexChange: React.PropTypes.func,
opened: React.PropTypes.bool,
onMenuToggle: React.PropTypes.func,
};
static defaultProps = {
@ -28,7 +33,7 @@ class DropDown extends React.Component {
menuId: _.uniqueId('drop-down-menu_'),
selectedIndex: props.selectedIndex || props.defaultSelectedIndex,
highlightedIndex: props.selectedIndex || props.defaultSelectedIndex,
opened: false
opened: false,
};
}
@ -44,7 +49,7 @@ class DropDown extends React.Component {
return {
defaultStyle: {opacity: 0, translateY: 20},
style: (this.state.opened) ? openedStyle : closedStyle
style: (this.getOpen()) ? openedStyle : closedStyle
};
}
@ -56,19 +61,29 @@ class DropDown extends React.Component {
<div {...this.getCurrentItemProps()}>
{this.props.children ? this.props.children : this.renderCurrentItem()}
</div>
<Motion defaultStyle={animation.defaultStyle} style={animation.style} onRest={this.onAnimationFinished.bind(this)}>
{this.renderList.bind(this)}
<Motion
defaultStyle={animation.defaultStyle}
style={animation.style}
onRest={this.onAnimationFinished.bind(this)}>
{this.renderList.bind(this)}
</Motion>
</div>
);
}
renderList({opacity, translateY}) {
let style = { opacity: opacity, transform: `translateY(${translateY}px)`};
let style = {
opacity: opacity,
transform: `translateY(${translateY}px)`
};
return (
<div className="drop-down__list-container" style={style}>
<Menu {...this.getMenuProps()} />
{this.props.loading ?
<div><Loading className='drop-down__loading' /></div> :
this.props.items.length ?
<Menu {...this.getMenuProps()} /> :
<div className='drop-down__empty-list'>Empty</div>}
</div>
);
}
@ -77,7 +92,6 @@ class DropDown extends React.Component {
let item = this.props.items[this.getSelectedIndex()];
let iconNode = null;
if (item.icon) {
iconNode = <Icon className="drop-down__current-item-icon" name={item.icon} />;
}
@ -90,12 +104,17 @@ class DropDown extends React.Component {
}
getClass() {
const {
className,
size,
} = this.props;
let classes = {
'drop-down': true,
'drop-down_closed': !this.state.opened,
'drop-down_closed': !this.getOpen(),
['drop-down_' + this.props.size]: (this.props.size),
[this.props.className]: (this.props.className)
['drop-down_' + size]: (size),
[className]: (className)
};
return classNames(classes);
@ -103,10 +122,10 @@ class DropDown extends React.Component {
getCurrentItemProps() {
return {
'aria-expanded': this.state.opened,
'aria-expanded': this.getOpen(),
'aria-autocomplete': 'list',
'aria-owns': this.state.menuId,
'aria-activedescendant': this.state.menuId + '__' + this.state.highlightedIndex,
'aria-activedescendant': this.state.menuId + '__' + this.getHighlightedIndex(),
className: 'drop-down__current-item',
onClick: this.handleClick.bind(this),
onKeyDown: this.onKeyDown.bind(this),
@ -123,7 +142,7 @@ class DropDown extends React.Component {
items: this.props.items,
onItemClick: this.handleItemClick.bind(this),
onMouseDown: this.handleListMouseDown.bind(this),
selectedIndex: this.state.highlightedIndex,
selectedIndex: this.getHighlightedIndex(),
role: 'listbox'
};
}
@ -138,26 +157,37 @@ class DropDown extends React.Component {
}
getKeyActions(event) {
const {highlightedIndex, opened} = this.state;
const highlightedIndex = this.getHighlightedIndex();
const opened = this.getOpen();
const itemsQuantity = this.props.items.length;
const {
onHighlightedIndexChange,
onMenuToggle,
} = this.props;
return {
'up': () => {
if (opened) {
const newHighlightedIndex = this.modulo(highlightedIndex - 1, itemsQuantity);
event.preventDefault();
this.setState({
highlightedIndex: this.modulo(highlightedIndex - 1, itemsQuantity)
highlightedIndex: newHighlightedIndex,
});
onHighlightedIndexChange && onHighlightedIndexChange(newHighlightedIndex);
}
},
'down': () => {
if (opened) {
const newHighlightedIndex = this.modulo(highlightedIndex + 1, itemsQuantity);
event.preventDefault();
this.setState({
highlightedIndex: this.modulo(highlightedIndex + 1, itemsQuantity)
highlightedIndex: newHighlightedIndex,
});
onHighlightedIndexChange && onHighlightedIndexChange(newHighlightedIndex);
}
},
'enter': () => {
@ -167,6 +197,8 @@ class DropDown extends React.Component {
this.setState({
opened: true
});
onMenuToggle && onMenuToggle(true);
}
},
'space': () => {
@ -175,32 +207,44 @@ class DropDown extends React.Component {
this.setState({
opened: true
});
onMenuToggle && onMenuToggle(true);
},
'esc': () => {
this.setState({
opened: false
});
onMenuToggle && onMenuToggle(false);
},
'tab': () => {
if (this.state.opened) {
if (this.getOpen()) {
event.preventDefault();
this.onIndexSelected(highlightedIndex)
onHighlightedIndexChange && this.onIndexSelected(highlightedIndex);
}
}
};
}
handleBlur() {
const { onMenuToggle, } = this.props;
this.setState({
opened: false
});
onMenuToggle && onMenuToggle(false);
}
handleClick() {
const { onMenuToggle, } = this.props;
this.setState({
opened: !this.state.opened
opened: !this.getOpen()
});
onMenuToggle && onMenuToggle(!this.getOpen());
}
handleItemClick(index) {
@ -208,16 +252,25 @@ class DropDown extends React.Component {
}
onIndexSelected(index) {
this.setState({
opened: false,
selectedIndex: index,
highlightedIndex: index
});
const {
onMenuToggle,
onHighlightedIndexChange,
onChange,
loading,
} = this.props;
if (this.props.onChange) {
this.props.onChange({
index
if (!loading){
this.setState({
opened: false,
selectedIndex: index,
highlightedIndex: index
});
onHighlightedIndexChange && onHighlightedIndexChange(index);
onMenuToggle && onMenuToggle(false);
onChange && onChange({ index });
}
}
@ -226,20 +279,37 @@ class DropDown extends React.Component {
}
onAnimationFinished() {
if (!this.state.opened && this.state.highlightedIndex !== this.getSelectedIndex()) {
const { onHighlightedIndexChange, } = this.props;
if (!this.getOpen() && this.getHighlightedIndex() !== this.getSelectedIndex()) {
this.setState({
highlightedIndex: this.getSelectedIndex()
highlightedIndex: this.getSelectedIndex(),
});
onHighlightedIndexChange && onHighlightedIndexChange(this.getSelectedIndex());
}
}
getSelectedIndex() {
return (this.props.selectedIndex !== undefined) ? this.props.selectedIndex : this.state.selectedIndex;
const { selectedIndex, } = this.props;
return (selectedIndex !== undefined) ? selectedIndex : this.state.selectedIndex;
}
modulo(number, mod) {
return ((number % mod) + mod) % mod;
}
getOpen(){
const { opened, } = this.props;
return (opened !== undefined) ? opened : this.state.opened;
}
getHighlightedIndex() {
const { highlightedIndex, } = this.props;
return (highlightedIndex !== undefined) ? highlightedIndex : this.state.highlightedIndex;
}
}
export default DropDown;

View File

@ -28,6 +28,25 @@
position: absolute;
width: 150px;
z-index: 100;
background-color: white;
height: 40px;
}
&__loading {
padding-top: 12.5%;
.loading__icon {
border-color: rgba(150, 150, 150, 0.2);
border-left-color: $primary-red;
}
}
&__empty-list {
padding: 10px;
background-color: white;
color: $dark-grey;
font-style: italic;
cursor: default;
}
&_closed {

View File

@ -1,8 +1,6 @@
import React from 'react';
import _ from 'lodash';
import Icon from 'core-components/icon';
import DropDown from 'core-components/drop-down';
import Tag from 'core-components/tag';
import Autocomplete from 'core-components/autocomplete';
class TagSelector extends React.Component {
@ -13,55 +11,34 @@ class TagSelector extends React.Component {
})),
values: React.PropTypes.arrayOf(React.PropTypes.string),
onRemoveClick: React.PropTypes.func,
onTagSelected: React.PropTypes.func,
loading: React.PropTypes.bool,
};
render() {
const items = this.props.items.map(tag => ({...tag, content: this.renderTagOption(tag)}));
const values = items.filter(item => _.includes(this.props.values, item.name));
return (
<div className="tag-selector">
<DropDown className="tag-selector__drop-down" items={this.renderTagOptions().map(tag => ({content: tag}))} selectedIndex={-1} size="large">
{this.renderSelectedTags()}
</DropDown>
<Autocomplete
items={items}
values={values}
onRemoveClick={this.props.onRemoveClick}
onTagSelected={this.props.onTagSelected}
disabled={this.props.loading} />
</div>
);
}
renderSelectedTags() {
const itemList = this.props.values.map(value => _.find(this.props.items, {name:value}));
return itemList.map(this.renderSelectedTag.bind(this));
}
renderSelectedTag(item,index) {
return <Tag name={item.name} color={item.color} showDeleteButton onRemoveClick={this.onRemoveClick.bind(this,item.id)} key={index}/>;
}
renderTagOptions() {
const itemList = _.filter(this.props.items,(item) => !_.includes(this.props.values,item.name));
return itemList.map(this.renderTagOption.bind(this));
}
renderTagOption(item,index) {
renderTagOption(item) {
return (
<div onClick={this.onTagSelected.bind(this,item.id)} className="tag-selector__tag-option" key={index}>
<div className="tag-selector__tag-option" key={`tag-option-${item.id}`}>
<span className="tag-selector__tag-option-square" style={{backgroundColor:item.color}}/>
<span className="tag-selector__tag-option-name" >{item.name}</span>
</div>
);
}
onRemoveClick(tagId) {
if(this.props.onRemoveClick){
this.props.onRemoveClick(tagId);
}
}
onTagSelected(tagId) {
if(this.props.onTagSelected){
this.props.onTagSelected(tagId);
}
}
}
export default TagSelector;