protofunc()

Tutorial: Barrierearme Checkbox und Radio Button Ersetzung mit jQuery UI und der UI-Reflecting- Technik

Tags: accessibility, deutsch, javascript, jquery, tutorial

Demo und Script-Download

Problem und Lösungsansatz oder was ist UI-State-Reflecting

Eine gerne durchgeführte Aufgabe ist es, die in manchen Browsern recht hässlichen Checkboxen und Radiobuttons durch schönere, dem Design angepasste Eingabeelemente zu ersetzen. (Auf mögliche Verschlimmbesserungen durch schlechtes UI-Design möchte ich hier nicht eingehen. Dies gut zu machen, ist eine Herausforderung für den Designer!)
Möchte man hierbei möglichst barrierearm arbeiten, müssen die Zustände und Rollen der Eingabeelemente nicht nur optisch, sondern auch semantisch deutlich gemacht werden (hierzu eignet sich beispielsweise WAI-Aria).
Zusätzlich muss die Tastaturnutzung der Rolle und seines Zustandes entsprechend implementiert werden (für Radio Buttons für Checkboxen).

Insbesondere für die Implementierung der Tastaturnutzung für Radiobuttons ist ein etwas höherer Scripting-Aufwand nötig und leider sind die meisten JavaScript Entwickler in diesem Punkt entweder faul oder ignorant.

Reflektieren statt Simulieren

Dabei ist dies mit dem UI-Reflecting Ansatz mehr als einfach zu implementieren. Ausgangspunkt von UI-Reflecting ist die Erkenntnis, dass bereits der „Fallback“ entweder die Rollen und Zusände vollständig/teilweise abdeckt oder zumindest einen Teil der Funktionalität. In unserem Fall wird sowohl Funktionalität als auch Rolle und Zustand vollständig durch das Fallback abgedeckt und es findet lediglich eine optische Ersetzung statt. Beim UI-Reflecting lässt man also das Fallback die ganze Arbeit machen und der Entwickler bemüht sich lediglich darum dies auf dem neuen User Interface abzubilden.

UI-Widget-Factory

Die UI-Widget-Factory ist Teil von jQuery UI (in der ui.core.js). Um die Funktionsweise der Factory zu verstehen, empfehle ich folgendes UI Widget Tutorial.
Der Grundaufbau unseres unseres Scripts, wird wie folgt aussehen:

$.widget('ui.checkBox', {

});
$.ui.checkBox.defaults = {
    focusClass: 'focus',
    checkedClass: 'checked',
    disabledClass: 'disabled',
    hideInput: true,
    radioElements: false
};

In das noch leere Object-Literal werden wir die im folgenden beschriebenen Methoden integrieren:

Die Init-Methode:

Als 1. sammeln wir alle label-Elemente, welche mit dem jeweiligen input-Element durch das for-Attribut in Beziehung stehen. Diese(s) label-Element(e) werden durch entsprechende Hintergundbilder später die Rolle und den Zustand des input-Elements deutlich machen. Solltet ihr hierfür aufgrund von Designgründen nicht, die bereits vorhanden label-Elemente nutzen, sondern ein eigenes Element für die Optik des input-Elements nutzen wollen, könnt ihr vor dem instanziieren des Checkbox-Objects einfach ein weiteres label-Element einfügen. Beispiel:

$('input:radio').each(function(){
	var jElm = $(this);
		jElm
			.after('<label class="visualRadio" for="'+jElm.attr(id)+'">')
			.checkBox();
});

In der Regel wird dies jedoch nicht nötig sein.

Sofern es sich um ein Radiobutton handelt, holen wir uns alle Radiobuttons der Gruppe über das name-Attribut. Hintergrund ist, dass Radiobuttons sofern sie ungecheckt werden kein Cross-Browser-taugliches Event werfen und wir dies manuell nachholen müssen.

Im Anschluss verschieben wir das Original-input-Element aus dem Viewport. Dies machen wir allerdings davon abhängig, ob der User sich im Usermode (auch Kontrastmodus genannt) befindet, wofür wir ein weiteres kleines Script einsetzen. Sollte das Script nicht verwendet werden, gehen wird davon aus, dass sich der User nicht im Usermodus befindet.

