protofunc()

Meinung zu HTML5: Semantik noch Flop, progressive enhancement Top

Tags: HTML 5, deutsch, javascript

Ich bin ein großer Freund von HTML5. Allerdings kann ich nicht nachvollziehen, in welcher Form einige – bereits heute – HTML5 nutzen wollen bzw. zu nutzen empfehlen. Die tollen HTML5-Elemente (nav, aside, header, footer..), die wir mit dreckigen Tricks nutzen könnten, bringen nichts. Kein Browser, kein Screenreader und auch keine ernstzunehmende Suchmaschine unterstützen die Semantik der neuen Tags, gleichzeitig bereiten Sie in allen Versionen des Internet Explorer sowie im Firefox 2 erhebliche Probleme und machen einiges kaputt.

Auf der anderen Seite gibt es die schönen WAI-Aria-Landmarks. Sie sind für eine zusätzliche Semantik definitiv der bessere Weg, weil sie im Gegensatz zu den oben genannten HTML 5 Element einfach funktionieren, helfen und nichts kaputt machen.

Andere Elemente wie figure und legend können teilweise durch aria-labelledby/aria-describedby substituiert werden.

Dies ist kein Widerspruch zu HTML 5. Im Gegenteil der HTML 5 Validator validiert inzwischen nicht nur Teile von Wai-Aria, sondern eben auch die Aria-Landmarks.

Neben den problemlos verwendbaren Aria-Attributen, gibt es in HTML5 viele schöne neue Sachen zu entdecken.

  • Man kann sich endlich den doctype merken.
  • Das canvas-Element wird von allen modernen Browsern unterstützt und kann mit excanvas auch im Internet Explorer zum Laufen gebracht werden (allerdings mit deutlich geringerer Performance).
  • Attribute wie autocomplete sind endlich Bestandteil von HTML 5. Ohne dieses Attribut sind mit Javascript erstellte Auto-Suggest-Listen für Eingabefelder unmöglich.
  • Es gibt neue, befreiende Verschachtelungsregeln.
  • Best practices wie beispielsweise document.documentElement.className += ‘js-on’; erzeugen endlich validen Code.

Vor allem jedoch bietet HTML 5 – insbesondere bei Formularelementen – semantische Extras, die Javascript-Entwickler, gleichgültig von der derzeitigen Unterstützung im Browser, dringend benötigen, um eleganten, unobtrusiven Code schreiben zu können.

Wie zusätzliche semantische HTML5-Attribute beim Aufbau von Javascript helfen

Man stelle sich vor, man möchte einen Slider mit numerischen Werten realisieren. Dieser Slider soll jeweils einen vom Backend vorgegeben Startwert, Mindestwert und Höchstwert kennen. Als Javascript-Fallback soll ein einfaches Texteingabefeld dienen.

Während HTML 4 sowie XHTML 1.0 lediglich ein Attribut für den Startwert bieten, hilft uns HTML 5 ebenso beim Mindest- und Höchstwert unseres unobtrusiv zu erstellenden Sliders.

<input type=“number“ value=“3“ min=“1“ max=“10“ />
<!-- oder in Schritten -->
<input type=“number“ value=“3“ min=“1“ max=“10“ step="1" />
<!-- oder gleich als 'slider' -->
<input type=“range“ value=“3“ min=“1“ max=“10“ />

HTML 5 und progressive enhacement

HTML 5 bietet einige interessante Features, welche bereits heute mit Hilfe von Javascript implementiert werden können.

Das oben beschriebene canvas-Element ist ein Beispiel hierfür, ein weiteres sind die zahlreichen Erweiterungen für Formularelemente, welche in Teilen bereits von Browsern implementiert wurden.

Die HTML 5 – Attribute liefern hierbei die semantische Grundlage für die Aktivierung und Konfiguierung des Javascripts.

