@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
|
@@ -39,6 +39,17 @@ Use the `with-reset` attribute to add a button that resets the form's fields.
|
|
|
39
39
|
</form>
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
### Hide Until Changed
|
|
43
|
+
|
|
44
|
+
Use the `hide-until-changed` attribute to keep the row hidden until a field in the form changes. It hides again once the form is back to its original values — undone by hand, reset, or saved.
|
|
45
|
+
|
|
46
|
+
```html:preview
|
|
47
|
+
<form>
|
|
48
|
+
<zn-input name="title" label="Title" value="Edit me"></zn-input>
|
|
49
|
+
<zn-form-actions hide-until-changed with-reset></zn-form-actions>
|
|
50
|
+
</form>
|
|
51
|
+
```
|
|
52
|
+
|
|
42
53
|
### Custom Text and Icons
|
|
43
54
|
|
|
44
55
|
Each button's text and icon can be overridden with `submit-text` / `submit-icon`, `cancel-text` / `cancel-icon` and `reset-text` / `reset-icon`.
|
package/package.json
CHANGED
|
@@ -89,6 +89,13 @@ zn-sp[divide] > zn-form-group ~ zn-form-group {
|
|
|
89
89
|
border-top: 0;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Divided rows of fields keep the plain field spacing, half each side of the line, where divided sections keep the
|
|
93
|
+
// full gap either side.
|
|
94
|
+
zn-sp[divide]:has(> zn-toggle, > zn-input, > zn-select, > zn-checkbox, > zn-textarea, > zn-datepicker,
|
|
95
|
+
> zn-radio-group, > zn-checkbox-group) {
|
|
96
|
+
--zn-sp-gap: calc(var(--zn-form-row-gap, var(--zn-spacing-medium)) / 2);
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
|
|
93
100
|
.form-spacing > * {
|
|
94
101
|
grid-column: span 6 / span 6;
|
package/scss/_root.scss
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
--zn-page-closer-size: 36px;
|
|
15
15
|
--zn-page-closer-gap: var(--zn-spacing-x-small);
|
|
16
16
|
--zn-gutter: 2px;
|
|
17
|
+
// zn-page centres its content box, so containers inside it start at its left edge, in line with the header
|
|
17
18
|
--zn-default-margin: 0 auto 0 0;
|
|
18
19
|
--zn-icon-size: 20px;
|
|
19
20
|
--zn-button-gutter: 2px;
|
|
@@ -75,11 +76,6 @@
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
@media (min-width: 2500px) {
|
|
79
|
-
:root {
|
|
80
|
-
--zn-default-margin: 0 auto;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
79
|
|
|
84
80
|
|
|
85
81
|
:root {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {type CSSResultGroup, html, unsafeCSS} from 'lit';
|
|
2
|
+
import {formCollections} from '../../internal/form';
|
|
2
3
|
import {idSelector} from "../../utilities/query";
|
|
3
4
|
import {property, state} from 'lit/decorators.js';
|
|
4
5
|
import ZincElement from '../../internal/zinc-element';
|
|
@@ -21,10 +22,20 @@ import styles from './form-actions.scss';
|
|
|
21
22
|
* number can sit on either side. Write them in the order they should be read — the sides are set by CSS ordering, so
|
|
22
23
|
* markup order is what a keyboard follows.
|
|
23
24
|
*
|
|
25
|
+
* @csspart base - The row holding the actions.
|
|
24
26
|
* @csspart cancel-button - The cancel button.
|
|
25
27
|
* @csspart reset-button - The reset button.
|
|
26
28
|
* @csspart submit-button - The submit button.
|
|
27
29
|
*/
|
|
30
|
+
const CHANGE_EVENTS = ['input', 'change', 'zn-input', 'zn-change'];
|
|
31
|
+
|
|
32
|
+
// An empty multi-select reports '', [''] or [] depending on whether it was loaded, reset or cleared by hand
|
|
33
|
+
const normaliseValue = (value: unknown): unknown =>
|
|
34
|
+
typeof value === 'string' || Array.isArray(value)
|
|
35
|
+
? (Array.isArray(value) ? value : [value]).filter(v => v !== '')
|
|
36
|
+
: value;
|
|
37
|
+
const INTERACTION_EVENTS = ['pointerdown', 'keydown'];
|
|
38
|
+
|
|
28
39
|
export default class ZnFormActions extends ZincElement {
|
|
29
40
|
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
30
41
|
static dependencies = {
|
|
@@ -64,9 +75,18 @@ export default class ZnFormActions extends ZincElement {
|
|
|
64
75
|
*/
|
|
65
76
|
@property({attribute: 'loading-on-submit', type: Boolean}) loadingOnSubmit = false;
|
|
66
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Hides the row until the form's values differ from when it loaded or was last saved, and hides it again once
|
|
80
|
+
* they match.
|
|
81
|
+
*/
|
|
82
|
+
@property({attribute: 'hide-until-changed', type: Boolean, reflect: true}) hideUntilChanged = false;
|
|
83
|
+
|
|
67
84
|
@state() private submitting = false;
|
|
68
85
|
|
|
69
86
|
private watchedForm: HTMLFormElement | null = null;
|
|
87
|
+
private changeForm: HTMLFormElement | null = null;
|
|
88
|
+
private baseline = '';
|
|
89
|
+
private interacted = false;
|
|
70
90
|
|
|
71
91
|
connectedCallback() {
|
|
72
92
|
super.connectedCallback();
|
|
@@ -76,6 +96,17 @@ export default class ZnFormActions extends ZincElement {
|
|
|
76
96
|
this.watchedForm?.addEventListener('requested', this.handleRequested);
|
|
77
97
|
['complete', 'error', 'cancelled'].forEach(e => this.watchedForm?.addEventListener(e, this.handleDone));
|
|
78
98
|
}
|
|
99
|
+
|
|
100
|
+
this.changeForm = this.hideUntilChanged ? this.getForm() : null;
|
|
101
|
+
if (this.changeForm) {
|
|
102
|
+
CHANGE_EVENTS.forEach(e => this.changeForm!.addEventListener(e, this.checkForChanges));
|
|
103
|
+
INTERACTION_EVENTS.forEach(e => this.changeForm!.addEventListener(e, this.refreshBaseline, {capture: true}));
|
|
104
|
+
this.changeForm.addEventListener('reset', this.handleFormReset);
|
|
105
|
+
this.changeForm.addEventListener('complete', this.markSaved);
|
|
106
|
+
this.toggleAttribute('data-unchanged', true);
|
|
107
|
+
// Controls later in the form may not have upgraded or set their values yet
|
|
108
|
+
requestAnimationFrame(this.markSaved);
|
|
109
|
+
}
|
|
79
110
|
}
|
|
80
111
|
|
|
81
112
|
disconnectedCallback() {
|
|
@@ -83,9 +114,74 @@ export default class ZnFormActions extends ZincElement {
|
|
|
83
114
|
this.watchedForm?.removeEventListener('requested', this.handleRequested);
|
|
84
115
|
['complete', 'error', 'cancelled'].forEach(e => this.watchedForm?.removeEventListener(e, this.handleDone));
|
|
85
116
|
this.watchedForm = null;
|
|
117
|
+
|
|
118
|
+
CHANGE_EVENTS.forEach(e => this.changeForm?.removeEventListener(e, this.checkForChanges));
|
|
119
|
+
INTERACTION_EVENTS.forEach(e => this.changeForm?.removeEventListener(e, this.refreshBaseline, {capture: true}));
|
|
120
|
+
this.interacted = false;
|
|
121
|
+
this.changeForm?.removeEventListener('reset', this.handleFormReset);
|
|
122
|
+
this.changeForm?.removeEventListener('complete', this.markSaved);
|
|
123
|
+
this.changeForm = null;
|
|
86
124
|
super.disconnectedCallback();
|
|
87
125
|
}
|
|
88
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The form's values, compared against the baseline. Read from the controls directly, since building FormData runs
|
|
129
|
+
* zinc controls' formdata handlers, which trim the value being typed.
|
|
130
|
+
*/
|
|
131
|
+
private snapshot(): string {
|
|
132
|
+
const form = this.changeForm;
|
|
133
|
+
if (!form) return '';
|
|
134
|
+
|
|
135
|
+
const entries: unknown[] = [];
|
|
136
|
+
for (const el of Array.from(form.elements)) {
|
|
137
|
+
if (!(el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement)
|
|
138
|
+
|| !el.name) continue;
|
|
139
|
+
if (el instanceof HTMLInputElement && (el.type === 'checkbox' || el.type === 'radio')) {
|
|
140
|
+
entries.push([el.name, el.value, el.checked]);
|
|
141
|
+
} else if (el instanceof HTMLInputElement && el.type === 'file') {
|
|
142
|
+
entries.push([el.name, Array.from(el.files ?? [], f => f.name)]);
|
|
143
|
+
} else if (el instanceof HTMLSelectElement) {
|
|
144
|
+
entries.push([el.name, Array.from(el.selectedOptions, o => o.value)]);
|
|
145
|
+
} else {
|
|
146
|
+
entries.push([el.name, el.value]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
formCollections.get(form)?.forEach(control => {
|
|
150
|
+
// Disabled controls still count: a conditional control disabling is a side effect of another change
|
|
151
|
+
if (!control.name) return;
|
|
152
|
+
// A toggle's value only appears once it's first clicked, so on/off controls compare by checked alone
|
|
153
|
+
const checked = (control as {checked?: boolean}).checked;
|
|
154
|
+
entries.push(typeof checked === 'boolean' ? [control.name, checked] : [control.name, normaliseValue(control.value)]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return JSON.stringify(entries);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private checkForChanges = () => {
|
|
161
|
+
// Before the user touches the form, changes come from controls settling (options loading, select-first)
|
|
162
|
+
if (!this.interacted) {
|
|
163
|
+
this.baseline = this.snapshot();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.toggleAttribute('data-unchanged', this.snapshot() === this.baseline);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
private markSaved = () => {
|
|
170
|
+
this.baseline = this.snapshot();
|
|
171
|
+
this.toggleAttribute('data-unchanged', true);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Values set by code between edits (remote options, late data) aren't user changes, so re-read them while unchanged
|
|
175
|
+
private refreshBaseline = () => {
|
|
176
|
+
this.interacted = true;
|
|
177
|
+
if (this.hasAttribute('data-unchanged')) this.baseline = this.snapshot();
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Controls reset their values after the reset event
|
|
181
|
+
private handleFormReset = () => {
|
|
182
|
+
setTimeout(this.checkForChanges);
|
|
183
|
+
};
|
|
184
|
+
|
|
89
185
|
// Only the original submit is blocked; the console re-dispatches it as a composed event
|
|
90
186
|
private handleFormSubmit = (e: Event) => {
|
|
91
187
|
if (this.submitting && !e.composed) {
|
|
@@ -127,19 +223,21 @@ export default class ZnFormActions extends ZincElement {
|
|
|
127
223
|
|
|
128
224
|
render() {
|
|
129
225
|
return html`
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
${this.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
${this.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
226
|
+
<div part="base" class="form-actions">
|
|
227
|
+
<slot></slot>
|
|
228
|
+
<span class="form-actions__spacer"></span>
|
|
229
|
+
${this.withCancel ? html`
|
|
230
|
+
<zn-button part="cancel-button" panel-bg modal-closer icon="${this.cancelIcon}" @click="${this.handleCancel}">
|
|
231
|
+
${this.cancelText}
|
|
232
|
+
</zn-button>` : ''}
|
|
233
|
+
${this.withReset ? html`
|
|
234
|
+
<zn-button part="reset-button" panel-bg icon="${this.resetIcon}" @click="${this.handleReset}">
|
|
235
|
+
${this.resetText}
|
|
236
|
+
</zn-button>` : ''}
|
|
237
|
+
<zn-button part="submit-button" color="primary" icon="${this.submitIcon}" @click="${this.handleSubmit}"
|
|
238
|
+
?loading=${this.submitting} ?disabled=${this.submitting} loading-text="${this.submitText}">
|
|
239
|
+
${this.submitText}
|
|
240
|
+
</zn-button>
|
|
241
|
+
</div>`;
|
|
144
242
|
}
|
|
145
243
|
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
@use "../../wc";
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// One grid row, so hide-until-changed can collapse the height by animating the track between 1fr and 0fr.
|
|
4
4
|
:host {
|
|
5
|
+
--form-actions-ease: var(--zn-ease-out, cubic-bezier(0.23, 1, 0.32, 1));
|
|
6
|
+
display: grid;
|
|
7
|
+
grid-template-rows: 1fr;
|
|
8
|
+
transition: grid-template-rows 200ms var(--form-actions-ease),
|
|
9
|
+
opacity 200ms var(--form-actions-ease),
|
|
10
|
+
display 200ms allow-discrete;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Keep in sync with the global .form-actions layout in scss/_global-spacing.scss.
|
|
14
|
+
.form-actions {
|
|
5
15
|
display: flex;
|
|
6
16
|
justify-content: flex-end;
|
|
7
17
|
align-items: center;
|
|
8
18
|
gap: var(--zn-form-actions-gap, var(--zn-spacing-small));
|
|
19
|
+
min-height: 0;
|
|
9
20
|
}
|
|
10
21
|
|
|
11
22
|
// The spacer, not an auto margin, is what splits the two sides: an auto margin on every left-hand action would open
|
|
@@ -24,3 +35,28 @@ zn-button {
|
|
|
24
35
|
::slotted([align='start']) {
|
|
25
36
|
order: 0;
|
|
26
37
|
}
|
|
38
|
+
|
|
39
|
+
// display: none lands once the collapse finishes, dropping the parent's gap around the row as well
|
|
40
|
+
:host([data-unchanged]) {
|
|
41
|
+
display: none;
|
|
42
|
+
grid-template-rows: 0fr;
|
|
43
|
+
opacity: 0;
|
|
44
|
+
transition-duration: var(--zn-transition-fast, 150ms);
|
|
45
|
+
|
|
46
|
+
.form-actions {
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@starting-style {
|
|
52
|
+
:host([hide-until-changed]:not([data-unchanged])) {
|
|
53
|
+
grid-template-rows: 0fr;
|
|
54
|
+
opacity: 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@media (prefers-reduced-motion: reduce) {
|
|
59
|
+
:host {
|
|
60
|
+
transition-property: opacity, display;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -45,6 +45,143 @@ describe('<zn-form-actions>', () => {
|
|
|
45
45
|
expect(submit.left).to.be.closeTo(cancel.right + 16, 1);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
it('shows only while the form differs from its loaded values', async () => {
|
|
49
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
50
|
+
<form>
|
|
51
|
+
<input name="title" value="a">
|
|
52
|
+
<zn-form-actions hide-until-changed></zn-form-actions>
|
|
53
|
+
</form>
|
|
54
|
+
`);
|
|
55
|
+
const actions = form.querySelector('zn-form-actions')!;
|
|
56
|
+
const input = form.querySelector('input')!;
|
|
57
|
+
const visible = async () => {
|
|
58
|
+
await Promise.all(actions.getAnimations().map(animation => animation.finished));
|
|
59
|
+
return getComputedStyle(actions).display !== 'none';
|
|
60
|
+
};
|
|
61
|
+
const type = (value: string) => {
|
|
62
|
+
input.dispatchEvent(new Event('keydown', { bubbles: true }));
|
|
63
|
+
input.value = value;
|
|
64
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
65
|
+
};
|
|
66
|
+
await new Promise(requestAnimationFrame);
|
|
67
|
+
|
|
68
|
+
expect(await visible()).to.be.false;
|
|
69
|
+
|
|
70
|
+
type('b');
|
|
71
|
+
expect(await visible()).to.be.true;
|
|
72
|
+
|
|
73
|
+
type('a');
|
|
74
|
+
expect(await visible()).to.be.false;
|
|
75
|
+
|
|
76
|
+
type('b');
|
|
77
|
+
form.reset();
|
|
78
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
79
|
+
expect(await visible()).to.be.false;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('hides again when a toggle is switched back', async () => {
|
|
83
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
84
|
+
<form>
|
|
85
|
+
<zn-toggle name="auto"></zn-toggle>
|
|
86
|
+
<zn-form-actions hide-until-changed></zn-form-actions>
|
|
87
|
+
</form>
|
|
88
|
+
`);
|
|
89
|
+
const actions = form.querySelector('zn-form-actions')!;
|
|
90
|
+
const toggle = form.querySelector('zn-toggle')!;
|
|
91
|
+
await new Promise(requestAnimationFrame);
|
|
92
|
+
const flip = async () => {
|
|
93
|
+
toggle.dispatchEvent(new Event('pointerdown', { bubbles: true }));
|
|
94
|
+
toggle.shadowRoot!.querySelector('input')!.click();
|
|
95
|
+
await (toggle as HTMLElement & { updateComplete: Promise<unknown> }).updateComplete;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await flip();
|
|
99
|
+
expect(actions.hasAttribute('data-unchanged')).to.be.false;
|
|
100
|
+
|
|
101
|
+
await flip();
|
|
102
|
+
expect(actions.hasAttribute('data-unchanged')).to.be.true;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('hides again when an empty multi-select is changed then reset', async () => {
|
|
106
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
107
|
+
<form>
|
|
108
|
+
<zn-select name="groups" multiple>
|
|
109
|
+
<zn-option value="a">A</zn-option>
|
|
110
|
+
<zn-option value="b">B</zn-option>
|
|
111
|
+
</zn-select>
|
|
112
|
+
<zn-form-actions hide-until-changed></zn-form-actions>
|
|
113
|
+
</form>
|
|
114
|
+
`);
|
|
115
|
+
const actions = form.querySelector('zn-form-actions')!;
|
|
116
|
+
const select = form.querySelector<HTMLElement & { value: string[] }>('zn-select')!;
|
|
117
|
+
await new Promise(requestAnimationFrame);
|
|
118
|
+
|
|
119
|
+
select.dispatchEvent(new Event('pointerdown', { bubbles: true }));
|
|
120
|
+
select.value = ['a'];
|
|
121
|
+
select.dispatchEvent(new Event('zn-change', { bubbles: true }));
|
|
122
|
+
expect(actions.hasAttribute('data-unchanged')).to.be.false;
|
|
123
|
+
|
|
124
|
+
form.reset();
|
|
125
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
126
|
+
expect(actions.hasAttribute('data-unchanged')).to.be.true;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('hides again when a reset re-enables a conditional select', async () => {
|
|
130
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
131
|
+
<form>
|
|
132
|
+
<zn-select id="allowed" name="allowed" multiple value="" provider="conditional" conditional="blocked">
|
|
133
|
+
<zn-option value="a">A</zn-option>
|
|
134
|
+
</zn-select>
|
|
135
|
+
<zn-select id="blocked" name="blocked" multiple value="" provider="conditional" conditional="allowed">
|
|
136
|
+
<zn-option value="a">A</zn-option>
|
|
137
|
+
</zn-select>
|
|
138
|
+
<zn-form-actions hide-until-changed></zn-form-actions>
|
|
139
|
+
</form>
|
|
140
|
+
`);
|
|
141
|
+
const actions = form.querySelector('zn-form-actions')!;
|
|
142
|
+
const blocked = form.querySelector<HTMLElement & { value: string[] }>('#blocked')!;
|
|
143
|
+
await new Promise(requestAnimationFrame);
|
|
144
|
+
|
|
145
|
+
blocked.dispatchEvent(new Event('pointerdown', { bubbles: true }));
|
|
146
|
+
blocked.value = ['a'];
|
|
147
|
+
blocked.dispatchEvent(new Event('zn-change', { bubbles: true }));
|
|
148
|
+
expect(actions.hasAttribute('data-unchanged')).to.be.false;
|
|
149
|
+
|
|
150
|
+
form.reset();
|
|
151
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
152
|
+
expect(actions.hasAttribute('data-unchanged')).to.be.true;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('treats changes before any interaction as the starting values', async () => {
|
|
156
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
157
|
+
<form>
|
|
158
|
+
<input name="type" value="">
|
|
159
|
+
<zn-form-actions hide-until-changed></zn-form-actions>
|
|
160
|
+
</form>
|
|
161
|
+
`);
|
|
162
|
+
const actions = form.querySelector('zn-form-actions')!;
|
|
163
|
+
const input = form.querySelector('input')!;
|
|
164
|
+
await new Promise(requestAnimationFrame);
|
|
165
|
+
|
|
166
|
+
// As zn-select does when its options load or select-first picks one
|
|
167
|
+
input.value = 'team';
|
|
168
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
169
|
+
|
|
170
|
+
expect(actions.hasAttribute('data-unchanged')).to.be.true;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('stays visible in an unchanged form without hide-until-changed', async () => {
|
|
174
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
175
|
+
<form>
|
|
176
|
+
<input name="title" value="a">
|
|
177
|
+
<zn-form-actions></zn-form-actions>
|
|
178
|
+
</form>
|
|
179
|
+
`);
|
|
180
|
+
await new Promise(requestAnimationFrame);
|
|
181
|
+
|
|
182
|
+
expect(getComputedStyle(form.querySelector('zn-form-actions')!).display).to.not.equal('none');
|
|
183
|
+
});
|
|
184
|
+
|
|
48
185
|
it('should close a containing dialog when the cancel button is clicked', async () => {
|
|
49
186
|
const dialog = await fixture<HTMLElement & { open: boolean }>(html`
|
|
50
187
|
<zn-dialog open label="Test">
|
|
@@ -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;
|