Hiernach rufen wir unsere – noch zu definierende – reflectUI-Methode auf welche die Zusätnde des input-Elements durch CSS-Klassen auf unser(e) label-Element(e) überträgt.

Im Anschluss fügen wir noch die entsprechenden Event-Handler hinzu, um die Zustandsänderungen dynamisch ändern zu können. Damit das this weiterhin auf unser instanziiertes Object zeigt, setzen wir die bind-Function-Methode ein.

Hiermit ist unsere init-Methode fertig:

init: function(){
	var that = this,
		opts = this.options;
	this.labels = $('label[for='+this.element.attr('id')+']');
	this.checkedStatus = false;

        this.radio = opts.radioElements ||
		(this.element.is(':radio')) ?
		$(document.getElementsByName(this.element.attr('name'))) :
		false;

	var usermode = ($.userMode && $.userMode.get) ?
		$.userMode.get() :
		false;

	if(!usermode && opts.hideInput){
		var css = ($('html').attr('dir') == 'rtl') ?
			{position: 'absolute', right: '-999em', width: '1px'} :
			{position: 'absolute', left: '-999em', width: '1px'};
		this.element.css(css)
			.bind('usermode', function(e, o){
				if(o.usermode){
					that.destroy.call(that, true);
				}
			});
	}
	this.reflectUI({type: 'initialReflect'});
	this.element
		.bind('click.checkBox', $.bind(this, this.reflectUI))
		.bind('focus.checkBox blur.checkBox', function(){
			that.labels.toggleClass(opts.focusClass);
		});
}

Die reflectUI-Methode

Diese Methode ist sozusagen der Kern unserers Scripts. Sie stellt lediglich sicher, dass die Zustände (disabled und vor allem checked) auf unser User-Interface übertragen werden.

Wird eine Veränderung zum vorherigen Zustand erkannt, ruft die Methode die propagate-Methode auf, welche in vielen jQuery-UI-Widgets vorkommt und sehr interessant ist:

reflectUI: function(elm, e){
	var checked = this.element.is(':checked'),
		oldChecked = this.checkedStatus,
		o = this.options;
	e = e ||
		elm;
	this.labels[(this.element.is(':disabled'))?
		'addClass' :
		'removeClass'](o.disabledClass);

        this.checkedStatus = checked;

        if (checked !== oldChecked) {
            this.labels.toggleClass(o.checkedClass);
            this.propagate('change', e);
        }
}

Die propagate-Methode

Die propagate-Methode triggert an dem jeweiligen input-Element ein custom-Event, welche durch entsprechende Event-Objekte sowie Übergabe der eigenen Objektreferenz seine Zustände nach aussen trägt. Dies ist eine sehr gute Möglichkeit, um andere Widgets mit dem checkBox-Widget zu verbinden, ohne lästige starke Abhängigkeiten zu schaffen oder in den Original-Code einzugreifen. Es lässt sich aber auch für profaneres Gebrauchen. So kann man beispielsweise die Veränderung des checked-Status dynamisch durch eine Animation kenntlich machen.
Sofern es sich bei dem input-Element, um einen Radiobutton handelt, wird für alle Radiobuttons zusätzlich die reflectUI-Methode aufgerufen.

propagate: function(n, e){
	if(this.radio){
		this.radio.checkBox('reflectUI', e);
	}
    this.element.triggerHandler("checkbox" + n, [e, {
        instance: this,
        options: this.options,
        checked: this.checkedStatus,
		labels: this.labels
    }]);
}

Die destroy-Methode

Viele jQuery-UI-Widgets enthalten eine eigene destroy-Methode, welche dazu dient, das Widget mehr oder weniger auszuschalten und den Original-Zustand herzustellen. Bei unserer destroy-Methode kann gewählt werden, ob nur das Original-input-Element wieder sichtbar sein soll. Die Events und Zustandsanzeigen am label-Element bleiben erhalten oder ob zusätzlich alle Event-Handler entfernt werden sollen.

destroy: function(onlyCss){
	this.element.css({position: '', left: '', right: '', width: ''});
		if(!onlyCss){
			var o = this.options;
			this.element
				.unbind('.checkBox');
			this.labels
				.removeClass(o.disabledClass+' '+o.checkedClass+' '+o.focusClass);
		}
}

Kleine Verfeinerungen

