@pie-lib/math-input-mathlive 1.1.4 → 1.1.5-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +14 -5
  2. package/lib/__tests__/composites.test.js +255 -0
  3. package/lib/__tests__/drop-in-compat.test.js +133 -0
  4. package/lib/__tests__/embedded-fonts.test.js +121 -0
  5. package/lib/__tests__/index.test.js +118 -0
  6. package/lib/__tests__/input-imperative.test.js +155 -0
  7. package/lib/__tests__/keypad-label.test.js +231 -0
  8. package/lib/__tests__/keypad-render.test.js +206 -0
  9. package/lib/__tests__/latex-bridge.test.js +386 -0
  10. package/lib/__tests__/mathlive-instance.test.js +419 -0
  11. package/lib/__tests__/mount-race.test.js +218 -0
  12. package/lib/__tests__/static-prompts.test.js +405 -0
  13. package/lib/__tests__/tmp-markup.test.js +26 -0
  14. package/lib/keypad/index.js +50 -28
  15. package/lib/keypad/index.js.map +1 -1
  16. package/lib/keys/geometry.js +7 -4
  17. package/lib/keys/geometry.js.map +1 -1
  18. package/lib/keys/misc.js +9 -2
  19. package/lib/keys/misc.js.map +1 -1
  20. package/lib/latex-bridge.js +207 -7
  21. package/lib/latex-bridge.js.map +1 -1
  22. package/lib/mathlive-instance.js +176 -12
  23. package/lib/mathlive-instance.js.map +1 -1
  24. package/lib/mf/common-styles.js +89 -10
  25. package/lib/mf/common-styles.js.map +1 -1
  26. package/lib/mf/index.js +12 -0
  27. package/lib/mf/index.js.map +1 -1
  28. package/lib/mf/input.js +34 -5
  29. package/lib/mf/input.js.map +1 -1
  30. package/lib/mf/static.js +107 -7
  31. package/lib/mf/static.js.map +1 -1
  32. package/package.json +3 -4
  33. package/src/__tests__/composites.test.jsx +220 -0
  34. package/src/__tests__/keypad-label.test.jsx +229 -2
  35. package/src/__tests__/keypad-render.test.jsx +123 -0
  36. package/src/__tests__/latex-bridge.test.js +159 -0
  37. package/src/__tests__/mathlive-instance.test.js +237 -0
  38. package/src/__tests__/mount-race.test.jsx +142 -0
  39. package/src/__tests__/static-prompts.test.jsx +364 -0
  40. package/src/keypad/index.jsx +33 -17
  41. package/src/keys/geometry.js +7 -2
  42. package/src/keys/misc.js +8 -2
  43. package/src/latex-bridge.js +223 -7
  44. package/src/mathlive-instance.js +196 -11
  45. package/src/mf/common-styles.js +97 -8
  46. package/src/mf/index.js +4 -0
  47. package/src/mf/input.jsx +42 -5
  48. package/src/mf/static.jsx +124 -6
  49. package/CHANGELOG.md +0 -30
  50. package/LICENSE.md +0 -5
