mirror of
https://github.com/mue/mue.git
synced 2026-07-27 02:31:06 +02:00
feat(Dropdown): enhance dropdown menu with position calculation and closing animations
This commit is contained in:
@@ -44,16 +44,38 @@ const Dropdown = memo((props) => {
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [closeDropdown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && controlRef.current) {
|
||||
const calculatePosition = useCallback(() => {
|
||||
if (controlRef.current) {
|
||||
const rect = controlRef.current.getBoundingClientRect();
|
||||
setMenuPosition({
|
||||
top: rect.bottom + 4,
|
||||
const gap = 4;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Estimate menu height (will be more accurate after first render)
|
||||
const estimatedMenuHeight = Math.min(props.items.filter((i) => i !== null).length * 44, 250);
|
||||
|
||||
// Calculate if dropdown would overflow bottom of viewport
|
||||
const spaceBelow = viewportHeight - rect.bottom - gap;
|
||||
const spaceAbove = rect.top - gap;
|
||||
|
||||
// If not enough space below but more space above, flip to top
|
||||
const shouldFlipUp = spaceBelow < estimatedMenuHeight && spaceAbove > spaceBelow;
|
||||
|
||||
return {
|
||||
top: shouldFlipUp ? rect.top - gap : rect.bottom + gap,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
});
|
||||
maxHeight: shouldFlipUp ? Math.min(250, spaceAbove) : Math.min(250, spaceBelow),
|
||||
flipped: shouldFlipUp,
|
||||
};
|
||||
}
|
||||
}, [isOpen]);
|
||||
return { top: 0, left: 0, width: 0, maxHeight: 250, flipped: false };
|
||||
}, [props.items]);
|
||||
|
||||
const openDropdown = useCallback(() => {
|
||||
const position = calculatePosition();
|
||||
setMenuPosition(position);
|
||||
setIsOpen(true);
|
||||
}, [calculatePosition]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(newValue) => {
|
||||
@@ -98,7 +120,7 @@ const Dropdown = memo((props) => {
|
||||
if (isOpen) {
|
||||
closeDropdown();
|
||||
} else {
|
||||
setIsOpen(true);
|
||||
openDropdown();
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
@@ -107,7 +129,7 @@ const Dropdown = memo((props) => {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
openDropdown();
|
||||
} else {
|
||||
setFocusedIndex((prev) =>
|
||||
prev < props.items.filter((i) => i !== null).length - 1 ? prev + 1 : prev,
|
||||
@@ -122,7 +144,7 @@ const Dropdown = memo((props) => {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[isOpen, props.items, props.disabled],
|
||||
[isOpen, props.items, props.disabled, openDropdown, closeDropdown],
|
||||
);
|
||||
|
||||
const handleOptionKeyDown = useCallback(
|
||||
@@ -164,7 +186,7 @@ const Dropdown = memo((props) => {
|
||||
if (isOpen) {
|
||||
closeDropdown();
|
||||
} else {
|
||||
setIsOpen(true);
|
||||
openDropdown();
|
||||
}
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -181,13 +203,15 @@ const Dropdown = memo((props) => {
|
||||
createPortal(
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={`dropdown-menu ${isClosing ? 'closing' : ''}`}
|
||||
className={`dropdown-menu ${isClosing ? 'closing' : ''} ${menuPosition.flipped ? 'flipped' : ''}`}
|
||||
role="listbox"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: `${menuPosition.top}px`,
|
||||
left: `${menuPosition.left}px`,
|
||||
width: `${menuPosition.width}px`,
|
||||
maxHeight: menuPosition.maxHeight ? `${menuPosition.maxHeight}px` : '250px',
|
||||
transform: menuPosition.flipped ? 'translateY(-100%)' : 'none',
|
||||
}}
|
||||
>
|
||||
{props.items.map((item, index) =>
|
||||
|
||||
@@ -13,6 +13,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
@include keyframes(dropdownSlideOut) {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@include keyframes(dropdownSlideInUp) {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-100%) translateY(10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
@include keyframes(dropdownSlideOutUp) {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-100%) translateY(10px);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
@@ -21,7 +57,6 @@
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
@@ -121,98 +156,107 @@
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
z-index: 9999;
|
||||
@include animation(dropdownSlideIn 0.2s ease-out);
|
||||
.dropdown-menu {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
z-index: 9999;
|
||||
@include animation(dropdownSlideIn 0.2s ease-out);
|
||||
will-change: transform, opacity;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-background);
|
||||
border: 1px solid t($modal-sidebarActive);
|
||||
border-radius: t($borderRadius);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
@include themed {
|
||||
background: t($modal-background);
|
||||
border: 1px solid t($modal-sidebarActive);
|
||||
border-radius: t($borderRadius);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&.flipped {
|
||||
@include animation(dropdownSlideInUp 0.2s ease-out);
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
@include themed {
|
||||
background: t($modal-sidebar);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@include themed {
|
||||
background: t($modal-sidebarActive);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themed {
|
||||
background: t($color);
|
||||
}
|
||||
}
|
||||
&.closing {
|
||||
@include animation(dropdownSlideOutUp 0.2s ease-out forwards);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
&.closing:not(.flipped) {
|
||||
@include animation(dropdownSlideOut 0.2s ease-out forwards);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
@include themed {
|
||||
color: t($color);
|
||||
background: t($modal-sidebar);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: t($modal-sidebarActive);
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: t($modal-sidebar);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
background: t($modal-sidebarActive);
|
||||
border-left: 2px solid t($link);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@include themed {
|
||||
background: t($modal-sidebarActive);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dropdown-option-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-option-check {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
@include themed {
|
||||
background: t($link);
|
||||
color: white;
|
||||
background: t($color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
|
||||
@include themed {
|
||||
color: t($color);
|
||||
|
||||
&:hover {
|
||||
background: t($modal-sidebarActive);
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: t($modal-sidebar);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
background: t($modal-sidebarActive);
|
||||
border-left: 2px solid t($link);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-option-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-option-check {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include themed {
|
||||
background: t($link);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user