@kubex/zinc 1.1.62 → 1.1.64

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.
Files changed (36) hide show
  1. package/dist/custom-elements.json +3141 -1885
  2. package/dist/vscode.html-custom-data.json +102 -3
  3. package/dist/web-types.json +224 -6
  4. package/dist/zn.d.ts +235 -0
  5. package/dist/zn.min.js +795 -679
  6. package/docs/data/preview-frame-payload-error.json +4 -0
  7. package/docs/data/preview-frame-payload.json +6 -0
  8. package/docs/pages/components/form-group.md +12 -0
  9. package/docs/pages/components/preview-frame-demo.njk +138 -0
  10. package/docs/pages/components/preview-frame.md +105 -0
  11. package/docs/pages/components/remarkd-editor.md +103 -0
  12. package/docs/pages/components/toggle.md +2 -2
  13. package/package.json +3 -2
  14. package/src/components/file/file.component.ts +16 -1
  15. package/src/components/form-group/form-group.component.ts +24 -12
  16. package/src/components/form-group/form-group.scss +10 -0
  17. package/src/components/form-group/form-group.test.ts +19 -0
  18. package/src/components/preview-frame/index.ts +12 -0
  19. package/src/components/preview-frame/preview-frame.component.ts +246 -0
  20. package/src/components/preview-frame/preview-frame.scss +27 -0
  21. package/src/components/preview-frame/preview-frame.test.ts +268 -0
  22. package/src/components/query-builder/query-builder.component.ts +14 -6
  23. package/src/components/remarkd-editor/index.ts +12 -0
  24. package/src/components/remarkd-editor/remarkd-editor.component.ts +752 -0
  25. package/src/components/remarkd-editor/remarkd-editor.scss +241 -0
  26. package/src/components/remarkd-editor/remarkd-editor.test.ts +147 -0
  27. package/src/components/select/select.component.ts +4 -0
  28. package/src/components/select/select.test.ts +29 -0
  29. package/src/components/sp/sp.scss +5 -1
  30. package/src/components/tabs/tabs.component.ts +9 -4
  31. package/src/components/tabs/tabs.test.ts +42 -1
  32. package/src/components/tile/tile.scss +5 -1
  33. package/src/components/toggle/toggle.component.ts +3 -12
  34. package/src/components/toggle/toggle.test.ts +29 -0
  35. package/src/events/zn-error.ts +1 -1
  36. package/src/zinc.ts +2 -0
