0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-04 10:39:43 +08:00
discourse/lib/category_badge.rb
Kris c1614b9a67
A11Y: don't use the title attribute for category and tag descriptions (#42005)
We've been improperly adding category and tag descriptions to the title
attribute of category and tag links. This removes them.


[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/title)
summarizes the problem with title attributes used as supplemental
information:

> Use of the title attribute is highly problematic for:
>
>    People using touch-only devices
>    People navigating with keyboards
> People navigating with assistive technology such as screen readers or
magnifiers
>    People experiencing fine motor control impairment
>    People with cognitive concerns
>
> This is due to inconsistent browser support, compounded by the
additional assistive technology parsing of the browser-rendered page. If
a tooltip effect is desired, it is better to [use a more accessible
technique](https://inclusive-components.design/tooltips-toggletips/)
that can be accessed with the above browsing methods.



The most immediate issue is that the title descriptions can be very long
for screenreaders and can make navigating the page fairly verbose.

What someone using JAWS comes across every time they tab through this
category:

<img width="450" alt="image"
src="https://github.com/user-attachments/assets/2277df83-4cad-4f86-8c86-147c0458bd26"
/>

We already have this information available in the "about this category"
topic, in category dropdowns, category headings (when visible) and other
places, so it's not critical to have here.

There's also a general asymmetry: 

* mouse users: can optionally see them on hover
* keyboard users: can never access them
* touch users: can never access them 
* screenreaders: some read them so they have to be skipped every time 

If we find these sort of tooltips valuable and want to add a description
on hover, we should do it in a way that's reachable to
hover/keyboard/touch without adding verbosity to typical nav for
screenreaders.
2026-07-24 09:49:41 -04:00

111 lines
3.7 KiB
Ruby
Vendored

# frozen_string_literal: true
module CategoryBadge
def self.html_for(category, opts = nil)
opts ||= {}
# Bail if there is no category, hide uncategorized by default
return "" if category.blank? || (category.uncategorized? && !opts[:show_uncategorized])
if opts[:inline_style]
# Inline styles for email
style_for_email(category, opts)
else
# Browser styles
style_for_browser(category, opts)
end
end
def self.shared_data(category, opts)
{
parent_category: fetch_parent_category(category),
category_url:
opts[:absolute_url] ? "#{Discourse.base_url_no_prefix}#{category.url}" : category.url,
extra_classes: opts[:extra_classes].to_s,
}
end
def self.fetch_parent_category(category)
Category.find_by(id: category.parent_category_id) if category.parent_category_id
end
def self.map_styles_to_string(styles)
styles.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)};" }.join(" ")
end
def self.wrap_in_link(content, url, extra_classes = "", style_value = nil)
style_attr = style_value ? " style='#{style_value}'" : ""
"<a class='badge-category__wrapper #{extra_classes}' href='#{url}'#{style_attr}>#{content}</a>".html_safe
end
def self.style_for_browser(category, opts)
data = shared_data(category, opts)
style_class =
if category.respond_to?(:style_type) && category.style_type.present?
case category.style_type
when "icon"
"--style-icon"
when "emoji"
"--style-emoji"
end
end || "--style-square"
class_names = "badge-category #{style_class} #{data[:parent_category] ? "--has-parent" : ""}"
badge_styles = {
"--category-badge-color": "##{category.color}",
"--category-badge-text-color": "##{category.text_color}",
}
badge_styles["--parent-category-badge-color"] = "##{data[:parent_category].color}" if data[
:parent_category
]
result = +""
result << "<span data-category-id='#{category.id}'"
result << " style='#{map_styles_to_string(badge_styles)}'"
result << " data-parent-category-id='#{data[:parent_category].id}'" if data[:parent_category]
result << " data-drop-close='true' class='#{class_names}'>"
if category.style_type == "icon" && category.icon.present?
result << SvgSprite.raw_svg(category.icon)
elsif category.style_type == "emoji" && category.emoji.present?
result << Emoji.codes_to_img(":#{category.emoji}:")
end
result << "<span class='badge-category__name'>"
result << ERB::Util.html_escape(category.name)
result << "</span></span>"
wrap_in_link(result, data[:category_url], data[:extra_classes])
end
def self.style_for_email(category, opts)
data = shared_data(category, opts)
badge_styles = {
display: "inline-block",
width: "0.72em",
height: "0.72em",
"margin-right": "0.33em",
"background-color": "##{category.color}",
}
result = +""
result << "<span data-category-id='#{category.id}'"
result << " data-parent-category-id='#{data[:parent_category].id}'" if data[:parent_category]
result << " data-drop-close='true'>"
result << "<span>"
result << "<span style='#{map_styles_to_string(badge_styles)}'>"
if data[:parent_category]
parent_badge_styles = { display: "block", width: "0.36em", height: "0.72em" }
parent_badge_styles["background-color"] = "##{data[:parent_category].color}"
parent_badge_style_value = map_styles_to_string(parent_badge_styles)
result << "<span style='#{parent_badge_style_value}'></span>"
end
result << "</span>"
result << ERB::Util.html_escape(category.name)
result << "</span></span>"
wrap_in_link(result, data[:category_url])
end
end