kmaterialize 2.3.3-1.0.34 → 2.3.3-1.0.36

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.
@@ -0,0 +1,90 @@
1
+ // Match the documentation's compact code blocks and Atom One Dark palette.
2
+ .card.code-card {
3
+ --code-card-background: #282c34;
4
+ --code-card-color: #abb2bf;
5
+ --code-card-border-color: #8888;
6
+ --code-card-font-size: 0.85rem;
7
+ --code-card-padding: 1em;
8
+
9
+ position: relative;
10
+ min-width: 0;
11
+ max-width: 100%;
12
+ margin: 1em 0;
13
+ overflow: hidden;
14
+ font: var(--code-card-font-size)/1.25 monospace, monospace;
15
+ background: var(--code-card-background);
16
+ color: var(--code-card-color);
17
+ border: 1px solid var(--code-card-border-color);
18
+ // The standard .card supplies the 12px border radius.
19
+ box-shadow: none;
20
+
21
+ .code-card-status, .code-card-copy {
22
+ position: absolute;
23
+ z-index: 2;
24
+ color: var(--md-sys-color-on-surface-variant, #abb2bf);
25
+ }
26
+
27
+ .code-card-status {
28
+ top: 15px;
29
+ right: 45px;
30
+ font: 14px/1.25 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
31
+ pointer-events: none;
32
+ }
33
+
34
+ .code-card-copy {
35
+ top: 6px;
36
+ right: 6px;
37
+
38
+ > .btn {
39
+ color: inherit;
40
+ }
41
+ }
42
+ .code-card-copy[hidden] { display: none; }
43
+ .code-card-copy > .btn:focus-visible, .code-card-body:focus-visible {
44
+ outline: 2px solid var(--md-sys-color-primary, #61aeee);
45
+ outline-offset: -2px;
46
+ }
47
+
48
+ pre.code-card-body {
49
+ display: block;
50
+ margin: 0;
51
+ padding: 0;
52
+ max-height: var(--code-card-max-height, none);
53
+ overflow: auto;
54
+ border: 0;
55
+ border-radius: 0;
56
+ background: transparent;
57
+ color: inherit;
58
+ text-align: start;
59
+ direction: ltr;
60
+ white-space: pre;
61
+ }
62
+ pre.code-card-body, pre.code-card-body > code {
63
+ font: inherit;
64
+ text-shadow: none;
65
+ }
66
+ pre.code-card-body > code {
67
+ display: block;
68
+ width: max-content;
69
+ min-width: 100%;
70
+ margin: 0;
71
+ padding: var(--code-card-padding);
72
+ border: 0;
73
+ background: transparent;
74
+ color: inherit;
75
+ white-space: inherit;
76
+ }
77
+
78
+ .token { background: transparent; text-shadow: none; }
79
+ .token.comment, .token.prolog, .token.doctype, .token.cdata { color: #5c6370; font-style: italic; }
80
+ .token.punctuation, .token.operator, .token.property { color: var(--code-card-color); }
81
+ .token.keyword, .token.atrule { color: #c678dd; }
82
+ .token.tag, .token.selector, .token.deleted { color: #e06c75; }
83
+ .token.function, .token.function-variable, .token.url { color: #61aeee; }
84
+ .token.class-name, .token.builtin { color: #e6c07b; }
85
+ .token.attr-name, .token.number, .token.constant, .token.variable, .token.builtin-variable { color: #d19a66; }
86
+ .token.string, .token.string-property, .token.attr-value, .token.regex, .token.inserted { color: #98c379; }
87
+ .token.boolean, .token.entity { color: #56b6c2; }
88
+ .token.important, .token.bold { font-weight: bold; }
89
+ .token.italic { font-style: italic; }
90
+ }
@@ -0,0 +1,198 @@
1
+ import { BaseOptions, Component, InitElements, MElement } from "../../src/component";
2
+ import CrazyButton from "../extensions/web/crazy-button";
3
+ import { loadPeer } from "../../src/peer-loader";
4
+ import type * as Prism from "prismjs";
5
+
6
+ /** Display source code with optional syntax highlighting and clipboard controls. */
7
+ export interface CodeCardOptions extends BaseOptions {
8
+ code:string;
9
+ language:string;
10
+ title:string;
11
+ copy:boolean;
12
+ highlight:boolean;
13
+ copyLabel:string;
14
+ copiedLabel:string;
15
+ errorLabel:string;
16
+ }
17
+
18
+ const _defaults:CodeCardOptions = {
19
+ code: "",
20
+ language: "plain",
21
+ title: "",
22
+ copy: true,
23
+ highlight: true,
24
+ copyLabel: "Copy code",
25
+ copiedLabel: "Copied!",
26
+ errorLabel: "Unable to copy",
27
+ };
28
+
29
+ /** A themed code card. Source text is never executed or interpreted as markup. */
30
+ export class CodeCard extends Component<CodeCardOptions> {
31
+
32
+ public ready:Promise<void>;
33
+ private _prism?:typeof Prism;
34
+ private _originalNodes:Node[];
35
+ private _addedClasses:string[];
36
+ private _code:HTMLElement;
37
+ private _pre:HTMLPreElement;
38
+ private _button:CrazyButton;
39
+ private _status:HTMLElement;
40
+ private _timer:ReturnType<typeof setTimeout>;
41
+ private _copying:boolean = false;
42
+ private _destroyed:boolean = false;
43
+ private _revision:number = 0;
44
+
45
+ public constructor(el:HTMLElement, options:Partial<CodeCardOptions> = {}){
46
+ super(el, options, CodeCard);
47
+ const source = el.querySelector("pre > code, code");
48
+ const language = Array.from(source?.classList || []).find(name => name.startsWith("language-"))?.slice(9);
49
+ this.options = {
50
+ ...CodeCard.defaults,
51
+ code: source?.textContent || "",
52
+ language: el.dataset.codeLanguage || language || "plain",
53
+ title: el.dataset.codeTitle || "",
54
+ copy: el.dataset.codeCopy !== "false",
55
+ highlight: el.dataset.codeHighlight !== "false",
56
+ ...options,
57
+ };
58
+ this._originalNodes = Array.from(el.childNodes);
59
+ this._addedClasses = ["card", "code-card"].filter(name => !el.classList.contains(name));
60
+ el.classList.add(...this._addedClasses);
61
+ el["M_CodeCard"] = this;
62
+ this._createElements();
63
+ this._render();
64
+ this.ready = this.update({});
65
+ }
66
+
67
+ public static get defaults():CodeCardOptions { return _defaults; }
68
+ public static init(el:HTMLElement, options?:Partial<CodeCardOptions>):CodeCard;
69
+ public static init(els:InitElements<MElement>, options?:Partial<CodeCardOptions>):CodeCard[];
70
+ public static init(els:HTMLElement|InitElements<MElement>, options:Partial<CodeCardOptions> = {}):CodeCard|CodeCard[] {
71
+ return super.init(els, options, CodeCard);
72
+ }
73
+ public static getInstance(el:HTMLElement):CodeCard { return el["M_CodeCard"]; }
74
+
75
+ /** Update content, language, title, or the optional copy control. */
76
+ public async update(options:Partial<CodeCardOptions>):Promise<void> {
77
+ if(this._destroyed) return;
78
+ this.options = { ...this.options, ...options };
79
+ this._revision++;
80
+ clearTimeout(this._timer);
81
+ this._setCopyFeedback("content_copy");
82
+ this._render();
83
+ if(this.options.highlight && !this._prism){
84
+ this._prism = await loadPeer<typeof Prism>({
85
+ specifier: "prismjs",
86
+ globalName: "Prism",
87
+ feature: "CodeCard syntax highlighting",
88
+ cdnHint: '<script src="path/to/prism.js" data-manual></script> (include the language grammars you use)',
89
+ }, async() => {
90
+ const scope = window as Window & { Prism?:typeof Prism|{ manual:boolean } };
91
+ const created = !scope.Prism;
92
+ if(created) scope.Prism = { manual: true };
93
+ try { return await import("prismjs"); }
94
+ catch(error){ if(created) delete scope.Prism; throw error; }
95
+ });
96
+ }
97
+ if(!this._destroyed) this._render();
98
+ }
99
+
100
+ public getCode():string { return this.options.code; }
101
+
102
+ /** Copy the original source, preserving whitespace and excluding UI labels. */
103
+ public async copy():Promise<boolean> {
104
+ if(this._destroyed || this._copying || !this.options.copy) return false;
105
+ const revision = this._revision;
106
+ const restoreFocus = this._button.contains(document.activeElement);
107
+ this._copying = true;
108
+ this._button.setProperty("disabled", true);
109
+ clearTimeout(this._timer);
110
+ this._setCopyFeedback("content_copy");
111
+ let success = false;
112
+ try {
113
+ await navigator.clipboard.writeText(this.options.code);
114
+ success = true;
115
+ }catch{
116
+ // Leave the source available for manual selection when clipboard access fails.
117
+ }finally{
118
+ this._copying = false;
119
+ }
120
+ if(this._destroyed) return success;
121
+ this._button.setProperty("disabled", false);
122
+ if(revision === this._revision){
123
+ this._setCopyFeedback(success ? "check" : "error_outline", success ? this.options.copiedLabel : this.options.errorLabel);
124
+ this._timer = setTimeout(() => this._setCopyFeedback("content_copy"), 2000);
125
+ }
126
+ await this._button.updateComplete;
127
+ if(this._destroyed) return success;
128
+ if(restoreFocus && document.activeElement === document.body && this.options.copy && this._button.isConnected)
129
+ this._button.querySelector("button")?.focus({ preventScroll: true });
130
+ this.el.dispatchEvent(new CustomEvent("codecopy", { bubbles: true, detail: { success } }));
131
+ return success;
132
+ }
133
+
134
+ /** Restore the authored markup and remove component-owned listeners. */
135
+ public destroy():void {
136
+ if(this._destroyed) return;
137
+ this._destroyed = true;
138
+ clearTimeout(this._timer);
139
+ this._button.removeEventListener("buttonaction", this._onCopy);
140
+ this.el.replaceChildren(...this._originalNodes);
141
+ this.el.classList.remove(...this._addedClasses);
142
+ delete this.el["M_CodeCard"];
143
+ }
144
+
145
+ private _onCopy = ():void => { void this.copy(); };
146
+
147
+ private _createElements():void {
148
+ this._status = document.createElement("span");
149
+ this._status.className = "code-card-status";
150
+ this._status.setAttribute("role", "status");
151
+ this._button = new CrazyButton();
152
+ this._button.className = "code-card-copy";
153
+ this._button.setAttribute("type", "icon");
154
+ this._button.setAttribute("variant", "standard");
155
+ this._button.setAttribute("size", "xs");
156
+ this._button.addEventListener("buttonaction", this._onCopy);
157
+ this._pre = document.createElement("pre");
158
+ this._pre.className = "code-card-body";
159
+ this._pre.tabIndex = 0;
160
+ this._code = document.createElement("code");
161
+ this._pre.append(this._code);
162
+ this.el.replaceChildren(this._status, this._button, this._pre);
163
+ }
164
+
165
+ private _setCopyFeedback(icon:string, message:string = ""):void {
166
+ this._status.textContent = message;
167
+ this._button.setAttribute("icon-text", icon);
168
+ this._button.setAttribute("aria-label", message || this.options.copyLabel);
169
+ this._button.title = message || this.options.copyLabel;
170
+ }
171
+
172
+ private _render():void {
173
+ const requested = this.options.language.trim().toLowerCase();
174
+ const aliases = { html: "markup", xml: "markup", js: "javascript", ts: "typescript", shell: "bash", text: "plain", plaintext: "plain" };
175
+ const language = /^[a-z0-9-]+$/.test(requested) ? aliases[requested] || requested : "plain";
176
+ const labels = { markup: "HTML", css: "CSS", javascript: "JavaScript", typescript: "TypeScript", php: "PHP", json: "JSON", yaml: "YAML", bash: "Shell", python: "Python", handlebars: "Handlebars", plain: "Plain text" };
177
+ this._pre.setAttribute("aria-label", this.options.title || (labels[language] || requested.toUpperCase()) + " code");
178
+ this._button.hidden = !this.options.copy;
179
+ this._code.className = "language-" + language;
180
+ let grammar = this._prism?.languages[language];
181
+ if(grammar && (language === "javascript" || language === "typescript")){
182
+ // Match the documentation's class and browser-global colors without changing shared grammars.
183
+ const classes = grammar["class-name"];
184
+ grammar = {
185
+ ...grammar,
186
+ "class-name": [
187
+ ...(Array.isArray(classes) ? classes : classes ? [classes] : []),
188
+ { pattern: /\b[A-Z][\w$]*(?=\s*\.)/ },
189
+ ],
190
+ "builtin-variable": /\b(?:document|window|console|navigator|globalThis)\b/,
191
+ };
192
+ }
193
+ if(this.options.highlight && grammar)
194
+ this._code.innerHTML = this._prism.highlight(this.options.code, grammar, language);
195
+ else
196
+ this._code.textContent = this.options.code;
197
+ }
198
+ }
@@ -0,0 +1,170 @@
1
+ describe("CodeCard", function(){
2
+ let host;
3
+ let instance;
4
+ let clipboard;
5
+ let writeText;
6
+
7
+ beforeEach(function(){
8
+ host = document.createElement("div");
9
+ host.innerHTML = '<pre><code class="language-javascript">const answer = 42;</code></pre>';
10
+ document.body.append(host);
11
+ clipboard = Object.getOwnPropertyDescriptor(navigator, "clipboard");
12
+ writeText = jasmine.createSpy("writeText").and.returnValue(Promise.resolve());
13
+ Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } });
14
+ });
15
+
16
+ afterEach(function(){
17
+ instance?.destroy();
18
+ instance = undefined;
19
+ host.remove();
20
+ if(clipboard) Object.defineProperty(navigator, "clipboard", clipboard);
21
+ else delete navigator.clipboard;
22
+ });
23
+
24
+ it("renders source as text without executing HTML", async function(){
25
+ const code = '<img src=x onerror="window.__codeExecuted=true">\n<script>alert(1)</script>';
26
+ instance = M.CodeCard.init(host, { code, language: "html", highlight: false });
27
+ await instance.ready;
28
+ expect(host.querySelector("pre code").textContent).toBe(code);
29
+ expect(host.querySelector("img, script")).toBeNull();
30
+ expect(instance.getCode()).toBe(code);
31
+ });
32
+
33
+ it("copies original source including indentation and trailing newline", async function(){
34
+ const code = "first\n second\n";
35
+ instance = M.CodeCard.init(host, { code, title: "Do not copy this title", highlight: false });
36
+ await instance.ready;
37
+ expect(await instance.copy()).toBeTrue();
38
+ expect(writeText).toHaveBeenCalledOnceWith(code);
39
+ expect(host.querySelector('[role="status"]').textContent).toBe("Copied!");
40
+ expect(host.querySelector("crazy-button i").textContent).toBe("check");
41
+ expect(host.querySelector("button").getAttribute("aria-label")).toBe("Copied!");
42
+ });
43
+
44
+ it("hides and disables the copy action when requested", async function(){
45
+ host.dataset.codeCopy = "false";
46
+ instance = M.CodeCard.init(host, { highlight: false });
47
+ await instance.ready;
48
+ expect(host.querySelector("crazy-button").hidden).toBeTrue();
49
+ expect(await instance.copy()).toBeFalse();
50
+ expect(writeText).not.toHaveBeenCalled();
51
+ await instance.update({ copy: true });
52
+ expect(host.querySelector("crazy-button").hidden).toBeFalse();
53
+ });
54
+
55
+ it("reports clipboard errors instead of announcing success", async function(){
56
+ writeText.and.returnValue(Promise.reject(new Error("Permission denied")));
57
+ instance = M.CodeCard.init(host, { highlight: false });
58
+ await instance.ready;
59
+ expect(await instance.copy()).toBeFalse();
60
+ expect(host.querySelector('[role="status"]').textContent).toBe("Unable to copy");
61
+ expect(host.querySelector("button").disabled).toBeFalse();
62
+ expect(host.querySelector("crazy-button i").textContent).toBe("error_outline");
63
+ expect(host.querySelector("button").getAttribute("aria-label")).toBe("Unable to copy");
64
+ });
65
+
66
+ it("updates custom content and treats unknown language names as plain text", async function(){
67
+ instance = M.CodeCard.init(host, { highlight: false });
68
+ await instance.ready;
69
+ await instance.update({ code: "custom => value", language: "custom", title: "My format", copy: false });
70
+ expect(host.querySelector("pre code").textContent).toBe("custom => value");
71
+ expect(host.querySelector("pre").getAttribute("aria-label")).toBe("My format");
72
+ await instance.update({ title: "" });
73
+ expect(host.querySelector("pre").getAttribute("aria-label")).toBe("CUSTOM code");
74
+ expect(host.querySelector("crazy-button").hidden).toBeTrue();
75
+ });
76
+
77
+ it("restores the authored DOM and listeners on destroy", async function(){
78
+ const original = host.firstElementChild;
79
+ instance = M.CodeCard.init(host, { highlight: false });
80
+ await instance.ready;
81
+ const button = host.querySelector("button");
82
+ instance.destroy();
83
+ instance = undefined;
84
+ button.click();
85
+ expect(writeText).not.toHaveBeenCalled();
86
+ expect(host.firstElementChild).toBe(original);
87
+ expect(host.classList.contains("code-card")).toBeFalse();
88
+ expect(M.CodeCard.getInstance(host)).toBeUndefined();
89
+ });
90
+
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
+ }
103
+ instance = M.CodeCard.init(host);
104
+ await instance.ready;
105
+ expect(host.querySelectorAll(".token").length).toBeGreaterThan(0);
106
+ expect(host.querySelector("pre code").textContent).toBe("const answer = 42;");
107
+ });
108
+
109
+ it("keeps a replacement instance intact when an old owner destroys twice", async function(){
110
+ const old = M.CodeCard.init(host, { highlight: false });
111
+ await old.ready;
112
+ instance = M.CodeCard.init(host, { code: "replacement", highlight: false });
113
+ await instance.ready;
114
+ old.destroy();
115
+ expect(M.CodeCard.getInstance(host)).toBe(instance);
116
+ expect(host.querySelector("pre code").textContent).toBe("replacement");
117
+ });
118
+
119
+ it("does not mutate restored markup when a pending copy completes", async function(){
120
+ let resolve;
121
+ writeText.and.returnValue(new Promise(done => { resolve = done; }));
122
+ instance = M.CodeCard.init(host, { highlight: false });
123
+ await instance.ready;
124
+ const copying = instance.copy();
125
+ instance.destroy();
126
+ instance = undefined;
127
+ const restored = host.innerHTML;
128
+ resolve();
129
+ await copying;
130
+ expect(host.innerHTML).toBe(restored);
131
+ });
132
+
133
+ it("uses CrazyButton activation and restores its icon after success feedback", async function(){
134
+ jasmine.clock().install();
135
+ try {
136
+ instance = M.CodeCard.init(host, { highlight: false });
137
+ await instance.ready;
138
+ const button = host.querySelector("crazy-button");
139
+ const copied = new Promise(resolve => host.addEventListener("codecopy", resolve, { once: true }));
140
+ button.querySelector("button").click();
141
+ await copied;
142
+ expect(writeText).toHaveBeenCalledTimes(1);
143
+ expect(button.querySelector("i").textContent).toBe("check");
144
+ jasmine.clock().tick(2000);
145
+ await button.updateComplete;
146
+ expect(button.querySelector("i").textContent).toBe("content_copy");
147
+ expect(button.querySelector("button").getAttribute("aria-label")).toBe("Copy code");
148
+ expect(host.querySelector('[role="status"]').textContent).toBe("");
149
+ }finally{
150
+ jasmine.clock().uninstall();
151
+ }
152
+ });
153
+
154
+ it("does not show stale success when the source changes during a copy", async function(){
155
+ let resolve;
156
+ writeText.and.returnValue(new Promise(done => { resolve = done; }));
157
+ instance = M.CodeCard.init(host, { highlight: false });
158
+ await instance.ready;
159
+ const copying = instance.copy();
160
+ expect(await instance.copy()).toBeFalse();
161
+ await instance.update({ code: "new source" });
162
+ resolve();
163
+ await copying;
164
+ expect(writeText).toHaveBeenCalledTimes(1);
165
+ expect(host.querySelector("crazy-button i").textContent).toBe("content_copy");
166
+ expect(host.querySelector("button").getAttribute("aria-label")).toBe("Copy code");
167
+ expect(host.querySelector('[role="status"]').textContent).toBe("");
168
+ });
169
+
170
+ });
@@ -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');
@@ -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;
@@ -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
  };
@@ -49,6 +49,10 @@
49
49
  animation: popup-stepper-spin 1s linear infinite;
50
50
  }
51
51
  }
52
+ &[data-state='waiting'] .popup-stepper-marker {
53
+ background: var(--md-sys-color-primary-container);
54
+ color: var(--md-sys-color-on-primary-container);
55
+ }
52
56
  &[data-state='complete'] .popup-stepper-marker {
53
57
  background: var(--md-sys-color-primary);
54
58
  color: var(--md-sys-color-on-primary);
@@ -70,6 +74,9 @@
70
74
  color: var(--md-sys-color-on-surface-variant);
71
75
  font-weight: 600;
72
76
  }
77
+ .popup-stepper-content:not(:empty) { margin-top: 16px; }
78
+ .popup-stepper-validation { color: var(--md-sys-color-error); }
79
+ .popup-stepper-content .input-field { margin-block: 16px 8px; }
73
80
  .popup-stepper-state { display: block; font-size: .75rem; }
74
81
  .popup-stepper-status { font-size: .875rem; color: var(--md-sys-color-on-surface-variant); }
75
82
  }