@@ -0,0 +1,241 @@
1
+ @use "../../wc";
2
+ // Remarkd's official output styles, scoped under .remarkd-rendered.
3
+ @use "../../../node_modules/remarkd-js/docs/assets/remarkd-output";
4
+
5
+ :host {
6
+ display: block;
7
+ }
8
+
9
+ .remarkd-editor {
10
+ position: relative;
11
+ border: 1px solid rgb(var(--zn-border-color));
12
+ border-radius: var(--zn-border-radius);
13
+ background-color: rgba(var(--zn-panel), var(--zn-panel-opacity));
14
+ }
15
+
16
+ .remarkd-editor__toolbar {
17
+ display: flex;
18
+ gap: 2px;
19
+ flex-wrap: wrap;
20
+ padding: var(--zn-spacing-x-small);
21
+ border-bottom: 1px solid rgb(var(--zn-border-color));
22
+ }
23
+
24
+ .remarkd-editor__body {
25
+ padding: var(--zn-spacing-large) var(--zn-spacing-large) var(--zn-spacing-large) 88px;
26
+ min-height: 10rem;
27
+ }
28
+
29
+ .remarkd-editor--disabled {
30
+ opacity: 0.6;
31
+ pointer-events: none;
32
+ }
33
+
34
+ .remarkd-editor__block {
35
+ position: relative;
36
+ border-radius: var(--zn-border-radius);
37
+ }
38
+
39
+ .remarkd-editor__actions {
40
+ position: absolute;
41
+ left: -80px;
42
+ top: 0;
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 2px;
46
+ opacity: 0;
47
+ transition: opacity 0.15s ease;
48
+ }
49
+
50
+ .remarkd-editor__block:hover .remarkd-editor__actions,
51
+ .remarkd-editor__actions:focus-within {
52
+ opacity: 1;
53
+ }
54
+
55
+ .remarkd-editor__drag-handle {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ padding: 2px;
59
+ color: var(--zn-color-neutral-400, #9aa4b2);
60
+ cursor: grab;
61
+ touch-action: none;
62
+ user-select: none;
63
+
64
+ &:active {
65
+ cursor: grabbing;
66
+ }
67
+ }
68
+
69
+ .remarkd-editor__ghost {
70
+ position: fixed;
71
+ top: 0;
72
+ left: 0;
73
+ z-index: 20;
74
+ pointer-events: none;
75
+ opacity: 0.9;
76
+ background: var(--zn-color-neutral-0, white);
77
+ border-radius: var(--zn-border-radius);
78
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
79
+ max-width: 50%;
80
+ max-height: 160px;
81
+ overflow: hidden;
82
+ }
83
+
84
+ .remarkd-editor__block--dragging {
85
+ opacity: 0.4;
86
+ }
87
+
88
+ .remarkd-editor__block--drop-before::before,
89
+ .remarkd-editor__block--drop-after::after {
90
+ content: '';
91
+ position: absolute;
92
+ left: 0;
93
+ right: 0;
94
+ height: 6px;
95
+ border-radius: 3px;
96
+ background: rgba(var(--zn-primary), 0.6);
97
+ pointer-events: none;
98
+ }
99
+
100
+ .remarkd-editor__block--drop-before::before {
101
+ top: -4px;
102
+ }
103
+
104
+ .remarkd-editor__block--drop-after::after {
105
+ bottom: -4px;
106
+ }
107
+
108
+ .remarkd-editor__rendered {
109
+ padding: var(--zn-spacing-x-small);
110
+ border-radius: var(--zn-border-radius);
111
+ cursor: text;
112
+
113
+ &:hover {
114
+ background: var(--zn-color-neutral-100);
115
+ }
116
+ }
117
+
118
+ // The shell keeps the block's rendered chrome while its source is edited —
119
+ // remarkd hint classes (hint-note, hint-tip, …) apply their own styling here.
120
+ .remarkd-editor__edit-shell {
121
+ border: 1px solid rgb(var(--zn-border-color));
122
+ border-radius: var(--zn-border-radius);
123
+ background: var(--zn-color-neutral-0, white);
124
+ padding: var(--zn-spacing-x-small);
125
+
126
+ // Hint shells keep their own left accent — only tint the other edges.
127
+ &:focus-within {
128
+ border-top-color: rgb(var(--zn-primary));
129
+ border-right-color: rgb(var(--zn-primary));
130
+ border-bottom-color: rgb(var(--zn-primary));
131
+ }
132
+
133
+ &:not([class*="hint-"]):focus-within {
134
+ border-left-color: rgb(var(--zn-primary));
135
+ }
136
+ }
137
+
138
+ .remarkd-editor__input {
139
+ display: block;
140
+ width: 100%;
141
+ resize: none;
142
+ overflow: hidden;
143
+ border: 0;
144
+ background: transparent;
145
+ color: inherit;
146
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
147
+ font-size: 0.875rem;
148
+ line-height: 1.5;
149
+ padding: 0;
150
+ outline: none;
151
+ }
152
+
153
+ // Hints edit in the rendered font so they read as the same block.
154
+ .remarkd-editor__edit-shell[class*="hint-"] .remarkd-editor__input {
155
+ font: inherit;
156
+ }
157
+
158
+ .remarkd-editor__edit-shell--h1 .remarkd-editor__input,
159
+ .remarkd-editor__edit-shell--h2 .remarkd-editor__input,
160
+ .remarkd-editor__edit-shell--h3 .remarkd-editor__input {
161
+ font-family: inherit;
162
+ font-weight: 700;
163
+ line-height: 1.2;
164
+ }
165
+
166
+ .remarkd-editor__edit-shell--h1 .remarkd-editor__input {
167
+ font-size: 26px;
168
+ }
169
+
170
+ .remarkd-editor__edit-shell--h2 .remarkd-editor__input {
171
+ font-size: 21px;
172
+ }
173
+
174
+ .remarkd-editor__edit-shell--h3 .remarkd-editor__input {
175
+ font-size: 17px;
176
+ }
177
+
178
+ .remarkd-editor__edit-shell--code {
179
+ background: var(--rmd-code-bg, #0f1626);
180
+
181
+ .remarkd-editor__input {
182
+ color: var(--rmd-code-text, #f5f8ff);
183
+ }
184
+ }
185
+
186
+ .remarkd-editor__add {
187
+ padding: var(--zn-spacing-x-small);
188
+ min-height: 1.8em;
189
+ color: var(--zn-color-neutral-400, #9aa4b2);
190
+ cursor: text;
191
+ }
192
+
193
+ .remarkd-editor__edit-wrap {
194
+ position: relative;
195
+ }
196
+
197
+ .remarkd-editor__slash-menu {
198
+ position: absolute;
199
+ top: 100%;
200
+ left: 0;
201
+ z-index: 10;
202
+ min-width: 12rem;
203
+ margin-top: 4px;
204
+ padding: 4px;
205
+ border: 1px solid rgb(var(--zn-border-color));
206
+ border-radius: var(--zn-border-radius);
207
+ background: var(--zn-color-neutral-0, white);
208
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
209
+ display: flex;
210
+ flex-direction: column;
211
+ }
212
+
213
+ .remarkd-editor__slash-item {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: var(--zn-spacing-x-small);
217
+ padding: 6px 8px;
218
+ border: 0;
219
+ border-radius: var(--zn-border-radius);
220
+ background: none;
221
+ color: var(--zn-color-text, #222);
222
+ font: inherit;
223
+ font-size: 0.875rem;
224
+ text-align: left;
225
+ cursor: pointer;
226
+
227
+ &:hover,
228
+ &--active {
229
+ background: var(--zn-color-neutral-100);
230
+ }
231
+ }
232
+
233
+ .remarkd-editor__validation {
234
+ position: absolute;
235
+ width: 1px;
236
+ height: 1px;
237
+ opacity: 0;
238
+ pointer-events: none;
239
+ border: 0;
240
+ padding: 0;
241
+ }
@@ -0,0 +1,147 @@
1
+ import '../../../dist/zn.min.js';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+ import type ZnRemarkdEditor from './remarkd-editor.component';
4
+
5
+ describe('<zn-remarkd-editor>', () => {
6
+ it('should render a component', async () => {
7
+ const el = await fixture(html`
8
+ <zn-remarkd-editor></zn-remarkd-editor>`);
9
+ expect(el).to.exist;
10
+ });
11
+
12
+ it('should split the value into rendered remarkd blocks', async () => {
13
+ const el = await fixture<ZnRemarkdEditor>(html`
14
+ <zn-remarkd-editor value="# Title
15
+
16
+ NOTE: a note"></zn-remarkd-editor>`);
17
+ const blocks = el.shadowRoot!.querySelectorAll('.remarkd-editor__block');
18
+ expect(blocks.length).to.equal(2);
19
+ expect(blocks[1].innerHTML).to.contain('hint-note');
20
+ });
21
+
22
+ it('should keep fenced content as a single block', async () => {
23
+ const fenced = '```\n\ncode\n\n```';
24
+ const el = await fixture<ZnRemarkdEditor>(html`
25
+ <zn-remarkd-editor value=${fenced}></zn-remarkd-editor>`);
26
+ expect(el.shadowRoot!.querySelectorAll('.remarkd-editor__block').length).to.equal(1);
27
+ });
28
+
29
+ it('should swap a block to source editing on click and commit on blur', async () => {
30
+ const el = await fixture<ZnRemarkdEditor>(html`
31
+ <zn-remarkd-editor value="# Title"></zn-remarkd-editor>`);
32
+ el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
33
+ await el.updateComplete;
34
+
35
+ const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
36
+ expect(input).to.exist;
37
+ expect(input.value).to.equal('# Title');
38
+
39
+ input.value = '# Changed';
40
+ input.dispatchEvent(new Event('input', {bubbles: true}));
41
+ input.dispatchEvent(new Event('blur'));
42
+ await el.updateComplete;
43
+
44
+ expect(el.value).to.equal('# Changed');
45
+ expect(el.shadowRoot!.querySelector('.remarkd-editor__rendered')!.innerHTML).to.contain('Changed');
46
+ });
47
+
48
+ it('should remove a block committed as empty', async () => {
49
+ const el = await fixture<ZnRemarkdEditor>(html`
50
+ <zn-remarkd-editor value="# Title
51
+
52
+ Second"></zn-remarkd-editor>`);
53
+ el.shadowRoot!.querySelectorAll<HTMLElement>('.remarkd-editor__rendered')[1].click();
54
+ await el.updateComplete;
55
+
56
+ const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
57
+ input.value = '';
58
+ input.dispatchEvent(new Event('input', {bubbles: true}));
59
+ input.dispatchEvent(new Event('blur'));
60
+ await el.updateComplete;
61
+
62
+ expect(el.value).to.equal('# Title');
63
+ expect(el.shadowRoot!.querySelectorAll('.remarkd-editor__block').length).to.equal(1);
64
+ });
65
+
66
+ it('should render an always-visible toolbar', async () => {
67
+ const el = await fixture<ZnRemarkdEditor>(html`
68
+ <zn-remarkd-editor></zn-remarkd-editor>`);
69
+ expect(el.shadowRoot!.querySelector('.remarkd-editor__toolbar')).to.exist;
70
+ });
71
+
72
+ it('should open the slash menu when "/" starts an empty block', async () => {
73
+ const el = await fixture<ZnRemarkdEditor>(html`
74
+ <zn-remarkd-editor value="Hello"></zn-remarkd-editor>`);
75
+ el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
76
+ await el.updateComplete;
77
+
78
+ const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
79
+ input.value = '/';
80
+ input.dispatchEvent(new Event('input', {bubbles: true}));
81
+ await el.updateComplete;
82
+
83
+ expect(el.shadowRoot!.querySelector('.remarkd-editor__slash-menu')).to.exist;
84
+ });
85
+
86
+ it('should start a new block on shift+enter', async () => {
87
+ const el = await fixture<ZnRemarkdEditor>(html`
88
+ <zn-remarkd-editor value="First"></zn-remarkd-editor>`);
89
+ el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
90
+ await el.updateComplete;
91
+
92
+ const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
93
+ input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', shiftKey: true, bubbles: true}));
94
+ await el.updateComplete;
95
+
96
+ // First block committed and rendered; a fresh draft textarea sits below it.
97
+ expect(el.shadowRoot!.querySelectorAll('.remarkd-editor__block').length).to.equal(1);
98
+ const draft = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
99
+ expect(draft).to.exist;
100
+ expect(draft.value).to.equal('');
101
+ });
102
+
103
+ it('should open the image dialog from the toolbar', async () => {
104
+ const el = await fixture<ZnRemarkdEditor>(html`
105
+ <zn-remarkd-editor></zn-remarkd-editor>`);
106
+ const buttons = el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button');
107
+ const imageButton = buttons[buttons.length - 1];
108
+ imageButton.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
109
+ await el.updateComplete;
110
+
111
+ const dialog = el.shadowRoot!.querySelector('.remarkd-editor__image-dialog')!;
112
+ expect(dialog).to.exist;
113
+ // No attachment-url configured — the dialog falls back to a URL input.
114
+ expect(el.shadowRoot!.querySelector('.remarkd-editor__image-url')).to.exist;
115
+ });
116
+
117
+ it('should toggle a checkbox in the source instead of opening the editor', async () => {
118
+ const el = await fixture<ZnRemarkdEditor>(html`
119
+ <zn-remarkd-editor value="- [ ] First
120
+ - [x] Second"></zn-remarkd-editor>`);
121
+ const boxes = el.shadowRoot!.querySelectorAll<HTMLInputElement>('input[type="checkbox"]');
122
+ boxes[0].dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
123
+ await el.updateComplete;
124
+
125
+ expect(el.value).to.equal('- [x] First\n- [x] Second');
126
+ expect(el.shadowRoot!.querySelector('.remarkd-editor__input')).to.not.exist;
127
+ });
128
+
129
+ it('should emit zn-change when a block edit changes the value', async () => {
130
+ const el = await fixture<ZnRemarkdEditor>(html`
131
+ <zn-remarkd-editor value="Hello"></zn-remarkd-editor>`);
132
+ let fired = false;
133
+ el.addEventListener('zn-change', () => {
134
+ fired = true;
135
+ });
136
+
137
+ el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
138
+ await el.updateComplete;
139
+ const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
140
+ input.value = 'Hello world';
141
+ input.dispatchEvent(new Event('input', {bubbles: true}));
142
+ input.dispatchEvent(new Event('blur'));
143
+ await el.updateComplete;
144
+
145
+ expect(fired).to.be.true;
146
+ });
147
+ });
@@ -1109,6 +1109,10 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
1109
1109
  this.updateComplete.then(() => {
1110
1110
  this.emit('zn-input');
1111
1111
  this.emit('zn-change');
1112
+
1113
+ if (this.triggerSubmit) {
1114
+ this.formControlController.submit();
1115
+ }
1112
1116
  });
1113
1117
  }
1114
1118
  }
@@ -260,6 +260,35 @@ describe('<zn-select>', () => {
260
260
  });
261
261
  });
262
262
 
263
+ describe('trigger-submit', () => {
264
+ it('submits the form when a tag is removed', async () => {
265
+ const form = await fixture<HTMLFormElement>(html`
266
+ <form>
267
+ <zn-select multiple trigger-submit value="a b">
268
+ <zn-option value="a">Option 1</zn-option>
269
+ <zn-option value="b">Option 2</zn-option>
270
+ </zn-select>
271
+ </form>
272
+ `);
273
+ const el = form.querySelector<ZnSelect>('zn-select')!;
274
+ await el.updateComplete;
275
+
276
+ let submitted = false;
277
+ form.addEventListener('submit', e => {
278
+ e.preventDefault();
279
+ submitted = true;
280
+ });
281
+
282
+ const removeIcon = el.shadowRoot!.querySelector<HTMLElement>('.select__tags zn-icon[slot="action"]')!;
283
+ removeIcon.click();
284
+ await el.updateComplete;
285
+ await el.updateComplete;
286
+
287
+ expect(el.value).to.deep.equal(['b']);
288
+ expect(submitted).to.be.true;
289
+ });
290
+ });
291
+
263
292
  describe('free-text', () => {
264
293
  const type = (el: ZnSelect, text: string) => {
265
294
  const displayInput = el.shadowRoot!.querySelector<HTMLInputElement>('.select__display-input')!;
@@ -36,6 +36,10 @@
36
36
  height: 100%;
37
37
  margin: var(--zn-default-margin, 0 auto);
38
38
 
39
+ &--divide {
40
+ gap: calc(var(--zn-sp-gap) * 2);
41
+ }
42
+
39
43
  &--divide.sp--no-gap {
40
44
  ::slotted(*:not(:last-child)) {
41
45
  border-bottom: 1px solid rgb(var(--zn-border-color)) !important;
@@ -54,7 +58,7 @@
54
58
  left: 0;
55
59
  width: 100%;
56
60
  max-width: 100%;
57
- bottom: calc((var(--zn-sp-gap) / 2) * -1);
61
+ bottom: calc((var(--zn-sp-gap) / 1) * -1);
58
62
  background-color: rgb(var(--zn-border-color));
59
63
  }
60
64
  }
@@ -10,6 +10,11 @@ import ZincElement from '../../internal/zinc-element';
10
10
 
11
11
  import styles from './tabs.scss';
12
12
 
13
+ // Every element that manages its own tabs (ZnTabs and its subclasses). Tab
14
+ // registration must stop at these so an outer container never claims a nested
15
+ // container's tabs.
16
+ const tabContainerSelector = 'zn-tabs, zn-page, zn-page-nav';
17
+
13
18
  /**
14
19
  * @summary Short summary of the component's intended use.
15
20
  * @documentation https://zinc.style/components/tabs
@@ -228,7 +233,7 @@ export default class ZnTabs extends ZincElement {
228
233
  }
229
234
  }
230
235
 
231
- for (const uriTab of deepQuerySelectorAll("[tab-uri]", this, '')) {
236
+ for (const uriTab of deepQuerySelectorAll("[tab-uri]", this, tabContainerSelector)) {
232
237
  const uri: string = uriTab.getAttribute("tab-uri")!;
233
238
  const eleTabId = this._uriToId(uri);
234
239
  if (eleTabId === tabId) {
@@ -459,11 +464,11 @@ export default class ZnTabs extends ZincElement {
459
464
  }
460
465
 
461
466
  _registerTabs = () => {
462
- deepQuerySelectorAll('[tab]', this, 'zn-tabs').forEach(ele => {
467
+ deepQuerySelectorAll('[tab]', this, tabContainerSelector).forEach(ele => {
463
468
  this._addTab(ele as HTMLElement);
464
469
  });
465
470
 
466
- deepQuerySelectorAll('[tab-uri]', this, 'zn-tabs').forEach(ele => {
471
+ deepQuerySelectorAll('[tab-uri]', this, tabContainerSelector).forEach(ele => {
467
472
  if (ele.getAttribute('tab-uri') === "") {
468
473
  ele.setAttribute('tab', "");
469
474
  ele.removeAttribute('tab-uri');
@@ -471,7 +476,7 @@ export default class ZnTabs extends ZincElement {
471
476
  this._addTab(ele as HTMLElement);
472
477
  });
473
478
 
474
- deepQuerySelectorAll('[ref-tab-uri]', this, 'zn-tabs').forEach(ele => {
479
+ deepQuerySelectorAll('[ref-tab-uri]', this, tabContainerSelector).forEach(ele => {
475
480
  if (!ele.hasAttribute('ref-tab')) {
476
481
  ele.setAttribute('ref-tab', this._uriToId(ele.getAttribute('ref-tab-uri')!));
477
482
  ele.removeAttribute('ref-tab-uri');
@@ -1,5 +1,6 @@
1
1
  import '../../../dist/zn.min.js';
2
- import { expect, fixture, html } from '@open-wc/testing';
2
+ import { aTimeout, expect, fixture, html } from '@open-wc/testing';
3
+ import type ZnTabs from './tabs.component';
3
4
 
4
5
  describe('<zn-tabs>', () => {
5
6
  it('should render a component', async () => {
@@ -7,4 +8,44 @@ describe('<zn-tabs>', () => {
7
8
 
8
9
  expect(el).to.exist;
9
10
  });
11
+
12
+ it('does not register tabs belonging to a nested zn-page', async () => {
13
+ const el = await fixture<ZnTabs>(html`
14
+ <zn-tabs>
15
+ <div class="outer-home">Outer Home</div>
16
+ <div id="outer-second">
17
+ <zn-page caption="Nested Page">
18
+ <zn-tab caption="Overview">Overview Content</zn-tab>
19
+ <zn-tab caption="Details">Details Content</zn-tab>
20
+ </zn-page>
21
+ </div>
22
+ </zn-tabs>
23
+ `);
24
+ await aTimeout(60);
25
+
26
+ el.setActiveTab('outer-second', false, false);
27
+ await aTimeout(20);
28
+
29
+ // A menu selection bubbling out of the page (e.g. an ellipses action menu)
30
+ // makes the outer tabs re-register; it must not claim the page's nav items.
31
+ const page = el.querySelector('zn-page')!;
32
+ page.dispatchEvent(new CustomEvent('zn-menu-select', { bubbles: true, composed: true }));
33
+ await aTimeout(250);
34
+
35
+ const navbar = page.shadowRoot!.querySelector('zn-navbar')!;
36
+ const navItems = Array.from(navbar.shadowRoot!.querySelectorAll<HTMLElement>('li:not(.more)'));
37
+
38
+ navItems[1].click();
39
+ await aTimeout(40);
40
+ navItems[0].click();
41
+ await aTimeout(40);
42
+
43
+ const outerHome = el.querySelector('.outer-home')!;
44
+ const outerSecond = el.querySelector('#outer-second')!;
45
+ expect(outerSecond.hasAttribute('selected')).to.equal(true);
46
+ expect(outerHome.hasAttribute('selected')).to.equal(false);
47
+
48
+ const selectedPagePanel = page.shadowRoot!.querySelector<HTMLElement>('#content > div[selected]')!;
49
+ expect(selectedPagePanel.id).to.equal('');
50
+ });
10
51
  });
@@ -25,8 +25,12 @@
25
25
  padding-inline: var(--zn-base-gap);
26
26
  }
27
27
 
28
+ &--has-description#{&}--has-caption {
29
+ padding-block: var(--zn-spacing-small);
30
+ }
31
+
28
32
  &--has-image {
29
- padding: var(--zn-base-gap);
33
+ padding: var(--zn-spacing-small);
30
34
  }
31
35
 
32
36
  &--has-href {
@@ -36,7 +36,7 @@ export default class ZnToggle extends ZincElement implements ZincFormControl {
36
36
  private readonly hasSlotController = new HasSlotController(this, 'help-text', 'description');
37
37
 
38
38
  private readonly formControlController = new FormControlController(this, {
39
- value: (control: ZnToggle) => (control.checked ? control.value || 'on' : undefined),
39
+ value: (control: ZnToggle) => (control.checked ? control.value || 'on' : control.fallbackValue),
40
40
  defaultValue: (control: ZnToggle) => control.defaultChecked,
41
41
  setValue: (control: ZnToggle, checked: boolean) => (control.checked = checked)
42
42
  });
@@ -51,7 +51,8 @@ export default class ZnToggle extends ZincElement implements ZincFormControl {
51
51
 
52
52
  @property() value: string;
53
53
 
54
- @property({ attribute: 'fallback' }) fallbackValue: string = '';
54
+ /** The value submitted when the toggle is unchecked, so the toggle always submits a value. */
55
+ @property({ attribute: 'fallback' }) fallbackValue: string = 'off';
55
56
 
56
57
  @property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
57
58
 
@@ -167,15 +168,6 @@ export default class ZnToggle extends ZincElement implements ZincFormControl {
167
168
  // End Public API methods
168
169
 
169
170
  render() {
170
- let fallback = html``;
171
- if (this.fallbackValue !== '') {
172
- fallback = html`
173
- <input type="hidden"
174
- name=${this.name}
175
- value=${this.fallbackValue}
176
- />`;
177
- }
178
-
179
171
  const tooltipContent = this.checked ? this.onText : this.offText;
180
172
  const showTooltip = !!tooltipContent;
181
173
  const hasHelpText = this.helpText ? true : this.hasSlotController.test('help-text');
@@ -183,7 +175,6 @@ export default class ZnToggle extends ZincElement implements ZincFormControl {
183
175
 
184
176
  const toggle = html`
185
177
  <div class="switch__input-wrapper" part="base">
186
- ${fallback}
187
178
  <input
188
179
  class="switch__input"
189
180
  type="checkbox"
@@ -18,4 +18,33 @@ describe('<zn-toggle>', () => {
18
18
  expect(description.textContent).to.include('Receive an email when mentioned.');
19
19
  expect(label.nextElementSibling).to.equal(description);
20
20
  });
21
+
22
+ describe('form submission', () => {
23
+ it('should submit the value when checked', async () => {
24
+ const form = await fixture<HTMLFormElement>(html`
25
+ <form>
26
+ <zn-toggle name="feature" value="enabled" checked></zn-toggle>
27
+ </form>`);
28
+
29
+ expect(new FormData(form).get('feature')).to.equal('enabled');
30
+ });
31
+
32
+ it('should submit "off" when unchecked', async () => {
33
+ const form = await fixture<HTMLFormElement>(html`
34
+ <form>
35
+ <zn-toggle name="feature" value="enabled"></zn-toggle>
36
+ </form>`);
37
+
38
+ expect(new FormData(form).get('feature')).to.equal('off');
39
+ });
40
+
41
+ it('should submit the fallback value when unchecked', async () => {
42
+ const form = await fixture<HTMLFormElement>(html`
43
+ <form>
44
+ <zn-toggle name="feature" value="enabled" fallback="disabled"></zn-toggle>
45
+ </form>`);
46
+
47
+ expect(new FormData(form).get('feature')).to.equal('disabled');
48
+ });
49
+ });
21
50
  });
@@ -1,4 +1,4 @@
1
- export type ZnErrorEvent = CustomEvent<{ status?: number }>;
1
+ export type ZnErrorEvent = CustomEvent<{ status?: number; message?: string }>;
2
2
 
3
3
  declare global {
4
4
  interface GlobalEventHandlersEventMap {
package/src/zinc.ts CHANGED
@@ -101,6 +101,7 @@ export { default as TranslationGroup } from './components/translation-group';
101
101
  export { default as OptGroup } from './components/opt-group';
102
102
  export { default as PriorityList } from './components/priority-list';
103
103
  export { default as MarkdownEditor } from './components/markdown-editor';
104
+ export { default as RemarkdEditor } from './components/remarkd-editor';
104
105
  export { default as FlowBuilder } from './components/flow-builder';
105
106
  export { default as FlowSteps } from './components/flow-builder/modules/flow-steps';
106
107
  export { default as FlowStepGroup } from './components/flow-builder/modules/flow-step-group';
@@ -109,6 +110,7 @@ export { default as FlowBranchConditions } from './components/flow-builder/modul
109
110
  export { default as PageBuilder } from './components/page-builder';
110
111
  export { default as PagePaletteItem } from './components/page-builder/modules/page-palette-item';
111
112
  export { default as PageSectionCard } from './components/page-builder/modules/page-section-card';
113
+ export { default as PreviewFrame } from './components/preview-frame';
112
114
  /* plop:component */
113
115
 
114
116
  // Base Component