Vor der Implementierung mit Javascript sollte man grundsätzlich testen, ob das Feature nicht bereits Teil des Browsers ist. Da die neuen HTML-Attribute in der Regel mit einem DOM-Interface korrelieren, ist dieser Test kinderleicht. Hier einige Beispiele mit jQuery:

(function($){

	var form = $('<form><fieldset><textarea /></fieldset></form>');

	$.extend($.support, {
		//wird die Constrain Validation unterstützt?
		checkValidity: !!(form[0].checkValidity),
		// wird das maxlength-Attribut am textarea-Element unterstützt?
		textareaMaxlength: !!( $('textarea', form)[0].maxLength !== undefined || $('textarea', form)[0].maxlength !== undefined)
	});

})(jQuery);

Eine Beispielimplementierung für das placeholder-Attribut

Das placeholder-Attribut bietet ein Formularfeature, welches Frontend-Entwickler tagtäglich bereits heute mit Javascript in Webseiten nutzen; nämlich das Entfernen von vorbelegten Werten in Eingabefeldern. Hierbei ist das Standardverhalten wie folgt definiert: Der Placeholder stellt einen kurzen Hinweistext dar, welcher dem User als vorläufiger Wert im Eingabefeld angezeigt wird, solange das Feld keinen Wert hat und nicht fokusiert ist. Das placeholder-Attribut ist kein Ersatz für ein assoziertes label-Element!

<label for="birthday">Ihr Geburtstag</label>
<input type="text" placeholder="TT.MM.JJJJ" name="birthday" id="birthday" />

In HTML4/XHTML 1 ist die Unterscheidung ob das Eingabefeld einen Wert, welcher sich wie ein Placeholder verhalten soll oder nicht, nicht gelöst. Die derzeit eleganteste Javascript-Technik verwendet hierzu einen Vergleich zwischen der value- und der defaultValue-Eigenschaft. Diese Form der Implementierung kommt jedoch an Ihre Grenzen, wenn der User beispielsweise ein Suchformular absendet und der Suchbegriff aus Usability-Gründen wiederholt wird, um es dem User zu gestatten, ohne Neueingabe den Suchbegriff zu erweitern, verkürzen oder sonst wie zu modifizieren. Da in diesem Beispiel ausnahmsweise der defaultValue nicht die Aufgabe eines Placeholders besitzt, fehlt dem Javascript die entscheidende Information sich korrekt zu verhalten. Dieses Problem kann letztlich nur durch Hinzufügen dieser Extrainformation gelöst werden. Und die Standardisierung dieser Extrainformation stellt gerade eben das placeholder-Attribut dar.

Wie könnte nun eine placeholder-Implementierung aussehen?

Als erstes erweitern wir unseren oben genannten HTML5-Implementierungstest:

var form = $('<form><fieldset><input type=“text“ /><textarea /></fieldset></form>');

$.extend($.support, {
	checkValidity: !!(form[0].checkValidity),
	textareaMaxlength: !!( $('textarea', form)[0].maxLength !== undefined || $('textarea', form)[0].maxlength !== undefined),
	placeHolder: !!($('input', form)[0].placeholder !== undefined || $('input', form)[0].placeHolder !== undefined)
});

Wird das placeholder-Attribut nicht unterstützt, kann mit der Implementierung durch Javascript fortgefahren werden, was wie folgt aussehen könnte.

if(!$.support.placeHolder){

	function resetInput(){
		if(this.value == this.getAttribute('placeholder')){
			this.value = '';
		}
	}

	function fillInput(){
		if(!this.value){
			this.value = this.getAttribute('placeholder');
		}
	}
	$(function(){
		$('input[placeholder]')
			.each(function(){
				var placeHolder = this.getAttribute('placeholder').replace(/\n|\r|\f|\t/g, '');
				this.setAttribute('placeholder', placeHolder);
				if(!this.value){
					this.value = placeHolder;
				}

			})
			.bind('blur', fillInput)
			.bind('focus', resetInput)
		;
	});
}

