Skip to content

Sidebar - Comunicazione con Iframe

Panoramica

Il controllo della sidebar può essere gestito dall'interno dell'iframe usando l'API esposta su window.parent.sidebar. Questo permette all'applicazione PHP legacy di controllare lo stato della sidebar nella SPA React.

La sidebar ha comportamenti differenti basati sulla risoluzione dello schermo per garantire la migliore esperienza utente su tutti i dispositivi.

Comportamenti Basati sulla Risoluzione

La sidebar utilizza due breakpoint principali per determinare il suo comportamento:

Breakpoint: 1024px (Mobile Mode)

Larghezza finestra < 1024px:

  • La sidebar passa in modalità mobile (Sheet laterale)
  • Si sovrappone al contenuto invece di spostarlo
  • Usa componente SheetRoot invece della sidebar desktop
  • La chiusura avviene anche cliccando sull'overlay

Larghezza finestra >= 1024px:

  • La sidebar è in modalità desktop
  • Può essere collapsed/expanded
  • Il contenuto si adatta alla larghezza disponibile

Breakpoint: 1280px (Stato Iniziale)

Larghezza finestra < 1280px:

  • ✅ La sidebar parte chiusa al caricamento iniziale
  • ✅ Il fallback automatico è disabilitato (non si riapre automaticamente)
  • ℹ️ L'utente può comunque aprirla manualmente con il toggle
  • ℹ️ L'iframe può comunque controllarla tramite window.parent.sidebar.open()

