@dile/ui 3.13.1 → 3.15.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.
@@ -30,4 +30,83 @@ describe('dile-drop-file', () => {
30
30
 
31
31
  expect(el.fileName).toBe('');
32
32
  });
33
+
34
+ it('renders accept attribute when accept property is defined', async () => {
35
+ const el = await renderDropFile('<dile-drop-file accept="application/pdf,image/*"></dile-drop-file>');
36
+ const fileInput = el.shadowRoot.querySelector('input[type="file"]');
37
+ expect(fileInput.getAttribute('accept')).toBe('application/pdf,image/*');
38
+ });
39
+
40
+ it('does not render accept attribute when accept property is empty', async () => {
41
+ const el = await renderDropFile('<dile-drop-file accept=""></dile-drop-file>');
42
+ const fileInput = el.shadowRoot.querySelector('input[type="file"]');
43
+ expect(fileInput.getAttribute('accept')).toBeNull();
44
+ });
45
+
46
+ it('renders capture attribute when capture property is defined', async () => {
47
+ const el = await renderDropFile('<dile-drop-file capture="environment"></dile-drop-file>');
48
+ const fileInput = el.shadowRoot.querySelector('input[type="file"]');
49
+ expect(fileInput.getAttribute('capture')).toBe('environment');
50
+ });
51
+
52
+ it('does not render capture attribute when capture property is empty', async () => {
53
+ const el = await renderDropFile('<dile-drop-file capture=""></dile-drop-file>');
54
+ const fileInput = el.shadowRoot.querySelector('input[type="file"]');
55
+ expect(fileInput.getAttribute('capture')).toBeNull();
56
+ });
57
+
58
+ it('updates accept attribute when property changes after render', async () => {
59
+ const el = await renderDropFile('<dile-drop-file></dile-drop-file>');
60
+ let fileInput = el.shadowRoot.querySelector('input[type="file"]');
61
+ expect(fileInput.getAttribute('accept')).toBeNull();
62
+
63
+ el.accept = 'image/*';
64
+ await el.updateComplete;
65
+
66
+ fileInput = el.shadowRoot.querySelector('input[type="file"]');
67
+ expect(fileInput.getAttribute('accept')).toBe('image/*');
68
+ });
69
+
70
+ it('updates capture attribute when property changes after render', async () => {
71
+ const el = await renderDropFile('<dile-drop-file></dile-drop-file>');
72
+ let fileInput = el.shadowRoot.querySelector('input[type="file"]');
73
+ expect(fileInput.getAttribute('capture')).toBeNull();
74
+
75
+ el.capture = 'user';
76
+ await el.updateComplete;
77
+
78
+ fileInput = el.shadowRoot.querySelector('input[type="file"]');
79
+ expect(fileInput.getAttribute('capture')).toBe('user');
80
+ });
81
+
82
+ it('emits dile-drop-file-changed event when file is selected', async () => {
83
+ const el = await renderDropFile('<dile-drop-file></dile-drop-file>');
84
+ let eventDetail = null;
85
+ el.addEventListener('dile-drop-file-changed', (e) => { eventDetail = e.detail; });
86
+
87
+ const fileInput = el.shadowRoot.querySelector('input[type="file"]');
88
+ const file = new File(['content'], 'test.pdf', { type: 'application/pdf' });
89
+ Object.defineProperty(fileInput, 'files', {
90
+ value: [file],
91
+ writable: false,
92
+ });
93
+
94
+ fileInput.dispatchEvent(new Event('change', { bubbles: true }));
95
+ await el.updateComplete;
96
+
97
+ expect(eventDetail).toBeTruthy();
98
+ expect(eventDetail.file).toEqual(file);
99
+ });
100
+
101
+ it('emits dile-drop-file-changed event with null when file is cleared', async () => {
102
+ const el = await renderDropFile('<dile-drop-file></dile-drop-file>');
103
+ let eventDetail = null;
104
+ el.addEventListener('dile-drop-file-changed', (e) => { eventDetail = e.detail; });
105
+
106
+ el.clear();
107
+ await el.updateComplete;
108
+
109
+ expect(eventDetail).toBeTruthy();
110
+ expect(eventDetail.file).toBeNull();
111
+ });
33
112
  });
@@ -1,4 +1,5 @@
1
1
  import { LitElement, html, css } from 'lit';
2
+ import { ifDefined } from 'lit/directives/if-defined.js';
2
3
  import '../file-preview.js';
3
4
  import '../../button/button.js';
4
5
  import { labelStyles } from '../../input/src/label-styles.js';
