Skip to content

Commit

Permalink
Merge pull request #319 from desmosinc/dynamically-set-tabbable
Browse files Browse the repository at this point in the history
add "tabindex" property and methods
  • Loading branch information
eluberoff authored Feb 5, 2025
2 parents 13a922b + 0737790 commit d1db179
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 43 deletions.
12 changes: 5 additions & 7 deletions docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ The configuration options object is of the following form:
autoCommands: 'pi theta sqrt sum',
autoOperatorNames: 'sin cos',
maxDepth: 10,
substituteTextarea: function(tabbable) {
substituteTextarea: function() {
const textarea = document.createElement('textarea');
textarea.setAttribute('tabindex', tabbable ? '0' : '-1');
return textarea;
},
handlers: {
Expand Down Expand Up @@ -125,13 +124,12 @@ You can also specify a speech-friendly representation of the operator name by su
`substituteTextarea` is a function that creates a focusable DOM element that is called when setting up a math field. Overwriting this may be useful for hacks like suppressing built-in virtual keyboards. It defaults to `<textarea autocorrect=off .../>`.
For example, [Desmos](https://www.desmos.com/calculator) substitutes `<textarea inputmode=none />` to suppress the native virtual keyboard in favor of a custom math keypad that calls the MathQuill API. On old iOS versions that don't support `inputmode=none`, it uses `<span tabindex=0></span>` to suppress the native virtual keyboard, at the cost of bluetooth keyboards not working.

The `substituteTextarea` takes one argument, a boolean `tabbable` that is true for editable math fields and for static math fields configured with `{tabbable: true}`. The textarea is permanently mounted to the page, so it should have `tabindex=-1` if `tabbable` is false.
## tabindex

## tabbable
Sets a tabindex on the field, following the standard spec. When tabindex is -1,
the math field is not part of the page's tab order. Despite that, the math field can still be focused when selected by a mouse.

For static and editable math fields, when `tabbable` is false, the math field is not part of the page's tab order. Despite that, the math field can still be focused when selected by a mouse.

Static math fields default to `tabbable: false`, Editable math fields default to `tabbable:true`.
Static math fields default to `tabindex: -1`, Editable math fields default to `tabindex: 0`.

## disableAutoSubstitutionInSubscripts

Expand Down
10 changes: 4 additions & 6 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,17 @@ class ControllerBase {
return this;
}
setAriaLabel(ariaLabel: string) {
var oldAriaLabel = this.getAriaLabel();
if (ariaLabel && typeof ariaLabel === 'string' && ariaLabel !== '') {
this.ariaLabel = ariaLabel;
} else if (this.editable) {
const oldAriaLabel = this.getAriaLabel();
if (!ariaLabel && this.editable) {
this.ariaLabel = 'Math Input';
} else {
this.ariaLabel = '';
this.ariaLabel = ariaLabel;
}
// If this field doesn't have focus, update its computed mathspeak value.
// We check for focus because updating the aria-label attribute of a focused element will cause most screen readers to announce the new value (in our case, label along with the expression's mathspeak).
// If the field does have focus at the time, it will be updated once a blur event occurs.
// Unless we stop using fake text inputs and emulating screen reader behavior, this is going to remain a problem.
if (this.ariaLabel !== oldAriaLabel && !this.containerHasFocus()) {
if (ariaLabel !== oldAriaLabel && !this.containerHasFocus()) {
this.updateMathspeak();
}
return this;
Expand Down
2 changes: 1 addition & 1 deletion src/mathquill.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ declare namespace MathQuill {
typingSlashWritesDivisionSymbol?: boolean;
typingPercentWritesPercentOf?: boolean;
resetCursorOnBlur?: boolean | undefined;
tabbable?: boolean;
tabindex?: number;
leftRightIntoCmdGoes?: 'up' | 'down';
enableDigitGrouping?: boolean;
tripleDotsAreEllipsis?: boolean;
Expand Down
7 changes: 5 additions & 2 deletions src/publicapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class Options {
constructor(public version: 1 | 2 | 3) {}

ignoreNextMousedown: (_el: MouseEvent) => boolean;
substituteTextarea: (tabbable?: boolean) => HTMLElement;
substituteTextarea: () => HTMLElement;
/** Only used in interface versions 1 and 2. */
substituteKeyboardEvents: SubstituteKeyboardEvents;

Expand All @@ -107,7 +107,7 @@ class Options {
leftRightIntoCmdGoes?: 'up' | 'down';
enableDigitGrouping?: boolean;
tripleDotsAreEllipsis?: boolean;
tabbable?: boolean;
tabindex?: number;
mouseEvents?: boolean;
maxDepth?: number;
disableCopyPaste?: boolean;
Expand Down Expand Up @@ -312,6 +312,9 @@ function getInterface(v: number): MathQuill.v3.API | MathQuill.v1.API {
}
config(opts: ConfigOptions) {
config(this.__options, opts);
if (opts.tabindex !== undefined) {
this.__controller.setTabindex(opts.tabindex);
}
return this;
}
el() {
Expand Down
50 changes: 34 additions & 16 deletions src/services/textarea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
* Manage the MathQuill instance's textarea
* (as owned by the Controller)
********************************************/
Options.prototype.substituteTextarea = function (tabbable?: boolean) {
Options.prototype.substituteTextarea = function () {
return h('textarea', {
autocapitalize: 'off',
autocomplete: 'off',
autocorrect: 'off',
spellcheck: false,
'x-palm-disable-ste-all': true,
tabindex: tabbable ? undefined : '-1'
'x-palm-disable-ste-all': true
});
};

Expand All @@ -30,22 +29,16 @@ Options.prototype.substituteKeyboardEvents = defaultSubstituteKeyboardEvents;
class Controller extends Controller_scrollHoriz {
selectFn: (text: string) => void = noop;

previousTabindex: number | undefined;

createTextarea() {
this.textareaSpan = h('span', { class: 'mq-textarea' });

const tabbable =
this.options.tabbable !== undefined
? this.options.tabbable
: this.KIND_OF_MQ !== 'StaticMath';

const textarea = this.options.substituteTextarea(tabbable);
const textarea = this.options.substituteTextarea();
if (!textarea.nodeType) {
throw 'substituteTextarea() must return a DOM element, got ' + textarea;
}
if (!this.options.tabbable && this.KIND_OF_MQ === 'StaticMath') {
// aria-hide noninteractive textarea element for static math
textarea.setAttribute('aria-hidden', 'true');
}

this.textarea = domFrag(textarea)
.appendTo(this.textareaSpan)
.oneElement() as HTMLTextAreaElement;
Expand All @@ -61,14 +54,39 @@ class Controller extends Controller_scrollHoriz {
if (this.mathspeakId) {
textarea?.setAttribute('aria-labelledby', this.mathspeakId);
}
if (tabbable && this.mathspeakSpan) {
this.mathspeakSpan.setAttribute('aria-hidden', 'true');
}

var ctrlr = this;
ctrlr.cursor.selectionChanged = function () {
ctrlr.selectionChanged();
};

const tabindex =
this.options.tabindex !== undefined
? this.options.tabindex
: this.KIND_OF_MQ === 'StaticMath'
? -1
: 0;

this.setTabindex(tabindex);
}

setTabindex(tabindex: number) {
if (tabindex === this.previousTabindex || !this.textarea) return;
this.previousTabindex = tabindex;

this.textarea?.setAttribute('tabindex', '' + tabindex);

if (tabindex < 0 && this.KIND_OF_MQ === 'StaticMath') {
this.textarea?.setAttribute('aria-hidden', 'true');
} else {
this.textarea?.removeAttribute('aria-hidden');
}

if (tabindex >= 0) {
this.mathspeakSpan?.setAttribute('aria-hidden', 'true');
} else {
this.mathspeakSpan?.removeAttribute('aria-hidden');
}
}

selectionChanged() {
Expand Down
3 changes: 2 additions & 1 deletion test/basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ <h1>
});
});
var mq = MQ.MathField($('#basic')[0], {
tabbable: false,
tabindex: -1,
autoSubscriptNumerals: true,
autoCommands:
'alpha beta sqrt theta phi pi tau nthroot cbrt prod int ans percent mid square',
Expand All @@ -58,6 +58,7 @@ <h1>
}
});
latex.val(mq.latex());
mq.config({ tabindex: 0 });
</script>
</body>
</html>
8 changes: 4 additions & 4 deletions test/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ <h1>
</p>

<p>
On the other hand, you can make static math tabbable to appear in the
tab order despite being non-editable. The entire range is selected when
tabbed into:
On the other hand, you can make static math appear in the tab order
despite being non-editable by providing a tabindex. The entire range is
selected when tabbed into:
<span class="static-math-tabbable">1.234\times 10^{8}</span>.
</p>

Expand Down Expand Up @@ -169,7 +169,7 @@ <h1>
MQ.StaticMath(this, { mouseEvents: false });
});
$('.static-math-tabbable').each(function () {
MQ.StaticMath(this, { tabbable: true });
MQ.StaticMath(this, { tabindex: 0 });
});
$('.mathquill-math-field').each(function () {
MQ.MathField(this);
Expand Down
25 changes: 21 additions & 4 deletions test/unit/aria.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,36 @@ suite('aria', function () {
staticMath.latex('1+\\frac{1}{x}');
var ariaHiddenChildren = $(container).find('[aria-hidden]="true"');
assert.equal(ariaHiddenChildren.length, 2, '2 aria-hidden elements');
assert.ok(
ariaHiddenChildren[1].nodeName,
'textarea',
assert.equal(
ariaHiddenChildren[0].nodeName,
'TEXTAREA',
'aria-hidden is set on static math textarea'
);
assert.ok(
ariaHiddenChildren[1].classList.contains('mq-root-block'),
'aria-hidden is set on mq-root-block'
);

staticMath.config({ tabindex: 0 });
var ariaHiddenChildren = $(container).find('[aria-hidden]="true"');
assert.equal(ariaHiddenChildren.length, 2, '2 aria-hidden elements');
assert.equal(
ariaHiddenChildren[0].nodeName,
'SPAN',
'aria-hidden is set on mathspeak span when tabbable'
);

staticMath.config({ tabindex: -1 });
var ariaHiddenChildren = $(container).find('[aria-hidden]="true"');
assert.equal(
ariaHiddenChildren[0].nodeName,
'TEXTAREA',
'aria-hidden is again set on textarea when no longer tabbable'
);
});

test('Tabbable static math aria-hidden', function () {
var staticMath = MQ.StaticMath(container, { tabbable: true });
var staticMath = MQ.StaticMath(container, { tabindex: 0 });
staticMath.latex('1+\\frac{1}{x}');
var ariaHiddenChildren = $(container).find('[aria-hidden]="true"');
// There will be two hidden children: the raw text of the field, and its mathspeak representation.
Expand Down
47 changes: 45 additions & 2 deletions test/unit/focusBlur.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ suite('focusBlur', function () {
test('full range selected on focusing tabbable static math', function () {
var mq = MQ.StaticMath(
$('<span>1234\\times 10^{23}</span>').appendTo('#mock')[0],
{ tabbable: true }
{ tabindex: 0 }
);

mq.focus();
Expand All @@ -118,7 +118,50 @@ suite('focusBlur', function () {
'full textarea selected'
);

assert.equal($(document.activeElement).attr('tabindex'), 0);
assert.equal($(document.activeElement).attr('tabindex'), '0');
mq.config({ tabindex: -1 });
assert.equal(
$(document.activeElement).attr('tabindex'),
'-1',
'tab index updated when tabindex is set to -1'
);

mq.config({ tabindex: 0 });
assert.equal(
$(document.activeElement).attr('tabindex'),
'0',
'tab index restored when tabindex is set to 0'
);

mq.blur();
assertHasFocus(mq, 'math field', 'not');
});

test('tabindex for editable math', function () {
var mq = MQ.MathField($('<span></span>').appendTo('#mock')[0], {
tabindex: -1
});

mq.focus();
mq.typedText('1+1');

assertHasFocus(mq, 'math field');
assert.equal(mq.latex(), '1+1', 'latex populated');

assert.equal($(document.activeElement).attr('tabindex'), '-1');
mq.config({ tabindex: 0 });
assert.equal(
$(document.activeElement).attr('tabindex'),
'0',
'tab index updated tabindex is set to 0'
);

mq.config({ tabindex: -1 });
assert.equal(
$(document.activeElement).attr('tabindex'),
'-1',
'tab index restored when tabindex is set to -1'
);

mq.blur();
assertHasFocus(mq, 'math field', 'not');
Expand Down

0 comments on commit d1db179

Please sign in to comment.