From 0d8300493e81ab8bd52c91039824b8086feedaf0 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sun, 17 May 2026 10:15:25 +0330 Subject: [PATCH] feat(select): enhance dropdown positioning and filtering functionality - Introduced a new measureDropdownRect function to accurately position the dropdown menu based on viewport dimensions, improving usability. - Updated the Select component to utilize the new dropdown measurement logic, ensuring consistent placement above or below the trigger. - Refactored panelRect state management to use a more structured DropdownRect type, enhancing type safety and clarity. - Improved event handling for dropdown visibility and positioning during scroll and resize events, ensuring a responsive user experience. - Adjusted styling in related components for better alignment and visual consistency. --- .../components/tender-details-header.tsx | 11 +- src/components/FormElements/select.tsx | 287 ++++++++---------- .../Tables/companies/CompanyListFilters.tsx | 3 +- src/components/ui/InlineListFiltersPanel.tsx | 10 +- 4 files changed, 133 insertions(+), 178 deletions(-) diff --git a/src/app/tenders/[details]/components/tender-details-header.tsx b/src/app/tenders/[details]/components/tender-details-header.tsx index a798596..d92e7b4 100644 --- a/src/app/tenders/[details]/components/tender-details-header.tsx +++ b/src/app/tenders/[details]/components/tender-details-header.tsx @@ -43,16 +43,19 @@ export function TenderDetailsHeader({
-
+
@@ -79,7 +82,7 @@ export function TenderDetailsHeader({ className={cn( "w-full shrink-0 space-y-0", "[&_label]:sr-only", - "[&_button]:h-11 [&_button]:min-h-[2.75rem] [&_button]:rounded-full [&_button]:border-stroke/60 [&_button]:bg-white/95 [&_button]:py-0 [&_button]:text-sm [&_button]:shadow-sm dark:[&_button]:border-white/10 dark:[&_button]:bg-dark-2/92", + "[&_button]:h-11 [&_button]:min-h-[2.75rem] [&_button]:rounded-full [&_button]:border-stroke/60 [&_button]:bg-white/95 [&_button]:py-0 [&_button]:text-sm [&_button]:shadow-sm dark:[&_button]:border-white/10 dark:[&_button]:bg-dark-2 dark:[&_button]:shadow-none", )} value={selectedLanguage} onChange={onLanguageChange as LanguageSelectOnChange} diff --git a/src/components/FormElements/select.tsx b/src/components/FormElements/select.tsx index f9f5443..19d1ea6 100644 --- a/src/components/FormElements/select.tsx +++ b/src/components/FormElements/select.tsx @@ -28,6 +28,50 @@ import { export type { SelectSearchMode } from "./use-select-server-search"; +const DROPDOWN_GAP_PX = 8; +const DROPDOWN_MIN_WIDTH_PX = 200; +const DROPDOWN_MAX_HEIGHT_PX = 352; + +type DropdownPlacement = "above" | "below"; + +type DropdownRect = { + top: number; + left: number; + width: number; + placement: DropdownPlacement; +}; + +/** Anchor a fixed portal menu to the trigger; avoids native ` setQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Escape") { - e.stopPropagation(); - setOpen(false); - setQuery(""); - triggerRef.current?.focus(); - } - }} - placeholder={searchPlaceholder} - data-cy={dataCy ? `${dataCy}-search` : undefined} - autoComplete="off" - spellCheck={false} - className="w-full rounded-lg border border-stroke/60 bg-white/95 px-3 py-2 text-sm text-dark outline-none ring-primary/20 placeholder:text-dark-5 focus:border-primary/60 focus:ring-2 dark:border-white/10 dark:bg-dark-3/90 dark:text-white dark:placeholder:text-dark-6" - /> -
+ {searchable ? ( +
+ + setQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Escape") { + e.stopPropagation(); + setOpen(false); + setQuery(""); + triggerRef.current?.focus(); + } + }} + placeholder={searchPlaceholder} + data-cy={dataCy ? `${dataCy}-search` : undefined} + autoComplete="off" + spellCheck={false} + className="w-full rounded-lg border border-stroke/60 bg-white/95 px-3 py-2 text-sm text-dark outline-none ring-primary/20 placeholder:text-dark-5 focus:border-primary/60 focus:ring-2 dark:border-white/10 dark:bg-dark-3/90 dark:text-white dark:placeholder:text-dark-6" + /> +
+ ) : null}
    - {isSearchLoading ? ( + {searchable && isSearchLoading ? (
  • Searching…
  • - ) : isSearchError ? ( + ) : searchable && isSearchError ? (
  • Could not load options. Try again.
  • - ) : listItems.length === 0 ? ( + ) : dropdownItems.length === 0 ? (
  • - No matches. + {searchable ? "No matches." : "No options."}
  • ) : ( - listItems.map((item) => { + dropdownItems.map((item) => { const v = String(item.value); const isActive = v === String(value); return ( @@ -370,16 +400,19 @@ export function Select({ data-value={v} onClick={() => pickItem(v)} className={cn( - "flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 text-start text-sm transition", + "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-start text-sm transition", + searchable ? "justify-between" : "justify-start", isActive ? "bg-primary/12 font-medium text-primary dark:bg-primary/20 dark:text-white" : "text-dark hover:bg-stroke/30 dark:text-white dark:hover:bg-white/[0.06]", )} > {item.label} - - {v} - + {searchable ? ( + + {v} + + ) : null} ); @@ -390,9 +423,8 @@ export function Select({ document.body, ); - if (searchable) { - return ( -
    + return ( +
    - ); - } - - return ( -
    - - -
    - {prefixIcon && ( -
    - {prefixIcon} -
    - )} - - - - {clearable && value && ( - - )} - - -
    - {error && (

    {error.message as string} diff --git a/src/components/Tables/companies/CompanyListFilters.tsx b/src/components/Tables/companies/CompanyListFilters.tsx index 90e0259..a35c6ab 100644 --- a/src/components/Tables/companies/CompanyListFilters.tsx +++ b/src/components/Tables/companies/CompanyListFilters.tsx @@ -5,6 +5,7 @@ import { Select } from "@/components/FormElements/select"; import { Button } from "@/components/ui-elements/button"; import InlineListFiltersPanel from "@/components/ui/InlineListFiltersPanel"; import { Currencies } from "@/constants/enums"; +import { useMemo } from "react"; import { FieldValues, UseFormHandleSubmit, @@ -12,7 +13,6 @@ import { UseFormSetValue, UseFormWatch, } from "react-hook-form"; -import { useMemo } from "react"; interface CompanyListFiltersProps { filterRegister: UseFormRegister; @@ -60,7 +60,6 @@ const CompanyListFilters = ({ return ( -

    - +
    + {title} @@ -198,7 +198,7 @@ export default function InlineListFiltersPanel({
    -
    +
    {activeFilterCount > 0 ? (
    @@ -206,7 +206,7 @@ export default function InlineListFiltersPanel({
    ) : ( - + No filters applied )}