Add a bunch of WAI-ARIA stuff for accessibility, #37. I'm not ready to call this fixed yet.

This commit is contained in:
Andy Janata 2013-04-20 12:21:28 -07:00
parent 83eaf8628a
commit 743cdbf6c1
6 changed files with 137 additions and 35 deletions

View File

@ -80,7 +80,7 @@ HttpSession hSession = request.getSession(true);
--%>
<div id="welcome">
<h1>
<h1 tabindex="0">
Pretend You're <dfn style="border-bottom: 1px dotted black"
title="Xyzzy is an Artificial Unintelligence bot. You'll be making more sense than him in this game.">
Xyzzy</dfn>
@ -95,20 +95,28 @@ HttpSession hSession = request.getSession(true);
If this is your first time playing, you may wish to read <a href="/">the changelog and list of
known issues</a>.
</p>
<p>Most recent updates: 13 and 14 April 2013:</p>
<p tabindex="0">Most recent update: 20 April 2013:</p>
<ul>
<li>Added client-side option to hide game password in the game options area. This is useful for
streaming the game and not letting people see the password. ;)</li>
<li>Added option to "not use" the idle timer. In reality, it just sets it to about 25 days.</li>
<li>Internal cleanups.</li>
<li>Fixed the game list sometimes showing the same game over and over, and not loading the list
of card sets to display in game options.</li>
<li>Made game list cards bigger, and fixed HTML entities displayed in them.</li>
<li>Fixed even-numbered rows in the scoreboard not using the correct background color when
displaying that the person won.</li>
<li tabindex="0">A bunch of accessibility things for screen readers. If you are not using a
screen reader, you don't care about any of this. If you are, tab to the next element for more
details.</li>
</ul>
<div style="position:absolute; left:-999999px" tabindex="0" id="screenreader-intro">
I have spent a couple hours attempting to make this usable with screen readers. I have probably
missed a few things, but I believe the game is actually playable now. You should be able to get
to all of the interactive elements using only tab and shift tab, and all toggles should respond
to the space bar. To select a card, tab to it and press the space bar. You will have to get to
the Confirm Selection button to actually play it. I may eventually remove this requirement if
the card is selected with the space bar. The list of games leaves some information that is
visible out in an attempt to prevent each game from requiring a minute to read. You can still
access that information by navigating through the elements directly. I attempted to make the
notifications not be too chatty, but I may have failed. Please let me know if there are any
major issues.
</div>
<div id="nickbox">
Nickname: <input type="text" id="nickname" value="" maxlength="30" />
Nickname:
<input type="text" id="nickname" value="" maxlength="30" role="textbox"
aria-label="Enter your nickname." />
<input type="button" id="nicknameconfirm" value="Set" />
<span id="nickbox_error" class="error"></span>
</div>
@ -156,16 +164,16 @@ HttpSession hSession = request.getSession(true);
<div id="tab-preferences">
<input type="button" value="Save" onclick="save_preferences();" />
<input type="button" value="Revert" onclick="load_preferences();" />
<label for="hide_connect_quit">Hide connect/quit events: </label>
<label for="hide_connect_quit">Hide connect and quit events: </label>
<input type="checkbox" id="hide_connect_quit" />
<br />
<label for="ignore_list">Ignore list, one name per line:</label>
<label for="ignore_list">Chat ignore list, one name per line:</label>
<br/>
<textarea id="ignore_list" style="width: 200px; height: 150px"></textarea>
</div>
<div id="tab-global">
<div class="log"></div>
<input type="text" class="chat" maxlength="200" />
<input type="text" class="chat" maxlength="200" aria-label="Type here to chat." />
<input type="button" class="chat_submit" value="Chat" />
</div>
</div>
@ -173,7 +181,7 @@ HttpSession hSession = request.getSession(true);
<!-- Template for game lobbies in the game list. -->
<div class="hide">
<div id="gamelist_lobby_template" class="gamelist_lobby">
<div id="gamelist_lobby_template" class="gamelist_lobby" tabindex="0">
<div class="gamelist_lobby_left">
<h3>
<span class="gamelist_lobby_host">host</span>'s Game
@ -227,7 +235,7 @@ HttpSession hSession = request.getSession(true);
<!-- Template for face-up white cards. -->
<div class="hide">
<div id="white_up_template" class="card whitecard">
<span class="card_text">The quick brown fox jumped over the lazy dog.</span>
<span class="card_text" role="button" tabindex="0">The quick brown fox jumped over the lazy dog.</span>
<div class="logo">
<div class="logo_1 logo_element">
</div>
@ -256,7 +264,7 @@ HttpSession hSession = request.getSession(true);
<input type="button" class="game_show_last_round game_menu_bar" value="Show Last Round"
disabled="disabled" />
<input type="button" class="game_show_options game_menu_bar" value="Hide Game Options" />
<div class="game_message">
<div class="game_message" role="status">
Waiting for server...
</div>
</div>
@ -264,8 +272,10 @@ HttpSession hSession = request.getSession(true);
<div style="width:100%; height:100%;">
<div class="game_left_side">
<div class="game_black_card_wrapper">
The black card for <span class="game_black_card_round_indicator">this round is</span>:
<div class="game_black_card">
<span tabindex="0">The black card for
<span class="game_black_card_round_indicator">this round is</span>:
</span>
<div class="game_black_card" tabindex="0">
</div>
</div>
<input type="button" class="confirm_card" value="Confirm Selection" />
@ -274,7 +284,7 @@ HttpSession hSession = request.getSession(true);
</div>
<div class="game_right_side hide">
<div class="game_right_side_box game_white_card_wrapper">
The white cards played this round are:
<span tabindex="0">The white cards played this round are:</span>
<div class="game_white_cards game_right_side_cards">
</div>
</div>
@ -289,7 +299,7 @@ HttpSession hSession = request.getSession(true);
<div class="game_hand_filter hide">
<span class="game_hand_filter_text"></span>
</div>
<span class="your_hand">Your Hand</span>
<span class="your_hand" tabindex="0">Your Hand</span>
<div class="game_hand_cards">
</div>
</div>
@ -300,12 +310,13 @@ HttpSession hSession = request.getSession(true);
<!-- Template for scoreboard container. Holder for design. -->
<div style="height: 215px; border: 1px solid black;" class="hide">
<div id="scoreboard_template" class="scoreboard">
<div class="game_message" tabindex="0">Scoreboard</div>
</div>
</div>
<!-- Template for scoreboard score card. Holder for design. -->
<div class="scoreboard hide" style="height: 215px;">
<div id="scorecard_template" class="scorecard">
<div id="scorecard_template" class="scorecard" tabindex="0">
<span class="scorecard_player">PlayerName</span>
<div class="clear"></div>
<span class="scorecard_score">0</span> <span class="scorecard_point_title">Awesome Point<span class="scorecard_s">s</span></span>
@ -343,17 +354,20 @@ HttpSession hSession = request.getSession(true);
</select>
<br/>
<label id="player_limit_template_label" for="player_limit_template">Player limit:</label>
<select id="player_limit_template" class="player_limit">
<select id="player_limit_template" class="player_limit"
aria-label="Player limit. Having more than 10 players may cause issues both for screen readers and traditional browsers.">
<% int defaultPlayerLimit = 10; for (int i = 3; i <= 20; i++) { %>
<option <%= i == defaultPlayerLimit ? "selected='selected' " : "" %>value="<%= i %>"><%= i %></option>
<% } %>
</select>
Having more than 10 players may get cramped!
<br/>
<input type="checkbox" checked="checked" id="use_timer_template" class="use_timer" />
<input type="checkbox" checked="checked" id="use_timer_template" class="use_timer"
title="Players will be skipped if they have not played within a reasonable amount of time."
aria-label="Use idle timer. Players will be skipped if they have not played within a reasonable amount of time."/>
<label id="use_timer_template_label" for="use_timer_template"
title="Players will be skipped if they have not played within a reasonable amount of time.">
Use idle timer
Use idle timer.
</label>
<br/>
<fieldset class="card_sets">
@ -364,10 +378,12 @@ HttpSession hSession = request.getSession(true);
</fieldset>
<br/>
<label id="game_password_template_label" for="game_password_template">Game password:</label>
<input type="text" id="game_password_template" class="game_password" />
<input type="text" id="game_password_template" class="game_password"
aria-label="Game password. You must tab outside of the box to apply the password."/>
<input type="password" id="game_fake_password_template" class="game_fake_password hide" />
<input type="checkbox" id="game_hide_password_template" class="game_hide_password" />
<label id="game_hide_password_template_label" for="game_hide_password_template"
aria-label="Hide password from your screen."
title="Hides the password from your screen, so people watching your stream can't see it.">
Hide password.
</label>
@ -375,6 +391,6 @@ HttpSession hSession = request.getSession(true);
</fieldset>
</div>
</div>
<div style="position:absolute; left:-99999px" role="alert" id="aria-notifications"></div>
</body>
</html>