@@ -0,0 +1,142 @@
1
+ /**
2
+ * MathLive loads asynchronously, so `componentDidMount` resumes after an await
3
+ * before appending a <math-field>. Two orderings must both hold:
4
+ *
5
+ * - StrictMode dev double-mount (mount -> unmount -> mount on the SAME
6
+ * instance) must end up with exactly ONE field. A boolean "unmounted" flag
7
+ * latches here and blocks the remount entirely - which is why this uses a
8
+ * generation counter.
9
+ * - A genuine unmount while the load is in flight must append nothing.
10
+ *
11
+ * MathLive cannot load under jsdom, so it is mocked with a minimal stub in
12
+ * order to actually count appended fields.
13
+ */
14
+ jest.mock('../mathlive-instance', () => {
15
+ // Must be a real DOM node: the component uses replaceChildren(), which
16
+ // rejects anything that is not a Node. Returning an element from the
17
+ // constructor replaces `this`, so callers get a genuine <math-field>.
18
+ class FakeMathfieldElement {
19
+ constructor() {
20
+ // `global` rather than `document`: jest forbids out-of-scope variables in
21
+ // a mock factory, and `global` is on its allow-list.
22
+ const el = global.document.createElement('math-field');
23
+
24
+ el.value = '';
25
+ el.getValue = () => el.value;
26
+
27
+ return el;
28
+ }
29
+ }
30
+
31
+ return {
32
+ loadMathLive: () => Promise.resolve({ MathfieldElement: FakeMathfieldElement }),
33
+ getMacros: () => ({}),
34
+ // must mirror the real export, otherwise the component sets `undefined`
35
+ MATH_MODE_SPACE: '\\ ',
36
+ applyShadowStyles: () => {},
37
+ trackPlaceholderCaret: () => () => {},
38
+ latexToMarkup: (l) => `<span>${l}</span>`,
39
+ };
40
+ });
41
+
42
+ // eslint-disable-next-line import/first
43
+ import Input from '../mf/input';
44
+
45
+ const holderFor = (cmp) => {
46
+ const el = document.createElement('span');
47
+
48
+ cmp.holderRef = { current: el };
49
+
50
+ return el;
51
+ };
52
+
53
+ const fieldCount = (el) => Array.from(el.childNodes).filter((n) => n.tagName === 'MATH-FIELD').length;
54
+
55
+ describe('async mount race', () => {
56
+ it('StrictMode mount -> unmount -> mount yields exactly one field', async () => {
57
+ const input = new Input({});
58
+ const el = holderFor(input);
59
+
60
+ const first = input.componentDidMount();
61
+
62
+ input.componentWillUnmount();
63
+
64
+ const second = input.componentDidMount();
65
+
66
+ await Promise.all([first, second]);
67
+
68
+ expect(fieldCount(el)).toBe(1);
69
+ expect(input.mathField).toBeDefined();
70
+ });
71
+
72
+ it('appends nothing when unmounted while the load is in flight', async () => {
73
+ const input = new Input({});
74
+ const el = holderFor(input);
75
+
76
+ const pending = input.componentDidMount();
77
+
78
+ input.componentWillUnmount();
79
+
80
+ await pending;
81
+
82
+ expect(fieldCount(el)).toBe(0);
83
+ expect(input.mathField).toBeUndefined();
84
+ });
85
+
86
+ it('a normal single mount creates the field', async () => {
87
+ const input = new Input({});
88
+ const el = holderFor(input);
89
+
90
+ await input.componentDidMount();
91
+
92
+ expect(fieldCount(el)).toBe(1);
93
+ });
94
+
95
+ it('repeated mounts never accumulate fields', async () => {
96
+ const input = new Input({});
97
+ const el = holderFor(input);
98
+
99
+ await input.componentDidMount();
100
+ await input.componentDidMount();
101
+ await input.componentDidMount();
102
+
103
+ expect(fieldCount(el)).toBe(1);
104
+ });
105
+
106
+ // MathLive's mathModeSpace defaults to '', which makes the spacebar a no-op.
107
+ it('configures the spacebar so a space can be typed', async () => {
108
+ const input = new Input({});
109
+
110
+ holderFor(input);
111
+ await input.componentDidMount();
112
+
113
+ // a LaTeX control space - what MathQuill produced, and MathJax understands
114
+ expect(input.mathField.mathModeSpace).toEqual('\\ ');
115
+ expect(input.mathField.mathModeSpace).not.toEqual('');
116
+ });
117
+
118
+ // MathLive leaves a superscript after one digit by default (`smartSuperscript`),
119
+ // so x^23 / 10^6 cannot be typed.
120
+ it('stays in the exponent after a digit instead of jumping out', async () => {
121
+ const input = new Input({});
122
+
123
+ holderFor(input);
124
+ await input.componentDidMount();
125
+
126
+ expect(input.mathField.smartSuperscript).toBe(false);
127
+ });
128
+
129
+ it('reports itself through innerRef on mount and clears it on unmount', async () => {
130
+ const innerRef = jest.fn();
131
+ const input = new Input({ innerRef });
132
+
133
+ holderFor(input);
134
+ await input.componentDidMount();
135
+
136
+ expect(innerRef).toHaveBeenCalledWith(input);
137
+
138
+ input.componentWillUnmount();
139
+
140
+ expect(innerRef).toHaveBeenLastCalledWith(null);
141
+ });
142
+ });
@@ -0,0 +1,364 @@
1
+ import Static from '../mf/static';
2
+
3
+ /**
4
+ * The answer-block contract: `\MathQuillMathField[id]{}` in stored latex becomes
5
+ * a MathLive prompt, and sub-field values/focus are surfaced through the same
6
+ * callbacks @pie-lib/math-input exposed (`onSubFieldChange`, `onSubFieldFocus`,
7
+ * `getFieldName`, `setInput`).
8
+ *
9
+ * Driven against a stub mathfield: a real <math-field> needs a browser, and what
10
+ * matters here is the mapping, not MathLive's rendering.
11
+ */
12
+ const stubField = (values = {}, selection = null) => ({
13
+ values: { ...values },
14
+ getPrompts() {
15
+ return Object.keys(this.values);
16
+ },
17
+ getPromptValue(id) {
18
+ return this.values[id];
19
+ },
20
+ setPromptValue(id, v) {
21
+ this.values[id] = v;
22
+ },
23
+ getPromptRange(id) {
24
+ const i = Object.keys(this.values).indexOf(id);
25
+
26
+ return [i * 10, i * 10 + 5];
27
+ },
28
+ selection,
29
+ // Caret bookkeeping: MathLive exposes the caret offset as `position`, and any
30
+ // executeCommand triggers the render that reveals the caret.
31
+ position: undefined,
32
+ commands: [],
33
+ executeCommand(name) {
34
+ this.commands.push(name);
35
+ },
36
+ addEventListener() {},
37
+ removeEventListener() {},
38
+ focus() {
39
+ this.focused = true;
40
+ },
41
+ blur() {
42
+ this.blurred = true;
43
+ },
44
+ remove() {},
45
+ });
46
+
47
+ const make = (props = {}) => new Static({ latex: 'x=\\MathQuillMathField[r1]{}', getFieldName: () => {}, ...props });
48
+
49
+ describe('Static: answer blocks / prompts', () => {
50
+ describe('isInteractive', () => {
51
+ it('is interactive when the latex has answer blocks', () => {
52
+ expect(make({ latex: 'x=\\MathQuillMathField[r1]{}' }).isInteractive()).toBe(true);
53
+ expect(make({ latex: 'x=\\placeholder[r1]{}' }).isInteractive()).toBe(true);
54
+ });
55
+
56
+ it('is display-only for plain latex', () => {
57
+ expect(make({ latex: '\\frac{1}{2}' }).isInteractive()).toBe(false);
58
+ expect(make({ latex: '' }).isInteractive()).toBe(false);
59
+ });
60
+ });
61
+
62
+ describe('values', () => {
63
+ it('returns {} before the field exists', () => {
64
+ expect(make().values()).toEqual({});
65
+ });
66
+
67
+ it('maps each prompt id to its stored-form latex', () => {
68
+ const s = make();
69
+
70
+ s.mathField = stubField({ r1: '7', r2: '\\placeholder{}' });
71
+
72
+ // \placeholder must not leak into stored values
73
+ expect(s.values()).toEqual({ r1: '7', r2: '' });
74
+ });
75
+ });
76
+
77
+ describe('promptHandle / innerFields', () => {
78
+ it('exposes a MathQuill-field-like handle with get/set latex', () => {
79
+ const s = make();
80
+
81
+ s.mathField = stubField({ r1: '7' });
82
+
83
+ const h = s.promptHandle('r1');
84
+
85
+ expect(h.id).toEqual('r1');
86
+ expect(h.latex()).toEqual('7');
87
+
88
+ h.latex('9');
89
+ expect(s.mathField.values.r1).toEqual('9');
90
+ });
91
+
92
+ it('handle latex() is safe with no field', () => {
93
+ expect(make().promptHandle('r1').latex()).toEqual('');
94
+ });
95
+
96
+ it('innerFields returns one handle per prompt', () => {
97
+ const s = make();
98
+
99
+ s.mathField = stubField({ r1: '1', r2: '2' });
100
+
101
+ expect(s.innerFields.map((f) => f.id)).toEqual(['r1', 'r2']);
102
+ expect(make().innerFields).toEqual([]);
103
+ });
104
+ });
105
+
106
+ describe('onSubFieldChange', () => {
107
+ it('fires only for prompts whose value changed', () => {
108
+ const onSubFieldChange = jest.fn();
109
+ const s = make({ onSubFieldChange });
110
+
111
+ s.mathField = stubField({ r1: '1', r2: '2' });
112
+ s.promptValues = { r1: '1', r2: '2' };
113
+
114
+ s.mathField.values.r2 = '5';
115
+ s.onPromptInput();
116
+
117
+ expect(onSubFieldChange).toHaveBeenCalledTimes(1);
118
+ expect(onSubFieldChange).toHaveBeenCalledWith('r2', '5');
119
+ });
120
+
121
+ it('uses getFieldName when it returns a name', () => {
122
+ const onSubFieldChange = jest.fn();
123
+ const s = make({ onSubFieldChange, getFieldName: (handle) => 'named-' + handle.id });
124
+
125
+ s.mathField = stubField({ r1: '1' });
126
+ s.promptValues = {};
127
+
128
+ s.onPromptInput();
129
+
130
+ expect(onSubFieldChange).toHaveBeenCalledWith('named-r1', '1');
131
+ });
132
+
133
+ it('still tracks values when no callback is supplied', () => {
134
+ const s = make();
135
+
136
+ s.mathField = stubField({ r1: '3' });
137
+ s.onPromptInput();
138
+
139
+ expect(s.promptValues).toEqual({ r1: '3' });
140
+ });
141
+ });
142
+
143
+ describe('onSubFieldFocus', () => {
144
+ it('reports the prompt containing the selection, plus setInput', () => {
145
+ const onSubFieldFocus = jest.fn();
146
+ const setInput = jest.fn();
147
+ const s = make({ onSubFieldFocus, setInput });
148
+
149
+ // r2 occupies offsets 10..15
150
+ s.mathField = stubField({ r1: '', r2: '' }, { ranges: [[11, 11]] });
151
+ s.onPromptFocus();
152
+
153
+ expect(onSubFieldFocus).toHaveBeenCalledTimes(1);
154
+ expect(onSubFieldFocus.mock.calls[0][0]).toEqual('r2');
155
+ expect(onSubFieldFocus.mock.calls[0][1].id).toEqual('r2');
156
+ expect(setInput).toHaveBeenCalled();
157
+ });
158
+
159
+ it('does nothing without a selection', () => {
160
+ const onSubFieldFocus = jest.fn();
161
+ const s = make({ onSubFieldFocus });
162
+
163
+ s.mathField = stubField({ r1: '' }, null);
164
+ s.onPromptFocus();
165
+
166
+ expect(onSubFieldFocus).not.toHaveBeenCalled();
167
+ });
168
+
169
+ it('does not throw when the field is missing', () => {
170
+ const s = make({ onSubFieldFocus: jest.fn() });
171
+
172
+ expect(() => s.onPromptFocus()).not.toThrow();
173
+ });
174
+ });
175
+
176
+ describe('accessibility announcements', () => {
177
+ afterEach(() => {
178
+ document.body.innerHTML = '';
179
+ });
180
+
181
+ it('creates a polite off-screen live region', () => {
182
+ const s = make();
183
+
184
+ s.createLiveRegion();
185
+
186
+ expect(s.liveRegion.getAttribute('aria-live')).toEqual('polite');
187
+ expect(s.liveRegion.getAttribute('aria-atomic')).toEqual('true');
188
+ expect(document.body.contains(s.liveRegion)).toBe(true);
189
+
190
+ s.removeLiveRegion();
191
+ expect(s.liveRegion).toBeUndefined();
192
+ });
193
+
194
+ it('does not create a second region', () => {
195
+ const s = make();
196
+
197
+ s.createLiveRegion();
198
+ const first = s.liveRegion;
199
+
200
+ s.createLiveRegion();
201
+ expect(s.liveRegion).toBe(first);
202
+ s.removeLiveRegion();
203
+ });
204
+
205
+ it('announces when typed input expands into a command', () => {
206
+ const s = make();
207
+
208
+ s.createLiveRegion();
209
+ s.promptValues = { r1: 'x' };
210
+ // grew by more than one char AND gained a backslash => a conversion
211
+ s.announceConversion({ r1: 'x\\frac{}{}' });
212
+
213
+ expect(s.liveRegion.textContent).toEqual('Converted to math symbol');
214
+ s.removeLiveRegion();
215
+ });
216
+
217
+ it('does not announce ordinary typing', () => {
218
+ const s = make();
219
+
220
+ s.createLiveRegion();
221
+ s.promptValues = { r1: 'x' };
222
+ s.announceConversion({ r1: 'xy' });
223
+
224
+ expect(s.liveRegion.textContent).toEqual('');
225
+ s.removeLiveRegion();
226
+ });
227
+
228
+ it('does not announce a deletion', () => {
229
+ const s = make();
230
+
231
+ s.createLiveRegion();
232
+ s.promptValues = { r1: 'x\\frac{}{}' };
233
+ s.onKeyDown({ key: 'Backspace' });
234
+ s.announceConversion({ r1: 'x' });
235
+
236
+ expect(s.liveRegion.textContent).toEqual('');
237
+ s.removeLiveRegion();
238
+ });
239
+ });
240
+
241
+ describe('focus / blur', () => {
242
+ it('delegates to the field and is safe without one', () => {
243
+ const s = make();
244
+
245
+ expect(() => {
246
+ s.focus();
247
+ s.blur();
248
+ }).not.toThrow();
249
+
250
+ s.mathField = stubField();
251
+ s.focus();
252
+ s.blur();
253
+
254
+ expect(s.mathField.focused).toBe(true);
255
+ expect(s.mathField.blurred).toBe(true);
256
+ });
257
+ });
258
+
259
+ /**
260
+ * MathLive shows the caret only while the field carries `ML__focused`, which
261
+ * it adds during a render gated on `isSelectionEditable && hasFocus()`. In a
262
+ * read-only field only prompt interiors are editable, so after a bare
263
+ * `focus()` - whose selection sits outside every prompt - the answer block
264
+ * looked focused but had no blinking cursor. Verified in Chromium: the caret
265
+ * span goes from visibility:hidden to visible once the selection is inside a
266
+ * prompt AND a render has run.
267
+ */
268
+ describe('caret placement', () => {
269
+ it('puts a collapsed caret in the first prompt on focus', () => {
270
+ const s = make();
271
+
272
+ s.mathField = stubField({ r1: '', r2: '' });
273
+ s.focus();
274
+
275
+ // stub ranges are [0,5] for r1, [10,15] for r2
276
+ expect(s.mathField.selection).toEqual({ ranges: [[0, 0]] });
277
+ });
278
+
279
+ it('forces the render that makes the caret visible', () => {
280
+ const s = make();
281
+
282
+ s.mathField = stubField({ r1: '' });
283
+ s.focus();
284
+
285
+ // assigning `selection` alone updates the model without re-rendering, so
286
+ // ML__focused would never be added
287
+ expect(s.mathField.commands).toEqual(['scrollIntoView']);
288
+ });
289
+
290
+ it('does not move a caret that is already inside a prompt', () => {
291
+ const s = make();
292
+
293
+ s.mathField = stubField({ r1: '', r2: '' });
294
+ s.mathField.position = 12; // inside r2
295
+ s.focus();
296
+
297
+ expect(s.mathField.selection).toBe(null);
298
+ expect(s.mathField.commands).toEqual([]);
299
+ });
300
+
301
+ it('targets a specific prompt', () => {
302
+ const s = make();
303
+
304
+ s.mathField = stubField({ r1: '', r2: '' });
305
+ s.focusPrompt('r2');
306
+
307
+ expect(s.mathField.selection).toEqual({ ranges: [[10, 10]] });
308
+ });
309
+
310
+ it('falls back to the first prompt for an unknown id', () => {
311
+ const s = make();
312
+
313
+ s.mathField = stubField({ r1: '', r2: '' });
314
+ s.focusPrompt('nope');
315
+
316
+ expect(s.mathField.selection).toEqual({ ranges: [[0, 0]] });
317
+ });
318
+
319
+ it('is a no-op when there are no prompts', () => {
320
+ const s = make();
321
+
322
+ s.mathField = stubField({});
323
+
324
+ expect(() => s.focusPrompt()).not.toThrow();
325
+ expect(s.mathField.selection).toBe(null);
326
+ });
327
+
328
+ it('is safe without a field', () => {
329
+ expect(() => make().focusPrompt('r1')).not.toThrow();
330
+ });
331
+
332
+ describe('promptContaining', () => {
333
+ it('finds the prompt whose range holds the offset, inclusive', () => {
334
+ const s = make();
335
+
336
+ s.mathField = stubField({ r1: '', r2: '' });
337
+
338
+ expect(s.promptContaining(0)).toEqual('r1');
339
+ expect(s.promptContaining(5)).toEqual('r1');
340
+ expect(s.promptContaining(10)).toEqual('r2');
341
+ expect(s.promptContaining(7)).toBeUndefined();
342
+ });
343
+
344
+ it('treats a missing position as outside', () => {
345
+ const s = make();
346
+
347
+ s.mathField = stubField({ r1: '' });
348
+
349
+ expect(s.promptContaining(undefined)).toBeUndefined();
350
+ });
351
+ });
352
+
353
+ // MathQuill's inner fields could be focused directly.
354
+ it('promptHandle exposes focus() for its own prompt', () => {
355
+ const s = make();
356
+
357
+ s.mathField = stubField({ r1: '', r2: '' });
358
+ s.promptHandle('r2').focus();
359
+
360
+ expect(s.mathField.focused).toBe(true);
361
+ expect(s.mathField.selection).toEqual({ ranges: [[10, 10]] });
362
+ });
363
+ });
364
+ });
@@ -9,7 +9,7 @@ import { color } from '@pie-lib/render-ui';
9
9
 
