From d5c31033bc4d60dfd1c0948979278a016bd0ebf5 Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Wed, 26 Aug 2026 01:12:15 -0700 Subject: [PATCH 1/9] Implemented lang parameter for describe functions --- src/accessibility/describe.js | 58 +++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/accessibility/describe.js b/src/accessibility/describe.js index 1d5511cbdf..720fc46d9c 100644 --- a/src/accessibility/describe.js +++ b/src/accessibility/describe.js @@ -19,12 +19,6 @@ 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. * * Read * Writing accessible canvas descriptions @@ -32,7 +26,6 @@ function describe(p5, fn) { * * @method describe * @param {String} text description of the canvas. - * @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK. * * @example * function setup() { @@ -110,11 +103,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 +118,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 +147,7 @@ function describe(p5, fn) { this._describeHTML('label', text); } } + _setDescriptionLang(this, lang); }; /** @@ -162,10 +160,6 @@ function describe(p5, fn) { * * 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 @@ -229,11 +223,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); @@ -281,6 +278,7 @@ function describe(p5, fn) { this._describeElementHTML('label', name, inner); } } + _setDescriptionLang(this, lang); }; /* @@ -289,6 +287,40 @@ function describe(p5, fn) { * */ + function _parseOptions(pInst, langOrDisplay, display) { + if (typeof langOrDisplay === 'object' && langOrDisplay !== null) { + return { + display: display === undefined ? langOrDisplay.display : display, + lang: langOrDisplay.lang + }; + } + if ( + langOrDisplay === pInst.LABEL || + langOrDisplay === pInst.FALLBACK + ) { + return { display: langOrDisplay }; + } + return { display, lang: langOrDisplay }; + } + + function _setDescriptionLang(pInst, lang) { + if (typeof lang !== 'string') { + return; + } + const cnvId = pInst.canvas.id; + const canvas = pInst.canvas.elt || pInst.elt || pInst.canvas; + canvas.setAttribute('lang', lang); + const containers = pInst.dummyDOM.querySelectorAll( + `#${cnvId}${descContainer}, #${cnvId}${labelContainer}` + ); + containers.forEach(container => { + container.setAttribute('lang', lang); + container + .querySelectorAll('p, table, caption, tr, th, td') + .forEach(element => element.setAttribute('lang', lang)); + }); + } + // check that text is not LABEL or FALLBACK and ensure text ends with punctuation mark function _descriptionText(text) { if (text === 'label' || text === 'fallback') { From 91be0c35ff981630377bae368dd8915423f1cb2c Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Wed, 26 Aug 2026 19:24:26 -0700 Subject: [PATCH 2/9] Refactored some files to make it easier to read and made the parameters more beginner friendly --- src/accessibility/describe.js | 110 ++++++++++++++++++++++++++-------- 1 file changed, 85 insertions(+), 25 deletions(-) diff --git a/src/accessibility/describe.js b/src/accessibility/describe.js index 720fc46d9c..8d8faaa5e9 100644 --- a/src/accessibility/describe.js +++ b/src/accessibility/describe.js @@ -19,6 +19,24 @@ function describe(p5, fn) { * * The first parameter, `text`, is the description of the canvas. * + * 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 @@ -26,6 +44,8 @@ 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|String)} [display] either LABEL or FALLBACK. * * @example * function setup() { @@ -159,6 +179,16 @@ 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, `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. * * duplicates for screen readers. Only use `LABEL` during development. If * `FALLBACK` is passed, as in `describe('A description.', FALLBACK)`, the @@ -172,8 +202,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|String)} [display] either LABEL or FALLBACK. * @example * function setup() { * background('pink'); @@ -239,8 +269,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; @@ -278,7 +311,6 @@ function describe(p5, fn) { this._describeElementHTML('label', name, inner); } } - _setDescriptionLang(this, lang); }; /* @@ -288,37 +320,65 @@ function describe(p5, fn) { */ 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: display === undefined ? langOrDisplay.display : display, + display: finalDisplay, lang: langOrDisplay.lang }; } - if ( - langOrDisplay === pInst.LABEL || - langOrDisplay === pInst.FALLBACK - ) { - return { display: langOrDisplay }; + + // 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}; } - return { display, lang: langOrDisplay }; + + // langOrDisplay is lang + // Example: describe('text', 'es') + // If 3 parameters: describe('text', 'es', LABEL) + return {display: display, lang: langOrDisplay}; } function _setDescriptionLang(pInst, lang) { - if (typeof lang !== 'string') { - return; - } - const cnvId = pInst.canvas.id; const canvas = pInst.canvas.elt || pInst.elt || pInst.canvas; - canvas.setAttribute('lang', lang); - const containers = pInst.dummyDOM.querySelectorAll( - `#${cnvId}${descContainer}, #${cnvId}${labelContainer}` - ); - containers.forEach(container => { - container.setAttribute('lang', lang); - container - .querySelectorAll('p, table, caption, tr, th, td') - .forEach(element => element.setAttribute('lang', lang)); - }); + 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 From 771cb45405382669367d31095564a09fb696f1b0 Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Wed, 26 Aug 2026 21:10:21 -0700 Subject: [PATCH 3/9] Tests 5 variations of parameters for describe and describeElement --- test/unit/accessibility/describe.js | 83 ++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/test/unit/accessibility/describe.js b/test/unit/accessibility/describe.js index 427aa16ce0..1721ad3b82 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,51 @@ 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 only', function () { + mockP5Prototype.describe('Chào các bạn'); + let actual = document.getElementById(myID + '_fallbackDesc').innerHTML; + assert.deepEqual(actual, 'Chào các bạn.'); + assert.isNull(document.getElementById(myID + '_Label')); + assert.isNull(document.getElementById(myID + '_fallbackDesc').getAttribute('lang')); + }); + + 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 and display', function () { + mockP5Prototype.describe('Chào các bạn', mockP5Prototype.LABEL); + let actual = document.getElementById(myID + '_labelDesc').innerHTML; + assert.deepEqual(actual, 'Chào các bạn.'); + assert.isNull(document.getElementById(myID + '_labelDesc').getAttribute('lang')); + }); + + 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 +153,38 @@ suite('describe', function () { assert.deepEqual(actual, 'az:b.'); }); + test('should support element text only', function () { + mockP5Prototype.describeElement('ai', 'Chào các bạn'); + let actual = document.getElementById(myID + '_fte_ai').innerHTML; + assert.deepEqual(actual, 'ai:Chào các bạn.'); + }); + + 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 and display', function () { + mockP5Prototype.describeElement('ak', 'Chào các bạn', mockP5Prototype.LABEL); + let actual = document.getElementById(myID + '_lte_ak').innerHTML; + assert.deepEqual(actual, 'ak: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; From 01c3161b21bd9ff53f8c5c48a077954c5f1db8ee Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Wed, 26 Aug 2026 21:13:58 -0700 Subject: [PATCH 4/9] Removed my tests that might have preexisted --- test/unit/accessibility/describe.js | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/test/unit/accessibility/describe.js b/test/unit/accessibility/describe.js index 1721ad3b82..b0a583a680 100644 --- a/test/unit/accessibility/describe.js +++ b/test/unit/accessibility/describe.js @@ -43,14 +43,6 @@ suite('describe', function () { assert.deepEqual(actual.innerHTML, 'a.'); }); - test('should support text only', function () { - mockP5Prototype.describe('Chào các bạn'); - let actual = document.getElementById(myID + '_fallbackDesc').innerHTML; - assert.deepEqual(actual, 'Chào các bạn.'); - assert.isNull(document.getElementById(myID + '_Label')); - assert.isNull(document.getElementById(myID + '_fallbackDesc').getAttribute('lang')); - }); - test('should support text and lang', function () { mockP5Prototype.describe('Chào các bạn', 'vi'); let actual = document.getElementById(myID + '_fallbackDesc'); @@ -60,13 +52,6 @@ suite('describe', function () { assert.isNull(document.getElementById(myID + '_Label')); }); - test('should support text and display', function () { - mockP5Prototype.describe('Chào các bạn', mockP5Prototype.LABEL); - let actual = document.getElementById(myID + '_labelDesc').innerHTML; - assert.deepEqual(actual, 'Chào các bạn.'); - assert.isNull(document.getElementById(myID + '_labelDesc').getAttribute('lang')); - }); - 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'); @@ -153,12 +138,6 @@ suite('describe', function () { assert.deepEqual(actual, 'az:b.'); }); - test('should support element text only', function () { - mockP5Prototype.describeElement('ai', 'Chào các bạn'); - let actual = document.getElementById(myID + '_fte_ai').innerHTML; - assert.deepEqual(actual, 'ai:Chào các bạn.'); - }); - 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; @@ -166,12 +145,6 @@ suite('describe', function () { ); }); - test('should support element text and display', function () { - mockP5Prototype.describeElement('ak', 'Chào các bạn', mockP5Prototype.LABEL); - let actual = document.getElementById(myID + '_lte_ak').innerHTML; - assert.deepEqual(actual, 'ak: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; From 31e0438e6b87468aa80a9e7f324f500883eb38b2 Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Wed, 26 Aug 2026 21:54:44 -0700 Subject: [PATCH 5/9] Manual test pages from the research are added. Also included some tests on canvas when updating the describe functions. --- .../accessibility/describe/index.html | 46 +++++++++++++++++++ .../accessibility/describe/sketch.js | 40 ++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 test/manual-test-examples/accessibility/describe/index.html create mode 100644 test/manual-test-examples/accessibility/describe/sketch.js 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 From 6cfaf8aa892694a52b6c1a4b891c50c0fcd4f9f5 Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Wed, 26 Aug 2026 23:47:35 -0700 Subject: [PATCH 6/9] small JSDoc updates for param and helper function --- src/accessibility/describe.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/accessibility/describe.js b/src/accessibility/describe.js index 8d8faaa5e9..2d9bce0ee8 100644 --- a/src/accessibility/describe.js +++ b/src/accessibility/describe.js @@ -45,7 +45,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|String)} [display] either LABEL or FALLBACK. + * @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK. * * @example * function setup() { @@ -190,10 +190,11 @@ function describe(p5, fn) { * to determine how the description is displayed as well. In this case, they can * pass either `LABEL` or `FALLBACK` as the fourth parameter. * - * 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. + * 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 @@ -203,7 +204,7 @@ function describe(p5, fn) { * @param {String} name name of the element. * @param {String} text description of the element. * @param {(FALLBACK|LABEL|String)} [langOrDisplay] valid lang attribute or either LABEL or FALLBACK. - * @param {(FALLBACK|LABEL|String)} [display] either LABEL or FALLBACK. + * @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK. * @example * function setup() { * background('pink'); @@ -319,6 +320,7 @@ 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) { From bda2d12704310ee8e744798ccc978b2dcf154883 Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Fri, 25 Sep 2026 20:30:27 -0700 Subject: [PATCH 7/9] Modified parameter orders for describe functions and incorporated decorator patterns --- src/accessibility/describe.js | 126 ++++++++++++++++------------------ 1 file changed, 58 insertions(+), 68 deletions(-) diff --git a/src/accessibility/describe.js b/src/accessibility/describe.js index 2d9bce0ee8..a49eae58d8 100644 --- a/src/accessibility/describe.js +++ b/src/accessibility/describe.js @@ -19,23 +19,11 @@ function describe(p5, fn) { * * The first parameter, `text`, is the description of the canvas. * - * 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. + * The second parameter, `display`, is optional and can be `LABEL` or + * `FALLBACK`. The third parameter, `lang`, is optional and sets the language + * of the description for screen readers. However, if only the optional 'lang' + * parameter is passed, it will be treated as the second optional parameter and + * the default option 'FALLBACK' will be used as the display. * * * Read @@ -44,8 +32,8 @@ 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. + * @param {String} [lang] valid lang attribute. * * @example * function setup() { @@ -123,14 +111,11 @@ 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, langOrDisplay, display) { + fn.describe = function (text, display, lang) { // 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); @@ -170,6 +155,40 @@ function describe(p5, fn) { _setDescriptionLang(this, lang); }; + p5.registerDecorator('p5.prototype.describe', function (target) { + return function (text, display, lang) { + // Checks for optional parameters and rearranges if needed + if (arguments.length === 2 && typeof display === 'string') { + // (text, display) + if (display === this.LABEL || display === this.FALLBACK) { + return target.call(this, text, display, undefined); + } + // (text, lang) + return target.call(this, text, undefined, display); + } + _checkDescriptionOptions(this, 'describe', display, lang); + // (text, display, lang) or just (text) + return target.call(this, text, display, lang); + }; + }); + + p5.registerDecorator('p5.prototype.describeElement', function (target) { + return function (name, text, display, lang) { + // Checks for optional parameters and rearranges if needed + if (arguments.length === 3 && typeof display === 'string') { + // (name, text, display) + if (display === this.LABEL || display === this.FALLBACK) { + return target.call(this, name, text, display, undefined); + } + // (name, text, lang) + return target.call(this, name, text, undefined, display); + } + _checkDescriptionOptions(this, 'describeElement', display, lang); + // (name, text, display, lang) or just (name, text) + return target.call(this, name, text, display, lang); + }; + }); + /** * Creates a screen reader-accessible description of elements in the canvas. * @@ -180,15 +199,11 @@ function describe(p5, fn) { * * The second parameter, `text`, is the description of the element. * - * 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. + * The third parameter, `display`, is optional and can be `LABEL` or + * `FALLBACK`. The fourth parameter, `lang`, is optional and sets the language + * of the description for screen readers. However, if only the optional 'lang' + * parameter is passed, it will be treated as the third optional parameter and + * the default option 'FALLBACK' will be used as the display. * * If `LABEL` is passed, as in `describe('A description.', LABEL)`, * the description will be visible in a div element next to the canvas. Using @@ -203,8 +218,8 @@ function describe(p5, fn) { * @method describeElement * @param {String} name name of the element. * @param {String} text description of the element. - * @param {(FALLBACK|LABEL|String)} [langOrDisplay] valid lang attribute or either LABEL or FALLBACK. - * @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK. + * @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK. + * @param {String} [lang] valid lang attribute. * @example * function setup() { * background('pink'); @@ -254,14 +269,11 @@ function describe(p5, fn) { * } */ - fn.describeElement = function (name, text, langOrDisplay, display) { + fn.describeElement = function (name, text, display, lang) { // 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); @@ -312,6 +324,7 @@ function describe(p5, fn) { this._describeElementHTML('label', name, inner); } } + _setDescriptionLang(this, lang); }; /* @@ -320,39 +333,16 @@ 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 - }; - } + function _checkDescriptionOptions(pInst, methodName, display, lang) { + // Ensures display and lang are in the correct orders and are valid + const validDisplay = display === undefined || display === pInst.LABEL || display === pInst.FALLBACK; + const validLanguage = lang === undefined || typeof lang === 'string'; - // 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}; + if (!validDisplay || !validLanguage) { + throw new TypeError( + `${methodName}() expects display (LABEL or FALLBACK) before lang.` + ); } - - // langOrDisplay is lang - // Example: describe('text', 'es') - // If 3 parameters: describe('text', 'es', LABEL) - return {display: display, lang: langOrDisplay}; } function _setDescriptionLang(pInst, lang) { From 2c170b356edc4d261e22fef263228812c8e3c348 Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Fri, 25 Sep 2026 21:06:39 -0700 Subject: [PATCH 8/9] Updated unit and manual tests --- src/accessibility/describe.js | 74 ++++++++++--------- .../accessibility/describe/sketch.js | 16 ++-- test/unit/accessibility/describe.js | 45 ++++++++++- 3 files changed, 91 insertions(+), 44 deletions(-) diff --git a/src/accessibility/describe.js b/src/accessibility/describe.js index a49eae58d8..cda9f90c9d 100644 --- a/src/accessibility/describe.js +++ b/src/accessibility/describe.js @@ -155,40 +155,6 @@ function describe(p5, fn) { _setDescriptionLang(this, lang); }; - p5.registerDecorator('p5.prototype.describe', function (target) { - return function (text, display, lang) { - // Checks for optional parameters and rearranges if needed - if (arguments.length === 2 && typeof display === 'string') { - // (text, display) - if (display === this.LABEL || display === this.FALLBACK) { - return target.call(this, text, display, undefined); - } - // (text, lang) - return target.call(this, text, undefined, display); - } - _checkDescriptionOptions(this, 'describe', display, lang); - // (text, display, lang) or just (text) - return target.call(this, text, display, lang); - }; - }); - - p5.registerDecorator('p5.prototype.describeElement', function (target) { - return function (name, text, display, lang) { - // Checks for optional parameters and rearranges if needed - if (arguments.length === 3 && typeof display === 'string') { - // (name, text, display) - if (display === this.LABEL || display === this.FALLBACK) { - return target.call(this, name, text, display, undefined); - } - // (name, text, lang) - return target.call(this, name, text, undefined, display); - } - _checkDescriptionOptions(this, 'describeElement', display, lang); - // (name, text, display, lang) or just (name, text) - return target.call(this, name, text, display, lang); - }; - }); - /** * Creates a screen reader-accessible description of elements in the canvas. * @@ -327,6 +293,40 @@ function describe(p5, fn) { _setDescriptionLang(this, lang); }; + p5.registerDecorator('p5.prototype.describe', function (target) { + return function (text, display, lang) { + // Checks for optional parameters and rearranges if needed + if (arguments.length === 2 && typeof display === 'string') { + // (text, display) + if (display === this.LABEL || display === this.FALLBACK) { + return target.call(this, text, display, undefined); + } + // (text, lang) + return target.call(this, text, undefined, display); + } + _checkDescriptionOptions(this, 'describe', display, lang); + // (text, display, lang) or just (text) + return target.call(this, text, display, lang); + }; + }); + + p5.registerDecorator('p5.prototype.describeElement', function (target) { + return function (name, text, display, lang) { + // Checks for optional parameters and rearranges if needed + if (arguments.length === 3 && typeof display === 'string') { + // (name, text, display) + if (display === this.LABEL || display === this.FALLBACK) { + return target.call(this, name, text, display, undefined); + } + // (name, text, lang) + return target.call(this, name, text, undefined, display); + } + _checkDescriptionOptions(this, 'describeElement', display, lang); + // (name, text, display, lang) or just (name, text) + return target.call(this, name, text, display, lang); + }; + }); + /* * * Helper functions for describe() and describeElement(). @@ -343,6 +343,12 @@ function describe(p5, fn) { `${methodName}() expects display (LABEL or FALLBACK) before lang.` ); } + + if (!validDisplay || !validLanguage) { + throw new TypeError( + `${methodName}() expects display (LABEL or FALLBACK) before lang.` + ); + } } function _setDescriptionLang(pInst, lang) { diff --git a/test/manual-test-examples/accessibility/describe/sketch.js b/test/manual-test-examples/accessibility/describe/sketch.js index d0a992a505..e655653a74 100644 --- a/test/manual-test-examples/accessibility/describe/sketch.js +++ b/test/manual-test-examples/accessibility/describe/sketch.js @@ -12,11 +12,11 @@ function setup() { // 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'); + // describe('Cái này là bài thi', LABEL, 'vi'); + + // Text and lang + // describe('यह टेस्ट है', 'hi'); @@ -29,11 +29,11 @@ function setup() { // 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'); + // describeElement('d', 'Cái này là bài thi', LABEL, 'vi'); + + // Name, text, and lang + // describeElement('e', 'यह टेस्ट है', 'hi'); fill('blue'); circle(200, 150, 100); diff --git a/test/unit/accessibility/describe.js b/test/unit/accessibility/describe.js index b0a583a680..cc57a7993c 100644 --- a/test/unit/accessibility/describe.js +++ b/test/unit/accessibility/describe.js @@ -5,6 +5,13 @@ suite('describe', function () { const myID = 'myCanvasID'; beforeAll(function () { + mockP5.registerDecorator = (patterns, decorator) => { + const patternList = Array.isArray(patterns) ? patterns : [patterns]; + patternList.forEach(pattern => { + const name = pattern.split('.').pop(); + mockP5Prototype[name] = decorator(mockP5Prototype[name], { name }); + }); + }; describe(mockP5, mockP5Prototype); mockP5Prototype.LABEL = 'label'; @@ -52,8 +59,17 @@ suite('describe', function () { assert.isNull(document.getElementById(myID + '_Label')); }); + test('should support text and display', function () { + mockP5Prototype.describe('Texto visible', mockP5Prototype.LABEL); + assert.equal( + document.getElementById(myID + '_labelDesc').innerHTML, + 'Texto visible.' + ); + assert.isNull(mockP5Prototype.elt.getAttribute('lang')); + }); + test('should support text lang and display', function () { - mockP5Prototype.describe('Chào các bạn', 'vi', mockP5Prototype.FALLBACK); + mockP5Prototype.describe('Chào các bạn', mockP5Prototype.FALLBACK, 'vi'); let actual = document.getElementById(myID + '_fallbackDesc'); assert.deepEqual(actual.innerHTML, 'Chào các bạn.'); assert.deepEqual(actual.getAttribute('lang'), 'vi'); @@ -67,6 +83,12 @@ suite('describe', function () { assert.deepEqual(actual.getAttribute('lang'), 'vi'); }); + test('should reject language before display', function () { + assert.throws(function () { + mockP5Prototype.describe('Chào các bạn', 'vi', mockP5Prototype.LABEL); + }, TypeError, 'expects display (LABEL or FALLBACK) before lang'); + }); + test('should not add extra period if string ends in "."', function () { mockP5Prototype.describe('A.'); let actual = document.getElementById(myID + '_fallbackDesc'); @@ -145,8 +167,16 @@ suite('describe', function () { ); }); + test('should support element text and display', function () { + mockP5Prototype.describeElement('ao', 'Texto visible', mockP5Prototype.LABEL); + assert.equal( + document.getElementById(myID + '_lte_ao').innerHTML, + 'ao:Texto visible.' + ); + }); + test('should support element text lang and display', function () { - mockP5Prototype.describeElement('al', 'Chào các bạn', 'vi', mockP5Prototype.FALLBACK); + mockP5Prototype.describeElement('al', 'Chào các bạn', mockP5Prototype.FALLBACK, 'vi'); let actual = document.getElementById(myID + '_fte_al').innerHTML; assert.deepEqual(actual, 'al:Chào các bạn.' ); @@ -158,6 +188,17 @@ suite('describe', function () { assert.deepEqual(actual, 'am:Chào các bạn.'); }); + test('should reject element language before display', function () { + assert.throws(function () { + mockP5Prototype.describeElement( + 'an', + 'Chào các bạn', + 'vi', + mockP5Prototype.LABEL + ); + }, TypeError, 'expects display (LABEL or FALLBACK) before lang'); + }); + test('should not add extra ":" if element name ends in colon', function () { mockP5Prototype.describeElement('ab:', 'b.'); let actual = document.getElementById(myID + '_fte_ab').innerHTML; From 93cb3ae8b00892409a605cd7f173c2448b6ff03b Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Fri, 25 Sep 2026 21:49:28 -0700 Subject: [PATCH 9/9] Throws friendly error instead of TypeError --- src/accessibility/describe.js | 21 +++++++++++---------- test/unit/accessibility/describe.js | 25 ++++++++++++++----------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/accessibility/describe.js b/src/accessibility/describe.js index cda9f90c9d..35a0739705 100644 --- a/src/accessibility/describe.js +++ b/src/accessibility/describe.js @@ -304,7 +304,9 @@ function describe(p5, fn) { // (text, lang) return target.call(this, text, undefined, display); } - _checkDescriptionOptions(this, 'describe', display, lang); + if (!_checkDescriptionOptions(this, 'describe', display, lang)) { + return; + } // (text, display, lang) or just (text) return target.call(this, text, display, lang); }; @@ -321,7 +323,9 @@ function describe(p5, fn) { // (name, text, lang) return target.call(this, name, text, undefined, display); } - _checkDescriptionOptions(this, 'describeElement', display, lang); + if (!_checkDescriptionOptions(this, 'describeElement', display, lang)) { + return; + } // (name, text, display, lang) or just (name, text) return target.call(this, name, text, display, lang); }; @@ -339,16 +343,13 @@ function describe(p5, fn) { const validLanguage = lang === undefined || typeof lang === 'string'; if (!validDisplay || !validLanguage) { - throw new TypeError( - `${methodName}() expects display (LABEL or FALLBACK) before lang.` - ); - } - - if (!validDisplay || !validLanguage) { - throw new TypeError( - `${methodName}() expects display (LABEL or FALLBACK) before lang.` + p5._friendlyError( + `${methodName}() expects display (LABEL or FALLBACK) before lang.`, + methodName ); + return false; } + return true; } function _setDescriptionLang(pInst, lang) { diff --git a/test/unit/accessibility/describe.js b/test/unit/accessibility/describe.js index cc57a7993c..776f2cd03e 100644 --- a/test/unit/accessibility/describe.js +++ b/test/unit/accessibility/describe.js @@ -26,6 +26,7 @@ suite('describe', function () { mockP5Prototype.elt.removeAttribute('lang'); mockP5Prototype.dummyDOM = undefined; mockP5Prototype.descriptions = undefined; + mockP5._friendlyError.mockReset(); }); suite('p5.prototype.describe', function () { @@ -84,9 +85,10 @@ suite('describe', function () { }); test('should reject language before display', function () { - assert.throws(function () { - mockP5Prototype.describe('Chào các bạn', 'vi', mockP5Prototype.LABEL); - }, TypeError, 'expects display (LABEL or FALLBACK) before lang'); + mockP5Prototype.describe('Chào các bạn', 'vi', mockP5Prototype.LABEL); + assert.isNull(document.getElementById(myID + '_fallbackDesc')); + assert.lengthOf(mockP5._friendlyError.mock.calls, 1); + assert.equal(mockP5._friendlyError.mock.calls[0][1], 'describe'); }); test('should not add extra period if string ends in "."', function () { @@ -189,14 +191,15 @@ suite('describe', function () { }); test('should reject element language before display', function () { - assert.throws(function () { - mockP5Prototype.describeElement( - 'an', - 'Chào các bạn', - 'vi', - mockP5Prototype.LABEL - ); - }, TypeError, 'expects display (LABEL or FALLBACK) before lang'); + mockP5Prototype.describeElement( + 'an', + 'Chào các bạn', + 'vi', + mockP5Prototype.LABEL + ); + assert.isNull(document.getElementById(myID + '_fte_an')); + assert.lengthOf(mockP5._friendlyError.mock.calls, 1); + assert.equal(mockP5._friendlyError.mock.calls[0][1], 'describeElement'); }); test('should not add extra ":" if element name ends in colon', function () {