@kubex/zinc 1.1.106 → 1.1.108

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.106",
3
+ "version": "1.1.108",
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",
@@ -9,6 +9,7 @@ import {
9
9
  usStateDataProvider,
10
10
  } from "./providers/provider";
11
11
  import {type CSSResultGroup, html, nothing, type PropertyValues, unsafeCSS} from 'lit';
12
+ import {defaultValue} from "../../internal/default-value";
12
13
  import {FormControlController} from "../../internal/form";
13
14
  import {MutationController} from '@lit-labs/observers/mutation-controller.js';
14
15
  import {property, query} from 'lit/decorators.js';
@@ -72,6 +73,9 @@ export default class ZnDataSelect extends ZincElement implements ZincFormControl
72
73
  }
73
74
  }) value: string | string[] = '';
74
75
 
76
+ /** The default value of the form control. Primarily used for resetting the form control. */
77
+ @defaultValue() defaultValue: string | string[] = '';
78
+
75
79
  /** The provider of the select. */
76
80
  @property() provider: 'color' | 'currency' | 'country' | 'phone' | 'us-state';
77
81
 
@@ -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: auto;
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% - var(--zn-spacing-2x-large));
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,5 @@
1
1
  import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
2
+ import {defaultValue} from "../../internal/default-value";
2
3
  import {FormControlController} from "../../internal/form";
3
4
  import {property, query} from 'lit/decorators.js';
4
5
  import ZincElement from '../../internal/zinc-element';
@@ -40,6 +41,9 @@ export default class ZnLinkedSelect extends ZincElement implements ZincFormContr
40
41
  @property() name: string = "";
41
42
  @property() value: string;
42
43
 
44
+ /** The default value of the form control. Primarily used for resetting the form control. */
45
+ @defaultValue() defaultValue: string = '';
46
+
43
47
  @property({type: Boolean, reflect: true}) checked = false;
44
48
  @property({type: Array}) options: linkedSelectOptions;
45
49
  @property({attribute: 'linked-select'}) linkedSelect: string = "";
@@ -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
- setValue: (control: ZnPriorityList, value: string) => {
72
- if (value) {
73
- try {
74
- const parsed: unknown = JSON.parse(value);
75
- if (Array.isArray(parsed) && parsed.every((item: unknown): item is string => typeof item === 'string')) {
76
- control.value = parsed as string[];
77
- }
78
- } catch {
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">
@@ -1,5 +1,6 @@
1
1
  import {classMap} from "lit/directives/class-map.js";
2
2
  import {type CSSResultGroup, html, unsafeCSS} from 'lit';
3
+ import {defaultValue} from "../../internal/default-value";
3
4
  import {FormControlController, validValidityState} from "../../internal/form";
4
5
  import {HasSlotController} from "../../internal/slot";
5
6
  import {property, query, state} from 'lit/decorators.js';
@@ -61,6 +62,9 @@ export default class ZnRating extends ZincElement implements ZincFormControl {
61
62
 
62
63
  @property({type: Number}) value: number = 0;
63
64
 
65
+ /** The default value of the form control. Primarily used for resetting the form control. */
66
+ @defaultValue() defaultValue: number = 0;
67
+
64
68
  @property({type: Number}) max: number = 5;
65
69
 
66
70
  @property({type: Number}) precision: number = 1;
@@ -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;
@@ -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
+ }