ultisuite-client/components/gmail/settings/mail-settings-search-bar.tsx
R3D347HR4Y 6ec95262af Add OnlyOffice integration and update project configurations
- Updated .env.example to include configuration for OnlyOffice Document Server.
- Modified the workspace configuration to remove the drive-suite path.
- Adjusted TypeScript environment imports for consistency.
- Enhanced Next.js configuration to disable canvas in Webpack.
- Updated package.json to include new dependencies for OnlyOffice and PDF.js.
- Added global styles for OnlyOffice theme integration in the CSS.
- Created new layout and page components for the Drive feature, including public sharing and editing functionalities.
- Updated metadata handling across various layouts to reflect the new app structure.
2026-06-07 15:49:21 +02:00

206 lines
6.3 KiB
TypeScript

"use client"
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type KeyboardEvent,
} from "react"
import { useRouter } from "next/navigation"
import { Search, X } from "lucide-react"
import { Button } from "@/components/ui/button"
import { MAIL_SEARCH_SUGGESTIONS_DROPDOWN_CLASS } from "@/lib/mail-chrome-classes"
import { DRIVE_SEARCH_INPUT_WRAP_CLASS } from "@/lib/drive/drive-chrome-classes"
import { searchMailSettings } from "@/lib/mail-settings/search-settings"
import type { MailSettingsSearchEntry } from "@/lib/mail-settings/settings-search-index"
import { cn } from "@/lib/utils"
export function MailSettingsSearchBar({ className }: { className?: string }) {
const router = useRouter()
const containerRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const [query, setQuery] = useState("")
const [focused, setFocused] = useState(false)
const [open, setOpen] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
const suggestions = useMemo(
() => searchMailSettings(query),
[query]
)
const showDropdown =
open && focused && query.trim().length > 0 && suggestions.length > 0
const navigateTo = useCallback(
(entry: MailSettingsSearchEntry) => {
router.push(entry.href)
setOpen(false)
setQuery("")
setSelectedIndex(-1)
inputRef.current?.blur()
},
[router]
)
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (!showDropdown && e.key !== "Escape") return
switch (e.key) {
case "ArrowDown":
e.preventDefault()
setSelectedIndex((i) =>
i < suggestions.length - 1 ? i + 1 : 0
)
break
case "ArrowUp":
e.preventDefault()
setSelectedIndex((i) =>
i > 0 ? i - 1 : suggestions.length - 1
)
break
case "Enter":
e.preventDefault()
if (selectedIndex >= 0 && suggestions[selectedIndex]) {
navigateTo(suggestions[selectedIndex])
} else if (suggestions[0]) {
navigateTo(suggestions[0])
}
break
case "Escape":
e.preventDefault()
setOpen(false)
setSelectedIndex(-1)
inputRef.current?.blur()
break
}
},
[showDropdown, selectedIndex, suggestions, navigateTo]
)
useEffect(() => {
setSelectedIndex(suggestions.length > 0 ? 0 : -1)
}, [query, suggestions.length])
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (!containerRef.current?.contains(event.target as Node)) {
setOpen(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => document.removeEventListener("mousedown", handleClickOutside)
}, [])
return (
<div
ref={containerRef}
data-mail-settings-search
className={cn(
"relative flex w-full min-w-0 flex-col overflow-visible",
className
)}
>
<div
className={cn(
DRIVE_SEARCH_INPUT_WRAP_CLASS,
"text-[#5f6368] dark:text-[#9aa0a6]",
focused && "shadow-md ring-1 ring-gray-300 dark:ring-gray-600"
)}
>
<div className="pointer-events-none absolute left-3.5 flex items-center">
<Search className="size-5 shrink-0" />
</div>
<input
ref={inputRef}
type="search"
value={query}
onChange={(e) => {
setQuery(e.target.value)
setOpen(true)
}}
onFocus={() => {
setFocused(true)
if (query.trim()) setOpen(true)
}}
onBlur={() => setFocused(false)}
onKeyDown={handleKeyDown}
placeholder="Rechercher dans les réglages"
className={cn(
"h-full w-full rounded-full border-0 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground",
query ? "pl-11 pr-12" : "pl-11 pr-4"
)}
role="combobox"
aria-expanded={showDropdown}
aria-controls="mail-settings-search-listbox"
aria-autocomplete="list"
autoComplete="off"
/>
{query ? (
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-2 shrink-0 rounded-full text-muted-foreground hover:text-foreground"
aria-label="Effacer la recherche"
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
setQuery("")
setOpen(false)
inputRef.current?.focus()
}}
>
<X className="size-4" />
</Button>
) : null}
</div>
{showDropdown ? (
<ul
id="mail-settings-search-listbox"
role="listbox"
className={cn(
"absolute left-0 right-0 top-[calc(100%+4px)] z-50 overflow-hidden rounded-2xl py-1",
MAIL_SEARCH_SUGGESTIONS_DROPDOWN_CLASS
)}
>
{suggestions.map((entry, index) => {
const selected = index === selectedIndex
return (
<li key={entry.id} role="presentation">
<button
type="button"
role="option"
aria-selected={selected}
className={cn(
"flex w-full items-start gap-3 px-4 py-2.5 text-left text-sm transition-colors",
selected
? "bg-accent text-accent-foreground"
: "hover:bg-accent/60"
)}
onMouseDown={(e) => e.preventDefault()}
onClick={() => navigateTo(entry)}
onMouseEnter={() => setSelectedIndex(index)}
>
<div className="min-w-0 flex-1">
<div className="truncate font-medium text-foreground">
{entry.label}
</div>
<div className="truncate text-xs text-muted-foreground">
{entry.sectionLabel}
{entry.description ? ` · ${entry.description}` : null}
</div>
</div>
</button>
</li>
)
})}
</ul>
) : null}
</div>
)
}