UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: portal popup рабочей области и sidebar-search
This commit is contained in:
parent
b46390ccdd
commit
01eb3d4c8a
|
|
@ -4,10 +4,11 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useMemo, useCallback, useEffect } from "react";
|
import { useState, useMemo, useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
||||||
import { Command } from "cmdk";
|
import { Command } from "cmdk";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
// hooks
|
// hooks
|
||||||
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
|
import { CloseIcon, SearchIcon } from "@plane/propel/icons";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
|
|
@ -37,6 +38,14 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
const [activeCommand, setActiveCommand] = useState<TPowerKCommandConfig | null>(null);
|
const [activeCommand, setActiveCommand] = useState<TPowerKCommandConfig | null>(null);
|
||||||
const [shouldShowContextBasedActions, setShouldShowContextBasedActions] = useState(true);
|
const [shouldShowContextBasedActions, setShouldShowContextBasedActions] = useState(true);
|
||||||
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||||
|
const [sidebarSearchPosition, setSidebarSearchPosition] = useState<{
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const sidebarSearchPortalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const sidebarSearchButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { activeContext, setActivePage, activePage, setTopNavInputRef } = usePowerK();
|
const { activeContext, setActivePage, activePage, setTopNavInputRef } = usePowerK();
|
||||||
|
|
@ -59,6 +68,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
openPanel,
|
openPanel,
|
||||||
} = useExpandableSearch({
|
} = useExpandableSearch({
|
||||||
onClose: handleOnClose,
|
onClose: handleOnClose,
|
||||||
|
additionalRefs: [sidebarSearchPortalRef],
|
||||||
});
|
});
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
|
|
@ -113,6 +123,37 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateSidebarSearchPosition = useCallback(() => {
|
||||||
|
if (variant !== "sidebar" || !sidebarSearchButtonRef.current || typeof window === "undefined") return;
|
||||||
|
|
||||||
|
const rect = sidebarSearchButtonRef.current.getBoundingClientRect();
|
||||||
|
const width = 320;
|
||||||
|
const viewportPadding = 16;
|
||||||
|
const left = Math.min(rect.left, window.innerWidth - width - viewportPadding);
|
||||||
|
const top = rect.top;
|
||||||
|
|
||||||
|
setSidebarSearchPosition({
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
width,
|
||||||
|
});
|
||||||
|
}, [variant]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!isOpen || variant !== "sidebar") return;
|
||||||
|
|
||||||
|
updateSidebarSearchPosition();
|
||||||
|
|
||||||
|
const handlePositionUpdate = () => updateSidebarSearchPosition();
|
||||||
|
window.addEventListener("resize", handlePositionUpdate);
|
||||||
|
window.addEventListener("scroll", handlePositionUpdate, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handlePositionUpdate);
|
||||||
|
window.removeEventListener("scroll", handlePositionUpdate, true);
|
||||||
|
};
|
||||||
|
}, [isOpen, updateSidebarSearchPosition, variant]);
|
||||||
|
|
||||||
// Handle command selection
|
// Handle command selection
|
||||||
const handleCommandSelect = useCallback(
|
const handleCommandSelect = useCallback(
|
||||||
(command: TPowerKCommandConfig) => {
|
(command: TPowerKCommandConfig) => {
|
||||||
|
|
@ -211,6 +252,37 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
[searchTerm, activePage, context, shouldShowContextBasedActions, setActivePage, closePanel]
|
[searchTerm, activePage, context, shouldShowContextBasedActions, setActivePage, closePanel]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const searchCommandContent = (
|
||||||
|
<Command
|
||||||
|
filter={(i18nValue: string, search: string) => {
|
||||||
|
if (i18nValue === "no-results") return 1;
|
||||||
|
if (i18nValue.toLowerCase().includes(search.toLowerCase())) return 1;
|
||||||
|
return 0;
|
||||||
|
}}
|
||||||
|
shouldFilter={searchTerm.length > 0}
|
||||||
|
className="flex h-full w-full flex-col"
|
||||||
|
>
|
||||||
|
<Command.Input value={searchTerm} hidden />
|
||||||
|
<Command.List className="vertical-scrollbar scrollbar-sm max-h-[60vh] overflow-y-auto px-2 pb-4 outline-none">
|
||||||
|
<ProjectsAppPowerKCommandsList
|
||||||
|
activePage={activePage}
|
||||||
|
context={context}
|
||||||
|
handleCommandSelect={handleCommandSelect}
|
||||||
|
handlePageDataSelection={handlePageDataSelection}
|
||||||
|
isWorkspaceLevel={isWorkspaceLevel}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
setSearchTerm={setSearchTerm}
|
||||||
|
handleSearchMenuClose={() => closePanel()}
|
||||||
|
/>
|
||||||
|
</Command.List>
|
||||||
|
<PowerKModalFooter
|
||||||
|
isWorkspaceLevel={isWorkspaceLevel}
|
||||||
|
projectId={context.params.projectId?.toString()}
|
||||||
|
onWorkspaceLevelChange={setIsWorkspaceLevel}
|
||||||
|
/>
|
||||||
|
</Command>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="relative">
|
<div ref={containerRef} className="relative">
|
||||||
{variant === "top-navigation" ? (
|
{variant === "top-navigation" ? (
|
||||||
|
|
@ -252,15 +324,13 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="relative z-30 h-8 w-8">
|
||||||
className={cn("relative z-30 h-8 transition-all duration-300 ease-in-out", {
|
|
||||||
"w-[19.5rem]": isOpen,
|
|
||||||
"w-8": !isOpen,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
|
ref={sidebarSearchButtonRef}
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute left-0 top-0 z-10 flex size-8 items-center justify-center rounded-full border border-white/8 bg-white/[0.04] text-placeholder backdrop-blur-[18px] outline-none transition-all hover:bg-white/[0.07]"
|
className={cn(
|
||||||
|
"absolute left-0 top-0 z-[161] flex size-8 items-center justify-center rounded-full border border-white/8 bg-white/[0.04] text-placeholder backdrop-blur-[18px] outline-none transition-all hover:bg-white/[0.07]"
|
||||||
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
closePanel();
|
closePanel();
|
||||||
|
|
@ -273,15 +343,39 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
>
|
>
|
||||||
<SearchIcon className="size-3.5 shrink-0 text-placeholder" />
|
<SearchIcon className="size-3.5 shrink-0 text-placeholder" />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{variant === "top-navigation" && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-8 w-full items-center overflow-hidden rounded-full transition-all duration-300",
|
"absolute z-20 flex flex-col overflow-hidden px-0 transition-all duration-300 ease-in-out",
|
||||||
{
|
{
|
||||||
"nodedc-glass-surface pl-10 pr-3": isOpen,
|
"max-h-[80vh] w-[574px] opacity-100": isOpen,
|
||||||
"border-transparent bg-transparent pl-0 pr-0 shadow-none": !isOpen,
|
"h-0 w-0 opacity-0": !isOpen,
|
||||||
|
"-top-[6px] left-1/2 -translate-x-1/2 rounded-md border border-subtle bg-surface-1 shadow-lg pt-10":
|
||||||
|
true,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{isOpen && searchCommandContent}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{variant === "sidebar" &&
|
||||||
|
isOpen &&
|
||||||
|
sidebarSearchPosition &&
|
||||||
|
typeof document !== "undefined" &&
|
||||||
|
createPortal(
|
||||||
|
<div
|
||||||
|
ref={sidebarSearchPortalRef}
|
||||||
|
className="fixed z-[160]"
|
||||||
|
style={{
|
||||||
|
left: `${sidebarSearchPosition.left}px`,
|
||||||
|
top: `${sidebarSearchPosition.top}px`,
|
||||||
|
width: `${sidebarSearchPosition.width}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="nodedc-glass-modal nodedc-glass-surface flex h-8 w-full items-center overflow-hidden rounded-full pl-11 pr-3">
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -294,68 +388,22 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Search commands..."
|
placeholder="Search commands..."
|
||||||
className={cn(
|
className="placeholder-text-placeholder min-w-0 flex-1 bg-transparent text-13 text-primary outline-none"
|
||||||
"placeholder-text-placeholder min-w-0 flex-1 bg-transparent text-13 text-primary outline-none transition-all",
|
autoFocus
|
||||||
{
|
|
||||||
"pointer-events-none w-0 opacity-0": !isOpen,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
{isOpen && searchTerm && (
|
{searchTerm && (
|
||||||
<button type="button" onClick={handleClear} className="ml-2 shrink-0">
|
<button type="button" onClick={handleClear} className="ml-2 shrink-0">
|
||||||
<CloseIcon className="size-3.5 text-placeholder hover:text-primary" />
|
<CloseIcon className="size-3.5 text-placeholder hover:text-primary" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="nodedc-glass-modal nodedc-glass-surface mt-3 flex max-h-[70vh] w-full flex-col overflow-hidden rounded-[1.5rem] pt-3">
|
||||||
|
{searchCommandContent}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"absolute z-20 flex flex-col overflow-hidden px-0 transition-all duration-300 ease-in-out",
|
|
||||||
{
|
|
||||||
"max-h-[80vh] w-[574px] opacity-100": isOpen && variant === "top-navigation",
|
|
||||||
"max-h-[70vh] w-[20rem] opacity-100": isOpen && variant === "sidebar",
|
|
||||||
"h-0 w-0 opacity-0": !isOpen,
|
|
||||||
"-top-[6px] left-1/2 -translate-x-1/2 rounded-md border border-subtle bg-surface-1 shadow-lg pt-10": variant === "top-navigation",
|
|
||||||
"nodedc-glass-modal nodedc-glass-surface bottom-11 left-0 rounded-[1.5rem] pt-3": variant === "sidebar",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isOpen && (
|
|
||||||
<Command
|
|
||||||
filter={(i18nValue: string, search: string) => {
|
|
||||||
if (i18nValue === "no-results") return 1;
|
|
||||||
if (i18nValue.toLowerCase().includes(search.toLowerCase())) return 1;
|
|
||||||
return 0;
|
|
||||||
}}
|
|
||||||
shouldFilter={searchTerm.length > 0}
|
|
||||||
className="flex h-full w-full flex-col"
|
|
||||||
>
|
|
||||||
<Command.Input value={searchTerm} hidden />
|
|
||||||
{/* We can skip the header input since we have the main input above,
|
|
||||||
but we might need the context indicator if we want that feature.
|
|
||||||
For now, let's just render the list. */}
|
|
||||||
|
|
||||||
<Command.List className="vertical-scrollbar scrollbar-sm max-h-[60vh] overflow-y-auto px-2 pb-4 outline-none">
|
|
||||||
<ProjectsAppPowerKCommandsList
|
|
||||||
activePage={activePage}
|
|
||||||
context={context}
|
|
||||||
handleCommandSelect={handleCommandSelect}
|
|
||||||
handlePageDataSelection={handlePageDataSelection}
|
|
||||||
isWorkspaceLevel={isWorkspaceLevel}
|
|
||||||
searchTerm={searchTerm}
|
|
||||||
setSearchTerm={setSearchTerm}
|
|
||||||
handleSearchMenuClose={() => closePanel()}
|
|
||||||
/>
|
|
||||||
</Command.List>
|
|
||||||
<PowerKModalFooter
|
|
||||||
isWorkspaceLevel={isWorkspaceLevel}
|
|
||||||
projectId={context.params.projectId?.toString()}
|
|
||||||
onWorkspaceLevelChange={setIsWorkspaceLevel}
|
|
||||||
/>
|
|
||||||
</Command>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Fragment, useState, useEffect } from "react";
|
import { Fragment, useState, useEffect, useCallback, useLayoutEffect, useRef } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
// icons
|
// icons
|
||||||
import { CirclePlus, LogOut, Mails } from "lucide-react";
|
import { CirclePlus, LogOut, Mails } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
|
|
@ -48,6 +49,13 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// local state
|
// local state
|
||||||
const [isWorkspaceMenuOpen, setIsWorkspaceMenuOpen] = useState(false);
|
const [isWorkspaceMenuOpen, setIsWorkspaceMenuOpen] = useState(false);
|
||||||
|
const [sidebarPanelMenuPosition, setSidebarPanelMenuPosition] = useState<{
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const sidebarPanelButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const handleWorkspaceNavigation = (workspace: IWorkspace) => updateUserProfile({ last_workspace_id: workspace?.id });
|
const handleWorkspaceNavigation = (workspace: IWorkspace) => updateUserProfile({ last_workspace_id: workspace?.id });
|
||||||
|
|
||||||
|
|
@ -69,11 +77,40 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
||||||
const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {}));
|
const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {}));
|
||||||
// TODO: fix workspaces list scroll
|
// TODO: fix workspaces list scroll
|
||||||
|
|
||||||
|
const updateSidebarPanelMenuPosition = useCallback(() => {
|
||||||
|
if (variant !== "sidebar-panel" || !sidebarPanelButtonRef.current || typeof window === "undefined") return;
|
||||||
|
|
||||||
|
const rect = sidebarPanelButtonRef.current.getBoundingClientRect();
|
||||||
|
const width = 480;
|
||||||
|
const viewportPadding = 16;
|
||||||
|
|
||||||
|
setSidebarPanelMenuPosition({
|
||||||
|
left: Math.min(rect.left, window.innerWidth - width - viewportPadding),
|
||||||
|
top: rect.bottom + 8,
|
||||||
|
width,
|
||||||
|
});
|
||||||
|
}, [variant]);
|
||||||
|
|
||||||
// Toggle sidebar dropdown state when either menu is open
|
// Toggle sidebar dropdown state when either menu is open
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
toggleAnySidebarDropdown(isWorkspaceMenuOpen);
|
toggleAnySidebarDropdown(isWorkspaceMenuOpen);
|
||||||
}, [isWorkspaceMenuOpen, toggleAnySidebarDropdown]);
|
}, [isWorkspaceMenuOpen, toggleAnySidebarDropdown]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!isWorkspaceMenuOpen || variant !== "sidebar-panel") return;
|
||||||
|
|
||||||
|
updateSidebarPanelMenuPosition();
|
||||||
|
|
||||||
|
const handlePositionUpdate = () => updateSidebarPanelMenuPosition();
|
||||||
|
window.addEventListener("resize", handlePositionUpdate);
|
||||||
|
window.addEventListener("scroll", handlePositionUpdate, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handlePositionUpdate);
|
||||||
|
window.removeEventListener("scroll", handlePositionUpdate, true);
|
||||||
|
};
|
||||||
|
}, [isWorkspaceMenuOpen, updateSidebarPanelMenuPosition, variant]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
as="div"
|
as="div"
|
||||||
|
|
@ -138,6 +175,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
||||||
)}
|
)}
|
||||||
{variant === "sidebar-panel" && (
|
{variant === "sidebar-panel" && (
|
||||||
<Menu.Button
|
<Menu.Button
|
||||||
|
ref={sidebarPanelButtonRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/menu-button flex w-full items-center justify-between gap-2 px-0 py-1 text-left text-13 font-medium text-secondary transition-all focus:outline-none",
|
"group/menu-button flex w-full items-center justify-between gap-2 px-0 py-1 text-left text-13 font-medium text-secondary transition-all focus:outline-none",
|
||||||
{
|
{
|
||||||
|
|
@ -165,26 +203,31 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
||||||
/>
|
/>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
)}
|
)}
|
||||||
<Transition
|
{(() => {
|
||||||
as={Fragment}
|
const menuItems = (
|
||||||
enter="transition ease-out duration-100"
|
<Menu.Items
|
||||||
enterFrom="transform opacity-0 scale-95"
|
as="div"
|
||||||
enterTo="trnsform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Menu.Items as={Fragment}>
|
|
||||||
<div
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-21 mt-1 flex min-w-[30rem] origin-top-left flex-col divide-y outline-none",
|
"z-21 mt-1 flex min-w-[30rem] origin-top-left flex-col divide-y overflow-hidden outline-none",
|
||||||
{
|
{
|
||||||
"fixed divide-subtle rounded-md border-[0.5px] border-strong bg-surface-1 shadow-raised-200": variant !== "sidebar-panel",
|
"fixed divide-subtle rounded-md border-[0.5px] border-strong bg-surface-1 shadow-raised-200":
|
||||||
|
variant !== "sidebar-panel",
|
||||||
"top-11 left-14": variant === "sidebar",
|
"top-11 left-14": variant === "sidebar",
|
||||||
"top-10 left-4": variant === "top-navigation",
|
"top-10 left-4": variant === "top-navigation",
|
||||||
"nodedc-glass-modal nodedc-glass-surface absolute top-full left-0 z-[140] mt-2 rounded-[1.5rem] divide-white/10": variant === "sidebar-panel",
|
"nodedc-glass-modal nodedc-glass-surface rounded-[1.5rem] divide-white/10":
|
||||||
|
variant === "sidebar-panel",
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
style={
|
||||||
|
variant === "sidebar-panel" && sidebarPanelMenuPosition
|
||||||
|
? {
|
||||||
|
position: "fixed",
|
||||||
|
left: `${sidebarPanelMenuPosition.left}px`,
|
||||||
|
top: `${sidebarPanelMenuPosition.top}px`,
|
||||||
|
width: `${sidebarPanelMenuPosition.width}px`,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="vertical-scrollbar flex scrollbar-sm max-h-96 flex-col items-start justify-start overflow-x-hidden overflow-y-scroll">
|
<div className="vertical-scrollbar flex scrollbar-sm max-h-96 flex-col items-start justify-start overflow-x-hidden overflow-y-scroll">
|
||||||
<span
|
<span
|
||||||
|
|
@ -261,9 +304,28 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (variant === "sidebar-panel") {
|
||||||
|
if (!open || !sidebarPanelMenuPosition || typeof document === "undefined") return null;
|
||||||
|
return createPortal(menuItems, document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="trnsform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
{menuItems}
|
||||||
</Transition>
|
</Transition>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,12 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState, type RefObject } from "react";
|
||||||
import { useOutsideClickDetector } from "@plane/hooks";
|
import { useOutsideClickDetector } from "@plane/hooks";
|
||||||
|
|
||||||
type UseExpandableSearchOptions = {
|
type UseExpandableSearchOptions = {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
additionalRefs?: Array<RefObject<HTMLElement | null>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,7 +18,7 @@ type UseExpandableSearchOptions = {
|
||||||
* Opens on click, typing, or keyboard shortcut (via PowerK Cmd+F)
|
* Opens on click, typing, or keyboard shortcut (via PowerK Cmd+F)
|
||||||
*/
|
*/
|
||||||
export const useExpandableSearch = (options?: UseExpandableSearchOptions) => {
|
export const useExpandableSearch = (options?: UseExpandableSearchOptions) => {
|
||||||
const { onClose } = options || {};
|
const { onClose, additionalRefs = [] } = options || {};
|
||||||
|
|
||||||
// states
|
// states
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
@ -28,6 +29,15 @@ export const useExpandableSearch = (options?: UseExpandableSearchOptions) => {
|
||||||
const wasClickedRef = useRef<boolean>(false);
|
const wasClickedRef = useRef<boolean>(false);
|
||||||
const wasKeyboardTriggeredRef = useRef<boolean>(false);
|
const wasKeyboardTriggeredRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const isWithinTrackedElements = useCallback(
|
||||||
|
(target: Node | null) => {
|
||||||
|
if (!target) return false;
|
||||||
|
if (containerRef.current?.contains(target)) return true;
|
||||||
|
return additionalRefs.some((ref) => ref.current?.contains(target));
|
||||||
|
},
|
||||||
|
[additionalRefs]
|
||||||
|
);
|
||||||
|
|
||||||
// Handle close
|
// Handle close
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
@ -51,7 +61,7 @@ export const useExpandableSearch = (options?: UseExpandableSearchOptions) => {
|
||||||
const handlePointerDown = (event: PointerEvent) => {
|
const handlePointerDown = (event: PointerEvent) => {
|
||||||
const target = event.target as Node | null;
|
const target = event.target as Node | null;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
if (containerRef.current?.contains(target)) return;
|
if (isWithinTrackedElements(target)) return;
|
||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -59,7 +69,7 @@ export const useExpandableSearch = (options?: UseExpandableSearchOptions) => {
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("pointerdown", handlePointerDown, true);
|
document.removeEventListener("pointerdown", handlePointerDown, true);
|
||||||
};
|
};
|
||||||
}, [isOpen, handleClose]);
|
}, [handleClose, isOpen, isWithinTrackedElements]);
|
||||||
|
|
||||||
// Track keyboard shortcuts that trigger focus (Cmd+F / Ctrl+F)
|
// Track keyboard shortcuts that trigger focus (Cmd+F / Ctrl+F)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -218,14 +218,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-glass-surface {
|
.nodedc-glass-surface {
|
||||||
background: rgba(11, 11, 14, 0.82);
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.025) 0%, rgba(255, 255, 255, 0.01) 100%),
|
||||||
|
rgba(9, 9, 12, 0.88);
|
||||||
|
@apply border border-subtle/70 backdrop-blur-2xl;
|
||||||
-webkit-backdrop-filter: blur(40px);
|
-webkit-backdrop-filter: blur(40px);
|
||||||
backdrop-filter: blur(40px);
|
backdrop-filter: blur(40px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 20px 56px rgba(0, 0, 0, 0.34),
|
0 20px 56px rgba(0, 0, 0, 0.34),
|
||||||
0 4px 16px rgba(0, 0, 0, 0.18);
|
0 4px 16px rgba(0, 0, 0, 0.18);
|
||||||
isolation: isolate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-glass-modal [data-slot="button"],
|
.nodedc-glass-modal [data-slot="button"],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue