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;