Repository: wagtail/wagtail

Issue:
Wagtail 6.1.x update breaks `RadioSelect` initialiastion after replacing jQuery with vanilla JS
### Issue Summary

Wagtail 6.1.x update breaks `RadioSelect` initialiastion after replacing jQuery with vanilla JS.  

The JS adapter [`BoundRadioSelect`](https://github.com/wagtail/wagtail/blob/main/client/src/entrypoints/admin/telepath/widgets.js#L150) previously tested and set the checked option by comparing the element value against an array value - an array value is passed to the constructor:

#### Wagtail 6.0 implementation
```
setState(state) {
    this.element.find('input[name="' + this.name + '"]').val([state]);
}
```

It now tests against a string, and the equality operator fails, so the radio option is not correctly set as `checked` on initialisation:

#### Wagtail 6.1.x implementation
```
  setState(state) {
    const inputs = this.element.querySelectorAll(`input[name="${this.name}"]`);
    for (let i = 0; i < inputs.length; i += 1) {
      inputs[i].checked = inputs[i].value === state;
    }
  }
  ```

The breaking change is here:

https://github.com/wagtail/wagtail/compare/v6.0.3...v6.1#diff-70fd977dfb7c5b823eab70485fa842987589fc8656203ffb729a4b7d688c1d04L130

### Steps to Reproduce

1. Use a choice block within a Streamfield such as:

```
link_type = blocks.ChoiceBlock(
    choices=[
        ('page', 'Page'),
        ('document', 'Document'),
        ('url', 'URL'),
    ],
    widget=forms.RadioSelect,
    required=True,
    default='page',
)
```
2. View/edit a page/snippet where this field is used
3. In Wagtail 6.0.x the first choice is `checked` as expected
4. In Wagtail 6.1.x the first choice is not `checked` as expected

-   I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes

### Technical details

-   Python version: Python 3.12.3
-   Django version: 5.0.6
-   Wagtail version: 6.1.x
-   Browser version: Chrome 125

### Working on this

Anyone can contribute to this. View our [contributing guidelines](https://docs.wagtail.org/en/latest/contributing/index.html), add a comment to the issue once you’re ready to start.



Discussion:
Thanks for the report! Can confirm I'm seeing this too - the bug is present on [bakerydemo](https://github.com/wagtail/bakerydemo/), on the "difficulty" options on the recipe page.

Resolving patch:
diff --git a/wagtail/widget_adapters.py b/wagtail/widget_adapters.py
--- a/wagtail/widget_adapters.py
+++ b/wagtail/widget_adapters.py
@@ -36,7 +36,6 @@ def media(self):
 
 register(WidgetAdapter(), forms.widgets.Input)
 register(WidgetAdapter(), forms.Textarea)
-register(WidgetAdapter(), forms.CheckboxSelectMultiple)
 
 
 class CheckboxInputAdapter(WidgetAdapter):
@@ -51,6 +50,7 @@ class RadioSelectAdapter(WidgetAdapter):
 
 
 register(RadioSelectAdapter(), forms.RadioSelect)
+register(RadioSelectAdapter(), forms.CheckboxSelectMultiple)
 
 
 class SelectAdapter(WidgetAdapter):


Test patch:
diff --git a/client/src/entrypoints/admin/telepath/__snapshots__/widgets.test.js.snap b/client/src/entrypoints/admin/telepath/__snapshots__/widgets.test.js.snap
--- a/client/src/entrypoints/admin/telepath/__snapshots__/widgets.test.js.snap
+++ b/client/src/entrypoints/admin/telepath/__snapshots__/widgets.test.js.snap
@@ -1,5 +1,22 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`telepath: wagtail.widgets.RadioSelect for CheckboxSelectMultiple it renders correctly 1`] = `
+"<ul id="the-id">
+          <li>
+            <label for="the-id_0">
+            <input type="checkbox" name="the-name" value="red" id="the-id_0"> Red</label>
+          </li>
+          <li>
+            <label for="the-id_1">
+            <input type="checkbox" name="the-name" value="green" id="the-id_1"> Green</label>
+          </li>
+          <li>
+            <label for="the-id_2">
+            <input type="checkbox" name="the-name" value="blue" id="the-id_2"> Blue</label>
+          </li>
+        </ul>"
+`;
+
 exports[`telepath: wagtail.widgets.RadioSelect it renders correctly 1`] = `
 "<ul id="the-id">
           <li>
@@ -19,3 +36,11 @@ exports[`telepath: wagtail.widgets.Select it renders correctly 1`] = `
           <option value="2">Option 2</option>
         </select>"
 `;
+
+exports[`telepath: wagtail.widgets.Select multiple it renders correctly 1`] = `
+"<select name="the-name" id="the-id" multiple="">
+          <option value="red">Red</option>
+          <option value="green">Green</option>
+          <option value="blue">Blue</option>
+        </select>"
+`;
diff --git a/client/src/entrypoints/admin/telepath/widgets.test.js b/client/src/entrypoints/admin/telepath/widgets.test.js
--- a/client/src/entrypoints/admin/telepath/widgets.test.js
+++ b/client/src/entrypoints/admin/telepath/widgets.test.js
@@ -183,7 +183,7 @@ describe('telepath: wagtail.widgets.RadioSelect', () => {
       document.getElementById('placeholder'),
       'the-name',
       'the-id',
-      'tea',
+      ['tea'],
     );
   });
 
@@ -198,11 +198,11 @@ describe('telepath: wagtail.widgets.RadioSelect', () => {
   });
 
   test('getState() returns the current state', () => {
-    expect(boundWidget.getState()).toBe('tea');
+    expect(boundWidget.getState()).toStrictEqual(['tea']);
   });
 
   test('setState() changes the current state', () => {
-    boundWidget.setState('coffee');
+    boundWidget.setState(['coffee']);
     expect(document.querySelector('input[value="tea"]').checked).toBe(false);
     expect(document.querySelector('input[value="coffee"]').checked).toBe(true);
   });
@@ -216,6 +216,64 @@ describe('telepath: wagtail.widgets.RadioSelect', () => {
   });
 });
 
+describe('telepath: wagtail.widgets.RadioSelect for CheckboxSelectMultiple', () => {
+  let boundWidget;
+
+  beforeEach(() => {
+    // Create a placeholder to render the widget
+    document.body.innerHTML = '<div id="placeholder"></div>';
+
+    const widgetDef = window.telepath.unpack({
+      _type: 'wagtail.widgets.RadioSelect',
+      _args: [
+        `<ul id="__ID__">
+          <li>
+            <label for="__ID___0">
+            <input type="checkbox" name="__NAME__" value="red" id="__ID___0"> Red</label>
+          </li>
+          <li>
+            <label for="__ID___1">
+            <input type="checkbox" name="__NAME__" value="green" id="__ID___1"> Green</label>
+          </li>
+          <li>
+            <label for="__ID___2">
+            <input type="checkbox" name="__NAME__" value="blue" id="__ID___2"> Blue</label>
+          </li>
+        </ul>`,
+        '__ID___0',
+      ],
+    });
+    boundWidget = widgetDef.render(
+      document.getElementById('placeholder'),
+      'the-name',
+      'the-id',
+      ['red', 'blue'],
+    );
+  });
+
+  test('it renders correctly', () => {
+    expect(document.body.innerHTML).toMatchSnapshot();
+    expect(document.querySelector('input[value="red"]').checked).toBe(true);
+    expect(document.querySelector('input[value="green"]').checked).toBe(false);
+    expect(document.querySelector('input[value="blue"]').checked).toBe(true);
+  });
+
+  test('getValue() returns the current value', () => {
+    expect(boundWidget.getValue()).toStrictEqual(['red', 'blue']);
+  });
+
+  test('getState() returns the current state', () => {
+    expect(boundWidget.getState()).toStrictEqual(['red', 'blue']);
+  });
+
+  test('setState() changes the current state', () => {
+    boundWidget.setState(['red', 'green']);
+    expect(document.querySelector('input[value="red"]').checked).toBe(true);
+    expect(document.querySelector('input[value="green"]').checked).toBe(true);
+    expect(document.querySelector('input[value="blue"]').checked).toBe(false);
+  });
+});
+
 describe('telepath: wagtail.widgets.CheckboxInput', () => {
   let boundWidget;
 
@@ -284,7 +342,7 @@ describe('telepath: wagtail.widgets.Select', () => {
       document.getElementById('placeholder'),
       'the-name',
       'the-id',
-      '1',
+      ['1'],
     );
   });
 
@@ -292,11 +350,86 @@ describe('telepath: wagtail.widgets.Select', () => {
     expect(document.body.innerHTML).toMatchSnapshot();
     const select = document.querySelector('select');
     expect(select.options[select.selectedIndex].value).toBe('1');
+    const selectedOptions = document.querySelector('select').selectedOptions;
+    expect(selectedOptions.length).toBe(1);
+    expect(selectedOptions[0].value).toBe('1');
   });
 
   test('getTextLabel() returns the text of selected option', () => {
     expect(boundWidget.getTextLabel()).toBe('Option 1');
   });
+
+  test('getValue() returns the current value', () => {
+    expect(boundWidget.getValue()).toBe('1');
+  });
+
+  test('getState() returns the current state', () => {
+    expect(boundWidget.getState()).toStrictEqual(['1']);
+  });
+
+  test('setState() changes the current state', () => {
+    boundWidget.setState(['2']);
+    const selectedOptions = document.querySelector('select').selectedOptions;
+    expect(selectedOptions.length).toBe(1);
+    expect(selectedOptions[0].value).toBe('2');
+  });
+});
+
+describe('telepath: wagtail.widgets.Select multiple', () => {
+  let boundWidget;
+
+  beforeEach(() => {
+    // Create a placeholder to render the widget
+    document.body.innerHTML = '<div id="placeholder"></div>';
+
+    const widgetDef = window.telepath.unpack({
+      _type: 'wagtail.widgets.Select',
+      _args: [
+        `<select name="__NAME__" id="__ID__" multiple>
+          <option value="red">Red</option>
+          <option value="green">Green</option>
+          <option value="blue">Blue</option>
+        </select>`,
+        '__ID__',
+      ],
+    });
+    boundWidget = widgetDef.render(
+      document.getElementById('placeholder'),
+      'the-name',
+      'the-id',
+      ['red', 'blue'],
+    );
+  });
+
+  test('it renders correctly', () => {
+    expect(document.body.innerHTML).toMatchSnapshot();
+    const select = document.querySelector('select');
+    expect(select.options[select.selectedIndex].value).toBe('red');
+    const selectedOptions = document.querySelector('select').selectedOptions;
+    expect(selectedOptions.length).toBe(2);
+    expect(selectedOptions[0].value).toBe('red');
+    expect(selectedOptions[1].value).toBe('blue');
+  });
+
+  test('getTextLabel() returns the text of selected options', () => {
+    expect(boundWidget.getTextLabel()).toBe('Red, Blue');
+  });
+
+  test('getValue() returns the current values', () => {
+    expect(boundWidget.getValue()).toStrictEqual(['red', 'blue']);
+  });
+
+  test('getState() returns the current state', () => {
+    expect(boundWidget.getState()).toStrictEqual(['red', 'blue']);
+  });
+
+  test('setState() changes the current state', () => {
+    boundWidget.setState(['red', 'green']);
+    const selectedOptions = document.querySelector('select').selectedOptions;
+    expect(selectedOptions.length).toBe(2);
+    expect(selectedOptions[0].value).toBe('red');
+    expect(selectedOptions[1].value).toBe('green');
+  });
 });
 
 describe('telepath: wagtail.widgets.DraftailRichTextArea', () => {
diff --git a/wagtail/tests/test_blocks.py b/wagtail/tests/test_blocks.py
--- a/wagtail/tests/test_blocks.py
+++ b/wagtail/tests/test_blocks.py
@@ -1122,6 +1122,18 @@ def validate_tea_is_selected(value):
         with self.assertRaises(ValidationError):
             block.clean("coffee")
 
+    def test_get_form_state(self):
+        block = blocks.ChoiceBlock(choices=[("tea", "Tea"), ("coffee", "Coffee")])
+        form_state = block.get_form_state("tea")
+        self.assertEqual(form_state, ["tea"])
+
+    def test_get_form_state_with_radio_widget(self):
+        block = blocks.ChoiceBlock(
+            choices=[("tea", "Tea"), ("coffee", "Coffee")], widget=forms.RadioSelect
+        )
+        form_state = block.get_form_state("tea")
+        self.assertEqual(form_state, ["tea"])
+
 
 class TestMultipleChoiceBlock(WagtailTestUtils, SimpleTestCase):
     def setUp(self):
@@ -1508,6 +1520,21 @@ def validate_tea_is_selected(value):
         with self.assertRaises(ValidationError):
             block.clean("coffee")
 
+    def test_get_form_state(self):
+        block = blocks.MultipleChoiceBlock(
+            choices=[("tea", "Tea"), ("coffee", "Coffee")]
+        )
+        form_state = block.get_form_state(["tea", "coffee"])
+        self.assertEqual(form_state, ["tea", "coffee"])
+
+    def test_get_form_state_with_checkbox_widget(self):
+        block = blocks.ChoiceBlock(
+            choices=[("tea", "Tea"), ("coffee", "Coffee")],
+            widget=forms.CheckboxSelectMultiple,
+        )
+        form_state = block.get_form_state(["tea", "coffee"])
+        self.assertEqual(form_state, ["tea", "coffee"])
+
 
 class TestRawHTMLBlock(unittest.TestCase):
     def test_get_default_with_fallback_value(self):