@kubex/zinc 1.1.134 → 1.1.135
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 +1065 -1008
- package/dist/vscode.html-custom-data.json +175 -160
- package/dist/web-types.json +384 -354
- package/dist/zn.d.ts +25 -1
- package/dist/zn.min.js +228 -227
- package/package.json +1 -1
- package/src/components/data-select/data-select.component.ts +7 -0
- package/src/components/inline-edit/inline-edit.component.ts +6 -0
- package/src/components/select/select.component.ts +16 -2
- package/src/components/select/select.test.ts +103 -0
- package/src/internal/conditional.ts +53 -35
package/package.json
CHANGED
|
@@ -137,6 +137,12 @@ export default class ZnDataSelect extends ZincElement implements ZincFormControl
|
|
|
137
137
|
|
|
138
138
|
@property() conditional = "";
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Ids or names of controls that must have a value before this one becomes
|
|
142
|
+
* usable, comma separated. Forwarded to the underlying select.
|
|
143
|
+
*/
|
|
144
|
+
@property() requires = "";
|
|
145
|
+
|
|
140
146
|
protected readonly formControlController = new FormControlController(this);
|
|
141
147
|
|
|
142
148
|
private readonly selectObserver = new MutationController(this, {
|
|
@@ -316,6 +322,7 @@ export default class ZnDataSelect extends ZincElement implements ZincFormControl
|
|
|
316
322
|
?disabled="${this.disabled}"
|
|
317
323
|
?select-first="${this.selectFirst}"
|
|
318
324
|
conditional="${this.conditional}"
|
|
325
|
+
requires="${this.requires}"
|
|
319
326
|
@zn-input="${this.handleInput}"
|
|
320
327
|
@zn-clear="${this.handleClear}"
|
|
321
328
|
@blur=${this.blur}
|
|
@@ -74,6 +74,12 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
74
74
|
|
|
75
75
|
@property() conditional = '';
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Ids or names of controls that must have a value before this one becomes
|
|
79
|
+
* usable, comma separated.
|
|
80
|
+
*/
|
|
81
|
+
@property() requires = '';
|
|
82
|
+
|
|
77
83
|
@property({ type: Boolean }) disabled: boolean
|
|
78
84
|
|
|
79
85
|
@property({ type: Boolean }) inline: boolean
|
|
@@ -356,6 +356,13 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
356
356
|
|
|
357
357
|
@property() conditional = "";
|
|
358
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Ids or names of controls that must have a value before this one becomes
|
|
361
|
+
* usable, comma separated. The control stays disabled until every one of them
|
|
362
|
+
* is filled, and is disabled again if any is cleared.
|
|
363
|
+
*/
|
|
364
|
+
@property() requires = "";
|
|
365
|
+
|
|
359
366
|
/**
|
|
360
367
|
* The URL to fetch options from. When set, the component fetches JSON from this URL and renders the results as
|
|
361
368
|
* options. The expected format is an array of objects with `key` and `value` properties:
|
|
@@ -691,8 +698,15 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
691
698
|
const isExpandIcon = path.some(el => el instanceof Element && el.classList.contains('select__expand-icon'));
|
|
692
699
|
// if click is inside the prefix area (e.g. checkbox), don't toggle the select
|
|
693
700
|
const inPrefix = path.some(el => el instanceof Element && el.classList.contains('select__prefix'));
|
|
694
|
-
//
|
|
695
|
-
|
|
701
|
+
// A control that cannot be used never toggles, whatever was clicked — the
|
|
702
|
+
// expand icon included, or a disabled select still opens from its chevron.
|
|
703
|
+
if (this.disabled || this._fetchLoading || this._fetchError) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
// Icon clicks (e.g. a tag's remove button) don't toggle, but the expand
|
|
707
|
+
// icon is what opens the control; the prefix area (e.g. a checkbox) never
|
|
708
|
+
// toggles.
|
|
709
|
+
if ((isIcon && !isExpandIcon) || inPrefix) {
|
|
696
710
|
return;
|
|
697
711
|
}
|
|
698
712
|
|
|
@@ -865,3 +865,106 @@ describe('<zn-select>', () => {
|
|
|
865
865
|
expect(popoverOf(second).matches(':popover-open')).to.be.true;
|
|
866
866
|
});
|
|
867
867
|
});
|
|
868
|
+
|
|
869
|
+
describe('<zn-select> disabled', () => {
|
|
870
|
+
it('should not open when the expand icon is clicked', async () => {
|
|
871
|
+
const el: any = await fixture(html`
|
|
872
|
+
<zn-select disabled>
|
|
873
|
+
<zn-option value="a">A</zn-option>
|
|
874
|
+
</zn-select>`);
|
|
875
|
+
await el.updateComplete;
|
|
876
|
+
|
|
877
|
+
el.shadowRoot.querySelector('.select__expand-icon')
|
|
878
|
+
.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, composed: true}));
|
|
879
|
+
await el.updateComplete;
|
|
880
|
+
|
|
881
|
+
expect(el.open).to.be.false;
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it('should open from the expand icon when enabled', async () => {
|
|
885
|
+
const el: any = await fixture(html`
|
|
886
|
+
<zn-select>
|
|
887
|
+
<zn-option value="a">A</zn-option>
|
|
888
|
+
</zn-select>`);
|
|
889
|
+
await el.updateComplete;
|
|
890
|
+
|
|
891
|
+
el.shadowRoot.querySelector('.select__expand-icon')
|
|
892
|
+
.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, composed: true}));
|
|
893
|
+
await el.updateComplete;
|
|
894
|
+
|
|
895
|
+
expect(el.open).to.be.true;
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
describe('<zn-select> requires', () => {
|
|
900
|
+
const pair = () => fixture(html`
|
|
901
|
+
<div>
|
|
902
|
+
<zn-select id="parent" name="parent">
|
|
903
|
+
<zn-option value="p1">P1</zn-option>
|
|
904
|
+
</zn-select>
|
|
905
|
+
<zn-select id="child" name="child" requires="parent" disabled>
|
|
906
|
+
<zn-option value="c1">C1</zn-option>
|
|
907
|
+
</zn-select>
|
|
908
|
+
</div>`);
|
|
909
|
+
|
|
910
|
+
it('should stay disabled while the required control is empty', async () => {
|
|
911
|
+
const root: any = await pair();
|
|
912
|
+
const child = root.querySelector('#child');
|
|
913
|
+
await child.updateComplete;
|
|
914
|
+
|
|
915
|
+
expect(child.disabled).to.be.true;
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it('should enable once the required control has a value', async () => {
|
|
919
|
+
const root: any = await pair();
|
|
920
|
+
const parent = root.querySelector('#parent');
|
|
921
|
+
const child = root.querySelector('#child');
|
|
922
|
+
await child.updateComplete;
|
|
923
|
+
|
|
924
|
+
parent.value = 'p1';
|
|
925
|
+
parent.dispatchEvent(new CustomEvent('zn-change', {bubbles: true, composed: true}));
|
|
926
|
+
await child.updateComplete;
|
|
927
|
+
|
|
928
|
+
expect(child.disabled).to.be.false;
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it('should disable again when the required control is cleared', async () => {
|
|
932
|
+
const root: any = await pair();
|
|
933
|
+
const parent = root.querySelector('#parent');
|
|
934
|
+
const child = root.querySelector('#child');
|
|
935
|
+
await child.updateComplete;
|
|
936
|
+
|
|
937
|
+
parent.value = 'p1';
|
|
938
|
+
parent.dispatchEvent(new CustomEvent('zn-change', {bubbles: true, composed: true}));
|
|
939
|
+
await child.updateComplete;
|
|
940
|
+
|
|
941
|
+
parent.value = '';
|
|
942
|
+
parent.dispatchEvent(new CustomEvent('zn-change', {bubbles: true, composed: true}));
|
|
943
|
+
await child.updateComplete;
|
|
944
|
+
|
|
945
|
+
expect(child.disabled).to.be.true;
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('should require every named control', async () => {
|
|
949
|
+
const root: any = await fixture(html`
|
|
950
|
+
<div>
|
|
951
|
+
<zn-select id="a" name="a"><zn-option value="1">1</zn-option></zn-select>
|
|
952
|
+
<zn-select id="b" name="b"><zn-option value="2">2</zn-option></zn-select>
|
|
953
|
+
<zn-select id="c" name="c" requires="a,b" disabled></zn-select>
|
|
954
|
+
</div>`);
|
|
955
|
+
const a = root.querySelector('#a');
|
|
956
|
+
const b = root.querySelector('#b');
|
|
957
|
+
const c = root.querySelector('#c');
|
|
958
|
+
await c.updateComplete;
|
|
959
|
+
|
|
960
|
+
a.value = '1';
|
|
961
|
+
a.dispatchEvent(new CustomEvent('zn-change', {bubbles: true, composed: true}));
|
|
962
|
+
await c.updateComplete;
|
|
963
|
+
expect(c.disabled, 'one of two filled').to.be.true;
|
|
964
|
+
|
|
965
|
+
b.value = '2';
|
|
966
|
+
b.dispatchEvent(new CustomEvent('zn-change', {bubbles: true, composed: true}));
|
|
967
|
+
await c.updateComplete;
|
|
968
|
+
expect(c.disabled, 'both filled').to.be.false;
|
|
969
|
+
});
|
|
970
|
+
});
|
|
@@ -3,13 +3,26 @@ import type {ReactiveController, ReactiveControllerHost} from 'lit';
|
|
|
3
3
|
|
|
4
4
|
export interface ConditionalHost {
|
|
5
5
|
conditional: string;
|
|
6
|
+
requires: string;
|
|
6
7
|
disabled: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
type ValuedElement = Element & { value: string | string[] };
|
|
11
|
+
|
|
12
|
+
function hasValue(el: ValuedElement): boolean {
|
|
13
|
+
const values = Array.isArray(el.value) ? el.value : [el.value];
|
|
14
|
+
return values.some(v => v !== '' && v !== '0');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A reactive controller that drives the host's disabled state from other form
|
|
19
|
+
* controls: `conditional` disables the host once a linked control has a value,
|
|
20
|
+
* `requires` keeps it disabled until every named control has one.
|
|
21
|
+
*/
|
|
10
22
|
export class ConditionalController implements ReactiveController {
|
|
11
23
|
host: ReactiveControllerHost & Element & ConditionalHost;
|
|
12
|
-
private conditionals:
|
|
24
|
+
private conditionals: ValuedElement[] = [];
|
|
25
|
+
private required: ValuedElement[] = [];
|
|
13
26
|
private initiallyDisabled = false;
|
|
14
27
|
private readonly handleChange = () => this.checkConditionals();
|
|
15
28
|
|
|
@@ -19,43 +32,48 @@ export class ConditionalController implements ReactiveController {
|
|
|
19
32
|
|
|
20
33
|
/** Call from the host's `firstUpdated()` to parse IDs, find elements, attach listeners, and run the initial check. */
|
|
21
34
|
setup() {
|
|
22
|
-
|
|
35
|
+
this.conditionals = this.resolve(this.host.conditional);
|
|
36
|
+
this.required = this.resolve(this.host.requires);
|
|
37
|
+
|
|
38
|
+
this.initiallyDisabled = this.host.disabled;
|
|
39
|
+
|
|
40
|
+
const watched = [...this.conditionals, ...this.required];
|
|
41
|
+
if (watched.length === 0) {
|
|
23
42
|
return;
|
|
24
43
|
}
|
|
25
44
|
|
|
26
|
-
|
|
45
|
+
watched.forEach(el => {
|
|
46
|
+
el.addEventListener('zn-change', this.handleChange);
|
|
47
|
+
el.addEventListener('zn-input', this.handleChange);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// trigger the check once to initialize
|
|
51
|
+
this.checkConditionals();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Resolve a comma-separated list of ids or names to the controls they name. */
|
|
55
|
+
private resolve(list: string): ValuedElement[] {
|
|
56
|
+
if (!list) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
27
59
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
})[];
|
|
60
|
+
const found: ValuedElement[] = [];
|
|
61
|
+
const seen = new Set<Element>();
|
|
62
|
+
|
|
63
|
+
list.split(',').map(id => id.trim()).filter(Boolean).forEach(id => {
|
|
64
|
+
const byId = deepQuerySelectorAll(idSelector(id), document.documentElement, '') as ValuedElement[];
|
|
65
|
+
const byName = deepQuerySelectorAll(`[name="${id}"]`, document.documentElement, '') as ValuedElement[];
|
|
35
66
|
|
|
36
67
|
// de-duplicate in case the same element matches both id and name
|
|
37
|
-
const seen = new Set<Element>();
|
|
38
68
|
[...byId, ...byName].forEach(el => {
|
|
39
69
|
if (!seen.has(el)) {
|
|
40
70
|
seen.add(el);
|
|
41
|
-
|
|
71
|
+
found.push(el);
|
|
42
72
|
}
|
|
43
73
|
});
|
|
44
74
|
});
|
|
45
75
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (ids.length === 0) {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
this.conditionals.forEach(select => {
|
|
53
|
-
select.addEventListener('zn-change', this.handleChange);
|
|
54
|
-
select.addEventListener('zn-input', this.handleChange);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// trigger the check once to initialize
|
|
58
|
-
this.checkConditionals();
|
|
76
|
+
return found;
|
|
59
77
|
}
|
|
60
78
|
|
|
61
79
|
hostConnected() {
|
|
@@ -63,12 +81,13 @@ export class ConditionalController implements ReactiveController {
|
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
hostDisconnected() {
|
|
66
|
-
this.conditionals.forEach(
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
[...this.conditionals, ...this.required].forEach(el => {
|
|
85
|
+
el.removeEventListener('zn-change', this.handleChange);
|
|
86
|
+
el.removeEventListener('zn-input', this.handleChange);
|
|
69
87
|
});
|
|
70
88
|
|
|
71
89
|
this.conditionals = [];
|
|
90
|
+
this.required = [];
|
|
72
91
|
}
|
|
73
92
|
|
|
74
93
|
/** Re-evaluate the conditional state. Call from the host when needed. */
|
|
@@ -77,12 +96,11 @@ export class ConditionalController implements ReactiveController {
|
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
private checkConditionals() {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
});
|
|
99
|
+
const blocked = this.conditionals.some(hasValue);
|
|
100
|
+
const unmet = this.required.length > 0 && !this.required.every(hasValue);
|
|
101
|
+
|
|
102
|
+
const latched = this.required.length > 0 ? false : this.initiallyDisabled;
|
|
85
103
|
|
|
86
|
-
this.host.disabled =
|
|
104
|
+
this.host.disabled = latched || blocked || unmet;
|
|
87
105
|
}
|
|
88
106
|
}
|