Menu

Um menu é um <details> com um painel estilizado. É uma escolha deliberada em vez da API de popover: um popover vive na camada de topo e não pode ser posicionado em relação ao seu acionador sem posicionamento por âncora, que ainda não está em toda a parte. Um <details> posiciona-se bem já hoje e não precisa de nada carregado.

O acionador é esse <summary>, com ar de botão — por isso passas a etiqueta e as props de botão ao menu() em vez de passares um button() já desenhado. Um summary já é interativo, e um botão lá dentro aninha dois controlos onde existe uma só ação: marcação inválida, e duas paragens de tabulação para uma coisa.

Fechar ao clicar fora e com Escape vem de um handler ontoggle que os importa na primeira vez que um menu é aberto — e só então. Se esse módulo nunca chegar, um menu continua a abrir e fechar pelo seu próprio summary.

menu({ trigger: 'Ações' },
  menuItem({ href: '#edit' }, 'Editar'),
  menuItem({ href: '#duplicate' }, 'Duplicar'),
  menuSeparator(),
  menuItem({ href: '#delete' }, 'Eliminar'),
)

Alinhamento

Um menu abre a partir da berma inicial do seu acionador. align: 'end' inverte isso, que é o que um menu junto à berma direita de uma barra precisa.

Alinhado ao início
Alinhado ao fim
stack({ direction: 'row', gap: 'xl', justify: 'space-between', style: 'width: 100%' },
  menu({ trigger: 'Alinhado ao início', variant: 'soft' },
    menuItem({ href: '#a' }, 'Primeiro'),
    menuItem({ href: '#b' }, 'Segundo'),
  ),
  menu({ trigger: 'Alinhado ao fim', variant: 'soft', align: 'end' },
    menuItem({ href: '#c' }, 'Primeiro'),
    menuItem({ href: '#d' }, 'Segundo'),
  ),
)

Acionadores de ícone

Um ícone sem texto de trigger precisa de um label — passa a ser o nome acessível que o ícone não consegue dar.

stack({ direction: 'row', gap: 'md' },
  menu({
    align: 'end',
    label: 'Mais ações',
    variant: 'ghost',
    icon: icon('more-horizontal'),
  },
    menuItem({ href: '#rename' }, 'Mudar o nome'),
    menuItem({ href: '#move' }, 'Mover'),
    menuSeparator(),
    menuItem({ href: '#archive' }, 'Arquivar'),
  ),
)

Itens com ícones

menu({ trigger: 'Ficheiro' },
  menuItem({
    href: '#new',
    icon: icon('plus'),
  }, 'Página nova'),
  menuItem({
    href: '#open',
    icon: icon('folder'),
  }, 'Abrir…'),
  menuSeparator(),
  menuItem({
    href: '#build',
    icon: icon('zap'),
  }, 'Construir o site'),
)

Botões em vez de ligações

Um item sem href desenha um <button> — para uma ação que acontece na página em vez de uma navegação.

Exportar
menu({ trigger: 'Exportar', variant: 'soft', color: 'primary' },
  menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Exportado como JSON.',{color:'success'}))" }, 'Como JSON'),
  menuItem({ onclick: "import('/su/toast.js').then(m=>m.toast('Exportado como CSV.',{color:'success'}))" }, 'Como CSV'),
)

Numa barra da aplicação

appBar({ brand: 'sitelo' },
  appBarSpacer(),
  appBarActions(
    themeToggle(),
    menu({
      align: 'end',
      label: 'Mais',
      variant: 'ghost',
      icon: icon('more-horizontal'),
    },
      menuItem({ href: '/pt/docs' }, 'Documentação'),
      menuItem({ href: '/pt/examples' }, 'Exemplos'),
      menuSeparator(),
      menuItem({ href: 'https://github.com/paul-browne/sitelo' }, 'GitHub'),
    ),
  ),
)

Acessibilidade

O painel é um role="menu" cujos itens são role="menuitem", e o summary leva aria-haspopup. Um <details> não é um widget de menu nativo, por isso isto é uma aproximação razoável e não uma perfeita — para uma lista simples de ligações, um nav dentro do details é igualmente válido e promete menos.

Props

menu() — as props do acionador são as do botão:

PropTipoPredefiniçãoDescrição
triggerChild—Etiqueta visível. Passa texto, não um button() já desenhado.
iconChild—Marcação antes da etiqueta, ou sozinha para um acionador só de ícone.
labelstring—Nome acessível. Obrigatório quando há ícone e não há texto de trigger.
variant'solid' | 'soft' | 'outline' | 'ghost' | 'link''outline'Estilo do acionador.
color'primary' | 'neutral' | 'success' | 'warning' | 'danger''neutral'De que paleta o acionador bebe.
size'sm' | 'md' | 'lg''md'Tamanho do acionador.
align'start' | 'end''start'Com que berma do acionador o painel se alinha.
triggerClassstring—Classes extra para o acionador em vez do details que o envolve.

menuItem():

PropTipoPredefiniçãoDescrição
hrefstring—Desenha uma âncora; sem ele, um botão.
iconChild—Marcação antes da etiqueta.
asstring'button'Elemento a renderizar quando não há href.

menuSeparator() não aceita props — é o fio entre grupos de itens.