`;
//if there is no dummyDOM
if (!this.dummyDOM) {
this.dummyDOM = document.getElementById(cnvId).parentNode;
@@ -281,14 +301,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;