Better Dropdown size calculation for scrolling when not enough room

This commit is contained in:
Gregory Schier
2024-10-02 16:17:28 -07:00
parent e5c6c31e02
commit 4b7712df80

View File

@@ -229,17 +229,8 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
defaultSelectedIndex ?? null, defaultSelectedIndex ?? null,
[defaultSelectedIndex], [defaultSelectedIndex],
); );
const [menuStyles, setMenuStyles] = useState<CSSProperties>({});
const [filter, setFilter] = useState<string>(''); const [filter, setFilter] = useState<string>('');
// Calculate the max height so we can scroll
const initMenu = useCallback((el: HTMLDivElement | null) => {
if (el === null) return {};
const windowBox = document.documentElement.getBoundingClientRect();
const menuBox = el.getBoundingClientRect();
setMenuStyles({ maxHeight: windowBox.height - menuBox.top - 5 });
}, []);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
onClose(); onClose();
setSelectedIndex(null); setSelectedIndex(null);
@@ -355,11 +346,12 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
[handleClose, handleNext, handlePrev, handleSelect, items, selectedIndex], [handleClose, handleNext, handlePrev, handleSelect, items, selectedIndex],
); );
const { containerStyles, triangleStyles } = useMemo<{ const styles = useMemo<{
containerStyles: CSSProperties; container: CSSProperties;
triangleStyles: CSSProperties | null; menu: CSSProperties;
triangle: CSSProperties;
}>(() => { }>(() => {
if (triggerShape == null) return { containerStyles: {}, triangleStyles: null }; if (triggerShape == null) return { container: {}, triangle: {}, menu: {} };
const menuMarginY = 5; const menuMarginY = 5;
const docRect = document.documentElement.getBoundingClientRect(); const docRect = document.documentElement.getBoundingClientRect();
@@ -371,27 +363,31 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
const onRight = horizontalSpaceRemaining < 200; const onRight = horizontalSpaceRemaining < 200;
const upsideDown = heightBelow < heightAbove && heightBelow < items.length * 25 + 20 + 200; const upsideDown = heightBelow < heightAbove && heightBelow < items.length * 25 + 20 + 200;
const triggerWidth = triggerShape.right - triggerShape.left; const triggerWidth = triggerShape.right - triggerShape.left;
const containerStyles = { return {
top: !upsideDown ? top + menuMarginY : undefined, container: {
bottom: upsideDown top: !upsideDown ? top + menuMarginY : undefined,
? docRect.height - top - (triggerShape.top - triggerShape.bottom) + menuMarginY bottom: upsideDown
: undefined, ? docRect.height - top - (triggerShape.top - triggerShape.bottom) + menuMarginY
right: onRight ? docRect.width - triggerShape.right : undefined, : undefined,
left: !onRight ? triggerShape.left : undefined, right: onRight ? docRect.width - triggerShape.right : undefined,
minWidth: fullWidth ? triggerWidth : undefined, left: !onRight ? triggerShape.left : undefined,
maxWidth: '40rem', minWidth: fullWidth ? triggerWidth : undefined,
maxWidth: '40rem',
},
triangle: {
width: '0.4rem',
height: '0.4rem',
...(onRight
? { right: width / 2, marginRight: '-0.2rem' }
: { left: width / 2, marginLeft: '-0.2rem' }),
...(upsideDown
? { bottom: '-0.2rem', rotate: '225deg' }
: { top: '-0.2rem', rotate: '45deg' }),
},
menu: {
maxHeight: `${(upsideDown ? heightAbove : heightBelow) - 15}px`,
},
}; };
const triangleStyles: CSSProperties = {
width: '0.4rem',
height: '0.4rem',
...(onRight
? { right: width / 2, marginRight: '-0.2rem' }
: { left: width / 2, marginLeft: '-0.2rem' }),
...(upsideDown
? { bottom: '-0.2rem', rotate: '225deg' }
: { top: '-0.2rem', rotate: '45deg' }),
};
return { containerStyles, triangleStyles };
}, [fullWidth, items.length, triggerShape]); }, [fullWidth, items.length, triggerShape]);
const filteredItems = useMemo( const filteredItems = useMemo(
@@ -435,61 +431,58 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
role="menu" role="menu"
aria-orientation="vertical" aria-orientation="vertical"
dir="ltr" dir="ltr"
style={containerStyles} style={styles.container}
className={classNames(className, 'outline-none my-1 pointer-events-auto fixed z-50')} className={classNames(className, 'outline-none my-1 pointer-events-auto fixed z-50')}
> >
{triangleStyles && showTriangle && ( {showTriangle && (
<span <span
aria-hidden aria-hidden
style={triangleStyles} style={styles.triangle}
className="bg-surface absolute border-border-subtle border-t border-l" className="bg-surface absolute border-border-subtle border-t border-l"
/> />
)} )}
{containerStyles && ( <VStack
<VStack style={styles.menu}
ref={initMenu} className={classNames(
style={menuStyles} className,
className={classNames( 'h-auto bg-surface rounded-md shadow-lg py-1.5 border',
className, 'border-border-subtle overflow-auto mx-0.5',
'h-auto bg-surface rounded-md shadow-lg py-1.5 border', )}
'border-border-subtle overflow-auto mx-0.5', >
)} {filter && (
> <HStack
{filter && ( space={2}
<HStack className="pb-0.5 px-1.5 mb-2 text-sm border border-border-subtle mx-2 rounded font-mono h-xs"
space={2} >
className="pb-0.5 px-1.5 mb-2 text-sm border border-border-subtle mx-2 rounded font-mono h-xs" <Icon icon="search" size="xs" className="text-text-subtle" />
> <div className="text">{filter}</div>
<Icon icon="search" size="xs" className="text-text-subtle" /> </HStack>
<div className="text">{filter}</div> )}
</HStack> {filteredItems.length === 0 && (
)} <span className="text-text-subtlest text-center px-2 py-1">No matches</span>
{filteredItems.length === 0 && ( )}
<span className="text-text-subtlest text-center px-2 py-1">No matches</span> {filteredItems.map((item, i) => {
)} if (item.hidden) {
{filteredItems.map((item, i) => { return null;
if (item.hidden) { }
return null; if (item.type === 'separator') {
}
if (item.type === 'separator') {
return (
<Separator key={i} className={classNames('my-1.5', item.label && 'ml-2')}>
{item.label}
</Separator>
);
}
return ( return (
<MenuItem <Separator key={i} className={classNames('my-1.5', item.label && 'ml-2')}>
focused={i === selectedIndex} {item.label}
onFocus={handleFocus} </Separator>
onSelect={handleSelect}
key={item.key}
item={item}
/>
); );
})} }
</VStack> return (
)} <MenuItem
focused={i === selectedIndex}
onFocus={handleFocus}
onSelect={handleSelect}
key={item.key}
item={item}
/>
);
})}
</VStack>
</motion.div> </motion.div>
</div> </div>
</Overlay> </Overlay>