@vaadin/select 24.2.0-alpha3 → 24.2.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.
@@ -9,23 +9,13 @@ import './vaadin-select-list-box.js';
9
9
  import './vaadin-select-overlay.js';
10
10
  import './vaadin-select-value-button.js';
11
11
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
12
- import { setAriaIDReference } from '@vaadin/a11y-base/src/aria-id-reference.js';
13
- import { DelegateFocusMixin } from '@vaadin/a11y-base/src/delegate-focus-mixin.js';
14
- import { KeyboardMixin } from '@vaadin/a11y-base/src/keyboard-mixin.js';
15
12
  import { screenReaderOnly } from '@vaadin/a11y-base/src/styles/sr-only-styles.js';
16
- import { DelegateStateMixin } from '@vaadin/component-base/src/delegate-state-mixin.js';
17
13
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
18
- import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js';
19
- import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
20
14
  import { processTemplates } from '@vaadin/component-base/src/templates.js';
21
- import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
22
- import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
23
- import { FieldMixin } from '@vaadin/field-base/src/field-mixin.js';
24
- import { LabelController } from '@vaadin/field-base/src/label-controller.js';
25
15
  import { fieldShared } from '@vaadin/field-base/src/styles/field-shared-styles.js';
26
16
  import { inputFieldContainer } from '@vaadin/field-base/src/styles/input-field-container-styles.js';
