@kubex/zinc 1.1.39 → 1.1.40
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/dist/custom-elements.json +305 -93
- package/dist/vscode.html-custom-data.json +33 -14
- package/dist/web-types.json +92 -28
- package/dist/zn.d.ts +220 -173
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +532 -512
- package/docs/_utilities/code-previews.cjs +6 -2
- package/docs/pages/components/datepicker.md +10 -1
- package/docs/pages/components/style.md +46 -0
- package/package.json +1 -1
- package/scss/_root.scss +21 -1
- package/scss/air-datapicker.scss +50 -0
- package/scss/boot.scss +1 -48
- package/src/components/data-table/data-table.component.ts +152 -73
- package/src/components/data-table/data-table.scss +6 -0
- package/src/components/data-table/data-table.test.ts +15 -0
- package/src/components/data-table-search/data-table-search.component.ts +13 -1
- package/src/components/datepicker/datepicker.component.ts +66 -12
- package/src/components/datepicker/datepicker.scss +1 -0
- package/src/components/query-builder/query-builder.component.ts +62 -21
- package/src/components/query-builder/query-builder.scss +4 -0
- package/src/components/style/style.component.ts +43 -4
- package/src/components/style/style.test.ts +71 -0
- package/src/utilities/query.test.ts +63 -0
- package/src/utilities/query.ts +27 -0
|
@@ -153,9 +153,39 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
|
|
|
153
153
|
*/
|
|
154
154
|
@property() format: string = 'MM/dd/yyyy';
|
|
155
155
|
|
|
156
|
+
/** Display time selector. **/
|
|
157
|
+
@property({attribute: 'time-picker',type: Boolean, reflect: true}) timePicker?: boolean = false;
|
|
158
|
+
|
|
159
|
+
/** Display only time selector, without date. **/
|
|
160
|
+
@property({attribute: 'only-time'}) onlyTimepicker?: boolean = false;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Time format for display and input selector. Uses AirDatepicker format tokens.
|
|
164
|
+
* Default : hh:mm AA
|
|
165
|
+
*
|
|
166
|
+
* Possible symbols:
|
|
167
|
+
* h — hours in 12-hour mode
|
|
168
|
+
* hh — hours in 12-hour mode with leading zero
|
|
169
|
+
* H — hours in 24-hour mode
|
|
170
|
+
* HH — hours in 24-hour mode with leading zero
|
|
171
|
+
* m — minutes
|
|
172
|
+
* mm — minutes with leading zero
|
|
173
|
+
* aa — day period lower case
|
|
174
|
+
* AA — day period upper case
|
|
175
|
+
*/
|
|
176
|
+
@property({attribute: 'format-time'}) timeFormat?: string = "hh:mm AA";
|
|
177
|
+
@property({attribute: 'container'}) container?: string | HTMLElement;
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
156
181
|
private _instance: AirDatepicker<HTMLInputElement>;
|
|
157
182
|
|
|
158
183
|
|
|
184
|
+
get timestamp(): number {
|
|
185
|
+
const timestamp = (this.input.parentElement!.querySelector("#date-picker-timestamp")! as HTMLInputElement).value ?? '';
|
|
186
|
+
return Number(timestamp);
|
|
187
|
+
}
|
|
188
|
+
|
|
159
189
|
/** Gets the validity state object */
|
|
160
190
|
get validity() {
|
|
161
191
|
return this.input?.validity;
|
|
@@ -194,6 +224,8 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
|
|
|
194
224
|
@watch('minDate', {waitUntilFirstUpdate: true})
|
|
195
225
|
@watch('maxDate', {waitUntilFirstUpdate: true})
|
|
196
226
|
@watch('disablePastDates', {waitUntilFirstUpdate: true})
|
|
227
|
+
@watch('timePicker', {waitUntilFirstUpdate: true})
|
|
228
|
+
@watch('timeFormat', {waitUntilFirstUpdate: true})
|
|
197
229
|
handleDatepickerOptionsChange() {
|
|
198
230
|
this.init();
|
|
199
231
|
}
|
|
@@ -241,24 +273,44 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
|
|
|
241
273
|
}
|
|
242
274
|
}
|
|
243
275
|
|
|
244
|
-
const inputElement = this.shadowRoot
|
|
276
|
+
const inputElement = this.shadowRoot!.querySelector('input')! as HTMLInputElement;
|
|
245
277
|
if (inputElement) {
|
|
278
|
+
const timestampField = this.shadowRoot!.querySelector("#date-picker-timestamp")! as HTMLElement;
|
|
279
|
+
|
|
246
280
|
const options: AirDatepickerOptions = {
|
|
247
281
|
locale: enLocale,
|
|
248
282
|
dateFormat: this.format,
|
|
249
283
|
range: this.range,
|
|
250
284
|
toggleSelected: false,
|
|
285
|
+
timepicker: this.timePicker,
|
|
286
|
+
onlyTimepicker: this.onlyTimepicker,
|
|
287
|
+
timeFormat: this.timeFormat,
|
|
288
|
+
container: this.container,
|
|
289
|
+
altField: timestampField,
|
|
251
290
|
onSelect: ({date}) => {
|
|
291
|
+
// Blur the input after selection to prevent invisible keyboard navigation
|
|
292
|
+
if (this.timePicker || this.onlyTimepicker){
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
if (date && !this.range) {
|
|
296
|
+
// For single date selection, blur immediately
|
|
297
|
+
this.input.blur();
|
|
252
298
|
this.handleChange();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
} else if (date && this.range && Array.isArray(date) && date.length === 2) {
|
|
258
|
-
// For range selection, blur only after both dates are selected
|
|
259
|
-
this.input.blur();
|
|
260
|
-
}
|
|
299
|
+
} else if (date && this.range && Array.isArray(date) && date.length === 2) {
|
|
300
|
+
// For range selection, blur only after both dates are selected
|
|
301
|
+
this.input.blur();
|
|
302
|
+
this.handleChange();
|
|
261
303
|
}
|
|
304
|
+
},
|
|
305
|
+
onHide:(isAnimationComplete) => {
|
|
306
|
+
if (!isAnimationComplete) {
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (this.timePicker || this.onlyTimepicker){
|
|
311
|
+
this.handleChange();
|
|
312
|
+
}
|
|
313
|
+
},
|
|
262
314
|
};
|
|
263
315
|
|
|
264
316
|
if (this.minDate) {
|
|
@@ -532,8 +584,11 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
|
|
|
532
584
|
return formatted;
|
|
533
585
|
}
|
|
534
586
|
|
|
535
|
-
|
|
587
|
+
updated(_changedProperties: PropertyValues) {
|
|
536
588
|
super.updated(_changedProperties);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
firstUpdated(){
|
|
537
592
|
this.init();
|
|
538
593
|
}
|
|
539
594
|
|
|
@@ -617,13 +672,12 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
|
|
|
617
672
|
?autofocus=${this.autofocus}
|
|
618
673
|
spellcheck=${this.spellcheck}
|
|
619
674
|
aria-describedby="help-text"
|
|
620
|
-
@change=${this.handleChange}
|
|
621
675
|
@input=${this.handleInput}
|
|
622
676
|
@invalid=${this.handleInvalid}
|
|
623
677
|
@keydown=${this.handleKeyDown}
|
|
624
678
|
@paste=${this.handlePaste}
|
|
625
679
|
@blur=${this.handleBlur}>
|
|
626
|
-
|
|
680
|
+
<input id="date-picker-timestamp" type="hidden">
|
|
627
681
|
<span part="suffix" class="input__suffix">
|
|
628
682
|
<slot name="suffix"></slot>
|
|
629
683
|
</span>
|
|
@@ -13,6 +13,7 @@ import type {ZnChangeEvent} from "../../events/zn-change";
|
|
|
13
13
|
import type {ZnInputEvent} from "../../events/zn-input";
|
|
14
14
|
|
|
15
15
|
import styles from './query-builder.scss';
|
|
16
|
+
import type ZnDatepicker from "../datepicker";
|
|
16
17
|
|
|
17
18
|
export type QueryBuilderData = QueryBuilderItem[];
|
|
18
19
|
|
|
@@ -22,10 +23,24 @@ export interface QueryBuilderItem {
|
|
|
22
23
|
type?: QueryBuilderType;
|
|
23
24
|
options?: QueryBuilderOptions;
|
|
24
25
|
operators: QueryBuilderOperators[];
|
|
26
|
+
dateSubmitFormat?: QueryBuilderDateSubmitFormat;
|
|
25
27
|
maxOptionsVisible?: string;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Controls how `date` and `dateTime` filter values are serialized when the
|
|
32
|
+
* query is submitted.
|
|
33
|
+
*
|
|
34
|
+
* - `'iso'` — RFC 3339 / ISO 8601 (e.g. `2026-06-09T16:05:00Z`).
|
|
35
|
+
* - `'timestamp'` — Unix timestamp in seconds since epoch.
|
|
36
|
+
* - `'legacy'` — whatever format the current system emits. Kept so existing
|
|
37
|
+
* backends keep working while consumers migrate to one of the
|
|
38
|
+
* formats above. - DEFAULT
|
|
39
|
+
*/
|
|
40
|
+
export type QueryBuilderDateSubmitFormat = 'iso' | 'timestamp' | 'legacy';
|
|
41
|
+
|
|
42
|
+
export type QueryBuilderType = 'bool' | 'boolean' | 'date' | 'dateTime' | 'number';
|
|
43
|
+
|
|
29
44
|
|
|
30
45
|
export interface QueryBuilderOptions {
|
|
31
46
|
[key: string | number]: string | number;
|
|
@@ -187,6 +202,7 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
|
|
|
187
202
|
</zn-option>`)}
|
|
188
203
|
</zn-select>
|
|
189
204
|
<input id="main-input" name="${this.name}" value="${this.value}" hidden>
|
|
205
|
+
<div id="air-datepicker-query-builder-container" class="air-datepicker-global-container"></div>
|
|
190
206
|
</div>
|
|
191
207
|
`;
|
|
192
208
|
}
|
|
@@ -214,6 +230,7 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
|
|
|
214
230
|
if (filter === undefined) return;
|
|
215
231
|
|
|
216
232
|
const uniqueId = Math.random().toString(36).substring(7);
|
|
233
|
+
|
|
217
234
|
this._selectedRules.set(uniqueId, {
|
|
218
235
|
id: filter.id,
|
|
219
236
|
name: filter.name,
|
|
@@ -306,7 +323,7 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
|
|
|
306
323
|
}
|
|
307
324
|
|
|
308
325
|
private _createInput(filter: QueryBuilderItem, uniqueId: string, selectedComparator: QueryBuilderOperators) {
|
|
309
|
-
let input: ZnSelect | ZnInput | null;
|
|
326
|
+
let input: ZnSelect | ZnInput | ZnDatepicker | null;
|
|
310
327
|
switch (filter.type) {
|
|
311
328
|
case 'bool':
|
|
312
329
|
case 'boolean': {
|
|
@@ -318,7 +335,11 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
|
|
|
318
335
|
break;
|
|
319
336
|
}
|
|
320
337
|
case 'date': {
|
|
321
|
-
input = this._createDateInput(uniqueId);
|
|
338
|
+
input = this._createDateInput(uniqueId, false, filter.dateSubmitFormat);
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case 'dateTime': {
|
|
342
|
+
input = this._createDateInput(uniqueId, true , filter.dateSubmitFormat);
|
|
322
343
|
break;
|
|
323
344
|
}
|
|
324
345
|
default: {
|
|
@@ -373,13 +394,19 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
|
|
|
373
394
|
return litToHTML<ZnInput>(input);
|
|
374
395
|
}
|
|
375
396
|
|
|
376
|
-
private _createDateInput(uniqueId: string):
|
|
397
|
+
private _createDateInput(uniqueId: string, hasTime: boolean, submitFormat: QueryBuilderDateSubmitFormat = 'legacy'): ZnDatepicker | null {
|
|
377
398
|
const input = html`
|
|
378
|
-
<zn-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
399
|
+
<zn-datepicker
|
|
400
|
+
name="${uniqueId}"
|
|
401
|
+
format-time="HH:mm"
|
|
402
|
+
.container=${this.container.querySelector('#air-datepicker-query-builder-container')}
|
|
403
|
+
time-picker="${hasTime}"
|
|
404
|
+
class="query-builder__value"
|
|
405
|
+
@zn-change="${(e: ZnChangeEvent) => this._updateDateValue(uniqueId, e, submitFormat)}"
|
|
406
|
+
>
|
|
407
|
+
</zn-datepicker>
|
|
408
|
+
`;
|
|
409
|
+
return litToHTML<ZnDatepicker>(input);
|
|
383
410
|
}
|
|
384
411
|
|
|
385
412
|
private _createSelectInput(uniqueId: string, filter: QueryBuilderItem, selectedComparator: QueryBuilderOperators): ZnSelect | null {
|
|
@@ -426,23 +453,36 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
|
|
|
426
453
|
this._handleChange();
|
|
427
454
|
}
|
|
428
455
|
|
|
429
|
-
private _updateDateValue(id: string, event: Event | { target:
|
|
456
|
+
private _updateDateValue(id: string, event: Event | { target: ZnDatepicker | HTMLDivElement }, submitFormat: QueryBuilderDateSubmitFormat = 'legacy') {
|
|
430
457
|
const filter = this._selectedRules.get(id);
|
|
431
458
|
if (!filter) return;
|
|
432
|
-
const input = event.target as ZnSelect | ZnInput;
|
|
433
|
-
const operator = filter.operator as QueryBuilderOperators;
|
|
434
|
-
let timestamp: string;
|
|
435
459
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
460
|
+
const input = event.target as ZnDatepicker;
|
|
461
|
+
const operator = filter.operator as QueryBuilderOperators;
|
|
462
|
+
let d: Date;
|
|
463
|
+
let value: string;
|
|
464
|
+
|
|
465
|
+
switch (submitFormat) {
|
|
466
|
+
case "legacy":
|
|
467
|
+
if (operator === QueryBuilderOperators.Eq || operator === QueryBuilderOperators.Neq) {
|
|
468
|
+
value = (input.timestamp / 1000).toString();
|
|
469
|
+
} else {
|
|
470
|
+
// Dodgy logic to offset backend filter comparator values
|
|
471
|
+
// Ref: backend/src/Infrastructure/Helpers/AdvancedFilterHelper.php:106
|
|
472
|
+
const multiplier = operator === QueryBuilderOperators.Before ? -1 : 1;
|
|
473
|
+
value = (Math.floor((Date.now() - input.timestamp) / 1000 / 60) * multiplier).toString();
|
|
474
|
+
}
|
|
475
|
+
break;
|
|
476
|
+
case "timestamp":
|
|
477
|
+
value = input.timestamp.toString();
|
|
478
|
+
break;
|
|
479
|
+
case "iso":
|
|
480
|
+
d = new Date(input.timestamp);
|
|
481
|
+
value = new Date(d.getTime() - d.getTimezoneOffset() * 60_000).toISOString();
|
|
482
|
+
break;
|
|
443
483
|
}
|
|
444
484
|
|
|
445
|
-
filter.value =
|
|
485
|
+
filter.value = value;
|
|
446
486
|
|
|
447
487
|
this._selectedRules.set(id, filter);
|
|
448
488
|
this._handleChange();
|
|
@@ -459,6 +499,7 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
|
|
|
459
499
|
this._handleChange();
|
|
460
500
|
}
|
|
461
501
|
|
|
502
|
+
|
|
462
503
|
private updateInValue(id: string, event: Event) {
|
|
463
504
|
const filter = this._selectedRules.get(id);
|
|
464
505
|
if (!filter) return;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {type CSSResultGroup, unsafeCSS} from 'lit';
|
|
2
|
+
import {customElement, property} from 'lit/decorators.js';
|
|
2
3
|
import {LocalizeController} from '../../utilities/localize';
|
|
3
|
-
import {property} from 'lit/decorators.js';
|
|
4
4
|
import ZincElement from '../../internal/zinc-element';
|
|
5
5
|
|
|
6
6
|
import styles from './style.scss';
|
|
7
7
|
|
|
8
|
+
@customElement('zn-style')
|
|
8
9
|
export default class ZnStyle extends ZincElement {
|
|
9
10
|
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
10
11
|
|
|
@@ -12,7 +13,8 @@ export default class ZnStyle extends ZincElement {
|
|
|
12
13
|
private readonly localize = new LocalizeController(this);
|
|
13
14
|
|
|
14
15
|
@property() color = '';
|
|
15
|
-
@property(
|
|
16
|
+
@property() border = '';
|
|
17
|
+
@property() size = '';
|
|
16
18
|
@property({type: Boolean}) error = false;
|
|
17
19
|
@property({type: Boolean}) success = false;
|
|
18
20
|
@property({type: Boolean}) info = false;
|
|
@@ -26,6 +28,7 @@ export default class ZnStyle extends ZincElement {
|
|
|
26
28
|
@property() height = '';
|
|
27
29
|
@property() pad = '';
|
|
28
30
|
@property() margin = '';
|
|
31
|
+
@property({type: Boolean}) muted = false;
|
|
29
32
|
@property({type: Boolean}) gutter = false;
|
|
30
33
|
@property({attribute: 'a-margin'}) autoMargin = '';
|
|
31
34
|
|
|
@@ -81,9 +84,34 @@ export default class ZnStyle extends ZincElement {
|
|
|
81
84
|
this.classList.toggle('h-full', true);
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
|
|
87
|
+
// border accepts any combination of t/b/l/r, plus 'a' (or 'tblr') as a
|
|
88
|
+
// shortcut for all four sides. Existing `<zn-style border>` (boolean
|
|
89
|
+
// attribute → empty string value with attribute present) keeps rendering
|
|
90
|
+
// all four sides; pure absence renders no border.
|
|
91
|
+
const borderSides = this.hasAttribute('border')
|
|
92
|
+
? (this.border || 'a')
|
|
93
|
+
: '';
|
|
94
|
+
if (borderSides) {
|
|
85
95
|
display = 'inline-block'
|
|
86
|
-
|
|
96
|
+
for (const c of borderSides) {
|
|
97
|
+
switch (c) {
|
|
98
|
+
case 'a':
|
|
99
|
+
this.classList.toggle('zn-border', true);
|
|
100
|
+
break;
|
|
101
|
+
case 't':
|
|
102
|
+
this.classList.toggle('zn-bt', true);
|
|
103
|
+
break;
|
|
104
|
+
case 'b':
|
|
105
|
+
this.classList.toggle('zn-bb', true);
|
|
106
|
+
break;
|
|
107
|
+
case 'l':
|
|
108
|
+
this.classList.toggle('zn-bl', true);
|
|
109
|
+
break;
|
|
110
|
+
case 'r':
|
|
111
|
+
this.classList.toggle('zn-br', true);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
87
115
|
}
|
|
88
116
|
|
|
89
117
|
if (this.pad) {
|
|
@@ -174,6 +202,17 @@ export default class ZnStyle extends ZincElement {
|
|
|
174
202
|
}
|
|
175
203
|
}
|
|
176
204
|
|
|
205
|
+
if (this.muted) {
|
|
206
|
+
display = 'inline-block';
|
|
207
|
+
this.classList.toggle('zn-muted', true);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// size: xs/s/l/xl each toggle a sizing class. 'm' (and empty) are the
|
|
211
|
+
// default and apply no class.
|
|
212
|
+
if (this.size && this.size !== 'm') {
|
|
213
|
+
this.classList.toggle('zn-size-' + this.size, true);
|
|
214
|
+
}
|
|
215
|
+
|
|
177
216
|
if (this.display) {
|
|
178
217
|
// Force attribute display
|
|
179
218
|
display = this.display
|
|
@@ -7,4 +7,75 @@ describe('<zn-style>', () => {
|
|
|
7
7
|
|
|
8
8
|
expect(el).to.exist;
|
|
9
9
|
});
|
|
10
|
+
|
|
11
|
+
describe('border', () => {
|
|
12
|
+
it('border="t" applies only zn-bt', async () => {
|
|
13
|
+
const el = await fixture<HTMLElement>(html`<zn-style border="t"></zn-style>`);
|
|
14
|
+
expect(el.classList.contains('zn-bt')).to.be.true;
|
|
15
|
+
expect(el.classList.contains('zn-bb')).to.be.false;
|
|
16
|
+
expect(el.classList.contains('zn-bl')).to.be.false;
|
|
17
|
+
expect(el.classList.contains('zn-br')).to.be.false;
|
|
18
|
+
expect(el.classList.contains('zn-border')).to.be.false;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('border="tb" applies zn-bt and zn-bb', async () => {
|
|
22
|
+
const el = await fixture<HTMLElement>(html`<zn-style border="tb"></zn-style>`);
|
|
23
|
+
expect(el.classList.contains('zn-bt')).to.be.true;
|
|
24
|
+
expect(el.classList.contains('zn-bb')).to.be.true;
|
|
25
|
+
expect(el.classList.contains('zn-bl')).to.be.false;
|
|
26
|
+
expect(el.classList.contains('zn-br')).to.be.false;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('border (attribute present but empty) applies zn-border (all four sides)', async () => {
|
|
30
|
+
const el = await fixture<HTMLElement>(html`<zn-style border></zn-style>`);
|
|
31
|
+
expect(el.classList.contains('zn-border')).to.be.true;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('no border attribute applies no border classes', async () => {
|
|
35
|
+
const el = await fixture<HTMLElement>(html`<zn-style></zn-style>`);
|
|
36
|
+
expect(el.classList.contains('zn-border')).to.be.false;
|
|
37
|
+
expect(el.classList.contains('zn-bt')).to.be.false;
|
|
38
|
+
expect(el.classList.contains('zn-bb')).to.be.false;
|
|
39
|
+
expect(el.classList.contains('zn-bl')).to.be.false;
|
|
40
|
+
expect(el.classList.contains('zn-br')).to.be.false;
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('size', () => {
|
|
45
|
+
it('size="s" applies zn-size-s', async () => {
|
|
46
|
+
const el = await fixture<HTMLElement>(html`<zn-style size="s"></zn-style>`);
|
|
47
|
+
expect(el.classList.contains('zn-size-s')).to.be.true;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('size="xs" applies zn-size-xs', async () => {
|
|
51
|
+
const el = await fixture<HTMLElement>(html`<zn-style size="xs"></zn-style>`);
|
|
52
|
+
expect(el.classList.contains('zn-size-xs')).to.be.true;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('size="xl" applies zn-size-xl', async () => {
|
|
56
|
+
const el = await fixture<HTMLElement>(html`<zn-style size="xl"></zn-style>`);
|
|
57
|
+
expect(el.classList.contains('zn-size-xl')).to.be.true;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('size="m" applies no size class', async () => {
|
|
61
|
+
const el = await fixture<HTMLElement>(html`<zn-style size="m"></zn-style>`);
|
|
62
|
+
expect(el.classList.contains('zn-size-m')).to.be.false;
|
|
63
|
+
expect(el.classList.contains('zn-size-xs')).to.be.false;
|
|
64
|
+
expect(el.classList.contains('zn-size-s')).to.be.false;
|
|
65
|
+
expect(el.classList.contains('zn-size-l')).to.be.false;
|
|
66
|
+
expect(el.classList.contains('zn-size-xl')).to.be.false;
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('muted', () => {
|
|
71
|
+
it('applies zn-muted class when present', async () => {
|
|
72
|
+
const el = await fixture<HTMLElement>(html`<zn-style muted></zn-style>`);
|
|
73
|
+
expect(el.classList.contains('zn-muted')).to.be.true;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('does not apply zn-muted when absent', async () => {
|
|
77
|
+
const el = await fixture<HTMLElement>(html`<zn-style></zn-style>`);
|
|
78
|
+
expect(el.classList.contains('zn-muted')).to.be.false;
|
|
79
|
+
});
|
|
80
|
+
});
|
|
10
81
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {expect, fixture, html} from '@open-wc/testing';
|
|
2
|
+
import {deepQuery} from './query';
|
|
3
|
+
|
|
4
|
+
describe('deepQuery', () => {
|
|
5
|
+
it('returns null when no element matches', async () => {
|
|
6
|
+
await fixture(html`<div></div>`);
|
|
7
|
+
expect(deepQuery('.does-not-exist')).to.be.null;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('finds an element in the light DOM of document', async () => {
|
|
11
|
+
const el = await fixture<HTMLElement>(html`<div class="needle"></div>`);
|
|
12
|
+
expect(deepQuery('.needle')).to.equal(el);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('finds an element nested inside a shadow root', async () => {
|
|
16
|
+
const host = await fixture<HTMLElement>(html`<div></div>`);
|
|
17
|
+
const shadow = host.attachShadow({mode: 'open'});
|
|
18
|
+
const needle = document.createElement('span');
|
|
19
|
+
needle.className = 'needle';
|
|
20
|
+
shadow.appendChild(needle);
|
|
21
|
+
|
|
22
|
+
expect(deepQuery('.needle')).to.equal(needle);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('finds an element across two nested shadow roots', async () => {
|
|
26
|
+
const outer = await fixture<HTMLElement>(html`<div></div>`);
|
|
27
|
+
const outerShadow = outer.attachShadow({mode: 'open'});
|
|
28
|
+
|
|
29
|
+
const inner = document.createElement('div');
|
|
30
|
+
outerShadow.appendChild(inner);
|
|
31
|
+
const innerShadow = inner.attachShadow({mode: 'open'});
|
|
32
|
+
|
|
33
|
+
const needle = document.createElement('span');
|
|
34
|
+
needle.className = 'needle';
|
|
35
|
+
innerShadow.appendChild(needle);
|
|
36
|
+
|
|
37
|
+
expect(deepQuery('.needle')).to.equal(needle);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns the first match in document order', async () => {
|
|
41
|
+
const root = await fixture<HTMLElement>(html`
|
|
42
|
+
<div>
|
|
43
|
+
<span class="needle" data-which="light"></span>
|
|
44
|
+
</div>
|
|
45
|
+
`);
|
|
46
|
+
const shadow = root.attachShadow({mode: 'open'});
|
|
47
|
+
const inShadow = document.createElement('span');
|
|
48
|
+
inShadow.className = 'needle';
|
|
49
|
+
inShadow.setAttribute('data-which', 'shadow');
|
|
50
|
+
shadow.appendChild(inShadow);
|
|
51
|
+
|
|
52
|
+
const hit = deepQuery<HTMLElement>('.needle');
|
|
53
|
+
expect(hit?.getAttribute('data-which')).to.equal('light');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('accepts a custom root', async () => {
|
|
57
|
+
const a = await fixture<HTMLElement>(html`<div><span class="needle"></span></div>`);
|
|
58
|
+
const b = await fixture<HTMLElement>(html`<div></div>`);
|
|
59
|
+
|
|
60
|
+
expect(deepQuery('.needle', a)).to.exist;
|
|
61
|
+
expect(deepQuery('.needle', b)).to.be.null;
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/utilities/query.ts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
export function deepQuery<T extends Element = Element>(
|
|
2
|
+
selector: string,
|
|
3
|
+
root: Document | ShadowRoot | Element = document,
|
|
4
|
+
): T | null {
|
|
5
|
+
const direct = root.querySelector<T>(selector);
|
|
6
|
+
if (direct) return direct;
|
|
7
|
+
for (const el of root.querySelectorAll<Element>('*')) {
|
|
8
|
+
if (el.shadowRoot) {
|
|
9
|
+
const hit = deepQuery<T>(selector, el.shadowRoot);
|
|
10
|
+
if (hit) return hit;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function deepQueryAll<T extends Element = Element>(
|
|
17
|
+
selector: string,
|
|
18
|
+
root: Document | ShadowRoot | Element = document,
|
|
19
|
+
out: T[] = [],
|
|
20
|
+
): T[] {
|
|
21
|
+
out.push(...root.querySelectorAll<T>(selector));
|
|
22
|
+
for (const el of root.querySelectorAll<Element>('*')) {
|
|
23
|
+
if (el.shadowRoot) deepQueryAll<T>(selector, el.shadowRoot, out);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
1
28
|
export function deepQuerySelectorAll(selector: string, element: Element, stopSelector: string): Element[] {
|
|
2
29
|
const arr: Element[] = [];
|
|
3
30
|
|