Das schöne, das Script kann bereits heute für ein besseres placeholder/input-rest-Script eingesetzt werden. (Ich kenne keines mit einem besseren Verhalten). Bei Browsern (zum Beispiel Safari 4), welche dieses Attribut bereits unterstützen, funktioniert das ganze dann auch ohne Javascript. Die Benutzung ist sehr einfach. Konfiguration und Start des Scripts geschehen komplett automatisch.

<label for="birthday">Ihr Geburtstag</label>
<input type="text" id="birthday" placeholder="TT.MM.JJJJ" name="birthday" />

Hier das gesamte Script (Eine verbesserte Variante des Placeholder-Scripts):

(function($){

var form = $('<form><fieldset><input type=“text“ /><textarea /></fieldset></form>');

$.extend($.support, {
	checkValidity: !!(form[0].checkValidity),
	textareaMaxlength: !!( $('textarea', form)[0].maxLength !== undefined || $('textarea', form)[0].maxlength !== undefined),
	placeHolder: !!($('input', form)[0].placeholder !== undefined || $('input', form)[0].placeHolder !== undefined)
});

if(!$.support.placeHolder){
	$(function(){
		$('input[placeholder]')
			.each(function(){
				var placeHolder = this.getAttribute('placeholder').replace(/\n|\r|\f|\t/g, '');
				this.setAttribute('placeholder', placeHolder);
				if(!this.value){
					this.value = placeHolder;
				}

			})
			.bind('blur', function fillInput(){
				if(!this.value){
					this.value = this.getAttribute('placeholder');
				}
			})
			.bind('focus', function resetInput(){
				if(this.value == this.getAttribute('placeholder')){
					this.value = '';
				}
			})
		;
	});
}
})(jQuery);

Fazit

HTML 5 hat deutlich mehr zu bieten als nicht funktionierende HTML-Elemente. Diese sind oftmals nicht mehr als semantischer „Zuckerguß“. HTML 5 kann jetzt schon vielmehr: Es ermöglicht wirklich elegantes und unobtrusives Javascript.

Written August 16, 2009 by
protofunc

Wie man Tabs wirklich wirklich zugänglich macht

Tags: accessibility, deutsch, javascript

Vor etwa einem halben Jahr veröffentlichte Dirk Ginader eine zugängliche Tab-Lösung. Dieser Artikel versucht auf einem ähnlichem Grundscript durch Hinzufügen von WAI-Aria- Rollen und Zuständen die Zugänglichkeit zu erhöhen. Daneben deckt der Artikel einen Firefox/Jaws-Bug inklusive eines brauchbaren Workarounds auf.

Warum ich nicht die Aria Tab-Semantik nutze

Einigen wird nachfolgend (negativ) auffallen, daß ich eben nicht die Tabsematik anwende, welche durch das Aria Best Practices Dokument vorgeschlagen wird, sondern eine neue zusammenstelle. Dies begründet sich aus folgenden Punkten:

1. Abwärtskompatibilität: Tabben vs. erweiterte Tastaturnutzung

Aria im allgemeinen und insbesondere das Tab-Widget folgen der One-Tab-Stop-Technik”. Tatsächlich ist diese Technik sehr cool, um die Usability in aria-fähigen Screenreader-/Browserkombinationen drastisch zu erhöhen. Sie kann jedoch in manchen Situationen die Zugänglichkeit für nicht aria-fähige Systeme reduzieren.

Dies könnte beim Tab-Widget der Fall sein. Nur der aktive Tab soll nämlich in der Tabreihenfolge sein; alle inaktiven Tabs dagegen ausserhalb der, durch den Nutzer, erreichbaren Tabreihenfolge. Das Umschalten zwischen den Tabs erfolgt durch besondere Tastaturkürzel, die gescriptet werden müssen. Während das Herausnehmen von inaktiven Tabs aus der Tabreihenfolge in aria-unfähigen Systemen funktionieren kann, funktioniert das Scripten von Tastatureingaben im Normalmodus der meisten Screenreader ohne Aria in der Regel eben nicht.

