Skip to content

Commit d749747

Browse files
refactor!: update select overlay to use native popover (#9751)
Co-authored-by: Sascha Ißbrücker <sissbruecker@vaadin.com>
1 parent 1d903fc commit d749747

File tree

16 files changed

+436
-209
lines changed

16 files changed

+436
-209
lines changed

packages/field-highlighter/src/fields/vaadin-select-observer.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ export class SelectObserver extends FieldObserver {
1313
}
1414

1515
onFocusIn(event) {
16-
if (this.overlay.contains(event.relatedTarget)) {
16+
if (this.overlay._contentRoot.contains(event.target)) {
17+
// Focus moves to the overlay item, do nothing.
18+
return;
19+
}
20+
21+
if (this.overlay._contentRoot.contains(event.relatedTarget)) {
1722
// Focus returns on item select, do nothing.
1823
return;
1924
}
@@ -22,10 +27,16 @@ export class SelectObserver extends FieldObserver {
2227
}
2328

2429
onFocusOut(event) {
25-
if (this.overlay.contains(event.relatedTarget)) {
26-
// Do nothing, overlay is opening.
30+
if (this.overlay._contentRoot.contains(event.relatedTarget)) {
31+
// Focus moves to the overlay on opening, do nothing.
2732
return;
2833
}
34+
35+
if (this.overlay._contentRoot.contains(event.target) && this.component.contains(event.relatedTarget)) {
36+
// Focus returns from the overlay on closing, do nothing.
37+
return;
38+
}
39+
2940
super.onFocusOut(event);
3041
}
3142
}

packages/select/src/styles/vaadin-select-base-styles.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export const selectStyles = css`
1515
flex: 1;
1616
}
1717
18+
::slotted(div[slot='overlay']) {
19+
display: contents;
20+
}
21+
1822
:host(:not([focus-ring])) [part='input-field'] {
1923
outline: none;
2024
}

packages/select/src/styles/vaadin-select-core-styles.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@ export const selectStyles = css`
1313
::slotted([slot='value']) {
1414
flex-grow: 1;
1515
}
16+
17+
::slotted(div[slot='overlay']) {
18+
display: contents;
19+
}
1620
`;

packages/select/src/vaadin-select-base-mixin.js

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,15 @@ export const SelectBaseMixin = (superClass) =>
204204
this.addController(this._tooltipController);
205205
}
206206

207+
/** @protected */
208+
updated(props) {
209+
super.updated(props);
210+
211+
if (props.has('_phone')) {
212+
this.toggleAttribute('phone', this._phone);
213+
}
214+
}
215+
207216
/**
208217
* Requests an update for the content of the select.
209218
* While performing the update, it invokes the renderer passed in the `renderer` property.
@@ -216,10 +225,6 @@ export const SelectBaseMixin = (superClass) =>
216225
}
217226

218227
this._overlayElement.requestContentUpdate();
219-
220-
if (this._menuElement && this._menuElement.items) {
221-
this._updateSelectedItem(this.value, this._menuElement.items);
222-
}
223228
}
224229

225230
/**
@@ -280,6 +285,12 @@ export const SelectBaseMixin = (superClass) =>
280285
// Store the menu element reference
281286
this.__lastMenuElement = menuElement;
282287
}
288+
289+
// When the renderer was re-assigned so that menu element is preserved
290+
// but its items have changed, make sure selected property is updated.
291+
if (this._menuElement && this._menuElement.items) {
292+
this._updateSelectedItem(this.value, this._menuElement.items);
293+
}
283294
}
284295

285296
/** @private */
@@ -361,8 +372,16 @@ export const SelectBaseMixin = (superClass) =>
361372
* @protected
362373
*/
363374
_onKeyDownInside(e) {
364-
if (/^(Tab)$/u.test(e.key)) {
375+
if (e.key === 'Tab') {
376+
// Temporarily set tabindex to prevent moving focus
377+
// to the value button element on item Shift + Tab
378+
this.focusElement.setAttribute('tabindex', '-1');
379+
this._overlayElement.restoreFocusOnClose = false;
365380
this.opened = false;
381+
setTimeout(() => {
382+
this.focusElement.setAttribute('tabindex', '0');
383+
this._overlayElement.restoreFocusOnClose = true;
384+
});
366385
}
367386
}
368387

@@ -585,8 +604,8 @@ export const SelectBaseMixin = (superClass) =>
585604
* @protected
586605
* @override
587606
*/
588-
_shouldRemoveFocus() {
589-
return !this.opened;
607+
_shouldRemoveFocus(event) {
608+
return !this.contains(event.relatedTarget);
590609
}
591610

592611
/**

packages/select/src/vaadin-select-overlay-mixin.js

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,37 @@ export const SelectOverlayMixin = (superClass) =>
2626
this.restoreFocusOnClose = true;
2727
}
2828

29+
/**
30+
* @override
31+
* @protected
32+
*/
33+
get _contentRoot() {
34+
if (!this.__savedRoot) {
35+
const root = document.createElement('div');
36+
root.setAttribute('slot', 'overlay');
37+
this.owner.appendChild(root);
38+
this.__savedRoot = root;
39+
}
40+
41+
return this.__savedRoot;
42+
}
43+
44+
/**
45+
* @protected
46+
* @override
47+
*/
48+
_attachOverlay() {
49+
this.showPopover();
50+
}
51+
52+
/**
53+
* @protected
54+
* @override
55+
*/
56+
_detachOverlay() {
57+
this.hidePopover();
58+
}
59+
2960
/**
3061
* Override method inherited from `Overlay` to always close on outside click,
3162
* in order to avoid problem when using inside of the modeless dialog.
@@ -51,22 +82,13 @@ export const SelectOverlayMixin = (superClass) =>
5182

5283
/** @protected */
5384
_getMenuElement() {
54-
return Array.from(this.children).find((el) => el.localName !== 'style');
85+
return Array.from(this._contentRoot.children).find((el) => el.localName !== 'style');
5586
}
5687

5788
/** @private */
5889
_updateOverlayWidth(opened, positionTarget) {
5990
if (opened && positionTarget) {
6091
this.style.setProperty('--_vaadin-select-overlay-default-width', `${positionTarget.offsetWidth}px`);
61-
62-
const widthProperty = '--vaadin-select-overlay-width';
63-
const customWidth = getComputedStyle(this.owner).getPropertyValue(widthProperty);
64-
65-
if (customWidth === '') {
66-
this.style.removeProperty(widthProperty);
67-
} else {
68-
this.style.setProperty(widthProperty, customWidth);
69-
}
7092
}
7193
}
7294

packages/select/src/vaadin-select-overlay.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class SelectOverlay extends SelectOverlayMixin(ThemableMixin(PolylitMixin
3434
render() {
3535
return html`
3636
<div id="backdrop" part="backdrop" ?hidden="${!this.withBackdrop}"></div>
37-
<div part="overlay" id="overlay" tabindex="0">
37+
<div part="overlay" id="overlay">
3838
<div part="content" id="content">
3939
<slot></slot>
4040
</div>

packages/select/src/vaadin-select.d.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -126,26 +126,30 @@ export interface SelectEventMap extends HTMLElementEventMap, SelectCustomEventMa
126126
*
127127
* The following custom properties are available for styling:
128128
*
129-
* Custom property | Description | Target element | Default
130-
* -----------------------------------|------------------------------|-------------------------|--------
131-
* `--vaadin-field-default-width` | Default width of the field | :host | `12em`
132-
* `--vaadin-select-overlay-width` | Width of the overlay | `vaadin-select-overlay` |
129+
* Custom property | Description | Default
130+
* ---------------------------------|-----------------------------|--------
131+
* `--vaadin-field-default-width` | Default width of the field | `12em`
132+
* `--vaadin-select-overlay-width` | Width of the overlay |
133133
*
134134
* `<vaadin-select>` provides mostly the same set of shadow DOM parts and state attributes as `<vaadin-text-field>`.
135135
* See [`<vaadin-text-field>`](#/elements/vaadin-text-field) for the styling documentation.
136136
*
137137
*
138138
* In addition to `<vaadin-text-field>` parts, the following parts are available for theming:
139139
*
140-
* Part name | Description
141-
* ----------------|----------------
142-
* `toggle-button` | The toggle button
140+
* Part name | Description
141+
* -----------------|----------------
142+
* `toggle-button` | The toggle button
143+
* `backdrop` | Backdrop of the overlay
144+
* `overlay` | The overlay container
145+
* `content` | The overlay content
143146
*
144147
* In addition to `<vaadin-text-field>` state attributes, the following state attributes are available for theming:
145148
*
146-
* Attribute | Description | Part name
147-
* ----------|-----------------------------|-----------
148-
* `opened` | Set when the select is open | :host
149+
* Attribute | Description
150+
* ----------|-----------------------------
151+
* `opened` | Set when the select is open
152+
* `phone` | Set when the overlay is shown in phone mode
149153
*
150154
* There are two exceptions in terms of styling compared to `<vaadin-text-field>`:
151155
* - the `clear-button` shadow DOM part does not exist in `<vaadin-select>`.

packages/select/src/vaadin-select.js

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,26 +80,30 @@ import { SelectBaseMixin } from './vaadin-select-base-mixin.js';
8080
*
8181
* The following custom properties are available for styling:
8282
*
83-
* Custom property | Description | Target element | Default
84-
* -----------------------------------|------------------------------|-------------------------|--------
85-
* `--vaadin-field-default-width` | Default width of the field | :host | `12em`
86-
* `--vaadin-select-overlay-width` | Width of the overlay | `vaadin-select-overlay` |
83+
* Custom property | Description | Default
84+
* ---------------------------------|-----------------------------|--------
85+
* `--vaadin-field-default-width` | Default width of the field | `12em`
86+
* `--vaadin-select-overlay-width` | Width of the overlay |
8787
*
8888
* `<vaadin-select>` provides mostly the same set of shadow DOM parts and state attributes as `<vaadin-text-field>`.
8989
* See [`<vaadin-text-field>`](#/elements/vaadin-text-field) for the styling documentation.
9090
*
9191
*
9292
* In addition to `<vaadin-text-field>` parts, the following parts are available for theming:
9393
*
94-
* Part name | Description
95-
* ----------------|----------------
96-
* `toggle-button` | The toggle button
94+
* Part name | Description
95+
* -----------------|----------------
96+
* `toggle-button` | The toggle button
97+
* `backdrop` | Backdrop of the overlay
98+
* `overlay` | The overlay container
99+
* `content` | The overlay content
97100
*
98101
* In addition to `<vaadin-text-field>` state attributes, the following state attributes are available for theming:
99102
*
100-
* Attribute | Description | Part name
101-
* ----------|-----------------------------|-----------
102-
* `opened` | Set when the select is open | :host
103+
* Attribute | Description
104+
* ----------|-----------------------------
105+
* `opened` | Set when the select is open
106+
* `phone` | Set when the overlay is shown in phone mode
103107
*
104108
* There are two exceptions in terms of styling compared to `<vaadin-text-field>`:
105109
* - the `clear-button` shadow DOM part does not exist in `<vaadin-select>`.
@@ -173,6 +177,7 @@ class Select extends SelectBaseMixin(ElementMixin(ThemableMixin(PolylitMixin(Lum
173177
174178
<vaadin-select-overlay
175179
id="overlay"
180+
popover="manual"
176181
.owner="${this}"
177182
.positionTarget="${this._inputContainer}"
178183
.opened="${this.opened}"
@@ -181,9 +186,12 @@ class Select extends SelectBaseMixin(ElementMixin(ThemableMixin(PolylitMixin(Lum
181186
?phone="${this._phone}"
182187
theme="${ifDefined(this._theme)}"
183188
?no-vertical-overlap="${this.noVerticalOverlap}"
189+
exportparts="backdrop, overlay, content"
184190
@opened-changed="${this._onOpenedChanged}"
185191
@vaadin-overlay-open="${this._onOverlayOpen}"
186-
></vaadin-select-overlay>
192+
>
193+
<slot name="overlay"></slot>
194+
</vaadin-select-overlay>
187195
188196
<slot name="tooltip"></slot>
189197
<div class="sr-only">

0 commit comments

Comments
 (0)