From 2bcb4ddea7dd54209d5a97fd699308d8e97e27e1 Mon Sep 17 00:00:00 2001 From: Larry Reid Date: Wed, 26 Nov 2025 20:28:59 +0000 Subject: [PATCH 1/8] Test for aria-labelledby More and fixed tests --- test/bootstrap_checkbox_test.rb | 4 +-- test/bootstrap_fields_for_test.rb | 2 +- test/bootstrap_fields_test.rb | 8 ++--- test/bootstrap_form_group_test.rb | 42 ++++++-------------------- test/bootstrap_form_test.rb | 32 ++++++++++---------- test/bootstrap_radio_button_test.rb | 4 +-- test/bootstrap_selects_test.rb | 38 +++++++++++------------ test/special_form_class_models_test.rb | 4 +-- 8 files changed, 56 insertions(+), 78 deletions(-) diff --git a/test/bootstrap_checkbox_test.rb b/test/bootstrap_checkbox_test.rb index 121ba276..6289c2c0 100644 --- a/test/bootstrap_checkbox_test.rb +++ b/test/bootstrap_checkbox_test.rb @@ -195,11 +195,11 @@ class BootstrapCheckboxTest < ActionView::TestCase
- + -
You must accept the terms.
+
You must accept the terms.
HTML diff --git a/test/bootstrap_fields_for_test.rb b/test/bootstrap_fields_for_test.rb index eedcef36..f79fa2a1 100644 --- a/test/bootstrap_fields_for_test.rb +++ b/test/bootstrap_fields_for_test.rb @@ -41,7 +41,7 @@ class BootstrapFieldsForTest < ActionView::TestCase
- +
diff --git a/test/bootstrap_fields_test.rb b/test/bootstrap_fields_test.rb index e456c992..3e62ea95 100644 --- a/test/bootstrap_fields_test.rb +++ b/test/bootstrap_fields_test.rb @@ -93,8 +93,8 @@ class BootstrapFieldsTest < ActionView::TestCase
- -
error for test
+ +
error for test
HTML @@ -108,8 +108,8 @@ class BootstrapFieldsTest < ActionView::TestCase
- -
must exist
+ +
must exist
HTML diff --git a/test/bootstrap_form_group_test.rb b/test/bootstrap_form_group_test.rb index 6356afea..cf526b58 100644 --- a/test/bootstrap_form_group_test.rb +++ b/test/bootstrap_form_group_test.rb @@ -187,9 +187,9 @@ class BootstrapFormGroupTest < ActionView::TestCase
$ - + .00 -
can't be blank, is too short (minimum is 5 characters) +
can't be blank, is too short (minimum is 5 characters)
@@ -434,28 +434,6 @@ class BootstrapFormGroupTest < ActionView::TestCase assert_equivalent_html expected, output end - test 'upgrade doc for form_group renders the "error" class and message correctly when object is invalid' do - @user.email = nil - assert @user.invalid? - - output = @builder.form_group :email do - html = '

Bar

'.html_safe - unless @user.errors[:email].empty? - html << tag.div(@user.errors[:email].join(", "), class: "invalid-feedback", - style: "display: block;") - end - html - end - - expected = <<~HTML -
-

Bar