2. Abwärtskompatibilität: Fehlende Rückmeldung

Im normalen Aria-Tabwidget findet die Rückmeldung über das Switchen von Tabs ausschließlich über Aria statt. Eine Fokusverlagerung, die in aria-unfähigen Screenreader/Browserkombinationen für eine solche Rückmeldung sorgen könnte, ist dagegen nicht vorgehsehen.

3. Konflikt mit Tastaturnutzung zwischen Webseite und Browser

Das Tabwidget sollte desweiteren eine Tastaturkombination verwenden (Ctrl+PageUp/Ctrl+PageDown), welche mit der des Browsers in Konflikt geraten kann.

4. Wir machen doch nicht nur Tabwidgets

Eine extrem häufige Aufgabe ist es einen Schalter zu platzieren, welcher die Sichtbarkeit eines anderen Elements steuert. Hierbei handelt es sich dann aber noch nicht um ein Tabwidget. Aber eben auch für diese Schalter wollen wir eine zugängliche Technik entwickeln. Ein Tabwidget ist aus dieser Sicht dann nichts anderes als die Zusammenfassung solcher Buttons mit ihren Containern zu einer Gruppe.

Auch wenn mittelfristig – durch steigende Aria-Unterstützung – das hier gezeigte Aria-Markup für Tabs weniger sinnvoll erscheinen wird, so bleibt es dennoch für Accordions oder Schalter, welche irgendwelche anderen Elemente in ihrer Sichtbarkeit toggeln, sinnvoll.

Das ariafizierte Markup

<div>
	<ul>
		<li><a role="button" aria-expanded="true" tabindex="-1" id="tab-1" aria-controls="panel-1" href="#">Tab 1</a></li>
		<li><a role="button" aria-expanded="false" tabindex="0" id="tab-2" aria-controls="panel-2" href="#">Tab 2</a></li>
	</ul>
	<div role="group" aria-expanded="true" aria-labelledby="tab-1" "id="panel-1">
		<p tabindex="-1">Text für Panel 1. Startet manchmal halt ohne Überschrift (schade ist aber so)</p>
	</div>
	<div role="group" aria-expanded="false" aria-labelledby="tab-2" "id="panel-2">
		<h3 tabindex="-1">Überschrift für Panel 2</h3>
		<p>Weiterer Text für Panel 2</p>
	</div>
</div>

Mit dem tabindex-Attribut kontrollieren wir die Tabreihenfolge. Wie sich zeigt drehen wir die Logik des W3C zum tabwidget um. Alle Tabs sind in der Tabreihenfolge nur eben das aktive Tab-Element nicht. Wir geben dem Tab die Rolle Button und die Eigenschaft expanded. Die expanded-Eigenschaft an Button-Elementen wird derzeit nur von Firefox 3.5 an Screenreader weitergeleitet. Die Eigenschaft controls baut eine logische Beziehung zwischen Button und Panel auf, wird jedoch noch von keinem Screenreader richtig unterstüzt (schadet wahrscheinlich nicht). Das Fokusieren des 2. Tab würde sich durch obige Auszeichnung wie folgt anhören: Tab 2 [Schalter reduziert].

Das Tabpanel bekommt die Rolle ‘group’ sowie die Eigenschaften expanded und labelledby. Die labelledby-Eigenschaft setzt eine logische Beziehung von dem group-Element zum Button. Anders als Controls wird sie gut unterstüzt. Auch die Eigenschaft expanded ist am group-Element besser unterstützt als am button-Element. Sobald der User auf Tab 2 klickt, fokusieren wir mit einem kurzen Delay das 1. Kindelement des Tabpanels, was sich ungefähr wie folgt anhören würde: [Tab 2 offen] Überschrift für Panel 2 [Überschrift Ebene 3].

Da wir die Ariafizierung mit Javascript vornehmen, hier 2 Scripte die uns dabei helfen:

