Skip to content

Commit

Permalink
Merge pull request #311 from desmosinc/sclower/link-focusable-static-…
Browse files Browse the repository at this point in the history
…math-and-textarea-aria

Link focusable static math textarea with mathspeak region through ARIA
  • Loading branch information
sclower authored Oct 22, 2024
2 parents 49de1c6 + b8aecc6 commit a7fed4a
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 21 deletions.
1 change: 1 addition & 0 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class ControllerBase {

textareaSpan: HTMLElement | undefined;
mathspeakSpan: HTMLElement | undefined;
mathspeakId: string | undefined;

constructor(
root: ControllerRoot,
Expand Down
41 changes: 23 additions & 18 deletions src/services/textarea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ Options.prototype.substituteTextarea = function (tabbable?: boolean) {
tabindex: tabbable ? undefined : '-1'
});
};

/* A light-weight function to generate a UUID */
function generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}

function defaultSubstituteKeyboardEvents(jq: $, controller: Controller) {
return saneKeyboardEvents(jq[0] as HTMLTextAreaElement, controller);
}
Expand Down Expand Up @@ -39,6 +49,13 @@ class Controller extends Controller_scrollHoriz {
this.textarea = domFrag(textarea)
.appendTo(this.textareaSpan)
.oneElement() as HTMLTextAreaElement;
this.mathspeakId = generateUUID();
this.mathspeakSpan = h('span', {
class: 'mq-mathspeak',
id: this.mathspeakId
});
textarea?.setAttribute('aria-labelledby', this.mathspeakId);
domFrag(this.textareaSpan).prepend(domFrag(this.mathspeakSpan));

var ctrlr = this;
ctrlr.cursor.selectionChanged = function () {
Expand Down Expand Up @@ -95,6 +112,8 @@ class Controller extends Controller_scrollHoriz {
const textarea = this.getTextarea();
const { select } = saneKeyboardEvents(textarea, this);
this.selectFn = select;
const textareaSpan = this.getTextareaSpan();
domFrag(this.container).prepend(domFrag(textareaSpan));
}

/** Requires `this.textarea` to be initialized. */
Expand Down Expand Up @@ -187,11 +206,8 @@ class Controller extends Controller_scrollHoriz {
}
}

/** Set up for a static MQ field (i.e., create and attach the mathspeak element and initialize the focus state to blurred) */
/** Set up for a static MQ field (i.e., initialize the focus state to blurred) */
setupStaticField() {
this.mathspeakSpan = h('span', { class: 'mq-mathspeak' });
domFrag(this.container).prepend(domFrag(this.mathspeakSpan));
domFrag(this.container).prepend(domFrag(this.textareaSpan));
this.updateMathspeak();
this.blurred = true;
this.cursor.hide().parent.blur(this.cursor);
Expand All @@ -207,25 +223,14 @@ class Controller extends Controller_scrollHoriz {
var mathspeak = ctrlr.root.mathspeak().trim();
this.aria.clear();

const textarea = ctrlr.getTextarea();
// For static math, provide mathspeak in a visually hidden span to allow screen readers and other AT to traverse the content.
// For editable math, assign the mathspeak to the textarea's ARIA label (AT can use text navigation to interrogate the content).
// Be certain to include the mathspeak for only one of these, though, as we don't want to include outdated labels if a field's editable state changes.
// By design, also take careful note that the ariaPostLabel is meant to exist only for editable math (e.g. to serve as an evaluation or error message)
// so it is not included for static math mathspeak calculations.
// The mathspeakSpan should exist only for static math, so we use its presence to decide which approach to take.
if (!!ctrlr.mathspeakSpan) {
textarea.setAttribute('aria-label', '');
ctrlr.mathspeakSpan.textContent = (
labelWithSuffix +
' ' +
mathspeak
mathspeak +
' ' +
ctrlr.ariaPostLabel
).trim();
} else {
textarea.setAttribute(
'aria-label',
(labelWithSuffix + ' ' + mathspeak + ' ' + ctrlr.ariaPostLabel).trim()
);
}
}
}
17 changes: 14 additions & 3 deletions test/unit/aria.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ suite('aria', function () {
ariaHiddenChildren.hasClass('mq-root-block'),
'aria-hidden is set on mq-root-block'
);
var mathspeak = $(container).find('.mq-mathspeak');
assert.equal(mathspeak.length, 1, 'One mathspeak region');
var mathspeakId = mathspeak[0].getAttribute('id');
assert.ok(!!mathspeakId, 'mathspeak element assigned an id');
var textarea = $(container).find('textarea');
assert.equal(textarea.length, 1, 'One textarea');
assert.equal(
textarea[0].getAttribute('aria-labelledby'),
mathspeakId,
'textarea is aria-labelledby mathspeak region'
);
});

test('MathQuillMathField aria-hidden', function () {
Expand All @@ -68,11 +79,11 @@ suite('aria', function () {
'Textarea has one aria-hidden parent'
);
var mathSpeak = $(container).find('.mq-mathspeak');
assert.equal(mathSpeak.length, 1, 'One mathspeak region');
assert.equal(mathSpeak.length, 2, 'Two mathspeak regions');
assert.equal(
mathSpeak.closest('[aria-hidden]="true"').length,
0,
'Textarea has no aria-hidden parent'
'Mathspeak has no aria-hidden parent'
);
var nHiddenTexts = 0;
var allChildren = $(container).find('*');
Expand Down Expand Up @@ -249,7 +260,7 @@ suite('aria', function () {
mathField.blur();
setTimeout(function () {
assert.equal(
mathField.__controller.textarea.getAttribute('aria-label'),
mathField.__controller.mathspeakSpan.textContent,
'Math Input: "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'
);
done();
Expand Down

0 comments on commit a7fed4a

Please sign in to comment.