Skip to content

Commit

Permalink
Change how the accessibility fallback button works (#178)
Browse files Browse the repository at this point in the history
* Change how the accessibility fallback button works

This appears to work better, successfully triggered it on desktop &
Android. Next up I'll test it on iOS.

* Remove unneeded font-size css

* WIP code to detect accessibility clicks on iOS

* Change the accessibility detecting to be better.

* Clean up code.

* Fix reference to renamed function
  • Loading branch information
Jezzamonn authored Dec 11, 2023
1 parent 9084f0d commit 1184f71
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 28 deletions.
4 changes: 2 additions & 2 deletions static/scenes/railroad/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
<body>

<div id="content">
<button class="canvas-button" aria-label="throw"></button>
<img src="img/tutorial-hand.png" class="tutorial-hand">
<img src="img/tutorial-hand.png" class="tutorial-hand" role="presentation">
<button class="throw-accessibility-button" aria-label="throw"></button>
</div>

<script src="index.js" type="module"></script>
Expand Down
2 changes: 1 addition & 1 deletion static/scenes/railroad/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ api.config({
});

function initialize() {
const contentElement = document.querySelector('.canvas-button');
const contentElement = document.querySelector('#content');

const game = new Game();
api.addEventListener('pause', (ev) => game.pause());
Expand Down
13 changes: 8 additions & 5 deletions static/scenes/railroad/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@ body {
pointer-events: none;
}

.canvas-button {
.throw-accessibility-button {
// Should only be triggered by keyboard or accessiblity tools.
pointer-events: none;

margin: 0;
padding: 0;
overflow: hidden;
position: absolute;
border: none;
background: none;
left: 0;
right: 0;
top: 0;
bottom: 0;
left: 2%;
right: 2%;
top: 100px;
bottom: 2%;
}

.canvas-button:focus-visible {
Expand Down
85 changes: 65 additions & 20 deletions static/scenes/railroad/js/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,9 @@ class Game {

this.previousSeconds = Date.now() / 1000;

// Accessibility devices on Android fire off click events in the center of
// the Canvas element. We use these counters to guess if the user is using
// an accessibility tool.
this.numSuspectedAccessibilityClicks = 0;
this.numSuspectedNotAccessibilityClicks = 0;
// Previous clicks, used to guess if the user is using an accessibilty tool.
/** @type {Array<MouseEvent>} */
this.previousClicks = []
}

/**
Expand Down Expand Up @@ -144,36 +142,34 @@ class Game {
this.previousSeconds = nowSeconds;
}

setUpListeners(containerButton) {
setUpListeners(container) {
// Use `touchstart` for touch interfaces.
containerButton.addEventListener('touchstart', e => {
container.addEventListener('touchstart', e => {
const touch = e.changedTouches[0];
this.handleClick(touch.clientX, touch.clientY);
});

// Prevent the click event from firing on touch devices.
containerButton.addEventListener('touchend', e => {
container.addEventListener('touchend', e => {
e.preventDefault();
}, {passive: false})

// Use `click` for mouse interfaces and for accessibility
containerButton.addEventListener('click', e => {
console.log(e);
if (isPossibleClickFromAccessibiltyTool(containerButton, e)) {
this.numSuspectedAccessibilityClicks++;
}
else {
this.numSuspectedNotAccessibilityClicks++;
}

if (this.numSuspectedAccessibilityClicks > this.numSuspectedNotAccessibilityClicks) {
container.addEventListener('click', e => {
if (this.isSuspectedClickFromAccessibilityTool(container, e)) {
this.level.throwToClosest();
}
else {
this.handleClick(e.clientX, e.clientY);
}
});

document.querySelector('.throw-accessibility-button').addEventListener('click', e => {
console.log('throw to closest from button');
this.level.throwToClosest();
e.stopPropagation();
})

window.addEventListener('resize', () => {
this.renderer.setSize(window.innerWidth, window.innerHeight);
if (this.level) {
Expand All @@ -191,14 +187,63 @@ class Game {
this.level.handleClick(clientX, clientY);
}
}

/**
* @param {HTMLElement} element Element that handles click events
* @param {MouseEvent} e Click event
* @returns {boolean} Whether this click is likely from an accessibility tool (e.g. TalkBack).
*/
isSuspectedClickFromAccessibilityTool(element, e) {
// Keep track of the first few clicks. Only need the first few to detect
// accessibility tools.
if (this.previousClicks.length < 25) {
this.previousClicks.push(e);
}

// Not enough clicks to compare them without having false positives
if (this.previousClicks.length < 3) {
// Use the click positions that a few accessibility tools use. This
// doesn't detect all tools so we have the fallback below.
return this.previousClicks
.every(click => isPossibleAccessibilityToolClickPosition(element, click));
}
else {
// Return whether this click is in the same position as 90% of the previous clicks.
const numClicksPerPosition = new Map();
for (const click of this.previousClicks) {
const clickStr = mouseEventToString(click);
if (!numClicksPerPosition.has(clickStr)) {
numClicksPerPosition.set(clickStr, 0);
}
const prevValue = numClicksPerPosition.get(clickStr);
numClicksPerPosition.set(clickStr, prevValue + 1)
}

const percentOfClicksAtThisPosition =
numClicksPerPosition.get(mouseEventToString(e)) / this.previousClicks.length
return percentOfClicksAtThisPosition >= 0.9;
}
}
}

/**
* @param {MouseEvent} e Mouse event
* @returns A string representation that works in a map.
*/
function mouseEventToString(e) {
return Math.round(e.clientX) + ":" + Math.round(e.clientY);
}

/**
* Checks if this click was in one of the specific places that some
* accessibility tools send their events.
*
* @param {HTMLElement} element Element that handles click events
* @param {MouseEvent} e Click event
* @returns {boolean} Whether this click could've been fired from an accessibility tool (e.g. TalkBack).
* @returns {boolean} Whether this click could've been fired from an
* accessibility tool (e.g. TalkBack).
*/
function isPossibleClickFromAccessibiltyTool(element, e) {
function isPossibleAccessibilityToolClickPosition(element, e) {
if (e.clientX === 0 && e.clientY === 0) {
return true;
}
Expand Down

0 comments on commit 1184f71

Please sign in to comment.