@govbr-ds/webcomponents
Version:
Biblioteca de Web Components baseado no GovBR-DS
614 lines (613 loc) • 24.4 kB
JavaScript
/*!
* 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