Skip to content

Commit 0104125

Browse files
authored
refactor!: update login overlay to use native popover (#9794)
1 parent 56fcff3 commit 0104125

18 files changed

+658
-945
lines changed

packages/login/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"lit": "^3.0.0"
4848
},
4949
"devDependencies": {
50+
"@vaadin/a11y-base": "25.0.0-alpha8",
5051
"@vaadin/chai-plugins": "25.0.0-alpha8",
5152
"@vaadin/checkbox": "25.0.0-alpha8",
5253
"@vaadin/test-runner-commands": "25.0.0-alpha8",

packages/login/src/styles/vaadin-login-overlay-wrapper-base-styles.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const loginOverlayWrapper = css`
3737
padding: var(--vaadin-login-overlay-brand-padding, var(--vaadin-padding));
3838
}
3939
40-
[part='title'] {
40+
::slotted([slot='title']) {
4141
color: var(--vaadin-login-overlay-title-color, var(--vaadin-color));
4242
font-size: var(--vaadin-login-overlay-title-font-size, inherit);
4343
font-weight: var(--vaadin-login-overlay-title-font-weight, 600);
@@ -50,10 +50,6 @@ const loginOverlayWrapper = css`
5050
font-weight: var(--vaadin-login-overlay-description-font-weight, inherit);
5151
line-height: var(--vaadin-login-overlay-description-line-height, inherit);
5252
}
53-
54-
[part='form'] ::slotted(vaadin-login-form) {
55-
display: flex;
56-
}
5753
`;
5854

5955
export const loginOverlayWrapperStyles = [overlayStyles, loginOverlayWrapper];

packages/login/src/styles/vaadin-login-overlay-wrapper-core-styles.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const loginOverlayWrapper = css`
2929
justify-content: flex-end;
3030
}
3131
32-
[part='title'] {
32+
::slotted([slot='title']) {
3333
color: inherit;
3434
margin: 0;
3535
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2018 - 2025 Vaadin Ltd.
4+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5+
*/
6+
import { SlotChildObserveController } from '@vaadin/component-base/src/slot-child-observe-controller.js';
7+
8+
/**
9+
* A controller to manage the title element.
10+
*/
11+
export class TitleController extends SlotChildObserveController {}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2018 - 2025 Vaadin Ltd.
4+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5+
*/
6+
import { SlotChildObserveController } from '@vaadin/component-base/src/slot-child-observe-controller.js';
7+
8+
/**
9+
* A controller to manage the title element.
10+
*/
11+
export class TitleController extends SlotChildObserveController {
12+
constructor(host) {
13+
super(host, 'title', 'div');
14+
}
15+
16+
/**
17+
* Set title based on corresponding host property.
18+
*
19+
* @param {string} title
20+
*/
21+
setTitle(title) {
22+
this.title = title;
23+
24+
// Restore the default title, if needed.
25+
const titleNode = this.getSlotChild();
26+
if (!titleNode) {
27+
this.restoreDefaultNode();
28+
}
29+
30+
// When default title is used, update it.
31+
if (this.node === this.defaultNode) {
32+
this.updateDefaultNode(this.node);
33+
}
34+
}
35+
36+
/**
37+
* Set level based on corresponding host property.
38+
*
39+
* @param {string} level
40+
*/
41+
setLevel(level) {
42+
this.level = level;
43+
44+
// When default title is used, update it.
45+
if (this.node === this.defaultNode) {
46+
this.updateDefaultNode(this.node);
47+
}
48+
}
49+
50+
/**
51+
* Override method inherited from `SlotController`
52+
* to customize heading on the default title node.
53+
*
54+
* @param {Node} node
55+
* @protected
56+
* @override
57+
*/
58+
initNode(node) {
59+
if (node === this.defaultNode) {
60+
node.setAttribute('role', 'heading');
61+
}
62+
63+
this.host.setAttribute('aria-labelledby', node.id);
64+
}
65+
66+
/**
67+
* Override method inherited from `SlotChildObserveController`
68+
* to restore the default title element.
69+
*
70+
* @protected
71+
* @override
72+
*/
73+
restoreDefaultNode() {
74+
const { title } = this;
75+
76+
// Restore the default title.
77+
if (title && title.trim() !== '') {
78+
const node = this.attachDefaultNode();
79+
this.initNode(node);
80+
}
81+
}
82+
83+
/**
84+
* Override method inherited from `SlotChildObserveController`
85+
* to update the default title element.
86+
*
87+
* @param {Node | undefined} node
88+
* @protected
89+
* @override
90+
*/
91+
updateDefaultNode(node) {
92+
if (node) {
93+
node.textContent = this.title;
94+
node.setAttribute('aria-level', this.level);
95+
}
96+
97+
// Notify the host after update.
98+
super.updateDefaultNode(node);
99+
}
100+
}

packages/login/src/vaadin-login-overlay-mixin.d.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
*/
66
import type { Constructor } from '@open-wc/dedupe-mixin';
77
import type { OverlayClassMixinClass } from '@vaadin/component-base/src/overlay-class-mixin.js';
8-
import type { LoginMixinClass } from './vaadin-login-mixin.js';
98

109
export declare function LoginOverlayMixin<T extends Constructor<HTMLElement>>(
1110
base: T,
12-
): Constructor<LoginMixinClass> & Constructor<LoginOverlayMixinClass> & Constructor<OverlayClassMixinClass> & T;
11+
): Constructor<LoginOverlayMixinClass> & Constructor<OverlayClassMixinClass> & T;
1312

1413
export declare class LoginOverlayMixinClass {
1514
/**

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

Lines changed: 50 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
55
*/
66
import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
7-
import { LoginMixin } from './vaadin-login-mixin.js';
7+
import { TitleController } from './title-controller.js';
88

99
/**
1010
* @polymerMixin
11-
* @mixes LoginMixin
1211
* @mixes OverlayClassMixin
1312
*/
1413
export const LoginOverlayMixin = (superClass) =>
15-
class LoginOverlayMixin extends OverlayClassMixin(LoginMixin(superClass)) {
14+
class LoginOverlayMixin extends OverlayClassMixin(superClass) {
1615
static get properties() {
1716
return {
1817
/**
@@ -32,8 +31,8 @@ export const LoginOverlayMixin = (superClass) =>
3231
opened: {
3332
type: Boolean,
3433
value: false,
34+
reflectToAttribute: true,
3535
sync: true,
36-
observer: '_onOpenedChange',
3736
},
3837

3938
/**
@@ -47,15 +46,43 @@ export const LoginOverlayMixin = (superClass) =>
4746
};
4847
}
4948

50-
static get observers() {
51-
return ['__i18nChanged(__effectiveI18n)'];
49+
/** @protected */
50+
firstUpdated() {
51+
super.firstUpdated();
52+
53+
this.setAttribute('role', 'dialog');
54+
55+
this.__titleController = new TitleController(this);
56+
this.addController(this.__titleController);
57+
58+
this._overlayElement = this.$.overlay;
59+
}
60+
61+
/** @protected */
62+
willUpdate(props) {
63+
super.willUpdate(props);
64+
65+
if (props.has('__effectiveI18n') && this.__effectiveI18n.header) {
66+
this.title = this.__effectiveI18n.header.title;
67+
this.description = this.__effectiveI18n.header.description;
68+
}
5269
}
5370

5471
/** @protected */
55-
ready() {
56-
super.ready();
72+
updated(props) {
73+
super.updated(props);
74+
75+
if (props.has('title') || props.has('__effectiveI18n')) {
76+
this.__titleController.setTitle(this.title);
77+
}
78+
79+
if (props.has('headingLevel')) {
80+
this.__titleController.setLevel(this.headingLevel);
81+
}
5782

58-
this._overlayElement = this.$.vaadinLoginOverlayWrapper;
83+
if (props.has('opened')) {
84+
this._openedChanged(this.opened);
85+
}
5986
}
6087

6188
/** @protected */
@@ -72,91 +99,32 @@ export const LoginOverlayMixin = (superClass) =>
7299
disconnectedCallback() {
73100
super.disconnectedCallback();
74101

75-
// Close overlay and memorize opened state
76-
this.__restoreOpened = this.opened;
77-
this.opened = false;
78-
}
79-
80-
/** @private */
81-
__i18nChanged(effectiveI18n) {
82-
const header = effectiveI18n && effectiveI18n.header;
83-
if (!header) {
84-
return;
85-
}
86-
this.title = header.title;
87-
this.description = header.description;
102+
// Using a timeout to avoid toggling opened state
103+
// when just moving the overlay in the DOM
104+
setTimeout(() => {
105+
if (!this.isConnected) {
106+
this.__restoreOpened = this.opened;
107+
this.opened = false;
108+
}
109+
});
88110
}
89111

90112
/** @protected */
91113
_preventClosingLogin(e) {
92114
e.preventDefault();
93115
}
94116

95-
/**
96-
* @param {!Event} e
97-
* @protected
98-
*/
99-
_retargetEvent(e) {
100-
e.stopPropagation();
101-
const { detail, composed, cancelable, bubbles } = e;
102-
103-
const firedEvent = this.dispatchEvent(new CustomEvent(e.type, { bubbles, cancelable, composed, detail }));
104-
// Check if `eventTarget.preventDefault()` was called to prevent default in the original event
105-
if (!firedEvent) {
106-
e.preventDefault();
107-
}
108-
}
109-
110117
/** @private */
111-
_onOpenedChange() {
112-
const form = this.$.vaadinLoginForm;
113-
114-
if (!this.opened) {
115-
form._userNameField.value = '';
116-
form._passwordField.value = '';
118+
_openedChanged(opened, oldOpened) {
119+
if (oldOpened) {
120+
this._userNameField.value = '';
121+
this._passwordField.value = '';
117122
this.disabled = false;
118-
119-
if (this._undoTitleTeleport) {
120-
this._undoTitleTeleport();
121-
}
122-
123-
if (this._undoFieldsTeleport) {
124-
this._undoFieldsTeleport();
125-
}
126-
127-
if (this._undoFooterTeleport) {
128-
this._undoFooterTeleport();
129-
}
130-
} else {
131-
this._undoTitleTeleport = this._teleport('title', this.$.vaadinLoginOverlayWrapper);
132-
this._undoFieldsTeleport = this._teleport('custom-form-area', form, form.querySelector('vaadin-button'));
133-
this._undoFooterTeleport = this._teleport('footer', form);
134-
123+
} else if (opened) {
135124
// Overlay sets pointerEvents on body to `none`, which breaks LastPass popup
136125
// Reverting it back to the previous state
137126
// https://github.com/vaadin/vaadin-overlay/blob/041cde4481b6262eac68d3a699f700216d897373/src/vaadin-overlay.html#L660
138-
document.body.style.pointerEvents = this.$.vaadinLoginOverlayWrapper._previousDocumentPointerEvents;
127+
document.body.style.pointerEvents = this.$.overlay._previousDocumentPointerEvents;
139128
}
140129
}
141-
142-
/** @private */
143-
_teleport(slot, target, refNode) {
144-
const teleported = [...this.querySelectorAll(`[slot="${slot}"]`)].map((el) => {
145-
if (refNode) {
146-
target.insertBefore(el, refNode);
147-
} else {
148-
target.appendChild(el);
149-
}
150-
return el;
151-
});
152-
// Function to undo the teleport
153-
return () => {
154-
this.append(...teleported);
155-
};
156-
}
157-
158-
/** @private */
159-
__computeHeadingLevel(headingLevel) {
160-
return headingLevel + 1;
161-
}
162130
};

0 commit comments

Comments
 (0)