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:
@@ -21,14 +21,23 @@ class Admin::ActionComponent < ApplicationComponent
|
|||||||
def html_options
|
def html_options
|
||||||
{
|
{
|
||||||
class: html_class,
|
class: html_class,
|
||||||
|
"aria-label": label,
|
||||||
data: { confirm: confirmation_text }
|
data: { confirm: confirmation_text }
|
||||||
}.merge(options.except(:confirm, :path, :text))
|
}.merge(options.except(:"aria-label", :confirm, :path, :text))
|
||||||
end
|
end
|
||||||
|
|
||||||
def html_class
|
def html_class
|
||||||
"#{action.to_s.gsub("_", "-")}-link"
|
"#{action.to_s.gsub("_", "-")}-link"
|
||||||
end
|
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
|
def confirmation_text
|
||||||
if options[:confirm] == true
|
if options[:confirm] == true
|
||||||
t("admin.actions.confirm")
|
t("admin.actions.confirm")
|
||||||
@@ -37,6 +46,14 @@ class Admin::ActionComponent < ApplicationComponent
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def record_name
|
||||||
|
if record.respond_to?(:human_name)
|
||||||
|
record.human_name
|
||||||
|
else
|
||||||
|
record.to_s.humanize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def default_path
|
def default_path
|
||||||
if %i[answers configure destroy preview show].include?(action.to_sym)
|
if %i[answers configure destroy preview show].include?(action.to_sym)
|
||||||
namespaced_polymorphic_path(namespace, record)
|
namespaced_polymorphic_path(namespace, record)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class Admin::TableActionsComponent < ApplicationComponent
|
|||||||
end
|
end
|
||||||
|
|
||||||
def action(action_name, **args)
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class ApplicationRecord < ActiveRecord::Base
|
class ApplicationRecord < ActiveRecord::Base
|
||||||
|
include HumanName
|
||||||
self.abstract_class = true
|
self.abstract_class = true
|
||||||
|
|
||||||
def self.sample(count = 1)
|
def self.sample(count = 1)
|
||||||
|
|||||||
9
app/models/concerns/human_name.rb
Normal file
9
app/models/concerns/human_name.rb
Normal 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
|
||||||
@@ -8,4 +8,8 @@ class Dashboard::AdministratorTask < ApplicationRecord
|
|||||||
|
|
||||||
scope :pending, -> { where(executed_at: nil) }
|
scope :pending, -> { where(executed_at: nil) }
|
||||||
scope :done, -> { where.not(executed_at: nil) }
|
scope :done, -> { where.not(executed_at: nil) }
|
||||||
|
|
||||||
|
def title
|
||||||
|
"#{source.proposal.title} #{source.action.title}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ class LocalCensusRecord < ApplicationRecord
|
|||||||
|
|
||||||
scope :search, ->(terms) { where("document_number ILIKE ?", "%#{terms}%") }
|
scope :search, ->(terms) { where("document_number ILIKE ?", "%#{terms}%") }
|
||||||
|
|
||||||
|
def title
|
||||||
|
"#{ApplicationController.helpers.humanize_document_type(document_type)} #{document_number}"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def sanitize
|
def sanitize
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ class Poll
|
|||||||
belongs_to :booth
|
belongs_to :booth
|
||||||
belongs_to :poll
|
belongs_to :poll
|
||||||
|
|
||||||
|
delegate :name, to: :booth
|
||||||
|
|
||||||
before_destroy :destroy_poll_shifts, only: :destroy
|
before_destroy :destroy_poll_shifts, only: :destroy
|
||||||
|
|
||||||
has_many :officer_assignments, dependent: :destroy
|
has_many :officer_assignments, dependent: :destroy
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ class Poll
|
|||||||
after_create :create_officer_assignments
|
after_create :create_officer_assignments
|
||||||
before_destroy :destroy_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
|
def persist_data
|
||||||
self.officer_name = officer.name
|
self.officer_name = officer.name
|
||||||
self.officer_email = officer.email
|
self.officer_email = officer.email
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ en:
|
|||||||
confirm_hide: Confirm moderation
|
confirm_hide: Confirm moderation
|
||||||
hide: Hide
|
hide: Hide
|
||||||
hide_author: Hide author
|
hide_author: Hide author
|
||||||
|
label: "%{action} %{name}"
|
||||||
restore: Restore
|
restore: Restore
|
||||||
mark_featured: Featured
|
mark_featured: Featured
|
||||||
unmark_featured: Unmark featured
|
unmark_featured: Unmark featured
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ es:
|
|||||||
confirm_hide: Confirmar moderación
|
confirm_hide: Confirmar moderación
|
||||||
hide: Ocultar
|
hide: Ocultar
|
||||||
hide_author: Bloquear al autor
|
hide_author: Bloquear al autor
|
||||||
|
label: "%{action} %{name}"
|
||||||
restore: Volver a mostrar
|
restore: Volver a mostrar
|
||||||
mark_featured: Destacar
|
mark_featured: Destacar
|
||||||
unmark_featured: Quitar destacado
|
unmark_featured: Quitar destacado
|
||||||
|
|||||||
@@ -6,4 +6,58 @@ describe Admin::ActionComponent do
|
|||||||
|
|
||||||
expect(page).to have_css "a.edit-link"
|
expect(page).to have_css "a.edit-link"
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
describe Admin::TableActionsComponent, controller: Admin::BaseController do
|
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
|
it "renders links to edit and destroy a record by default" do
|
||||||
render_inline Admin::TableActionsComponent.new(record)
|
render_inline Admin::TableActionsComponent.new(record)
|
||||||
|
|
||||||
expect(page).to have_css "a", count: 2
|
expect(page).to have_css "a", count: 2
|
||||||
expect(page).to have_css "a[href*='edit']", text: "Edit"
|
expect(page).to have_css "a[href*='edit']", exact_text: "Edit"
|
||||||
expect(page).to have_css "a[data-method='delete']", text: "Delete"
|
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
|
end
|
||||||
|
|
||||||
context "actions parameter is passed" do
|
context "actions parameter is passed" do
|
||||||
|
|||||||
85
spec/models/human_name_spec.rb
Normal file
85
spec/models/human_name_spec.rb
Normal 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
|
||||||
Reference in New Issue
Block a user