[SugarCube 2.29] Shifting Focus with JavaScript

Hi again,

I’ve been playing around with a new project in which I’m creating a grid-based tile map of an area. Each grid cell contains an anchor (<<link>>) which will be used by the player to interact with each tile.

If this were all I wanted to do, it would be working fine so far. However, I’m a blind developer attempting to develop with accessibility in mind. The problem I’m running into is that when the map loads, a user using keyboard navigation (such as with a screen-reader) will always have to start at the top-left corner of the grid. I would like to use JavaScript to automatically set the focus to a specific cell in teh grid (in the following case, the current center). However, the .focus() method does not appear to be doing anything.

This is my in-passage code for drawing the grid:


<div id="map" role="grid">
	<<for _y, _row range $Map>>
		<div @data-row="_y" role="row">
		<<for _x, _tile range _row>>
			<<set _classes = _tile.className + " x-" + _x + " y-" + _y>>
			<<if _tile.discovered>>
			<div @class="_classes" @data-x="_x + ''" @data-y="_y + ''" @data-ter="$MapMatrix[_y][_x]" role="gridcell">
				<<link " ">>
				
				<</link>>
				<span class="tile-name">
					_tile.name
				</span>
			</div>
			<<else>>
			<div @class="_classes + ' fog-of-war'" @data-x="_x + ''" @data-y="_y + ''" @data-ter="$MapMatrix[_y][_x]" role="gridcell">
				<<link " ">>
				
				<</link>>
				<span class="tile-name">
					_tile.name
				</span>
			</div>
			<</if>>
		<</for>>
		</div>
	<</for>>
</div>

And this is the snippet of code in which I’m trying to set the focus. I’m setting ‘tabindex’ to -1 because something I was reading on this topic mentioned that was required to set the focus, but I don’t understand this enough to know if that is actually the way it’s done.


$(document).on(":passagedisplay", function (ev) {
	if (ev.passage.title == "Forest Map") {
		setTileLabels();
		$("#map a").attr("tabindex", "-1");
		$(".tile-name").hide();
		$("div.fog-of-war a").prop("disabled", true);
		$("div[role='gridcell']:not(.fog-of-war) a")
			.on("mouseenter", showTileLabel)
			.on("focus", showTileLabel)
			.on("mouseleave", hideTileLabel)
			.on("blur", hideTileLabel);
			
		$("div.x-6.y-6 a").focus();
	}
});

Any pointers would be greatly appreciated. Thank you.

In future. For something like this, please either provide either a full working example or, at least, a full generated result from said code. Seeing the code always helps, but in cases where you’re generating content based on data we don’t have, it helps less than you might think.


The tabindex content attribute does not need to be set to any particular value for the .focus() method to work.

Your basic problem is probably that you’re overriding the default tabindex value on the generated anchor elements by setting it to -1. A value of -1 means that the element should not be focusable via keyboard navigation. You may still focus it via JavaScript, but that’s not going to help users with navigation. How were you testing to see if the element received focus?

There’s nothing obviously wrong with your JavaScript—aside from changing the default tabindex values—so assuming that the element you’re attempting to select actually exists it should receive focus just fine. Did you place it within the Story JavaScript or somewhere else?

PS: I created some dummy data, based on my reading of your example code, and after removing the line that alters the tabindex value the element in question receives focus as it should.

PPS: All interactables generated by SugarCube have various accessibility attributes and properties set by default—e.g., the tabindex content attribute.

1 Like

I’ve been testing using both the tab key and my screen-reader’s built-in navigation short-cuts. Everything is still focus-able when I have tabindex set to -1, and taking it out produces no change at all as far as I can tell. The JavaScript code is all in the Story JavaScript section.

Unfortunately, I’m still considered to be a new user on this site, meaning I can’t upload my project. I don’t have easy access to anywhere else at the moment, so the best I can do is give you everything I have here:

My JavaScript:


config.passages.nobr = true;

$(document).on(":passagedisplay", function (ev) {
	if (ev.passage.title == "Forest Map") {
		setTileLabels();
		$(".tile-name").hide();
		$("div.fog-of-war a").prop("disabled", true);
		$("div[role='gridcell']:not(.fog-of-war) a")
			.on("mouseenter", showTileLabel)
			.on("focus", showTileLabel)
			.on("mouseleave", hideTileLabel)
			.on("blur", hideTileLabel);
		
		
		$("div.x-6.y-6 a").focus();
	}
});

function setTileLabels() {
	$("div[role='gridcell'] a").each(function () {
		if ($(this).parent().hasClass("fog-of-war")) {
			$(this).attr("aria-label", "Undiscovered");
		}
		else {
			var x = parseInt($(this).parent().attr("data-x"));
			var y = parseInt($(this).parent().attr("data-y"));
			var t = parseInt($(this).parent().attr("data-ter"));
			var tile = state.variables.Map[x][y];
			$(this).attr("aria-label", tile.name + ", x:" + x +
				", y:" + y);
		}
	});
}
function showTileLabel() {
	$(this).siblings("span").show();
}
function hideTileLabel() {
	$(this).siblings("span").hide();
}

My StoryInit passage:


<<set $DeepWater = {
	"name" : "deep water",
	"className" : "deep-water",
}>>
<<set $ShallowWater = {
	"name" : "shallow water",
	"className" : "shallow-water",
}>>
<<set $Forest = {
	"name" : "forest",
	"className" : "forest",
}>>
<<set $Meadow = {
	"name" : "meadow",
	"className" : "meadow",
}>>

