# Select

![Status](https://img.shields.io/badge/status-stable-brightgreen)

Componente de seleção dropdown genérico com suporte a seleção simples e múltipla, filtro, agrupamento, virtual scroll e acessibilidade ARIA completa. Implementa `ControlValueAccessor` para integração com Angular Forms.

## Quando usar

- Selecionar uma ou mais opções de uma lista fixa de itens
- Substituir `<select>` nativo com estilo customizado e recursos avançados
- Listas longas que precisam de busca textual (`filter`)
- Opções organizadas por categoria (`group`)
- Listas com muitos itens que exigem virtualização (`virtualScroll`)

## Quando não usar

- Listas com poucas opções sempre visíveis — prefira `SelectButton` ou `RadioButtonGroup`
- Busca com carregamento dinâmico de resultados — prefira `Autocomplete`
- Entrada de múltiplos valores livres — prefira `Chips`

## Instalação

```typescript
import { SelectComponent } from '@seniorsistemas/angular-components/select';

@Component({ standalone: true, imports: [SelectComponent] })
export class MeuComponent {}
```

## Uso básico

```html
<s-select
  formControlName="pais"
  [options]="paises"
  optionLabel="nome"
  optionValue="codigo"
  placeholder="Selecione um país" />
```

## API

### Inputs

| Propriedade | Tipo | Padrão | Obrigatório | Descrição |
|-------------|------|--------|:-----------:|-----------|
| `options` | `T[]` | — | Sim | Lista de opções disponíveis para seleção |
| `placeholder` | `string` | `''` | Não | Texto exibido quando nenhuma opção está selecionada |
| `optionLabel` | `keyof T` | `undefined` | Não | Chave do objeto usada como texto de exibição. Obrigatório quando `options` são objetos |
| `optionValue` | `keyof T` | `undefined` | Não | Chave do objeto cujo valor será emitido ao formulário |
| `multiple` | `boolean` | `false` | Não | Habilita seleção de múltiplas opções com checkboxes |
| `filter` | `boolean` | `false` | Não | Exibe campo de busca textual no topo do dropdown |
| `filterBy` | `keyof T \| (keyof T)[]` | `undefined` | Não | Campo(s) usados para filtragem textual. Padrão: `optionLabel` |
| `showClear` | `boolean` | `false` | Não | Exibe botão X para limpar a seleção atual |
| `checkmark` | `boolean` | `false` | Não | Exibe ícone de checkmark ao lado da opção selecionada |
| `group` | `boolean` | `false` | Não | Habilita agrupamento. As opções devem ter estrutura `{ name, items[] }` |
| `virtualScroll` | `boolean` | `false` | Não | Virtualiza a lista para melhor performance com grandes volumes |
| `virtualScrollItemSize` | `number` | `37` | Não | Altura fixa de cada item em pixels para cálculo do virtual scroll |
| `emptyMessage` | `string \| null` | `null` | Não | Mensagem exibida quando nenhuma opção corresponde ao filtro |
| `disabled` | `boolean` (model) | `false` | Não | Controla o estado desabilitado via two-way binding |
| `readonly` | `boolean` | `false` | Não | Modo somente leitura: a lista abre e mostra as opções selecionadas, mas não é possível selecionar, desmarcar nem limpar |
| `showOptions` | `boolean` (model) | `false` | Não | Controla a visibilidade do painel via two-way binding |
| `ariaLabel` | `string` | `''` | Não | Label de acessibilidade do componente |
| `ariaLabelledBy` | `string` | `''` | Não | ID do elemento externo que descreve o componente |

> **Nota:** o ícone exibido por `checkmark` usa por padrão a classe `far fa-check` (Font Awesome Free), definida internamente pelo `SelectOptionComponent` (sub-componente interno que renderiza cada opção do dropdown) através do input `checkIconClass`. Esse input não é público — não é exposto nem repassado pelo `SelectComponent`.

### Tipos

```typescript
// Estrutura para opções agrupadas
interface GroupedOption {
  name: string;   // label do grupo
  items: T[];     // opções filhas
}
```

## Exemplos

### Seleção simples

```html
<form [formGroup]="form">
  <s-select
    formControlName="pais"
    [options]="paises"
    optionLabel="nome"
    optionValue="nome"
    placeholder="Selecione um país"
    [showClear]="true"
    ariaLabel="Selecione um país"
  ></s-select>
</form>
```

### Com filtro de busca

```html
<s-select
  formControlName="pais"
  [options]="paises"
  optionLabel="nome"
  optionValue="nome"
  placeholder="Buscar país..."
  [filter]="true"
  [showClear]="true"
></s-select>
```

### Seleção múltipla

```html
<s-select
  formControlName="paises"
  [options]="paises"
  optionLabel="nome"
  optionValue="nome"
  placeholder="Selecione países..."
  [multiple]="true"
  [filter]="true"
  [showClear]="true"
></s-select>
```

### Opções agrupadas

```html
<s-select
  formControlName="pais"
  [options]="paisesPorContinente"
  optionLabel="nome"
  optionValue="nome"
  placeholder="Selecione por continente..."
  [group]="true"
  ariaLabel="Selecione um país por continente"
></s-select>
```

### Somente leitura

Com `readonly`, o usuário continua podendo abrir e fechar a lista (mouse, `Enter`, `Espaço`, `ArrowDown`) e ver quais opções estão selecionadas, mas selecionar, desmarcar, "Selecionar todos" e limpar não têm efeito. Funciona igual em seleção simples e múltipla. O filtro de busca continua disponível.

```html
<s-select
  formControlName="pais"
  [options]="paises"
  optionLabel="nome"
  optionValue="nome"
  [readonly]="true"
></s-select>
```

Diferença para `disabled`: o campo desabilitado fica cinza, sai da ordem de tabulação e não abre a lista; o somente leitura mantém o fundo branco e o texto normal (só a borda fica mais suave), continua focável e abre a lista. Se `disabled` e `readonly` estiverem ativos juntos, prevalece o comportamento de `disabled`. No `DynamicForm`, use `readonly: () => boolean` na configuração do campo `enum`.

## Acessibilidade

- Com `readonly`, o combobox e o listbox expõem `aria-readonly="true"` e as opções `aria-disabled="true"`; ao tentar alterar a seleção (clique, `Enter` ou `Espaço` numa opção, ou "Selecionar todos"), o `LiveAnnouncer` anuncia que o campo é somente leitura, em vez de a tecla virar um no-op silencioso. O checkbox "Selecionar todos" fica desabilitado e, com `filter`, o foco inicial vai direto para o campo de busca
- O componente implementa o padrão ARIA `combobox` com `role="combobox"`, `aria-expanded` e `aria-haspopup`; o painel usa `role="listbox"` e cada opção `role="option"` com `aria-selected`
- Os wrappers sem papel ARIA entre o `listbox` e as `option` (`<ul>`, `<s-select-option>`, e o `<cdk-virtual-scroll-viewport>` quando `virtualScroll` está habilitado) recebem `role="presentation"`, para que a relação pai/filho da árvore de acessibilidade não quebre no Chrome (bug relatado via NVDA: sem isso, as opções eram anunciadas só como "seção", sem texto nem semântica de opção)
- Navegação pelo teclado: `ArrowDown`/`ArrowUp` para mover entre opções, `Enter`/`Espaço` para selecionar, `Escape` para fechar
- `Tab`/`Shift+Tab` com o dropdown aberto navegam entre as opções (mesmo efeito de `ArrowDown`/`ArrowUp`) em vez de fechar o painel — as opções não são elementos tabuláveis (foco virtual via `aria-activedescendant`), então um `Tab` nativo "vazaria" o foco pra fora do widget sem o usuário nunca ter alcançado a lista. Com `filter`, isso vale a partir do último controle real do overlay (campo de busca ou botão "Limpar filtro"); antes disso, `Tab` navega normalmente entre os controles reais (checkbox "Selecionar Todos" → campo de busca → botão "Limpar filtro")
- Com `filter` e `multiple`, o checkbox "Selecionar Todos" recebe o foco automaticamente ao abrir; só com `filter` (sem `multiple`), o foco vai direto para o campo de busca. O checkbox tem um `aria-label` contextual ("Selecionar todas as opções"/"Desmarcar todas as opções", conforme o estado atual) — sem isso, o leitor de tela só anunciava "checkbox, não marcado"
- Com o campo de busca focado (`filter`), `Espaço` também seleciona a opção com foco virtual (igual `Enter`) quando já existe uma opção focada — sem uma opção focada, o espaço é deixado passar pro comportamento nativo do `<input>` (digitar um espaço, necessário pra buscas com mais de uma palavra)
- Os inputs `ariaLabel` e `ariaLabelledBy` permitem associar descrições externas para leitores de tela
- Ao fechar o dropdown (`Escape`, clique fora, seleção de opção, redimensionamento da janela), o foco retorna automaticamente ao combobox — mas só quando ele estava de fato dentro do painel sendo fechado (ex.: campo de filtro); se o foco nunca saiu do combobox (seleção via `aria-activedescendant`, sem filtro), ele não é movido à toa
- O botão de limpar seleção ("X") isola o próprio `keydown` (`stopPropagation`) pra não deixar o `Espaço`/`Enter` borbulhar até o handler de teclado do combobox, que cancelaria a ativação nativa do botão
- Selecionar uma opção anuncia via `LiveAnnouncer` (`@angular/cdk/a11y`) qual opção foi selecionada; em `multiple`, alterna entre "selecionado"/"desselecionado" a cada toggle
- Com `multiple`, alternar "Selecionar Todos"/"Limpar Todos" (checkbox ou botão "X") também anuncia via `LiveAnnouncer` quantas opções foram selecionadas ou que a seleção foi limpa, já que o checkbox focado só anuncia seu próprio estado
- Limpar a seleção única (botão "X") anuncia via `LiveAnnouncer` qual opção foi removida
- Com `group`, a primeira opção de cada grupo tem um `aria-describedby` apontando pra um texto oculto visualmente (ex.: "Agrupado por América do Sul") — o cabeçalho visual do grupo tem `role="presentation"` (silencioso pro leitor de tela), então sem isso nada indicava a que grupo a opção pertencia ao navegar até ela
- Cada opção expõe `aria-posinset`/`aria-setsize` (posição 1-based e tamanho total do conjunto navegável, ignorando agrupadores) — necessário com `virtualScroll`, já que só uma janela de opções existe de fato no DOM a cada momento; sem esses atributos explícitos, o leitor de tela tenta inferir a posição contando os `role="option"` irmãos presentes no DOM, o que fica errado numa lista virtualizada
- No modo `multiple`, selecionar uma opção com o **mouse** também move o `aria-activedescendant` pra ela — sem isso (bug relatado via NVDA), cliques do mouse não atualizavam o foco virtual, que ficava parado na última opção alcançada por teclado (ou nulo), fazendo o leitor de tela anunciar sempre a mesma opção errada independente de qual fosse clicada
- No modo `multiple`, o checkbox visual dentro de cada opção é `aria-hidden` — sem `aria-label` e com seu próprio `aria-checked` independente, ele era um segundo objeto de acessibilidade concorrente anunciando o mesmo estado que o `aria-selected` da própria opção (o clique nele já borbulha pro clique da opção, então ele é puramente visual)
- O `id` de cada opção é prefixado pelo `componentId` (único por instância) além do timestamp — duas instâncias de `s-select` na mesma página podem inicializar no mesmo milissegundo (`Date.now()` tem resolução de 1ms), gerando ids colidentes sem esse prefixo. Com ids duplicados no documento, `aria-activedescendant`/`getElementById` podem resolver pro elemento de outro `s-select`, fazendo o leitor de tela anunciar uma opção de outro select em vez da que foi de fato clicada (bug relatado via NVDA, confirmado via inspeção direta: duas instâncias geraram o mesmo id pra primeira opção)
- Com `filter`, digitar um texto que reduz a lista (ou limpar o filtro, que a expande de volta) reseta o foco virtual (`focusedIndex`) pra -1 — sem isso (bug relatado via NVDA, reproduzido em teclado puro: seta + espaço, sem mouse), navegar fundo na lista e depois filtrar deixava o índice fora dos limites da nova lista, fazendo `ArrowDown`/`Espaço`/`Enter` virarem no-op silencioso; o usuário só continuava ouvindo o anúncio da última seleção válida, sem nenhum feedback de que a tecla não fez nada

## Componentes relacionados

- [`SelectButton`](../select-button/README.md) — quando as opções devem estar sempre visíveis como botões
- [`Autocomplete`](../autocomplete/README.md) — quando a lista é carregada dinamicamente via busca
