Skip to content

Commit 255fc36

Browse files
authored
Ensure PopoverPanel can be used inside <transition> (#1653)
* ensure there is an animatable root node This is a bit sad, but it is how Vue works... We used to render just a simple PopoverPanel that resolved to let's say a `<div>`, that's all good. Because the native `<transition>` component requires that there is only 1 DOM child (regardless of the Vue "tree"). This is the sad part, because we simplified focus trapping for the Popover by introducing sibling hidden buttons to capture focus instead of managing this ourselves. Since we can't just return multiple items we wrap them in a `Fragment` component. If you wrap items in a Fragment, then a lot of Vue's magic goes away (automatically adding `class` to the root node). Luckily, Vue has a solution for that, which is `inheritAttrs: false` and then manually spreading the `attrs` onto the correct element. This all works beautiful, but not for the `<transition>` component... so... let's move the focus trappable elements inside the actual Panel and update the logic slightly to go to the Next/Previous item instead of the First/Last because the First/Last will now be the actual focus guards. * update changelog * make TypeScript a bit happier * improve `default` slot in `PopoverPanel`
1 parent 0260afa commit 255fc36

File tree

5 files changed

+43
-37
lines changed

5 files changed

+43
-37
lines changed

packages/@headlessui-react/src/test-utils/interactions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export async function click(
234234
) {
235235
try {
236236
if (element === null) return expect(element).not.toBe(null)
237-
if (element.disabled) return
237+
if (element instanceof HTMLButtonElement && element.disabled) return
238238

239239
let options = { button }
240240

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Fix getting Vue dom elements ([#1610](https://github.com/tailwindlabs/headlessui/pull/1610))
1818
- Ensure `CMD`+`Backspace` works in nullable mode for `Combobox` component ([#1617](https://github.com/tailwindlabs/headlessui/pull/1617))
1919
- Properly merge incoming props with own props ([#1651](https://github.com/tailwindlabs/headlessui/pull/1651))
20+
- Ensure `PopoverPanel` can be used inside `<transition>` ([#1653](https://github.com/tailwindlabs/headlessui/pull/1653))
2021

2122
## [1.6.5] - 2022-06-20
2223

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

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ export let PopoverPanel = defineComponent({
542542
function run() {
543543
match(direction.value, {
544544
[TabDirection.Forwards]: () => {
545-
focusIn(el, Focus.First)
545+
focusIn(el, Focus.Next)
546546
},
547547
[TabDirection.Backwards]: () => {
548548
// Coming from the Popover.Panel (which is portalled to somewhere else). Let's redirect
@@ -592,7 +592,7 @@ export let PopoverPanel = defineComponent({
592592

593593
focusIn(combined, Focus.First, false)
594594
},
595-
[TabDirection.Backwards]: () => focusIn(el, Focus.Last),
595+
[TabDirection.Backwards]: () => focusIn(el, Focus.Previous),
596596
})
597597
}
598598

@@ -618,38 +618,43 @@ export let PopoverPanel = defineComponent({
618618
tabIndex: -1,
619619
}
620620

621-
return h(Fragment, [
622-
visible.value &&
623-
api.isPortalled.value &&
624-
h(Hidden, {
625-
id: beforePanelSentinelId,
626-
ref: api.beforePanelSentinel,
627-
features: HiddenFeatures.Focusable,
628-
as: 'button',
629-
type: 'button',
630-
onFocus: handleBeforeFocus,
631-
}),
632-
render({
633-
ourProps,
634-
theirProps: { ...attrs, ...props },
635-
slot,
636-
attrs,
637-
slots,
638-
features: Features.RenderStrategy | Features.Static,
639-
visible: visible.value,
640-
name: 'PopoverPanel',
641-
}),
642-
visible.value &&
643-
api.isPortalled.value &&
644-
h(Hidden, {
645-
id: afterPanelSentinelId,
646-
ref: api.afterPanelSentinel,
647-
features: HiddenFeatures.Focusable,
648-
as: 'button',
649-
type: 'button',
650-
onFocus: handleAfterFocus,
651-
}),
652-
])
621+
return render({
622+
ourProps,
623+
theirProps: { ...attrs, ...props },
624+
attrs,
625+
slot,
626+
slots: {
627+
...slots,
628+
default: (...args) => [
629+
h(Fragment, [
630+
visible.value &&
631+
api.isPortalled.value &&
632+
h(Hidden, {
633+
id: beforePanelSentinelId,
634+
ref: api.beforePanelSentinel,
635+
features: HiddenFeatures.Focusable,
636+
as: 'button',
637+
type: 'button',
638+
onFocus: handleBeforeFocus,
639+
}),
640+
slots.default?.(...args),
641+
visible.value &&
642+
api.isPortalled.value &&
643+
h(Hidden, {
644+
id: afterPanelSentinelId,
645+
ref: api.afterPanelSentinel,
646+
features: HiddenFeatures.Focusable,
647+
as: 'button',
648+
type: 'button',
649+
onFocus: handleAfterFocus,
650+
}),
651+
]),
652+
],
653+
},
654+
features: Features.RenderStrategy | Features.Static,
655+
visible: visible.value,
656+
name: 'PopoverPanel',
657+
})
653658
}
654659
},
655660
})

packages/@headlessui-vue/src/internal/focus-sentinel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export let FocusSentinel = defineComponent({
3333

3434
// Try to move focus to the correct element. This depends on the implementation
3535
// of `onFocus` of course since it would be different for each place we use it in.
36-
if (props.onFocus()) {
36+
if (props.onFocus?.()) {
3737
enabled.value = false
3838
cancelAnimationFrame(frame)
3939
return

packages/@headlessui-vue/src/test-utils/interactions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ export async function click(
232232
) {
233233
try {
234234
if (element === null) return expect(element).not.toBe(null)
235-
if (element.disabled) return
235+
if (element instanceof HTMLButtonElement && element.disabled) return
236236

237237
let options = { button }
238238

0 commit comments

Comments
 (0)