A11Y: fix and improve user card accessibility (#29399)

This commit is contained in:
Kris 2024-10-25 12:43:43 -04:00 committed by GitHub
parent 92cd2818ad
commit 74bb520877
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 31 additions and 28 deletions

View File

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

View File

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

View File

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

View File

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

View File

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