<<set $TerrainByNumber = [
	$DeepWater,
	$ShallowWater,
	$Forest,
	$Meadow,
	$Hill,
	$ForestedHill,
	$SteepHill,
	$Mountain,
	$Chasm,
]>>


<<set $MapMatrix = [
	[0, 0, 1, 2, 2, 2, 2, 2, 1, 0, 0],
	[0, 1, 2, 2, 2, 2, 2, 2, 2, 1, 0],
	[1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1],
	[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
	[2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2],
	[2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2],
	[2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2],
	[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
	[1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1],
	[0, 1, 2, 2, 2, 2, 2, 2, 2, 1, 0],
	[0, 0, 1, 2, 2, 2, 2, 2, 1, 0, 0],
]>>
<<set $FogOfWarMap = [
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
	[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
	[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
]>>
<<set $StartingX = 6>>
<<set $StartingY = 6>>

<<set $Map = []>>
<<for _r, _row range $MapMatrix>>
	<<set $Map[_r] = []>>
	<<for _c, _cell range _row>>
		<<set $Map[_r][_c] = clone($TerrainByNumber[$MapMatrix[_r][_c]])>>
		<<if $FogOfWarMap[_r][_c] === 1>>
			<<set $Map[_r][_c].discovered = true>>
		<<else>>
			<<set $Map[_r][_c].discovered = false>>
		<</if>>
	<</for>>
<</for>>

The passage where the map is drawn (same as above, but I’ll include it again):


<div id="map" role="grid">
	<<for _y, _row range $Map>>
		<div @data-row="_y" role="row">
		<<for _x, _tile range _row>>
			<<set _classes = _tile.className + " x-" + _x + " y-" + _y>>
			<<if _tile.discovered>>
			<div @class="_classes" @data-x="_x + ''" @data-y="_y + ''" @data-ter="$MapMatrix[_y][_x]" role="gridcell">
				<<link " ">>
				
				<</link>>
				<span class="tile-name">
					_tile.name
				</span>
			</div>
			<<else>>
			<div @class="_classes + ' fog-of-war'" @data-x="_x + ''" @data-y="_y + ''" @data-ter="$MapMatrix[_y][_x]" role="gridcell">
				<<link " ">>
				
				<</link>>
				<span class="tile-name">
					_tile.name
				</span>
			</div>
			<</if>>
		<</for>>
		</div>
	<</for>>
</div>

And my StoryStylesheet:


/* ==========
// Global Tags and Classes
// ========== */

* {
	box-size: border-box;
}
#passages {
	width: 95%;
	margin: auto;
}

/* ==========
// Map Styles
// ========== */

#map {
	display: grid;
	width: 100%;
}
#map div[role='row'] {
	width: 100%;
	display: grid;
	grid-template-columns: 4em 4em 4em 4em 4em 4em 4em 4em 4em 4em 4em;
	justify-content: center;
}
#map div[role='gridcell'] {
	position: relative;
	width: 4em;
	height: 3em;
	border: 1px solid #eee;
	overflow-x: visible;
}
#map a {
	display: block;
	width: 100%;
	height: 100%;
}
#map span.tile-name {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	z-index: 3;
	text-align: center;
	font-size: small;
	background-color: black;
}
#map div.fog-of-war {
	background-color: black;
}
div.fog-of-war * {
	display: none;
}

/* ==========
// Terrain Styles
// ========== */

.deep-water:not(.fog-of-war) {
	background-color: blue;
}
.shallow-water:not(.fog-of-war) {
	background-color: lightblue;
}
.forest:not(.fog-of-war) {
	background-color: forestgreen;
}
.meadow:not(.fog-of-war) {
	background-color: lawngreen;
}

I’m sorry I can’t just send you the story itself, but these four bits are the only thing I have at present. (I didn’t want to move forward with the project until I knew I could make this part work).

1 Like

Well, I found a way around the problem. It doesn’t work automatically, but I had intended to put something like this on the page eventually anyway. I’ve put in a “bookmark” system, assigning an ID to the player’s current position on the map and placing a link to that ID at the top of the passage. (Might move eventually, but it works).

So, for the benefit of anyone else trying to accomplish something similar, here are the pieces I’m using to do this.

This is the anchor tag I’ve put in my passage:


<a id="jumpt-to-cp" href="#current-position">
	Jump to Current Position
</a>

Then, in my JavaScript, I simply assign the ID #current-position to the anchor within the grid cell that matches the player’s current x and y coordinates. I attempted to then call a .click() on the jump anchor, but this didn’t work for a reason I haven’t yet figured out.

I would love to solve the mystery of why .focus() does nothing, but this method works well enough that I can move forward with the project.

You might want to take a look at some of the grid sample code found through the WAI-ARIA (Web Accessibility Initiative - Accessible Rich Internet Applications) “Grids: Interactive Tabular Data and Layout Containers” (section 3.12) page.

Specifically, check out the “Data Grid Examples” sample code. There are three examples there showing you how to make a grid which you can tab to, and it will return you to the last focused cell within that grid.

Oh, and don’t bother trying to check the “Advanced Data Grid Example” sample code there, since there’s nothing but a placeholder on that page for where that sample code should go. Apparently they’ve been trying to come up with some good sample code for the last three years, but they’re having trouble coming to an agreement on how best to do it.

Anyways, sorry for the late reply, I wanted to read through it before I passed on the links, but I didn’t have time until now.

Hope that helps! :slight_smile:

1 Like

That’s alright. I appreciate the effort. I’ve really struggled with the documentation on WAI-ARIA as it’s so dense, but I’ll take a look at these code samples and see if I can implement any of them to improve the functionality of what I have.

Thanks again! :smiley: