From a50351bbfa0001ca4a2d8d979cb0ed4df1b1d25e Mon Sep 17 00:00:00 2001 From: Larry Reid Date: Wed, 26 Nov 2025 20:28:59 +0000 Subject: [PATCH 1/4] Test for aria-labelledby More and fixed tests --- test/bootstrap_checkbox_test.rb | 16 +++++----- 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 | 10 +++--- test/bootstrap_selects_test.rb | 38 +++++++++++------------ test/special_form_class_models_test.rb | 4 +-- 8 files changed, 65 insertions(+), 87 deletions(-) diff --git a/test/bootstrap_checkbox_test.rb b/test/bootstrap_checkbox_test.rb index 48a8bb94..61516ec3 100644 --- a/test/bootstrap_checkbox_test.rb +++ b/test/bootstrap_checkbox_test.rb @@ -543,13 +543,13 @@ class BootstrapCheckboxTest < ActionView::TestCase
- +
- + -
a box must be checked
+
a box must be checked
@@ -593,13 +593,13 @@ class BootstrapCheckboxTest < ActionView::TestCase
- +
- + -
error for test
+
error for test
@@ -617,11 +617,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 6bd2deb0..c2ae7a97 100644 --- a/test/bootstrap_form_test.rb +++ b/test/bootstrap_form_test.rb @@ -451,8 +451,8 @@ def warn(message, ...) expected = <<~HTML
- - + +
HTML @@ -466,9 +466,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 @@ -484,9 +484,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 @@ -617,7 +617,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 @@ -710,8 +710,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
@@ -734,9 +734,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
@@ -756,7 +756,7 @@ def warn(message, ...)
- + This is required
@@ -800,7 +800,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 @@ -809,7 +809,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 c7543bdf..38428b51 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 @@ -187,13 +187,13 @@ class BootstrapRadioButtonTest < ActionView::TestCase
- +
- + -
error for test
+
error for test
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 50f7e64773cbe63ada9613cb18c0a3cab6e23d99 Mon Sep 17 00:00:00 2001 From: Larry Reid Date: Thu, 27 Nov 2025 03:11:23 +0000 Subject: [PATCH 2/4] aria-labelledby for errors Fix line lengths --- .yarnrc | 5 ---- 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 +++++-- 9 files changed, 32 insertions(+), 25 deletions(-) delete mode 100644 .yarnrc diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 index adf41e8f..00000000 --- a/.yarnrc +++ /dev/null @@ -1,5 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -lastUpdateCheck 1763256402231 diff --git a/README.md b/README.md index b233c2d3..b786e982 100644 --- a/README.md +++ b/README.md @@ -1490,38 +1490,38 @@ Generated HTML:
- -
is invalid
+ +
is invalid
- +
- + -
is invalid
+
is invalid
- +
- + -
is invalid
+
is invalid
- -
is invalid
+ +
is invalid
``` @@ -1551,8 +1551,8 @@ Generated HTML: ```html
- - + +
``` @@ -1649,7 +1649,7 @@ Which outputs: ```html
-
Email is invalid
+
Email is invalid
``` @@ -1670,7 +1670,7 @@ Which outputs: ```html
-
is invalid
+
is invalid
``` @@ -1689,7 +1689,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 579957ed..f2c2b855 100644 --- a/lib/bootstrap_form/components/labels.rb +++ b/lib/bootstrap_form/components/labels.rb @@ -18,6 +18,7 @@ def generate_label(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 label(name, label_text(name, options), options.except(:text)) 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 79eff7e5..b85b41e5 100644 --- a/lib/bootstrap_form/form_group_builder.rb +++ b/lib/bootstrap_form/form_group_builder.rb @@ -92,7 +92,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 c2ae7a97..5ec2e3bb 100644 --- a/test/bootstrap_form_test.rb +++ b/test/bootstrap_form_test.rb @@ -800,7 +800,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 @@ -809,7 +811,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 95104a7484be08b1841358e5333a497c1d93f365 Mon Sep 17 00:00:00 2001 From: Larry Reid Date: Fri, 28 Nov 2025 16:54:55 +0000 Subject: [PATCH 3/4] Test for custom ID --- test/bootstrap_checkbox_test.rb | 76 +++++++++++++++++++++++++ test/bootstrap_fields_test.rb | 29 ++++++++++ test/bootstrap_form_test.rb | 55 ++++++++++++++++++ test/bootstrap_radio_button_test.rb | 45 +++++++++++++++ test/bootstrap_selects_test.rb | 87 +++++++++++++++++++++++++++++ 5 files changed, 292 insertions(+) diff --git a/test/bootstrap_checkbox_test.rb b/test/bootstrap_checkbox_test.rb index 61516ec3..0428658c 100644 --- a/test/bootstrap_checkbox_test.rb +++ b/test/bootstrap_checkbox_test.rb @@ -562,6 +562,35 @@ class BootstrapCheckboxTest < 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" }], @@ -611,6 +640,33 @@ class BootstrapCheckboxTest < ActionView::TestCase 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 + test "check_box renders error when asked" do @user.errors.add(:terms, "You must accept the terms.") expected = <<~HTML @@ -631,6 +687,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 5ec2e3bb..cb11863e 100644 --- a/test/bootstrap_form_test.rb +++ b/test/bootstrap_form_test.rb @@ -459,6 +459,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? @@ -475,6 +490,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" } } }) @@ -495,6 +528,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 38428b51..b9e82d04 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
@@ -205,6 +224,32 @@ class BootstrapRadioButtonTest < 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_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 bec939c75150f79904c349c9cc6449d29fa3888a Mon Sep 17 00:00:00 2001 From: Larry Reid Date: Wed, 3 Dec 2025 01:24:59 +0000 Subject: [PATCH 4/4] 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 f2c2b855..e1e5c801 100644 --- a/lib/bootstrap_form/components/labels.rb +++ b/lib/bootstrap_form/components/labels.rb @@ -18,7 +18,7 @@ def generate_label(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 label(name, label_text(name, options), options.except(:text)) 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 b85b41e5..488a5d87 100644 --- a/lib/bootstrap_form/form_group_builder.rb +++ b/lib/bootstrap_form/form_group_builder.rb @@ -94,7 +94,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