@kubex/zinc 1.1.159 → 1.1.161

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.
@@ -133,6 +133,21 @@ Use `priority` to control tab order. Lower numbers appear first after the empty-
133
133
  </zn-page>
134
134
  ```
135
135
 
136
+ ### Linking to a Tab
137
+
138
+ Give any element in the page content a `tab` attribute holding a tab's `id` to open that tab when it is clicked. Links inside a nested `zn-page` or `zn-tabs` open tabs in that nested container.
139
+
140
+ ```html:preview
141
+ <zn-page caption="Scorecard">
142
+ <zn-tab caption="Details" id="details">
143
+ <zn-button tab="section-general" icon="edit">Edit General section</zn-button>
144
+ </zn-tab>
145
+ <zn-tab caption="General" id="section-general">
146
+ General section settings
147
+ </zn-tab>
148
+ </zn-page>
149
+ ```
150
+
136
151
  ### Dynamic Tabs
137
152
 
138
153
  Set `uri` on `zn-tab` to create a dynamic tab. The generated navigation item receives `tab-uri`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.159",
3
+ "version": "1.1.161",
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",
@@ -128,6 +128,9 @@ export default class ZnButton extends ZincElement implements ZincFormControl {
128
128
  /** Accessible name for the button. Icon-only buttons fall back to `tooltip`. */
129
129
  @property() label: string;
130
130
 
131
+ /** Toggle state, forwarded to the inner button's `aria-pressed`. */
132
+ @property({attribute: 'aria-pressed'}) pressed: 'true' | 'false' | 'mixed';
133
+
131
134
  // Auto Click Specific
132
135
  @property({type: Boolean, attribute: 'auto-click'}) autoClick = false;
133
136
  @property({type: Number, attribute: 'auto-click-delay'}) autoClickDelay = 2000;
@@ -436,6 +439,7 @@ export default class ZnButton extends ZincElement implements ZincFormControl {
436
439
  rel="${ifDefined(isLink ? this.rel : undefined)}"
437
440
  gaid="${ifDefined(this.gaid)}"
438
441
  aria-label="${ifDefined(ariaLabel)}"
442
+ aria-pressed="${ifDefined(isLink ? undefined : this.pressed)}"
439
443
  data-notification="${ifDefined(notification)}"
440
444
  disabled="${this.disabled || nothing}"
441
445
  style=${styleMap(buttonStyles)}
@@ -29,4 +29,14 @@ describe('<zn-button>', () => {
29
29
  expect(el.color).to.equal('default');
30
30
  });
31
31
 
32
+ it('forwards aria-pressed to the inner button', async () => {
33
+ const el = await fixture<ZnButton>(html`<zn-button icon="sun" label="Toggle" aria-pressed="true"></zn-button>`);
34
+ const base = el.shadowRoot!.querySelector('[part~="base"]')!;
35
+ expect(base.getAttribute('aria-pressed')).to.equal('true');
36
+
37
+ el.setAttribute('aria-pressed', 'false');
38
+ await el.updateComplete;
39
+ expect(base.getAttribute('aria-pressed')).to.equal('false');
40
+ });
41
+
32
42
  });
@@ -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`${unsafeHTML(line)}<br>`)}
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;
@@ -19,7 +19,7 @@ describe('<zn-flow-node>', () => {
19
19
  expect(el).to.exist;
20
20
  });
21
21
 
22
- it('should render the type label and one port per output', async () => {
22
+ it('should render the type label and a single output stem', async () => {
23
23
  const el = await fixture<ZnFlowNode>(html`
