Fix search dropdown (#3269)

* validate dropdown

* validate

* update organization

* move to utils
This commit is contained in:
pablodanswer
2024-11-27 08:10:07 -08:00
committed by GitHub
parent 0553062ac6
commit 28e2b78b2e
3 changed files with 139 additions and 77 deletions

View File

@@ -10,6 +10,8 @@ import {
import { ChevronDownIcon } from "./icons/icons"; import { ChevronDownIcon } from "./icons/icons";
import { FiCheck, FiChevronDown } from "react-icons/fi"; import { FiCheck, FiChevronDown } from "react-icons/fi";
import { Popover } from "./popover/Popover"; import { Popover } from "./popover/Popover";
import { createPortal } from "react-dom";
import { useDropdownPosition } from "@/lib/dropdown";
export interface Option<T> { export interface Option<T> {
name: string; name: string;
@@ -60,6 +62,7 @@ export function SearchMultiSelectDropdown({
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const dropdownMenuRef = useRef<HTMLDivElement>(null);
const handleSelect = (option: StringOrNumberOption) => { const handleSelect = (option: StringOrNumberOption) => {
onSelect(option); onSelect(option);
@@ -75,7 +78,9 @@ export function SearchMultiSelectDropdown({
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if ( if (
dropdownRef.current && 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); setIsOpen(false);
} }
@@ -87,105 +92,103 @@ export function SearchMultiSelectDropdown({
}; };
}, []); }, []);
useDropdownPosition({ isOpen, dropdownRef, dropdownMenuRef });
return ( return (
<div className="relative inline-block text-left w-full" ref={dropdownRef}> <div className="relative text-left w-full" ref={dropdownRef}>
<div> <div>
<input <input
type="text" type="text"
placeholder="Search..." placeholder="Search..."
value={searchTerm} value={searchTerm}
onChange={(e: ChangeEvent<HTMLInputElement>) => { onChange={(e: ChangeEvent<HTMLInputElement>) => {
if (!searchTerm) { setSearchTerm(e.target.value);
if (e.target.value) {
setIsOpen(true); setIsOpen(true);
} } else {
if (!e.target.value) {
setIsOpen(false); setIsOpen(false);
} }
setSearchTerm(e.target.value);
}} }}
onFocus={() => setIsOpen(true)} onFocus={() => setIsOpen(true)}
className={`inline-flex className={`inline-flex
justify-between justify-between
w-full w-full
px-4 px-4
py-2 py-2
text-sm text-sm
bg-background bg-background
border border
border-border border-border
rounded-md rounded-md
shadow-sm shadow-sm
`} `}
onClick={(e) => e.stopPropagation()}
/> />
<button <button
type="button" type="button"
className={`absolute top-0 right-0 className={`absolute top-0 right-0
text-sm text-sm
h-full px-2 border-l border-border`} h-full px-2 border-l border-border`}
aria-expanded="true" aria-expanded={isOpen}
aria-haspopup="true" aria-haspopup="true"
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
> >
<ChevronDownIcon className="my-auto" /> <ChevronDownIcon className="my-auto w-4 h-4" />
</button> </button>
</div> </div>
{isOpen && ( {isOpen &&
<div createPortal(
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`}
>
<div <div
role="menu" ref={dropdownMenuRef}
aria-orientation="vertical" className={`origin-top-right
aria-labelledby="options-menu" rounded-md
shadow-lg
bg-background
border
border-border
max-h-80
overflow-y-auto
overscroll-contain`}
> >
{filteredOptions.length ? ( <div
filteredOptions.map((option, index) => role="menu"
itemComponent ? ( aria-orientation="vertical"
<div aria-labelledby="options-menu"
key={option.name} >
onClick={() => { {filteredOptions.length ? (
setIsOpen(false); filteredOptions.map((option, index) =>
handleSelect(option); itemComponent ? (
}} <div
> key={option.name}
{itemComponent({ option })} onClick={() => {
</div> handleSelect(option);
) : ( }}
<StandardDropdownOption >
key={index} {itemComponent({ option })}
option={option} </div>
index={index} ) : (
handleSelect={handleSelect} <StandardDropdownOption
/> key={index}
option={option}
index={index}
handleSelect={handleSelect}
/>
)
) )
) ) : (
) : ( <button
<button key={0}
key={0} className={`w-full text-left block px-4 py-2.5 text-sm hover:bg-hover`}
className={`w-full text-left block px-4 py-2.5 text-sm hover:bg-hover`} role="menuitem"
role="menuitem" onClick={() => setIsOpen(false)}
onClick={() => setIsOpen(false)} >
> No matches found...
No matches found... </button>
</button> )}
)} </div>
</div> </div>,
</div> document.body
)} )}
</div> </div>
); );
} }

View File

@@ -66,11 +66,21 @@ export function Modal({
e.stopPropagation(); e.stopPropagation();
} }
}} }}
className={`bg-background text-emphasis rounded shadow-2xl className={`
transform transition-all duration-300 ease-in-out 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"} ${width ?? "w-11/12 max-w-4xl"}
${noPadding ? "" : "p-10"} ${noPadding ? "" : "p-10"}
${className || ""}`} ${className || ""}
`}
> >
{onOutsideClick && !hideCloseButton && ( {onOutsideClick && !hideCloseButton && (
<div className="absolute top-2 right-2"> <div className="absolute top-2 right-2">

49
web/src/lib/dropdown.ts Normal file
View File

@@ -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;
};