Skip to content
Open
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
5 changes: 4 additions & 1 deletion js/artwork.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

129 changes: 129 additions & 0 deletions js/piemenus.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,135 @@ const enableWheelScroll = (wheel, itemCount) => {
wheelDiv.addEventListener("wheel", scrollHandler, { passive: false });
};

// Accessibility helpers for pie menus (screen reader announcements)
/**
* this function is to ensure that the pie menu live region is created and returned
*/
const __ensurePieMenuLiveRegion = () => {
let liveRegion = docById("pieMenuLiveRegion");
if (liveRegion) {
return liveRegion;
}
liveRegion = document.createElement("div");
liveRegion.id = "pieMenuLiveRegion";
liveRegion.setAttribute("role", "status");
liveRegion.setAttribute("aria-live", "polite");
liveRegion.setAttribute("aria-atomic", "true");
liveRegion.style.position = "absolute";
liveRegion.style.left = "-9999px";
liveRegion.style.width = "1px";
liveRegion.style.height = "1px";
liveRegion.style.overflow = "hidden";
document.body.appendChild(liveRegion);
return liveRegion;
};


/**
* this function is to get the aria label from the title of the pie menu item
*/
const __pieMenuA11yLabelFromTitle = title => {
if (title === null || title === undefined) {
return "";
}

const text = String(title).trim();
if (text.length === 0) {
return "";
}

if (text === "×") {
return _("Close");
}
if (text === "+") {
return _("Increase");
}
if (text === "-") {
return _("Decrease");
}
if (text === "▶") {
return _("Play");
}

if (text.startsWith("imgsrc:")) {
const raw = text.slice("imgsrc:".length);
const file = raw.split("/").pop() || raw;
const base = file.replace(/\.[^/.]+$/, "");
return base.replace(/[-_]+/g, " ").trim();
}

return text;
};

const __announcePieMenuLabel = label => {
const trimmed = String(label || "").trim();
if (!trimmed) {
return;
}
/**
* this logic is to prevent any duplicate announcements
* i.e if the same label is announced within 400ms, it will not be announced again
*/
const now = Date.now();
if (
window.__wheelnavA11yLastLabel === trimmed &&
window.__wheelnavA11yLastTime &&
now - window.__wheelnavA11yLastTime < 400
) {
return;
}

window.__wheelnavA11yLastLabel = trimmed;
window.__wheelnavA11yLastTime = now;
__ensurePieMenuLiveRegion().textContent = trimmed;
};

/**
* this function is to speak the pie menu label to the screen reader
*/
const __speakPieMenuLabel = label => {
const trimmed = String(label || "").trim();
if (!trimmed) {
return;
}
//If the browser doesn’t support TTS, do nothing.
if (!("speechSynthesis" in window)) {
return;
}
//If the user toggled speech off, do nothing.
if (window.__wheelnavA11ySpeakEnabled === false) {
return;
}

try {
window.speechSynthesis.cancel(); //Stop any current speech so the new label replaces it immediately.
const utterance = new SpeechSynthesisUtterance(trimmed);
utterance.rate = 1;
utterance.pitch = 1;
window.speechSynthesis.speak(utterance);
} catch (e) {
// Ignore speech synthesis errors
}
};

// Expose hooks for the wheelnav library to call
if (window.__wheelnavA11ySpeakEnabled === undefined) {
window.__wheelnavA11ySpeakEnabled = true;
}

window.__wheelnavA11yLabelForItem = navItem =>
__pieMenuA11yLabelFromTitle(navItem && navItem.title);

// When called, it writes the label into the live region so screen readers announce it.
window.__wheelnavA11yAnnounce = navItem => {
const label = __pieMenuA11yLabelFromTitle(navItem && navItem.title);
__announcePieMenuLabel(label);
};

