A11Y: Improve select-kit accessibility (#21400)
This improves keyboard navigation in and out of select-kit components. The improvements include: - `Tab` will now dismiss the dropdown once the active element is outside the select-kit element - pressing `Escape` will not bubble, this is most noticeable in the composer, pressing `Esc` there now when a dropdown is expanded will not dismiss the composer - `Shift+Tab` will also dismiss the dropdown once focus is outside it
This commit is contained in:
parent
b5292c8139
commit
e8aea3c558
|
@ -114,6 +114,7 @@ acceptance("Category Edit", function (needs) {
|
|||
await allowedTagChooser.expand();
|
||||
await allowedTagChooser.selectRowByValue("monkey");
|
||||
|
||||
await allowedTagChooser.collapse();
|
||||
const allowedTagGroupChooser = selectKit("#category-allowed-tag-groups");
|
||||
await allowedTagGroupChooser.expand();
|
||||
await allowedTagGroupChooser.selectRowByValue("TagGroup1");
|
||||
|
@ -127,6 +128,7 @@ acceptance("Category Edit", function (needs) {
|
|||
assert.deepEqual(payload.allowed_tags, ["monkey"]);
|
||||
assert.deepEqual(payload.allowed_tag_groups, ["TagGroup1"]);
|
||||
|
||||
await allowedTagGroupChooser.collapse();
|
||||
await allowedTagChooser.expand();
|
||||
await allowedTagChooser.deselectItemByValue("monkey");
|
||||
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, tab, triggerKeyEvent, visit } from "@ember/test-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("Select-kit - Composer - Accessibility", function (needs) {
|
||||
needs.user();
|
||||
needs.site({ can_tag_topics: true });
|
||||
needs.settings({ allow_uncategorized_topics: true });
|
||||
|
||||
test("tabbing works", async function (assert) {
|
||||
await visit("/");
|
||||
await click("#create-topic");
|
||||
|
||||
const tagChooser = selectKit(".mini-tag-chooser");
|
||||
await tagChooser.expand();
|
||||
|
||||
assert
|
||||
.dom(".mini-tag-chooser .filter-input")
|
||||
.isFocused("it should focus the filter by default");
|
||||
|
||||
await tab();
|
||||
|
||||
assert
|
||||
.dom(".mini-tag-chooser .select-kit-row:first-child")
|
||||
.isFocused("it should focus the first row next");
|
||||
|
||||
await tab({ backwards: true });
|
||||
|
||||
assert
|
||||
.dom(".mini-tag-chooser .filter-input")
|
||||
.isFocused("it should focus the filter again when tabbing backwards");
|
||||
|
||||
await tab({ backwards: true });
|
||||
|
||||
assert
|
||||
.dom(".mini-tag-chooser .select-kit-header")
|
||||
.isFocused("it should focus the tag chooser header next");
|
||||
|
||||
await tab({ backwards: true });
|
||||
|
||||
assert
|
||||
.dom(".category-chooser .select-kit-header")
|
||||
.isFocused("it should focus the category chooser header next");
|
||||
|
||||
await tab();
|
||||
|
||||
assert
|
||||
.dom(".mini-tag-chooser .select-kit-header")
|
||||
.isFocused("it should focus the tag chooser again");
|
||||
|
||||
await tagChooser.expand();
|
||||
|
||||
await triggerKeyEvent(
|
||||
".mini-tag-chooser .select-kit-row:first-child",
|
||||
"keydown",
|
||||
"Escape"
|
||||
);
|
||||
|
||||
assert.notOk(
|
||||
exists(".mini-tag-chooser .select-kit-body .select-kit-row"),
|
||||
"Hitting Escape dismisses the tag chooser"
|
||||
);
|
||||
|
||||
assert.ok(exists(".composer-fields"), "Escape does not dismiss composer");
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { render } from "@ember/test-helpers";
|
||||
import { render, tab } from "@ember/test-helpers";
|
||||
import I18n from "I18n";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
|
@ -63,6 +63,54 @@ module("Integration | Component | select-kit/single-select", function (hooks) {
|
|||
);
|
||||
});
|
||||
|
||||
test("accessibility", async function (assert) {
|
||||
setDefaultState(this);
|
||||
|
||||
await render(hbs`<SingleSelect @content={{this.content}} />`);
|
||||
|
||||
await this.subject.expand();
|
||||
|
||||
const content = this.subject.displayedContent();
|
||||
assert.strictEqual(content.length, 3, "it shows rows");
|
||||
|
||||
assert
|
||||
.dom(".select-kit-header")
|
||||
.isFocused("it should focus the header first");
|
||||
|
||||
await tab();
|
||||
|
||||
assert
|
||||
.dom(".select-kit-row:first-child")
|
||||
.isFocused("it should focus the first row next");
|
||||
|
||||
await tab();
|
||||
|
||||
assert
|
||||
.dom(".select-kit-row:nth-child(2)")
|
||||
.isFocused("tab moves focus to 2nd row");
|
||||
|
||||
await tab();
|
||||
|
||||
assert
|
||||
.dom(".select-kit-row:nth-child(3)")
|
||||
.isFocused("tab moves focus to 3rd row");
|
||||
|
||||
await tab();
|
||||
|
||||
assert.notOk(
|
||||
this.subject.isExpanded(),
|
||||
"when there are no more rows, Tab collapses the dropdown"
|
||||
);
|
||||
|
||||
await this.subject.expand();
|
||||
|
||||
assert.ok(this.subject.isExpanded(), "dropdown is expanded again");
|
||||
|
||||
await tab({ backwards: true });
|
||||
|
||||
assert.notOk(this.subject.isExpanded(), "Shift+Tab collapses the dropdown");
|
||||
});
|
||||
|
||||
test("value", async function (assert) {
|
||||
setDefaultState(this);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Component from "@ember/component";
|
||||
import { bind } from "@ember/runloop";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { later } from "@ember/runloop";
|
||||
import { computed } from "@ember/object";
|
||||
|
||||
export default Component.extend({
|
||||
|
@ -10,55 +11,53 @@ export default Component.extend({
|
|||
return false;
|
||||
}),
|
||||
|
||||
rootEventType: "click",
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.handleRootMouseDownHandler = bind(this, this.handleRootMouseDown);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.element.style.position = "relative";
|
||||
|
||||
document.addEventListener(
|
||||
this.rootEventType,
|
||||
this.handleRootMouseDownHandler,
|
||||
true
|
||||
);
|
||||
document.addEventListener("click", this.handleClick, true);
|
||||
this.selectKit
|
||||
.mainElement()
|
||||
.addEventListener("keydown", this._handleKeydown, true);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
document.removeEventListener(
|
||||
this.rootEventType,
|
||||
this.handleRootMouseDownHandler,
|
||||
true
|
||||
);
|
||||
document.removeEventListener("click", this.handleClick, true);
|
||||
this.selectKit
|
||||
.mainElement()
|
||||
.removeEventListener("keydown", this._handleKeydown, true);
|
||||
},
|
||||
|
||||
handleRootMouseDown(event) {
|
||||
if (!this.selectKit.isExpanded) {
|
||||
@bind
|
||||
handleClick(event) {
|
||||
if (!this.selectKit.isExpanded || !this.selectKit.mainElement()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headerElement = document.querySelector(
|
||||
`#${this.selectKit.uniqueID}-header`
|
||||
);
|
||||
|
||||
if (headerElement && headerElement.contains(event.target)) {
|
||||
if (this.selectKit.mainElement().contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.element.contains(event.target)) {
|
||||
this.selectKit.close(event);
|
||||
},
|
||||
|
||||
@bind
|
||||
_handleKeydown(event) {
|
||||
if (!this.selectKit.isExpanded || event.key !== "Tab") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectKit.mainElement()) {
|
||||
later(() => {
|
||||
if (
|
||||
this.isDestroying ||
|
||||
this.isDestroyed ||
|
||||
this.selectKit.mainElement().contains(document.activeElement)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectKit.close(event);
|
||||
}
|
||||
}, 50);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -94,6 +94,8 @@ export default Component.extend(UtilsMixin, {
|
|||
if (event.key === "Escape") {
|
||||
this.selectKit.close(event);
|
||||
this.selectKit.headerElement().focus();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,6 @@ export default Component.extend(UtilsMixin, {
|
|||
if (!this.site.mobileView) {
|
||||
this.element.addEventListener("mouseenter", this.handleMouseEnter);
|
||||
this.element.addEventListener("focus", this.handleMouseEnter);
|
||||
this.element.addEventListener("blur", this.handleBlur);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -49,9 +48,8 @@ export default Component.extend(UtilsMixin, {
|
|||
this._super(...arguments);
|
||||
|
||||
if (!this.site.mobileView) {
|
||||
this.element.removeEventListener("mouseenter", this.handleBlur);
|
||||
this.element.removeEventListener("mouseenter", this.handleMouseEnter);
|
||||
this.element.removeEventListener("focus", this.handleMouseEnter);
|
||||
this.element.removeEventListener("blur", this.handleMouseEnter);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -134,20 +132,6 @@ export default Component.extend(UtilsMixin, {
|
|||
return false;
|
||||
},
|
||||
|
||||
@action
|
||||
handleBlur(event) {
|
||||
if (
|
||||
(!this.isDestroying || !this.isDestroyed) &&
|
||||
event.target &&
|
||||
this.selectKit.mainElement()
|
||||
) {
|
||||
if (!this.selectKit.mainElement().contains(event.target)) {
|
||||
this.selectKit.close(event);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
click(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -193,6 +177,8 @@ export default Component.extend(UtilsMixin, {
|
|||
} else if (event.key === "Escape") {
|
||||
this.selectKit.close(event);
|
||||
this.selectKit.headerElement().focus();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
} else {
|
||||
if (this.isValidInput(event.key)) {
|
||||
this.selectKit.set("filter", event.key);
|
||||
|
|
Loading…
Reference in New Issue