@@ -61,6 +62,8 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
61
62
  allowedExtensions: { type: Array },
62
63
  extensionErrorMessage: { type: String },
63
64
  name: { type: String, reflect: true },
65
+ accept: { type: String },
66
+ capture: { type: String },
64
67
  };
65
68
  }
66
69
 
@@ -78,6 +81,8 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
78
81
  this.selectedFileLabel = "Selected file";
79
82
  this.extensionErrorMessage = "Only this file extensions are allowed: "
80
83
  this.allowedExtensions = [];
84
+ this.accept = '';
85
+ this.capture = '';
81
86
  this.internals = this.attachInternals();
82
87
  }
83
88
 
@@ -104,7 +109,7 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
104
109
  ${this.dropLabelTemplate}
105
110
  </div>
106
111
  ${this.messageTemplate}
107
- <input type="file" id="fileInput" @change=${this._handleFileInput}>
112
+ <input type="file" id="fileInput" @change=${this._handleFileInput} accept=${ifDefined(this.accept || undefined)} capture=${ifDefined(this.capture || undefined)}>
108
113
  <dile-file-preview
109
114
  fileName="${this.fileName}"
110
115
  selectedFileLabel="${this.selectedFileLabel}"
@@ -166,6 +171,7 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
166
171
  if (files.length > 0) {
167
172
  this.fileName = files[0].name;
168
173
  this.emmitChange();
174
+ this._emitDropFileChanged(files[0]);
169
175
  if(this._isValidExtension(this.fileName)) {
170
176
  this.message = this.firstMessage || '';
171
177
  this.errored = false;
@@ -205,6 +211,16 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
205
211
  }
206
212
  }
207
213
 
214
+ _emitDropFileChanged(file) {
215
+ this.dispatchEvent(new CustomEvent('dile-drop-file-changed', {
216
+ bubbles: true,
217
+ composed: true,
218
+ detail: {
219
+ file: file
220
+ }
221
+ }));
222
+ }
223
+
208
224
  openFileDialog() {
209
225
  this.fileInput.click();
210
226
  }
@@ -213,6 +229,7 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
213
229
  this.fileInput.value = "";
214
230
  this.fileName = "";
215
231
  this.internals.setFormValue(null);
232
+ this._emitDropFileChanged(null);
216
233
  this.emmitChange();
217
234
  }
218
235
 
@@ -31,6 +31,48 @@ describe('dile-input-search', () => {
31
31
  expect(input.getAttribute('data-form-type')).toBe('other');
32
32
  });
33
33
 
