@kubex/zinc 1.1.75 → 1.1.77
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 +4 -4
- package/dist/vscode.html-custom-data.json +1 -1
- package/dist/web-types.json +3 -3
- package/dist/zn.d.ts +8 -1
- package/dist/zn.min.js +76 -72
- package/docs/pages/components/preview-frame.md +14 -6
- package/docs/pages/components/theme-editor.md +4 -0
- package/package.json +1 -1
- package/src/components/content-block/content-block.component.ts +2 -3
- package/src/components/content-block/content-block.test.ts +26 -2
- package/src/components/preview-frame/preview-frame.component.ts +12 -2
- package/src/components/preview-frame/preview-frame.test.ts +38 -0
- package/src/components/theme-editor/theme-editor.component.ts +7 -4
- package/src/components/theme-editor/theme-editor.scss +4 -2
- package/src/components/theme-editor/theme-editor.test.ts +16 -0
|
@@ -56,7 +56,13 @@ handlers can't be cancelled from out here — blocking pointer input is the only
|
|
|
56
56
|
way to stop them, and hover goes with it. Scrolling doesn't: an overflowing page
|
|
57
57
|
is scrolled by the panel instead, as below.
|
|
58
58
|
|
|
59
|
-
Set `interactive` when the embed is meant to be used rather than looked at.
|
|
59
|
+
Set `interactive` when the embed is meant to be used rather than looked at. The
|
|
60
|
+
frame then behaves as a viewport: it stays the panel's own height and the embed
|
|
61
|
+
scrolls itself, so there's a single scrollbar and the embed's `100vh`,
|
|
62
|
+
`position: fixed` and sticky content size to what's actually on screen. The
|
|
63
|
+
[reported content height](#overflowing-content) is ignored while `interactive`
|
|
64
|
+
is set — it exists to make an *inert* frame's overflow reachable, which an
|
|
65
|
+
interactive one does for itself.
|
|
60
66
|
|
|
61
67
|
```html:preview
|
|
62
68
|
<zn-preview-frame
|
|
@@ -73,11 +79,13 @@ Set `interactive` when the embed is meant to be used rather than looked at.
|
|
|
73
79
|
|
|
74
80
|
## Overflowing Content
|
|
75
81
|
|
|
76
|
-
|
|
77
|
-
frame can't do it itself: a cross-origin document can't be scrolled
|
|
78
|
-
(`contentWindow.scrollTo` is blocked), and with pointer input off
|
|
79
|
-
reaches it anyway. So the frame is instead laid out at its full
|
|
80
|
-
nothing scrolls inside it — and the panel scrolls that.
|
|
82
|
+
An inert page taller than the panel is scrolled by the panel, not inside the
|
|
83
|
+
frame. The frame can't do it itself: a cross-origin document can't be scrolled
|
|
84
|
+
from the host (`contentWindow.scrollTo` is blocked), and with pointer input off
|
|
85
|
+
the wheel never reaches it anyway. So the frame is instead laid out at its full
|
|
86
|
+
content height — nothing scrolls inside it — and the panel scrolls that. An
|
|
87
|
+
[`interactive`](#interactivity) frame doesn't need any of this and opts out of
|
|
88
|
+
it: it keeps the panel's height and the embed scrolls itself.
|
|
81
89
|
|
|
82
90
|
For the frame to be sized that way, the embed reports its height alongside
|
|
83
91
|
`hp-preview:rendered`:
|
|
@@ -24,6 +24,10 @@ Set `controls-caption` and `preview-caption` to label each column's header
|
|
|
24
24
|
row — both are empty by default, rendering no text (the controls column's
|
|
25
25
|
header row still renders either way, so the two columns stay aligned).
|
|
26
26
|
|
|
27
|
+
The preview is [interactive](/components/preview-frame/#interactivity): the
|
|
28
|
+
embedded page can be clicked and hovered, and it scrolls itself, so the preview
|
|
29
|
+
column never adds a second scrollbar beside the embed's own.
|
|
30
|
+
|
|
27
31
|
The preview always fills its column, leaving no dead space beneath it.
|
|
28
32
|
`min-height` (default `480`) is a floor for that column, not a fixed height —
|
|
29
33
|
it's still forwarded to the [preview frame](/components/preview-frame/), which
|
package/package.json
CHANGED
|
@@ -4,7 +4,6 @@ import {HasSlotController} from "../../internal/slot";
|
|
|
4
4
|
import {html, unsafeCSS} from 'lit';
|
|
5
5
|
import {MutationController} from '@lit-labs/observers/mutation-controller.js';
|
|
6
6
|
import {property, queryAssignedNodes, queryAsync, state} from 'lit/decorators.js';
|
|
7
|
-
import {unsafeHTML} from 'lit-html/directives/unsafe-html.js';
|
|
8
7
|
import ZincElement from "../../internal/zinc-element";
|
|
9
8
|
import type {PropertyValues} from 'lit';
|
|
10
9
|
import type ZnTile from "../tile";
|
|
@@ -280,7 +279,7 @@ export default class ContentBlock extends ZincElement {
|
|
|
280
279
|
'text-section--text': section.type === 'text',
|
|
281
280
|
'hidden': section.type === 'reply'
|
|
282
281
|
})}>
|
|
283
|
-
${section.lines.map((line) => html`${
|
|
282
|
+
${section.lines.map((line) => html`${line}<br>`)}
|
|
284
283
|
</div>
|
|
285
284
|
`)}
|
|
286
285
|
</div>
|
|
@@ -375,7 +374,7 @@ export default class ContentBlock extends ZincElement {
|
|
|
375
374
|
const textRows: TextRow[] = [];
|
|
376
375
|
|
|
377
376
|
if (textContent) {
|
|
378
|
-
const text = textContent.innerText;
|
|
377
|
+
const text = textContent.innerText.replace(/<br\s*\/?>/gi, '\n');
|
|
379
378
|
const rows = text.split('\n');
|
|
380
379
|
let previousType: TextRow['type'] | null = null;
|
|
381
380
|
let forceReply = false;
|
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
-
import { expect, fixture
|
|
2
|
+
import { expect, fixture } from '@open-wc/testing';
|
|
3
|
+
import type { LitElement } from 'lit';
|
|
4
|
+
|
|
5
|
+
const textSection = async (body: string) => {
|
|
6
|
+
const el = await fixture<LitElement>(
|
|
7
|
+
`<zn-content-block><div slot="text">${body}</div></zn-content-block>`
|
|
8
|
+
);
|
|
9
|
+
await el.updateComplete;
|
|
10
|
+
return el.shadowRoot!.querySelector<HTMLDivElement>('.text-content')!;
|
|
11
|
+
};
|
|
3
12
|
|
|
4
13
|
describe('<zn-content-block>', () => {
|
|
5
14
|
it('should render a component', async () => {
|
|
6
|
-
const el = await fixture(
|
|
15
|
+
const el = await fixture('<zn-content-block></zn-content-block>');
|
|
7
16
|
|
|
8
17
|
expect(el).to.exist;
|
|
9
18
|
});
|
|
19
|
+
|
|
20
|
+
it('should render markup in the text body as text', async () => {
|
|
21
|
+
const content = await textSection('<img src="x" onerror="window.__xss = true"> hello');
|
|
22
|
+
|
|
23
|
+
expect(content.querySelector('img')).to.be.null;
|
|
24
|
+
expect(content.textContent).to.contain('<img src="x" onerror="window.__xss = true"> hello');
|
|
25
|
+
expect((window as unknown as Record<string, unknown>).__xss).to.be.undefined;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should treat escaped breaks in the text body as line breaks', async () => {
|
|
29
|
+
const content = await textSection('line1<br />line2');
|
|
30
|
+
|
|
31
|
+
expect(content.textContent).to.not.contain('<br');
|
|
32
|
+
expect(content.querySelectorAll('br').length).to.be.greaterThan(1);
|
|
33
|
+
});
|
|
10
34
|
});
|
|
@@ -111,6 +111,12 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
111
111
|
* be reached from here to cancel its own handlers, so this blocks pointer
|
|
112
112
|
* input entirely — hover goes with it. Scrolling doesn't: an overflowing
|
|
113
113
|
* page is scrolled by the panel rather than by the frame (see _contentHeight).
|
|
114
|
+
*
|
|
115
|
+
* Set, the frame becomes a real viewport instead: it stays the panel's own
|
|
116
|
+
* height and the embed scrolls itself, so there is one scrollbar rather than
|
|
117
|
+
* a panel scrolling an oversized frame, and the embed's viewport-relative
|
|
118
|
+
* layout (`100vh`, `position: fixed`, sticky headers) sizes to what's on
|
|
119
|
+
* screen. _contentHeight is ignored while this is set.
|
|
114
120
|
*/
|
|
115
121
|
@property({type: Boolean, reflect: true}) interactive = false;
|
|
116
122
|
|
|
@@ -125,7 +131,8 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
125
131
|
* height rather than the panel's, so the page never scrolls inside the frame
|
|
126
132
|
* — the panel scrolls instead, which is what makes an overflowing preview
|
|
127
133
|
* reachable while pointer input to the frame is blocked. 0 = unknown, and the
|
|
128
|
-
* frame falls back to filling the panel.
|
|
134
|
+
* frame falls back to filling the panel. Kept up to date either way, but only
|
|
135
|
+
* laid out when the frame is inert: an `interactive` frame scrolls itself.
|
|
129
136
|
*/
|
|
130
137
|
@state() private _contentHeight = 0;
|
|
131
138
|
|
|
@@ -396,7 +403,10 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
396
403
|
// transformed back down, so the frame fills the panel while the content
|
|
397
404
|
// renders smaller and more of the page is visible. Percentage width means
|
|
398
405
|
// nothing is measured — no layout feedback loop.
|
|
399
|
-
|
|
406
|
+
// Growing the frame past the panel is the workaround for an inert frame not
|
|
407
|
+
// being scrollable; interactive, that trades one scrollbar for two and hands
|
|
408
|
+
// the embed a viewport taller than the panel it's shown in.
|
|
409
|
+
const content = this.error || this.interactive ? 0 : this._contentHeight;
|
|
400
410
|
const iframeStyles = this.fill
|
|
401
411
|
? {width: '100%', height: content ? `max(${content}px, 100%)` : '100%'}
|
|
402
412
|
: {
|
|
@@ -683,6 +683,44 @@ describe('<zn-preview-frame>', () => {
|
|
|
683
683
|
const stage = el.shadowRoot!.querySelector<HTMLDivElement>('.preview__stage')!;
|
|
684
684
|
expect(stage.style.height).to.equal('max(1500px, 100%)');
|
|
685
685
|
});
|
|
686
|
+
|
|
687
|
+
it('leaves an interactive frame at the panel height, so the embed scrolls itself', async () => {
|
|
688
|
+
const el = await fixture(html`
|
|
689
|
+
<zn-preview-frame
|
|
690
|
+
src="about:blank"
|
|
691
|
+
frame-origin="https://site.example"
|
|
692
|
+
data-uri="/payload"
|
|
693
|
+
fill
|
|
694
|
+
interactive></zn-preview-frame>`);
|
|
695
|
+
|
|
696
|
+
const iframe = el.shadowRoot!.querySelector('iframe')!;
|
|
697
|
+
const panel = el.shadowRoot!.querySelector<HTMLDivElement>('.preview')!;
|
|
698
|
+
const stage = el.shadowRoot!.querySelector<HTMLDivElement>('.preview__stage')!;
|
|
699
|
+
|
|
700
|
+
reportHeight(el, 1500);
|
|
701
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
702
|
+
|
|
703
|
+
expect(iframe.style.height).to.equal('100%');
|
|
704
|
+
expect(stage.style.height).to.equal('');
|
|
705
|
+
expect(panel.scrollHeight).to.equal(panel.clientHeight);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('inflates the frame again when interactive is turned off', async () => {
|
|
709
|
+
const el = await fixture(html`
|
|
710
|
+
<zn-preview-frame
|
|
711
|
+
src="about:blank"
|
|
712
|
+
frame-origin="https://site.example"
|
|
713
|
+
data-uri="/payload"
|
|
714
|
+
interactive></zn-preview-frame>`) as HTMLElement & {interactive: boolean};
|
|
715
|
+
const iframe = el.shadowRoot!.querySelector('iframe')!;
|
|
716
|
+
|
|
717
|
+
reportHeight(el, 1400);
|
|
718
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
719
|
+
expect(iframe.style.height).to.equal('480px');
|
|
720
|
+
|
|
721
|
+
el.interactive = false;
|
|
722
|
+
await waitUntil(() => iframe.style.height === '1400px', 'kept the reported height unused');
|
|
723
|
+
});
|
|
686
724
|
});
|
|
687
725
|
|
|
688
726
|
it('skips the config fetch when data-uri is empty', async () => {
|
|
@@ -840,12 +840,14 @@ export default class ZnThemeEditor extends ZincElement {
|
|
|
840
840
|
<slot @slotchange="${this._onSlotChange}"></slot>
|
|
841
841
|
${this._hasNestedGroups()
|
|
842
842
|
? this._renderTabs(section => html`
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
843
|
+
${this._hasAssignedControls(section.name) ? html`
|
|
844
|
+
<slot name="${section.name}" class="editor__section-slot"
|
|
845
|
+
@slotchange="${this._onSlotChange}"></slot>` : nothing}
|
|
846
|
+
${this._renderGroups(section)}`)
|
|
846
847
|
: this.sectionLayout === 'tabs'
|
|
847
848
|
? this._renderTabs(section => html`
|
|
848
|
-
|
|
849
|
+
<slot name="${section.name}" class="editor__section-slot"
|
|
850
|
+
@slotchange="${this._onSlotChange}"></slot>`)
|
|
849
851
|
: this._renderSections()}
|
|
850
852
|
</div>
|
|
851
853
|
${this.hasSlotController.test('footer') ? html`
|
|
@@ -910,6 +912,7 @@ export default class ZnThemeEditor extends ZincElement {
|
|
|
910
912
|
device="${this.device}"
|
|
911
913
|
min-height="${this.minHeight}"
|
|
912
914
|
fill
|
|
915
|
+
interactive
|
|
913
916
|
backdrop="${this.standalone ? 'panel' : 'dots'}"
|
|
914
917
|
exportparts="base:preview__base,stage:preview__stage,iframe:preview__iframe,error:preview__error"
|
|
915
918
|
@zn-error="${this._onFrameError}"></zn-preview-frame>
|
|
@@ -180,9 +180,11 @@
|
|
|
180
180
|
margin-top: auto;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
//
|
|
183
|
+
// Holds the preview below the toolbar, and never scrolls: the frame is its own
|
|
184
|
+
// scroll region (interactive, the embed is), so a scroller here would only ever
|
|
185
|
+
// be a second scrollbar beside that one.
|
|
184
186
|
.editor__preview {
|
|
185
|
-
overflow:
|
|
187
|
+
overflow: hidden;
|
|
186
188
|
flex: 1 1 auto;
|
|
187
189
|
min-width: 0;
|
|
188
190
|
min-height: 0;
|
|
@@ -729,6 +729,22 @@ describe('<zn-theme-editor>', () => {
|
|
|
729
729
|
expect(controlsHeader).to.exist;
|
|
730
730
|
expect(controlsHeader!.textContent?.trim()).to.equal('');
|
|
731
731
|
});
|
|
732
|
+
|
|
733
|
+
it('gives the preview an interactive frame that scrolls itself, so the column never adds a second scrollbar', async () => {
|
|
734
|
+
const el = await fixture(FIXTURE);
|
|
735
|
+
await (el as HTMLElement & {updateComplete: Promise<unknown>}).updateComplete;
|
|
736
|
+
|
|
737
|
+
const preview = el.shadowRoot!.querySelector<HTMLElement>('[part="preview"]')!;
|
|
738
|
+
const frame = el.shadowRoot!.querySelector<HTMLElement>('zn-preview-frame')!;
|
|
739
|
+
await (frame as HTMLElement & {updateComplete: Promise<unknown>}).updateComplete;
|
|
740
|
+
|
|
741
|
+
expect(frame.hasAttribute('interactive')).to.equal(true);
|
|
742
|
+
expect(getComputedStyle(preview).overflowY).to.equal('hidden');
|
|
743
|
+
expect(preview.scrollHeight).to.equal(preview.clientHeight);
|
|
744
|
+
|
|
745
|
+
const iframe = frame.shadowRoot!.querySelector('iframe')!;
|
|
746
|
+
expect(getComputedStyle(iframe).pointerEvents).to.equal('auto');
|
|
747
|
+
});
|
|
732
748
|
});
|
|
733
749
|
|
|
734
750
|
describe('tabbed sections (section-layout="tabs")', () => {
|