A11Y: select kit close on focus out (#21274)

When navigating with the keyboard, the select-kit would not close when
focus was moved to an element outside of the body. For example, when
navigating via Tab or Shift+Tab, once the end (or beginning) of the list
was reached, focus would move out of the SK element, but the SK itself
would stay visible.

Switching from a click event to a focusout event solves the issue and
covers both mouse and keyboard navigation.
This commit is contained in:
Penar Musaraj 2023-05-02 14:22:31 -04:00 committed by GitHub
parent bfd3bd5516
commit 1b2a1c94d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 46 additions and 53 deletions

View File

@ -113,10 +113,12 @@ acceptance("Category Edit", function (needs) {
const allowedTagChooser = selectKit("#category-allowed-tags"); const allowedTagChooser = selectKit("#category-allowed-tags");
await allowedTagChooser.expand(); await allowedTagChooser.expand();
await allowedTagChooser.selectRowByValue("monkey"); await allowedTagChooser.selectRowByValue("monkey");
await allowedTagChooser.collapse();
const allowedTagGroupChooser = selectKit("#category-allowed-tag-groups"); const allowedTagGroupChooser = selectKit("#category-allowed-tag-groups");
await allowedTagGroupChooser.expand(); await allowedTagGroupChooser.expand();
await allowedTagGroupChooser.selectRowByValue("TagGroup1"); await allowedTagGroupChooser.selectRowByValue("TagGroup1");
await allowedTagGroupChooser.collapse();
await click("#save-category"); await click("#save-category");
@ -129,6 +131,7 @@ acceptance("Category Edit", function (needs) {
await allowedTagChooser.expand(); await allowedTagChooser.expand();
await allowedTagChooser.deselectItemByValue("monkey"); await allowedTagChooser.deselectItemByValue("monkey");
await allowedTagChooser.collapse();
await allowedTagGroupChooser.expand(); await allowedTagGroupChooser.expand();
await allowedTagGroupChooser.deselectItemByValue("TagGroup1"); await allowedTagGroupChooser.deselectItemByValue("TagGroup1");

View File

@ -39,10 +39,16 @@ acceptance("User Preferences - Categories", function (needs) {
".tracking-controls__regular-categories .category-selector" ".tracking-controls__regular-categories .category-selector"
); );
await trackedCategoriesSelector.collapse();
await regularCategoriesSelector.expand(); await regularCategoriesSelector.expand();
await regularCategoriesSelector.deselectItemByValue("4"); await regularCategoriesSelector.deselectItemByValue("4");
await regularCategoriesSelector.collapse();
await trackedCategoriesSelector.expand(); await trackedCategoriesSelector.expand();
await trackedCategoriesSelector.selectRowByValue("4"); await trackedCategoriesSelector.selectRowByValue("4");
await trackedCategoriesSelector.collapse();
await click(".save-changes"); await click(".save-changes");
assert.deepEqual(putRequestData, { assert.deepEqual(putRequestData, {
@ -63,6 +69,7 @@ acceptance("User Preferences - Categories", function (needs) {
await categorySelector.expand(); await categorySelector.expand();
// User has `regular_category_ids` set to [4] in fixtures // User has `regular_category_ids` set to [4] in fixtures
await categorySelector.selectRowByValue(4); await categorySelector.selectRowByValue(4);
await categorySelector.collapse();
await click(".save-changes"); await click(".save-changes");
assert.deepEqual(putRequestData, { assert.deepEqual(putRequestData, {

View File

@ -173,17 +173,25 @@ acceptance("User Preferences - Tracking", function (needs) {
assert.notOk( assert.notOk(
trackedCategoriesSelector.rowByValue("4").exists(), trackedCategoriesSelector.rowByValue("4").exists(),
"category that is set to regular is not available for selection" "category that is set to regular is not available for selection under tracked"
); );
const regularCategoriesSelector = selectKit( const regularCategoriesSelector = selectKit(
".tracking-controls__regular-categories .category-selector" ".tracking-controls__regular-categories .category-selector"
); );
await trackedCategoriesSelector.collapse();
await regularCategoriesSelector.expand(); await regularCategoriesSelector.expand();
await regularCategoriesSelector.deselectItemByValue("4"); await regularCategoriesSelector.deselectItemByValue("4");
assert.ok(
regularCategoriesSelector.rowByValue("4").exists(),
"category is no longer selected under regular"
);
await regularCategoriesSelector.collapse();
await trackedCategoriesSelector.expand(); await trackedCategoriesSelector.expand();
await trackedCategoriesSelector.selectRowByValue("4"); await trackedCategoriesSelector.selectRowByValue("4");
await trackedCategoriesSelector.collapse();
await click(".save-changes"); await click(".save-changes");
assert.deepEqual(putRequestData, { assert.deepEqual(putRequestData, {

View File

@ -58,9 +58,8 @@ module(
await this.subject.fillInFilter("baz"); await this.subject.fillInFilter("baz");
await this.subject.selectRowByValue("monkey"); await this.subject.selectRowByValue("monkey");
const error = query(".select-kit-error").innerText;
assert.strictEqual( assert.strictEqual(
error, query(".select-kit-error").innerText,
I18n.t("select_kit.max_content_reached", { I18n.t("select_kit.max_content_reached", {
count: this.siteSettings.max_tags_per_topic, count: this.siteSettings.max_tags_per_topic,
}) })

View File

@ -75,6 +75,7 @@ export default SelectKitComponent.extend({
}, },
select(value, item) { select(value, item) {
this.selectKit.set("multiSelectInFocus", true);
if (this.selectKit.hasSelection && this.selectKit.options.maximum === 1) { if (this.selectKit.hasSelection && this.selectKit.options.maximum === 1) {
this.selectKit.deselectByValue( this.selectKit.deselectByValue(
this.getValue(this.selectedContent.firstObject) this.getValue(this.selectedContent.firstObject)

View File

@ -94,6 +94,7 @@ export default Component.extend(
noneItem: null, noneItem: null,
newItem: null, newItem: null,
filter: null, filter: null,
multiSelectInFocus: null,
modifyContent: bind(this, this._modifyContentWrapper), modifyContent: bind(this, this._modifyContentWrapper),
modifySelection: bind(this, this._modifySelectionWrapper), modifySelection: bind(this, this._modifySelectionWrapper),
@ -441,6 +442,7 @@ export default Component.extend(
items = makeArray(items); items = makeArray(items);
if (this.multiSelect) { if (this.multiSelect) {
this.selectKit.set("multiSelectInFocus", true);
items = items.filter( items = items.filter(
(i) => (i) =>
i !== this.newItem && i !== this.newItem &&

View File

@ -1,5 +1,5 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { bind } from "@ember/runloop"; import { bind } from "discourse-common/utils/decorators";
import { computed } from "@ember/object"; import { computed } from "@ember/object";
export default Component.extend({ export default Component.extend({
@ -10,55 +10,41 @@ export default Component.extend({
return false; return false;
}), }),
rootEventType: "click",
init() {
this._super(...arguments);
this.handleRootMouseDownHandler = bind(this, this.handleRootMouseDown);
},
didInsertElement() { didInsertElement() {
this._super(...arguments); this._super(...arguments);
this.element.style.position = "relative"; this.element.style.position = "relative";
this.element.addEventListener("focusout", this._handleFocusOut, true);
document.addEventListener(
this.rootEventType,
this.handleRootMouseDownHandler,
true
);
}, },
willDestroyElement() { willDestroyElement() {
this._super(...arguments); this._super(...arguments);
this.element.removeEventListener("focusout", this._handleFocusOut, true);
document.removeEventListener(
this.rootEventType,
this.handleRootMouseDownHandler,
true
);
}, },
handleRootMouseDown(event) { @bind
_handleFocusOut(event) {
if (!this.selectKit.isExpanded) { if (!this.selectKit.isExpanded) {
return; return;
} }
const headerElement = document.querySelector( if (!this.selectKit.mainElement()) {
`#${this.selectKit.uniqueID}-header`
);
if (headerElement && headerElement.contains(event.target)) {
return; return;
} }
if (this.element.contains(event.target)) { if (this.selectKit.mainElement().contains(event.relatedTarget)) {
return;
}
// We have to use a custom flag for multi-selects to keep UI visible.
// We can't rely on event.relatedTarget for these cases because,
// when adding/removing items in a multi-select, the DOM element
// has already been removed by this point, and therefore
// event.relatedTarget is going to be null.
if (this.selectKit.multiSelectInFocus) {
this.selectKit.set("multiSelectInFocus", false);
return; return;
} }
if (this.selectKit.mainElement()) {
this.selectKit.close(event); this.selectKit.close(event);
}
}, },
}); });

View File

@ -122,7 +122,10 @@ export default Component.extend(UtilsMixin, {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); // prevents the space to trigger a scroll page-next event.preventDefault(); // prevents the space to trigger a scroll page-next
this.selectKit.open(event); this.selectKit.open(event);
} else if (event.key === "Escape") { } else if (
event.key === "Escape" ||
(event.shiftKey && event.key === "Tab")
) {
event.stopPropagation(); event.stopPropagation();
if (this.selectKit.isExpanded) { if (this.selectKit.isExpanded) {
this.selectKit.close(event); this.selectKit.close(event);

View File

@ -41,7 +41,6 @@ export default Component.extend(UtilsMixin, {
if (!this.site.mobileView) { if (!this.site.mobileView) {
this.element.addEventListener("mouseenter", this.handleMouseEnter); this.element.addEventListener("mouseenter", this.handleMouseEnter);
this.element.addEventListener("focus", 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); this._super(...arguments);
if (!this.site.mobileView) { 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("focus", this.handleMouseEnter);
this.element.removeEventListener("blur", this.handleMouseEnter);
} }
}, },
@ -134,20 +132,6 @@ export default Component.extend(UtilsMixin, {
return false; 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) { click(event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();