From e1f8fc0339337c7ce8850928940af6cd71c88e1d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 1 Dec 2025 14:59:34 +0530 Subject: [PATCH 1/6] chore: useExpandableSearch hook added and code refactor --- .../components/navigation/top-nav-power-k.tsx | 62 ++++++++-------- apps/web/core/hooks/use-expandable-search.ts | 73 +++++++++++++++++++ 2 files changed, 102 insertions(+), 33 deletions(-) create mode 100644 apps/web/core/hooks/use-expandable-search.ts diff --git a/apps/web/core/components/navigation/top-nav-power-k.tsx b/apps/web/core/components/navigation/top-nav-power-k.tsx index 6ca9af0a4a0..cdf6a47aac2 100644 --- a/apps/web/core/components/navigation/top-nav-power-k.tsx +++ b/apps/web/core/components/navigation/top-nav-power-k.tsx @@ -1,9 +1,8 @@ -import { useState, useRef, useMemo, useCallback, useEffect } from "react"; +import { useState, useMemo, useCallback, useEffect } from "react"; import { Command } from "cmdk"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // hooks -import { useOutsideClickDetector } from "@plane/hooks"; import { CloseIcon, SearchIcon } from "@plane/propel/icons"; import { cn } from "@plane/utils"; // power-k @@ -14,6 +13,7 @@ import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { usePowerK } from "@/hooks/store/use-power-k"; import { useUser } from "@/hooks/store/user"; import { useAppRouter } from "@/hooks/use-app-router"; +import { useExpandableSearch } from "@/hooks/use-expandable-search"; export const TopNavPowerK = observer(() => { // router @@ -22,7 +22,6 @@ export const TopNavPowerK = observer(() => { const { projectId: routerProjectId, workItem: workItemIdentifier } = params; // states - const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [activeCommand, setActiveCommand] = useState(null); const [shouldShowContextBasedActions, setShouldShowContextBasedActions] = useState(true); @@ -32,6 +31,23 @@ export const TopNavPowerK = observer(() => { const { activeContext, setActivePage, activePage, setTopNavInputRef } = usePowerK(); const { data: currentUser } = useUser(); + // expandable search hook + const { + isOpen, + containerRef, + inputRef, + handleClose: closePanel, + handleMouseDown, + handleFocus, + openPanel, + } = useExpandableSearch({ + onClose: () => { + setSearchTerm(""); + setActivePage(null); + setActiveCommand(null); + }, + }); + // derived values const { issue: { getIssueById, getIssueIdByIdentifier }, @@ -54,12 +70,7 @@ export const TopNavPowerK = observer(() => { projectId, }, router, - closePalette: () => { - setIsOpen(false); - setSearchTerm(""); - setActivePage(null); - setActiveCommand(null); - }, + closePalette: closePanel, setActiveCommand, setActivePage, }), @@ -72,12 +83,10 @@ export const TopNavPowerK = observer(() => { projectId, router, setActivePage, + closePanel, ] ); - const containerRef = useRef(null); - const inputRef = useRef(null); - // Register input ref with PowerK store for keyboard shortcut access useEffect(() => { setTopNavInputRef(inputRef); @@ -86,18 +95,6 @@ export const TopNavPowerK = observer(() => { }; }, [setTopNavInputRef]); - useOutsideClickDetector(containerRef, () => { - if (isOpen) { - setIsOpen(false); - setActivePage(null); - setActiveCommand(null); - } - }); - - const handleFocus = () => { - setIsOpen(true); - }; - const handleClear = () => { setSearchTerm(""); inputRef.current?.focus(); @@ -136,10 +133,7 @@ export const TopNavPowerK = observer(() => { // Cmd/Ctrl+K closes the search dropdown if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); - setIsOpen(false); - setSearchTerm(""); - setActivePage(null); - context.setActiveCommand(null); + closePanel(); return; } @@ -148,9 +142,7 @@ export const TopNavPowerK = observer(() => { if (searchTerm) { setSearchTerm(""); } - setIsOpen(false); - inputRef.current?.blur(); - + closePanel(); return; } @@ -203,7 +195,7 @@ export const TopNavPowerK = observer(() => { return; } }, - [searchTerm, activePage, context, shouldShowContextBasedActions, setActivePage, isOpen] + [searchTerm, activePage, context, shouldShowContextBasedActions, setActivePage, closePanel] ); return ( @@ -228,7 +220,11 @@ export const TopNavPowerK = observer(() => { ref={inputRef} type="text" value={searchTerm} - onChange={(e) => setSearchTerm(e.target.value)} + onChange={(e) => { + setSearchTerm(e.target.value); + if (!isOpen) openPanel(); + }} + onMouseDown={handleMouseDown} onFocus={handleFocus} onKeyDown={handleKeyDown} placeholder="Search commands..." diff --git a/apps/web/core/hooks/use-expandable-search.ts b/apps/web/core/hooks/use-expandable-search.ts new file mode 100644 index 00000000000..829fbc64d6b --- /dev/null +++ b/apps/web/core/hooks/use-expandable-search.ts @@ -0,0 +1,73 @@ +import { useCallback, useRef, useState } from "react"; +import { useOutsideClickDetector } from "@plane/hooks"; + +type UseExpandableSearchOptions = { + onClose?: () => void; +}; + +/** + * Custom hook for expandable search input behavior + * Handles focus management to prevent unwanted opening on programmatic focus restoration + */ +export const useExpandableSearch = (options?: UseExpandableSearchOptions) => { + const { onClose } = options || {}; + + // states + const [isOpen, setIsOpen] = useState(false); + + // refs + const containerRef = useRef(null); + const inputRef = useRef(null); + const wasClickedRef = useRef(false); + + // Handle close + const handleClose = useCallback(() => { + setIsOpen(false); + inputRef.current?.blur(); + onClose?.(); + }, [onClose]); + + // Outside click detection + useOutsideClickDetector(containerRef, () => { + if (isOpen) { + handleClose(); + } + }); + + // Track explicit clicks + const handleMouseDown = useCallback(() => { + wasClickedRef.current = true; + }, []); + + // Only open on explicit clicks, not programmatic focus + const handleFocus = useCallback(() => { + if (wasClickedRef.current) { + setIsOpen(true); + wasClickedRef.current = false; + } + }, []); + + // Helper to open panel (for typing/onChange) + const openPanel = useCallback(() => { + if (!isOpen) { + setIsOpen(true); + } + }, [isOpen]); + + return { + // State + isOpen, + setIsOpen, + + // Refs + containerRef, + inputRef, + + // Handlers + handleClose, + handleMouseDown, + handleFocus, + openPanel, + }; +}; + From 60bd2c06c07b140a066692ef106c57571318c3c7 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 1 Dec 2025 17:13:51 +0530 Subject: [PATCH 2/6] chore: navigation improvements --- .../navigation/use-responsive-tab-layout.ts | 5 +++-- .../components/sidebar/sidebar-wrapper.tsx | 22 ++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/apps/web/core/components/navigation/use-responsive-tab-layout.ts b/apps/web/core/components/navigation/use-responsive-tab-layout.ts index 3e2126ece41..4e91fea85d8 100644 --- a/apps/web/core/components/navigation/use-responsive-tab-layout.ts +++ b/apps/web/core/components/navigation/use-responsive-tab-layout.ts @@ -42,9 +42,10 @@ export const useResponsiveTabLayout = ({ const gap = 4; // gap-1 = 4px const overflowButtonWidth = 40; + const container = containerRef?.current; + // ResizeObserver to measure container width useEffect(() => { - const container = containerRef.current; if (!container) return; const resizeObserver = new ResizeObserver((entries) => { @@ -58,7 +59,7 @@ export const useResponsiveTabLayout = ({ return () => { resizeObserver.disconnect(); }; - }, []); + }, [container]); // Calculate how many items can fit useEffect(() => { diff --git a/apps/web/core/components/sidebar/sidebar-wrapper.tsx b/apps/web/core/components/sidebar/sidebar-wrapper.tsx index c458f74efcc..a80155986e3 100644 --- a/apps/web/core/components/sidebar/sidebar-wrapper.tsx +++ b/apps/web/core/components/sidebar/sidebar-wrapper.tsx @@ -49,16 +49,18 @@ export const SidebarWrapper = observer(function SidebarWrapper(props: TSidebarWr
{title} -
- - -
+ {title === "Projects" && ( +
+ + +
+ )}
{/* Quick actions */} {quickActions} From 3eeef1b30f8d495a42c652f0e3f8991953540810 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 1 Dec 2025 17:17:52 +0530 Subject: [PATCH 3/6] fix: format errors --- apps/web/core/hooks/use-expandable-search.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/core/hooks/use-expandable-search.ts b/apps/web/core/hooks/use-expandable-search.ts index 829fbc64d6b..e16d2359451 100644 --- a/apps/web/core/hooks/use-expandable-search.ts +++ b/apps/web/core/hooks/use-expandable-search.ts @@ -70,4 +70,3 @@ export const useExpandableSearch = (options?: UseExpandableSearchOptions) => { openPanel, }; }; - From 8cb3cd688596c481a54429d5d440e641dcb02f00 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 1 Dec 2025 17:20:21 +0530 Subject: [PATCH 4/6] chore: copilot comment --- .../core/components/navigation/top-nav-power-k.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/web/core/components/navigation/top-nav-power-k.tsx b/apps/web/core/components/navigation/top-nav-power-k.tsx index cdf6a47aac2..054aceaf322 100644 --- a/apps/web/core/components/navigation/top-nav-power-k.tsx +++ b/apps/web/core/components/navigation/top-nav-power-k.tsx @@ -31,6 +31,12 @@ export const TopNavPowerK = observer(() => { const { activeContext, setActivePage, activePage, setTopNavInputRef } = usePowerK(); const { data: currentUser } = useUser(); + const handleOnClose = useCallback(() => { + setSearchTerm(""); + setActivePage(null); + setActiveCommand(null); + }, [setSearchTerm, setActivePage, setActiveCommand]); + // expandable search hook const { isOpen, @@ -41,11 +47,7 @@ export const TopNavPowerK = observer(() => { handleFocus, openPanel, } = useExpandableSearch({ - onClose: () => { - setSearchTerm(""); - setActivePage(null); - setActiveCommand(null); - }, + onClose: handleOnClose, }); // derived values From f5091194c1d197f9e1a3e3b13f110b4e5c6be44b Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 1 Dec 2025 17:20:55 +0530 Subject: [PATCH 5/6] chore: code refactor --- apps/web/core/hooks/use-expandable-search.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/web/core/hooks/use-expandable-search.ts b/apps/web/core/hooks/use-expandable-search.ts index e16d2359451..583a35c3c78 100644 --- a/apps/web/core/hooks/use-expandable-search.ts +++ b/apps/web/core/hooks/use-expandable-search.ts @@ -27,12 +27,15 @@ export const useExpandableSearch = (options?: UseExpandableSearchOptions) => { onClose?.(); }, [onClose]); - // Outside click detection - useOutsideClickDetector(containerRef, () => { + // Outside click handler - memoized to prevent unnecessary re-registrations + const handleOutsideClick = useCallback(() => { if (isOpen) { handleClose(); } - }); + }, [isOpen, handleClose]); + + // Outside click detection + useOutsideClickDetector(containerRef, handleOutsideClick); // Track explicit clicks const handleMouseDown = useCallback(() => { From f0667fa94e00fd9ca769e50f15c108016782d3d5 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Mon, 1 Dec 2025 18:27:18 +0530 Subject: [PATCH 6/6] chore: code refactor --- packages/i18n/src/locales/de/empty-state.ts | 3 ++- packages/i18n/src/locales/pt-BR/empty-state.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/i18n/src/locales/de/empty-state.ts b/packages/i18n/src/locales/de/empty-state.ts index 55dfe580045..087e51e7b47 100644 --- a/packages/i18n/src/locales/de/empty-state.ts +++ b/packages/i18n/src/locales/de/empty-state.ts @@ -28,7 +28,8 @@ export default { project_empty_state: { no_access: { title: "Es scheint, als hätten Sie keinen Zugriff auf dieses Projekt", - restricted_description: "Kontaktieren Sie den Administrator, um Zugriff anzufordern, damit Sie hier fortfahren können.", + restricted_description: + "Kontaktieren Sie den Administrator, um Zugriff anzufordern, damit Sie hier fortfahren können.", join_description: "Klicken Sie unten auf die Schaltfläche, um beizutreten.", cta_primary: "Projekt beitreten", cta_loading: "Projekt wird beigetreten", diff --git a/packages/i18n/src/locales/pt-BR/empty-state.ts b/packages/i18n/src/locales/pt-BR/empty-state.ts index 9ceb00fe5d1..697cb3cb7c6 100644 --- a/packages/i18n/src/locales/pt-BR/empty-state.ts +++ b/packages/i18n/src/locales/pt-BR/empty-state.ts @@ -28,7 +28,8 @@ export default { project_empty_state: { no_access: { title: "Parece que você não tem acesso a este projeto", - restricted_description: "Entre em contato com o administrador para solicitar acesso e você poderá continuar aqui.", + restricted_description: + "Entre em contato com o administrador para solicitar acesso e você poderá continuar aqui.", join_description: "Clique no botão abaixo para participar.", cta_primary: "Participar do projeto", cta_loading: "Participando do projeto",