@kubex/zinc 1.1.85 → 1.1.87
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 +566 -546
- package/dist/vscode.html-custom-data.json +44 -44
- package/dist/web-types.json +93 -93
- package/dist/zn.d.ts +9 -1
- package/dist/zn.min.js +3 -2
- package/package.json +1 -1
- package/src/components/preview-frame/preview-frame.component.ts +25 -25
- package/src/components/preview-frame/preview-frame.test.ts +48 -0
- package/src/components/slash-menu/slash-menu.component.ts +23 -5
- package/src/components/slash-menu/slash-menu.scss +2 -2
|
@@ -62,6 +62,11 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
62
62
|
* refresh the preview. These are never intercepted: the shell submits them
|
|
63
63
|
* (so its own response handling — alerts, refreshes — runs as normal) and the
|
|
64
64
|
* preview re-fetches its config once the shell reports the save complete.
|
|
65
|
+
* Matched by delegation on the shell's bubbled `complete` event rather than
|
|
66
|
+
* by attaching to the forms themselves, so a save anywhere on the page
|
|
67
|
+
* refreshes the preview — including forms in a different DOM root, e.g. a
|
|
68
|
+
* page-level form saved while the preview sits inside a tab panel's shadow
|
|
69
|
+
* root (a page's Template select lives on one tab, its preview on another).
|
|
65
70
|
* Set empty to disable.
|
|
66
71
|
*/
|
|
67
72
|
@property({attribute: 'refresh-on'}) refreshOn = 'form';
|
|
@@ -141,8 +146,13 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
141
146
|
private _contentObserver: ResizeObserver | undefined;
|
|
142
147
|
|
|
143
148
|
private readonly _watchedForms = new Set<HTMLFormElement>();
|
|
144
|
-
private readonly _refreshForms = new Set<HTMLFormElement>();
|
|
145
149
|
private readonly _debounceTimers = new Map<HTMLFormElement, number>();
|
|
150
|
+
// 'complete' delegation targets: the document always, plus the component's
|
|
151
|
+
// own shadow root when it has one (a non-composed event never reaches the
|
|
152
|
+
// document). A composed event inside the shadow root hits both, so the
|
|
153
|
+
// handler dedupes by event object.
|
|
154
|
+
private _shellSaveRoots: EventTarget[] = [];
|
|
155
|
+
private _lastShellSave: Event | undefined;
|
|
146
156
|
|
|
147
157
|
// Forms are siblings in light DOM and get replaced when other content
|
|
148
158
|
// re-renders; re-resolve them whenever the surrounding DOM changes.
|
|
@@ -158,6 +168,8 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
158
168
|
window.addEventListener('message', this._onMessage);
|
|
159
169
|
this._attachForms();
|
|
160
170
|
const root = this.getRootNode();
|
|
171
|
+
this._shellSaveRoots = root instanceof ShadowRoot ? [document, root] : [document];
|
|
172
|
+
this._shellSaveRoots.forEach(target => target.addEventListener('complete', this._onShellSave));
|
|
161
173
|
if (root instanceof Document) {
|
|
162
174
|
this._formObserver.observe(root.body);
|
|
163
175
|
} else if (root instanceof ShadowRoot) {
|
|
@@ -178,8 +190,8 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
178
190
|
this._contentObserver = undefined;
|
|
179
191
|
this._watchedForms.forEach(form => this._detachForm(form));
|
|
180
192
|
this._watchedForms.clear();
|
|
181
|
-
this.
|
|
182
|
-
this.
|
|
193
|
+
this._shellSaveRoots.forEach(target => target.removeEventListener('complete', this._onShellSave));
|
|
194
|
+
this._shellSaveRoots = [];
|
|
183
195
|
}
|
|
184
196
|
|
|
185
197
|
private readonly _onMessage = (e: MessageEvent) => {
|
|
@@ -315,30 +327,18 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
315
327
|
form.addEventListener('zn-input', this._onChange);
|
|
316
328
|
form.addEventListener('change', this._onChange);
|
|
317
329
|
});
|
|
318
|
-
|
|
319
|
-
// Shell-saved forms: the shell fires 'complete' on the form it submitted.
|
|
320
|
-
const refreshMatched = new Set<HTMLFormElement>();
|
|
321
|
-
if (this.refreshOn) {
|
|
322
|
-
root.querySelectorAll(this.refreshOn).forEach(node => {
|
|
323
|
-
if (node instanceof HTMLFormElement && !matched.has(node)) refreshMatched.add(node);
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
this._refreshForms.forEach(form => {
|
|
328
|
-
if (!refreshMatched.has(form)) {
|
|
329
|
-
form.removeEventListener('complete', this._onShellSave);
|
|
330
|
-
this._refreshForms.delete(form);
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
refreshMatched.forEach(form => {
|
|
335
|
-
if (this._refreshForms.has(form)) return;
|
|
336
|
-
this._refreshForms.add(form);
|
|
337
|
-
form.addEventListener('complete', this._onShellSave);
|
|
338
|
-
});
|
|
339
330
|
}
|
|
340
331
|
|
|
341
|
-
|
|
332
|
+
// Shell-saved forms: the shell fires a bubbling 'complete' on the form it
|
|
333
|
+
// submitted. composedPath()[0] recovers the form when the event was
|
|
334
|
+
// retargeted crossing a shadow boundary on its way to the document.
|
|
335
|
+
private readonly _onShellSave = (e: Event) => {
|
|
336
|
+
if (e === this._lastShellSave) return;
|
|
337
|
+
this._lastShellSave = e;
|
|
338
|
+
if (!this.refreshOn) return;
|
|
339
|
+
const origin = e.composedPath()[0];
|
|
340
|
+
if (!(origin instanceof HTMLFormElement) || !origin.matches(this.refreshOn)) return;
|
|
341
|
+
if (this._watchedForms.has(origin)) return; // auto-saved forms refresh via _save
|
|
342
342
|
void this._sendConfig();
|
|
343
343
|
};
|
|
344
344
|
|
|
@@ -248,6 +248,54 @@ describe('<zn-preview-frame>', () => {
|
|
|
248
248
|
expect(posted[0]['type']).to.equal('hp-preview:config');
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
+
// A uri tab panel puts the preview in the tabs component's shadow root while
|
|
252
|
+
// page-level forms (e.g. the page's Template select) stay in the document —
|
|
253
|
+
// a save out there must still refresh the preview.
|
|
254
|
+
it('refreshes on a shell save of a form outside the component\'s own root', async () => {
|
|
255
|
+
const wrapper = await fixture(html`
|
|
256
|
+
<div>
|
|
257
|
+
<form action="/save" method="post"><input name="template" value="flow"></form>
|
|
258
|
+
<div id="host"></div>
|
|
259
|
+
</div>`);
|
|
260
|
+
|
|
261
|
+
const shadow = wrapper.querySelector('#host')!.attachShadow({mode: 'open'});
|
|
262
|
+
const el = document.createElement('zn-preview-frame');
|
|
263
|
+
el.setAttribute('src', 'about:blank');
|
|
264
|
+
el.setAttribute('frame-origin', 'https://site.example');
|
|
265
|
+
el.setAttribute('data-uri', '/payload');
|
|
266
|
+
shadow.appendChild(el);
|
|
267
|
+
await (el as HTMLElement & {updateComplete: Promise<boolean>}).updateComplete;
|
|
268
|
+
|
|
269
|
+
wrapper.querySelector('form')!.dispatchEvent(
|
|
270
|
+
new CustomEvent('complete', {bubbles: true, detail: {}}));
|
|
271
|
+
|
|
272
|
+
await waitUntil(() => fetchCalls.length === 1);
|
|
273
|
+
expect(fetchCalls[0].uri).to.equal('/payload');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('refreshes once for a composed complete event inside its own shadow root', async () => {
|
|
277
|
+
const wrapper = await fixture(html`<div><div id="host"></div></div>`);
|
|
278
|
+
|
|
279
|
+
const shadow = wrapper.querySelector('#host')!.attachShadow({mode: 'open'});
|
|
280
|
+
const form = document.createElement('form');
|
|
281
|
+
form.setAttribute('action', '/save');
|
|
282
|
+
shadow.appendChild(form);
|
|
283
|
+
const el = document.createElement('zn-preview-frame');
|
|
284
|
+
el.setAttribute('src', 'about:blank');
|
|
285
|
+
el.setAttribute('frame-origin', 'https://site.example');
|
|
286
|
+
el.setAttribute('data-uri', '/payload');
|
|
287
|
+
shadow.appendChild(el);
|
|
288
|
+
await (el as HTMLElement & {updateComplete: Promise<boolean>}).updateComplete;
|
|
289
|
+
|
|
290
|
+
// the shell fires composed events, so this reaches both the shadow root
|
|
291
|
+
// and the document — it must trigger a single refresh
|
|
292
|
+
form.dispatchEvent(new CustomEvent('complete', {bubbles: true, composed: true, detail: {}}));
|
|
293
|
+
|
|
294
|
+
await waitUntil(() => fetchCalls.length === 1);
|
|
295
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
296
|
+
expect(fetchCalls.length).to.equal(1);
|
|
297
|
+
});
|
|
298
|
+
|
|
251
299
|
it('does not intercept the submit of a refresh-on form', async () => {
|
|
252
300
|
const wrapper = await fixture(html`
|
|
253
301
|
<div>
|
|
@@ -137,6 +137,21 @@ export default class ZnSlashMenu extends ZincElement {
|
|
|
137
137
|
disconnectedCallback() {
|
|
138
138
|
super.disconnectedCallback();
|
|
139
139
|
this.stopPositioner();
|
|
140
|
+
this.hidePanelPopover();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private showPanelPopover() {
|
|
144
|
+
const panel = this.panel;
|
|
145
|
+
if (typeof panel?.showPopover === 'function' && !panel.matches(':popover-open')) {
|
|
146
|
+
panel.showPopover();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private hidePanelPopover() {
|
|
151
|
+
const panel = this.panel;
|
|
152
|
+
if (typeof panel?.hidePopover === 'function' && panel.matches(':popover-open')) {
|
|
153
|
+
panel.hidePopover();
|
|
154
|
+
}
|
|
140
155
|
}
|
|
141
156
|
|
|
142
157
|
private startPositioner() {
|
|
@@ -156,6 +171,8 @@ export default class ZnSlashMenu extends ZincElement {
|
|
|
156
171
|
const {anchor, panel} = this;
|
|
157
172
|
if (!this.open || !anchor || !panel) return;
|
|
158
173
|
|
|
174
|
+
this.showPanelPopover();
|
|
175
|
+
|
|
159
176
|
const {x, y} = await computePosition(anchor, panel, {
|
|
160
177
|
placement: this.placement,
|
|
161
178
|
strategy: 'fixed',
|
|
@@ -233,20 +250,20 @@ export default class ZnSlashMenu extends ZincElement {
|
|
|
233
250
|
protected updated(changed: PropertyValues) {
|
|
234
251
|
super.updated(changed);
|
|
235
252
|
|
|
236
|
-
// A new list, or a reopen, starts at the top rather than wherever the last one was scrolled to
|
|
237
|
-
if (changed.has('items') || (changed.has('open') && this.open)) {
|
|
238
|
-
this.scrollActiveIntoView();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
253
|
if (changed.has('open') || changed.has('anchor')) {
|
|
242
254
|
if (this.open) {
|
|
243
255
|
this.startPositioner();
|
|
244
256
|
} else {
|
|
245
257
|
this.stopPositioner();
|
|
258
|
+
this.hidePanelPopover();
|
|
246
259
|
}
|
|
247
260
|
}
|
|
248
261
|
|
|
249
262
|
if (this.open) void this.position();
|
|
263
|
+
|
|
264
|
+
if (changed.has('items') || (changed.has('open') && this.open)) {
|
|
265
|
+
this.scrollActiveIntoView();
|
|
266
|
+
}
|
|
250
267
|
}
|
|
251
268
|
|
|
252
269
|
private renderItem(item: SlashMenuItem, index: number, showIcons: boolean) {
|
|
@@ -310,6 +327,7 @@ export default class ZnSlashMenu extends ZincElement {
|
|
|
310
327
|
<div
|
|
311
328
|
part="panel"
|
|
312
329
|
class="slash-menu__panel"
|
|
330
|
+
popover="manual"
|
|
313
331
|
role="listbox"
|
|
314
332
|
aria-hidden=${this.open ? 'false' : 'true'}
|
|
315
333
|
aria-label=${this.query ? `Matches for ${this.query}` : this.heading}>
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
display: flex;
|
|
16
16
|
flex-direction: column;
|
|
17
17
|
position: fixed;
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
inset: 0 auto auto 0;
|
|
19
|
+
margin: 0;
|
|
20
20
|
width: var(--slash-menu-width);
|
|
21
21
|
max-width: var(--auto-size-available-width, none);
|
|
22
22
|
max-height: min(var(--slash-menu-max-height), var(--auto-size-available-height, 100vh));
|