A11Y: improve /badges structure for screen readers (#27698)
This commit is contained in:
parent
65be7a7880
commit
3a6762d2be
|
@ -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>
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue