UNPKG

@govbr-ds/webcomponents

Version:

Biblioteca de Web Components baseado no GovBR-DS

614 lines (613 loc) 24.4 kB
/*! * Construído por SERPRO * © https://serpro.gov.br/ - MIT License. */ import { h, Host, } from "@stencil/core"; /** * Para uma descrição detalhada, consulte a [documentação do GovBR-DS](https://www.gov.br/ds/components/item?tab=designer). * * @slot default - Área de conteúdo, podendo conter qualquer componente, exceto botões primários e componentes relacionados à navegação (como carrosséis, paginações, abas, menus, etc.). * * @slot start - Área de recursos visuais, podendo conter elementos como ícones, avatares e mídias. * * @slot end - Área de recursos complementares, podendo conter componentes interativos, metadados e informações adicionais. */ export class Item { /** * Referência ao elemento host do componente. * Utilize esta propriedade para acessar e manipular o elemento do DOM associado ao componente. */ el; elementInternals; /** * Desativa o item, tornando-o não interativo. */ disabled = false; /** * Identificador único. * Caso não seja fornecido, um ID gerado automaticamente será usado. */ customId = `br-item-${itemId++}`; /** * Indica se o item está no estado ativo. * Se definido como verdadeiro, o item será exibido como ativo. */ isActive = false; /** * Indica se o item está no estado selecionado. * Se definido como verdadeiro, o item será exibido como selecionado. */ isSelected = false; /** * Marca o item como interativo, permitindo que toda a superfície do item seja clicável. */ isInteractive = false; /** * URL ou caminho para o qual o usuário será direcionado ao clicar no item. Quando definido, o item será renderizado como um link. */ href; /** * Define o alvo do link quando `href` está presente. Pode ser: * - `_blank` para abrir em uma nova aba, * - `_self` para abrir na mesma aba, * - `_parent` para abrir na aba pai, * - `_top` para abrir na aba superior. */ target; /** * Quando definido como `true`, o item será tratado como um botão. */ isButton = false; /** * Tipo do botão, aplicável apenas se `isButton` for `true`. Pode ser: * - `'submit'` para enviar um formulário, * - `'reset'` para redefinir um formulário, * - `'button'` para um botão padrão. */ type; /** * Define um valor associado ao br-item quando renderizado como um botão, utilizado em contextos de formulário. */ value; /** * Ajusta a densidade, alterando o espaçamento interno para um visual mais compacto ou mais expandido. */ density = 'medium'; // Propriedade interna para verificar a presença de conteúdo no slot default hasSlotDefault = false; hasSlotStart = false; hasSlotEnd = false; // Propriedade interna para armazenar o valor selecionado selectedValue = false; // Propriedade para rastrear a presença de rádios dentro do item hasRadios = false; // Referência ao elemento interno que deve receber o foco focusableElement; handleRadioChange() { requestAnimationFrame(() => { this.updateRadioSelectionState(); }); } handleCheckboxChange(event) { const target = event.target; if (this.el.contains(target)) { this.updateCheckboxSelectionState(); } } /** * Evento customizado emitido quando o item é clicado, aplicável apenas se o item for um botão (`<button>`). * Pode ser utilizado para ações personalizadas, exceto quando o item está desativado. */ brDidClick; /** * Evento customizado aplicável para todos os tipos de elementos (`div`, `button`, `a`), emitido quando o item é selecionado e desde que a propriedade `isInteractive` esteja presente. */ brDidSelect; componentWillLoad() { this.hasSlotContentDefault(); this.hasSlotStart = this.hasSlotContent('start'); this.hasSlotEnd = this.hasSlotContent('end'); this.selectedValue = this.isSelected; if (this.isButton) { this.type = this.type || 'button'; } const radios = Array.from(this.el.querySelectorAll('br-radio')); const checkboxes = Array.from(this.el.querySelectorAll('br-checkbox')); this.hasRadios = radios.length > 0; if (this.isSelected) { if (checkboxes.length > 0) { checkboxes.forEach((checkbox) => (checkbox.checked = true)); } if (radios.length > 0) { radios[0].checked = true; } } this.updateRadioSelectionState(); this.updateCheckboxSelectionState(); } hasSlotContentDefault() { this.hasSlotDefault = this.el.innerHTML !== ''; } hasSlotContent(slotName) { return this.el.querySelector(`[slot="${slotName}"]`) !== null; } getCssClassMap() { return { 'br-item': true, disabled: this.disabled, active: this.isActive, selected: this.selectedValue, 'py-3': this.density === 'medium', 'py-4': this.density === 'small', }; } getTagType() { if (this.isButton) { return 'button'; } if (this.href?.length > 0) { return 'a'; } return 'div'; } getAttributes() { const TagType = this.getTagType(); const baseAttributes = TagType === 'button' ? { type: this.type, value: this.value } : { href: this.href, target: this.target }; return { ...baseAttributes, 'aria-disabled': this.disabled ? 'true' : null, 'aria-current': this.isActive ? 'true' : null, 'data-toggle': this.isInteractive ? 'selection' : null, 'is-selected': this.selectedValue ? true : null, }; } updateCheckboxSelectionState() { if (this.hasRadios) return; const checkboxes = Array.from(this.el.querySelectorAll('br-checkbox')); this.selectedValue = checkboxes.some((checkbox) => checkbox.checked); this.brDidSelect.emit({ selected: this.selectedValue }); } updateRadioSelectionState() { if (!this.hasRadios) return; const radios = Array.from(this.el.querySelectorAll('br-radio')); this.selectedValue = radios.some((radio) => radio.checked); this.brDidSelect.emit({ selected: this.selectedValue }); } // Função para verificar se o clique foi dentro do slot do item. isClickInsideInteractiveElement(target) { const interactiveSelectors = 'br-checkbox, br-radio, br-input, br-button, br-select, br-textarea'; const interactiveElements = this.el.querySelectorAll(interactiveSelectors); return Array.from(interactiveElements).some((el) => el.contains(target) || target === el); } updateCheckboxes() { const checkboxes = Array.from(this.el.querySelectorAll('br-checkbox')); checkboxes.forEach((checkbox) => { checkbox.checked = this.selectedValue; }); } handleClick = (event) => { // Se o componente estiver desabilitado, impede a ação e sai imediatamente. if (this.disabled) { event.preventDefault(); event.stopPropagation(); return; } const clickedElement = event.target; if (this.hasRadios) { return; } if (this.isButton) { this.brDidClick.emit(); } else if (this.isInteractive && !this.isClickInsideInteractiveElement(clickedElement)) { // Armazena o estado anterior para emitir o evento apenas se houver mudança. const previousSelectedValue = this.selectedValue; this.selectedValue = !this.selectedValue; if (previousSelectedValue !== this.selectedValue) { this.brDidSelect.emit({ selected: this.selectedValue }); } // Atualiza o estado dos checkboxes internos. this.updateCheckboxes(); } event.stopPropagation(); }; handleKeydown(event) { if (this.disabled) { console.log(`[br-item ${this.customId}] Item desativado, ignorando tecla pressionada(keydown).`); return; } // Use closest para encontrar o br-list ancestral, mais robusto que parentElement const listContainer = this.el.closest('br-list'); if (listContainer === null || listContainer === undefined) { console.error(`[br-item ${this.customId}] Não foi possível encontrar o container pai <br-list>.`); return; } // Busca itens diretamente dentro do listContainer encontrado const listItems = Array.from(listContainer.querySelectorAll('br-item:not([disabled])')); if (listItems.length === 0) { console.warn(`[br-item ${this.customId}] Nenhum item navegável encontrado no container da lista.`); return; } // Encontra o índice do item atual na lista de itens navegáveis const currentIndex = listItems.indexOf(this.el); if (currentIndex === -1) { console.error(`[br-item ${this.customId}] Item atual não encontrado nos itens navegáveis.`); return; // O item atual não está na lista (talvez desabilitado ou erro) } let targetItem = null; switch (event.key) { case 'ArrowDown': // Seta para baixo event.preventDefault(); if (currentIndex < listItems.length - 1) { targetItem = listItems[currentIndex + 1]; } else { console.log(`[br-item ${this.customId}] ArrowDown: Último item já selecionado.`); } break; case 'ArrowUp': // Seta para cima event.preventDefault(); if (currentIndex > 0) { targetItem = listItems[currentIndex - 1]; } else { console.log(`[br-item ${this.customId}] ArrowUp: Usuário já está no primeiro item da lista.`); } break; case 'Enter': case ' ': // Barra de espaço event.preventDefault(); this.handleClick(new MouseEvent('click')); break; case 'Home': event.preventDefault(); targetItem = listItems[0]; break; case 'End': event.preventDefault(); targetItem = listItems[listItems.length - 1]; break; default: return; // Ignorar outras teclas } if (targetItem && targetItem !== this.el) { // Verifica se o item alvo é diferente do atual if ('setFocus' in targetItem) { // Se o item alvo tiver o método setFocus, chama-o ; targetItem.setFocus(); } // Caso contrário, apenas defina o foco no elemento alvo targetItem.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); } } /** * Define o foco no elemento interno do componente. * Este método pode ser chamado externamente para garantir que o foco seja aplicado ao elemento correto. */ async setFocus() { this.focusableElement?.focus(); } render() { const TagType = this.getTagType(); const attributes = this.getAttributes(); const classList = this.getCssClassMap(); return (h(Host, { key: 'f61dbe87513b7975c1eb1a7e121addb2cc3d3703' }, h(TagType, { key: '56f0183bc036790c8ce8792753559fd147ecf59a', class: classList, ...attributes, onClick: this.handleClick, id: this.customId, tabIndex: this.disabled ? -1 : this.isInteractive ? 0 : -1, ref: (el) => (this.focusableElement = el), onKeyDown: (event) => this.handleKeydown(event), onMouseDown: (e) => e.preventDefault() }, this.hasSlotStart || this.hasSlotEnd ? (h("div", { class: "row align-items-center" }, this.hasSlotStart && (h("div", { class: "col-auto" }, h("slot", { name: "start" }))), h("div", { class: "col" }, h("slot", null)), this.hasSlotEnd && (h("div", { class: "col-auto" }, h("slot", { name: "end" }))))) : (h("slot", null))))); } static get is() { return "br-item"; } static get encapsulation() { return "shadow"; } static get formAssociated() { return true; } static get originalStyleUrls() { return { "$": ["item.scss"] }; } static get styleUrls() { return { "$": ["item.css"] }; } static get properties() { return { "disabled": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Desativa o item, tornando-o n\u00E3o interativo." }, "getter": false, "setter": false, "attribute": "disabled", "reflect": true, "defaultValue": "false" }, "customId": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Identificador \u00FAnico.\nCaso n\u00E3o seja fornecido, um ID gerado automaticamente ser\u00E1 usado." }, "getter": false, "setter": false, "attribute": "custom-id", "reflect": true, "defaultValue": "`br-item-${itemId++}`" }, "isActive": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Indica se o item est\u00E1 no estado ativo.\nSe definido como verdadeiro, o item ser\u00E1 exibido como ativo." }, "getter": false, "setter": false, "attribute": "is-active", "reflect": true, "defaultValue": "false" }, "isSelected": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Indica se o item est\u00E1 no estado selecionado.\nSe definido como verdadeiro, o item ser\u00E1 exibido como selecionado." }, "getter": false, "setter": false, "attribute": "is-selected", "reflect": true, "defaultValue": "false" }, "isInteractive": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Marca o item como interativo, permitindo que toda a superf\u00EDcie do item seja clic\u00E1vel." }, "getter": false, "setter": false, "attribute": "is-interactive", "reflect": true, "defaultValue": "false" }, "href": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "URL ou caminho para o qual o usu\u00E1rio ser\u00E1 direcionado ao clicar no item. Quando definido, o item ser\u00E1 renderizado como um link." }, "getter": false, "setter": false, "attribute": "href", "reflect": true }, "target": { "type": "string", "mutable": false, "complexType": { "original": "'_blank' | '_self' | '_parent' | '_top'", "resolved": "\"_blank\" | \"_parent\" | \"_self\" | \"_top\"", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Define o alvo do link quando `href` est\u00E1 presente. Pode ser:\n- `_blank` para abrir em uma nova aba,\n- `_self` para abrir na mesma aba,\n- `_parent` para abrir na aba pai,\n- `_top` para abrir na aba superior." }, "getter": false, "setter": false, "attribute": "target", "reflect": true }, "isButton": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Quando definido como `true`, o item ser\u00E1 tratado como um bot\u00E3o." }, "getter": false, "setter": false, "attribute": "is-button", "reflect": true, "defaultValue": "false" }, "type": { "type": "string", "mutable": true, "complexType": { "original": "'submit' | 'reset' | 'button'", "resolved": "\"button\" | \"reset\" | \"submit\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Tipo do bot\u00E3o, aplic\u00E1vel apenas se `isButton` for `true`. Pode ser:\n- `'submit'` para enviar um formul\u00E1rio,\n- `'reset'` para redefinir um formul\u00E1rio,\n- `'button'` para um bot\u00E3o padr\u00E3o." }, "getter": false, "setter": false, "attribute": "type", "reflect": true }, "value": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Define um valor associado ao br-item quando renderizado como um bot\u00E3o, utilizado em contextos de formul\u00E1rio." }, "getter": false, "setter": false, "attribute": "value", "reflect": true }, "density": { "type": "string", "mutable": false, "complexType": { "original": "'large' | 'medium' | 'small'", "resolved": "\"large\" | \"medium\" | \"small\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Ajusta a densidade, alterando o espa\u00E7amento interno para um visual mais compacto ou mais expandido." }, "getter": false, "setter": false, "attribute": "density", "reflect": true, "defaultValue": "'medium'" } }; } static get states() { return { "hasSlotDefault": {}, "hasSlotStart": {}, "hasSlotEnd": {}, "selectedValue": {} }; } static get events() { return [{ "method": "brDidClick", "name": "brDidClick", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Evento customizado emitido quando o item \u00E9 clicado, aplic\u00E1vel apenas se o item for um bot\u00E3o (`<button>`).\nPode ser utilizado para a\u00E7\u00F5es personalizadas, exceto quando o item est\u00E1 desativado." }, "complexType": { "original": "any", "resolved": "any", "references": {} } }, { "method": "brDidSelect", "name": "brDidSelect", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Evento customizado aplic\u00E1vel para todos os tipos de elementos (`div`, `button`, `a`), emitido quando o item \u00E9 selecionado e desde que a propriedade `isInteractive` esteja presente." }, "complexType": { "original": "{ selected: boolean }", "resolved": "{ selected: boolean; }", "references": {} } }]; } static get methods() { return { "setFocus": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<void>" }, "docs": { "text": "Define o foco no elemento interno do componente.\nEste m\u00E9todo pode ser chamado externamente para garantir que o foco seja aplicado ao elemento correto.", "tags": [] } } }; } static get elementRef() { return "el"; } static get listeners() { return [{ "name": "checkedChange", "method": "handleRadioChange", "target": "document", "capture": false, "passive": false }, { "name": "checkedChange", "method": "handleCheckboxChange", "target": "document", "capture": false, "passive": false }]; } static get attachInternalsMemberName() { return "elementInternals"; } } let itemId = 0; //# sourceMappingURL=item.js.map