diff --git a/src/accessibility/describe.js b/src/accessibility/describe.js
index 1d5511cbdf..35a0739705 100644
--- a/src/accessibility/describe.js
+++ b/src/accessibility/describe.js
@@ -19,12 +19,12 @@ 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, `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
* Writing accessible canvas descriptions
@@ -33,6 +33,7 @@ function describe(p5, fn) {
* @method describe
* @param {String} text description of the canvas.
* @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK.
+ * @param {String} [lang] valid lang attribute.
*
* @example
* function setup() {
@@ -110,7 +111,7 @@ 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, display, lang) {
// p5._validateParameters('describe', arguments);
if (typeof text !== 'string') {
return;
@@ -122,6 +123,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 +152,7 @@ function describe(p5, fn) {
this._describeHTML('label', text);
}
}
+ _setDescriptionLang(this, lang);
};
/**
@@ -161,15 +164,18 @@ 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, `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
+ * `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
@@ -179,7 +185,7 @@ function describe(p5, fn) {
* @param {String} name name of the element.
* @param {String} text description of the element.
* @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK.
- *
+ * @param {String} [lang] valid lang attribute.
* @example
* function setup() {
* background('pink');
@@ -229,7 +235,7 @@ function describe(p5, fn) {
* }
*/
- fn.describeElement = function (name, text, display) {
+ fn.describeElement = function (name, text, display, lang) {
// p5._validateParameters('describeElement', arguments);
if (typeof text !== 'string' || typeof name !== 'string') {
return;
@@ -242,8 +248,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;
@@ -281,14 +290,96 @@ function describe(p5, fn) {
this._describeElementHTML('label', name, inner);
}
}
+ _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);
+ }
+ if (!_checkDescriptionOptions(this, 'describe', display, lang)) {
+ return;
+ }
+ // (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);
+ }
+ if (!_checkDescriptionOptions(this, 'describeElement', display, lang)) {
+ return;
+ }
+ // (name, text, display, lang) or just (name, text)
+ return target.call(this, name, text, display, lang);
+ };
+ });
+
/*
*
* Helper functions for describe() and describeElement().
*
*/
+ 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';
+
+ if (!validDisplay || !validLanguage) {
+ p5._friendlyError(
+ `${methodName}() expects display (LABEL or FALLBACK) before lang.`,
+ methodName
+ );
+ return false;
+ }
+ return true;
+ }
+
+ 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..e655653a74
--- /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, display, and lang
+ // describe('Cái này là bài thi', LABEL, 'vi');
+
+ // Text and lang
+ // describe('यह टेस्ट है', '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, display, and lang
+ // 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);
+}
\ No newline at end of file
diff --git a/test/unit/accessibility/describe.js b/test/unit/accessibility/describe.js
index 427aa16ce0..776f2cd03e 100644
--- a/test/unit/accessibility/describe.js
+++ b/test/unit/accessibility/describe.js
@@ -5,12 +5,30 @@ 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';
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;
+ mockP5._friendlyError.mockReset();
+ });
+
suite('p5.prototype.describe', function () {
test('should be a function', function () {
assert.ok(mockP5Prototype.describe);
@@ -27,12 +45,52 @@ 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 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', 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');
+ 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 reject language before display', function () {
+ 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 () {
mockP5Prototype.describe('A.');
let actual = document.getElementById(myID + '_fallbackDesc');
@@ -104,6 +162,46 @@ 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 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', mockP5Prototype.FALLBACK, 'vi');
+ 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 reject element language before display', function () {
+ 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 () {
mockP5Prototype.describeElement('ab:', 'b.');
let actual = document.getElementById(myID + '_fte_ab').innerHTML;