Menu

Un menu è un <details> con un pannello stilizzato. È una scelta voluta rispetto all’API popover: un popover vive nel livello superiore e non si può posizionare rispetto al proprio innesco senza l’anchor positioning, che non è ancora ovunque. Un <details> si posiziona correttamente già oggi e non ha bisogno di caricare nulla.

L’innesco è quel <summary>, stilizzato come un pulsante — quindi passi l’etichetta e le props del pulsante a menu() invece di passare un button() già renderizzato. Un summary è già interattivo, e un pulsante al suo interno annida due controlli dove c’è una sola azione: markup non valido, e due fermate di tabulazione per una cosa sola.

La chiusura al clic fuori e con Escape arrivano da un handler ontoggle che le importa la prima volta che un menu viene aperto — e solo allora. Se quel modulo non arriva mai, un menu si apre e si chiude comunque dal proprio summary.

menu({ trigger: 'Azioni' },
  menuItem({ href: '#edit' }, 'Modifica'),
  menuItem({ href: '#duplicate' }, 'Duplica'),
  menuSeparator(),
  menuItem({ href: '#delete' }, 'Elimina'),
)

Allineamento

Un menu si apre dal bordo iniziale del proprio innesco. align: 'end' lo ribalta, che è ciò di cui ha bisogno un menu vicino al bordo destro di una barra.

Allineato all’inizio
Allineato alla fine
stack({ direction: 'row', gap: 'xl', justify: 'space-between', style: 'width: 100%' },
  menu({ trigger: 'Allineato all’inizio', variant: 'soft' },
    menuItem({ href: '#a' }, 'Primo'),
    menuItem({ href: '#b' }, 'Secondo'),
  ),
  menu({ trigger: 'Allineato alla fine', variant: 'soft', align: 'end' },
    menuItem({ href: '#c' }, 'Primo'),
    menuItem({ href: '#d' }, 'Secondo'),
  ),
)

Inneschi a icona

Un’icona senza testo in trigger ha bisogno di una label — diventa il nome accessibile che l’icona non può fornire.

stack({ direction: 'row', gap: 'md' },
  menu({
    align: 'end',
    label: 'Altre azioni',
    variant: 'ghost',
    icon: icon('more-horizontal'),
  },
    menuItem({ href: '#rename' }, 'Rinomina'),
    menuItem({ href: '#move' }, 'Sposta'),
    menuSeparator(),
    menuItem({ href: '#archive' }, 'Archivia'),
  ),
)

Elementi con icone

menu({ trigger: 'File' },
  menuItem({
    href: '#new',
    icon: icon('plus'),
  }, 'Nuova pagina'),
  menuItem({
    href: '#open',
    icon: icon('folder'),
  }, 'Apri…'),
  menuSeparator(),
  menuItem({
    href: '#build',
    icon: icon('zap'),
  }, 'Costruisci il sito'),
)

Un elemento senza href renderizza un <button> — per un’azione che avviene nella pagina invece di una navigazione.

Esporta
menu({ trigger: 'Esporta', variant: 'soft', color: 'primary' },
  menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Esportato come JSON.',{color:'success'}))" }, 'Come JSON'),
  menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Esportato come CSV.',{color:'success'}))" }, 'Come CSV'),
)

In una barra applicazione

appBar({ brand: 'sitelo' },
  appBarSpacer(),
  appBarActions(
    themeToggle(),
    menu({
      align: 'end',
      label: 'Altro',
      variant: 'ghost',
      icon: icon('more-horizontal'),
    },
      menuItem({ href: '/docs' }, 'Documentazione'),
      menuItem({ href: '/examples' }, 'Esempi'),
      menuSeparator(),
      menuItem({ href: 'https://github.com/paul-browne/sitelo' }, 'GitHub'),
    ),
  ),
)

Accessibilità

Il pannello è un role="menu" i cui elementi sono role="menuitem", e il summary porta aria-haspopup. Un <details> non è un widget menu nativo, quindi questa è un’approssimazione ragionevole più che una perfetta — per un semplice elenco di link, un nav dentro il details è altrettanto valido e promette meno.

Props

menu() — le props dell’innesco sono quelle del pulsante:

PropTipoPredefinitoDescrizione
triggerChild—Etichetta visibile. Passa del testo, non un button() già renderizzato.
iconChild—Markup prima dell’etichetta, o da solo per un innesco di sola icona.
labelstring—Nome accessibile. Obbligatorio quando c’è un’icona e nessun testo in trigger.
variant'solid' | 'soft' | 'outline' | 'ghost' | 'link''outline'Stile dell’innesco.
color'primary' | 'neutral' | 'success' | 'warning' | 'danger''neutral'Da quale palette attinge l’innesco.
size'sm' | 'md' | 'lg''md'Dimensione dell’innesco.
align'start' | 'end''start'A quale bordo dell’innesco si allinea il pannello.
triggerClassstring—Classi extra per l’innesco invece che per il details che lo avvolge.

menuItem():

PropTipoPredefinitoDescrizione
hrefstring—Renderizza un’ancora; senza, un pulsante.
iconChild—Markup prima dell’etichetta.
asstring'button'Elemento da renderizzare quando non c’è href.

menuSeparator() non accetta props — è il filetto fra gruppi di elementi.