diff --git a/components/mjs/core/config.json b/components/mjs/core/config.json
index 7f032dbc4..87e95dc00 100644
--- a/components/mjs/core/config.json
+++ b/components/mjs/core/config.json
@@ -4,6 +4,8 @@
"targets": [
"mathjax.ts",
"core", "util", "handlers",
+ "ui/dialog/DraggableDialog.ts",
+ "ui/dialog/InfoDialog.ts",
"adaptors/HTMLAdaptor.ts",
"adaptors/browserAdaptor.ts",
"components/global.ts"
diff --git a/testsuite/tests/util/StyleJson.test.ts b/testsuite/tests/util/StyleJson.test.ts
index 84a013515..08d76dd2c 100644
--- a/testsuite/tests/util/StyleJson.test.ts
+++ b/testsuite/tests/util/StyleJson.test.ts
@@ -36,4 +36,20 @@ describe('StyleJsonSheet object', () => {
expect(styles.cssText).toBe('');
});
+ test('Compound style', () => {
+ expect(new StyleJsonSheet({
+ '@media (prefers-color-scheme: dark)': {
+ 'mjx-container': {
+ 'color': '#E0E0E0',
+ },
+ }
+ }).cssText).toBe([
+ '@media (prefers-color-scheme: dark) {',
+ ' mjx-container {',
+ ' color: #E0E0E0;',
+ ' }',
+ '}'
+ ].join('\n'));
+ });
+
});
diff --git a/ts/a11y/complexity/collapse.ts b/ts/a11y/complexity/collapse.ts
index f20db9065..24ce13109 100644
--- a/ts/a11y/complexity/collapse.ts
+++ b/ts/a11y/complexity/collapse.ts
@@ -599,7 +599,7 @@ export class Collapse {
),
},
[
- factory.create('mtext', { mathcolor: 'blue', ...variant }, [
+ factory.create('mtext', variant, [
(factory.create('text') as TextNode).setText(marker),
]),
]
diff --git a/ts/a11y/explorer.ts b/ts/a11y/explorer.ts
index c951780b3..89c03b0d3 100644
--- a/ts/a11y/explorer.ts
+++ b/ts/a11y/explorer.ts
@@ -321,6 +321,7 @@ export function ExplorerMathDocumentMixin<
public static OPTIONS: OptionList = {
...BaseDocument.OPTIONS,
enableExplorer: hasWindow, // only activate in interactive contexts
+ enableExplorerHelp: true, // help dialog is enabled
renderActions: expandable({
...BaseDocument.OPTIONS.renderActions,
explorable: [STATE.EXPLORER]
@@ -426,77 +427,20 @@ export function ExplorerMathDocumentMixin<
display: 'inline-flex',
'align-items': 'center',
},
-
- 'mjx-help-sizer': {
- position: 'fixed',
- width: '40%',
- 'max-width': '30em',
- top: '3em',
- left: '50%',
- },
- 'mjx-help-dialog': {
- position: 'absolute',
- width: '200%',
- left: '-100%',
- border: '3px outset',
- 'border-radius': '15px',
- color: 'black',
- 'background-color': '#DDDDDD',
- 'z-index': '301',
- 'text-align': 'right',
- 'font-style': 'normal',
- 'text-indent': 0,
- 'text-transform': 'none',
- 'line-height': 'normal',
- 'letter-spacing': 'normal',
- 'word-spacing': 'normal',
- 'word-wrap': 'normal',
- float: 'none',
- 'box-shadow': '0px 10px 20px #808080',
- outline: 'none',
- },
- 'mjx-help-dialog > h1': {
- 'font-size': '24px',
- 'text-align': 'center',
- margin: '.5em 0',
- },
- 'mjx-help-dialog > div': {
- margin: '0 1em',
- padding: '3px',
- overflow: 'auto',
- height: '20em',
- border: '2px inset black',
- 'background-color': 'white',
- 'text-align': 'left',
- },
- 'mjx-help-dialog > input': {
- margin: '.5em 2em',
- },
- 'mjx-help-dialog kbd': {
- display: 'inline-block',
- padding: '3px 5px',
- 'font-size': '11px',
- 'line-height': '10px',
- color: '#444d56',
- 'vertical-align': 'middle',
- 'background-color': '#fafbfc',
- border: 'solid 1.5px #c6cbd1',
- 'border-bottom-color': '#959da5',
- 'border-radius': '3px',
- 'box-shadow': 'inset -.5px -1px 0 #959da5',
- },
- 'mjx-help-dialog ul': {
- 'list-style-type': 'none',
- },
- 'mjx-help-dialog li': {
- 'margin-bottom': '.5em',
- },
- 'mjx-help-background': {
- position: 'fixed',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
+ '@media (prefers-color-scheme: dark) /* explorer */': {
+ 'mjx-help > svg': {
+ stroke: '#E0E0E0',
+ },
+ 'mjx-help > svg > circle': {
+ fill: '#404040',
+ },
+ 'mjx-help > svg > circle:nth-child(2)': {
+ fill: 'rgba(132, 132, 255, .3)',
+ },
+ 'mjx-help:hover > svg > circle:nth-child(2)': {
+ stroke: '#AAAAAA',
+ fill: '#404040',
+ },
},
};
@@ -565,7 +509,7 @@ export function ExplorerMathDocumentMixin<
SVGNS
),
]);
- this.tmpFocus = this.adaptor.node('mjx-focus', {
+ this.tmpFocus = adaptor.node('mjx-focus', {
tabIndex: 0,
style: {
outline: 'none',
diff --git a/ts/a11y/explorer/Highlighter.ts b/ts/a11y/explorer/Highlighter.ts
index 24af02e2e..c3e00869b 100644
--- a/ts/a11y/explorer/Highlighter.ts
+++ b/ts/a11y/explorer/Highlighter.ts
@@ -21,26 +21,9 @@
interface NamedColor {
color: string;
alpha?: number;
+ type?: string;
}
-interface ChannelColor {
- red: number;
- green: number;
- blue: number;
- alpha?: number;
-}
-
-const namedColors: { [key: string]: ChannelColor } = {
- red: { red: 255, green: 0, blue: 0 },
- green: { red: 0, green: 255, blue: 0 },
- blue: { red: 0, green: 0, blue: 255 },
- yellow: { red: 255, green: 255, blue: 0 },
- cyan: { red: 0, green: 255, blue: 255 },
- magenta: { red: 255, green: 0, blue: 255 },
- white: { red: 255, green: 255, blue: 255 },
- black: { red: 0, green: 0, blue: 0 },
-};
-
/**
* Turns a named color into a channel color.
*
@@ -49,30 +32,22 @@ const namedColors: { [key: string]: ChannelColor } = {
* @returns {string} The channel color.
*/
function getColorString(color: NamedColor, deflt: NamedColor): string {
- const channel = namedColors[color.color] || namedColors[deflt.color];
- channel.alpha = color.alpha ?? deflt.alpha;
- return rgba(channel);
-}
-
-/**
- * RGBa string version of the channel color.
- *
- * @param {ChannelColor} color The channel color.
- * @returns {string} The color in RGBa format.
- */
-function rgba(color: ChannelColor): string {
- return `rgba(${color.red},${color.green},${color.blue},${color.alpha ?? 1})`;
+ const type = deflt.type;
+ const name = color.color ?? deflt.color;
+ const opacity = color.alpha ?? deflt.alpha;
+ const alpha = opacity === 1 ? 1 : `var(--mjx-${type}-alpha)`;
+ return `rgba(var(--mjx-${type}-${name}), ${alpha})`;
}
/**
* The default background color if a none existing color is provided.
*/
-const DEFAULT_BACKGROUND: NamedColor = { color: 'blue', alpha: 1 };
+const DEFAULT_BACKGROUND: NamedColor = { color: 'blue', alpha: 1, type: 'bg' };
/**
* The default color if a none existing color is provided.
*/
-const DEFAULT_FOREGROUND: NamedColor = { color: 'black', alpha: 1 };
+const DEFAULT_FOREGROUND: NamedColor = { color: 'black', alpha: 1, type: 'fg' };
export interface Highlighter {
/**
diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts
index d12eee9a4..106ea23ba 100644
--- a/ts/a11y/explorer/KeyExplorer.ts
+++ b/ts/a11y/explorer/KeyExplorer.ts
@@ -30,6 +30,9 @@ import { MmlNode } from '../../core/MmlTree/MmlNode.js';
import { honk, SemAttr } from '../speech/SpeechUtil.js';
import { GeneratorPool } from '../speech/GeneratorPool.js';
import { context } from '../../util/context.js';
+import { InfoDialog } from '../../ui/dialog/InfoDialog.js';
+
+/**********************************************************************/
/**
* Interface for keyboard explorers. Adds the necessary keyboard events.
@@ -70,7 +73,7 @@ export interface KeyExplorer extends Explorer {
/**
* Type of function that implements a key press action
*/
-type keyMapping = (
+export type keyMapping = (
explorer: SpeechExplorer,
event: KeyboardEvent
) => boolean | void;
@@ -106,149 +109,162 @@ export function hasModifiers(
);
}
+/**********************************************************************/
/**********************************************************************/
/**
- * Creates a customized help dialog
+ * @class
+ * @augments {AbstractExplorer}
*
- * @param {string} title The title to use for the message
- * @param {string} select Additional ways to select the typeset math
- * @returns {string} The customized message
+ * @template T The type that is consumed by the Region of this explorer.
*/
-function helpMessage(title: string, select: string): string {
- return `
-
Exploring expressions ${title}
+export class SpeechExplorer
+ extends AbstractExplorer
+ implements KeyExplorer
+{
+ /**
+ * Creates a customized help dialog
+ *
+ * @param {string} title The title to use for the message
+ * @param {string} select Additional ways to select the typeset math
+ * @returns {string} The customized message
+ */
+ protected static helpMessage(title: string, select: string): string {
+ return `
+ Exploring expressions ${title}
-The mathematics on this page is being rendered by MathJax, which
-generates both the text spoken by screen readers, as well as the
-visual layout for sighted users.
+ The mathematics on this page is being rendered by MathJax, which
+ generates both the text spoken by screen readers, as well as the
+ visual layout for sighted users.
-Expressions typeset by MathJax can be explored interactively, and
-are focusable. You can use the Tab key to move to a typeset
-expression${select}. Initially, the expression will be read in full,
-but you can use the following keys to explore the expression
-further:
+
Expressions typeset by MathJax can be explored interactively, and
+ are focusable. You can use the Tab key to move to a typeset
+ expression${select}. Initially, the expression will be read in full,
+ but you can use the following keys to explore the expression
+ further:
-
+
-- Down Arrow moves one level deeper into the expression to
-allow you to explore the current subexpression term by term.
+ - Down Arrow moves one level deeper into the
+ expression to allow you to explore the current subexpression term by
+ term.
-- Up Arrow moves back up a level within the expression.
+ - Up Arrow moves back up a level within the
+ expression.
-- Right Arrow moves to the next term in the current
-subexpression.
+ - Right Arrow moves to the next term in the
+ current subexpression.
-- Left Arrow moves to the next term in the current
-subexpression.
+ - Left Arrow moves to the next term in the
+ current subexpression.
-- Shift+Arrow moves to a neighboring cell within a table.
+
- Shift+Arrow moves to a
+ neighboring cell within a table.
-- 0-9+0-9 jumps to a cell by its index in the table, where 0 = 10.
+
- 0-9+0-9 jumps to a cell
+ by its index in the table, where 0 = 10.
-- Home takes you to the top of the expression.
+ - Home takes you to the top of the
+ expression.
-- Enter or Return clicks a link or activates an active
-subexpression.
+ - Enter or Return clicks a
+ link or activates an active subexpression.
-- Space opens the MathJax contextual menu where you can view
-or copy the source format of the expression, or modify MathJax's
-settings.
+ - Space opens the MathJax contextual menu
+ where you can view or copy the source format of the expression, or
+ modify MathJax's settings.
-- Escape exits the expression explorer.
+ - Escape exits the expression
+ explorer.
-- x gives a summary of the current subexpression.
+ - x gives a summary of the current
+ subexpression.
-- z gives the full text of a collapsed expression.
+ - z gives the full text of a collapsed
+ expression.
-- d gives the current depth within the expression.
+ - d gives the current depth within the
+ expression.
-- s starts or stops auto-voicing with synchronized highlighting.
+ - s starts or stops auto-voicing with
+ synchronized highlighting.
-- v marks the current position in the expression.
+ - v marks the current position in the
+ expression.
-- p cycles through the marked positions in the expression.
+ - p cycles through the marked positions in
+ the expression.
-- u clears all marked positions and returns to the starting position.
+ - u clears all marked positions and returns
+ to the starting position.
-- > cycles through the available speech rule sets
-(MathSpeak, ClearSpeak).
+ - > cycles through the available speech
+ rule sets (MathSpeak, ClearSpeak).
-- < cycles through the verbosity levels for the current
-rule set.
+ - < cycles through the verbosity levels
+ for the current rule set.
-- h produces this help listing.
-
+ - h produces this help listing.
+
-The MathJax contextual menu allows you to enable or disable speech
-or Braille generation for mathematical expressions, the language to
-use for the spoken mathematics, and other features of MathJax. In
-particular, the Explorer submenu allows you to specify how the
-mathematics should be identified in the page (e.g., by saying "math"
-when the expression is spoken), and whether or not to include a
-message about the letter "h" bringing up this dialog box.
+ The MathJax contextual menu allows you to enable or disable speech
+ or Braille generation for mathematical expressions, the language to
+ use for the spoken mathematics, and other features of MathJax. In
+ particular, the Explorer submenu allows you to specify how the
+ mathematics should be identified in the page (e.g., by saying "math"
+ when the expression is spoken), and whether or not to include a
+ message about the letter "h" bringing up this dialog box. Turning off
+ speech and Braille will disable the expression explorer, its
+ highlighting, and its help icon.
-The contextual menu also provides options for viewing or copying a
-MathML version of the expression or its original source format,
-creating an SVG version of the expression, and viewing various other
-information.
+ The contextual menu also provides options for viewing or copying a
+ MathML version of the expression or its original source format,
+ creating an SVG version of the expression, and viewing various other
+ information.
-For more help, see the MathJax accessibility documentation.
-`;
-}
+ For more help, see the MathJax accessibility documentation.
+ `;
+ }
-/**
- * Help for the different OS versions
- */
-const helpData: Map = new Map([
- [
- 'MacOS',
+ /**
+ * Help for the different OS versions
+ */
+ protected static helpData: Map = new Map([
[
- 'on MacOS and iOS using VoiceOver',
- ', or the VoiceOver arrow keys to select an expression',
+ 'MacOS',
+ [
+ 'on MacOS and iOS using VoiceOver',
+ ', or the VoiceOver arrow keys to select an expression',
+ ],
],
- ],
- [
- 'Windows',
[
- 'in Windows using NVDA or JAWS',
- `. The screen reader should enter focus or forms mode automatically
-when the expression gets the browser focus, but if not, you can toggle
-focus mode using NVDA+space in NVDA; for JAWS, Enter should start
-forms mode while Numpad Plus leaves it. Also note that you can use
-the NVDA or JAWS key plus the arrow keys to explore the expression
-even in browse mode, and you can use NVDA+shift+arrow keys to
-navigate out of an expression that has the focus in NVDA`,
+ 'Windows',
+ [
+ 'in Windows using NVDA or JAWS',
+ `. The screen reader should enter focus or forms mode automatically
+ when the expression gets the browser focus, but if not, you can toggle
+ focus mode using NVDA+space in NVDA; for JAWS, Enter should start
+ forms mode while Numpad Plus leaves it. Also note that you can use
+ the NVDA or JAWS key plus the arrow keys to explore the expression
+ even in browse mode, and you can use NVDA+shift+arrow keys to
+ navigate out of an expression that has the focus in NVDA`,
+ ],
],
- ],
- [
- 'Unix',
[
- 'in Unix using Orca',
- `, and Orca should enter focus mode automatically. If not, use the
-Orca+a key to toggle focus mode on or off. Also note that you can use
-Orca+arrow keys to explore expressions even in browse mode`,
+ 'Unix',
+ [
+ 'in Unix using Orca',
+ `, and Orca should enter focus mode automatically. If not, use the
+ Orca+a key to toggle focus mode on or off. Also note that you can use
+ Orca+arrow keys to explore expressions even in browse mode`,
+ ],
],
- ],
- ['unknown', ['with a Screen Reader.', '']],
-]);
-
-/**********************************************************************/
-/**********************************************************************/
+ ['unknown', ['with a Screen Reader.', '']],
+ ]);
-/**
- * @class
- * @augments {AbstractExplorer}
- *
- * @template T The type that is consumed by the Region of this explorer.
- */
-export class SpeechExplorer
- extends AbstractExplorer
- implements KeyExplorer
-{
/*
* The explorer key mapping
*/
@@ -621,8 +637,13 @@ export class SpeechExplorer
/**
* Open the help dialog, and refocus when it closes.
+ *
+ * @returns {boolean | void} True cancels the event
*/
- protected hKey() {
+ protected hKey(): boolean | void {
+ if (!this.document.options.enableExplorerHelp) {
+ return true;
+ }
this.refocus = this.current;
this.help();
}
@@ -984,40 +1005,36 @@ export class SpeechExplorer
* Displays the help dialog.
*/
protected help() {
- const adaptor = this.document.adaptor;
- const helpBackground = adaptor.node('mjx-help-background');
- const close = (event: Event) => {
- helpBackground.remove();
- this.node.focus();
- this.stopEvent(event);
- };
- helpBackground.addEventListener('click', close);
- const helpSizer = adaptor.node('mjx-help-sizer', {}, [
- adaptor.node(
- 'mjx-help-dialog',
- { tabindex: 0, role: 'dialog', 'aria-labeledby': 'mjx-help-label' },
- [
- adaptor.node('h1', { id: 'mjx-help-label' }, [
- adaptor.text('MathJax Expression Explorer Help'),
- ]),
- adaptor.node('div'),
- adaptor.node('input', { type: 'button', value: 'Close' }),
- ]
- ),
- ]);
- helpBackground.append(helpSizer);
- const help = helpSizer.firstChild as HTMLElement;
- help.addEventListener('click', (event) => this.stopEvent(event));
- help.lastChild.addEventListener('click', close);
- help.addEventListener('keydown', (event: KeyboardEvent) => {
- if (event.code === 'Escape') {
- close(event);
- }
+ if (!this.document.options.enableExplorerHelp) {
+ return;
+ }
+ const CLASS = this.constructor as typeof SpeechExplorer;
+ const [title, select] = CLASS.helpData.get(context.os);
+ InfoDialog.post({
+ title: 'MathJax Expression Explorer Help',
+ message: CLASS.helpMessage(title, select),
+ node: this.node,
+ adaptor: this.document.adaptor,
+ styles: {
+ '.mjx-dialog': {
+ 'max-height': 'calc(min(35em, 90%))',
+ },
+ 'mjx-dialog mjx-title': {
+ 'font-size': '133%',
+ margin: '.5em 1.75em',
+ },
+ 'mjx-dialog h2': {
+ 'font-size': '20px',
+ margin: '.5em 0',
+ },
+ 'mjx-dialog ul': {
+ 'list-style-type': 'none',
+ },
+ 'mjx-dialog li': {
+ 'margin-bottom': '.5em',
+ },
+ },
});
- const [title, select] = helpData.get(context.os);
- (help.childNodes[1] as HTMLElement).innerHTML = helpMessage(title, select);
- document.body.append(helpBackground);
- help.focus();
}
/********************************************************************/
@@ -1157,7 +1174,10 @@ export class SpeechExplorer
if (describe) {
let description =
this.description === this.none ? '' : ', ' + this.description;
- if (this.document.options.a11y.help) {
+ if (
+ this.document.options.a11y.help &&
+ this.document.options.enableExplorerHelp
+ ) {
description += ', press h for help';
}
speech += description;
@@ -1717,7 +1737,9 @@ export class SpeechExplorer
// and add the info icon.
//
this.node.classList.add('mjx-explorer-active');
- this.node.append(this.document.infoIcon);
+ if (this.document.options.enableExplorerHelp) {
+ this.node.append(this.document.infoIcon);
+ }
//
// Get the node to make current, and determine if we need to add a
// speech node (or just use the top-level node), then set the
@@ -1753,7 +1775,9 @@ export class SpeechExplorer
this.node.setAttribute('aria-roledescription', description);
}
this.node.classList.remove('mjx-explorer-active');
- this.document.infoIcon.remove();
+ if (this.document.options.enableExplorerHelp) {
+ this.document.infoIcon.remove();
+ }
this.pool.unhighlight();
this.magnifyRegion.Hide();
this.region.Hide();
diff --git a/ts/a11y/explorer/Region.ts b/ts/a11y/explorer/Region.ts
index 109a42d2b..1303fb7f2 100644
--- a/ts/a11y/explorer/Region.ts
+++ b/ts/a11y/explorer/Region.ts
@@ -73,13 +73,6 @@ export abstract class AbstractRegion implements Region {
*/
protected static className: string;
- /**
- * True if the style has already been added to the document.
- *
- * @type {boolean}
- */
- protected static styleAdded: boolean = false;
-
/**
* The CSS style that needs to be added for this type of region.
*
@@ -117,20 +110,27 @@ export abstract class AbstractRegion implements Region {
this.AddStyles();
}
+ /**
+ * @returns {string} The stylesheet ID
+ */
+ public static get sheetId(): string {
+ return 'MJX-' + this.name + '-styles';
+ }
+
/**
* @override
*/
public AddStyles() {
- if (this.CLASS.styleAdded) {
+ const id = this.CLASS.sheetId;
+ if (
+ !this.CLASS.style ||
+ this.document.adaptor.head().querySelector('#' + id)
+ ) {
return;
}
- // TODO: should that be added to document.documentStyleSheet()?
- const node = this.document.adaptor.node('style');
+ const node = this.document.adaptor.node('style', { id });
node.innerHTML = this.CLASS.style.cssText;
- this.document.adaptor
- .head(this.document.adaptor.document)
- .appendChild(node);
- this.CLASS.styleAdded = true;
+ this.document.adaptor.head().appendChild(node);
}
/**
@@ -177,7 +177,7 @@ export abstract class AbstractRegion implements Region {
*/
public Hide() {
if (!this.div) return;
- this.div.parentNode.removeChild(this.div);
+ this.div.remove();
this.div = null;
this.inner = null;
}
@@ -335,6 +335,13 @@ export class ToolTip extends StringRegion {
'border-radius': 'inherit',
padding: '0 2px',
},
+ '@media (prefers-color-scheme: dark)': {
+ ['.' + ToolTip.className]: {
+ 'background-color': '#222025',
+ 'box-shadow': '0px 5px 20px #000',
+ border: '1px solid #7C7C7C',
+ },
+ },
});
}
@@ -348,6 +355,43 @@ export class LiveRegion extends StringRegion {
* @override
*/
protected static style: StyleJsonSheet = new StyleJsonSheet({
+ ':root': {
+ '--mjx-fg-red': '255, 0, 0',
+ '--mjx-fg-green': '0, 255, 0',
+ '--mjx-fg-blue': '0, 0, 255',
+ '--mjx-fg-yellow': '255, 255, 0',
+ '--mjx-fg-cyan': '0, 255, 255',
+ '--mjx-fg-magenta': '255, 0, 255',
+ '--mjx-fg-white': '255, 255, 255',
+ '--mjx-fg-black': '0, 0, 0',
+ '--mjx-bg-red': '255, 0, 0',
+ '--mjx-bg-green': '0, 255, 0',
+ '--mjx-bg-blue': '0, 0, 255',
+ '--mjx-bg-yellow': '255, 255, 0',
+ '--mjx-bg-cyan': '0, 255, 255',
+ '--mjx-bg-magenta': '255, 0, 255',
+ '--mjx-bg-white': '255, 255, 255',
+ '--mjx-bg-black': '0, 0, 0',
+ '--mjx-live-bg-color': 'white',
+ '--mjx-live-shadow-color': '#888',
+ '--mjx-live-border-color': '#CCCCCC',
+ '--mjx-bg-alpha': 0.2,
+ '--mjx-fg-alpha': 1,
+ },
+ '@media (prefers-color-scheme: dark)': {
+ ':root': {
+ '--mjx-bg-blue': '132, 132, 255',
+ '--mjx-bg-white': '0, 0, 0',
+ '--mjx-bg-black': '255, 255, 255',
+ '--mjx-fg-white': '0, 0, 0',
+ '--mjx-fg-black': '255, 255, 255',
+ '--mjx-live-bg-color': '#222025',
+ '--mjx-live-shadow-color': 'black',
+ '--mjx-live-border-color': '#7C7C7C',
+ '--mjx-bg-alpha': 0.3,
+ '--mjx-fg-alpha': 1,
+ },
+ },
['.' + LiveRegion.className]: {
position: 'absolute',
top: 0,
@@ -360,20 +404,41 @@ export class LiveRegion extends StringRegion {
left: 0,
right: 0,
margin: '0 auto',
- 'background-color': 'white',
- 'box-shadow': '0px 5px 20px #888',
- border: '2px solid #CCCCCC',
+ 'background-color': 'var(--mjx-live-bg-color)',
+ 'box-shadow': '0px 5px 20px var(--mjx-live-shadow-color)',
+ border: '2px solid var(--mjx-live-border-color)',
},
['.' + LiveRegion.className + '_Show']: {
display: 'block',
},
});
+
+ /**
+ * @param {string} type The type of alpha to set (fg or bg)
+ * @param {number} alpha The alpha value to use
+ * @param {Document} document The document whose CSS styles are to be adjusted
+ */
+ public static setAlpha(type: string, alpha: number, document: Document) {
+ const style = document.head.querySelector(
+ '#' + this.sheetId
+ ) as HTMLStyleElement;
+ if (style) {
+ const name = `--mjx-${type}-alpha`;
+ (style.sheet.cssRules[0] as any).style.setProperty(name, alpha);
+ (style.sheet.cssRules[1] as any).cssRules[0].style.setProperty(
+ name,
+ alpha ** 0.7071
+ );
+ }
+ }
}
/**
* Region class that enables auto voicing of content via SSML markup.
*/
export class SpeechRegion extends LiveRegion {
+ protected static style: StyleJsonSheet = null;
+
/**
* Flag to activate auto voicing.
*/
@@ -583,6 +648,13 @@ export class HoverRegion extends AbstractRegion {
['.' + HoverRegion.className + ' > div']: {
overflow: 'hidden',
},
+ '@media (prefers-color-scheme: dark)': {
+ ['.' + HoverRegion.className]: {
+ 'background-color': '#222025',
+ 'box-shadow': '0px 5px 20px #000',
+ border: '1px solid #7C7C7C',
+ },
+ },
});
/**
diff --git a/ts/a11y/speech/SpeechMenu.ts b/ts/a11y/speech/SpeechMenu.ts
index 97663197d..e76a927cc 100644
--- a/ts/a11y/speech/SpeechMenu.ts
+++ b/ts/a11y/speech/SpeechMenu.ts
@@ -22,7 +22,12 @@
*/
import { ExplorerMathItem } from '../explorer.js';
-import { MJContextMenu } from '../../ui/menu/MJContextMenu.js';
+import { MJContextMenu, SubmenuCallback } from '../../ui/menu/MJContextMenu.js';
+import {
+ SelectionDialog,
+ SelectionOrder,
+ SelectionGrid,
+} from '../../ui/dialog/SelectionDialog.js';
import { SubMenu, Submenu } from '../../ui/menu/mj-context-menu.js';
import * as Sre from '../sre.js';
@@ -113,7 +118,7 @@ let counter = 0;
function csSelectionBox(menu: MJContextMenu, locale: string): object {
const props = localePreferences.get(locale);
csPrefsVariables(menu, Object.keys(props));
- const items = [];
+ const items: any[] = [];
for (const prop of Object.getOwnPropertyNames(props)) {
items.push({
title: prop,
@@ -121,22 +126,19 @@ function csSelectionBox(menu: MJContextMenu, locale: string): object {
variable: 'csprf_' + prop,
});
}
- const sb = menu.factory.get('selectionBox')(
- menu.factory,
- {
- title: 'Clearspeak Preferences',
- signature: '',
- order: 'alphabetic',
- grid: 'square',
- selections: items,
- },
+ const sb = new SelectionDialog(
+ 'Clearspeak Preferences',
+ '',
+ items,
+ SelectionOrder.ALPHABETICAL,
+ SelectionGrid.SQUARE,
menu
);
return {
type: 'command',
id: 'ClearspeakPreferences',
content: 'Select Preferences',
- action: () => sb.post(0, 0),
+ action: () => sb.post(),
};
}
@@ -223,13 +225,13 @@ function smartPreferences(
*
* @param {MJContextMenu} menu The context menu.
* @param {Submenu} sub The submenu to attach elements to.
- * @param {(sub: SubMenu) => void} callback Callback to apply on the constructed
+ * @param {SubmenuCallback} callback Callback to apply on the constructed
* submenu.
*/
export async function clearspeakMenu(
menu: MJContextMenu,
sub: Submenu,
- callback: (sub: SubMenu) => void
+ callback: SubmenuCallback
) {
const exit = (items: object[]) => {
callback(
@@ -288,13 +290,13 @@ let LOCALE_MENU: SubMenu = null;
*
* @param {MJContextMenu} menu The context menu.
* @param {Submenu} sub The submenu to attach elements to.
- * @param {(sub: SubMenu) => void} callback Callback to apply on the constructed
+ * @param {SubmenuCallback} callback Callback to apply on the constructed
* submenu.
*/
export function localeMenu(
menu: MJContextMenu,
sub: Submenu,
- callback: (sub: SubMenu) => void
+ callback: SubmenuCallback
) {
if (LOCALE_MENU) {
callback(LOCALE_MENU);
diff --git a/ts/output/chtml.ts b/ts/output/chtml.ts
index 82711afdd..f2edddf5c 100644
--- a/ts/output/chtml.ts
+++ b/ts/output/chtml.ts
@@ -165,6 +165,11 @@ export class CHTML extends CommonOutputJax<
'mjx-container .mjx-selected': {
outline: '2px solid black',
},
+ '@media (prefers-color-scheme: dark)': {
+ 'mjx-container .mjx-selected': {
+ outline: '2px solid #C8C8C8',
+ },
+ },
//
// These don't have Wrapper subclasses, so add their styles here
diff --git a/ts/output/chtml/Wrappers/maction.ts b/ts/output/chtml/Wrappers/maction.ts
index eddafb694..9f66e54a8 100644
--- a/ts/output/chtml/Wrappers/maction.ts
+++ b/ts/output/chtml/Wrappers/maction.ts
@@ -188,6 +188,25 @@ export const ChtmlMaction = (function (): ChtmlMactionClass {
'background-color': '#F8F8F8',
color: 'black',
},
+ 'mjx-maction[data-collapsible][toggle="1"]': {
+ color: '#55F',
+ },
+
+ '@media (prefers-color-scheme: dark) /* chtml maction */': {
+ 'mjx-tool > mjx-tip': {
+ border: '1px solid #888',
+ 'background-color': '#303030',
+ color: '#E0E0E0',
+ 'box-shadow': '2px 2px 5px #000',
+ },
+ 'mjx-status': {
+ 'background-color': '#303030',
+ color: '#E0E0E0',
+ },
+ 'mjx-maction[data-collapsible][toggle="1"]': {
+ color: '#88F',
+ },
+ },
};
/**
diff --git a/ts/output/svg.ts b/ts/output/svg.ts
index 1e1113f0e..b722332d5 100644
--- a/ts/output/svg.ts
+++ b/ts/output/svg.ts
@@ -117,6 +117,14 @@ export class SVG extends CommonOutputJax<
stroke: 'black',
'stroke-width': '80px',
},
+ '@media (prefers-color-scheme: dark)': {
+ [[
+ 'rect[data-sre-highlighter-added]:has(+ .mjx-selected)',
+ 'rect[data-sre-highlighter-bbox].mjx-selected',
+ ].join(', ')]: {
+ stroke: '#C8C8C8',
+ },
+ },
};
/**
diff --git a/ts/output/svg/Wrappers/maction.ts b/ts/output/svg/Wrappers/maction.ts
index 0a551cce2..298c40f11 100644
--- a/ts/output/svg/Wrappers/maction.ts
+++ b/ts/output/svg/Wrappers/maction.ts
@@ -187,6 +187,24 @@ export const SvgMaction = (function (): SvgMactionClass {
'background-color': '#F8F8F8',
color: 'black',
},
+ 'g[data-mml-node="maction"][data-collapsible][data-toggle="1"]': {
+ fill: '#55F',
+ },
+
+ '@media (prefers-color-scheme: dark) /* svg maction */': {
+ 'mjx-tool > mjx-tip': {
+ 'background-color': '#303030',
+ color: '#E0E0E0',
+ 'box-shadow': '2px 2px 5px #000',
+ },
+ 'mjx-status': {
+ 'background-color': '#303030',
+ color: '#E0E0E0',
+ },
+ 'g[data-mml-node="maction"][data-collapsible][data-toggle="1"]': {
+ fill: '#88F',
+ },
+ },
};
/**
diff --git a/ts/ui/dialog/CopyDialog.ts b/ts/ui/dialog/CopyDialog.ts
new file mode 100644
index 000000000..d31caebc3
--- /dev/null
+++ b/ts/ui/dialog/CopyDialog.ts
@@ -0,0 +1,77 @@
+/*************************************************************
+ *
+ * Copyright (c) 2025 The MathJax Consortium
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @file Implements the CopyDialog class (InfoDialog with copy button).
+ *
+ * @author dpvc@mathjax.org (Davide Cervone)
+ */
+
+import { InfoDialog, InfoDialogArgs } from './InfoDialog.js';
+
+/**
+ * The args for a CopyDialog
+ */
+export type CopyDialogArgs = InfoDialogArgs & { code?: boolean };
+
+/**
+ * The CopyDialog subclass of InfoDialog
+ */
+export class CopyDialog extends InfoDialog {
+ /**
+ * @override
+ */
+ public static post(args: CopyDialogArgs) {
+ return super.post(args);
+ }
+
+ /**
+ * @override
+ */
+ protected html(args: CopyDialogArgs) {
+ //
+ // Add a copy-to-clipboard button
+ //
+ args.extraNodes ??= [];
+ const copy = args.adaptor.node('input', {
+ type: 'button',
+ value: 'Copy to Clipboard',
+ 'data-drag': 'none',
+ });
+ copy.addEventListener('click', this.copyToClipboard.bind(this));
+ args.extraNodes.push(copy);
+ //
+ // If this is a code dialog, format the source and set in a pre element
+ //
+ if (args.code) {
+ args.message = '' + this.formatSource(args.message) + '
';
+ }
+ return super.html(args);
+ }
+
+ /**
+ * @param {string} text The text to be displayed in the Info box
+ * @returns {string} The text with HTML specials being escaped
+ */
+ protected formatSource(text: string): string {
+ return text
+ .trim()
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+ }
+}
diff --git a/ts/ui/dialog/DraggableDialog.ts b/ts/ui/dialog/DraggableDialog.ts
new file mode 100644
index 000000000..e7090e898
--- /dev/null
+++ b/ts/ui/dialog/DraggableDialog.ts
@@ -0,0 +1,1156 @@
+/*************************************************************
+ *
+ * Copyright (c) 2025 The MathJax Consortium
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @file Implements a draggable dialog class.
+ *
+ * @author dpvc@mathjax.org (Davide Cervone)
+ */
+
+import { DOMAdaptor } from '../../core/DOMAdaptor.js';
+import { StyleJson, StyleJsonSheet } from '../../util/StyleJson.js';
+import { context } from '../../util/context.js';
+
+export type ADAPTOR = DOMAdaptor;
+
+export type Action = (
+ dialog: DraggableDialog,
+ event: MouseEvent
+) => void | number[];
+export type ActionList = { [action: string]: Action };
+export type ActionMap = { [type: string]: ActionList };
+
+/**
+ * The arguments that control the dialog contents
+ */
+export type DialogArgs = {
+ title?: string; // // the dialog title HTML
+ message: string; // // the dialog message HTML
+ adaptor: ADAPTOR; // // the adaptor to use to create the dialog
+ node?: HTMLElement; // // the node to focus when the dialog closes, if any
+ styles?: StyleJson; // // extra styles to use for the dialog
+ extraNodes?: HTMLElement[]; // // extra HTML nodes to put at the bottom of the dialog
+ className?: string; // // optional class to apply to the dialog
+};
+
+/**
+ * Type of function that implements a key press action
+ */
+export type keyMapping = (
+ dialog: DraggableDialog,
+ event: KeyboardEvent
+) => void;
+
+/**
+ * True if we can rely on an HTML dialog element.
+ */
+export const isDialog: boolean = !!context.window?.HTMLDialogElement;
+
+/*========================================================================*/
+
+/**
+ * The draggable dialog class
+ */
+export class DraggableDialog {
+ /**
+ * The minimum width of the dialog
+ */
+ protected minW = 200;
+ /**
+ * The maximum width of the dialog
+ */
+ protected minH = 80;
+
+ /**
+ * The current x translation of the dialog
+ */
+ protected tx: number = 0;
+ /**
+ * The current y translation of the dialog
+ */
+ protected ty: number = 0;
+
+ /**
+ * The current mouse x position
+ */
+ protected x: number;
+ /**
+ * The current mouse y position
+ */
+ protected y: number;
+ /**
+ * The current dialog width
+ */
+ protected w: number;
+ /**
+ * The current dialog height
+ */
+ protected h: number;
+ /**
+ * True when the dialog is being dragged or sized
+ */
+ protected dragging: boolean = false;
+ /**
+ * The drag acction being taken (move, left, right, top, bottom, etc.)
+ */
+ protected action: string;
+
+ /**
+ * Elements where clicking doesn't cause dragging
+ */
+ protected noDrag: HTMLElement[];
+ /**
+ * The title element
+ */
+ protected title: HTMLElement;
+ /**
+ * The content div element
+ */
+ protected content: HTMLElement;
+
+ /**
+ * The node to focus when dialog closes
+ */
+ protected node: HTMLElement;
+ /**
+ * The background element when