@kubex/zinc 2.0.3 → 2.0.4
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 +621 -428
- package/dist/vscode.html-custom-data.json +57 -42
- package/dist/web-types.json +140 -118
- package/dist/zn.d.ts +42 -20
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +337 -327
- package/docs/pages/components/form-actions.md +11 -0
- package/package.json +1 -1
- package/scss/_global-spacing.scss +7 -0
- package/scss/_root.scss +1 -5
- package/src/components/form-actions/form-actions.component.ts +112 -14
- package/src/components/form-actions/form-actions.scss +37 -1
- package/src/components/form-actions/form-actions.test.ts +137 -0
- package/src/components/input/input.scss +28 -29
- package/src/components/key-value/key-value.component.ts +123 -12
- package/src/components/key-value/key-value.scss +13 -0
- package/src/components/key-value/key-value.test.ts +65 -2
- package/src/components/select/select.test.ts +24 -0
- package/src/components/table/table.component.ts +5 -2
- package/src/components/table/table.scss +4 -0
- package/src/components/table/table.test.ts +13 -0
- package/src/internal/conditional.ts +9 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {classMap} from 'lit/directives/class-map.js';
|
|
2
2
|
import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
|
|
3
|
+
import {defaultValue} from '../../internal/default-value';
|
|
3
4
|
import {FormControlController} from '../../internal/form';
|
|
4
5
|
import {HasSlotController} from '../../internal/slot';
|
|
5
6
|
import {property, state} from 'lit/decorators.js';
|
|
6
7
|
import ZincElement, {type ZincFormControl} from '../../internal/zinc-element';
|
|
8
|
+
import type ZnInput from '../input';
|
|
7
9
|
|
|
8
10
|
import formControlStyles from '../../form-control.scss';
|
|
9
11
|
import styles from './key-value.scss';
|
|
@@ -13,6 +15,8 @@ interface KeyValueRow {
|
|
|
13
15
|
value: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
const EASE_OUT = 'cubic-bezier(0.23, 1, 0.32, 1)';
|
|
19
|
+
|
|
16
20
|
/**
|
|
17
21
|
* @summary Edits a list of name and value pairs, submitted as a JSON object.
|
|
18
22
|
* @documentation https://zinc.style/components/key-value
|
|
@@ -20,6 +24,8 @@ interface KeyValueRow {
|
|
|
20
24
|
* @since 1.0
|
|
21
25
|
*
|
|
22
26
|
* @dependency zn-input
|
|
27
|
+
* @dependency zn-select
|
|
28
|
+
* @dependency zn-option
|
|
23
29
|
* @dependency zn-button
|
|
24
30
|
*
|
|
25
31
|
* @event zn-change - Emitted when a pair is added, removed or edited.
|
|
@@ -41,9 +47,15 @@ export default class ZnKeyValue extends ZincElement implements ZincFormControl {
|
|
|
41
47
|
|
|
42
48
|
@property() name = '';
|
|
43
49
|
|
|
44
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* The pairs as a JSON object, such as `{"1": "{{recipient.firstName}}"}`. With `list`, the names as a JSON array,
|
|
52
|
+
* such as `["10.0.0.0/24"]`.
|
|
53
|
+
*/
|
|
45
54
|
@property() value = '{}';
|
|
46
55
|
|
|
56
|
+
/** The value a form reset restores, taken from the initial `value` attribute. */
|
|
57
|
+
@defaultValue() defaultValue = '{}';
|
|
58
|
+
|
|
47
59
|
@property() label = '';
|
|
48
60
|
|
|
49
61
|
@property({attribute: 'help-text'}) helpText = '';
|
|
@@ -57,6 +69,15 @@ export default class ZnKeyValue extends ZincElement implements ZincFormControl {
|
|
|
57
69
|
/** Passed to each value input, so its slash menu can insert merge fields. */
|
|
58
70
|
@property({attribute: 'slash-items'}) slashItems = '';
|
|
59
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Makes each value a choice instead of free text: a JSON object of option values to labels, such as
|
|
74
|
+
* `{"default": "System default", "auto": "Auto approve"}`. New rows take the first option.
|
|
75
|
+
*/
|
|
76
|
+
@property({attribute: 'value-options', type: Object}) valueOptions: Record<string, string> | null = null;
|
|
77
|
+
|
|
78
|
+
/** Edits a plain list of names with no values, submitted as a JSON array. */
|
|
79
|
+
@property({type: Boolean, reflect: true}) list = false;
|
|
80
|
+
|
|
60
81
|
@property({reflect: true}) form = '';
|
|
61
82
|
|
|
62
83
|
@property({type: Boolean, reflect: true}) disabled = false;
|
|
@@ -87,15 +108,35 @@ export default class ZnKeyValue extends ZincElement implements ZincFormControl {
|
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
protected willUpdate(changed: PropertyValues) {
|
|
111
|
+
if (this.list && this.value === '{}') {
|
|
112
|
+
this.value = '[]';
|
|
113
|
+
}
|
|
90
114
|
if (changed.has('value') && this.value !== this._serialise()) {
|
|
91
115
|
this._rows = this._parse(this.value);
|
|
92
116
|
}
|
|
117
|
+
this._rows = this._withSpareRow(this._rows);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private get _defaultValue(): string {
|
|
121
|
+
return this.valueOptions ? Object.keys(this.valueOptions)[0] ?? '' : '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private _emptyRow(): KeyValueRow {
|
|
125
|
+
return {key: '', value: this._defaultValue};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// There's always a row to type into, so an empty list never leaves the add button on its own
|
|
129
|
+
private _withSpareRow(rows: KeyValueRow[]): KeyValueRow[] {
|
|
130
|
+
return rows.length ? rows : [this._emptyRow()];
|
|
93
131
|
}
|
|
94
132
|
|
|
95
133
|
private _parse(value: string): KeyValueRow[] {
|
|
96
134
|
try {
|
|
97
|
-
const parsed: unknown = JSON.parse(value || '{}');
|
|
98
|
-
if (
|
|
135
|
+
const parsed: unknown = JSON.parse(value || (this.list ? '[]' : '{}'));
|
|
136
|
+
if (Array.isArray(parsed)) {
|
|
137
|
+
return parsed.map(key => ({key: String(key ?? ''), value: this._defaultValue}));
|
|
138
|
+
}
|
|
139
|
+
if (parsed && typeof parsed === 'object') {
|
|
99
140
|
return Object.entries(parsed as Record<string, unknown>).map(([key, v]) => ({key, value: String(v ?? '')}));
|
|
100
141
|
}
|
|
101
142
|
} catch {
|
|
@@ -105,8 +146,12 @@ export default class ZnKeyValue extends ZincElement implements ZincFormControl {
|
|
|
105
146
|
}
|
|
106
147
|
|
|
107
148
|
private _serialise(): string {
|
|
149
|
+
const named = this._rows.filter(row => row.key.trim());
|
|
150
|
+
if (this.list) {
|
|
151
|
+
return JSON.stringify(named.map(row => row.key.trim()));
|
|
152
|
+
}
|
|
108
153
|
const pairs: Record<string, string> = {};
|
|
109
|
-
|
|
154
|
+
named.forEach(row => pairs[row.key.trim()] = row.value);
|
|
110
155
|
return JSON.stringify(pairs);
|
|
111
156
|
}
|
|
112
157
|
|
|
@@ -122,9 +167,70 @@ export default class ZnKeyValue extends ZincElement implements ZincFormControl {
|
|
|
122
167
|
this._update(rows);
|
|
123
168
|
}
|
|
124
169
|
|
|
170
|
+
private get _reduceMotion(): boolean {
|
|
171
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private _easing(): string {
|
|
175
|
+
return getComputedStyle(this).getPropertyValue('--zn-ease-out').trim() || EASE_OUT;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async _add() {
|
|
179
|
+
this._update([...this._rows, this._emptyRow()]);
|
|
180
|
+
await this.updateComplete;
|
|
181
|
+
const rows = this.shadowRoot!.querySelectorAll<HTMLElement>('.key-value__row');
|
|
182
|
+
const row = rows[rows.length - 1];
|
|
183
|
+
if (!row) return;
|
|
184
|
+
// Opacity and transform only: the new inputs render for the first time during these frames
|
|
185
|
+
row.animate(
|
|
186
|
+
this._reduceMotion
|
|
187
|
+
? [{opacity: 0}, {opacity: 1}]
|
|
188
|
+
: [{opacity: 0, transform: 'translateY(-4px)'}, {opacity: 1, transform: 'translateY(0)'}],
|
|
189
|
+
{duration: this._reduceMotion ? 150 : 180, easing: this._easing()});
|
|
190
|
+
const input = row.querySelector<ZnInput>('.key-value__key');
|
|
191
|
+
await input?.updateComplete;
|
|
192
|
+
input?.focus();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async _remove(row: KeyValueRow, rowElement: HTMLElement) {
|
|
196
|
+
if (this._rows.length === 1) {
|
|
197
|
+
this._update([this._emptyRow()]);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
rowElement.style.pointerEvents = 'none';
|
|
201
|
+
rowElement.style.overflow = 'hidden';
|
|
202
|
+
const height = rowElement.getBoundingClientRect().height;
|
|
203
|
+
await rowElement.animate(
|
|
204
|
+
this._reduceMotion
|
|
205
|
+
? [{opacity: 1}, {opacity: 0}]
|
|
206
|
+
: [{opacity: 1, transform: 'scale(1)', height: `${height}px`}, {opacity: 0, transform: 'scale(0.98)', height: '0px'}],
|
|
207
|
+
{duration: this._reduceMotion ? 150 : 220, easing: this._easing()}).finished.catch(() => undefined);
|
|
208
|
+
rowElement.style.pointerEvents = '';
|
|
209
|
+
rowElement.style.overflow = '';
|
|
210
|
+
this._update(this._rows.filter(r => r !== row));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private _renderValue(row: KeyValueRow, index: number) {
|
|
214
|
+
if (this.list) return null;
|
|
215
|
+
if (this.valueOptions) {
|
|
216
|
+
return html`
|
|
217
|
+
<zn-select class="key-value__value" .value=${row.value} ?disabled=${this.disabled} aria-label=${this.valueLabel}
|
|
218
|
+
@zn-change=${(e: Event) => this._edit(index, 'value', e)}>
|
|
219
|
+
${Object.entries(this.valueOptions).map(([value, label]) => html`
|
|
220
|
+
<zn-option value=${value}>${label}</zn-option>`)}
|
|
221
|
+
</zn-select>`;
|
|
222
|
+
}
|
|
223
|
+
return html`
|
|
224
|
+
<zn-input class="key-value__value" placeholder=${this.valueLabel} .value=${row.value}
|
|
225
|
+
slash-items=${this.slashItems} ?disabled=${this.disabled}
|
|
226
|
+
@zn-input=${(e: Event) => this._edit(index, 'value', e)}></zn-input>`;
|
|
227
|
+
}
|
|
228
|
+
|
|
125
229
|
render() {
|
|
126
230
|
const hasLabel = this.label || this.hasSlotController.test('label');
|
|
127
231
|
const hasHelpText = this.helpText || this.hasSlotController.test('help-text');
|
|
232
|
+
// A lone empty row has nothing to remove; hidden rather than dropped so its inputs don't change width
|
|
233
|
+
const lonelyEmpty = this._rows.length === 1 && !this._rows[0].key.trim();
|
|
128
234
|
|
|
129
235
|
return html`
|
|
130
236
|
<div part="base" class=${classMap({
|
|
@@ -137,19 +243,24 @@ export default class ZnKeyValue extends ZincElement implements ZincFormControl {
|
|
|
137
243
|
<slot name="label">${this.label}</slot>
|
|
138
244
|
</label>
|
|
139
245
|
|
|
140
|
-
<div class
|
|
246
|
+
<div class=${classMap({
|
|
247
|
+
'key-value__rows': true,
|
|
248
|
+
'key-value__rows--list': this.list,
|
|
249
|
+
'key-value__rows--options': !!this.valueOptions && !this.list,
|
|
250
|
+
})}>
|
|
141
251
|
${this._rows.map((row, index) => html`
|
|
142
252
|
<div part="row" class="key-value__row">
|
|
143
253
|
<zn-input class="key-value__key" placeholder=${this.keyLabel} .value=${row.key}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
254
|
+
aria-label=${this.keyLabel} ?disabled=${this.disabled}
|
|
255
|
+
@zn-input=${(e: Event) => this._edit(index, 'key', e)}></zn-input>
|
|
256
|
+
${this._renderValue(row, index)}
|
|
257
|
+
<zn-button class=${classMap({'key-value__remove': true, 'key-value__remove--hidden': lonelyEmpty})}
|
|
258
|
+
icon="x@lu" color="transparent" size="small" title="Remove" icon-button="small" plain
|
|
259
|
+
?disabled=${this.disabled}
|
|
260
|
+
@click=${(e: Event) => this._remove(row, (e.currentTarget as HTMLElement).closest('.key-value__row')!)}></zn-button>
|
|
150
261
|
</div>`)}
|
|
151
262
|
<zn-button class="key-value__add" icon="plus@lu" color="secondary" size="small" ?disabled=${this.disabled}
|
|
152
|
-
@click=${() => this.
|
|
263
|
+
@click=${() => this._add()}>${this.addText}</zn-button>
|
|
153
264
|
</div>
|
|
154
265
|
|
|
155
266
|
<div class="form-control__help-text" aria-hidden=${hasHelpText ? 'false' : 'true'}>
|
|
@@ -18,3 +18,16 @@
|
|
|
18
18
|
gap: var(--zn-spacing-x-small);
|
|
19
19
|
width: 100%;
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
.key-value__rows--list .key-value__row {
|
|
23
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.key-value__rows--options .key-value__row {
|
|
27
|
+
grid-template-columns: minmax(0, 1fr) var(--zn-key-value-option-width, 13rem) auto;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Keeps its space so the inputs don't change width when it appears
|
|
31
|
+
.key-value__remove--hidden {
|
|
32
|
+
visibility: hidden;
|
|
33
|
+
}
|
|
@@ -2,11 +2,15 @@ import '../../../dist/zn.min.js';
|
|
|
2
2
|
import {expect, fixture, html} from '@open-wc/testing';
|
|
3
3
|
import type ZnKeyValue from './key-value.component';
|
|
4
4
|
|
|
5
|
+
const rows = (el: ZnKeyValue) => el.shadowRoot!.querySelectorAll<HTMLElement>('.key-value__row');
|
|
6
|
+
// zn-button.click() skips the click event that the row buttons listen for, so press them as a user would
|
|
7
|
+
const press = (button: Element) => button.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true}));
|
|
8
|
+
|
|
5
9
|
describe('<zn-key-value>', () => {
|
|
6
10
|
it('should show a row per pair in its value', async () => {
|
|
7
11
|
const el = await fixture<ZnKeyValue>(html`<zn-key-value value='{"1":"{{recipient.firstName}}","2":"x"}'></zn-key-value>`);
|
|
8
12
|
|
|
9
|
-
expect(el
|
|
13
|
+
expect(rows(el).length).to.equal(2);
|
|
10
14
|
});
|
|
11
15
|
|
|
12
16
|
it('should submit the pairs as a JSON object, leaving out unnamed rows', async () => {
|
|
@@ -14,9 +18,68 @@ describe('<zn-key-value>', () => {
|
|
|
14
18
|
<form><zn-key-value name="pairs" value='{"1":"a"}'></zn-key-value></form>`);
|
|
15
19
|
const el = form.querySelector<ZnKeyValue>('zn-key-value')!;
|
|
16
20
|
|
|
17
|
-
el.shadowRoot!.querySelector
|
|
21
|
+
press(el.shadowRoot!.querySelector('.key-value__add')!);
|
|
18
22
|
await el.updateComplete;
|
|
19
23
|
|
|
20
24
|
expect(new FormData(form).get('pairs')).to.equal('{"1":"a"}');
|
|
21
25
|
});
|
|
26
|
+
|
|
27
|
+
it('keeps one empty row to type into, with nothing to remove', async () => {
|
|
28
|
+
const el = await fixture<ZnKeyValue>(html`<zn-key-value></zn-key-value>`);
|
|
29
|
+
|
|
30
|
+
expect(rows(el).length).to.equal(1);
|
|
31
|
+
expect(rows(el)[0].querySelector('.key-value__remove--hidden')).to.exist;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('clears the last row instead of removing it', async () => {
|
|
35
|
+
const el = await fixture<ZnKeyValue>(html`<zn-key-value value='{"a":"1"}'></zn-key-value>`);
|
|
36
|
+
|
|
37
|
+
press(rows(el)[0].querySelector('.key-value__remove')!);
|
|
38
|
+
await el.updateComplete;
|
|
39
|
+
|
|
40
|
+
expect(rows(el).length).to.equal(1);
|
|
41
|
+
expect(el.value).to.equal('{}');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('submits a list as a JSON array of names', async () => {
|
|
45
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
46
|
+
<form><zn-key-value name="entries" list value='["10.0.0.1","10.0.0.0/24"]'></zn-key-value></form>`);
|
|
47
|
+
const el = form.querySelector<ZnKeyValue>('zn-key-value')!;
|
|
48
|
+
|
|
49
|
+
expect(rows(el).length).to.equal(2);
|
|
50
|
+
expect(rows(el)[0].querySelector('.key-value__value')).to.not.exist;
|
|
51
|
+
expect(new FormData(form).get('entries')).to.equal('["10.0.0.1","10.0.0.0/24"]');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('renders value options as a select, defaulting new rows to the first option', async () => {
|
|
55
|
+
const options = JSON.stringify({default: 'System default', auto: 'Auto approve'});
|
|
56
|
+
const el = await fixture<ZnKeyValue>(html`
|
|
57
|
+
<zn-key-value value-options="${options}" value='{"a.com":"auto"}'></zn-key-value>`);
|
|
58
|
+
|
|
59
|
+
expect(rows(el)[0].querySelector('zn-select.key-value__value')).to.exist;
|
|
60
|
+
|
|
61
|
+
press(el.shadowRoot!.querySelector('.key-value__add')!);
|
|
62
|
+
await el.updateComplete;
|
|
63
|
+
const key = rows(el)[1].querySelector<HTMLElement & {value: string}>('.key-value__key')!;
|
|
64
|
+
key.value = 'b.com';
|
|
65
|
+
key.dispatchEvent(new CustomEvent('zn-input', {bubbles: true, composed: true}));
|
|
66
|
+
await el.updateComplete;
|
|
67
|
+
|
|
68
|
+
expect(JSON.parse(el.value)).to.deep.equal({'a.com': 'auto', 'b.com': 'default'});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('restores its rows on form reset', async () => {
|
|
72
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
73
|
+
<form><zn-key-value name="pairs" value='{"a":"1"}'></zn-key-value></form>`);
|
|
74
|
+
const el = form.querySelector<ZnKeyValue>('zn-key-value')!;
|
|
75
|
+
|
|
76
|
+
press(rows(el)[0].querySelector('.key-value__remove')!);
|
|
77
|
+
await el.updateComplete;
|
|
78
|
+
expect(el.value).to.equal('{}');
|
|
79
|
+
|
|
80
|
+
form.reset();
|
|
81
|
+
await el.updateComplete;
|
|
82
|
+
expect(el.value).to.equal('{"a":"1"}');
|
|
83
|
+
expect(rows(el).length).to.equal(1);
|
|
84
|
+
});
|
|
22
85
|
});
|
|
@@ -989,6 +989,30 @@ describe('<zn-select> requires', () => {
|
|
|
989
989
|
await c.updateComplete;
|
|
990
990
|
expect(c.disabled, 'both filled').to.be.false;
|
|
991
991
|
});
|
|
992
|
+
|
|
993
|
+
it('re-enables a conditional select when the form resets', async () => {
|
|
994
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
995
|
+
<form>
|
|
996
|
+
<zn-select id="allowed" name="allowed" multiple value="" provider="conditional" conditional="blocked">
|
|
997
|
+
<zn-option value="a">A</zn-option>
|
|
998
|
+
</zn-select>
|
|
999
|
+
<zn-select id="blocked" name="blocked" multiple value="" provider="conditional" conditional="allowed">
|
|
1000
|
+
<zn-option value="a">A</zn-option>
|
|
1001
|
+
</zn-select>
|
|
1002
|
+
</form>
|
|
1003
|
+
`);
|
|
1004
|
+
const allowed = form.querySelector<ZnSelect>('#allowed')!;
|
|
1005
|
+
const blocked = form.querySelector<ZnSelect>('#blocked')!;
|
|
1006
|
+
|
|
1007
|
+
blocked.value = ['a'];
|
|
1008
|
+
blocked.dispatchEvent(new Event('zn-change'));
|
|
1009
|
+
expect(allowed.disabled).to.be.true;
|
|
1010
|
+
|
|
1011
|
+
form.reset();
|
|
1012
|
+
|
|
1013
|
+
await waitUntil(() => !allowed.disabled);
|
|
1014
|
+
expect(allowed.disabled).to.be.false;
|
|
1015
|
+
});
|
|
992
1016
|
});
|
|
993
1017
|
|
|
994
1018
|
describe('<zn-select> empty state', () => {
|
|
@@ -150,6 +150,7 @@ export default class ZnTable extends ZincElement {
|
|
|
150
150
|
tableBody() {
|
|
151
151
|
const rows: any = [];
|
|
152
152
|
this.hasActions = false;
|
|
153
|
+
const anyRowHasActions = this.rows.some((row: any) => Array.isArray(row?.actions) && row.actions.length > 0);
|
|
153
154
|
this.rows.forEach((row: any, _: any) => {
|
|
154
155
|
const rowHtml = [];
|
|
155
156
|
const basicData = !row.hasOwnProperty('caption') && !row.hasOwnProperty('data');
|
|
@@ -185,8 +186,10 @@ export default class ZnTable extends ZincElement {
|
|
|
185
186
|
iconHtml = html`
|
|
186
187
|
<zn-icon size="${iconSize}" src="${icon}"></zn-icon>`;
|
|
187
188
|
}
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
// An invisible menu icon keeps captions aligned with rows that do have a menu.
|
|
190
|
+
let actionsHtml = anyRowHasActions
|
|
191
|
+
? html`<div class="actions actions--placeholder" aria-hidden="true"><zn-icon src="more_vert"></zn-icon></div>`
|
|
192
|
+
: html`<div class="actions"></div>`;
|
|
190
193
|
if (actions && actions.length > 0) {
|
|
191
194
|
actionsHtml = html`
|
|
192
195
|
<div class="actions">
|
|
@@ -24,4 +24,17 @@ describe('<zn-table>', () => {
|
|
|
24
24
|
link.click();
|
|
25
25
|
expect(clicks).to.equal(2);
|
|
26
26
|
});
|
|
27
|
+
|
|
28
|
+
it('reserves the menu column on rows without actions when another row has them', async () => {
|
|
29
|
+
const data = {header: ['Name'], items: [
|
|
30
|
+
{caption: 'Me', data: []},
|
|
31
|
+
{caption: 'Them', actions: [{title: 'Edit', path: '/edit'}], data: []},
|
|
32
|
+
]};
|
|
33
|
+
const el = await fixture<HTMLElement>(html`<zn-table data="${JSON.stringify(data)}"></zn-table>`);
|
|
34
|
+
await (el as unknown as {updateComplete: Promise<unknown>}).updateComplete;
|
|
35
|
+
|
|
36
|
+
const cells = el.shadowRoot!.querySelectorAll<HTMLElement>('td.capcol .actions');
|
|
37
|
+
expect(cells[0].classList.contains('actions--placeholder')).to.be.true;
|
|
38
|
+
expect(cells[0].getBoundingClientRect().width).to.equal(cells[1].getBoundingClientRect().width);
|
|
39
|
+
});
|
|
27
40
|
});
|
|
@@ -24,7 +24,10 @@ export class ConditionalController implements ReactiveController {
|
|
|
24
24
|
private conditionals: ValuedElement[] = [];
|
|
25
25
|
private required: ValuedElement[] = [];
|
|
26
26
|
private initiallyDisabled = false;
|
|
27
|
+
private forms = new Set<HTMLFormElement>();
|
|
27
28
|
private readonly handleChange = () => this.checkConditionals();
|
|
29
|
+
// Controls restore their values in their own reset listeners, so check once they all have
|
|
30
|
+
private readonly handleReset = () => setTimeout(this.handleChange);
|
|
28
31
|
|
|
29
32
|
constructor(host: ReactiveControllerHost & Element & ConditionalHost) {
|
|
30
33
|
(this.host = host).addController(this);
|
|
@@ -45,7 +48,10 @@ export class ConditionalController implements ReactiveController {
|
|
|
45
48
|
watched.forEach(el => {
|
|
46
49
|
el.addEventListener('zn-change', this.handleChange);
|
|
47
50
|
el.addEventListener('zn-input', this.handleChange);
|
|
51
|
+
const form = el.closest('form');
|
|
52
|
+
if (form) this.forms.add(form);
|
|
48
53
|
});
|
|
54
|
+
this.forms.forEach(form => form.addEventListener('reset', this.handleReset));
|
|
49
55
|
|
|
50
56
|
// trigger the check once to initialize
|
|
51
57
|
this.checkConditionals();
|
|
@@ -86,8 +92,11 @@ export class ConditionalController implements ReactiveController {
|
|
|
86
92
|
el.removeEventListener('zn-input', this.handleChange);
|
|
87
93
|
});
|
|
88
94
|
|
|
95
|
+
this.forms.forEach(form => form.removeEventListener('reset', this.handleReset));
|
|
96
|
+
|
|
89
97
|
this.conditionals = [];
|
|
90
98
|
this.required = [];
|
|
99
|
+
this.forms.clear();
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
/** Re-evaluate the conditional state. Call from the host when needed. */
|