mirror of
https://github.com/discourse/discourse.git
synced 2026-08-09 12:38:21 -05:00
A11Y: add H1 tags for screenreaders on discovery routes (#32422)
Over the years we've received multiple pieces of feedback around our lack of meaningful H1 tags on our topic lists. This adds an `<H1>`, hidden with `.sr-only`, for screenreaders that describes the content of our discovery routes. I've tried to keep these structured in a way that's somewhat consistent and uses natural language. The translations could probably be more efficiently structured, but I think we need a good amount of flexibility here for translators to change the sentence structure. Ultimately this means people using screenreaders will know which page they're on while navigating headings... and without having to parse through our navigation. For example (made visible to demonstrate):    
This commit is contained in:
+94
@@ -0,0 +1,94 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class AccessibleDiscoveryHeading extends Component {
|
||||
get filterKey() {
|
||||
const filter = this.args.filter;
|
||||
|
||||
if (filter === "categories") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (filter.includes("/")) {
|
||||
return filter.split("/").pop();
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
get filterLabel() {
|
||||
const key = this.filterKey;
|
||||
|
||||
// fallback to nothing, e.g, "topics in uncategorized"
|
||||
// rather than "[en.discovery.headings.filter_labels.foo] topics in uncategorized"
|
||||
const fallback = "";
|
||||
const label = i18n(`discovery.headings.filter_labels.${key}`);
|
||||
|
||||
return label.includes("discovery.headings.filter_labels.")
|
||||
? fallback
|
||||
: label;
|
||||
}
|
||||
|
||||
get type() {
|
||||
const { category, tag, additionalTags } = this.args;
|
||||
|
||||
if (category && tag) {
|
||||
return "category_tag";
|
||||
}
|
||||
|
||||
if (tag && additionalTags?.length) {
|
||||
return "multi_tag";
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
return "single_tag";
|
||||
}
|
||||
|
||||
if (category) {
|
||||
return "category";
|
||||
}
|
||||
|
||||
return "all";
|
||||
}
|
||||
|
||||
get label() {
|
||||
const { category, tag, additionalTags, filter } = this.args;
|
||||
const type = this.type;
|
||||
|
||||
if (filter === "categories") {
|
||||
return i18n("discovery.headings.categories");
|
||||
}
|
||||
|
||||
const key = this.filterKey;
|
||||
const isSpecial = ["posted", "bookmarks"].includes(key);
|
||||
const scope = `discovery.headings`;
|
||||
|
||||
if (tag?.id === "none" && !additionalTags?.length) {
|
||||
const noTagsType = category ? "category" : "all";
|
||||
|
||||
const noTagsKey = isSpecial
|
||||
? `${scope}.no_tags.${noTagsType}.${key}`
|
||||
: `${scope}.no_tags.${noTagsType}_default`;
|
||||
|
||||
return i18n(noTagsKey, {
|
||||
filter: this.filterLabel,
|
||||
category: category?.name,
|
||||
});
|
||||
}
|
||||
|
||||
const translationKey = isSpecial
|
||||
? `${scope}.${type}.${key}`
|
||||
: `${scope}.${type}.default`;
|
||||
|
||||
return i18n(translationKey, {
|
||||
filter: this.filterLabel,
|
||||
category: category?.name,
|
||||
tag: tag?.id,
|
||||
tags: [tag?.id, ...(additionalTags || [])].join(" + "),
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<h1 id="topic-list-heading" class="sr-only">{{this.label}}</h1>
|
||||
</template>
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { service } from "@ember/service";
|
||||
import AddCategoryTagClasses from "discourse/components/add-category-tag-classes";
|
||||
import CategoryLogo from "discourse/components/category-logo";
|
||||
import DNavigation from "discourse/components/d-navigation";
|
||||
import AccessibleDiscoveryHeading from "discourse/components/discovery/accessible-discovery-heading";
|
||||
import ReorderCategories from "discourse/components/modal/reorder-categories";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import bodyClass from "discourse/helpers/body-class";
|
||||
@@ -74,6 +75,13 @@ export default class DiscoveryNavigation extends Component {
|
||||
@tags={{if @tag (array @tag.id)}}
|
||||
/>
|
||||
|
||||
<AccessibleDiscoveryHeading
|
||||
@category={{@category}}
|
||||
@tag={{@tag}}
|
||||
@additionalTags={{@additionalTags}}
|
||||
@filter={{this.filterMode}}
|
||||
/>
|
||||
|
||||
{{#if @category}}
|
||||
<PluginOutlet
|
||||
@name="above-category-heading"
|
||||
|
||||
@@ -188,6 +188,7 @@ export default class TopicList extends Component {
|
||||
(if this.bulkSelectEnabled "sticky-header bulk-select-enabled")
|
||||
this.additionalClasses
|
||||
}}
|
||||
aria-labelledby="topic-list-heading"
|
||||
...attributes
|
||||
>
|
||||
<caption class="sr-only">{{i18n "sr_topic_list_caption"}}</caption>
|
||||
|
||||
@@ -475,3 +475,8 @@ div.education {
|
||||
align-self: center;
|
||||
line-height: var(--line-height-medium);
|
||||
}
|
||||
|
||||
#topic-list-heading {
|
||||
line-height: var(--line-height-medium);
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
@@ -5200,6 +5200,53 @@ en:
|
||||
powered_by_discourse: "Powered by Discourse"
|
||||
safari_15_warning: "Your browser will soon be incompatible with this community. To keep participating here, please upgrade your browser or <a href='%{url}'>learn more</a>."
|
||||
|
||||
discovery:
|
||||
headings:
|
||||
all:
|
||||
default: "All %{filter} topics"
|
||||
posted: "All topics you’ve posted in"
|
||||
bookmarks: "All topics you’ve bookmarked"
|
||||
|
||||
category:
|
||||
default: "%{filter} topics in %{category}"
|
||||
posted: "Topics in %{category} you’ve posted in"
|
||||
bookmarks: "Topics in %{category} you’ve bookmarked"
|
||||
|
||||
no_tags:
|
||||
all_default: "All topics without tags"
|
||||
category_default: "%{filter} topics in %{category} without tags"
|
||||
all:
|
||||
posted: "All topics without tags you've posted in"
|
||||
bookmarks: "All topics without tags you've bookmarked"
|
||||
category:
|
||||
posted: "Topics in %{category} without tags you've posted in"
|
||||
bookmarks: "Topics in %{category} without tags you've bookmarked"
|
||||
|
||||
single_tag:
|
||||
default: "%{filter} topics tagged %{tag}"
|
||||
posted: "Topics tagged %{tag} you've posted in"
|
||||
bookmarks: "Topics tagged %{tag} you’ve bookmarked"
|
||||
|
||||
multi_tag:
|
||||
default: "%{filter} topics tagged %{tags}"
|
||||
posted: "Topics tagged %{tags} you've posted in"
|
||||
bookmarks: "Topics tagged %{tags} you’ve bookmarked"
|
||||
|
||||
category_tag:
|
||||
default: "%{filter} topics in %{category} tagged %{tags}"
|
||||
posted: "Topics in %{category} tagged %{tags} you’ve posted in"
|
||||
bookmarks: "Topics in %{category} tagged %{tags} you’ve bookmarked"
|
||||
|
||||
categories: "All categories"
|
||||
|
||||
filter_labels:
|
||||
latest: "Latest"
|
||||
top: "Top"
|
||||
new: "New"
|
||||
unread: "Unread"
|
||||
hot: "Hot"
|
||||
unseen: "Unseen"
|
||||
|
||||
# This section is exported to the javascript for i18n in the admin section
|
||||
admin_js:
|
||||
# This is a text input placeholder, keep the translation short
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "Discovery heading accessibility", type: :system do
|
||||
let(:discovery) { PageObjects::Pages::Discovery.new }
|
||||
|
||||
fab!(:category) { Fabricate(:category, name: "General") }
|
||||
fab!(:tag) { Fabricate(:tag, name: "help") }
|
||||
|
||||
fab!(:topic_in_category) { Fabricate(:topic, category: category) }
|
||||
fab!(:topic_with_tag) { Fabricate(:topic, tags: [tag]) }
|
||||
fab!(:topic_in_category_tagged) { Fabricate(:topic, category: category, tags: [tag]) }
|
||||
|
||||
it "shows correct heading for category view" do
|
||||
visit "/c/#{category.slug}/#{category.id}"
|
||||
expect(page).to have_selector("h1", text: "Latest topics in General")
|
||||
end
|
||||
|
||||
it "shows correct heading for tagged view" do
|
||||
visit "/tags/#{tag.name}"
|
||||
expect(page).to have_selector("h1", text: "Latest topics tagged help")
|
||||
end
|
||||
|
||||
it "shows correct heading for category + tag view" do
|
||||
visit "/tags/c/#{category.slug}/#{category.id}/#{tag.name}"
|
||||
expect(page).to have_selector("h1", text: "Latest topics in General tagged help")
|
||||
end
|
||||
|
||||
it "shows correct heading for no tags view" do
|
||||
visit "/tags/none"
|
||||
expect(page).to have_selector("h1", text: "All topics without tags")
|
||||
end
|
||||
|
||||
it "shows default heading for latest topics" do
|
||||
visit "/latest"
|
||||
expect(page).to have_selector("h1", text: "All Latest topics")
|
||||
end
|
||||
|
||||
it "shows heading for all categories view" do
|
||||
visit "/categories"
|
||||
expect(page).to have_selector("h1", text: "All categories")
|
||||
end
|
||||
|
||||
it "shows heading for bookmarked topics" do
|
||||
sign_in(Fabricate(:user))
|
||||
|
||||
visit "/bookmarks"
|
||||
expect(page).to have_selector("h1", text: "All topics you’ve bookmarked")
|
||||
end
|
||||
|
||||
it "shows heading for posted topics" do
|
||||
sign_in(Fabricate(:user))
|
||||
|
||||
visit "/posted"
|
||||
expect(page).to have_selector("h1", text: "All topics you’ve posted in")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user