Hiermit ist das Script bereits fertig und einsatzbereit. Als kleine Verfeinerung unseres Scripts fügen wir noch ein paar weitere öffentliche Methoden hinzu, die uns in anderen Situation noch hilfreich sein könnten:

disable: function(){
	this.element[0].disabled = true;
	this.reflectUI({type: 'manuallyDisabled'});
},
enable: function(){
	this.element[0].disabled = false;
	this.reflectUI({type: 'manuallyenabled'});
},
toggle: function(){
	this.changeCheckStatus((this.element.is(':checked'))?false:true);
},
changeCheckStatus: function(status){
	this.element.attr({'checked': status});
	this.reflectUI({type: 'changeCheckStatus'});
}

Fazit:

Wie sich gezeigt hat, ist der Scripting-Aufwand im Vergleich zum Ergebnis (komplette Tastaturnutzung wie Original-Elemente, richtige Rollen und Zustände bei Screenreadern etc.) relativ gering gewesen. Der Grund: Wir haben an keiner Stelle eine Tastaturnutzung oder ähnliches Scripten müssen. All das besorgt der Browser für uns.
Demo und Script-Download

Written September 6, 2008 by
alexander farkas

33 Comments »

  1. Could be in English…

    Comment by Tiago — December 30, 2008 @ 10:02 am

  2. [...] Barriereärmeres UI mit jQuery Das Tutorial erklärt die Ersetzung von Checkboxen und Radiobuttons mit jQuery UI und UI-Reflecting. [...]

    Pingback by 50+ jQuery Tutorials und mehr für Einsteiger und Fortgeschrittene — January 6, 2009 @ 12:29 am

  3. unfortunately this also converts every input html tag into the combobox for me, which has prevented me from being able to use text and submit inputs (prefer them to buttons).

    Any chance someone could fix this issue please? Perhaps making it only change inputs that have a specific class etc would be great.

    Comment by Stephen — January 13, 2009 @ 6:07 pm

  4. @Stephen
    Try another selector:
    $(‘input’).filter(‘[type=checkbox], [type=radio]‘).checkBox();

    Comment by alex — January 20, 2009 @ 2:58 pm

  5. Great Work!

    I’ve a question:

    the click event only fires when clicking an a label, but does not happen if I click on the styled checkbox.

    eg:
    Test

    What do I miss?

    Comment by Igor — February 16, 2009 @ 11:37 am

  6. Hi Igor,

    no you arn´t missing something. If you click on the visual-Replacement. The state of the real checkbox/radiobutton will be changed by accessing the checked property of the DOM-element. This doesn´t trigger an event. I will do this in the future. This is a nice improvement.

    But the widget creates his on event, you can use this instead. Try, the following code with Firefox and Firebug turned on:

    $(‘input’)
    .filter(‘[type=radio], [type=checkbox]‘)
    .bind(‘checkBoxchange’, function(event, widgetEvent, widgetEvent2){
    if(widgetEvent === true){
    widgetEvent = widgetEvent2;
    }
    //Firebug console logging
    console.log(arguments)
    console.log(this)
    })
    .checkBox();

    Oh my lastname is Farkas. You know, what this means :-) . Sorry, I can´t write hungarian (but I can speak.)

    Comment by alexander — February 16, 2009 @ 4:23 pm

  7. Dear Alex!

    thank you for the help, this worked as a charm :)

    I am very glad that we originate from the same country, go on with your excellent work.

    Best Wishes,
    Zoltan Medovarszky

    Comment by Igor — February 18, 2009 @ 6:51 am

  8. Hello Alex,
    Excellent plugin. Really neat and very nicely programmed.
    The only thing I was missing was that the state change on the checkbox/radiobutton does not trigger a click event when a mouse click occurs.
    But your workaround fixes it.
    Great work, I’m using your plugin extensively in a new web I’m working on and I’m very happy with it.
    I’ll show you the result once is finished.

    Best regards,
    Alex

    Comment by Alex — March 26, 2009 @ 4:28 pm

  9. hello,

    nice plugin that i’ll be using in some projects.

    nadim

    alienworkers.com
    mauritius

    Comment by nadim — April 24, 2009 @ 8:24 am

  10. Thanks a lot, that really helped me. I use different types of checkboxes, so the ‘ui’-prefix should be configurable. It would also help if the ‘ui-radio’/'ui-checkbox’ classed would also be added to the label-element when “addLabel: true”. In my application each change triggers an ajax call and the change is only applied on success:

    function checkBoxchange(e, ui) {
    var checkbox = $(this);
    checkbox.checkBox(“disable”).unbind(‘checkBoxchange’, checkBoxchange);
    $.ajax({
    url: “…..”,
    cache: false,
    success: function(html) {
    checkbox.checkBox(“enable”).bind(‘checkBoxchange’, checkBoxchange);
    },
    error: function(html) {
    checkbox.checkBox(“toggle”); // toggle back without event
    checkbox.checkBox(“enable”).bind(‘checkBoxchange’, checkBoxchange);
    }
    });
    };
    $(‘input’).checkBox().bind(‘checkBoxchange’, checkBoxchange);

    Comment by Jakob — May 11, 2009 @ 8:05 am

  11. Excellent plug-in. Really neat and very nicely programmed.

    Comment by wpdigger — June 3, 2009 @ 12:44 am

  12. thank you for the help, this worked as a charm :)

    Comment by wpdigger — June 3, 2009 @ 2:40 am

  13. Exzellentes Plugin, dohc leider vermisse ich die Möglichkeit “non-nagtive” Zustände darzustellen, wie zB “ui-state-error” für die Radiobuttons.

    Kommt da noch was nach?

    Danke,
    HFr

    Comment by H Frosch — August 7, 2009 @ 1:12 am

  14. Hallo Frosch,

    Ich verstehe deine Anfrage nicht ganz. Es steht dir natrülich immer frei, weitere Klassen an das label, die checkbox sowie an die checkbox-Ersetzung zu packen. Dafür brauchst grundsätzlich nicht das Plugin. Dieses ist lediglich dafür da, die normalen Zustände einer Checkbox/eines Radiobuttons zu spiegeln. Kannst du bitte etwas genauer sagen, was du vermißt und wie du dir die API hier vorstellst. Wenn das sinnvol ist, mache ich das natürlich.
    grüsse
    alex

    Comment by alexander farkas — August 9, 2009 @ 3:42 am

  15. This plugin is tremendous mate-well written too. medal is in the post.

    Comment by SEO Manchester — November 12, 2009 @ 10:19 pm

  16. Magnific plugin, but I’m trying to use the “reflectUI” method in checkboxes without success.

    I tried it at the same demo page (using firebug) and it didn’t work…

    This is the code I’ve used:

    $(‘#c1′).attr({checked: true, disabled: true}).checkBox(‘reflectUI’);

    Obviously, the checkbox disables and checks well, but the ‘reflectUI’ method does not convert the checkbox status.

    Could you help me? Thx

    Comment by Oscar — January 13, 2010 @ 8:18 am

  17. Hi Oscar,

    your tested script works well. The fact, that you don´t see a visual reaction is, due to the fact, that I didn´t handle the class ‘ui-checkbox-state-checked-disabled’ in my stylesheet. But the state-class is set prperly by the reflectUI-medthod. If you change your line to:

    $(‘#c1′).attr({checked: true, disabled: false}).checkBox(‘reflectUI’);

    You will see an reaction. In the upcomming next release (in feburary). I will add to the “states-class” (one class for all states) + normal state-classes (every state has his own class). This means, it will be much more easier to style the checkbox/radiobutton and if you run into the doubble-class-bug in IE6 you can use the states-class to attach your style.

    Comment by alexander farkas — January 13, 2010 @ 11:59 am

  18. Werter Alex
    Auf einer Webseite habe ich mehrere Checkboxen in vier Bereichen. Ich möchte je nach Bereich ein unterschiedliches Aussehen für diese Checkboxen. Beispielsweise in Bereich 1 eine blaue Checkbox, in Bereich 2 eine rote Checkbox, usw. Ist es möglich, verschiedene Checkbox-Design auf einer Seite zu haben?
    Grüsse
    Paul

    Comment by Paul — July 30, 2010 @ 10:19 pm

  19. Hallo Paul,

    natürlich geht: über den Kontext ->
    .ui-radio {
    // standard styles für alle
    }
    form.style-a .ui-radio {
    //abwandlung für form 1
    }
    form.style-b .ui-radio {
    //abwandlung für form 2
    }
    fieldset.style-a .ui-radio {
    //abwandlung für fieldset
    }

    grüße
    alex

    Comment by alexander farkas — August 1, 2010 @ 6:13 pm

  20. Hallo Alex
    ich habe folgende Ergänzungen gemacht:

    .ui-radio-state-disabled,
    .ui-radio-state-checked-disabled,
    .ui-radio-state-disabled-hover,
    .ui-radio-state-checked-disabled-hover {
    color: #999;
    }
    span.ui-checkbox,
    span.ui-radio {
    display: block;
    float: left;
    width: 16px;
    height: 16px;
    background: url(grafics/icon_checkbox.png) 0 -40px no-repeat;
    }
    form.style-A .ui-radio {
    background: url(grafics/icon_checkboxA.png) 0 -40px no-repeat;
    }
    form.style-B .ui-radio {
    background: url(grafics/icon_checkboxB.png) 0 -40px no-repeat;
    }

    Wie muss ich nun HTML-Skript auf dies zugreifen? Ich habe es so probiert:

    [input name=”sitzplatz[]” type=”checkbox” class=”style-B” value=”234″ /]

    Aber das geht wohl nicht.
    Grüsse
    Paul

    Comment by Paul — August 5, 2010 @ 6:35 pm

  21. Hallo Paul,

    nein, das HTML müßte dann wie folgt aussehen:

    [form class="style-B"]
    [input name=”sitzplatz[]” type=”checkbox” value=”234″ /]
    [/form]

    Du kannst natürlich, wenn du innerhalb eines Forms unterschiedliche radios haben möchtest die stye-A hook auf ein tieferes div legen.

    Grüße
    Alex

    Comment by alexander farkas — August 5, 2010 @ 8:11 pm

  22. Lieber Alex
    ja, ich versuche verschiedene Checkboxen im selben Formular zu haben. Der Code um die Checkboxen anzuzeigen sieht jetzt so aus:
    [?php
    switch($kategorie){
    case A:
    echo'[div title="Platz '.$wert.'" style="float: left; width=16px; height=16px;" ]‘;
    echo ‘[input class="style-A" name="sitzplatz[]” type=”checkbox” value=”‘.$wert.’” /]’;
    echo’[/div]‘;
    break;
    case B:
    echo’[div title="Platz '.$wert.'" style="float: left; width=16px; height=16px;" ]‘;
    echo ‘[input class="style-B" name="sitzplatz[]” type=”checkbox” value=”‘.$wert.’” /]’;
    echo’[/div]‘;
    break;
    }

    und im css folgendes:
    span.ui-radio {
    display: block;
    float: left;
    width: 16px;
    height: 16px;
    background: url(grafics/icon_checkbox.png) 0 -40px no-repeat;
    }
    style-A .ui-radio {
    background: url(grafics/icon_checkboxA.png) 0 -40px no-repeat;
    }
    style-B .ui-radio {
    background: url(grafics/icon_checkboxB.png) 0 -40px no-repeat;
    }

    Aber das geht nicht.
    Gruss
    Paul

    Comment by Paul — August 5, 2010 @ 9:30 pm

  23. Hey Alex!

    Du hast beim 2. Codeblock die Anführungszeichen vergessen:
    jElm.attr(id) -> jElm.attr(‘id’)

    Grüsse
    Fabian

    Comment by Fabian Horlacher — January 26, 2011 @ 12:50 pm

  24. Gute Arbeit. Aber ein Problem gibt es da doch: Das Skript funktioniert nicht einwandfrei, wenn die input-Elemente IN den Labels (ohne for-Attribut) sind; da werden dann alle auf einmal angesprochen und geändert. Oder habe ich da eine Einstellung verpasst? Ich will ungern dynamisch die Inputs aus dem Label rausnehmen und extra IDs und for-Attribute einfügen.

    Comment by Stephan — March 25, 2011 @ 4:27 pm

  25. Ich hatte ein Problem mit dem Plugin, da ich wegen einem Framework etwas spezielle ID’s mit eckigen Klammern verwenden muss. So erhielt ich manchmal folgende Fehlermeldung:

    Syntax error, unrecognized expression: [for=cwAutoControl[MjI0fDF8aW5wdXR8MTMwNTU0OTU1OC43NjU5NHx4]]

    Lösung: ui.checkbox.js Zeile 47 ist das Problem, da die Anführungszeichen im die ID fehlen. Also bitte folgendermassen in der nächsten Version korrigieren:

    this.labels = $(‘label[for="' + this.element.attr('id') + '"]‘)

    Comment by Fabian Horlacher — May 19, 2011 @ 11:30 am

  26. eine Frage: ich hab ein Formular, wo ich paar checkboxes nicht customisen möchte. Gibt es ein Attribut um bei einem input das script auf False zu stellen?

    sonst funzt dein Scirpt 1A, DANKE!

    Comment by coder — June 28, 2011 @ 12:56 am

  27. @coder

    Benutze einfach entsprechende selectoren:

    $(‘input.customize’).checkBox();

    oder

    $(‘fieldset.special input’).filter(‘[type="checkbox"]‘).checkBox();

    Comment by alexander farkas — June 28, 2011 @ 9:13 am

  28. danke für deine schnelle antwort farkas

    Comment by coder — June 28, 2011 @ 11:56 am

  29. I have implemented the buttonstyle on my site to replace a simple Yes/No Radio button set. I’ve got them to appear, and the hover works, however when i click them, the button that was clicked disappears! Any ideas what might cause this?

    Comment by Jeremy DeStefano — July 14, 2011 @ 12:24 am

  30. Hallo Alex.

    Ich habe dein Script mal ein wenig verändert und erweitert, damit ich SVG-Grafiken nutzen kann. (Die gehen nicht gut als Background.)

    Zu sehen unter: http://tlprod.de/cb2/

    Leider werden die Events (vorallem mouseout) nicht korrekt übermittelt, wenn ich über die Checkbox fahre. Beim Label klappt es super.
    Kannst du mir dabei helfen? Wäre super, danke.

    Comment by Terraloader — August 7, 2011 @ 1:19 pm

  31. @Terraloader
    Kannst du dein genaues Problem beschreiben. Ich habe mir das kurz im FF5 angeguckt und es scheint zu funktionieren, wie ich das erwarten würde.

    Zu deinen Scriptanpassungen: Hier finden sich mehrere Brüche mit Best Practices (invalider Code, Vermischung von Style und Behavior, anlegen von Daten im DOM, die bereits im DOM als auch im puren checkBox Objekt vorhanden sind etc.).

    Letztendlich mußt du keine einzige Zeile des Scripts anfassen, um an dein Ziel zu kommen:

    Ich habe unter https://github.com/aFarkas/accessible–stylable-Radiobuttons-and-Checkboxes/blob/master/tmp_svg.html dein Beispiel mit meinem (unangetasteten) Original-Script hochgeladen. Wie du siehst muß nur eine einzige Zeile JS in der configuration geschrieben werden. (https://github.com/aFarkas/accessible–stylable-Radiobuttons-and-Checkboxes/blob/master/tmp_svg.html#L72)

    Das einzige was ich machen mußte, war ein update der jQuery UI widget factory. (Um den create-callback zu bekommen)

    Comment by alexander farkas — August 8, 2011 @ 7:19 am

  32. Sehr cool. Vielen Dank Alex.

    Ja, dass meine Bearbeitungen nicht so sauber waren, hab ich mir schon gedacht, aber wusste nicht wo ich es sonst reinhacken sollte. Deine Variante ist so natürlich viel sauberer. Dachte garnicht, dass mit nem Span und Img genügend Elemente schon da wären.

    Die fehlenden Events haben sich hiermit auch erledigt. Vorher war es eben so, dass wenn man schnell von einer Checkbox zur nächsten gefahren ist, noch bei beiden der Mouseoverstate zu sehen war (auch im FF5). Jetzt funzt es perfekt. :-)

    Btw: Mit SVG ist es sogar barriereärmer als die native Version, da SVG beim Zoomen mit vergrößert. ;)

    Comment by Terraloader — August 8, 2011 @ 10:48 am

  33. Hi,
    man ne Frage eines JQuery-Anfängers:
    Wie Frage ich den Staus der Checkbox ab?

    Und wie setzte ich den Staus gezielt im Code?

    Comment by Chris — February 1, 2012 @ 4:22 pm

RSS feed for comments on this post | TrackBack URL

Leave a comment