2021-02-02 05:28:25 +01:00
|
|
|
import { h, Fragment } from 'preact';
|
|
|
|
import { createPortal } from 'preact/compat';
|
2021-02-04 19:13:47 +01:00
|
|
|
import { DarkModeProvider } from '../context';
|
2021-02-04 00:43:24 +01:00
|
|
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
|
|
|
|
|
|
const WINDOW_PADDING = 20;
|
2021-02-02 05:28:25 +01:00
|
|
|
|
|
|
|
export default function RelativeModal({ className, role = 'dialog', children, onDismiss, portalRootID, relativeTo }) {
|
|
|
|
const [position, setPosition] = useState({ top: -999, left: 0, width: 0 });
|
|
|
|
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]
|
|
|
|
);
|
|
|
|
|
2021-02-04 00:43:24 +01:00
|
|
|
useLayoutEffect(() => {
|
2021-02-02 05:28:25 +01:00
|
|
|
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, height } = relativeTo.current.getBoundingClientRect();
|
|
|
|
let top = y + height;
|
|
|
|
let left = x;
|
|
|
|
// too far right
|
2021-02-04 00:43:24 +01:00
|
|
|
if (left + menuWidth >= windowWidth - WINDOW_PADDING) {
|
|
|
|
left = windowWidth - menuWidth - WINDOW_PADDING;
|
2021-02-02 05:28:25 +01:00
|
|
|
}
|
|
|
|
// too far left
|
2021-02-04 00:43:24 +01:00
|
|
|
else if (left < WINDOW_PADDING) {
|
|
|
|
left = WINDOW_PADDING;
|
2021-02-02 05:28:25 +01:00
|
|
|
}
|
|
|
|
// too close to bottom
|
2021-02-04 00:43:24 +01:00
|
|
|
if (top + menuHeight > windowHeight - WINDOW_PADDING) {
|
2021-02-02 05:28:25 +01:00
|
|
|
top = y - menuHeight;
|
|
|
|
}
|
|
|
|
setPosition({ left, top, width });
|
|
|
|
const focusable = ref.current.querySelector('[tabindex]');
|
|
|
|
focusable && console.log('focusing');
|
|
|
|
focusable && focusable.focus();
|
|
|
|
}
|
|
|
|
}, [relativeTo && relativeTo.current, ref && ref.current]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (position.width) {
|
|
|
|
setShow(true);
|
|
|
|
} else {
|
|
|
|
setShow(false);
|
|
|
|
}
|
|
|
|
}, [show, position.width, ref.current]);
|
|
|
|
|
|
|
|
const menu = (
|
2021-02-04 19:13:47 +01:00
|
|
|
<DarkModeProvider>
|
2021-02-02 05:28:25 +01:00
|
|
|
<div className="absolute inset-0" onClick={handleDismiss} />
|
|
|
|
<div
|
2021-01-31 15:24:04 +01:00
|
|
|
className={`z-10 bg-white dark:bg-gray-700 dark:text-white absolute shadow-lg rounded w-auto max-h-48 transition-all duration-75 transform scale-90 opacity-0 ${
|
2021-02-02 05:28:25 +01:00
|
|
|
show ? 'scale-100 opacity-100' : ''
|
|
|
|
} ${className}`}
|
|
|
|
onkeydown={handleKeydown}
|
|
|
|
role={role}
|
|
|
|
ref={ref}
|
2021-01-31 15:24:04 +01:00
|
|
|
style={
|
|
|
|
position.width > 0 ? `min-width: ${position.width}px; top: ${position.top}px; left: ${position.left}px` : ''
|
|
|
|
}
|
2021-02-02 05:28:25 +01:00
|
|
|
>
|
|
|
|
{children}
|
|
|
|
</div>
|
2021-02-04 19:13:47 +01:00
|
|
|
</DarkModeProvider>
|
2021-02-02 05:28:25 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
return portalRoot ? createPortal(menu, portalRoot) : menu;
|
|
|
|
}
|