@kubex/zinc 1.1.107 → 1.1.109
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 +1847 -1847
- package/dist/vscode.html-custom-data.json +71 -71
- package/dist/web-types.json +156 -156
- package/dist/zn.d.ts +7 -0
- package/dist/zn.min.js +319 -321
- package/docs/pages/components/opt-group.md +3 -1
- package/docs/pages/components/select.md +4 -2
- package/package.json +1 -1
- package/src/components/dialog/dialog.scss +11 -2
- package/src/components/opt-group/opt-group.component.ts +4 -4
- package/src/components/opt-group/opt-group.scss +10 -11
- package/src/components/page/page.test.ts +27 -0
- package/src/components/priority-list/priority-list.component.ts +17 -12
- package/src/components/priority-list/priority-list.test.ts +43 -0
- package/src/components/select/select.component.ts +11 -0
- package/src/components/select/select.scss +5 -3
- package/src/components/select/select.test.ts +59 -0
- package/src/components/tabs/tabs.component.ts +5 -0
- package/src/components/tabs/tabs.test.ts +64 -0
- package/src/internal/scroll.ts +25 -0
|
@@ -47,8 +47,10 @@ Use the `disabled` attribute to disable all options within a group. Disabled gro
|
|
|
47
47
|
|
|
48
48
|
When used inside a `<zn-select>` with the `search` attribute, group headers automatically hide when all of their child options are filtered out by the search query.
|
|
49
49
|
|
|
50
|
+
The `label` is matched by the search too: a query matching a group label keeps every option in that group visible, so searching `europe` returns the whole European group even though no option label contains the word. Group-label and option-label matches combine, so `japan` still narrows to the single option within Asia.
|
|
51
|
+
|
|
50
52
|
```html:preview
|
|
51
|
-
<zn-select label="Select a country" search placeholder="
|
|
53
|
+
<zn-select label="Select a country" search placeholder="Try 'europe' or 'japan'...">
|
|
52
54
|
<zn-opt-group label="North America">
|
|
53
55
|
<zn-option value="us">United States</zn-option>
|
|
54
56
|
<zn-option value="ca">Canada</zn-option>
|
|
@@ -148,7 +148,7 @@ Use the `clearable` attribute to make the control clearable. The clear button on
|
|
|
148
148
|
|
|
149
149
|
### Search
|
|
150
150
|
|
|
151
|
-
Use the `search` attribute to allow users to type into the select to filter the list of options. When the dropdown opens, the input becomes editable and options are filtered as you type using case-insensitive substring matching against labels and values.
|
|
151
|
+
Use the `search` attribute to allow users to type into the select to filter the list of options. When the dropdown opens, the input becomes editable and options are filtered as you type using case-insensitive substring matching against labels and values. Group labels are matched too — see [Grouping with Search](#grouping-with-search).
|
|
152
152
|
|
|
153
153
|
```html:preview
|
|
154
154
|
<zn-select label="Search for a country" search placeholder="Type to search...">
|
|
@@ -642,8 +642,10 @@ Use `<zn-opt-group>` to group options under labeled headers, similar to `<optgro
|
|
|
642
642
|
|
|
643
643
|
When `search` is enabled, group headers automatically hide when all of their child options are filtered out.
|
|
644
644
|
|
|
645
|
+
Group labels are searchable as well: typing text that matches a `<zn-opt-group>` label keeps every option in that group visible, so searching `europe` returns the whole European group even though no option label contains the word.
|
|
646
|
+
|
|
645
647
|
```html:preview
|
|
646
|
-
<zn-select label="Select a country" search placeholder="
|
|
648
|
+
<zn-select label="Select a country" search placeholder="Try 'europe' or 'japan'...">
|
|
647
649
|
<zn-opt-group label="North America">
|
|
648
650
|
<zn-option value="us">United States</zn-option>
|
|
649
651
|
<zn-option value="ca">Canada</zn-option>
|
package/package.json
CHANGED
|
@@ -13,10 +13,14 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
.dialog-container {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex: 1 1 auto;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
min-height: 0;
|
|
16
20
|
border-radius: var(--zn-border-radius);
|
|
17
21
|
background-color: var(--zn-panel-background-color);
|
|
18
22
|
margin: 2px;
|
|
19
|
-
overflow:
|
|
23
|
+
overflow: hidden;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
.dialog__header + .dialog__body {
|
|
@@ -29,13 +33,17 @@
|
|
|
29
33
|
width: calc(var(--width) + 1px);
|
|
30
34
|
overflow: hidden;
|
|
31
35
|
max-width: calc(100% - var(--zn-spacing-2x-large));
|
|
32
|
-
max-height: calc(100% -
|
|
36
|
+
max-height: calc(100% - 20vh);
|
|
33
37
|
background-color: var(--zn-panel-background-color);
|
|
34
38
|
border-radius: var(--zn-border-radius);
|
|
35
39
|
box-shadow: var(--zn-shadow-x-large);
|
|
36
40
|
padding: 0;
|
|
37
41
|
margin: 10vh auto;
|
|
38
42
|
|
|
43
|
+
&[open] {
|
|
44
|
+
display: flex;
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
&__header {
|
|
40
48
|
flex: 0 0 auto;
|
|
41
49
|
display: flex;
|
|
@@ -69,6 +77,7 @@
|
|
|
69
77
|
|
|
70
78
|
&__body {
|
|
71
79
|
flex: 1 1 auto;
|
|
80
|
+
min-height: 0;
|
|
72
81
|
display: block;
|
|
73
82
|
padding: var(--body-spacing);
|
|
74
83
|
overflow: auto;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type CSSResultGroup, html, unsafeCSS} from 'lit';
|
|
1
|
+
import {type CSSResultGroup, html, nothing, unsafeCSS} from 'lit';
|
|
2
2
|
import {property} from 'lit/decorators.js';
|
|
3
3
|
import {watch} from '../../internal/watch';
|
|
4
4
|
import ZincElement from '../../internal/zinc-element';
|
|
@@ -50,9 +50,9 @@ export default class ZnOptGroup extends ZincElement {
|
|
|
50
50
|
render() {
|
|
51
51
|
return html`
|
|
52
52
|
<div part="base" class="opt-group">
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
${this.label
|
|
54
|
+
? html`<div part="label" class="opt-group__label" aria-hidden="true">${this.label}</div>`
|
|
55
|
+
: nothing}
|
|
56
56
|
<div class="opt-group__options" role="none">
|
|
57
57
|
<slot @slotchange=${this.handleSlotChange}></slot>
|
|
58
58
|
</div>
|
|
@@ -8,30 +8,29 @@
|
|
|
8
8
|
display: none !important;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
:host(:not(:first-child)) {
|
|
12
|
+
border-block-start: solid var(--zn-panel-border-width) var(--zn-panel-border-color);
|
|
13
|
+
margin-block-start: var(--zn-spacing-2x-small);
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
.opt-group__label {
|
|
12
17
|
font-family: var(--zn-font-sans);
|
|
13
18
|
font-size: var(--zn-font-size-x-small);
|
|
14
19
|
font-weight: var(--zn-font-weight-semibold);
|
|
15
20
|
line-height: var(--zn-line-height-normal);
|
|
16
|
-
letter-spacing: var(--zn-letter-spacing-
|
|
17
|
-
color: var(--zn-color-neutral-500);
|
|
21
|
+
letter-spacing: var(--zn-letter-spacing-loose);
|
|
18
22
|
text-transform: uppercase;
|
|
19
|
-
|
|
20
|
-
padding-
|
|
23
|
+
color: var(--zn-color-neutral-600);
|
|
24
|
+
padding-block: var(--zn-spacing-x-small) var(--zn-spacing-2x-small);
|
|
25
|
+
padding-inline: var(--zn-spacing-small);
|
|
21
26
|
user-select: none;
|
|
22
27
|
-webkit-user-select: none;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
:host(:not(:first-of-type)) .opt-group__label {
|
|
26
|
-
border-block-start: solid var(--zn-panel-border-width) var(--zn-panel-border-color);
|
|
27
|
-
margin-block-start: var(--zn-spacing-x-small);
|
|
28
|
-
padding-block-start: var(--zn-spacing-small);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
30
|
.opt-group__options {
|
|
32
31
|
display: block;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
:host([disabled]) {
|
|
34
|
+
:host([disabled]) .opt-group__label {
|
|
36
35
|
opacity: 0.5;
|
|
37
36
|
}
|
|
@@ -499,6 +499,33 @@ describe('<zn-page>', () => {
|
|
|
499
499
|
expect(navItems[0].getAttribute('tab')).to.equal('');
|
|
500
500
|
});
|
|
501
501
|
|
|
502
|
+
it('opens the next tab at the top of the content', async () => {
|
|
503
|
+
const el = await fixture<ZnPage>(html`
|
|
504
|
+
<zn-page caption="Page Title" style="height: 200px;">
|
|
505
|
+
<zn-tab caption="Overview">
|
|
506
|
+
<div style="height: 2000px;">Overview Content</div>
|
|
507
|
+
</zn-tab>
|
|
508
|
+
<zn-tab caption="Details">
|
|
509
|
+
<div style="height: 2000px;">Details Content</div>
|
|
510
|
+
</zn-tab>
|
|
511
|
+
</zn-page>
|
|
512
|
+
`);
|
|
513
|
+
await aTimeout(60);
|
|
514
|
+
|
|
515
|
+
const content = el.shadowRoot!.querySelector<HTMLElement>('#content')!;
|
|
516
|
+
content.scrollTop = 600;
|
|
517
|
+
expect(content.scrollTop, 'the content never scrolled').to.be.greaterThan(0);
|
|
518
|
+
|
|
519
|
+
// The page's first tab is registered under an empty id, so leaving it is the
|
|
520
|
+
// case a "did the tab change?" test on the id alone gets wrong.
|
|
521
|
+
const navbar = el.shadowRoot!.querySelector('zn-navbar')!;
|
|
522
|
+
navbar.shadowRoot!.querySelectorAll<HTMLElement>('li:not(.more)')[1].click();
|
|
523
|
+
await aTimeout(80);
|
|
524
|
+
|
|
525
|
+
expect(el.getAttribute('active')).to.equal('details');
|
|
526
|
+
expect(content.scrollTop).to.equal(0);
|
|
527
|
+
});
|
|
528
|
+
|
|
502
529
|
it('shows a header border when scrolled without visible navigation', async () => {
|
|
503
530
|
const el = await fixture<ZnPage>(html`
|
|
504
531
|
<zn-page caption="Page Title" style="height: 120px;">
|
|
@@ -19,6 +19,14 @@ export interface PriorityItem {
|
|
|
19
19
|
priority: number;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function safeParse(value: string): unknown {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(value);
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
/**
|
|
23
31
|
* @summary A reorderable list where each item receives a numerical priority based on its position. Supports drag-and-drop
|
|
24
32
|
* and keyboard reordering. Priority values are submitted as form data via hidden inputs.
|
|
@@ -68,16 +76,14 @@ export default class ZnPriorityList extends ZincElement implements ZincFormContr
|
|
|
68
76
|
return JSON.stringify(control.getPriorityMap());
|
|
69
77
|
},
|
|
70
78
|
defaultValue: (control: ZnPriorityList) => control.defaultValue,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// ignore parse errors
|
|
80
|
-
}
|
|
79
|
+
// A form reset passes defaultValue straight back in, so this has to accept the
|
|
80
|
+
// key array as well as the serialised value other callers set.
|
|
81
|
+
setValue: (control: ZnPriorityList, value: string | string[]) => {
|
|
82
|
+
if (!value || (Array.isArray(value) && value.length === 0)) return;
|
|
83
|
+
|
|
84
|
+
const keys: unknown = Array.isArray(value) ? value : safeParse(value);
|
|
85
|
+
if (Array.isArray(keys) && keys.every((item: unknown): item is string => typeof item === 'string')) {
|
|
86
|
+
control.value = keys as string[];
|
|
81
87
|
}
|
|
82
88
|
},
|
|
83
89
|
});
|
|
@@ -199,9 +205,8 @@ export default class ZnPriorityList extends ZincElement implements ZincFormContr
|
|
|
199
205
|
} else {
|
|
200
206
|
this._value = domKeys;
|
|
201
207
|
}
|
|
202
|
-
|
|
203
|
-
this.defaultValue = [...this._value];
|
|
204
208
|
}
|
|
209
|
+
this.defaultValue = [...this._value];
|
|
205
210
|
this._assignSlotNames();
|
|
206
211
|
this._syncHiddenInputs();
|
|
207
212
|
this.requestUpdate();
|
|
@@ -84,6 +84,49 @@ describe('<zn-priority-list>', () => {
|
|
|
84
84
|
expect(hiddenInputs[1].value).to.equal('2');
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
it('should restore the original order when its form is reset', async () => {
|
|
88
|
+
const form = await fixture(html`
|
|
89
|
+
<form>
|
|
90
|
+
<zn-priority-list name="order">
|
|
91
|
+
<div value="alpha">Alpha</div>
|
|
92
|
+
<div value="beta">Beta</div>
|
|
93
|
+
<div value="gamma">Gamma</div>
|
|
94
|
+
</zn-priority-list>
|
|
95
|
+
</form>
|
|
96
|
+
`) as HTMLFormElement;
|
|
97
|
+
|
|
98
|
+
const el = form.querySelector('zn-priority-list') as any;
|
|
99
|
+
await el.updateComplete;
|
|
100
|
+
expect(el.value).to.deep.equal(['alpha', 'beta', 'gamma']);
|
|
101
|
+
|
|
102
|
+
el.value = ['gamma', 'alpha', 'beta'];
|
|
103
|
+
await el.updateComplete;
|
|
104
|
+
expect(el.value).to.deep.equal(['gamma', 'alpha', 'beta']);
|
|
105
|
+
|
|
106
|
+
form.reset();
|
|
107
|
+
await el.updateComplete;
|
|
108
|
+
expect(el.value).to.deep.equal(['alpha', 'beta', 'gamma']);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should not empty a populated list on reset', async () => {
|
|
112
|
+
const form = await fixture(html`
|
|
113
|
+
<form>
|
|
114
|
+
<zn-priority-list name="order">
|
|
115
|
+
<div value="alpha">Alpha</div>
|
|
116
|
+
<div value="beta">Beta</div>
|
|
117
|
+
</zn-priority-list>
|
|
118
|
+
</form>
|
|
119
|
+
`) as HTMLFormElement;
|
|
120
|
+
|
|
121
|
+
const el = form.querySelector('zn-priority-list') as any;
|
|
122
|
+
await el.updateComplete;
|
|
123
|
+
|
|
124
|
+
el.defaultValue = [];
|
|
125
|
+
form.reset();
|
|
126
|
+
await el.updateComplete;
|
|
127
|
+
expect(el.value).to.deep.equal(['alpha', 'beta']);
|
|
128
|
+
});
|
|
129
|
+
|
|
87
130
|
it('should have listbox role on the list container', async () => {
|
|
88
131
|
const el = await fixture(html`
|
|
89
132
|
<zn-priority-list label="My List">
|
|
@@ -832,10 +832,21 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
832
832
|
return;
|
|
833
833
|
}
|
|
834
834
|
|
|
835
|
+
// A group whose label matches keeps its whole set of options, so searching "citrus" surfaces the group.
|
|
836
|
+
const matchedGroups = new Set(
|
|
837
|
+
this.getAllOptGroups().filter(group => group.label.toLowerCase().includes(searchQuery))
|
|
838
|
+
);
|
|
839
|
+
|
|
835
840
|
allOptions.forEach(option => {
|
|
836
841
|
// Don't un-hide selected options that are only in the DOM for value preservation
|
|
837
842
|
if (option.selected && option.hidden) return;
|
|
838
843
|
|
|
844
|
+
const group = option.closest<ZnOptGroup>('zn-opt-group');
|
|
845
|
+
if (group && matchedGroups.has(group)) {
|
|
846
|
+
option.hidden = false;
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
839
850
|
const label = option.getTextLabel().toLowerCase();
|
|
840
851
|
const value = option.value.toLowerCase();
|
|
841
852
|
const matches = label.includes(searchQuery) || value.includes(searchQuery);
|
|
@@ -407,9 +407,11 @@
|
|
|
407
407
|
font-size: var(--zn-font-size-x-small);
|
|
408
408
|
font-weight: var(--zn-font-weight-semibold);
|
|
409
409
|
line-height: var(--zn-line-height-normal);
|
|
410
|
-
letter-spacing: var(--zn-letter-spacing-
|
|
411
|
-
|
|
412
|
-
|
|
410
|
+
letter-spacing: var(--zn-letter-spacing-loose);
|
|
411
|
+
text-transform: uppercase;
|
|
412
|
+
color: var(--zn-color-neutral-600);
|
|
413
|
+
padding-block: var(--zn-spacing-x-small) var(--zn-spacing-2x-small);
|
|
414
|
+
padding-inline: var(--zn-spacing-small);
|
|
413
415
|
user-select: none;
|
|
414
416
|
-webkit-user-select: none;
|
|
415
417
|
}
|
|
@@ -163,6 +163,65 @@ describe('<zn-select>', () => {
|
|
|
163
163
|
});
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
+
describe('search across opt-group labels', () => {
|
|
167
|
+
let el: ZnSelect;
|
|
168
|
+
|
|
169
|
+
const type = async (text: string) => {
|
|
170
|
+
const displayInput = el.shadowRoot!.querySelector<HTMLInputElement>('.select__display-input')!;
|
|
171
|
+
displayInput.value = text;
|
|
172
|
+
displayInput.dispatchEvent(new Event('input'));
|
|
173
|
+
await el.updateComplete;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const visibleValues = () =>
|
|
177
|
+
[...el.querySelectorAll<ZnOption>('zn-option')].filter(o => !o.hidden).map(o => o.value);
|
|
178
|
+
|
|
179
|
+
beforeEach(async () => {
|
|
180
|
+
el = await fixture<ZnSelect>(html`
|
|
181
|
+
<zn-select search>
|
|
182
|
+
<zn-opt-group label="Citrus">
|
|
183
|
+
<zn-option value="orange">Orange</zn-option>
|
|
184
|
+
<zn-option value="lemon">Lemon</zn-option>
|
|
185
|
+
</zn-opt-group>
|
|
186
|
+
<zn-opt-group label="Berries">
|
|
187
|
+
<zn-option value="strawberry">Strawberry</zn-option>
|
|
188
|
+
<zn-option value="blueberry">Blueberry</zn-option>
|
|
189
|
+
</zn-opt-group>
|
|
190
|
+
<zn-option value="citrus-punch">Citrus Punch</zn-option>
|
|
191
|
+
</zn-select>
|
|
192
|
+
`);
|
|
193
|
+
await el.updateComplete;
|
|
194
|
+
await el.show();
|
|
195
|
+
await el.updateComplete;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('keeps every option in a group whose label matches, alongside option-label matches', async () => {
|
|
199
|
+
await type('citrus');
|
|
200
|
+
|
|
201
|
+
expect(visibleValues()).to.deep.equal(['orange', 'lemon', 'citrus-punch']);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('matches group labels case-insensitively', async () => {
|
|
205
|
+
await type('BERR');
|
|
206
|
+
|
|
207
|
+
expect(visibleValues()).to.deep.equal(['strawberry', 'blueberry']);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('keeps the matching group visible and hides the others', async () => {
|
|
211
|
+
await type('citrus');
|
|
212
|
+
|
|
213
|
+
const groups = el.querySelectorAll('zn-opt-group');
|
|
214
|
+
expect(groups[0].hidden, 'Citrus group').to.be.false;
|
|
215
|
+
expect(groups[1].hidden, 'Berries group').to.be.true;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('still matches option labels inside a non-matching group', async () => {
|
|
219
|
+
await type('lemon');
|
|
220
|
+
|
|
221
|
+
expect(visibleValues()).to.deep.equal(['lemon']);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
166
225
|
describe('Escape key', () => {
|
|
167
226
|
it('blurs the display input when dropdown is closed and Escape is pressed', async () => {
|
|
168
227
|
const el = await fixture<ZnSelect>(html`<zn-select></zn-select>`);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
replaceHistoryTab,
|
|
10
10
|
TAB_STORE_PREFIX
|
|
11
11
|
} from './tabs-navigation';
|
|
12
|
+
import {getScrolledAncestors, scrollToTop} from "../../internal/scroll";
|
|
12
13
|
import {HasSlotController} from "../../internal/slot";
|
|
13
14
|
import {ifDefined} from "lit/directives/if-defined.js";
|
|
14
15
|
import {md5} from "../../utilities/md5";
|
|
@@ -548,8 +549,12 @@ export default class ZnTabs extends ZincElement {
|
|
|
548
549
|
this._current = tabName;
|
|
549
550
|
};
|
|
550
551
|
|
|
552
|
+
const scrolled = sameTab ? [] : getScrolledAncestors(this._panel);
|
|
553
|
+
|
|
551
554
|
updateTabs();
|
|
552
555
|
|
|
556
|
+
scrollToTop(scrolled);
|
|
557
|
+
|
|
553
558
|
return true;
|
|
554
559
|
}
|
|
555
560
|
|
|
@@ -50,6 +50,70 @@ describe('<zn-tabs>', () => {
|
|
|
50
50
|
expect(selectedPagePanel.id).to.equal('');
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
describe('scroll position', () => {
|
|
54
|
+
const panels = html`
|
|
55
|
+
<zn-navbar slot="top">
|
|
56
|
+
<li tab="first">First</li>
|
|
57
|
+
<li tab="second">Second</li>
|
|
58
|
+
</zn-navbar>
|
|
59
|
+
<div id="first">
|
|
60
|
+
<div style="height: 2000px;">First panel</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div id="second">
|
|
63
|
+
<div style="height: 2000px;">Second panel</div>
|
|
64
|
+
</div>
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const openSecondTab = async (el: ZnTabs) => {
|
|
68
|
+
el.querySelector('zn-navbar')!.querySelectorAll<HTMLElement>('li')[1].click();
|
|
69
|
+
await aTimeout(40);
|
|
70
|
+
expect(el.getAttribute('active')).to.equal('second');
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
it('opens a newly selected tab at the top of its own scroller', async () => {
|
|
74
|
+
// zn-tabs is display:contents, so #content only has a height to overflow
|
|
75
|
+
// when an ancestor gives it one.
|
|
76
|
+
const wrapper = await fixture<HTMLElement>(html`
|
|
77
|
+
<div style="height: 200px; display: flex; flex-direction: column;">
|
|
78
|
+
<zn-tabs active="first">${panels}</zn-tabs>
|
|
79
|
+
</div>
|
|
80
|
+
`);
|
|
81
|
+
const el = wrapper.querySelector<ZnTabs>('zn-tabs')!;
|
|
82
|
+
await aTimeout(40);
|
|
83
|
+
|
|
84
|
+
const content = el.shadowRoot!.querySelector('#content')!;
|
|
85
|
+
content.scrollTop = 500;
|
|
86
|
+
expect(content.scrollTop, 'the content never scrolled').to.be.greaterThan(0);
|
|
87
|
+
|
|
88
|
+
await openSecondTab(el);
|
|
89
|
+
|
|
90
|
+
expect(content.scrollTop).to.equal(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('opens a newly selected tab at the top of an enclosing scroller', async () => {
|
|
94
|
+
// Unconstrained, the panels grow instead of scrolling and something further
|
|
95
|
+
// out - an app shell, an outer page - is what the reader has scrolled.
|
|
96
|
+
const wrapper = await fixture<HTMLElement>(html`
|
|
97
|
+
<div style="height: 200px; overflow: auto;">
|
|
98
|
+
<div>
|
|
99
|
+
<zn-tabs active="first">${panels}</zn-tabs>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
`);
|
|
103
|
+
const el = wrapper.querySelector<ZnTabs>('zn-tabs')!;
|
|
104
|
+
await aTimeout(40);
|
|
105
|
+
|
|
106
|
+
const content = el.shadowRoot!.querySelector('#content')!;
|
|
107
|
+
wrapper.scrollTop = 500;
|
|
108
|
+
expect(wrapper.scrollTop, 'the enclosing scroller never scrolled').to.be.greaterThan(0);
|
|
109
|
+
expect(content.scrollTop, 'the panels scrolled themselves').to.equal(0);
|
|
110
|
+
|
|
111
|
+
await openSecondTab(el);
|
|
112
|
+
|
|
113
|
+
expect(wrapper.scrollTop).to.equal(0);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
53
117
|
describe('selection persistence', () => {
|
|
54
118
|
const storeKey = 'tabs-navigation-test';
|
|
55
119
|
const originalHref = window.location.href;
|
package/src/internal/scroll.ts
CHANGED
|
@@ -67,3 +67,28 @@ export function scrollIntoView(
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Every ancestor of `element`, itself included, that is scrolled away from the top. Shadow
|
|
73
|
+
* boundaries are crossed, so a host's own scroller and the app's outer one are both found.
|
|
74
|
+
*/
|
|
75
|
+
export function getScrolledAncestors(element: Element | null | undefined): Element[] {
|
|
76
|
+
const scrolled: Element[] = [];
|
|
77
|
+
let node: Node | null | undefined = element;
|
|
78
|
+
|
|
79
|
+
while (node) {
|
|
80
|
+
if (node instanceof Element && node.scrollTop > 0) {
|
|
81
|
+
scrolled.push(node);
|
|
82
|
+
}
|
|
83
|
+
node = node.parentNode instanceof ShadowRoot ? node.parentNode.host : node.parentNode;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return scrolled;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Returns containers to the top. */
|
|
90
|
+
export function scrollToTop(containers: Iterable<Element>, behavior: ScrollBehavior = 'auto') {
|
|
91
|
+
for (const container of containers) {
|
|
92
|
+
container.scrollTo({ top: 0, behavior });
|
|
93
|
+
}
|
|
94
|
+
}
|