Crie um widget de upload de arquivos acessível com web components
Os Web Components oferecem uma forma poderosa de criar elementos de interface reutilizáveis e encapsulados que funcionam sem atrito em diferentes frameworks. Neste DevTip, vamos ver como criar um widget de upload de arquivos acessível usando Web Components, Shadow DOM e JavaScript moderno. Vamos garantir que o widget use controles de teclado nativos e uma região de status dinâmica (live region). Ele seleciona um arquivo e emite um evento; a aplicação hospedeira é responsável por fazer o upload e informar o progresso do upload.
Por que usar web components em widgets de upload de arquivos?
Web Components são um conjunto de APIs da plataforma web que permitem criar elementos HTML personalizados e reutilizáveis. Eles encapsulam funcionalidade e estilo, o que os torna ideais para criar componentes de interface consistentes em vários projetos e frameworks. Com o Shadow DOM, podemos isolar os estilos e a estrutura do widget, evitando conflitos com outras partes da aplicação.
Configurando a estrutura do web component
Vamos definir nosso elemento personalizado, FileUploadWidget. Essa classe vai encapsular toda a lógica e
a apresentação do widget. Vamos configurar o Shadow DOM, renderizar a estrutura HTML inicial e
registrar os event listeners necessários.
class FileUploadWidget extends HTMLElement {
static observedAttributes = ['disabled'];
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.render();
this.addEventListeners();
}
connectedCallback() {
this.syncDisabled();
}
attributeChangedCallback() {
this.syncDisabled();
}
syncDisabled() {
const disabled = this.hasAttribute('disabled');
this.shadowRoot.querySelector('button').disabled = disabled;
this.shadowRoot.querySelector('input').disabled = disabled;
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
border: 2px dashed var(--border-color, #ccc); /* Added default for --border-color */
padding: 20px;
text-align: center;
background-color: #f9f9f9;
}
button:focus-visible {
outline: 2px solid blue;
outline-offset: 4px;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
button {
max-width: 100%;
white-space: normal;
padding: 12px;
}
.file-info {
margin-top: 10px;
font-size: 0.9em;
color: #333;
overflow-wrap: anywhere;
}
</style>
<input type="file" id="fileInput" hidden />
<button type="button" aria-describedby="fileHint">Choose a file</button>
<p id="fileHint">Maximum file size: 10 MiB.</p>
<p class="file-info" id="fileInfo" role="status" aria-atomic="true"></p>
`;
}
addEventListeners() {
const fileInput = this.shadowRoot.querySelector('#fileInput');
const fileInfo = this.shadowRoot.querySelector('#fileInfo');
this.shadowRoot.querySelector('button').addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', () => {
if (this.hasAttribute('disabled')) return;
const file = fileInput.files[0];
if (file) {
// Example validation (can be expanded)
if (file.size > 10 * 1024 * 1024) {
fileInfo.textContent = 'File too large (max 10 MiB). Please select another file.';
fileInput.value = ''; // Clear the invalid selection
return;
}
fileInfo.textContent = `Selected file: ${file.name}`;
// Dispatch an event for parent components
this.dispatchEvent(new CustomEvent('file-selected', {
detail: { file },
bubbles: true, // Allows event to bubble up through the DOM
composed: true // Allows event to cross shadow DOM boundaries
}));
fileInput.value = ''; // Allow the same file to be selected again.
} else {
fileInfo.textContent = '';
}
});
}
}
customElements.define('file-upload-widget', FileUploadWidget);
Inclua o script uma vez e depois adicione <file-upload-widget></file-upload-widget> à sua página. O
construtor cria o Shadow DOM e seus listeners uma única vez, sem alterar atributos do elemento
hospedeiro. Reconectar o mesmo elemento preserva o status dele e não duplica listeners. O atributo
observado disabled atualiza os controles nativos, inclusive quando muda após a conexão.
Implementando navegação por teclado e gerenciamento de foco
O botão nativo recebe o foco com Tab e trata Enter e Espaço sem um handler de teclado personalizado.
O clique nele abre o seletor de arquivos durante a ativação feita pelo usuário. O contorno de foco
visível é mantido, e adicionar disabled ao elemento personalizado desativa tanto o botão quanto o
input de arquivo.
Adicionando atributos ARIA para suporte a leitores de tela
O texto visível do botão fornece o nome acessível dele. aria-describedby associa o limite de
tamanho a esse botão. A região separada role="status" anuncia o feedback de seleção e validação sem
interromper o que o leitor de tela está falando; ela não fica aninhada dentro de um papel de botão,
o que poderia ocultar sua semântica.
Tratando a seleção e a validação de arquivos
O método addEventListeners inclui um event listener change no
<input type="file"> oculto. Quando um arquivo é selecionado, esse listener é acionado. O código atualizado inclui:
- A exibição do nome do arquivo selecionado como texto, não como HTML.
- Um exemplo básico de validação do tamanho do arquivo (verificando se ele tem mais de 10 MiB). Se a validação falhar, uma mensagem de erro é exibida e o input de arquivo é limpo.
- O disparo de um
CustomEventchamadofile-selected. Esse evento se propaga pelo DOM e pode atravessar os limites do Shadow DOM (composed: true), permitindo que componentes pais ou outro código JavaScript reajam à seleção do arquivo. O objetofileselecionado é passado na propriedadedetaildo evento.
A validação no lado do cliente é um feedback, não uma barreira de segurança. O servidor de upload precisa validar tamanho e conteúdo de forma independente, aplicar autorização e cotas de armazenamento e rejeitar arquivos inseguros. Este exemplo oferece intencionalmente a seleção de arquivos, e não arrastar e soltar.
Estilização personalizada com propriedades personalizadas de CSS
Para permitir que os usuários personalizem a aparência do widget, as propriedades personalizadas de
CSS são uma ótima solução. No bloco <style> do nosso componente, usamos var(--border-color, #ccc). Isso
significa que a borda vai usar a variável --border-color se o usuário a tiver definido; caso contrário, o
padrão é #ccc.
/* Example of how a user might set the custom property */
file-upload-widget {
--border-color: #007bff;
}
O componente usa o valor de fallback diretamente na declaração da borda:
:host {
border: 2px dashed var(--border-color, #ccc);
}
A sintaxe var(--border-color, #ccc) usada diretamente na propriedade border é a forma padrão de
fornecer um valor de fallback.
Aprimoramento progressivo e estratégias de fallback
É importante garantir que o widget degrade de forma elegante se o JavaScript estiver desativado. A
tag <noscript> oferece um fallback básico.
<noscript>
<p>
JavaScript is required for the enhanced file upload widget. Please enable JavaScript or use the
basic file input below:
</p>
<label for="basic-file">Choose a file</label>
<input type="file" id="basic-file" name="file" />
</noscript>
Esse fallback cobre o JavaScript desativado, não uma falha no download ou na inicialização do script. Se a seleção de arquivos também precisar resistir a essas falhas, renderize um input nativo com rótulo no HTML inicial e aprimore-o somente depois que o componente for inicializado com sucesso. Um input de arquivo sozinho não faz upload; conecte o fallback ao formulário e ao endpoint de upload da sua aplicação.
Testando a acessibilidade com leitores de tela
Sempre teste o widget com leitores de tela como NVDA (Windows), JAWS (Windows) ou VoiceOver (macOS) para garantir que ele seja totalmente acessível. Verifique se:
- O widget recebe foco com a tecla Tab.
- Ele pode ser ativado com “Enter” ou “Espaço”.
- O leitor de tela anuncia o nome, o papel e a descrição do limite de tamanho do botão.
- A seleção de arquivos e quaisquer mensagens de status são anunciadas adequadamente.
- Desativar, reativar, remover e reconectar o widget não duplica as interações.
Exemplos de integração com frameworks
Os Web Components foram projetados para se integrar sem atrito aos frameworks mais populares:
- React: no React 19, elementos personalizados aceitam propriedades e handlers de eventos personalizados.
O event listener baseado em ref abaixo também funciona em versões mais antigas do React. Registre
o elemento personalizado antes de renderizá-lo.
import { useEffect, useRef } from 'react'; export function MyReactApp() { const widgetRef = useRef(null); useEffect(() => { const node = widgetRef.current; const handleFileSelected = (event) => console.log('File selected:', event.detail.file); node?.addEventListener('file-selected', handleFileSelected); return () => node?.removeEventListener('file-selected', handleFileSelected); }, []); return <file-upload-widget ref={widgetRef}></file-upload-widget>; } - Vue: registre o widget antes de montar a aplicação. Informe ao Vue que ele deve tratar
file-upload-widgetcomo um elemento personalizado usandocompilerOptions.isCustomElement. Para Single-File Components compilados, defina essa opção na sua configuração de build do Vue. A configuração em tempo de execução a seguir só funciona quando o Vue compila os templates no navegador:// In-browser template compilation only, before app.mount() app.config.compilerOptions.isCustomElement = tag => tag === 'file-upload-widget';<template><file-upload-widget @file-selected="onFileSelected"></file-upload-widget></template> <script setup> const onFileSelected = (event) => { console.log('File selected in Vue:', event.detail.file); }; </script> - Angular: inclua
CUSTOM_ELEMENTS_SCHEMAno arrayschemasdoNgModulecorrespondente para permitir o uso de tags personalizadas sem que o Angular gere erros. Depois, você pode usar o elemento personalizado nos seus templates e escutar os eventos dele.// In your Angular module (e.g., app.module.ts) import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class AppModule { }<!-- In your Angular component template --> <file-upload-widget (file-selected)="onFileSelected($event)"></file-upload-widget>
Conclusão
Criar um widget de upload de arquivos acessível com Web Components garante reutilização, encapsulamento e acessibilidade em todos os seus projetos. Essa abordagem oferece uma base sólida para criar experiências de seleção de arquivos robustas e fáceis de usar. Para um tratamento de arquivos mais robusto, considere a integração com o Robot /upload/handle da Transloadit.
