• This is still a great plugin, but I’d like to see it more accessible, including for those using screen readers. So I’d like to suggest two sets of enhancements. Those in PHP are as follows:

    In template,php I suggest changing lines 181-185 to this:

    
     <fieldset class="question <?php if ($questionAsTitle == "yes") { echo "qTitle";} ?>">
    <?php
    	if (!$questionAsTitle || $questionAsTitle != "yes") { ?>
    
    		<legend tabindex="-1"><?php if ($tooltip) {echo '<a class ="hdQuTooltip">? <span><b></b>'.$tooltip.'</span></a>';} ?><?php echo $questionText; ?> <?php if ($questionNumber >= 1) { echo $questionNumber; $questionNumber++; } else { echo $i;}?>: <?php the_title();?></legend>
    

    Then change line 203 to this:

    
    <legend tabindex="-1"><?php if ($tooltip) {echo '<a class ="hdQuTooltip">? <span><b></b>'.$tooltip.'</span></a>';} ?><?php the_title();?></legend>
    

    Then change line 212 from </div> to </fieldset>

    You will need to make a slight change to the CSS to reflect the fact that a couple of <h3> tags would then be replaced by <legend> tags.

    I’ll make my suggestions for the javascript changes in another post in this thread.

Viewing 6 replies - 1 through 6 (of 6 total)
  • Thread Starter KTS915

    (@kts915)

    The following is my suggestion for the complete custom.js file. I have added the accessibility enhancements at the top, but have also changed the notation a little to reflect the fact that WordPress always loads jQuery in no-conflict mode:

    
    jQuery(document).ready(function ($) {
    
    	// Make first question and all answers tabbable
    	$('#hdQuestionnaireContent').find('legend:first, label').attr('tabindex','0');
    
    	// Prevent scroll down when spacebar pressed	
    	document.getElementById('questionnaire1').addEventListener('keydown', function(e) {
    		if ( (e.keycode || e.which) === 32) {
    			e.preventDefault();
    		}
    	}, false);
    
    	//	Navigate between questions using arrow keys
    	$('#hdQuestionnaireContent').find('legend').on('keyup', function(e) {
    
    		if (e.which == 39 || e.which == 40) { // 'down' or 'right' arrow pressed
    			if ($(this).parent('.question').is('.question:last')) {
    				$(this).attr('tabindex','-1'); // set this question tabindex to -1
    				$(this).parent('.question').siblings('.question').first().children('legend').focus().attr('tabindex','0'); // set receiving question tabindex to 0
    			} else {	
    				$(this).attr('tabindex','-1'); // set this question tabindex to -1
    				$(this).parent('.question').next().children('legend').focus().attr('tabindex','0'); // set receiving question tabindex to 0
    			}
            }
    
            if (e.which == 37 || e.which == 38) { // 'up' or 'left' arrow pressed
    			if ($(this).parent('.question').is('.question:first')) {	
    				$(this).attr('tabindex','-1'); // set this question tabindex to -1
    				$(this).parent('.question').siblings('.question').last().children('legend').focus().attr('tabindex','0'); // set receiving question tabindex to 0
    			} else {
    				$(this).attr('tabindex','-1'); // set this question tabindex to -1
    				$(this).parent('.question').prev().children('legend').focus().attr('tabindex','0'); // set receiving question tabindex to 0
    			}
    		}
    
    	});
    
    	// Select answers
    	var label = $('#hdQuestionnaireContent').find('label');
    	$(label).prop('checked','false').attr('aria-selected','false');
    	$(label).on('click keydown', function(e) {
    		if (e.type == 'click' || e.which == 13 || e.which == 32) {
    			var ans = $(this).prev('input');
    			$(ans).prop('checked','true');
    			$(this).addClass('selected').attr('aria-selected','true');
    			$(this).siblings('label').removeClass('selected').attr('aria-selected','false');
    		}
    	});
    	
    	if (jPagination != "yes" && jPagination != "no") {
    		jPagination ="no";
    	}
    	if (hdQuizTimer != "yes" && hdQuizTimer != "no") {
    		hdQuizTimer ="no";
    	}
    
    	// if paginated and next page link is clicked
    	var currentScore = $("#currentScore").val(); 
    	var maxValue = $("#maxValue").val(); 
    
    	var nextLink = $("#hdQuNext").attr("href"); 
    	$("#hdQuNext").attr("href", nextLink + currentScore);
    
    	$("#hdQuNext").click(function(e){	
    
    		e.preventDefault();
    
    		currentScore = $("#currentScore").val(); 
    		if (currentScore == "NaN") {currentScore = 0;}
    
    		maxValue = $("#maxValue").val(); 
    
    		$('#questionnaire1 *').filter(":input[type='radio']:checked").each(function(){
    			var id = $(this).attr('id');
    			if ($(this).val() == "1") { currentScore++;}
    		});
    
    		$('#questionnaire1 *').filter(".hdHidden").each(function(){
    			currentScore = parseInt(currentScore) + parseInt($(this).val());
    		});
    
    		if (currentScore == 0) {currentScore = "NaN";}
    
    		$("#hdQuNext").attr("href", nextLink + currentScore + "&maxValue=" + maxValue); 
    		var goTo = $(this).attr('href');
    
    		setTimeout(function(){
    			window.location = goTo;
    		},100); 
     
    	});
    
    	// for jQuery pagination 
    	jPage = 0;
    	jPageFinish = "no";
    	if (jPagination == "yes") {
    		$("#hdQuFinish").hide();
    		$(".jPaginate").nextAll(".question").hide();
    
    		$("#jPaginateNext").click(function() {
    			$(".jPaginate:visible").prevAll(".question").hide();
    			$(".jPaginate:eq(" + jPage + ")").nextUntil(".jPaginate").show();
    			jPage++;
    
    			if (parseInt(jPaged) == jPage){
    				$("#jPaginateNext").hide();
    				$("#hdQuFinish").show();
    			}		
    
    			$('html, body').delay(200).animate({
    				scrollTop: $("#hdQuestionnaireContent").offset().top - 100
    			}, 150);
    		});
    
    	}
    
    	// If question uses image answers, only allow for image selection
    	$(".imageAnswer").click(function() {
    		$(this).closest('.question').children('.imageAnswer.selected').removeClass("selected");
    		$( this ).toggleClass( "selected" );
    
    		// Get the index of selected image so we can select the correct radio box
    		var listItem = $( this );
    		var listItem2 = $(this).closest('.question').children('.imageAnswer').index( listItem );
    	
    		if ( $( this ).hasClass( "imageCorrect" ) ) {
    			$(this).closest('.question').children('.hdHidden').val("1");
    		}
    		else {$(this).closest('.question').children('.hdHidden').val("0");}
    
    	});
    
    	// Countdown Timer
    	$.fn.countdown = function (callback, durationS, durationM) {
    		durationM = durationM || "";
    
    		var container = $(this[0]).html(durationS);
    		var countdown = setInterval(function () {
    			// If seconds remain
    			if (--durationS) {
    				// Update our container's message
    				if (quizCompleted != "yes") {
    					container.html(durationS + durationM);
    				}
    				if (durationS == 5) {
    					$('.hdCountdown').addClass("warning");
    				}
    			} else {
    				// Clear the countdown interval
    				clearInterval(countdown);
    				callback.call(container); 
    			}
    		}, 1000);
    
    	};
    
    	function submitForm () {
    		if (quizCompleted != "yes") {
    			this.html("0s");
    			$("#hdQuFinish").click();
    		}
    	}
    
    	if (hdQuizTimer == "yes") { 
    		$(".hdCountdown").countdown(submitForm, quizTimerS, "s");
    	}
    
    	// Submit the form
    	var quizCompleted = "no";
    
    	$("#questionnaire1").submit(function() {
    		$("#results").fadeIn('slow');
    
    		var url = pluginURL + "result.php"; // the script where you handle the form input.
    
    		$.ajax({
    			type: "POST",
    			url: url,
    			data: $("#questionnaire1").serialize(), // serializes the form's elements.
    			success: function(data) {
    
    				quizCompleted = "yes";
    
                    $('#resultsTotal').html(data);
    
    				$('.hdQuFin').addClass("hd-animate");
    
    				$('html, body').delay(1200).animate({
    					scrollTop: $("#results").offset().top - 100
    				}, 1500);
    		
    				var showResults = $('#showResults').val();
    
    				if (showResults == "yes") {
    					if (jPagination == "yes") {$(".question").show();}			
    
    					// Test normal questions	
    					$('#questionnaire1 *').filter(":input[type='radio']:checked").each(function(){
    						var id = $(this).attr('id');
    						if ($(this).val() == "1") { $(this).next('label').addClass("correct"); }
    						else $(this).next('label').addClass("wrong");
    					});
    
    					// Test questions with image answers
    					$('#questionnaire1 *').filter(".hdHidden").each(function(){				
    						var id = $(this).attr('id');
    						if ($( this ).hasClass( "hdHidden" ) && $(this).val() == "1") { 					$(this).closest('.question').children('.imageAnswer.selected').addClass("correct2");}
    						else { $(this).closest('.question').children('.imageAnswer.selected').addClass("wrong2"); }				
    					});
    
    					// Search all questions, see if question was answered incorrectly (or not at all), and display the questionContent if there is one
    					$('#questionnaire1 *').filter(".question").each(function(){
    						if ($(this).children(".imageAnswer").hasClass("wrong2")){
    							$(this).closest(".question").children('.questionContent').fadeIn("slow");					
    						}
    						if ($(this).children("label").hasClass("wrong")){					
    							$(this).closest(".question").children('.questionContent').fadeIn("slow");				
    						}
    					});
    
    				}
    				if (showResults == "yes" && hdQuizShowResultsCorrect == "yes") {			
    
    					// Test normal questions	
    					$('#questionnaire1 *').filter(":input[type='radio']").each(function(){
    						var id = $(this).attr('id');
    						if ($(this).val() == "1") { $(this).next('label').addClass("correct correctAfter"); }
    					});
    
    					// Test questions with image answers
    					$('#questionnaire1 *').filter(".imageCorrect").each(function(){
    						$(this).addClass("correct2");				
    					});
    				}
    			}
    		});
    
    		return false; // avoid executing the actual submit of the form
    
    	});
    
    });
    
    Thread Starter KTS915

    (@kts915)

    My previous attempt at this was colored by the behavior of Firefox, which simply wouldn’t respond to the usual cues. At last I’ve found the reason for its strange behavior: it doesn’t like the radio input buttons being invisible.

    This means that we can do a much better job for accessibility purposes if we start by changing line 60 of `admin.css’ from this:

    
    #hdQuestionnaireContent input[type="radio"] {display:none !important; }
    

    to this:

    
    #hdQuestionnaireContent input[type="radio"] {position: absolute; left: -99999px; }
    

    That still keeps the radio buttons off-screen, but now Firefox can “see” them.

    We still need to add the <fieldset> and <legend> tags as explained above in my original comment, but the custom.js file now looks like this:

    
    jQuery(document).ready(function ($) {
    
    	// Set variables and make answers and inputs focusable but not tabbable
    	var legend = $('#hdQuestionnaireContent legend');
    	var label = $('#hdQuestionnaireContent label');
    	$(label).attr('tabindex','-1');
    	$('#hdQuestionnaireContent input').attr('tabindex','-1');
    
    	// Prevent arrowing out of answers
    	$(label).on('keydown', function(e) {
    		if (e.which == 37 || e.which == 38) { // 'up' or 'left' arrow pressed
    			if ($(this).is(':first-of-type')) {
    				$(this).siblings().last('label').focus(); // last answer
    				return false;
    			} else {
    				$(this).prev().prev('label').focus(); // previous answer
    				return false;
    			}
    		}
    		if (e.which == 39 || e.which == 40) { // 'down' or 'right' arrow pressed
    			if ($(this).is(':last-of-type')) {
    				$(this).siblings().eq(2).focus(); // first answer
    				return false;
    			} else {
    				$(this).next().next('label').focus(); // next answer
    				return false;
    			}
    		}
    	});
    	
    	// Enable arrowing from question to answers 
    	$(legend).on('keydown', function(e) {
    		if (e.which == 37 || e.which == 38) { // 'up' or 'left' arrow pressed
    			$(this).siblings().last('label').focus(); // last answer
    			return false;
    		}
    		if (e.which == 39 || e.which == 40) { // 'down' or 'right' arrow pressed
    			$(this).next().next('label').focus(); // first answer
    			return false;
    		}
    	});
    
    	// Select answer on click or when pressing Enter or spacebar	
    	// Note: Firefox selects answer automatically on focus
    	$(label).prop('checked','false').attr('aria-selected','false');
    	$(label).on('click keydown', function(e) {
    		if (e.type == 'click' || e.which == 13 || e.which == 32) {
    			var ans = $(this).prev('input');
    			$(ans).prop('checked','true');
    			$(this).addClass('selected').attr('aria-selected','true');
    			$(this).siblings('label').removeClass('selected').attr('aria-selected','false');
    			return false;
    		}
    	});
    
    	// Scroll to and focus on results when submit quiz
    	$('#resultsTotal').attr('tabindex','0');	
    	$('#hdQuFinish').on('click', function(e) {
    		setTimeout(function() {
    			$('#resultsTotal').focus();
    		}, 1500);
    	});
    
    	if (jPagination != "yes" && jPagination != "no") {
    		jPagination ="no";
    	}
    	if (hdQuizTimer != "yes" && hdQuizTimer != "no") {
    		hdQuizTimer ="no";
    	}
    
    	// if paginated and next page link is clicked
    	var currentScore = $("#currentScore").val(); 
    	var maxValue = $("#maxValue").val(); 
    
    	var nextLink = $("#hdQuNext").attr("href"); 
    	$("#hdQuNext").attr("href", nextLink + currentScore);
    
    	$("#hdQuNext").click(function(e){	
    
    		e.preventDefault();
    
    		currentScore = $("#currentScore").val(); 
    		if (currentScore == "NaN") {currentScore = 0;}
    
    		maxValue = $("#maxValue").val(); 
    
    		$('#questionnaire1 *').filter(":input[type='radio']:checked").each(function(){
    			var id = $(this).attr('id');
    			if ($(this).val() == "1") { currentScore++;}
    		});
    
    		$('#questionnaire1 *').filter(".hdHidden").each(function(){
    			currentScore = parseInt(currentScore) + parseInt($(this).val());
    		});
    
    		if (currentScore == 0) {currentScore = "NaN";}
    
    		$("#hdQuNext").attr("href", nextLink + currentScore + "&maxValue=" + maxValue); 
    		var goTo = $(this).attr('href');
    
    		setTimeout(function(){
    			window.location = goTo;
    		},100); 
     
    	});
    
    	// for jQuery pagination 
    	jPage = 0;
    	jPageFinish = "no";
    	if (jPagination == "yes") {
    		$("#hdQuFinish").hide();
    		$(".jPaginate").nextAll(".question").hide();
    
    		$("#jPaginateNext").click(function() {
    			$(".jPaginate:visible").prevAll(".question").hide();
    			$(".jPaginate:eq(" + jPage + ")").nextUntil(".jPaginate").show();
    			jPage++;
    
    			if (parseInt(jPaged) == jPage){
    				$("#jPaginateNext").hide();
    				$("#hdQuFinish").show();
    			}		
    
    			$('html, body').delay(200).animate({
    				scrollTop: $("#hdQuestionnaireContent").offset().top - 100
    			}, 150);
    		});
    
    	}
    
    	// If question uses image answers, only allow for image selection
    	$(".imageAnswer").click(function() {
    		$(this).closest('.question').children('.imageAnswer.selected').removeClass("selected");
    		$( this ).toggleClass( "selected" );
    
    		// Get the index of selected image so we can select the correct radio box
    		var listItem = $( this );
    		var listItem2 = $(this).closest('.question').children('.imageAnswer').index( listItem );
    	
    		if ( $( this ).hasClass( "imageCorrect" ) ) {
    			$(this).closest('.question').children('.hdHidden').val("1");
    		}
    		else {$(this).closest('.question').children('.hdHidden').val("0");}
    
    	});
    
    	// Countdown Timer
    	$.fn.countdown = function (callback, durationS, durationM) {
    		durationM = durationM || "";
    
    		var container = $(this[0]).html(durationS);
    		var countdown = setInterval(function () {
    			// If seconds remain
    			if (--durationS) {
    				// Update our container's message
    				if (quizCompleted != "yes") {
    					container.html(durationS + durationM);
    				}
    				if (durationS == 5) {
    					$('.hdCountdown').addClass("warning");
    				}
    			} else {
    				// Clear the countdown interval
    				clearInterval(countdown);
    				callback.call(container); 
    			}
    		}, 1000);
    
    	};
    
    	function submitForm () {
    		if (quizCompleted != "yes") {
    			this.html("0s");
    			$("#hdQuFinish").click();
    		}
    	}
    
    	if (hdQuizTimer == "yes") { 
    		$(".hdCountdown").countdown(submitForm, quizTimerS, "s");
    	}
    
    	// Submit the form
    	var quizCompleted = "no";
    
    	$("#questionnaire1").submit(function() {
    		$("#results").fadeIn('slow');
    
    		var url = pluginURL + "result.php"; // the script where you handle the form input.
    
    		$.ajax({
    			type: "POST",
    			url: url,
    			data: $("#questionnaire1").serialize(), // serializes the form's elements.
    			success: function(data) {
    
    				quizCompleted = "yes";
    
                    $('#resultsTotal').html(data);
    
    				$('.hdQuFin').addClass("hd-animate");
    
    				$('html, body').delay(1200).animate({
    					scrollTop: $("#results").offset().top - 100
    				}, 1500);
    		
    				var showResults = $('#showResults').val();
    
    				if (showResults == "yes") {
    					if (jPagination == "yes") {$(".question").show();}			
    
    					// Test normal questions	
    					$('#questionnaire1 *').filter(":input[type='radio']:checked").each(function(){
    						var id = $(this).attr('id');
    						if ($(this).val() == "1") { $(this).next('label').addClass("correct"); }
    						else $(this).next('label').addClass("wrong");
    					});
    
    					// Test questions with image answers
    					$('#questionnaire1 *').filter(".hdHidden").each(function(){				
    						var id = $(this).attr('id');
    						if ($( this ).hasClass( "hdHidden" ) && $(this).val() == "1") { 					$(this).closest('.question').children('.imageAnswer.selected').addClass("correct2");}
    						else { $(this).closest('.question').children('.imageAnswer.selected').addClass("wrong2"); }				
    					});
    
    					// Search all questions, see if question was answered incorrectly (or not at all), and display the questionContent if there is one
    					$('#questionnaire1 *').filter(".question").each(function(){
    						if ($(this).children(".imageAnswer").hasClass("wrong2")){
    							$(this).closest(".question").children('.questionContent').fadeIn("slow");					
    						}
    						if ($(this).children("label").hasClass("wrong")){					
    							$(this).closest(".question").children('.questionContent').fadeIn("slow");				
    						}
    					});
    
    				}
    				if (showResults == "yes" && hdQuizShowResultsCorrect == "yes") {			
    
    					// Test normal questions	
    					$('#questionnaire1 *').filter(":input[type='radio']").each(function(){
    						var id = $(this).attr('id');
    						if ($(this).val() == "1") { $(this).next('label').addClass("correct correctAfter"); }
    					});
    
    					// Test questions with image answers
    					$('#questionnaire1 *').filter(".imageCorrect").each(function(){
    						$(this).addClass("correct2");				
    					});
    				}
    			}
    		});
    
    		return false; // avoid executing the actual submit of the form
    
    	});
    
    });
    

    I believe that this makes the plugin fully WAI-ARIA compliant. Users can tab between questions, while being able to scroll between answers using the arrow keys. They can then select answers by using the Enter key or spacebar as alternatives to clicking on them.

    Firefox still has a couple of quirks up its sleeve, in that (a) it requires that you tab into the answers first before using the arrows and (b) it insists on selecting an answer as soon as the latter gets focus (whereas the other browsers rightly wait for manual selection of an answer) but those seem to be issues with Firefox that we can’t hope to solve here.

    I’m sure you’ll want to test these suggestions, but I hope you’ll feel able to include them (or something like them) in a future update.

    Thanks for the great plugin!

    Thread Starter KTS915

    (@kts915)

    Hmm, line 58 of custom.js should actually read:

    
    $('#hdQuFinish').attr('tabindex','0').on('click', function(e) {
    

    That ensures that the submit button retains its tabbability.

    Plugin Author Harmonic Design

    (@harmonic_design)

    Whoa, kts915!

    I never noticed this thread until now; great work! You sure put a lot of thought into this.

    I’ll take a look at your suggested updates and test everything out. If all goes well, I’ll include it into the next update. No reason we can’t have WAI-ARIA compliance 🙂

    Thread Starter KTS915

    (@kts915)

    Glad you like the idea; hope you like the code (or something similar)! Happy New Year!

    Thread Starter KTS915

    (@kts915)

    Just been testing this some more. It looks like we also need to add a role to the answer label to ensure that the ARIA attributes are interpreted properly. So line 45 in custom.js should actually look like this:

    
    $(label).attr('role','option').prop('checked','false').attr('aria-selected','false');
    
Viewing 6 replies - 1 through 6 (of 6 total)

The topic ‘Accessibility Enhancements’ is closed to new replies.