@kubex/zinc 2.0.2 → 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 +356 -115
- package/dist/vscode.html-custom-data.json +29 -14
- package/dist/web-types.json +67 -45
- package/dist/zn.d.ts +47 -21
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +333 -309
- 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/button/button.component.ts +2 -2
- package/src/components/button/button.test.ts +7 -0
- package/src/components/chart/chart.component.ts +1 -1
- package/src/components/editor/modules/dialog/dialog.test.ts +1 -1
- package/src/components/expanding-action/expanding-action.component.ts +1 -1
- package/src/components/filter-wrapper/filter-wrapper.component.ts +1 -1
- 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/inline-edit/inline-edit.component.ts +1 -2
- package/src/components/inline-edit/inline-edit.scss +20 -20
- package/src/components/input/input.component.ts +38 -2
- package/src/components/input/input.scss +96 -33
- package/src/components/input/input.test.ts +1 -1
- 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/nav-list/nav-list.test.ts +1 -1
- package/src/components/navbar/navbar.component.ts +14 -8
- package/src/components/panel/panel.component.ts +1 -1
- package/src/components/priority-list/priority-list.test.ts +1 -1
- package/src/components/select/select.test.ts +24 -0
- package/src/components/split-pane/split-pane.component.ts +1 -1
- 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/components/tabs/tabs.component.ts +2 -2
- package/src/internal/conditional.ts +9 -0
|
@@ -192,7 +192,7 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
192
192
|
/** Determines whether or no the password is currently visible. Only applies to password types **/
|
|
193
193
|
@property({attribute: 'password-visible', type: Boolean}) passwordVisible: boolean = false;
|
|
194
194
|
|
|
195
|
-
/** Hides the
|
|
195
|
+
/** Hides the increment/decrement buttons on number inputs **/
|
|
196
196
|
@property({attribute: 'no-spin-buttons', type: Boolean}) noSpinButtons: boolean = false;
|
|
197
197
|
|
|
198
198
|
/**
|
|
@@ -505,6 +505,27 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
/** Whether the value has reached `limit`; `direction` 1 checks a maximum, -1 a minimum. */
|
|
509
|
+
private isAtLimit(limit: number | string | undefined, direction: 1 | -1): boolean {
|
|
510
|
+
const value = parseFloat(String(this.value));
|
|
511
|
+
const bound = parseFloat(String(limit));
|
|
512
|
+
return !isNaN(value) && !isNaN(bound) && (value - bound) * direction >= 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private handleStep(direction: 'up' | 'down') {
|
|
516
|
+
const previous: unknown = this.value;
|
|
517
|
+
if (direction === 'up') {
|
|
518
|
+
this.stepUp();
|
|
519
|
+
} else {
|
|
520
|
+
this.stepDown();
|
|
521
|
+
}
|
|
522
|
+
if (this.value === previous) return;
|
|
523
|
+
// stepUp()/stepDown() don't fire events, unlike the native spinner they replace
|
|
524
|
+
this.formControlController.updateValidity();
|
|
525
|
+
this.emit('zn-input');
|
|
526
|
+
this.emit('zn-change');
|
|
527
|
+
}
|
|
528
|
+
|
|
508
529
|
private handlePasswordToggle() {
|
|
509
530
|
this.passwordVisible = !this.passwordVisible;
|
|
510
531
|
}
|
|
@@ -861,7 +882,6 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
861
882
|
'input--focused': this.hasFocus,
|
|
862
883
|
'input--empty': !this.value,
|
|
863
884
|
'input--range': isRange,
|
|
864
|
-
'input--no-spin-buttons': this.noSpinButtons && this.type !== 'currency',
|
|
865
885
|
'input--color': this.type === 'color',
|
|
866
886
|
'input--color-invalid': this.colorInvalid,
|
|
867
887
|
})}>
|
|
@@ -973,6 +993,22 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
973
993
|
</button>`
|
|
974
994
|
: ''}
|
|
975
995
|
|
|
996
|
+
${this.type === 'currency' || (this.type === 'number' && !this.noSpinButtons)
|
|
997
|
+
? html`
|
|
998
|
+
<span part="stepper" class="input__stepper">
|
|
999
|
+
<button class="input__stepper-button" type="button" tabindex="-1" aria-label="Decrement"
|
|
1000
|
+
?disabled=${this.disabled || this.readonly || this.isAtLimit(this.min, -1)}
|
|
1001
|
+
@click=${() => this.handleStep('down')}>
|
|
1002
|
+
<zn-icon src="minus@lu" size="16"></zn-icon>
|
|
1003
|
+
</button>
|
|
1004
|
+
<button class="input__stepper-button" type="button" tabindex="-1" aria-label="Increment"
|
|
1005
|
+
?disabled=${this.disabled || this.readonly || this.isAtLimit(this.max, 1)}
|
|
1006
|
+
@click=${() => this.handleStep('up')}>
|
|
1007
|
+
<zn-icon src="plus@lu" size="16"></zn-icon>
|
|
1008
|
+
</button>
|
|
1009
|
+
</span>`
|
|
1010
|
+
: ''}
|
|
1011
|
+
|
|
976
1012
|
<span part="suffix" class="input__suffix">
|
|
977
1013
|
<slot name="suffix"></slot>
|
|
978
1014
|
</span>
|
|
@@ -162,6 +162,29 @@ input[type='text'].input__control[data-color-input] {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// The same gap right of a suffix as above and below one 24px tall (an icon or copy button). Built-in controls such as
|
|
166
|
+
// the clear button and stepper keep their own spacing.
|
|
167
|
+
.input__suffix ::slotted(*) {
|
|
168
|
+
margin-inline-end: max(2px, calc((var(--input-height) - var(--zn-input-border-width) * 2 - 24px) / 2));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Slotted buttons are held to the small button height, shrunk to fit smaller inputs, with the same gap all round.
|
|
172
|
+
// Read from the density token: redefining --zn-button-height-small from itself would be a cycle.
|
|
173
|
+
.input__suffix ::slotted(zn-button) {
|
|
174
|
+
--input-suffix-button-height: min(var(--zn-density-control-height-sm, 28px), calc(var(--input-height) - var(--zn-input-border-width) * 2 - 4px));
|
|
175
|
+
--zn-button-height: var(--input-suffix-button-height);
|
|
176
|
+
--zn-button-height-small: var(--input-suffix-button-height);
|
|
177
|
+
margin-inline-end: calc((var(--input-height) - var(--zn-input-border-width) * 2 - var(--input-suffix-button-height)) / 2);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.input__suffix ::slotted(zn-checkbox) {
|
|
181
|
+
margin-inline-end: 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.input__suffix-default {
|
|
185
|
+
margin-inline-end: var(--zn-spacing-small);
|
|
186
|
+
}
|
|
187
|
+
|
|
165
188
|
.input__prefix ::slotted(zn-icon),
|
|
166
189
|
.input__suffix ::slotted(zn-icon) {
|
|
167
190
|
--icon-size: var(--zn-font-size-medium);
|
|
@@ -174,6 +197,7 @@ input[type='text'].input__control[data-color-input] {
|
|
|
174
197
|
*/
|
|
175
198
|
|
|
176
199
|
.input--x-small {
|
|
200
|
+
--input-height: var(--zn-input-height-x-small);
|
|
177
201
|
border-radius: var(--zn-input-border-radius-x-small);
|
|
178
202
|
font-size: var(--zn-input-font-size-small);
|
|
179
203
|
height: var(--zn-input-height-x-small);
|
|
@@ -194,20 +218,14 @@ input[type='text'].input__control[data-color-input] {
|
|
|
194
218
|
margin-inline-start: var(--zn-spacing-x-small);
|
|
195
219
|
}
|
|
196
220
|
|
|
197
|
-
.input--x-small .input__suffix ::slotted(*),
|
|
198
|
-
.input__suffix-default {
|
|
199
|
-
margin-inline-end: var(--zn-input-spacing-x-small);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
.input--x-small .input__suffix ::slotted(zn-button),
|
|
203
221
|
.input--x-small .input__prefix ::slotted(zn-button),
|
|
204
|
-
.input--x-small .
|
|
205
|
-
.input--x-small .input__prefix ::slotted(zn-checkbox), {
|
|
222
|
+
.input--x-small .input__prefix ::slotted(zn-checkbox) {
|
|
206
223
|
margin-inline-end: 0;
|
|
207
224
|
margin-inline-start: 0;
|
|
208
225
|
}
|
|
209
226
|
|
|
210
227
|
.input--small {
|
|
228
|
+
--input-height: var(--zn-input-height-small);
|
|
211
229
|
border-radius: var(--zn-input-border-radius-small);
|
|
212
230
|
font-size: var(--zn-input-font-size-small);
|
|
213
231
|
height: var(--zn-input-height-small);
|
|
@@ -228,20 +246,14 @@ input[type='text'].input__control[data-color-input] {
|
|
|
228
246
|
margin-inline-start: var(--zn-input-spacing-small);
|
|
229
247
|
}
|
|
230
248
|
|
|
231
|
-
.input--small .input__suffix ::slotted(*),
|
|
232
|
-
.input__suffix-default {
|
|
233
|
-
margin-inline-end: var(--zn-input-spacing-small);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
.input--small .input__suffix ::slotted(zn-button),
|
|
237
249
|
.input--small .input__prefix ::slotted(zn-button),
|
|
238
|
-
.input--small .input__suffix ::slotted(zn-checkbox),
|
|
239
250
|
.input--small .input__prefix ::slotted(zn-checkbox) {
|
|
240
251
|
margin-inline-end: 0;
|
|
241
252
|
margin-inline-start: 0;
|
|
242
253
|
}
|
|
243
254
|
|
|
244
255
|
.input--medium {
|
|
256
|
+
--input-height: var(--zn-input-height-medium);
|
|
245
257
|
border-radius: var(--zn-input-border-radius-medium);
|
|
246
258
|
font-size: var(--zn-input-font-size-medium);
|
|
247
259
|
height: var(--zn-input-height-medium);
|
|
@@ -262,34 +274,23 @@ input[type='text'].input__control[data-color-input] {
|
|
|
262
274
|
margin-inline-start: var(--zn-spacing-small);
|
|
263
275
|
}
|
|
264
276
|
|
|
265
|
-
.input--medium .input__suffix ::slotted(*),
|
|
266
|
-
.input__suffix-default {
|
|
267
|
-
margin-inline-end: var(--zn-spacing-small);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
.input--medium .input__suffix ::slotted(zn-button),
|
|
271
277
|
.input--medium .input__prefix ::slotted(zn-button),
|
|
272
|
-
.input--medium .input__suffix ::slotted(zn-checkbox),
|
|
273
278
|
.input--medium .input__prefix ::slotted(zn-checkbox) {
|
|
274
279
|
margin-inline-end: 0;
|
|
275
280
|
margin-inline-start: 0;
|
|
276
281
|
}
|
|
277
282
|
|
|
278
|
-
.input__suffix ::slotted(zn-button),
|
|
279
283
|
.input__prefix ::slotted(zn-button) {
|
|
280
284
|
--zn-button-border-color: rgb(var(--zn-color-border));
|
|
281
285
|
--zn-button-border-width: 0;
|
|
282
286
|
}
|
|
283
287
|
|
|
284
|
-
.input__suffix ::slotted(zn-button) {
|
|
285
|
-
--zn-button-border-left-width: 1px;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
288
|
.input__prefix ::slotted(zn-button) {
|
|
289
289
|
--zn-button-border-right-width: 1px;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
.input--large {
|
|
293
|
+
--input-height: var(--zn-input-height-large);
|
|
293
294
|
border-radius: var(--zn-input-border-radius-large);
|
|
294
295
|
font-size: var(--zn-input-font-size-large);
|
|
295
296
|
height: var(--zn-input-height-large);
|
|
@@ -305,9 +306,7 @@ input[type='text'].input__control[data-color-input] {
|
|
|
305
306
|
width: calc(1em + var(--zn-input-spacing-large) * 2);
|
|
306
307
|
}
|
|
307
308
|
|
|
308
|
-
.input--large .input__suffix ::slotted(zn-button),
|
|
309
309
|
.input--large .input__prefix ::slotted(zn-button),
|
|
310
|
-
.input--large .input__suffix ::slotted(zn-checkbox),
|
|
311
310
|
.input--large .input__prefix ::slotted(zn-checkbox) {
|
|
312
311
|
margin-inline-end: 0;
|
|
313
312
|
margin-inline-start: 0;
|
|
@@ -386,17 +385,81 @@ input[type='text'].input__control[data-color-input] {
|
|
|
386
385
|
display: none;
|
|
387
386
|
}
|
|
388
387
|
|
|
389
|
-
/*
|
|
390
|
-
|
|
391
|
-
|
|
388
|
+
/* The built-in spinner is replaced by .input__stepper */
|
|
389
|
+
input[type='number']::-webkit-outer-spin-button,
|
|
390
|
+
input[type='number']::-webkit-inner-spin-button {
|
|
392
391
|
-webkit-appearance: none;
|
|
393
392
|
display: none;
|
|
394
393
|
}
|
|
395
394
|
|
|
396
|
-
|
|
395
|
+
input[type='number'] {
|
|
397
396
|
-moz-appearance: textfield;
|
|
398
397
|
}
|
|
399
398
|
|
|
399
|
+
.input__stepper {
|
|
400
|
+
--stepper-button-size: calc(var(--zn-input-height-medium) - 10px);
|
|
401
|
+
display: flex;
|
|
402
|
+
flex: 0 0 auto;
|
|
403
|
+
align-items: center;
|
|
404
|
+
gap: 4px;
|
|
405
|
+
padding-inline: 4px 5px;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.input--x-small .input__stepper {
|
|
409
|
+
--stepper-button-size: calc(var(--zn-input-height-x-small) - 6px);
|
|
410
|
+
gap: 2px;
|
|
411
|
+
padding-inline: 2px 3px;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.input--small .input__stepper {
|
|
415
|
+
--stepper-button-size: calc(var(--zn-input-height-small) - 8px);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.input--large .input__stepper {
|
|
419
|
+
--stepper-button-size: calc(var(--zn-input-height-large) - 10px);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.input__stepper-button {
|
|
423
|
+
display: grid;
|
|
424
|
+
place-content: center;
|
|
425
|
+
flex: 0 0 auto;
|
|
426
|
+
width: var(--stepper-button-size);
|
|
427
|
+
height: var(--stepper-button-size);
|
|
428
|
+
padding: 0;
|
|
429
|
+
border: none;
|
|
430
|
+
border-radius: var(--zn-border-radius-small);
|
|
431
|
+
background: none;
|
|
432
|
+
cursor: pointer;
|
|
433
|
+
transition: var(--zn-transition-fast) background-color,
|
|
434
|
+
transform 160ms var(--zn-ease-out, cubic-bezier(0.23, 1, 0.32, 1));
|
|
435
|
+
|
|
436
|
+
zn-icon {
|
|
437
|
+
--icon-color: var(--zn-input-icon-color);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
&:hover:not(:disabled) {
|
|
441
|
+
background-color: var(--zn-color-neutral-100);
|
|
442
|
+
|
|
443
|
+
zn-icon {
|
|
444
|
+
--icon-color: var(--zn-input-icon-color-hover);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Deeper than zn-button's 0.97, which is imperceptible at this size
|
|
449
|
+
&:active:not(:disabled) {
|
|
450
|
+
background-color: var(--zn-color-neutral-200);
|
|
451
|
+
transform: scale(0.9);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
&:disabled {
|
|
455
|
+
cursor: not-allowed;
|
|
456
|
+
|
|
457
|
+
zn-icon {
|
|
458
|
+
--icon-color: var(--zn-input-border-color);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
400
463
|
/* Replace the -webkit-calendar-picker icon */
|
|
401
464
|
input[type='date']::-webkit-calendar-picker-indicator {
|
|
402
465
|
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" height="14" width="12.25" viewBox="0 0 448 512"><path fill="%236d7176" d="M128 0c13.3 0 24 10.7 24 24V64H296V24c0-13.3 10.7-24 24-24s24 10.7 24 24V64h40c35.3 0 64 28.7 64 64v16 48V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V192 144 128C0 92.7 28.7 64 64 64h40V24c0-13.3 10.7-24 24-24zM400 192H48V448c0 8.8 7.2 16 16 16H384c8.8 0 16-7.2 16-16V192zM112 256h96c8.8 0 16 7.2 16 16v96c0 8.8-7.2 16-16 16H112c-8.8 0-16-7.2-16-16V272c0-8.8 7.2-16 16-16z"/></svg>');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
-
import {expect, fixture, html, waitUntil} from '@open-wc/testing';
|
|
3
2
|
import {clearFormStoreValues} from '../../utilities/form';
|
|
3
|
+
import {expect, fixture, html, waitUntil} from '@open-wc/testing';
|
|
4
4
|
import type ZnInput from './input.component';
|
|
5
5
|
import type ZnSlashMenu from '../slash-menu/slash-menu.component';
|
|
6
6
|
|
|
@@ -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
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
2
|
import {aTimeout, expect, fixture, html} from '@open-wc/testing';
|
|
3
|
-
import {centre,
|
|
3
|
+
import {centre, dragTo, drop, startDrag} from '../../internal/test-drag';
|
|
4
4
|
import type ZnNavItem from '../nav-item';
|
|
5
5
|
import type ZnNavList from './nav-list.component';
|
|
6
6
|
|