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);
|
||||
}
|
||||
|
||||
_positionCard(target, event) {
|
||||
_positionCard(target) {
|
||||
schedule("afterRender", async () => {
|
||||
if (this.site.desktopView) {
|
||||
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
|
||||
// run afterwards, if we allowed this to happen the usercard
|
||||
// may be offscreen and we may scroll all the way to it on focus
|
||||
if (event?.pointerId === -1) {
|
||||
|
||||
discourseLater(() => {
|
||||
this.element.querySelector("a")?.focus();
|
||||
this.element.querySelector("a.user-profile-link")?.focus();
|
||||
}, 350);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
</div>
|
||||
{{else}}
|
||||
<div class="card-row first-row">
|
||||
<div class="user-card-avatar">
|
||||
<div class="user-card-avatar" aria-hidden="true">
|
||||
{{#if this.contentHidden}}
|
||||
<span class="card-huge-avatar">{{bound-avatar
|
||||
this.user
|
||||
|
@ -38,6 +38,7 @@
|
|||
{{on "click" this.handleShowUser}}
|
||||
href={{this.user.path}}
|
||||
class="card-huge-avatar"
|
||||
tabindex="-1"
|
||||
>{{bound-avatar this.user "huge"}}</a>
|
||||
{{/if}}
|
||||
|
||||
|
@ -59,10 +60,7 @@
|
|||
{{if this.nameFirst 'full-name' 'username'}}"
|
||||
>
|
||||
{{#if this.contentHidden}}
|
||||
<span
|
||||
id="discourse-user-card-title"
|
||||
class="name-username-wrapper"
|
||||
>
|
||||
<span class="name-username-wrapper">
|
||||
{{if
|
||||
this.nameFirst
|
||||
this.user.name
|
||||
|
@ -74,11 +72,12 @@
|
|||
{{on "click" this.handleShowUser}}
|
||||
href={{this.user.path}}
|
||||
class="user-profile-link"
|
||||
aria-label={{i18n
|
||||
"user.profile_link"
|
||||
username=this.user.username
|
||||
}}
|
||||
>
|
||||
<span
|
||||
id="discourse-user-card-title"
|
||||
class="name-username-wrapper"
|
||||
>
|
||||
<span class="name-username-wrapper">
|
||||
{{if
|
||||
this.nameFirst
|
||||
this.user.name
|
||||
|
@ -185,13 +184,13 @@
|
|||
{{#if this.user.profile_hidden}}
|
||||
<div class="card-row second-row">
|
||||
<div class="profile-hidden">
|
||||
<span>{{i18n "user.profile_hidden"}}</span>
|
||||
<span role="alert">{{i18n "user.profile_hidden"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{else if this.user.inactive}}
|
||||
<div class="card-row second-row">
|
||||
<div class="inactive-user">
|
||||
<span>{{i18n "user.inactive_user"}}</span>
|
||||
<span role="alert">{{i18n "user.inactive_user"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -31,7 +31,7 @@ import I18n from "discourse-i18n";
|
|||
"usernameClass",
|
||||
"primaryGroup"
|
||||
)
|
||||
@attributeBindings("labelledBy:aria-labelledby")
|
||||
@attributeBindings("ariaLabel:aria-label")
|
||||
export default class UserCardContents extends CardContentsBase.extend(
|
||||
CanCheckEmails,
|
||||
CleansUp
|
||||
|
@ -40,6 +40,7 @@ export default class UserCardContents extends CardContentsBase.extend(
|
|||
avatarSelector = "[data-user-card]";
|
||||
avatarDataAttrKey = "userCard";
|
||||
mentionSelector = "a.mention";
|
||||
ariaLabel = I18n.t("user.card");
|
||||
|
||||
@setting("allow_profile_backgrounds") allowBackgrounds;
|
||||
@setting("enable_badges") showBadges;
|
||||
|
@ -75,11 +76,6 @@ export default class UserCardContents extends CardContentsBase.extend(
|
|||
return this.user.name !== this.user.username;
|
||||
}
|
||||
|
||||
@discourseComputed("user")
|
||||
labelledBy(user) {
|
||||
return user ? "discourse-user-card-title" : null;
|
||||
}
|
||||
|
||||
@discourseComputed("user")
|
||||
hasLocaleOrWebsite(user) {
|
||||
return user.location || user.website_name || this.userTimezone;
|
||||
|
|
|
@ -3,7 +3,7 @@ import Modifier from "ember-modifier";
|
|||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
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 {
|
||||
element = null;
|
||||
|
@ -50,10 +50,17 @@ export default class TrapTabModifier extends Modifier {
|
|||
}
|
||||
|
||||
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 (document.activeElement === firstFocusableElement) {
|
||||
|
@ -63,7 +70,6 @@ export default class TrapTabModifier extends Modifier {
|
|||
} else {
|
||||
if (document.activeElement === lastFocusableElement) {
|
||||
event.preventDefault();
|
||||
|
||||
(
|
||||
this.element.querySelector(".modal-close") || firstFocusableElement
|
||||
)?.focus({ preventScroll: this.preventScroll });
|
||||
|
|
|
@ -2132,6 +2132,9 @@ en:
|
|||
private_message: "message"
|
||||
the_topic: "the topic"
|
||||
|
||||
card: "User card"
|
||||
profile_link: "%{username}, visit profile"
|
||||
|
||||
user_status:
|
||||
save: "Save"
|
||||
set_custom_status: "Set custom status"
|
||||
|
|
Loading…
Reference in New Issue