Skip to content

Commit 9a24198

Browse files
Allow users customize ID generation (#2959)
* Move `useId` calls inside setup() * Allow injecting custom id generator Vue does not currently have a native `useId` helper. However, Nuxt has created their own that they ensure works across the client/server boundary. Now a user can use `provide()` in their app to inject a custom useId generation function which, for Nuxt users, can defer to the one provided by Nuxt. * Add tests * Export a `provideUseId` helper
1 parent 7c6fa3d commit 9a24198

File tree

14 files changed

+168
-80
lines changed

14 files changed

+168
-80
lines changed

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -779,9 +779,10 @@ export let ComboboxLabel = defineComponent({
779779
name: 'ComboboxLabel',
780780
props: {
781781
as: { type: [Object, String], default: 'label' },
782-
id: { type: String, default: () => `headlessui-combobox-label-${useId()}` },
782+
id: { type: String, default: null },
783783
},
784784
setup(props, { attrs, slots }) {
785+
let id = props.id ?? `headlessui-combobox-label-${useId()}`
785786
let api = useComboboxContext('ComboboxLabel')
786787

787788
function handleClick() {
@@ -794,7 +795,7 @@ export let ComboboxLabel = defineComponent({
794795
disabled: api.disabled.value,
795796
}
796797

797-
let { id, ...theirProps } = props
798+
let { ...theirProps } = props
798799
let ourProps = { id, ref: api.labelRef, onClick: handleClick }
799800

800801
return render({
@@ -815,9 +816,10 @@ export let ComboboxButton = defineComponent({
815816
name: 'ComboboxButton',
816817
props: {
817818
as: { type: [Object, String], default: 'button' },
818-
id: { type: String, default: () => `headlessui-combobox-button-${useId()}` },
819+
id: { type: String, default: null },
819820
},
820821
setup(props, { attrs, slots, expose }) {
822+
let id = props.id ?? `headlessui-combobox-button-${useId()}`
821823
let api = useComboboxContext('ComboboxButton')
822824

823825
expose({ el: api.buttonRef, $el: api.buttonRef })
@@ -884,7 +886,7 @@ export let ComboboxButton = defineComponent({
884886
disabled: api.disabled.value,
885887
value: api.value.value,
886888
}
887-
let { id, ...theirProps } = props
889+
let { ...theirProps } = props
888890
let ourProps = {
889891
ref: api.buttonRef,
890892
id,
@@ -921,12 +923,13 @@ export let ComboboxInput = defineComponent({
921923
unmount: { type: Boolean, default: true },
922924
displayValue: { type: Function as PropType<(item: unknown) => string> },
923925
defaultValue: { type: String, default: undefined },
924-
id: { type: String, default: () => `headlessui-combobox-input-${useId()}` },
926+
id: { type: String, default: null },
925927
},
926928
emits: {
927929
change: (_value: Event & { target: HTMLInputElement }) => true,
928930
},
929931
setup(props, { emit, attrs, slots, expose }) {
932+
let id = props.id ?? `headlessui-combobox-input-${useId()}`
930933
let api = useComboboxContext('ComboboxInput')
931934
let ownerDocument = computed(() => getOwnerDocument(dom(api.inputRef)))
932935

@@ -1284,7 +1287,7 @@ export let ComboboxInput = defineComponent({
12841287

12851288
return () => {
12861289
let slot = { open: api.comboboxState.value === ComboboxStates.Open }
1287-
let { id, displayValue, onChange: _onChange, ...theirProps } = props
1290+
let { displayValue, onChange: _onChange, ...theirProps } = props
12881291
let ourProps = {
12891292
'aria-controls': api.optionsRef.value?.id,
12901293
'aria-expanded': api.comboboxState.value === ComboboxStates.Open,

packages/@headlessui-vue/src/components/description/description.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,18 @@ export let Description = defineComponent({
6666
name: 'Description',
6767
props: {
6868
as: { type: [Object, String], default: 'p' },
69-
id: { type: String, default: () => `headlessui-description-${useId()}` },
69+
id: { type: String, default: null },
7070
},
7171
setup(myProps, { attrs, slots }) {
72+
let id = myProps.id ?? `headlessui-description-${useId()}`
7273
let context = useDescriptionContext()
7374

74-
onMounted(() => onUnmounted(context.register(myProps.id)))
75+
onMounted(() => onUnmounted(context.register(id)))
7576

7677
return () => {
7778
let { name = 'Description', slot = ref({}), props = {} } = context
78-
let { id, ...theirProps } = myProps
79+
let { id: _id, ...theirProps } = myProps
80+
7981
let ourProps = {
8082
...Object.entries(props).reduce(
8183
(acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }),

packages/@headlessui-vue/src/components/dialog/dialog.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,12 @@ export let Dialog = defineComponent({
7373
unmount: { type: Boolean, default: true },
7474
open: { type: [Boolean, String], default: Missing },
7575
initialFocus: { type: Object as PropType<HTMLElement | null>, default: null },
76-
id: { type: String, default: () => `headlessui-dialog-${useId()}` },
76+
id: { type: String, default: null },
7777
role: { type: String as PropType<'dialog' | 'alertdialog'>, default: 'dialog' },
7878
},
7979
emits: { close: (_close: boolean) => true },
8080
setup(props, { emit, attrs, slots, expose }) {
81+
let id = props.id ?? `headlessui-dialog-${useId()}`
8182
let ready = ref(false)
8283
onMounted(() => {
8384
ready.value = true
@@ -292,7 +293,7 @@ export let Dialog = defineComponent({
292293
})
293294

294295
return () => {
295-
let { id, open: _, initialFocus, ...theirProps } = props
296+
let { open: _, initialFocus, ...theirProps } = props
296297
let ourProps = {
297298
// Manually passthrough the attributes, because Vue can't automatically pass
298299
// it to the underlying div because of all the wrapper components below.
@@ -352,9 +353,10 @@ export let DialogOverlay = defineComponent({
352353
name: 'DialogOverlay',
353354
props: {
354355
as: { type: [Object, String], default: 'div' },
355-
id: { type: String, default: () => `headlessui-dialog-overlay-${useId()}` },
356+
id: { type: String, default: null },
356357
},
357358
setup(props, { attrs, slots }) {
359+
let id = props.id ?? `headlessui-dialog-overlay-${useId()}`
358360
let api = useDialogContext('DialogOverlay')
359361

360362
function handleClick(event: MouseEvent) {
@@ -365,7 +367,7 @@ export let DialogOverlay = defineComponent({
365367
}
366368

367369
return () => {
368-
let { id, ...theirProps } = props
370+
let { ...theirProps } = props
369371
let ourProps = {
370372
id,
371373
'aria-hidden': true,
@@ -390,10 +392,11 @@ export let DialogBackdrop = defineComponent({
390392
name: 'DialogBackdrop',
391393
props: {
392394
as: { type: [Object, String], default: 'div' },
393-
id: { type: String, default: () => `headlessui-dialog-backdrop-${useId()}` },
395+
id: { type: String, default: null },
394396
},
395397
inheritAttrs: false,
396398
setup(props, { attrs, slots, expose }) {
399+
let id = props.id ?? `headlessui-dialog-backdrop-${useId()}`
397400
let api = useDialogContext('DialogBackdrop')
398401
let internalBackdropRef = ref(null)
399402

@@ -408,7 +411,7 @@ export let DialogBackdrop = defineComponent({
408411
})
409412

410413
return () => {
411-
let { id, ...theirProps } = props
414+
let { ...theirProps } = props
412415
let ourProps = {
413416
id,
414417
ref: internalBackdropRef,
@@ -437,9 +440,10 @@ export let DialogPanel = defineComponent({
437440
name: 'DialogPanel',
438441
props: {
439442
as: { type: [Object, String], default: 'div' },
440-
id: { type: String, default: () => `headlessui-dialog-panel-${useId()}` },
443+
id: { type: String, default: null },
441444
},
442445
setup(props, { attrs, slots, expose }) {
446+
let id = props.id ?? `headlessui-dialog-panel-${useId()}`
443447
let api = useDialogContext('DialogPanel')
444448

445449
expose({ el: api.panelRef, $el: api.panelRef })
@@ -449,7 +453,7 @@ export let DialogPanel = defineComponent({
449453
}
450454

451455
return () => {
452-
let { id, ...theirProps } = props
456+
let { ...theirProps } = props
453457
let ourProps = {
454458
id,
455459
ref: api.panelRef,
@@ -474,18 +478,19 @@ export let DialogTitle = defineComponent({
474478
name: 'DialogTitle',
475479
props: {
476480
as: { type: [Object, String], default: 'h2' },
477-
id: { type: String, default: () => `headlessui-dialog-title-${useId()}` },
481+
id: { type: String, default: null },
478482
},
479483
setup(props, { attrs, slots }) {
484+
let id = props.id ?? `headlessui-dialog-title-${useId()}`
480485
let api = useDialogContext('DialogTitle')
481486

482487
onMounted(() => {
483-
api.setTitleId(props.id)
488+
api.setTitleId(id)
484489
onUnmounted(() => api.setTitleId(null))
485490
})
486491

487492
return () => {
488-
let { id, ...theirProps } = props
493+
let { ...theirProps } = props
489494
let ourProps = { id }
490495

491496
return render({

packages/@headlessui-vue/src/components/label/label.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,17 @@ export let Label = defineComponent({
6565
props: {
6666
as: { type: [Object, String], default: 'label' },
6767
passive: { type: [Boolean], default: false },
68-
id: { type: String, default: () => `headlessui-label-${useId()}` },
68+
id: { type: String, default: null },
6969
},
7070
setup(myProps, { slots, attrs }) {
71+
let id = myProps.id ?? `headlessui-label-${useId()}`
7172
let context = useLabelContext()
7273

73-
onMounted(() => onUnmounted(context.register(myProps.id)))
74+
onMounted(() => onUnmounted(context.register(id)))
7475

7576
return () => {
7677
let { name = 'Label', slot = {}, props = {} } = context
77-
let { id, passive, ...theirProps } = myProps
78+
let { id: _id, passive, ...theirProps } = myProps
7879
let ourProps = {
7980
...Object.entries(props).reduce(
8081
(acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }),

packages/@headlessui-vue/src/components/listbox/listbox.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -426,9 +426,10 @@ export let ListboxLabel = defineComponent({
426426
name: 'ListboxLabel',
427427
props: {
428428
as: { type: [Object, String], default: 'label' },
429-
id: { type: String, default: () => `headlessui-listbox-label-${useId()}` },
429+
id: { type: String, default: null },
430430
},
431431
setup(props, { attrs, slots }) {
432+
let id = props.id ?? `headlessui-listbox-label-${useId()}`
432433
let api = useListboxContext('ListboxLabel')
433434

434435
function handleClick() {
@@ -440,7 +441,7 @@ export let ListboxLabel = defineComponent({
440441
open: api.listboxState.value === ListboxStates.Open,
441442
disabled: api.disabled.value,
442443
}
443-
let { id, ...theirProps } = props
444+
let { ...theirProps } = props
444445
let ourProps = { id, ref: api.labelRef, onClick: handleClick }
445446

446447
return render({
@@ -461,9 +462,10 @@ export let ListboxButton = defineComponent({
461462
name: 'ListboxButton',
462463
props: {
463464
as: { type: [Object, String], default: 'button' },
464-
id: { type: String, default: () => `headlessui-listbox-button-${useId()}` },
465+
id: { type: String, default: null },
465466
},
466467
setup(props, { attrs, slots, expose }) {
468+
let id = props.id ?? `headlessui-listbox-button-${useId()}`
467469
let api = useListboxContext('ListboxButton')
468470

469471
expose({ el: api.buttonRef, $el: api.buttonRef })
@@ -529,7 +531,7 @@ export let ListboxButton = defineComponent({
529531
value: api.value.value,
530532
}
531533

532-
let { id, ...theirProps } = props
534+
let { ...theirProps } = props
533535
let ourProps = {
534536
ref: api.buttonRef,
535537
id,
@@ -564,9 +566,10 @@ export let ListboxOptions = defineComponent({
564566
as: { type: [Object, String], default: 'ul' },
565567
static: { type: Boolean, default: false },
566568
unmount: { type: Boolean, default: true },
567-
id: { type: String, default: () => `headlessui-listbox-options-${useId()}` },
569+
id: { type: String, default: null },
568570
},
569571
setup(props, { attrs, slots, expose }) {
572+
let id = props.id ?? `headlessui-listbox-options-${useId()}`
570573
let api = useListboxContext('ListboxOptions')
571574
let searchDebounce = ref<ReturnType<typeof setTimeout> | null>(null)
572575

@@ -656,7 +659,7 @@ export let ListboxOptions = defineComponent({
656659

657660
return () => {
658661
let slot = { open: api.listboxState.value === ListboxStates.Open }
659-
let { id, ...theirProps } = props
662+
let { ...theirProps } = props
660663
let ourProps = {
661664
'aria-activedescendant':
662665
api.activeOptionIndex.value === null
@@ -696,17 +699,18 @@ export let ListboxOption = defineComponent({
696699
>,
697700
},
698701
disabled: { type: Boolean, default: false },
699-
id: { type: String, default: () => `headlessui-listbox.option-${useId()}` },
702+
id: { type: String, default: null },
700703
},
701704
setup(props, { slots, attrs, expose }) {
705+
let id = props.id ?? `headlessui-listbox-option-${useId()}`
702706
let api = useListboxContext('ListboxOption')
703707
let internalOptionRef = ref<HTMLElement | null>(null)
704708

705709
expose({ el: internalOptionRef, $el: internalOptionRef })
706710

707711
let active = computed(() => {
708712
return api.activeOptionIndex.value !== null
709-
? api.options.value[api.activeOptionIndex.value].id === props.id
713+
? api.options.value[api.activeOptionIndex.value].id === id
710714
: false
711715
})
712716

@@ -727,7 +731,7 @@ export let ListboxOption = defineComponent({
727731
return (
728732
api.options.value.find((option) =>
729733
currentValues.some((value) => api.compare(toRaw(value), toRaw(option.dataRef.value)))
730-
)?.id === props.id
734+
)?.id === id
731735
)
732736
},
733737
[ValueMode.Single]: () => selected.value,
@@ -744,8 +748,8 @@ export let ListboxOption = defineComponent({
744748
domRef: internalOptionRef,
745749
}))
746750

747-
onMounted(() => api.registerOption(props.id, dataRef))
748-
onUnmounted(() => api.unregisterOption(props.id))
751+
onMounted(() => api.registerOption(id, dataRef))
752+
onUnmounted(() => api.unregisterOption(id))
749753

750754
onMounted(() => {
751755
watch(
@@ -756,10 +760,10 @@ export let ListboxOption = defineComponent({
756760

757761
match(api.mode.value, {
758762
[ValueMode.Multi]: () => {
759-
if (isFirstSelected.value) api.goToOption(Focus.Specific, props.id)
763+
if (isFirstSelected.value) api.goToOption(Focus.Specific, id)
760764
},
761765
[ValueMode.Single]: () => {
762-
api.goToOption(Focus.Specific, props.id)
766+
api.goToOption(Focus.Specific, id)
763767
},
764768
})
765769
},
@@ -785,7 +789,7 @@ export let ListboxOption = defineComponent({
785789

786790
function handleFocus() {
787791
if (props.disabled) return api.goToOption(Focus.Nothing)
788-
api.goToOption(Focus.Specific, props.id)
792+
api.goToOption(Focus.Specific, id)
789793
}
790794

791795
let pointer = useTrackedPointer()
@@ -798,7 +802,7 @@ export let ListboxOption = defineComponent({
798802
if (!pointer.wasMoved(evt)) return
799803
if (props.disabled) return
800804
if (active.value) return
801-
api.goToOption(Focus.Specific, props.id, ActivationTrigger.Pointer)
805+
api.goToOption(Focus.Specific, id, ActivationTrigger.Pointer)
802806
}
803807

804808
function handleLeave(evt: PointerEvent) {
@@ -811,7 +815,7 @@ export let ListboxOption = defineComponent({
811815
return () => {
812816
let { disabled } = props
813817
let slot = { active: active.value, selected: selected.value, disabled }
814-
let { id, value: _value, disabled: _disabled, ...theirProps } = props
818+
let { value: _value, disabled: _disabled, ...theirProps } = props
815819
let ourProps = {
816820
id,
817821
ref: internalOptionRef,

0 commit comments

Comments
 (0)