UNPKG

p5

Version:

[![npm version](https://badge.fury.io/js/p5.svg)](https://www.npmjs.com/package/p5)

516 lines (501 loc) 18.5 kB
/** * @module Environment * @submodule Environment * @for p5 * @requires core */ function describe(p5, fn){ const descContainer = '_Description'; //Fallback container const fallbackDescId = '_fallbackDesc'; //Fallback description const fallbackTableId = '_fallbackTable'; //Fallback Table const fallbackTableElId = '_fte_'; //Fallback Table Element const labelContainer = '_Label'; //Label container const labelDescId = '_labelDesc'; //Label description const labelTableId = '_labelTable'; //Label Table const labelTableElId = '_lte_'; //Label Table Element /** * Creates a screen reader-accessible description of the canvas. * * 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 * <a href="/learn/accessible-labels.html">Writing accessible canvas descriptions</a> * to learn more about making sketches accessible. * * @method describe * @param {String} text description of the canvas. * @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK. * * @example * <div> * <code> * function setup() { * background('pink'); * * // Draw a heart. * fill('red'); * noStroke(); * circle(67, 67, 20); * circle(83, 67, 20); * triangle(91, 73, 75, 95, 59, 73); * * // Add a general description of the canvas. * describe('A pink square with a red heart in the bottom-right corner.'); * } * </code> * </div> * * <div> * <code> * function setup() { * background('pink'); * * // Draw a heart. * fill('red'); * noStroke(); * circle(67, 67, 20); * circle(83, 67, 20); * triangle(91, 73, 75, 95, 59, 73); * * // Add a general description of the canvas * // and display it for debugging. * describe('A pink square with a red heart in the bottom-right corner.', LABEL); * } * </code> * </div> * * <div> * <code> * * function setup(){ * createCanvas(100, 100); * }; * * function draw() { * background(200); * * // The expression * // frameCount % 100 * // causes x to increase from 0 * // to 99, then restart from 0. * let x = frameCount % 100; * * // Draw the circle. * fill(0, 255, 0); * circle(x, 50, 40); * * // Add a general description of the canvas. * describe(`A green circle at (${x}, 50) moves from left to right on a gray square.`); * } * </code> * </div> * * <div> * <code> * * function setup(){ * createCanvas(100, 100); * } * * function draw() { * background(200); * * // The expression * // frameCount % 100 * // causes x to increase from 0 * // to 99, then restart from 0. * let x = frameCount % 100; * * // Draw the circle. * fill(0, 255, 0); * circle(x, 50, 40); * * // Add a general description of the canvas * // and display it for debugging. * describe(`A green circle at (${x}, 50) moves from left to right on a gray square.`, LABEL); * } * </code> * </div> */ fn.describe = function(text, display) { // p5._validateParameters('describe', arguments); if (typeof text !== 'string') { return; } const cnvId = this.canvas.id; //calls function that adds punctuation for better screen reading text = _descriptionText(text); //if there is no dummyDOM if (!this.dummyDOM) { this.dummyDOM = document.getElementById(cnvId).parentNode; } if (!this.descriptions) { this.descriptions = {}; } //check if html structure for description is ready if (this.descriptions.fallback) { //check if text is different from current description if (this.descriptions.fallback.innerHTML !== text) { //update description this.descriptions.fallback.innerHTML = text; } } else { //create fallback html structure this._describeHTML('fallback', text); } //if display is LABEL if (display === this.LABEL) { //check if html structure for label is ready if (this.descriptions.label) { //check if text is different from current label if (this.descriptions.label.innerHTML !== text) { //update label description this.descriptions.label.innerHTML = text; } } else { //create label html structure this._describeHTML('label', text); } } }; /** * Creates a screen reader-accessible description of elements in the canvas. * * Elements are shapes or groups of shapes that create meaning together. For * example, a few overlapping circles could make an "eye" element. * * 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. * * Read * <a href="/learn/accessible-labels.html">Writing accessible canvas descriptions</a> * to learn more about making sketches accessible. * * @method describeElement * @param {String} name name of the element. * @param {String} text description of the element. * @param {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK. * * @example * <div> * <code> * function setup() { * background('pink'); * * // Describe the first element * // and draw it. * describeElement('Circle', 'A yellow circle in the top-left corner.'); * noStroke(); * fill('yellow'); * circle(25, 25, 40); * * // Describe the second element * // and draw it. * describeElement('Heart', 'A red heart in the bottom-right corner.'); * fill('red'); * circle(66.6, 66.6, 20); * circle(83.2, 66.6, 20); * triangle(91.2, 72.6, 75, 95, 58.6, 72.6); * * // Add a general description of the canvas. * describe('A red heart and yellow circle over a pink background.'); * } * </code> * </div> * * <div> * <code> * function setup() { * background('pink'); * * // Describe the first element * // and draw it. Display the * // description for debugging. * describeElement('Circle', 'A yellow circle in the top-left corner.', LABEL); * noStroke(); * fill('yellow'); * circle(25, 25, 40); * * // Describe the second element * // and draw it. Display the * // description for debugging. * describeElement('Heart', 'A red heart in the bottom-right corner.', LABEL); * fill('red'); * circle(66.6, 66.6, 20); * circle(83.2, 66.6, 20); * triangle(91.2, 72.6, 75, 95, 58.6, 72.6); * * // Add a general description of the canvas. * describe('A red heart and yellow circle over a pink background.'); * } * </code> * </div> */ fn.describeElement = function(name, text, display) { // p5._validateParameters('describeElement', arguments); if (typeof text !== 'string' || typeof name !== 'string') { return; } const cnvId = this.canvas.id; //calls function that adds punctuation for better screen reading text = _descriptionText(text); //calls function that adds punctuation for better screen reading let elementName = _elementName(name); //remove any special characters from name to use it as html id name = name.replace(/[^a-zA-Z0-9]/g, ''); //store element description let inner = `<th scope="row">${elementName}</th><td>${text}</td>`; //if there is no dummyDOM if (!this.dummyDOM) { this.dummyDOM = document.getElementById(cnvId).parentNode; } if (!this.descriptions) { this.descriptions = { fallbackElements: {} }; } else if (!this.descriptions.fallbackElements) { this.descriptions.fallbackElements = {}; } //check if html structure for element description is ready if (this.descriptions.fallbackElements[name]) { //if current element description is not the same as inner if (this.descriptions.fallbackElements[name].innerHTML !== inner) { //update element description this.descriptions.fallbackElements[name].innerHTML = inner; } } else { //create fallback html structure this._describeElementHTML('fallback', name, inner); } //if display is LABEL if (display === this.LABEL) { if (!this.descriptions.labelElements) { this.descriptions.labelElements = {}; } //if html structure for label element description is ready if (this.descriptions.labelElements[name]) { //if label element description is different if (this.descriptions.labelElements[name].innerHTML !== inner) { //update label element description this.descriptions.labelElements[name].innerHTML = inner; } } else { //create label element html structure this._describeElementHTML('label', name, inner); } } }; /* * * Helper functions for describe() and describeElement(). * */ // check that text is not LABEL or FALLBACK and ensure text ends with punctuation mark function _descriptionText(text) { if (text === 'label' || text === 'fallback') { throw new Error('description should not be LABEL or FALLBACK'); } //if string does not end with '.' if ( !text.endsWith('.') && !text.endsWith(';') && !text.endsWith(',') && !text.endsWith('?') && !text.endsWith('!') ) { //add '.' to the end of string text = text + '.'; } return text; } /* * Helper functions for describe() */ //creates HTML structure for canvas descriptions fn._describeHTML = function(type, text) { const cnvId = this.canvas.id; if (type === 'fallback') { //if there is no description container if (!this.dummyDOM.querySelector(`#${cnvId + descContainer}`)) { //if there are no accessible outputs (see textOutput() and gridOutput()) let html = `<div id="${cnvId}${descContainer}" role="region" aria-label="Canvas Description"><p id="${cnvId}${fallbackDescId}"></p></div>`; if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutput`)) { //create description container + <p> for fallback description this.dummyDOM.querySelector(`#${cnvId}`).innerHTML = html; } else { //create description container + <p> for fallback description before outputs this.dummyDOM .querySelector(`#${cnvId}accessibleOutput`) .insertAdjacentHTML('beforebegin', html); } } else { //if describeElement() has already created the container and added a table of elements //create fallback description <p> before the table this.dummyDOM .querySelector('#' + cnvId + fallbackTableId) .insertAdjacentHTML( 'beforebegin', `<p id="${cnvId + fallbackDescId}"></p>` ); } //if the container for the description exists this.descriptions.fallback = this.dummyDOM.querySelector( `#${cnvId}${fallbackDescId}` ); this.descriptions.fallback.innerHTML = text; return; } else if (type === 'label') { //if there is no label container if (!this.dummyDOM.querySelector(`#${cnvId + labelContainer}`)) { let html = `<div id="${cnvId}${labelContainer}" class="p5Label"><p id="${cnvId}${labelDescId}"></p></div>`; //if there are no accessible outputs (see textOutput() and gridOutput()) if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutputLabel`)) { //create label container + <p> for label description this.dummyDOM .querySelector('#' + cnvId) .insertAdjacentHTML('afterend', html); } else { //create label container + <p> for label description before outputs this.dummyDOM .querySelector(`#${cnvId}accessibleOutputLabel`) .insertAdjacentHTML('beforebegin', html); } } else if (this.dummyDOM.querySelector(`#${cnvId + labelTableId}`)) { //if describeElement() has already created the container and added a table of elements //create label description <p> before the table this.dummyDOM .querySelector(`#${cnvId + labelTableId}`) .insertAdjacentHTML( 'beforebegin', `<p id="${cnvId}${labelDescId}"></p>` ); } this.descriptions.label = this.dummyDOM.querySelector( '#' + cnvId + labelDescId ); this.descriptions.label.innerHTML = text; return; } }; /* * Helper functions for describeElement(). */ //check that name is not LABEL or FALLBACK and ensure text ends with colon function _elementName(name) { if (name === 'label' || name === 'fallback') { throw new Error('element name should not be LABEL or FALLBACK'); } //check if last character of string n is '.', ';', or ',' if (name.endsWith('.') || name.endsWith(';') || name.endsWith(',')) { //replace last character with ':' name = name.replace(/.$/, ':'); } else if (!name.endsWith(':')) { //if string n does not end with ':' //add ':'' at the end of string name = name + ':'; } return name; } //creates HTML structure for element descriptions fn._describeElementHTML = function(type, name, text) { const cnvId = this.canvas.id; if (type === 'fallback') { //if there is no description container if (!this.dummyDOM.querySelector(`#${cnvId + descContainer}`)) { //if there are no accessible outputs (see textOutput() and gridOutput()) let html = `<div id="${cnvId}${descContainer}" role="region" aria-label="Canvas Description"><table id="${cnvId}${fallbackTableId}"><caption>Canvas elements and their descriptions</caption></table></div>`; if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutput`)) { //create container + table for element descriptions this.dummyDOM.querySelector('#' + cnvId).innerHTML = html; } else { //create container + table for element descriptions before outputs this.dummyDOM .querySelector(`#${cnvId}accessibleOutput`) .insertAdjacentHTML('beforebegin', html); } } else if (!this.dummyDOM.querySelector('#' + cnvId + fallbackTableId)) { //if describe() has already created the container and added a description //and there is no table create fallback table for element description after //fallback description this.dummyDOM .querySelector('#' + cnvId + fallbackDescId) .insertAdjacentHTML( 'afterend', `<table id="${cnvId}${fallbackTableId}"><caption>Canvas elements and their descriptions</caption></table>` ); } //create a table row for the element let tableRow = document.createElement('tr'); tableRow.id = cnvId + fallbackTableElId + name; this.dummyDOM .querySelector('#' + cnvId + fallbackTableId) .appendChild(tableRow); //update element description this.descriptions.fallbackElements[name] = this.dummyDOM.querySelector( `#${cnvId}${fallbackTableElId}${name}` ); this.descriptions.fallbackElements[name].innerHTML = text; return; } else if (type === 'label') { //If display is LABEL creates a div adjacent to the canvas element with //a table, a row header cell with the name of the elements, //and adds the description of the element in adjacent cell. //if there is no label description container if (!this.dummyDOM.querySelector(`#${cnvId + labelContainer}`)) { //if there are no accessible outputs (see textOutput() and gridOutput()) let html = `<div id="${cnvId}${labelContainer}" class="p5Label"><table id="${cnvId}${labelTableId}"></table></div>`; if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutputLabel`)) { //create container + table for element descriptions this.dummyDOM .querySelector('#' + cnvId) .insertAdjacentHTML('afterend', html); } else { //create container + table for element descriptions before outputs this.dummyDOM .querySelector(`#${cnvId}accessibleOutputLabel`) .insertAdjacentHTML('beforebegin', html); } } else if (!this.dummyDOM.querySelector(`#${cnvId + labelTableId}`)) { //if describe() has already created the label container and added a description //and there is no table create label table for element description after //label description this.dummyDOM .querySelector('#' + cnvId + labelDescId) .insertAdjacentHTML( 'afterend', `<table id="${cnvId + labelTableId}"></table>` ); } //create a table row for the element label description let tableRow = document.createElement('tr'); tableRow.id = cnvId + labelTableElId + name; this.dummyDOM .querySelector('#' + cnvId + labelTableId) .appendChild(tableRow); //update element label description this.descriptions.labelElements[name] = this.dummyDOM.querySelector( `#${cnvId}${labelTableElId}${name}` ); this.descriptions.labelElements[name].innerHTML = text; } }; } if(typeof p5 !== 'undefined'){ describe(p5, p5.prototype); } export { describe as default };