ModuleMenuMobile - Menu Mobile per Moduli
Panoramica
ModuleMenuMobile è il componente dedicato alla visualizzazione del menu dei moduli su dispositivi mobili e tablet. Sostituisce il menu desktop ModuleMenu quando la larghezza della finestra è inferiore a 1024px, fornendo un'esperienza ottimizzata per schermi touch.
Caratteristiche
- 🎯 Menu Sheet Laterale: Usa
SheetRootda@elerama/uiper un drawer scorrevole da sinistra - 📱 Touch-Optimized: Implementa fix specifici per lo scroll su dispositivi touch
- 🔄 Collapsible Submenu: Supporta menu a più livelli con animazioni e icone dinamiche
- ✨ Auto-Close: Si chiude automaticamente al click su un link per migliore UX mobile
Quando Viene Usato
Il componente viene renderizzato condizionalmente in ModuleMenu basandosi sul breakpoint:
File: app/components/layout/topbar/module-menu.tsx
const isMobile = useIsMobile(1024);
return (
<>
{isMobile ? (
<ModuleMenuMobile
menuData={menuData}
activeSubmenuId={activeSubmenuId}
/>
) : (
// Desktop menu...
)}
</>
);2
3
4
5
6
7
8
9
10
11
12
13
14
Breakpoint:
- < 1024px: ModuleMenuMobile (Sheet laterale)
- >= 1024px: ModuleMenu desktop (dropdown)
Struttura del Componente
Props
interface ModuleMenuMobileProps {
menuData: MenuItem[]; // Dati menu dal backend
activeSubmenuId: string | null; // ID del submenu attivo
}2
3
4
Componenti Usati
SheetRoot: Container principale del drawer (da@elerama/ui)SheetContent: Contenuto scrollabile con fix touchCollapsible: Gestisce l'espansione/collasso dei submenuCollapsibleTrigger: Pulsante per toggle del submenuCollapsibleContent: Contenuto del submenu
Implementazione
File: app/components/layout/topbar/module-menu-mobile.tsx
Apertura Sheet
<Button
variant="ghost"
size="icon"
onClick={() => setOpenMobile(true)}
aria-label="Apri menu modulo"
>
<Menu />
</Button>2
3
4
5
6
7
8
Sheet con Fix Touch Scroll
<SheetRoot open={openMobile} onOpenChange={setOpenMobile}>
<SheetContent
side="left"
className="w-[280px] p-0 flex flex-col overflow-hidden"
hideCloseButton={true}
>
{/* Container scrollabile con fix touch */}
<div
ref={scrollContainerRef}
className="flex-1 overflow-y-scroll overscroll-contain p-4"
style={{ touchAction: 'pan-y' }}
onTouchMove={(e) => e.stopPropagation()}
>
{renderMobileMenuItems(menuData)}
</div>
</SheetContent>
</SheetRoot>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Nota: Il fix touch scroll è documentato in dettaglio in Touch Scroll Fix.
Rendering Menu Items
Il metodo renderMobileMenuItems gestisce tre tipi di voci:
Voci semplici (primo livello)
typescript<Link to={`/r${item.link}`} onClick={() => setOpenMobile(false)} className={cn("rounded-md px-3 py-2...", isActive && "bg-accent")} > {item.text} </Link>1
2
3
4
5
6
7Voci con submenu (Collapsible)
typescript<CollapsibleMenuItem title={item.text} defaultOpen={isActive} > {/* Sottovoci qui */} </CollapsibleMenuItem>1
2
3
4
5
6Sottomenu annidati
- Supporta fino a 3 livelli di profondità
- Renderizza header e sottovoci raggruppate
Auto-Close al Click
Ogni link chiude automaticamente lo Sheet:
onClick={() => setOpenMobile(false)}Questo migliora l'esperienza utente mobile evitando che l'utente debba chiudere manualmente il menu dopo aver selezionato una voce.
CollapsibleMenuItem Component
Componente interno per gestire submenu collassabili con stato controllato:
function CollapsibleMenuItem({
title,
defaultOpen = false,
children
}: CollapsibleMenuItemProps) {
const [isOpen, setIsOpen] = React.useState(defaultOpen);
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger className="...">
<span>{title}</span>
{isOpen ? <ChevronDown /> : <ChevronRight />}
</CollapsibleTrigger>
<CollapsibleContent className="pl-4 pt-1 data-[state=closed]:animate-none">
{children}
</CollapsibleContent>
</Collapsible>
);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Caratteristiche:
- ✅ Icona dinamica:
ChevronDownquando aperto,ChevronRightquando chiuso - ✅ Animazione rimossa alla chiusura (
data-[state=closed]:animate-none) - ✅ Aperto di default se contiene la voce attiva (
defaultOpen={isActive})
Active State Detection
Il componente usa le utility da module-menu.utils.ts per determinare quale voce è attiva:
import { isSubmenuActive, normalizeId } from "./module-menu.utils";
const isActive = isDirectMatch || isSubmenuActive(item, activeSubmenuId);2
3
Normalizzazione ID:
- Rimuove prefissi
/erp/e/modules/ - Converte in lowercase
- Permette match flessibili tra diversi formati di URL
Styling
Larghezza Sheet
className="w-[280px]" // Più stretto del mobile standard (288px)Stati Visivi
Link Attivo:
className={cn(
"rounded-md px-3 py-2 text-sm transition-colors",
isActive
? "bg-accent text-accent-foreground font-medium"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
)}2
3
4
5
6
Collapsible Trigger:
className="flex w-full items-center justify-between rounded-md px-3 py-2
text-sm font-medium transition-colors hover:bg-accent/50
data-[state=open]:bg-accent/30"2
3
Differenze con ModuleMenu Desktop
| Caratteristica | Mobile (< 1024px) | Desktop (>= 1024px) |
|---|---|---|
| Container | Sheet laterale (drawer) | Dropdown menu |
| Trigger | Button con icona Menu | Button con testo modulo |
| Submenu | Collapsible inline | Nested dropdown |
| Scroll | Verticale con fix touch | Verticale standard |
| Auto-close | Sì, sempre | Solo su selezione |
| Larghezza | 280px fisso | Auto (min-content) |
Touch Scroll Fix
ModuleMenuMobile implementa un fix critico per lo scroll su dispositivi touch:
style={{ touchAction: 'pan-y' }}
onTouchMove={(e) => e.stopPropagation()}2
Problema risolto:
- Radix UI Sheet applica gestori touch sull'overlay per implementare la chiusura al click fuori
- Questi gestori interferiscono con lo scroll interno su dispositivi touch
- Senza il fix, gli utenti non possono scrollare il contenuto del menu su mobile
Documentazione completa: Touch Scroll Fix
Test
Pagina di test: /test-iframe nella tab del menu modulo
Test Manuale
- Riduci la finestra del browser < 1024px
- Verifica che appaia l'icona Menu invece del dropdown desktop
- Clicca sull'icona Menu
- Verifica che si apra lo Sheet da sinistra
- Testa lo scroll verticale su dispositivo touch o con DevTools touch emulation
- Clicca su una voce di menu
- Verifica che lo Sheet si chiuda automaticamente
Test Collapsible
- Apri il menu mobile
- Clicca su una voce con submenu
- Verifica che si espanda con animazione
- Verifica che l'icona cambi da ChevronRight a ChevronDown
- Clicca nuovamente per collassare
- Verifica che l'icona torni ChevronRight
Compatibilità
- ✅ iOS Safari (fix touch scroll necessario)
- ✅ Android Chrome (fix touch scroll necessario)
- ✅ Desktop in modalità mobile (DevTools)
- ✅ Tablet in orientamento portrait e landscape
Note Tecniche
Perché SheetRoot invece di Sheet?
Dal commit 857829e, il progetto usa SheetRoot invece di Sheet:
Motivazione:
- Migliore gestione del mobile handling
- Più controllo sul comportamento del drawer
- Compatibilità migliorata con fix touch scroll
Vedi anche: Sidebar Migration Note
Key Forcing per Re-render
Il menu mobile viene forzato a re-renderizzare usando activeSubmenuId nelle key:
key={`mobile-${activeSubmenuId}-${item.link}-${index}`}Questo previene problemi di stato stale quando l'utente naviga tra moduli.
Best Practices
DO ✅
- Usa sempre
setOpenMobile(false)al click sui link - Implementa il fix touch scroll per scroll container
- Usa Collapsible per submenu invece di nested Sheet
- Normalizza gli ID prima di confrontare active state
DON'T ❌
- Non omettere
stopPropagation()sugli eventi touch - Non usare nested Sheet per submenu (usa Collapsible)
- Non dimenticare
hideCloseButton={true}su SheetContent - Non fare scroll orizzontale (usa sempre
touchAction: 'pan-y')
Risorse Correlate
- Touch Scroll Fix - Dettagli fix scroll touch
- Sidebar Mobile - Sidebar mobile con Sheet