27
17
  import { registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
28
- import { ButtonController } from './button-controller.js';
18
+ import { SelectBaseMixin } from './vaadin-select-base-mixin.js';
29
19
 
30
20
  registerStyles('vaadin-select', [fieldShared, inputFieldContainer, screenReaderOnly], {
31
21
  moduleId: 'vaadin-select-styles',
@@ -138,16 +128,10 @@ registerStyles('vaadin-select', [fieldShared, inputFieldContainer, screenReaderO
138
128
  *
139
129
  * @extends HTMLElement
140
130
  * @mixes ElementMixin
131
+ * @mixes SelectBaseMixin
141
132
  * @mixes ThemableMixin
142
- * @mixes FieldMixin
143
- * @mixes DelegateFocusMixin
144
- * @mixes DelegateStateMixin
145
- * @mixes KeyboardMixin
146
- * @mixes OverlayClassMixin
147
133
  */
148
- class Select extends OverlayClassMixin(
149
- DelegateFocusMixin(DelegateStateMixin(KeyboardMixin(FieldMixin(ElementMixin(ThemableMixin(PolymerElement)))))),
150
- ) {
134
+ class Select extends SelectBaseMixin(ElementMixin(ThemableMixin(PolymerElement))) {
151
135
  static get is() {
152
136
  return 'vaadin-select';
153
137
  }
@@ -194,6 +178,7 @@ class Select extends OverlayClassMixin(
194
178
  with-backdrop="[[_phone]]"
195
179
  phone$="[[_phone]]"
196
180
  theme$="[[_theme]]"
181
+ on-vaadin-overlay-open="_onOverlayOpen"
197
182
  ></vaadin-select-overlay>
198
183
 
199
184
  <slot name="tooltip"></slot>
@@ -203,202 +188,15 @@ class Select extends OverlayClassMixin(
203
188
  `;
204
189
  }
205
190
 
206
- static get properties() {
207
- return {
208
- /**
209
- * An array containing items that will be rendered as the options of the select.
210
- *
211
- * #### Example
212
- * ```js
213
- * select.items = [
214
- * { label: 'Most recent first', value: 'recent' },
215
- * { component: 'hr' },
216
- * { label: 'Rating: low to high', value: 'rating-asc' },
217
- * { label: 'Rating: high to low', value: 'rating-desc' },
218
- * { component: 'hr' },
219
- * { label: 'Price: low to high', value: 'price-asc', disabled: true },
220
- * { label: 'Price: high to low', value: 'price-desc', disabled: true }
221
- * ];
222
- * ```
223
- *
224
- * Note: each item is rendered by default as the internal `<vaadin-select-item>` that is an extension of `<vaadin-item>`.
225
- * To render the item with a custom component, provide a tag name by the `component` property.
226
- *
227
- * @type {!Array<!SelectItem>}
228
- */
229
- items: {
230
- type: Array,
231
- observer: '__itemsChanged',
232
- },
233
-
234
- /**
235
- * Set when the select is open
236
- * @type {boolean}
237
- */
238
- opened: {
239
- type: Boolean,
240
- value: false,
241
- notify: true,
242
- reflectToAttribute: true,
243
- observer: '_openedChanged',
244
- },
245
-
246
- /**
247
- * Custom function for rendering the content of the `<vaadin-select>`.
248
- * Receives two arguments:
249
- *
250
- * - `root` The `<vaadin-select-overlay>` internal container
251
- * DOM element. Append your content to it.
252
- * - `select` The reference to the `<vaadin-select>` element.
253
- * @type {!SelectRenderer | undefined}
254
- */
255
- renderer: Function,
256
-
257
- /**
258
- * The `value` property of the selected item, or an empty string
259
- * if no item is selected.
260
- * On change or initialization, the component finds the item which matches the
261
- * value and displays it.
262
- * If no value is provided to the component, it selects the first item without
263
- * value or empty value.
264
- * Hint: If you do not want to select any item by default, you can either set all
265
- * the values of inner vaadin-items, or set the vaadin-select value to
266
- * an inexistent value in the items list.
267
- * @type {string}
268
- */
269
- value: {
270
- type: String,
271
- value: '',
272
- notify: true,
273
- observer: '_valueChanged',
274
- },
275
-
276
- /**
277
- * The name of this element.
278
- */
279
- name: {
280
- type: String,
281
- },
282
-
283
- /**
284
- * A hint to the user of what can be entered in the control.
285
- * The placeholder will be displayed in the case that there
286
- * is no item selected, or the selected item has an empty
287
- * string label, or the selected item has no label and it's
288
- * DOM content is empty.
289
- */
290
- placeholder: {
291
- type: String,
292
- },
293
-
294
- /**
295
- * When present, it specifies that the element is read-only.
296
- * @type {boolean}
297
- */
298
- readonly: {
299
- type: Boolean,
300
- value: false,
301
- reflectToAttribute: true,
302
- },
303
-
304
- /** @private */
305
- _phone: Boolean,
306
-
307
- /** @private */
308
- _phoneMediaQuery: {
309
- value: '(max-width: 420px), (max-height: 420px)',
310
- },
311
-
312
- /** @private */
313
- _inputContainer: Object,
314
-
315
- /** @private */
316
- _items: Object,
317
- };
318
- }
319
-
320
- static get delegateAttrs() {
321
- return [...super.delegateAttrs, 'invalid'];
322
- }
323
-
324
191
  static get observers() {
325
- return [
326
- '_updateAriaExpanded(opened, focusElement)',
327
- '_updateSelectedItem(value, _items, placeholder)',
328
- '_rendererChanged(renderer, _overlayElement)',
329
- ];
330
- }
331
-
332
- constructor() {
333
- super();
334
-
335
- this._itemId = `value-${this.localName}-${generateUniqueId()}`;
336
- this._srLabelController = new LabelController(this);
337
- this._srLabelController.slotName = 'sr-label';
338
- }
339
-
340
- /** @protected */
341
- disconnectedCallback() {
342
- super.disconnectedCallback();
343
-
344
- // Making sure the select is closed and removed from DOM after detaching the select.
345
- this.opened = false;
192
+ return ['_rendererChanged(renderer, _overlayElement)'];
346
193
  }
347
194
 
348
195
  /** @protected */
349
196
  ready() {
350
197
  super.ready();
351
198
 
352
- this._overlayElement = this.shadowRoot.querySelector('vaadin-select-overlay');
353
- this._inputContainer = this.shadowRoot.querySelector('[part~="input-field"]');
354
-
355
- this._valueButtonController = new ButtonController(this);
356
- this.addController(this._valueButtonController);
357
- this.addController(this._srLabelController);
358
- this.addController(
359
- new MediaQueryController(this._phoneMediaQuery, (matches) => {
360
- this._phone = matches;
361
- }),
362
- );
363
-
364
199
  processTemplates(this);
365
-
366
- this._tooltipController = new TooltipController(this);
367
- this._tooltipController.setPosition('top');
368
- this.addController(this._tooltipController);
369
- }
370
-
371
- /**
372
- * Requests an update for the content of the select.
373
- * While performing the update, it invokes the renderer passed in the `renderer` property.
374
- *
375
- * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
376
- */
377
- requestContentUpdate() {
378
- if (!this._overlayElement) {
379
- return;
380
- }
381
-
382
- this._overlayElement.requestContentUpdate();
383
-
384
- if (this._menuElement && this._menuElement.items) {
385
- this._updateSelectedItem(this.value, this._menuElement.items);
386
- }
387
- }
388
-
389
- /**
390
- * Override an observer from `FieldMixin`
391
- * to validate when required is removed.
392
- *
393
- * @protected
394
- * @override
395
- */
396
- _requiredChanged(required) {
397
- super._requiredChanged(required);
398
-
399
- if (required === false) {
400
- this.validate();
401
- }
402
200
  }
403
201
 
404
202
  /**
@@ -411,387 +209,18 @@ class Select extends OverlayClassMixin(
411
209
  return;
412
210
  }
413
211
 
414
- overlay.setProperties({ owner: this, renderer: renderer || this.__defaultRenderer });
212
+ overlay.renderer = renderer || this.__defaultRenderer;
415
213
 
416
214
  this.requestContentUpdate();
417
215
  }
418
216
 
419
- /**
420
- * @param {SelectItem[] | undefined | null} newItems
421
- * @param {SelectItem[] | undefined | null} oldItems
422
- * @private
423
- */
424
- __itemsChanged(newItems, oldItems) {
425
- if (newItems || oldItems) {
426
- this.requestContentUpdate();
427
- }
428
- }
429
-
430
- /**
431
- * @param {HTMLElement} menuElement
432
- * @protected
433
- */
434
- _assignMenuElement(menuElement) {
435
- if (menuElement && menuElement !== this.__lastMenuElement) {
436
- this._menuElement = menuElement;
437
-
438
- // Ensure items are initialized
439
- this.__initMenuItems(menuElement);
440
-
441
- menuElement.addEventListener('items-changed', () => {
442
- this.__initMenuItems(menuElement);
443
- });
444
-
445
- menuElement.addEventListener('selected-changed', () => this.__updateValueButton());
446
- // Use capture phase to make it possible for `<vaadin-grid-pro-edit-select>`
447
- // to override and handle the keydown event before the value change happens.
448
- menuElement.addEventListener('keydown', (e) => this._onKeyDownInside(e), true);
449
- menuElement.addEventListener(
450
- 'click',
451
- () => {
452
- this.__userInteraction = true;
453
- this.opened = false;
454
- },
455
- true,
456
- );
457
-
458
- // Store the menu element reference
459
- this.__lastMenuElement = menuElement;
460
- }
461
- }
462
-
463
- /** @private */
464
- __initMenuItems(menuElement) {
465
- if (menuElement.items) {
466
- this._items = menuElement.items;
467
- }
468
- }
469
-
470
- /** @private */
471
- _valueChanged(value, oldValue) {
472
- this.toggleAttribute('has-value', Boolean(value));
473
-
474
- // Validate only if `value` changes after initialization.
475
- if (oldValue !== undefined) {
476
- this.validate();
477
- }
478
- }
479
-
480
- /**
481
- * Opens the overlay if the field is not read-only.
482
- *
483
- * @private
484
- */
485
- _onClick(event) {
486
- // Prevent parent components such as `vaadin-grid`
487
- // from handling the click event after it bubbles.
488
- event.preventDefault();
489
-
490
- this.opened = !this.readonly;
491
- }
492
-
493
217
  /** @private */
494
- _onToggleMouseDown(event) {
495
- // Prevent mousedown event to avoid blur and preserve focused state
496
- // while opening, and to restore focus-ring attribute on closing.
497
- event.preventDefault();
498
- }
499
-
500
- /**
501
- * @param {!KeyboardEvent} e
502
- * @protected
503
- * @override
504
- */
505
- _onKeyDown(e) {
506
- if (e.target === this.focusElement && !this.readonly && !this.opened) {
507
- if (/^(Enter|SpaceBar|\s|ArrowDown|Down|ArrowUp|Up)$/u.test(e.key)) {
508
- e.preventDefault();
509
- this.opened = true;
510
- } else if (/[\p{L}\p{Nd}]/u.test(e.key) && e.key.length === 1) {
511
- const selected = this._menuElement.selected;
512
- const currentIdx = selected !== undefined ? selected : -1;
513
- const newIdx = this._menuElement._searchKey(currentIdx, e.key);
514
- if (newIdx >= 0) {
515
- this.__userInteraction = true;
516
-
517
- // Announce the value selected with the first letter shortcut
518
- this._updateAriaLive(true);
519
- this._menuElement.selected = newIdx;
520
- }
521
- }
522
- }
523
- }
524
-
525
- /**
526
- * @param {!KeyboardEvent} e
527
- * @protected
528
- */
529
- _onKeyDownInside(e) {
530
- if (/^(Tab)$/u.test(e.key)) {
531
- this.opened = false;
532
- }
533
- }
534
-
535
- /** @private */
536
- _openedChanged(opened, wasOpened) {
537
- if (opened) {
538
- // Avoid multiple announcements when a value gets selected from the dropdown
539
- this._updateAriaLive(false);
540
-
541
- if (!this._overlayElement || !this._menuElement || !this.focusElement || this.disabled || this.readonly) {
542
- this.opened = false;
543
- return;
544
- }
545
-
546
- this._overlayElement.style.setProperty(
547
- '--vaadin-select-text-field-width',
548
- `${this._inputContainer.offsetWidth}px`,
549
- );
550
-
551
- // Preserve focus-ring to restore it later
552
- const hasFocusRing = this.hasAttribute('focus-ring');
553
- this._openedWithFocusRing = hasFocusRing;
554
-
555
- // Opened select should not keep focus-ring
556
- if (hasFocusRing) {
557
- this.removeAttribute('focus-ring');
558
- }
559
-
218
+ _onOverlayOpen() {
219
+ if (this._menuElement) {
560
220
  this._menuElement.focus();
561
- } else if (wasOpened) {
562
- this.focus();
563
- if (this._openedWithFocusRing) {
564
- this.setAttribute('focus-ring', '');
565
- }
566
- this.validate();
567
221
  }
568
222
  }
569
223
 
570
- /** @private */
571
- _updateAriaExpanded(opened, focusElement) {
572
- if (focusElement) {
573
- focusElement.setAttribute('aria-expanded', opened ? 'true' : 'false');
574
- }
575
- }
576
-
577
- /** @private */
578
- _updateAriaLive(ariaLive) {
579
- if (this.focusElement) {
580
- if (ariaLive) {
581
- this.focusElement.setAttribute('aria-live', 'polite');
582
- } else {
583
- this.focusElement.removeAttribute('aria-live');
584
- }
585
- }
586
- }
587
-
588
- /** @private */
589
- __attachSelectedItem(selected) {
590
- let labelItem;
591
-
592
- const label = selected.getAttribute('label');
593
- if (label) {
594
- labelItem = this.__createItemElement({ label });
595
- } else {
596
- labelItem = selected.cloneNode(true);
597
- }
598
-
599
- // Store reference to the original item
600
- labelItem._sourceItem = selected;
601
-
602
- this.__appendValueItemElement(labelItem, this.focusElement);
603
-
604
- // Ensure the item gets proper styles
605
- labelItem.selected = true;
606
- }
607
-
608
- /**
609
- * @param {!SelectItem} item
610
- * @private
611
- */
612
- __createItemElement(item) {
613
- const itemElement = document.createElement(item.component || 'vaadin-select-item');
614
- if (item.label) {
615
- itemElement.textContent = item.label;
616
- }
617
- if (item.value) {
618
- itemElement.value = item.value;
619
- }
620
- if (item.disabled) {
621
- itemElement.disabled = item.disabled;
622
- }
623
- return itemElement;
624
- }
625
-
626
- /**
627
- * @param {!HTMLElement} itemElement
628
- * @param {!HTMLElement} parent
629
- * @private
630
- */
631
- __appendValueItemElement(itemElement, parent) {
632
- parent.appendChild(itemElement);
633
- itemElement.removeAttribute('tabindex');
634
- itemElement.removeAttribute('aria-selected');
635
- itemElement.removeAttribute('role');
636
- itemElement.setAttribute('id', this._itemId);
637
- }
638
-
639
- /**
640
- * @param {string} accessibleName
641
- * @protected
642
- */
643
- _accessibleNameChanged(accessibleName) {
644
- this._srLabelController.setLabel(accessibleName);
645
- this._setCustomAriaLabelledBy(accessibleName ? this._srLabelController.defaultId : null);
646
- }
647
-
648
- /**
649
- * @param {string} accessibleNameRef
650
- * @protected
651
- */
652
- _accessibleNameRefChanged(accessibleNameRef) {
653
- this._setCustomAriaLabelledBy(accessibleNameRef);
654
- }
655
-
656
- /**
657
- * @param {string} ariaLabelledby
658
- * @private
659
- */
660
- _setCustomAriaLabelledBy(ariaLabelledby) {
661
- const labelId = this._getLabelIdWithItemId(ariaLabelledby);
662
- this._fieldAriaController.setLabelId(labelId, true);
663
- }
664
-
665
- /**
666
- * @param {string | null} labelId
667
- * @returns string | null
668
- * @private
669
- */
670
- _getLabelIdWithItemId(labelId) {
671
- const selected = this._items ? this._items[this._menuElement.selected] : false;
672
- const itemId = selected || this.placeholder ? this._itemId : '';
673
-
674
- return labelId ? `${labelId} ${itemId}`.trim() : null;
675
- }
676
-
677
- /** @private */
678
- __updateValueButton() {
679
- const valueButton = this.focusElement;
680
-
681
- if (!valueButton) {
682
- return;
683
- }
684
-
685
- valueButton.innerHTML = '';
686
-
687
- const selected = this._items[this._menuElement.selected];
688
-
689
- valueButton.removeAttribute('placeholder');
690
-
691
- if (!selected) {
692
- if (this.placeholder) {
693
- const item = this.__createItemElement({ label: this.placeholder });
694
- this.__appendValueItemElement(item, valueButton);
695
- valueButton.setAttribute('placeholder', '');
696
- }
697
- } else {
698
- this.__attachSelectedItem(selected);
699
-
700
- if (!this._valueChanging) {
701
- this._selectedChanging = true;
702
- this.value = selected.value || '';
703
- if (this.__userInteraction) {
704
- this.opened = false;
705
- this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
706
- this.__userInteraction = false;
707
- }
708
- delete this._selectedChanging;
709
- }
710
- }
711
-
712
- const labelledIdReferenceConfig = selected || this.placeholder ? { newId: this._itemId } : { oldId: this._itemId };
713
-
714
- setAriaIDReference(valueButton, 'aria-labelledby', labelledIdReferenceConfig);
715
- if (this.accessibleName || this.accessibleNameRef) {
716
- this._setCustomAriaLabelledBy(this.accessibleNameRef || this._srLabelController.defaultId);
717
- }
718
- }
719
-
720
- /** @private */
721
- _updateSelectedItem(value, items) {
722
- if (items) {
723
- const valueAsString = value == null ? value : value.toString();
724
- this._menuElement.selected = items.reduce((prev, item, idx) => {
725
- return prev === undefined && item.value === valueAsString ? idx : prev;
726
- }, undefined);
727
- if (!this._selectedChanging) {
728
- this._valueChanging = true;
729
- this.__updateValueButton();
730
- delete this._valueChanging;
731
- }
732
- }
733
- }
734
-
735
- /**
736
- * Override method inherited from `FocusMixin` to not remove focused
737
- * state when select is opened and focus moves to list-box.
738
- * @return {boolean}
739
- * @protected
740
- * @override
741
- */
742
- _shouldRemoveFocus() {
743
- return !this.opened;
744
- }
745
-
746
- /**
747
- * Override method inherited from `FocusMixin` to validate on blur.
748
- * @param {boolean} focused
749
- * @protected
750
- * @override
751
- */
752
- _setFocused(focused) {
753
- super._setFocused(focused);
754
-
755
- // Do not validate when focusout is caused by document
756
- // losing focus, which happens on browser tab switch.
757
- if (!focused && document.hasFocus()) {
758
- this.validate();
759
- }
760
- }
761
-
762
- /**
763
- * Returns true if the current value satisfies all constraints (if any)
764
- *
765
- * @return {boolean}
766
- */
767
- checkValidity() {
768
- return !this.required || this.readonly || !!this.value;
769
- }
770
-
771
- /**
772
- * Renders items when they are provided by the `items` property and clears the content otherwise.
773
- * @param {!HTMLElement} root
774
- * @param {!Select} _select
775
- * @private
776
- */
777
- __defaultRenderer(root, _select) {
778
- if (!this.items || this.items.length === 0) {
779
- root.textContent = '';
780
- return;
781
- }
782
-
783
- let listBox = root.firstElementChild;
784
- if (!listBox) {
785
- listBox = document.createElement('vaadin-select-list-box');
786
- root.appendChild(listBox);
787
- }
788
-
789
- listBox.textContent = '';
790
- this.items.forEach((item) => {
791
- listBox.appendChild(this.__createItemElement(item));
792
- });
793
- }
794
-
795
224
  /**
796
225
  * Fired when the user commits a value change.
797
226
  *
@@ -4,6 +4,5 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import '@vaadin/input-container/theme/lumo/vaadin-input-container.js';
7
- import '@vaadin/overlay/theme/lumo/vaadin-overlay.js';
8
7
  import './vaadin-select-styles.js';
9
8
  import '../../src/vaadin-select.js';
@@ -4,6 +4,5 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import '@vaadin/input-container/theme/material/vaadin-input-container.js';
7
- import '@vaadin/overlay/theme/material/vaadin-overlay.js';
8
7
  import './vaadin-select-styles.js';
9
8
  import '../../src/vaadin-select.js';