diff --git a/src/accessibility/describe.js b/src/accessibility/describe.js index 1d5511cbdf..2d9bce0ee8 100644 --- a/src/accessibility/describe.js +++ b/src/accessibility/describe.js @@ -19,12 +19,24 @@ function describe(p5, fn) { * * The first parameter, `text`, is the description of the canvas. * - * The second parameter, `display`, is optional. It determines how the - * description is displayed. If `LABEL` is passed, as in - * `describe('A description.', LABEL)`, the description will be visible in - * a div element next to the canvas. If `FALLBACK` is passed, as in - * `describe('A description.', FALLBACK)`, the description will only be - * visible to screen readers. This is the default mode. + * The second parameter, `langOrDisplay`, is optional. It can either + * determine the language of the description or how the description + * is displayed. The description can be displayed with either `LABEL` or `FALLBACK`. + * - If a lang is passed, as in `describe('A description.', 'en')`, + * the description will be read by the screen reader using the + * specified language's voice. The screen reader must have the specified + * language's voice installed for this to work. + * - If `LABEL` is passed, as in `describe('A description.', LABEL)`, + * the description will be visible in a div element next to the canvas. + * - If `FALLBACK` is passed, as in `describe('A description.', FALLBACK)`, + * the description will only be visible to screen readers. FALLBACK is + * the default mode. + * + * The third parameter, `display`, is optional but is only used if the second + * parameter is lang, as in the language of the description, and the user wants + * to determine how the description is displayed as well. In this case, they can + * pass either `LABEL` or `FALLBACK` as the third parameter. + * * * Read * Writing accessible canvas descriptions @@ -32,6 +44,7 @@ function describe(p5, fn) { * * @method describe * @param {String} text description of the canvas. + * @param {(FALLBACK|LABEL|String)} [langOrDisplay] valid lang attribute or either LABEL or FALLBACK. * @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK. * * @example @@ -110,11 +123,14 @@ function describe(p5, fn) { * describe(`A green circle at (${x}, 50) moves from left to right on a gray square.`, LABEL); * } */ - fn.describe = function (text, display) { + fn.describe = function (text, langOrDisplay, display) { // p5._validateParameters('describe', arguments); if (typeof text !== 'string') { return; } + const parsedOptions = _parseOptions(this, langOrDisplay, display); + display = parsedOptions.display; + const { lang } = parsedOptions; const cnvId = this.canvas.id; //calls function that adds punctuation for better screen reading text = _descriptionText(text); @@ -122,6 +138,7 @@ function describe(p5, fn) { if (!this.dummyDOM) { this.dummyDOM = document.getElementById(cnvId).parentNode; } + //check if html structure for description is ready if (!this.descriptions) { this.descriptions = {}; } @@ -150,6 +167,7 @@ function describe(p5, fn) { this._describeHTML('label', text); } } + _setDescriptionLang(this, lang); }; /** @@ -161,15 +179,22 @@ function describe(p5, fn) { * The first parameter, `name`, is the name of the element. * * The second parameter, `text`, is the description of the element. - * - * The third parameter, `display`, is optional. It determines how the - * description is displayed. If `LABEL` is passed, as in - * `describe('A description.', LABEL)`, the description will be visible in - * a div element next to the canvas. Using `LABEL` creates unhelpful - * duplicates for screen readers. Only use `LABEL` during development. If - * `FALLBACK` is passed, as in `describe('A description.', FALLBACK)`, the - * description will only be visible to screen readers. This is the default - * mode. + * + * The third parameter, `langOrDisplay`, is optional. It can either + * determine the language of the description or how the description + * is displayed. The description can be displayed with either `LABEL` + * or `FALLBACK` + * + * The fourth parameter, `display`, is optional but is only used if the third + * parameter is lang, as in the language of the description, and the user wants + * to determine how the description is displayed as well. In this case, they can + * pass either `LABEL` or `FALLBACK` as the fourth parameter. + * + * If `LABEL` is passed, as in `describe('A description.', LABEL)`, + * the description will be visible in a div element next to the canvas. Using + * `LABEL` creates unhelpful duplicates for screen readers. Only use `LABEL` + * during development. If `FALLBACK` is passed, as in `describe('A description.', FALLBACK)`, + * the description will only be visible to screen readers. This is the default mode. * * Read * Writing accessible canvas descriptions @@ -178,8 +203,8 @@ function describe(p5, fn) { * @method describeElement * @param {String} name name of the element. * @param {String} text description of the element. - * @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK. - * + * @param {(FALLBACK|LABEL|String)} [langOrDisplay] valid lang attribute or either LABEL or FALLBACK. + * @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK. * @example * function setup() { * background('pink'); @@ -229,11 +254,14 @@ function describe(p5, fn) { * } */ - fn.describeElement = function (name, text, display) { + fn.describeElement = function (name, text, langOrDisplay, display) { // p5._validateParameters('describeElement', arguments); if (typeof text !== 'string' || typeof name !== 'string') { return; } + const parsedOptions = _parseOptions(this, langOrDisplay, display); + display = parsedOptions.display; + const { lang } = parsedOptions; const cnvId = this.canvas.id; //calls function that adds punctuation for better screen reading text = _descriptionText(text); @@ -242,8 +270,11 @@ function describe(p5, fn) { //remove any special characters from name to use it as html id name = name.replace(/[^a-zA-Z0-9]/g, ''); + // Inject lang attribute + let langAttr = typeof lang === 'string' ? ` lang="${lang}"` : ''; + //store element description - let inner = `${elementName}${text}`; + let inner = `${elementName}${text}`; //if there is no dummyDOM if (!this.dummyDOM) { this.dummyDOM = document.getElementById(cnvId).parentNode; @@ -289,6 +320,69 @@ function describe(p5, fn) { * */ + // Helps parse the optional parameters regardless of order + function _parseOptions(pInst, langOrDisplay, display) { + // Check if 2nd parameter is an object + if (typeof langOrDisplay === 'object' && langOrDisplay !== null) { + let finalDisplay = display; + if (finalDisplay === undefined) { + finalDisplay = langOrDisplay.display; + } + return { + display: finalDisplay, + lang: langOrDisplay.lang + }; + } + + // Check if langOrDisplay is display (LABEL or FALLBACK) + // Example: describe describe('text', LABEL) + // If 3 parameters: describe('text', LABEL, 'es') + if (langOrDisplay === pInst.LABEL || langOrDisplay === pInst.FALLBACK) { + let finalLang = undefined; + // Check if 3rd parameter exists and is a string (lang attribute) + if (typeof display === 'string') { + finalLang = display; + } + else if (typeof display === 'object' && display !== null) { + finalLang = display.lang; + } + return {display: langOrDisplay, lang: finalLang}; + } + + // langOrDisplay is lang + // Example: describe('text', 'es') + // If 3 parameters: describe('text', 'es', LABEL) + return {display: display, lang: langOrDisplay}; + } + + function _setDescriptionLang(pInst, lang) { + const canvas = pInst.canvas.elt || pInst.elt || pInst.canvas; + if (typeof lang === 'string') { + canvas.setAttribute('lang', lang); + } + else { + canvas.removeAttribute('lang'); + } + + if (pInst.descriptions.fallback) { + if (typeof lang === 'string') { + pInst.descriptions.fallback.setAttribute('lang', lang); + } + else { + pInst.descriptions.fallback.removeAttribute('lang'); + } + } + + if (pInst.descriptions.label) { + if (typeof lang === 'string') { + pInst.descriptions.label.setAttribute('lang', lang); + } + else { + pInst.descriptions.label.removeAttribute('lang'); + } + } + } + // check that text is not LABEL or FALLBACK and ensure text ends with punctuation mark function _descriptionText(text) { if (text === 'label' || text === 'fallback') { diff --git a/test/manual-test-examples/accessibility/describe/index.html b/test/manual-test-examples/accessibility/describe/index.html new file mode 100644 index 0000000000..586c215bae --- /dev/null +++ b/test/manual-test-examples/accessibility/describe/index.html @@ -0,0 +1,46 @@ + + + + + + + Accessibility: describe language + + + + + + + +
+

Accessibility: describe() language test

+
+ +
+

Read each region with a screen reader. Compare regions without a lang attribute with those that have one.

+ +
+

this is a test

+
+
+

this is a test

+
+
+

esto es una prueba

+
+
+

esto es una prueba

+
+
+

Cái này là bài thi

+
+
+

Cái này là bài thi

+
+ +

p5.js generated descriptions

+

Read the descriptions generated by the canvases below. The visible LABEL descriptions are included for inspection.

+
+ + + diff --git a/test/manual-test-examples/accessibility/describe/sketch.js b/test/manual-test-examples/accessibility/describe/sketch.js new file mode 100644 index 0000000000..d0a992a505 --- /dev/null +++ b/test/manual-test-examples/accessibility/describe/sketch.js @@ -0,0 +1,40 @@ +function setup() { + // Uncomment any of the following lines to test + + createCanvas(400, 400); + + // Text only (fallback is the default) + // describe('Cái này là bài thi'); + + // Text and display + // describe('Esto es una prueba', LABEL); + + // Text and lang + //describe('Esto es una prueba', 'es'); + + // Text, lang, and display + // describe('Cái này là bài thi', 'vi', LABEL); + + // Text, display, and lang + // describe('यह टेस्ट है', LABEL, 'hi'); + + + + // Name and text only (fallback is the default) + // describeElement('a', 'Cái này là bài thi'); + + // Name, text, and display + // describeElement('b', 'Esto es una prueba', LABEL); + + // Name, text, and lang + // describeElement('c', 'Esto es una prueba', 'es'); + + // Name, text, lang, and display + // describeElement('d', 'Cái này là bài thi', 'vi', LABEL); + + // Name, text, display, and lang + // describeElement('e', 'यह टेस्ट है', LABEL, 'hi'); + + fill('blue'); + circle(200, 150, 100); +} \ No newline at end of file diff --git a/test/unit/accessibility/describe.js b/test/unit/accessibility/describe.js index 427aa16ce0..b0a583a680 100644 --- a/test/unit/accessibility/describe.js +++ b/test/unit/accessibility/describe.js @@ -11,6 +11,16 @@ suite('describe', function () { mockP5Prototype.FALLBACK = 'fallback'; }); + beforeEach(function () { + document.querySelectorAll('[id^="' + myID + '_"]').forEach(element => { + element.remove(); + }); + mockP5Prototype.elt.innerHTML = ''; + mockP5Prototype.elt.removeAttribute('lang'); + mockP5Prototype.dummyDOM = undefined; + mockP5Prototype.descriptions = undefined; + }); + suite('p5.prototype.describe', function () { test('should be a function', function () { assert.ok(mockP5Prototype.describe); @@ -27,12 +37,36 @@ suite('describe', function () { ); }); - test('should create description as fallback', function () { + test('should create description as FALLBACK', function () { mockP5Prototype.describe('a'); let actual = document.getElementById(myID + '_fallbackDesc'); assert.deepEqual(actual.innerHTML, 'a.'); }); + test('should support text and lang', function () { + mockP5Prototype.describe('Chào các bạn', 'vi'); + let actual = document.getElementById(myID + '_fallbackDesc'); + assert.deepEqual(actual.innerHTML, 'Chào các bạn.'); + assert.deepEqual(actual.getAttribute('lang'), 'vi'); + assert.deepEqual(mockP5Prototype.elt.getAttribute('lang'), 'vi'); + assert.isNull(document.getElementById(myID + '_Label')); + }); + + test('should support text lang and display', function () { + mockP5Prototype.describe('Chào các bạn', 'vi', mockP5Prototype.FALLBACK); + let actual = document.getElementById(myID + '_fallbackDesc'); + assert.deepEqual(actual.innerHTML, 'Chào các bạn.'); + assert.deepEqual(actual.getAttribute('lang'), 'vi'); + assert.isNull(document.getElementById(myID + '_Label')); + }); + + test('should support text display and lang', function () { + mockP5Prototype.describe('Chào các bạn', mockP5Prototype.LABEL, 'vi'); + let actual = document.getElementById(myID + '_labelDesc'); + assert.deepEqual(actual.innerHTML, 'Chào các bạn.'); + assert.deepEqual(actual.getAttribute('lang'), 'vi'); + }); + test('should not add extra period if string ends in "."', function () { mockP5Prototype.describe('A.'); let actual = document.getElementById(myID + '_fallbackDesc'); @@ -104,6 +138,26 @@ suite('describe', function () { assert.deepEqual(actual, 'az:b.'); }); + test('should support element text and lang', function () { + mockP5Prototype.describeElement('aj', 'Chào các bạn', 'vi'); + let actual = document.getElementById(myID + '_fte_aj').innerHTML; + assert.deepEqual(actual, 'aj:Chào các bạn.' + ); + }); + + test('should support element text lang and display', function () { + mockP5Prototype.describeElement('al', 'Chào các bạn', 'vi', mockP5Prototype.FALLBACK); + let actual = document.getElementById(myID + '_fte_al').innerHTML; + assert.deepEqual(actual, 'al:Chào các bạn.' + ); + }); + + test('should support element text display and lang', function () { + mockP5Prototype.describeElement('am', 'Chào các bạn', mockP5Prototype.LABEL, 'vi'); + let actual = document.getElementById(myID + '_lte_am').innerHTML; + assert.deepEqual(actual, 'am:Chào các bạn.'); + }); + test('should not add extra ":" if element name ends in colon', function () { mockP5Prototype.describeElement('ab:', 'b.'); let actual = document.getElementById(myID + '_fte_ab').innerHTML;