mirror of
https://github.com/mue/mue.git
synced 2026-07-22 00:07:23 +02:00
feat(Dropdown): optimize position calculation and add scroll/resize handling
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import variables from 'config/variables';
|
import variables from 'config/variables';
|
||||||
import { memo, useState, useCallback, useRef, useEffect } from 'react';
|
import { memo, useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { MdExpandMore, MdCheck, MdRefresh, MdClose } from 'react-icons/md';
|
import { MdExpandMore, MdCheck, MdRefresh, MdClose } from 'react-icons/md';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -47,6 +47,12 @@ const Dropdown = memo((props) => {
|
|||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, [closeDropdown]);
|
}, [closeDropdown]);
|
||||||
|
|
||||||
|
// Memoize items count to avoid unnecessary recalculations
|
||||||
|
const itemsCount = useMemo(() =>
|
||||||
|
props.items.filter((i) => i !== null).length,
|
||||||
|
[props.items]
|
||||||
|
);
|
||||||
|
|
||||||
const calculatePosition = useCallback(() => {
|
const calculatePosition = useCallback(() => {
|
||||||
if (controlRef.current) {
|
if (controlRef.current) {
|
||||||
const rect = controlRef.current.getBoundingClientRect();
|
const rect = controlRef.current.getBoundingClientRect();
|
||||||
@@ -54,7 +60,7 @@ const Dropdown = memo((props) => {
|
|||||||
const viewportHeight = window.innerHeight;
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
// Estimate menu height (will be more accurate after first render)
|
// Estimate menu height (will be more accurate after first render)
|
||||||
const estimatedMenuHeight = Math.min(props.items.filter((i) => i !== null).length * 44, 250);
|
const estimatedMenuHeight = Math.min(itemsCount * 44, 250);
|
||||||
|
|
||||||
// Calculate if dropdown would overflow bottom of viewport
|
// Calculate if dropdown would overflow bottom of viewport
|
||||||
const spaceBelow = viewportHeight - rect.bottom - gap;
|
const spaceBelow = viewportHeight - rect.bottom - gap;
|
||||||
@@ -72,7 +78,7 @@ const Dropdown = memo((props) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { top: 0, left: 0, width: 0, maxHeight: 250, flipped: false };
|
return { top: 0, left: 0, width: 0, maxHeight: 250, flipped: false };
|
||||||
}, [props.items]);
|
}, [itemsCount]);
|
||||||
|
|
||||||
const openDropdown = useCallback(() => {
|
const openDropdown = useCallback(() => {
|
||||||
const position = calculatePosition();
|
const position = calculatePosition();
|
||||||
@@ -80,6 +86,50 @@ const Dropdown = memo((props) => {
|
|||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}, [calculatePosition]);
|
}, [calculatePosition]);
|
||||||
|
|
||||||
|
// Update dropdown position on scroll or resize
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
let rafId = null;
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
if (rafId) window.cancelAnimationFrame(rafId);
|
||||||
|
|
||||||
|
rafId = window.requestAnimationFrame(() => {
|
||||||
|
const newPosition = calculatePosition();
|
||||||
|
setMenuPosition(newPosition);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen to window scroll and resize
|
||||||
|
window.addEventListener('scroll', updatePosition, { passive: true });
|
||||||
|
window.addEventListener('resize', updatePosition, { passive: true });
|
||||||
|
|
||||||
|
// Find and listen to scrollable ancestors
|
||||||
|
let element = controlRef.current?.parentElement;
|
||||||
|
const scrollableElements = [];
|
||||||
|
while (element) {
|
||||||
|
const hasScrollableContent = element.scrollHeight > element.clientHeight;
|
||||||
|
const overflowYStyle = window.getComputedStyle(element).overflowY;
|
||||||
|
const isOverflowYScrollable = overflowYStyle !== 'visible' && overflowYStyle !== 'hidden';
|
||||||
|
|
||||||
|
if (hasScrollableContent && isOverflowYScrollable) {
|
||||||
|
scrollableElements.push(element);
|
||||||
|
element.addEventListener('scroll', updatePosition, { passive: true });
|
||||||
|
}
|
||||||
|
element = element.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (rafId) window.cancelAnimationFrame(rafId);
|
||||||
|
window.removeEventListener('scroll', updatePosition);
|
||||||
|
window.removeEventListener('resize', updatePosition);
|
||||||
|
scrollableElements.forEach(el =>
|
||||||
|
el.removeEventListener('scroll', updatePosition)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [isOpen, calculatePosition]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && props.searchable && searchInputRef.current) {
|
if (isOpen && props.searchable && searchInputRef.current) {
|
||||||
// Focus the search input when dropdown opens
|
// Focus the search input when dropdown opens
|
||||||
|
|||||||
Reference in New Issue
Block a user