mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 08:57:10 -06:00
FIX: Prevent focus shift when navigating the user directory (#29209)
Prevent focus shift when filtering in the user directory. Also glimmerifies the table header toggle
This commit is contained in:
parent
35284c77f1
commit
c8a94b5b3d
@ -98,4 +98,12 @@ export default class AdminUsersListShowController extends Controller.extend(
|
||||
this.toggleProperty("showEmails");
|
||||
this.resetFilters();
|
||||
}
|
||||
|
||||
@action
|
||||
updateOrder(field, asc) {
|
||||
this.setProperties({
|
||||
order: field,
|
||||
asc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,7 @@
|
||||
>
|
||||
<:header>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@field="username"
|
||||
@labelKey="username"
|
||||
@order={{this.order}}
|
||||
@ -54,6 +55,7 @@
|
||||
class="directory-table__column-header--username"
|
||||
/>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@field="email"
|
||||
@labelKey="email"
|
||||
@order={{this.order}}
|
||||
@ -66,6 +68,7 @@
|
||||
}}
|
||||
/>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@field="last_emailed"
|
||||
@labelKey="admin.users.last_emailed"
|
||||
@order={{this.order}}
|
||||
@ -73,6 +76,7 @@
|
||||
@automatic={{true}}
|
||||
/>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@field="seen"
|
||||
@labelKey="last_seen"
|
||||
@order={{this.order}}
|
||||
@ -80,6 +84,7 @@
|
||||
@automatic={{true}}
|
||||
/>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@field="topics_viewed"
|
||||
@labelKey="admin.user.topics_entered"
|
||||
@order={{this.order}}
|
||||
@ -87,6 +92,7 @@
|
||||
@automatic={{true}}
|
||||
/>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@field="posts_read"
|
||||
@labelKey="admin.user.posts_read_count"
|
||||
@order={{this.order}}
|
||||
@ -94,6 +100,7 @@
|
||||
@automatic={{true}}
|
||||
/>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@field="read_time"
|
||||
@labelKey="admin.user.time_read"
|
||||
@order={{this.order}}
|
||||
@ -101,6 +108,7 @@
|
||||
@automatic={{true}}
|
||||
/>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@field="created"
|
||||
@labelKey="created"
|
||||
@order={{this.order}}
|
||||
|
@ -7,6 +7,7 @@
|
||||
/>
|
||||
{{#each this.columns as |column|}}
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@field={{column.name}}
|
||||
@icon={{column.icon}}
|
||||
@order={{this.order}}
|
||||
|
@ -29,4 +29,12 @@ export default class DirectoryTable extends Component {
|
||||
this._table.scrollLeft = scrollPixels;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
updateOrder(field, asc) {
|
||||
this.setProperties({
|
||||
order: field,
|
||||
asc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,130 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class TableHeaderToggle extends Component {
|
||||
get id() {
|
||||
return `table-header-toggle-${this.args.field.replace(/\s/g, "")}`;
|
||||
}
|
||||
|
||||
get labelKey() {
|
||||
if (!this.args.automatic && !this.args.translated) {
|
||||
return this.args.field;
|
||||
} else {
|
||||
return this.args.labelKey;
|
||||
}
|
||||
}
|
||||
|
||||
get ariaSort() {
|
||||
if (this.args.order === this.args.field) {
|
||||
return this.args.asc ? "ascending" : "descending";
|
||||
} else {
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
get chevronIcon() {
|
||||
if (this.args.order === this.args.field) {
|
||||
return this.args.asc ? "chevron-up" : "chevron-down";
|
||||
}
|
||||
}
|
||||
|
||||
get pressedState() {
|
||||
if (this.args.order === this.args.field) {
|
||||
return this.args.asc ? "mixed" : "true";
|
||||
} else {
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
get ariaLabel() {
|
||||
let criteria = "";
|
||||
|
||||
if (this.args.icon === "heart") {
|
||||
criteria += `${I18n.t("likes_lowercase", { count: 2 })} `;
|
||||
}
|
||||
|
||||
if (this.args.translated) {
|
||||
criteria += this.args.field;
|
||||
} else {
|
||||
const labelKey = this.labelKey || `directory.${this.args.field}`;
|
||||
criteria += I18n.t(`${labelKey}_long`, {
|
||||
defaultValue: I18n.t(labelKey),
|
||||
});
|
||||
}
|
||||
|
||||
return I18n.t("directory.sort.label", { criteria });
|
||||
}
|
||||
|
||||
get iconName() {
|
||||
return this.args.icon || null;
|
||||
}
|
||||
|
||||
get label() {
|
||||
const labelKey = this.args.labelKey || `directory.${this.args.field}`;
|
||||
return this.args.translated
|
||||
? this.args.field
|
||||
: I18n.t(`${labelKey}_long`, {
|
||||
defaultValue: I18n.t(labelKey),
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
toggleProperties() {
|
||||
const newAsc =
|
||||
this.args.order === this.args.field && !this.args.asc ? true : null;
|
||||
this.args.onToggle?.(this.args.field, newAsc);
|
||||
|
||||
schedule("afterRender", () => {
|
||||
document.getElementById(this.id)?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
click() {
|
||||
this.toggleProperties();
|
||||
}
|
||||
|
||||
@action
|
||||
keyPress(event) {
|
||||
if (event.key === "Enter") {
|
||||
this.toggleProperties();
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
...attributes
|
||||
class="directory-table__column-header sortable"
|
||||
aria-sort={{this.ariaSort}}
|
||||
role="columnheader"
|
||||
{{! template-lint-disable no-invalid-interactive }}
|
||||
{{on "click" this.click}}
|
||||
{{! template-lint-disable no-invalid-interactive }}
|
||||
{{on "keypress" this.keyPress}}
|
||||
>
|
||||
<div
|
||||
class="header-contents"
|
||||
id={{this.id}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label={{this.ariaLabel}}
|
||||
aria-pressed={{this.pressedState}}
|
||||
>
|
||||
{{yield}}
|
||||
<span class="text">
|
||||
{{#if this.iconName}}
|
||||
{{icon this.iconName}}
|
||||
{{/if}}
|
||||
{{this.label}}
|
||||
{{#if this.chevronIcon}}
|
||||
{{icon this.chevronIcon}}
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
<div
|
||||
class="header-contents"
|
||||
id={{this.id}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label={{this.ariaLabel}}
|
||||
aria-pressed={{this.pressedState}}
|
||||
>
|
||||
|
||||
{{yield}}
|
||||
<span class="text">
|
||||
{{directory-table-header-title
|
||||
field=this.field
|
||||
labelKey=this.labelKey
|
||||
icon=this.icon
|
||||
translated=this.translated
|
||||
}}
|
||||
{{this.chevronIcon}}
|
||||
</span>
|
||||
</div>
|
@ -1,119 +0,0 @@
|
||||
import Component from "@ember/component";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import {
|
||||
attributeBindings,
|
||||
classNames,
|
||||
tagName,
|
||||
} from "@ember-decorators/component";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
@tagName("div")
|
||||
@classNames("directory-table__column-header", "sortable")
|
||||
@attributeBindings("title", "colspan", "ariaSort:aria-sort", "role")
|
||||
export default class TableHeaderToggle extends Component {
|
||||
role = "columnheader";
|
||||
labelKey = null;
|
||||
chevronIcon = null;
|
||||
columnIcon = null;
|
||||
translated = false;
|
||||
automatic = false;
|
||||
onActiveRender = null;
|
||||
pressedState = null;
|
||||
ariaLabel = null;
|
||||
|
||||
@discourseComputed("order", "field", "asc")
|
||||
ariaSort() {
|
||||
if (this.order === this.field) {
|
||||
return this.asc ? "ascending" : "descending";
|
||||
} else {
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
toggleProperties() {
|
||||
if (this.order === this.field) {
|
||||
this.set("asc", this.asc ? null : true);
|
||||
} else {
|
||||
this.setProperties({ order: this.field, asc: null });
|
||||
}
|
||||
}
|
||||
|
||||
toggleChevron() {
|
||||
if (this.order === this.field) {
|
||||
let chevron = iconHTML(this.asc ? "chevron-up" : "chevron-down");
|
||||
this.set("chevronIcon", htmlSafe(`${chevron}`));
|
||||
} else {
|
||||
this.set("chevronIcon", null);
|
||||
}
|
||||
}
|
||||
|
||||
click() {
|
||||
this.toggleProperties();
|
||||
}
|
||||
|
||||
keyPress(e) {
|
||||
if (e.which === 13) {
|
||||
this.toggleProperties();
|
||||
}
|
||||
}
|
||||
|
||||
didReceiveAttrs() {
|
||||
super.didReceiveAttrs(...arguments);
|
||||
if (!this.automatic && !this.translated) {
|
||||
this.set("labelKey", this.field);
|
||||
}
|
||||
this.set("id", `table-header-toggle-${this.field.replace(/\s/g, "")}`);
|
||||
this.toggleChevron();
|
||||
this._updateA11yAttributes();
|
||||
}
|
||||
|
||||
didRender() {
|
||||
super.didRender(...arguments);
|
||||
|
||||
if (this.onActiveRender && this.chevronIcon) {
|
||||
this.onActiveRender(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
_updateA11yAttributes() {
|
||||
let criteria = "";
|
||||
const pressed = this.order === this.field;
|
||||
|
||||
if (this.icon === "heart") {
|
||||
criteria += `${I18n.t("likes_lowercase", { count: 2 })} `;
|
||||
}
|
||||
|
||||
if (this.translated) {
|
||||
criteria += this.field;
|
||||
} else {
|
||||
const labelKey = this.labelKey || `directory.${this.field}`;
|
||||
|
||||
criteria += I18n.t(labelKey + "_long", {
|
||||
defaultValue: I18n.t(labelKey),
|
||||
});
|
||||
}
|
||||
|
||||
this.set("ariaLabel", I18n.t("directory.sort.label", { criteria }));
|
||||
|
||||
if (pressed) {
|
||||
if (this.asc) {
|
||||
this.set("pressedState", "mixed");
|
||||
} else {
|
||||
this.set("pressedState", "true");
|
||||
}
|
||||
|
||||
this._focusHeader();
|
||||
} else {
|
||||
this.set("pressedState", "false");
|
||||
}
|
||||
}
|
||||
|
||||
_focusHeader() {
|
||||
schedule("afterRender", () => {
|
||||
document.getElementById(this.id)?.focus();
|
||||
});
|
||||
}
|
||||
}
|
@ -239,4 +239,12 @@ export default class GroupIndexController extends Controller {
|
||||
this.bulkSelection.removeObject(member);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
updateOrder(field, asc) {
|
||||
this.setProperties({
|
||||
order: field,
|
||||
asc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -114,4 +114,12 @@ export default class GroupRequestsController extends Controller {
|
||||
request_denied: true,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
updateOrder(field, asc) {
|
||||
this.setProperties({
|
||||
order: field,
|
||||
asc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,7 @@
|
||||
>
|
||||
<:header>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@order={{this.order}}
|
||||
@asc={{this.asc}}
|
||||
@field="username_lower"
|
||||
@ -103,6 +104,7 @@
|
||||
/>
|
||||
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@order={{this.order}}
|
||||
@asc={{this.asc}}
|
||||
@field="added_at"
|
||||
@ -111,6 +113,7 @@
|
||||
class="directory-table__column-header--added"
|
||||
/>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@order={{this.order}}
|
||||
@asc={{this.asc}}
|
||||
@field="last_posted_at"
|
||||
@ -119,6 +122,7 @@
|
||||
class="directory-table__column-header--last-posted"
|
||||
/>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@order={{this.order}}
|
||||
@asc={{this.asc}}
|
||||
@field="last_seen_at"
|
||||
|
@ -19,6 +19,7 @@
|
||||
<ResponsiveTable @className="group-members group-members__requests">
|
||||
<:header>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@order={{this.order}}
|
||||
@asc={{this.asc}}
|
||||
@field="username_lower"
|
||||
@ -27,6 +28,7 @@
|
||||
class="username"
|
||||
/>
|
||||
<TableHeaderToggle
|
||||
@onToggle={{this.updateOrder}}
|
||||
@order={{this.order}}
|
||||
@asc={{this.asc}}
|
||||
@field="requested_at"
|
||||
|
Loading…
Reference in New Issue
Block a user