kmaterialize 2.3.3-1.0.36 → 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/editor/editor.ts +71 -4
- package/components/editor/editorSpec.js +89 -0
- 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/radio-button/_radio-buttons.scss +47 -0
- package/components/slider/_range.scss +29 -1
- 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 +31 -2
- package/components/textfield/forms.ts +2 -0
- 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 +239 -27
- package/dist/css/materialize.min.css +2 -2
- package/dist/css/materialize.min.css.map +1 -1
- package/dist/js/materialize.cjs.js +254 -13
- package/dist/js/materialize.d.ts +9 -1
- package/dist/js/materialize.js +254 -13
- package/dist/js/materialize.min.js +3 -3
- package/dist/js/materialize.mjs +254 -13
- package/package.json +3 -3
|
@@ -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();
|
|
@@ -430,4 +430,93 @@ describe('Editor Handlebars workspace', function () {
|
|
|
430
430
|
parent.replaceWith(host);
|
|
431
431
|
}
|
|
432
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
|
+
|
|
433
522
|
});
|
|
@@ -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');
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,32 @@
|
|
|
1
|
-
.range-field {
|
|
1
|
+
.range-field {
|
|
2
|
+
position: relative;
|
|
3
|
+
min-width: 0;
|
|
4
|
+
|
|
5
|
+
// Keep supporting messages close without reducing the slider's interaction area.
|
|
6
|
+
&:has(> .supporting-text),
|
|
7
|
+
&:has(+ .supporting-text) {
|
|
8
|
+
> input[type='range'] { margin-bottom: 0; }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
> .supporting-text,
|
|
12
|
+
+ .supporting-text { margin-top: 0; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.range-field-header {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: baseline;
|
|
18
|
+
justify-content: space-between;
|
|
19
|
+
gap: 16px;
|
|
20
|
+
|
|
21
|
+
> label { min-width: 0; }
|
|
22
|
+
> output {
|
|
23
|
+
flex-shrink: 0;
|
|
24
|
+
color: var(--md-sys-color-on-surface);
|
|
25
|
+
font-size: 14px;
|
|
26
|
+
font-variant-numeric: tabular-nums;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
2
30
|
|
|
3
31
|
input[type='range'] {
|
|
4
32
|
--range-active-color: var(--md-sys-color-primary);
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
&:checked + .lever {
|
|
40
40
|
&:before, &:after {
|
|
41
41
|
top: var(--gap-on);
|
|
42
|
-
|
|
42
|
+
inset-inline-start: calc(var(--track-width) - var(--size-on) - var(--gap-on) - 2 * var(--border-width));
|
|
43
43
|
width: var(--size-on);
|
|
44
44
|
height: var(--size-on);
|
|
45
45
|
}
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
margin-right: 10px;
|
|
67
67
|
transition: background 0.3s ease;
|
|
68
68
|
vertical-align: middle;
|
|
69
|
+
touch-action: pan-y;
|
|
69
70
|
margin: 0 16px;
|
|
70
71
|
|
|
71
72
|
// DOT
|
|
@@ -77,10 +78,10 @@
|
|
|
77
78
|
height: var(--size-off);
|
|
78
79
|
border-radius: 50%;
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
inset-inline-start: var(--gap-off);
|
|
81
82
|
top: var(--gap-off);
|
|
82
83
|
|
|
83
|
-
transition:
|
|
84
|
+
transition: inset-inline-start 0.3s ease, top .3s ease, width .3s ease, height .3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
// NOT CHECKED [DOT]
|
|
@@ -150,7 +151,54 @@ input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before {
|
|
|
150
151
|
color: var(--md-sys-color-primary);
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
.switch
|
|
154
|
+
.switch label input[type="checkbox"]:focus-visible + .lever {
|
|
154
155
|
outline: 2px solid var(--md-sys-color-primary);
|
|
155
156
|
outline-offset: 4px;
|
|
156
157
|
}
|
|
158
|
+
|
|
159
|
+
/// Preview the pointer position while the native checked state waits for release.
|
|
160
|
+
.switch label input[type="checkbox"] + .lever.is-dragging {
|
|
161
|
+
--switch-drag-size: calc(var(--size-off) + (var(--size-on) - var(--size-off)) * var(--switch-drag-progress));
|
|
162
|
+
background-color: color-mix(in srgb, var(--md-sys-color-primary) var(--switch-drag-percent), var(--md-sys-color-surface-variant));
|
|
163
|
+
border-color: color-mix(in srgb, var(--md-sys-color-primary) var(--switch-drag-percent), var(--md-sys-color-outline));
|
|
164
|
+
transition: none;
|
|
165
|
+
|
|
166
|
+
&::before,
|
|
167
|
+
&::after {
|
|
168
|
+
width: var(--switch-drag-size);
|
|
169
|
+
height: var(--switch-drag-size);
|
|
170
|
+
top: calc((var(--track-height) - var(--switch-drag-size)) / 2 - var(--border-width));
|
|
171
|
+
inset-inline-start: calc(var(--track-height) / 2 - var(--border-width) + (var(--track-width) - var(--track-height)) * var(--switch-drag-progress) - var(--switch-drag-size) / 2);
|
|
172
|
+
transition: none;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
&::after {
|
|
176
|
+
background-color: color-mix(in srgb, var(--md-sys-color-surface) var(--switch-drag-percent), var(--md-sys-color-outline));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Keep the drag halo equally visible for plain and icon thumbs, on either side.
|
|
181
|
+
.switch label input[type="checkbox"]:not(:disabled) + .lever.is-dragging::before {
|
|
182
|
+
width: var(--size-on);
|
|
183
|
+
height: var(--size-on);
|
|
184
|
+
top: var(--gap-on);
|
|
185
|
+
inset-inline-start: calc(var(--track-height) / 2 - var(--border-width) + (var(--track-width) - var(--track-height)) * var(--switch-drag-progress) - var(--size-on) / 2);
|
|
186
|
+
transform: scale(2.4);
|
|
187
|
+
background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 18%);
|
|
188
|
+
pointer-events: none;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.switch.switch-with-icon label input[type="checkbox"] + .lever.is-dragging::after {
|
|
192
|
+
content: var(--switch-icon-off, "\e5cd");
|
|
193
|
+
color: color-mix(in srgb, var(--md-sys-color-primary) var(--switch-drag-percent), var(--md-sys-color-surface));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.switch.switch-with-icon label input[type="checkbox"] + .lever.is-dragging.is-drag-on::after {
|
|
197
|
+
content: var(--switch-icon-on, "\e5ca");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@media (prefers-reduced-motion: reduce) {
|
|
201
|
+
.switch label .lever,
|
|
202
|
+
.switch label .lever::before,
|
|
203
|
+
.switch label .lever::after { transition: none; }
|
|
204
|
+
}
|