@kubex/zinc 1.1.116 → 1.1.118
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 +252 -13
- package/dist/vscode.html-custom-data.json +9 -4
- package/dist/web-types.json +18 -8
- package/dist/zn.d.ts +57 -18
- package/dist/zn.min.js +188 -185
- package/docs/pages/components/form-group.md +42 -9
- package/docs/pages/components/translation-group.md +20 -18
- package/docs/pages/components/translations.md +3 -2
- package/package.json +1 -1
- package/src/components/form-group/form-group.component.ts +165 -2
- package/src/components/form-group/form-group.scss +18 -3
- package/src/components/form-group/form-group.test.ts +80 -0
- package/src/components/translation-group/translation-group.component.ts +55 -51
- package/src/components/translation-group/translation-group.scss +10 -74
- package/src/components/translation-group/translation-group.test.ts +76 -38
- package/src/components/translations/translations.component.ts +3 -7
|
@@ -358,27 +358,60 @@ This ensures forms remain usable on all devices without additional configuration
|
|
|
358
358
|
</zn-form-group>
|
|
359
359
|
```
|
|
360
360
|
|
|
361
|
+
### Sticky Label
|
|
362
|
+
|
|
363
|
+
The label column sticks to the top of the scroll container, so on a long form it stays beside the inputs instead of scrolling away. Use the `--zn-form-group-sticky-top` custom property to change the offset it settles at — useful when the scroll container has a sticky header of its own.
|
|
364
|
+
|
|
365
|
+
```html:preview
|
|
366
|
+
<div style="max-height: 300px; overflow-y: auto;">
|
|
367
|
+
<zn-form-group label="Delivery Details" help-text="This label follows the inputs as you scroll">
|
|
368
|
+
<zn-input label="Recipient" placeholder="Full name"></zn-input>
|
|
369
|
+
<zn-input label="Street Address" placeholder="123 Main Street"></zn-input>
|
|
370
|
+
<zn-input label="Apartment/Unit" placeholder="Apt 4B"></zn-input>
|
|
371
|
+
<zn-input label="City" placeholder="New York"></zn-input>
|
|
372
|
+
<zn-input label="Postal Code" placeholder="10001"></zn-input>
|
|
373
|
+
<zn-input label="Phone" type="tel" placeholder="(555) 123-4567"></zn-input>
|
|
374
|
+
<zn-textarea label="Delivery Notes" placeholder="Leave with the concierge..." rows="4"></zn-textarea>
|
|
375
|
+
</zn-form-group>
|
|
376
|
+
</div>
|
|
377
|
+
```
|
|
378
|
+
|
|
361
379
|
### Styling with CSS Parts
|
|
362
380
|
|
|
363
|
-
Form groups expose several CSS parts that can be styled to customize their appearance.
|
|
381
|
+
Form groups expose several CSS parts that can be styled to customize their appearance. `form-control-text` is the column holding the label, help text and chip — it is the part that sticks as the fields scroll, so give it the same background as the group to stop the fields showing through underneath it.
|
|
364
382
|
|
|
365
383
|
```html:preview
|
|
366
|
-
<
|
|
367
|
-
<zn-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
384
|
+
<div class="custom-form-group-scroller">
|
|
385
|
+
<zn-form-group class="custom-form-group" label="Custom Styled Form Group" help-text="This form group has custom spacing and borders">
|
|
386
|
+
<zn-input label="Field 1" span="3"></zn-input>
|
|
387
|
+
<zn-input label="Field 2" span="3"></zn-input>
|
|
388
|
+
<zn-input label="Field 3" span="2"></zn-input>
|
|
389
|
+
<zn-input label="Field 4" span="2"></zn-input>
|
|
390
|
+
<zn-input label="Field 5" span="2"></zn-input>
|
|
391
|
+
<zn-input label="Field 6" span="3"></zn-input>
|
|
392
|
+
<zn-input label="Field 7" span="3"></zn-input>
|
|
393
|
+
<zn-textarea label="Field 8" rows="4"></zn-textarea>
|
|
394
|
+
</zn-form-group>
|
|
395
|
+
</div>
|
|
373
396
|
|
|
374
397
|
<style>
|
|
398
|
+
.custom-form-group-scroller {
|
|
399
|
+
max-height: 320px;
|
|
400
|
+
overflow-y: auto;
|
|
401
|
+
}
|
|
402
|
+
|
|
375
403
|
.custom-form-group::part(form-control) {
|
|
376
|
-
padding: var(--zn-spacing-
|
|
404
|
+
padding: var(--zn-spacing-medium);
|
|
377
405
|
border: 2px solid var(--zn-color-primary-300);
|
|
378
406
|
border-radius: var(--zn-border-radius);
|
|
379
407
|
background-color: var(--zn-color-primary-50);
|
|
380
408
|
}
|
|
381
409
|
|
|
410
|
+
.custom-form-group::part(form-control-text) {
|
|
411
|
+
padding-bottom: var(--zn-spacing-small);
|
|
412
|
+
background-color: var(--zn-color-primary-50);
|
|
413
|
+
}
|
|
414
|
+
|
|
382
415
|
.custom-form-group::part(form-control-label) {
|
|
383
416
|
color: var(--zn-color-primary-700);
|
|
384
417
|
}
|
|
@@ -6,13 +6,16 @@ layout: component
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
The Translation Group component wraps multiple `zn-translations` components in a panel-styled container with a shared
|
|
9
|
-
language select. The
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
language select. The fields sit in a [form group](/components/form-group), so the caption, help text and the select
|
|
10
|
+
share its label column and the fields line up with every other form group around them. Closed, the select carries how
|
|
11
|
+
many languages are done as a `1/5` chip; open, each language is marked translated, partial or falling back to English.
|
|
12
|
+
Type to filter the list — by name or by language code, so `de` finds German. Choosing a language switches every child
|
|
13
|
+
at once.
|
|
12
14
|
|
|
13
15
|
```html:preview
|
|
14
16
|
<zn-translation-group
|
|
15
17
|
label="Product Content"
|
|
18
|
+
help-text="Every field is translated one language at a time"
|
|
16
19
|
languages='{"en":"English","fr":"French","de":"German"}'>
|
|
17
20
|
<zn-translations
|
|
18
21
|
label="Name"
|
|
@@ -40,14 +43,16 @@ A translation group with two translation inputs sharing one language select.
|
|
|
40
43
|
</zn-translation-group>
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
###
|
|
46
|
+
### Label and Help Text
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
`label` names the section in the form group's label column, and `help-text` sits under it — the pair behave as they do
|
|
49
|
+
on any other form group. The language select follows them, in the group's chip position. `language-label` sets the
|
|
50
|
+
select's accessible name — it is not shown, since the caption names the section on screen.
|
|
47
51
|
|
|
48
52
|
```html:preview
|
|
49
53
|
<zn-translation-group
|
|
50
54
|
label="Page Translations"
|
|
55
|
+
help-text="Pick a language to edit every field in it"
|
|
51
56
|
languages='{"en":"English","fr":"French","es":"Spanish"}'>
|
|
52
57
|
<zn-translations label="Heading" name="heading"></zn-translations>
|
|
53
58
|
<zn-translations label="Body" name="body"></zn-translations>
|
|
@@ -209,7 +214,8 @@ The group emits a `zn-language-change` event when the active language changes.
|
|
|
209
214
|
|
|
210
215
|
| Property | Type | Default | Description |
|
|
211
216
|
|------------------|--------------------------|--------------------|---------------------------------------------------|
|
|
212
|
-
| `label` | `string` | `''` |
|
|
217
|
+
| `label` | `string` | `''` | Names the section, in the form group's label column |
|
|
218
|
+
| `help-text` | `string` | `''` | Sits under the label, above the language select |
|
|
213
219
|
| `language-label` | `string` | `'Edit Languages'` | The select's accessible name; not shown on screen |
|
|
214
220
|
| `inline` | `boolean` | `false` | Drops the panel border, background and padding |
|
|
215
221
|
| `languages` | `Record<string, string>` | `{en: "EN"}` | Object mapping language codes to display names |
|
|
@@ -226,19 +232,15 @@ The group emits a `zn-language-change` event when the active language changes.
|
|
|
226
232
|
| Slot | Description |
|
|
227
233
|
|-----------|------------------------------------------------------------|
|
|
228
234
|
| (default) | Place `<zn-translations>` elements here |
|
|
229
|
-
| `label` | Alternative to the `label` attribute for rich HTML content |
|
|
230
235
|
| `actions` | Buttons for the bottom of the body; `align="start"` on a child moves it to the left |
|
|
231
236
|
| `footer` | Content displayed in the grey panel footer |
|
|
232
237
|
|
|
233
|
-
The header carries the caption and the language select alone; nothing else is slotted into it.
|
|
234
|
-
|
|
235
238
|
## CSS Parts
|
|
236
239
|
|
|
237
|
-
| Part | Description
|
|
238
|
-
|
|
239
|
-
| `base` | The outer panel wrapper
|
|
240
|
-
| `
|
|
241
|
-
| `language-field` | The container holding the language select
|
|
242
|
-
| `language-select` | The select itself
|
|
243
|
-
| `actions` | The row of buttons at the bottom of the body
|
|
244
|
-
| `translations` | The body container wrapping the slotted children |
|
|
240
|
+
| Part | Description |
|
|
241
|
+
|-------------------|----------------------------------------------------------------------|
|
|
242
|
+
| `base` | The outer panel wrapper |
|
|
243
|
+
| `form-group` | The form group holding the caption, the language select and the fields |
|
|
244
|
+
| `language-field` | The container holding the language select, in the group's chip slot |
|
|
245
|
+
| `language-select` | The select itself |
|
|
246
|
+
| `actions` | The row of buttons at the bottom of the body |
|
|
@@ -128,8 +128,9 @@ Add `inline-edit` to read the translation as text until it is clicked, through
|
|
|
128
128
|
|
|
129
129
|
### Many Languages
|
|
130
130
|
|
|
131
|
-
Each language becomes an option labelled
|
|
132
|
-
|
|
131
|
+
Each language becomes an option labelled with its configured name — or its code, where `languages` does not name it.
|
|
132
|
+
The select takes any number of them, and its listbox scrolls once the list is longer than the space below it. Type to
|
|
133
|
+
filter the list: matching runs over the code as well as the name, so `de` finds German.
|
|
133
134
|
|
|
134
135
|
```html:preview
|
|
135
136
|
<zn-translations
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { classMap } from "lit/directives/class-map.js";
|
|
2
|
-
import { type CSSResultGroup, html, unsafeCSS } from 'lit';
|
|
2
|
+
import { type CSSResultGroup, html, type PropertyValues, unsafeCSS } from 'lit';
|
|
3
3
|
import { HasSlotController } from "../../internal/slot";
|
|
4
4
|
import { property } from 'lit/decorators.js';
|
|
5
5
|
import ZincElement from '../../internal/zinc-element';
|
|
@@ -16,6 +16,10 @@ import styles from './form-group.scss';
|
|
|
16
16
|
* @slot - The default slot.
|
|
17
17
|
* @slot chip - A chip displayed under the form group's help text.
|
|
18
18
|
*
|
|
19
|
+
* @csspart form-control-text - The column holding the label, help text and chip.
|
|
20
|
+
*
|
|
21
|
+
* @cssproperty --zn-form-group-sticky-top - Offset the label column sticks at while the inputs scroll past.
|
|
22
|
+
*
|
|
19
23
|
*/
|
|
20
24
|
export default class ZnFormGroup extends ZincElement {
|
|
21
25
|
static styles: CSSResultGroup = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
|
|
@@ -41,6 +45,165 @@ export default class ZnFormGroup extends ZincElement {
|
|
|
41
45
|
|
|
42
46
|
@property({ attribute: 'pad', type: Boolean }) pad: boolean = false;
|
|
43
47
|
|
|
48
|
+
/** The scroller the label is tracked against by hand; null while native sticky is enough. */
|
|
49
|
+
private tracked: HTMLElement | null = null;
|
|
50
|
+
private stickyTop: number = 0;
|
|
51
|
+
private frame: number = 0;
|
|
52
|
+
private rebind: boolean = false;
|
|
53
|
+
private offset: number = 0;
|
|
54
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
55
|
+
|
|
56
|
+
connectedCallback() {
|
|
57
|
+
super.connectedCallback();
|
|
58
|
+
|
|
59
|
+
// Whether an ancestor scrolls depends on how tall this form has grown.
|
|
60
|
+
this.resizeObserver ??= new ResizeObserver(() => this.schedule(true));
|
|
61
|
+
this.resizeObserver.observe(this);
|
|
62
|
+
window.addEventListener('resize', this.onViewportResize);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
disconnectedCallback() {
|
|
66
|
+
super.disconnectedCallback();
|
|
67
|
+
this.resizeObserver?.disconnect();
|
|
68
|
+
window.removeEventListener('resize', this.onViewportResize);
|
|
69
|
+
this.trackScroller(null);
|
|
70
|
+
cancelAnimationFrame(this.frame);
|
|
71
|
+
this.frame = 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected firstUpdated(changedProperties: PropertyValues) {
|
|
75
|
+
super.firstUpdated(changedProperties);
|
|
76
|
+
this.schedule(true);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private get labelColumn(): HTMLElement | null {
|
|
80
|
+
return this.shadowRoot?.querySelector('.form-control__text') ?? null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Coalesces scroll and resize work into one frame, and out of the ResizeObserver callback. */
|
|
84
|
+
private schedule(rebind: boolean = false) {
|
|
85
|
+
this.rebind ||= rebind;
|
|
86
|
+
if (this.frame) return;
|
|
87
|
+
|
|
88
|
+
this.frame = requestAnimationFrame(() => {
|
|
89
|
+
this.frame = 0;
|
|
90
|
+
if (this.rebind) {
|
|
91
|
+
this.rebind = false;
|
|
92
|
+
this.findScroller();
|
|
93
|
+
}
|
|
94
|
+
this.positionLabel();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Native sticky only follows the nearest scroll container. Where that container isn't the one
|
|
100
|
+
* the user actually scrolls — a `zn-panel` body sized to its content inside a scrolling
|
|
101
|
+
* slideout, say — the label never moves, so it gets translated by hand instead.
|
|
102
|
+
*/
|
|
103
|
+
private findScroller() {
|
|
104
|
+
const column = this.labelColumn;
|
|
105
|
+
if (!column) return;
|
|
106
|
+
|
|
107
|
+
column.style.top = '';
|
|
108
|
+
this.stickyTop = parseFloat(getComputedStyle(column).top) || 0;
|
|
109
|
+
|
|
110
|
+
const anchor = this.nearestScrollContainer(column);
|
|
111
|
+
const scroller = anchor ? this.scrollingAncestor(column) : null;
|
|
112
|
+
const anchored = !anchor || scroller === anchor;
|
|
113
|
+
|
|
114
|
+
this.trackScroller(anchored ? null : scroller);
|
|
115
|
+
|
|
116
|
+
// A `top` inset against a box that never scrolls has nothing to hold the label back from:
|
|
117
|
+
// it only pushes the label down the page, so drop it and let the transform do the work.
|
|
118
|
+
if (!anchored) column.style.top = '0px';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private trackScroller(scroller: HTMLElement | null) {
|
|
122
|
+
if (scroller === this.tracked) return;
|
|
123
|
+
|
|
124
|
+
this.scrollTarget(this.tracked)?.removeEventListener('scroll', this.onScroll);
|
|
125
|
+
this.tracked = scroller;
|
|
126
|
+
this.scrollTarget(this.tracked)?.addEventListener('scroll', this.onScroll, { passive: true });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The document scrolls through the window, every other scroller reports its own events. */
|
|
130
|
+
private scrollTarget(scroller: HTMLElement | null): EventTarget | null {
|
|
131
|
+
if (!scroller) return null;
|
|
132
|
+
return scroller === document.scrollingElement ? window : scroller;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private readonly onScroll = () => this.schedule();
|
|
136
|
+
|
|
137
|
+
// A shorter viewport can make an ancestor scrollable without changing this form's size.
|
|
138
|
+
private readonly onViewportResize = () => this.schedule(true);
|
|
139
|
+
|
|
140
|
+
private positionLabel() {
|
|
141
|
+
const column = this.labelColumn;
|
|
142
|
+
const fieldset = this.shadowRoot?.querySelector<HTMLElement>('.form-control');
|
|
143
|
+
if (!column || !fieldset) return;
|
|
144
|
+
|
|
145
|
+
let offset = 0;
|
|
146
|
+
|
|
147
|
+
// The stylesheet drops sticky while the columns are stacked; tracking has to stand down too.
|
|
148
|
+
if (this.tracked && getComputedStyle(column).position === 'sticky') {
|
|
149
|
+
const visibleTop = this.tracked === document.scrollingElement
|
|
150
|
+
? 0
|
|
151
|
+
: this.tracked.getBoundingClientRect().top;
|
|
152
|
+
const restingTop = column.getBoundingClientRect().top - this.offset;
|
|
153
|
+
const travel = Math.max(0, fieldset.clientHeight - column.offsetHeight);
|
|
154
|
+
|
|
155
|
+
offset = Math.min(Math.max(visibleTop + this.stickyTop - restingTop, 0), travel);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (Math.round(offset) === Math.round(this.offset)) return;
|
|
159
|
+
|
|
160
|
+
this.offset = offset;
|
|
161
|
+
column.style.transform = offset ? `translateY(${offset}px)` : '';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The box native sticky would anchor to, whether or not it can be scrolled. */
|
|
165
|
+
private nearestScrollContainer(from: HTMLElement): HTMLElement | null {
|
|
166
|
+
return this.ancestors(from).find(element => {
|
|
167
|
+
const style = getComputedStyle(element);
|
|
168
|
+
return this.isScrollContainer(style.overflowY) || this.isScrollContainer(style.overflowX);
|
|
169
|
+
}) ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The nearest ancestor the user can actually scroll, falling back to the document. */
|
|
173
|
+
private scrollingAncestor(from: HTMLElement): HTMLElement | null {
|
|
174
|
+
const scroller = this.ancestors(from).find(element => {
|
|
175
|
+
const overflow = getComputedStyle(element).overflowY;
|
|
176
|
+
return (overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay')
|
|
177
|
+
&& element.scrollHeight > element.clientHeight + 1;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (scroller) return scroller;
|
|
181
|
+
|
|
182
|
+
const root = document.scrollingElement as HTMLElement | null;
|
|
183
|
+
return root && root.scrollHeight > root.clientHeight + 1 ? root : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private isScrollContainer(overflow: string) {
|
|
187
|
+
return overflow === 'auto' || overflow === 'scroll' || overflow === 'hidden' || overflow === 'overlay';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Walks the flattened tree, so slots and shadow boundaries are crossed the way layout does. */
|
|
191
|
+
private ancestors(from: HTMLElement): HTMLElement[] {
|
|
192
|
+
const out: HTMLElement[] = [];
|
|
193
|
+
let node: Node | null = from;
|
|
194
|
+
|
|
195
|
+
while (node) {
|
|
196
|
+
const parent: Node | null = node instanceof Element && node.assignedSlot
|
|
197
|
+
? node.assignedSlot
|
|
198
|
+
: node.parentNode instanceof ShadowRoot ? node.parentNode.host : node.parentNode;
|
|
199
|
+
|
|
200
|
+
if (parent instanceof HTMLElement) out.push(parent);
|
|
201
|
+
node = parent;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
44
207
|
render() {
|
|
45
208
|
const hasLabelSlot = this.hasSlotController.test('label');
|
|
46
209
|
const hasLabelTooltipSlot = this.hasSlotController.test('label-tooltip');
|
|
@@ -65,7 +228,7 @@ export default class ZnFormGroup extends ZincElement {
|
|
|
65
228
|
|
|
66
229
|
<zn-cols layout="${this.layout}" part="form-control-container" class="form-control__container">
|
|
67
230
|
${hasLabel || hasHelpText || hasChip || this.forceCols ? html`
|
|
68
|
-
<div class="form-control__text">
|
|
231
|
+
<div part="form-control-text" class="form-control__text">
|
|
69
232
|
|
|
70
233
|
${hasLabel ? html`
|
|
71
234
|
<label
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
display: block;
|
|
5
5
|
line-height: var(--zn-line-height-dense);
|
|
6
6
|
max-width: var(--zn-container-lg);
|
|
7
|
-
--zn-col-gap: calc(var(--zn-spacing-medium, 20px) * 3)
|
|
7
|
+
--zn-col-gap: calc(var(--zn-spacing-medium, 20px) * 3);
|
|
8
|
+
--zn-form-group-sticky-top: var(--zn-spacing-medium, 20px);
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
.form-control {
|
|
@@ -27,6 +28,18 @@
|
|
|
27
28
|
--zn-col-basis: 200px;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// Only while the label sits beside the inputs: once zn-cols wraps them into one column a sticky
|
|
32
|
+
// label would scroll over the fields it labels. zn-cols wraps below the two columns' bases plus
|
|
33
|
+
// the gap, so 670px = --zn-col-basis * (1 + 2) + 70px — keep in step with both values below.
|
|
34
|
+
@container (min-width: 670px) {
|
|
35
|
+
.form-control__text {
|
|
36
|
+
position: sticky;
|
|
37
|
+
top: var(--zn-form-group-sticky-top);
|
|
38
|
+
// A stretched flex item is as tall as the row, leaving sticky nothing to travel through.
|
|
39
|
+
align-self: flex-start;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
.form-control__chip {
|
|
31
44
|
margin-top: var(--zn-spacing-x-small);
|
|
32
45
|
}
|
|
@@ -45,11 +58,13 @@
|
|
|
45
58
|
|
|
46
59
|
.form-control-input {
|
|
47
60
|
display: grid;
|
|
48
|
-
grid-template-columns: 1fr;
|
|
61
|
+
grid-template-columns: minmax(0, 1fr);
|
|
49
62
|
gap: var(--zn-spacing-medium) var(--zn-spacing-small);
|
|
50
63
|
|
|
64
|
+
// `1fr` floors each track at its min-content width, so one long label steals width from
|
|
65
|
+
// the other tracks and the spans stop lining up.
|
|
51
66
|
@include wc.media-query(md) {
|
|
52
|
-
grid-template-columns: repeat(6, 1fr);
|
|
67
|
+
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
53
68
|
}
|
|
54
69
|
}
|
|
55
70
|
|
|
@@ -26,4 +26,84 @@ describe('<zn-form-group>', () => {
|
|
|
26
26
|
|
|
27
27
|
expect(el.shadowRoot!.querySelector('[part="form-control-chip"]')).to.not.exist;
|
|
28
28
|
});
|
|
29
|
+
|
|
30
|
+
describe('sticky label', () => {
|
|
31
|
+
const tallForm = Array.from({length: 25}, (_, i) => `<zn-input label="Field ${i}"></zn-input>`).join('');
|
|
32
|
+
|
|
33
|
+
async function scrollPast(group: HTMLElement) {
|
|
34
|
+
group.innerHTML = tallForm;
|
|
35
|
+
await new Promise(resolve => setTimeout(resolve, 400));
|
|
36
|
+
|
|
37
|
+
const label = group.shadowRoot!.querySelector<HTMLElement>('.form-control__text')!;
|
|
38
|
+
const before = label.getBoundingClientRect().top;
|
|
39
|
+
|
|
40
|
+
for (let node: Node | null = label; node; node = flatParent(node)) {
|
|
41
|
+
if (node instanceof HTMLElement && node.scrollHeight > node.clientHeight + 1) node.scrollTop = 500;
|
|
42
|
+
}
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
44
|
+
|
|
45
|
+
return {before, after: label.getBoundingClientRect().top};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function flatParent(node: Node): Node | null {
|
|
49
|
+
if (node instanceof Element && node.assignedSlot) return node.assignedSlot;
|
|
50
|
+
return node.parentNode instanceof ShadowRoot ? node.parentNode.host : node.parentNode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
it('holds the label in view when the scroll container is the nearest one', async () => {
|
|
54
|
+
const el = await fixture<HTMLElement>(html`
|
|
55
|
+
<div style="max-height: 300px; overflow-y: auto">
|
|
56
|
+
<zn-form-group label="Sticky"></zn-form-group>
|
|
57
|
+
</div>`);
|
|
58
|
+
|
|
59
|
+
const {before, after} = await scrollPast(el.querySelector('zn-form-group')!);
|
|
60
|
+
|
|
61
|
+
expect(after).to.be.closeTo(before, 4);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('gives up sticky once the columns stack', async () => {
|
|
65
|
+
const wide = await fixture<HTMLElement>(html`
|
|
66
|
+
<div style="width: 900px"><zn-form-group label="Sticky"></zn-form-group></div>`);
|
|
67
|
+
const narrow = await fixture<HTMLElement>(html`
|
|
68
|
+
<div style="width: 500px"><zn-form-group label="Sticky"></zn-form-group></div>`);
|
|
69
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
70
|
+
|
|
71
|
+
const position = (root: HTMLElement) => {
|
|
72
|
+
const group = root.querySelector('zn-form-group')!;
|
|
73
|
+
return getComputedStyle(group.shadowRoot!.querySelector('.form-control__text')!).position;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
expect(position(wide)).to.equal('sticky');
|
|
77
|
+
expect(position(narrow)).to.equal('static');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('leaves the label level with the inputs while nothing scrolls', async () => {
|
|
81
|
+
const el = await fixture<HTMLElement>(html`
|
|
82
|
+
<div style="width: 900px">
|
|
83
|
+
<zn-panel>
|
|
84
|
+
<zn-form-group label="Sticky"><zn-input label="Name"></zn-input></zn-form-group>
|
|
85
|
+
</zn-panel>
|
|
86
|
+
</div>`);
|
|
87
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
88
|
+
|
|
89
|
+
const root = el.querySelector('zn-form-group')!.shadowRoot!;
|
|
90
|
+
const label = root.querySelector('.form-control__text')!.getBoundingClientRect().top;
|
|
91
|
+
const inputs = root.querySelector('.form-control-input')!.getBoundingClientRect().top;
|
|
92
|
+
|
|
93
|
+
expect(label).to.be.closeTo(inputs, 2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('holds the label in view when a panel sits between the form and the scroll container', async () => {
|
|
97
|
+
const el = await fixture<HTMLElement>(html`
|
|
98
|
+
<div style="max-height: 300px; overflow-y: auto">
|
|
99
|
+
<zn-panel>
|
|
100
|
+
<zn-form-group label="Sticky"></zn-form-group>
|
|
101
|
+
</zn-panel>
|
|
102
|
+
</div>`);
|
|
103
|
+
|
|
104
|
+
const {before, after} = await scrollPast(el.querySelector('zn-form-group')!);
|
|
105
|
+
|
|
106
|
+
expect(after).to.be.closeTo(before, 4);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
29
109
|
});
|