@kubex/zinc 1.1.73 → 1.1.75
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 +575 -91
- package/dist/vscode.html-custom-data.json +20 -15
- package/dist/web-types.json +37 -27
- package/dist/zn.d.ts +71 -5
- package/dist/zn.min.js +229 -229
- package/docs/data/preview-frame-payload-tall.json +7 -0
- package/docs/pages/components/preview-frame-demo.njk +45 -2
- package/docs/pages/components/preview-frame.md +79 -0
- package/docs/pages/components/tabs.md +9 -2
- package/package.json +1 -2
- package/scripts/build.js +5 -5
- package/src/components/defined-label/defined-label.component.ts +10 -17
- package/src/components/defined-label/defined-label.test.ts +107 -1
- package/src/components/expanding-action/expanding-action.component.ts +4 -2
- package/src/components/expanding-action/expanding-action.scss +14 -2
- package/src/components/expanding-action/expanding-action.test.ts +44 -1
- package/src/components/page/page.component.ts +71 -6
- package/src/components/page/page.test.ts +247 -0
- package/src/components/preview-frame/preview-frame.component.ts +84 -8
- package/src/components/preview-frame/preview-frame.scss +7 -1
- package/src/components/preview-frame/preview-frame.test.ts +184 -0
- package/src/components/tabs/tabs-navigation.ts +197 -0
- package/src/components/tabs/tabs.component.ts +188 -56
- package/src/components/tabs/tabs.test.ts +128 -0
|
@@ -26,6 +26,7 @@ permalink: /components/preview-frame-demo/index.html
|
|
|
26
26
|
<input type="text" placeholder="123" disabled>
|
|
27
27
|
</label>
|
|
28
28
|
</div>
|
|
29
|
+
<div class="card__lines" id="lines"></div>
|
|
29
30
|
<button id="pay" type="button">Pay now</button>
|
|
30
31
|
</div>
|
|
31
32
|
</div>
|
|
@@ -38,6 +39,13 @@ permalink: /components/preview-frame-demo/index.html
|
|
|
38
39
|
justify-content: center;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/* A page taller than the frame: top-aligned, or the overflow clips the top. */
|
|
43
|
+
.shell--tall {
|
|
44
|
+
height: auto;
|
|
45
|
+
align-items: flex-start;
|
|
46
|
+
padding: 24px 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
#waiting {
|
|
42
50
|
color: rgb(var(--zn-color-muted-text));
|
|
43
51
|
}
|
|
@@ -91,6 +99,20 @@ permalink: /components/preview-frame-demo/index.html
|
|
|
91
99
|
gap: 12px;
|
|
92
100
|
}
|
|
93
101
|
|
|
102
|
+
.card__lines:not(:empty) {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
gap: 8px;
|
|
106
|
+
font-size: 0.8125rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.card__line {
|
|
110
|
+
display: flex;
|
|
111
|
+
justify-content: space-between;
|
|
112
|
+
padding-bottom: 8px;
|
|
113
|
+
border-bottom: 1px solid rgb(var(--zn-border-color));
|
|
114
|
+
}
|
|
115
|
+
|
|
94
116
|
.card__row label {
|
|
95
117
|
flex: 1;
|
|
96
118
|
}
|
|
@@ -124,6 +146,26 @@ permalink: /components/preview-frame-demo/index.html
|
|
|
124
146
|
// should use the host's origin instead of '*'.
|
|
125
147
|
const post = message => window.parent.postMessage(message, '*');
|
|
126
148
|
|
|
149
|
+
// Reporting a height lets the host grow the frame to fit, so an overflowing
|
|
150
|
+
// page is scrolled by the panel instead of inside the frame. Measured after a
|
|
151
|
+
// frame so the layout has settled.
|
|
152
|
+
const postRendered = () => requestAnimationFrame(() => post({
|
|
153
|
+
type: 'hp-preview:rendered',
|
|
154
|
+
height: document.documentElement.scrollHeight
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
const renderLines = count => {
|
|
158
|
+
const lines = document.getElementById('lines');
|
|
159
|
+
lines.innerHTML = '';
|
|
160
|
+
document.querySelector('.shell').classList.toggle('shell--tall', count > 0);
|
|
161
|
+
for (let i = 1; i <= count; i++) {
|
|
162
|
+
const line = document.createElement('div');
|
|
163
|
+
line.className = 'card__line';
|
|
164
|
+
line.innerHTML = `<span>Line item ${i}</span><span>£${(i * 1.5).toFixed(2)}</span>`;
|
|
165
|
+
lines.append(line);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
127
169
|
window.addEventListener('message', e => {
|
|
128
170
|
const data = e.data;
|
|
129
171
|
if (data?.type !== 'hp-preview:config') return;
|
|
@@ -140,7 +182,8 @@ permalink: /components/preview-frame-demo/index.html
|
|
|
140
182
|
document.getElementById('merchant').textContent = data.merchant || 'Merchant';
|
|
141
183
|
document.getElementById('amount').textContent = data.amount || '';
|
|
142
184
|
document.getElementById('pay').textContent = data.buttonLabel || 'Pay now';
|
|
143
|
-
|
|
185
|
+
renderLines(Number(data.lines) || 0);
|
|
186
|
+
postRendered();
|
|
144
187
|
});
|
|
145
188
|
|
|
146
189
|
// The theme half of the protocol: applied independently of the config, so
|
|
@@ -159,7 +202,7 @@ permalink: /components/preview-frame-demo/index.html
|
|
|
159
202
|
card.style.borderRadius = values.radius + 'px';
|
|
160
203
|
}
|
|
161
204
|
document.getElementById('waiting').hidden = true;
|
|
162
|
-
|
|
205
|
+
postRendered();
|
|
163
206
|
});
|
|
164
207
|
|
|
165
208
|
post({type: 'hp-preview:ready'});
|
|
@@ -47,6 +47,85 @@ fill — used by [`zn-theme-editor`](/components/theme-editor/)'s `standalone`
|
|
|
47
47
|
mode, where the frame is already inside its own bordered panel. `backdrop="dots"`
|
|
48
48
|
is the default.
|
|
49
49
|
|
|
50
|
+
## Interactivity
|
|
51
|
+
|
|
52
|
+
The preview is display-only: the iframe takes `pointer-events: none`, so clicks
|
|
53
|
+
never reach the embedded page and the previewed form can't be submitted or
|
|
54
|
+
navigated away from inside the frame. The embed is cross-origin, so its own
|
|
55
|
+
handlers can't be cancelled from out here — blocking pointer input is the only
|
|
56
|
+
way to stop them, and hover goes with it. Scrolling doesn't: an overflowing page
|
|
57
|
+
is scrolled by the panel instead, as below.
|
|
58
|
+
|
|
59
|
+
Set `interactive` when the embed is meant to be used rather than looked at.
|
|
60
|
+
|
|
61
|
+
```html:preview
|
|
62
|
+
<zn-preview-frame
|
|
63
|
+
id="preview-frame-interactive"
|
|
64
|
+
src="/components/preview-frame-demo/"
|
|
65
|
+
data-uri="/data/preview-frame-payload.json"
|
|
66
|
+
watch="#preview-frame-interactive-none"
|
|
67
|
+
interactive></zn-preview-frame>
|
|
68
|
+
|
|
69
|
+
<script>
|
|
70
|
+
document.getElementById('preview-frame-interactive').frameOrigin = location.origin;
|
|
71
|
+
</script>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Overflowing Content
|
|
75
|
+
|
|
76
|
+
A page taller than the panel is scrolled by the panel, not inside the frame. The
|
|
77
|
+
frame can't do it itself: a cross-origin document can't be scrolled from the host
|
|
78
|
+
(`contentWindow.scrollTo` is blocked), and with pointer input off the wheel never
|
|
79
|
+
reaches it anyway. So the frame is instead laid out at its full content height —
|
|
80
|
+
nothing scrolls inside it — and the panel scrolls that.
|
|
81
|
+
|
|
82
|
+
For the frame to be sized that way, the embed reports its height alongside
|
|
83
|
+
`hp-preview:rendered`:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
post({
|
|
87
|
+
type: 'hp-preview:rendered',
|
|
88
|
+
height: document.documentElement.scrollHeight
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
A page that grows after its first render — a revealed section, a lazy-loaded
|
|
93
|
+
image — reports the new height on its own:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
post({type: 'hp-preview:height', height: document.documentElement.scrollHeight});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Heights that aren't a positive number are ignored, as is one reported while the
|
|
100
|
+
error overlay is up. A height under the panel's own is kept but changes nothing:
|
|
101
|
+
the frame still fills the panel, so the backdrop never shows under a short page.
|
|
102
|
+
The height is dropped whenever `src` changes, since the next page has its own.
|
|
103
|
+
|
|
104
|
+
The example below previews a long itemised page, so the panel scrolls. Scrolling
|
|
105
|
+
works with the frame inert — clicking `Pay` still does nothing.
|
|
106
|
+
|
|
107
|
+
```html:preview
|
|
108
|
+
<zn-preview-frame
|
|
109
|
+
id="preview-frame-tall"
|
|
110
|
+
src="/components/preview-frame-demo/"
|
|
111
|
+
data-uri="/data/preview-frame-payload-tall.json"
|
|
112
|
+
watch="#preview-frame-tall-none"></zn-preview-frame>
|
|
113
|
+
|
|
114
|
+
<script>
|
|
115
|
+
document.getElementById('preview-frame-tall').frameOrigin = location.origin;
|
|
116
|
+
</script>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
:::tip
|
|
120
|
+
A **same-origin** embed doesn't need to report anything — its document is
|
|
121
|
+
measured directly. The measurement only ever grows the frame: the frame's own
|
|
122
|
+
height feeds back into it, so a value at or under the current height is ignored
|
|
123
|
+
rather than flipping the frame between two sizes forever. An embed that shrinks
|
|
124
|
+
has to report its height to be followed back down, and one whose root is sized
|
|
125
|
+
to the viewport (`html {height: 100%}`) can't be measured at all — it reports or
|
|
126
|
+
it clips.
|
|
127
|
+
:::
|
|
128
|
+
|
|
50
129
|
## Live Form Updates
|
|
51
130
|
|
|
52
131
|
In a real deployment, editing a watched form auto-saves it and the preview refreshes with the newly saved config. This docs site is static, so the example simulates the save: form changes are encoded into a `data:` payload URI and `refresh()` re-runs the fetch → `hp-preview:config` cycle — the same path a real save triggers.
|
|
@@ -200,8 +200,15 @@ Use the `active` attribute to specify which tab should be active by default.
|
|
|
200
200
|
|
|
201
201
|
### Storage Persistence
|
|
202
202
|
|
|
203
|
-
Use `store-key` to persist the active tab selection
|
|
204
|
-
|
|
203
|
+
Use `store-key` to persist the active tab selection.
|
|
204
|
+
|
|
205
|
+
Each tab you open becomes its own history entry, so Back and Forward step through the tabs you visited before leaving the
|
|
206
|
+
page, and the entry the page was first rendered on returns you to the default tab. Reloading restores the tab you were
|
|
207
|
+
on. Navigating to the page — including a client-side navigation back to somewhere visited earlier — starts from the
|
|
208
|
+
default tab.
|
|
209
|
+
|
|
210
|
+
Selections are held in session storage, keyed by location. Use `local-storage` when a selection should survive fresh
|
|
211
|
+
navigation and browser sessions; it is then keyed by `store-key` alone and takes no part in history.
|
|
205
212
|
|
|
206
213
|
```html:preview
|
|
207
214
|
<zn-tabs flush store-key="my-tabs">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubex/zinc",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.75",
|
|
4
4
|
"description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web components",
|
|
@@ -82,7 +82,6 @@
|
|
|
82
82
|
"custom-element-vuejs-integration": "^1.4.0",
|
|
83
83
|
"del": "^8.0.0",
|
|
84
84
|
"esbuild": "^0.27.7",
|
|
85
|
-
"esbuild-plugin-replace": "^1.4.0",
|
|
86
85
|
"esbuild-sass-plugin": "^3.7.0",
|
|
87
86
|
"eslint": "^8.57.1",
|
|
88
87
|
"eslint-plugin-chai-expect": "^3.1.0",
|
package/scripts/build.js
CHANGED
|
@@ -9,7 +9,6 @@ import * as path from 'path';
|
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import fs from 'fs/promises';
|
|
11
11
|
import {readFileSync} from 'fs';
|
|
12
|
-
import {replace} from 'esbuild-plugin-replace';
|
|
13
12
|
import {sassPlugin} from 'esbuild-sass-plugin';
|
|
14
13
|
import getPort, {portNumbers} from 'get-port';
|
|
15
14
|
import postCSS from 'postcss';
|
|
@@ -144,7 +143,11 @@ async function buildTheSource()
|
|
|
144
143
|
entryPoints,
|
|
145
144
|
define: {
|
|
146
145
|
// Floating UI requires this to be set
|
|
147
|
-
'process.env.NODE_ENV': '"production"'
|
|
146
|
+
'process.env.NODE_ENV': '"production"',
|
|
147
|
+
// Substituted natively by esbuild. Don't move this to an onLoad-based replace plugin: those
|
|
148
|
+
// read every module in the graph from Node with no concurrency cap, which exhausts the file
|
|
149
|
+
// table (ENFILE) once deps with thousands of modules (echarts, lucide) are bundled.
|
|
150
|
+
__ZINC_VERSION__: zincVersion
|
|
148
151
|
},
|
|
149
152
|
bundle: true,
|
|
150
153
|
splitting: true,
|
|
@@ -165,9 +168,6 @@ async function buildTheSource()
|
|
|
165
168
|
console.error(err);
|
|
166
169
|
}
|
|
167
170
|
}
|
|
168
|
-
}),
|
|
169
|
-
replace({
|
|
170
|
-
__ZINC_VERSION__: zincVersion
|
|
171
171
|
})
|
|
172
172
|
]
|
|
173
173
|
};
|
|
@@ -149,21 +149,15 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
|
|
|
149
149
|
private handleInputValueChange(e: Event) {
|
|
150
150
|
const target = e.target as HTMLInputElement | HTMLSelectElement;
|
|
151
151
|
this.inputValue = target.value.toLowerCase();
|
|
152
|
-
|
|
153
|
-
if (target.hasAttribute('data-label')) this.value = target.getAttribute('data-label') ?? "";
|
|
154
152
|
}
|
|
155
153
|
|
|
156
|
-
private handleFormSubmit(e: Event, label
|
|
157
|
-
//
|
|
158
|
-
// the row
|
|
154
|
+
private handleFormSubmit(e: Event, label: string) {
|
|
155
|
+
// Submit the key shown on the clicked row — a predefined name, or the typed
|
|
156
|
+
// key for the custom row — and discard values left behind on other rows
|
|
159
157
|
const row = (e.currentTarget as HTMLElement).closest('.defined-label__row');
|
|
160
158
|
const control = row?.querySelector<ZnInput | ZnSelect>('.defined-label__value');
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
if (control) {
|
|
165
|
-
this.inputValue = String(control.value ?? '').toLowerCase();
|
|
166
|
-
}
|
|
159
|
+
this.value = label;
|
|
160
|
+
this.inputValue = control ? String(control.value ?? '').toLowerCase() : '';
|
|
167
161
|
|
|
168
162
|
const form = this.formControlController.getForm();
|
|
169
163
|
|
|
@@ -181,7 +175,6 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
|
|
|
181
175
|
<zn-select
|
|
182
176
|
part="input-value"
|
|
183
177
|
class="defined-label__value"
|
|
184
|
-
data-label="${label.name}"
|
|
185
178
|
size="small"
|
|
186
179
|
@zn-change="${this.handleInputValueChange}"
|
|
187
180
|
@zn-input="${this.handleInputValueChange}">
|
|
@@ -197,16 +190,15 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
|
|
|
197
190
|
class="defined-label__value"
|
|
198
191
|
type="text"
|
|
199
192
|
placeholder="Label Value"
|
|
200
|
-
data-label="${label.name}"
|
|
201
193
|
size="small"
|
|
202
194
|
@zn-change="${this.handleInputValueChange}"
|
|
203
195
|
@zn-input="${this.handleInputValueChange}"></zn-input>`;
|
|
204
196
|
}
|
|
205
197
|
|
|
206
|
-
private renderRow(
|
|
198
|
+
private renderRow(label: string, control: TemplateResult): TemplateResult {
|
|
207
199
|
return html`
|
|
208
200
|
<div class="defined-label__row">
|
|
209
|
-
<small class="defined-label__row-label">${
|
|
201
|
+
<small class="defined-label__row-label">${label}</small>
|
|
210
202
|
<div class="defined-label__row-controls">
|
|
211
203
|
${control}
|
|
212
204
|
<zn-button
|
|
@@ -219,6 +211,7 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
|
|
|
219
211
|
|
|
220
212
|
render() {
|
|
221
213
|
const labels = this.getFilteredLabels();
|
|
214
|
+
const showCustom = this.allowCustom && this.value !== '' && !labels.some(label => label.name === this.value);
|
|
222
215
|
|
|
223
216
|
return html`
|
|
224
217
|
<zn-dropdown class="defined-label__dropdown" sync="width">
|
|
@@ -243,11 +236,11 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
|
|
|
243
236
|
|
|
244
237
|
<div class="defined-label__panel">
|
|
245
238
|
${labels.length > 0
|
|
246
|
-
? labels.map(label => this.renderRow(label.name, this.renderValueControl(label)
|
|
239
|
+
? labels.map(label => this.renderRow(label.name, this.renderValueControl(label)))
|
|
247
240
|
: html`
|
|
248
241
|
<div class="defined-label__empty">Cannot find any predefined labels</div>`}
|
|
249
242
|
|
|
250
|
-
${
|
|
243
|
+
${showCustom ? this.renderRow(this.value, html`
|
|
251
244
|
<zn-input
|
|
252
245
|
part="input-value"
|
|
253
246
|
class="defined-label__value"
|
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
-
import { expect, fixture, html } from '@open-wc/testing';
|
|
2
|
+
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
|
3
|
+
import type ZnDefinedLabel from './defined-label.component';
|
|
4
|
+
|
|
5
|
+
const predefined = [
|
|
6
|
+
{ name: 'outbound', options: [] },
|
|
7
|
+
{ name: 'another', options: ['one', 'two'] }
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
async function fixtureWithForm() {
|
|
11
|
+
const form: HTMLFormElement = await fixture(html`
|
|
12
|
+
<form>
|
|
13
|
+
<zn-defined-label allow-custom name="label" .predefinedLabels="${predefined}"></zn-defined-label>
|
|
14
|
+
</form>`);
|
|
15
|
+
|
|
16
|
+
const el = form.querySelector<ZnDefinedLabel>('zn-defined-label')!;
|
|
17
|
+
await el.updateComplete;
|
|
18
|
+
|
|
19
|
+
const submissions: string[] = [];
|
|
20
|
+
form.addEventListener('submit', event => {
|
|
21
|
+
event.preventDefault();
|
|
22
|
+
submissions.push([...new FormData(form).entries()].map(([key, value]) => `${key}=${String(value)}`).join('&'));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return { el, submissions };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function type(el: ZnDefinedLabel, control: Element, text: string) {
|
|
29
|
+
const native = control.shadowRoot!.querySelector('input')!;
|
|
30
|
+
native.focus();
|
|
31
|
+
|
|
32
|
+
for (const character of text) {
|
|
33
|
+
native.value += character;
|
|
34
|
+
native.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
|
|
35
|
+
await el.updateComplete;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rows(el: ZnDefinedLabel) {
|
|
40
|
+
return [...(el.shadowRoot?.querySelectorAll('.defined-label__row') ?? [])];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function rowLabels(el: ZnDefinedLabel) {
|
|
44
|
+
return rows(el).map(row => row.querySelector('.defined-label__row-label')?.textContent);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function clickAdd(row: Element, submissions: string[]) {
|
|
48
|
+
const button = row.querySelector('zn-button')!.shadowRoot!.querySelector('button')!;
|
|
49
|
+
button.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, composed: true }));
|
|
50
|
+
button.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
|
51
|
+
await waitUntil(() => submissions.length > 0, 'the form never submitted');
|
|
52
|
+
}
|
|
3
53
|
|
|
4
54
|
describe('<zn-defined-label>', () => {
|
|
5
55
|
it('should render a component', async () => {
|
|
@@ -7,4 +57,60 @@ describe('<zn-defined-label>', () => {
|
|
|
7
57
|
|
|
8
58
|
expect(el).to.exist;
|
|
9
59
|
});
|
|
60
|
+
|
|
61
|
+
it('should offer a custom row alongside partially matching predefined labels', async () => {
|
|
62
|
+
const { el } = await fixtureWithForm();
|
|
63
|
+
|
|
64
|
+
await type(el, el.input, 'out');
|
|
65
|
+
|
|
66
|
+
expect(rowLabels(el)).to.eql(['outbound', 'out']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should keep the typed key when a value is entered on a partially matching predefined row', async () => {
|
|
70
|
+
const { el } = await fixtureWithForm();
|
|
71
|
+
|
|
72
|
+
await type(el, el.input, 'out');
|
|
73
|
+
await type(el, rows(el)[0].querySelector('.defined-label__value')!, 'x');
|
|
74
|
+
|
|
75
|
+
expect(el.value).to.equal('out');
|
|
76
|
+
expect(rowLabels(el)).to.eql(['outbound', 'out']);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should submit the typed key from the custom row, not a partially matching predefined label', async () => {
|
|
80
|
+
const { el, submissions } = await fixtureWithForm();
|
|
81
|
+
|
|
82
|
+
await type(el, el.input, 'out');
|
|
83
|
+
await type(el, rows(el)[1].querySelector('.defined-label__value')!, 'bob');
|
|
84
|
+
await clickAdd(rows(el)[1], submissions);
|
|
85
|
+
|
|
86
|
+
expect(submissions[0]).to.equal('label=out:bob');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should submit the predefined key when its own row is added', async () => {
|
|
90
|
+
const { el, submissions } = await fixtureWithForm();
|
|
91
|
+
|
|
92
|
+
await type(el, el.input, 'out');
|
|
93
|
+
await type(el, rows(el)[0].querySelector('.defined-label__value')!, 'x');
|
|
94
|
+
await clickAdd(rows(el)[0], submissions);
|
|
95
|
+
|
|
96
|
+
expect(submissions[0]).to.equal('label=outbound:x');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should discard a value left on another row', async () => {
|
|
100
|
+
const { el, submissions } = await fixtureWithForm();
|
|
101
|
+
|
|
102
|
+
await type(el, el.input, 'out');
|
|
103
|
+
await type(el, rows(el)[0].querySelector('.defined-label__value')!, 'x');
|
|
104
|
+
await clickAdd(rows(el)[1], submissions);
|
|
105
|
+
|
|
106
|
+
expect(submissions[0]).to.equal('label=out');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should not duplicate a predefined label as a custom row', async () => {
|
|
110
|
+
const { el } = await fixtureWithForm();
|
|
111
|
+
|
|
112
|
+
await type(el, el.input, 'outbound');
|
|
113
|
+
|
|
114
|
+
expect(rowLabels(el)).to.eql(['outbound']);
|
|
115
|
+
});
|
|
10
116
|
});
|
|
@@ -325,8 +325,10 @@ export default class ZnExpandingAction extends ZincElement {
|
|
|
325
325
|
plain
|
|
326
326
|
notification="${this.count || nothing}">
|
|
327
327
|
</zn-button>
|
|
328
|
-
<div
|
|
329
|
-
<
|
|
328
|
+
<div class="expanding-action__frame">
|
|
329
|
+
<div id="content" class="expanding-action__content">
|
|
330
|
+
<slot></slot>
|
|
331
|
+
</div>
|
|
330
332
|
</div>
|
|
331
333
|
</zn-dropdown>`
|
|
332
334
|
}
|
|
@@ -72,13 +72,18 @@
|
|
|
72
72
|
height: 100%;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
.expanding-action--drop .expanding-
|
|
75
|
+
.expanding-action--drop .expanding-action__frame {
|
|
76
|
+
@include wc.scrollbars;
|
|
77
|
+
|
|
76
78
|
--expanding-action-trigger-width: 36px;
|
|
77
79
|
|
|
78
80
|
position: relative;
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: column;
|
|
79
83
|
min-width: 300px;
|
|
80
84
|
width: var(--expanding-action-basis, var(--zn-container-smp, 320px));
|
|
81
85
|
max-width: 80vw;
|
|
86
|
+
max-height: var(--expanding-action-max-height, var(--auto-size-available-height, 80vh));
|
|
82
87
|
background-color: rgb(var(--zn-panel));
|
|
83
88
|
box-shadow: 0 8px 16px -6px rgba(33, 33, 33, 0.18);
|
|
84
89
|
border: solid 1px rgb(var(--zn-border-color));
|
|
@@ -98,7 +103,14 @@
|
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
|
|
106
|
+
.expanding-action--drop .expanding-action__content {
|
|
107
|
+
flex: 1 1 auto;
|
|
108
|
+
min-height: 0; // Without this the flex item refuses to shrink below its content height
|
|
109
|
+
overflow-y: auto;
|
|
110
|
+
border-radius: inherit;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
:host([data-trigger-side="left"]) .expanding-action--drop .expanding-action__frame {
|
|
102
114
|
border-radius: 0 var(--zn-border-radius) var(--zn-border-radius) var(--zn-border-radius);
|
|
103
115
|
|
|
104
116
|
&::before {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
-
import { expect, fixture, html } from '@open-wc/testing';
|
|
2
|
+
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
|
3
3
|
|
|
4
4
|
describe('<zn-expanding-action>', () => {
|
|
5
5
|
it('should render a component', async () => {
|
|
@@ -7,4 +7,47 @@ describe('<zn-expanding-action>', () => {
|
|
|
7
7
|
|
|
8
8
|
expect(el).to.exist;
|
|
9
9
|
});
|
|
10
|
+
|
|
11
|
+
describe('drop panel overflow', () => {
|
|
12
|
+
const openPanel = async (el: HTMLElement) => {
|
|
13
|
+
const dropdown = el.shadowRoot!.querySelector('zn-dropdown') as HTMLElement & { show: () => Promise<unknown> };
|
|
14
|
+
await dropdown.show();
|
|
15
|
+
await waitUntil(() => el.hasAttribute('open'));
|
|
16
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const tallContent = html`
|
|
20
|
+
<zn-expanding-action method="drop" icon="note">
|
|
21
|
+
<div style="height: 4000px">tall</div>
|
|
22
|
+
</zn-expanding-action>`;
|
|
23
|
+
|
|
24
|
+
it('should bound the panel height so it cannot run off the screen', async () => {
|
|
25
|
+
const el = await fixture<HTMLElement>(tallContent);
|
|
26
|
+
await openPanel(el);
|
|
27
|
+
|
|
28
|
+
const frame = el.shadowRoot!.querySelector<HTMLElement>('.expanding-action__frame')!;
|
|
29
|
+
expect(frame).to.exist;
|
|
30
|
+
expect(frame.getBoundingClientRect().height).to.be.at.most(window.innerHeight);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should scroll overflowing content rather than clipping it', async () => {
|
|
34
|
+
const el = await fixture<HTMLElement>(tallContent);
|
|
35
|
+
await openPanel(el);
|
|
36
|
+
|
|
37
|
+
const content = el.shadowRoot!.querySelector<HTMLElement>('#content')!;
|
|
38
|
+
expect(getComputedStyle(content).overflowY).to.equal('auto');
|
|
39
|
+
expect(content.scrollHeight).to.be.greaterThan(content.clientHeight);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should honour an explicit max-height', async () => {
|
|
43
|
+
const el = await fixture<HTMLElement>(html`
|
|
44
|
+
<zn-expanding-action method="drop" icon="note" max-height="250">
|
|
45
|
+
<div style="height: 4000px">tall</div>
|
|
46
|
+
</zn-expanding-action>`);
|
|
47
|
+
await openPanel(el);
|
|
48
|
+
|
|
49
|
+
const frame = el.shadowRoot!.querySelector<HTMLElement>('.expanding-action__frame')!;
|
|
50
|
+
expect(Math.round(frame.getBoundingClientRect().height)).to.equal(250);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
10
53
|
});
|
|
@@ -62,6 +62,7 @@ export default class ZnPage extends ZnTabs {
|
|
|
62
62
|
@state() private hasExpandingActions = false;
|
|
63
63
|
private actionObserver: MutationObserver | null = null;
|
|
64
64
|
private tabObserver: MutationObserver | null = null;
|
|
65
|
+
private pageHistoryKey: string | null = null;
|
|
65
66
|
|
|
66
67
|
async connectedCallback() {
|
|
67
68
|
const connected = super.connectedCallback();
|
|
@@ -281,22 +282,45 @@ export default class ZnPage extends ZnTabs {
|
|
|
281
282
|
|
|
282
283
|
private handleNavigationSelect(event: ZnSelectEvent) {
|
|
283
284
|
const item = event.detail.item as HTMLElement;
|
|
285
|
+
if (this.getNavigationItemPage(item) !== this) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
284
289
|
const tabUri = item.getAttribute('tab-uri');
|
|
285
290
|
const tabId = item.getAttribute('tab');
|
|
286
291
|
|
|
287
292
|
if (tabUri) {
|
|
288
|
-
this.clickTab(item, false);
|
|
293
|
+
this.clickTab(item, false, true);
|
|
289
294
|
this.syncNavigationActive(item);
|
|
290
295
|
return;
|
|
291
296
|
}
|
|
292
297
|
|
|
293
298
|
if (tabId !== null) {
|
|
294
|
-
this.activateTab(tabId, true);
|
|
299
|
+
this.activateTab(tabId, true, true);
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
302
|
|
|
298
|
-
private
|
|
299
|
-
|
|
303
|
+
private getNavigationItemPage(item: HTMLElement): ZnPage | null {
|
|
304
|
+
let current: Node | null = item;
|
|
305
|
+
|
|
306
|
+
while (current) {
|
|
307
|
+
if (current instanceof ZnPage) {
|
|
308
|
+
return current;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const root = current.getRootNode();
|
|
312
|
+
if (!(root instanceof ShadowRoot)) {
|
|
313
|
+
return item.closest<ZnPage>('zn-page');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
current = root.host;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private activateTab(tabId: string, store: boolean, pushHistory = false) {
|
|
323
|
+
this.setActiveTab(tabId, store, false, null, pushHistory);
|
|
300
324
|
const navItem = this.shadowRoot?.querySelector<HTMLElement>(`zn-navbar li[tab="${CSS.escape(tabId)}"]`);
|
|
301
325
|
if (navItem) {
|
|
302
326
|
this.syncNavigationActive(navItem);
|
|
@@ -304,6 +328,15 @@ export default class ZnPage extends ZnTabs {
|
|
|
304
328
|
}
|
|
305
329
|
|
|
306
330
|
private activateInitialPageTab() {
|
|
331
|
+
const restoredTab = this.getStoredTab();
|
|
332
|
+
if (restoredTab !== null) {
|
|
333
|
+
const definition = this.tabDefinitions.find(tab => tab.id === restoredTab || tab.uri === restoredTab);
|
|
334
|
+
if (definition) {
|
|
335
|
+
this.activateTabDefinition(definition);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
307
340
|
const preselected = this.tabDefinitions.find(tab => tab.selected);
|
|
308
341
|
if (preselected) {
|
|
309
342
|
this.activateTabDefinition(preselected);
|
|
@@ -318,7 +351,39 @@ export default class ZnPage extends ZnTabs {
|
|
|
318
351
|
}
|
|
319
352
|
}
|
|
320
353
|
|
|
321
|
-
|
|
354
|
+
// Pages have no store-key, so they persist against their own identity.
|
|
355
|
+
protected getTabStoreKey(): string {
|
|
356
|
+
return this.getPageHistoryKey();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// A page's default is its preselected tab, or the first one - not the empty
|
|
360
|
+
// active tab zn-tabs starts from.
|
|
361
|
+
protected activateDefaultTab() {
|
|
362
|
+
const preselected = this.tabDefinitions.find(tab => tab.selected) ?? this.tabDefinitions[0];
|
|
363
|
+
if (preselected) {
|
|
364
|
+
this.activateTabDefinition(preselected, true);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private getPageHistoryKey(): string {
|
|
369
|
+
if (this.pageHistoryKey !== null) {
|
|
370
|
+
return this.pageHistoryKey;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const pages: ZnPage[] = [this];
|
|
374
|
+
let ancestor = this.parentElement?.closest<ZnPage>('zn-page') ?? null;
|
|
375
|
+
while (ancestor) {
|
|
376
|
+
pages.unshift(ancestor);
|
|
377
|
+
ancestor = ancestor.parentElement?.closest<ZnPage>('zn-page') ?? null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.pageHistoryKey = pages
|
|
381
|
+
.map(page => page.id || page.getAttribute('caption') || 'page')
|
|
382
|
+
.join('/');
|
|
383
|
+
return this.pageHistoryKey;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private activateTabDefinition(tab: TabDefinition, store = false) {
|
|
322
387
|
if (tab.uri) {
|
|
323
388
|
const navItem = this.findNavItemForUri(tab.uri);
|
|
324
389
|
if (navItem) {
|
|
@@ -327,7 +392,7 @@ export default class ZnPage extends ZnTabs {
|
|
|
327
392
|
return;
|
|
328
393
|
}
|
|
329
394
|
}
|
|
330
|
-
this.activateTab(tab.id,
|
|
395
|
+
this.activateTab(tab.id, store);
|
|
331
396
|
}
|
|
332
397
|
|
|
333
398
|
private findNavItemForUri(uri: string): HTMLElement | null {
|