ID-Attribute erstellen/ermitteln und labelledby-Eigenschaft setzen

(function($){

	var uId = new Date().getTime();
	$.fn.getID = function(){
		var id = '';
		if(this[0]){
			var elem = $(this[0]);
			id = elem.attr('id');
			if(!id){
				id = 'ID-' + (uId++);
				elem.attr({'id': id});
			}
		}
		return id;
	};

	$.each({
		labelWith: 'aria-labelledby',
		describeWith: 'aria-describedby',
		ownsThis: 'aria-owns',
		controlsThis: 'aria-controls'
	}, function(name, prop){
		$.fn[name] = function(elem){
			return this.attr(prop, $(elem).getID());
		};
	});
})(jQuery);

Das jQuery-Plugin getID gibt den Wert des id-Attributs eines Elements zurück. Ist keine id vorhanden wird eine eindeutige id erstellt. Daneben werden hierauf aufbauend die Methoden labelWith, describeWith, ownsThis und controlsThis erstellt.

Benutzung:

// Gibt den ID-Wert von panel zurück
$(panel).getID();

// gib dem Panel die Eigenschaft labelledby mit dem id-Wert von button
$(panel).labelWith(button);

Element fokusieren

Ein Script, welches ein beliebiges Element nach einem kurzen Delay fokusiert, könnte dagegen wie folgt aussehen:

(function($){

	function addTabindex(jElm){
		if(!jElm.is('a[href], area, input, button, select, textarea')){
			jElm.css({outline: 'none'}).attr({tabindex: '-1'});
		}
		return jElm;
	}
	var focusTimer;
    $.fn.setFocus = function(time){
		if(!this[0]){return this;}
		var jElm = $(this[0]);
		clearTimeout(focusTimer);
		focusTimer = setTimeout(function(){
			try{
				addTabindex(jElm)[0].focus();
			} catch(e){}
		}, time || 0);
		return this;
	};

})(jQuery);

Benutzung des Focus-Scripts:

// 1. sichtbares Kindelement von panel nach 0 ms fokusieren
$('> :visible:first', panel).setFocus();

Sollte man das Panel oder ein anderes wrapper-Element des zu fokusierenden Elements animieren, empfiehlt es sich mit der Fokusierung bis zum Ende der animation zu warten, da beim Internet Explorer der Fokus bei manchen Animation ab und zu “verspringt”. Eine Verzögerung von weniger als 500ms (Die Standarddaauer einer jQuery-Animation beträgt 400ms) beim Fokusieren wird nicht als störend empfunden.

Wenn das ganze funktionieren würde…

… hätte ich diesen Blogeintrag nie geschrieben. Es ist etwa einen Monat her, da wies mich Timo Wirth daraufhin, daß das Script in einem seiner Projekte nicht funktioniert. Das Problem war, daß sobald das HTML eben mit Aria erweitert wurde, insbesondere die Aria-Rolle Button vergeben wurde, Jaws mit Firefox den Buffer nicht mehr beim Sichtbarkeitswechsel updaten wollte. Da aber die vorbereiteten Beispiele funktionierten, stellte sich die Frage, welcher andere Faktor an dem Scheitern verantworlich ist. Es begann die Suche nach der Nadel im Heuhaufen.

Das Problem ist kurz gesagt dermaßen banal und es ist eine Schande, wie lange die Ursachenforschung gedauert hat:

Das Element, dessen Sichtbarkeit beeinflußt wird, darf unter bestimmten Bedingungen (beispielsweise Kontrollelement hat die Rolle Button) nicht die CSS-Eigenschaft overflow: hidden haben. Um sicherzustellen, daß das Panel diese CSS-Eigenschaft weder durch CSS noch durch eine Javascript-Animation erhält, bekommt es daher von mir eine CSS-Klasse. Ein dynamisch erzeugtes Stylesheet sorgt dafür, dass diese Klasse ein overvlow: visible erhält:

