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:
Penar Musaraj 2023-05-09 09:46:05 -04:00 committed by GitHub
parent b5292c8139
commit e8aea3c558
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 153 additions and 49 deletions

View File

@ -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");

View File

@ -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");
});
});

View File

@ -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);

View File

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

View File

@ -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;
}

View File

@ -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);