kmaterialize 2.3.3-1.0.35 → 2.3.3-1.0.38
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.
- package/components/code-card/_code-card.scss +0 -1
- package/components/code-card/codeCardSpec.js +11 -0
- package/components/editor/editor.ts +71 -4
- package/components/editor/editorSpec.js +92 -1
- package/components/extensions/web/crazy-button.ts +3 -1
- package/components/file-input/_file-input-enhanced.scss +4 -0
- package/components/otp-input/_otp-input.scss +20 -6
- package/components/otp-input/otp-input.ts +6 -1
- package/components/password-input/password-input.ts +14 -5
- package/components/radio-button/_radio-buttons.scss +47 -0
- package/components/rich-textarea/_rich-textarea.scss +13 -1
- package/components/rich-textarea/rich-textarea.ts +3 -0
- package/components/slider/_range.scss +234 -1
- package/components/slider/range.ts +359 -2
- package/components/switch/_switches.scss +52 -4
- package/components/switch/switch-drag.ts +198 -0
- package/components/switch/switchDragSpec.js +230 -0
- package/components/textfield/_file-input.scss +11 -1
- package/components/textfield/_input-fields.scss +62 -2
- package/components/textfield/forms.ts +16 -60
- package/components/textfield/outlined-notch.ts +13 -0
- package/components/textfield/outlinedLabelSpec.js +58 -0
- package/components/tom-select/_tom-select-field.scss +10 -0
- package/components/tom-select/tom-select-field.ts +5 -0
- package/dist/css/materialize.colors.min.css +1 -1
- package/dist/css/materialize.css +551 -28
- package/dist/css/materialize.min.css +2 -2
- package/dist/css/materialize.min.css.map +1 -1
- package/dist/js/materialize.cjs.js +631 -91
- package/dist/js/materialize.d.ts +65 -3
- package/dist/js/materialize.js +631 -91
- package/dist/js/materialize.min.js +3 -3
- package/dist/js/materialize.mjs +631 -92
- package/package.json +3 -3
|
@@ -89,6 +89,17 @@ describe("CodeCard", function(){
|
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it("highlights JavaScript while preserving the source text", async function(){
|
|
92
|
+
// Load this test's peer explicitly instead of depending on Editor test order.
|
|
93
|
+
if(!window.Prism){
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
const script = document.createElement("script");
|
|
96
|
+
script.dataset.manual = "";
|
|
97
|
+
script.src = "/__spec__/node_modules/prismjs/prism.js";
|
|
98
|
+
script.onload = resolve;
|
|
99
|
+
script.onerror = () => reject(new Error("Failed to load Prism"));
|
|
100
|
+
document.head.append(script);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
92
103
|
instance = M.CodeCard.init(host);
|
|
93
104
|
await instance.ready;
|
|
94
105
|
expect(host.querySelectorAll(".token").length).toBeGreaterThan(0);
|
|
@@ -72,6 +72,8 @@ export class Editor {
|
|
|
72
72
|
private prism?: typeof Prism;
|
|
73
73
|
private dropCaret = make('span', 'editor-drop-caret');
|
|
74
74
|
private dragPoint?: { x: number; y: number };
|
|
75
|
+
private _dragScroll?: { top: number; left: number; anchorY?: number; direction: number; speed: number };
|
|
76
|
+
private _dragScrollFrame?: number;
|
|
75
77
|
private helpers: EditorHelpers;
|
|
76
78
|
private undoStack: EditorEdit[] = [];
|
|
77
79
|
private redoStack: EditorEdit[] = [];
|
|
@@ -310,7 +312,16 @@ export class Editor {
|
|
|
310
312
|
this.lastTyping = undefined;
|
|
311
313
|
}
|
|
312
314
|
}, { signal });
|
|
313
|
-
this.source.addEventListener('scroll', () => {
|
|
315
|
+
this.source.addEventListener('scroll', () => {
|
|
316
|
+
// Native textarea drag scrolling must not bypass the controlled edge scroll.
|
|
317
|
+
if (this._dragScroll) {
|
|
318
|
+
this.source.scrollTop = this._dragScroll.top;
|
|
319
|
+
this.source.scrollLeft = this._dragScroll.left;
|
|
320
|
+
}
|
|
321
|
+
this.lines.scrollTop = this.source.scrollTop;
|
|
322
|
+
this.syncHighlight();
|
|
323
|
+
if (this.dragPoint) this.showDropCaret();
|
|
324
|
+
}, { signal });
|
|
314
325
|
this.selector.addEventListener('change', () => { void this.setSource(this.selector.value || this.activeSource.id).catch(() => {}); }, { signal });
|
|
315
326
|
this.templateSelector.addEventListener('change', () => { void this.selectTemplate(this.templateSelector.value || this.activeTemplate.id).catch(() => {}); }, { signal });
|
|
316
327
|
this.search.addEventListener('input', () => this.renderTokens(), { signal });
|
|
@@ -327,6 +338,7 @@ export class Editor {
|
|
|
327
338
|
event.dataTransfer.setData(helperMime, helper.dataset.editorHelper!);
|
|
328
339
|
event.dataTransfer.setData('text/plain', `{{${helper.dataset.editorHelper} }}`);
|
|
329
340
|
event.dataTransfer.effectAllowed = 'copy';
|
|
341
|
+
this._startDragScroll();
|
|
330
342
|
return;
|
|
331
343
|
}
|
|
332
344
|
if (!token || token.disabled || !event.dataTransfer) return;
|
|
@@ -334,19 +346,22 @@ export class Editor {
|
|
|
334
346
|
event.dataTransfer.setData(tokenMime, JSON.stringify(path));
|
|
335
347
|
event.dataTransfer.setData('text/plain', this.expression(path));
|
|
336
348
|
event.dataTransfer.effectAllowed = 'copy';
|
|
349
|
+
this._startDragScroll();
|
|
350
|
+
}, { signal });
|
|
351
|
+
this.source.addEventListener('dragenter', event => {
|
|
352
|
+
if (!this.source.readOnly && event.dataTransfer?.types.some(type => type === tokenMime || type === helperMime)) this._startDragScroll();
|
|
337
353
|
}, { signal });
|
|
338
354
|
this.source.addEventListener('dragover', event => {
|
|
339
355
|
if (!this.source.readOnly && event.dataTransfer?.types.some(type => type === tokenMime || type === helperMime)) {
|
|
340
356
|
event.preventDefault(); event.dataTransfer.dropEffect = 'copy';
|
|
341
357
|
code.classList.add('is-drop-target');
|
|
342
358
|
this.dragPoint = { x: event.clientX, y: event.clientY };
|
|
343
|
-
|
|
344
|
-
if (event.clientY < bounds.top + 20) this.source.scrollTop -= 16;
|
|
345
|
-
else if (event.clientY > bounds.bottom - 20) this.source.scrollTop += 16;
|
|
359
|
+
this._updateDragScroll(event.clientX, event.clientY);
|
|
346
360
|
this.showDropCaret();
|
|
347
361
|
}
|
|
348
362
|
}, { signal });
|
|
349
363
|
const clearDrop = () => {
|
|
364
|
+
this._stopDragScroll();
|
|
350
365
|
this.spreadsheetView?.clearDropCarets();
|
|
351
366
|
code.classList.remove('is-drop-target');
|
|
352
367
|
this.dropCaret.hidden = true;
|
|
@@ -382,6 +397,57 @@ export class Editor {
|
|
|
382
397
|
this.renderTokens();
|
|
383
398
|
}
|
|
384
399
|
|
|
400
|
+
/** Freeze native scrolling until movement inside the source requests edge scrolling. */
|
|
401
|
+
private _startDragScroll(): void {
|
|
402
|
+
if (this._dragScroll || this.spreadsheetView || this.source.readOnly) return;
|
|
403
|
+
const state = this._dragScroll = {
|
|
404
|
+
top: this.source.scrollTop, left: this.source.scrollLeft, anchorY: undefined as number | undefined,
|
|
405
|
+
direction: 0, speed: 0
|
|
406
|
+
};
|
|
407
|
+
let previous = performance.now();
|
|
408
|
+
const step = (time: number) => {
|
|
409
|
+
if (this._dragScroll !== state) return;
|
|
410
|
+
const elapsed = Math.min(32, Math.max(0, time - previous));
|
|
411
|
+
previous = time;
|
|
412
|
+
// Retain fractional pixels so slow movement also works on high-refresh displays.
|
|
413
|
+
state.top = Math.max(0, Math.min(this.source.scrollHeight - this.source.clientHeight, state.top + state.speed * elapsed / 1000));
|
|
414
|
+
this.source.scrollTop = state.top;
|
|
415
|
+
this.source.scrollLeft = state.left;
|
|
416
|
+
if (this.dragPoint) this.showDropCaret();
|
|
417
|
+
this._dragScrollFrame = requestAnimationFrame(step);
|
|
418
|
+
};
|
|
419
|
+
this._dragScrollFrame = requestAnimationFrame(step);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private _updateDragScroll(x: number, y: number): void {
|
|
423
|
+
this._startDragScroll();
|
|
424
|
+
const state = this._dragScroll;
|
|
425
|
+
if (!state) return;
|
|
426
|
+
const bounds = this.source.getBoundingClientRect();
|
|
427
|
+
if (x < bounds.left || x > bounds.right || y < bounds.top || y > bounds.bottom) {
|
|
428
|
+
state.speed = state.direction = 0;
|
|
429
|
+
state.anchorY = undefined;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
// The first point establishes entry; small pointer jitter does not enable scrolling.
|
|
433
|
+
if (state.anchorY === undefined) state.anchorY = y;
|
|
434
|
+
const movement = y - state.anchorY;
|
|
435
|
+
if (Math.abs(movement) >= 6) {
|
|
436
|
+
state.direction = Math.sign(movement);
|
|
437
|
+
state.anchorY = y;
|
|
438
|
+
}
|
|
439
|
+
const edge = Math.min(32, bounds.height / 4);
|
|
440
|
+
const depth = state.direction < 0 ? bounds.top + edge - y : y - (bounds.bottom - edge);
|
|
441
|
+
state.speed = edge > 0 && state.direction && depth > 0
|
|
442
|
+
? state.direction * 120 * Math.min(1, depth / edge) : 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private _stopDragScroll(): void {
|
|
446
|
+
if (this._dragScrollFrame !== undefined) cancelAnimationFrame(this._dragScrollFrame);
|
|
447
|
+
this._dragScrollFrame = undefined;
|
|
448
|
+
this._dragScroll = undefined;
|
|
449
|
+
}
|
|
450
|
+
|
|
385
451
|
private expression(path: string[]): string {
|
|
386
452
|
// Segment literals preserve dotted keys and numeric array indices.
|
|
387
453
|
if (path.some(key => key.includes(']') || key.includes('[') || key === '')) {
|
|
@@ -932,6 +998,7 @@ export class Editor {
|
|
|
932
998
|
destroy(): void {
|
|
933
999
|
if (this.destroyed) return;
|
|
934
1000
|
this.destroyed = true; this.version++;
|
|
1001
|
+
this._stopDragScroll();
|
|
935
1002
|
this.templateRequest?.abort(); this.dataRequest?.abort();
|
|
936
1003
|
this.listRequests.forEach(request => request.abort()); this.listRequests.clear();
|
|
937
1004
|
this.spreadsheetView?.destroy();
|
|
@@ -273,7 +273,9 @@ describe('Editor Handlebars workspace', function () {
|
|
|
273
273
|
script.onload = () => resolve(); script.onerror = () => reject(new Error('Failed to load Prism'));
|
|
274
274
|
document.head.append(script);
|
|
275
275
|
});
|
|
276
|
-
|
|
276
|
+
// Preserve the Prism instance already used by other components and the peer cache.
|
|
277
|
+
if (!window.Prism) await load('prism.js');
|
|
278
|
+
await load('components/prism-markup-templating.js'); await load('components/prism-handlebars.js');
|
|
277
279
|
const template = '<h1>{{name}}</h1>\n<style>h1 { color: red; }</style>\n' + 'x'.repeat(200);
|
|
278
280
|
await start({ template, data: { name: 'Ada' }, highlight: true });
|
|
279
281
|
const input = host.querySelector('textarea'), mirror = host.querySelector('.editor-highlight');
|
|
@@ -428,4 +430,93 @@ describe('Editor Handlebars workspace', function () {
|
|
|
428
430
|
parent.replaceWith(host);
|
|
429
431
|
}
|
|
430
432
|
});
|
|
433
|
+
describe('token drag scrolling', function () {
|
|
434
|
+
let input, bounds, transfer, frames, clock, nextFrame;
|
|
435
|
+
async function begin(helper = false) {
|
|
436
|
+
await start({ template: ('long line '.repeat(30) + '\n').repeat(100), data: { name: 'Ada' }, helpers: { greet: value => value }, templateSelect: false, sourceSelect: false });
|
|
437
|
+
input = host.querySelector('.editor-source');
|
|
438
|
+
input.scrollTop = 240;
|
|
439
|
+
input.scrollLeft = 50;
|
|
440
|
+
bounds = input.getBoundingClientRect();
|
|
441
|
+
frames = new Map(); clock = performance.now(); nextFrame = 0;
|
|
442
|
+
spyOn(window, 'requestAnimationFrame').and.callFake(callback => { frames.set(++nextFrame, callback); return nextFrame; });
|
|
443
|
+
spyOn(window, 'cancelAnimationFrame').and.callFake(id => frames.delete(id));
|
|
444
|
+
transfer = new DataTransfer();
|
|
445
|
+
host.querySelector(helper ? '[data-editor-helper="greet"]' : '[aria-label="Insert name"]').dispatchEvent(new DragEvent('dragstart', { bubbles: true, dataTransfer: transfer }));
|
|
446
|
+
}
|
|
447
|
+
function over(y) {
|
|
448
|
+
input.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: transfer, clientX: bounds.left + 60, clientY: y }));
|
|
449
|
+
}
|
|
450
|
+
function tick(count = 1) {
|
|
451
|
+
for (let i = 0; i < count; i++) {
|
|
452
|
+
clock += 16;
|
|
453
|
+
const pending = [...frames.values()]; frames.clear();
|
|
454
|
+
pending.forEach(callback => callback(clock));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
it('freezes entry at the bottom and rejects native scroll jumps and pointer jitter', async function () {
|
|
458
|
+
await begin();
|
|
459
|
+
over(bounds.bottom - 4); tick(20);
|
|
460
|
+
expect(input.scrollTop).toBe(240);
|
|
461
|
+
over(bounds.bottom - 3); tick(20);
|
|
462
|
+
expect(input.scrollTop).toBe(240);
|
|
463
|
+
input.scrollTop = 900; input.scrollLeft = 200;
|
|
464
|
+
input.dispatchEvent(new Event('scroll'));
|
|
465
|
+
expect(input.scrollTop).toBe(240);
|
|
466
|
+
expect(input.scrollLeft).toBe(50);
|
|
467
|
+
});
|
|
468
|
+
it('scrolls gradually only after downward movement inside and stops away from the edge', async function () {
|
|
469
|
+
await begin();
|
|
470
|
+
over(bounds.top + bounds.height / 2);
|
|
471
|
+
over(bounds.bottom - 2);
|
|
472
|
+
expect(input.scrollTop).toBe(240);
|
|
473
|
+
tick();
|
|
474
|
+
expect(input.scrollTop).toBeGreaterThan(240);
|
|
475
|
+
expect(input.scrollTop).toBeLessThanOrEqual(244);
|
|
476
|
+
tick(15);
|
|
477
|
+
expect(input.scrollTop).toBeLessThan(290);
|
|
478
|
+
over(bounds.top + bounds.height / 2);
|
|
479
|
+
const stopped = input.scrollTop; tick(20);
|
|
480
|
+
expect(input.scrollTop).toBe(stopped);
|
|
481
|
+
});
|
|
482
|
+
it('accumulates slow scrolling at the beginning of the edge zone', async function () {
|
|
483
|
+
await begin();
|
|
484
|
+
over(bounds.top + bounds.height / 2); over(bounds.bottom - 31); tick(20);
|
|
485
|
+
expect(input.scrollTop).toBeGreaterThan(240);
|
|
486
|
+
expect(input.scrollTop).toBeLessThan(244);
|
|
487
|
+
});
|
|
488
|
+
it('supports gentle upward scrolling for helper drags too', async function () {
|
|
489
|
+
await begin(true);
|
|
490
|
+
over(bounds.top + 2); tick(10);
|
|
491
|
+
expect(input.scrollTop).toBe(240);
|
|
492
|
+
over(bounds.top + bounds.height / 2);
|
|
493
|
+
over(bounds.top + 2); tick(10);
|
|
494
|
+
expect(input.scrollTop).toBeLessThan(240);
|
|
495
|
+
expect(input.scrollTop).toBeGreaterThan(210);
|
|
496
|
+
});
|
|
497
|
+
it('resets the entry gate on leaving and reentering', async function () {
|
|
498
|
+
await begin();
|
|
499
|
+
over(bounds.top + bounds.height / 2); over(bounds.bottom - 2); tick(10);
|
|
500
|
+
input.dispatchEvent(new DragEvent('dragleave'));
|
|
501
|
+
const stopped = input.scrollTop; tick(10);
|
|
502
|
+
expect(input.scrollTop).toBe(stopped);
|
|
503
|
+
over(bounds.bottom - 2); tick(10);
|
|
504
|
+
expect(input.scrollTop).toBe(stopped);
|
|
505
|
+
});
|
|
506
|
+
for (const end of ['drop', 'dragend', 'Escape', 'destroy']) {
|
|
507
|
+
it(`releases scrolling and cancels animation on ${end}`, async function () {
|
|
508
|
+
await begin();
|
|
509
|
+
over(bounds.top + bounds.height / 2); over(bounds.bottom - 2);
|
|
510
|
+
if (end === 'destroy') editor.destroy();
|
|
511
|
+
else if (end === 'Escape') input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
512
|
+
else document.dispatchEvent(new DragEvent(end));
|
|
513
|
+
expect(frames.size).toBe(0);
|
|
514
|
+
if (end !== 'destroy') {
|
|
515
|
+
input.scrollTop = 400; input.dispatchEvent(new Event('scroll'));
|
|
516
|
+
expect(input.scrollTop).toBe(400);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
431
522
|
});
|
|
@@ -33,6 +33,7 @@ export default class CrazyButton extends Kmcomponent {
|
|
|
33
33
|
'icon-position': string('right', ['left', 'right']),
|
|
34
34
|
'color-primary': string(), 'color-secondary': string(),
|
|
35
35
|
'data-view': string(), 'aria-current': string(),
|
|
36
|
+
'aria-controls': string(), 'aria-pressed': string(),
|
|
36
37
|
href: string(), target: string('_self', ['_self', '_blank']),
|
|
37
38
|
variant: string('', ['', 'filled', 'tonal', 'outlined', 'elevated', 'text', 'standard']),
|
|
38
39
|
disabled: boolean(), toggle: boolean(), pressed: boolean(),
|
|
@@ -84,7 +85,8 @@ export default class CrazyButton extends Kmcomponent {
|
|
|
84
85
|
${a['aria-label'] || iconOnly ? `aria-label="${escape(a['aria-label'] || a.label || a['icon-text'] || 'Button')}"` : ''}
|
|
85
86
|
${a['data-view'] ? `data-view="${escape(a['data-view'])}"` : ''}
|
|
86
87
|
${a['aria-current'] ? `aria-current="${escape(a['aria-current'])}"` : ''}
|
|
87
|
-
${a.toggle ? `aria-pressed="${a.pressed}"` : ''
|
|
88
|
+
${a.toggle ? `aria-pressed="${a.pressed}"` : a['aria-pressed'] ? `aria-pressed="${escape(a['aria-pressed'])}"` : ''}
|
|
89
|
+
${a['aria-controls'] && !menu ? `aria-controls="${escape(a['aria-controls'])}"` : ''} ${menu ? menuAttrs : ''}>${content}</${tag}>`;
|
|
88
90
|
return a.split && a['menu-target']
|
|
89
91
|
? `<span class="btn-split">${button}<button type="button" class="${escape(classes)} btn-icon btn-menu-trigger dropdown-trigger no-autoinit" ${menuAttrs} aria-label="${escape(a['menu-label'])}" ${a.disabled ? 'disabled' : ''}><i class="material-icons" aria-hidden="true">arrow_drop_down</i></button></span>`
|
|
90
92
|
: button;
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
width: max-content;
|
|
7
7
|
max-width: 100%;
|
|
8
8
|
margin-block: 8px;
|
|
9
|
+
margin-left: auto;
|
|
10
|
+
margin-right: auto;
|
|
11
|
+
|
|
12
|
+
&[data-otp-align='left'] { margin-left: 0; margin-right: auto; }
|
|
13
|
+
&[data-otp-align='right'] { margin-left: auto; margin-right: 0; }
|
|
9
14
|
border-radius: var(--otp-slot-radius);
|
|
10
15
|
|
|
11
16
|
// One native control preserves selection, password semantics, autofill and form validation.
|
|
@@ -61,12 +66,21 @@
|
|
|
61
66
|
// Standard text-field markup: input, label, supporting text. Keep the label
|
|
62
67
|
// above the entire code so it never overlaps an individual slot or separator.
|
|
63
68
|
.input-field:has(> .otp-input) {
|
|
64
|
-
display:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
> .otp-input {
|
|
69
|
+
display: grid;
|
|
70
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, max-content) minmax(0, 1fr);
|
|
71
|
+
justify-items: start;
|
|
72
|
+
&:has(> .otp-input[data-otp-align='left']) { grid-template-columns: 0 minmax(0, max-content) minmax(0, 1fr); }
|
|
73
|
+
&:has(> .otp-input[data-otp-align='right']) { grid-template-columns: minmax(0, 1fr) minmax(0, max-content) 0; }
|
|
74
|
+
> .otp-input {
|
|
75
|
+
grid-column: 2;
|
|
76
|
+
grid-row: 2;
|
|
77
|
+
margin: 0;
|
|
78
|
+
&[data-otp-align] { margin: 0; }
|
|
79
|
+
}
|
|
69
80
|
> label {
|
|
81
|
+
grid-column: 2;
|
|
82
|
+
grid-row: 1;
|
|
83
|
+
max-width: 100%;
|
|
70
84
|
position: static;
|
|
71
85
|
order: -1;
|
|
72
86
|
transform: none;
|
|
@@ -76,7 +90,7 @@
|
|
|
76
90
|
line-height: 1.4;
|
|
77
91
|
background: transparent;
|
|
78
92
|
}
|
|
79
|
-
> .supporting-text {
|
|
93
|
+
> .supporting-text { grid-column: 2; grid-row: 3; max-width: 100%; padding-inline: 0; }
|
|
80
94
|
&:focus-within > label { color: var(--input-color); }
|
|
81
95
|
&:has(input:disabled) > label { opacity: .38; }
|
|
82
96
|
&.error, &:has(input[aria-invalid='true']), &:has(.otp-input[data-invalid]), &:has(input:user-invalid) {
|
|
@@ -6,6 +6,8 @@ export interface OtpInputOptions extends BaseOptions {
|
|
|
6
6
|
/** Number of editable slots (1–32); inferred when pattern is supplied. */
|
|
7
7
|
length?: number;
|
|
8
8
|
characters?: 'digits' | 'alphanumeric';
|
|
9
|
+
/** Horizontal alignment of the code within its container. Defaults to center. */
|
|
10
|
+
align?: 'left' | 'center' | 'right';
|
|
9
11
|
/** # = digit, A = ASCII letter, * = alphanumeric; backslash escapes literals. */
|
|
10
12
|
pattern?: string;
|
|
11
13
|
/** Maskito processing and overwrite behavior. The slot pattern owns the mask. */
|
|
@@ -40,6 +42,7 @@ export class OtpInput extends Component<OtpInputOptions> {
|
|
|
40
42
|
constructor(el: HTMLInputElement, options: Partial<OtpInputOptions> = {}) {
|
|
41
43
|
if (!['text', 'tel', 'password'].includes(el.type)) throw new TypeError('OtpInput requires a text, tel, or password input.');
|
|
42
44
|
const settings = { ...OtpInput.defaults, ...OtpInput._markup(el), ...options };
|
|
45
|
+
if (!['left', 'center', 'right'].includes(settings.align!)) throw new TypeError('Unknown OTP alignment.');
|
|
43
46
|
if (!['digits', 'alphanumeric'].includes(settings.characters!)) throw new TypeError('Unknown OTP character set.');
|
|
44
47
|
if (!Number.isInteger(settings.groupSize) || settings.groupSize! < 0 || settings.groupSize! > 32) throw new TypeError('OTP groupSize must be an integer between 0 and 32.');
|
|
45
48
|
if (settings.length !== undefined && (!Number.isInteger(settings.length) || settings.length < 1 || settings.length > 32)) throw new TypeError('OTP length must be an integer between 1 and 32.');
|
|
@@ -65,7 +68,7 @@ export class OtpInput extends Component<OtpInputOptions> {
|
|
|
65
68
|
this._hadClass = el.classList.contains('otp-input-native');
|
|
66
69
|
this.ready = this._setup().catch(error => { this.destroy(); throw error; });
|
|
67
70
|
}
|
|
68
|
-
static get defaults(): OtpInputOptions { return { characters: 'digits', groupSize: 0 }; }
|
|
71
|
+
static get defaults(): OtpInputOptions { return { characters: 'digits', groupSize: 0, align: 'center' }; }
|
|
69
72
|
static init(el: HTMLInputElement, options?: Partial<OtpInputOptions>): OtpInput;
|
|
70
73
|
static init(els: InitElements<HTMLInputElement | MElement>, options?: Partial<OtpInputOptions>): OtpInput[];
|
|
71
74
|
static init(els: HTMLInputElement | InitElements<HTMLInputElement | MElement>, options: Partial<OtpInputOptions> = {}): OtpInput | OtpInput[] {
|
|
@@ -74,6 +77,7 @@ export class OtpInput extends Component<OtpInputOptions> {
|
|
|
74
77
|
static getInstance(el: HTMLInputElement): OtpInput { return el['M_OtpInput']; }
|
|
75
78
|
private static _markup(el: HTMLInputElement): Partial<OtpInputOptions> {
|
|
76
79
|
return {
|
|
80
|
+
...(el.dataset.otpAlign !== undefined ? { align: el.dataset.otpAlign as OtpInputOptions['align'] } : {}),
|
|
77
81
|
...(el.dataset.otpLength !== undefined ? { length: Number(el.dataset.otpLength) } : {}),
|
|
78
82
|
...(el.dataset.otpPattern !== undefined ? { pattern: el.dataset.otpPattern } : {}),
|
|
79
83
|
...(el.dataset.otpGroupSize !== undefined ? { groupSize: Number(el.dataset.otpGroupSize) } : {}),
|
|
@@ -102,6 +106,7 @@ export class OtpInput extends Component<OtpInputOptions> {
|
|
|
102
106
|
const wrapper = this._wrapper = document.createElement('div');
|
|
103
107
|
wrapper.className = 'otp-input';
|
|
104
108
|
wrapper.dir = 'ltr';
|
|
109
|
+
wrapper.dataset.otpAlign = this.options.align;
|
|
105
110
|
const cells = document.createElement('div');
|
|
106
111
|
cells.className = 'otp-input-slots';
|
|
107
112
|
cells.setAttribute('aria-hidden', 'true');
|
|
@@ -31,12 +31,12 @@ export class PasswordInput extends Component<PasswordInputOptions> {
|
|
|
31
31
|
this._suffixEl = this.el.parentElement?.querySelector<HTMLElement>('[data-password-toggle-icon]') ?? null;
|
|
32
32
|
|
|
33
33
|
if (this._suffixEl) {
|
|
34
|
-
for (const name of ['type', 'role', 'tabindex', 'aria-label', 'aria-pressed', 'aria-controls', 'aria-disabled', 'disabled'])
|
|
34
|
+
for (const name of ['type', 'role', 'tabindex', 'aria-label', 'aria-pressed', 'aria-controls', 'aria-disabled', 'disabled', 'icon-text'])
|
|
35
35
|
this._originalAttributes.set(name, this._suffixEl.getAttribute(name));
|
|
36
36
|
|
|
37
37
|
if (this._suffixEl instanceof HTMLButtonElement) {
|
|
38
38
|
this._suffixEl.type = 'button';
|
|
39
|
-
} else {
|
|
39
|
+
} else if (!this._isCrazyButton()) {
|
|
40
40
|
this._suffixEl.setAttribute('role', 'button');
|
|
41
41
|
this._suffixEl.tabIndex = 0;
|
|
42
42
|
}
|
|
@@ -96,6 +96,10 @@ export class PasswordInput extends Component<PasswordInputOptions> {
|
|
|
96
96
|
this._syncState();
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
private _isCrazyButton(): boolean {
|
|
100
|
+
return this._suffixEl?.matches('crazy-button, regular-btn') ?? false;
|
|
101
|
+
}
|
|
102
|
+
|
|
99
103
|
private _syncState() {
|
|
100
104
|
const visible = this.el.type === 'text';
|
|
101
105
|
const disabled = this.el.matches(':disabled');
|
|
@@ -104,8 +108,13 @@ export class PasswordInput extends Component<PasswordInputOptions> {
|
|
|
104
108
|
this._suffixEl?.setAttribute('aria-pressed', String(visible));
|
|
105
109
|
this._suffixEl?.setAttribute('aria-disabled', String(disabled));
|
|
106
110
|
if (this._suffixEl instanceof HTMLButtonElement) this._suffixEl.disabled = disabled;
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
if (this._isCrazyButton()) {
|
|
112
|
+
this._suffixEl?.toggleAttribute('disabled', disabled);
|
|
113
|
+
this._suffixEl?.setAttribute('icon-text', visible ? 'visibility_off' : 'visibility');
|
|
114
|
+
} else {
|
|
115
|
+
const icon = this._suffixEl?.querySelector('i');
|
|
116
|
+
if (icon) icon.textContent = visible ? 'visibility_off' : 'visibility';
|
|
117
|
+
}
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
private _handleToggleClick = (event: MouseEvent) => {
|
|
@@ -115,7 +124,7 @@ export class PasswordInput extends Component<PasswordInputOptions> {
|
|
|
115
124
|
|
|
116
125
|
private _handleToggleKeydown = (event: KeyboardEvent) => {
|
|
117
126
|
// Native buttons already dispatch clicks for Enter and Space.
|
|
118
|
-
if (this._suffixEl instanceof HTMLButtonElement || !['Enter', ' '].includes(event.key)) return;
|
|
127
|
+
if (this._suffixEl instanceof HTMLButtonElement || this._isCrazyButton() || !['Enter', ' '].includes(event.key)) return;
|
|
119
128
|
event.preventDefault();
|
|
120
129
|
this.toggle();
|
|
121
130
|
};
|
|
@@ -128,3 +128,50 @@
|
|
|
128
128
|
border-radius: 4px;
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
// A semantic group needs its own layout; floating text-field labels do not apply.
|
|
133
|
+
fieldset.radio-group {
|
|
134
|
+
min-width: 0;
|
|
135
|
+
margin: 0;
|
|
136
|
+
padding: 8px 16px 12px;
|
|
137
|
+
border: 1px solid var(--md-sys-color-outline);
|
|
138
|
+
border-radius: 4px;
|
|
139
|
+
|
|
140
|
+
> legend {
|
|
141
|
+
padding: 0 4px;
|
|
142
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
143
|
+
font-size: 12px;
|
|
144
|
+
line-height: 16px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
> p:not(.supporting-text) { margin: 0; }
|
|
148
|
+
|
|
149
|
+
label {
|
|
150
|
+
position: relative;
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
min-height: 44px;
|
|
154
|
+
color: var(--md-sys-color-on-surface);
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
input[type="radio"] + span {
|
|
159
|
+
height: auto;
|
|
160
|
+
min-height: 24px;
|
|
161
|
+
padding-left: 28px;
|
|
162
|
+
line-height: 24px;
|
|
163
|
+
&::before, &::after { margin: 4px 0; }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
input:focus-visible + span {
|
|
167
|
+
outline: 2px solid var(--md-sys-color-primary);
|
|
168
|
+
outline-offset: 4px;
|
|
169
|
+
border-radius: 4px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
&.error {
|
|
173
|
+
background: transparent;
|
|
174
|
+
border-color: var(--md-sys-color-error);
|
|
175
|
+
> legend { color: var(--md-sys-color-error); }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -65,16 +65,28 @@
|
|
|
65
65
|
button:hover .ql-fill, button.ql-active .ql-fill { fill: currentColor; }
|
|
66
66
|
a { color: var(--md-sys-color-primary); }
|
|
67
67
|
}
|
|
68
|
-
.ql-container.ql-snow {
|
|
68
|
+
.ql-container.ql-snow {
|
|
69
|
+
height: auto;
|
|
70
|
+
border: 0;
|
|
71
|
+
border-radius: 0 0 11px 11px;
|
|
72
|
+
font-family: inherit;
|
|
73
|
+
font-size: 1rem;
|
|
74
|
+
&:first-child { border-radius: 11px; }
|
|
75
|
+
}
|
|
69
76
|
.ql-editor {
|
|
70
77
|
min-height: var(--rich-textarea-min-height);
|
|
71
78
|
max-height: var(--rich-textarea-max-height, 480px);
|
|
72
79
|
padding: 16px;
|
|
73
80
|
line-height: 1.6;
|
|
81
|
+
font-family: var(--rich-textarea-font-family, inherit);
|
|
74
82
|
color: inherit;
|
|
83
|
+
// Keep the editor surface inside the rounded border without clipping toolbar popups.
|
|
84
|
+
background: transparent;
|
|
85
|
+
border-radius: inherit;
|
|
75
86
|
overflow-wrap: anywhere;
|
|
76
87
|
outline: none;
|
|
77
88
|
&.ql-blank::before { left: 16px; right: 16px; color: var(--md-sys-color-on-surface-variant); font-style: normal; }
|
|
89
|
+
strong, b { font-weight: 800; }
|
|
78
90
|
blockquote { border-color: var(--md-sys-color-outline); }
|
|
79
91
|
}
|
|
80
92
|
.ql-tooltip {
|
|
@@ -9,6 +9,8 @@ export interface RichTextareaOptions extends BaseOptions {
|
|
|
9
9
|
toolbar?: false | Array<ToolbarItem | ToolbarItem[]>;
|
|
10
10
|
formats?: string[];
|
|
11
11
|
placeholder?: string;
|
|
12
|
+
/** Default font stack for editor content; load any web fonts separately. */
|
|
13
|
+
fontFamily?: string;
|
|
12
14
|
label?: string;
|
|
13
15
|
}
|
|
14
16
|
let sequence = 0;
|
|
@@ -80,6 +82,7 @@ export class RichTextarea extends Component<RichTextareaOptions> {
|
|
|
80
82
|
const wrapper = document.createElement('div');
|
|
81
83
|
this._wrapper = wrapper;
|
|
82
84
|
wrapper.className = 'rich-textarea';
|
|
85
|
+
if (this.options.fontFamily) wrapper.style.setProperty('--rich-textarea-font-family', this.options.fontFamily);
|
|
83
86
|
const editor = document.createElement('div');
|
|
84
87
|
const error = document.createElement('div');
|
|
85
88
|
this._error = error;
|