@kubex/zinc 1.1.50 → 1.1.52
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/chunks/zn.HEY2XOTB.js +4 -0
- package/dist/custom-elements.json +139 -74
- package/dist/vscode.html-custom-data.json +27 -12
- package/dist/web-types.json +54 -24
- package/dist/zn.d.ts +13 -0
- package/dist/zn.min.js +330 -321
- package/docs/pages/components/inline-edit.md +41 -0
- package/docs/pages/components/reveal.md +16 -0
- package/package.json +1 -1
- package/src/components/inline-edit/inline-edit.component.ts +26 -2
- package/src/components/inline-edit/inline-edit.scss +24 -0
- package/src/components/inline-edit/inline-edit.test.ts +113 -0
- package/src/components/reveal/reveal.component.ts +7 -1
- package/src/components/reveal/reveal.test.ts +25 -0
- package/src/components/tile/tile.component.ts +6 -3
- package/dist/chunks/zn.ZGI3WHSG.js +0 -4
|
@@ -54,6 +54,47 @@ Use the `edit-text` attribute to customize the edit button text instead of showi
|
|
|
54
54
|
</div>
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
### Masked Values
|
|
58
|
+
|
|
59
|
+
Use the `display-value` attribute to mask sensitive data at rest. The masked value is shown in place of the input and
|
|
60
|
+
the real value is revealed on hover (via [`zn-reveal`](/components/reveal)). Clicking the field or the edit button
|
|
61
|
+
switches to the normal inline edit flow, and the real `value` — never the mask — is what gets edited and submitted.
|
|
62
|
+
|
|
63
|
+
```html:preview
|
|
64
|
+
<div class="form-spacing">
|
|
65
|
+
<zn-inline-edit name="email"
|
|
66
|
+
value="john.doe@example.com"
|
|
67
|
+
display-value="j***@example.com">
|
|
68
|
+
</zn-inline-edit>
|
|
69
|
+
|
|
70
|
+
<zn-inline-edit name="phone"
|
|
71
|
+
value="555-123-1234"
|
|
72
|
+
display-value="•••-•••-1234">
|
|
73
|
+
</zn-inline-edit>
|
|
74
|
+
</div>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
:::tip
|
|
78
|
+
Show enough context in the mask (e.g. the email domain or last 4 digits) so users know what they're looking at before
|
|
79
|
+
revealing. The component doesn't re-mask automatically after a save — update `display-value` in your `zn-submit`
|
|
80
|
+
handler once the server confirms the change.
|
|
81
|
+
:::
|
|
82
|
+
|
|
83
|
+
### Clear on Edit
|
|
84
|
+
|
|
85
|
+
Add the `clear-on-edit` attribute to start editing with an empty input instead of the current value — useful for
|
|
86
|
+
secrets where the real value shouldn't be pre-filled. Cancelling (or clicking outside without typing) restores the
|
|
87
|
+
original value.
|
|
88
|
+
|
|
89
|
+
```html:preview
|
|
90
|
+
<zn-inline-edit name="api-key"
|
|
91
|
+
value="sk_live_1234567890abcdef"
|
|
92
|
+
display-value="sk_live_••••••••••••••••"
|
|
93
|
+
clear-on-edit
|
|
94
|
+
style="font-family: monospace;">
|
|
95
|
+
</zn-inline-edit>
|
|
96
|
+
```
|
|
97
|
+
|
|
57
98
|
### Textarea Input Type
|
|
58
99
|
|
|
59
100
|
Use `input-type="textarea"` for multi-line text content.
|
|
@@ -149,6 +149,22 @@ Without a duration or with hover, the content is revealed only while hovering an
|
|
|
149
149
|
</p>
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
### Disabling Click Toggle
|
|
153
|
+
|
|
154
|
+
Add the `no-toggle` attribute to disable click-to-toggle entirely — the value is only revealed on hover. Clicks still
|
|
155
|
+
bubble, so a parent component can handle them (this is how [`zn-inline-edit`](/components/inline-edit#masked-values)
|
|
156
|
+
uses click to enter edit mode).
|
|
157
|
+
|
|
158
|
+
```html:preview
|
|
159
|
+
<zn-reveal no-toggle
|
|
160
|
+
initial="j***@example.com"
|
|
161
|
+
revealed="john.doe@example.com">
|
|
162
|
+
</zn-reveal>
|
|
163
|
+
<p style="font-size: 0.875rem; color: var(--zn-color-neutral-600); margin-top: 0.5rem;">
|
|
164
|
+
Hover to reveal — clicking does nothing
|
|
165
|
+
</p>
|
|
166
|
+
```
|
|
167
|
+
|
|
152
168
|
### Click and Hover Combined
|
|
153
169
|
|
|
154
170
|
When duration is set, clicking toggles temporarily while hovering still works. After clicking, the content remains revealed for the duration, and hovering won't hide it until the duration expires.
|
package/package.json
CHANGED
|
@@ -58,6 +58,18 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
58
58
|
|
|
59
59
|
@property({ attribute: 'edit-text' }) editText: string;
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* A masked value (e.g. `j***@example.com`) shown in place of the input when not editing. Hovering reveals the
|
|
63
|
+
* real value via an embedded `zn-reveal`. The real `value` is still what gets edited and submitted.
|
|
64
|
+
*/
|
|
65
|
+
@property({ attribute: 'display-value' }) displayValue: string = '';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* When set, the edit input starts empty instead of pre-filled with the current value. Cancelling restores the
|
|
69
|
+
* original value. Useful with `display-value` when the real value shouldn't be pre-filled into the input.
|
|
70
|
+
*/
|
|
71
|
+
@property({ type: Boolean, attribute: 'clear-on-edit' }) clearOnEdit: boolean = false;
|
|
72
|
+
|
|
61
73
|
@property() conditional = '';
|
|
62
74
|
|
|
63
75
|
@property({ type: Boolean }) disabled: boolean
|
|
@@ -131,6 +143,7 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
131
143
|
@state() private isEditing: boolean;
|
|
132
144
|
|
|
133
145
|
private _valueBeforeEdit: string | string[];
|
|
146
|
+
private _editStartValue: string | string[];
|
|
134
147
|
|
|
135
148
|
@query('.ai__input') input: ZnInput | ZnSelect;
|
|
136
149
|
|
|
@@ -212,11 +225,12 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
212
225
|
mouseEventHandler = (e: MouseEvent) => {
|
|
213
226
|
if (this.isEditing && !this.contains(e.target as Node)) {
|
|
214
227
|
const hasChanged = Array.isArray(this.value)
|
|
215
|
-
? JSON.stringify(this.value) !== JSON.stringify(this.
|
|
216
|
-
: this.value !== this.
|
|
228
|
+
? JSON.stringify(this.value) !== JSON.stringify(this._editStartValue)
|
|
229
|
+
: this.value !== this._editStartValue;
|
|
217
230
|
|
|
218
231
|
if (!hasChanged) {
|
|
219
232
|
this.isEditing = false;
|
|
233
|
+
this.value = this._valueBeforeEdit;
|
|
220
234
|
this.input.blur();
|
|
221
235
|
}
|
|
222
236
|
}
|
|
@@ -260,6 +274,10 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
260
274
|
}
|
|
261
275
|
if (!this.isEditing) {
|
|
262
276
|
this._valueBeforeEdit = this.value;
|
|
277
|
+
if (this.clearOnEdit) {
|
|
278
|
+
this.value = this.multiple ? [] : '';
|
|
279
|
+
}
|
|
280
|
+
this._editStartValue = this.value;
|
|
263
281
|
}
|
|
264
282
|
this.isEditing = true;
|
|
265
283
|
}
|
|
@@ -352,9 +370,15 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
352
370
|
'ai--disabled': this.disabled,
|
|
353
371
|
'ai--inline': this.inline,
|
|
354
372
|
'ai--padded': this.padded,
|
|
373
|
+
'ai--masked': !!this.displayValue,
|
|
355
374
|
})}" dir="ltr">
|
|
356
375
|
|
|
357
376
|
<div class="ai__left" @click="${this.disabled ? undefined : this.handleEditClick}">
|
|
377
|
+
${this.displayValue && !this.isEditing ? html`
|
|
378
|
+
<zn-reveal class="ai__reveal"
|
|
379
|
+
initial="${this.displayValue}"
|
|
380
|
+
revealed="${Array.isArray(this.value) ? this.value.join(' ') : this.value}"
|
|
381
|
+
no-toggle></zn-reveal>` : ''}
|
|
358
382
|
${input}
|
|
359
383
|
</div>
|
|
360
384
|
|
|
@@ -127,6 +127,26 @@
|
|
|
127
127
|
width: 100%;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
&--masked:not(.ai--editing) {
|
|
131
|
+
.ai__input {
|
|
132
|
+
display: none;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
&__reveal {
|
|
137
|
+
line-height: var(--zn-line-height-looser);
|
|
138
|
+
color: rgb(var(--zn-text));
|
|
139
|
+
|
|
140
|
+
// Fill the row so hovering anywhere over the field reveals the value
|
|
141
|
+
&::part(base) {
|
|
142
|
+
width: 100%;
|
|
143
|
+
text-decoration: underline;
|
|
144
|
+
text-decoration-style: dashed;
|
|
145
|
+
text-decoration-color: rgb(var(--zn-primary), 0.5);
|
|
146
|
+
text-underline-offset: 3px;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
130
150
|
&:not(.ai--disabled) {
|
|
131
151
|
.ai__left {
|
|
132
152
|
cursor: pointer;
|
|
@@ -162,6 +182,10 @@
|
|
|
162
182
|
border-radius: 0;
|
|
163
183
|
}
|
|
164
184
|
|
|
185
|
+
.ai__reveal::part(base) {
|
|
186
|
+
text-decoration: none;
|
|
187
|
+
}
|
|
188
|
+
|
|
165
189
|
.ai__input::part(display-input) {
|
|
166
190
|
cursor: text;
|
|
167
191
|
}
|
|
@@ -370,4 +370,117 @@ describe('<zn-inline-edit>', () => {
|
|
|
370
370
|
|
|
371
371
|
expect(el.value).to.equal('a b');
|
|
372
372
|
});
|
|
373
|
+
|
|
374
|
+
// -- Masked display value --
|
|
375
|
+
|
|
376
|
+
it('should render a zn-reveal with the masked and real values when display-value is set', async () => {
|
|
377
|
+
const el = await fixture<ZnInlineEdit>(html`
|
|
378
|
+
<zn-inline-edit value="real@example.com" display-value="*****@example.com"></zn-inline-edit>
|
|
379
|
+
`);
|
|
380
|
+
await el.updateComplete;
|
|
381
|
+
|
|
382
|
+
const reveal = el.shadowRoot!.querySelector('zn-reveal')!;
|
|
383
|
+
expect(reveal).to.exist;
|
|
384
|
+
expect(reveal.getAttribute('initial')).to.equal('*****@example.com');
|
|
385
|
+
expect(reveal.getAttribute('revealed')).to.equal('real@example.com');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should not render a zn-reveal when display-value is not set', async () => {
|
|
389
|
+
const el = await fixture<ZnInlineEdit>(
|
|
390
|
+
html`<zn-inline-edit value="hello"></zn-inline-edit>`
|
|
391
|
+
);
|
|
392
|
+
await el.updateComplete;
|
|
393
|
+
|
|
394
|
+
expect(el.shadowRoot!.querySelector('zn-reveal')).to.not.exist;
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should enter edit mode and hide the zn-reveal when the masked display is clicked', async () => {
|
|
398
|
+
const el = await fixture<ZnInlineEdit>(html`
|
|
399
|
+
<zn-inline-edit value="real@example.com" display-value="*****@example.com"></zn-inline-edit>
|
|
400
|
+
`);
|
|
401
|
+
await el.updateComplete;
|
|
402
|
+
|
|
403
|
+
const reveal = el.shadowRoot!.querySelector('zn-reveal')!;
|
|
404
|
+
reveal.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true}));
|
|
405
|
+
await el.updateComplete;
|
|
406
|
+
|
|
407
|
+
expect(el.shadowRoot!.querySelector('.ai--editing')).to.exist;
|
|
408
|
+
expect(el.shadowRoot!.querySelector('zn-reveal')).to.not.exist;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should not enter edit mode from the masked display when disabled', async () => {
|
|
412
|
+
const el = await fixture<ZnInlineEdit>(html`
|
|
413
|
+
<zn-inline-edit value="real@example.com" display-value="*****@example.com" disabled></zn-inline-edit>
|
|
414
|
+
`);
|
|
415
|
+
await el.updateComplete;
|
|
416
|
+
|
|
417
|
+
const reveal = el.shadowRoot!.querySelector('zn-reveal')!;
|
|
418
|
+
reveal.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true}));
|
|
419
|
+
await el.updateComplete;
|
|
420
|
+
|
|
421
|
+
expect(el.shadowRoot!.querySelector('.ai--editing')).to.not.exist;
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('should submit the real value, not the display-value, in form data', async () => {
|
|
425
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
426
|
+
<form>
|
|
427
|
+
<zn-inline-edit name="email" value="real@example.com" display-value="*****@example.com"></zn-inline-edit>
|
|
428
|
+
</form>
|
|
429
|
+
`);
|
|
430
|
+
const el = form.querySelector<ZnInlineEdit>('zn-inline-edit')!;
|
|
431
|
+
await el.updateComplete;
|
|
432
|
+
|
|
433
|
+
const formData = new FormData(form);
|
|
434
|
+
expect(formData.get('email')).to.equal('real@example.com');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// -- clear-on-edit --
|
|
438
|
+
|
|
439
|
+
it('should start with an empty input when clear-on-edit is set', async () => {
|
|
440
|
+
const el = await fixture<ZnInlineEdit>(html`
|
|
441
|
+
<zn-inline-edit value="real@example.com" clear-on-edit></zn-inline-edit>
|
|
442
|
+
`);
|
|
443
|
+
await el.updateComplete;
|
|
444
|
+
|
|
445
|
+
const editBtn = el.shadowRoot!.querySelector<HTMLElement>('.button--edit')!;
|
|
446
|
+
editBtn.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true}));
|
|
447
|
+
await el.updateComplete;
|
|
448
|
+
|
|
449
|
+
expect(el.value).to.equal('');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should restore the original value on cancel when clear-on-edit is set', async () => {
|
|
453
|
+
const el = await fixture<ZnInlineEdit>(html`
|
|
454
|
+
<zn-inline-edit value="real@example.com" clear-on-edit></zn-inline-edit>
|
|
455
|
+
`);
|
|
456
|
+
await el.updateComplete;
|
|
457
|
+
|
|
458
|
+
const editBtn = el.shadowRoot!.querySelector<HTMLElement>('.button--edit')!;
|
|
459
|
+
editBtn.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true}));
|
|
460
|
+
await el.updateComplete;
|
|
461
|
+
|
|
462
|
+
const cancelBtn = el.shadowRoot!.querySelector<HTMLElement>('zn-button[icon="close"]')!;
|
|
463
|
+
cancelBtn.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true}));
|
|
464
|
+
await el.updateComplete;
|
|
465
|
+
|
|
466
|
+
expect(el.shadowRoot!.querySelector('.ai--editing')).to.not.exist;
|
|
467
|
+
expect(el.value).to.equal('real@example.com');
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should auto-cancel and restore the value on outside click when clear-on-edit is set and nothing was typed', async () => {
|
|
471
|
+
const el = await fixture<ZnInlineEdit>(html`
|
|
472
|
+
<zn-inline-edit value="real@example.com" clear-on-edit></zn-inline-edit>
|
|
473
|
+
`);
|
|
474
|
+
await el.updateComplete;
|
|
475
|
+
|
|
476
|
+
const editBtn = el.shadowRoot!.querySelector<HTMLElement>('.button--edit')!;
|
|
477
|
+
editBtn.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true}));
|
|
478
|
+
await el.updateComplete;
|
|
479
|
+
|
|
480
|
+
document.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
|
481
|
+
await el.updateComplete;
|
|
482
|
+
|
|
483
|
+
expect(el.shadowRoot!.querySelector('.ai--editing')).to.not.exist;
|
|
484
|
+
expect(el.value).to.equal('real@example.com');
|
|
485
|
+
});
|
|
373
486
|
});
|
|
@@ -41,6 +41,9 @@ export default class ZnReveal extends ZincElement {
|
|
|
41
41
|
|
|
42
42
|
@property({type: Number, attribute: 'hide-delay'}) hideDelay: number = 150;
|
|
43
43
|
|
|
44
|
+
/** Disables click-to-toggle so the value is only revealed on hover. Clicks still bubble to the parent. */
|
|
45
|
+
@property({type: Boolean, attribute: 'no-toggle'}) noToggle: boolean = false;
|
|
46
|
+
|
|
44
47
|
private _isRevealed: boolean = false;
|
|
45
48
|
private _isToggled: boolean = false;
|
|
46
49
|
private _hideTimer?: ReturnType<typeof setTimeout>;
|
|
@@ -58,6 +61,8 @@ export default class ZnReveal extends ZincElement {
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
protected handleToggleReveal() {
|
|
64
|
+
if (this.noToggle) return;
|
|
65
|
+
|
|
61
66
|
this._clearHideTimer();
|
|
62
67
|
|
|
63
68
|
if (this.duration) {
|
|
@@ -98,7 +103,8 @@ export default class ZnReveal extends ZincElement {
|
|
|
98
103
|
|
|
99
104
|
render() {
|
|
100
105
|
return html`
|
|
101
|
-
<div
|
|
106
|
+
<div part="base"
|
|
107
|
+
class=${classMap({
|
|
102
108
|
'reveal': true,
|
|
103
109
|
'reveal--revealed': this._isRevealed,
|
|
104
110
|
'reveal--toggled': this._isToggled,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
2
|
import { expect, fixture, html } from '@open-wc/testing';
|
|
3
|
+
import type ZnReveal from './reveal.component';
|
|
3
4
|
|
|
4
5
|
describe('<zn-reveal>', () => {
|
|
5
6
|
it('should render a component', async () => {
|
|
@@ -7,4 +8,28 @@ describe('<zn-reveal>', () => {
|
|
|
7
8
|
|
|
8
9
|
expect(el).to.exist;
|
|
9
10
|
});
|
|
11
|
+
|
|
12
|
+
it('should toggle the revealed state on click by default', async () => {
|
|
13
|
+
const el = await fixture<ZnReveal>(html`
|
|
14
|
+
<zn-reveal initial="****" revealed="real"></zn-reveal>
|
|
15
|
+
`);
|
|
16
|
+
const container = el.shadowRoot!.querySelector<HTMLElement>('.reveal')!;
|
|
17
|
+
|
|
18
|
+
container.click();
|
|
19
|
+
await el.updateComplete;
|
|
20
|
+
|
|
21
|
+
expect(container.classList.contains('reveal--revealed')).to.be.true;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should not toggle on click when no-toggle is set', async () => {
|
|
25
|
+
const el = await fixture<ZnReveal>(html`
|
|
26
|
+
<zn-reveal initial="****" revealed="real" no-toggle></zn-reveal>
|
|
27
|
+
`);
|
|
28
|
+
const container = el.shadowRoot!.querySelector<HTMLElement>('.reveal')!;
|
|
29
|
+
|
|
30
|
+
container.click();
|
|
31
|
+
await el.updateComplete;
|
|
32
|
+
|
|
33
|
+
expect(container.classList.contains('reveal--revealed')).to.be.false;
|
|
34
|
+
});
|
|
10
35
|
});
|
|
@@ -102,13 +102,15 @@ export default class ZnTile extends ZincElement {
|
|
|
102
102
|
<div
|
|
103
103
|
class="tile__link">
|
|
104
104
|
<div class="tile__left">
|
|
105
|
-
${hasImage ? html
|
|
105
|
+
${hasImage ? html`
|
|
106
|
+
<slot name="image" part="image" class="tile__image"></slot>` : html``}
|
|
106
107
|
<div class="tile__content">
|
|
107
108
|
<p part="caption" class="tile__caption">
|
|
108
109
|
<slot name="caption">${this.caption}</slot>
|
|
109
110
|
</p>
|
|
110
111
|
${hasDescription ? html`
|
|
111
|
-
<p part="description" class="tile__description"
|
|
112
|
+
<p part="description" class="tile__description">
|
|
113
|
+
${this.description}</p>` : html`<slot name="description" class="tile__description"></slot>`}
|
|
112
114
|
</div>
|
|
113
115
|
</div>
|
|
114
116
|
</div>
|
|
@@ -117,7 +119,8 @@ export default class ZnTile extends ZincElement {
|
|
|
117
119
|
${hasProperties ? html`
|
|
118
120
|
<slot name="properties" part="properties" class="tile__properties"></slot>` : ''}
|
|
119
121
|
${hasActions ? html`
|
|
120
|
-
<slot name="actions" part="actions" class="tile__actions"
|
|
122
|
+
<slot name="actions" part="actions" class="tile__actions"
|
|
123
|
+
@click=${this._handleActionsClick}></slot>` : ''}
|
|
121
124
|
</div>`}
|
|
122
125
|
</${tag}>`;
|
|
123
126
|
}
|