//When called (on click), it speaks the label using speechSynthesis
window.__wheelnavA11ySpeak = navItem => {
const label = __pieMenuA11yLabelFromTitle(navItem && navItem.title);
__speakPieMenuLabel(label);
// Ensure exit wheels behave like stateless buttons (no sticky selection)
const configureExitWheel = exitWheel => {
if (!exitWheel || !exitWheel.navItems) {
Expand Down
26 changes: 26 additions & 0 deletions js/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,20 @@ class Toolbar {
const stopIcon = docById("stop");
const recordButton = docById("record");
let isPlayIconRunning = false;
const speakToolbarLabel = label => {
if (!label) return;
if (!("speechSynthesis" in window)) return;
if (window.__wheelnavA11ySpeakEnabled === false) return;
try {
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(label);
utterance.rate = 1;
utterance.pitch = 1;
window.speechSynthesis.speak(utterance);
} catch {
// Ignore speech synthesis errors
}
};

function handleClick() {
if (!isPlayIconRunning) {
Expand All @@ -419,6 +433,7 @@ class Toolbar {
this.activity.hideMsgs();
};
isPlayIconRunning = false;
speakToolbarLabel(_("Play"));
onclick(this.activity);
handleClick();
stopIcon.style.color = this.stopIconColorWhenPlaying;
Expand Down Expand Up @@ -449,6 +464,17 @@ class Toolbar {
const stopIcon = docById("stop");
const recordButton = docById("record");
stopIcon.onclick = () => {
if ("speechSynthesis" in window && window.__wheelnavA11ySpeakEnabled !== false) {
try {
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(_("Stop"));
utterance.rate = 1;
utterance.pitch = 1;
window.speechSynthesis.speak(utterance);
} catch {
// Ignore speech synthesis errors
}
}
onclick(this.activity);
stopIcon.style.color = "white";
saveButton.disabled = false;
Expand Down
41 changes: 35 additions & 6 deletions js/turtles.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
setupPitchActions, setupIntervalsActions, setupToneActions, setupOrnamentActions,
setupVolumeActions, setupDrumActions, setupDictActions, _, Turtle, TURTLESVG, METRONOMESVG,
FILLCOLORS, STROKECOLORS, getMunsellColor, DEFAULTVALUE, DEFAULTCHROMA,
jQuery, docById, LEADING, CARTESIANBUTTON, piemenuGrid, CLEARBUTTON, COLLAPSEBUTTON,
EXPANDBUTTON, MBOUNDARY
jQuery, docById, LEADING, CARTESIANBUTTON, piemenuGrid, CLEARBUTTON, SPEAKBUTTON,
COLLAPSEBUTTON, EXPANDBUTTON, MBOUNDARY
*/

/* exported Turtles */
Expand Down Expand Up @@ -983,7 +983,7 @@ Turtles.TurtlesView = class {
name: "Grid",
label: _("Grid")
},
this._w - 10 - 3 * 55,
this._w - 10 - 4 * 55,
70 + LEADING + 6
);
const that = this;
Expand All @@ -1000,7 +1000,7 @@ Turtles.TurtlesView = class {
name: "Clear",
label: _("Clear")
},
this._w - 5 - 2 * 55,
this._w - 5 - 3 * 55,
70 + LEADING + 6
);

Expand All @@ -1010,6 +1010,34 @@ Turtles.TurtlesView = class {
};
};

const __makeSpeakToggleButton = () => {
this._speakToggleButton = _makeButton(
SPEAKBUTTON,
{
name: "PieMenuSpeech",
label: _("Pie menu speech on")
},
this._w - 5 - 2 * 55,
70 + LEADING + 6
);
this._speakToggleButton.id = "PieMenuSpeech";

const updateSpeakButton = () => {
const enabled = window.__wheelnavA11ySpeakEnabled !== false;
this._speakToggleButton.style.opacity = enabled ? "1" : "0.4";
const label = enabled ? _("Pie menu speech on") : _("Pie menu speech off");
this._speakToggleButton.setAttribute("data-tooltip", label);
};

updateSpeakButton();

this._speakToggleButton.onclick = () => {
window.__wheelnavA11ySpeakEnabled =
window.__wheelnavA11ySpeakEnabled === false ? true : false;
updateSpeakButton();
};
};

/**
* Makes collapse button by initailising 'COLLAPSEBUTTON' SVG.
* Assigns click listener function to call __collapse() method.
Expand Down Expand Up @@ -1143,13 +1171,13 @@ Turtles.TurtlesView = class {
this._clearButton.scaleX = 1;
this._clearButton.scaleY = 1;
this._clearButton.scale = 1;
this._clearButton.x = this._w - 5 - 2 * 55;
this._clearButton.x = this._w - 5 - 3 * 55;

if (this.gridButton !== null) {
this.gridButton.scaleX = 1;
this.gridButton.scaleY = 1;
this.gridButton.scale = 1;
this.gridButton.x = this._w - 10 - 3 * 55;
this.gridButton.x = this._w - 10 - 4 * 55;
this.gridButton.visible = true;
}

Expand Down Expand Up @@ -1186,6 +1214,7 @@ Turtles.TurtlesView = class {
cont.setAttribute("id", "buttoncontainerTOP");
__makeCollapseButton();
__makeExpandButton();
__makeSpeakToggleButton();
__makeClearButton();
__makeGridButton();
jQuery
Expand Down
3 changes: 2 additions & 1 deletion lib/wheelnav.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading