@dile/ui 3.14.0 → 3.16.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.
- package/components/drop-file/drop-file.spec.js +133 -0
- package/components/drop-file/src/DileDropFile.js +95 -6
- package/components/input/input-search.spec.js +42 -0
- package/components/input/src/DileInputSearch.js +32 -13
- package/components/order-switch/order-switch.spec.js +4 -4
- package/components/select/__screenshots__/select-search.spec.js/dile-select-search-Value-API-uses-the-label-as-accessible-name-of-the-selected-field-1.png +0 -0
- package/components/select/index.js +3 -1
- package/components/select/select-overlay.spec.js +16 -0
- package/components/select/select-search.js +6 -0
- package/components/select/select-search.spec.js +318 -0
- package/components/select/src/DileSelectAjaxOverlay.js +18 -449
- package/components/select/src/DileSelectOverlay.js +3 -0
- package/components/select/src/DileSelectSearch.js +129 -0
- package/components/select/src/DileSelectSearchableBase.js +544 -0
- package/package.json +2 -2
|
@@ -78,4 +78,137 @@ describe('dile-drop-file', () => {
|
|
|
78
78
|
fileInput = el.shadowRoot.querySelector('input[type="file"]');
|
|
79
79
|
expect(fileInput.getAttribute('capture')).toBe('user');
|
|
80
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
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('multiple mode', () => {
|
|
114
|
+
function createFile(name, content = 'content') {
|
|
115
|
+
return new File([content], name, { lastModified: 1 });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function dropFiles(el, files) {
|
|
119
|
+
const dataTransfer = new DataTransfer();
|
|
120
|
+
files.forEach(file => dataTransfer.items.add(file));
|
|
121
|
+
const dropZone = el.shadowRoot.querySelector('#dropZone');
|
|
122
|
+
dropZone.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true, cancelable: true }));
|
|
123
|
+
return el.updateComplete;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
it('renders the file input with the multiple attribute', async () => {
|
|
127
|
+
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
128
|
+
expect(el.shadowRoot.querySelector('input[type="file"]').hasAttribute('multiple')).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('does not render the multiple attribute by default', async () => {
|
|
132
|
+
const el = await renderDropFile('<dile-drop-file></dile-drop-file>');
|
|
133
|
+
expect(el.shadowRoot.querySelector('input[type="file"]').hasAttribute('multiple')).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('accepts several dropped files and renders a preview for each one', async () => {
|
|
137
|
+
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
138
|
+
let eventDetail = null;
|
|
139
|
+
el.addEventListener('dile-drop-file-changed', (e) => { eventDetail = e.detail; });
|
|
140
|
+
|
|
141
|
+
await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
|
|
142
|
+
|
|
143
|
+
expect(el.shadowRoot.querySelectorAll('dile-file-preview').length).toBe(2);
|
|
144
|
+
expect(el.value.map(file => file.name)).toEqual(['a.txt', 'b.txt']);
|
|
145
|
+
expect(el.getFiles().length).toBe(2);
|
|
146
|
+
expect(el.shadowRoot.querySelector('#fileInput').files.length).toBe(2);
|
|
147
|
+
expect(eventDetail.files.map(file => file.name)).toEqual(['a.txt', 'b.txt']);
|
|
148
|
+
expect(eventDetail.file.name).toBe('a.txt');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('accumulates files between selections without duplicates', async () => {
|
|
152
|
+
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
153
|
+
await dropFiles(el, [createFile('a.txt')]);
|
|
154
|
+
await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
|
|
155
|
+
|
|
156
|
+
expect(el.value.map(file => file.name)).toEqual(['a.txt', 'b.txt']);
|
|
157
|
+
expect(el.shadowRoot.querySelectorAll('dile-file-preview').length).toBe(2);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('removes a single file with removeFile', async () => {
|
|
161
|
+
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
162
|
+
await dropFiles(el, [createFile('a.txt'), createFile('b.txt'), createFile('c.txt')]);
|
|
163
|
+
|
|
164
|
+
el.removeFile(1);
|
|
165
|
+
await el.updateComplete;
|
|
166
|
+
|
|
167
|
+
expect(el.value.map(file => file.name)).toEqual(['a.txt', 'c.txt']);
|
|
168
|
+
expect(el.shadowRoot.querySelector('#fileInput').files.length).toBe(2);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('removes a file when its preview clear link is clicked', async () => {
|
|
172
|
+
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
173
|
+
await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
|
|
174
|
+
|
|
175
|
+
const preview = el.shadowRoot.querySelectorAll('dile-file-preview')[0];
|
|
176
|
+
await preview.updateComplete;
|
|
177
|
+
preview.shadowRoot.querySelector('a').click();
|
|
178
|
+
await el.updateComplete;
|
|
179
|
+
|
|
180
|
+
expect(el.value.map(file => file.name)).toEqual(['b.txt']);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('discards files with invalid extensions and keeps the valid ones', async () => {
|
|
184
|
+
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
185
|
+
el.allowedExtensions = ['pdf'];
|
|
186
|
+
await dropFiles(el, [createFile('a.pdf'), createFile('b.exe')]);
|
|
187
|
+
|
|
188
|
+
expect(el.value.map(file => file.name)).toEqual(['a.pdf']);
|
|
189
|
+
expect(el.errored).toBe(true);
|
|
190
|
+
expect(el.shadowRoot.querySelector('.message.errored-msg')).toBeTruthy();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('clears all the files', async () => {
|
|
194
|
+
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
195
|
+
await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
|
|
196
|
+
|
|
197
|
+
el.clear();
|
|
198
|
+
await el.updateComplete;
|
|
199
|
+
|
|
200
|
+
expect(el.value).toEqual([]);
|
|
201
|
+
expect(el.shadowRoot.querySelectorAll('dile-file-preview').length).toBe(0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('sets all the files as form value', async () => {
|
|
205
|
+
document.body.innerHTML = '<form><dile-drop-file name="docs" multiple></dile-drop-file></form>';
|
|
206
|
+
const el = document.querySelector('dile-drop-file');
|
|
207
|
+
await el.updateComplete;
|
|
208
|
+
await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
|
|
209
|
+
|
|
210
|
+
const formData = new FormData(document.querySelector('form'));
|
|
211
|
+
expect(formData.getAll('docs').map(file => file.name)).toEqual(['a.txt', 'b.txt']);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
81
214
|
});
|
|
@@ -64,6 +64,8 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
|
|
|
64
64
|
name: { type: String, reflect: true },
|
|
65
65
|
accept: { type: String },
|
|
66
66
|
capture: { type: String },
|
|
67
|
+
multiple: { type: Boolean, reflect: true },
|
|
68
|
+
files: { state: true },
|
|
67
69
|
};
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -83,6 +85,8 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
|
|
|
83
85
|
this.allowedExtensions = [];
|
|
84
86
|
this.accept = '';
|
|
85
87
|
this.capture = '';
|
|
88
|
+
this.multiple = false;
|
|
89
|
+
this.files = [];
|
|
86
90
|
this.internals = this.attachInternals();
|
|
87
91
|
}
|
|
88
92
|
|
|
@@ -109,12 +113,23 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
|
|
|
109
113
|
${this.dropLabelTemplate}
|
|
110
114
|
</div>
|
|
111
115
|
${this.messageTemplate}
|
|
112
|
-
<input type="file" id="fileInput" @change=${this._handleFileInput} accept=${ifDefined(this.accept || undefined)} capture=${ifDefined(this.capture || undefined)}>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
<input type="file" id="fileInput" ?multiple=${this.multiple} @change=${this._handleFileInput} accept=${ifDefined(this.accept || undefined)} capture=${ifDefined(this.capture || undefined)}>
|
|
117
|
+
${this.multiple
|
|
118
|
+
? this.files.map((file, index) => html`
|
|
119
|
+
<dile-file-preview
|
|
120
|
+
fileName="${file.name}"
|
|
121
|
+
selectedFileLabel="${this.selectedFileLabel}"
|
|
122
|
+
@dile-file-clear=${() => this.removeFile(index)}
|
|
123
|
+
></dile-file-preview>
|
|
124
|
+
`)
|
|
125
|
+
: html`
|
|
126
|
+
<dile-file-preview
|
|
127
|
+
fileName="${this.fileName}"
|
|
128
|
+
selectedFileLabel="${this.selectedFileLabel}"
|
|
129
|
+
@dile-file-clear=${this.clear}
|
|
130
|
+
></dile-file-preview>
|
|
131
|
+
`
|
|
132
|
+
}
|
|
118
133
|
`;
|
|
119
134
|
}
|
|
120
135
|
|
|
@@ -167,10 +182,15 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
|
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
_processFile(files) {
|
|
185
|
+
if (this.multiple) {
|
|
186
|
+
this._processMultipleFiles(files);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
170
189
|
this.internals.setFormValue(files.length > 0 ? files[0] : null);
|
|
171
190
|
if (files.length > 0) {
|
|
172
191
|
this.fileName = files[0].name;
|
|
173
192
|
this.emmitChange();
|
|
193
|
+
this._emitDropFileChanged(files[0]);
|
|
174
194
|
if(this._isValidExtension(this.fileName)) {
|
|
175
195
|
this.message = this.firstMessage || '';
|
|
176
196
|
this.errored = false;
|
|
@@ -182,6 +202,56 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
|
|
|
182
202
|
}
|
|
183
203
|
}
|
|
184
204
|
|
|
205
|
+
_processMultipleFiles(files) {
|
|
206
|
+
const incoming = Array.from(files);
|
|
207
|
+
if (incoming.length === 0) {
|
|
208
|
+
// Restore the input files, as a canceled dialog empties it
|
|
209
|
+
this._syncFileInput();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const validFiles = incoming.filter(file => this._isValidExtension(file.name));
|
|
213
|
+
const newFiles = validFiles.filter(file => !this.files.some(current => this._isSameFile(current, file)));
|
|
214
|
+
this.files = [...this.files, ...newFiles];
|
|
215
|
+
if (validFiles.length < incoming.length) {
|
|
216
|
+
this.message = `${this.extensionErrorMessage} ${this.allowedExtensions.join(', ')}`;
|
|
217
|
+
this.errored = true;
|
|
218
|
+
} else {
|
|
219
|
+
this.message = this.firstMessage || '';
|
|
220
|
+
this.errored = false;
|
|
221
|
+
}
|
|
222
|
+
this._syncFiles();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
_isSameFile(a, b) {
|
|
226
|
+
return a.name === b.name && a.size === b.size && a.lastModified === b.lastModified;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_syncFileInput() {
|
|
230
|
+
if (!this.fileInput) return;
|
|
231
|
+
const dataTransfer = new DataTransfer();
|
|
232
|
+
this.files.forEach(file => dataTransfer.items.add(file));
|
|
233
|
+
this.fileInput.files = dataTransfer.files;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_syncFiles() {
|
|
237
|
+
this._syncFileInput();
|
|
238
|
+
if (this.files.length > 0) {
|
|
239
|
+
const formData = new FormData();
|
|
240
|
+
this.files.forEach(file => formData.append(this.name || 'files', file));
|
|
241
|
+
this.internals.setFormValue(formData);
|
|
242
|
+
} else {
|
|
243
|
+
this.internals.setFormValue(null);
|
|
244
|
+
}
|
|
245
|
+
this.emmitChange();
|
|
246
|
+
this._emitDropFileChanged(this.files[0] || null);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
removeFile(index) {
|
|
250
|
+
if (index < 0 || index >= this.files.length) return;
|
|
251
|
+
this.files = this.files.filter((_, i) => i !== index);
|
|
252
|
+
this._syncFiles();
|
|
253
|
+
}
|
|
254
|
+
|
|
185
255
|
_isValidExtension(fileName) {
|
|
186
256
|
if(this.allowedExtensions.length === 0) {
|
|
187
257
|
return true;
|
|
@@ -210,6 +280,17 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
|
|
|
210
280
|
}
|
|
211
281
|
}
|
|
212
282
|
|
|
283
|
+
_emitDropFileChanged(file) {
|
|
284
|
+
this.dispatchEvent(new CustomEvent('dile-drop-file-changed', {
|
|
285
|
+
bubbles: true,
|
|
286
|
+
composed: true,
|
|
287
|
+
detail: {
|
|
288
|
+
file: file,
|
|
289
|
+
files: this.multiple ? [...this.files] : (file ? [file] : []),
|
|
290
|
+
}
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
|
|
213
294
|
openFileDialog() {
|
|
214
295
|
this.fileInput.click();
|
|
215
296
|
}
|
|
@@ -217,15 +298,23 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
|
|
|
217
298
|
clear() {
|
|
218
299
|
this.fileInput.value = "";
|
|
219
300
|
this.fileName = "";
|
|
301
|
+
this.files = [];
|
|
220
302
|
this.internals.setFormValue(null);
|
|
303
|
+
this._emitDropFileChanged(null);
|
|
221
304
|
this.emmitChange();
|
|
222
305
|
}
|
|
223
306
|
|
|
224
307
|
getFiles() {
|
|
308
|
+
if (this.multiple) {
|
|
309
|
+
return [...this.files];
|
|
310
|
+
}
|
|
225
311
|
return this.fileInput.files;
|
|
226
312
|
}
|
|
227
313
|
|
|
228
314
|
get value() {
|
|
315
|
+
if (this.multiple) {
|
|
316
|
+
return [...this.files];
|
|
317
|
+
}
|
|
229
318
|
if(this.fileInput) {
|
|
230
319
|
return this.fileInput.files[0];
|
|
231
320
|
} else {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
119
|
-
:
|
|
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 ||
|
|
147
|
+
aria-label="${this.accessibleLabel || this.name || this.searchButtonLabel}"
|
|
136
148
|
?aria-invalid="${this.errored}"
|
|
137
|
-
aria-describedby="${this.
|
|
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
|
-
|
|
154
|
-
<
|
|
155
|
-
|
|
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('
|
|
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('
|
|
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('
|
|
51
|
+
el.shadowRoot.querySelector('a').click();
|
|
52
52
|
await el.updateComplete;
|
|
53
53
|
|
|
54
54
|
expect(el.value).toBe('desc'); // toggled
|
|
Binary file
|
|
@@ -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
|