10
10
  import { baseSet } from '../keys';
11
11
  import { sortKeys } from './keys-layout';
12
- import { commonKeyboardStyles } from '../mf/common-styles';
12
+ import { commonKeyboardStyles, rootIndexStyles } from '../mf/common-styles';
13
13
  import { loadMathLive, latexToMarkup } from '../mathlive-instance';
14
14
 
15
15
  const log = debug('pie-lib:math-input-mathlive:keypad');
@@ -22,29 +22,42 @@ const log = debug('pie-lib:math-input-mathlive:keypad');
22
22
  * `convertLatexToMarkup`, so a 40-key keypad no longer creates 40 live math
23
23
  * instances.
24
24
  */
25
+ // Labels that the legacy keypad drew slanted. MathQuill styled `\parallel` as
26
+ // italic, which slants the two bars; MathLive draws it upright. A skew is used
27
+ // rather than `font-style: italic` so the slant does not depend on the font
28
+ // having an italic face for this glyph.
29
+ const SLANTED_LABELS = ['\\parallel'];
30
+ const SLANT = 'skewX(-15deg)';
31
+ // MathLive's `∥` has hairline bars next to the radical sign and the other keys, so
32
+ // they are thickened with a text stroke and stretched a little taller.
33
+ const SLANTED_STROKE = '0.03em currentColor';
34
+ const SLANTED_SCALE_Y = 1.25;
35
+
25
36
  // The `color.*()` helpers must be called lazily, inside the styled callback.
