Skip to content

Commit

Permalink
Allow users customize ID generation (#2959)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
thecrypticace authored Feb 2, 2024
1 parent 7c6fa3d commit 9a24198
Show file tree
Hide file tree
Showing 14 changed files with 168 additions and 80 deletions.
15 changes: 9 additions & 6 deletions packages/@headlessui-vue/src/components/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -779,9 +779,10 @@ export let ComboboxLabel = defineComponent({
name: 'ComboboxLabel',
props: {
as: { type: [Object, String], default: 'label' },
id: { type: String, default: () => `headlessui-combobox-label-${useId()}` },
id: { type: String, default: null },
},
setup(props, { attrs, slots }) {
let id = props.id ?? `headlessui-combobox-label-${useId()}`
let api = useComboboxContext('ComboboxLabel')

function handleClick() {
Expand All @@ -794,7 +795,7 @@ export let ComboboxLabel = defineComponent({
disabled: api.disabled.value,
}

let { id, ...theirProps } = props
let { ...theirProps } = props
let ourProps = { id, ref: api.labelRef, onClick: handleClick }

return render({
Expand All @@ -815,9 +816,10 @@ export let ComboboxButton = defineComponent({
name: 'ComboboxButton',
props: {
as: { type: [Object, String], default: 'button' },
id: { type: String, default: () => `headlessui-combobox-button-${useId()}` },
id: { type: String, default: null },
},
setup(props, { attrs, slots, expose }) {
let id = props.id ?? `headlessui-combobox-button-${useId()}`
let api = useComboboxContext('ComboboxButton')

expose({ el: api.buttonRef, $el: api.buttonRef })
Expand Down Expand Up @@ -884,7 +886,7 @@ export let ComboboxButton = defineComponent({
disabled: api.disabled.value,
value: api.value.value,
}
let { id, ...theirProps } = props
let { ...theirProps } = props
let ourProps = {
ref: api.buttonRef,
id,
Expand Down Expand Up @@ -921,12 +923,13 @@ export let ComboboxInput = defineComponent({
unmount: { type: Boolean, default: true },
displayValue: { type: Function as PropType<(item: unknown) => string> },
defaultValue: { type: String, default: undefined },
id: { type: String, default: () => `headlessui-combobox-input-${useId()}` },
id: { type: String, default: null },
},
emits: {
change: (_value: Event & { target: HTMLInputElement }) => true,
},
setup(props, { emit, attrs, slots, expose }) {
let id = props.id ?? `headlessui-combobox-input-${useId()}`
let api = useComboboxContext('ComboboxInput')
let ownerDocument = computed(() => getOwnerDocument(dom(api.inputRef)))

Expand Down Expand Up @@ -1284,7 +1287,7 @@ export let ComboboxInput = defineComponent({

return () => {
let slot = { open: api.comboboxState.value === ComboboxStates.Open }
let { id, displayValue, onChange: _onChange, ...theirProps } = props
let { displayValue, onChange: _onChange, ...theirProps } = props
let ourProps = {
'aria-controls': api.optionsRef.value?.id,
'aria-expanded': api.comboboxState.value === ComboboxStates.Open,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,18 @@ export let Description = defineComponent({
name: 'Description',
props: {
as: { type: [Object, String], default: 'p' },
id: { type: String, default: () => `headlessui-description-${useId()}` },
id: { type: String, default: null },
},
setup(myProps, { attrs, slots }) {
let id = myProps.id ?? `headlessui-description-${useId()}`
let context = useDescriptionContext()

onMounted(() => onUnmounted(context.register(myProps.id)))
onMounted(() => onUnmounted(context.register(id)))

return () => {
let { name = 'Description', slot = ref({}), props = {} } = context
let { id, ...theirProps } = myProps
let { id: _id, ...theirProps } = myProps

let ourProps = {
...Object.entries(props).reduce(
(acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }),
Expand Down
27 changes: 16 additions & 11 deletions packages/@headlessui-vue/src/components/dialog/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,12 @@ export let Dialog = defineComponent({
unmount: { type: Boolean, default: true },
open: { type: [Boolean, String], default: Missing },
initialFocus: { type: Object as PropType<HTMLElement | null>, default: null },
id: { type: String, default: () => `headlessui-dialog-${useId()}` },
id: { type: String, default: null },
role: { type: String as PropType<'dialog' | 'alertdialog'>, default: 'dialog' },
},
emits: { close: (_close: boolean) => true },
setup(props, { emit, attrs, slots, expose }) {
let id = props.id ?? `headlessui-dialog-${useId()}`
let ready = ref(false)
onMounted(() => {
ready.value = true
Expand Down Expand Up @@ -292,7 +293,7 @@ export let Dialog = defineComponent({
})

return () => {
let { id, open: _, initialFocus, ...theirProps } = props
let { open: _, initialFocus, ...theirProps } = props
let ourProps = {
// Manually passthrough the attributes, because Vue can't automatically pass
// it to the underlying div because of all the wrapper components below.
Expand Down Expand Up @@ -352,9 +353,10 @@ export let DialogOverlay = defineComponent({
name: 'DialogOverlay',
props: {
as: { type: [Object, String], default: 'div' },
id: { type: String, default: () => `headlessui-dialog-overlay-${useId()}` },
id: { type: String, default: null },
},
setup(props, { attrs, slots }) {
let id = props.id ?? `headlessui-dialog-overlay-${useId()}`
let api = useDialogContext('DialogOverlay')

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

return () => {
let { id, ...theirProps } = props
let { ...theirProps } = props
let ourProps = {
id,
'aria-hidden': true,
Expand All @@ -390,10 +392,11 @@ export let DialogBackdrop = defineComponent({
name: 'DialogBackdrop',
props: {
as: { type: [Object, String], default: 'div' },
id: { type: String, default: () => `headlessui-dialog-backdrop-${useId()}` },
id: { type: String, default: null },
},
inheritAttrs: false,
setup(props, { attrs, slots, expose }) {
let id = props.id ?? `headlessui-dialog-backdrop-${useId()}`
let api = useDialogContext('DialogBackdrop')
let internalBackdropRef = ref(null)

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

return () => {
let { id, ...theirProps } = props
let { ...theirProps } = props
let ourProps = {
id,
ref: internalBackdropRef,
Expand Down Expand Up @@ -437,9 +440,10 @@ export let DialogPanel = defineComponent({
name: 'DialogPanel',
props: {
as: { type: [Object, String], default: 'div' },
id: { type: String, default: () => `headlessui-dialog-panel-${useId()}` },
id: { type: String, default: null },
},
setup(props, { attrs, slots, expose }) {
let id = props.id ?? `headlessui-dialog-panel-${useId()}`
let api = useDialogContext('DialogPanel')

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

return () => {
let { id, ...theirProps } = props
let { ...theirProps } = props
let ourProps = {
id,
ref: api.panelRef,
Expand All @@ -474,18 +478,19 @@ export let DialogTitle = defineComponent({
name: 'DialogTitle',
props: {
as: { type: [Object, String], default: 'h2' },
id: { type: String, default: () => `headlessui-dialog-title-${useId()}` },
id: { type: String, default: null },
},
setup(props, { attrs, slots }) {
let id = props.id ?? `headlessui-dialog-title-${useId()}`
let api = useDialogContext('DialogTitle')

onMounted(() => {
api.setTitleId(props.id)
api.setTitleId(id)
onUnmounted(() => api.setTitleId(null))
})

return () => {
let { id, ...theirProps } = props
let { ...theirProps } = props
let ourProps = { id }

return render({
Expand Down
7 changes: 4 additions & 3 deletions packages/@headlessui-vue/src/components/label/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,17 @@ export let Label = defineComponent({
props: {
as: { type: [Object, String], default: 'label' },
passive: { type: [Boolean], default: false },
id: { type: String, default: () => `headlessui-label-${useId()}` },
id: { type: String, default: null },
},
setup(myProps, { slots, attrs }) {
let id = myProps.id ?? `headlessui-label-${useId()}`
let context = useLabelContext()

onMounted(() => onUnmounted(context.register(myProps.id)))
onMounted(() => onUnmounted(context.register(id)))

return () => {
let { name = 'Label', slot = {}, props = {} } = context
let { id, passive, ...theirProps } = myProps
let { id: _id, passive, ...theirProps } = myProps
let ourProps = {
...Object.entries(props).reduce(
(acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }),
Expand Down
36 changes: 20 additions & 16 deletions packages/@headlessui-vue/src/components/listbox/listbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,9 +426,10 @@ export let ListboxLabel = defineComponent({
name: 'ListboxLabel',
props: {
as: { type: [Object, String], default: 'label' },
id: { type: String, default: () => `headlessui-listbox-label-${useId()}` },
id: { type: String, default: null },
},
setup(props, { attrs, slots }) {
let id = props.id ?? `headlessui-listbox-label-${useId()}`
let api = useListboxContext('ListboxLabel')

function handleClick() {
Expand All @@ -440,7 +441,7 @@ export let ListboxLabel = defineComponent({
open: api.listboxState.value === ListboxStates.Open,
disabled: api.disabled.value,
}
let { id, ...theirProps } = props
let { ...theirProps } = props
let ourProps = { id, ref: api.labelRef, onClick: handleClick }

return render({
Expand All @@ -461,9 +462,10 @@ export let ListboxButton = defineComponent({
name: 'ListboxButton',
props: {
as: { type: [Object, String], default: 'button' },
id: { type: String, default: () => `headlessui-listbox-button-${useId()}` },
id: { type: String, default: null },
},
setup(props, { attrs, slots, expose }) {
let id = props.id ?? `headlessui-listbox-button-${useId()}`
let api = useListboxContext('ListboxButton')

expose({ el: api.buttonRef, $el: api.buttonRef })
Expand Down Expand Up @@ -529,7 +531,7 @@ export let ListboxButton = defineComponent({
value: api.value.value,
}

let { id, ...theirProps } = props
let { ...theirProps } = props
let ourProps = {
ref: api.buttonRef,
id,
Expand Down Expand Up @@ -564,9 +566,10 @@ export let ListboxOptions = defineComponent({
as: { type: [Object, String], default: 'ul' },
static: { type: Boolean, default: false },
unmount: { type: Boolean, default: true },
id: { type: String, default: () => `headlessui-listbox-options-${useId()}` },
id: { type: String, default: null },
},
setup(props, { attrs, slots, expose }) {
let id = props.id ?? `headlessui-listbox-options-${useId()}`
let api = useListboxContext('ListboxOptions')
let searchDebounce = ref<ReturnType<typeof setTimeout> | null>(null)

Expand Down Expand Up @@ -656,7 +659,7 @@ export let ListboxOptions = defineComponent({

return () => {
let slot = { open: api.listboxState.value === ListboxStates.Open }
let { id, ...theirProps } = props
let { ...theirProps } = props
let ourProps = {
'aria-activedescendant':
api.activeOptionIndex.value === null
Expand Down Expand Up @@ -696,17 +699,18 @@ export let ListboxOption = defineComponent({
>,
},
disabled: { type: Boolean, default: false },
id: { type: String, default: () => `headlessui-listbox.option-${useId()}` },
id: { type: String, default: null },
},
setup(props, { slots, attrs, expose }) {
let id = props.id ?? `headlessui-listbox-option-${useId()}`
let api = useListboxContext('ListboxOption')
let internalOptionRef = ref<HTMLElement | null>(null)

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

let active = computed(() => {
return api.activeOptionIndex.value !== null
? api.options.value[api.activeOptionIndex.value].id === props.id
? api.options.value[api.activeOptionIndex.value].id === id
: false
})

Expand All @@ -727,7 +731,7 @@ export let ListboxOption = defineComponent({
return (
api.options.value.find((option) =>
currentValues.some((value) => api.compare(toRaw(value), toRaw(option.dataRef.value)))
)?.id === props.id
)?.id === id
)
},
[ValueMode.Single]: () => selected.value,
Expand All @@ -744,8 +748,8 @@ export let ListboxOption = defineComponent({
domRef: internalOptionRef,
}))

onMounted(() => api.registerOption(props.id, dataRef))
onUnmounted(() => api.unregisterOption(props.id))
onMounted(() => api.registerOption(id, dataRef))
onUnmounted(() => api.unregisterOption(id))

onMounted(() => {
watch(
Expand All @@ -756,10 +760,10 @@ export let ListboxOption = defineComponent({

match(api.mode.value, {
[ValueMode.Multi]: () => {
if (isFirstSelected.value) api.goToOption(Focus.Specific, props.id)
if (isFirstSelected.value) api.goToOption(Focus.Specific, id)
},
[ValueMode.Single]: () => {
api.goToOption(Focus.Specific, props.id)
api.goToOption(Focus.Specific, id)
},
})
},
Expand All @@ -785,7 +789,7 @@ export let ListboxOption = defineComponent({

function handleFocus() {
if (props.disabled) return api.goToOption(Focus.Nothing)
api.goToOption(Focus.Specific, props.id)
api.goToOption(Focus.Specific, id)
}

let pointer = useTrackedPointer()
Expand All @@ -798,7 +802,7 @@ export let ListboxOption = defineComponent({
if (!pointer.wasMoved(evt)) return
if (props.disabled) return
if (active.value) return
api.goToOption(Focus.Specific, props.id, ActivationTrigger.Pointer)
api.goToOption(Focus.Specific, id, ActivationTrigger.Pointer)
}

function handleLeave(evt: PointerEvent) {
Expand All @@ -811,7 +815,7 @@ export let ListboxOption = defineComponent({
return () => {
let { disabled } = props
let slot = { active: active.value, selected: selected.value, disabled }
let { id, value: _value, disabled: _disabled, ...theirProps } = props
let { value: _value, disabled: _disabled, ...theirProps } = props
let ourProps = {
id,
ref: internalOptionRef,
Expand Down
Loading

0 comments on commit 9a24198

Please sign in to comment.