Use aria-label in admin table actions

This way screen reader users will know which record they're going to
access when focusing on a link to a certain action. Otherwise they'd
hear something like "Edit, link", and they wouldn't know which record
they'll end up editing if they follow the link.
This commit is contained in:
Javi Martín
2021-08-17 22:29:11 +02:00
parent 6a2c01b119
commit 2b4b2f3442
13 changed files with 189 additions and 5 deletions

View File

@@ -21,14 +21,23 @@ class Admin::ActionComponent < ApplicationComponent
def html_options
{
class: html_class,
"aria-label": label,
data: { confirm: confirmation_text }
}.merge(options.except(:confirm, :path, :text))
}.merge(options.except(:"aria-label", :confirm, :path, :text))
end
def html_class
"#{action.to_s.gsub("_", "-")}-link"
end
def label
if options[:"aria-label"] == true
t("admin.actions.label", action: text, name: record_name)
else
options[:"aria-label"]
end
end
def confirmation_text
if options[:confirm] == true
t("admin.actions.confirm")
@@ -37,6 +46,14 @@ class Admin::ActionComponent < ApplicationComponent
end
end
def record_name
if record.respond_to?(:human_name)
record.human_name
else
record.to_s.humanize
end
end
def default_path
if %i[answers configure destroy preview show].include?(action.to_sym)
namespaced_polymorphic_path(namespace, record)

View File

@@ -7,7 +7,7 @@ class Admin::TableActionsComponent < ApplicationComponent
end
def action(action_name, **args)
render Admin::ActionComponent.new(action_name, record, **args)
render Admin::ActionComponent.new(action_name, record, "aria-label": true, **args)
end
private

View File

@@ -1,4 +1,5 @@
class ApplicationRecord < ActiveRecord::Base
include HumanName
self.abstract_class = true
def self.sample(count = 1)

View File

@@ -0,0 +1,9 @@
module HumanName
def human_name
%i[title name subject].each do |method|
return send(method) if respond_to?(method)
end
raise "Must implement a method defining a human name"
end
end

View File

@@ -8,4 +8,8 @@ class Dashboard::AdministratorTask < ApplicationRecord
scope :pending, -> { where(executed_at: nil) }
scope :done, -> { where.not(executed_at: nil) }
def title
"#{source.proposal.title} #{source.action.title}"
end
end

View File

@@ -10,6 +10,10 @@ class LocalCensusRecord < ApplicationRecord
scope :search, ->(terms) { where("document_number ILIKE ?", "%#{terms}%") }
def title
"#{ApplicationController.helpers.humanize_document_type(document_type)} #{document_number}"
end
private
def sanitize

View File

@@ -3,6 +3,8 @@ class Poll
belongs_to :booth
belongs_to :poll
delegate :name, to: :booth
before_destroy :destroy_poll_shifts, only: :destroy
has_many :officer_assignments, dependent: :destroy

View File

@@ -16,6 +16,10 @@ class Poll
after_create :create_officer_assignments
before_destroy :destroy_officer_assignments
def title
"#{I18n.t("admin.poll_shifts.#{task}")} #{officer_name} #{I18n.l(date.to_date, format: :long)}"
end
def persist_data
self.officer_name = officer.name
self.officer_email = officer.email

View File

@@ -8,6 +8,7 @@ en:
confirm_hide: Confirm moderation
hide: Hide
hide_author: Hide author
label: "%{action} %{name}"
restore: Restore
mark_featured: Featured
unmark_featured: Unmark featured

View File

@@ -8,6 +8,7 @@ es:
confirm_hide: Confirmar moderación
hide: Ocultar
hide_author: Bloquear al autor
label: "%{action} %{name}"
restore: Volver a mostrar
mark_featured: Destacar
unmark_featured: Quitar destacado

View File

