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
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
label | string | — | Obrigatória. Rótulo exibido acima do campo. |
name | string | undefined | Atributo name para envio em formulários. |
value | string | "" | Texto plano, com as menções aparecendo pelo label ( |
options | MentionOption[] | [] | Lista exibida, exatamente como recebida. O componente não filtra nem ordena. |
triggers | string[] | ["@", "#"] | Caracteres que abrem uma sessão. Um caractere cada, não alfanumérico. |
allowDuplicates | boolean | false | Permite mencionar o mesmo |
rows | number | 1 | 1 → single-line. > 1 → multiline com auto-grow até --mk-mentions-max-height. |
placeholder | string | undefined | Texto exibido com o campo vazio. |
supportText | string | undefined | Texto de apoio abaixo do campo quando não há erro. |
errorMessage | string | undefined | Mensagem de erro. Quando definida, o campo entra em estado inválido. |
maxLength | number | undefined | Limite de caracteres do texto plano. Exibe contador. |
required | boolean | false | Valida texto plano não-vazio. |
requiredMessage | string | "This field is required" | Mensagem da validação nativa. |
disabled | boolean | false | Bloqueia edição; dropdown e card nunca abrem. |
readonly | boolean | false | Bloqueia edição; o card continua abrindo. |
loading | boolean | false | Spinner na lista. Suprime a mensagem de vazio. |
minLength | number | 0 | Mínimo de caracteres depois do gatilho para emitir mkQuery. |
debounce | number | 300 | Espera em ms antes de emitir mkQuery. Não afeta mkInput. |
emptyMessage | string | undefined | Substitui a mensagem de vazio. Sem interpolação. |
mentionCard | boolean | true | Habilita o card flutuante. false deixa a tag apenas estilizada. |
open | boolean | false | Espelha a visibilidade real do dropdown — a sessão de gatilho é quem decide. |
locale | string | undefined | Tag BCP 47 para i18n. Recai para o lang do ancestral, depois <html lang>, depois pt-BR. |
MentionOption
| Campo | Tipo | Descrição |
|---|---|---|
id | string | number | Identificador único, repassado intacto em mkSelect e mkRemove. |
label | string | Texto da menção. Escrito no campo precedido do gatilho: @ + Ana Silva. |
avatar | string | URL do avatar, usada na option e no card. Sem ela, cai nas iniciais. |
subtitle | string | Linha secundária — "Product / Squad Checkout". Aparece na option e no card. |
disabled | boolean | Aparece na lista, mas não é selecionável. |
Eventos
| Evento | Payload | Descrição |
|---|---|---|
mkInput | string | A cada mutação do conteúdo, sem debounce. É o evento do texto, não da busca. |
mkQuery | MentionQueryDetail | null | Com sessão ativa e |
mkSelect | MentionOption | Uma option foi confirmada e a tag foi inserida. Payload é o objeto de options intacto. |
mkRemove | MentionOption | Uma tag deixou o editor, por qualquer caminho de remoção. Um evento por tag removida. |
mkChange | string | No blur, se o texto plano mudou desde o último foco. |
mkOpen | void | O dropdown abriu. Não borbulha. |
mkClose | void | O dropdown fechou. Não borbulha. |
mkFocus | void | O editor recebeu foco. |
mkBlur | void | O 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"earia-autocomplete="list"no editor, sempre.aria-controlsaponta sempre para oiddo listbox, inclusive fechado.aria-activedescendantsó existe com uma option em foco de teclado; o foco do DOM nunca sai do editor.aria-labelledbysubstitui<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-multilinemesmo comrows > 1: ARIA 1.2 só admite esse atributo emtextbox, não emcombobox. - 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
| Tecla | Comportamento |
|---|---|
↓ / ↑ | Dropdown aberto: próxima/anterior option habilitada, sem wrap. Fechado: caret nativo |
Home / End | Dropdown aberto: primeira/última option habilitada. Fechado: início/fim da linha |
Enter | Dropdown aberto com option em foco: confirma a menção. Sem foco: não confirma nada |
Escape | Fecha o dropdown mantendo o texto, ou fecha o card |
Tab | Sai do campo. Não confirma menção |
Backspace / Delete | Adjacente 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 —
valueexterno 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
valuepara reconstruir identidade — usemkSelectemkRemovepara manter seu próprio conjunto de menções. - Não mute
optionsin-place; reatribua o array.