From 89fd4b202e3be6422d119e2e6627da3c2fa13601 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 27 Jan 2022 13:49:26 -0500 Subject: [PATCH] Update minimum Vue to 3.2 (#1072) * Remove vercel json file * Don't use provide/inject outside of setup * Upgrade minimum vue version * Mark vue as an external * Update lockfile * WIP move render functions into setup * WIP * WIP * Use setup returning render fns for tests --- packages/@headlessui-vue/package.json | 8 +- .../src/components/combobox/combobox.ts | 236 +++++----- .../description/description.test.ts | 61 ++- .../src/components/description/description.ts | 41 +- .../src/components/dialog/dialog.test.ts | 87 ++-- .../src/components/dialog/dialog.ts | 166 +++---- .../src/components/disclosure/disclosure.ts | 188 ++++---- .../src/components/focus-trap/focus-trap.ts | 29 +- .../src/components/label/label.test.ts | 59 ++- .../src/components/label/label.ts | 51 +- .../src/components/listbox/listbox.ts | 174 ++++--- .../src/components/menu/menu.ts | 123 +++-- .../src/components/popover/popover.ts | 442 +++++++++--------- .../src/components/radio-group/radio-group.ts | 157 +++---- .../src/components/switch/switch.ts | 88 ++-- .../src/components/tabs/tabs.ts | 113 ++--- .../src/components/transitions/transition.ts | 163 ++++--- packages/@headlessui-vue/vercel.json | 3 - .../src/components/menu/menu.vue | 2 +- yarn.lock | 101 ++++ 20 files changed, 1124 insertions(+), 1168 deletions(-) delete mode 100644 packages/@headlessui-vue/vercel.json diff --git a/packages/@headlessui-vue/package.json b/packages/@headlessui-vue/package.json index f6c49d6..6d26c21 100644 --- a/packages/@headlessui-vue/package.json +++ b/packages/@headlessui-vue/package.json @@ -33,19 +33,19 @@ }, "scripts": { "prepublishOnly": "npm run build", - "build": "../../scripts/build.sh", - "watch": "../../scripts/watch.sh", + "build": "../../scripts/build.sh --external:vue", + "watch": "../../scripts/watch.sh --external:vue", "test": "../../scripts/test.sh", "lint": "../../scripts/lint.sh", "playground": "yarn workspace playground-vue dev", "clean": "rimraf ./dist" }, "peerDependencies": { - "vue": "^3.0.0" + "vue": "^3.2.0" }, "devDependencies": { "@testing-library/vue": "^5.8.2", "@vue/test-utils": "^2.0.0-rc.18", - "vue": "^3.2.27" + "vue": "^3.2.29" } } diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts index 4bb3d67..d8c9321 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts @@ -256,33 +256,29 @@ export let Combobox = defineComponent({ export let ComboboxLabel = defineComponent({ name: 'ComboboxLabel', props: { as: { type: [Object, String], default: 'label' } }, - render() { - let api = useComboboxContext('ComboboxLabel') - - let slot = { - open: api.ComboboxState.value === ComboboxStates.Open, - disabled: api.disabled.value, - } - let propsWeControl = { id: this.id, ref: 'el', onClick: this.handleClick } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - name: 'ComboboxLabel', - }) - }, - setup() { + setup(props, { attrs, slots }) { let api = useComboboxContext('ComboboxLabel') let id = `headlessui-combobox-label-${useId()}` - return { - id, - el: api.labelRef, - handleClick() { - dom(api.inputRef)?.focus({ preventScroll: true }) - }, + function handleClick() { + dom(api.inputRef)?.focus({ preventScroll: true }) + } + + return () => { + let slot = { + open: api.ComboboxState.value === ComboboxStates.Open, + disabled: api.disabled.value, + } + + let propsWeControl = { id, ref: api.labelRef, onClick: handleClick } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs, + slots, + name: 'ComboboxLabel', + }) } }, }) @@ -294,40 +290,7 @@ export let ComboboxButton = defineComponent({ props: { as: { type: [Object, String], default: 'button' }, }, - render() { - let api = useComboboxContext('ComboboxButton') - - let slot = { - open: api.ComboboxState.value === ComboboxStates.Open, - disabled: api.disabled.value, - } - let propsWeControl = { - ref: 'el', - id: this.id, - type: this.type, - tabindex: '-1', - 'aria-haspopup': true, - 'aria-controls': dom(api.optionsRef)?.id, - 'aria-expanded': api.disabled.value - ? undefined - : api.ComboboxState.value === ComboboxStates.Open, - 'aria-labelledby': api.labelRef.value - ? [dom(api.labelRef)?.id, this.id].join(' ') - : undefined, - disabled: api.disabled.value === true ? true : undefined, - onKeydown: this.handleKeydown, - onClick: this.handleClick, - } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - name: 'ComboboxButton', - }) - }, - setup(props, { attrs }) { + setup(props, { attrs, slots }) { let api = useComboboxContext('ComboboxButton') let id = `headlessui-combobox-button-${useId()}` @@ -394,16 +357,39 @@ export let ComboboxButton = defineComponent({ } } - return { - api, - id, - el: api.buttonRef, - type: useResolveButtonType( - computed(() => ({ as: props.as, type: attrs.type })), - api.buttonRef - ), - handleClick, - handleKeydown, + let type = useResolveButtonType( + computed(() => ({ as: props.as, type: attrs.type })), + api.buttonRef + ) + + return () => { + let slot = { + open: api.ComboboxState.value === ComboboxStates.Open, + disabled: api.disabled.value, + } + let propsWeControl = { + ref: api.buttonRef, + id, + type: type.value, + tabindex: '-1', + 'aria-haspopup': true, + 'aria-controls': dom(api.optionsRef)?.id, + 'aria-expanded': api.disabled.value + ? undefined + : api.ComboboxState.value === ComboboxStates.Open, + 'aria-labelledby': api.labelRef.value ? [dom(api.labelRef)?.id, id].join(' ') : undefined, + disabled: api.disabled.value === true ? true : undefined, + onKeydown: handleKeydown, + onClick: handleClick, + } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs, + slots, + name: 'ComboboxButton', + }) } }, }) @@ -421,36 +407,7 @@ export let ComboboxInput = defineComponent({ emits: { change: (_value: Event & { target: HTMLInputElement }) => true, }, - render() { - let api = useComboboxContext('ComboboxInput') - - let slot = { open: api.ComboboxState.value === ComboboxStates.Open } - let propsWeControl = { - 'aria-activedescendant': - api.activeOptionIndex.value === null - ? undefined - : api.options.value[api.activeOptionIndex.value]?.id, - 'aria-labelledby': dom(api.labelRef)?.id ?? dom(api.buttonRef)?.id, - 'aria-orientation': api.orientation.value, - id: this.id, - onKeydown: this.handleKeyDown, - onChange: this.handleChange, - role: 'combobox', - tabIndex: 0, - ref: 'el', - } - let passThroughProps = this.$props - - return render({ - props: { ...passThroughProps, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - features: Features.RenderStrategy | Features.Static, - name: 'ComboboxInput', - }) - }, - setup(props, { emit }) { + setup(props, { emit, attrs, slots }) { let api = useComboboxContext('ComboboxInput') let id = `headlessui-combobox-input-${useId()}` api.inputPropsRef = computed(() => props) @@ -530,7 +487,33 @@ export let ComboboxInput = defineComponent({ emit('change', event) } - return { id, el: api.inputRef, handleKeyDown, handleChange } + return () => { + let slot = { open: api.ComboboxState.value === ComboboxStates.Open } + let propsWeControl = { + 'aria-activedescendant': + api.activeOptionIndex.value === null + ? undefined + : api.options.value[api.activeOptionIndex.value]?.id, + 'aria-labelledby': dom(api.labelRef)?.id ?? dom(api.buttonRef)?.id, + 'aria-orientation': api.orientation.value, + id, + onKeydown: handleKeyDown, + onChange: handleChange, + role: 'combobox', + tabIndex: 0, + ref: api.inputRef, + } + let passThroughProps = props + + return render({ + props: { ...passThroughProps, ...propsWeControl }, + slot, + attrs, + slots, + features: Features.RenderStrategy | Features.Static, + name: 'ComboboxInput', + }) + } }, }) @@ -543,34 +526,7 @@ export let ComboboxOptions = defineComponent({ static: { type: Boolean, default: false }, unmount: { type: Boolean, default: true }, }, - render() { - let api = useComboboxContext('ComboboxOptions') - - let slot = { open: api.ComboboxState.value === ComboboxStates.Open } - let propsWeControl = { - 'aria-activedescendant': - api.activeOptionIndex.value === null - ? undefined - : api.options.value[api.activeOptionIndex.value]?.id, - 'aria-labelledby': dom(api.labelRef)?.id ?? dom(api.buttonRef)?.id, - 'aria-orientation': api.orientation.value, - id: this.id, - ref: 'el', - role: 'listbox', - } - let passThroughProps = this.$props - - return render({ - props: { ...passThroughProps, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - features: Features.RenderStrategy | Features.Static, - visible: this.visible, - name: 'ComboboxOptions', - }) - }, - setup() { + setup(props, { attrs, slots }) { let api = useComboboxContext('ComboboxOptions') let id = `headlessui-combobox-options-${useId()}` @@ -583,7 +539,31 @@ export let ComboboxOptions = defineComponent({ return api.ComboboxState.value === ComboboxStates.Open }) - return { id, el: api.optionsRef, visible } + return () => { + let slot = { open: api.ComboboxState.value === ComboboxStates.Open } + let propsWeControl = { + 'aria-activedescendant': + api.activeOptionIndex.value === null + ? undefined + : api.options.value[api.activeOptionIndex.value]?.id, + 'aria-labelledby': dom(api.labelRef)?.id ?? dom(api.buttonRef)?.id, + 'aria-orientation': api.orientation.value, + id, + ref: api.optionsRef, + role: 'listbox', + } + let passThroughProps = props + + return render({ + props: { ...passThroughProps, ...propsWeControl }, + slot, + attrs, + slots, + features: Features.RenderStrategy | Features.Static, + visible: visible.value, + name: 'ComboboxOptions', + }) + } }, }) diff --git a/packages/@headlessui-vue/src/components/description/description.test.ts b/packages/@headlessui-vue/src/components/description/description.test.ts index 65e6722..fdedf82 100644 --- a/packages/@headlessui-vue/src/components/description/description.test.ts +++ b/packages/@headlessui-vue/src/components/description/description.test.ts @@ -27,12 +27,11 @@ it('should be possible to use useDescriptions without using a Description', asyn let { container } = render( defineComponent({ components: { Description }, - render() { - return h('div', [h('div', { 'aria-describedby': this.describedby }, ['No description'])]) - }, setup() { let describedby = useDescriptions() - return { describedby } + + return () => + h('div', [h('div', { 'aria-describedby': describedby.value }, ['No description'])]) }, }) ) @@ -50,17 +49,16 @@ it('should be possible to use useDescriptions and a single Description, and have let { container } = render( defineComponent({ components: { Description }, - render() { - return h('div', [ - h('div', { 'aria-describedby': this.describedby }, [ - h(Description, () => 'I am a description'), - h('span', 'Contents'), - ]), - ]) - }, setup() { let describedby = useDescriptions() - return { describedby } + + return () => + h('div', [ + h('div', { 'aria-describedby': describedby.value }, [ + h(Description, () => 'I am a description'), + h('span', 'Contents'), + ]), + ]) }, }) ) @@ -83,18 +81,17 @@ it('should be possible to use useDescriptions and multiple Description component let { container } = render( defineComponent({ components: { Description }, - render() { - return h('div', [ - h('div', { 'aria-describedby': this.describedby }, [ - h(Description, () => 'I am a description'), - h('span', 'Contents'), - h(Description, () => 'I am also a description'), - ]), - ]) - }, setup() { let describedby = useDescriptions() - return { describedby } + + return () => + h('div', [ + h('div', { 'aria-describedby': describedby.value }, [ + h(Description, () => 'I am a description'), + h('span', 'Contents'), + h(Description, () => 'I am also a description'), + ]), + ]) }, }) ) @@ -118,18 +115,18 @@ it('should be possible to update a prop from the parent and it should reflect in let { container } = render( defineComponent({ components: { Description }, - render() { - return h('div', [ - h('div', { 'aria-describedby': this.describedby }, [ - h(Description, () => 'I am a description'), - h('button', { onClick: () => this.count++ }, '+1'), - ]), - ]) - }, setup() { let count = ref(0) let describedby = useDescriptions({ props: { 'data-count': count } }) - return { count, describedby } + + return () => { + return h('div', [ + h('div', { 'aria-describedby': describedby.value }, [ + h(Description, () => 'I am a description'), + h('button', { onClick: () => count.value++ }, '+1'), + ]), + ]) + } }, }) ) diff --git a/packages/@headlessui-vue/src/components/description/description.ts b/packages/@headlessui-vue/src/components/description/description.ts index 80f8ac9..6ae3423 100644 --- a/packages/@headlessui-vue/src/components/description/description.ts +++ b/packages/@headlessui-vue/src/components/description/description.ts @@ -70,31 +70,30 @@ export let Description = defineComponent({ props: { as: { type: [Object, String], default: 'p' }, }, - render() { - let { name = 'Description', slot = ref({}), props = {} } = this.context - let passThroughProps = this.$props - let propsWeControl = { - ...Object.entries(props).reduce( - (acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }), - {} - ), - id: this.id, - } - - return render({ - props: { ...passThroughProps, ...propsWeControl }, - slot: slot.value, - attrs: this.$attrs, - slots: this.$slots, - name, - }) - }, - setup() { + setup(myProps, { attrs, slots }) { let context = useDescriptionContext() let id = `headlessui-description-${useId()}` onMounted(() => onUnmounted(context.register(id))) - return { id, context } + return () => { + let { name = 'Description', slot = ref({}), props = {} } = context + let passThroughProps = myProps + let propsWeControl = { + ...Object.entries(props).reduce( + (acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }), + {} + ), + id, + } + + return render({ + props: { ...passThroughProps, ...propsWeControl }, + slot: slot.value, + attrs, + slots, + name, + }) + } }, }) diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.test.ts b/packages/@headlessui-vue/src/components/dialog/dialog.test.ts index 94e8b83..a54a50f 100644 --- a/packages/@headlessui-vue/src/components/dialog/dialog.test.ts +++ b/packages/@headlessui-vue/src/components/dialog/dialog.test.ts @@ -935,57 +935,46 @@ describe('Nesting', () => { components: { Dialog, DialogOverlay }, emits: ['close'], props: ['level'], - render() { - let level = this.$props.level ?? 1 - return h(Dialog, { open: true, onClose: this.onClose }, () => [ - h(DialogOverlay), - h('div', [ - h('p', `Level: ${level}`), - h( - 'button', - { - onClick: () => { - this.showChild = true - }, - }, - `Open ${level + 1} a` - ), - h( - 'button', - { - onClick: () => { - this.showChild = true - }, - }, - `Open ${level + 1} b` - ), - h( - 'button', - { - onClick: () => { - this.showChild = true - }, - }, - `Open ${level + 1} c` - ), - ]), - this.showChild && - h(Nested, { - onClose: () => { - this.showChild = false - }, - level: level + 1, - }), - ]) - }, - setup(_props, { emit }) { + setup(props, { emit }) { let showChild = ref(false) + function onClose() { + emit('close', false) + } - return { - showChild, - onClose() { - emit('close', false) - }, + return () => { + let level = props.level ?? 1 + return h(Dialog, { open: true, onClose: onClose }, () => [ + h(DialogOverlay), + h('div', [ + h('p', `Level: ${level}`), + h( + 'button', + { + onClick: () => (showChild.value = true), + }, + `Open ${level + 1} a` + ), + h( + 'button', + { + onClick: () => (showChild.value = true), + }, + `Open ${level + 1} b` + ), + h( + 'button', + { + onClick: () => (showChild.value = true), + }, + `Open ${level + 1} c` + ), + ]), + showChild.value && + h(Nested, { + onClose: () => (showChild.value = false), + level: level + 1, + }), + ]) } }, }) diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.ts b/packages/@headlessui-vue/src/components/dialog/dialog.ts index 5fe71c9..057e78a 100644 --- a/packages/@headlessui-vue/src/components/dialog/dialog.ts +++ b/packages/@headlessui-vue/src/components/dialog/dialog.ts @@ -75,42 +75,7 @@ export let Dialog = defineComponent({ initialFocus: { type: Object as PropType, default: null }, }, emits: { close: (_close: boolean) => true }, - render() { - let propsWeControl = { - // Manually passthrough the attributes, because Vue can't automatically pass - // it to the underlying div because of all the wrapper components below. - ...this.$attrs, - ref: 'el', - id: this.id, - role: 'dialog', - 'aria-modal': this.dialogState === DialogStates.Open ? true : undefined, - 'aria-labelledby': this.titleId, - 'aria-describedby': this.describedby, - onClick: this.handleClick, - } - let { open: _, initialFocus, ...passThroughProps } = this.$props - - let slot = { open: this.dialogState === DialogStates.Open } - - return h(ForcePortalRoot, { force: true }, () => - h(Portal, () => - h(PortalGroup, { target: this.dialogRef }, () => - h(ForcePortalRoot, { force: false }, () => - render({ - props: { ...passThroughProps, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - visible: this.visible, - features: Features.RenderStrategy | Features.Static, - name: 'Dialog', - }) - ) - ) - ) - ) - }, - setup(props, { emit }) { + setup(props, { emit, attrs, slots }) { let containers = ref>(new Set()) let usesOpenClosedState = useOpenClosed() @@ -256,19 +221,44 @@ export let Dialog = defineComponent({ onInvalidate(() => observer.disconnect()) }) - return { - id, - el: internalDialogRef, - dialogRef: internalDialogRef, - containers, - dialogState, - titleId, - describedby, - visible, - open, - handleClick(event: MouseEvent) { - event.stopPropagation() - }, + function handleClick(event: MouseEvent) { + event.stopPropagation() + } + + return () => { + let propsWeControl = { + // Manually passthrough the attributes, because Vue can't automatically pass + // it to the underlying div because of all the wrapper components below. + ...attrs, + ref: internalDialogRef, + id, + role: 'dialog', + 'aria-modal': dialogState.value === DialogStates.Open ? true : undefined, + 'aria-labelledby': titleId.value, + 'aria-describedby': describedby.value, + onClick: handleClick, + } + let { open: _, initialFocus, ...passThroughProps } = props + + let slot = { open: dialogState.value === DialogStates.Open } + + return h(ForcePortalRoot, { force: true }, () => + h(Portal, () => + h(PortalGroup, { target: internalDialogRef.value }, () => + h(ForcePortalRoot, { force: false }, () => + render({ + props: { ...passThroughProps, ...propsWeControl }, + slot, + attrs, + slots, + visible: visible.value, + features: Features.RenderStrategy | Features.Static, + name: 'Dialog', + }) + ) + ) + ) + ) } }, }) @@ -280,36 +270,32 @@ export let DialogOverlay = defineComponent({ props: { as: { type: [Object, String], default: 'div' }, }, - render() { - let api = useDialogContext('DialogOverlay') - let propsWeControl = { - ref: 'el', - id: this.id, - 'aria-hidden': true, - onClick: this.handleClick, - } - let passThroughProps = this.$props - - return render({ - props: { ...passThroughProps, ...propsWeControl }, - slot: { open: api.dialogState.value === DialogStates.Open }, - attrs: this.$attrs, - slots: this.$slots, - name: 'DialogOverlay', - }) - }, - setup() { + setup(props, { attrs, slots }) { let api = useDialogContext('DialogOverlay') let id = `headlessui-dialog-overlay-${useId()}` - return { - id, - handleClick(event: MouseEvent) { - if (event.target !== event.currentTarget) return - event.preventDefault() - event.stopPropagation() - api.close() - }, + function handleClick(event: MouseEvent) { + if (event.target !== event.currentTarget) return + event.preventDefault() + event.stopPropagation() + api.close() + } + + return () => { + let propsWeControl = { + id, + 'aria-hidden': true, + onClick: handleClick, + } + let passThroughProps = props + + return render({ + props: { ...passThroughProps, ...propsWeControl }, + slot: { open: api.dialogState.value === DialogStates.Open }, + attrs, + slots, + name: 'DialogOverlay', + }) } }, }) @@ -321,20 +307,7 @@ export let DialogTitle = defineComponent({ props: { as: { type: [Object, String], default: 'h2' }, }, - render() { - let api = useDialogContext('DialogTitle') - let propsWeControl = { id: this.id } - let passThroughProps = this.$props - - return render({ - props: { ...passThroughProps, ...propsWeControl }, - slot: { open: api.dialogState.value === DialogStates.Open }, - attrs: this.$attrs, - slots: this.$slots, - name: 'DialogTitle', - }) - }, - setup() { + setup(props, { attrs, slots }) { let api = useDialogContext('DialogTitle') let id = `headlessui-dialog-title-${useId()}` @@ -343,7 +316,18 @@ export let DialogTitle = defineComponent({ onUnmounted(() => api.setTitleId(null)) }) - return { id } + return () => { + let propsWeControl = { id } + let passThroughProps = props + + return render({ + props: { ...passThroughProps, ...propsWeControl }, + slot: { open: api.dialogState.value === DialogStates.Open }, + attrs, + slots, + name: 'DialogTitle', + }) + } }, }) diff --git a/packages/@headlessui-vue/src/components/disclosure/disclosure.ts b/packages/@headlessui-vue/src/components/disclosure/disclosure.ts index 23fe0a3..2f6c5fe 100644 --- a/packages/@headlessui-vue/src/components/disclosure/disclosure.ts +++ b/packages/@headlessui-vue/src/components/disclosure/disclosure.ts @@ -133,40 +133,7 @@ export let DisclosureButton = defineComponent({ as: { type: [Object, String], default: 'button' }, disabled: { type: [Boolean], default: false }, }, - render() { - let api = useDisclosureContext('DisclosureButton') - - let slot = { open: api.disclosureState.value === DisclosureStates.Open } - let propsWeControl = this.isWithinPanel - ? { - ref: 'el', - type: this.type, - onClick: this.handleClick, - onKeydown: this.handleKeyDown, - } - : { - id: this.id, - ref: 'el', - type: this.type, - 'aria-expanded': this.$props.disabled - ? undefined - : api.disclosureState.value === DisclosureStates.Open, - 'aria-controls': dom(api.panel) ? api.panelId : undefined, - disabled: this.$props.disabled ? true : undefined, - onClick: this.handleClick, - onKeydown: this.handleKeyDown, - onKeyup: this.handleKeyUp, - } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - name: 'DisclosureButton', - }) - }, - setup(props, { attrs }) { + setup(props, { attrs, slots }) { let api = useDisclosureContext('DisclosureButton') let panelContext = useDisclosurePanelContext() @@ -180,58 +147,86 @@ export let DisclosureButton = defineComponent({ }) } - return { - isWithinPanel, - id: api.buttonId, - el: elementRef, - type: useResolveButtonType( - computed(() => ({ as: props.as, type: attrs.type })), - elementRef - ), - handleClick() { - if (props.disabled) return + let type = useResolveButtonType( + computed(() => ({ as: props.as, type: attrs.type })), + elementRef + ) - if (isWithinPanel) { - api.toggleDisclosure() - dom(api.button)?.focus() - } else { - api.toggleDisclosure() - } - }, - handleKeyDown(event: KeyboardEvent) { - if (props.disabled) return + function handleClick() { + if (props.disabled) return - if (isWithinPanel) { - switch (event.key) { - case Keys.Space: - case Keys.Enter: - event.preventDefault() - event.stopPropagation() - api.toggleDisclosure() - dom(api.button)?.focus() - break - } - } else { - switch (event.key) { - case Keys.Space: - case Keys.Enter: - event.preventDefault() - event.stopPropagation() - api.toggleDisclosure() - break - } - } - }, - handleKeyUp(event: KeyboardEvent) { + if (isWithinPanel) { + api.toggleDisclosure() + dom(api.button)?.focus() + } else { + api.toggleDisclosure() + } + } + function handleKeyDown(event: KeyboardEvent) { + if (props.disabled) return + + if (isWithinPanel) { switch (event.key) { case Keys.Space: - // Required for firefox, event.preventDefault() in handleKeyDown for - // the Space key doesn't cancel the handleKeyUp, which in turn - // triggers a *click*. + case Keys.Enter: event.preventDefault() + event.stopPropagation() + api.toggleDisclosure() + dom(api.button)?.focus() break } - }, + } else { + switch (event.key) { + case Keys.Space: + case Keys.Enter: + event.preventDefault() + event.stopPropagation() + api.toggleDisclosure() + break + } + } + } + function handleKeyUp(event: KeyboardEvent) { + switch (event.key) { + case Keys.Space: + // Required for firefox, event.preventDefault() in handleKeyDown for + // the Space key doesn't cancel the handleKeyUp, which in turn + // triggers a *click*. + event.preventDefault() + break + } + } + + return () => { + let slot = { open: api.disclosureState.value === DisclosureStates.Open } + let propsWeControl = isWithinPanel + ? { + ref: elementRef, + type: type.value, + onClick: handleClick, + onKeydown: handleKeyDown, + } + : { + id: api.buttonId, + ref: elementRef, + type: type.value, + 'aria-expanded': props.disabled + ? undefined + : api.disclosureState.value === DisclosureStates.Open, + 'aria-controls': dom(api.panel) ? api.panelId : undefined, + disabled: props.disabled ? true : undefined, + onClick: handleClick, + onKeydown: handleKeyDown, + onKeyup: handleKeyUp, + } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs, + slots, + name: 'DisclosureButton', + }) } }, }) @@ -245,23 +240,7 @@ export let DisclosurePanel = defineComponent({ static: { type: Boolean, default: false }, unmount: { type: Boolean, default: true }, }, - render() { - let api = useDisclosureContext('DisclosurePanel') - - let slot = { open: api.disclosureState.value === DisclosureStates.Open, close: api.close } - let propsWeControl = { id: this.id, ref: 'el' } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - features: Features.RenderStrategy | Features.Static, - visible: this.visible, - name: 'DisclosurePanel', - }) - }, - setup() { + setup(props, { attrs, slots }) { let api = useDisclosureContext('DisclosurePanel') provide(DisclosurePanelContext, api.panelId) @@ -275,10 +254,19 @@ export let DisclosurePanel = defineComponent({ return api.disclosureState.value === DisclosureStates.Open }) - return { - id: api.panelId, - el: api.panel, - visible, + return () => { + let slot = { open: api.disclosureState.value === DisclosureStates.Open, close: api.close } + let propsWeControl = { id: api.panelId, ref: api.panel } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs, + slots, + features: Features.RenderStrategy | Features.Static, + visible: visible.value, + name: 'DisclosurePanel', + }) } }, }) diff --git a/packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts b/packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts index 48e9629..dd9bc87 100644 --- a/packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts +++ b/packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts @@ -17,20 +17,7 @@ export let FocusTrap = defineComponent({ as: { type: [Object, String], default: 'div' }, initialFocus: { type: Object as PropType, default: null }, }, - render() { - let slot = {} - let propsWeControl = { ref: 'el' } - let { initialFocus, ...passThroughProps } = this.$props - - return render({ - props: { ...passThroughProps, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - name: 'FocusTrap', - }) - }, - setup(props) { + setup(props, { attrs, slots }) { let containers = ref(new Set()) let container = ref(null) let enabled = ref(true) @@ -47,6 +34,18 @@ export let FocusTrap = defineComponent({ enabled.value = false }) - return { el: container } + return () => { + let slot = {} + let propsWeControl = { ref: container } + let { initialFocus, ...passThroughProps } = props + + return render({ + props: { ...passThroughProps, ...propsWeControl }, + slot, + attrs, + slots, + name: 'FocusTrap', + }) + } }, }) diff --git a/packages/@headlessui-vue/src/components/label/label.test.ts b/packages/@headlessui-vue/src/components/label/label.test.ts index 67a4113..fff8167 100644 --- a/packages/@headlessui-vue/src/components/label/label.test.ts +++ b/packages/@headlessui-vue/src/components/label/label.test.ts @@ -27,12 +27,10 @@ it('should be possible to use useLabels without using a Label', async () => { let { container } = render( defineComponent({ components: { Label }, - render() { - return h('div', [h('div', { 'aria-labelledby': this.labelledby }, ['No label'])]) - }, setup() { let labelledby = useLabels() - return { labelledby } + + return () => h('div', [h('div', { 'aria-labelledby': labelledby.value }, ['No label'])]) }, }) ) @@ -50,17 +48,16 @@ it('should be possible to use useLabels and a single Label, and have them linked let { container } = render( defineComponent({ components: { Label }, - render() { - return h('div', [ - h('div', { 'aria-labelledby': this.labelledby }, [ - h(Label, () => 'I am a label'), - h('span', 'Contents'), - ]), - ]) - }, setup() { let labelledby = useLabels() - return { labelledby } + + return () => + h('div', [ + h('div', { 'aria-labelledby': labelledby.value }, [ + h(Label, () => 'I am a label'), + h('span', 'Contents'), + ]), + ]) }, }) ) @@ -83,18 +80,17 @@ it('should be possible to use useLabels and multiple Label components, and have let { container } = render( defineComponent({ components: { Label }, - render() { - return h('div', [ - h('div', { 'aria-labelledby': this.labelledby }, [ - h(Label, () => 'I am a label'), - h('span', 'Contents'), - h(Label, () => 'I am also a label'), - ]), - ]) - }, setup() { let labelledby = useLabels() - return { labelledby } + + return () => + h('div', [ + h('div', { 'aria-labelledby': labelledby.value }, [ + h(Label, () => 'I am a label'), + h('span', 'Contents'), + h(Label, () => 'I am also a label'), + ]), + ]) }, }) ) @@ -118,18 +114,17 @@ it('should be possible to update a prop from the parent and it should reflect in let { container } = render( defineComponent({ components: { Label }, - render() { - return h('div', [ - h('div', { 'aria-labelledby': this.labelledby }, [ - h(Label, () => 'I am a label'), - h('button', { onClick: () => this.count++ }, '+1'), - ]), - ]) - }, setup() { let count = ref(0) let labelledby = useLabels({ props: { 'data-count': count } }) - return { count, labelledby } + + return () => + h('div', [ + h('div', { 'aria-labelledby': labelledby.value }, [ + h(Label, () => 'I am a label'), + h('button', { onClick: () => count.value++ }, '+1'), + ]), + ]) }, }) ) diff --git a/packages/@headlessui-vue/src/components/label/label.ts b/packages/@headlessui-vue/src/components/label/label.ts index 77b9bd7..f29b99f 100644 --- a/packages/@headlessui-vue/src/components/label/label.ts +++ b/packages/@headlessui-vue/src/components/label/label.ts @@ -69,36 +69,35 @@ export let Label = defineComponent({ as: { type: [Object, String], default: 'label' }, passive: { type: [Boolean], default: false }, }, - render() { - let { name = 'Label', slot = {}, props = {} } = this.context - let { passive, ...passThroughProps } = this.$props - let propsWeControl = { - ...Object.entries(props).reduce( - (acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }), - {} - ), - id: this.id, - } - let allProps = { ...passThroughProps, ...propsWeControl } - - // @ts-expect-error props are dynamic via context, some components will - // provide an onClick then we can delete it. - if (passive) delete allProps['onClick'] - - return render({ - props: allProps, - slot, - attrs: this.$attrs, - slots: this.$slots, - name, - }) - }, - setup() { + setup(myProps, { slots, attrs }) { let context = useLabelContext() let id = `headlessui-label-${useId()}` onMounted(() => onUnmounted(context.register(id))) - return { id, context } + return () => { + let { name = 'Label', slot = {}, props = {} } = context + let { passive, ...passThroughProps } = myProps + let propsWeControl = { + ...Object.entries(props).reduce( + (acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }), + {} + ), + id, + } + let allProps = { ...passThroughProps, ...propsWeControl } + + // @ts-expect-error props are dynamic via context, some components will + // provide an onClick then we can delete it. + if (passive) delete allProps['onClick'] + + return render({ + props: allProps, + slot, + attrs, + slots, + name, + }) + } }, }) diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts index 680a2f4..812192d 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.ts +++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts @@ -245,30 +245,28 @@ export let Listbox = defineComponent({ export let ListboxLabel = defineComponent({ name: 'ListboxLabel', props: { as: { type: [Object, String], default: 'label' } }, - render() { - let api = useListboxContext('ListboxLabel') - - let slot = { open: api.listboxState.value === ListboxStates.Open, disabled: api.disabled.value } - let propsWeControl = { id: this.id, ref: 'el', onClick: this.handleClick } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - name: 'ListboxLabel', - }) - }, - setup() { + setup(props, { attrs, slots }) { let api = useListboxContext('ListboxLabel') let id = `headlessui-listbox-label-${useId()}` - return { - id, - el: api.labelRef, - handleClick() { - dom(api.buttonRef)?.focus({ preventScroll: true }) - }, + function handleClick() { + dom(api.buttonRef)?.focus({ preventScroll: true }) + } + + return () => { + let slot = { + open: api.listboxState.value === ListboxStates.Open, + disabled: api.disabled.value, + } + let propsWeControl = { id, ref: api.labelRef, onClick: handleClick } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs, + slots, + name: 'ListboxLabel', + }) } }, }) @@ -280,37 +278,7 @@ export let ListboxButton = defineComponent({ props: { as: { type: [Object, String], default: 'button' }, }, - render() { - let api = useListboxContext('ListboxButton') - - let slot = { open: api.listboxState.value === ListboxStates.Open, disabled: api.disabled.value } - let propsWeControl = { - ref: 'el', - id: this.id, - type: this.type, - 'aria-haspopup': true, - 'aria-controls': dom(api.optionsRef)?.id, - 'aria-expanded': api.disabled.value - ? undefined - : api.listboxState.value === ListboxStates.Open, - 'aria-labelledby': api.labelRef.value - ? [dom(api.labelRef)?.id, this.id].join(' ') - : undefined, - disabled: api.disabled.value === true ? true : undefined, - onKeydown: this.handleKeyDown, - onKeyup: this.handleKeyUp, - onClick: this.handleClick, - } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - name: 'ListboxButton', - }) - }, - setup(props, { attrs }) { + setup(props, { attrs, slots }) { let api = useListboxContext('ListboxButton') let id = `headlessui-listbox-button-${useId()}` @@ -363,16 +331,39 @@ export let ListboxButton = defineComponent({ } } - return { - id, - el: api.buttonRef, - type: useResolveButtonType( - computed(() => ({ as: props.as, type: attrs.type })), - api.buttonRef - ), - handleKeyDown, - handleKeyUp, - handleClick, + let type = useResolveButtonType( + computed(() => ({ as: props.as, type: attrs.type })), + api.buttonRef + ) + + return () => { + let slot = { + open: api.listboxState.value === ListboxStates.Open, + disabled: api.disabled.value, + } + let propsWeControl = { + ref: api.buttonRef, + id, + type: type.value, + 'aria-haspopup': true, + 'aria-controls': dom(api.optionsRef)?.id, + 'aria-expanded': api.disabled.value + ? undefined + : api.listboxState.value === ListboxStates.Open, + 'aria-labelledby': api.labelRef.value ? [dom(api.labelRef)?.id, id].join(' ') : undefined, + disabled: api.disabled.value === true ? true : undefined, + onKeydown: handleKeyDown, + onKeyup: handleKeyUp, + onClick: handleClick, + } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs, + slots, + name: 'ListboxButton', + }) } }, }) @@ -386,36 +377,7 @@ export let ListboxOptions = defineComponent({ static: { type: Boolean, default: false }, unmount: { type: Boolean, default: true }, }, - render() { - let api = useListboxContext('ListboxOptions') - - let slot = { open: api.listboxState.value === ListboxStates.Open } - let propsWeControl = { - 'aria-activedescendant': - api.activeOptionIndex.value === null - ? undefined - : api.options.value[api.activeOptionIndex.value]?.id, - 'aria-labelledby': dom(api.labelRef)?.id ?? dom(api.buttonRef)?.id, - 'aria-orientation': api.orientation.value, - id: this.id, - onKeydown: this.handleKeyDown, - role: 'listbox', - tabIndex: 0, - ref: 'el', - } - let passThroughProps = this.$props - - return render({ - props: { ...passThroughProps, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - features: Features.RenderStrategy | Features.Static, - visible: this.visible, - name: 'ListboxOptions', - }) - }, - setup() { + setup(props, { attrs, slots }) { let api = useListboxContext('ListboxOptions') let id = `headlessui-listbox-options-${useId()}` let searchDebounce = ref | null>(null) @@ -500,7 +462,33 @@ export let ListboxOptions = defineComponent({ return api.listboxState.value === ListboxStates.Open }) - return { id, el: api.optionsRef, handleKeyDown, visible } + return () => { + let slot = { open: api.listboxState.value === ListboxStates.Open } + let propsWeControl = { + 'aria-activedescendant': + api.activeOptionIndex.value === null + ? undefined + : api.options.value[api.activeOptionIndex.value]?.id, + 'aria-labelledby': dom(api.labelRef)?.id ?? dom(api.buttonRef)?.id, + 'aria-orientation': api.orientation.value, + id, + onKeydown: handleKeyDown, + role: 'listbox', + tabIndex: 0, + ref: api.optionsRef, + } + let passThroughProps = props + + return render({ + props: { ...passThroughProps, ...propsWeControl }, + slot, + attrs, + slots, + features: Features.RenderStrategy | Features.Static, + visible: visible.value, + name: 'ListboxOptions', + }) + } }, }) diff --git a/packages/@headlessui-vue/src/components/menu/menu.ts b/packages/@headlessui-vue/src/components/menu/menu.ts index 3ffe7bd..f154625 100644 --- a/packages/@headlessui-vue/src/components/menu/menu.ts +++ b/packages/@headlessui-vue/src/components/menu/menu.ts @@ -172,6 +172,7 @@ export let Menu = defineComponent({ // @ts-expect-error Types of property 'dataRef' are incompatible. provide(MenuContext, api) + useOpenClosedProvider( computed(() => match(menuState.value, { @@ -194,31 +195,7 @@ export let MenuButton = defineComponent({ disabled: { type: Boolean, default: false }, as: { type: [Object, String], default: 'button' }, }, - render() { - let api = useMenuContext('MenuButton') - - let slot = { open: api.menuState.value === MenuStates.Open } - let propsWeControl = { - ref: 'el', - id: this.id, - type: this.type, - 'aria-haspopup': true, - 'aria-controls': dom(api.itemsRef)?.id, - 'aria-expanded': this.$props.disabled ? undefined : api.menuState.value === MenuStates.Open, - onKeydown: this.handleKeyDown, - onKeyup: this.handleKeyUp, - onClick: this.handleClick, - } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - name: 'MenuButton', - }) - }, - setup(props, { attrs }) { + setup(props, { attrs, slots }) { let api = useMenuContext('MenuButton') let id = `headlessui-menu-button-${useId()}` @@ -274,16 +251,32 @@ export let MenuButton = defineComponent({ } } - return { - id, - el: api.buttonRef, - type: useResolveButtonType( - computed(() => ({ as: props.as, type: attrs.type })), - api.buttonRef - ), - handleKeyDown, - handleKeyUp, - handleClick, + let type = useResolveButtonType( + computed(() => ({ as: props.as, type: attrs.type })), + api.buttonRef + ) + + return () => { + let slot = { open: api.menuState.value === MenuStates.Open } + let propsWeControl = { + ref: api.buttonRef, + id, + type: type.value, + 'aria-haspopup': true, + 'aria-controls': dom(api.itemsRef)?.id, + 'aria-expanded': props.disabled ? undefined : api.menuState.value === MenuStates.Open, + onKeydown: handleKeyDown, + onKeyup: handleKeyUp, + onClick: handleClick, + } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs, + slots, + name: 'MenuButton', + }) } }, }) @@ -295,36 +288,7 @@ export let MenuItems = defineComponent({ static: { type: Boolean, default: false }, unmount: { type: Boolean, default: true }, }, - render() { - let api = useMenuContext('MenuItems') - - let slot = { open: api.menuState.value === MenuStates.Open } - let propsWeControl = { - 'aria-activedescendant': - api.activeItemIndex.value === null - ? undefined - : api.items.value[api.activeItemIndex.value]?.id, - 'aria-labelledby': dom(api.buttonRef)?.id, - id: this.id, - onKeydown: this.handleKeyDown, - onKeyup: this.handleKeyUp, - role: 'menu', - tabIndex: 0, - ref: 'el', - } - let passThroughProps = this.$props - - return render({ - props: { ...passThroughProps, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - features: Features.RenderStrategy | Features.Static, - visible: this.visible, - name: 'MenuItems', - }) - }, - setup() { + setup(props, { attrs, slots }) { let api = useMenuContext('MenuItems') let id = `headlessui-menu-items-${useId()}` let searchDebounce = ref | null>(null) @@ -430,7 +394,34 @@ export let MenuItems = defineComponent({ return api.menuState.value === MenuStates.Open }) - return { id, el: api.itemsRef, handleKeyDown, handleKeyUp, visible } + return () => { + let slot = { open: api.menuState.value === MenuStates.Open } + let propsWeControl = { + 'aria-activedescendant': + api.activeItemIndex.value === null + ? undefined + : api.items.value[api.activeItemIndex.value]?.id, + 'aria-labelledby': dom(api.buttonRef)?.id, + id, + onKeydown: handleKeyDown, + onKeyup: handleKeyUp, + role: 'menu', + tabIndex: 0, + ref: api.itemsRef, + } + + let passThroughProps = props + + return render({ + props: { ...passThroughProps, ...propsWeControl }, + slot, + attrs, + slots, + features: Features.RenderStrategy | Features.Static, + visible: visible.value, + name: 'MenuItems', + }) + } }, }) diff --git a/packages/@headlessui-vue/src/components/popover/popover.ts b/packages/@headlessui-vue/src/components/popover/popover.ts index f4cba1b..67d2d01 100644 --- a/packages/@headlessui-vue/src/components/popover/popover.ts +++ b/packages/@headlessui-vue/src/components/popover/popover.ts @@ -206,40 +206,7 @@ export let PopoverButton = defineComponent({ as: { type: [Object, String], default: 'button' }, disabled: { type: [Boolean], default: false }, }, - render() { - let api = usePopoverContext('PopoverButton') - - let slot = { open: api.popoverState.value === PopoverStates.Open } - let propsWeControl = this.isWithinPanel - ? { - ref: 'el', - type: this.type, - onKeydown: this.handleKeyDown, - onClick: this.handleClick, - } - : { - ref: 'el', - id: api.buttonId, - type: this.type, - 'aria-expanded': this.$props.disabled - ? undefined - : api.popoverState.value === PopoverStates.Open, - 'aria-controls': dom(api.panel) ? api.panelId : undefined, - disabled: this.$props.disabled ? true : undefined, - onKeydown: this.handleKeyDown, - onKeyup: this.handleKeyUp, - onClick: this.handleClick, - } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - name: 'PopoverButton', - }) - }, - setup(props, { attrs }) { + setup(props, { attrs, slots }) { let api = usePopoverContext('PopoverButton') let groupContext = usePopoverGroupContext() @@ -271,124 +238,153 @@ export let PopoverButton = defineComponent({ }) } - return { - isWithinPanel, - el: elementRef, - type: useResolveButtonType( - computed(() => ({ as: props.as, type: attrs.type })), - elementRef - ), - handleKeyDown(event: KeyboardEvent) { - if (isWithinPanel) { - if (api.popoverState.value === PopoverStates.Closed) return - switch (event.key) { - case Keys.Space: - case Keys.Enter: - event.preventDefault() // Prevent triggering a *click* event - event.stopPropagation() - api.closePopover() - dom(api.button)?.focus() // Re-focus the original opening Button - break - } - } else { - switch (event.key) { - case Keys.Space: - case Keys.Enter: - event.preventDefault() // Prevent triggering a *click* event - event.stopPropagation() - if (api.popoverState.value === PopoverStates.Closed) closeOthers?.(api.buttonId) - api.togglePopover() - break + let type = useResolveButtonType( + computed(() => ({ as: props.as, type: attrs.type })), + elementRef + ) - case Keys.Escape: - if (api.popoverState.value !== PopoverStates.Open) return closeOthers?.(api.buttonId) - if (!dom(api.button)) return - if (!dom(api.button)?.contains(document.activeElement)) return - event.preventDefault() - event.stopPropagation() - api.closePopover() - break - - case Keys.Tab: - if (api.popoverState.value !== PopoverStates.Open) return - if (!api.panel) return - if (!api.button) return - - // TODO: Revisit when handling Tab/Shift+Tab when using Portal's - if (event.shiftKey) { - // Check if the last focused element exists, and check that it is not inside button or panel itself - if (!previousActiveElementRef.value) return - if (dom(api.button)?.contains(previousActiveElementRef.value)) return - if (dom(api.panel)?.contains(previousActiveElementRef.value)) return - - // Check if the last focused element is *after* the button in the DOM - let focusableElements = getFocusableElements() - let previousIdx = focusableElements.indexOf( - previousActiveElementRef.value as HTMLElement - ) - let buttonIdx = focusableElements.indexOf(dom(api.button)!) - if (buttonIdx > previousIdx) return - - event.preventDefault() - event.stopPropagation() - - focusIn(dom(api.panel)!, Focus.Last) - } else { - event.preventDefault() - event.stopPropagation() - - focusIn(dom(api.panel)!, Focus.First) - } - - break - } - } - }, - handleKeyUp(event: KeyboardEvent) { - if (isWithinPanel) return - if (event.key === Keys.Space) { - // Required for firefox, event.preventDefault() in handleKeyDown for - // the Space key doesn't cancel the handleKeyUp, which in turn - // triggers a *click*. - event.preventDefault() - } - if (api.popoverState.value !== PopoverStates.Open) return - if (!api.panel) return - if (!api.button) return - - // TODO: Revisit when handling Tab/Shift+Tab when using Portal's + function handleKeyDown(event: KeyboardEvent) { + if (isWithinPanel) { + if (api.popoverState.value === PopoverStates.Closed) return switch (event.key) { - case Keys.Tab: - // Check if the last focused element exists, and check that it is not inside button or panel itself - if (!previousActiveElementRef.value) return - if (dom(api.button)?.contains(previousActiveElementRef.value)) return - if (dom(api.panel)?.contains(previousActiveElementRef.value)) return - - // Check if the last focused element is *after* the button in the DOM - let focusableElements = getFocusableElements() - let previousIdx = focusableElements.indexOf( - previousActiveElementRef.value as HTMLElement - ) - let buttonIdx = focusableElements.indexOf(dom(api.button)!) - if (buttonIdx > previousIdx) return - - event.preventDefault() + case Keys.Space: + case Keys.Enter: + event.preventDefault() // Prevent triggering a *click* event event.stopPropagation() - focusIn(dom(api.panel)!, Focus.Last) + api.closePopover() + dom(api.button)?.focus() // Re-focus the original opening Button break } - }, - handleClick() { - if (props.disabled) return - if (isWithinPanel) { - api.closePopover() - dom(api.button)?.focus() // Re-focus the original opening Button - } else { - if (api.popoverState.value === PopoverStates.Closed) closeOthers?.(api.buttonId) - dom(api.button)?.focus() - api.togglePopover() + } else { + switch (event.key) { + case Keys.Space: + case Keys.Enter: + event.preventDefault() // Prevent triggering a *click* event + event.stopPropagation() + if (api.popoverState.value === PopoverStates.Closed) closeOthers?.(api.buttonId) + api.togglePopover() + break + + case Keys.Escape: + if (api.popoverState.value !== PopoverStates.Open) return closeOthers?.(api.buttonId) + if (!dom(api.button)) return + if (!dom(api.button)?.contains(document.activeElement)) return + event.preventDefault() + event.stopPropagation() + api.closePopover() + break + + case Keys.Tab: + if (api.popoverState.value !== PopoverStates.Open) return + if (!api.panel) return + if (!api.button) return + + // TODO: Revisit when handling Tab/Shift+Tab when using Portal's + if (event.shiftKey) { + // Check if the last focused element exists, and check that it is not inside button or panel itself + if (!previousActiveElementRef.value) return + if (dom(api.button)?.contains(previousActiveElementRef.value)) return + if (dom(api.panel)?.contains(previousActiveElementRef.value)) return + + // Check if the last focused element is *after* the button in the DOM + let focusableElements = getFocusableElements() + let previousIdx = focusableElements.indexOf( + previousActiveElementRef.value as HTMLElement + ) + let buttonIdx = focusableElements.indexOf(dom(api.button)!) + if (buttonIdx > previousIdx) return + + event.preventDefault() + event.stopPropagation() + + focusIn(dom(api.panel)!, Focus.Last) + } else { + event.preventDefault() + event.stopPropagation() + + focusIn(dom(api.panel)!, Focus.First) + } + + break } - }, + } + } + + function handleKeyUp(event: KeyboardEvent) { + if (isWithinPanel) return + if (event.key === Keys.Space) { + // Required for firefox, event.preventDefault() in handleKeyDown for + // the Space key doesn't cancel the handleKeyUp, which in turn + // triggers a *click*. + event.preventDefault() + } + if (api.popoverState.value !== PopoverStates.Open) return + if (!api.panel) return + if (!api.button) return + + // TODO: Revisit when handling Tab/Shift+Tab when using Portal's + switch (event.key) { + case Keys.Tab: + // Check if the last focused element exists, and check that it is not inside button or panel itself + if (!previousActiveElementRef.value) return + if (dom(api.button)?.contains(previousActiveElementRef.value)) return + if (dom(api.panel)?.contains(previousActiveElementRef.value)) return + + // Check if the last focused element is *after* the button in the DOM + let focusableElements = getFocusableElements() + let previousIdx = focusableElements.indexOf(previousActiveElementRef.value as HTMLElement) + let buttonIdx = focusableElements.indexOf(dom(api.button)!) + if (buttonIdx > previousIdx) return + + event.preventDefault() + event.stopPropagation() + focusIn(dom(api.panel)!, Focus.Last) + break + } + } + + function handleClick() { + if (props.disabled) return + if (isWithinPanel) { + api.closePopover() + dom(api.button)?.focus() // Re-focus the original opening Button + } else { + if (api.popoverState.value === PopoverStates.Closed) closeOthers?.(api.buttonId) + dom(api.button)?.focus() + api.togglePopover() + } + } + + return () => { + let slot = { open: api.popoverState.value === PopoverStates.Open } + let propsWeControl = isWithinPanel + ? { + ref: elementRef, + type: type.value, + onKeydown: handleKeyDown, + onClick: handleClick, + } + : { + ref: elementRef, + id: api.buttonId, + type: type.value, + 'aria-expanded': props.disabled + ? undefined + : api.popoverState.value === PopoverStates.Open, + 'aria-controls': dom(api.panel) ? api.panelId : undefined, + disabled: props.disabled ? true : undefined, + onKeydown: handleKeyDown, + onKeyup: handleKeyUp, + onClick: handleClick, + } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs: attrs, + slots: slots, + name: 'PopoverButton', + }) } }, }) @@ -402,29 +398,9 @@ export let PopoverOverlay = defineComponent({ static: { type: Boolean, default: false }, unmount: { type: Boolean, default: true }, }, - render() { - let api = usePopoverContext('PopoverOverlay') - - let slot = { open: api.popoverState.value === PopoverStates.Open } - let propsWeControl = { - id: this.id, - ref: 'el', - 'aria-hidden': true, - onClick: this.handleClick, - } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - features: Features.RenderStrategy | Features.Static, - visible: this.visible, - name: 'PopoverOverlay', - }) - }, - setup() { + setup(props, { attrs, slots }) { let api = usePopoverContext('PopoverOverlay') + let id = `headlessui-popover-overlay-${useId()}` let usesOpenClosedState = useOpenClosed() let visible = computed(() => { @@ -435,12 +411,27 @@ export let PopoverOverlay = defineComponent({ return api.popoverState.value === PopoverStates.Open }) - return { - id: `headlessui-popover-overlay-${useId()}`, - handleClick() { - api.closePopover() - }, - visible, + function handleClick() { + api.closePopover() + } + + return () => { + let slot = { open: api.popoverState.value === PopoverStates.Open } + let propsWeControl = { + id, + 'aria-hidden': true, + onClick: handleClick, + } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs, + slots, + features: Features.RenderStrategy | Features.Static, + visible: visible.value, + name: 'PopoverOverlay', + }) } }, }) @@ -455,31 +446,7 @@ export let PopoverPanel = defineComponent({ unmount: { type: Boolean, default: true }, focus: { type: Boolean, default: false }, }, - render() { - let api = usePopoverContext('PopoverPanel') - - let slot = { - open: api.popoverState.value === PopoverStates.Open, - close: api.close, - } - - let propsWeControl = { - ref: 'el', - id: this.id, - onKeydown: this.handleKeyDown, - } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - features: Features.RenderStrategy | Features.Static, - visible: this.visible, - name: 'PopoverPanel', - }) - }, - setup(props) { + setup(props, { attrs, slots }) { let { focus } = props let api = usePopoverContext('PopoverPanel') @@ -563,23 +530,41 @@ export let PopoverPanel = defineComponent({ return api.popoverState.value === PopoverStates.Open }) - return { - id: api.panelId, - el: api.panel, - handleKeyDown(event: KeyboardEvent) { - switch (event.key) { - case Keys.Escape: - if (api.popoverState.value !== PopoverStates.Open) return - if (!dom(api.panel)) return - if (!dom(api.panel)?.contains(document.activeElement)) return - event.preventDefault() - event.stopPropagation() - api.closePopover() - dom(api.button)?.focus() - break - } - }, - visible, + function handleKeyDown(event: KeyboardEvent) { + switch (event.key) { + case Keys.Escape: + if (api.popoverState.value !== PopoverStates.Open) return + if (!dom(api.panel)) return + if (!dom(api.panel)?.contains(document.activeElement)) return + event.preventDefault() + event.stopPropagation() + api.closePopover() + dom(api.button)?.focus() + break + } + } + + return () => { + let slot = { + open: api.popoverState.value === PopoverStates.Open, + close: api.close, + } + + let propsWeControl = { + ref: api.panel, + id: api.panelId, + onKeydown: handleKeyDown, + } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs, + slots, + features: Features.RenderStrategy | Features.Static, + visible: visible.value, + name: 'PopoverPanel', + }) } }, }) @@ -591,18 +576,7 @@ export let PopoverGroup = defineComponent({ props: { as: { type: [Object, String], default: 'div' }, }, - render() { - let propsWeControl = { ref: 'el' } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot: {}, - attrs: this.$attrs, - slots: this.$slots, - name: 'PopoverGroup', - }) - }, - setup() { + setup(props, { attrs, slots }) { let groupRef = ref(null) let popovers = ref([]) @@ -645,6 +619,16 @@ export let PopoverGroup = defineComponent({ closeOthers, }) - return { el: groupRef } + return () => { + let propsWeControl = { ref: groupRef } + + return render({ + props: { ...props, ...propsWeControl }, + slot: {}, + attrs, + slots, + name: 'PopoverGroup', + }) + } }, }) diff --git a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts index 13557f2..44481ee 100644 --- a/packages/@headlessui-vue/src/components/radio-group/radio-group.ts +++ b/packages/@headlessui-vue/src/components/radio-group/radio-group.ts @@ -17,7 +17,7 @@ import { dom } from '../../utils/dom' import { Keys } from '../../keyboard' import { focusIn, Focus, FocusResult } from '../../utils/focus-management' import { useId } from '../../hooks/use-id' -import { render } from '../../utils/render' +import { omit, render } from '../../utils/render' import { Label, useLabels } from '../label/label' import { Description, useDescriptions } from '../description/description' import { useTreeWalker } from '../../hooks/use-tree-walker' @@ -66,27 +66,7 @@ export let RadioGroup = defineComponent({ disabled: { type: [Boolean], default: false }, modelValue: { type: [Object, String, Number, Boolean] }, }, - render() { - let { modelValue, disabled, ...passThroughProps } = this.$props - - let propsWeControl = { - ref: 'el', - id: this.id, - role: 'radiogroup', - 'aria-labelledby': this.labelledby, - 'aria-describedby': this.describedby, - onKeydown: this.handleKeyDown, - } - - return render({ - props: { ...passThroughProps, ...propsWeControl }, - slot: {}, - attrs: this.$attrs, - slots: this.$slots, - name: 'RadioGroup', - }) - }, - setup(props, { emit }) { + setup(props, { emit, attrs, slots }) { let radioGroupRef = ref(null) let options = ref([]) let labelledby = useLabels({ name: 'RadioGroupLabel' }) @@ -209,12 +189,25 @@ export let RadioGroup = defineComponent({ let id = `headlessui-radiogroup-${useId()}` - return { - id, - labelledby, - describedby, - el: radioGroupRef, - handleKeyDown, + return () => { + let { modelValue, disabled, ...passThroughProps } = props + + let propsWeControl = { + ref: radioGroupRef, + id, + role: 'radiogroup', + 'aria-labelledby': labelledby.value, + 'aria-describedby': describedby.value, + onKeydown: handleKeyDown, + } + + return render({ + props: { ...passThroughProps, ...propsWeControl }, + slot: {}, + attrs, + slots, + name: 'RadioGroup', + }) } }, }) @@ -233,38 +226,7 @@ export let RadioGroupOption = defineComponent({ value: { type: [Object, String, Number, Boolean] }, disabled: { type: Boolean, default: false }, }, - render() { - let { value, disabled, ...passThroughProps } = this.$props - - let slot = { - checked: this.checked, - disabled: this.disabled, - active: Boolean(this.state & OptionState.Active), - } - - let propsWeControl = { - id: this.id, - ref: 'el', - role: 'radio', - 'aria-checked': this.checked ? 'true' : 'false', - 'aria-labelledby': this.labelledby, - 'aria-describedby': this.describedby, - 'aria-disabled': this.disabled ? true : undefined, - tabIndex: this.tabIndex, - onClick: this.disabled ? undefined : this.handleClick, - onFocus: this.disabled ? undefined : this.handleFocus, - onBlur: this.disabled ? undefined : this.handleBlur, - } - - return render({ - props: { ...passThroughProps, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - name: 'RadioGroupOption', - }) - }, - setup(props) { + setup(props, { attrs, slots }) { let api = useRadioGroupContext('RadioGroupOption') let id = `headlessui-radiogroup-option-${useId()}` let labelledby = useLabels({ name: 'RadioGroupLabel' }) @@ -280,33 +242,58 @@ export let RadioGroupOption = defineComponent({ let isFirstOption = computed(() => api.firstOption.value?.id === id) let disabled = computed(() => api.disabled.value || props.disabled) let checked = computed(() => toRaw(api.value.value) === toRaw(props.value)) + let tabIndex = computed(() => { + if (disabled.value) return -1 + if (checked.value) return 0 + if (!api.containsCheckedOption.value && isFirstOption.value) return 0 + return -1 + }) - return { - id, - el: optionRef, - labelledby, - describedby, - state, - disabled, - checked, - tabIndex: computed(() => { - if (disabled.value) return -1 - if (checked.value) return 0 - if (!api.containsCheckedOption.value && isFirstOption.value) return 0 - return -1 - }), - handleClick() { - if (!api.change(props.value)) return + function handleClick() { + if (!api.change(props.value)) return - state.value |= OptionState.Active - optionRef.value?.focus() - }, - handleFocus() { - state.value |= OptionState.Active - }, - handleBlur() { - state.value &= ~OptionState.Active - }, + state.value |= OptionState.Active + optionRef.value?.focus() + } + + function handleFocus() { + state.value |= OptionState.Active + } + + function handleBlur() { + state.value &= ~OptionState.Active + } + + return () => { + let passThroughProps = omit(props, ['value', 'disabled']) + + let slot = { + checked: checked.value, + disabled: disabled.value, + active: Boolean(state.value & OptionState.Active), + } + + let propsWeControl = { + id, + ref: optionRef, + role: 'radio', + 'aria-checked': checked.value ? 'true' : 'false', + 'aria-labelledby': labelledby.value, + 'aria-describedby': describedby.value, + 'aria-disabled': disabled.value ? true : undefined, + tabIndex: tabIndex.value, + onClick: disabled.value ? undefined : handleClick, + onFocus: disabled.value ? undefined : handleFocus, + onBlur: disabled.value ? undefined : handleBlur, + } + + return render({ + props: { ...passThroughProps, ...propsWeControl }, + slot, + attrs, + slots, + name: 'RadioGroupOption', + }) } }, }) diff --git a/packages/@headlessui-vue/src/components/switch/switch.ts b/packages/@headlessui-vue/src/components/switch/switch.ts index ae202f8..8ca770e 100644 --- a/packages/@headlessui-vue/src/components/switch/switch.ts +++ b/packages/@headlessui-vue/src/components/switch/switch.ts @@ -64,31 +64,8 @@ export let Switch = defineComponent({ as: { type: [Object, String], default: 'button' }, modelValue: { type: Boolean, default: false }, }, - render() { - let slot = { checked: this.$props.modelValue } - let propsWeControl = { - id: this.id, - ref: 'el', - role: 'switch', - type: this.type, - tabIndex: 0, - 'aria-checked': this.$props.modelValue, - 'aria-labelledby': this.labelledby, - 'aria-describedby': this.describedby, - onClick: this.handleClick, - onKeyup: this.handleKeyUp, - onKeypress: this.handleKeyPress, - } - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - name: 'Switch', - }) - }, - setup(props, { emit, attrs }) { + setup(props, { emit, attrs, slots }) { let api = inject(GroupContext, null) let id = `headlessui-switch-${useId()}` @@ -98,28 +75,49 @@ export let Switch = defineComponent({ let internalSwitchRef = ref(null) let switchRef = api === null ? internalSwitchRef : api.switchRef + let type = useResolveButtonType( + computed(() => ({ as: props.as, type: attrs.type })), + switchRef + ) - return { - id, - el: switchRef, - type: useResolveButtonType( - computed(() => ({ as: props.as, type: attrs.type })), - switchRef - ), - labelledby: api?.labelledby, - describedby: api?.describedby, - handleClick(event: MouseEvent) { - event.preventDefault() - toggle() - }, - handleKeyUp(event: KeyboardEvent) { - if (event.key !== Keys.Tab) event.preventDefault() - if (event.key === Keys.Space) toggle() - }, - // This is needed so that we can "cancel" the click event when we use the `Enter` key on a button. - handleKeyPress(event: KeyboardEvent) { - event.preventDefault() - }, + function handleClick(event: MouseEvent) { + event.preventDefault() + toggle() + } + + function handleKeyUp(event: KeyboardEvent) { + if (event.key !== Keys.Tab) event.preventDefault() + if (event.key === Keys.Space) toggle() + } + + // This is needed so that we can "cancel" the click event when we use the `Enter` key on a button. + function handleKeyPress(event: KeyboardEvent) { + event.preventDefault() + } + + return () => { + let slot = { checked: props.modelValue } + let propsWeControl = { + id, + ref: switchRef, + role: 'switch', + type: type.value, + tabIndex: 0, + 'aria-checked': props.modelValue, + 'aria-labelledby': api?.labelledby.value, + 'aria-describedby': api?.describedby.value, + onClick: handleClick, + onKeyup: handleKeyUp, + onKeypress: handleKeyPress, + } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs, + slots, + name: 'Switch', + }) } }, }) diff --git a/packages/@headlessui-vue/src/components/tabs/tabs.ts b/packages/@headlessui-vue/src/components/tabs/tabs.ts index 4799dfb..b01e779 100644 --- a/packages/@headlessui-vue/src/components/tabs/tabs.ts +++ b/packages/@headlessui-vue/src/components/tabs/tabs.ts @@ -181,33 +181,7 @@ export let Tab = defineComponent({ as: { type: [Object, String], default: 'button' }, disabled: { type: [Boolean], default: false }, }, - render() { - let api = useTabsContext('Tab') - - let slot = { selected: this.selected } - let propsWeControl = { - ref: 'el', - onKeydown: this.handleKeyDown, - onFocus: api.activation.value === 'manual' ? this.handleFocus : this.handleSelection, - onClick: this.handleSelection, - id: this.id, - role: 'tab', - type: this.type, - 'aria-controls': api.panels.value[this.myIndex]?.value?.id, - 'aria-selected': this.selected, - tabIndex: this.selected ? 0 : -1, - disabled: this.$props.disabled ? true : undefined, - } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - name: 'Tab', - }) - }, - setup(props, { attrs }) { + setup(props, { attrs, slots }) { let api = useTabsContext('Tab') let id = `headlessui-tabs-tab-${useId()}` @@ -271,18 +245,34 @@ export let Tab = defineComponent({ api.setSelectedIndex(myIndex.value) } - return { - el: tabRef, - id, - selected, - myIndex, - type: useResolveButtonType( - computed(() => ({ as: props.as, type: attrs.type })), - tabRef - ), - handleKeyDown, - handleFocus, - handleSelection, + let type = useResolveButtonType( + computed(() => ({ as: props.as, type: attrs.type })), + tabRef + ) + + return () => { + let slot = { selected: selected.value } + let propsWeControl = { + ref: tabRef, + onKeydown: handleKeyDown, + onFocus: api.activation.value === 'manual' ? handleFocus : handleSelection, + onClick: handleSelection, + id, + role: 'tab', + type: type.value, + 'aria-controls': api.panels.value[myIndex.value]?.value?.id, + 'aria-selected': selected.value, + tabIndex: selected.value ? 0 : -1, + disabled: props.disabled ? true : undefined, + } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs, + slots, + name: 'Tab', + }) } }, }) @@ -318,29 +308,7 @@ export let TabPanel = defineComponent({ static: { type: Boolean, default: false }, unmount: { type: Boolean, default: true }, }, - render() { - let api = useTabsContext('TabPanel') - - let slot = { selected: this.selected } - let propsWeControl = { - ref: 'el', - id: this.id, - role: 'tabpanel', - 'aria-labelledby': api.tabs.value[this.myIndex]?.value?.id, - tabIndex: this.selected ? 0 : -1, - } - - return render({ - props: { ...this.$props, ...propsWeControl }, - slot, - attrs: this.$attrs, - slots: this.$slots, - features: Features.Static | Features.RenderStrategy, - visible: this.selected, - name: 'TabPanel', - }) - }, - setup() { + setup(props, { attrs, slots }) { let api = useTabsContext('TabPanel') let id = `headlessui-tabs-panel-${useId()}` @@ -352,6 +320,25 @@ export let TabPanel = defineComponent({ let myIndex = computed(() => api.panels.value.indexOf(panelRef)) let selected = computed(() => myIndex.value === api.selectedIndex.value) - return { id, el: panelRef, selected, myIndex } + return () => { + let slot = { selected: selected.value } + let propsWeControl = { + ref: panelRef, + id, + role: 'tabpanel', + 'aria-labelledby': api.tabs.value[myIndex.value]?.value?.id, + tabIndex: selected.value ? 0 : -1, + } + + return render({ + props: { ...props, ...propsWeControl }, + slot, + attrs, + slots, + features: Features.Static | Features.RenderStrategy, + visible: selected.value, + name: 'TabPanel', + }) + } }, }) diff --git a/packages/@headlessui-vue/src/components/transitions/transition.ts b/packages/@headlessui-vue/src/components/transitions/transition.ts index 46dd51b..cdf4500 100644 --- a/packages/@headlessui-vue/src/components/transitions/transition.ts +++ b/packages/@headlessui-vue/src/components/transitions/transition.ts @@ -18,7 +18,7 @@ import { import { useId } from '../../hooks/use-id' import { match } from '../../utils/match' -import { Features, render, RenderStrategy } from '../../utils/render' +import { Features, omit, render, RenderStrategy } from '../../utils/render' import { Reason, transition } from './utils/transition' import { dom } from '../../utils/dom' import { @@ -151,54 +151,20 @@ export let TransitionChild = defineComponent({ beforeLeave: () => true, afterLeave: () => true, }, - render() { - if (this.renderAsRoot) { - return h( - TransitionRoot, - { - ...this.$props, - onBeforeEnter: () => this.$emit('beforeEnter'), - onAfterEnter: () => this.$emit('afterEnter'), - onBeforeLeave: () => this.$emit('beforeLeave'), - onAfterLeave: () => this.$emit('afterLeave'), - }, - this.$slots - ) - } - - let { - appear, - show, - - // Class names - enter, - enterFrom, - enterTo, - entered, - leave, - leaveFrom, - leaveTo, - ...rest - } = this.$props - - let propsWeControl = { ref: 'el' } - let passthroughProps = rest - - return render({ - props: { ...passthroughProps, ...propsWeControl }, - slot: {}, - slots: this.$slots, - attrs: this.$attrs, - features: TransitionChildRenderFeatures, - visible: this.state === TreeStates.Visible, - name: 'TransitionChild', - }) - }, - setup(props, { emit }) { + setup(props, { emit, attrs, slots }) { if (!hasTransitionContext() && hasOpenClosed()) { - return { - renderAsRoot: true, - } + return () => + h( + TransitionRoot, + { + ...props, + onBeforeEnter: () => emit('beforeEnter'), + onAfterEnter: () => emit('afterEnter'), + onBeforeLeave: () => emit('beforeLeave'), + onAfterLeave: () => emit('afterLeave'), + }, + slots + ) } let container = ref(null) @@ -341,7 +307,35 @@ export let TransitionChild = defineComponent({ ) ) - return { el: container, renderAsRoot: false, state } + return () => { + let { + appear, + show, + + // Class names + enter, + enterFrom, + enterTo, + entered, + leave, + leaveFrom, + leaveTo, + ...rest + } = props + + let propsWeControl = { ref: container } + let passthroughProps = rest + + return render({ + props: { ...passthroughProps, ...propsWeControl }, + slot: {}, + slots, + attrs, + features: TransitionChildRenderFeatures, + visible: state.value === TreeStates.Visible, + name: 'TransitionChild', + }) + } }, }) @@ -368,41 +362,7 @@ export let TransitionRoot = defineComponent({ beforeLeave: () => true, afterLeave: () => true, }, - render() { - let { show, appear, unmount, ...passThroughProps } = this.$props - let sharedProps = { unmount } - - return render({ - props: { - ...sharedProps, - as: 'template', - }, - slot: {}, - slots: { - ...this.$slots, - default: () => [ - h( - TransitionChild, - { - onBeforeEnter: () => this.$emit('beforeEnter'), - onAfterEnter: () => this.$emit('afterEnter'), - onBeforeLeave: () => this.$emit('beforeLeave'), - onAfterLeave: () => this.$emit('afterLeave'), - ...this.$attrs, - ...sharedProps, - ...passThroughProps, - }, - this.$slots.default - ), - ], - }, - attrs: {}, - features: TransitionChildRenderFeatures, - visible: this.state === TreeStates.Visible, - name: 'Transition', - }) - }, - setup(props) { + setup(props, { emit, attrs, slots }) { let usesOpenClosedState = useOpenClosed() let show = computed(() => { @@ -449,6 +409,39 @@ export let TransitionRoot = defineComponent({ provide(NestingContext, nestingBag) provide(TransitionContext, transitionBag) - return { state, show } + return () => { + let passThroughProps = omit(props, ['show', 'appear', 'unmount']) + let sharedProps = { unmount: props.unmount } + + return render({ + props: { + ...sharedProps, + as: 'template', + }, + slot: {}, + slots: { + ...slots, + default: () => [ + h( + TransitionChild, + { + onBeforeEnter: () => emit('beforeEnter'), + onAfterEnter: () => emit('afterEnter'), + onBeforeLeave: () => emit('beforeLeave'), + onAfterLeave: () => emit('afterLeave'), + ...attrs, + ...sharedProps, + ...passThroughProps, + }, + slots.default + ), + ], + }, + attrs: {}, + features: TransitionChildRenderFeatures, + visible: state.value === TreeStates.Visible, + name: 'Transition', + }) + } }, }) diff --git a/packages/@headlessui-vue/vercel.json b/packages/@headlessui-vue/vercel.json deleted file mode 100644 index 0f32683..0000000 --- a/packages/@headlessui-vue/vercel.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] -} diff --git a/packages/playground-vue/src/components/menu/menu.vue b/packages/playground-vue/src/components/menu/menu.vue index 8836767..5cb7cbd 100644 --- a/packages/playground-vue/src/components/menu/menu.vue +++ b/packages/playground-vue/src/components/menu/menu.vue @@ -41,7 +41,7 @@