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.
Files changed (34) hide show
  1. package/components/code-card/_code-card.scss +0 -1
  2. package/components/code-card/codeCardSpec.js +11 -0
  3. package/components/editor/editor.ts +71 -4
  4. package/components/editor/editorSpec.js +92 -1
  5. package/components/extensions/web/crazy-button.ts +3 -1
  6. package/components/file-input/_file-input-enhanced.scss +4 -0
  7. package/components/otp-input/_otp-input.scss +20 -6
  8. package/components/otp-input/otp-input.ts +6 -1
  9. package/components/password-input/password-input.ts +14 -5
  10. package/components/radio-button/_radio-buttons.scss +47 -0
  11. package/components/rich-textarea/_rich-textarea.scss +13 -1
  12. package/components/rich-textarea/rich-textarea.ts +3 -0
  13. package/components/slider/_range.scss +234 -1
  14. package/components/slider/range.ts +359 -2
  15. package/components/switch/_switches.scss +52 -4
  16. package/components/switch/switch-drag.ts +198 -0
  17. package/components/switch/switchDragSpec.js +230 -0
  18. package/components/textfield/_file-input.scss +11 -1
  19. package/components/textfield/_input-fields.scss +62 -2
  20. package/components/textfield/forms.ts +16 -60
  21. package/components/textfield/outlined-notch.ts +13 -0
  22. package/components/textfield/outlinedLabelSpec.js +58 -0
  23. package/components/tom-select/_tom-select-field.scss +10 -0
  24. package/components/tom-select/tom-select-field.ts +5 -0
  25. package/dist/css/materialize.colors.min.css +1 -1
  26. package/dist/css/materialize.css +551 -28
  27. package/dist/css/materialize.min.css +2 -2
  28. package/dist/css/materialize.min.css.map +1 -1
  29. package/dist/js/materialize.cjs.js +631 -91
  30. package/dist/js/materialize.d.ts +65 -3
  31. package/dist/js/materialize.js +631 -91
  32. package/dist/js/materialize.min.js +3 -3
  33. package/dist/js/materialize.mjs +631 -92
  34. package/package.json +3 -3
@@ -36,7 +36,6 @@
36
36
  right: 6px;
37
37
 
38
38
  > .btn {
39
- --button-icon: 24px;
40
39
  color: inherit;
41
40
  }
42
41
  }
@@ -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', () => { this.lines.scrollTop = this.source.scrollTop; this.syncHighlight(); if (this.dragPoint) this.showDropCaret(); }, { signal });
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
- const bounds = this.source.getBoundingClientRect();
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
- await load('prism.js'); await load('components/prism-markup-templating.js'); await load('components/prism-handlebars.js');
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}"` : ''} ${menu ? menuAttrs : ''}>${content}</${tag}>`;
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;
@@ -33,6 +33,10 @@
33
33
  font-size: 1rem;
34
34
  }
35
35
 
36
+ &:has(> .supporting-text) .filepond--root {
37
+ margin-bottom: 0;
38
+ }
39
+
36
40
  .filepond--panel-root {
37
41
  border-radius: 12px;
38
42
  background-color: var(--md-sys-color-surface-variant);
@@ -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: flex;
65
- flex-direction: column;
66
- align-items: flex-start;
67
- margin-block: 24px;
68
- > .otp-input { margin: 0; }
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 { display: block; padding-inline: 0; }
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
- const icon = this._suffixEl?.querySelector('i');
108
- if (icon) icon.textContent = visible ? 'visibility_off' : 'visibility';
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 { height: auto; border: 0; font-family: inherit; font-size: 1rem; }
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;