Barrierefreies Datei-Upload-Widget mit Web Components bauen
Verwenden Sie einen nativen Button und ein Dateieingabefeld in einem benutzerdefinierten Element,
um eine Datei auszuwählen und das zugehörige Objekt File an Ihre Anwendung zu übergeben.
Dieses Datei-Upload-Widget kapselt seine Styles im Shadow DOM, prüft eine Größenbegrenzung von
10 MiB und meldet die Auswahl über einen Statusbereich. Die Host-Anwendung übernimmt den Upload;
dieses Beispiel sendet keine Netzwerkanfragen.
Warum Web Components für Datei-Upload-Widgets?
Wählen Sie ein benutzerdefiniertes Element, wenn mehrere Anwendungen dieselbe Auswahlsteuerung benötigen, ohne ein gemeinsames UI-Framework zu verwenden. Shadow DOM hält die internen Styles getrennt, während ein DOM-Ereignis dem Host eine kleine Schnittstelle bietet. Wenn Sie nur ein gestaltetes Eingabefeld in einem Formular benötigen, ist ein natives Dateieingabefeld einfacher.
Die Struktur der Web Component einrichten
Sie benötigen einen Browser mit Custom Elements und Shadow DOM, einen Texteditor und eine lokale Testdatei. Die reine HTML-Demo benötigt weder Paketmanager noch Server oder Build-Schritt. Das Verhalten bei Tastaturbedienung und Dateiauswahl wurde mit Chromium 145 unter Linux getestet. Testen Sie vor der Veröffentlichung die Kombinationen aus Browser und assistiver Technologie, die Ihre Zielgruppe verwendet.
Erstellen Sie in Ihrem Editor ein neues Verzeichnis. Speichern Sie darin die folgende vollständige
Komponente als file-upload-widget.js:
class FileUploadWidget extends HTMLElement {
static observedAttributes = ['disabled'];
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.render();
this.addEventListeners();
this.ready = true;
}
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);
padding: 20px;
text-align: center;
color-scheme: light dark;
background-color: Canvas;
color: CanvasText;
}
:host([hidden]) { display: none; }
button:focus-visible {
outline: 2px solid Highlight;
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;
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')) {
fileInput.value = '';
return;
}
const file = fileInput.files[0];
if (file) {
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}`;
this.dispatchEvent(new CustomEvent('file-selected', {
detail: { file },
bubbles: true,
composed: true
}));
fileInput.value = ''; // Allow the same file to be selected again.
} else {
fileInfo.textContent = '';
}
});
}
}
customElements.define('file-upload-widget', FileUploadWidget);
Der Konstruktor erstellt den Shadow-Baum und registriert die Listener einmalig. Der Callback beim Verbinden synchronisiert nur den deaktivierten Zustand. Wenn dasselbe Element entfernt und wieder verbunden wird, bleibt seine Rückmeldung daher erhalten, ohne dass weitere Listener hinzukommen. Das beobachtete Attribut berücksichtigt auch spätere Änderungen; siehe die MDN-Dokumentation zum Lebenszyklus benutzerdefinierter Elemente.
Speichern Sie diese Seite als index.html im selben Verzeichnis:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Web component file selection</title>
<script src="file-upload-widget.js" defer></script>
<script src="demo.js" defer></script>
</head>
<body>
<main>
<h1>Select one file</h1>
<p id="basic-picker">
<label for="basic-file">Choose a file</label>
<input type="file" id="basic-file" name="file" />
</p>
<file-upload-widget id="picker" hidden></file-upload-widget>
<p id="received">No file received by the host.</p>
<noscript><p>The basic input selects a file; this demo does not upload it.</p></noscript>
</main>
</body>
</html>
Speichern Sie die empfangende Seite als demo.js:
const widget = document.querySelector('#picker');
const basicPicker = document.querySelector('#basic-picker');
const received = document.querySelector('#received');
widget.addEventListener('file-selected', (event) => {
const { file } = event.detail;
window.selectedFile = file;
received.textContent = `Host received: ${file.name} (${file.size} bytes).`;
});
// Keep the basic input available if the component failed to load or initialize.
if (widget.ready === true) {
basicPicker.hidden = true;
widget.hidden = false;
}
Öffnen Sie index.html im Browser. Drücken Sie Tab, um Choose a file zu erreichen,
und dann die Eingabe- oder Leertaste, um den nativen Dateiauswahldialog zu öffnen. Wählen Sie eine
Datei mit höchstens 10 MiB. Das Widget zeigt
Selected file: gefolgt vom Dateinamen an, der Host zeigt
Host received: gefolgt vom Namen und der Anzahl der Bytes an.
window.selectedFile enthält das eigentliche Objekt File, das der Host verwenden kann.
Tastaturnavigation und Fokusverwaltung implementieren
Der native Button verarbeitet die Eingabe- und Leertaste und benötigt daher weder einen eigenen Handler für Tastendruckereignisse noch eine explizite Button-Rolle. Ein Klick öffnet das versteckte Eingabefeld während der Nutzeraktivierung. Behalten Sie die sichtbare Fokusumrandung bei und prüfen Sie, ob der Fokus nach der Dateiauswahl oder dem Abbrechen des Dialogs zum Button zurückkehrt. Das WAI-Button-Muster beschreibt diese Anforderungen an die Tastaturbedienung.
Fügen Sie das boolesche Attribut disabled zu file-upload-widget hinzu, um beide nativen Bedienelemente zu deaktivieren.
Entfernen Sie das Attribut, um sie wieder zu aktivieren; disabled="false" deaktiviert das Element weiterhin,
weil das Attribut vorhanden ist. Wenn Ihre Anwendung das aktuell fokussierte Bedienelement
deaktiviert oder entfernt, verschieben Sie den Fokus auf ein sinnvolles verbleibendes Bedienelement
im Host. Das erneute Verbinden eines Knotens stellt einen verlorenen Fokus nicht von selbst wieder her.
ARIA-Attribute zur Unterstützung von Screenreadern hinzufügen
Der sichtbare Text des Buttons liefert seinen zugänglichen Namen. aria-describedby="fileHint" verknüpft
die Größenbegrenzung innerhalb desselben Shadow Roots. Die separate, zunächst leere
Region mit role="status" hat die Semantik einer zurückhaltend angekündigten Live-Region;
aria-atomic="true" fordert die vollständige aktualisierte Meldung an. Eine Textaktualisierung verschiebt
den Fokus nicht auf den Statusbereich.
Diese Semantik unterstützt assistive Technologien. Sichtbare Rückmeldungen und ein Barrierefreiheitsbaum belegen jedoch keine Sprachausgabe. Bei der getesteten Kombination aus Linux Orca 46.1 und Chromium 145 erfolgte die automatische Ansage der Auswahl nach der Rückkehr aus dem nativen Dateiauswahldialog nur sporadisch. Wird dieselbe Datei erneut ausgewählt, entsteht zudem identischer Text, den ein Screenreader möglicherweise nicht erneut ansagt. Lassen Sie die Rückmeldung sichtbar und führen Sie die unten beschriebenen Screenreader-Prüfungen für Ihre Zielkombinationen durch.
Dateiauswahl und Validierung verarbeiten
Der Listener für change liest das ausgewählte Objekt File, schreibt dessen Namen als Text statt als HTML
und löst für eine akzeptierte Auswahl ein Ereignis file-selected aus. event.detail.file ist die Datei selbst,
nicht ihr Pfad oder eine rekonstruierte Kopie. Das Ereignis propagiert zu übergeordneten Elementen
und überquert Shadow-DOM-Grenzen, sodass es auch ein Host außerhalb eines weiteren Shadow Roots
empfangen kann.
Die Grenze beträgt 10 × 1.024 × 1.024 Bytes. Leere Dateien sowie Dateien bis einschließlich dieser Grenze werden akzeptiert. Bei einer größeren Datei erscheint File too large (max 10 MiB). Please select another file. und es wird kein Auswahlereignis ausgelöst. Die zuvor im Host ausgewählte Datei bleibt erhalten. Das Eingabefeld wird nach einer akzeptierten oder abgelehnten Auswahl sowie nach einer im deaktivierten Zustand ignorierten Auswahl geleert. Daher kann die erneute Auswahl derselben Datei nach dem Wiederaktivieren ein weiteres Ereignis auslösen. Das Abbrechen des Dateiauswahldialogs erzeugt keine neue Auswahl.
Die clientseitige Validierung liefert Rückmeldungen; Ihr Upload-Server muss Größe und Inhalt
unabhängig validieren sowie Autorisierung und Speicherquoten durchsetzen. Diese Komponente bietet
weder Drag-and-drop noch Upload-Fortschritt oder Formularübermittlung. Weil ihr Eingabefeld geleert
wird, übermittelt auch ein umgebendes Formular die ausgewählte Datei nicht automatisch. Verwenden
Sie File aus dem Ereignis im Upload-Code Ihres Hosts.
Individuelle Gestaltung mit benutzerdefinierten CSS-Eigenschaften
Der Rahmen der Komponente verwendet var(--border-color, #ccc). Um ihn zu überschreiben, setzen Sie diese Regel
in ein Element style im Kopfbereich von index.html. Sie erreicht die Komponente, ohne Elemente
im Shadow-Baum auszuwählen:
file-upload-widget {
--border-color: #007bff;
}
Die Demo verwendet mit color-scheme: light dark die Systemfarben für Fläche, Text und Fokus. Prüfen Sie beim
Hinzufügen von Styles den Kontrast und den Fokusindikator Ihres eigenen Designs. Lange Dateinamen
werden umgebrochen, statt das Bedienelement über seinen Container hinaus zu verbreitern.
Progressive Verbesserung und Fallback-Strategien
Das ursprüngliche HTML zeigt ein beschriftetes natives Eingabefeld und hält das erweiterte Widget
versteckt. Erst wenn der Konstruktor abgeschlossen und das empfangende Skript bereit ist, tauscht
demo.js die beiden aus. Wenn JavaScript deaktiviert ist, eines der Skripte nicht heruntergeladen
werden kann oder die Initialisierung der Komponente fehlschlägt, bleibt das einfache Eingabefeld
verfügbar. Das deckt mehr Fälle ab als ein Fallback allein mit noscript.
Das einfache Eingabefeld wählt nur eine Datei aus: Es bietet weder Größenvalidierung noch einen Handler für Host-Ereignisse oder einen Upload-Endpunkt. Wenn ein Formular auch ohne JavaScript Daten übermitteln muss, verbinden Sie dieses native Eingabefeld mit Ihrem tatsächlichen Multipart-Formular und Server. Verstecken Sie es erst, wenn der Ersatz die Aufgabe erfüllen kann.
Barrierefreiheit mit Screenreadern testen
Testen Sie mit den von Ihnen unterstützten Kombinationen aus Browser und Screenreader, etwa NVDA oder JAWS unter Windows, VoiceOver unter macOS und Orca unter Linux. Die automatisierte Linux-Prüfung in diesem Beispiel ist keine Usability-Studie mit menschlichen Screenreader-Nutzern und belegt keine universelle Barrierefreiheit. Prüfen Sie Folgendes:
- Tab erreicht Choose a file einmal, mit einem sichtbaren Fokusindikator.
- Die Eingabe- und Leertaste öffnen den tatsächlichen Dateiauswahldialog. Nach Auswahl und Abbruch kehrt der Fokus zum Button zurück.
- Der Button hat einen Namen, die Button-Rolle und eine Beschreibung der Größenbegrenzung. Prüfen Sie, was tatsächlich angesagt wird und ob dafür der Hilfebefehl des Screenreaders nötig ist.
- Ein neuer Dateiname und die Meldung bei einer zu großen Datei sind für den Screenreader verfügbar. Prüfen Sie automatische Ansagen getrennt vom manuellen Lesen der Rückmeldung, auch bei wiederholter Auswahl.
- Das Deaktivieren und Wiederaktivieren des Bedienelements funktioniert. Entfernen und erneutes Verbinden erhalten die Rückmeldung, ohne Ereignisse zu duplizieren; der Host stellt den Fokus wieder her, wenn er ein fokussiertes Bedienelement entfernt.
- Bei deaktiviertem JavaScript und bei einem fehlerhaften Komponentenskript lässt sich mit dem einfachen Eingabefeld weiterhin eine Datei auswählen.
In ein Framework integrieren
Registrieren Sie file-upload-widget.js einmal im Browser, bevor Sie Ihre Anwendung einbinden. Empfangen Sie
anschließend file-selected und lesen Sie event.detail.file im Host. Serverseitiges Rendering und den
Upload-Transport übernehmen Sie außerhalb der Komponente. Dies sind Integrationspunkte für eine
bestehende App, keine separaten Anleitungen für vollständige Framework-Projekte:
- In React 19 unterstützen benutzerdefinierte Elemente Eigenschaften und Handler für benutzerdefinierte Ereignisse. Behalten Sie beim Binden eines benutzerdefinierten Ereignisses dessen Bindestrich sowie die Groß- und Kleinschreibung bei. Alternativ registrieren Sie einen DOM-Listener über eine Ref und entfernen denselben Listener, wenn die Wrapper-Komponente entfernt wird.
- Kennzeichnen Sie dieses Tag in Vue mit
compilerOptions.isCustomElementin Ihrer Vue-Build-Konfiguration als benutzerdefiniertes Element. Das Setzen vonapp.config.compilerOptionsfunktioniert nur, wenn Templates im Browser kompiliert werden, nicht bei vorkompilierten Ein-Datei-Komponenten. Ein Template-Listener empfängt das DOM-Ereignis. - Mit
CUSTOM_ELEMENTS_SCHEMAerlaubt Angular benutzerdefinierte Elemente mit Bindestrichen im Namen. Fügen Sie es in der verwendenden eigenständigen Komponente unterschemashinzu oder bei einer modulbasierten App unterschemasim deklarierenden NgModule. Binden Sie das DOM-Ereignis im Template dieser Komponente und verarbeiten Sie dessenFilein der Anwendung.
Den Host mit einem Upload-Dienst verbinden
Ersetzen Sie den empfangenden Handler der Demo durch den Upload-Vorgang Ihrer Anwendung und geben Sie für erfolgreiche und fehlgeschlagene Uploads jeweils eine eigene Rückmeldung. Eine Dateiauswahl belegt nicht, dass ein Server die Datei akzeptiert hat. Der Robot /upload/handle von Transloadit bietet weiterführende Informationen zur serverseitigen Dateientgegennahme; er ist kein direkt einsetzbarer Endpunkt für diese lokale Dateiauswahl-Demo.
