refactor(web): menu positioning

This commit is contained in:
Paul Armstrong 2021-02-06 20:59:11 -08:00 committed by Blake Blackshear
parent 19bd5ace7d
commit f00628f4e5
4 changed files with 32 additions and 23 deletions

View File

@ -82,7 +82,7 @@ export default function AppBar({ title }) {
</div> </div>
<LinkedLogo /> <LinkedLogo />
<div className="flex-grow-1 flex justify-end w-full"> <div className="flex-grow-1 flex justify-end w-full">
<div ref={moreRef}> <div className="w-auto" ref={moreRef}>
<Button color="black" className="rounded-full w-12 h-12" onClick={handleShowMenu} type="text"> <Button color="black" className="rounded-full w-12 h-12" onClick={handleShowMenu} type="text">
<MoreIcon /> <MoreIcon />
</Button> </Button>

View File

@ -2,7 +2,7 @@ import { h } from 'preact';
import RelativeModal from './RelativeModal'; import RelativeModal from './RelativeModal';
import { useCallback, useEffect } from 'preact/hooks'; import { useCallback, useEffect } from 'preact/hooks';
export default function Menu({ className, children, onDismiss, relativeTo }) { export default function Menu({ className, children, onDismiss, relativeTo, widthRelative }) {
return relativeTo ? ( return relativeTo ? (
<RelativeModal <RelativeModal
children={children} children={children}
@ -11,6 +11,7 @@ export default function Menu({ className, children, onDismiss, relativeTo }) {
onDismiss={onDismiss} onDismiss={onDismiss}
portalRootID="menus" portalRootID="menus"
relativeTo={relativeTo} relativeTo={relativeTo}
widthRelative={widthRelative}
/> />
) : null; ) : null;
} }
@ -38,11 +39,11 @@ export function MenuItem({ focus, icon: Icon, label, onSelect, value }) {
role="option" role="option"
> >
{Icon ? ( {Icon ? (
<div className="w-6 h-6 self-center mr-4 text-gray-500"> <div className="w-6 h-6 self-center mr-4 text-gray-500 flex-shrink-0">
<Icon /> <Icon />
</div> </div>
) : null} ) : null}
{label} <div class="whitespace-nowrap">{label}</div>
</div> </div>
); );
} }

View File

@ -2,10 +2,18 @@ import { h, Fragment } from 'preact';
import { createPortal } from 'preact/compat'; import { createPortal } from 'preact/compat';
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks'; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
const WINDOW_PADDING = 20; const WINDOW_PADDING = 10;
export default function RelativeModal({ className, role = 'dialog', children, onDismiss, portalRootID, relativeTo }) { export default function RelativeModal({
const [position, setPosition] = useState({ top: -999, left: 0, width: 0 }); className,
role = 'dialog',
children,
onDismiss,
portalRootID,
relativeTo,
widthRelative = false,
}) {
const [position, setPosition] = useState({ top: -999, left: -999 });
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const portalRoot = portalRootID && document.getElementById(portalRootID); const portalRoot = portalRootID && document.getElementById(portalRootID);
const ref = useRef(null); const ref = useRef(null);
@ -44,12 +52,15 @@ export default function RelativeModal({ className, role = 'dialog', children, on
const windowWidth = window.innerWidth; const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight; const windowHeight = window.innerHeight;
const { width: menuWidth, height: menuHeight } = ref.current.getBoundingClientRect(); const { width: menuWidth, height: menuHeight } = ref.current.getBoundingClientRect();
const { x, y, width, height } = relativeTo.current.getBoundingClientRect(); const { x, y, width: relativeWidth, height } = relativeTo.current.getBoundingClientRect();
const width = widthRelative ? relativeWidth : menuWidth;
let top = y + height; let top = y + height;
let left = x; let left = x;
// too far right // too far right
if (left + menuWidth >= windowWidth - WINDOW_PADDING) { if (left + width >= windowWidth - WINDOW_PADDING) {
left = windowWidth - menuWidth - WINDOW_PADDING; left = windowWidth - width - WINDOW_PADDING;
} }
// too far left // too far left
else if (left < WINDOW_PADDING) { else if (left < WINDOW_PADDING) {
@ -65,20 +76,23 @@ export default function RelativeModal({ className, role = 'dialog', children, on
} }
const maxHeight = windowHeight - WINDOW_PADDING * 2 > menuHeight ? null : windowHeight - WINDOW_PADDING * 2; const maxHeight = windowHeight - WINDOW_PADDING * 2 > menuHeight ? null : windowHeight - WINDOW_PADDING * 2;
setPosition({ left, top: top + window.scrollY, width, height: maxHeight }); const newPosition = { left: left + window.scrollX, top: top + window.scrollY, maxHeight };
if (widthRelative) {
newPosition.width = relativeWidth;
}
setPosition(newPosition);
const focusable = ref.current.querySelector('[tabindex]'); const focusable = ref.current.querySelector('[tabindex]');
focusable && console.log('focusing');
focusable && focusable.focus(); focusable && focusable.focus();
} }
}, [relativeTo && relativeTo.current, ref && ref.current]); }, [relativeTo && relativeTo.current, ref && ref.current, widthRelative]);
useEffect(() => { useEffect(() => {
if (position.width) { if (position.top >= 0) {
setShow(true); setShow(true);
} else { } else {
setShow(false); setShow(false);
} }
}, [show, position.width, ref.current]); }, [show, position.top, ref.current]);
const menu = ( const menu = (
<Fragment> <Fragment>
@ -91,13 +105,7 @@ export default function RelativeModal({ className, role = 'dialog', children, on
onkeydown={handleKeydown} onkeydown={handleKeydown}
role={role} role={role}
ref={ref} ref={ref}
style={ style={position.top >= 0 ? position : null}
position.width > 0
? `min-width: ${position.width}px; ${position.height ? `max-height: ${position.height}px;` : ''} top: ${
position.top
}px; left: ${position.left}px`
: ''
}
> >
{children} {children}
</div> </div>

View File

@ -95,7 +95,7 @@ export default function Select({ label, onChange, options: inputOptions = [], se
value={options[selected]?.label} value={options[selected]?.label}
/> />
{showMenu ? ( {showMenu ? (
<Menu className="rounded-t-none" onDismiss={handleDismiss} relativeTo={ref}> <Menu className="rounded-t-none" onDismiss={handleDismiss} relativeTo={ref} widthRelative>
{options.map(({ value, label }, i) => ( {options.map(({ value, label }, i) => (
<MenuItem key={value} label={label} focus={focused === i} onSelect={handleSelect} value={value} /> <MenuItem key={value} label={label} focus={focused === i} onSelect={handleSelect} value={value} />
))} ))}