FEATURE: improves keyboard handling of select-box

- arrow keys
- escape key
This commit is contained in:
Joffrey JAFFEUX
2017-09-10 19:12:03 +02:00
committed by GitHub
parent a11eec47bb
commit 4b558638c8
7 changed files with 206 additions and 31 deletions

View File

@ -26,6 +26,12 @@ export default SelectBoxComponent.extend({
this.set("content", this.get("categories")); this.set("content", this.get("categories"));
this._scopeCategories(); this._scopeCategories();
} }
if (Ember.isNone(this.get("value"))) {
if (this.siteSettings.allow_uncategorized_topics && this.get("allowUncategorized") !== false) {
this.set("value", Category.findUncategorized().id);
}
}
}, },
filterFunction: function(content) { filterFunction: function(content) {

View File

@ -23,6 +23,7 @@ export default Ember.Component.extend({
clearable: false, clearable: false,
value: null, value: null,
highlightedValue: null,
selectedContent: null, selectedContent: null,
noContentLabel: I18n.t("select_box.no_content"), noContentLabel: I18n.t("select_box.no_content"),
clearSelectionLabel: null, clearSelectionLabel: null,
@ -68,7 +69,15 @@ export default Ember.Component.extend({
shouldHighlightRow: function() { shouldHighlightRow: function() {
return (rowComponent) => { return (rowComponent) => {
const id = this._castInteger(rowComponent.get(`content.${this.get("idKey")}`)); const id = this._castInteger(rowComponent.get(`content.${this.get("idKey")}`));
return id === this.get("value"); return id === this.get("highlightedValue");
};
},
@computed("value", "idKey")
shouldSelectRow(value, idKey) {
return (rowComponent) => {
const id = this._castInteger(rowComponent.get(`content.${idKey}`));
return id === value;
}; };
}, },
@ -150,7 +159,7 @@ export default Ember.Component.extend({
@on("willDestroyElement") @on("willDestroyElement")
_removeDocumentListeners: function() { _removeDocumentListeners: function() {
$(document).off("click.select-box", "keydown.select-box"); $(document).off("click.select-box");
$(window).off("resize.select-box"); $(window).off("resize.select-box");
}, },
@ -195,8 +204,45 @@ export default Ember.Component.extend({
} }
}, },
keyDown(event) {
const keyCode = event.keyCode || event.which;
if (this.get("expanded")) {
if (keyCode === 9) {
this.set("expanded", false);
}
if (keyCode === 27) {
this.set("expanded", false);
event.stopPropagation();
}
if (keyCode === 13 && Ember.isPresent(this.get("highlightedValue"))) {
event.preventDefault();
this.setProperties({
value: this._castInteger(this.get("highlightedValue")),
expanded: false
});
}
if (keyCode === 38) {
event.preventDefault();
const self = this;
Ember.run.throttle(self, this._handleUpArrow, 50);
}
if (keyCode === 40) {
event.preventDefault();
const self = this;
Ember.run.throttle(self, this._handleDownArrow, 50);
}
}
},
@on("didRender") @on("didRender")
_setupDocumentListeners: function() { _setupDocumentListeners: function() {
$(document).off("click.select-box");
$(document) $(document)
.on("click.select-box", (event) => { .on("click.select-box", (event) => {
if (this.isDestroying || this.isDestroyed) { return; } if (this.isDestroying || this.isDestroyed) { return; }
@ -207,13 +253,6 @@ export default Ember.Component.extend({
if (!$target.closest($element).length) { if (!$target.closest($element).length) {
this.set("expanded", false); this.set("expanded", false);
} }
})
.on("keydown.select-box", (event) => {
const keyCode = event.keyCode || event.which;
if (this.get("expanded") && keyCode === 9) {
this.set("expanded", false);
}
}); });
$(window).on("resize.select-box", () => this.set("expanded", false) ); $(window).on("resize.select-box", () => this.set("expanded", false) );
@ -234,12 +273,7 @@ export default Ember.Component.extend({
if (keyCode === 13 || keyCode === 40) { if (keyCode === 13 || keyCode === 40) {
this.setProperties({ expanded: true, focused: false }); this.setProperties({ expanded: true, focused: false });
return false; event.stopPropagation();
}
if (keyCode === 27) {
this.$(".select-box-offscreen").blur();
return false;
} }
if (keyCode >= 65 && keyCode <= 90) { if (keyCode >= 65 && keyCode <= 90) {
@ -254,7 +288,7 @@ export default Ember.Component.extend({
@observes("expanded") @observes("expanded")
_expandedChanged: function() { _expandedChanged: function() {
if (this.get("expanded")) { if (this.get("expanded")) {
this.setProperties({ focused: false, renderBody: true }); this.setProperties({ highlightedValue: null, renderBody: true, focused: false });
if (this.get("filterable")) { if (this.get("filterable")) {
Ember.run.schedule("afterRender", () => this.$(".filter-query").focus()); Ember.run.schedule("afterRender", () => this.$(".filter-query").focus());
@ -313,6 +347,11 @@ export default Ember.Component.extend({
this.set("filter", filter); this.set("filter", filter);
}, },
onHoverRow(content) {
const id = this._castInteger(content[this.get("idKey")]);
this.set("highlightedValue", id);
},
onSelectRow(content) { onSelectRow(content) {
this.setProperties({ this.setProperties({
value: this._castInteger(content[this.get("idKey")]), value: this._castInteger(content[this.get("idKey")]),
@ -369,5 +408,45 @@ export default Ember.Component.extend({
}); });
this.get("scrollableParent").off("scroll.select-box"); this.get("scrollableParent").off("scroll.select-box");
},
_handleDownArrow() {
this._handleArrow("down");
},
_handleUpArrow() {
this._handleArrow("up");
},
_handleArrow(direction) {
const content = this.get("filteredContent");
const idKey = this.get("idKey");
const selectedContent = content.findBy(idKey, this.get("highlightedValue"));
const currentIndex = content.indexOf(selectedContent);
if (direction === "down") {
if (currentIndex < 0) {
this.set("highlightedValue", this._castInteger(content[0][idKey]));
} else if(currentIndex + 1 < content.length) {
this.set("highlightedValue", this._castInteger(content[currentIndex + 1][idKey]));
}
} else {
if (currentIndex <= 0) {
this.set("highlightedValue", this._castInteger(content[0][idKey]));
} else if(currentIndex - 1 < content.length) {
this.set("highlightedValue", this._castInteger(content[currentIndex - 1][idKey]));
}
}
Ember.run.schedule("afterRender", () => {
const $highlightedRow = this.$(".select-box-row.is-highlighted");
if ($highlightedRow.length === 0) { return; }
const $collection = this.$(".select-box-collection");
const rowOffset = $highlightedRow.offset();
const bodyOffset = $collection.offset();
$collection.scrollTop(rowOffset.top - bodyOffset.top);
});
} }
}); });

View File

@ -9,7 +9,7 @@ export default Ember.Component.extend({
attributeBindings: ["title"], attributeBindings: ["title"],
classNameBindings: ["isHighlighted:is-highlighted"], classNameBindings: ["isHighlighted:is-highlighted", "isSelected:is-selected"],
@computed("titleForRow") @computed("titleForRow")
title(titleForRow) { title(titleForRow) {
@ -21,11 +21,16 @@ export default Ember.Component.extend({
return templateForRow(this); return templateForRow(this);
}, },
@computed("shouldHighlightRow", "value") @computed("shouldHighlightRow", "highlightedValue")
isHighlighted(shouldHighlightRow) { isHighlighted(shouldHighlightRow) {
return shouldHighlightRow(this); return shouldHighlightRow(this);
}, },
@computed("shouldSelectRow", "value")
isSelected(shouldSelectRow) {
return shouldSelectRow(this);
},
mouseEnter() { mouseEnter() {
this.sendAction("onHover", this.get("content")); this.sendAction("onHover", this.get("content"));
}, },

View File

@ -35,10 +35,13 @@
selectBoxRowComponent=selectBoxRowComponent selectBoxRowComponent=selectBoxRowComponent
templateForRow=templateForRow templateForRow=templateForRow
shouldHighlightRow=shouldHighlightRow shouldHighlightRow=shouldHighlightRow
shouldSelectRow=shouldSelectRow
titleForRow=titleForRow titleForRow=titleForRow
onSelectRow=(action "onSelectRow") onSelectRow=(action "onSelectRow")
onHoverRow=(action "onHoverRow")
onClearSelection=(action "onClearSelection") onClearSelection=(action "onClearSelection")
noContentLabel=noContentLabel noContentLabel=noContentLabel
highlightedValue=highlightedValue
value=value value=value
}} }}
{{/if}} {{/if}}

View File

@ -11,7 +11,10 @@
templateForRow=templateForRow templateForRow=templateForRow
titleForRow=titleForRow titleForRow=titleForRow
shouldHighlightRow=shouldHighlightRow shouldHighlightRow=shouldHighlightRow
shouldSelectRow=shouldSelectRow
highlightedValue=highlightedValue
onSelect=onSelectRow onSelect=onSelectRow
onHover=onHoverRow
value=value value=value
}} }}
{{else}} {{else}}

View File

@ -161,11 +161,15 @@
} }
&.is-highlighted { &.is-highlighted {
background: $tertiary-low;
}
&.is-selected {
background: $highlight-medium; background: $highlight-medium;
} }
&:hover { &.is-selected.is-highlighted {
background: $highlight-medium; background: $tertiary-low;
} }
} }
@ -194,12 +198,8 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
&:hover .select-box-row.is-highlighted {
background: none;
}
&:hover .select-box-row.is-highlighted:hover { &:hover .select-box-row.is-highlighted:hover {
background: $highlight-medium; background: $tertiary-low;
} }
} }

View File

@ -33,7 +33,7 @@ componentTest('accepts a value by reference', {
andThen(() => { andThen(() => {
assert.equal( assert.equal(
find(".select-box-row.is-highlighted .text").html().trim(), "robin", find(".select-box-row.is-selected .text").html().trim(), "robin",
"it highlights the row corresponding to the value" "it highlights the row corresponding to the value"
); );
}); });
@ -168,7 +168,7 @@ componentTest('accepts custom id/text keys', {
click(".select-box-header"); click(".select-box-header");
andThen(() => { andThen(() => {
assert.equal(find(".select-box-row.is-highlighted .text").html().trim(), "robin"); assert.equal(find(".select-box-row.is-selected .text").html().trim(), "robin");
}); });
} }
}); });
@ -268,7 +268,7 @@ componentTest('supports converting select value to integer', {
click(".select-box-header"); click(".select-box-header");
andThen(() => { andThen(() => {
assert.equal(find(".select-box-row.is-highlighted .text").text(), "régis"); assert.equal(find(".select-box-row.is-selected .text").text(), "régis");
}); });
andThen(() => { andThen(() => {
@ -277,7 +277,7 @@ componentTest('supports converting select value to integer', {
}); });
andThen(() => { andThen(() => {
assert.equal(find(".select-box-row.is-highlighted .text").text(), "jeff", "it works with dynamic content"); assert.equal(find(".select-box-row.is-selected .text").text(), "jeff", "it works with dynamic content");
}); });
} }
}); });
@ -339,3 +339,82 @@ componentTest('supports custom row title', {
}); });
} }
}); });
componentTest('supports keyboard events', {
template: '{{select-box content=content}}',
beforeEach() {
this.set("content", [{ id: 1, text: "robin" }, { id: 2, text: "regis" }]);
},
test(assert) {
const arrowDownEvent = () => {
const event = jQuery.Event("keydown");
event.keyCode = 40;
find(".select-box").trigger(event);
};
const arrowUpEvent = () => {
const event = jQuery.Event("keydown");
event.keyCode = 38;
find(".select-box").trigger(event);
};
const escapeEvent = () => {
const event = jQuery.Event("keydown");
event.keyCode = 27;
find(".select-box").trigger(event);
};
const enterEvent = () => {
const event = jQuery.Event("keydown");
event.keyCode = 13;
find(".select-box").trigger(event);
};
click(".select-box-header");
andThen(() => arrowDownEvent() );
andThen(() => {
assert.equal(find(".select-box-row.is-highlighted").attr("title"), "robin", "it highlights the first row");
});
andThen(() => arrowDownEvent() );
andThen(() => {
assert.equal(find(".select-box-row.is-highlighted").attr("title"), "regis", "it highlights the next row");
});
andThen(() => arrowDownEvent() );
andThen(() => {
assert.equal(find(".select-box-row.is-highlighted").attr("title"), "regis", "it keeps highlighting the last row when reaching the end");
});
andThen(() => arrowUpEvent() );
andThen(() => {
assert.equal(find(".select-box-row.is-highlighted").attr("title"), "robin", "it highlights the previous row");
});
andThen(() => enterEvent() );
andThen(() => {
assert.equal(find(".select-box-row.is-selected").attr("title"), "robin", "it selects the row when pressing enter");
assert.equal(find(".select-box").hasClass("is-expanded"), false, "it collapses the select box when selecting a row");
});
click(".select-box-header");
andThen(() => {
assert.equal(find(".select-box").hasClass("is-expanded"), true);
});
andThen(() => escapeEvent() );
andThen(() => {
assert.equal(find(".select-box").hasClass("is-expanded"), false, "it collapses the select box");
});
}
});