Skip to content

Commit

Permalink
add "setTabbable" method
Browse files Browse the repository at this point in the history
this dynamically sets tabbability, and mutates tabbability
inside of mathquill, removing the "tabbable" argument to substituteTextarea.

this is a no-op if it's set the previous value.
tiny related performance optimization for setAriaLabel, returning early
if the update will be a no-op
  • Loading branch information
eluberoff committed Jan 30, 2025
1 parent 13a922b commit c32cd5e
Show file tree
Hide file tree
Showing 5 changed files with 33 additions and 20 deletions.
4 changes: 4 additions & 0 deletions docs/Api_Methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ mathField.typedText('x=-b\\pm \\sqrt b^2 -4ac');

Specify an [ARIA label][`aria-label`] for this field, for screen readers. The actual [`aria-label`] includes this label followed by the math content of the field as speech. Default: `'Math Input'`

## .setTabbable(tabbable)

Specify whether this field should be in the tab order.

## .getAriaLabel()

Returns the [ARIA label][`aria-label`] for this field, for screen readers. If no ARIA label has been specified, `'Math Input'` is returned.
Expand Down
5 changes: 1 addition & 4 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,8 +124,6 @@ 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.

## tabbable

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.
Expand Down
1 change: 1 addition & 0 deletions src/mathquill.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ declare namespace MathQuill {
latex(latex: string): this;
latex(): string;
setAriaLabel(str: string): this;
setTabbable(tabbable: boolean): this;
blur(): this;
focus(): this;
select(): this;
Expand Down
7 changes: 6 additions & 1 deletion 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 Down Expand Up @@ -304,12 +304,17 @@ function getInterface(v: number): MathQuill.v3.API | MathQuill.v1.API {
}

setAriaLabel(ariaLabel: string) {
if (ariaLabel === this.__controller.getAriaLabel()) return this;
this.__controller.setAriaLabel(ariaLabel);
return this;
}
getAriaLabel() {
return this.__controller.getAriaLabel();
}
setTabbable(tabbable: boolean) {
this.__controller.setTabbable(tabbable);
return this;
}
config(opts: ConfigOptions) {
config(this.__options, opts);
return this;
Expand Down
36 changes: 21 additions & 15 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'
});
};

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

wasTabbable: boolean | 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,27 @@ 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 tabbable =
this.options.tabbable !== undefined
? this.options.tabbable
: this.KIND_OF_MQ !== 'StaticMath';

this.setTabbable(tabbable);
}

setTabbable(tabbable: boolean) {
if (tabbable === this.wasTabbable) return;
this.wasTabbable = tabbable;

this.textarea?.setAttribute('tabindex', tabbable ? '0' : '-1');
this.textarea?.setAttribute('aria-hidden', !this.options.tabbable && this.KIND_OF_MQ === 'StaticMath' ? 'true' : 'false');
this.mathspeakSpan?.setAttribute('aria-hidden', tabbable ? 'true' : 'false');
}

selectionChanged() {
Expand Down

0 comments on commit c32cd5e

Please sign in to comment.