From ee13f8ba994a482d74343298ad0fe1cbf20d83e0 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Wed, 18 Feb 2026 08:25:08 +0100 Subject: [PATCH] feat: enhance dropdown components by integrating useClickOutside hook for improved user experience and accessibility in NewWorkshopDropdown and WorkshopTabs --- src/app/sessions/NewWorkshopDropdown.tsx | 8 +- src/app/sessions/WorkshopTabs.tsx | 8 +- src/components/collaboration/ShareModal.tsx | 114 ++++++++---------- src/components/layout/Header.tsx | 17 +-- src/components/swot/ActionPanel.tsx | 25 ++-- src/components/ui/Select.tsx | 64 +++++++--- src/components/ui/index.ts | 1 + src/components/weather/WeatherCard.tsx | 125 ++++++-------------- src/hooks/useClickOutside.ts | 24 ++++ 9 files changed, 189 insertions(+), 197 deletions(-) create mode 100644 src/hooks/useClickOutside.ts diff --git a/src/app/sessions/NewWorkshopDropdown.tsx b/src/app/sessions/NewWorkshopDropdown.tsx index adbbe8a..2db5c31 100644 --- a/src/app/sessions/NewWorkshopDropdown.tsx +++ b/src/app/sessions/NewWorkshopDropdown.tsx @@ -1,21 +1,23 @@ 'use client'; -import { useState } from 'react'; +import { useState, useRef } from 'react'; import Link from 'next/link'; import { Button } from '@/components/ui'; import { WORKSHOPS } from '@/lib/workshops'; +import { useClickOutside } from '@/hooks/useClickOutside'; export function NewWorkshopDropdown() { const [open, setOpen] = useState(false); + const containerRef = useRef(null); + useClickOutside(containerRef, () => setOpen(false), open); return ( -
+
- + setRole(e.target.value as ShareRole)} + options={[...ROLE_OPTIONS]} + wrapperClassName="w-auto shrink-0 min-w-[7rem]" + />
)} @@ -211,39 +211,27 @@ export function ShareModal({ .

) : ( - <> -
- -
- - - -
-
-
- -
- - - -
-
- +
+ setRole(e.target.value as ShareRole)} + options={[...ROLE_OPTIONS]} + wrapperClassName="w-auto shrink-0 min-w-[7rem]" + /> +
)} )} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 66822bc..f172a97 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -4,15 +4,20 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { useSession, signOut } from 'next-auth/react'; import { useTheme } from '@/contexts/ThemeContext'; -import { useState } from 'react'; +import { useState, useRef } from 'react'; import { Avatar, RocketIcon } from '@/components/ui'; import { WORKSHOPS } from '@/lib/workshops'; +import { useClickOutside } from '@/hooks/useClickOutside'; export function Header() { const { theme, toggleTheme } = useTheme(); const { data: session, status } = useSession(); const [menuOpen, setMenuOpen] = useState(false); const [workshopsOpen, setWorkshopsOpen] = useState(false); + const workshopsDropdownRef = useRef(null); + const userMenuRef = useRef(null); + useClickOutside(workshopsDropdownRef, () => setWorkshopsOpen(false), workshopsOpen); + useClickOutside(userMenuRef, () => setMenuOpen(false), menuOpen); const pathname = usePathname(); const isActiveLink = (path: string) => pathname.startsWith(path); @@ -61,10 +66,9 @@ export function Header() { {/* New Workshop Dropdown */} -
+
{menuOpen && ( - <> -
setMenuOpen(false)} /> -
+

Connecté en tant que

@@ -175,7 +177,6 @@ export function Header() { Se déconnecter

- )}
) : ( diff --git a/src/components/swot/ActionPanel.tsx b/src/components/swot/ActionPanel.tsx index fff5c8f..88c7df0 100644 --- a/src/components/swot/ActionPanel.tsx +++ b/src/components/swot/ActionPanel.tsx @@ -2,7 +2,7 @@ import { useState, useTransition } from 'react'; import type { SwotItem, Action, ActionLink, SwotCategory } from '@prisma/client'; -import { Button, Badge, Modal, ModalFooter, Input, Textarea } from '@/components/ui'; +import { Button, Badge, Modal, ModalFooter, Input, Textarea, Select } from '@/components/ui'; import { createAction, updateAction, deleteAction } from '@/actions/swot'; type ActionWithLinks = Action & { @@ -40,11 +40,11 @@ const categoryShort: Record = { }; const priorityLabels = ['Basse', 'Moyenne', 'Haute']; -const statusLabels: Record = { - todo: 'À faire', - in_progress: 'En cours', - done: 'Terminé', -}; +const statusOptions = [ + { value: 'todo', label: '📋 À faire' }, + { value: 'in_progress', label: '⏳ En cours' }, + { value: 'done', label: '✅ Terminé' }, +]; export function ActionPanel({ sessionId, @@ -279,16 +279,15 @@ export function ActionPanel({ > {priorityLabels[action.priority]} - + />
))} diff --git a/src/components/ui/Select.tsx b/src/components/ui/Select.tsx index b6a740f..a6ac04d 100644 --- a/src/components/ui/Select.tsx +++ b/src/components/ui/Select.tsx @@ -1,24 +1,63 @@ import { forwardRef, SelectHTMLAttributes } from 'react'; -interface SelectOption { +export interface SelectOption { value: string; label: string; disabled?: boolean; } -interface SelectProps extends Omit, 'children'> { +const SIZE_STYLES = { + xs: 'px-2 py-1 pr-7 text-xs', + sm: 'px-2 py-2 pr-8 text-sm', + md: 'px-4 py-2.5 pr-10 text-sm', + lg: 'px-4 py-2.5 pr-10 text-base', +} as const; + +const ICON_SIZES = { + xs: 'h-3 w-3', + sm: 'h-4 w-4', + md: 'h-5 w-5', + lg: 'h-5 w-5', +} as const; + +const ICON_POSITION = { + xs: 'right-2', + sm: 'right-2', + md: 'right-3', + lg: 'right-3', +} as const; + +interface SelectProps extends Omit, 'children' | 'size'> { label?: string; error?: string; options: SelectOption[]; placeholder?: string; + size?: keyof typeof SIZE_STYLES; + wrapperClassName?: string; } export const Select = forwardRef( - ({ className = '', label, error, id, options, placeholder, ...props }, ref) => { + ( + { + className = '', + label, + error, + id, + options, + placeholder, + size = 'md', + wrapperClassName = '', + ...props + }, + ref + ) => { const selectId = id || props.name; + const sizeStyles = SIZE_STYLES[size]; + const iconSize = ICON_SIZES[size]; + const iconPosition = ICON_POSITION[size]; return ( -
+
{label && (
+ handleEmojiChange('valueCreation', e.target.value || null)} - className="w-16 appearance-none rounded-lg border border-border bg-card px-2 py-2.5 pr-8 text-center text-lg text-foreground transition-colors hover:bg-card-hover focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20" - > - {WEATHER_EMOJIS.map(({ emoji }) => ( - - ))} - -
- - - -
-
+