mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 21:21:19 +08:00
FEATURE: improves keyboard handling of select-box
- arrow keys - escape key
This commit is contained in:
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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"));
|
||||||
},
|
},
|
||||||
|
@ -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}}
|
||||||
|
@ -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}}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user