$(function(){
	var style = document.createElement('style'),
		styleS
	;
	style.setAttribute('type', 'text/css');
	style = $(style).prependTo('head');

	styleS = document.styleSheets[0];

	function add(sel, prop){
		if (styleS.cssRules || styleS.rules) {
			if (styleS.insertRule) {
				styleS.insertRule(sel +' {'+ prop +';}', styleS.cssRules.length);
			} else if (styleS.addRule) {
				styleS.addRule(sel, prop);
			}
		}
	}

	add('.a11y-visibility-change', 'overflow:visible !important');

	$.cssRule = {
		add: add
	};
});
//über die Klasse reden wir noch :-)
$(panel).addClass('a11y-visibility-change');

Ein div sie zu knechten

Nun stellt die CSS-Eigenschaft overflow: hidden eine recht wichtige Eigenschaft dar. Einerseits wird sie gerne genommen, um floats einzuschließen andererseits ist sie essentieller Bestandteil einiger JavaScript-Animationen, beispielsweise (slideUp, slideDown etc.). Unser Stylesheet-Problem läßt sich recht einfach lösen, sowohl Vorfahren- als auch Nachfahrenelemente dürfen wieder die CSS-Eigenschaft hidden aufweisen. Bei Animationen wie slideDown/slideUp kann das Elternelement mit einer Höhen-Animation und entsprechendem overflow: hidden verwendet werden. Zu beachten ist allerdings, daß in unserem Fall das group-Element selbst die display: none/block Eigenschaft bekommen sollte und eben nicht das Elternelement. Eine Alternative zu slideUp/slideDown könnten daher folgende Plugins bieten:

$.fn.slideParentDown = function(opts){
	opts = $.extend({}, $.fn.slideParentDown.defaults, opts);
	var fn = opts.complete;

	return this.each(function(){
		var jElm 		= $(this),
			parent		= jElm.parent().css({overflow: 'hidden', height: '0px'}),
			outerHeight = jElm.css({display: 'block'}).outerHeight({margin: true})
		;
		parent.animate({
			height: outerHeight
		}, $.extend({}, opts, {complete: function(){
			parent.css({height: '', overflow: ''});
			fn.apply(this, arguments);
		}}));
	});
};
$.fn.slideParentDown.defaults = {
	duration: 400,
	complete: function(){}
};
$.fn.slideParentUp = function(opts){
	opts = $.extend({}, $.fn.slideParentDown.defaults, opts);
	var fn = opts.complete;
	return this.each(function(){
		var jElm 		= $(this),
			parent		= jElm.parent().css({overflow: 'hidden', height: jElm.outerHeight({margin: true})})
		;
		parent.animate({
			height: '0px'
		}, $.extend({}, opts, {complete: function(){
			jElm.css({display: 'none'});
			parent.css({height: '', overflow: '', display: ''});
			fn.apply(this, arguments);
		}}));
	});
};
$.fn.slideParentDown.defaults = {
	duration: 400,
	complete: function(){}
};

Kurz gesagt, wenn man die overflow: hidden Eigenschaft braucht, nimmt man ein zusätzliches div-Element und alles wird gut.

Fazit

Testen, testen, testen!!!

Das Ergebnis ist mit Sicherheit noch nicht des Weisheits letzter Schluß, aber hört sich schon ganz nett an. Für Leute die dies einmal testen möchten hier nun abschließend eine kleine Demonstration des zusammengebauten Tab-Scripts.

Die vorgenannte Demo wurde bisher mit folgenden Browser/Screenreader-Kombinationen erfolgreich getestet:

IE8: Jaws 10, Cobra (Webformator 2.4) | Firefox. 3.5: Jaws 10, NVDA 0.6p2 | IE 7: Jaws 9/Jaws 8

Update 06.10.2009: Das Script enthielt zuvor einen kleinen Fehler, so daß es nicht im IE7 mit Jaws 8/9 funktionierten konnte. Dieser Bug ist nun gefixt.

Written August 2, 2009 by
protofunc