Skip to content

Commit

Permalink
Merge pull request #316 from cibernox/focused-keyboard-selection
Browse files Browse the repository at this point in the history
[FEATURE] Typing in focused selects highlight or select the first match as native selects do
  • Loading branch information
cibernox committed Feb 11, 2016
2 parents 26b1905 + 90b22a7 commit 207d7d5
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
3 changes: 3 additions & 0 deletions addon/components/power-select-multiple.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
},
Expand Down
16 changes: 15 additions & 1 deletion addon/components/power-select.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default Ember.Component.extend({
// Attrs
searchText: '',
lastSearchedText: '',
expirableSearchText: '',
activeSearch: null,
openingEvent: null,
loading: false,
Expand All @@ -53,6 +54,7 @@ export default Ember.Component.extend({
willDestroy() {
this._super(...arguments);
this.activeSearch = null;
run.cancel(this.expirableSearchDebounceId);
},

// CPs
Expand Down Expand Up @@ -169,14 +171,25 @@ 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);
}
} else if (dropdown.isOpen && e.keyCode === 13) { // ENTER
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);
}
}
}
},

Expand Down Expand Up @@ -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);
},

Expand Down
21 changes: 21 additions & 0 deletions tests/dummy/app/templates/legacy-demo.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@
<input type="text" value="sample input">
<h2 id="title">Welcome to the demo of ember-power-select (provisional name)</h2>

<h4>Select of strings with value tracking</h4>
{{#power-select options=(readonly simpleOptions) selected=(readonly simpleSelected) onchange=(action (mut simpleSelected)) as |option|}}
{{option}}
{{/power-select}}

<h4>Select of strings with value tracking</h4>
{{#power-select searchEnabled=false options=(readonly simpleOptions) selected=(readonly simpleSelected) onchange=(action (mut simpleSelected)) as |option|}}
{{option}}
{{/power-select}}

<h4>Multiple without search</h4>
{{#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}}
<p><strong>Trying to reproduce bug with store.findAll('user')</strong></p>
{{#power-select options=model selected=selectedUser onchange=(action (mut selectedUser)) as |user|}}
Expand Down Expand Up @@ -178,6 +198,7 @@
{{opt}}
{{/power-select}}
</div>
--}}
<br>
<br>
<br>
Expand Down
117 changes: 117 additions & 0 deletions tests/integration/components/power-select/keyboard-control-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

0 comments on commit 207d7d5

Please sign in to comment.