Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 23, 2026

What are you trying to accomplish?

Checkbox field IDs were being sanitized by Rails' default behavior, converting brackets to underscores. A checkbox with name="permissions[3]" and value="foo" generated id="permissions_3_foo" instead of id="permissions[3]_foo".

This PR overrides ID generation in CheckBox component to preserve special characters like brackets, maintaining proper HTML semantics for nested form structures.

Integration

Risk Assessment

  • Low risk - Isolated change to ID generation logic only. All 769 existing tests pass. Custom ID only applies when none explicitly provided. Rails escapes HTML attributes automatically, preventing XSS.

What approach did you choose and why?

Override ID generation in CheckBox#initialize:

  • Boolean scheme: ID = name (e.g., long_o)
  • Array scheme: ID = name + "_" + value (e.g., permissions[3]_foo)

The distinction matters because array scheme checkboxes share a name but need unique IDs, while boolean scheme checkboxes have unique names.

Rails appends [] to array scheme names at render time, so we strip trailing [] during ID generation to avoid duplication.

Anything you want to highlight for special attention from reviewers?

The label's for attribute is updated to match the custom ID. Without an explicit ID in input_arguments, the custom generator runs; with an explicit ID, it's skipped entirely.

Accessibility

  • No new axe scan violation - This change does not introduce any new axe scan violations.

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Lookbook)
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • accounts.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=5926 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/ferrum_user_data_dir_20260124-5898-1fmoyr --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,6134350618444463348,6714943023189237086,262144 --enable-features=NetworkService,NetworkServiceInProcess --disable-features=IsolateOrigins,PaintHolding,TranslateUI,site-per-process --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /opt/google/chrome/chrome /usr/bin/google-chrome --headless --disable-gpu --hide-scrollbars --mute-audio --enable-automation --disable-web-security --disable-session-crashed-bubble --disable-breakpad --disable-sync --no-first-run --use-mock-keychain --keep-alive-for-test --disable-popup-blocking --disable-extensions --disable-component-extensions-with-REDACTED-pages --disable-hang-monitor --disable-features=site-per-process,IsolateOrigins,TranslateUI --disable-translate --disable-REDACTED-networking (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=6192 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/ferrum_user_data_dir_20260124-6164-bs65w6 --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,10032288820533622983,11681080222740272597,262144 --enable-features=NetworkService,NetworkServiceInProcess --disable-features=IsolateOrigins,PaintHolding,TranslateUI,site-per-process --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
  • clients2.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=5926 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/ferrum_user_data_dir_20260124-5898-1fmoyr --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,6134350618444463348,6714943023189237086,262144 --enable-features=NetworkService,NetworkServiceInProcess --disable-features=IsolateOrigins,PaintHolding,TranslateUI,site-per-process --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /opt/google/chrome/chrome /usr/bin/google-chrome --headless --disable-gpu --hide-scrollbars --mute-audio --enable-automation --disable-web-security --disable-session-crashed-bubble --disable-breakpad --disable-sync --no-first-run --use-mock-keychain --keep-alive-for-test --disable-popup-blocking --disable-extensions --disable-component-extensions-with-REDACTED-pages --disable-hang-monitor --disable-features=site-per-process,IsolateOrigins,TranslateUI --disable-translate --disable-REDACTED-networking (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=6192 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/ferrum_user_data_dir_20260124-6164-bs65w6 --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,10032288820533622983,11681080222740272597,262144 --enable-features=NetworkService,NetworkServiceInProcess --disable-features=IsolateOrigins,PaintHolding,TranslateUI,site-per-process --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
  • content-autofill.googleapis.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=5926 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/ferrum_user_data_dir_20260124-5898-1fmoyr --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,6134350618444463348,6714943023189237086,262144 --enable-features=NetworkService,NetworkServiceInProcess --disable-features=IsolateOrigins,PaintHolding,TranslateUI,site-per-process --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /opt/google/chrome/chrome /usr/bin/google-chrome --headless --disable-gpu --hide-scrollbars --mute-audio --enable-automation --disable-web-security --disable-session-crashed-bubble --disable-breakpad --disable-sync --no-first-run --use-mock-keychain --keep-alive-for-test --disable-popup-blocking --disable-extensions --disable-component-extensions-with-REDACTED-pages --disable-hang-monitor --disable-features=site-per-process,IsolateOrigins,TranslateUI --disable-translate --disable-REDACTED-networking (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=6192 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/ferrum_user_data_dir_20260124-6164-bs65w6 --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,10032288820533622983,11681080222740272597,262144 --enable-features=NetworkService,NetworkServiceInProcess --disable-features=IsolateOrigins,PaintHolding,TranslateUI,site-per-process --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
  • optimizationguide-pa.googleapis.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=5926 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/ferrum_user_data_dir_20260124-5898-1fmoyr --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,6134350618444463348,6714943023189237086,262144 --enable-features=NetworkService,NetworkServiceInProcess --disable-features=IsolateOrigins,PaintHolding,TranslateUI,site-per-process --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /opt/google/chrome/chrome /usr/bin/google-chrome --headless --disable-gpu --hide-scrollbars --mute-audio --enable-automation --disable-web-security --disable-session-crashed-bubble --disable-breakpad --disable-sync --no-first-run --use-mock-keychain --keep-alive-for-test --disable-popup-blocking --disable-extensions --disable-component-extensions-with-REDACTED-pages --disable-hang-monitor --disable-features=site-per-process,IsolateOrigins,TranslateUI --disable-translate --disable-REDACTED-networking (dns block)
  • safebrowsingohttpgateway.googleapis.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=5926 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/ferrum_user_data_dir_20260124-5898-1fmoyr --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,6134350618444463348,6714943023189237086,262144 --enable-features=NetworkService,NetworkServiceInProcess --disable-features=IsolateOrigins,PaintHolding,TranslateUI,site-per-process --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /opt/google/chrome/chrome /usr/bin/google-chrome --headless --disable-gpu --hide-scrollbars --mute-audio --enable-automation --disable-web-security --disable-session-crashed-bubble --disable-breakpad --disable-sync --no-first-run --use-mock-keychain --keep-alive-for-test --disable-popup-blocking --disable-extensions --disable-component-extensions-with-REDACTED-pages --disable-hang-monitor --disable-features=site-per-process,IsolateOrigins,TranslateUI --disable-translate --disable-REDACTED-networking (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=6192 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/ferrum_user_data_dir_20260124-6164-bs65w6 --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,10032288820533622983,11681080222740272597,262144 --enable-features=NetworkService,NetworkServiceInProcess --disable-features=IsolateOrigins,PaintHolding,TranslateUI,site-per-process --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
  • www.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=5926 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/ferrum_user_data_dir_20260124-5898-1fmoyr --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,6134350618444463348,6714943023189237086,262144 --enable-features=NetworkService,NetworkServiceInProcess --disable-features=IsolateOrigins,PaintHolding,TranslateUI,site-per-process --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /opt/google/chrome/chrome /usr/bin/google-chrome --headless --disable-gpu --hide-scrollbars --mute-audio --enable-automation --disable-web-security --disable-session-crashed-bubble --disable-breakpad --disable-sync --no-first-run --use-mock-keychain --keep-alive-for-test --disable-popup-blocking --disable-extensions --disable-component-extensions-with-REDACTED-pages --disable-hang-monitor --disable-features=site-per-process,IsolateOrigins,TranslateUI --disable-translate --disable-REDACTED-networking (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=6192 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/ferrum_user_data_dir_20260124-6164-bs65w6 --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,10032288820533622983,11681080222740272597,262144 --enable-features=NetworkService,NetworkServiceInProcess --disable-features=IsolateOrigins,PaintHolding,TranslateUI,site-per-process --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>Generated field ids include brackets, etc.</issue_title>
<issue_description>To reproduce:

<%=
  render(
    Primer::Alpha::CheckBox.new(
      scheme: :array,
      name: "permissions[#{role.id}]",
      value: "foo",
      checked: true,
      label: "Foo"
    )
  )
%>

Expected value

<div class="FormControl-checkbox-wrap">
  <input class="FormControl-checkbox" type="checkbox" value="foo" checked="checked" name="permissions[3][]" id="permissions[3]_foo">
  <span class="FormControl-checkbox-labelWrap">
    <label class="sr-only FormControl-label" for="permissions[3]_foo">
      Foo
    </label>      
  </span>
</div>

Actual value

<div class="FormControl-checkbox-wrap">
  <input class="FormControl-checkbox" type="checkbox" value="foo" checked="checked" name="permissions[3][]" id="permissions_3_foo">
  <span class="FormControl-checkbox-labelWrap">
    <label class="sr-only FormControl-label" for="permissions_3_foo">
      Foo
    </label>      
  </span>
</div>

more details will be added soon</issue_description>

Comments on the Issue (you are @copilot in this section)

@siddharthkp Hi @myabc, thanks for taking the time to create the issue.

Things are a bit slow at work right now because of the holidays, I'll try to get you a response next week once folks are back at work :)</comment_new>


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@changeset-bot
Copy link

changeset-bot bot commented Jan 23, 2026

🦋 Changeset detected

Latest commit: 90914ef

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/view-components Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copilot AI changed the title [WIP] Fix generated field ids to remove brackets Fix checkbox ID generation to preserve brackets in field names Jan 23, 2026
Copilot AI requested a review from jonrohan January 23, 2026 18:42
@jonrohan jonrohan marked this pull request as ready for review January 23, 2026 22:30
@jonrohan jonrohan requested a review from a team as a code owner January 23, 2026 22:30
Copilot AI review requested due to automatic review settings January 23, 2026 22:30
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes checkbox ID generation to preserve brackets and other special characters in field names, addressing issue #3811. Previously, Rails' default sanitization converted brackets to underscores (e.g., permissions[3] became permissions_3), which broke proper HTML semantics for nested form structures.

Changes:

  • Added custom ID generation logic in CheckBox#initialize that preserves special characters
  • Distinguished between boolean scheme (ID = name) and array scheme (ID = name + "_" + value)
  • Updated label for attributes to match the custom IDs
  • Added comprehensive tests for both checkbox schemes

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
app/lib/primer/forms/check_box.rb Implements custom ID generation logic that preserves brackets in names for both boolean and array checkbox schemes
test/lib/primer/forms/checkbox_input_test.rb Adds tests verifying bracket preservation in generated IDs for both boolean and array schemes
.changeset/brown-wasps-exercise.md Documents the change (but description is incorrect)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"@primer/view-components": patch
---

Fix generated field ids to remove brackets
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changeset description says "Fix generated field ids to remove brackets" but this is incorrect. The fix actually preserves brackets in generated field IDs, not removes them. The PR description and implementation both confirm that brackets are being preserved (e.g., permissions[3]_foo instead of permissions_3_foo).

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

Comment on lines +84 to +99
def test_preserves_brackets_in_generated_ids_with_boolean_scheme
render_in_view_context do
primer_form_with(url: "/foo") do |f|
render_inline_form(f) do |check_form|
check_form.check_box(
name: "user[settings][email]",
label: "Email notifications"
)
end
end
end

# For boolean scheme, the ID should just be the name with brackets preserved
assert_selector "input[id='user[settings][email]']"
assert_selector "label[for='user[settings][email]']"
end
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a test case to verify that when an explicit ID is provided via the id parameter, it is not overridden by the custom ID generation logic. This would ensure the guard condition on line 15 works correctly and that users can still provide custom IDs when needed.

Copilot uses AI. Check for mistakes.
@jonrohan jonrohan enabled auto-merge January 29, 2026 05:42
"@primer/view-components": patch
---

Fix generated field ids to remove brackets
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

@jonrohan jonrohan added this pull request to the merge queue Jan 29, 2026
Merged via the queue into main with commit 4e702c8 Jan 29, 2026
38 checks passed
@jonrohan jonrohan deleted the copilot/fix-generated-field-ids branch January 29, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated field ids include brackets, etc.

3 participants