Makuco UI
ComponentesData Entry & Selection

Mentions

Campo de texto onde `@` e `#` abrem um dropdown, e a option escolhida vira uma tag indivisível dentro do texto. O `mk-mentions` não filtra e não busca — ele detecta o gatilho, emite a query, e renderiza exatamente o que você devolver.

O mk-mentions é o campo de comentário com menções do Makuco UI. Use quando o usuário precisa escrever texto corrido e, no meio dele, apontar para pessoas (@) ou etiquetas (#) — comentários, chat, descrição de tarefa, campo de "Atribuir a". O componente cuida do caret, do chrome do campo, do posicionamento do dropdown e do card, da navegação por teclado e do contrato ARIA de combobox; você é dono dos dados.

O componente não filtra

A lista exibida é sempre exatamente options. Escute mkQuery para saber qual trigger está ativo e qual query buscar, e devolva o resultado em options. O componente nunca faz fetch, nunca ordena, nunca normaliza acento.

Reidratação não é suportada na v1

value é texto plano e não carrega identidade. Atribuí-lo de fora — abrir um comentário salvo e continuar editando — reconstrói o editor como texto puro: sem tags estilizadas, sem ids, sem card. Para exibir um comentário salvo, renderize-o fora do campo; use o mk-mentions só para escrever um novo.

Padrão

Digite @ para mencionar alguém, ou # para etiquetar.

import { useState } from 'react';
import { MkMentions } from '@db1/makuco-ui-react';
import type { MentionOption, MentionQueryDetail } from '@db1/makuco-ui-core';

const PEOPLE = [
  { id: 1, label: 'Ana Silva', subtitle: 'Product / Squad Checkout' },
  { id: 2, label: 'Bruno Costa', subtitle: 'Engenharia / Squad Checkout' },
];
const TAGS = [{ id: 'urgente', label: 'urgente' }, { id: 'bug', label: 'bug' }];

const [options, setOptions] = useState<MentionOption[]>([]);

function handleQuery(event: CustomEvent<MentionQueryDetail | null>) {
  const detail = event.detail;
  if (!detail) return setOptions([]);
  const source = detail.trigger === '#' ? TAGS : PEOPLE;
  setOptions(source.filter((o) => o.label.toLowerCase().includes(detail.query.toLowerCase())));
}

<MkMentions
  label="Comentário"
  placeholder="Escreva um comentário..."
  options={options}
  onMkQuery={handleQuery}
  onMkSelect={(event) => console.log('mencionado:', event.detail)}
  onMkRemove={(event) => console.log('removido:', event.detail)}
/>
// component.ts
import type { MentionOption, MentionQueryDetail } from '@db1/makuco-ui-core';

people: MentionOption[] = [
  { id: 1, label: 'Ana Silva', subtitle: 'Product / Squad Checkout' },
  { id: 2, label: 'Bruno Costa', subtitle: 'Engenharia / Squad Checkout' },
];
tags: MentionOption[] = [{ id: 'urgente', label: 'urgente' }, { id: 'bug', label: 'bug' }];
options: MentionOption[] = [];

handleQuery(event: CustomEvent<MentionQueryDetail | null>) {
  const detail = event.detail;
  if (!detail) { this.options = []; return; }
  const source = detail.trigger === '#' ? this.tags : this.people;
  this.options = source.filter((o) =>
    o.label.toLowerCase().includes(detail.query.toLowerCase()),
  );
}
<mk-mentions
  label="Comentário"
  placeholder="Escreva um comentário..."
  [options]="options"
  (mkQuery)="handleQuery($event)"
  (mkSelect)="onSelect($event)"
  (mkRemove)="onRemove($event)"
></mk-mentions>

Dois gatilhos no mesmo campo

triggers aceita qualquer conjunto de caracteres únicos — o padrão é ["@", "#"]. mkQuery carrega o trigger ativo, então o mesmo handler roteia para bases diferentes.

Multiline

rows = 1 (padrão) é single-line, sem quebra de linha, com rolagem horizontal. rows > 1 cresce com o texto até --mk-mentions-max-height (160px por padrão) e então rola verticalmente.

<MkMentions label="Descrição da tarefa" rows={4} options={options} onMkQuery={handleQuery} />
<mk-mentions
  label="Descrição da tarefa"
  [rows]="4"
  [options]="options"
  (mkQuery)="handleQuery($event)"
></mk-mentions>

Card ao mencionar

Passar o mouse sobre uma tag, ou encostar o caret nela com as setas do teclado ou com um clique, abre um card com avatar, nome e subtítulo. mentionCard={false} deixa a tag só estilizada, sem card.

A tag é atômica para o caret: clicar nela leva o caret para a borda mais próxima, nunca para dentro. O @ da própria tag não reabre o dropdown, e uma menção nunca entra dentro de outra.

<MkMentions label="Comentário" options={options} mentionCard={false} onMkQuery={handleQuery} />
<mk-mentions
  label="Comentário"
  [options]="options"
  [mentionCard]="false"
  (mkQuery)="handleQuery($event)"
></mk-mentions>

Obrigatório e erro

required valida o texto plano não-vazio. errorMessage põe o campo em estado inválido, com borda em --stroke-danger e aria-invalid="true" no editor.

<MkMentions
  label="Comentário"
  options={options}
  required
  errorMessage="Escreva um comentário antes de enviar."
/>
<mk-mentions
  label="Comentário"
  [options]="options"
  [required]="true"
  error-message="Escreva um comentário antes de enviar."
></mk-mentions>

Limite de caracteres

maxLength mede o texto plano e exibe um contador {n}/{maxLength} no rodapé. A inserção que estouraria o limite é rejeitada — incluindo a de uma menção, o que torna nomes longos inselecionáveis com um limite apertado.

<MkMentions label="Comentário" options={options} maxLength={120} onMkQuery={handleQuery} />
<mk-mentions
  label="Comentário"
  [options]="options"
  [maxLength]="120"
  (mkQuery)="handleQuery($event)"
></mk-mentions>

Desabilitado e somente leitura

Em ambos a edição é bloqueada e o dropdown nunca abre. A diferença está no card: disabled também bloqueia o card; readonly continua abrindo — ler um comentário salvo e consultar quem foi mencionado é exatamente o caso do readonly.

value definido de fora aparece só como texto

Os exemplos acima usam value para preencher o campo, então @Ana Silva aparece como texto plano — não como tag estilizada. É a limitação de reidratação descrita no topo desta página.

<MkMentions label="Comentário" value="Oi @Ana Silva, revisa isso?" disabled />
<MkMentions label="Comentário" value="Oi @Ana Silva, revisa isso?" readonly />
<mk-mentions label="Comentário" value="Oi @Ana Silva, revisa isso?" [disabled]="true"></mk-mentions>
<mk-mentions label="Comentário" value="Oi @Ana Silva, revisa isso?" [readonly]="true"></mk-mentions>

Formulários

O componente é form-associated: submete o texto plano. required valida esse texto não-vazio — sem serialização, não há "quantidade de menções" para validar, só o texto.

No Angular, MkMentionsModule traz o MentionsValueAccessor, que liga ngModel / formControl ao texto plano via mkInput:

import { MkMentionsModule } from "@db1/makuco-ui-angular";
<mk-mentions label="Comentário" [options]="options" [(ngModel)]="comment" required></mk-mentions>

Props — mk-mentions

PropTipoPadrãoDescrição
labelstringObrigatória. Rótulo exibido acima do campo.
namestringundefinedAtributo name para envio em formulários.
valuestring""

Texto plano, com as menções aparecendo pelo label (Oi @Ana Silva). Mutável — o componente reescreve a cada mutação. Não carrega identidade: definir de fora reidrata como texto puro.

optionsMentionOption[][]Lista exibida, exatamente como recebida. O componente não filtra nem ordena.
triggersstring[]["@", "#"]Caracteres que abrem uma sessão. Um caractere cada, não alfanumérico.
allowDuplicatesbooleanfalse

Permite mencionar o mesmo id mais de uma vez. Com false, a option já mencionada renderiza desabilitada na lista.

rowsnumber11 → single-line. > 1 → multiline com auto-grow até --mk-mentions-max-height.
placeholderstringundefinedTexto exibido com o campo vazio.
supportTextstringundefinedTexto de apoio abaixo do campo quando não há erro.
errorMessagestringundefinedMensagem de erro. Quando definida, o campo entra em estado inválido.
maxLengthnumberundefinedLimite de caracteres do texto plano. Exibe contador.
requiredbooleanfalseValida texto plano não-vazio.
requiredMessagestring"This field is required"Mensagem da validação nativa.
disabledbooleanfalseBloqueia edição; dropdown e card nunca abrem.
readonlybooleanfalseBloqueia edição; o card continua abrindo.
loadingbooleanfalseSpinner na lista. Suprime a mensagem de vazio.
minLengthnumber0Mínimo de caracteres depois do gatilho para emitir mkQuery.
debouncenumber300Espera em ms antes de emitir mkQuery. Não afeta mkInput.
emptyMessagestringundefinedSubstitui a mensagem de vazio. Sem interpolação.
mentionCardbooleantrueHabilita o card flutuante. false deixa a tag apenas estilizada.
openbooleanfalseEspelha a visibilidade real do dropdown — a sessão de gatilho é quem decide.
localestringundefinedTag BCP 47 para i18n. Recai para o lang do ancestral, depois <html lang>, depois pt-BR.

MentionOption

CampoTipoDescrição
idstring | numberIdentificador único, repassado intacto em mkSelect e mkRemove.
labelstringTexto da menção. Escrito no campo precedido do gatilho: @ + Ana Silva.
avatarstringURL do avatar, usada na option e no card. Sem ela, cai nas iniciais.
subtitlestringLinha secundária — "Product / Squad Checkout". Aparece na option e no card.
disabledbooleanAparece na lista, mas não é selecionável.

Eventos

EventoPayloadDescrição
mkInputstringA cada mutação do conteúdo, sem debounce. É o evento do texto, não da busca.
mkQueryMentionQueryDetail | null

Com sessão ativa e query.length >= minLength, após debounce ms. Emite null imediatamente, sem debounce, quando a sessão encerra — descarte resultados em voo nesse momento.

mkSelectMentionOptionUma option foi confirmada e a tag foi inserida. Payload é o objeto de options intacto.
mkRemoveMentionOptionUma tag deixou o editor, por qualquer caminho de remoção. Um evento por tag removida.
mkChangestringNo blur, se o texto plano mudou desde o último foco.
mkOpenvoidO dropdown abriu. Não borbulha.
mkClosevoidO dropdown fechou. Não borbulha.
mkFocusvoidO editor recebeu foco.
mkBlurvoidO editor perdeu foco.

Acessibilidade

Segue o WAI-ARIA APG — Combobox, aplicado a um editing host (contenteditable) em vez de um <input>. Nada é implícito: nome, valor e estado vêm todos de atributos explícitos.

  • role="combobox" e aria-autocomplete="list" no editor, sempre.
  • aria-controls aponta sempre para o id do listbox, inclusive fechado.
  • aria-activedescendant só existe com uma option em foco de teclado; o foco do DOM nunca sai do editor.
  • aria-labelledby substitui <label for>, que não vincula a um <div>.
  • Uma região role="status" aria-live="polite" anuncia contagem de resultados, menção inserida ou removida, e o conteúdo do card quando aberto pela navegação do caret.
  • Sem aria-multiline mesmo com rows > 1: ARIA 1.2 só admite esse atributo em textbox, não em combobox.
  • O card é informativo (role="tooltip", sem link, sem botão, sem foco) e pode ser aberto por teclado — encostar o caret numa tag abre o card imediatamente, sem depender do mouse.

Teclado

TeclaComportamento
/ Dropdown aberto: próxima/anterior option habilitada, sem wrap. Fechado: caret nativo
Home / EndDropdown aberto: primeira/última option habilitada. Fechado: início/fim da linha
EnterDropdown aberto com option em foco: confirma a menção. Sem foco: não confirma nada
EscapeFecha o dropdown mantendo o texto, ou fecha o card
TabSai do campo. Não confirma menção
Backspace / DeleteAdjacente a uma tag: remove a menção inteira em um único toque
/ Atravessando uma tag: o caret pula a tag como uma unidade; encostar nela abre o card

Evitar

  • Não espere reabrir um comentário salvo com as tags no lugar — value externo sempre reidrata como texto puro.
  • Não use para uma seleção só que substitui o campo inteiro — isso é mk-autocomplete.
  • Não confie em value para reconstruir identidade — use mkSelect e mkRemove para manter seu próprio conjunto de menções.
  • Não mute options in-place; reatribua o array.

On this page