-
can't be blank, is too short (minimum is 5 characters)
-
- HTML - assert_equivalent_html expected, output - end - test "upgrade doc for form_group renders check box correctly when object is invalid" do @user.errors.add(:misc, "Must select one.") @@ -471,17 +449,17 @@ class BootstrapFormGroupTest < ActionView::TestCase
- +
- +
- + -
Must select one.
+
Must select one.
@@ -509,9 +487,9 @@ class BootstrapFormGroupTest < ActionView::TestCase
- +
-
can't be blank, is too short (minimum is 5 characters)
+
can't be blank, is too short (minimum is 5 characters)
HTML output = @builder.email_field(:email, wrapper_class: "none-margin") @@ -530,8 +508,8 @@ class BootstrapFormGroupTest < ActionView::TestCase
- -
can't be blank, is too short (minimum is 5 characters)
+ +
can't be blank, is too short (minimum is 5 characters)
This is required
diff --git a/test/bootstrap_form_test.rb b/test/bootstrap_form_test.rb index 22050769..0141b287 100644 --- a/test/bootstrap_form_test.rb +++ b/test/bootstrap_form_test.rb @@ -458,8 +458,8 @@ def warn(message, ...) expected = <<~HTML
- - + +
HTML @@ -473,9 +473,9 @@ def warn(message, ...) expected = <<~HTML
- - -
can't be blank, is too short (minimum is 5 characters) + + +
can't be blank, is too short (minimum is 5 characters)
HTML @@ -491,9 +491,9 @@ def warn(message, ...) expected = <<~HTML
- - -
can't be blank, is too short (minimum is 5 characters)
+ + +
can't be blank, is too short (minimum is 5 characters)
HTML @@ -624,7 +624,7 @@ def warn(message, ...) assert @user.invalid? expected = <<~HTML -
Email can't be blank, Email is too short (minimum is 5 characters)
+
Email can't be blank, Email is too short (minimum is 5 characters)
HTML assert_equivalent_html expected, @builder.errors_on(:email) end @@ -717,8 +717,8 @@ def warn(message, ...)
- -
can't be blank, is too short (minimum is 5 characters)
+ +
can't be blank, is too short (minimum is 5 characters)
This is required
@@ -741,9 +741,9 @@ def warn(message, ...)
- +
-
can't be blank, is too short (minimum is 5 characters)
+
can't be blank, is too short (minimum is 5 characters)
This is required
@@ -763,7 +763,7 @@ def warn(message, ...)
- + This is required
@@ -807,7 +807,7 @@ def warn(message, ...) @user.email = nil assert @user.invalid? - expected = '
can\'t be blank, is too short (minimum is 5 characters)
' + expected = '
can\'t be blank, is too short (minimum is 5 characters)
' assert_equivalent_html expected, @builder.errors_on(:email, hide_attribute_name: true) end @@ -816,7 +816,7 @@ def warn(message, ...) @user.email = nil assert @user.invalid? - expected = '
Email can\'t be blank, Email is too short (minimum is 5 characters)
' + expected = '
Email can\'t be blank, Email is too short (minimum is 5 characters)
' assert_equivalent_html expected, @builder.errors_on(:email, custom_class: "custom-error-class") end diff --git a/test/bootstrap_radio_button_test.rb b/test/bootstrap_radio_button_test.rb index 50b4d6fc..e2bc211b 100644 --- a/test/bootstrap_radio_button_test.rb +++ b/test/bootstrap_radio_button_test.rb @@ -35,11 +35,11 @@ class BootstrapRadioButtonTest < ActionView::TestCase expected = <<~HTML
- + -
error for test
+
error for test
HTML diff --git a/test/bootstrap_selects_test.rb b/test/bootstrap_selects_test.rb index 13cbb2e9..c56cb8e0 100644 --- a/test/bootstrap_selects_test.rb +++ b/test/bootstrap_selects_test.rb @@ -42,8 +42,8 @@ def options_range(start: 1, stop: 31, selected: nil, months: false)
- -
error for test
+ +
error for test
HTML @@ -205,8 +205,8 @@ def options_range(start: 1, stop: 31, selected: nil, months: false)
- -
error for test
+ +
error for test
HTML @@ -285,8 +285,8 @@ def options_range(start: 1, stop: 31, selected: nil, months: false)
- -
error for test
+ +
error for test
HTML @@ -417,16 +417,16 @@ def options_range(start: 1, stop: 31, selected: nil, months: false)
- #{options_range(start: 2007, stop: 2017, selected: 2012)} - #{options_range(start: 1, stop: 12, selected: 2, months: true)} - #{options_range(start: 1, stop: 31, selected: 3)} -
error for test
+
error for test
@@ -520,14 +520,14 @@ def options_range(start: 1, stop: 31, selected: nil, months: false) - #{options_range(start: '00', stop: '23', selected: '12')} : - #{options_range(start: '00', stop: '59', selected: '00')} -
error for test
+
error for test
@@ -624,24 +624,24 @@ def options_range(start: 1, stop: 31, selected: nil, months: false)
- #{options_range(start: 2007, stop: 2017, selected: 2012)} - #{options_range(start: 1, stop: 12, selected: 2, months: true)} - #{options_range(start: 1, stop: 31, selected: 3)} — - #{options_range(start: '00', stop: '23', selected: '12')} : - #{options_range(start: '00', stop: '59', selected: '00')} -
error for test
+
error for test
diff --git a/test/special_form_class_models_test.rb b/test/special_form_class_models_test.rb index 98550b39..d2857ea8 100644 --- a/test/special_form_class_models_test.rb +++ b/test/special_form_class_models_test.rb @@ -79,9 +79,9 @@ def user_klass.model_name
- +
-
can't be blank
+
can't be blank
HTML assert_equivalent_html expected, @builder.text_field(:password) From 3530be9d55411cc391aa52b1dcd500ac3e36dca3 Mon Sep 17 00:00:00 2001 From: Larry Reid Date: Thu, 27 Nov 2025 03:11:23 +0000 Subject: [PATCH 2/8] aria-labelledby for errors Fix line lengths --- README.md | 30 ++++++++++----------- lib/bootstrap_form/components/labels.rb | 1 + lib/bootstrap_form/components/validation.rb | 2 +- lib/bootstrap_form/form_group_builder.rb | 5 +++- lib/bootstrap_form/helpers/bootstrap.rb | 2 +- lib/bootstrap_form/inputs/check_box.rb | 1 + lib/bootstrap_form/inputs/radio_button.rb | 1 + test/bootstrap_form_test.rb | 10 +++++-- 8 files changed, 32 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b6b3d637..4c44423b 100644 --- a/README.md +++ b/README.md @@ -1493,38 +1493,38 @@ Generated HTML:
- -
is invalid
+ +
is invalid
Misc
- +
- + -
is invalid
+
is invalid
Preferences
- +
- + -
is invalid
+
is invalid
- -
is invalid
+ +
is invalid
``` @@ -1554,8 +1554,8 @@ Generated HTML: ```html
- - + +
``` @@ -1652,7 +1652,7 @@ Which outputs: ```html
-
Email is invalid
+
Email is invalid
``` @@ -1673,7 +1673,7 @@ Which outputs: ```html
-
is invalid
+
is invalid
``` @@ -1692,7 +1692,7 @@ Which outputs: ```html
-
Email is invalid
+
Email is invalid
``` diff --git a/lib/bootstrap_form/components/labels.rb b/lib/bootstrap_form/components/labels.rb index d8f03655..d175d8c4 100644 --- a/lib/bootstrap_form/components/labels.rb +++ b/lib/bootstrap_form/components/labels.rb @@ -52,6 +52,7 @@ def prepare_label_options(id, name, options, custom_label_col, group_layout) options[:class] = label_classes(name, options, custom_label_col, group_layout) options.delete(:class) if options[:class].none? + options[:id] = field_id(name, :feedback) if error?(name) && label_errors end end end diff --git a/lib/bootstrap_form/components/validation.rb b/lib/bootstrap_form/components/validation.rb index affba190..297acb3d 100644 --- a/lib/bootstrap_form/components/validation.rb +++ b/lib/bootstrap_form/components/validation.rb @@ -68,7 +68,7 @@ def generate_error(name) help_klass = "invalid-feedback" help_tag = :div - content_tag(help_tag, help_text, class: help_klass) + content_tag(help_tag, help_text, class: help_klass, id: field_id(name, :feedback)) end def get_error_messages(name) diff --git a/lib/bootstrap_form/form_group_builder.rb b/lib/bootstrap_form/form_group_builder.rb index 36e7155a..a751ca6b 100644 --- a/lib/bootstrap_form/form_group_builder.rb +++ b/lib/bootstrap_form/form_group_builder.rb @@ -98,7 +98,10 @@ def form_group_css_options(method, html_options, options) # Add control_class; allow it to be overridden by :control_class option control_classes = css_options.delete(:control_class) { control_class } css_options[:class] = safe_join([control_classes, css_options[:class]].compact, " ") - css_options[:class] << " is-invalid" if error?(method) + if error?(method) + css_options[:class] << " is-invalid" + css_options[:aria] = { labelledby: field_id(method, :feedback) } + end css_options[:placeholder] = form_group_placeholder(options, method) if options[:label_as_placeholder] css_options end diff --git a/lib/bootstrap_form/helpers/bootstrap.rb b/lib/bootstrap_form/helpers/bootstrap.rb index 2048c448..c30ea9f8 100644 --- a/lib/bootstrap_form/helpers/bootstrap.rb +++ b/lib/bootstrap_form/helpers/bootstrap.rb @@ -34,7 +34,7 @@ def errors_on(name, options={}) hide_attribute_name = options[:hide_attribute_name] || false custom_class = options[:custom_class] || false - tag.div class: custom_class || "invalid-feedback" do + tag.div(class: custom_class || "invalid-feedback", id: field_id(name, :feedback)) do errors = if hide_attribute_name object.errors[name] else diff --git a/lib/bootstrap_form/inputs/check_box.rb b/lib/bootstrap_form/inputs/check_box.rb index fca776d0..96a9519c 100644 --- a/lib/bootstrap_form/inputs/check_box.rb +++ b/lib/bootstrap_form/inputs/check_box.rb @@ -41,6 +41,7 @@ def check_box_options(name, options) :inline, :label, :label_class, :label_col, :layout, :skip_label, :switch, :wrapper, :wrapper_class) check_box_options[:class] = check_box_classes(name, options) + check_box_options[:aria] = { labelledby: field_id(name, :feedback) } if error?(name) check_box_options.merge!(required_field_options(options, name)) end diff --git a/lib/bootstrap_form/inputs/radio_button.rb b/lib/bootstrap_form/inputs/radio_button.rb index d78e0e11..2c30a657 100644 --- a/lib/bootstrap_form/inputs/radio_button.rb +++ b/lib/bootstrap_form/inputs/radio_button.rb @@ -28,6 +28,7 @@ def radio_button_options(name, options) radio_button_options = options.except(:class, :label, :label_class, :error_message, :help, :inline, :hide_label, :skip_label, :wrapper, :wrapper_class) radio_button_options[:class] = radio_button_classes(name, options) + radio_button_options[:aria] = { labelledby: field_id(name, :feedback) } if error?(name) radio_button_options.merge!(required_field_options(options, name)) end diff --git a/test/bootstrap_form_test.rb b/test/bootstrap_form_test.rb index 0141b287..5b60d867 100644 --- a/test/bootstrap_form_test.rb +++ b/test/bootstrap_form_test.rb @@ -807,7 +807,9 @@ def warn(message, ...) @user.email = nil assert @user.invalid? - expected = '
can\'t be blank, is too short (minimum is 5 characters)
' + expected = <<~HTML +
can't be blank, is too short (minimum is 5 characters)
+ HTML assert_equivalent_html expected, @builder.errors_on(:email, hide_attribute_name: true) end @@ -816,7 +818,11 @@ def warn(message, ...) @user.email = nil assert @user.invalid? - expected = '
Email can\'t be blank, Email is too short (minimum is 5 characters)
' + expected = <<~HTML +
+ Email can't be blank, Email is too short (minimum is 5 characters) +
+ HTML assert_equivalent_html expected, @builder.errors_on(:email, custom_class: "custom-error-class") end From 8385ae9fe36b9cdb087f50ed08f3411ca4bd8fe9 Mon Sep 17 00:00:00 2001 From: Larry Reid Date: Fri, 28 Nov 2025 16:54:55 +0000 Subject: [PATCH 3/8] Test for custom ID --- test/bootstrap_checkbox_test.rb | 20 +++++++ test/bootstrap_fields_test.rb | 29 ++++++++++ test/bootstrap_form_test.rb | 55 ++++++++++++++++++ test/bootstrap_radio_button_test.rb | 19 +++++++ test/bootstrap_selects_test.rb | 87 +++++++++++++++++++++++++++++ 5 files changed, 210 insertions(+) diff --git a/test/bootstrap_checkbox_test.rb b/test/bootstrap_checkbox_test.rb index 6289c2c0..95692b82 100644 --- a/test/bootstrap_checkbox_test.rb +++ b/test/bootstrap_checkbox_test.rb @@ -209,6 +209,26 @@ class BootstrapCheckboxTest < ActionView::TestCase assert_equivalent_html expected, actual end + test "check_box renders error when asked with specified id:" do + @user.errors.add(:terms, "You must accept the terms.") + expected = <<~HTML +
+
+ + + +
You must accept the terms.
+
+
+ HTML + actual = bootstrap_form_for(@user) do |f| + f.check_box(:terms, label: "I agree to the terms", error_message: true, id: "custom-id") + end + assert_equivalent_html expected, actual + end + test "check box with custom wrapper class" do expected = <<~HTML
diff --git a/test/bootstrap_fields_test.rb b/test/bootstrap_fields_test.rb index 3e62ea95..efce7615 100644 --- a/test/bootstrap_fields_test.rb +++ b/test/bootstrap_fields_test.rb @@ -101,6 +101,20 @@ class BootstrapFieldsTest < ActionView::TestCase assert_equivalent_html expected, bootstrap_form_for(@user) { |f| f.file_field(:misc) } end + test "file fields are wrapped correctly with error with specified id:" do + @user.errors.add(:misc, "error for test") + expected = <<~HTML +
+
+ + +
error for test
+
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user) { |f| f.file_field(:misc, id: "custom-id") } + end + test "errors are correctly displayed for belongs_to association fields" do @address.valid? @@ -116,6 +130,21 @@ class BootstrapFieldsTest < ActionView::TestCase assert_equivalent_html expected, bootstrap_form_for(@address, url: users_path) { |f| f.text_field(:user_id) } end + test "errors are correctly displayed for belongs_to association fields with specified id:" do + @address.valid? + + expected = <<~HTML +
+
+ + +
must exist
+
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@address, url: users_path) { |f| f.text_field(:user_id, id: "custom-id") } + end + test "hidden fields are supported" do expected = <<~HTML diff --git a/test/bootstrap_form_test.rb b/test/bootstrap_form_test.rb index 5b60d867..06e67e9e 100644 --- a/test/bootstrap_form_test.rb +++ b/test/bootstrap_form_test.rb @@ -466,6 +466,21 @@ def warn(message, ...) assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true) { |f| f.text_field :email } end + test "errors display correctly and inline_errors are turned off by default when label_errors is true with specified id:" do + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
+
+ + +
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true) { |f| f.text_field :email, id: "custom-id" } + end + test "errors display correctly and inline_errors can also be on when label_errors is true" do @user.email = nil assert @user.invalid? @@ -482,6 +497,24 @@ def warn(message, ...) assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true, inline_errors: true) { |f| f.text_field :email } end + test "errors display correctly and inline_errors can also be on when label_errors is true with specified id:" do + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
+
+ + +
can't be blank, is too short (minimum is 5 characters) +
+ + HTML + assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true, inline_errors: true) { |f| + f.text_field :email, id: "custom-id" + } + end + test "label error messages use humanized attribute names" do I18n.backend.store_translations(:en, activerecord: { attributes: { user: { email: "Your e-mail address" } } }) @@ -502,6 +535,28 @@ def warn(message, ...) I18n.backend.store_translations(:en, activerecord: { attributes: { user: { email: nil } } }) end + test "label error messages use humanized attribute names with specified id:" do + I18n.backend.store_translations(:en, activerecord: { attributes: { user: { email: "Your e-mail address" } } }) + + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
+
+ + +
can't be blank, is too short (minimum is 5 characters)
+
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true, inline_errors: true) { |f| + f.text_field :email, id: "custom-id" + } + ensure + I18n.backend.store_translations(:en, activerecord: { attributes: { user: { email: nil } } }) + end + test "alert message is wrapped correctly" do @user.email = nil assert @user.invalid? diff --git a/test/bootstrap_radio_button_test.rb b/test/bootstrap_radio_button_test.rb index e2bc211b..27afd5f9 100644 --- a/test/bootstrap_radio_button_test.rb +++ b/test/bootstrap_radio_button_test.rb @@ -49,6 +49,25 @@ class BootstrapRadioButtonTest < ActionView::TestCase assert_equivalent_html expected, actual end + test "radio_button with error is wrapped correctly with specified id:" do + @user.errors.add(:misc, "error for test") + expected = <<~HTML +
+
+ + +
error for test
+
+
+ HTML + actual = bootstrap_form_for(@user) do |f| + f.radio_button(:misc, "1", label: "This is a radio button", error_message: true, id: "custom-id") + end + assert_equivalent_html expected, actual + end + test "radio_button disabled label is set correctly" do expected = <<~HTML
diff --git a/test/bootstrap_selects_test.rb b/test/bootstrap_selects_test.rb index c56cb8e0..a8b23599 100644 --- a/test/bootstrap_selects_test.rb +++ b/test/bootstrap_selects_test.rb @@ -435,6 +435,32 @@ def options_range(start: 1, stop: 31, selected: nil, months: false) end end + test "date selects are wrapped correctly with error with specified id:" do + @user.errors.add(:misc, "error for test") + travel_to(Time.utc(2012, 2, 3)) do + expected = <<~HTML +
+
+ +
+ + + +
error for test
+
+
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user) { |f| f.date_select(:misc, id: "custom-id") } + end + end + test "date selects with options are wrapped correctly" do travel_to(Time.utc(2012, 2, 3)) do expected = <<~HTML @@ -536,6 +562,33 @@ def options_range(start: 1, stop: 31, selected: nil, months: false) end end + test "time selects are wrapped correctly with error with specified id:" do + @user.errors.add(:misc, "error for test") + travel_to(Time.utc(2012, 2, 3, 12, 0, 0)) do + expected = <<~HTML +
+
+ +
+ + + + + : + +
error for test
+
+
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user) { |f| f.time_select(:misc, id: "custom-id") } + end + end + test "time selects with options are wrapped correctly" do travel_to(Time.utc(2012, 2, 3, 12, 0, 0)) do expected = <<~HTML @@ -650,6 +703,40 @@ def options_range(start: 1, stop: 31, selected: nil, months: false) end end + test "datetime selects are wrapped correctly with error with specified id:" do + @user.errors.add(:misc, "error for test") + travel_to(Time.utc(2012, 2, 3, 12, 0, 0)) do + expected = <<~HTML +
+
+ +
+ + + + — + + : + +
error for test
+
+
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user) { |f| f.datetime_select(:misc, id: "custom-id") } + end + end + test "datetime selects with options are wrapped correctly" do travel_to(Time.utc(2012, 2, 3, 12, 0, 0)) do expected = <<~HTML From d7e3ac62b7dcd250a869f8db910989ae1f8d7568 Mon Sep 17 00:00:00 2001 From: Larry Reid Date: Wed, 3 Dec 2025 01:24:59 +0000 Subject: [PATCH 4/8] Test passing. Collections don't respect id: --- lib/bootstrap_form/components/labels.rb | 2 +- lib/bootstrap_form/components/validation.rb | 5 +++-- lib/bootstrap_form/form_group.rb | 2 +- lib/bootstrap_form/form_group_builder.rb | 3 ++- lib/bootstrap_form/helpers/bootstrap.rb | 7 ++++--- lib/bootstrap_form/inputs/base.rb | 1 + lib/bootstrap_form/inputs/check_box.rb | 7 +++++-- lib/bootstrap_form/inputs/collection_check_boxes.rb | 1 + lib/bootstrap_form/inputs/collection_radio_buttons.rb | 1 + lib/bootstrap_form/inputs/radio_button.rb | 7 +++++-- lib/bootstrap_form/inputs/time_zone_select.rb | 2 +- 11 files changed, 25 insertions(+), 13 deletions(-) diff --git a/lib/bootstrap_form/components/labels.rb b/lib/bootstrap_form/components/labels.rb index d175d8c4..aefa512a 100644 --- a/lib/bootstrap_form/components/labels.rb +++ b/lib/bootstrap_form/components/labels.rb @@ -52,7 +52,7 @@ def prepare_label_options(id, name, options, custom_label_col, group_layout) options[:class] = label_classes(name, options, custom_label_col, group_layout) options.delete(:class) if options[:class].none? - options[:id] = field_id(name, :feedback) if error?(name) && label_errors + options[:id] = id.present? ? "#{id}_feedback" : field_id(name, :feedback) if error?(name) && label_errors end end end diff --git a/lib/bootstrap_form/components/validation.rb b/lib/bootstrap_form/components/validation.rb index 297acb3d..8b4ba29d 100644 --- a/lib/bootstrap_form/components/validation.rb +++ b/lib/bootstrap_form/components/validation.rb @@ -61,14 +61,15 @@ def inline_error?(name) error?(name) && inline_errors end - def generate_error(name) + def generate_error(name, id) return unless inline_error?(name) help_text = get_error_messages(name) help_klass = "invalid-feedback" help_tag = :div + id = id.present? ? "#{id}_feedback" : field_id(name, :feedback) - content_tag(help_tag, help_text, class: help_klass, id: field_id(name, :feedback)) + content_tag(help_tag, help_text, class: help_klass, id:) end def get_error_messages(name) diff --git a/lib/bootstrap_form/form_group.rb b/lib/bootstrap_form/form_group.rb index c244f4b2..aeda4a76 100644 --- a/lib/bootstrap_form/form_group.rb +++ b/lib/bootstrap_form/form_group.rb @@ -23,7 +23,7 @@ def form_group_content_tag(name, field_name, without_field_name, options, html_o html_class = control_specific_class(field_name) html_class = "#{html_class} col-auto g-3" if @layout == :horizontal && options[:skip_inline].blank? tag.div(class: html_class) do - input_with_error(name) do + input_with_error(name, options[:id]) do send(without_field_name, name, options, html_options) end end diff --git a/lib/bootstrap_form/form_group_builder.rb b/lib/bootstrap_form/form_group_builder.rb index a751ca6b..a1fdb522 100644 --- a/lib/bootstrap_form/form_group_builder.rb +++ b/lib/bootstrap_form/form_group_builder.rb @@ -100,7 +100,8 @@ def form_group_css_options(method, html_options, options) css_options[:class] = safe_join([control_classes, css_options[:class]].compact, " ") if error?(method) css_options[:class] << " is-invalid" - css_options[:aria] = { labelledby: field_id(method, :feedback) } + labelledby = options[:id].present? ? "#{options[:id]}_feedback" : field_id(method, :feedback) + css_options[:aria] = { labelledby: } end css_options[:placeholder] = form_group_placeholder(options, method) if options[:label_as_placeholder] css_options diff --git a/lib/bootstrap_form/helpers/bootstrap.rb b/lib/bootstrap_form/helpers/bootstrap.rb index c30ea9f8..0f1422b8 100644 --- a/lib/bootstrap_form/helpers/bootstrap.rb +++ b/lib/bootstrap_form/helpers/bootstrap.rb @@ -66,20 +66,21 @@ def custom_control(*args, &) end def prepend_and_append_input(name, options, &) + id = options[:id] options = options.extract!(:prepend, :append, :input_group_class).compact input = capture(&) || ActiveSupport::SafeBuffer.new input = attach_input(options, :prepend) + input + attach_input(options, :append) - input << generate_error(name) + input << generate_error(name, id) options.present? && input = tag.div(input, class: ["input-group", options[:input_group_class]].compact) input end - def input_with_error(name, &) + def input_with_error(name, id, &) input = capture(&) - input << generate_error(name) + input << generate_error(name, id) end def input_group_content(content) diff --git a/lib/bootstrap_form/inputs/base.rb b/lib/bootstrap_form/inputs/base.rb index 94833bd9..807dccfa 100644 --- a/lib/bootstrap_form/inputs/base.rb +++ b/lib/bootstrap_form/inputs/base.rb @@ -24,6 +24,7 @@ def bootstrap_field(field_name) def bootstrap_select_group(field_name) define_method(:"#{field_name}_with_bootstrap") do |name, options={}, html_options={}| + options.delete(:id) html_options = html_options.reverse_merge(control_class: "form-select") form_group_builder(name, options, html_options) do form_group_content_tag(name, field_name, "#{field_name}_without_bootstrap", options, html_options) diff --git a/lib/bootstrap_form/inputs/check_box.rb b/lib/bootstrap_form/inputs/check_box.rb index 96a9519c..866a9aa6 100644 --- a/lib/bootstrap_form/inputs/check_box.rb +++ b/lib/bootstrap_form/inputs/check_box.rb @@ -13,7 +13,7 @@ def check_box_with_bootstrap(name, options={}, checked_value="1", unchecked_valu content = tag.div(class: check_box_wrapper_class(options), **options[:wrapper].to_h.except(:class)) do html = check_box_without_bootstrap(name, check_box_options(name, options), checked_value, unchecked_value) html << check_box_label(name, options, checked_value, &block) unless options[:skip_label] - html << generate_error(name) if options[:error_message] + html << generate_error(name, options[:id]) if options[:error_message] html end wrapper(content, options) @@ -41,7 +41,10 @@ def check_box_options(name, options) :inline, :label, :label_class, :label_col, :layout, :skip_label, :switch, :wrapper, :wrapper_class) check_box_options[:class] = check_box_classes(name, options) - check_box_options[:aria] = { labelledby: field_id(name, :feedback) } if error?(name) + if error?(name) + labelledby = options[:id].present? ? "#{options[:id]}_feedback" : field_id(name, :feedback) + check_box_options[:aria] = { labelledby: } + end check_box_options.merge!(required_field_options(options, name)) end diff --git a/lib/bootstrap_form/inputs/collection_check_boxes.rb b/lib/bootstrap_form/inputs/collection_check_boxes.rb index 6b18edb7..4c028427 100644 --- a/lib/bootstrap_form/inputs/collection_check_boxes.rb +++ b/lib/bootstrap_form/inputs/collection_check_boxes.rb @@ -9,6 +9,7 @@ module CollectionCheckBoxes included do def collection_check_boxes_with_bootstrap(*args) + args[4]&.delete(:id) html = inputs_collection(*args) do |name, value, options| options[:multiple] = true check_box(name, options, value, nil) diff --git a/lib/bootstrap_form/inputs/collection_radio_buttons.rb b/lib/bootstrap_form/inputs/collection_radio_buttons.rb index be07db35..d6d6d5c5 100644 --- a/lib/bootstrap_form/inputs/collection_radio_buttons.rb +++ b/lib/bootstrap_form/inputs/collection_radio_buttons.rb @@ -9,6 +9,7 @@ module CollectionRadioButtons included do def collection_radio_buttons_with_bootstrap(*args) + args[4]&.delete(:id) inputs_collection(*args) do |name, value, options| radio_button(name, value, options) end diff --git a/lib/bootstrap_form/inputs/radio_button.rb b/lib/bootstrap_form/inputs/radio_button.rb index 2c30a657..9d69395f 100644 --- a/lib/bootstrap_form/inputs/radio_button.rb +++ b/lib/bootstrap_form/inputs/radio_button.rb @@ -14,7 +14,7 @@ def radio_button_with_bootstrap(name, value, *args) tag.div(**wrapper_attributes) do html = radio_button_without_bootstrap(name, value, radio_button_options(name, options)) html << radio_button_label(name, value, options) unless options[:skip_label] - html << generate_error(name) if options[:error_message] + html << generate_error(name, options[:id]) if options[:error_message] html end end @@ -28,7 +28,10 @@ def radio_button_options(name, options) radio_button_options = options.except(:class, :label, :label_class, :error_message, :help, :inline, :hide_label, :skip_label, :wrapper, :wrapper_class) radio_button_options[:class] = radio_button_classes(name, options) - radio_button_options[:aria] = { labelledby: field_id(name, :feedback) } if error?(name) + if error?(name) + labelledby = options[:id].present? ? "#{options[:id]}_feedback" : field_id(name, :feedback) + radio_button_options[:aria] = { labelledby: } + end radio_button_options.merge!(required_field_options(options, name)) end diff --git a/lib/bootstrap_form/inputs/time_zone_select.rb b/lib/bootstrap_form/inputs/time_zone_select.rb index 555c2902..e174ce9b 100644 --- a/lib/bootstrap_form/inputs/time_zone_select.rb +++ b/lib/bootstrap_form/inputs/time_zone_select.rb @@ -10,7 +10,7 @@ module TimeZoneSelect def time_zone_select_with_bootstrap(method, priority_zones=nil, options={}, html_options={}) html_options = html_options.reverse_merge(control_class: "form-select") form_group_builder(method, options, html_options) do - input_with_error(method) do + input_with_error(method, options[:id]) do time_zone_select_without_bootstrap(method, priority_zones, options, html_options) end end From 6aade3f5cd2d4aa63017f2248cf294aadc16b82d Mon Sep 17 00:00:00 2001 From: Larry Reid Date: Thu, 4 Dec 2025 03:46:50 +0000 Subject: [PATCH 5/8] Beautify HTML in test failures to make it easier to see diff --- test/test_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_helper.rb b/test/test_helper.rb index dcfed122..83fee89e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -79,8 +79,8 @@ def assert_equivalent_html(expected, actual) assert equivalent, lambda { # using a lambda because diffing is expensive Diffy::Diff.new( - expected_html.to_html(indent: 2), - actual_html.to_html(indent: 2) + HtmlBeautifier.beautify(expected_html.to_html(indent: 2)), + HtmlBeautifier.beautify(actual_html.to_html(indent: 2)) ).to_s(:color) } end From 6bfc6974010d75f7cf902daad8c6d6512583e28e Mon Sep 17 00:00:00 2001 From: Larry Reid Date: Thu, 4 Dec 2025 03:48:17 +0000 Subject: [PATCH 6/8] Manually merging --- test/bootstrap_collection_checkboxes_test.rb | 136 ++++++++++++++++-- ...bootstrap_collection_radio_buttons_test.rb | 64 ++++++++- test/bootstrap_form_test.rb | 93 +++++++++--- 3 files changed, 259 insertions(+), 34 deletions(-) diff --git a/test/bootstrap_collection_checkboxes_test.rb b/test/bootstrap_collection_checkboxes_test.rb index 667b3008..7bf80353 100644 --- a/test/bootstrap_collection_checkboxes_test.rb +++ b/test/bootstrap_collection_checkboxes_test.rb @@ -368,13 +368,13 @@ class BootstrapCollectionCheckboxesTest < ActionView::TestCase
Misc
- +
- + -
a box must be checked
+
a box must be checked
@@ -387,6 +387,35 @@ class BootstrapCollectionCheckboxesTest < ActionView::TestCase assert_equivalent_html expected, actual end + test "collection_check_boxes renders error after last check box with specified id:" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + @user.errors.add(:misc, "a box must be checked") + + expected = <<~HTML +
+ +
+
Misc
+
+ + +
+
+ + +
a box must be checked
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_check_boxes(:misc, collection, :id, :street, { id: "custom-id" }) + end + + assert_equivalent_html expected, actual + end + test "collection_check_boxes renders data attributes" do collection = [ ["1", "Foo", { "data-city": "east" }], @@ -418,13 +447,13 @@ class BootstrapCollectionCheckboxesTest < ActionView::TestCase
Misc
- +
- + -
error for test
+
error for test
@@ -435,6 +464,33 @@ class BootstrapCollectionCheckboxesTest < ActionView::TestCase end assert_equivalent_html expected, actual end + + test "collection_check_boxes renders multiple check boxes with error correctly with specified id:" do + @user.errors.add(:misc, "error for test") + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+ +
+
Misc
+
+ + +
+
+ + +
error for test
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_check_boxes(:misc, collection, :id, :street, checked: collection, id: "custom-id") + end + assert_equivalent_html expected, actual + end end class BootstrapLegacyCollectionCheckboxesTest < ActionView::TestCase @@ -803,13 +859,13 @@ class BootstrapLegacyCollectionCheckboxesTest < ActionView::TestCase
- +
- + -
a box must be checked
+
a box must be checked
@@ -822,6 +878,35 @@ class BootstrapLegacyCollectionCheckboxesTest < ActionView::TestCase assert_equivalent_html expected, actual end + test "collection_check_boxes renders error after last check box with specified id:" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + @user.errors.add(:misc, "a box must be checked") + + expected = <<~HTML +
+ +
+ +
+ + +
+
+ + +
a box must be checked
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_check_boxes(:misc, collection, :id, :street, { id: "custom-id" }) + end + + assert_equivalent_html expected, actual + end + test "collection_check_boxes renders data attributes" do collection = [ ["1", "Foo", { "data-city": "east" }], @@ -853,13 +938,13 @@ class BootstrapLegacyCollectionCheckboxesTest < ActionView::TestCase
- +
- + -
error for test
+
error for test
@@ -870,4 +955,31 @@ class BootstrapLegacyCollectionCheckboxesTest < ActionView::TestCase end assert_equivalent_html expected, actual end + + test "collection_check_boxes renders multiple check boxes with error correctly with specified id:" do + @user.errors.add(:misc, "error for test") + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+ +
+ +
+ + +
+
+ + +
error for test
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_check_boxes(:misc, collection, :id, :street, checked: collection, id: "custom-id") + end + assert_equivalent_html expected, actual + end end diff --git a/test/bootstrap_collection_radio_buttons_test.rb b/test/bootstrap_collection_radio_buttons_test.rb index 2c9344fe..a57603a5 100644 --- a/test/bootstrap_collection_radio_buttons_test.rb +++ b/test/bootstrap_collection_radio_buttons_test.rb @@ -61,13 +61,13 @@ class BootstrapCollectionRadioButtonsTest < ActionView::TestCase
Misc
- +
- + -
error for test
+
error for test
@@ -79,6 +79,32 @@ class BootstrapCollectionRadioButtonsTest < ActionView::TestCase assert_equivalent_html expected, actual end + test "collection_radio_buttons renders multiple radios with error correctly with specified id:" do + @user.errors.add(:misc, "error for test") + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+
+
Misc
+
+ + +
+
+ + +
error for test
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_radio_buttons(:misc, collection, :id, :street, { id: "custom-id" }) + end + assert_equivalent_html expected, actual + end + test "collection_radio_buttons renders inline radios correctly" do collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] expected = <<~HTML @@ -327,13 +353,13 @@ class BootstrapCLegacyollectionRadioButtonsTest < ActionView::TestCase
- +
- + -
error for test
+
error for test
@@ -345,6 +371,32 @@ class BootstrapCLegacyollectionRadioButtonsTest < ActionView::TestCase assert_equivalent_html expected, actual end + test "collection_radio_buttons renders multiple radios with error correctly with specified id:" do + @user.errors.add(:misc, "error for test") + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+
+ +
+ + +
+
+ + +
error for test
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_radio_buttons(:misc, collection, :id, :street, { id: "custom-id" }) + end + assert_equivalent_html expected, actual + end + test "collection_radio_buttons renders inline radios correctly" do collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] expected = <<~HTML diff --git a/test/bootstrap_form_test.rb b/test/bootstrap_form_test.rb index 06e67e9e..a64767af 100644 --- a/test/bootstrap_form_test.rb +++ b/test/bootstrap_form_test.rb @@ -1172,14 +1172,29 @@ def warn(message, ...) expected = <<~HTML
- - + +
HTML assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true) { |f| f.text_field :email } end + test "errors display correctly and inline_errors are turned off by default when label_errors is true with specified id:" do + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
+
+ + +
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true) { |f| f.text_field :email, id: "custom-id" } + end + test "errors display correctly and inline_errors can also be on when label_errors is true" do @user.email = nil assert @user.invalid? @@ -1187,15 +1202,33 @@ def warn(message, ...) expected = <<~HTML
- - -
can't be blank, is too short (minimum is 5 characters) + + +
can't be blank, is too short (minimum is 5 characters)
HTML assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true, inline_errors: true) { |f| f.text_field :email } end + test "errors display correctly and inline_errors can also be on when label_errors is true with specified id:" do + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
+
+ + +
can't be blank, is too short (minimum is 5 characters) +
+ + HTML + assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true, inline_errors: true) { |f| + f.text_field :email, id: "custom-id" + } + end + test "label error messages use humanized attribute names" do I18n.backend.store_translations(:en, activerecord: { attributes: { user: { email: "Your e-mail address" } } }) @@ -1205,9 +1238,9 @@ def warn(message, ...) expected = <<~HTML
- - -
can't be blank, is too short (minimum is 5 characters)
+ + +
can't be blank, is too short (minimum is 5 characters)
HTML @@ -1216,6 +1249,28 @@ def warn(message, ...) I18n.backend.store_translations(:en, activerecord: { attributes: { user: { email: nil } } }) end + test "label error messages use humanized attribute names with specified id:" do + I18n.backend.store_translations(:en, activerecord: { attributes: { user: { email: "Your e-mail address" } } }) + + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
+
+ + +
can't be blank, is too short (minimum is 5 characters)
+
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true, inline_errors: true) { |f| + f.text_field :email, id: "custom-id" + } + ensure + I18n.backend.store_translations(:en, activerecord: { attributes: { user: { email: nil } } }) + end + test "alert message is wrapped correctly" do @user.email = nil assert @user.invalid? @@ -1338,7 +1393,7 @@ def warn(message, ...) assert @user.invalid? expected = <<~HTML -
Email can't be blank, Email is too short (minimum is 5 characters)
+
Email can't be blank, Email is too short (minimum is 5 characters)
HTML assert_equivalent_html expected, @builder.errors_on(:email) end @@ -1431,8 +1486,8 @@ def warn(message, ...)
- -
can't be blank, is too short (minimum is 5 characters)
+ +
can't be blank, is too short (minimum is 5 characters)
This is required
@@ -1455,9 +1510,9 @@ def warn(message, ...)
- +
-
can't be blank, is too short (minimum is 5 characters)
+
can't be blank, is too short (minimum is 5 characters)
This is required
@@ -1477,7 +1532,7 @@ def warn(message, ...)
- + This is required
@@ -1521,7 +1576,9 @@ def warn(message, ...) @user.email = nil assert @user.invalid? - expected = '
can\'t be blank, is too short (minimum is 5 characters)
' + expected = <<~HTML +
can't be blank, is too short (minimum is 5 characters)
+ HTML assert_equivalent_html expected, @builder.errors_on(:email, hide_attribute_name: true) end @@ -1530,7 +1587,11 @@ def warn(message, ...) @user.email = nil assert @user.invalid? - expected = '
Email can\'t be blank, Email is too short (minimum is 5 characters)
' + expected = <<~HTML +
+ Email can't be blank, Email is too short (minimum is 5 characters) +
+ HTML assert_equivalent_html expected, @builder.errors_on(:email, custom_class: "custom-error-class") end From 35f432c06c46230eb0ab17f585936b34c0070530 Mon Sep 17 00:00:00 2001 From: Larry Reid Date: Fri, 5 Dec 2025 05:21:54 +0000 Subject: [PATCH 7/8] Use id: in errors_on --- lib/bootstrap_form/helpers/bootstrap.rb | 5 ++++- lib/bootstrap_form/inputs/base.rb | 2 ++ test/bootstrap_form_test.rb | 13 +++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/bootstrap_form/helpers/bootstrap.rb b/lib/bootstrap_form/helpers/bootstrap.rb index 0f1422b8..e4c145da 100644 --- a/lib/bootstrap_form/helpers/bootstrap.rb +++ b/lib/bootstrap_form/helpers/bootstrap.rb @@ -34,7 +34,10 @@ def errors_on(name, options={}) hide_attribute_name = options[:hide_attribute_name] || false custom_class = options[:custom_class] || false - tag.div(class: custom_class || "invalid-feedback", id: field_id(name, :feedback)) do + tag.div( + class: custom_class || "invalid-feedback", + id: options[:id].present? ? "#{options[:id]}_feedback" : field_id(name, :feedback) + ) do errors = if hide_attribute_name object.errors[name] else diff --git a/lib/bootstrap_form/inputs/base.rb b/lib/bootstrap_form/inputs/base.rb index 807dccfa..759c19be 100644 --- a/lib/bootstrap_form/inputs/base.rb +++ b/lib/bootstrap_form/inputs/base.rb @@ -24,6 +24,8 @@ def bootstrap_field(field_name) def bootstrap_select_group(field_name) define_method(:"#{field_name}_with_bootstrap") do |name, options={}, html_options={}| + # Specifying the id for a select doesn't work. The Rails helpers need to generate + # what they generate, and that includes the ids for each select option. options.delete(:id) html_options = html_options.reverse_merge(control_class: "form-select") form_group_builder(name, options, html_options) do diff --git a/test/bootstrap_form_test.rb b/test/bootstrap_form_test.rb index a64767af..4cbb7fb5 100644 --- a/test/bootstrap_form_test.rb +++ b/test/bootstrap_form_test.rb @@ -1596,6 +1596,19 @@ def warn(message, ...) assert_equivalent_html expected, @builder.errors_on(:email, custom_class: "custom-error-class") end + test "errors_on with specified id:" do + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
+ Email can't be blank, Email is too short (minimum is 5 characters) +
+ HTML + + assert_equivalent_html expected, @builder.errors_on(:email, id: "custom-id") + end + test "horizontal-style forms" do expected = <<~HTML
From 055b1e6510b037fd3dd2712c9a9c791c1c228b79 Mon Sep 17 00:00:00 2001 From: Larry Reid Date: Fri, 5 Dec 2025 06:02:14 +0000 Subject: [PATCH 8/8] DRY up id generation --- lib/bootstrap_form/components/labels.rb | 2 +- lib/bootstrap_form/components/validation.rb | 7 +++++-- lib/bootstrap_form/form_group_builder.rb | 3 +-- lib/bootstrap_form/helpers/bootstrap.rb | 2 +- lib/bootstrap_form/inputs/check_box.rb | 5 +---- lib/bootstrap_form/inputs/inputs_collection.rb | 7 +++---- lib/bootstrap_form/inputs/radio_button.rb | 5 +---- 7 files changed, 13 insertions(+), 18 deletions(-) diff --git a/lib/bootstrap_form/components/labels.rb b/lib/bootstrap_form/components/labels.rb index aefa512a..fe6eec97 100644 --- a/lib/bootstrap_form/components/labels.rb +++ b/lib/bootstrap_form/components/labels.rb @@ -52,7 +52,7 @@ def prepare_label_options(id, name, options, custom_label_col, group_layout) options[:class] = label_classes(name, options, custom_label_col, group_layout) options.delete(:class) if options[:class].none? - options[:id] = id.present? ? "#{id}_feedback" : field_id(name, :feedback) if error?(name) && label_errors + options[:id] = aria_feedback_id(name:, id:) if error?(name) && label_errors end end end diff --git a/lib/bootstrap_form/components/validation.rb b/lib/bootstrap_form/components/validation.rb index 8b4ba29d..48d1dfcb 100644 --- a/lib/bootstrap_form/components/validation.rb +++ b/lib/bootstrap_form/components/validation.rb @@ -67,9 +67,8 @@ def generate_error(name, id) help_text = get_error_messages(name) help_klass = "invalid-feedback" help_tag = :div - id = id.present? ? "#{id}_feedback" : field_id(name, :feedback) - content_tag(help_tag, help_text, class: help_klass, id:) + content_tag(help_tag, help_text, class: help_klass, id: aria_feedback_id(id:, name:)) end def get_error_messages(name) @@ -85,6 +84,10 @@ def get_error_messages(name) safe_join(object.errors[name], ", ") end # rubocop:enable Metrics/AbcSize + + def aria_feedback_id(name:, id: nil) + id.present? ? "#{id}_feedback" : field_id(name, :feedback) + end end end end diff --git a/lib/bootstrap_form/form_group_builder.rb b/lib/bootstrap_form/form_group_builder.rb index a1fdb522..8e4989c6 100644 --- a/lib/bootstrap_form/form_group_builder.rb +++ b/lib/bootstrap_form/form_group_builder.rb @@ -100,8 +100,7 @@ def form_group_css_options(method, html_options, options) css_options[:class] = safe_join([control_classes, css_options[:class]].compact, " ") if error?(method) css_options[:class] << " is-invalid" - labelledby = options[:id].present? ? "#{options[:id]}_feedback" : field_id(method, :feedback) - css_options[:aria] = { labelledby: } + css_options[:aria] = { labelledby: aria_feedback_id(id: options[:id], name: method) } end css_options[:placeholder] = form_group_placeholder(options, method) if options[:label_as_placeholder] css_options diff --git a/lib/bootstrap_form/helpers/bootstrap.rb b/lib/bootstrap_form/helpers/bootstrap.rb index e4c145da..1cf39ce4 100644 --- a/lib/bootstrap_form/helpers/bootstrap.rb +++ b/lib/bootstrap_form/helpers/bootstrap.rb @@ -36,7 +36,7 @@ def errors_on(name, options={}) tag.div( class: custom_class || "invalid-feedback", - id: options[:id].present? ? "#{options[:id]}_feedback" : field_id(name, :feedback) + id: aria_feedback_id(id: options[:id], name:) ) do errors = if hide_attribute_name object.errors[name] diff --git a/lib/bootstrap_form/inputs/check_box.rb b/lib/bootstrap_form/inputs/check_box.rb index 866a9aa6..840b84ed 100644 --- a/lib/bootstrap_form/inputs/check_box.rb +++ b/lib/bootstrap_form/inputs/check_box.rb @@ -41,10 +41,7 @@ def check_box_options(name, options) :inline, :label, :label_class, :label_col, :layout, :skip_label, :switch, :wrapper, :wrapper_class) check_box_options[:class] = check_box_classes(name, options) - if error?(name) - labelledby = options[:id].present? ? "#{options[:id]}_feedback" : field_id(name, :feedback) - check_box_options[:aria] = { labelledby: } - end + check_box_options[:aria] = { labelledby: aria_feedback_id(id: options[:id], name:) } if error?(name) check_box_options.merge!(required_field_options(options, name)) end diff --git a/lib/bootstrap_form/inputs/inputs_collection.rb b/lib/bootstrap_form/inputs/inputs_collection.rb index 2b5dc808..9d742255 100644 --- a/lib/bootstrap_form/inputs/inputs_collection.rb +++ b/lib/bootstrap_form/inputs/inputs_collection.rb @@ -89,7 +89,7 @@ def field_group(name, options, &) :add_control_col_class, :append, :control_col, :floating, :help, :icon, :id, :input_group_class, :label, :label_col, :layout, :prepend ), - aria: { labelledby: options[:id] || default_id(name) }, + aria: { labelledby: group_label_div_id(id: options[:id], name:) }, role: :group ) do group_label_div = generate_group_label_div(name, options) @@ -100,15 +100,14 @@ def field_group(name, options, &) def generate_group_label_div(name, options) group_label_div_class = options.dig(:label, :class) || "form-label" - id = options[:id] || default_id(name) tag.div( **{ class: group_label_div_class }.compact, - id: + id: group_label_div_id(id: options[:id], name:) ) { label_text(name, options.dig(:label, :text)) } end - def default_id(name) = raw("#{object_name}_#{name}") # rubocop:disable Rails/OutputSafety + def group_label_div_id(id:, name:) = id || field_id(name) end end end diff --git a/lib/bootstrap_form/inputs/radio_button.rb b/lib/bootstrap_form/inputs/radio_button.rb index 9d69395f..ad7ddbe5 100644 --- a/lib/bootstrap_form/inputs/radio_button.rb +++ b/lib/bootstrap_form/inputs/radio_button.rb @@ -28,10 +28,7 @@ def radio_button_options(name, options) radio_button_options = options.except(:class, :label, :label_class, :error_message, :help, :inline, :hide_label, :skip_label, :wrapper, :wrapper_class) radio_button_options[:class] = radio_button_classes(name, options) - if error?(name) - labelledby = options[:id].present? ? "#{options[:id]}_feedback" : field_id(name, :feedback) - radio_button_options[:aria] = { labelledby: } - end + radio_button_options[:aria] = { labelledby: aria_feedback_id(id: options[:id], name:) } if error?(name) radio_button_options.merge!(required_field_options(options, name)) end