diff --git a/CHANGELOG.md b/CHANGELOG.md index 6880322d5..8a4a40904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Master +- [FEATURE] Now you can type in closed single selects and automatically select the first match as you type. + The typed text is erased after one second. It doesn't work in closed multiple select without searchbox (what would be the correct behavior?) +- [FEATURE] Now you can type in opened selects witout searcbox (multiple or single) to highlight the + first marching option as you type. The typed text is erased after one second. - [FEATURE] Search can be disabled in multiple selects, instead of only in single selects. - [BUGFIX] Pressing enter in a select without searchbox correctly selects the highlighted element diff --git a/addon/components/power-select-multiple.js b/addon/components/power-select-multiple.js index 694c92094..88f5c3237 100644 --- a/addon/components/power-select-multiple.js +++ b/addon/components/power-select-multiple.js @@ -51,6 +51,9 @@ export default Ember.Component.extend({ } else { select.actions.close(e); } + } else if (!select.isOpen && e.keyCode >= 48 && e.keyCode <= 90 || e.keyCode === 32) { // Keys 0-9, a-z or SPACE + // Closed multiple selects should not do anything when typing on them + e.preventDefault(); } } }, diff --git a/addon/components/power-select.js b/addon/components/power-select.js index 64a17916e..4607d87be 100644 --- a/addon/components/power-select.js +++ b/addon/components/power-select.js @@ -39,6 +39,7 @@ export default Ember.Component.extend({ // Attrs searchText: '', lastSearchedText: '', + expirableSearchText: '', activeSearch: null, openingEvent: null, loading: false, @@ -53,6 +54,7 @@ export default Ember.Component.extend({ willDestroy() { this._super(...arguments); this.activeSearch = null; + run.cancel(this.expirableSearchDebounceId); }, // CPs @@ -169,7 +171,6 @@ export default Ember.Component.extend({ e.preventDefault(); const newHighlighted = this.advanceSelectableOption(this.get('highlighted'), e.keyCode === 40 ? 1 : -1); this.send('highlight', dropdown, newHighlighted, e); - run.scheduleOnce('afterRender', this, this.scrollIfHighlightedIsOutOfViewport); } else { dropdown.actions.open(e); } @@ -177,6 +178,18 @@ export default Ember.Component.extend({ this.send('choose', dropdown, this.get('highlighted'), e); } else if (e.keyCode === 9 || e.keyCode === 27) { // Tab or ESC dropdown.actions.close(e); + } else if (e.keyCode >= 48 && e.keyCode <= 90 || e.keyCode === 32) { // Keys 0-9, a-z or SPACE + let term = this.get('expirableSearchText') + String.fromCharCode(e.keyCode); + this.set('expirableSearchText', term); + this.expirableSearchDebounceId = run.debounce(this, 'set', 'expirableSearchText', '', 1000); + let firstMatch = this.filter(this.get('results'), term)[0]; // TODO: match only words starting with this substr? + if (firstMatch !== undefined) { + if (dropdown.isOpen) { + this._doHighlight(dropdown, firstMatch, e); + } else { + this._doSelect(dropdown, firstMatch, e); + } + } } }, @@ -298,6 +311,7 @@ export default Ember.Component.extend({ _doHighlight(dropdown, option) { if (option && get(option, 'disabled')) { return; } + run.scheduleOnce('afterRender', this, this.scrollIfHighlightedIsOutOfViewport); this.set('currentlyHighlighted', option); }, diff --git a/tests/dummy/app/templates/legacy-demo.hbs b/tests/dummy/app/templates/legacy-demo.hbs index 2a080b766..37284c60e 100644 --- a/tests/dummy/app/templates/legacy-demo.hbs +++ b/tests/dummy/app/templates/legacy-demo.hbs @@ -2,6 +2,26 @@

Welcome to the demo of ember-power-select (provisional name)

+

Select of strings with value tracking