34
+ it('does not show an empty error box when errored without error slot content', async () => {
35
+ const el = await renderInputSearch('<dile-input-search errored></dile-input-search>');
36
+
37
+ const errorMessage = el.shadowRoot.getElementById('error-msg');
38
+ expect(errorMessage.hidden).toBe(true);
39
+ expect(errorMessage.getBoundingClientRect().height).toBe(0);
40
+ // Only the search box itself gets the bordered box styles
41
+ expect(getComputedStyle(errorMessage).borderTopStyle).toBe('none');
42
+ });
43
+
44
+ it('shows the error slot content when errored', async () => {
45
+ const el = await renderInputSearch('<dile-input-search errored><span slot="error">Wrong search</span></dile-input-search>');
46
+ await el.updateComplete;
47
+
48
+ const errorMessage = el.shadowRoot.getElementById('error-msg');
49
+ expect(errorMessage.hidden).toBe(false);
50
+ expect(el.shadowRoot.querySelector('input').getAttribute('aria-describedby')).toBe('error-msg');
51
+ });
52
+
53
+ it('labels the button in English by default, with configurable texts', async () => {
54
+ const el = await renderInputSearch('<dile-input-search></dile-input-search>');
55
+ const button = el.shadowRoot.querySelector('button');
56
+ expect(button.getAttribute('aria-label')).toBe('Search');
57
+ expect(el.shadowRoot.querySelector('input').getAttribute('aria-label')).toBe('Search');
58
+
59
+ el.value = 'lit';
60
+ await el.updateComplete;
61
+ expect(button.getAttribute('aria-label')).toBe('Clear search');
62
+
63
+ el.clearButtonLabel = 'Limpiar búsqueda';
64
+ await el.updateComplete;
65
+ expect(button.getAttribute('aria-label')).toBe('Limpiar búsqueda');
66
+ });
67
+
68
+ it('uses accessibleLabel as aria-label, falling back to name', async () => {
69
+ const named = await renderInputSearch('<dile-input-search name="Products"></dile-input-search>');
70
+ expect(named.shadowRoot.querySelector('input').getAttribute('aria-label')).toBe('Products');
71
+
72
+ const labelled = await renderInputSearch('<dile-input-search name="Products" accessibleLabel="Search products"></dile-input-search>');
73
+ expect(labelled.shadowRoot.querySelector('input').getAttribute('aria-label')).toBe('Search products');
74
+ });
75
+
34
76
  it('updates the value on input and dispatches dile-input-search after the delay', async () => {
35
77
  const el = await renderInputSearch('<dile-input-search delay="10"></dile-input-search>');
36
78
  const input = el.shadowRoot.querySelector('input');
@@ -8,7 +8,7 @@ export class DileInputSearch extends LitElement {
8
8
  :host {
9
9
  display: block;
10
10
  }
11
- div {
11
+ .search-box {
12
12
  display: flex;
13
13
  box-sizing: border-box;
14
14
  border-radius: var(--dile-input-border-radius, 5px);
@@ -17,7 +17,7 @@ export class DileInputSearch extends LitElement {
17
17
  width: var(--dile-input-width, 100%);
18
18
  background-color: var(--dile-input-background-color, #fff);
19
19
  }
20
- div:focus-within {
20
+ .search-box:focus-within {
21
21
  border-color: var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
22
22
  /* Outline adicional para mayor visibilidad */
23
23
  outline: 2px solid var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
@@ -25,6 +25,7 @@ export class DileInputSearch extends LitElement {
25
25
  }
26
26
  input {
27
27
  flex-grow: 1;
28
+ min-width: 0;
28
29
  border: none;
29
30
  color: var(--dile-input-color, #303030);
30
31
  font-size: var(--dile-input-font-size, 1em);
@@ -97,6 +98,14 @@ export class DileInputSearch extends LitElement {
97
98
  errored: { type: Boolean },
98
99
  /** Name for this input field */
99
100
  name: { type: String },
101
+ /** Whether there is content in the "error" slot */
102
+ _hasErrorContent: { state: true },
103
+ /** Accessible name of the input field, read by screen readers. Defaults to name, or to searchButtonLabel. */
104
+ accessibleLabel: { type: String },
105
+ /** Accessible name of the button when the field is empty */
106
+ searchButtonLabel: { type: String },
107
+ /** Accessible name of the button that clears the field */
108
+ clearButtonLabel: { type: String },
100
109
  };
101
110
  }
102
111
 
@@ -107,6 +116,9 @@ export class DileInputSearch extends LitElement {
107
116
  this.value = '';
108
117
  this.readOnly = false;
109
118
  this.errored = false;
119
+ this._hasErrorContent = false;
120
+ this.searchButtonLabel = 'Search';
121
+ this.clearButtonLabel = 'Clear search';
110
122
  }
111
123
 
112
124
  firstUpdated() {
@@ -114,12 +126,12 @@ export class DileInputSearch extends LitElement {
114
126
  }
115
127
 
116
128
  render() {
117
- const iconLabel = this.value.length
118
- ? 'Limpiar búsqueda'
119
- : 'Buscar';
129
+ const iconLabel = this.value.length
130
+ ? this.clearButtonLabel
131
+ : this.searchButtonLabel;
120
132
 
121
133
  return html`
122
- <div class="${this.errored ? "errored" : ""}">
134
+ <div class="search-box ${this.errored ? "errored" : ""}">
123
135
  <input
124
136
  id="elinput"
125
137
  type="search"
@@ -132,9 +144,9 @@ export class DileInputSearch extends LitElement {
132
144
  .value="${this.value}"
133
145
  ?disabled="${this.disabled}"
134
146
  ?readonly="${this.readOnly}"
135
- aria-label="${this.name || 'Búsqueda'}"
147
+ aria-label="${this.accessibleLabel || this.name || this.searchButtonLabel}"
136
148
  ?aria-invalid="${this.errored}"
137
- aria-describedby="${this.errored ? 'error-msg' : null}"
149
+ aria-describedby="${this.showErrorMessage ? 'error-msg' : null}"
138
150
  >
139
151
  <button
140
152
  @click=${this.iconClick}
@@ -150,14 +162,21 @@ export class DileInputSearch extends LitElement {
150
162
  ></dile-icon>
151
163
  </button>
152
164
  </div>
153
- ${this.errored ? html`
154
- <div id="error-msg" class="error-message" role="alert">
155
- <slot name="error"></slot>
156
- </div>
157
- ` : ''}
165
+ <div id="error-msg" class="error-message" role="alert" ?hidden=${!this.showErrorMessage}>
166
+ <slot name="error" @slotchange=${this.errorSlotChanged}></slot>
167
+ </div>
158
168
  `;
159
169
  }
160
170
 
171
+ /** The error message is only shown in the errored state, when there is an "error" slot content */
172
+ get showErrorMessage() {
173
+ return this.errored && this._hasErrorContent;
174
+ }
175
+
176
+ errorSlotChanged(e) {
177
+ this._hasErrorContent = e.target.assignedNodes({ flatten: true }).length > 0;
178
+ }
179
+
161
180
  inputHandler(e) {
162
181
  this.value = e.target.value;
163
182
  if (this.timeout) {
@@ -17,8 +17,8 @@ describe('dile-order-switch', () => {
17
17
  const el = await renderOrderSwitch('<dile-order-switch label="Name"></dile-order-switch>');
18
18
 
19
19
  expect(el.value).toBe('asc');
20
- expect(el.shadowRoot.querySelector('dile-icon')).toBeTruthy();
21
- expect(el.shadowRoot.querySelector('span').textContent.trim()).toBe('Name');
20
+ expect(el.shadowRoot.querySelector('dile-button-icon')).toBeTruthy();
21
+ expect(el.shadowRoot.querySelector('a').textContent.trim()).toBe('Name');
22
22
  });
23
23
 
24
24
  it('toggles between asc and desc', async () => {
@@ -35,7 +35,7 @@ describe('dile-order-switch', () => {
35
35
  let receivedDetail = null;
36
36
  el.addEventListener('element-changed', (e) => { receivedDetail = e.detail; });
37
37
 
38
- el.shadowRoot.querySelector('span').click();
38
+ el.shadowRoot.querySelector('a').click();
39
39
  await el.updateComplete;
40
40
 
41
41
  expect(el.value).toBe('asc'); // unchanged
@@ -48,7 +48,7 @@ describe('dile-order-switch', () => {
48
48
  let receivedDetail = null;
49
49
  el.addEventListener('element-changed', (e) => { receivedDetail = e.detail; });
50
50
 
51
- el.shadowRoot.querySelector('span').click();
51
+ el.shadowRoot.querySelector('a').click();
52
52
  await el.updateComplete;
53
53
 
54
54
  expect(el.value).toBe('desc'); // toggled
@@ -2,4 +2,6 @@ export { DileSelect } from './src/DileSelect.js';
2
2
  export { DileSelectAjax } from './src/DileSelectAjax.js';
3
3
  export { DileSelectBoolean } from './src/DileSelectBoolean.js';
4
4
  export { DileSelectOverlay } from './src/DileSelectOverlay.js';
5
- export { DileSelectAjaxOverlay } from './src/DileSelectAjaxOverlay.js';
5
+ export { DileSelectAjaxOverlay } from './src/DileSelectAjaxOverlay.js';
6
+ export { DileSelectSearch } from './src/DileSelectSearch.js';
7
+ export { DileSelectSearchableBase } from './src/DileSelectSearchableBase.js';
@@ -321,6 +321,22 @@ describe('dile-select-overlay', () => {
321
321
  });
322
322
 
323
323
  describe('Positioning', () => {
324
+ it('can open a popup wider than a narrow trigger with --dile-select-overlay-min-width', async () => {
325
+ const el = await renderSelectOverlay(`
326
+ <dile-select-overlay style="width: 80px; --dile-select-overlay-min-width: 200px;">
327
+ <select slot="select">
328
+ <option value="1">A long option text</option>
329
+ </select>
330
+ </dile-select-overlay>
331
+ `);
332
+
333
+ await open(el);
334
+
335
+ const overlay = el.shadowRoot.getElementById('overlay');
336
+ expect(overlay.getBoundingClientRect().width).toBe(200);
337
+ expect(el.shadowRoot.getElementById('trigger').getBoundingClientRect().width).toBe(80);
338
+ });
339
+
324
340
  it('keeps the same moveTop-tightened gap when flipping above the trigger as when sitting below it', async () => {
325
341
  // Regression test: moveTop used to only tighten the gap for the normal (below) position.
326
342
  // The flip-to-above fallback used a fixed 10px margin unrelated to moveTop, so a popup
@@ -0,0 +1,6 @@
1
+ import './select-overlay.js'
2
+ import { DileSelectSearch } from "./src/DileSelectSearch.js";
3
+
4
+ if (!customElements.get("dile-select-search")) {
5
+ window.customElements.define("dile-select-search", DileSelectSearch);
6
+ }