@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
|
@@ -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 {
|
|
@@ -12,8 +12,8 @@ import ZnDropdown from "../dropdown";
|
|
|
12
12
|
import ZnIcon from "../icon";
|
|
13
13
|
import ZnTooltip from "../tooltip";
|
|
14
14
|
import type {IconColor, IconLibrary} from "../icon";
|
|
15
|
-
import type ZnMenu from "../menu";
|
|
16
15
|
import type {ZincFormControl} from '../../internal/zinc-element';
|
|
16
|
+
import type ZnMenu from "../menu";
|
|
17
17
|
|
|
18
18
|
import sharedStyles from '../../../scss/shared/index.scss';
|
|
19
19
|
import styles from './button.scss';
|
|
@@ -641,7 +641,7 @@ export default class ZnButton extends ZincElement implements ZincFormControl {
|
|
|
641
641
|
'button--icon-button-small': this.iconButton === 'small',
|
|
642
642
|
'button--icon-button-round': this.iconButton === 'round',
|
|
643
643
|
'button--grow': this.grow,
|
|
644
|
-
'button--standard': !this.outline && !this.text,
|
|
644
|
+
'button--standard': !this.outline && (!this.text || this.solid),
|
|
645
645
|
'button--with-icon': this.icon,
|
|
646
646
|
'button--icon-left': this.iconPosition === 'left',
|
|
647
647
|
'button--icon-right': this.iconPosition === 'right',
|
|
@@ -66,6 +66,13 @@ describe('<zn-button>', () => {
|
|
|
66
66
|
expect(base.classList.contains('button--large')).to.be.true;
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
it('keeps the filled style when solid overrides text', async () => {
|
|
70
|
+
const el = await fixture<ZnButton>(html`<zn-button text solid color="primary">Create</zn-button>`);
|
|
71
|
+
const base = el.shadowRoot!.querySelector('[part~="base"]')!;
|
|
72
|
+
expect(base.classList.contains('button--standard')).to.be.true;
|
|
73
|
+
expect(base.classList.contains('button--text')).to.be.false;
|
|
74
|
+
});
|
|
75
|
+
|
|
69
76
|
it('applies the ai color', async () => {
|
|
70
77
|
const el = await fixture<ZnButton>(html`<zn-button color="ai">Summarise</zn-button>`);
|
|
71
78
|
const base = el.shadowRoot!.querySelector('[part~="base"]')!;
|
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
type RadarIndicator,
|
|
17
17
|
type SeriesItem,
|
|
18
18
|
} from './builders';
|
|
19
|
-
import { type CSSResultGroup, html, type PropertyValues, unsafeCSS } from 'lit';
|
|
20
19
|
import { classMap } from 'lit/directives/class-map.js';
|
|
20
|
+
import { type CSSResultGroup, html, type PropertyValues, unsafeCSS } from 'lit';
|
|
21
21
|
import { type EChartsModule, loadECharts } from './echarts-loader';
|
|
22
22
|
import { LiveTransport, LiveWindow } from './live';
|
|
23
23
|
import { property, state } from 'lit/decorators.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import '../../../../../dist/zn.min.js';
|
|
2
2
|
import {expect, fixture, html, oneEvent} from '@open-wc/testing';
|
|
3
|
-
import type DialogComponent from './dialog.component';
|
|
4
3
|
import type {ZnEditorInsertEvent} from '../events/zn-editor-insert';
|
|
4
|
+
import type DialogComponent from './dialog.component';
|
|
5
5
|
|
|
6
6
|
describe('<zn-editor-dialog> content inserts', () => {
|
|
7
7
|
const dialog = () => fixture<DialogComponent>(html`
|
|
@@ -3,10 +3,10 @@ import {type CSSResultGroup, html, nothing, type PropertyValues, unsafeCSS} from
|
|
|
3
3
|
import {deepQuerySelectorAll} from "../../utilities/query";
|
|
4
4
|
import {md5} from "../../utilities/md5";
|
|
5
5
|
import {MutationController} from '@lit-labs/observers/mutation-controller.js';
|
|
6
|
+
import {onHoverIntent} from '../../internal/hover-intent';
|
|
6
7
|
import {property} from 'lit/decorators.js';
|
|
7
8
|
import {styleMap} from "lit/directives/style-map.js";
|
|
8
9
|
import ZincElement from '../../internal/zinc-element';
|
|
9
|
-
import {onHoverIntent} from '../../internal/hover-intent';
|
|
10
10
|
|
|
11
11
|
import styles from './expanding-action.scss';
|
|
12
12
|
|
|
@@ -3,8 +3,8 @@ import {property, state} from "lit/decorators.js";
|
|
|
3
3
|
import {trimControlValue} from '../../internal/form';
|
|
4
4
|
import ZincElement from '../../internal/zinc-element';
|
|
5
5
|
|
|
6
|
-
import type ZnDataSelect from "../data-select";
|
|
7
6
|
import type ZnButton from "../button";
|
|
7
|
+
import type ZnDataSelect from "../data-select";
|
|
8
8
|
import type ZnDataTable from "../data-table";
|
|
9
9
|
import type ZnInput from "../input";
|
|
10
10
|
import type ZnQueryBuilder from "../query-builder";
|
|
@@ -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">
|
|
@@ -86,7 +86,7 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
86
86
|
|
|
87
87
|
@property({ type: Boolean }) inline: boolean
|
|
88
88
|
|
|
89
|
-
@property({ type: Boolean }) padded: boolean
|
|
89
|
+
@property({ type: Boolean, reflect: true }) padded: boolean
|
|
90
90
|
|
|
91
91
|
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
|
92
92
|
|
|
@@ -436,7 +436,6 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
436
436
|
'ai--focused': this.hasFocus,
|
|
437
437
|
'ai--disabled': this.disabled,
|
|
438
438
|
'ai--inline': this.inline,
|
|
439
|
-
'ai--padded': this.padded,
|
|
440
439
|
'ai--masked': !!this.displayValue,
|
|
441
440
|
})}" dir="ltr">
|
|
442
441
|
|
|
@@ -1,30 +1,34 @@
|
|
|
1
1
|
@use "../../wc";
|
|
2
2
|
|
|
3
3
|
:host {
|
|
4
|
+
--ai-input-height: var(--zn-input-height-medium);
|
|
5
|
+
|
|
4
6
|
display: block;
|
|
7
|
+
box-sizing: border-box;
|
|
5
8
|
container-type: inline-size;
|
|
6
9
|
width: 100%;
|
|
7
10
|
}
|
|
8
11
|
|
|
12
|
+
// On the host rather than the row, so the hover fill and edit actions stay hugging the field
|
|
13
|
+
:host([padded]) {
|
|
14
|
+
padding: var(--zn-spacing-large);
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
.ai {
|
|
10
18
|
--bleed: var(--zn-inline-edit-bleed, 10px);
|
|
11
19
|
display: flex;
|
|
12
|
-
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
// The hover fill hangs out past the value's left edge, so the text stays aligned with the column
|
|
22
|
+
width: calc(100% + var(--bleed));
|
|
23
|
+
margin-inline-start: calc(-1 * var(--bleed));
|
|
24
|
+
padding-inline-start: var(--bleed);
|
|
13
25
|
flex-direction: row;
|
|
14
26
|
gap: 10px;
|
|
15
27
|
border-radius: var(--zn-border-radius-medium);
|
|
16
28
|
transition: background-color var(--zn-transition-fast) ease;
|
|
17
29
|
|
|
18
|
-
// The hover fill hangs out past the value's left edge, so the text stays aligned with the column
|
|
19
|
-
&:not(.ai--padded) {
|
|
20
|
-
box-sizing: border-box;
|
|
21
|
-
width: calc(100% + var(--bleed));
|
|
22
|
-
margin-inline-start: calc(-1 * var(--bleed));
|
|
23
|
-
padding-inline-start: var(--bleed);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
30
|
// The input's own padding takes over from the bleed, keeping its text roughly where the value was
|
|
27
|
-
&--editing
|
|
31
|
+
&--editing {
|
|
28
32
|
padding-inline-start: 0;
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -116,10 +120,6 @@
|
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
&--padded {
|
|
120
|
-
padding: var(--zn-spacing-large);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
123
|
&--inline {
|
|
124
124
|
display: flex;
|
|
125
125
|
flex-direction: row;
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
position: absolute;
|
|
161
161
|
inset-block-start: 0;
|
|
162
162
|
inset-inline-end: var(--action-inset);
|
|
163
|
-
height: var(--
|
|
163
|
+
height: var(--ai-input-height);
|
|
164
164
|
gap: 2px;
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -238,7 +238,7 @@
|
|
|
238
238
|
|
|
239
239
|
&__reveal {
|
|
240
240
|
// Matches the height of the input it stands in for, so entering edit mode doesn't resize the row
|
|
241
|
-
line-height: var(--
|
|
241
|
+
line-height: var(--ai-input-height);
|
|
242
242
|
color: rgb(var(--zn-text));
|
|
243
243
|
|
|
244
244
|
// Fill the row so hovering anywhere over the field reveals the value
|
|
@@ -312,10 +312,10 @@ zn-textarea::part(textarea) {
|
|
|
312
312
|
resize: none;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
:host([size="small"])
|
|
316
|
-
|
|
315
|
+
:host([size="small"]) {
|
|
316
|
+
--ai-input-height: var(--zn-input-height-small);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
:host([size="large"])
|
|
320
|
-
|
|
319
|
+
:host([size="large"]) {
|
|
320
|
+
--ai-input-height: var(--zn-input-height-large);
|
|
321
321
|
}
|