In using SimpleForm we are often finding that we need aria-labelledby and aria-describedby; I have a rudimentary solution in place but it looks very fragile and subject to breakage as SimpleForm moves forward.
I'm exploring adding a new component (akin to the SimpleForm::Components::HTML5) for SimpleForm::Components::AriaLabelledBy and SimpleForm::Components::AriaDescribedBy.
I'm presently looking at how best to go about submitting a pull request for this behavior. Its looking like a rather lengthy process as I orient to the code of SimpleForm. I want to make sure that this is something that would be a reasonable addition to the SimpleForm ecosystem.
I believe it is not a good fit for the library but I agree we should make easier to create and register new components.
See #1176
Thank you for the prompt response. And more importantly, thank you for your time on SimpleForm (and other open soure projects).
I spent a bit of time exploring adding a component, and due to the inter-relation of labels and inputs the aria-labelledby bled into the other components. It was a bit confusing for me.
For reference, I was able to "cheat" and bring aria-labelledby and aria-describedby behaviors into play via the following. It is not complete, but works for our narrow case. It does provide some potential guidance for others; Though a proper component would be a better solution.
SimpleForm.setup do |config|
config.wrappers :bootstrap, :tag => 'div', :class => 'control-group', :error_class => 'error' do |b|
b.use :html5
b.use :placeholder
b.wrapper :tag => 'span', :class => 'control-label' do |bb|
bb.use :label
bb.use :override_hint
end
b.wrapper :tag => 'div', :class => 'controls' do |ba|
ba.use :override_help
ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' }
end
end
end
module SimpleForm
module Components
module Labels
# I chose Labels because it extends ActiveSupport::Concern and thus
# I can more readily guarantee that the below module is mixed into all
# classes, SimpleForm::Inputs::Base and its descendant classes
included do
include SimpleFormOverride::Labels
end
end
end
end
module SimpleFormOverride
module Labels
extend ActiveSupport::Concern
# To provide the proper arialabelled-by attribute, I'm adding an ID to the
# label that can be referenced.
def raw_label_text
template.content_tag('span', id: override_dom_id('label')) do
super
end
end
def hint_override
return '' unless has_hint?
template.content_tag('span', class: 'field-hint', role: 'tooltip', id: override_dom_id('hint')) do
hint
end
end
def input_override
@input_html_options[:'aria-labelledby'] = override_dom_id('label')
@input_html_options[:'aria-describedby'] = override_dom_id('hint')
input
end
private
def override_dom_id(suffix)
"#{object_name}_#{attribute_name}_#{suffix}"
end
end
end
Is there any progress in this? I'm facing the task to make Rails forms more accessible which needs to assign the validation errors to the specific input fields so screenreader users see them. Also, the focus should be set into the first invalid input field.
At the time being, I'm simply using a JavaScript workaround to do this:
class App.FormErrorsAccessibilizer
constructor: (el) ->
@$el = $(el) # Always pass an element to the constructor and make it available as a jQuery selector!
@formGroups = @$el.find('.form-group.has-error')
if @formGroups.size() > 0
@describe_inputs_with_help_blocks()
@focus_first_input_with_help_block()
describe_inputs_with_help_blocks: ->
for formGroup in @formGroups
$formGroup = $(formGroup)
$help = $formGroup.find('.help-block')
$input = $formGroup.find(':input')
input_id = $input.attr('id')
help_id = "#{input_id}_help"
$help.attr('id', help_id)
$input.attr('aria-describedby', help_id)
focus_first_input_with_help_block: ->
@formGroups.first().find(':input').focus()
This works nicely, but it's a bit error-prone and hard to test. It would be very nice if Simple_Form would offer some accessibility plugin.
If anyone is interested in my specs, here they are:
require 'rails_helper'
describe 'Accessibility' do
describe 'form validations' do
it 'assigns the help blocks with the inputs through aria-describedby', js: true do
visit new_user_registration_path
click_button 'Sign up'
expect(page).to have_css 'input#user_name[aria-describedby="user_name_help"]'
expect(page).to have_css '#user_name_help'
end
it 'sets the focus to the first invalid element', js: true do
visit new_user_registration_path
fill_in 'user_name', with: 'daisy' # Make sure the first occurring element is valid, so explicitly the second one (email) should gain focus
click_button 'Sign up'
# Check for focused element, see http://stackoverflow.com/questions/7940525/testing-focus-with-capybara
expect(page.evaluate_script('document.activeElement.id')).to eq 'user_email'
end
end
end
I just had another idea, something like fixing this problem in the configuration:
ba.use :input, aria: {describedby: 'xyz_error xyz_hint'} # xyz would be the input's ID
ba.use :error, wrap_with: { tag: 'span', class: 'help-block', id: 'xyz_error' }
ba.use :hint, wrap_with: { tag: 'p', class: 'help-block', id: 'xyz_hint' }
The tricky part here is that aria-describedby should only be assigned to the input when there's an alert and/or a hint. Any idea on how to accomplish this? One could maybe monkey-patch the Input class, but I don't know whether this is the best approach.
@jmuheim I believe the changes I proposed might work for what you need. But I've had to set them aside. Keep me posted if you dive into this.
Have there been any developments on this topic? We're in the same boat, where something like https://github.com/plataformatec/simple_form/issues/1175#issuecomment-86887671 would be awesome. Thanks!
@jeremyf I realize that your work on this was a long time ago, but I wonder if you've had any chance to look at it further. It would be very valuable to me to see a more complete implementation.
For others finding this issue later, I got this working by using:
input_html: { aria: { labelledby: 'your-label' } }
maybe this is obvious for some, but it took a bit of time to get it right.
Most helpful comment
For others finding this issue later, I got this working by using:
maybe this is obvious for some, but it took a bit of time to get it right.