24
24
  <zn-flow-node></zn-flow-node>`);
25
25
  el.node = node;
@@ -27,8 +27,7 @@ describe('<zn-flow-node>', () => {
27
27
  await el.updateComplete;
28
28
 
29
29
  expect(el.shadowRoot?.querySelector('.title')?.textContent).to.contain('Conditional Split');
30
- expect(el.shadowRoot?.querySelectorAll('.port--out')?.length).to.equal(2);
31
- expect(el.shadowRoot?.querySelector('.port-label')?.textContent).to.contain('TRUE');
30
+ expect(el.shadowRoot?.querySelectorAll('.port--out')?.length).to.equal(1);
32
31
  });
33
32
 
34
33
  it('should emit flow-node-grab when the card body is pressed', async () => {
@@ -1,20 +1,26 @@
1
1
  import '../../../../../dist/zn.min.js';
2
2
  import {expect, fixture, html} from '@open-wc/testing';
3
+ import type ZnCollapsible from '../../../collapsible/collapsible.component';
3
4
  import type ZnFlowStepGroup from './flow-step-group.component';
4
5
 
6
+ async function collapsibleOf(el: ZnFlowStepGroup): Promise<ZnCollapsible> {
7
+ const collapsible = el.shadowRoot!.querySelector<ZnCollapsible>('zn-collapsible')!;
8
+ await collapsible.updateComplete;
9
+ return collapsible;
10
+ }
11
+
5
12
  describe('<zn-flow-step-group>', () => {
6
- it('should render a component with its caption', async () => {
13
+ it('should render a collapsible with its caption', async () => {
7
14
  const el = await fixture<ZnFlowStepGroup>(html`
8
15
  <zn-flow-step-group caption="Contacts"></zn-flow-step-group>`);
9
- expect(el.shadowRoot?.querySelector('.header')?.textContent).to.contain('Contacts');
16
+ const collapsible = await collapsibleOf(el);
17
+ expect(collapsible.caption).to.equal('Contacts');
18
+ expect(collapsible.expanded).to.equal(true);
10
19
  });
11
20
 
12
- it('should toggle collapsed when the header is clicked', async () => {
21
+ it('should start closed when collapsed', async () => {
13
22
  const el = await fixture<ZnFlowStepGroup>(html`
14
- <zn-flow-step-group caption="Contacts"></zn-flow-step-group>`);
15
- const header = el.shadowRoot?.querySelector('.header') as HTMLElement;
16
- header.click();
17
- await el.updateComplete;
18
- expect(el.collapsed).to.equal(true);
23
+ <zn-flow-step-group caption="Contacts" collapsed></zn-flow-step-group>`);
24
+ expect((await collapsibleOf(el)).expanded).to.equal(false);
19
25
  });
20
26
  });
@@ -5,6 +5,9 @@ import { expect, fixture, html, oneEvent } from '@open-wc/testing';
5
5
  const clickButton = (button: HTMLElement) =>
6
6
  button.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true, cancelable: true }));
7
7
 
