Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue/145 Automated aria levels #146

Merged
merged 4 commits into from
Jul 4, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 59 additions & 24 deletions js/a11y.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Popup from 'core/js/a11y/popup';
import defaultAriaLevels from 'core/js/enums/defaultAriaLevels';
import deprecated from 'core/js/a11y/deprecated';
import logging from 'core/js/logging';
import data from './data';

class A11y extends Backbone.Controller {

Expand Down Expand Up @@ -163,32 +164,66 @@ class A11y extends Backbone.Controller {

/**
* Calculate the aria level for a heading
* @param {string|number} [defaultLevelOrType] Specify a default level or type group.
* @param {string|number} [overrideLevelOrType] Override with a level or type group from the designer.
* @param {object} [options]
* @param {string|number} [options.id] Used to automate the heading level when relative increments are used
* @param {string|number} [options.level] Specify a default level, "component" / "menu" / "componentItem" etc
* @param {string|number} [options.override] Override with a default level, an absolute value or a relative increment
* @returns {number}
* @notes
* Default levels come from config.json:_accessibility._ariaLevels attribute names, they are:
* "menu", "menuGroup", "menuItem", "page", "article", "block", "component", "componentItem" and "notify"
* An absolute value would be "1" or "2" etc.
* A relative increment would be "@page+1" or "@block+1". They are calculated from ancestor values,
* respecting both _ariaLevel overrides and not incrementing for missing displayTitle values.
*/
ariaLevel(defaultLevelOrType = 1, overrideLevelOrType) {
// get the global configuration from config.json
const cfg = Adapt.config.get('_accessibility');

// first check to see if the Handlebars context has an override
if (overrideLevelOrType) {
defaultLevelOrType = overrideLevelOrType;
ariaLevel({
id = null,
level = "1",
override = null
} = {}) {
if (arguments.length > 1) {
oliverfoster marked this conversation as resolved.
Show resolved Hide resolved
// backward compatibility
level = arguments[0];
override = arguments[1];
id = null;
}

if (!isNaN(defaultLevelOrType)) {
// if a number is passed just use this
return defaultLevelOrType;
}

if (_.isString(defaultLevelOrType)) {
// if a string is passed, check if it is defined in global configuration
const ariaLevels = cfg._ariaLevels ?? defaultAriaLevels;
return ariaLevels?.['_' + defaultLevelOrType] ?? defaultLevelOrType;
// get the global configuration from config.json
const ariaLevels = Adapt.config.get('_accessibility')?._ariaLevels ?? defaultAriaLevels;
/**
* Recursive function to calculate aria-level
* @param {string} id Model id
* @param {string} level Default name, relative increment or absolute level
* @param {number} [offset=0] Total offset count from first absolute value
* @returns
*/
function calculateLevel(id = null, level, offset = 0) {
const isNumber = !isNaN(level);
const isTypeName = /[a-zA-z]/.test(level);
if (!isTypeName && isNumber) {
// if an absolute value is found, use it, adding the accumulated offset
return parseInt(level) + offset;
}
// parse the level value as a relative string
const relativeDescriptor = Adapt.parseRelativeString(level);
// lookup the default value from `config.json:_accessibility._ariaLevels`
const nextLevel = ariaLevels?.['_' + relativeDescriptor.type];
const hasModelId = Boolean(id);
if (!hasModelId) {
logging.warnOnce('Cannot calculate appropriate heading level, no model id was specified');
return calculateLevel(id, nextLevel, offset + relativeDescriptor.offset);
}
// try to find the next relevant ancestor, or use the specified model
const nextModel = data.findById(id)?.findAncestor(relativeDescriptor.type?.toLowerCase()) ?? data.findById(id);
const nextModelId = nextModel?.get('_id') ?? id;
// check overrides, check title existence, adjust offset accordingly
const hasNextTitle = Boolean(nextModel.get('displayTitle'));
const nextModelOverride = nextModel.get('_ariaLevel');
const accumulatedOffset = offset + (hasNextTitle ? relativeDescriptor.offset : 0);
const resolvedLevel = nextModelOverride ?? nextLevel;
// move towards the parents until an absolute value is found
return calculateLevel(nextModelId, resolvedLevel, accumulatedOffset);
}

// default level to use if nothing overrides it
return defaultLevelOrType;
return calculateLevel(id, override ?? level)
}

/**
Expand Down Expand Up @@ -273,7 +308,7 @@ class A11y extends Backbone.Controller {
}
return this;
}

/**
* Toggles tabindexes off and on all tabbable descendants.
* @param {Object|string|Array} $element
Expand Down Expand Up @@ -688,7 +723,7 @@ class A11y extends Backbone.Controller {
}
function perform() {
if ($element.attr('tabindex') === undefined) {
$element.attr({
$element.attr({
// JAWS reads better with 0, do not use -1
tabindex: '0',
'data-a11y-force-focus': 'true'
Expand Down
40 changes: 30 additions & 10 deletions js/helpers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Adapt from 'core/js/adapt';
import a11y from 'core/js/a11y';
import logging from './logging';

const helpers = {

Expand Down Expand Up @@ -122,7 +123,7 @@ const helpers = {
* Allow JSON to be a template and accessible text
*/
compile_a11y_text(template, context) {
a11y.log.deprecated('a11y_text is no longer required. https://tink.uk/understanding-screen-reader-interaction-modes/');
logging.deprecated('a11y_text is no longer required. https://tink.uk/understanding-screen-reader-interaction-modes/');
return helpers.compile.call(this, template, context);
},

Expand Down Expand Up @@ -231,7 +232,7 @@ const helpers = {
},

a11y_text(text) {
a11y.log.deprecated('a11y_text is no longer required. https://tink.uk/understanding-screen-reader-interaction-modes/');
logging.deprecated('a11y_text is no longer required. https://tink.uk/understanding-screen-reader-interaction-modes/');
return text;
},

Expand Down Expand Up @@ -318,16 +319,35 @@ const helpers = {
* context if specified, a number if given as the `levelOrType` parameter,
* or a name from the configured aria levels hash.
*
* @param {number|string} levelOrType
* @param {number|string} level
* @returns {string}
* @deprecated Please use a11y_aria_level or a11y.ariaLevel accordingly
*/
a11y_attrs_heading(levelOrType) {
const level = a11y.ariaLevel(levelOrType, this._ariaLevel);
return new Handlebars.SafeString(' role="heading" aria-level="' + level + '" ');
a11y_attrs_heading(level) {
logging.deprecated('a11y_attrs_heading, please use a11y_aria_level or a11y.ariaLevel');
const resolvedLevel = a11y.ariaLevel({
id: this._id ?? Array.from(arguments).lastItem?.data?.root?._id,
level,
override: this._ariaLevel
});
return new Handlebars.SafeString(' role="heading" aria-level="' + resolvedLevel + '" ');
oliverfoster marked this conversation as resolved.
Show resolved Hide resolved
},

/**
* Creates the value of the aria-level attribute for a subject heading text.
*
* @param {number|string} id The originating model id
* @param {number|string} level An explicit level number ("1"), a relative increment ("@block+1") or a default type name ("menu").
* @param {number|string} [override=null] An explicit level number ("1"), a relative increment ("@block+1") or a default type name ("menu"), usually passed through from the config to override the default.
* @returns {string}
*/
a11y_aria_level(id, level, override = null) {
const resolvedLevel = a11y.ariaLevel({ id, level, override });
return resolvedLevel;
},

a11y_attrs_tabbable() {
a11y.log.deprecated('a11y_attrs_tabbable should not be used. tabbable elements should be natively tabbable.');
logging.deprecated('a11y_attrs_tabbable should not be used. tabbable elements should be natively tabbable.');
return new Handlebars.SafeString(' role="region" tabindex="0" ');
},

Expand All @@ -349,17 +369,17 @@ const helpers = {
Object.assign(helpers, {

if_value_equals() {
a11y.log.deprecated('if_value_equals, use equals instead.');
logging.deprecated('if_value_equals, use equals instead.');
return helpers.equals.apply(this, arguments);
},

numbers() {
a11y.log.deprecated('numbers, use inc instead.');
logging.deprecated('numbers, use inc instead.');
return helpers.inc.apply(this, arguments);
},

lowerCase() {
a11y.log.deprecated('lowerCase, use lowercase instead.');
logging.deprecated('lowerCase, use lowercase instead.');
return helpers.lowercase.apply(this, arguments);
}

Expand Down
2 changes: 1 addition & 1 deletion templates/heading.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{{import_globals}}

<div id="{{_id}}-heading" class="js-heading-inner" {{{a11y_attrs_heading _type}}}>
<div id="{{_id}}-heading" class="js-heading-inner" role="heading" aria-level="{{a11y_aria_level _id _type _ariaLevel}}">

<span class="aria-label">
{{#if _isA11yCompletionDescriptionEnabled}}
Expand Down