Skip to content

Commit

Permalink
Fix search dropdown (#3269)
Browse files Browse the repository at this point in the history
* validate dropdown

* validate

* update organization

* move to utils
  • Loading branch information
pablonyx authored Nov 27, 2024
1 parent 0553062 commit 28e2b78
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 77 deletions.
151 changes: 77 additions & 74 deletions web/src/components/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
import { ChevronDownIcon } from "./icons/icons";
import { FiCheck, FiChevronDown } from "react-icons/fi";
import { Popover } from "./popover/Popover";
import { createPortal } from "react-dom";
import { useDropdownPosition } from "@/lib/dropdown";

export interface Option<T> {
name: string;
Expand Down Expand Up @@ -60,6 +62,7 @@ export function SearchMultiSelectDropdown({
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const dropdownRef = useRef<HTMLDivElement>(null);
const dropdownMenuRef = useRef<HTMLDivElement>(null);

const handleSelect = (option: StringOrNumberOption) => {
onSelect(option);
Expand All @@ -75,7 +78,9 @@ export function SearchMultiSelectDropdown({
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
!dropdownRef.current.contains(event.target as Node) &&
dropdownMenuRef.current &&
!dropdownMenuRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
Expand All @@ -87,105 +92,103 @@ export function SearchMultiSelectDropdown({
};
}, []);

useDropdownPosition({ isOpen, dropdownRef, dropdownMenuRef });

return (
<div className="relative inline-block text-left w-full" ref={dropdownRef}>
<div className="relative text-left w-full" ref={dropdownRef}>
<div>
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
if (!searchTerm) {
setSearchTerm(e.target.value);
if (e.target.value) {
setIsOpen(true);
}
if (!e.target.value) {
} else {
setIsOpen(false);
}
setSearchTerm(e.target.value);
}}
onFocus={() => setIsOpen(true)}
className={`inline-flex
justify-between
w-full
px-4
py-2
text-sm
bg-background
border
border-border
rounded-md
shadow-sm
`}
onClick={(e) => e.stopPropagation()}
justify-between
w-full
px-4
py-2
text-sm
bg-background
border
border-border
rounded-md
shadow-sm
`}
/>
<button
type="button"
className={`absolute top-0 right-0
text-sm
h-full px-2 border-l border-border`}
aria-expanded="true"
text-sm
h-full px-2 border-l border-border`}
aria-expanded={isOpen}
aria-haspopup="true"
onClick={() => setIsOpen(!isOpen)}
>
<ChevronDownIcon className="my-auto" />
<ChevronDownIcon className="my-auto w-4 h-4" />
</button>
</div>

{isOpen && (
<div
className={`origin-top-right
absolute
left-0
mt-3
w-full
rounded-md
shadow-lg
bg-background
border
border-border
max-h-80
overflow-y-auto
overscroll-contain`}
>
{isOpen &&
createPortal(
<div
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
ref={dropdownMenuRef}
className={`origin-top-right
rounded-md
shadow-lg
bg-background
border
border-border
max-h-80
overflow-y-auto
overscroll-contain`}
>
{filteredOptions.length ? (
filteredOptions.map((option, index) =>
itemComponent ? (
<div
key={option.name}
onClick={() => {
setIsOpen(false);
handleSelect(option);
}}
>
{itemComponent({ option })}
</div>
) : (
<StandardDropdownOption
key={index}
option={option}
index={index}
handleSelect={handleSelect}
/>
<div
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
>
{filteredOptions.length ? (
filteredOptions.map((option, index) =>
itemComponent ? (
<div
key={option.name}
onClick={() => {
handleSelect(option);
}}
>
{itemComponent({ option })}
</div>
) : (
<StandardDropdownOption
key={index}
option={option}
index={index}
handleSelect={handleSelect}
/>
)
)
)
) : (
<button
key={0}
className={`w-full text-left block px-4 py-2.5 text-sm hover:bg-hover`}
role="menuitem"
onClick={() => setIsOpen(false)}
>
No matches found...
</button>
)}
</div>
</div>
)}
) : (
<button
key={0}
className={`w-full text-left block px-4 py-2.5 text-sm hover:bg-hover`}
role="menuitem"
onClick={() => setIsOpen(false)}
>
No matches found...
</button>
)}
</div>
</div>,
document.body
)}
</div>
);
}
Expand Down
16 changes: 13 additions & 3 deletions web/src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,21 @@ export function Modal({
e.stopPropagation();
}
}}
className={`bg-background text-emphasis rounded shadow-2xl
transform transition-all duration-300 ease-in-out
className={`
bg-background
text-emphasis
rounded
shadow-2xl
transform
transition-all
duration-300
ease-in-out
relative
overflow-visible
${width ?? "w-11/12 max-w-4xl"}
${noPadding ? "" : "p-10"}
${className || ""}`}
${className || ""}
`}
>
{onOutsideClick && !hideCloseButton && (
<div className="absolute top-2 right-2">
Expand Down
49 changes: 49 additions & 0 deletions web/src/lib/dropdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { RefObject, useCallback, useEffect } from "react";

interface DropdownPositionProps {
isOpen: boolean;
dropdownRef: RefObject<HTMLElement>;
dropdownMenuRef: RefObject<HTMLElement>;
}

// This hook manages the positioning of a dropdown menu relative to its trigger element.
// It ensures the menu is positioned correctly, adjusting for viewport boundaries and scroll position.
// Also adds event listeners for window resize and scroll to update the position dynamically.
export const useDropdownPosition = ({
isOpen,
dropdownRef,
dropdownMenuRef,
}: DropdownPositionProps) => {
const updateMenuPosition = useCallback(() => {
if (isOpen && dropdownRef.current && dropdownMenuRef.current) {
const rect = dropdownRef.current.getBoundingClientRect();
const menuRect = dropdownMenuRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;

let top = rect.bottom + window.scrollY;

if (top + menuRect.height > viewportHeight) {
top = rect.top + window.scrollY - menuRect.height;
}

dropdownMenuRef.current.style.position = "absolute";
dropdownMenuRef.current.style.top = `${top}px`;
dropdownMenuRef.current.style.left = `${rect.left + window.scrollX}px`;
dropdownMenuRef.current.style.width = `${rect.width}px`;
dropdownMenuRef.current.style.zIndex = "10000";
}
}, [isOpen, dropdownRef, dropdownMenuRef]);

useEffect(() => {
updateMenuPosition();
window.addEventListener("resize", updateMenuPosition);
window.addEventListener("scroll", updateMenuPosition);

return () => {
window.removeEventListener("resize", updateMenuPosition);
window.removeEventListener("scroll", updateMenuPosition);
};
}, [isOpen, updateMenuPosition]);

return updateMenuPosition;
};

0 comments on commit 28e2b78

Please sign in to comment.