Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 60 additions & 15 deletions src/components/common/dropdownmenu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ const easeIn = [0.4, 0, 1, 1] as const;

const MENU_ITEM_HEIGHT = 52;
const MENU_VERTICAL_PADDING = 32;
const DEFAULT_MENU_WIDTH = 224;

function getClippingBounds(element: HTMLElement) {
let bottom = window.innerHeight;
let top = 0;
let left = 0;
let right = window.innerWidth;
let parent = element.parentElement;

while (parent) {
Expand All @@ -45,12 +48,14 @@ function getClippingBounds(element: HTMLElement) {
const rect = parent.getBoundingClientRect();
bottom = Math.min(bottom, rect.bottom);
top = Math.max(top, rect.top);
left = Math.max(left, rect.left);
right = Math.min(right, rect.right);
}

parent = parent.parentElement;
}

return { top, bottom };
return { top, bottom, left, right };
}

function resolveAutoPlacement(
Expand All @@ -65,11 +70,37 @@ function resolveAutoPlacement(
const spaceBelow = containerBottom - triggerRect.bottom;
const spaceAbove = triggerRect.top - containerTop;

return spaceBelow < estimatedMenuHeight && spaceAbove > spaceBelow
const visibleHeight = containerBottom - containerTop;

const inLowerArea =
visibleHeight > 0 &&
triggerRect.bottom > containerTop + visibleHeight * 0.65;

if (inLowerArea && spaceAbove >= estimatedMenuHeight) {
return "top";
}

return spaceBelow < estimatedMenuHeight && spaceAbove >= estimatedMenuHeight
? "top"
: "bottom";
}

function resolveHorizontalAlign(
element: HTMLElement,
menuWidth = DEFAULT_MENU_WIDTH,
): "left" | "right" {
const triggerRect = element.getBoundingClientRect();
const { left: containerLeft, right: containerRight } =
getClippingBounds(element);
const spaceLeft = triggerRect.right - containerLeft;
const spaceRight = containerRight - triggerRect.left;

if (spaceLeft < menuWidth && spaceRight > spaceLeft) {
return "left";
}
return "right";
}

export function DropdownMenu({
trigger,
items,
Expand Down Expand Up @@ -98,6 +129,9 @@ export function DropdownMenu({
const [resolvedPlacement, setResolvedPlacement] = useState<"bottom" | "top">(
"bottom",
);
const [horizontalAlign, setHorizontalAlign] = useState<"left" | "right">(
"right",
);
const ref = useRef<HTMLDivElement | null>(null);
const menuId = useId();

Expand All @@ -118,30 +152,34 @@ export function DropdownMenu({
useLayoutEffect(() => {
if (!open || inFlow) {
setResolvedPlacement("bottom");
setHorizontalAlign("right");
return;
}

const el = ref.current;
if (!el) return;

if (placement === "bottom") {
setResolvedPlacement("bottom");
return;
}

if (placement === "top") {
} else if (placement === "top") {
setResolvedPlacement("top");
return;
} else {
setResolvedPlacement(resolveAutoPlacement(el, items.length));
}

const el = ref.current;
if (!el) return;

setResolvedPlacement(resolveAutoPlacement(el, items.length));
}, [open, placement, items.length, inFlow]);
if (!fullWidth) {
setHorizontalAlign(resolveHorizontalAlign(el));
} else {
setHorizontalAlign("right");
}
}, [open, placement, items.length, inFlow, fullWidth]);

useEffect(() => {
onOpenChange?.(open);
}, [open, onOpenChange]);

const isTopPlacement = !inFlow && resolvedPlacement === "top";
const isLeftAlign = !inFlow && !fullWidth && horizontalAlign === "left";

return (
<div
Expand Down Expand Up @@ -180,8 +218,12 @@ export function DropdownMenu({
? "bottom center"
: "top center"
: isTopPlacement
? "bottom right"
: "top right",
? isLeftAlign
? "bottom left"
: "bottom right"
: isLeftAlign
? "top left"
: "top right",
}}
className={twMerge(
"absolute z-50 rounded-2xl border border-surface-300 bg-surface-100 py-3 px-1 shadow-Soft",
Expand All @@ -193,7 +235,10 @@ export function DropdownMenu({
),
fullWidth
? "left-0 right-0 w-full"
: "right-0 w-56 max-w-[calc(100vw-40px)]",
: twMerge(
"w-56 max-w-[calc(100vw-40px)]",
isLeftAlign ? "left-0 right-auto" : "right-0",
),
menuClassName,
)}
initial={{ opacity: 0, scale: reduceMotion ? 1 : 0.96 }}
Expand Down
2 changes: 0 additions & 2 deletions src/components/common/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,6 @@ function Modal({
"relative mx-4 w-full max-h-[90vh] overflow-auto rounded-2xl bg-surface-100 shadow-Soft",
sizeClasses[size],
paddingClasses[padding],
// absolute 닫기 버튼(top-4 right-4)과 본문 겹침 방지
!hideCloseButton && "pr-12",
className,
)}
initial={{ opacity: 0, scale: reduceMotion ? 1 : 0.95 }}
Expand Down
7 changes: 5 additions & 2 deletions src/components/common/select/SearchSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,12 @@ export default function SearchSelect<T>({
aria-controls={listboxId}
aria-haspopup="listbox"
aria-autocomplete="list"
className="h-13 w-full rounded-2xl border border-info-blue px-4 pl-5 font-body1 outline-none transition-colors placeholder:text-text-placeholder focus:border-info-blue"
containerClassName="h-13 w-full rounded-2xl border border-info-blue font-body1 outline-none transition-colors placeholder:text-text-placeholder focus:border-info-blue"
inputClassName="truncate"
rightElement={
<SearchIcon className="h-5 w-5 text-info-blue" aria-hidden />
}
/>
<SearchIcon className="pointer-events-none absolute right-4 top-1/2 h-5 w-5 -translate-y-1/2 text-info-blue" />
</div>

{isOpen && (
Expand Down
Loading
Loading