+ {{#power-select options=(readonly simpleOptions) selected=(readonly simpleSelected) onchange=(action (mut simpleSelected)) as |option|}} + {{option}} + {{/power-select}} + +

Select of strings with value tracking

+ {{#power-select searchEnabled=false options=(readonly simpleOptions) selected=(readonly simpleSelected) onchange=(action (mut simpleSelected)) as |option|}} + {{option}} + {{/power-select}} + +

Multiple without search

+ {{#power-select searchEnabled=false options=(readonly simpleOptions) selected=(readonly someNumbers) onchange=(action (mut someNumbers)) as |option|}} + {{option}} + {{/power-select}} + + +{{!-- {{#power-select-multiple options=complexOptions selected=choosenCountry onchange=(action (mut choosenCountry)) searchField='name' as |country|}} + {{country.name}} + {{/power-select-multiple}} +

Trying to reproduce bug with store.findAll('user')

{{#power-select options=model selected=selectedUser onchange=(action (mut selectedUser)) as |user|}} @@ -178,6 +198,7 @@ {{opt}} {{/power-select}} + --}}


diff --git a/tests/integration/components/power-select/keyboard-control-test.js b/tests/integration/components/power-select/keyboard-control-test.js index 2f3cf70df..9a2cb2e86 100644 --- a/tests/integration/components/power-select/keyboard-control-test.js +++ b/tests/integration/components/power-select/keyboard-control-test.js @@ -332,3 +332,120 @@ test('in multiple-mode if the users calls preventDefault on the event received i triggerKeydown($('.ember-power-select-trigger-multiple-input')[0], 13); assert.equal($('.ember-power-select-dropdown').length, 1, 'The select is still opened'); }); + +test('Typing on a closed single select selects the value that matches the string typed so far', function(assert) { + assert.expect(3); + + this.numbers = numbers; + this.render(hbs` + {{#power-select options=numbers selected=selected onchange=(action (mut selected)) as |option|}} + {{option}} + {{/power-select}} + `); + + let trigger = this.$('.ember-power-select-trigger')[0]; + trigger.focus(); + assert.equal($('.ember-power-select-dropdown').length, 0, 'The dropdown is closed'); + triggerKeydown(trigger, 78); // n + triggerKeydown(trigger, 73); // i + triggerKeydown(trigger, 78); // n + assert.equal(trigger.textContent.trim(), 'nine', '"nine" has been selected'); + assert.equal($('.ember-power-select-dropdown').length, 0, 'The dropdown is still closed'); +}); + +// +// I'm actually not sure what multiple selects closed should do when typing on them. +// For now they just do nothing +// +// test('Typing on a closed multiple select with no searchbox does nothing', function(assert) { +// }); + +test('Typing on a opened single select highlights the value that matches the string typed so far, scrolling if needed', function(assert) { + assert.expect(4); + + this.numbers = numbers; + this.render(hbs` + {{#power-select options=numbers selected=selected onchange=(action (mut selected)) as |option|}} + {{option}} + {{/power-select}} + `); + + let trigger = this.$('.ember-power-select-trigger')[0]; + clickTrigger(); + assert.equal($('.ember-power-select-dropdown').length, 1, 'The dropdown is open'); + triggerKeydown(trigger, 78); // n + triggerKeydown(trigger, 73); // i + triggerKeydown(trigger, 78); // n + assert.equal(trigger.textContent.trim(), '', 'nothing has been selected'); + assert.equal($('.ember-power-select-option[aria-current=true]').text().trim(), 'nine', 'The option containing "nine" has been highlighted'); + assert.equal($('.ember-power-select-dropdown').length, 1, 'The dropdown is still closed'); +}); + +test('Typing on a opened multiple select highlights the value that matches the string typed so far, scrolling if needed', function(assert) { + assert.expect(4); + + this.numbers = numbers; + this.render(hbs` + {{#power-select-multiple options=numbers selected=selected onchange=(action (mut selected)) as |option|}} + {{option}} + {{/power-select-multiple}} + `); + + let trigger = this.$('.ember-power-select-trigger')[0]; + clickTrigger(); + assert.equal($('.ember-power-select-dropdown').length, 1, 'The dropdown is open'); + triggerKeydown(trigger, 78); // n + triggerKeydown(trigger, 73); // i + triggerKeydown(trigger, 78); // n + assert.equal(trigger.textContent.trim(), '', 'nothing has been selected'); + assert.equal($('.ember-power-select-option[aria-current=true]').text().trim(), 'nine', 'The option containing "nine" has been highlighted'); + assert.equal($('.ember-power-select-dropdown').length, 1, 'The dropdown is still closed'); +}); + +test('The typed string gets reset after 1s idle', function(assert) { + let done = assert.async(); + assert.expect(5); + + this.numbers = numbers; + this.render(hbs` + {{#power-select options=numbers selected=selected onchange=(action (mut selected)) as |option|}} + {{option}} + {{/power-select}} + `); + + let trigger = this.$('.ember-power-select-trigger')[0]; + trigger.focus(); + assert.equal($('.ember-power-select-dropdown').length, 0, 'The dropdown is closed'); + triggerKeydown(trigger, 84); // t + triggerKeydown(trigger, 87); // w + assert.equal(trigger.textContent.trim(), 'two', '"two" has been selected'); + assert.equal($('.ember-power-select-dropdown').length, 0, 'The dropdown is still closed'); + setTimeout(function() { + triggerKeydown(trigger, 79); // o + assert.equal(trigger.textContent.trim(), 'one', '"one" has been selected, instead of "two", because the typing started over'); + assert.equal($('.ember-power-select-dropdown').length, 0, 'The dropdown is still closed'); + done(); + }, 1100); +}); + +test('Type something that doesn\'t give you any result leaves the current selection', function(assert) { + assert.expect(3); + + this.numbers = numbers; + this.render(hbs` + {{#power-select options=numbers selected=selected onchange=(action (mut selected)) as |option|}} + {{option}} + {{/power-select}} + `); + + let trigger = this.$('.ember-power-select-trigger')[0]; + trigger.focus(); + assert.equal(trigger.textContent.trim(), '', 'nothing is selected'); + triggerKeydown(trigger, 78); // n + triggerKeydown(trigger, 73); // i + triggerKeydown(trigger, 78); // n + triggerKeydown(trigger, 69); // e + assert.equal(trigger.textContent.trim(), 'nine', 'nine has been selected'); + triggerKeydown(trigger, 87); // w + assert.equal(trigger.textContent.trim(), 'nine', 'nine is still selected because "ninew" gave no results'); +}); \ No newline at end of file