Skip to content

Commit de7ce33

Browse files
authored
feat(Overlay): allow popper virtual elements as targets (#64)
1 parent 155aa93 commit de7ce33

File tree

3 files changed

+61
-11
lines changed

3 files changed

+61
-11
lines changed

src/Overlay.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import usePopper, {
88
Placement,
99
UsePopperOptions,
1010
UsePopperState,
11+
VirtualElement,
1112
} from './usePopper';
1213
import useRootClose, { RootCloseOptions } from './useRootClose';
1314
import useWaitForDOMRef, { DOMContainer } from './useWaitForDOMRef';
@@ -61,16 +62,16 @@ export interface OverlayProps extends TransitionCallbacks {
6162
popperConfig?: Omit<UsePopperOptions, 'placement'>;
6263

6364
/**
64-
* A DOM Element, Ref to an element, or function that returns either. The `container` will have the Portal children
65-
* appended to it.
65+
* A DOM Element, [Virtual Element](https://popper.js.org/docs/v2/virtual-elements/), Ref to an element, or
66+
* function that returns either. The `target` element is where the overlay is positioned relative to.
6667
*/
6768
container?: DOMContainer;
6869

6970
/**
7071
* A DOM Element, Ref to an element, or function that returns either. The `target` element is where
7172
* the overlay is positioned relative to.
7273
*/
73-
target: DOMContainer;
74+
target: DOMContainer<HTMLElement | VirtualElement>;
7475

7576
/**
7677
* Set the visibility of the Overlay

src/useWaitForDOMRef.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ import ownerDocument from 'dom-helpers/ownerDocument';
22
import canUseDOM from 'dom-helpers/canUseDOM';
33
import { useState, useEffect } from 'react';
44
import useWindow from './useWindow';
5+
import { VirtualElement } from './usePopper';
56

6-
export type DOMContainer<T extends HTMLElement = HTMLElement> =
7-
| T
8-
| React.RefObject<T>
9-
| null
10-
| (() => T | React.RefObject<T> | null);
7+
export type DOMContainer<T extends HTMLElement | VirtualElement = HTMLElement> =
8+
T | React.RefObject<T> | null | (() => T | React.RefObject<T> | null);
119

12-
export const resolveContainerRef = <T extends HTMLElement>(
10+
export const resolveContainerRef = <T extends HTMLElement | VirtualElement>(
1311
ref: DOMContainer<T> | undefined,
1412
document?: Document,
1513
): T | HTMLBodyElement | null => {
@@ -18,12 +16,14 @@ export const resolveContainerRef = <T extends HTMLElement>(
1816
if (typeof ref === 'function') ref = ref();
1917

2018
if (ref && 'current' in ref) ref = ref.current;
21-
if (ref?.nodeType) return ref || null;
19+
if (ref && ('nodeType' in ref || ref.getBoundingClientRect)) return ref;
2220

2321
return null;
2422
};
2523

26-
export default function useWaitForDOMRef<T extends HTMLElement = HTMLElement>(
24+
export default function useWaitForDOMRef<
25+
T extends HTMLElement | VirtualElement = HTMLElement,
26+
>(
2727
ref: DOMContainer<T> | undefined,
2828
onResolved?: (element: T | HTMLBodyElement) => void,
2929
) {

test/WaitForContainerSpec.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,30 @@ describe('useWaitForDOMRef', () => {
2929
onResolved.should.have.been.calledOnce;
3030
});
3131

32+
it('should resolve on first render if possible (Virtual Element)', () => {
33+
let renderCount = 0;
34+
const container = {
35+
getBoundingClientRect: () => new DOMRect(),
36+
};
37+
38+
function Test({ container, onResolved }) {
39+
useWaitForDOMRef(container, onResolved);
40+
renderCount++;
41+
return null;
42+
}
43+
44+
const onResolved = sinon.spy((resolved) => {
45+
expect(resolved).to.equal(container);
46+
});
47+
48+
act(() => {
49+
mount(<Test container={container} onResolved={onResolved} />);
50+
});
51+
52+
renderCount.should.equal(1);
53+
onResolved.should.have.been.calledOnce;
54+
});
55+
3256
it('should resolve on first render if possible (ref)', () => {
3357
let renderCount = 0;
3458
const container = React.createRef();
@@ -52,6 +76,31 @@ describe('useWaitForDOMRef', () => {
5276
onResolved.should.have.been.calledOnce;
5377
});
5478

79+
it('should resolve on first render if possible (Virtual Element ref)', () => {
80+
let renderCount = 0;
81+
const container = React.createRef();
82+
container.current = {
83+
getBoundingClientRect: () => new DOMRect(),
84+
};
85+
86+
function Test({ container, onResolved }) {
87+
useWaitForDOMRef(container, onResolved);
88+
renderCount++;
89+
return null;
90+
}
91+
92+
const onResolved = sinon.spy((resolved) => {
93+
expect(resolved).to.equal(container.current);
94+
});
95+
96+
act(() => {
97+
mount(<Test container={container} onResolved={onResolved} />);
98+
});
99+
100+
renderCount.should.equal(1);
101+
onResolved.should.have.been.calledOnce;
102+
});
103+
55104
it('should resolve on first render if possible (function)', () => {
56105
const div = document.createElement('div');
57106
const container = () => div;

0 commit comments

Comments
 (0)