A11Y: fix and improve user card accessibility (#29399)
This commit is contained in:
parent
92cd2818ad
commit
74bb520877
|
@ -193,7 +193,7 @@ export default class CardContentsBase extends Component {
|
||||||
return this._show(target.innerText.replace(/^@/, ""), target, event);
|
return this._show(target.innerText.replace(/^@/, ""), target, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
_positionCard(target, event) {
|
_positionCard(target) {
|
||||||
schedule("afterRender", async () => {
|
schedule("afterRender", async () => {
|
||||||
if (this.site.desktopView) {
|
if (this.site.desktopView) {
|
||||||
this._menuInstance = await this.menu.show(target, {
|
this._menuInstance = await this.menu.show(target, {
|
||||||
|
@ -228,11 +228,10 @@ export default class CardContentsBase extends Component {
|
||||||
// note: we DO NOT use afterRender here cause _positionCard may
|
// note: we DO NOT use afterRender here cause _positionCard may
|
||||||
// run afterwards, if we allowed this to happen the usercard
|
// run afterwards, if we allowed this to happen the usercard
|
||||||
// may be offscreen and we may scroll all the way to it on focus
|
// may be offscreen and we may scroll all the way to it on focus
|
||||||
if (event?.pointerId === -1) {
|
|
||||||
discourseLater(() => {
|
discourseLater(() => {
|
||||||
this.element.querySelector("a")?.focus();
|
this.element.querySelector("a.user-profile-link")?.focus();
|
||||||
}, 350);
|
}, 350);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="card-row first-row">
|
<div class="card-row first-row">
|
||||||
<div class="user-card-avatar">
|
<div class="user-card-avatar" aria-hidden="true">
|
||||||
{{#if this.contentHidden}}
|
{{#if this.contentHidden}}
|
||||||
<span class="card-huge-avatar">{{bound-avatar
|
<span class="card-huge-avatar">{{bound-avatar
|
||||||
this.user
|
this.user
|
||||||
|
@ -38,6 +38,7 @@
|
||||||
{{on "click" this.handleShowUser}}
|
{{on "click" this.handleShowUser}}
|
||||||
href={{this.user.path}}
|
href={{this.user.path}}
|
||||||
class="card-huge-avatar"
|
class="card-huge-avatar"
|
||||||
|
tabindex="-1"
|
||||||
>{{bound-avatar this.user "huge"}}</a>
|
>{{bound-avatar this.user "huge"}}</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -59,10 +60,7 @@
|
||||||
{{if this.nameFirst 'full-name' 'username'}}"
|
{{if this.nameFirst 'full-name' 'username'}}"
|
||||||
>
|
>
|
||||||
{{#if this.contentHidden}}
|
{{#if this.contentHidden}}
|
||||||
<span
|
<span class="name-username-wrapper">
|
||||||
id="discourse-user-card-title"
|
|
||||||
class="name-username-wrapper"
|
|
||||||
>
|
|
||||||
{{if
|
{{if
|
||||||
this.nameFirst
|
this.nameFirst
|
||||||
this.user.name
|
this.user.name
|
||||||
|
@ -74,11 +72,12 @@
|
||||||
{{on "click" this.handleShowUser}}
|
{{on "click" this.handleShowUser}}
|
||||||
href={{this.user.path}}
|
href={{this.user.path}}
|
||||||
class="user-profile-link"
|
class="user-profile-link"
|
||||||
|
aria-label={{i18n
|
||||||
|
"user.profile_link"
|
||||||
|
username=this.user.username
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span class="name-username-wrapper">
|
||||||
id="discourse-user-card-title"
|
|
||||||
class="name-username-wrapper"
|
|
||||||
>
|
|
||||||
{{if
|
{{if
|
||||||
this.nameFirst
|
this.nameFirst
|
||||||
this.user.name
|
this.user.name
|
||||||
|
@ -185,13 +184,13 @@
|
||||||
{{#if this.user.profile_hidden}}
|
{{#if this.user.profile_hidden}}
|
||||||
<div class="card-row second-row">
|
<div class="card-row second-row">
|
||||||
<div class="profile-hidden">
|
<div class="profile-hidden">
|
||||||
<span>{{i18n "user.profile_hidden"}}</span>
|
<span role="alert">{{i18n "user.profile_hidden"}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{else if this.user.inactive}}
|
{{else if this.user.inactive}}
|
||||||
<div class="card-row second-row">
|
<div class="card-row second-row">
|
||||||
<div class="inactive-user">
|
<div class="inactive-user">
|
||||||
<span>{{i18n "user.inactive_user"}}</span>
|
<span role="alert">{{i18n "user.inactive_user"}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -31,7 +31,7 @@ import I18n from "discourse-i18n";
|
||||||
"usernameClass",
|
"usernameClass",
|
||||||
"primaryGroup"
|
"primaryGroup"
|
||||||
)
|
)
|
||||||
@attributeBindings("labelledBy:aria-labelledby")
|
@attributeBindings("ariaLabel:aria-label")
|
||||||
export default class UserCardContents extends CardContentsBase.extend(
|
export default class UserCardContents extends CardContentsBase.extend(
|
||||||
CanCheckEmails,
|
CanCheckEmails,
|
||||||
CleansUp
|
CleansUp
|
||||||
|
@ -40,6 +40,7 @@ export default class UserCardContents extends CardContentsBase.extend(
|
||||||
avatarSelector = "[data-user-card]";
|
avatarSelector = "[data-user-card]";
|
||||||
avatarDataAttrKey = "userCard";
|
avatarDataAttrKey = "userCard";
|
||||||
mentionSelector = "a.mention";
|
mentionSelector = "a.mention";
|
||||||
|
ariaLabel = I18n.t("user.card");
|
||||||
|
|
||||||
@setting("allow_profile_backgrounds") allowBackgrounds;
|
@setting("allow_profile_backgrounds") allowBackgrounds;
|
||||||
@setting("enable_badges") showBadges;
|
@setting("enable_badges") showBadges;
|
||||||
|
@ -75,11 +76,6 @@ export default class UserCardContents extends CardContentsBase.extend(
|
||||||
return this.user.name !== this.user.username;
|
return this.user.name !== this.user.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
@discourseComputed("user")
|
|
||||||
labelledBy(user) {
|
|
||||||
return user ? "discourse-user-card-title" : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed("user")
|
@discourseComputed("user")
|
||||||
hasLocaleOrWebsite(user) {
|
hasLocaleOrWebsite(user) {
|
||||||
return user.location || user.website_name || this.userTimezone;
|
return user.location || user.website_name || this.userTimezone;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Modifier from "ember-modifier";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
const FOCUSABLE_ELEMENTS =
|
const FOCUSABLE_ELEMENTS =
|
||||||
'details:not(.is-disabled) summary, [autofocus], a, input, select, textarea, summary, [tabindex]:not([tabindex="-1"])';
|
"details:not(.is-disabled) summary, [autofocus], a, input, select, textarea, summary";
|
||||||
|
|
||||||
export default class TrapTabModifier extends Modifier {
|
export default class TrapTabModifier extends Modifier {
|
||||||
element = null;
|
element = null;
|
||||||
|
@ -50,10 +50,17 @@ export default class TrapTabModifier extends Modifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusableElements = FOCUSABLE_ELEMENTS + ", button:enabled";
|
const focusableElements = FOCUSABLE_ELEMENTS + ", button:enabled";
|
||||||
const firstFocusableElement = this.element.querySelector(focusableElements);
|
|
||||||
const focusableContent = this.element.querySelectorAll(focusableElements);
|
|
||||||
|
|
||||||
const lastFocusableElement = focusableContent[focusableContent.length - 1];
|
const filteredFocusableElements = Array.from(
|
||||||
|
this.element.querySelectorAll(focusableElements)
|
||||||
|
).filter((element) => {
|
||||||
|
const tabindex = element.getAttribute("tabindex");
|
||||||
|
return tabindex !== "-1";
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstFocusableElement = filteredFocusableElements[0];
|
||||||
|
const lastFocusableElement =
|
||||||
|
filteredFocusableElements[filteredFocusableElements.length - 1];
|
||||||
|
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
if (document.activeElement === firstFocusableElement) {
|
if (document.activeElement === firstFocusableElement) {
|
||||||
|
@ -63,7 +70,6 @@ export default class TrapTabModifier extends Modifier {
|
||||||
} else {
|
} else {
|
||||||
if (document.activeElement === lastFocusableElement) {
|
if (document.activeElement === lastFocusableElement) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
(
|
(
|
||||||
this.element.querySelector(".modal-close") || firstFocusableElement
|
this.element.querySelector(".modal-close") || firstFocusableElement
|
||||||
)?.focus({ preventScroll: this.preventScroll });
|
)?.focus({ preventScroll: this.preventScroll });
|
||||||
|
|
|
@ -2132,6 +2132,9 @@ en:
|
||||||
private_message: "message"
|
private_message: "message"
|
||||||
the_topic: "the topic"
|
the_topic: "the topic"
|
||||||
|
|
||||||
|
card: "User card"
|
||||||
|
profile_link: "%{username}, visit profile"
|
||||||
|
|
||||||
user_status:
|
user_status:
|
||||||
save: "Save"
|
save: "Save"
|
||||||
set_custom_status: "Set custom status"
|
set_custom_status: "Set custom status"
|
||||||
|
|
Loading…
Reference in New Issue