Skip to content

Commit a7309f2

Browse files
fix: popover works correctly when inside modals, including polyfilled browsers
1 parent 45fb6ca commit a7309f2

File tree

7 files changed

+75
-8
lines changed

7 files changed

+75
-8
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
2+
import {
3+
Modal,
4+
Select,
5+
SelectPopover,
6+
SelectListbox,
7+
SelectOption,
8+
SelectTrigger,
9+
SelectValue,
10+
} from '@qwik-ui/headless';
11+
import styles from '../snippets/modal.css?inline';
12+
13+
export default component$(() => {
14+
useStyles$(styles);
15+
const isOpen = useSignal(false);
16+
17+
return (
18+
<>
19+
<button class="modal-trigger" onClick$={() => (isOpen.value = true)}>
20+
Open Modal
21+
</button>
22+
<Modal class="modal" bind:show={isOpen}>
23+
Modal Content
24+
<Select class="select">
25+
<SelectTrigger class="select-trigger">
26+
<SelectValue placeholder="Select an option" />
27+
</SelectTrigger>
28+
<SelectPopover class="select-popover">
29+
<SelectListbox class="select-listbox">
30+
<SelectOption>Option 1</SelectOption>
31+
<SelectOption>Option 2</SelectOption>
32+
</SelectListbox>
33+
</SelectPopover>
34+
</Select>
35+
</Modal>
36+
</>
37+
);
38+
});

apps/website/src/routes/docs/headless/modal/index.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ A window overlaid on either the primary window or another dialog window. Modal c
1515

1616
<Showcase name="hero" />
1717

18+
<Showcase name="nested-popover" />
19+
1820
The modal makes use of the HTML `dialog` element, which is supported in [every major browser](https://caniuse.com/?search=dialog).
1921

2022
> For non-modal UI elements, it is preferred to use [Qwik UI's popover component](../../../docs/headless/popover/). Qwik's popover component can be applied to the most-semantically-relevant HTML element, including the `<dialog>` element itself for non-modal dialogs.

apps/website/src/routes/docs/headless/popover/index.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ A non-modal primitive with overlays that open and close around a DOM element.
2323
]}
2424
/>
2525

26-
The Qwik UI Popover component is designed to align with the [HTML Popover API Specification](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API). As of now, native support for this API is around `72%`. To ensure consistent behavior across all browsers, Qwik UI provides a `polyfill` for browsers that do not support the API natively.
26+
The Qwik UI Popover component is designed to align with the [HTML Popover API Specification](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API). As of now, native support for this API is around `82.5%`, and in almost every browser.
27+
28+
To ensure consistent behavior across all browsers, Qwik UI provides a `polyfill` for browser versions that do not support the API natively.
2729

2830
<div class="flex flex-col gap-2">
2931
<div>

packages/kit-headless/src/components/modal/modal.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,17 @@ export const Modal = component$((props: ModalProps) => {
6363
});
6464
});
6565

66-
const closeOnBackdropClick$ = $(async (event: MouseEvent) => {
66+
const closeOnBackdropClick$ = $(async (e: MouseEvent) => {
6767
if (props.alert === true || props.closeOnBackdropClick === false) {
6868
return;
6969
}
7070

71-
console.log('CLICK BACKDROP', event);
71+
// We do not want to close elements that dangle outside of the modal
72+
if (!(e.target instanceof HTMLDialogElement)) {
73+
return;
74+
}
7275

73-
if (await wasModalBackdropClicked(modalRef.value, event)) {
76+
if (await wasModalBackdropClicked(modalRef.value, e)) {
7477
showSig.value = false;
7578
}
7679
});

packages/kit-headless/src/components/modal/use-modal.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ export function useModal() {
5050
if (animationDuration === '0s' && transitionDuration === '0s') {
5151
modal.classList.remove('modal-closing');
5252
enableBodyScroll(modal);
53-
console.log('modal closed: ', modal);
5453
modal.close();
5554
}
5655
});

packages/kit-headless/src/components/popover/popover-impl.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const PopoverImpl = component$<PopoverImplProps>((props) => {
5555
/** have we rendered on the client yet? 0: no, 1: force, 2: yes */
5656
const hasRenderedOnClientSig = useSignal(isServer ? 0 : 2);
5757
const teleportSig = useSignal(false);
58+
const hasTopLayerAncestorSig = useSignal(false);
5859

5960
// This forces a re-render on each popover instance when the signal changes
6061
if (hasRenderedOnClientSig.value === 1) {
@@ -69,6 +70,19 @@ export const PopoverImpl = component$<PopoverImplProps>((props) => {
6970

7071
isPolyfillSig.value = true;
7172

73+
const findTopLayerAncestor$ = $((element: HTMLElement | null): HTMLElement | null => {
74+
while (element?.parentElement) {
75+
if (
76+
element.parentElement?.tagName === 'DIALOG' ||
77+
element.parentElement?.hasAttribute('popover')
78+
) {
79+
return element.parentElement;
80+
}
81+
element = element.parentElement;
82+
}
83+
return null;
84+
});
85+
7286
let polyfillContainer: HTMLDivElement | null = document.querySelector(
7387
'div[data-qwik-ui-popover-polyfill]',
7488
);
@@ -80,7 +94,13 @@ export const PopoverImpl = component$<PopoverImplProps>((props) => {
8094
}
8195

8296
if (popoverRef.value) {
83-
polyfillContainer.appendChild(popoverRef.value);
97+
const topLayerAncestor = await findTopLayerAncestor$(popoverRef.value!);
98+
99+
if (topLayerAncestor === null) {
100+
polyfillContainer.appendChild(popoverRef.value);
101+
} else {
102+
hasTopLayerAncestorSig.value = true;
103+
}
84104

85105
document.dispatchEvent(new CustomEvent('showpopover'));
86106

@@ -119,7 +139,8 @@ export const PopoverImpl = component$<PopoverImplProps>((props) => {
119139
// move opened polyfill popovers are always above the other
120140
if (
121141
popoverRef.value.classList.contains(':popover-open') &&
122-
popoverRef.value.parentElement
142+
popoverRef.value.parentElement &&
143+
!hasTopLayerAncestorSig.value
123144
) {
124145
popoverRef.value.parentElement.appendChild(popoverRef.value);
125146
}

packages/kit-headless/src/components/popover/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
// TODO: fix the setTimeouts
2+
// TODO: move this file into the usePopover hook
3+
14
/**
25
* Adds CSS-Class to support popover-opening-animation
36
*/
47
export function supportShowAnimation(popover: HTMLElement, isPolyfill: boolean) {
58
const { transitionDuration } = getComputedStyle(popover);
69

710
if (isPolyfill) {
8-
console.log('polyfill run!');
911
// polyfill needs a bit of extra time to execute
1012
if (transitionDuration !== '0s') {
1113
setTimeout(() => {

0 commit comments

Comments
 (0)