From 505b93be4fe9e5019293738652793105cfe2025d Mon Sep 17 00:00:00 2001 From: nessi Date: Thu, 12 Feb 2026 15:36:09 +0100 Subject: [PATCH] Add dropdown with toggle functionality to OwnerPicker Introduced a dropdown feature to the OwnerPicker component, allowing users to search or browse owners more effectively. The dropdown can be toggled open/closed and includes improved styling for better user experience. Added click-outside functionality to automatically close the dropdown when users interact elsewhere. --- frontend/src/pages/TargetsPage.jsx | 73 ++++++++++++++++++++---------- frontend/src/styles.css | 37 ++++++++++++++- 2 files changed, 84 insertions(+), 26 deletions(-) diff --git a/frontend/src/pages/TargetsPage.jsx b/frontend/src/pages/TargetsPage.jsx index 6923bc0..eef8651 100644 --- a/frontend/src/pages/TargetsPage.jsx +++ b/frontend/src/pages/TargetsPage.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Link } from "react-router-dom"; import { apiFetch } from "../api"; import { useAuth } from "../state"; @@ -34,13 +34,25 @@ function toggleOwner(ids, userId) { } function OwnerPicker({ candidates, selectedIds, onToggle, query, onQueryChange }) { + const pickerRef = useRef(null); + const [open, setOpen] = useState(false); const filtered = candidates.filter((item) => item.email.toLowerCase().includes(query.trim().toLowerCase()) ); const selected = candidates.filter((item) => selectedIds.includes(item.user_id)); + useEffect(() => { + const onPointerDown = (event) => { + if (!pickerRef.current?.contains(event.target)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", onPointerDown); + return () => document.removeEventListener("mousedown", onPointerDown); + }, []); + return ( -
+
{selected.length > 0 ? ( selected.map((item) => ( @@ -59,30 +71,41 @@ function OwnerPicker({ candidates, selectedIds, onToggle, query, onQueryChange } No owners selected yet. )}
- onQueryChange(e.target.value)} - placeholder="Search users by email..." - /> -
- {filtered.map((item) => { - const active = selectedIds.includes(item.user_id); - return ( - - ); - })} - {filtered.length === 0 &&
No matching users.
} +
+ onQueryChange(e.target.value)} + onFocus={() => setOpen(true)} + onClick={() => setOpen(true)} + placeholder="Search users by email..." + /> +
+ {open && ( +
+
+ {filtered.map((item) => { + const active = selectedIds.includes(item.user_id); + return ( + + ); + })} + {filtered.length === 0 &&
No matching users.
} +
+
+ )}
); } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 18b0890..6a8f09e 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -752,6 +752,7 @@ button { .owner-picker { display: grid; gap: 8px; + position: relative; } .owner-selected { @@ -774,14 +775,48 @@ button { font-weight: 600; } +.owner-search-shell { + display: grid; + grid-template-columns: 1fr auto; + gap: 0; +} + +.owner-search-shell.open .owner-search-input { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + .owner-search-input { width: 100%; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.owner-search-toggle { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + min-width: 38px; + font-weight: 700; + padding: 0 10px; +} + +.owner-dropdown { + position: absolute; + left: 0; + right: 0; + top: calc(100% - 2px); + z-index: 10; + border: 1px solid #3c73ac; + border-radius: 0 0 10px 10px; + background: #0f2549; + box-shadow: 0 12px 26px #03112777; + padding: 8px; } .owner-search-results { display: grid; gap: 6px; - max-height: 150px; + max-height: 180px; overflow: auto; padding-right: 2px; }