@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.
- package/components/drop-file/drop-file.spec.js +79 -0
- package/components/drop-file/src/DileDropFile.js +18 -1
- 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
|
@@ -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
|
-
|
|
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
|