@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
- button {
105
- display: flex;
106
- align-items: center;
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
- @click=${this._onClick}
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, LitElement } from "lit";
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
- render() {
35
+ get iconTemplate() {
36
+ const side = this.iconPosition === 'right' ? 'icon-right' : 'icon-left';
36
37
  return html`
37
- <button @click="${this.doClick}">
38
- <dile-icon .icon=${this.icon}></dile-icon>
39
- <slot></slot>
40
- </button>
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: block;
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, transparent);
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, #039be5);
30
- color: var(--dile-tab-selected-text-color, #fff);
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, #0070c0);
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.8.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": "807eac93fd36691a8fb656b5e0f2549ed9c887cb"
31
+ "gitHead": "9e326dc3d88fc6a0154d7e8833034469aae9c8d1"
32
32
  }