Skip to content

Commit b5e127a

Browse files
authored
Merge pull request #782 from lcreid/743-aria-describedby-for-errors
`aria-describedby` for Errors
2 parents 54094b7 + 38f4b34 commit b5e127a

24 files changed

+594
-144
lines changed

README.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,38 +1493,38 @@ Generated HTML:
14931493
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
14941494
<div class="mb-3">
14951495
<label class="form-label required" for="user_email">Email</label>
1496-
<input class="form-control is-invalid" id="user_email" name="user[email]" required="required" type="email" value="steve.example.com">
1497-
<div class="invalid-feedback">is invalid</div>
1496+
<input aria-describedby="user_email_feedback" class="form-control is-invalid" id="user_email" name="user[email]" required="required" type="email" value="steve.example.com">
1497+
<div class="invalid-feedback" id="user_email_feedback">is invalid</div>
14981498
</div>
14991499
<div aria-labelledby="user_misc" class="mb-3" role="group">
15001500
<div class="form-label" id="user_misc">Misc</div>
15011501
<div class="form-check">
1502-
<input checked class="form-check-input is-invalid" id="user_misc_1" name="user[misc]" type="radio" value="1">
1502+
<input aria-describedby="user_misc_feedback" checked class="form-check-input is-invalid" id="user_misc_1" name="user[misc]" type="radio" value="1">
15031503
<label class="form-check-label" for="user_misc_1">Mind reading</label>
15041504
</div>
15051505
<div class="form-check">
1506-
<input class="form-check-input is-invalid" id="user_misc_2" name="user[misc]" type="radio" value="2">
1506+
<input aria-describedby="user_misc_feedback" class="form-check-input is-invalid" id="user_misc_2" name="user[misc]" type="radio" value="2">
15071507
<label class="form-check-label" for="user_misc_2">Farming</label>
1508-
<div class="invalid-feedback">is invalid</div>
1508+
<div class="invalid-feedback" id="user_misc_feedback">is invalid</div>
15091509
</div>
15101510
</div>
15111511
<input id="user_preferences" name="user[preferences][]" type="hidden" value="">
15121512
<div aria-labelledby="user_preferences" class="mb-3" role="group">
15131513
<div class="form-label" id="user_preferences">Preferences</div>
15141514
<div class="form-check">
1515-
<input checked class="form-check-input is-invalid" id="user_preferences_1" name="user[preferences][]" type="checkbox" value="1">
1515+
<input aria-describedby="user_preferences_feedback" checked class="form-check-input is-invalid" id="user_preferences_1" name="user[preferences][]" type="checkbox" value="1">
15161516
<label class="form-check-label" for="user_preferences_1">Good</label>
15171517
</div>
15181518
<div class="form-check">
1519-
<input class="form-check-input is-invalid" id="user_preferences_2" name="user[preferences][]" type="checkbox" value="2">
1519+
<input aria-describedby="user_preferences_feedback" class="form-check-input is-invalid" id="user_preferences_2" name="user[preferences][]" type="checkbox" value="2">
15201520
<label class="form-check-label" for="user_preferences_2">Bad</label>
1521-
<div class="invalid-feedback">is invalid</div>
1521+
<div class="invalid-feedback" id="user_preferences_feedback">is invalid</div>
15221522
</div>
15231523
</div>
15241524
<div class="mb-3">
15251525
<label class="form-label" for="user_address_attributes_street">Street</label>
1526-
<input class="form-control is-invalid" id="user_address_attributes_street" name="user[address_attributes][street]" type="text" value="Bar">
1527-
<div class="invalid-feedback">is invalid</div>
1526+
<input aria-describedby="user_address_attributes_street_feedback" class="form-control is-invalid" id="user_address_attributes_street" name="user[address_attributes][street]" type="text" value="Bar">
1527+
<div class="invalid-feedback" id="user_address_attributes_street_feedback">is invalid</div>
15281528
</div>
15291529
</form>
15301530
```
@@ -1554,8 +1554,8 @@ Generated HTML:
15541554
```html
15551555
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
15561556
<div class="mb-3">
1557-
<label class="form-label required text-danger" for="user_email">Email is invalid</label>
1558-
<input class="form-control is-invalid" id="user_email" name="user[email]" required="required" type="email" value="steve.example.com">
1557+
<label class="form-label required text-danger" for="user_email" id="user_email_feedback">Email is invalid</label>
1558+
<input aria-describedby="user_email_feedback" class="form-control is-invalid" id="user_email" name="user[email]" required="required" type="email" value="steve.example.com">
15591559
</div>
15601560
</form>
15611561
```
@@ -1652,7 +1652,7 @@ Which outputs:
16521652
```html
16531653
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
16541654
<input autocomplete="off" class="is-invalid" disabled type="hidden">
1655-
<div class="invalid-feedback">Email is invalid</div>
1655+
<div class="invalid-feedback" id="user_email_feedback">Email is invalid</div>
16561656
</form>
16571657
```
16581658

@@ -1673,7 +1673,7 @@ Which outputs:
16731673
```html
16741674
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
16751675
<input autocomplete="off" class="is-invalid" disabled type="hidden">
1676-
<div class="invalid-feedback">is invalid</div>
1676+
<div class="invalid-feedback" id="user_email_feedback">is invalid</div>
16771677
</form>
16781678
```
16791679

@@ -1692,7 +1692,7 @@ Which outputs:
16921692
```html
16931693
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
16941694
<input autocomplete="off" class="is-invalid" disabled type="hidden">
1695-
<div class="custom-error">Email is invalid</div>
1695+
<div class="custom-error" id="user_email_feedback">Email is invalid</div>
16961696
</form>
16971697
```
16981698

lib/bootstrap_form/components/labels.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def prepare_label_options(id, name, options, custom_label_col, group_layout)
5252

5353
options[:class] = label_classes(name, options, custom_label_col, group_layout)
5454
options.delete(:class) if options[:class].none?
55+
options[:id] = aria_feedback_id(name:, id:) if error?(name) && label_errors
5556
end
5657
end
5758
end

lib/bootstrap_form/components/validation.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,14 @@ def inline_error?(name)
6161
error?(name) && inline_errors
6262
end
6363

64-
def generate_error(name)
64+
def generate_error(name, id)
6565
return unless inline_error?(name)
6666

6767
help_text = get_error_messages(name)
6868
help_klass = "invalid-feedback"
6969
help_tag = :div
7070

71-
content_tag(help_tag, help_text, class: help_klass)
71+
content_tag(help_tag, help_text, class: help_klass, id: aria_feedback_id(id:, name:))
7272
end
7373

7474
def get_error_messages(name)
@@ -84,6 +84,10 @@ def get_error_messages(name)
8484
safe_join(object.errors[name], ", ")
8585
end
8686
# rubocop:enable Metrics/AbcSize
87+
88+
def aria_feedback_id(name:, id: nil)
89+
id.present? ? "#{id}_feedback" : field_id(name, :feedback)
90+
end
8791
end
8892
end
8993
end

lib/bootstrap_form/form_group.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def form_group_content_tag(name, field_name, without_field_name, options, html_o
2323
html_class = control_specific_class(field_name)
2424
html_class = "#{html_class} col-auto g-3" if @layout == :horizontal && options[:skip_inline].blank?
2525
tag.div(class: html_class) do
26-
input_with_error(name) do
26+
input_with_error(name, options[:id]) do
2727
send(without_field_name, name, options, html_options)
2828
end
2929
end

lib/bootstrap_form/form_group_builder.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ def form_group_css_options(method, html_options, options)
9898
# Add control_class; allow it to be overridden by :control_class option
9999
control_classes = css_options.delete(:control_class) { control_class }
100100
css_options[:class] = safe_join([control_classes, css_options[:class]].compact, " ")
101-
css_options[:class] << " is-invalid" if error?(method)
101+
if error?(method)
102+
css_options[:class] << " is-invalid"
103+
css_options[:aria] = { describedby: aria_feedback_id(id: options[:id], name: method) }
104+
end
102105
css_options[:placeholder] = form_group_placeholder(options, method) if options[:label_as_placeholder]
103106
css_options
104107
end

lib/bootstrap_form/helpers/bootstrap.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ def errors_on(name, options={})
3434
hide_attribute_name = options[:hide_attribute_name] || false
3535
custom_class = options[:custom_class] || false
3636

37-
tag.div class: custom_class || "invalid-feedback" do
37+
tag.div(
38+
class: custom_class || "invalid-feedback",
39+
id: aria_feedback_id(id: options[:id], name:)
40+
) do
3841
errors = if hide_attribute_name
3942
object.errors[name]
4043
else
@@ -66,20 +69,21 @@ def custom_control(*args, &)
6669
end
6770

6871
def prepend_and_append_input(name, options, &)
72+
id = options[:id]
6973
options = options.extract!(:prepend, :append, :input_group_class).compact
7074

7175
input = capture(&) || ActiveSupport::SafeBuffer.new
7276

7377
input = attach_input(options, :prepend) + input + attach_input(options, :append)
74-
input << generate_error(name)
78+
input << generate_error(name, id)
7579
options.present? &&
7680
input = tag.div(input, class: ["input-group", options[:input_group_class]].compact)
7781
input
7882
end
7983

80-
def input_with_error(name, &)
84+
def input_with_error(name, id, &)
8185
input = capture(&)
82-
input << generate_error(name)
86+
input << generate_error(name, id)
8387
end
8488

8589
def input_group_content(content)

lib/bootstrap_form/inputs/base.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ def bootstrap_field(field_name)
2424

2525
def bootstrap_select_group(field_name)
2626
define_method(:"#{field_name}_with_bootstrap") do |name, options={}, html_options={}|
27+
# Specifying the id for a select doesn't work. The Rails helpers need to generate
28+
# what they generate, and that includes the ids for each select option.
29+
options.delete(:id)
2730
html_options = html_options.reverse_merge(control_class: "form-select")
2831
form_group_builder(name, options, html_options) do
2932
form_group_content_tag(name, field_name, "#{field_name}_without_bootstrap", options, html_options)

lib/bootstrap_form/inputs/check_box.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def check_box_with_bootstrap(name, options={}, checked_value="1", unchecked_valu
1313
content = tag.div(class: check_box_wrapper_class(options), **options[:wrapper].to_h.except(:class)) do
1414
html = check_box_without_bootstrap(name, check_box_options(name, options), checked_value, unchecked_value)
1515
html << check_box_label(name, options, checked_value, &block) unless options[:skip_label]
16-
html << generate_error(name) if options[:error_message]
16+
html << generate_error(name, options[:id]) if options[:error_message]
1717
html
1818
end
1919
wrapper(content, options)
@@ -41,6 +41,7 @@ def check_box_options(name, options)
4141
:inline, :label, :label_class, :label_col, :layout, :skip_label,
4242
:switch, :wrapper, :wrapper_class)
4343
check_box_options[:class] = check_box_classes(name, options)
44+
check_box_options[:aria] = { describedby: aria_feedback_id(id: options[:id], name:) } if error?(name)
4445
check_box_options.merge!(required_field_options(options, name))
4546
end
4647

lib/bootstrap_form/inputs/collection_check_boxes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module CollectionCheckBoxes
99

1010
included do
1111
def collection_check_boxes_with_bootstrap(*args)
12+
args[4]&.delete(:id)
1213
html = inputs_collection(*args) do |name, value, options|
1314
options[:multiple] = true
1415
check_box(name, options, value, nil)

lib/bootstrap_form/inputs/collection_radio_buttons.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module CollectionRadioButtons
99

1010
included do
1111
def collection_radio_buttons_with_bootstrap(*args)
12+
args[4]&.delete(:id)
1213
inputs_collection(*args) do |name, value, options|
1314
radio_button(name, value, options)
1415
end

0 commit comments

Comments
 (0)