8
+ // The test harness loads no theme stylesheet, so a layout test stamps on the spacing token a themed page provides.
9
+ const spacingTokens = '--zn-spacing-small: 16px;';
10
+
8
11
  describe('<zn-form-actions>', () => {
9
12
  it('should render a component', async () => {
10
13
  const el = await fixture(html` <zn-form-actions></zn-form-actions> `);
@@ -24,7 +27,7 @@ describe('<zn-form-actions>', () => {
24
27
 
25
28
  it('holds align="start" children left and the rest right, one gap apart on each side', async () => {
26
29
  const el = await fixture<HTMLElement>(html`
27
- <zn-form-actions with-cancel style="width: 600px">
30
+ <zn-form-actions with-cancel style="width: 600px; ${spacingTokens}">
28
31
  <zn-button align="start" id="translate">Translate</zn-button>
29
32
  <zn-button align="start" id="slug">Slug</zn-button>
30
33
  </zn-form-actions>
@@ -54,12 +54,18 @@ export default class ZnFormGroup extends ZincElement {
54
54
  private rebind: boolean = false;
55
55
  private offset: number = 0;
56
56
  private resizeObserver: ResizeObserver | null = null;
57
+ private observedAncestors: Set<HTMLElement> = new Set();
57
58
 
58
59
  connectedCallback() {
59
60
  super.connectedCallback();
60
61
 
61
- // Whether an ancestor scrolls depends on how tall this form has grown.
62
- this.resizeObserver ??= new ResizeObserver(() => this.schedule(true));
62
+ // Whether an ancestor scrolls depends on how tall this form has grown. Re-measured before paint rather than on the
63
+ // next frame, or the label is drawn for a frame against the old layout and flickers.
64
+ this.resizeObserver ??= new ResizeObserver(() => {
65
+ this.rebind = false;
66
+ this.bind();
67
+ this.positionLabel();
68
+ });
63
69
  this.resizeObserver.observe(this);
64
70
  window.addEventListener('resize', this.onViewportResize);
65
71
  }
@@ -67,6 +73,7 @@ export default class ZnFormGroup extends ZincElement {
67
73
  disconnectedCallback() {
68
74
  super.disconnectedCallback();
69
75
  this.resizeObserver?.disconnect();
76
+ this.observedAncestors.clear();
70
77
  window.removeEventListener('resize', this.onViewportResize);
71
78
  this.detach();
72
79
  cancelAnimationFrame(this.frame);
@@ -82,7 +89,7 @@ export default class ZnFormGroup extends ZincElement {
82
89
  return this.shadowRoot?.querySelector('.form-control__text') ?? null;
83
90
  }
84
91
 
85
- /** Coalesces scroll and resize work into one frame, and out of the ResizeObserver callback. */
92
+ /** Coalesces scroll and resize work into one frame. */
86
93
  private schedule(rebind: boolean = false) {
87
94
  this.rebind ||= rebind;
88
95
  if (this.frame) return;
@@ -120,6 +127,7 @@ export default class ZnFormGroup extends ZincElement {
120
127
  // A `top` inset against a box that never scrolls has nothing to hold the label back from:
121
128
  // it only pushes the label down the page, so drop it and stand in for sticky below.
122
129
  column.style.top = '0px';
130
+ this.observeAncestors(column, scroller);
123
131
  if (!scroller) return;
124
132
 
125
133
  this.tracked = scroller;
@@ -154,24 +162,26 @@ export default class ZnFormGroup extends ZincElement {
154
162
  // The stylesheet drops sticky while the columns are stacked, and there is nothing to follow.
155
163
  if (getComputedStyle(column).position !== 'sticky') return null;
156
164
 
165
+ if (!('rangeStart' in Animation.prototype)) return null;
166
+
157
167
  const travel = Math.max(0, fieldset.clientHeight - column.offsetHeight);
158
- const range = scroller.scrollHeight - scroller.clientHeight;
159
- if (travel < 1 || range < 1) return null;
168
+ if (travel < 1) return null;
160
169
 
161
170
  const start = scroller.scrollTop + column.getBoundingClientRect().top
162
171
  - this.visibleTop(scroller) - this.stickyTop;
163
- const held = (scroll: number) => Math.min(Math.max(scroll - start, 0), travel);
164
172
 
165
- const knees = [start, start + travel].filter(scroll => scroll > 0 && scroll < range);
166
- const keyframes = [0, ...knees, range].map(scroll => ({
167
- offset: Math.min(Math.max(scroll / range, 0), 1),
168
- transform: `translateY(${held(scroll)}px)`
169
- }));
170
-
171
- return new Animation(
172
- new KeyframeEffect(column, keyframes, { fill: 'both' }),
173
+ const animation = new Animation(
174
+ new KeyframeEffect(column, [
175
+ { transform: 'translateY(0px)' },
176
+ { transform: `translateY(${travel}px)` }
177
+ ], { fill: 'both' }),
173
178
  new ScrollTimeline({ source: scroller, axis: 'block' })
174
- );
179
+ ) as Animation & { rangeStart: string; rangeEnd: string };
180
+
181
+ // In scroll pixels rather than progress, so the scroller growing or shrinking can't shift the label.
182
+ animation.rangeStart = `${start}px`;
183
+ animation.rangeEnd = `${start + travel}px`;
184
+ return animation;
175
185
  }
176
186
 
177
187
  /** Drops everything this component has put on the label or on the scroller. */
@@ -190,6 +200,26 @@ export default class ZnFormGroup extends ZincElement {
190
200
  column.style.transform = '';
191
201
  }
192
202
 
203
+ /**
204
+ * The label's travel is measured against the scroller, so content growing elsewhere in it — a sibling group revealing
205
+ * more fields — leaves the label at a stale position until something re-measures. Any such growth resizes a box
206
+ * between this group and the scroller.
207
+ */
208
+ private observeAncestors(from: HTMLElement, scroller: HTMLElement | null) {
209
+ const ancestors = this.ancestors(from);
210
+ const end = scroller ? ancestors.indexOf(scroller) : -1;
211
+ const wanted = new Set(ancestors.slice(0, end < 0 ? undefined : end).filter(element => element !== this));
212
+
213
+ // Observing fires once straight away, so re-observing on every bind would rebind every frame.
214
+ for (const element of this.observedAncestors) {
215
+ if (!wanted.has(element)) this.resizeObserver?.unobserve(element);
216
+ }
217
+ for (const element of wanted) {
218
+ if (!this.observedAncestors.has(element)) this.resizeObserver?.observe(element);
219
+ }
220
+ this.observedAncestors = wanted;
221
+ }
222
+
193
223
  /** The document scrolls through the window, every other scroller reports its own events. */
194
224
  private scrollTarget(scroller: HTMLElement | null): EventTarget | null {
195
225
  if (!scroller) return null;
@@ -154,7 +154,7 @@ describe('<zn-navbar>', () => {
154
154
  expect(nav.classList.contains('has-hidden')).to.equal(true);
155
155
  });
156
156
 
157
- it('rounds the last visible nav item when items are hidden behind more', async () => {
157
+ it('rounds the more button instead of the last visible nav item when items are hidden', async () => {
158
158
  const el = await fixture<ZnNavbar>(html`
159
159
  <zn-navbar>
160
160
  <li>One</li>
@@ -187,7 +187,8 @@ describe('<zn-navbar>', () => {
187
187
  expect(nav.classList.contains('has-hidden')).to.equal(true);
188
188
  expect(items[0].classList.contains('last-visible')).to.equal(true);
189
189
  expect(items[1].classList.contains('hidden')).to.equal(true);
190
- expect(getComputedStyle(items[0]).borderTopRightRadius).to.equal('6px');
190
+ expect(getComputedStyle(items[0]).borderTopRightRadius).to.equal('0px');
191
+ expect(getComputedStyle(more).borderTopRightRadius).to.equal('6px');
191
192
  });
192
193
 
193
194
  it('keeps the left border when only the more button is visible', async () => {
@@ -2,6 +2,7 @@ import {classMap} from 'lit/directives/class-map.js';
2
2
  import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
3
3
  import {HasSlotController} from '../../internal/slot';
4
4
  import {property, state} from 'lit/decorators.js';
5
+ import {tabContainerSelector} from '../tabs/tabs.component';
5
6
  import ZnButton from '../button';
6
7
  import ZnCopyButton from '../copy-button';
7
8
  import ZnExpandingAction from '../expanding-action';
@@ -30,6 +31,7 @@ interface TabDefinition {
30
31
  * @since 1.0
31
32
  *
32
33
  * @slot - Page content. Use zn-tab for named tabs and header-action/header-actions for header actions.
34
+ * Clicking an element in the content with `tab="<tab id>"` opens that tab.
33
35
  * @slot description - Rich subtitle/description content. Falls back to the `summary` attribute when empty.
34
36
  * @slot bottom - Content rendered below the navbar row (e.g. chips, filters). Forwarded to the navbar's bottom slot.
35
37
  */
@@ -68,6 +70,7 @@ export default class ZnPage extends ZnTabs {
68
70
  const connected = super.connectedCallback();
69
71
  window.addEventListener('alt-press', this.handleAltPress);
70
72
  window.addEventListener('alt-up', this.handleAltUp);
73
+ this.addEventListener('click', this.handleContentTabClick);
71
74
  this.prepareTabs();
72
75
  this.refreshExpandingActionsState();
73
76
  this.tabObserver = new MutationObserver((mutations) => {
@@ -90,6 +93,7 @@ export default class ZnPage extends ZnTabs {
90
93
  super.disconnectedCallback();
91
94
  window.removeEventListener('alt-press', this.handleAltPress);
92
95
  window.removeEventListener('alt-up', this.handleAltUp);
96
+ this.removeEventListener('click', this.handleContentTabClick);
93
97
  this.tabObserver?.disconnect();
94
98
  this.tabObserver = null;
95
99
  this.actionObserver?.disconnect();
@@ -280,6 +284,23 @@ export default class ZnPage extends ZnTabs {
280
284
  }
281
285
  }
282
286
 
287
+ // Only elements whose nearest tab container is this page count, so links inside a nested
288
+ // zn-tabs or zn-page drive that container instead.
289
+ private handleContentTabClick = (event: MouseEvent) => {
290
+ const link = event.composedPath().find((node): node is HTMLElement =>
291
+ node instanceof HTMLElement && node.hasAttribute('tab') && node.closest(tabContainerSelector) === this
292
+ );
293
+ if (!link || link.hasAttribute('disabled')) {
294
+ return;
295
+ }
296
+
297
+ const definition = this.tabDefinitions.find(tab => tab.id === link.getAttribute('tab'));
298
+ if (definition) {
299
+ event.preventDefault();
300
+ this.activateTabDefinition(definition, true, true);
301
+ }
302
+ };
303
+
283
304
  private handleNavigationSelect(event: ZnSelectEvent) {
284
305
  const item = event.detail.item as HTMLElement;
285
306
  if (this.getNavigationItemPage(item) !== this) {
@@ -383,16 +404,16 @@ export default class ZnPage extends ZnTabs {
383
404
  return this.pageHistoryKey;
384
405
  }
385
406
 
386
- private activateTabDefinition(tab: TabDefinition, store = false) {
407
+ private activateTabDefinition(tab: TabDefinition, store = false, pushHistory = false) {
387
408
  if (tab.uri) {
388
409
  const navItem = this.findNavItemForUri(tab.uri);
389
410
  if (navItem) {
390
- this.clickTab(navItem, false);
411
+ this.clickTab(navItem, false, pushHistory);
391
412
  this.syncNavigationActive(navItem);
392
413
  return;
393
414
  }
394
415
  }
395
- this.activateTab(tab.id, store);
416
+ this.activateTab(tab.id, store, pushHistory);
396
417
  }
397
418
 
398
419
  private findNavItemForUri(uri: string): HTMLElement | null {
@@ -517,7 +538,7 @@ export default class ZnPage extends ZnTabs {
517
538
  </div>
518
539
 
519
540
  <div class="page__tabs">
520
- <div id="content" class="page__content" part="content">
541
+ <div id="content" class="page__content" part="content" @scroll="${this.handlePageScroll}">
521
542
  ${this.tabDefinitions
522
543
  .filter(tab => tab.slotName !== null)
523
544
  .map(tab => html`
@@ -17,9 +17,9 @@ describe('<zn-page>', () => {
17
17
  await aTimeout(20);
18
18
 
19
19
  const header = el.shadowRoot!.querySelector('[part="header"]')!;
20
- const caption = el.shadowRoot!.querySelector('[part="header-caption"]')!;
21
- const description = el.shadowRoot!.querySelector('[part="header-description"]')!;
22
- const headerRight = el.shadowRoot!.querySelector('[part="header-right"]')!;
20
+ const caption = el.shadowRoot!.querySelector('.header__caption')!;
21
+ const description = el.shadowRoot!.querySelector('.header__description')!;
22
+ const headerRight = el.shadowRoot!.querySelector('.header__right')!;
23
23
  const navbar = el.shadowRoot!.querySelector('zn-navbar')!;
24
24
  const navItems = navbar.shadowRoot!.querySelectorAll('li:not(.more)');
25
25
  const actionsSlot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="actions"]')!;
@@ -350,6 +350,58 @@ describe('<zn-page>', () => {
350
350
  expect(innerDynamicPanel.hasAttribute('selected')).to.equal(true);
351
351
  });
352
352
 
353
+ it('opens a tab when an element in the content with a tab attribute is clicked', async () => {
354
+ const el = await fixture<ZnPage>(html`
355
+ <zn-page caption="Content Tab Link">
356
+ <zn-tab caption="Overview" id="overview">
357
+ <zn-button tab="details">Open details</zn-button>
358
+ <zn-button tab="details" disabled>Disabled link</zn-button>
359
+ </zn-tab>
360
+ <zn-tab caption="Details" id="details">Details Content</zn-tab>
361
+ </zn-page>
362
+ `);
363
+ await aTimeout(40);
364
+
365
+ // zn-button.click() skips event dispatch, so click the rendered button as a user would.
366
+ const [link, disabledLink] = Array.from(el.querySelectorAll('zn-button'))
367
+ .map(button => button.shadowRoot!.querySelector<HTMLElement>('button')!);
368
+
369
+ disabledLink.click();
370
+ await aTimeout(40);
371
+ expect(el.getAttribute('active')).to.equal('overview');
372
+
373
+ link.click();
374
+ await aTimeout(40);
375
+ expect(el.getAttribute('active')).to.equal('details');
376
+ expect(el.shadowRoot!.querySelector('#details')!.hasAttribute('selected')).to.equal(true);
377
+ const activeItem = el.shadowRoot!.querySelector<HTMLElement>('zn-navbar li.active');
378
+ expect(activeItem?.getAttribute('tab')).to.equal('details');
379
+ });
380
+
381
+ it('leaves content tab links inside a nested page to that page', async () => {
382
+ const outerPage = await fixture<ZnPage>(html`
383
+ <zn-page caption="Nested Content Tab Link">
384
+ <zn-tab caption="Outer One" id="outer-one">
385
+ <zn-page caption="Nested Content Tab Link Inner" nested>
386
+ <zn-tab caption="Inner One" id="inner-one">
387
+ <zn-button tab="inner-two">Inner link</zn-button>
388
+ </zn-tab>
389
+ <zn-tab caption="Inner Two" id="inner-two">Inner Two Content</zn-tab>
390
+ </zn-page>
391
+ </zn-tab>
392
+ <zn-tab caption="Outer Two" id="inner-two">Outer Two Content</zn-tab>
393
+ </zn-page>
394
+ `);
395
+ await aTimeout(80);
396
+
397
+ const innerPage = outerPage.querySelector<ZnPage>('zn-page')!;
398
+ innerPage.querySelector('zn-button')!.shadowRoot!.querySelector<HTMLElement>('button')!.click();
399
+ await aTimeout(40);
400
+
401
+ expect(innerPage.getAttribute('active')).to.equal('inner-two');
402
+ expect(outerPage.getAttribute('active')).to.equal('outer-one');
403
+ });
404
+
353
405
  it('keeps nested page tab selections scoped to the nested page', async () => {
354
406
  const outerPage = await fixture<ZnPage>(html`
355
407
  <zn-page caption="Outer Page">
@@ -536,25 +588,25 @@ describe('<zn-page>', () => {
536
588
  `);
537
589
  await aTimeout(20);
538
590
 
539
- const page = el.shadowRoot!.querySelector<HTMLElement>('.page')!;
591
+ const content = el.shadowRoot!.querySelector<HTMLElement>('#content')!;
540
592
  const header = el.shadowRoot!.querySelector<HTMLElement>('.page__header')!;
541
593
 
542
594
  expect(header.classList.contains('page__header--scrolled-no-navigation')).to.equal(false);
543
595
 
544
- page.scrollTop = 10;
545
- page.dispatchEvent(new Event('scroll'));
596
+ content.scrollTop = 10;
597
+ content.dispatchEvent(new Event('scroll'));
546
598
  await el.updateComplete;
547
599
 
548
600
  expect(header.classList.contains('page__header--scrolled-no-navigation')).to.equal(false);
549
601
 
550
- page.scrollTop = 25;
551
- page.dispatchEvent(new Event('scroll'));
602
+ content.scrollTop = 25;
603
+ content.dispatchEvent(new Event('scroll'));
552
604
  await el.updateComplete;
553
605
 
554
606
  expect(header.classList.contains('page__header--scrolled-no-navigation')).to.equal(true);
555
607
 
556
- page.scrollTop = 0;
557
- page.dispatchEvent(new Event('scroll'));
608
+ content.scrollTop = 0;
609
+ content.dispatchEvent(new Event('scroll'));
558
610
  await el.updateComplete;
559
611
 
560
612
  expect(header.classList.contains('page__header--scrolled-no-navigation')).to.equal(false);
@@ -604,18 +656,18 @@ describe('<zn-page>', () => {
604
656
  await aTimeout(20);
605
657
 
606
658
  const page = el.querySelector<ZnPage>('zn-page')!;
607
- const scroller = page.shadowRoot!.querySelector<HTMLElement>('.page')!;
659
+ const content = page.shadowRoot!.querySelector<HTMLElement>('.page__tabs')!;
608
660
  const header = page.shadowRoot!.querySelector<HTMLElement>('.page__header')!;
609
661
 
610
- expect(getComputedStyle(scroller).backgroundColor).to.equal('rgb(12, 34, 56)');
662
+ expect(getComputedStyle(content).backgroundColor).to.equal('rgb(12, 34, 56)');
611
663
  expect(getComputedStyle(header).backgroundColor).to.equal('rgba(12, 34, 56, 0.95)');
612
664
  });
613
665
 
614
- it('uses the panel background when placed inside a panel', async () => {
666
+ it('uses the panel background for a page flagged as sitting in a panel', async () => {
615
667
  const el = await fixture<HTMLElement>(html`
616
668
  <div style="--zn-body: 12, 34, 56; --zn-panel: 1, 2, 3;">
617
669
  <zn-panel flush>
618
- <zn-page caption="Page Title" modal>
670
+ <zn-page caption="Page Title" modal panel panel-head>
619
671
  <zn-tab caption="Overview">Overview Content</zn-tab>
620
672
  </zn-page>
621
673
  </zn-panel>
@@ -624,10 +676,10 @@ describe('<zn-page>', () => {
624
676
  await aTimeout(20);
625
677
 
626
678
  const page = el.querySelector<ZnPage>('zn-page')!;
627
- const scroller = page.shadowRoot!.querySelector<HTMLElement>('.page')!;
679
+ const content = page.shadowRoot!.querySelector<HTMLElement>('.page__tabs')!;
628
680
  const header = page.shadowRoot!.querySelector<HTMLElement>('.page__header')!;
629
681
 
630
- expect(getComputedStyle(scroller).backgroundColor).to.equal('rgb(1, 2, 3)');
682
+ expect(getComputedStyle(content).backgroundColor).to.equal('rgb(1, 2, 3)');
631
683
  expect(getComputedStyle(header).backgroundColor).to.equal('rgba(1, 2, 3, 0.95)');
632
684
  });
633
685
 
@@ -39,10 +39,10 @@ describe('<zn-sp>', () => {
39
39
  expect(getComputedStyle(parent.querySelector('form')!).display).to.equal('block');
40
40
  });
41
41
 
42
- it('should fill its parent container height', async () => {
42
+ it('should fill its parent container height when growing', async () => {
43
43
  const parent = await fixture(html`
44
44
  <div style="height: 300px;">
45
- <zn-sp>
45
+ <zn-sp grow>
46
46
  <div>Content</div>
47
47
  </zn-sp>
48
48
  </div>
@@ -25,7 +25,7 @@ import styles from './tabs.scss';
25
25
  // Every element that manages its own tabs (ZnTabs and its subclasses). Tab
26
26
  // registration must stop at these so an outer container never claims a nested
27
27
  // container's tabs.
28
- const tabContainerSelector = 'zn-tabs, zn-page, zn-page-nav';
28
+ export const tabContainerSelector = 'zn-tabs, zn-page, zn-page-nav';
29
29
 
30
30
  /**
31
31
  * @summary Short summary of the component's intended use.
@@ -830,8 +830,8 @@ export default class ZnThemeEditor extends ZincElement {
830
830
  icon-size="20"
831
831
  icon="${this.mode === 'dark' ? 'sun@lu' : 'moon@lu'}"
832
832
  data-mode-toggle
833
+ label="${this.mode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}"
833
834
  tooltip="${this.mode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}"
834
- aria-label="${this.mode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}"
835
835
  @click="${this._toggleMode}"></zn-button>
836
836
  </div>
837
837
  <div class="editor__controls-body">
@@ -881,7 +881,8 @@ export default class ZnThemeEditor extends ZincElement {
881
881
  icon-size="20"
882
882
  icon="${d.icon}@lu"
883
883
  data-device="${d.id}"
884
- aria-label="${d.label}"
884
+ label="${d.label}"
885
+ tooltip="${d.label}"
885
886
  aria-pressed="${this.device === d.id ? 'true' : 'false'}"
886
887
  @click="${() => this._setDevice(d.id)}"></zn-button>`)}
887
888
  </zn-button-group>