@@ -6,4 +6,58 @@ describe Admin::ActionComponent do
expect(page).to have_css "a.edit-link"
end
describe "aria-label attribute" do
it "is not rendered by default" do
record = double(human_name: "Stay home")
render_inline Admin::ActionComponent.new(:edit, record, path: "/")
expect(page).to have_link count: 1
expect(page).not_to have_css "[aria-label]"
end
it "is not rendered when aria-label is nil" do
render_inline Admin::ActionComponent.new(:edit, double, path: "/", "aria-label": nil)
expect(page).to have_link count: 1
expect(page).not_to have_css "[aria-label]"
end
it "renders with the given value" do
render_inline Admin::ActionComponent.new(:edit, double, path: "/", "aria-label": "Modify")
expect(page).to have_link count: 1
expect(page).to have_css "[aria-label='Modify']"
end
context "when aria-label is true" do
it "includes the action and the human_name of the record" do
record = double(human_name: "Stay home")
render_inline Admin::ActionComponent.new(:edit, record, path: "/", "aria-label": true)
expect(page).to have_link count: 1
expect(page).to have_css "a[aria-label='Edit Stay home']", exact_text: "Edit"
end
it "uses the to_s method when there's no human_name" do
record = double(to_s: "do_not_go_out")
render_inline Admin::ActionComponent.new(:edit, record, path: "/", "aria-label": true)
expect(page).to have_link count: 1
expect(page).to have_css "a[aria-label='Edit Do not go out']", exact_text: "Edit"
end
it "uses the human_name when there are both human_name and to_s" do
record = double(human_name: "Stay home", to_s: "do_not_go_out")
render_inline Admin::ActionComponent.new(:edit, record, path: "/", "aria-label": true)
expect(page).to have_link count: 1
expect(page).to have_css "a[aria-label='Edit Stay home']", exact_text: "Edit"
end
end
end
end

View File

@@ -1,14 +1,16 @@
require "rails_helper"
describe Admin::TableActionsComponent, controller: Admin::BaseController do
let(:record) { create(:banner) }
let(:record) { create(:banner, title: "Important!") }
it "renders links to edit and destroy a record by default" do
render_inline Admin::TableActionsComponent.new(record)
expect(page).to have_css "a", count: 2
expect(page).to have_css "a[href*='edit']", text: "Edit"
expect(page).to have_css "a[data-method='delete']", text: "Delete"
expect(page).to have_css "a[href*='edit']", exact_text: "Edit"
expect(page).to have_css "a[aria-label='Edit Important!']", exact_text: "Edit"
expect(page).to have_css "a[data-method='delete']", exact_text: "Delete"
expect(page).to have_css "a[aria-label='Delete Important!']", exact_text: "Delete"
end
context "actions parameter is passed" do

View File

@@ -0,0 +1,85 @@
require "rails_helper"
describe HumanName do
describe "#human_name" do
it "uses the title when available" do
model = Class.new do
include HumanName
def title
"I am fire"
end
end
expect(model.new.human_name).to eq "I am fire"
end
it "uses the name when available" do
model = Class.new do
include HumanName
def name
"Be like water"
end
end
expect(model.new.human_name).to eq "Be like water"
end
it "uses the subject when available" do
model = Class.new do
include HumanName
def subject
"20% off on fire and water"
end
end
expect(model.new.human_name).to eq "20% off on fire and water"
end
it "prioritizes title over name and subject" do
model = Class.new do
include HumanName
def title
"I am fire"
end
def name
"Be like water"
end
def subject
"20% off on fire and water"
end
end
expect(model.new.human_name).to eq "I am fire"
end
it "prioritizes name over subject" do
model = Class.new do
include HumanName
def name
"Be like water"
end
def subject
"20% off on fire and water"
end
end
expect(model.new.human_name).to eq "Be like water"
end
it "raises an exception when no methods are defined" do
model = Class.new do
include HumanName
end
expect { model.new.human_name }.to raise_error RuntimeError
end
end
end