26
37
  // Evaluating them in a plain object literal runs them at module load, which
27
38
  // breaks any consumer that mocks a subset of @pie-lib/render-ui.
28
- const LabelHolder = styled('span')(() => ({
39
+ const LabelHolder = styled('span', { shouldForwardProp: (prop) => prop !== 'slanted' })(({ slanted }) => ({
40
+ ...(slanted && {
41
+ transform: `${SLANT} scaleY(${SLANTED_SCALE_Y})`,
42
+ '& *': { WebkitTextStroke: SLANTED_STROKE },
43
+ }),
29
44
  pointerEvents: 'none',
30
45
  textTransform: 'none',
31
46
  color: color.text(),
32
- display: 'inline-flex',
33
- alignItems: 'center',
34
- justifyContent: 'center',
47
+ // Deliberately inline-block, NOT inline-flex: MathLive's markup relies on
48
+ // normal inline flow. Making its spans flex items collapses their width, and
49
+ // stretchy accents render an <svg width="100%" preserveAspectRatio="none">
50
+ // that then squeezes into a spike instead of spanning the content.
51
+ display: 'inline-block',
52
+ textAlign: 'center',
35
53
  lineHeight: 1,
36
- // Never let a label size its grid column. Stretchy accents
37
- // (\overrightarrow, \overleftrightarrow, \overarc) render an <svg> that
38
- // mathlive-static.css positions absolutely at width:100%; if that stylesheet
39
- // is missing the svg lays out statically and expands without bound, dragging
40
- // the whole column across the screen. `overflow: hidden` also collapses the
41
- // label's min-content contribution, so the grid can't blow out either way.
54
+ // Keep a label from sizing its grid column. `overflow: hidden` also collapses
55
+ // the label's min-content contribution, so a wide accent cannot drag the
56
+ // column across the screen. The svg's own positioning is left to
57
+ // mathlive-static.css (injected by the package) - duplicating it here broke
58
+ // the accent's containing block.
42
59
  maxWidth: '100%',
43
60
  overflow: 'hidden',
44
- '& svg': {
45
- position: 'absolute',
46
- width: '100%',
47
- },
48
61
  // Answer-block / empty-slot boxes inside a label.
49
62
  '& .ML__placeholder': {
50
63
  backgroundColor: color.keypadEmptyPlaceholder ? color.keypadEmptyPlaceholder() : undefined,
@@ -55,6 +68,9 @@ const LabelHolder = styled('span')(() => ({
55
68
  '& [data-pie-empty]': {
56
69
  opacity: 0.5,
57
70
  },
71
+ // A readable index on the nth-root key. Carried by the label itself, not only
72
+ // by the keypad container, so it holds wherever a LatexLabel is mounted.
73
+ ...rootIndexStyles,
58
74
  }));
59
75
 
60
76
  export class LatexLabel extends React.Component {
@@ -111,9 +127,9 @@ export class LatexLabel extends React.Component {
111
127
  // Fall back to the raw latex so a button is never blank.
112
128
  return markup ? (
113
129
  // eslint-disable-next-line react/no-danger
114
- <LabelHolder dangerouslySetInnerHTML={{ __html: markup }} />
130
+ <LabelHolder slanted={SLANTED_LABELS.includes(this.props.latex)} dangerouslySetInnerHTML={{ __html: markup }} />
115
131
  ) : (
116
- <LabelHolder>{this.props.latex}</LabelHolder>
132
+ <LabelHolder slanted={SLANTED_LABELS.includes(this.props.latex)}>{this.props.latex}</LabelHolder>
117
133
  );
118
134
  }
119
135
  }
@@ -14,7 +14,10 @@ export const overRightArrow = set({
14
14
 
15
15
  export const overLeftRightArrow = set({
16
16
  name: 'Segment',
17
- latex: '\\overleftrightarrow{\\overline{}}', // used this notation to display the pink box
17
+ // MathQuill needed a nested \overline{} to draw the empty box; MathLive
18
+ // renders that as a real line, so the label showed a spurious bar under the
19
+ // arrows. `withVisibleEmptySlots` supplies the box for `{}` instead.
20
+ latex: '\\overleftrightarrow{}',
18
21
  symbol: 'AB',
19
22
  command: '\\overleftrightarrow',
20
23
  });
@@ -51,7 +54,9 @@ export const angle = set({
51
54
  });
52
55
  export const overArc = set({
53
56
  name: 'Over arc',
54
- latex: '\\overarc{\\overline{}}', // used this notation to display the pink box
57
+ // See the note on Segment above: the nested \overline{} drew a straight line
58
+ // instead of leaving the arc visible.
59
+ latex: '\\overarc{}',
55
60
  command: '\\overarc',
56
61
  });
57
62
  export const measureOfAngle = set({
package/src/keys/misc.js CHANGED
@@ -29,10 +29,16 @@ export const brackets = set({
29
29
  command: '[',
30
30
  });
31
31
 
32
+ // `%` must be escaped. MathQuill was not a real LaTeX parser: it defined
33
+ // `LatexCmds['%']` as a symbol that *serialised* to `\%`, so a bare `%` worked
34
+ // both as a label and as a command. MathLive parses LaTeX properly, where `%`
35
+ // begins a comment - it rendered nothing (a blank button) and, once inserted,
36
+ // silently swallowed the rest of the expression. `\%` is what MathQuill stored
37
+ // all along, so this changes the key only, not the saved latex.
32
38
  export const percentage = set({
33
39
  name: 'Percent',
34
- latex: '%',
35
- command: '%',
40
+ latex: '\\%',
41
+ command: '\\%',
36
42
  });
37
43
 
38
44
  export const approx = set({