@kubex/zinc 1.1.121 → 1.1.122
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 +3110 -2079
- package/dist/vscode.html-custom-data.json +200 -178
- package/dist/web-types.json +528 -462
- package/dist/zn.d.ts +401 -296
- package/dist/zn.min.js +1478 -1337
- package/docs/pages/components/button.md +9 -0
- package/docs/pages/components/data-table-filter.md +60 -4
- package/docs/pages/components/data-table-search.md +3 -3
- package/docs/pages/components/data-table.md +78 -9
- package/docs/pages/components/datepicker.md +10 -0
- package/docs/pages/components/menu-item.md +16 -2
- package/package.json +1 -1
- package/src/components/button/button.component.ts +11 -4
- package/src/components/button/button.scss +12 -1
- package/src/components/data-table/data-table.component.ts +341 -139
- package/src/components/data-table/data-table.scss +105 -42
- package/src/components/data-table/data-table.test.ts +110 -0
- package/src/components/data-table-filter/data-table-filter.component.ts +487 -49
- package/src/components/data-table-filter/data-table-filter.scss +163 -9
- package/src/components/data-table-filter/data-table-filter.test.ts +328 -2
- package/src/components/data-table-search/data-table-search.component.ts +1 -1
- package/src/components/datepicker/datepicker.component.ts +23 -2
- package/src/components/datepicker/datepicker.scss +25 -0
- package/src/components/datepicker/datepicker.test.ts +28 -1
- package/src/components/input/input.component.ts +1 -0
- package/src/components/input/input.scss +1 -1
- package/src/components/menu/menu.component.ts +2 -0
- package/src/components/menu/menu.scss +23 -2
- package/src/components/menu-item/menu-item.component.ts +7 -1
- package/src/components/menu-item/menu-item.scss +4 -4
- package/src/components/menu-item/menu-item.test.ts +39 -1
- package/src/components/panel/panel.component.ts +12 -5
- package/src/components/panel/panel.scss +3 -3
- package/src/components/query-builder/query-builder.component.ts +1 -1
- package/src/components/tooltip/tooltip.component.ts +2 -2
- package/src/components/tooltip/tooltip.test.ts +70 -2
|
@@ -1,22 +1,176 @@
|
|
|
1
1
|
@use "../../wc";
|
|
2
2
|
|
|
3
|
+
// Fields inside a panel that already draws its own edge
|
|
4
|
+
@mixin flush-input {
|
|
5
|
+
--zn-input-box-shadow: none;
|
|
6
|
+
--zn-input-background-color: transparent;
|
|
7
|
+
--zn-input-background-color-hover: transparent;
|
|
8
|
+
--zn-input-background-color-focus: transparent;
|
|
9
|
+
--zn-input-border-color: transparent;
|
|
10
|
+
--zn-input-border-color-hover: transparent;
|
|
11
|
+
--zn-input-border-color-focus: transparent;
|
|
12
|
+
--zn-input-focus-ring-color: transparent;
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
:host {
|
|
4
16
|
display: block;
|
|
5
17
|
}
|
|
6
18
|
|
|
7
|
-
.
|
|
19
|
+
.filter-bar {
|
|
8
20
|
display: flex;
|
|
9
|
-
flex-direction:
|
|
10
|
-
|
|
21
|
+
flex-direction: row;
|
|
22
|
+
align-items: center;
|
|
23
|
+
flex-wrap: wrap;
|
|
24
|
+
gap: var(--zn-spacing-x-small);
|
|
25
|
+
|
|
26
|
+
&__clear::part(base) {
|
|
27
|
+
height: 32px;
|
|
28
|
+
min-height: 32px;
|
|
29
|
+
padding: 0 var(--zn-spacing-x-small);
|
|
30
|
+
font-weight: var(--zn-font-weight-normal);
|
|
31
|
+
text-transform: none;
|
|
32
|
+
white-space: nowrap;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&__clear::part(base) {
|
|
36
|
+
color: rgb(var(--zn-primary));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The chip draws the outline so its trigger and remove button read as one control
|
|
40
|
+
&__chip {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
height: 32px;
|
|
44
|
+
padding-right: var(--zn-spacing-x-small);
|
|
45
|
+
border: 1px solid rgb(var(--zn-border-color));
|
|
46
|
+
border-radius: var(--zn-border-radius-pill);
|
|
47
|
+
background-color: rgb(var(--zn-panel));
|
|
48
|
+
|
|
49
|
+
&:hover {
|
|
50
|
+
background-color: rgba(var(--zn-color-tab), 0.5);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The X's own box carries the trailing gap, so the trigger adds none
|
|
55
|
+
&__pill::part(base),
|
|
56
|
+
&__pill::part(base):hover {
|
|
57
|
+
border-color: transparent;
|
|
58
|
+
background: transparent;
|
|
59
|
+
padding-right: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Same box as the trigger, so the X centres on the chip's axis rather than
|
|
63
|
+
// inside a smaller box centred within it. The hover tint is clipped to the
|
|
64
|
+
// padded content box so it reads as a disc on the icon, not a full-height bar
|
|
65
|
+
&__remove::part(base) {
|
|
66
|
+
width: 24px;
|
|
67
|
+
min-width: 24px;
|
|
68
|
+
height: 32px;
|
|
69
|
+
min-height: 32px;
|
|
70
|
+
max-height: 32px;
|
|
71
|
+
padding: 4px 0;
|
|
72
|
+
background: transparent;
|
|
73
|
+
background-clip: content-box;
|
|
74
|
+
border-radius: var(--zn-border-radius-circle);
|
|
75
|
+
color: var(--zn-text-color);
|
|
76
|
+
}
|
|
11
77
|
|
|
12
|
-
&
|
|
78
|
+
&__remove::part(base):hover {
|
|
79
|
+
background: rgb(var(--zn-color-border));
|
|
80
|
+
background-clip: content-box;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Value editors get their own panel: a calendar has no business inside a menu's
|
|
84
|
+
// scroll container. The panel draws the only edge, so the editor inside it runs
|
|
85
|
+
// flush to it
|
|
86
|
+
&__editor {
|
|
13
87
|
display: flex;
|
|
14
|
-
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
align-items: stretch;
|
|
90
|
+
overflow: hidden;
|
|
91
|
+
border: 1px solid rgb(var(--zn-border-color));
|
|
92
|
+
border-radius: var(--zn-border-radius);
|
|
93
|
+
background-color: rgb(var(--zn-panel));
|
|
94
|
+
filter: drop-shadow(0 10px 8px rgba(0, 0, 0, 0.04)) drop-shadow(0 4px 3px rgba(0, 0, 0, 0.1));
|
|
95
|
+
|
|
96
|
+
zn-datepicker {
|
|
97
|
+
--zn-datepicker-inline-border-color: transparent;
|
|
98
|
+
}
|
|
15
99
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
width: 100%;
|
|
100
|
+
zn-input {
|
|
101
|
+
@include flush-input;
|
|
102
|
+
min-width: 200px;
|
|
20
103
|
}
|
|
21
104
|
}
|
|
105
|
+
|
|
106
|
+
// A long option list has to scroll rather than run off the screen, and its search
|
|
107
|
+
// field stays put while it does
|
|
108
|
+
zn-menu {
|
|
109
|
+
--zn-menu-max-height: 320px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Full-bleed so the rows scrolling underneath never show around or above it
|
|
113
|
+
&__option-search {
|
|
114
|
+
@include flush-input;
|
|
115
|
+
position: sticky;
|
|
116
|
+
top: calc(-1 * var(--zn-spacing-2x-small));
|
|
117
|
+
z-index: 1;
|
|
118
|
+
margin: calc(-1 * var(--zn-spacing-2x-small)) calc(-1 * var(--zn-spacing-2x-small)) 0;
|
|
119
|
+
background-color: rgb(var(--zn-panel));
|
|
120
|
+
border-bottom: 1px solid rgb(var(--zn-border-color));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&__suggestions {
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-direction: column;
|
|
126
|
+
align-items: stretch;
|
|
127
|
+
max-height: 220px;
|
|
128
|
+
overflow-y: auto;
|
|
129
|
+
border-top: 1px solid rgb(var(--zn-border-color));
|
|
130
|
+
@include wc.scrollbars;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
&__suggestion::part(base) {
|
|
134
|
+
justify-content: flex-start;
|
|
135
|
+
padding: var(--zn-spacing-x-small) var(--zn-spacing-small);
|
|
136
|
+
font-weight: var(--zn-font-weight-normal);
|
|
137
|
+
text-transform: none;
|
|
138
|
+
color: rgb(var(--zn-text));
|
|
139
|
+
text-align: left;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Segmented operator picker at the top of a value editor
|
|
143
|
+
&__operators {
|
|
144
|
+
display: flex;
|
|
145
|
+
flex-wrap: wrap;
|
|
146
|
+
gap: var(--zn-spacing-2x-small);
|
|
147
|
+
padding: var(--zn-spacing-x-small);
|
|
148
|
+
border-bottom: 1px solid rgb(var(--zn-border-color));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
&__operator::part(base) {
|
|
152
|
+
height: 26px;
|
|
153
|
+
min-height: 26px;
|
|
154
|
+
min-width: 30px;
|
|
155
|
+
padding: 0 var(--zn-spacing-x-small);
|
|
156
|
+
font-size: var(--zn-font-size-small);
|
|
157
|
+
font-weight: var(--zn-font-weight-normal);
|
|
158
|
+
text-transform: none;
|
|
159
|
+
color: rgb(var(--zn-text));
|
|
160
|
+
background-color: transparent;
|
|
161
|
+
border-color: rgb(var(--zn-border-color));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
&__operator--selected::part(base) {
|
|
165
|
+
color: rgb(var(--zn-primary));
|
|
166
|
+
border-color: rgb(var(--zn-primary));
|
|
167
|
+
background-color: rgba(var(--zn-primary), 0.05);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
&__separator {
|
|
171
|
+
flex: none;
|
|
172
|
+
height: 1px;
|
|
173
|
+
margin: var(--zn-spacing-2x-small) 0;
|
|
174
|
+
background-color: rgb(var(--zn-border-color));
|
|
175
|
+
}
|
|
22
176
|
}
|
|
@@ -1,10 +1,336 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
-
import {
|
|
2
|
+
import {expect, fixture, html, waitUntil} from '@open-wc/testing';
|
|
3
|
+
import type ZnDataTableFilter from './data-table-filter.component';
|
|
4
|
+
import type ZnDatepicker from '../datepicker/datepicker.component';
|
|
5
|
+
import type ZnDropdown from '../dropdown/dropdown.component';
|
|
6
|
+
import type ZnInput from '../input/input.component';
|
|
7
|
+
|
|
8
|
+
const FILTERS = [
|
|
9
|
+
{id: 'role', name: 'role', operators: ['in'], options: {pm: 'Project Manager', eng: 'Engineer'}},
|
|
10
|
+
{id: 'status', name: 'status', operators: ['eq'], options: {active: 'Active', inactive: 'Inactive'}},
|
|
11
|
+
{id: 'email', name: 'email', operators: ['contains']},
|
|
12
|
+
{id: 'created', name: 'created', type: 'date', operators: ['eq'], dateSubmitFormat: 'timestamp'},
|
|
13
|
+
{id: 'age', name: 'age', type: 'number', operators: ['eq', 'gte']}
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const clickOn = (el: Element | null | undefined) =>
|
|
17
|
+
el?.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
|
|
18
|
+
|
|
19
|
+
const decode = (value: string) => JSON.parse(atob(value)) as { key: string; comparator: string; value: string }[];
|
|
20
|
+
|
|
21
|
+
async function filterBar(defaultFilters = '') {
|
|
22
|
+
const el = await fixture<ZnDataTableFilter>(html`
|
|
23
|
+
<zn-data-table-filter default-filters="${defaultFilters}" .filters="${FILTERS}"></zn-data-table-filter>`);
|
|
24
|
+
await el.updateComplete;
|
|
25
|
+
return el;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const pills = (el: ZnDataTableFilter) =>
|
|
29
|
+
[...el.shadowRoot!.querySelectorAll('.filter-bar__pill')].map(p => p.textContent!.trim());
|
|
3
30
|
|
|
4
31
|
describe('<zn-data-table-filter>', () => {
|
|
5
32
|
it('should render a component', async () => {
|
|
6
|
-
const el = await fixture(html`
|
|
33
|
+
const el = await fixture(html`
|
|
34
|
+
<zn-data-table-filter></zn-data-table-filter>`);
|
|
7
35
|
|
|
8
36
|
expect(el).to.exist;
|
|
9
37
|
});
|
|
38
|
+
|
|
39
|
+
it('renders a pill for each default filter', async () => {
|
|
40
|
+
const el = await filterBar('role');
|
|
41
|
+
|
|
42
|
+
expect(pills(el)).to.deep.equal(['Role']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('offers the filters that are not already active', async () => {
|
|
46
|
+
const el = await filterBar('role');
|
|
47
|
+
|
|
48
|
+
const options = [...el.shadowRoot!.querySelectorAll('.filter-bar__add + zn-menu zn-menu-item, zn-menu zn-menu-item')]
|
|
49
|
+
.map(i => i.getAttribute('value'));
|
|
50
|
+
|
|
51
|
+
expect(options).to.include('status');
|
|
52
|
+
expect(options).to.include('email');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('encodes a selected value as the query-builder payload', async () => {
|
|
56
|
+
const el = await filterBar('status');
|
|
57
|
+
|
|
58
|
+
clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
|
|
59
|
+
await el.updateComplete;
|
|
60
|
+
|
|
61
|
+
expect(decode(el.value)).to.deep.equal([{key: 'status', comparator: 'eq', value: 'active'}]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('accumulates values for an `in` filter and counts them on the pill', async () => {
|
|
65
|
+
const el = await filterBar('role');
|
|
66
|
+
|
|
67
|
+
clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="pm"]'));
|
|
68
|
+
await el.updateComplete;
|
|
69
|
+
clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="eng"]'));
|
|
70
|
+
await el.updateComplete;
|
|
71
|
+
|
|
72
|
+
expect(decode(el.value)[0].value).to.equal('pm eng');
|
|
73
|
+
expect(pills(el)).to.deep.equal(['Role (2)']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('unsets a single-value filter when its selected option is picked again', async () => {
|
|
77
|
+
const el = await filterBar('status');
|
|
78
|
+
|
|
79
|
+
clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
|
|
80
|
+
await el.updateComplete;
|
|
81
|
+
clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
|
|
82
|
+
await el.updateComplete;
|
|
83
|
+
|
|
84
|
+
expect(el.value).to.equal('');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('leaves valueless filters out of the payload', async () => {
|
|
88
|
+
const el = await filterBar('role,status');
|
|
89
|
+
|
|
90
|
+
clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
|
|
91
|
+
await el.updateComplete;
|
|
92
|
+
|
|
93
|
+
expect(decode(el.value)).to.deep.equal([{key: 'status', comparator: 'eq', value: 'active'}]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('emits zn-filter-change when a value changes', async () => {
|
|
97
|
+
const el = await filterBar('status');
|
|
98
|
+
let emitted = 0;
|
|
99
|
+
el.addEventListener('zn-filter-change', () => emitted++);
|
|
100
|
+
|
|
101
|
+
clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
|
|
102
|
+
await waitUntil(() => emitted > 0);
|
|
103
|
+
|
|
104
|
+
expect(emitted).to.equal(1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('keeps a multi-value filter open on select and closes a single-value one', async () => {
|
|
108
|
+
const el = await filterBar('role,status');
|
|
109
|
+
const dropdowns = [...el.shadowRoot!.querySelectorAll<ZnDropdown>('zn-dropdown')];
|
|
110
|
+
|
|
111
|
+
const multi = dropdowns.find(d => d.querySelector('zn-menu-item[value="pm"]'))!;
|
|
112
|
+
const single = dropdowns.find(d => d.querySelector('zn-menu-item[value="active"]'))!;
|
|
113
|
+
|
|
114
|
+
expect(multi.stayOpenOnSelect).to.be.true;
|
|
115
|
+
expect(single.stayOpenOnSelect).to.be.false;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('counts a typed value as one, however many spaces it contains', async () => {
|
|
119
|
+
const el = await filterBar('email');
|
|
120
|
+
|
|
121
|
+
const input = el.shadowRoot!.querySelector<ZnInput>('zn-input')!;
|
|
122
|
+
input.value = 'thats really bad';
|
|
123
|
+
input.dispatchEvent(new Event('zn-input', {bubbles: true, composed: true}));
|
|
124
|
+
await waitUntil(() => el.value.length > 0);
|
|
125
|
+
await el.updateComplete;
|
|
126
|
+
|
|
127
|
+
expect(pills(el)).to.deep.equal(['Email: thats really bad']);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('matches suggestions without regard to case', async () => {
|
|
131
|
+
const el = await filterBar('email');
|
|
132
|
+
el.suggestions = {email: ['Ada@example.com', 'grace@example.com']};
|
|
133
|
+
|
|
134
|
+
const input = el.shadowRoot!.querySelector<ZnInput>('zn-input')!;
|
|
135
|
+
input.value = 'ADA';
|
|
136
|
+
input.dispatchEvent(new Event('zn-input', {bubbles: true, composed: true}));
|
|
137
|
+
await waitUntil(() => el.shadowRoot!.querySelectorAll('.filter-bar__suggestion').length > 0);
|
|
138
|
+
|
|
139
|
+
const shown = [...el.shadowRoot!.querySelectorAll('.filter-bar__suggestion')].map(b => b.textContent!.trim());
|
|
140
|
+
|
|
141
|
+
expect(shown).to.deep.equal(['Ada@example.com']);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('renders a text input for a filter with no options', async () => {
|
|
145
|
+
const el = await filterBar('email');
|
|
146
|
+
|
|
147
|
+
expect(el.shadowRoot!.querySelector('.filter-bar__editor zn-input')).to.exist;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('renders an inline calendar for a date filter', async () => {
|
|
151
|
+
const el = await filterBar('created');
|
|
152
|
+
|
|
153
|
+
const picker = el.shadowRoot!.querySelector('.filter-bar__editor zn-datepicker');
|
|
154
|
+
|
|
155
|
+
expect(picker).to.exist;
|
|
156
|
+
expect(picker!.hasAttribute('inline')).to.be.true;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('encodes a picked date and labels the pill with it', async () => {
|
|
160
|
+
const el = await filterBar('created');
|
|
161
|
+
const picker = el.shadowRoot!.querySelector<ZnDatepicker>('zn-datepicker')!;
|
|
162
|
+
await picker.updateComplete;
|
|
163
|
+
|
|
164
|
+
picker.shadowRoot!.querySelector<HTMLElement>('.air-datepicker-cell.-day-:not(.-other-month-)')!.click();
|
|
165
|
+
// AirDatepicker reports a selection on the next task, not synchronously
|
|
166
|
+
await waitUntil(() => el.value.length > 0);
|
|
167
|
+
await el.updateComplete;
|
|
168
|
+
|
|
169
|
+
const [filter] = decode(el.value);
|
|
170
|
+
|
|
171
|
+
expect(filter.key).to.equal('created');
|
|
172
|
+
expect(filter.comparator).to.equal('eq');
|
|
173
|
+
expect(Number(filter.value)).to.be.greaterThan(0);
|
|
174
|
+
expect(pills(el)[0]).to.equal(`Created: ${picker.value as string}`);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('offers each operator when a filter defines more than one', async () => {
|
|
178
|
+
const el = await filterBar('age');
|
|
179
|
+
|
|
180
|
+
const operators = [...el.shadowRoot!.querySelectorAll('.filter-bar__operator')].map(o => o.textContent!.trim());
|
|
181
|
+
|
|
182
|
+
expect(operators).to.deep.equal(['is', '\u2265']);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('re-encodes the value with the operator the user picks', async () => {
|
|
186
|
+
const el = await filterBar('age');
|
|
187
|
+
|
|
188
|
+
const input = el.shadowRoot!.querySelector<ZnInput>('zn-input')!;
|
|
189
|
+
input.value = '18';
|
|
190
|
+
input.dispatchEvent(new Event('zn-input', {bubbles: true, composed: true}));
|
|
191
|
+
await waitUntil(() => el.value.length > 0);
|
|
192
|
+
|
|
193
|
+
clickOn(el.shadowRoot!.querySelectorAll('.filter-bar__operator')[1]);
|
|
194
|
+
await el.updateComplete;
|
|
195
|
+
|
|
196
|
+
expect(decode(el.value)).to.deep.equal([{key: 'age', comparator: 'gte', value: '18'}]);
|
|
197
|
+
expect(pills(el)).to.deep.equal(['Age \u2265 18']);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('lists operators above the options for a filter that has both', async () => {
|
|
201
|
+
const el = await fixture<ZnDataTableFilter>(html`
|
|
202
|
+
<zn-data-table-filter default-filters="state"
|
|
203
|
+
.filters="${[{
|
|
204
|
+
id: 'state',
|
|
205
|
+
name: 'state',
|
|
206
|
+
operators: ['eq', 'in'],
|
|
207
|
+
options: {open: 'Open', closed: 'Closed'}
|
|
208
|
+
}]}"></zn-data-table-filter>`);
|
|
209
|
+
await el.updateComplete;
|
|
210
|
+
|
|
211
|
+
const items = [...el.shadowRoot!.querySelectorAll('zn-menu-item')].map(i => i.textContent!.trim());
|
|
212
|
+
|
|
213
|
+
expect(items).to.deep.equal(['is', 'is any of', 'Open', 'Closed']);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('opens the new pill so its value can be set without a second click', async () => {
|
|
217
|
+
const el = await filterBar();
|
|
218
|
+
|
|
219
|
+
clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="status"]'));
|
|
220
|
+
await el.updateComplete;
|
|
221
|
+
|
|
222
|
+
const pill = [...el.shadowRoot!.querySelectorAll<ZnDropdown>('zn-dropdown')]
|
|
223
|
+
.find(dropdown => dropdown.dataset.key === 'status')!;
|
|
224
|
+
|
|
225
|
+
await waitUntil(() => pill.open, 'the new filter\'s panel never opened');
|
|
226
|
+
expect(pill.open).to.be.true;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('keeps the panel open for an operator but closes it for a value', async () => {
|
|
230
|
+
const el = await fixture<ZnDataTableFilter>(html`
|
|
231
|
+
<zn-data-table-filter default-filters="state"
|
|
232
|
+
.filters="${[{
|
|
233
|
+
id: 'state',
|
|
234
|
+
name: 'state',
|
|
235
|
+
operators: ['eq', 'in'],
|
|
236
|
+
options: {open: 'Open', closed: 'Closed'}
|
|
237
|
+
}]}"></zn-data-table-filter>`);
|
|
238
|
+
await el.updateComplete;
|
|
239
|
+
|
|
240
|
+
const pill = el.shadowRoot!.querySelector<ZnDropdown>('zn-dropdown')!;
|
|
241
|
+
await pill.show();
|
|
242
|
+
|
|
243
|
+
const [operator] = [...pill.querySelectorAll('zn-menu-item')];
|
|
244
|
+
clickOn(operator);
|
|
245
|
+
await el.updateComplete;
|
|
246
|
+
expect(pill.open, 'picking an operator should leave the panel open').to.be.true;
|
|
247
|
+
|
|
248
|
+
clickOn(pill.querySelector('zn-menu-item[value="open"]'));
|
|
249
|
+
await waitUntil(() => !pill.open, 'picking a value should close the panel');
|
|
250
|
+
expect(pill.open).to.be.false;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('long option lists', () => {
|
|
254
|
+
const many = Object.fromEntries(
|
|
255
|
+
['How To', 'Linux', 'Beta', 'Router', 'TV', 'Other', 'Mac Hotspot Shield', 'General', 'iOS', 'Tutorials']
|
|
256
|
+
.map((label, index) => [`c${index}`, label])
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
async function categoryBar() {
|
|
260
|
+
const el = await fixture<ZnDataTableFilter>(html`
|
|
261
|
+
<zn-data-table-filter default-filters="category"
|
|
262
|
+
.filters="${[{
|
|
263
|
+
id: 'category',
|
|
264
|
+
name: 'category',
|
|
265
|
+
operators: ['eq'],
|
|
266
|
+
options: many
|
|
267
|
+
}]}"></zn-data-table-filter>`);
|
|
268
|
+
await el.updateComplete;
|
|
269
|
+
return el;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const search = async (el: ZnDataTableFilter, term: string) => {
|
|
273
|
+
const input = el.shadowRoot!.querySelector<ZnInput>('.filter-bar__option-search')!;
|
|
274
|
+
input.value = term;
|
|
275
|
+
input.dispatchEvent(new Event('zn-input', {bubbles: true, composed: true}));
|
|
276
|
+
await el.updateComplete;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const labels = (el: ZnDataTableFilter) =>
|
|
280
|
+
[...el.shadowRoot!.querySelectorAll('zn-menu-item')].map(item => item.textContent!.trim());
|
|
281
|
+
|
|
282
|
+
it('offers a search above the options', async () => {
|
|
283
|
+
const el = await categoryBar();
|
|
284
|
+
|
|
285
|
+
expect(el.shadowRoot!.querySelector('.filter-bar__option-search')).to.exist;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('leaves the search off a short list', async () => {
|
|
289
|
+
const el = await filterBar('status');
|
|
290
|
+
|
|
291
|
+
expect(el.shadowRoot!.querySelector('.filter-bar__option-search')).to.not.exist;
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('narrows the options without regard to case', async () => {
|
|
295
|
+
const el = await categoryBar();
|
|
296
|
+
|
|
297
|
+
await search(el, 'HOTSPOT');
|
|
298
|
+
|
|
299
|
+
expect(labels(el)).to.deep.equal(['Mac Hotspot Shield']);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('says so when nothing matches', async () => {
|
|
303
|
+
const el = await categoryBar();
|
|
304
|
+
|
|
305
|
+
await search(el, 'nothing here');
|
|
306
|
+
|
|
307
|
+
expect(labels(el)).to.deep.equal(['No matches']);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('drops a filter when its chip\'s remove button is clicked', async () => {
|
|
312
|
+
const el = await filterBar('role,status');
|
|
313
|
+
clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
|
|
314
|
+
await el.updateComplete;
|
|
315
|
+
|
|
316
|
+
const chip = [...el.shadowRoot!.querySelectorAll('.filter-bar__chip')]
|
|
317
|
+
.find(item => item.querySelector('zn-dropdown')!.getAttribute('data-key') === 'status')!;
|
|
318
|
+
clickOn(chip.querySelector('.filter-bar__remove'));
|
|
319
|
+
await el.updateComplete;
|
|
320
|
+
|
|
321
|
+
expect(pills(el)).to.deep.equal(['Role']);
|
|
322
|
+
expect(el.value).to.equal('');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('clear() removes every pill and empties the value', async () => {
|
|
326
|
+
const el = await filterBar('role,status');
|
|
327
|
+
clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
|
|
328
|
+
await el.updateComplete;
|
|
329
|
+
|
|
330
|
+
el.clear();
|
|
331
|
+
await el.updateComplete;
|
|
332
|
+
|
|
333
|
+
expect(pills(el)).to.deep.equal([]);
|
|
334
|
+
expect(el.value).to.equal('');
|
|
335
|
+
});
|
|
10
336
|
});
|
|
@@ -187,7 +187,7 @@ export default class ZnDataTableSearch extends ZincElement implements ZincFormCo
|
|
|
187
187
|
clearable
|
|
188
188
|
@zn-input="${this.handleInput}"
|
|
189
189
|
@zn-clear="${this.handleClear}">
|
|
190
|
-
<zn-icon slot="
|
|
190
|
+
<zn-icon slot="suffix" src="search@lu" size="20"></zn-icon>
|
|
191
191
|
</zn-input>
|
|
192
192
|
<slot style="display: none"></slot>
|
|
193
193
|
</div>
|
|
@@ -53,6 +53,7 @@ import styles from './datepicker.scss';
|
|
|
53
53
|
* - yyyy/MM/dd (2024/12/31)
|
|
54
54
|
*
|
|
55
55
|
* @cssproperty --zn-input-* - Inherited input component CSS custom properties.
|
|
56
|
+
* @cssproperty --zn-datepicker-inline-border-color - Border colour of the calendar in `inline` mode.
|
|
56
57
|
*/
|
|
57
58
|
export default class ZnDatepicker extends ZincElement implements ZincFormControl {
|
|
58
59
|
static styles: CSSResultGroup = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
|
|
@@ -120,6 +121,9 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
|
|
|
120
121
|
|
|
121
122
|
@property({type: Boolean, reflect: true}) flush = false;
|
|
122
123
|
|
|
124
|
+
/** Renders the calendar in place instead of a popup, hiding the text input. */
|
|
125
|
+
@property({type: Boolean, reflect: true}) inline = false;
|
|
126
|
+
|
|
123
127
|
/** Makes the input a required field. */
|
|
124
128
|
@property({type: Boolean, reflect: true}) required = false;
|
|
125
129
|
|
|
@@ -232,6 +236,7 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
|
|
|
232
236
|
@watch('disablePastDates', {waitUntilFirstUpdate: true})
|
|
233
237
|
@watch('timePicker', {waitUntilFirstUpdate: true})
|
|
234
238
|
@watch('timeFormat', {waitUntilFirstUpdate: true})
|
|
239
|
+
@watch('inline', {waitUntilFirstUpdate: true})
|
|
235
240
|
handleDatepickerOptionsChange() {
|
|
236
241
|
this.init();
|
|
237
242
|
}
|
|
@@ -285,6 +290,7 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
|
|
|
285
290
|
|
|
286
291
|
const options: AirDatepickerOptions = {
|
|
287
292
|
locale: enLocale,
|
|
293
|
+
inline: this.inline,
|
|
288
294
|
dateFormat: this.format,
|
|
289
295
|
range: this.range,
|
|
290
296
|
toggleSelected: false,
|
|
@@ -296,6 +302,10 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
|
|
|
296
302
|
onSelect: ({date}) => {
|
|
297
303
|
// Blur the input after selection to prevent invisible keyboard navigation
|
|
298
304
|
if (this.timePicker || this.onlyTimepicker){
|
|
305
|
+
// An inline calendar never hides, so there's no later event to report the time on
|
|
306
|
+
if (this.inline) {
|
|
307
|
+
this.handleChange();
|
|
308
|
+
}
|
|
299
309
|
return
|
|
300
310
|
}
|
|
301
311
|
if (date && !this.range) {
|
|
@@ -333,6 +343,10 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
|
|
|
333
343
|
options.buttons = ['clear'];
|
|
334
344
|
}
|
|
335
345
|
|
|
346
|
+
if (this.inline) {
|
|
347
|
+
this.adoptCalendarStyles(this.shadowRoot!);
|
|
348
|
+
}
|
|
349
|
+
|
|
336
350
|
this._instance = new AirDatepicker(inputElement, options);
|
|
337
351
|
}
|
|
338
352
|
}
|
|
@@ -361,13 +375,20 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
|
|
|
361
375
|
|
|
362
376
|
// Document-level calendar styles can't pierce a shadow root, so adopt them where needed
|
|
363
377
|
const root = container.getRootNode();
|
|
364
|
-
if (root instanceof ShadowRoot
|
|
365
|
-
|
|
378
|
+
if (root instanceof ShadowRoot) {
|
|
379
|
+
this.adoptCalendarStyles(root);
|
|
366
380
|
}
|
|
367
381
|
|
|
368
382
|
return container;
|
|
369
383
|
}
|
|
370
384
|
|
|
385
|
+
private adoptCalendarStyles(root: ShadowRoot) {
|
|
386
|
+
const sheet = getAirDatepickerSheet();
|
|
387
|
+
if (!root.adoptedStyleSheets.includes(sheet)) {
|
|
388
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
371
392
|
/** Finds the closest `<dialog>` or popover ancestor, crossing shadow DOM boundaries. */
|
|
372
393
|
private findTopLayerAncestor(): HTMLElement | null {
|
|
373
394
|
let node: Node | null = this.assignedSlot ?? this.parentNode;
|
|
@@ -225,3 +225,28 @@
|
|
|
225
225
|
margin-inline-end: 0;
|
|
226
226
|
margin-inline-start: 0;
|
|
227
227
|
}
|
|
228
|
+
|
|
229
|
+
// Inline mode keeps the input in the DOM to carry the value and format, but the
|
|
230
|
+
// calendar is inserted as its sibling - so the input chrome has to stop clipping
|
|
231
|
+
// and sizing the row
|
|
232
|
+
:host([inline]) .input {
|
|
233
|
+
display: block;
|
|
234
|
+
height: auto;
|
|
235
|
+
overflow: visible;
|
|
236
|
+
border: none;
|
|
237
|
+
border-radius: 0;
|
|
238
|
+
background: none;
|
|
239
|
+
box-shadow: none;
|
|
240
|
+
cursor: default;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Lets a host that already draws a panel edge suppress the calendar's own
|
|
244
|
+
:host([inline]) .air-datepicker {
|
|
245
|
+
--adp-border-color-inline: var(--zn-datepicker-inline-border-color, rgb(var(--zn-border-color)));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
:host([inline]) .input__control,
|
|
249
|
+
:host([inline]) .input__prefix,
|
|
250
|
+
:host([inline]) .input__suffix {
|
|
251
|
+
display: none;
|
|
252
|
+
}
|
|
@@ -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
|
import type ZnDatepicker from './datepicker.component';
|
|
4
4
|
|
|
5
5
|
describe('<zn-datepicker>', () => {
|
|
@@ -9,6 +9,33 @@ describe('<zn-datepicker>', () => {
|
|
|
9
9
|
expect(el).to.exist;
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
+
describe('inline', () => {
|
|
13
|
+
it('renders the calendar in place and hides the text input', async () => {
|
|
14
|
+
const el = await fixture<ZnDatepicker>(html`<zn-datepicker inline></zn-datepicker>`);
|
|
15
|
+
await el.updateComplete;
|
|
16
|
+
|
|
17
|
+
const calendar = el.shadowRoot!.querySelector('.air-datepicker');
|
|
18
|
+
|
|
19
|
+
expect(calendar, 'calendar should live in the datepicker shadow root').to.exist;
|
|
20
|
+
expect(calendar!.classList.contains('-inline-')).to.be.true;
|
|
21
|
+
expect(getComputedStyle(el.input).display).to.equal('none');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('emits zn-change when a day is picked', async () => {
|
|
25
|
+
const el = await fixture<ZnDatepicker>(html`<zn-datepicker inline></zn-datepicker>`);
|
|
26
|
+
await el.updateComplete;
|
|
27
|
+
|
|
28
|
+
let emitted = 0;
|
|
29
|
+
el.addEventListener('zn-change', () => emitted++);
|
|
30
|
+
|
|
31
|
+
const day = el.shadowRoot!.querySelector<HTMLElement>('.air-datepicker-cell.-day-:not(.-other-month-)')!;
|
|
32
|
+
day.click();
|
|
33
|
+
await waitUntil(() => emitted > 0);
|
|
34
|
+
|
|
35
|
+
expect(el.value).to.not.equal('');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
12
39
|
describe('Escape key', () => {
|
|
13
40
|
it('blurs the inner input when focused and the calendar is closed', async () => {
|
|
14
41
|
const el = await fixture<ZnDatepicker>(html`<zn-datepicker></zn-datepicker>`);
|
|
@@ -68,6 +68,7 @@ import styles from './input.scss';
|
|
|
68
68
|
* @csspart password-toggle-button - The password toggle button.
|
|
69
69
|
* @csspart suffix - The container that wraps the suffix.
|
|
70
70
|
*
|
|
71
|
+
* @cssproperty --zn-input-box-shadow - Shadow cast by the field; set to `none` inside a panel that draws its own edge.
|
|
71
72
|
* @cssproperty --zn-range-track-height - The height of a range input's track.
|
|
72
73
|
* @cssproperty --zn-range-track-color - The color of a range input's track.
|
|
73
74
|
* @cssproperty --zn-range-thumb-size - The diameter of a range input's thumb.
|