View File

@ -54,6 +54,11 @@ to, for instance, display the number of connected players.
</p>
<p>Recent Changes:</p>
<ul>
<li>20 April 2013:<ul>
<li>A bunch of accessibility things for screen readers. If you are not using a
screen reader, you don't care about any of this. If you are, more information is available on
the game page.</li>
</ul></li>
<li>14 April 2013:<ul>
<li>Fixed the game list sometimes showing the same game over and over, and not loading the list
of card sets to display in game options.</li>

View File

@ -86,7 +86,7 @@ cah.card.BaseCard = function(opt_faceUp, opt_id) {
*/
this.faceUpElem_ = undefined;
this.element_ = $('<div id="card_' + this.id_ + '" class="card_holder" ><br/></div>')[0];
this.element_ = $('<div id="card_' + this.id_ + '" class="card_holder"><br/></div>')[0];
if (this.faceUp_) {
this.turnFaceUp();
} else {
@ -145,7 +145,19 @@ cah.card.BaseCard.prototype.getFaceUp_ = function() {
*/
cah.card.BaseCard.prototype.setText = function(text) {
this.ensureFaceUpElement_();
jQuery(".card_text", this.faceUpElem_).html(text);
$(".card_text", this.faceUpElem_).html(text);
// TODO do this better
$(".card_text", this.faceUpElem_).attr(
"aria-label",
text.replace("____", "blank").replace("&trade;", "").replace("&reg;", "").replace("&amp;",
"and"));
};
/**
* Gets the screen reader text from this card.
*/
cah.card.BaseCard.prototype.getAriaText = function() {
return $(".card_text", this.faceUpElem_).attr("aria-label");
};
/**

View File

@ -103,11 +103,12 @@ cah.Game = function(id) {
var title = cardSet.getDescription() + ' ' + cardSet.getBlackCardCount() + ' black card'
+ (cardSet.getBlackCardCount() == 1 ? '' : 's') + ', ' + cardSet.getWhiteCardCount()
+ ' white card' + (cardSet.getWhiteCardCount() == 1 ? '' : 's') + '.';
var aria_label = cardSet.getName() + '. ' + title;
// that space at the beginning matters
var html = ' <span class="nowrap"><input type="checkbox" id="' + cardSetElementId
+ '" class="card_set" title="' + title + '" value="' + cardSet.getId()
+ '" name="card_set" /><label for="' + cardSetElementId + '" title="' + title
+ '" class="card_set_label">' + cardSet.getName() + '</label></span>';
+ '" name="card_set" aria-label="' + aria_label + '" /><label for="' + cardSetElementId
+ '" title="' + title + '" class="card_set_label">' + cardSet.getName() + '</label></span>';
if (cardSet.isBaseDeck()) {
$(".base_card_sets", this.optionsElement_).append(html);
} else {
@ -428,7 +429,8 @@ cah.Game.prototype.dealtCard = function(card) {
};
$(element).on("mouseenter.hand", data, cah.bind(this, this.handCardMouseEnter_)).on(
"mouseleave.hand", data, cah.bind(this, this.handCardMouseLeave_)).on("click.hand", data,
cah.bind(this, this.handCardClick_));
cah.bind(this, this.handCardClick_)).on("keypress.hand", data,
cah.bind(this, this.handCardKeypress_));
this.resizeHandCards_();
};
@ -514,7 +516,8 @@ cah.Game.prototype.addRoundWhiteCard_ = function(cards) {
};
$(element).on("mouseenter.round", data, cah.bind(this, this.roundCardMouseEnter_)).on(
"mouseleave.round", data, cah.bind(this, this.roundCardMouseLeave_)).on("click.round",
data, cah.bind(this, this.roundCardClick_));
data, cah.bind(this, this.roundCardClick_)).on("keypress.round", data,
cah.bind(this, this.roundCardKeypress_));
}
this.roundCards_[cards[0].getServerId()] = cards;
@ -841,9 +844,11 @@ cah.Game.prototype.updateUserStatus = function(playerInfo) {
*/
cah.Game.prototype.roundComplete = function(data) {
var cards = this.roundCards_[data[cah.$.LongPollResponse.WINNING_CARD]];
var ariaText = '';
for ( var index in cards) {
var card = cards[index];
$(".card", card.getElement()).addClass("selected");
ariaText += card.getAriaText();
}
var roundWinner = data[cah.$.LongPollResponse.ROUND_WINNER];
var scoreCard = this.scoreCards_[roundWinner];
@ -858,6 +863,9 @@ cah.Game.prototype.roundComplete = function(data) {
$(".game_white_card_wrapper .card_holder", this.element_).clone());
this.lastBlackCard_ = this.blackCard_;
$(".game_show_last_round", this.element_).removeAttr("disabled");
// speak it in screen readers
cah.log.ariaStatus("The round was won by " + roundWinner + " with " + ariaText);
};
/**
@ -941,6 +949,18 @@ cah.Game.prototype.confirmClick_ = function() {
}
};
/**
* Event handler for pressing a key on a card in the hand.
*
* @param e
* @private
*/
cah.Game.prototype.handCardKeypress_ = function(e) {
if (32 == e.which) {
this.handCardClick_(e);
}
};
/**
* Event handler for clicking on a card in the hand.
*
@ -969,10 +989,24 @@ cah.Game.prototype.handCardClick_ = function(e) {
if (card == this.handSelectedCard_) {
this.handSelectedCard_ = null;
$(".confirm_card", this.element_).attr("disabled", "disabled");
cah.log.ariaStatus("Deselected card.");
} else {
this.handSelectedCard_ = card;
$(".card", card.getElement()).addClass("selected");
$(".confirm_card", this.element_).removeAttr("disabled");
cah.log.ariaStatus("Selected card.");
}
};
/**
* Event handler for pressing a key on a card in the round.
*
* @param e
* @private
*/
cah.Game.prototype.roundCardKeypress_ = function(e) {
if (32 == e.which) {
this.roundCardClick_(e);
}
};
@ -1000,10 +1034,12 @@ cah.Game.prototype.roundCardClick_ = function(e) {
if (card == this.roundSelectedCard_) {
this.roundSelectedCard_ = null;
$(".confirm_card", this.element_).attr("disabled", "disabled");
cah.log.ariaStatus("Deselected card.");
} else {
this.roundSelectedCard_ = card;
$(".card", card.getElement()).addClass("selected");
$(".confirm_card", this.element_).removeAttr("disabled");
cah.log.ariaStatus("Selected card.");
}
};
@ -1273,7 +1309,7 @@ cah.GameScorePanel = function(player) {
*/
this.status_ = cah.$.GamePlayerStatus.IDLE;
jQuery(".scorecard_player", this.element_).text(player);
$(".scorecard_player", this.element_).text(player);
this.update(this.score_, this.status_);
};
@ -1297,6 +1333,10 @@ cah.GameScorePanel.prototype.update = function(score, status) {
$(".scorecard_score", this.element_).text(score);
$(".scorecard_status", this.element_).text(cah.$.GamePlayerStatus_msg[status]);
$(".scorecard_s", this.element_).text(score == 1 ? "" : "s");
$(this.element_).attr(
"aria-label",
this.player_ + " has " + score + " Awesome Point" + (score == 1 ? "" : "s") + ". "
+ cah.$.GamePlayerStatus_msg[status]);
};
/**

View File

@ -210,6 +210,14 @@ cah.GameListLobby = function(parentElem, data) {
if (data[cah.$.GameInfo.HAS_PASSWORD]) {
$(".gamelist_lobby_join", this.element_).val("Join\n(Passworded)");
}
$(this.element_).attr(
"aria-label",
data[cah.$.GameInfo.HOST] + "'s game, with " + data[cah.$.GameInfo.PLAYERS].length + " of "
+ data[cah.$.GameInfo.PLAYER_LIMIT] + " players. " + statusMessage + ". Goal is "
+ data[cah.$.GameInfo.SCORE_LIMIT] + " Awesome Points. Using " + cardSetNames.length
+ " card set" + (cardSetNames.length == 1 ? "" : "s") + ". "
+ (data[cah.$.GameInfo.HAS_PASSWORD] ? "Has" : "Does not have") + " a password.");
};
/**

View File

@ -93,6 +93,10 @@ cah.log.status_with_game = function(game_or_id, text, opt_class) {
$(node).addClass(opt_class);
}
logElement.append(node);
// only announce things in our game, or if it has a class (admin or error, likely)
if (game_or_id !== null || opt_class) {
cah.log.ariaStatus(text);
}
if (scroll) {
logElement.prop("scrollTop", logElement.prop("scrollHeight"));
@ -129,6 +133,23 @@ cah.log.everyWindow = function(text, opt_class) {
}
};
/**
* Set the text of the aria-notification element, which should cause screen readers to read this
* text.
*
* @param {string}
* text Text to read.
*/
cah.log.ariaStatus = function(text) {
// TODO we should pull this regex from the java code. it's close enough for now
var chatMatch = text.match(/<([a-zA-Z0-9_]+)> (.*)/);
if (chatMatch) {
$('#aria-notifications').text(chatMatch[1] + ' says ' + chatMatch[2]);
} else {
$('#aria-notifications').text(text);
}
};
/**
* Log a message if debugging is enabled, optionally dumping the contents of an object.
*