Larghezza finestra >= 1280px:

  • ✅ La sidebar parte aperta al caricamento iniziale
  • ✅ Il fallback automatico è attivo (si riapre dopo 300ms se l'iframe non la controlla)
  • ℹ️ Comportamento ideale per schermi desktop

Riepilogo Comportamenti

RisoluzioneStato InizialeFallback AutoModalitàNote
< 1024pxChiusaDisabilitatoMobile (Sheet)Overlay sopra il contenuto
1024px - 1279pxChiusaDisabilitatoDesktopSidebar collassabile
>= 1280pxApertaAttivo (300ms)DesktopEsperienza desktop completa

Implementazione dei Breakpoint

File: app/components/ui/sidebar.tsx

typescript
function SidebarProvider({ defaultOpen, ... }) {
    // Breakpoint mobile: 1025px (attiva mobile mode quando width <= 1024px)
    const isMobile = useIsMobile(1025)

    // Stato iniziale basato sulla risoluzione
    const [_open, _setOpen] = React.useState(() => {
        if (defaultOpen !== undefined) {
            return defaultOpen
        }
        // >= 1280px: aperta, < 1280px: chiusa
        return window.innerWidth >= 1280
    })
    // ...
}

File: app/routes/r.$path.tsx

typescript
// Fallback per riaprire la sidebar dopo 300ms
const sidebarFallbackTimer = setTimeout(() => {
    const hasControlled = useSidebarStore.getState().iframeHasControlledSidebar;

    if (hasControlled) {
        console.log("Fallback saltato: iframe ha controllato la sidebar");
        return;
    }

    // Non riaprire automaticamente su schermi piccoli
    if (window.innerWidth < 1280) {
        console.log("Fallback saltato: schermo piccolo");
        return;
    }

    window.sidebar?.open();
}, 300);

API Disponibile

window.parent.sidebar.close()

Chiude la sidebar.

Esempio:

javascript
// Dall'interno dell'iframe
window.parent.sidebar.close();

window.parent.sidebar.open()

Apre la sidebar.

Esempio:

javascript
// Dall'interno dell'iframe
window.parent.sidebar.open();

window.parent.sidebar.toggle()

Alterna lo stato della sidebar (aperta ↔ chiusa).

Esempio:

javascript
// Dall'interno dell'iframe
window.parent.sidebar.toggle();

Implementazione

Hook: useTopbarSidebar

L'hook useTopbarSidebar gestisce la registrazione dell'API sidebar sul window parent. Segue lo stesso pattern degli altri hook di comunicazione (topbar, messages, cart, navigation).

File: app/hooks/ui/useTopbarSidebar.ts

typescript
import { useEffect } from "react";

export function useTopbarSidebar(setOpen?: (open: boolean) => void, toggleSidebar?: () => void) {
    useEffect(() => {
        console.log("[useTopbarSidebar] Hook montato, registrazione sidebar...");

        const closeFromIframe = () => {
            console.log("[useTopbarSidebar] 🚪 close chiamata dall'iframe");
            if (setOpen) {
                setOpen(false);
                console.log("[useTopbarSidebar] ✅ Sidebar chiusa");
            } else {
                console.warn("[useTopbarSidebar] ⚠️ setOpen non disponibile");
            }
        };

        const openFromIframe = () => {
            console.log("[useTopbarSidebar] 🚪 open chiamata dall'iframe");
            if (setOpen) {
                setOpen(true);
                console.log("[useTopbarSidebar] ✅ Sidebar aperta");
            } else {
                console.warn("[useTopbarSidebar] ⚠️ setOpen non disponibile");
            }
        };

        const toggleFromIframe = () => {
            console.log("[useTopbarSidebar] 🔄 toggle chiamata dall'iframe");
            if (toggleSidebar) {
                toggleSidebar();
                console.log("[useTopbarSidebar] ✅ Sidebar toggled");
            } else {
                console.warn("[useTopbarSidebar] ⚠️ toggleSidebar non disponibile");
            }
        };

        window.sidebar = {
            close: closeFromIframe,
            open: openFromIframe,
            toggle: toggleFromIframe,
        };

        console.log("[useTopbarSidebar] ✅ Oggetto sidebar registrato sul window");

        return () => {
            console.log("[useTopbarSidebar] ⚠️ Cleanup: rimozione sidebar...");
            delete window.sidebar;
        };
    }, [setOpen, toggleSidebar]);
}

Bridge Component: SidebarIframeBridge

Per accedere al useSidebar context, viene utilizzato un componente bridge che deve essere renderizzato dentro il SidebarProvider.

File: app/components/layout/sidebar/sidebar-iframe-bridge.tsx

typescript
import { useSidebar } from "@/components/ui/sidebar";
import { useTopbarSidebar } from "@/hooks/ui/useTopbarSidebar";

export function SidebarIframeBridge() {
    const { setOpen, toggleSidebar } = useSidebar();
    useTopbarSidebar(setOpen, toggleSidebar);
    return null;
}

Utilizzo in AuthenticatedLayout

Il bridge component viene renderizzato dentro il SidebarProvider (solo quando non è in minimal mode):

typescript
<SidebarProvider>
    <SidebarIframeBridge />
    <AppSidebar className="bg-secondary" />
    <SidebarInset>
        {/* ... */}
    </SidebarInset>
</SidebarProvider>

In minimalMode, la sidebar non è disponibile e l'API viene comunque registrata ma senza effetto.

Test

Pagina di Test

La pagina /test-iframe include test per verificare la funzionalità della sidebar:

  1. Check Sidebar API - Verifica che window.parent.sidebar sia disponibile con tutti i metodi
  2. Close Sidebar - Testa la chiusura della sidebar dall'iframe
  3. Open Sidebar - Testa l'apertura della sidebar dall'iframe
  4. Toggle Sidebar - Testa l'alternanza dello stato della sidebar dall'iframe

Test Manuale

  1. Avvia l'applicazione in development
  2. Naviga a /test-iframe
  3. Clicca sulla tab "🚪 Sidebar"
  4. Clicca su "Verifica Sidebar API" per controllare la disponibilità di tutti i metodi
  5. Prova i pulsanti:
    • "🚪 Chiudi Sidebar" per chiudere
    • "📖 Apri Sidebar" per aprire
    • "🔄 Toggle Sidebar" per alternare lo stato

Logging

L'hook logga tutte le operazioni nella console per facilitare il debug:

  • Montaggio dell'hook e registrazione dell'API
  • Chiamate dall'iframe
  • Cleanup alla rimozione del componente

Note Tecniche

Pattern di Implementazione

L'implementazione segue lo stesso pattern degli altri hook di comunicazione iframe:

  1. Hook React che gestisce la registrazione sul window
  2. Oggetto esposto con metodi callable dall'iframe
  3. Cleanup automatico al unmount del componente
  4. Type definitions in window.d.ts
  5. Test nell'iframe di test

Type Definitions

I types sono definiti in app/types/window.d.ts:

typescript
interface Window {
    sidebar?: {
        /**
         * Chiude la sidebar
         */
        close: () => void;
        /**
         * Apre la sidebar
         */
        open: () => void;
        /**
         * Alterna lo stato della sidebar
         */
        toggle: () => void;
    };
}

Compatibilità

  • ✅ Funziona solo quando l'iframe è caricato all'interno del AuthenticatedLayout
  • ✅ La sidebar deve essere disponibile (modalità non-minimal)
  • ✅ Compatibile con il pattern StrictMode-safe di React 19
  • ✅ Responsive: si adatta automaticamente a desktop (>= 1024px) e mobile (< 1024px)
  • ✅ Stato iniziale adattivo basato su risoluzione (breakpoint 1280px)
  • ✅ Fallback automatico intelligente (attivo solo su schermi >= 1280px)

Casi d'Uso

Controllo Responsive

È consigliabile verificare la risoluzione prima di controllare la sidebar per rispettare le convenzioni UX:

javascript
// Controllo adattivo della sidebar basato sulla risoluzione
function openSidebarIfDesktop() {
    if (window.parent && window.parent.sidebar) {
        // Apri la sidebar solo su schermi >= 1280px
        if (window.innerWidth >= 1280) {
            window.parent.sidebar.open();
            console.log('Sidebar aperta (desktop)');
        } else {
            console.log('Sidebar non aperta automaticamente (mobile/tablet)');
        }
    }
}

// Chiudi sempre la sidebar indipendentemente dalla risoluzione
function closeSidebar() {
    if (window.parent && window.parent.sidebar) {
        window.parent.sidebar.close();
        console.log('Sidebar chiusa');
    }
}

Chiusura Automatica

L'applicazione PHP può chiudere automaticamente la sidebar quando l'utente inizia a lavorare su una schermata specifica:

javascript
// Dall'interno dell'iframe PHP
if (window.parent && window.parent.sidebar) {
    window.parent.sidebar.close();
}

Nota: La chiusura funziona su tutte le risoluzioni e modalità (desktop/mobile).

Apertura al Click

L'applicazione PHP può aprire la sidebar per mostrare il menu:

javascript
// Dall'interno dell'iframe PHP
if (window.parent && window.parent.sidebar) {
    window.parent.sidebar.open();
}

Nota: L'apertura funziona su tutte le risoluzioni. Tuttavia, su schermi < 1280px, considera se l'apertura automatica migliora o peggiora l'esperienza utente (potrebbe coprire il contenuto).

Toggle per Shortcuts

L'applicazione PHP può alternare lo stato della sidebar con una scorciatoia:

javascript
// Dall'interno dell'iframe PHP
if (window.parent && window.parent.sidebar) {
    window.parent.sidebar.toggle();
}

Controllo Condizionale

javascript
// Verifica disponibilità prima di chiamare
function controlSidebar(action) {
    if (window.parent && window.parent.sidebar) {
        switch(action) {
            case 'close':
                if (typeof window.parent.sidebar.close === 'function') {
                    window.parent.sidebar.close();
                    console.log('Sidebar chiusa');
                }
                break;
            case 'open':
                if (typeof window.parent.sidebar.open === 'function') {
                    window.parent.sidebar.open();
                    console.log('Sidebar aperta');
                }
                break;
            case 'toggle':
                if (typeof window.parent.sidebar.toggle === 'function') {
                    window.parent.sidebar.toggle();
                    console.log('Sidebar toggled');
                }
                break;
        }
    } else {
        console.warn('Sidebar API non disponibile');
    }
}

Migrazione Sheet → SheetRoot

Contesto Storico

Commit: 857829e - "fix: replace Sheet component with SheetRoot in ModuleMenuMobile and Sidebar for improved mobile handling"

Data: Dicembre 2024

Cambiamento

Il progetto è migrato da Sheet a SheetRoot per i componenti mobili:

Prima:

typescript
import { Sheet, SheetContent } from "@elerama/ui"

<Sheet open={openMobile} onOpenChange={setOpenMobile}>
    <SheetContent side="left">
        {/* contenuto */}
    </SheetContent>
</Sheet>

Dopo:

typescript
import { SheetRoot, SheetContent } from "@elerama/ui"

<SheetRoot open={openMobile} onOpenChange={setOpenMobile}>
    <SheetContent side="left">
        {/* contenuto */}
    </SheetContent>
</SheetRoot>

Motivazioni

  1. Migliore Mobile Handling

    • SheetRoot offre un controllo più granulare sul comportamento del drawer
    • Compatibilità migliorata con il Touch Scroll Fix
    • Gestione più robusta degli eventi touch su dispositivi mobili
  2. Architettura Componenti

    • Separazione più chiara tra root container e content
    • Allineamento con pattern Radix UI moderni
    • Facilita l'implementazione di funzionalità avanzate
  3. Stabilità

    • Risolve edge cases su iOS Safari con scroll touch
    • Migliore gestione dell'overlay e click-outside
    • Performance migliorata su dispositivi low-end

Componenti Aggiornati

File modificati:

  • app/components/ui/sidebar.tsx:14,182 - Sidebar mobile
  • app/components/layout/topbar/module-menu-mobile.tsx:14,144 - Menu modulo mobile

Breaking Changes

Nessun breaking change per i consumer:

  • Le props sono identiche
  • Il comportamento è retrocompatibile
  • L'API esposta su window.sidebar non cambia

Migrazione per Altri Componenti

Se hai componenti custom che usano Sheet, migrali seguendo questo pattern:

  1. Aggiorna import

    typescript
    // Prima
    import { Sheet } from "@elerama/ui"
    
    // Dopo
    import { SheetRoot } from "@elerama/ui"
  2. Sostituisci componente

    typescript
    // Prima
    <Sheet open={open} onOpenChange={setOpen}>
    
    // Dopo
    <SheetRoot open={open} onOpenChange={setOpen}>
  3. Verifica touch scroll

    • Se il contenuto è scrollabile, applica il Touch Scroll Fix
    • Testa su dispositivi touch reali (non solo DevTools)

Riferimenti


Best Practices

Raccomandazioni per l'Uso dell'API

  1. Apertura Automatica

    • ✅ Consigliata solo su schermi >= 1280px
    • ⚠️ Valuta attentamente su schermi < 1280px (potrebbe coprire il contenuto)
    • ℹ️ Il fallback gestisce già l'apertura automatica su desktop
  2. Chiusura Automatica

    • ✅ Consigliata su tutte le risoluzioni quando necessario
    • ✅ Utile per massimizzare lo spazio disponibile durante il lavoro
  3. Toggle

    • ✅ Ideale per shortcuts da tastiera o click dell'utente
    • ✅ Funziona bene su tutte le risoluzioni
  4. Verifica della Risoluzione

    javascript
    // Pattern consigliato per apertura condizionale
    function shouldOpenSidebar() {
        return window.innerWidth >= 1280;
    }
    
    if (shouldOpenSidebar() && window.parent?.sidebar) {
        window.parent.sidebar.open();
    }
  5. Rispetta il Controllo dell'Utente

    • Se l'utente chiude manualmente la sidebar, evita di riaprirla automaticamente
    • Considera l'uso di localStorage per tracciare le preferenze utente
    • Il sistema di fallback rispetta già questa logica (verifica iframeHasControlledSidebar)

Casi da Evitare

Non aprire automaticamente la sidebar su mobile senza un motivo valido:

javascript
// NON FARE - ignora la risoluzione
window.parent.sidebar.open(); // Copre il contenuto su schermi piccoli

Non forzare lo stato ripetutamente:

javascript
// NON FARE - loop infinito
setInterval(() => {
    window.parent.sidebar.open();
}, 1000);

FARE - Rispetta la risoluzione:

javascript
// Pattern corretto
if (window.innerWidth >= 1280 && window.parent?.sidebar) {
    window.parent.sidebar.open();
}

Documentazione Elerama Frontend