Barrierefreies Datei-Upload-Widget mit Web Components erstellen
Web Components bieten eine leistungsfähige Möglichkeit, wiederverwendbare, gekapselte UI-Elemente zu erstellen, die über verschiedene Frameworks hinweg reibungslos funktionieren. In diesem DevTip zeigen wir, wie Sie mit Web Components, Shadow DOM und modernem JavaScript ein barrierefreies Datei-Upload-Widget erstellen. Dabei stellen wir sicher, dass unser Widget native Tastatursteuerung und einen Live-Statusbereich nutzt. Es wählt eine Datei aus und löst ein Event aus; die Host-Anwendung ist für das Hochladen und das Melden des Upload-Fortschritts verantwortlich.
Warum Web Components für Datei-Upload-Widgets?
Web Components sind eine Reihe von Web-Plattform-APIs, mit denen Sie eigene, wiederverwendbare HTML-Elemente erstellen können. Sie kapseln Funktionalität und Styling und eignen sich damit ideal für einheitliche UI-Komponenten über verschiedene Projekte und Frameworks hinweg. Mit Shadow DOM können wir Styles und Struktur unseres Widgets isolieren und so Konflikte mit anderen Teilen der Anwendung vermeiden.
Die Struktur für die Web Component einrichten
Definieren wir unser Custom Element FileUploadWidget. Diese Klasse kapselt die gesamte
Logik und Darstellung unseres Widgets. Wir richten das Shadow DOM ein, rendern die initiale
HTML-Struktur und registrieren die nötigen Event-Listener.
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);
Binden Sie das Skript einmal ein und fügen Sie dann <file-upload-widget></file-upload-widget> in Ihre Seite ein.
Der Konstruktor erstellt das Shadow DOM und seine Listener einmalig, ohne Attribute des
Host-Elements zu verändern. Wird dasselbe Element erneut verbunden, bleibt sein Status erhalten und
Listener werden nicht doppelt registriert. Das beobachtete Attribut
disabled aktualisiert die nativen Bedienelemente, auch wenn es sich nach dem
Verbinden ändert.
Tastaturnavigation und Fokusverwaltung umsetzen
Die native Schaltfläche erhält den Fokus per Tab und verarbeitet Enter und Space ohne eigenen
Tastatur-Handler. Ihr Klick öffnet den Dateiauswahldialog im Rahmen der Aktivierung durch den
Nutzer. Der sichtbare Fokusrahmen bleibt erhalten, und das Hinzufügen von
disabled zum Custom Element deaktiviert sowohl die Schaltfläche als auch das
Datei-Eingabefeld.
ARIA-Attribute für Screenreader-Unterstützung ergänzen
Der sichtbare Text der Schaltfläche liefert ihren zugänglichen Namen.
aria-describedby verknüpft die Größenbeschränkung mit dieser Schaltfläche. Der separate
Bereich role="status" gibt Rückmeldungen zu Auswahl und Validierung zurückhaltend
aus; er ist nicht in einer Button-Rolle verschachtelt, die seine Semantik verbergen könnte.
Dateiauswahl und Validierung verarbeiten
Die Methode addEventListeners registriert einen Event-Listener für
change am verborgenen <input type="file">. Sobald eine Datei
ausgewählt wird, wird dieser Listener aktiv. Der aktualisierte Code umfasst:
- Die Anzeige des Namens der ausgewählten Datei als Text, nicht als HTML.
- Ein einfaches Beispiel für die Validierung der Dateigröße (Prüfung, ob die Datei größer als 10 MiB ist). Schlägt die Validierung fehl, wird eine Fehlermeldung angezeigt und das Datei-Eingabefeld geleert.
- Das Auslösen eines
CustomEventmit dem Namenfile-selected. Dieses Event steigt im DOM auf und kann Shadow-DOM-Grenzen überschreiten (composed: true), sodass übergeordnete Komponenten oder anderer JavaScript-Code auf die Dateiauswahl reagieren können. Das ausgewählte Objekt vom Typfilewird in der Eigenschaftdetaildes Events übergeben.
Validierung auf Client-Seite ist eine Rückmeldung, keine Sicherheitsgrenze. Der Upload-Server muss Größe und Inhalt unabhängig davon validieren, Autorisierung und Speicherkontingente durchsetzen und unsichere Dateien ablehnen. Dieses Beispiel bietet bewusst eine Dateiauswahl und kein Drag-and-drop.
Eigenes Styling mit CSS Custom Properties
Damit Nutzer das Erscheinungsbild des Widgets anpassen können, sind CSS Custom Properties eine gute
Lösung. Im Block <style> unserer Komponente verwenden wir
var(--border-color, #ccc). Der Rahmen nutzt damit die Variable
--border-color, sofern sie vom Nutzer definiert ist; andernfalls greift der
Standardwert #ccc.
/* Example of how a user might set the custom property */
file-upload-widget {
--border-color: #007bff;
}
Die Komponente verwendet den Fallback direkt in ihrer Rahmen-Deklaration:
:host {
border: 2px dashed var(--border-color, #ccc);
}
Die Syntax var(--border-color, #ccc) direkt in der Eigenschaft border
ist die Standardmethode, um einen Fallback bereitzustellen.
Progressive Enhancement und Fallback-Strategien
Es ist wichtig, dass Ihr Widget bei deaktiviertem JavaScript sauber degradiert. Das Tag
<noscript> bietet einen einfachen Fallback.
<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>
Dieser Fallback deckt deaktiviertes JavaScript ab, nicht aber einen fehlgeschlagenen Skript-Download oder eine fehlgeschlagene Initialisierung. Soll die Dateiauswahl auch diese Fehler überstehen, rendern Sie ein beschriftetes natives Eingabefeld im initialen HTML und erweitern es erst, nachdem die Komponente erfolgreich initialisiert wurde. Ein Datei-Eingabefeld allein lädt nichts hoch; verbinden Sie den Fallback mit dem Formular und dem Upload-Endpunkt Ihrer Anwendung.
Barrierefreiheit mit Screenreadern testen
Testen Sie Ihr Widget immer mit Screenreadern wie NVDA (Windows), JAWS (Windows) oder VoiceOver (macOS), um sicherzustellen, dass es vollständig barrierefrei ist. Prüfen Sie dabei:
- Das Widget lässt sich mit der Tabulatortaste fokussieren.
- Es lässt sich mit „Enter“ oder „Space“ aktivieren.
- Der Screenreader gibt Name, Rolle und die Beschreibung der Größenbeschränkung der Schaltfläche aus.
- Dateiauswahl und etwaige Statusmeldungen werden angemessen angesagt.
- Deaktivieren, erneutes Aktivieren, Entfernen und erneutes Verbinden des Widgets führt nicht zu doppelten Interaktionen.
Beispiele für die Framework-Integration
Web Components sind darauf ausgelegt, sich nahtlos in verbreitete Frameworks zu integrieren:
- React: In React 19 unterstützen Custom Elements Eigenschaften und eigene Event-Handler.
Der ref-basierte Event-Listener unten funktioniert auch in älteren React-Versionen. Registrieren
Sie das Custom Element, bevor Sie es rendern.
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: Registrieren Sie das Widget, bevor Sie die App mounten. Weisen Sie Vue an,
file-upload-widgetmithilfe voncompilerOptions.isCustomElementals Custom Element zu behandeln. Für kompilierte Single-File Components setzen Sie diese Option in Ihrer Vue-Build-Konfiguration. Die folgende Laufzeitkonfiguration funktioniert nur, wenn Vue Vorlagen im Browser kompiliert:// 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: Nehmen Sie
CUSTOM_ELEMENTS_SCHEMAin das Arrayschemasin Ihrem jeweiligenNgModuleauf, damit sich eigene Tags verwenden lassen, ohne dass Angular Fehler auslöst. Anschließend können Sie das Custom Element in Ihren Vorlagen verwenden und auf dessen Events hören.// 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>
Fazit
Ein barrierefreies Datei-Upload-Widget mit Web Components sorgt für Wiederverwendbarkeit, Kapselung und Barrierefreiheit in all Ihren Projekten. Dieser Ansatz bildet eine solide Grundlage für robuste und benutzerfreundliche Datei-Eingaben. Für eine robustere Dateiverarbeitung sollten Sie eine Integration mit dem /upload/handle Robot von Transloadit in Betracht ziehen.
