From 70614ab7ff61382bc025eb00eda6fd295740f574 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 10 Apr 2026 17:35:02 -0500 Subject: [PATCH] Make the graphtool screen reader friendly. This sets the `aria` option from the JSXGraph library for all graphed objects. Thus objects are described as they are graphed. I am sure this will still need some work, but this makes the graphtool at least basically accessible. There is also a need to specifically set the tabindex for objects (usually to the empty string which is the closest thing to unsetting it that the JSXGraph library allows). Otherwise JSXGraph now sets that to -1 for everything and tries to focus those things with javascript and that is interfering with the graphtools own focus behavior. This is more of what was done in #1357. Note that the aria labels are not translated. Although nothing for the graphtool is. The usual data attribute approach really would be a mess for this. There are a lot of strings. Translating the graphtool would take a lot of effort with the current methods for javascript translation. --- htdocs/js/GraphTool/circletool.js | 38 ++++++- htdocs/js/GraphTool/cubictool.js | 31 +++++- htdocs/js/GraphTool/filltool.js | 55 ++++++++++- htdocs/js/GraphTool/graphtool.js | 31 ++++-- htdocs/js/GraphTool/intervaltools.js | 142 ++++++++++++++++++++++++--- htdocs/js/GraphTool/linetool.js | 39 +++++++- htdocs/js/GraphTool/parabolatool.js | 34 ++++++- htdocs/js/GraphTool/pointtool.js | 7 +- htdocs/js/GraphTool/quadratictool.js | 34 ++++++- htdocs/js/GraphTool/quadrilateral.js | 29 ++++-- htdocs/js/GraphTool/segments.js | 17 +++- htdocs/js/GraphTool/sinewavetool.js | 63 +++++++++++- htdocs/js/GraphTool/triangle.js | 52 ++++++++-- 13 files changed, 509 insertions(+), 63 deletions(-) diff --git a/htdocs/js/GraphTool/circletool.js b/htdocs/js/GraphTool/circletool.js index af51d27acf..0021ed02ba 100644 --- a/htdocs/js/GraphTool/circletool.js +++ b/htdocs/js/GraphTool/circletool.js @@ -16,9 +16,19 @@ fixed: true, highlight: false, strokeColor: gt.color.curve, - dash: solid ? 0 : 2 + dash: solid ? 0 : 2, + tabindex: '' }) ); + this.baseObj.setAttribute({ + aria: { + enabled: true, + label: this.constructor.ariaLabel, + roledescription: this.constructor.strId, + live: 'assertive', + atomic: true + } + }); this.definingPts.push(center, point); this.focusPoint = center; @@ -47,6 +57,14 @@ ); } + static ariaLabel(c) { + return ( + (c.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` circle centered at ${c.center.X()}, ${c.center.Y()} and ` + + `passing through the point ${c.point2.X()}, ${c.point2.Y()}` + ); + } + static restore(string) { let pointData = gt.pointRegexp.exec(string); const points = []; @@ -116,7 +134,9 @@ highlight: false, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, - withLabel: false + withLabel: false, + tabindex: 0, + aria: gt.pointAria }); this.hlObjs.hl_point.rendNode.focus(); } @@ -129,7 +149,15 @@ fixed: true, strokeColor: gt.color.underConstruction, highlight: false, - dash: gt.drawSolid ? 0 : 2 + dash: gt.drawSolid ? 0 : 2, + tabindex: '', + aria: { + enabled: true, + label: gt.graphObjectTypes[this.object].ariaLabel, + roledescription: this.object, + live: 'assertive', + atomic: true + } }); } @@ -151,7 +179,9 @@ highlight: false, snapToGrid: true, snapSizeX: gt.snapSizeX, - snapSizeY: gt.snapSizeY + snapSizeY: gt.snapSizeY, + tabindex: '', + aria: gt.pointAria }); this.center.setAttribute({ fixed: true }); diff --git a/htdocs/js/GraphTool/cubictool.js b/htdocs/js/GraphTool/cubictool.js index 46b4e23b27..0bd133925b 100644 --- a/htdocs/js/GraphTool/cubictool.js +++ b/htdocs/js/GraphTool/cubictool.js @@ -98,7 +98,19 @@ strokeWidth: 2, highlight: false, strokeColor: color ? color : gt.color.underConstruction, - dash: solid ? 0 : 2 + dash: solid ? 0 : 2, + tabindex: '', + aria: { + enabled: true, + label: (c) => + (c.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` cubic passing through ${point1.X()}, ${point1.Y()}, and ` + + `${point2.X()}, ${point2.Y()}, and ${point3.X()}, ${point3.Y()}, ` + + `and ${point4.X()}, ${point4.Y()}`, + roledescription: 'cubic', + live: 'assertive', + atomic: true + } } ); } @@ -294,7 +306,9 @@ snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, highlight: false, - withLabel: false + withLabel: false, + tabindex: '', + aria: gt.pointAria }); this.hlObjs.hl_point.rendNode.focus(); } @@ -341,7 +355,18 @@ fixed: true, strokeColor: gt.color.underConstruction, highlight: false, - dash: gt.drawSolid ? 0 : 2 + dash: gt.drawSolid ? 0 : 2, + tabindex: '', + aria: { + enabled: true, + label: (l) => + (l.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` line through ${l.point1.X()}, ${l.point1.Y()}, ` + + `and ${l.point2.X()}, ${l.point2.Y()}`, + roledescription: 'line', + live: 'assertive', + atomic: true + } }); } diff --git a/htdocs/js/GraphTool/filltool.js b/htdocs/js/GraphTool/filltool.js index 3935edfbcd..2c2331bc86 100644 --- a/htdocs/js/GraphTool/filltool.js +++ b/htdocs/js/GraphTool/filltool.js @@ -21,7 +21,15 @@ highlightStrokeOpacity: 0, fillOpacity: 0, highlightFillOpacity: 0, - fixed: gt.isStatic + fixed: gt.isStatic, + tabindex: 0, + aria: { + enabled: true, + label: (p) => `shade the region containing the point ${p.X()}, ${p.Y()}`, + roledescription: 'shading point', + live: 'assertive', + atomic: true + } }); this.definingPts.push(point); this.focusPoint = point; @@ -41,7 +49,15 @@ [() => point.X() - 12 / gt.board.unitX, () => point.Y() - 12 / gt.board.unitY], [() => 24 / gt.board.unitX, () => 24 / gt.board.unitY] ], - { withLabel: false, highlight: false, layer: 8, name: 'FillIcon', fixed: true } + { + withLabel: false, + highlight: false, + layer: 8, + name: 'FillIcon', + fixed: true, + tabindex: '', + aria: { enabled: true, hidden: true, live: 'off' } + } ); if (!gt.isStatic) { @@ -226,7 +242,21 @@ this.fillObj = gt.board.create( 'image', [dataURL, [bBox[0], bBox[3]], [bBox[2] - bBox[0], bBox[1] - bBox[3]]], - { withLabel: false, highlight: false, fixed: true, layer: 0 } + { + withLabel: false, + highlight: false, + fixed: true, + layer: 0, + tabindex: '', + aria: { + enabled: true, + label: () => + `shaded region containing the point ${this.baseObj.X()}, ${this.baseObj.Y()}`, + roledescription: 'shading', + live: 'assertive', + atomic: true + } + } ); }; @@ -343,7 +373,15 @@ withLabel: false, snapToGrid: true, snapSizeX: gt.snapSizeX, - snapSizeY: gt.snapSizeY + snapSizeY: gt.snapSizeY, + tabindex: 0, + aria: { + enabled: true, + label: (p) => `shade the region containing the point ${p.X()}, ${p.Y()}`, + roledescription: 'shading point', + live: 'assertive', + atomic: true + } }); this.hlObjs.hl_point.rendNode.classList.add('hidden-fill-point'); @@ -357,7 +395,14 @@ ], [() => 24 / gt.board.unitX, () => 24 / gt.board.unitY] ], - { withLabel: false, highlight: false, fixed: true, layer: 8 } + { + withLabel: false, + highlight: false, + fixed: true, + layer: 8, + tabindex: '', + aria: { enabled: true, hidden: true, live: 'off' } + } ); this.hlObjs.hl_point.rendNode.focus(); diff --git a/htdocs/js/GraphTool/graphtool.js b/htdocs/js/GraphTool/graphtool.js index 0991e15fee..7d7e0fb2b7 100644 --- a/htdocs/js/GraphTool/graphtool.js +++ b/htdocs/js/GraphTool/graphtool.js @@ -1,4 +1,4 @@ -/* global JXG */ +/* global JXG, MathJax */ 'use strict'; @@ -14,6 +14,7 @@ window.graphTool = (containerId, options) => { setTimeout(() => window.graphTool(containerId, options), 100); return; } + gt.graphContainer.role = 'application'; // Semantic color control gt.color = { @@ -95,9 +96,11 @@ window.graphTool = (containerId, options) => { lastArrow: { size: 7 }, straightFirst: false, straightLast: false, - fixed: true + fixed: true, + tabindex: '', + aria: { enabled: true, hidden: true, live: 'off' } }, - grid: { majorStep: [gt.snapSizeX, gt.snapSizeY] }, + grid: { majorStep: [gt.snapSizeX, gt.snapSizeY], aria: { enabled: true, hidden: true, live: 'off' } }, keyboard: { enabled: true, dx: gt.snapSizeX, @@ -711,15 +714,20 @@ window.graphTool = (containerId, options) => { }, 100); }); + let currentContent = ''; + gt.setMessageText = (content) => { if (gt.confirmationActive || !gt.helpEnabled) return; const newMessage = content instanceof Array ? content.join(' ') : content; if (newMessage) { + if (currentContent === newMessage) return; + currentContent = newMessage; const par = document.createElement('p'); par.textContent = newMessage; gt.setMessageContent(par); } else { + currentContent = ''; gt.setMessageContent(); } }; @@ -777,6 +785,14 @@ window.graphTool = (containerId, options) => { gt.pointRegexp = /\( *(-?[0-9]*(?:\.[0-9]*)?), *(-?[0-9]*(?:\.[0-9]*)?) *\)/g; + gt.pointAria = { + enabled: true, + label: (p) => `point at ${p.X()}, ${p.Y()}`, + roledescription: 'point', + live: 'assertive', + atomic: true + }; + // This returns true if the points p1, p2, and p3 are colinear. // Note that p1 must be an array of two numbers, and p2 and p3 must be JSXGraph points. gt.areColinear = (p1, p2, p3) => { @@ -913,6 +929,7 @@ window.graphTool = (containerId, options) => { const point = gt.board.create('point', [gt.snapRound(x, gt.snapSizeX), gt.snapRound(y, gt.snapSizeY)], { snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, + aria: gt.pointAria, ...gt.definingPointAttributes() }); point.setAttribute({ snapToGrid: true }); @@ -1514,12 +1531,8 @@ window.graphTool = (containerId, options) => { helpText() { return (gt.selectedObj && gt.selectedObj.supportsSolidDash) || (gt.activeTool && gt.activeTool.supportsSolidDash) - ? 'Use the ' + - '\\(\\rule[3px]{34px}{2px}\\) or ' + - '\\(\\rule[3px]{3px}{2px}' + - '\\hspace{4px}\\rule[3px]{4px}{2px}'.repeat(3) + - '\\hspace{4px}\\rule[3px]{3px}{2px}\\)' + - ' button or type s or d to make the selected object solid or dashed.' + ? 'Use the solid line or dashed line buttons or type s or d to ' + + 'make the selected object solid or dashed.' : ''; } } diff --git a/htdocs/js/GraphTool/intervaltools.js b/htdocs/js/GraphTool/intervaltools.js index 8fa9a720cb..9946f7ae11 100644 --- a/htdocs/js/GraphTool/intervaltools.js +++ b/htdocs/js/GraphTool/intervaltools.js @@ -43,7 +43,41 @@ 0 ] ], - { fixed: true, highlight: false, strokeColor: gt.color.curve, strokeWidth: 4 } + { + fixed: true, + highlight: false, + strokeColor: gt.color.curve, + strokeWidth: 4, + tabindex: '', + aria: { + enabled: true, + label: () => { + const left = point1.X() < point2.X() ? point1 : point2; + const right = point1.X() < point2.X() ? point2 : point1; + const leftIsInf = gt.isNegInfX(left.X()); + const rightIsInf = gt.isPosInfX(right.X()); + return ( + 'interval that starts at ' + + (leftIsInf + ? '' + : left.getAttribute('fillColor') === 'transparent' + ? 'but does not include ' + : 'and includes ') + + (leftIsInf ? 'negative infinity' : left.X()) + + ' and ends at ' + + (rightIsInf + ? '' + : right.getAttribute('fillColor') === 'transparent' + ? 'but does not include ' + : 'and includes ') + + (rightIsInf ? 'infinity' : right.X()) + ); + }, + roledescription: 'interval', + live: 'assertive', + atomic: true + } + } ); // Redefine the segment's hasPoint method to return true if either of the end points has the @@ -310,7 +344,9 @@ strokeWidth: 4, strokeColor: 'transparent', highlight: false, - lastArrow: { type: 2, size: 4 } + lastArrow: { type: 2, size: 4 }, + tabindex: '', + aria: { enabled: true, hidden: true, live: 'off' } } ); this.definingPts[index].arrow.rendNodeTriangleEnd.setAttribute('fill', gt.color.curve); @@ -361,7 +397,20 @@ snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, ...this.definingPointAttributes(), - ...this.maybeBracketAttributes() + ...this.maybeBracketAttributes(), + aria: { + enabled: true, + label: (p) => + gt.isNegInfX(p.X()) + ? 'end point negative infinity' + : gt.isPosInfX(p.X()) + ? 'end point infinity' + : (p.getAttribute('fillColor') === 'transparent' ? 'excluded' : 'included') + + ` end point ${p.X()}`, + roledescription: 'end point', + live: 'assertive', + atomic: true + } }); point.setAttribute({ snapToGrid: true }); @@ -407,7 +456,9 @@ display: 'internal', fixed: true, strokeColor: gt.color.focusCurve, - highlightStrokeColor: gt.color.pointHighlightDarker + highlightStrokeColor: gt.color.pointHighlightDarker, + tabindex: '', + aria: { enabled: true, hidden: true, live: 'off' } } ); @@ -507,9 +558,22 @@ snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false, - ...gt.graphObjectTypes.interval.maybeBracketAttributes() + ...gt.graphObjectTypes.interval.maybeBracketAttributes(), + tabindex: '', + aria: { + enabled: true, + label: (p) => + gt.isNegInfX(p.X()) + ? 'end point negative infinity' + : gt.isPosInfX(p.X()) + ? 'end point infinity' + : (p.getAttribute('fillColor') === 'transparent' ? 'excluded' : 'included') + + ` end point ${p.X()}`, + roledescription: 'end point', + live: 'assertive', + atomic: true + } }); - this.point1.setAttribute({ fixed: true }); if (gt.options.useBracketEnds) { const point = this.point1; @@ -540,7 +604,9 @@ fixed: true, highlight: false, strokeColor: gt.color.underConstructionFixed, - cssStyle: 'cursor:none;font-weight:900' + cssStyle: 'cursor:none;font-weight:900', + tabindex: '', + aria: { enabled: true, hidden: true, live: 'off' } } ); } @@ -654,7 +720,22 @@ fillColor: gt.toolTypes.IncludeExcludePointTool.include ? gt.color.underConstruction : 'transparent', - ...gt.graphObjectTypes.interval.maybeBracketAttributes() + ...gt.graphObjectTypes.interval.maybeBracketAttributes(), + tabindex: 0, + aria: { + enabled: true, + label: (p) => + gt.isNegInfX(p.X()) + ? 'end point negative infinity' + : gt.isPosInfX(p.X()) + ? 'end point infinity' + : (p.getAttribute('fillColor') === 'transparent' + ? 'excluded' + : 'included') + ` end point ${p.X()}`, + roledescription: 'end point', + live: 'assertive', + atomic: true + } }); if (gt.options.useBracketEnds) { @@ -684,7 +765,9 @@ fixed: true, highlight: false, strokeColor: gt.color.underConstruction, - cssStyle: 'cursor:none;font-weight:900' + cssStyle: 'cursor:none;font-weight:900', + tabindex: '', + aria: { enabled: true, hidden: true, live: 'off' } } ); } @@ -730,7 +813,42 @@ fixed: true, strokeWidth: 5, strokeColor: gt.color.underConstruction, - highlight: false + highlight: false, + tabindex: '', + aria: { + enabled: true, + label: () => { + const left = + this.point1 && this.point1.X() < this.hlObjs.hl_point.X() + ? this.point1 + : this.hlObjs.hl_point; + const right = + !this.point1 || this.point1.X() < this.hlObjs.hl_point.X() + ? this.hlObjs.hl_point + : this.point1; + const leftIsInf = gt.isNegInfX(left.X()); + const rightIsInf = gt.isPosInfX(right.X()); + return ( + 'interval that starts at ' + + (leftIsInf + ? '' + : left.getAttribute('fillColor') === 'transparent' + ? 'but does not include ' + : 'and includes ') + + (leftIsInf ? 'negative infinity' : left.X()) + + ' and ends at ' + + (rightIsInf + ? '' + : right.getAttribute('fillColor') === 'transparent' + ? 'but does not include ' + : 'and includes ') + + (rightIsInf ? 'infinity' : right.X()) + ); + }, + roledescription: 'interval', + live: 'assertive', + atomic: true + } } ); // The default layer for lines (of which arrows are a part) is 7. @@ -778,7 +896,9 @@ strokeWidth: 5, strokeColor: 'transparent', highlight: false, - lastArrow: { type: 2, size: 4 } + lastArrow: { type: 2, size: 4 }, + tabindex: '', + aria: { enabled: true, hidden: true, live: 'off' } } ); this.hlObjs.hl_arrow.rendNodeTriangleEnd.setAttribute( diff --git a/htdocs/js/GraphTool/linetool.js b/htdocs/js/GraphTool/linetool.js index 9e97d1a3c3..6660399315 100644 --- a/htdocs/js/GraphTool/linetool.js +++ b/htdocs/js/GraphTool/linetool.js @@ -16,9 +16,19 @@ fixed: true, highlight: false, strokeColor: gt.color.curve, - dash: solid ? 0 : 2 + dash: solid ? 0 : 2, + tabindex: '' }) ); + this.baseObj.setAttribute({ + aria: { + enabled: true, + label: this.constructor.ariaLabel, + roledescription: this.constructor.strId, + live: 'assertive', + atomic: true + } + }); this.definingPts.push(point1, point2); this.focusPoint = point1; } @@ -38,7 +48,7 @@ return gt.sign(JXG.Math.innerProduct(point, this.baseObj.stdform)); } - hasPoint(point) { + onBoundary(point) { return ( Math.abs(JXG.Math.innerProduct(point, this.baseObj.stdform)) / Math.sqrt(this.baseObj.stdform[1] ** 2 + this.baseObj.stdform[2] ** 2) < @@ -46,6 +56,13 @@ ); } + static ariaLabel(l) { + return ( + (l.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` line through ${l.point1.X()}, ${l.point1.Y()}, and ${l.point2.X()}, ${l.point2.Y()}` + ); + } + static restore(string) { let pointData = gt.pointRegexp.exec(string); const points = []; @@ -115,7 +132,9 @@ highlight: false, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, - withLabel: false + withLabel: false, + tabindex: 0, + aria: gt.pointAria }); this.hlObjs.hl_point.rendNode.focus(); } @@ -128,7 +147,15 @@ fixed: true, strokeColor: gt.color.underConstruction, highlight: false, - dash: gt.drawSolid ? 0 : 2 + dash: gt.drawSolid ? 0 : 2, + tabindex: '', + aria: { + enabled: true, + label: gt.graphObjectTypes[this.object].ariaLabel, + roledescription: this.object, + live: 'assertive', + atomic: true + } }); } @@ -151,7 +178,9 @@ highlight: false, snapToGrid: true, snapSizeX: gt.snapSizeX, - snapSizeY: gt.snapSizeY + snapSizeY: gt.snapSizeY, + tabindex: '', + aria: gt.pointAria }); this.point1.setAttribute({ fixed: true }); diff --git a/htdocs/js/GraphTool/parabolatool.js b/htdocs/js/GraphTool/parabolatool.js index e0d65cce5c..b8a9ea6df9 100644 --- a/htdocs/js/GraphTool/parabolatool.js +++ b/htdocs/js/GraphTool/parabolatool.js @@ -73,7 +73,18 @@ strokeWidth: 2, highlight: false, strokeColor: color ? color : gt.color.underConstruction, - dash: solid ? 0 : 2 + dash: solid ? 0 : 2, + tabindex: '', + aria: { + enabled: true, + label: (p) => + (p.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` vertical parabola with vertex ${vertex.X()}, ${vertex.Y()} and ` + + `passing through the point ${point.X()}, ${point.Y()}`, + roledescription: 'vertical parabola', + live: 'assertive', + atomic: true + } } ); else @@ -91,7 +102,18 @@ strokeWidth: 2, highlight: false, strokeColor: color ? color : gt.color.underConstruction, - dash: solid ? 0 : 2 + dash: solid ? 0 : 2, + tabindex: '', + aria: { + enabled: true, + label: (p) => + (p.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` horizontal parabola with vertex ${vertex.X()}, ${vertex.Y()} and ` + + `passing through the point ${point.X()}, ${point.Y()}`, + roledescription: 'horizontal parabola', + live: 'assertive', + atomic: true + } } ); } @@ -161,7 +183,9 @@ snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, highlight: false, - withLabel: false + withLabel: false, + tabindex: 0, + aria: gt.pointAria }); this.hlObjs.hl_point.rendNode.focus(); } @@ -197,7 +221,9 @@ highlight: false, snapToGrid: true, snapSizeX: gt.snapSizeX, - snapSizeY: gt.snapSizeY + snapSizeY: gt.snapSizeY, + tabindex: '', + aria: gt.pointAria }); this.vertex.setAttribute({ fixed: true }); diff --git a/htdocs/js/GraphTool/pointtool.js b/htdocs/js/GraphTool/pointtool.js index eea92d4fde..4b76c39200 100644 --- a/htdocs/js/GraphTool/pointtool.js +++ b/htdocs/js/GraphTool/pointtool.js @@ -23,7 +23,8 @@ fixed: gt.isStatic, highlightStrokeColor: gt.color.underConstruction, highlightFillColor: gt.color.pointHighlight, - tabindex: gt.isStatic ? -1 : 0 + tabindex: gt.isStatic ? -1 : 0, + aria: gt.pointAria }) ); @@ -160,7 +161,9 @@ highlight: false, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, - withLabel: false + withLabel: false, + tabindex: 0, + aria: gt.pointAria }); this.hlObjs.hl_point.rendNode.focus(); } diff --git a/htdocs/js/GraphTool/quadratictool.js b/htdocs/js/GraphTool/quadratictool.js index bef7eb2893..c25355f5cd 100644 --- a/htdocs/js/GraphTool/quadratictool.js +++ b/htdocs/js/GraphTool/quadratictool.js @@ -82,7 +82,18 @@ strokeWidth: 2, highlight: false, strokeColor: color ? color : gt.color.underConstruction, - dash: solid ? 0 : 2 + dash: solid ? 0 : 2, + tabindex: '', + aria: { + enabled: true, + label: (q) => + (q.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` quadratic passing through ${point1.X()}, ${point1.Y()}, and ` + + `${point2.X()}, ${point2.Y()}, and ${point3.X()}, ${point3.Y()}`, + roledescription: 'quadratic', + live: 'assertive', + atomic: true + } } ); } @@ -133,7 +144,9 @@ size: 2, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, - withLabel: false + withLabel: false, + tabindex: '', + aria: gt.pointAria } ); point.setAttribute({ snapToGrid: true }); @@ -311,7 +324,9 @@ snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, highlight: false, - withLabel: false + withLabel: false, + tabindex: 0, + aria: gt.pointAria }); this.hlObjs.hl_point.rendNode.focus(); } @@ -343,7 +358,18 @@ fixed: true, strokeColor: gt.color.underConstruction, highlight: false, - dash: gt.drawSolid ? 0 : 2 + dash: gt.drawSolid ? 0 : 2, + tabindex: '', + aria: { + enabled: true, + label: (l) => + (l.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` line through ${l.point1.X()}, ${l.point1.Y()}, ` + + `and ${l.point2.X()}, ${l.point2.Y()}`, + roledescription: 'line', + live: 'assertive', + atomic: true + } }); } diff --git a/htdocs/js/GraphTool/quadrilateral.js b/htdocs/js/GraphTool/quadrilateral.js index b9211b9680..4ad9cf554f 100644 --- a/htdocs/js/GraphTool/quadrilateral.js +++ b/htdocs/js/GraphTool/quadrilateral.js @@ -94,7 +94,7 @@ return -1; } - onBoundary(point, aVal, _from) { + onBoundary(point, aVal) { if (this.fillCmp(point) != aVal) return true; for (const border of this.baseObj.borders) { @@ -284,7 +284,9 @@ size: 2, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, - withLabel: false + withLabel: false, + tabindex: '', + aria: gt.pointAria } ); point.setAttribute({ snapToGrid: true }); @@ -344,7 +346,9 @@ highlight: false, snapToGrid: true, snapSizeX: gt.snapSizeX, - snapSizeY: gt.snapSizeY + snapSizeY: gt.snapSizeY, + tabindex: '', + aria: gt.pointAria }); this.point1.setAttribute({ fixed: true }); @@ -439,7 +443,7 @@ if (count == 0) { if (vDir != 0) ++times; count = times; - [hDir, vDir] = [!!hDir ? 0 : -vDir, !!vDir ? 0 : hDir]; + [hDir, vDir] = [hDir ? 0 : -vDir, vDir ? 0 : hDir]; } newX += hDir * gt.snapSizeX; newY += vDir * gt.snapSizeY; @@ -544,7 +548,9 @@ highlight: false, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, - withLabel: false + withLabel: false, + tabindex: 0, + aria: gt.pointAria }); this.hlObjs.hl_point.rendNode.focus(); } @@ -584,7 +590,18 @@ highlight: false, dash: gt.drawSolid ? 0 : 2, straightFirst: false, - straightLast: false + straightLast: false, + tabindex: '', + aria: { + enabled: true, + label: (l) => + (l.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` line segment between ${l.point1.X()}, ${l.point1.Y()} ` + + `and ${l.point2.X()}, ${l.point2.Y()}`, + roledescription: 'line', + live: 'assertive', + atomic: true + } }); } diff --git a/htdocs/js/GraphTool/segments.js b/htdocs/js/GraphTool/segments.js index 48e2c2fefc..ed472d6462 100644 --- a/htdocs/js/GraphTool/segments.js +++ b/htdocs/js/GraphTool/segments.js @@ -1,4 +1,4 @@ -/* global graphTool */ +/* global graphTool, JXG */ 'use strict'; @@ -59,6 +59,13 @@ 0.5 / Math.sqrt(gt.board.unitX * gt.board.unitY) ); } + + static ariaLabel(l) { + return ( + (l.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` segment between ${l.point1.X()}, ${l.point1.Y()} and ${l.point2.X()}, ${l.point2.Y()}` + ); + } }; }, @@ -97,6 +104,14 @@ super(point1, point2, solid); this.baseObj.setArrow(false, { type: 1, size: 4 }); } + + static ariaLabel(l) { + return ( + (l.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` vector with initial point ${l.point1.X()}, ${l.point1.Y()} ` + + `and terminal point ${l.point2.X()}, ${l.point2.Y()}` + ); + } }; }, diff --git a/htdocs/js/GraphTool/sinewavetool.js b/htdocs/js/GraphTool/sinewavetool.js index a46e597a13..b7ec48d607 100644 --- a/htdocs/js/GraphTool/sinewavetool.js +++ b/htdocs/js/GraphTool/sinewavetool.js @@ -104,7 +104,25 @@ strokeWidth: 2, highlight: false, strokeColor: color ? color : gt.color.underConstruction, - dash: solid ? 0 : 2 + dash: solid ? 0 : 2, + tabindex: '', + aria: { + enabled: true, + label: (s) => + (s.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` sine wave shifted ${Math.abs(point.X())} units to the ${ + point.X() < 0 ? 'left' : 'right' + }, shifted ${Math.abs(point.Y())} units ${ + point.X() < 0 ? 'downward' : 'upward' + }, with period ${ + (2 * Math.PI) / Math.abs(period()) + }, with amplitude ${Math.abs(amplitude())}` + + (period() < 0 ? ', sine wave horizontally reflected' : '') + + (amplitude() < 0 ? ', sine wave vertically reflected' : ''), + roledescription: 'sine wave', + live: 'assertive', + atomic: true + } } ); } @@ -199,7 +217,24 @@ size: 2, snapSizeX: periodPoint ? 1e-10 : gt.snapSizeX, snapSizeY: shiftPoint && !periodPoint ? 1e-10 : gt.snapSizeY, - withLabel: false + withLabel: false, + tabindex: '', + aria: { + enabled: true, + label: (p) => + periodPoint + ? `sine wave amplitude ${Math.abs(p.Y() - shiftPoint.Y())}` + + (p.Y() > shiftPoint.Y() ? '' : ', sine wave vertically reflected') + : shiftPoint + ? `sine wave period ${Math.abs(p.X() - shiftPoint.X())}` + + (p.X() > shiftPoint.X() ? '' : ', sine wave horizontally reflected') + : `sine wave shifted ${Math.abs(p.X())} units to the ${ + p.X() < 0 ? 'left' : 'right' + }, and shifted ${Math.abs(p.Y())} units ${p.Y() < 0 ? 'downward' : 'upward'}`, + roledescription: () => (periodPoint ? 'amplitude' : shiftPoint ? 'period' : 'shift'), + live: 'assertive', + atomic: true + } }); point.setAttribute({ snapToGrid: true }); @@ -376,7 +411,29 @@ snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, highlight: false, - withLabel: false + withLabel: false, + tabindex: 0, + aria: { + enabled: true, + label: (p) => + this.periodPoint + ? `sine wave amplitude ${Math.abs(p.Y() - this.shiftPoint.Y())}` + + (p.Y() > this.shiftPoint.Y() ? '' : ', sine wave vertically reflected') + : this.shiftPoint + ? `sine wave period ${Math.abs(p.X() - this.shiftPoint.X())}` + + (p.X() > this.shiftPoint.X() + ? '' + : ', sine wave horizontally reflected') + : `sine wave shifted ${Math.abs(p.X())} units to the ${ + p.X() < 0 ? 'left' : 'right' + }, and shifted ${Math.abs(p.Y())} units ${ + p.Y() < 0 ? 'downward' : 'upward' + }`, + roledescription: () => + this.periodPoint ? 'amplitude' : this.shiftPoint ? 'period' : 'shift', + live: 'assertive', + atomic: true + } }); this.hlObjs.hl_point.rendNode.focus(); } diff --git a/htdocs/js/GraphTool/triangle.js b/htdocs/js/GraphTool/triangle.js index 757b5eb665..a8e95948b3 100644 --- a/htdocs/js/GraphTool/triangle.js +++ b/htdocs/js/GraphTool/triangle.js @@ -82,7 +82,7 @@ return -1; } - onBoundary(point, aVal, _from) { + onBoundary(point, aVal) { if (this.fillCmp(point) != aVal) return true; for (const border of this.baseObj.borders) { @@ -122,7 +122,7 @@ } static createPolygon(points, solid, color) { - return gt.board.create('polygon', points, { + const polygon = gt.board.create('polygon', points, { highlight: false, fillOpacity: 0, fixed: true, @@ -132,8 +132,31 @@ fixed: true, strokeColor: color ? color : gt.color.underConstruction, dash: solid ? 0 : 2 + }, + tabindex: '', + aria: { + enabled: true, + label: (t) => + (t.borders[0].getAttribute('dash') == 0 ? 'solid ' : 'dashed ') + + (t.vertices.length === 4 + ? 'triangle' + : t.vertices.length == 5 + ? 'quadrilateral' + : 'polygon') + + ' with vertices ' + + t.vertices + .slice(0, -1) + .map((p) => `${p.X()}, ${p.Y()}`) + .join(', and '), + roledescription: 'polygon', + live: 'assertive', + atomic: true } }); + for (const border of polygon.borders) { + border.setAttribute({ tabindex: '', aria: { enabled: true, hidden: true, live: 'off' } }); + } + return polygon; } // Prevent a point from being moved off the board by a drag. If one or two other points are @@ -227,7 +250,9 @@ size: 2, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, - withLabel: false + withLabel: false, + tabindex: '', + aria: gt.pointAria } ); point.setAttribute({ snapToGrid: true }); @@ -284,7 +309,9 @@ highlight: false, snapToGrid: true, snapSizeX: gt.snapSizeX, - snapSizeY: gt.snapSizeY + snapSizeY: gt.snapSizeY, + tabindex: '', + aria: gt.pointAria }); this.point1.setAttribute({ fixed: true }); @@ -414,7 +441,9 @@ highlight: false, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, - withLabel: false + withLabel: false, + tabindex: 0, + aria: gt.pointAria }); this.hlObjs.hl_point.rendNode.focus(); } @@ -446,7 +475,18 @@ highlight: false, dash: gt.drawSolid ? 0 : 2, straightFirst: false, - straightLast: false + straightLast: false, + tabindex: '', + aria: { + enabled: true, + label: (l) => + (l.getAttribute('dash') == 0 ? 'solid' : 'dashed') + + ` line segment between ${l.point1.X()}, ${l.point1.Y()} ` + + `and ${l.point2.X()}, ${l.point2.Y()}`, + roledescription: 'line', + live: 'assertive', + atomic: true + } }); }