@dile/ui 3.8.3 → 3.9.1
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.
|
@@ -46,6 +46,58 @@ describe('dile-button', () => {
|
|
|
46
46
|
expect(el.shadowRoot.querySelector('dile-spinner-icon')).toBeTruthy();
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
it('renders the loading spinner after the label (far right)', async () => {
|
|
50
|
+
const el = await renderButton('<dile-button loading>Save</dile-button>');
|
|
51
|
+
const children = [...el.shadowRoot.querySelector('button').children].map(n => n.tagName.toLowerCase());
|
|
52
|
+
expect(children.indexOf('dile-spinner-icon')).toBeGreaterThan(children.indexOf('slot'));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('icon support', () => {
|
|
56
|
+
it('does not render an icon element when no icon is provided (backwards compatible)', async () => {
|
|
57
|
+
const el = await renderButton('<dile-button>Plain</dile-button>');
|
|
58
|
+
expect(el.shadowRoot.querySelector('dile-iconlib')).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('renders a dile-iconlib icon from a "family.name" string', async () => {
|
|
62
|
+
const el = await renderButton('<dile-button icon="lucide.rocket">Launch</dile-button>');
|
|
63
|
+
const icon = el.shadowRoot.querySelector('dile-iconlib');
|
|
64
|
+
expect(icon).toBeTruthy();
|
|
65
|
+
expect(icon.getAttribute('icon')).toBe('lucide.rocket');
|
|
66
|
+
expect(icon.getAttribute('aria-hidden')).toBe('true');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('places the icon before the label by default and after it when iconPosition is "right"', async () => {
|
|
70
|
+
const el = await renderButton('<dile-button icon="lucide.rocket">Launch</dile-button>');
|
|
71
|
+
let children = [...el.shadowRoot.querySelector('button').children].map(n => n.tagName.toLowerCase());
|
|
72
|
+
expect(children.indexOf('dile-iconlib')).toBeLessThan(children.indexOf('slot'));
|
|
73
|
+
|
|
74
|
+
el.iconPosition = 'right';
|
|
75
|
+
await el.updateComplete;
|
|
76
|
+
children = [...el.shadowRoot.querySelector('button').children].map(n => n.tagName.toLowerCase());
|
|
77
|
+
expect(children.indexOf('dile-iconlib')).toBeGreaterThan(children.indexOf('slot'));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('reads the icon position from the "icon-position" attribute', async () => {
|
|
81
|
+
const el = await renderButton('<dile-button icon="lucide.rocket" icon-position="right">Continue</dile-button>');
|
|
82
|
+
expect(el.iconPosition).toBe('right');
|
|
83
|
+
const children = [...el.shadowRoot.querySelector('button').children].map(n => n.tagName.toLowerCase());
|
|
84
|
+
expect(children.indexOf('dile-iconlib')).toBeGreaterThan(children.indexOf('slot'));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('marks the button as having text only when the slot has content', async () => {
|
|
88
|
+
const withText = await renderButton('<dile-button icon="lucide.rocket">Launch</dile-button>');
|
|
89
|
+
expect(withText.shadowRoot.querySelector('button').classList.contains('has-text')).toBe(true);
|
|
90
|
+
|
|
91
|
+
const iconOnly = await renderButton('<dile-button icon="lucide.rocket" label="Launch"></dile-button>');
|
|
92
|
+
expect(iconOnly.shadowRoot.querySelector('button').classList.contains('has-text')).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('exposes the label as the accessible name of the inner button', async () => {
|
|
96
|
+
const el = await renderButton('<dile-button icon="lucide.rocket" label="Launch rocket"></dile-button>');
|
|
97
|
+
expect(el.shadowRoot.querySelector('button').getAttribute('aria-label')).toBe('Launch rocket');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
49
101
|
describe('Native form submission', () => {
|
|
50
102
|
it('includes name/value in the FormData when clicked', async () => {
|
|
51
103
|
document.body.innerHTML = `
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { html, css, LitElement } from "lit";
|
|
2
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
3
|
+
import { ifDefined } from "lit/directives/if-defined.js";
|
|
2
4
|
import '../../spinner/spinner-icon.js';
|
|
5
|
+
import '@dile/iconlib/dile-iconlib.js';
|
|
3
6
|
|
|
4
7
|
export class DileButton extends LitElement {
|
|
5
8
|
|
|
@@ -12,6 +15,19 @@ export class DileButton extends LitElement {
|
|
|
12
15
|
name: { type: String, reflect: true },
|
|
13
16
|
type: { type: String },
|
|
14
17
|
value: { type: String },
|
|
18
|
+
/**
|
|
19
|
+
* Icon to display, in the dile-iconlib `"family.name"` format
|
|
20
|
+
* (e.g. `"lucide.rocket"`, `"material.home"`). When empty, the button
|
|
21
|
+
* behaves exactly as a plain text button (backwards compatible).
|
|
22
|
+
*/
|
|
23
|
+
icon: { type: String },
|
|
24
|
+
/** Icon placement relative to the label: `"left"` (default) or `"right"`. */
|
|
25
|
+
iconPosition: { type: String, reflect: true, attribute: 'icon-position' },
|
|
26
|
+
/** Prevent the label from wrapping into several lines. */
|
|
27
|
+
noWrap: { type: Boolean, reflect: true, attribute: 'no-wrap' },
|
|
28
|
+
/** Accessible name for the button. Required when the button only shows an icon. */
|
|
29
|
+
label: { type: String },
|
|
30
|
+
_hasText: { type: Boolean, state: true },
|
|
15
31
|
};
|
|
16
32
|
}
|
|
17
33
|
|
|
@@ -20,9 +36,21 @@ export class DileButton extends LitElement {
|
|
|
20
36
|
this.disabled = false;
|
|
21
37
|
this.loading = false;
|
|
22
38
|
this.type = "button";
|
|
39
|
+
this.icon = "";
|
|
40
|
+
this.iconPosition = "left";
|
|
41
|
+
this.noWrap = false;
|
|
42
|
+
this.label = "";
|
|
43
|
+
this._hasText = true;
|
|
23
44
|
this._internals = this.attachInternals();
|
|
24
45
|
}
|
|
25
46
|
|
|
47
|
+
connectedCallback() {
|
|
48
|
+
super.connectedCallback();
|
|
49
|
+
// Seed the layout state before the first slotchange so an icon-only button
|
|
50
|
+
// does not flash the text-spacing margin on first paint.
|
|
51
|
+
this._hasText = this._detectText();
|
|
52
|
+
}
|
|
53
|
+
|
|
26
54
|
static get styles() {
|
|
27
55
|
return css`
|
|
28
56
|
:host {
|
|
@@ -31,6 +59,9 @@ export class DileButton extends LitElement {
|
|
|
31
59
|
}
|
|
32
60
|
button {
|
|
33
61
|
cursor: pointer;
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: center;
|
|
34
65
|
padding-top: var(--dile-button-padding-y, 0.5rem);
|
|
35
66
|
padding-bottom: var(--dile-button-padding-y, 0.5rem);
|
|
36
67
|
padding-right: var(--dile-button-padding-x, 0.8rem);
|
|
@@ -43,18 +74,20 @@ export class DileButton extends LitElement {
|
|
|
43
74
|
transition-duration: 0.3s;
|
|
44
75
|
transition-timing-function: ease-in-out;
|
|
45
76
|
border-style: solid;
|
|
46
|
-
color: var(--dile-button-text-color, var(--dile-on-primary-color, #fff));
|
|
77
|
+
color: var(--dile-button-text-color, var(--dile-on-primary-color, #fff));
|
|
47
78
|
font-size: var(--dile-button-font-size, 1rem);
|
|
48
79
|
font-weight: var(--dile-button-font-weight, bold);
|
|
49
80
|
text-transform: var(--dile-button-text-transform, none);
|
|
50
81
|
letter-spacing: var(--dile-button-letter-spacing, 0);
|
|
51
82
|
user-select: none;
|
|
52
83
|
box-shadow: var(--dile-button-box-shadow, none);
|
|
84
|
+
--dile-icon-color: var(--dile-button-icon-color, var(--dile-button-text-color, var(--dile-on-primary-color, #fff)));
|
|
53
85
|
}
|
|
54
86
|
button:hover {
|
|
55
87
|
background-color: var(--dile-button-hover-background-color, var(--dile-button-background-color, var(--dile-primary-light-color, #f3f3ae)));
|
|
56
88
|
color: var(--dile-button-hover-text-color, var(--dile-button-text-color, var(--dile-on-primary-light-color, #303030)));
|
|
57
89
|
border-color: var(--dile-button-hover-border-color, var(--dile-button-border-color, var(--dile-primary-color, #666666)));
|
|
90
|
+
--dile-icon-color: var(--dile-button-icon-hover-color, var(--dile-button-hover-text-color, var(--dile-button-text-color, var(--dile-on-primary-light-color, #303030))));
|
|
58
91
|
}
|
|
59
92
|
button:focus-visible {
|
|
60
93
|
outline: 2px solid var(--dile-button-ring-color, #12c9e9);
|
|
@@ -101,32 +134,90 @@ export class DileButton extends LitElement {
|
|
|
101
134
|
:host([loading]) {
|
|
102
135
|
pointer-events: none;
|
|
103
136
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
justify-content: center;
|
|
108
|
-
gap: var(--dile-button-spinner-gap, 0.5rem);
|
|
137
|
+
|
|
138
|
+
:host([no-wrap]) button {
|
|
139
|
+
white-space: nowrap;
|
|
109
140
|
}
|
|
141
|
+
|
|
110
142
|
dile-spinner-icon {
|
|
111
143
|
--dile-icon-size: var(--dile-button-spinner-size, calc(var(--dile-button-font-size, 1rem) * 0.8));
|
|
112
144
|
--dile-icon-color: var(--dile-on-primary-color, #fff);
|
|
113
145
|
}
|
|
146
|
+
button.has-text dile-spinner-icon {
|
|
147
|
+
margin-left: var(--dile-button-spinner-gap, 0.5rem);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.button-icon {
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
--dile-icon-size: var(--dile-button-icon-size, calc(var(--dile-button-font-size, 1rem) * 1.125));
|
|
154
|
+
}
|
|
155
|
+
/* The icon-to-text separation is only applied when the button has a label,
|
|
156
|
+
so an icon-only button stays perfectly centered with no dead space. */
|
|
157
|
+
button.has-text .button-icon.icon-left {
|
|
158
|
+
margin-right: var(--dile-button-icon-separation, 0.3rem);
|
|
159
|
+
}
|
|
160
|
+
button.has-text .button-icon.icon-right {
|
|
161
|
+
margin-left: var(--dile-button-icon-separation, 0.3rem);
|
|
162
|
+
}
|
|
114
163
|
`;
|
|
115
164
|
}
|
|
116
165
|
|
|
117
166
|
render() {
|
|
167
|
+
const showIcon = !!this.icon && !this.loading;
|
|
118
168
|
return html`
|
|
119
|
-
<button
|
|
120
|
-
|
|
169
|
+
<button
|
|
170
|
+
class=${classMap({ 'has-text': this._hasText })}
|
|
171
|
+
@click=${this._onClick}
|
|
121
172
|
?disabled=${this.disabled || this.loading}
|
|
122
173
|
type=${this.type || "button"}
|
|
174
|
+
aria-label=${ifDefined(this._computedAriaLabel)}
|
|
175
|
+
aria-busy=${ifDefined(this.loading ? 'true' : undefined)}
|
|
123
176
|
>
|
|
177
|
+
${showIcon && this.iconPosition !== 'right' ? this.iconTemplate : ''}
|
|
178
|
+
<slot @slotchange=${this._onSlotChange}></slot>
|
|
179
|
+
${showIcon && this.iconPosition === 'right' ? this.iconTemplate : ''}
|
|
124
180
|
${this.loading ? html`<dile-spinner-icon active></dile-spinner-icon>` : ''}
|
|
125
|
-
<slot></slot>
|
|
126
181
|
</button>
|
|
127
182
|
`;
|
|
128
183
|
}
|
|
129
184
|
|
|
185
|
+
get iconTemplate() {
|
|
186
|
+
const side = this.iconPosition === 'right' ? 'icon-right' : 'icon-left';
|
|
187
|
+
return html`
|
|
188
|
+
<dile-iconlib
|
|
189
|
+
class="button-icon ${side}"
|
|
190
|
+
icon=${this.icon}
|
|
191
|
+
aria-hidden="true"
|
|
192
|
+
></dile-iconlib>
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get _computedAriaLabel() {
|
|
197
|
+
return this.label || undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_detectText() {
|
|
201
|
+
if (this.textContent && this.textContent.trim().length > 0) return true;
|
|
202
|
+
return this.childElementCount > 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_onSlotChange(e) {
|
|
206
|
+
const nodes = e.target.assignedNodes({ flatten: true });
|
|
207
|
+
this._hasText = nodes.some(node =>
|
|
208
|
+
node.nodeType === Node.ELEMENT_NODE ||
|
|
209
|
+
(node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '')
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
updated(changedProperties) {
|
|
214
|
+
if (changedProperties.has('icon') || changedProperties.has('label') || changedProperties.has('_hasText')) {
|
|
215
|
+
if (this.icon && !this._hasText && !this.label) {
|
|
216
|
+
console.warn('dile-button: an icon-only button should set the "label" property so it has an accessible name.');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
130
221
|
_onClick(e) {
|
|
131
222
|
if (this.disabled || this.loading) {
|
|
132
223
|
e.preventDefault();
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import { html, css
|
|
1
|
+
import { html, css } from "lit";
|
|
2
2
|
import { DileButton } from './DileButton.js';
|
|
3
3
|
import '../../icon/icon.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* dile-button-icon renders an icon supplied as a lit-html template (or any HTML
|
|
7
|
+
* template, e.g. an `<img>`). If you want string-based icons with icon-family
|
|
8
|
+
* support ("lucide.rocket", "material.home", ...), use `<dile-button>` together
|
|
9
|
+
* with its `icon` property instead.
|
|
10
|
+
*
|
|
11
|
+
* It inherits every feature from dile-button: `iconPosition`, `no-wrap`,
|
|
12
|
+
* `label`, loading spinner and form integration.
|
|
13
|
+
*/
|
|
5
14
|
export class DileButtonIcon extends DileButton {
|
|
6
15
|
static get properties() {
|
|
7
16
|
return {
|
|
@@ -14,30 +23,23 @@ export class DileButtonIcon extends DileButton {
|
|
|
14
23
|
super.styles,
|
|
15
24
|
css`
|
|
16
25
|
button {
|
|
17
|
-
display: flex;
|
|
18
|
-
align-items: center;
|
|
19
26
|
--dile-icon-color: var(--dile-button-icon-color, var(--dile-on-primary-color, #ffffff));
|
|
20
27
|
}
|
|
21
|
-
dile-icon {
|
|
22
|
-
--dile-icon-size: var(--dile-button-icon-size, calc(var(--dile-button-font-size, 1rem) * 1.125));
|
|
23
|
-
margin-right: var(--dile-button-icon-separation, 0.3rem);
|
|
24
|
-
}
|
|
25
28
|
button:hover {
|
|
26
29
|
--dile-icon-color: var(--dile-button-icon-hover-color, var(--dile-on-primary-light-color, #888));
|
|
27
30
|
}
|
|
28
|
-
:host([no-wrap]) {
|
|
29
|
-
white-space: nowrap;
|
|
30
|
-
}
|
|
31
31
|
`
|
|
32
32
|
];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
get iconTemplate() {
|
|
36
|
+
const side = this.iconPosition === 'right' ? 'icon-right' : 'icon-left';
|
|
36
37
|
return html`
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
<dile-icon
|
|
39
|
+
class="button-icon ${side}"
|
|
40
|
+
.icon=${this.icon}
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
></dile-icon>
|
|
41
43
|
`;
|
|
42
44
|
}
|
|
43
45
|
}
|
|
@@ -7,7 +7,8 @@ export class DileCard extends LitElement {
|
|
|
7
7
|
box-sizing: border-box;
|
|
8
8
|
}
|
|
9
9
|
:host {
|
|
10
|
-
display:
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
11
12
|
border: var(--dile-card-border, 1px solid #ccc);
|
|
12
13
|
border-radius: var(--dile-card-border-radius, 0.5rem);
|
|
13
14
|
background-color: var(--dile-card-background-color, #fff);
|
|
@@ -35,6 +36,7 @@ export class DileCard extends LitElement {
|
|
|
35
36
|
main {
|
|
36
37
|
padding-top: var(--dile-card-padding-y, 1rem);
|
|
37
38
|
padding-bottom: var(--dile-card-padding-y, 1rem);
|
|
39
|
+
flex-grow: 1;
|
|
38
40
|
}
|
|
39
41
|
footer {
|
|
40
42
|
border-top: var(--dile-card-footer-border-separator, 1px solid #ccc);
|
|
@@ -13,8 +13,8 @@ export class DileTab extends DileSelectableItem(LitElement) {
|
|
|
13
13
|
border-top-left-radius: var(--dile-tab-border-radius, 4px);
|
|
14
14
|
border-top-right-radius: var(--dile-tab-border-radius, 4px);
|
|
15
15
|
transition: all 0.3s ease;
|
|
16
|
-
color: var(--dile-tab-text-color, #666);
|
|
17
|
-
background-color: var(--dile-tab-background-color,
|
|
16
|
+
color: var(--dile-tab-text-color, var(--dile-on-primary-color, #666));
|
|
17
|
+
background-color: var(--dile-tab-background-color, var(--dile-primary-color, #eee));
|
|
18
18
|
cursor: pointer;
|
|
19
19
|
border: var(--dile-tab-border, none);
|
|
20
20
|
font-weight: var(--dile-tab-font-weight, normal);
|
|
@@ -26,15 +26,15 @@ export class DileTab extends DileSelectableItem(LitElement) {
|
|
|
26
26
|
white-space: nowrap;
|
|
27
27
|
}
|
|
28
28
|
.selected {
|
|
29
|
-
background-color: var(--dile-tab-selected-background-color, #
|
|
30
|
-
color: var(--dile-tab-selected-text-color, #
|
|
29
|
+
background-color: var(--dile-tab-selected-background-color, var(--dile-primary-dark-color, #888));
|
|
30
|
+
color: var(--dile-tab-selected-text-color, var(--dile-on-primary-dark-color, #000));
|
|
31
31
|
border: var(--dile-tab-selected-border, none);
|
|
32
32
|
}
|
|
33
33
|
span {
|
|
34
34
|
display: block;
|
|
35
35
|
height: var(--dile-tab-selected-line-height, 5px);
|
|
36
36
|
width: 0;
|
|
37
|
-
background-color: var(--dile-tab-selected-line-color, #
|
|
37
|
+
background-color: var(--dile-tab-selected-line-color, var(--dile-secondary-color, #333));
|
|
38
38
|
transition: width 0.3s ease;
|
|
39
39
|
}
|
|
40
40
|
.markselected {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.1",
|
|
4
4
|
"description": "UI Core components from dile-components.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "9e326dc3d88fc6a0154d7e8833034469aae9c8d1"
|
|
32
32
|
}
|