forked from blakeblackshear/frigate
-
Notifications
You must be signed in to change notification settings - Fork 0
/
RelativeModal.jsx
116 lines (103 loc) · 3.49 KB
/
RelativeModal.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import { h, Fragment } from 'preact';
import { createPortal } from 'preact/compat';
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
const WINDOW_PADDING = 10;
export default function RelativeModal({
className,
role = 'dialog',
children,
onDismiss,
portalRootID,
relativeTo,
widthRelative = false,
}) {
const [position, setPosition] = useState({ top: -999, left: -999 });
const [show, setShow] = useState(false);
const portalRoot = portalRootID && document.getElementById(portalRootID);
const ref = useRef(null);
const handleDismiss = useCallback(
(event) => {
onDismiss && onDismiss(event);
},
[onDismiss]
);
const handleKeydown = useCallback(
(event) => {
const focusable = ref.current.querySelectorAll('[tabindex]');
if (event.key === 'Tab' && focusable.length) {
if (event.shiftKey && document.activeElement === focusable[0]) {
focusable[focusable.length - 1].focus();
event.preventDefault();
} else if (document.activeElement === focusable[focusable.length - 1]) {
focusable[0].focus();
event.preventDefault();
}
return;
}
if (event.key === 'Escape') {
setShow(false);
return;
}
},
[ref.current]
);
useLayoutEffect(() => {
if (ref && ref.current && relativeTo && relativeTo.current) {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const { width: menuWidth, height: menuHeight } = ref.current.getBoundingClientRect();
const { x, y, width: relativeWidth, height } = relativeTo.current.getBoundingClientRect();
const width = widthRelative ? relativeWidth : menuWidth;
let top = y + height;
let left = x;
// too far right
if (left + width >= windowWidth - WINDOW_PADDING) {
left = windowWidth - width - WINDOW_PADDING;
}
// too far left
else if (left < WINDOW_PADDING) {
left = WINDOW_PADDING;
}
// too close to bottom
if (top + menuHeight > windowHeight - WINDOW_PADDING) {
top = y - menuHeight;
}
if (top <= WINDOW_PADDING) {
top = WINDOW_PADDING;
}
const maxHeight = windowHeight - WINDOW_PADDING * 2 > menuHeight ? null : windowHeight - WINDOW_PADDING * 2;
const newPosition = { left: left + window.scrollX, top: top + window.scrollY, maxHeight };
if (widthRelative) {
newPosition.width = relativeWidth;
}
setPosition(newPosition);
const focusable = ref.current.querySelector('[tabindex]');
focusable && focusable.focus();
}
}, [relativeTo && relativeTo.current, ref && ref.current, widthRelative]);
useEffect(() => {
if (position.top >= 0) {
setShow(true);
} else {
setShow(false);
}
}, [show, position.top, ref.current]);
const menu = (
<Fragment>
<div key="scrim" className="absolute inset-0 z-10" onClick={handleDismiss} />
<div
key="menu"
className={`z-10 bg-white dark:bg-gray-700 dark:text-white absolute shadow-lg rounded w-auto h-auto transition-all duration-75 transform scale-90 opacity-0 overflow-scroll ${
show ? 'scale-100 opacity-100' : ''
} ${className}`}
onkeydown={handleKeydown}
role={role}
ref={ref}
style={position.top >= 0 ? position : null}
>
{children}
</div>
</Fragment>
);
return portalRoot ? createPortal(menu, portalRoot) : menu;
}