@vaadin/component-base 24.0.0-alpha4 → 24.0.0-alpha5

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": "@vaadin/component-base",
3
- "version": "24.0.0-alpha4",
3
+ "version": "24.0.0-alpha5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,5 +42,5 @@
42
42
  "@vaadin/testing-helpers": "^0.3.2",
43
43
  "sinon": "^13.0.2"
44
44
  },
45
- "gitHead": "66be46e82c4d0a673859fbc9bdb1581dd89f360c"
45
+ "gitHead": "fc0b1721eda9e39cb289b239e440fc9e29573a31"
46
46
  }
@@ -39,7 +39,7 @@ const registered = new Set();
39
39
  export const ElementMixin = (superClass) =>
40
40
  class VaadinElementMixin extends DirMixin(superClass) {
41
41
  static get version() {
42
- return '24.0.0-alpha4';
42
+ return '24.0.0-alpha5';
43
43
  }
44
44
 
45
45
  /** @protected */
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2022 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import type { Constructor } from '@open-wc/dedupe-mixin';
7
+ import type { KeyboardDirectionMixinClass } from './keyboard-direction-mixin.js';
8
+ import type { KeyboardMixinClass } from './keyboard-mixin.js';
9
+
10
+ /**
11
+ * A mixin for list elements, facilitating navigation and selection of items.
12
+ */
13
+ export declare function ListMixin<T extends Constructor<HTMLElement>>(
14
+ base: T,
15
+ ): Constructor<KeyboardDirectionMixinClass> & Constructor<KeyboardMixinClass> & Constructor<ListMixinClass> & T;
16
+
17
+ export declare class ListMixinClass {
18
+ /**
19
+ * Used for mixin detection because `instanceof` does not work with mixins.
20
+ */
21
+ _hasVaadinListMixin: boolean;
22
+
23
+ /**
24
+ * The index of the item selected in the items array.
25
+ * Note: Not updated when used in `multiple` selection mode.
26
+ */
27
+ selected: number | null | undefined;
28
+
29
+ /**
30
+ * Define how items are disposed in the dom.
31
+ * Possible values are: `horizontal|vertical`.
32
+ * It also changes navigation keys from left/right to up/down.
33
+ */
34
+ orientation: 'horizontal' | 'vertical';
35
+
36
+ /**
37
+ * The list of items from which a selection can be made.
38
+ * It is populated from the elements passed to the light DOM,
39
+ * and updated dynamically when adding or removing items.
40
+ *
41
+ * The item elements must implement `Vaadin.ItemMixin`.
42
+ *
43
+ * Note: unlike `<vaadin-combo-box>`, this property is read-only,
44
+ * so if you want to provide items by iterating array of data,
45
+ * you have to use `dom-repeat` and place it to the light DOM.
46
+ */
47
+ readonly items: Element[] | undefined;
48
+
49
+ protected readonly _scrollerElement: HTMLElement;
50
+ }
@@ -0,0 +1,344 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2022 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
7
+ import { timeOut } from './async.js';
8
+ import { Debouncer } from './debounce.js';
9
+ import { DirHelper } from './dir-helper.js';
10
+ import { KeyboardDirectionMixin } from './keyboard-direction-mixin.js';
11
+
12
+ /**
13
+ * A mixin for list elements, facilitating navigation and selection of items.
14
+ *
15
+ * @polymerMixin
16
+ * @mixes KeyboardDirectionMixin
17
+ */
18
+ export const ListMixin = (superClass) =>
19
+ class ListMixinClass extends KeyboardDirectionMixin(superClass) {
20
+ static get properties() {
21
+ return {
22
+ /**
23
+ * Used for mixin detection because `instanceof` does not work with mixins.
24
+ * @type {boolean}
25
+ */
26
+ _hasVaadinListMixin: {
27
+ value: true,
28
+ },
29
+
30
+ /**
31
+ * The index of the item selected in the items array.
32
+ * Note: Not updated when used in `multiple` selection mode.
33
+ */
34
+ selected: {
35
+ type: Number,
36
+ reflectToAttribute: true,
37
+ notify: true,
38
+ },
39
+
40
+ /**
41
+ * Define how items are disposed in the dom.
42
+ * Possible values are: `horizontal|vertical`.
43
+ * It also changes navigation keys from left/right to up/down.
44
+ * @type {!ListOrientation}
45
+ */
46
+ orientation: {
47
+ type: String,
48
+ reflectToAttribute: true,
49
+ value: '',
50
+ },
51
+
52
+ /**
53
+ * The list of items from which a selection can be made.
54
+ * It is populated from the elements passed to the light DOM,
55
+ * and updated dynamically when adding or removing items.
56
+ *
57
+ * The item elements must implement `Vaadin.ItemMixin`.
58
+ *
59
+ * Note: unlike `<vaadin-combo-box>`, this property is read-only,
60
+ * so if you want to provide items by iterating array of data,
61
+ * you have to use `dom-repeat` and place it to the light DOM.
62
+ * @type {!Array<!Element> | undefined}
63
+ */
64
+ items: {
65
+ type: Array,
66
+ readOnly: true,
67
+ notify: true,
68
+ },
69
+
70
+ /**
71
+ * The search buffer for the keyboard selection feature.
72
+ * @private
73
+ */
74
+ _searchBuf: {
75
+ type: String,
76
+ value: '',
77
+ },
78
+ };
79
+ }
80
+
81
+ static get observers() {
82
+ return ['_enhanceItems(items, orientation, selected, disabled)'];
83
+ }
84
+
85
+ /** @protected */
86
+ ready() {
87
+ super.ready();
88
+
89
+ this.addEventListener('click', (e) => this._onClick(e));
90
+
91
+ this._observer = new FlattenedNodesObserver(this, () => {
92
+ this._setItems(this._filterItems(FlattenedNodesObserver.getFlattenedNodes(this)));
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Override method inherited from `KeyboardDirectionMixin`
98
+ * to use the stored list of item elements.
99
+ *
100
+ * @return {Element[]}
101
+ * @protected
102
+ * @override
103
+ */
104
+ _getItems() {
105
+ return this.items;
106
+ }
107
+
108
+ /** @private */
109
+ _enhanceItems(items, orientation, selected, disabled) {
110
+ if (!disabled) {
111
+ if (items) {
112
+ this.setAttribute('aria-orientation', orientation || 'vertical');
113
+ this.items.forEach((item) => {
114
+ if (orientation) {
115
+ item.setAttribute('orientation', orientation);
116
+ } else {
117
+ item.removeAttribute('orientation');
118
+ }
119
+ });
120
+
121
+ this._setFocusable(selected || 0);
122
+
123
+ const itemToSelect = items[selected];
124
+ items.forEach((item) => {
125
+ item.selected = item === itemToSelect;
126
+ });
127
+ if (itemToSelect && !itemToSelect.disabled) {
128
+ this._scrollToItem(selected);
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * @param {!Array<!Element>} array
136
+ * @return {!Array<!Element>}
137
+ * @protected
138
+ */
139
+ _filterItems(array) {
140
+ return array.filter((e) => e._hasVaadinItemMixin);
141
+ }
142
+
143
+ /**
144
+ * @param {!MouseEvent} event
145
+ * @protected
146
+ */
147
+ _onClick(event) {
148
+ if (event.metaKey || event.shiftKey || event.ctrlKey || event.defaultPrevented) {
149
+ return;
150
+ }
151
+
152
+ const item = this._filterItems(event.composedPath())[0];
153
+ let idx;
154
+ if (item && !item.disabled && (idx = this.items.indexOf(item)) >= 0) {
155
+ this.selected = idx;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * @param {number} currentIdx
161
+ * @param {string} key
162
+ * @return {number}
163
+ * @protected
164
+ */
165
+ _searchKey(currentIdx, key) {
166
+ this._searchReset = Debouncer.debounce(this._searchReset, timeOut.after(500), () => {
167
+ this._searchBuf = '';
168
+ });
169
+ this._searchBuf += key.toLowerCase();
170
+
171
+ if (!this.items.some((item) => this.__isMatchingKey(item))) {
172
+ this._searchBuf = key.toLowerCase();
173
+ }
174
+
175
+ const idx = this._searchBuf.length === 1 ? currentIdx + 1 : currentIdx;
176
+ return this._getAvailableIndex(
177
+ this.items,
178
+ idx,
179
+ 1,
180
+ (item) => this.__isMatchingKey(item) && getComputedStyle(item).display !== 'none',
181
+ );
182
+ }
183
+
184
+ /** @private */
185
+ __isMatchingKey(item) {
186
+ return item.textContent
187
+ .replace(/[^\p{L}\p{Nd}]/gu, '')
188
+ .toLowerCase()
189
+ .startsWith(this._searchBuf);
190
+ }
191
+
192
+ /**
193
+ * @return {boolean}
194
+ * @protected
195
+ */
196
+ get _isRTL() {
197
+ return !this._vertical && this.getAttribute('dir') === 'rtl';
198
+ }
199
+
200
+ /**
201
+ * Override an event listener from `KeyboardMixin`
202
+ * to search items by key.
203
+ *
204
+ * @param {!KeyboardEvent} event
205
+ * @protected
206
+ * @override
207
+ */
208
+ _onKeyDown(event) {
209
+ if (event.metaKey || event.ctrlKey) {
210
+ return;
211
+ }
212
+
213
+ const key = event.key;
214
+
215
+ const currentIdx = this.items.indexOf(this.focused);
216
+ if (/[a-zA-Z0-9]/.test(key) && key.length === 1) {
217
+ const idx = this._searchKey(currentIdx, key);
218
+ if (idx >= 0) {
219
+ this._focus(idx);
220
+ }
221
+ return;
222
+ }
223
+
224
+ super._onKeyDown(event);
225
+ }
226
+
227
+ /**
228
+ * @param {!Element} item
229
+ * @return {boolean}
230
+ * @protected
231
+ */
232
+ _isItemHidden(item) {
233
+ return getComputedStyle(item).display === 'none';
234
+ }
235
+
236
+ /**
237
+ * @param {number} idx
238
+ * @protected
239
+ */
240
+ _setFocusable(idx) {
241
+ idx = this._getAvailableIndex(this.items, idx, 1);
242
+ const item = this.items[idx];
243
+ this.items.forEach((e) => {
244
+ e.tabIndex = e === item ? 0 : -1;
245
+ });
246
+ }
247
+
248
+ /**
249
+ * @param {number} idx
250
+ * @protected
251
+ */
252
+ _focus(idx) {
253
+ this.items.forEach((e, index) => {
254
+ e.focused = index === idx;
255
+ });
256
+ this._setFocusable(idx);
257
+ this._scrollToItem(idx);
258
+ super._focus(idx);
259
+ }
260
+
261
+ focus() {
262
+ // In initialization (e.g vaadin-select) observer might not been run yet.
263
+ if (this._observer) {
264
+ this._observer.flush();
265
+ }
266
+ const firstItem = this.querySelector('[tabindex="0"]') || (this.items ? this.items[0] : null);
267
+ this._focusItem(firstItem);
268
+ }
269
+
270
+ /**
271
+ * @return {!HTMLElement}
272
+ * @protected
273
+ */
274
+ get _scrollerElement() {
275
+ // Returning scroller element of the component
276
+ console.warn(`Please implement the '_scrollerElement' property in <${this.localName}>`);
277
+ return this;
278
+ }
279
+
280
+ /**
281
+ * Scroll the container to have the next item by the edge of the viewport.
282
+ * @param {number} idx
283
+ * @protected
284
+ */
285
+ _scrollToItem(idx) {
286
+ const item = this.items[idx];
287
+ if (!item) {
288
+ return;
289
+ }
290
+
291
+ const props = this._vertical ? ['top', 'bottom'] : this._isRTL ? ['right', 'left'] : ['left', 'right'];
292
+
293
+ const scrollerRect = this._scrollerElement.getBoundingClientRect();
294
+ const nextItemRect = (this.items[idx + 1] || item).getBoundingClientRect();
295
+ const prevItemRect = (this.items[idx - 1] || item).getBoundingClientRect();
296
+
297
+ let scrollDistance = 0;
298
+ if (
299
+ (!this._isRTL && nextItemRect[props[1]] >= scrollerRect[props[1]]) ||
300
+ (this._isRTL && nextItemRect[props[1]] <= scrollerRect[props[1]])
301
+ ) {
302
+ scrollDistance = nextItemRect[props[1]] - scrollerRect[props[1]];
303
+ } else if (
304
+ (!this._isRTL && prevItemRect[props[0]] <= scrollerRect[props[0]]) ||
305
+ (this._isRTL && prevItemRect[props[0]] >= scrollerRect[props[0]])
306
+ ) {
307
+ scrollDistance = prevItemRect[props[0]] - scrollerRect[props[0]];
308
+ }
309
+
310
+ this._scroll(scrollDistance);
311
+ }
312
+
313
+ /**
314
+ * @return {boolean}
315
+ * @protected
316
+ */
317
+ get _vertical() {
318
+ return this.orientation !== 'horizontal';
319
+ }
320
+
321
+ /**
322
+ * @param {number} pixels
323
+ * @protected
324
+ */
325
+ _scroll(pixels) {
326
+ if (this._vertical) {
327
+ this._scrollerElement.scrollTop += pixels;
328
+ } else {
329
+ const dir = this.getAttribute('dir') || 'ltr';
330
+ const scrollType = DirHelper.detectScrollType();
331
+ const scrollLeft = DirHelper.getNormalizedScrollLeft(scrollType, dir, this._scrollerElement) + pixels;
332
+ DirHelper.setNormalizedScrollLeft(scrollType, dir, this._scrollerElement, scrollLeft);
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Fired when the selection is changed.
338
+ * Not fired when used in `multiple` selection mode.
339
+ *
340
+ * @event selected-changed
341
+ * @param {Object} detail
342
+ * @param {Object} detail.value the index of the item selected in the items array.
343
+ */
344
+ };
@@ -4,6 +4,7 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
7
+ import { isEmptyTextNode } from './dom-utils.js';
7
8
  import { generateUniqueId } from './unique-id-utils.js';
8
9
 
9
10
  /**
@@ -191,7 +192,10 @@ export class SlotController extends EventTarget {
191
192
 
192
193
  this.__slotObserver = new FlattenedNodesObserver(slot, (info) => {
193
194
  const current = this.multiple ? this.nodes : [this.node];
194
- const newNodes = info.addedNodes.filter((node) => !current.includes(node));
195
+
196
+ // Calling `slot.assignedNodes()` includes whitespace text nodes in case of default slot:
197
+ // unlike comment nodes, they are not filtered out. So we need to manually ignore them.
198
+ const newNodes = info.addedNodes.filter((node) => !isEmptyTextNode(node) && !current.includes(node));
195
199
 
196
200
  if (info.removedNodes.length) {
197
201
  info.removedNodes.forEach((node) => {