Skip to content

Commit 43a71cf

Browse files
authored
Ensure DOM ref is properly handled in the RadioGroup component (#2424)
* drop `by` prop Otherwise it ends up in the DOM which doesn't hurt but isn't ideal either. * ensure we are reading the underlying DOM correctly We assumed that the `optionRef` was `HTMLElement | null`, but if you use a custom component, then it is exposed as `{ $el: ref }`, this is why we use the `dom()` helper. * add test to ensure using a custom `as` prop works as expected * update changelog
1 parent 7ec0652 commit 43a71cf

File tree

6 files changed

+105
-4
lines changed

6 files changed

+105
-4
lines changed

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Disable `ComboboxInput` when its `Combobox` is disabled ([#2375](https://github.com/tailwindlabs/headlessui/pull/2375))
1414
- Add `FocusTrap` event listeners once document has loaded ([#2389](https://github.com/tailwindlabs/headlessui/pull/2389))
1515
- Don't scroll-lock `<Dialog>` when wrapping transition isn't showing ([#2422](https://github.com/tailwindlabs/headlessui/pull/2422))
16+
- Ensure DOM `ref` is properly handled in the `RadioGroup` component ([#2424](https://github.com/tailwindlabs/headlessui/pull/2424))
1617

1718
### Added
1819

packages/@headlessui-vue/src/components/combobox/combobox.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,33 @@ describe('Rendering', () => {
15231523
expect(handleChange).toHaveBeenNthCalledWith(2, 'bob')
15241524
})
15251525
})
1526+
1527+
it(
1528+
'should be possible to use a custom component using the `as` prop without crashing',
1529+
suppressConsoleLogs(async () => {
1530+
let CustomComponent = defineComponent({
1531+
template: html`<button><slot /></button>`,
1532+
})
1533+
1534+
renderTemplate({
1535+
template: html`
1536+
<Combobox name="assignee">
1537+
<ComboboxInput />
1538+
<ComboboxButton />
1539+
<ComboboxOptions>
1540+
<ComboboxOption :as="CustomComponent" value="alice">Alice</RadioGroupOption>
1541+
<ComboboxOption :as="CustomComponent" value="bob">Bob</RadioGroupOption>
1542+
<ComboboxOption :as="CustomComponent" value="charlie">Charlie</RadioGroupOption>
1543+
</ComboboxOptions>
1544+
</Combobox>
1545+
`,
1546+
setup: () => ({ CustomComponent }),
1547+
})
1548+
1549+
// Open combobox
1550+
await click(getComboboxButton())
1551+
})
1552+
)
15261553
})
15271554

15281555
describe('Rendering composition', () => {

packages/@headlessui-vue/src/components/listbox/listbox.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,6 +1270,32 @@ describe('Rendering', () => {
12701270
expect(handleChange).toHaveBeenNthCalledWith(2, 'bob')
12711271
})
12721272
})
1273+
1274+
it(
1275+
'should be possible to use a custom component using the `as` prop without crashing',
1276+
suppressConsoleLogs(async () => {
1277+
let CustomComponent = defineComponent({
1278+
template: html`<button><slot /></button>`,
1279+
})
1280+
1281+
renderTemplate({
1282+
template: html`
1283+
<Listbox name="assignee">
1284+
<ListboxButton />
1285+
<ListboxOptions>
1286+
<ListboxOption :as="CustomComponent" value="alice">Alice</RadioGroupOption>
1287+
<ListboxOption :as="CustomComponent" value="bob">Bob</RadioGroupOption>
1288+
<ListboxOption :as="CustomComponent" value="charlie">Charlie</RadioGroupOption>
1289+
</ListboxOptions>
1290+
</Listbox>
1291+
`,
1292+
setup: () => ({ CustomComponent }),
1293+
})
1294+
1295+
// Open listbox
1296+
await click(getListboxButton())
1297+
})
1298+
)
12731299
})
12741300

12751301
describe('Rendering composition', () => {

packages/@headlessui-vue/src/components/menu/menu.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,32 @@ describe('Rendering', () => {
818818
// Verify that the third menu item is active
819819
assertMenuLinkedWithMenuItem(items[2])
820820
})
821+
822+
it(
823+
'should be possible to use a custom component using the `as` prop without crashing',
824+
suppressConsoleLogs(async () => {
825+
let CustomComponent = defineComponent({
826+
template: `<button><slot /></button>`,
827+
})
828+
829+
renderTemplate({
830+
template: `
831+
<Menu>
832+
<MenuButton />
833+
<MenuOptions>
834+
<MenuOption :as="CustomComponent">Alice</RadioGroupOption>
835+
<MenuOption :as="CustomComponent">Bob</RadioGroupOption>
836+
<MenuOption :as="CustomComponent">Charlie</RadioGroupOption>
837+
</MenuOptions>
838+
</Menu>
839+
`,
840+
setup: () => ({ CustomComponent }),
841+
})
842+
843+
// Open menu
844+
await click(getMenuButton())
845+
})
846+
)
821847
})
822848

823849
describe('Rendering composition', () => {

packages/@headlessui-vue/src/components/radio-group/radio-group.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { nextTick, ref, watch, reactive } from 'vue'
1+
import { nextTick, ref, watch, reactive, defineComponent, defineExpose } from 'vue'
22
import { createRenderTemplate, render } from '../../test-utils/vue-testing-library'
33

44
import { RadioGroup, RadioGroupOption, RadioGroupLabel, RadioGroupDescription } from './radio-group'
@@ -496,6 +496,26 @@ describe('Rendering', () => {
496496
assertActiveElement(getByText('Option 3'))
497497
})
498498

499+
it(
500+
'should be possible to use a custom component using the `as` prop without crashing',
501+
suppressConsoleLogs(async () => {
502+
let CustomComponent = defineComponent({
503+
template: html`<button><slot /></button>`,
504+
})
505+
506+
renderTemplate({
507+
template: html`
508+
<RadioGroup name="assignee">
509+
<RadioGroupOption :as="CustomComponent" value="alice">Alice</RadioGroupOption>
510+
<RadioGroupOption :as="CustomComponent" value="bob">Bob</RadioGroupOption>
511+
<RadioGroupOption :as="CustomComponent" value="charlie">Charlie</RadioGroupOption>
512+
</RadioGroup>
513+
`,
514+
setup: () => ({ CustomComponent }),
515+
})
516+
})
517+
)
518+
499519
describe('Equality', () => {
500520
let options = [
501521
{ id: 1, name: 'Alice' },

packages/@headlessui-vue/src/components/radio-group/radio-group.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ export let RadioGroup = defineComponent({
272272
: []),
273273
render({
274274
ourProps,
275-
theirProps: { ...attrs, ...omit(theirProps, ['modelValue', 'defaultValue']) },
275+
theirProps: { ...attrs, ...omit(theirProps, ['modelValue', 'defaultValue', 'by']) },
276276
slot: {},
277277
attrs,
278278
slots,
@@ -309,7 +309,8 @@ export let RadioGroupOption = defineComponent({
309309

310310
expose({ el: optionRef, $el: optionRef })
311311

312-
onMounted(() => api.registerOption({ id: props.id, element: optionRef, propsRef }))
312+
let element = computed(() => dom(optionRef))
313+
onMounted(() => api.registerOption({ id: props.id, element, propsRef }))
313314
onUnmounted(() => api.unregisterOption(props.id))
314315

315316
let isFirstOption = computed(() => api.firstOption.value?.id === props.id)
@@ -326,7 +327,7 @@ export let RadioGroupOption = defineComponent({
326327
if (!api.change(props.value)) return
327328

328329
state.value |= OptionState.Active
329-
optionRef.value?.focus()
330+
dom(optionRef)?.focus()
330331
}
331332

332333
function handleFocus() {

0 commit comments

Comments
 (0)