@mfp-design-system/button 0.1.0 → 1.0.0
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/README.md +20 -9
- package/dist/button.d.ts +6 -0
- package/dist/button.d.ts.map +1 -1
- package/dist/button.js +137 -117
- package/dist/button.js.map +1 -1
- package/package.json +3 -3
- package/src/button.stories.ts +70 -70
- package/src/button.test.ts +79 -0
- package/src/button.ts +176 -149
package/README.md
CHANGED
|
@@ -52,22 +52,33 @@ For custom styling, the inner `<button>` is exposed as a CSS shadow part:
|
|
|
52
52
|
|
|
53
53
|
```css
|
|
54
54
|
mfp-button::part(button) {
|
|
55
|
-
|
|
55
|
+
border-radius: 999px;
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
## Framework notes
|
|
60
60
|
|
|
61
61
|
- **Vue 3 / Nuxt**: tell the template compiler that `mfp-` tags are custom elements:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
```ts
|
|
63
|
+
// vite.config.ts or nuxt.config.ts
|
|
64
|
+
compilerOptions: {
|
|
65
|
+
isCustomElement: (tag) => tag.startsWith('mfp-'),
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
68
|
- **Angular**: add `CUSTOM_ELEMENTS_SCHEMA` to any module/standalone component that uses `<mfp-button>`.
|
|
69
69
|
- **React**: works natively in React 19+; for older React, listen for the `click` event via a ref.
|
|
70
70
|
|
|
71
|
-
##
|
|
71
|
+
## Forms
|
|
72
72
|
|
|
73
|
-
-
|
|
73
|
+
`<mfp-button>` is form-associated via `ElementInternals`. Inside a `<form>`:
|
|
74
|
+
|
|
75
|
+
- `type="submit"` triggers `form.requestSubmit()` (full native flow including validation and the `submit` event)
|
|
76
|
+
- `type="reset"` triggers `form.reset()`
|
|
77
|
+
- The `form` getter returns the associated `<form>` element if any
|
|
78
|
+
|
|
79
|
+
```html
|
|
80
|
+
<form @submit.prevent="onSubmit">
|
|
81
|
+
<mfp-input name="email" type="email" required label="Email"></mfp-input>
|
|
82
|
+
<mfp-button type="submit" variant="primary">Sign up</mfp-button>
|
|
83
|
+
</form>
|
|
84
|
+
```
|
package/dist/button.d.ts
CHANGED
|
@@ -3,12 +3,18 @@ export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
|
|
|
3
3
|
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
4
4
|
export type ButtonType = 'button' | 'submit' | 'reset';
|
|
5
5
|
export declare class MfpButton extends LitElement {
|
|
6
|
+
static formAssociated: boolean;
|
|
7
|
+
private _internals;
|
|
8
|
+
constructor();
|
|
9
|
+
/** The associated <form>, if any. */
|
|
10
|
+
get form(): HTMLFormElement | null;
|
|
6
11
|
static styles: import("lit").CSSResult;
|
|
7
12
|
variant: ButtonVariant;
|
|
8
13
|
size: ButtonSize;
|
|
9
14
|
disabled: boolean;
|
|
10
15
|
loading: boolean;
|
|
11
16
|
type: ButtonType;
|
|
17
|
+
private _onClick;
|
|
12
18
|
render(): import("lit").TemplateResult<1>;
|
|
13
19
|
}
|
|
14
20
|
declare global {
|
package/dist/button.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../src/button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAG5C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AACzE,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEvD,qBACa,SAAU,SAAQ,UAAU;
|
|
1
|
+
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../src/button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAG5C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AACzE,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEvD,qBACa,SAAU,SAAQ,UAAU;IACrC,MAAM,CAAC,cAAc,UAAQ;IAE7B,OAAO,CAAC,UAAU,CAAmB;;IAOrC,qCAAqC;IACrC,IAAI,IAAI,IAAI,eAAe,GAAG,IAAI,CAEjC;IAED,OAAgB,MAAM,0BAqHpB;IAGF,OAAO,EAAE,aAAa,CAAa;IAGnC,IAAI,EAAE,UAAU,CAAQ;IAGxB,QAAQ,UAAS;IAGjB,OAAO,UAAS;IAGhB,IAAI,EAAE,UAAU,CAAY;IAE5B,OAAO,CAAC,QAAQ,CAOd;IAEO,MAAM;CAkBlB;AAED,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,YAAY,EAAE,SAAS,CAAC;KAC3B;CACJ"}
|
package/dist/button.js
CHANGED
|
@@ -7,145 +7,165 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { LitElement, html, css } from 'lit';
|
|
8
8
|
import { customElement, property } from 'lit/decorators.js';
|
|
9
9
|
let MfpButton = class MfpButton extends LitElement {
|
|
10
|
+
static { this.formAssociated = true; }
|
|
10
11
|
constructor() {
|
|
11
|
-
super(
|
|
12
|
+
super();
|
|
12
13
|
this.variant = 'primary';
|
|
13
14
|
this.size = 'md';
|
|
14
15
|
this.disabled = false;
|
|
15
16
|
this.loading = false;
|
|
16
17
|
this.type = 'button';
|
|
18
|
+
this._onClick = () => {
|
|
19
|
+
if (this.disabled || this.loading)
|
|
20
|
+
return;
|
|
21
|
+
if (this.type === 'submit') {
|
|
22
|
+
this.form?.requestSubmit();
|
|
23
|
+
}
|
|
24
|
+
else if (this.type === 'reset') {
|
|
25
|
+
this.form?.reset();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
this._internals = this.attachInternals();
|
|
29
|
+
}
|
|
30
|
+
/** The associated <form>, if any. */
|
|
31
|
+
get form() {
|
|
32
|
+
return this._internals.form;
|
|
17
33
|
}
|
|
18
34
|
static { this.styles = css `
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
35
|
+
:host {
|
|
36
|
+
display: inline-block;
|
|
37
|
+
}
|
|
22
38
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
button {
|
|
40
|
+
font-family: var(--font-family-sans, system-ui, -apple-system, sans-serif);
|
|
41
|
+
font-weight: var(--font-weight-medium, 500);
|
|
42
|
+
line-height: var(--font-line-height-tight, 1.2);
|
|
43
|
+
border: 1px solid transparent;
|
|
44
|
+
border-radius: var(--size-radius-md, 8px);
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
gap: var(--size-spacing-2, 8px);
|
|
50
|
+
white-space: nowrap;
|
|
51
|
+
user-select: none;
|
|
52
|
+
transition:
|
|
53
|
+
background var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
|
|
54
|
+
border-color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
|
|
55
|
+
color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
|
|
56
|
+
box-shadow var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);
|
|
57
|
+
}
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
button:focus-visible {
|
|
60
|
+
outline: 2px solid var(--color-brand-primary, #2563eb);
|
|
61
|
+
outline-offset: 2px;
|
|
62
|
+
}
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
button:disabled {
|
|
65
|
+
cursor: not-allowed;
|
|
66
|
+
opacity: 0.5;
|
|
67
|
+
}
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
/* Sizes — fall back to medium when no [size] attribute is set */
|
|
70
|
+
:host(:not([size])) button,
|
|
71
|
+
:host([size='md']) button {
|
|
72
|
+
padding: var(--size-spacing-2, 8px) var(--size-spacing-4, 16px);
|
|
73
|
+
font-size: var(--font-size-base, 16px);
|
|
74
|
+
min-height: 40px;
|
|
75
|
+
}
|
|
76
|
+
:host([size='sm']) button {
|
|
77
|
+
padding: var(--size-spacing-1, 4px) var(--size-spacing-3, 12px);
|
|
78
|
+
font-size: var(--font-size-sm, 14px);
|
|
79
|
+
min-height: 32px;
|
|
80
|
+
}
|
|
81
|
+
:host([size='lg']) button {
|
|
82
|
+
padding: var(--size-spacing-3, 12px) var(--size-spacing-5, 20px);
|
|
83
|
+
font-size: var(--font-size-lg, 18px);
|
|
84
|
+
min-height: 48px;
|
|
85
|
+
}
|
|
70
86
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
/* Variants — fall back to primary when no [variant] attribute is set */
|
|
88
|
+
:host(:not([variant])) button,
|
|
89
|
+
:host([variant='primary']) button {
|
|
90
|
+
background: var(--color-brand-primary, #2563eb);
|
|
91
|
+
color: var(--color-brand-primary-fg, #ffffff);
|
|
92
|
+
}
|
|
93
|
+
:host(:not([variant])) button:hover:not(:disabled),
|
|
94
|
+
:host([variant='primary']) button:hover:not(:disabled) {
|
|
95
|
+
background: var(--color-brand-primary-hover, #1d4ed8);
|
|
96
|
+
box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.08));
|
|
97
|
+
}
|
|
82
98
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
:host([variant='secondary']) button {
|
|
100
|
+
background: var(--color-neutral-0, #ffffff);
|
|
101
|
+
color: var(--color-text-default, #111827);
|
|
102
|
+
border-color: var(--color-border-default, #e5e7eb);
|
|
103
|
+
}
|
|
104
|
+
:host([variant='secondary']) button:hover:not(:disabled) {
|
|
105
|
+
background: var(--color-background-subtle, #f9fafb);
|
|
106
|
+
border-color: var(--color-border-strong, #9ca3af);
|
|
107
|
+
}
|
|
92
108
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
:host([variant='danger']) button {
|
|
110
|
+
background: var(--color-status-error-solid, #dc2626);
|
|
111
|
+
color: var(--color-neutral-0, #ffffff);
|
|
112
|
+
}
|
|
113
|
+
:host([variant='danger']) button:hover:not(:disabled) {
|
|
114
|
+
background: var(--color-status-error-fg, #991b1b);
|
|
115
|
+
box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.08));
|
|
116
|
+
}
|
|
101
117
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
:host([variant='ghost']) button {
|
|
119
|
+
background: transparent;
|
|
120
|
+
color: var(--color-text-default, #111827);
|
|
121
|
+
}
|
|
122
|
+
:host([variant='ghost']) button:hover:not(:disabled) {
|
|
123
|
+
background: var(--color-background-muted, #f3f4f6);
|
|
124
|
+
}
|
|
109
125
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
/* Loading spinner — sized relative to current font-size */
|
|
127
|
+
.spinner {
|
|
128
|
+
width: 1em;
|
|
129
|
+
height: 1em;
|
|
130
|
+
border: 2px solid currentColor;
|
|
131
|
+
border-top-color: transparent;
|
|
132
|
+
border-radius: 50%;
|
|
133
|
+
animation: mfp-button-spin 0.6s linear infinite;
|
|
134
|
+
flex: none;
|
|
135
|
+
}
|
|
120
136
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
@keyframes mfp-button-spin {
|
|
138
|
+
to {
|
|
139
|
+
transform: rotate(360deg);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
126
142
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
@media (prefers-reduced-motion: reduce) {
|
|
144
|
+
button {
|
|
145
|
+
transition: none;
|
|
146
|
+
}
|
|
147
|
+
.spinner {
|
|
148
|
+
animation-duration: 1.5s;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
`; }
|
|
136
152
|
render() {
|
|
137
153
|
const isInactive = this.disabled || this.loading;
|
|
154
|
+
// The inner button is always type="button" so it can't trigger native
|
|
155
|
+
// form behavior from inside shadow DOM. The host handles submit/reset
|
|
156
|
+
// via the click handler above and ElementInternals.form.
|
|
138
157
|
return html `
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
?disabled=${isInactive}
|
|
161
|
+
aria-busy=${this.loading ? 'true' : 'false'}
|
|
162
|
+
part="button"
|
|
163
|
+
@click=${this._onClick}
|
|
164
|
+
>
|
|
165
|
+
${this.loading ? html `<span class="spinner" aria-hidden="true"></span>` : ''}
|
|
166
|
+
<slot></slot>
|
|
167
|
+
</button>
|
|
168
|
+
`;
|
|
149
169
|
}
|
|
150
170
|
};
|
|
151
171
|
__decorate([
|
package/dist/button.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.js","sourceRoot":"","sources":["../src/button.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAOrD,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,UAAU;
|
|
1
|
+
{"version":3,"file":"button.js","sourceRoot":"","sources":["../src/button.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAOrD,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,UAAU;aAC9B,mBAAc,GAAG,IAAI,AAAP,CAAQ;IAI7B;QACI,KAAK,EAAE,CAAC;QAiIZ,YAAO,GAAkB,SAAS,CAAC;QAGnC,SAAI,GAAe,IAAI,CAAC;QAGxB,aAAQ,GAAG,KAAK,CAAC;QAGjB,YAAO,GAAG,KAAK,CAAC;QAGhB,SAAI,GAAe,QAAQ,CAAC;QAEpB,aAAQ,GAAG,GAAG,EAAE;YACpB,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO;gBAAE,OAAO;YAC1C,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACzB,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,CAAC;YAC/B,CAAC;iBAAM,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAC/B,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;YACvB,CAAC;QACL,CAAC,CAAC;QArJE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;IAC7C,CAAC;IAED,qCAAqC;IACrC,IAAI,IAAI;QACJ,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;IAChC,CAAC;aAEe,WAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAqH3B,AArHqB,CAqHpB;IA0BO,MAAM;QACX,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC;QACjD,sEAAsE;QACtE,sEAAsE;QACtE,yDAAyD;QACzD,OAAO,IAAI,CAAA;;;4BAGS,UAAU;4BACV,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO;;yBAElC,IAAI,CAAC,QAAQ;;kBAEpB,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA,kDAAkD,CAAC,CAAC,CAAC,EAAE;;;SAGnF,CAAC;IACN,CAAC;;AAxCD;IADC,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;0CACO;AAGnC;IADC,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;uCACJ;AAGxB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;2CAC1B;AAGjB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;0CAC3B;AAGhB;IADC,QAAQ,EAAE;uCACiB;AAnJnB,SAAS;IADrB,aAAa,CAAC,YAAY,CAAC;GACf,SAAS,CAgLrB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mfp-design-system/button",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Button web component for the mfp-design-system, built with Lit.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"lit": "^3.2.1"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@mfp-design-system/tokens": "^0.
|
|
28
|
+
"@mfp-design-system/tokens": "^0.3.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependenciesMeta": {
|
|
31
31
|
"@mfp-design-system/tokens": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"rimraf": "^6.0.1",
|
|
37
37
|
"typescript": "^5.6.3",
|
|
38
|
-
"@mfp-design-system/tokens": "0.
|
|
38
|
+
"@mfp-design-system/tokens": "0.3.0"
|
|
39
39
|
},
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
package/src/button.stories.ts
CHANGED
|
@@ -4,49 +4,49 @@ import './button.js';
|
|
|
4
4
|
import type { ButtonSize, ButtonVariant } from './button.js';
|
|
5
5
|
|
|
6
6
|
interface Args {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
variant: ButtonVariant;
|
|
8
|
+
size: ButtonSize;
|
|
9
|
+
disabled: boolean;
|
|
10
|
+
loading: boolean;
|
|
11
|
+
label: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const meta: Meta<Args> = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
title: 'Components/Button',
|
|
16
|
+
component: 'mfp-button',
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
argTypes: {
|
|
19
|
+
variant: {
|
|
20
|
+
control: 'select',
|
|
21
|
+
options: ['primary', 'secondary', 'danger', 'ghost'],
|
|
22
|
+
description: 'Visual style',
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
control: 'select',
|
|
26
|
+
options: ['sm', 'md', 'lg'],
|
|
27
|
+
description: 'Sizing',
|
|
28
|
+
},
|
|
29
|
+
disabled: { control: 'boolean' },
|
|
30
|
+
loading: { control: 'boolean' },
|
|
31
|
+
label: { control: 'text', description: 'Button label (slot content)' },
|
|
23
32
|
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
args: {
|
|
34
|
+
variant: 'primary',
|
|
35
|
+
size: 'md',
|
|
36
|
+
disabled: false,
|
|
37
|
+
loading: false,
|
|
38
|
+
label: 'Button',
|
|
28
39
|
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
render: (args) => html`
|
|
41
|
-
<mfp-button
|
|
42
|
-
variant=${args.variant}
|
|
43
|
-
size=${args.size}
|
|
44
|
-
?disabled=${args.disabled}
|
|
45
|
-
?loading=${args.loading}
|
|
46
|
-
>
|
|
47
|
-
${args.label}
|
|
48
|
-
</mfp-button>
|
|
49
|
-
`,
|
|
40
|
+
render: (args) => html`
|
|
41
|
+
<mfp-button
|
|
42
|
+
variant=${args.variant}
|
|
43
|
+
size=${args.size}
|
|
44
|
+
?disabled=${args.disabled}
|
|
45
|
+
?loading=${args.loading}
|
|
46
|
+
>
|
|
47
|
+
${args.label}
|
|
48
|
+
</mfp-button>
|
|
49
|
+
`,
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
export default meta;
|
|
@@ -54,60 +54,60 @@ export default meta;
|
|
|
54
54
|
type Story = StoryObj<Args>;
|
|
55
55
|
|
|
56
56
|
export const Primary: Story = {
|
|
57
|
-
|
|
57
|
+
args: { variant: 'primary', label: 'Primary' },
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
export const Secondary: Story = {
|
|
61
|
-
|
|
61
|
+
args: { variant: 'secondary', label: 'Secondary' },
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
export const Danger: Story = {
|
|
65
|
-
|
|
65
|
+
args: { variant: 'danger', label: 'Delete' },
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
export const Ghost: Story = {
|
|
69
|
-
|
|
69
|
+
args: { variant: 'ghost', label: 'More info' },
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
export const Sizes: Story = {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
parameters: { controls: { disable: true } },
|
|
74
|
+
render: () => html`
|
|
75
|
+
<div style="display: flex; gap: 12px; align-items: center;">
|
|
76
|
+
<mfp-button size="sm">Small</mfp-button>
|
|
77
|
+
<mfp-button size="md">Medium</mfp-button>
|
|
78
|
+
<mfp-button size="lg">Large</mfp-button>
|
|
79
|
+
</div>
|
|
80
|
+
`,
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
export const AllVariants: Story = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
parameters: { controls: { disable: true } },
|
|
85
|
+
render: () => html`
|
|
86
|
+
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
|
87
|
+
<mfp-button variant="primary">Primary</mfp-button>
|
|
88
|
+
<mfp-button variant="secondary">Secondary</mfp-button>
|
|
89
|
+
<mfp-button variant="danger">Danger</mfp-button>
|
|
90
|
+
<mfp-button variant="ghost">Ghost</mfp-button>
|
|
91
|
+
</div>
|
|
92
|
+
`,
|
|
93
93
|
};
|
|
94
94
|
|
|
95
95
|
export const Disabled: Story = {
|
|
96
|
-
|
|
96
|
+
args: { disabled: true, label: 'Disabled' },
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
export const Loading: Story = {
|
|
100
|
-
|
|
100
|
+
args: { loading: true, label: 'Saving…' },
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
export const LoadingStates: Story = {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
parameters: { controls: { disable: true } },
|
|
105
|
+
render: () => html`
|
|
106
|
+
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
|
107
|
+
<mfp-button variant="primary" loading>Saving…</mfp-button>
|
|
108
|
+
<mfp-button variant="secondary" loading>Loading</mfp-button>
|
|
109
|
+
<mfp-button variant="danger" loading>Deleting</mfp-button>
|
|
110
|
+
<mfp-button variant="ghost" loading>…</mfp-button>
|
|
111
|
+
</div>
|
|
112
|
+
`,
|
|
113
113
|
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { expect, fixture, html } from '@open-wc/testing';
|
|
2
|
+
import './button.js';
|
|
3
|
+
import type { MfpButton } from './button.js';
|
|
4
|
+
|
|
5
|
+
describe('<mfp-button>', () => {
|
|
6
|
+
it('renders with default variant and size', async () => {
|
|
7
|
+
const el = await fixture<MfpButton>(html`<mfp-button>Click</mfp-button>`);
|
|
8
|
+
expect(el.variant).to.equal('primary');
|
|
9
|
+
expect(el.size).to.equal('md');
|
|
10
|
+
expect(el.shadowRoot?.querySelector('button')).to.exist;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('reflects variant and size to attributes', async () => {
|
|
14
|
+
const el = await fixture<MfpButton>(
|
|
15
|
+
html`<mfp-button variant="danger" size="lg">Delete</mfp-button>`,
|
|
16
|
+
);
|
|
17
|
+
expect(el.getAttribute('variant')).to.equal('danger');
|
|
18
|
+
expect(el.getAttribute('size')).to.equal('lg');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('forwards clicks via the click event', async () => {
|
|
22
|
+
const el = await fixture<MfpButton>(html`<mfp-button>Click</mfp-button>`);
|
|
23
|
+
let clicks = 0;
|
|
24
|
+
el.addEventListener('click', () => clicks++);
|
|
25
|
+
el.shadowRoot!.querySelector('button')!.click();
|
|
26
|
+
expect(clicks).to.equal(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('does not fire submit when disabled', async () => {
|
|
30
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
31
|
+
<form>
|
|
32
|
+
<mfp-button type="submit" disabled>Submit</mfp-button>
|
|
33
|
+
</form>
|
|
34
|
+
`);
|
|
35
|
+
let submits = 0;
|
|
36
|
+
form.addEventListener('submit', (e) => {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
submits++;
|
|
39
|
+
});
|
|
40
|
+
const btn = form.querySelector<MfpButton>('mfp-button')!;
|
|
41
|
+
btn.shadowRoot!.querySelector('button')!.click();
|
|
42
|
+
expect(submits).to.equal(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('submits its associated form when type="submit" is clicked', async () => {
|
|
46
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
47
|
+
<form>
|
|
48
|
+
<input name="email" value="me@example.com" />
|
|
49
|
+
<mfp-button type="submit">Submit</mfp-button>
|
|
50
|
+
</form>
|
|
51
|
+
`);
|
|
52
|
+
let submits = 0;
|
|
53
|
+
form.addEventListener('submit', (e) => {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
submits++;
|
|
56
|
+
});
|
|
57
|
+
const btn = form.querySelector<MfpButton>('mfp-button')!;
|
|
58
|
+
btn.shadowRoot!.querySelector('button')!.click();
|
|
59
|
+
// requestSubmit is async; wait a microtask
|
|
60
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
61
|
+
expect(submits).to.equal(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('exposes the associated form via the .form getter', async () => {
|
|
65
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
66
|
+
<form id="my-form">
|
|
67
|
+
<mfp-button type="submit">Submit</mfp-button>
|
|
68
|
+
</form>
|
|
69
|
+
`);
|
|
70
|
+
const btn = form.querySelector<MfpButton>('mfp-button')!;
|
|
71
|
+
expect(btn.form).to.equal(form);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('shows spinner when loading', async () => {
|
|
75
|
+
const el = await fixture<MfpButton>(html`<mfp-button loading>Saving…</mfp-button>`);
|
|
76
|
+
expect(el.shadowRoot?.querySelector('.spinner')).to.exist;
|
|
77
|
+
expect(el.shadowRoot?.querySelector('button')?.getAttribute('aria-busy')).to.equal('true');
|
|
78
|
+
});
|
|
79
|
+
});
|
package/src/button.ts
CHANGED
|
@@ -7,158 +7,185 @@ export type ButtonType = 'button' | 'submit' | 'reset';
|
|
|
7
7
|
|
|
8
8
|
@customElement('mfp-button')
|
|
9
9
|
export class MfpButton extends LitElement {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
10
|
+
static formAssociated = true;
|
|
11
|
+
|
|
12
|
+
private _internals: ElementInternals;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this._internals = this.attachInternals();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The associated <form>, if any. */
|
|
20
|
+
get form(): HTMLFormElement | null {
|
|
21
|
+
return this._internals.form;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static override styles = css`
|
|
25
|
+
:host {
|
|
26
|
+
display: inline-block;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
button {
|
|
30
|
+
font-family: var(--font-family-sans, system-ui, -apple-system, sans-serif);
|
|
31
|
+
font-weight: var(--font-weight-medium, 500);
|
|
32
|
+
line-height: var(--font-line-height-tight, 1.2);
|
|
33
|
+
border: 1px solid transparent;
|
|
34
|
+
border-radius: var(--size-radius-md, 8px);
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
gap: var(--size-spacing-2, 8px);
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
user-select: none;
|
|
42
|
+
transition:
|
|
43
|
+
background var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
|
|
44
|
+
border-color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
|
|
45
|
+
color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
|
|
46
|
+
box-shadow var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
button:focus-visible {
|
|
50
|
+
outline: 2px solid var(--color-brand-primary, #2563eb);
|
|
51
|
+
outline-offset: 2px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
button:disabled {
|
|
55
|
+
cursor: not-allowed;
|
|
56
|
+
opacity: 0.5;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Sizes — fall back to medium when no [size] attribute is set */
|
|
60
|
+
:host(:not([size])) button,
|
|
61
|
+
:host([size='md']) button {
|
|
62
|
+
padding: var(--size-spacing-2, 8px) var(--size-spacing-4, 16px);
|
|
63
|
+
font-size: var(--font-size-base, 16px);
|
|
64
|
+
min-height: 40px;
|
|
65
|
+
}
|
|
66
|
+
:host([size='sm']) button {
|
|
67
|
+
padding: var(--size-spacing-1, 4px) var(--size-spacing-3, 12px);
|
|
68
|
+
font-size: var(--font-size-sm, 14px);
|
|
69
|
+
min-height: 32px;
|
|
70
|
+
}
|
|
71
|
+
:host([size='lg']) button {
|
|
72
|
+
padding: var(--size-spacing-3, 12px) var(--size-spacing-5, 20px);
|
|
73
|
+
font-size: var(--font-size-lg, 18px);
|
|
74
|
+
min-height: 48px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Variants — fall back to primary when no [variant] attribute is set */
|
|
78
|
+
:host(:not([variant])) button,
|
|
79
|
+
:host([variant='primary']) button {
|
|
80
|
+
background: var(--color-brand-primary, #2563eb);
|
|
81
|
+
color: var(--color-brand-primary-fg, #ffffff);
|
|
82
|
+
}
|
|
83
|
+
:host(:not([variant])) button:hover:not(:disabled),
|
|
84
|
+
:host([variant='primary']) button:hover:not(:disabled) {
|
|
85
|
+
background: var(--color-brand-primary-hover, #1d4ed8);
|
|
86
|
+
box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.08));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
:host([variant='secondary']) button {
|
|
90
|
+
background: var(--color-neutral-0, #ffffff);
|
|
91
|
+
color: var(--color-text-default, #111827);
|
|
92
|
+
border-color: var(--color-border-default, #e5e7eb);
|
|
93
|
+
}
|
|
94
|
+
:host([variant='secondary']) button:hover:not(:disabled) {
|
|
95
|
+
background: var(--color-background-subtle, #f9fafb);
|
|
96
|
+
border-color: var(--color-border-strong, #9ca3af);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
:host([variant='danger']) button {
|
|
100
|
+
background: var(--color-status-error-solid, #dc2626);
|
|
101
|
+
color: var(--color-neutral-0, #ffffff);
|
|
102
|
+
}
|
|
103
|
+
:host([variant='danger']) button:hover:not(:disabled) {
|
|
104
|
+
background: var(--color-status-error-fg, #991b1b);
|
|
105
|
+
box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.08));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
:host([variant='ghost']) button {
|
|
109
|
+
background: transparent;
|
|
110
|
+
color: var(--color-text-default, #111827);
|
|
111
|
+
}
|
|
112
|
+
:host([variant='ghost']) button:hover:not(:disabled) {
|
|
113
|
+
background: var(--color-background-muted, #f3f4f6);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Loading spinner — sized relative to current font-size */
|
|
117
|
+
.spinner {
|
|
118
|
+
width: 1em;
|
|
119
|
+
height: 1em;
|
|
120
|
+
border: 2px solid currentColor;
|
|
121
|
+
border-top-color: transparent;
|
|
122
|
+
border-radius: 50%;
|
|
123
|
+
animation: mfp-button-spin 0.6s linear infinite;
|
|
124
|
+
flex: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@keyframes mfp-button-spin {
|
|
128
|
+
to {
|
|
129
|
+
transform: rotate(360deg);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@media (prefers-reduced-motion: reduce) {
|
|
134
|
+
button {
|
|
135
|
+
transition: none;
|
|
136
|
+
}
|
|
137
|
+
.spinner {
|
|
138
|
+
animation-duration: 1.5s;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
118
142
|
|
|
119
|
-
@
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
@property({ reflect: true })
|
|
144
|
+
variant: ButtonVariant = 'primary';
|
|
145
|
+
|
|
146
|
+
@property({ reflect: true })
|
|
147
|
+
size: ButtonSize = 'md';
|
|
148
|
+
|
|
149
|
+
@property({ type: Boolean, reflect: true })
|
|
150
|
+
disabled = false;
|
|
151
|
+
|
|
152
|
+
@property({ type: Boolean, reflect: true })
|
|
153
|
+
loading = false;
|
|
154
|
+
|
|
155
|
+
@property()
|
|
156
|
+
type: ButtonType = 'button';
|
|
157
|
+
|
|
158
|
+
private _onClick = () => {
|
|
159
|
+
if (this.disabled || this.loading) return;
|
|
160
|
+
if (this.type === 'submit') {
|
|
161
|
+
this.form?.requestSubmit();
|
|
162
|
+
} else if (this.type === 'reset') {
|
|
163
|
+
this.form?.reset();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
override render() {
|
|
168
|
+
const isInactive = this.disabled || this.loading;
|
|
169
|
+
// The inner button is always type="button" so it can't trigger native
|
|
170
|
+
// form behavior from inside shadow DOM. The host handles submit/reset
|
|
171
|
+
// via the click handler above and ElementInternals.form.
|
|
172
|
+
return html`
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
?disabled=${isInactive}
|
|
176
|
+
aria-busy=${this.loading ? 'true' : 'false'}
|
|
177
|
+
part="button"
|
|
178
|
+
@click=${this._onClick}
|
|
179
|
+
>
|
|
180
|
+
${this.loading ? html`<span class="spinner" aria-hidden="true"></span>` : ''}
|
|
181
|
+
<slot></slot>
|
|
182
|
+
</button>
|
|
183
|
+
`;
|
|
126
184
|
}
|
|
127
|
-
`;
|
|
128
|
-
|
|
129
|
-
@property({ reflect: true })
|
|
130
|
-
variant: ButtonVariant = 'primary';
|
|
131
|
-
|
|
132
|
-
@property({ reflect: true })
|
|
133
|
-
size: ButtonSize = 'md';
|
|
134
|
-
|
|
135
|
-
@property({ type: Boolean, reflect: true })
|
|
136
|
-
disabled = false;
|
|
137
|
-
|
|
138
|
-
@property({ type: Boolean, reflect: true })
|
|
139
|
-
loading = false;
|
|
140
|
-
|
|
141
|
-
@property()
|
|
142
|
-
type: ButtonType = 'button';
|
|
143
|
-
|
|
144
|
-
override render() {
|
|
145
|
-
const isInactive = this.disabled || this.loading;
|
|
146
|
-
return html`
|
|
147
|
-
<button
|
|
148
|
-
type=${this.type}
|
|
149
|
-
?disabled=${isInactive}
|
|
150
|
-
aria-busy=${this.loading ? 'true' : 'false'}
|
|
151
|
-
part="button"
|
|
152
|
-
>
|
|
153
|
-
${this.loading ? html`<span class="spinner" aria-hidden="true"></span>` : ''}
|
|
154
|
-
<slot></slot>
|
|
155
|
-
</button>
|
|
156
|
-
`;
|
|
157
|
-
}
|
|
158
185
|
}
|
|
159
186
|
|
|
160
187
|
declare global {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
188
|
+
interface HTMLElementTagNameMap {
|
|
189
|
+
'mfp-button': MfpButton;
|
|
190
|
+
}
|
|
164
191
|
}
|