Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Player accessibility #37

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion src/player.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ HangmanEngine.controller 'HangmanEngineCtrl', ['$scope', '$timeout', 'Parse', 'R
$scope.anvilStage = 0
$scope.keyboard = null # Bound to onscreen keyboard, hit prop fades out key when 1

$scope.focusTitleMessage = ''
$scope.focusAnswerMessage = ''
$scope.focusQuestionMessage = ''
$scope.focusKeyboardMessage = ''

_updateAnvil = ->
# Get number of entered attempts
for i in [0...$scope.max.length]
Expand Down Expand Up @@ -214,19 +219,26 @@ HangmanEngine.controller 'HangmanEngineCtrl', ['$scope', '$timeout', 'Parse', 'R
if $scope.gameDone
$scope.endGame()
else if not $scope.loading
$scope.startGame()
$scope.startGame()

$scope.startGame = ->
return if $scope.inGame

liveRegionUpdate("The game has begun! Your topic is " + document.getElementsByClassName('title')[0].innerHTML + " with " + $scope.total + " questions.")
$scope.focusTitleMessage = document.getElementsByClassName('title')[0].innerHTML + " with " + $scope.total + " questions."

$scope.curItem++
$scope.anvilStage = 1
$scope.inGame = true
$scope.inQues = true
$scope.readyForInput = true
$scope.ques = _qset.items[0].items[$scope.curItem].questions[0].text
$scope.answer = Parse.forBoard _qset.items[0].items[$scope.curItem].answers[0].text
$scope.focusQuestionMessage = "Question " + ($scope.curItem + 1) + ": " + $scope.ques
$scope.focusAnswerMessage = "Current answer: " + condenseBlanks($scope.answer.guessed)
$scope.focusKeyboardMessage = "You have " + ($scope.max.length - $scope.anvilStage + 1) + " guesses. Press or type a letter."
$timeout ->
liveRegionUpdate("Question 1: " + $scope.ques + condenseBlanks($scope.answer.guessed))
Hangman.Draw.playAnimation 'torso', 'pull-card'
, 800

Expand All @@ -253,6 +265,68 @@ HangmanEngine.controller 'HangmanEngineCtrl', ['$scope', '$timeout', 'Parse', 'R
if $scope.inGame and !$scope.inQues
$timeout ->
$scope.startQuestion()
else
$scope.toggleGame()

liveRegionUpdate = (message) ->
document.getElementById('ariaLive').innerHTML = message

addBlanksForLiveRegion = (guessed) ->
# Takes the user's current answer and adds "blank" where there are blanks so the screen reader will read them out loud
# Used to tell screen reader users what their current board looks like
message = ''
words = 0
maxWords = 0
for word in guessed
maxWords += 1
for word in guessed
words += 1
for letter in word
if letter == ''
message = message.concat('blank, ')
else
message = message.concat(letter, ', ')
if words < maxWords
message = message.concat('new word, ')

return message

guessedToString = (guessed) ->
# Converts the user's current answer to a string to be read by the screen reader
# Used to read the final answer in full words so the player knows what they got
message = ''
for word in guessed
for letter in word
message = message.concat(letter)
message = message.concat(' ')
return message

usedKeysToString = () ->
# Converts the user's previously guessed letters to a string to be read by the screen reader
# Used to tell the player which letters they've already guessed, both correct and incorrect
message = ''
for index, letter of $scope.keyboard
if letter.hit == 1
message += index + ', '
return message

condenseBlanks = (guessed) ->
# Counts the words in the answer and the letters in each word and turns them into a string
# Used to tell the player how many words and letters are in the answer at the beginning of a question
message = ''
words = 0
for word in guessed
words += 1
message = message.concat(words + " words. ")
words = 0
for word in guessed
letters = 0
words += 1
message = message.concat("Word " + words + ": ")
for letter in word
letters += 1
message = message.concat(letters + " letters. ")
return message

$scope.getUserInput = (input) ->
# Keyboard appears slightly before question transition is complete, so ignore early inputs
Expand All @@ -270,20 +344,28 @@ HangmanEngine.controller 'HangmanEngineCtrl', ['$scope', '$timeout', 'Parse', 'R
if matches.length is 0
$scope.max = Input.incorrect $scope.max
_updateAnvil()
liveRegionUpdate(input + " is incorrect. " + ($scope.max.length - $scope.anvilStage + 1) + " guesses remaining.")
$scope.focusKeyboardMessage = ($scope.max.length - $scope.anvilStage + 1) + " guesses remaining. Letters guessed: " + usedKeysToString()

# User entered a correct guess
else
$scope.answer.guessed = Input.correct matches, input, $scope.answer.guessed
liveRegionUpdate(input + " is correct! Current answer: " + addBlanksForLiveRegion($scope.answer.guessed))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things:

  1. I think providing the current answer after each letter input might be kinda information overload. The answer is always available when focusing the answer element - I might just have this report whether the selection was correct or incorrect.
  2. Entering a letter (and getting validation on whether it was correct or incorrect) is a high-priority interaction. I would consider adding a second live region with the assertive property, and having these updates (correct or incorrect) report to that assertive live region instead. Those will interrupt whatever is currently being said, and also won't repeat the way polite ones do (which is a known bug).

$scope.focusAnswerMessage = "Current answer: " + addBlanksForLiveRegion($scope.answer.guessed)
$scope.focusKeyboardMessage = ($scope.max.length - $scope.anvilStage + 1) + " guesses remaining. Letters guessed: " + usedKeysToString()


# Find out if the user can continue to submit guesses
result = Input.cannotContinue $scope.max, $scope.answer.guessed
if result
liveRegionUpdate("Out of guesses. Press Enter to go to the next question.")
$scope.endQuestion()

# The user can't continue because they won and are awesomesauce
if result is 2
Hangman.Draw.playAnimation 'torso', 'pander'
$scope.anvilStage = 1
liveRegionUpdate(guessedToString($scope.answer.guessed) + " is correct! Press Enter to go to the next question.")

$scope.startQuestion = ->

Expand All @@ -296,6 +378,11 @@ HangmanEngine.controller 'HangmanEngineCtrl', ['$scope', '$timeout', 'Parse', 'R

$scope.readyForInput = true

liveRegionUpdate("Question " + ($scope.curItem + 1) + ": " + $scope.ques + condenseBlanks($scope.answer.guessed))
$scope.focusQuestionMessage = "Question " + ($scope.curItem + 1) + ": " + $scope.ques
$scope.focusAnswerMessage = "Current answer: " + condenseBlanks($scope.answer.guessed)
$scope.focusKeyboardMessage = "You have " + ($scope.max.length - $scope.anvilStage + 1) + " guesses. Press or type a letter."

Hangman.Draw.playAnimation 'torso', 'pull-card'

$scope.endQuestion = ->
Expand Down
17 changes: 10 additions & 7 deletions src/player.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@
<script src="player.js"></script>
</head>
<body ng-cloak>
<div id="ariaLive" aria-live="polite" aria-atomic="true"></div>
<div class="browserwarning">We recommend you upgrade your browser for a better experience</div>
<div id="browserfailure">
<p>We're sorry</p>
<p>But this widget requires features that your browser does not support.</p>
<p>Please upgrade to a newer version to play Guess the Phrase.</p>
</div>
<div ng-hide="inGame" hammer-tap="toggleGame()" class="overlay">
<div ng-hide="inGame" ng-click="toggleGame()" class="overlay">
<div class="portal">
<h1>Guess the Phrase</h1>
<p ng-show="!gameDone">Press or type letters to guess words and phrases.</p>
Expand All @@ -42,8 +43,8 @@ <h1>Guess the Phrase</h1>
<div>Loading...</div>
</center>
</div>
<div id="start" ng-show="!loading && !gameDone" class="start">Start</div>
<div ng-show="!loading && gameDone" class="finished"><span>FINISH</span></div>
<div id="start" ng-show="!loading && !gameDone" class="start" tabindex="0">Start</div>
Copy link
Member

@clpetersonucf clpetersonucf Mar 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a div, it's not immediately clear to the user that they are indeed focusing a button and need to interact with it to continue. Aside from changing the actual HTML element from div to button which may cause some unintended styling or behavioral changes, you can alternatively apply role="button" to indicate its purpose. ARIA role attributes are always secondary in preference to using semantic HTML, but for this widget it's acceptable.

<div ng-show="!loading && gameDone" class="finished" tabindex="0"><span>FINISH</span></div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the above. This should be given a button role to indicate its purpose as a focusable input.

</div>
</div>
<div class="game">
Expand All @@ -52,10 +53,10 @@ <h1>Guess the Phrase</h1>
<img src="assets/img/anvil.png" aria-hidden="true" class="anvil">
</div>
<div aria-hidden="true" class="podium"></div>
<div class="title"></div>
<div class="title" tabindex="0" aria-label="{{focusTitleMessage}}"></div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, anything in the tab sequence should have a clear indication of its purpose, and why it's focusable (as tab stops are normally tied to inputs). It might help to have a banner, or similar ARIA role, applied to this element. I might also consider prepending something to the focusTitleMessage string to indicate why the element is focusable. Maybe "Game status:" or something?

<div class="question-num">{{ curItem+1 }}</div>
<div class="total-questions"></div>
<div ng-class="{transition: inQues}" class="answer">
<div ng-class="{transition: inQues}" class="answer" tabindex="0" aria-label="{{focusAnswerMessage}}">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A thought: the focusAnswerMessage string is good here, but it's not immediately clear that the "current answer" mentioned in the string is associated with this element specifically, and why. It might help to replace "current answer" with a concise explanation of the focused element: "Your current answer is displayed here. It is currently: blank blank blank blank", that sort of thing. It might also help to append brief instructions after the current answer is relayed. "Guess another letter by pressing its corresponding key on the keyboard", etc.

<div ng-repeat="word in answer.string" class="row">
<span ng-repeat="letter in word" ng-class="{quirks: cssQuirks}" class="box">
<span class="letter"
Expand All @@ -72,7 +73,9 @@ <h1>Guess the Phrase</h1>
</div>
<div ng-class="{transition: inQues}"
ng-show="inGame"
class="question">
class="question"
tabindex="0"
aria-label="{{focusQuestionMessage}}">
{{ ques }}
<div class="tail"></div>
</div>
Expand All @@ -82,7 +85,7 @@ <h1>Guess the Phrase</h1>
class="icon-close">
</span>
</div>
<div aria-hidden="true" class="keyboard-bg"></div>
<div aria-hidden="true" class="keyboard-bg" tabindex="0" aria-label="{{focusKeyboardMessage}}"></div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be careful here, because your tabindex="0" attributes and aria-hidden="true" attributes are cancelling each other out. This element is never actually focusable because the aria-hidden attribute takes precedence. Similar to the answer element, I might also consider appending some directions after focusKeyboardMessage. "You must guess all the letters in the answer or run out of guesses before moving on to the next question", or something along those lines.

<div aria-hidden="true"
ng-show="inQues"
ng-init="keys=[
Expand Down
6 changes: 6 additions & 0 deletions src/player.scss
Original file line number Diff line number Diff line change
Expand Up @@ -540,3 +540,9 @@ div.browserwarning {
padding: 20px;
display: none;
}

#ariaLive {
width: 1px;
height: 1px;
overflow: hidden;
}
Loading