A11Y: improve /badges structure for screen readers (#27698)

This commit is contained in:
Kris 2024-07-03 17:16:21 -04:00 committed by GitHub
parent 65be7a7880
commit 3a6762d2be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 163 additions and 111 deletions

View File

@ -0,0 +1,129 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import { htmlSafe } from "@ember/template";
import { isEmpty } from "@ember/utils";
import { eq, not } from "truth-helpers";
import DButton from "discourse/components/d-button";
import iconOrImage from "discourse/helpers/icon-or-image";
import number from "discourse/helpers/number";
import { emojiUnescape, sanitize } from "discourse/lib/text";
import dIcon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import PluginOutlet from "./plugin-outlet";
export default class BadgeCard extends Component {
@tracked size = this.args.size || "medium";
get url() {
const { badge, filterUser, username } = this.args;
return filterUser ? `${badge.url}?username=${username}` : badge.url;
}
get displayCount() {
const { count, badge } = this.args;
if (count == null) {
return badge.grant_count;
}
if (count > 1) {
return count;
}
}
get summary() {
const { size, badge } = this.args;
if (size === "large" && !isEmpty(badge.long_description)) {
return emojiUnescape(sanitize(badge.long_description));
}
return sanitize(badge.description);
}
get showFavorite() {
const { badge } = this.args;
return ![1, 2, 3, 4].includes(badge.id);
}
<template>
<div
class="badge-card --badge-{{this.size}}"
data-badge-slug={{@badge.slug}}
>
<div class="badge-contents">
<PluginOutlet
@name="badge-contents-top"
@outletArgs={{hash badge=@badge url=this.url}}
/>
<span
class="badge-icon {{@badge.badgeTypeClassName}}"
aria-hidden="true"
>
{{iconOrImage @badge}}
</span>
<div class="badge-info">
<div class="badge-info-item">
<h3>
{{#if (eq this.size "large")}}
{{@badge.name}}
{{else}}
<a
href={{this.url}}
class="badge-link"
aria-describedby="badge-summary-{{@badge.slug}} badge-granted-{{@badge.slug}} badge-awarded-{{@badge.slug}}"
>
{{@badge.name}}
</a>
{{/if}}
</h3>
<div id="badge-summary-{{@badge.slug}}" class="badge-summary">
{{htmlSafe this.summary}}
</div>
{{#if this.displayCount}}
<div id="badge-granted-{{@badge.slug}}" class="badge-granted">
{{htmlSafe
(i18n
"badges.awarded"
count=this.displayCount
number=(number this.displayCount)
)
}}
</div>
{{/if}}
</div>
</div>
</div>
{{#if @badge.has_badge}}
<div
id="badge-awarded-{{@badge.slug}}"
class="check-display status-checked"
aria-label={{i18n "notifications.titles.granted_badge"}}
>
{{dIcon "check"}}
</div>
{{/if}}
{{#if @canFavorite}}
{{#if @isFavorite}}
<DButton
@icon="star"
@action={{@onFavoriteClick}}
class="favorite-btn"
/>
{{else}}
<DButton
@icon="far-star"
@action={{@onFavoriteClick}}
@title={{if
@canFavoriteMoreBadges
"badges.favorite_max_not_reached"
"badges.favorite_max_reached"
}}
@disabled={{not @canFavoriteMoreBadges}}
class="favorite-btn"
/>
{{/if}}
{{/if}}
</div>
</template>
}

View File

@ -1,62 +0,0 @@
{{#if this.badge.has_badge}}
<a href={{this.url}} class="check-display status-checked">{{d-icon
"check"
}}</a>
{{/if}}
{{#if this.canFavorite}}
{{#if this.isFavorite}}
<DButton
@icon="star"
@action={{this.onFavoriteClick}}
class="favorite-btn"
/>
{{else}}
<DButton
@icon="far-star"
@action={{this.onFavoriteClick}}
@title={{if
this.canFavoriteMoreBadges
"badges.favorite_max_not_reached"
"badges.favorite_max_reached"
}}
@disabled={{not this.canFavoriteMoreBadges}}
class="favorite-btn"
/>
{{/if}}
{{/if}}
<div class="badge-contents">
<PluginOutlet
@name="badge-contents-top"
@outletArgs={{hash badge=this.badge url=this.url}}
/>
<a
href={{this.url}}
class="badge-icon {{this.badge.badgeTypeClassName}}"
>{{icon-or-image this.badge}}</a>
<div class="badge-info">
<div class="badge-info-item">
<h3><a href={{this.url}} class="badge-link">{{this.badge.name}}</a></h3>
<div class="badge-summary">{{html-safe this.summary}}</div>
{{#if this.displayCount}}
<LinkTo
@route="badges.show"
@model={{this.badge}}
class="badge-granted"
>
{{html-safe
(i18n
"badges.awarded"
count=this.displayCount
number=(number this.displayCount)
)
}}
</LinkTo>
{{/if}}
</div>
</div>
</div>

View File

@ -1,39 +0,0 @@
import Component from "@ember/component";
import { isEmpty } from "@ember/utils";
import { emojiUnescape, sanitize } from "discourse/lib/text";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
size: "medium",
classNameBindings: [":badge-card", "size", "badge.slug"],
@discourseComputed("badge.url", "filterUser", "username")
url(badgeUrl, filterUser, username) {
return filterUser ? `${badgeUrl}?username=${username}` : badgeUrl;
},
@discourseComputed("count", "badge.grant_count")
displayCount(count, grantCount) {
if (count == null) {
return grantCount;
}
if (count > 1) {
return count;
}
},
@discourseComputed("size", "badge.long_description", "badge.description")
summary(size, longDescription, description) {
if (size === "large") {
if (!isEmpty(longDescription)) {
return emojiUnescape(sanitize(longDescription));
}
}
return sanitize(description);
},
@discourseComputed("badge.id")
showFavorite(badgeId) {
return ![1, 2, 3, 4].includes(badgeId);
},
});

View File

@ -157,7 +157,9 @@
position: absolute;
right: 0;
bottom: 0;
z-index: 1;
}
.badge-contents {
display: flex;
align-items: flex-start;
@ -166,22 +168,41 @@
.badge-link {
color: var(--primary);
display: inline-block;
line-height: var(--line-height-medium);
&:after {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
}
.badge-summary:has(a) {
// for summary links to be reachable
// they must be positioned above .badge-link:after
position: relative;
z-index: 1;
}
.badge-icon {
--badge-icon-size: 3.5em;
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
svg {
font-size: 3.5em;
font-size: var(--badge-icon-size);
}
img {
width: 100%;
max-width: 64px;
max-height: 64px;
max-width: var(--badge-icon-size);
max-height: var(--badge-icon-size);
}
&.badge-type-gold .fa {
@ -210,7 +231,7 @@
}
}
&.large {
&.--badge-large {
width: 100%;
align-self: flex-start;
@ -325,12 +346,15 @@
}
.check-display {
display: inline-block;
padding: 0 0.25em;
border-radius: 10px;
text-align: center;
.fa {
font-size: 0.9em;
display: flex;
width: 1.5em;
height: 1.5em;
border-radius: 100%;
align-items: center;
justify-content: center;
.d-icon {
font-size: var(--font-down-2);
color: var(--secondary);
}
}