Skip to content

Commit aff6590

Browse files
authored
refactor!: update login-form to use shadow DOM and slotted form (#9792)
1 parent bea137c commit aff6590

20 files changed

+1237
-1264
lines changed

dev/login-form.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import '@vaadin/radio-group';
1616

1717
const login = document.querySelector('vaadin-login-form');
18-
const wrapper = login.querySelector('vaadin-login-form-wrapper');
1918

2019
[
2120
{
@@ -39,9 +38,9 @@
3938
if (e.detail.value) {
4039
const elem = elementFactory();
4140
elem.slot = slot;
42-
wrapper.appendChild(elem);
41+
login.appendChild(elem);
4342
} else {
44-
wrapper.querySelector(`[slot=${slot}]`).remove();
43+
login.querySelector(`[slot=${slot}]`).remove();
4544
}
4645
});
4746
});

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

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ export const loginFormWrapperStyles = css`
1010
:host {
1111
background: var(--vaadin-login-form-background, transparent);
1212
border-radius: var(--vaadin-login-form-border-radius, 0);
13-
display: block;
13+
display: flex;
14+
box-sizing: border-box;
15+
flex-direction: column;
16+
gap: var(--vaadin-login-form-gap, var(--vaadin-gap-container-block));
17+
padding: var(--vaadin-login-form-padding, var(--vaadin-padding));
1418
max-width: 100%;
1519
width: var(--vaadin-login-form-width, 360px);
1620
}
@@ -19,22 +23,13 @@ export const loginFormWrapperStyles = css`
1923
display: none !important;
2024
}
2125
22-
[part='form'] {
23-
box-sizing: border-box;
24-
display: flex;
25-
flex: 1;
26-
flex-direction: column;
27-
gap: var(--vaadin-login-form-gap, var(--vaadin-gap-container-block));
28-
padding: var(--vaadin-login-form-padding, var(--vaadin-padding));
29-
}
30-
31-
[part='form'] ::slotted(form) {
26+
::slotted(form) {
3227
display: flex;
3328
flex-direction: column;
3429
gap: var(--vaadin-login-form-gap, var(--vaadin-gap-container-block));
3530
}
3631
37-
[part='form-title'] {
32+
::slotted([slot='form-title']) {
3833
color: var(--vaadin-login-form-title-color, var(--vaadin-color));
3934
font-size: var(--vaadin-login-form-title-font-size, 1.25rem);
4035
font-weight: var(--vaadin-login-form-title-font-weight, 600);

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

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,17 @@ import { css } from 'lit';
77

88
export const loginFormWrapperStyles = css`
99
:host {
10+
display: flex;
11+
flex-direction: column;
12+
box-sizing: border-box;
1013
overflow: hidden;
11-
display: inline-block;
1214
}
1315
1416
:host([hidden]) {
1517
display: none !important;
1618
}
1719
18-
[part='form'] {
19-
flex: 1;
20-
display: flex;
21-
flex-direction: column;
22-
box-sizing: border-box;
23-
}
24-
25-
[part='form-title'] {
20+
::slotted([slot='form-title']) {
2621
margin: 0;
2722
}
2823

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

Lines changed: 93 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* Copyright (c) 2018 - 2025 Vaadin Ltd.
44
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
55
*/
6+
import { html, render } from 'lit';
7+
import { ifDefined } from 'lit/directives/if-defined.js';
68
import { LoginMixin } from './vaadin-login-mixin.js';
79

810
function isCheckbox(field) {
@@ -15,40 +17,114 @@ function isCheckbox(field) {
1517
*/
1618
export const LoginFormMixin = (superClass) =>
1719
class LoginFormMixin extends LoginMixin(superClass) {
18-
static get observers() {
19-
return ['_errorChanged(error)'];
20-
}
21-
20+
/** @protected */
2221
get _customFields() {
23-
return [...this.$.vaadinLoginFormWrapper.children].filter((node) => {
22+
return [...this.children].filter((node) => {
2423
return node.getAttribute('slot') === 'custom-form-area' && node.hasAttribute('name');
2524
});
2625
}
2726

27+
/** @protected */
28+
get _userNameField() {
29+
return this.querySelector('#vaadinLoginUsername');
30+
}
31+
32+
/** @protected */
33+
get _passwordField() {
34+
return this.querySelector('#vaadinLoginPassword');
35+
}
36+
37+
/**
38+
* Override update to render slotted form and buttons
39+
* into light DOM after rendering shadow DOM.
40+
* @protected
41+
*/
42+
update(props) {
43+
super.update(props);
44+
45+
this.__renderSlottedForm();
46+
}
47+
48+
/** @protected */
49+
updated(props) {
50+
super.updated(props);
51+
52+
if (props.has('error') && this.error && !this._preventAutoEnable) {
53+
this.disabled = false;
54+
}
55+
}
56+
2857
/** @protected */
2958
async connectedCallback() {
3059
super.connectedCallback();
3160

3261
if (!this.noAutofocus) {
3362
// Wait for the form to fully render.
3463
await new Promise(requestAnimationFrame);
35-
this.$.vaadinLoginUsername.focus();
64+
this._userNameField.focus();
3665
}
3766
}
3867

39-
/** @private */
40-
_errorChanged() {
41-
if (this.error && !this._preventAutoEnable) {
42-
this.disabled = false;
43-
}
68+
__renderSlottedForm() {
69+
render(
70+
html`
71+
<form method="POST" action="${ifDefined(this.action)}" @formdata="${this._onFormData}" slot="form">
72+
<input id="csrf" type="hidden" />
73+
<vaadin-text-field
74+
name="username"
75+
.label="${this.__effectiveI18n.form.username}"
76+
.errorMessage="${this.__effectiveI18n.errorMessage.username}"
77+
id="vaadinLoginUsername"
78+
required
79+
@keydown="${this._handleInputKeydown}"
80+
autocapitalize="none"
81+
autocorrect="off"
82+
spellcheck="false"
83+
autocomplete="username"
84+
manual-validation
85+
>
86+
<input type="text" slot="input" @keyup="${this._handleInputKeyup}" />
87+
</vaadin-text-field>
88+
89+
<vaadin-password-field
90+
name="password"
91+
.label="${this.__effectiveI18n.form.password}"
92+
.errorMessage="${this.__effectiveI18n.errorMessage.password}"
93+
id="vaadinLoginPassword"
94+
required
95+
@keydown="${this._handleInputKeydown}"
96+
spellcheck="false"
97+
autocomplete="current-password"
98+
manual-validation
99+
>
100+
<input type="password" slot="input" @keyup="${this._handleInputKeyup}" />
101+
</vaadin-password-field>
102+
</form>
103+
104+
<vaadin-button slot="submit" theme="primary submit" @click="${this.submit}" .disabled="${this.disabled}">
105+
${this.__effectiveI18n.form.submit}
106+
</vaadin-button>
107+
108+
<vaadin-button
109+
slot="forgot-password"
110+
theme="tertiary small"
111+
@click="${this._onForgotPasswordClick}"
112+
?hidden="${this.noForgotPassword}"
113+
>
114+
${this.__effectiveI18n.form.forgotPassword}
115+
</vaadin-button>
116+
`,
117+
this,
118+
{ host: this },
119+
);
44120
}
45121

46122
/**
47123
* Submits the form.
48124
*/
49125
submit() {
50-
const userName = this.$.vaadinLoginUsername;
51-
const password = this.$.vaadinLoginPassword;
126+
const userName = this._userNameField;
127+
const password = this._passwordField;
52128

53129
// eslint-disable-next-line no-restricted-syntax
54130
userName.validate();
@@ -91,8 +167,9 @@ export const LoginFormMixin = (superClass) =>
91167
const csrfMetaName = document.querySelector('meta[name=_csrf_parameter]');
92168
const csrfMetaValue = document.querySelector('meta[name=_csrf]');
93169
if (csrfMetaName && csrfMetaValue) {
94-
this.$.csrf.name = csrfMetaName.content;
95-
this.$.csrf.value = csrfMetaValue.content;
170+
const csrf = this.querySelector('#csrf');
171+
csrf.name = csrfMetaName.content;
172+
csrf.value = csrfMetaValue.content;
96173
}
97174
this.querySelector('form').submit();
98175
}
@@ -117,8 +194,7 @@ export const LoginFormMixin = (superClass) =>
117194
_handleInputKeydown(e) {
118195
if (e.key === 'Enter') {
119196
const { currentTarget: inputActive } = e;
120-
const nextInput =
121-
inputActive.id === 'vaadinLoginUsername' ? this.$.vaadinLoginPassword : this.$.vaadinLoginUsername;
197+
const nextInput = inputActive.id === 'vaadinLoginUsername' ? this._passwordField : this._userNameField;
122198
// eslint-disable-next-line no-restricted-syntax
123199
if (inputActive.validate()) {
124200
if (nextInput.checkValidity()) {

packages/login/src/vaadin-login-form-wrapper.js

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -45,39 +45,30 @@ class LoginFormWrapper extends ThemableMixin(PolylitMixin(LumoInjectionMixin(Lit
4545
i18n: {
4646
type: Object,
4747
},
48-
49-
/**
50-
* Used to customize the `aria-level` attribute on the heading element.
51-
*/
52-
headingLevel: {
53-
type: Number,
54-
},
5548
};
5649
}
5750

5851
/** @protected */
5952
render() {
6053
return html`
61-
<section part="form">
62-
<div part="form-title" role="heading" aria-level="${this.headingLevel}">${this.i18n.form.title}</div>
63-
<div part="error-message" ?hidden="${!this.error}">
64-
<strong part="error-message-title">${this.i18n.errorMessage.title}</strong>
65-
<div part="error-message-description">${this.i18n.errorMessage.message}</div>
66-
</div>
54+
<slot name="form-title"></slot>
55+
<div part="error-message" ?hidden="${!this.error}">
56+
<strong part="error-message-title">${this.i18n.errorMessage.title}</strong>
57+
<div part="error-message-description">${this.i18n.errorMessage.message}</div>
58+
</div>
6759
68-
<slot name="form"></slot>
60+
<slot name="form"></slot>
6961
70-
<slot name="custom-form-area"></slot>
62+
<slot name="custom-form-area"></slot>
7163
72-
<slot name="submit"></slot>
64+
<slot name="submit"></slot>
7365
74-
<slot name="forgot-password"></slot>
66+
<slot name="forgot-password"></slot>
7567
76-
<div part="footer">
77-
<slot name="footer"></slot>
78-
<div>${this.i18n.additionalInformation}</div>
79-
</div>
80-
</section>
68+
<div part="footer">
69+
<slot name="footer"></slot>
70+
<div>${this.i18n.additionalInformation}</div>
71+
</div>
8172
`;
8273
}
8374
}

packages/login/src/vaadin-login-form.d.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,7 @@ export interface LoginFormEventMap extends HTMLElementEventMap, LoginFormCustomE
4848
*
4949
* ### Styling
5050
*
51-
* The component doesn't have a shadowRoot, so the `<form>` and input fields can be styled from a global scope.
52-
*
53-
* Use `<vaadin-login-form-wrapper>` themable component to apply styles.
54-
* The following shadow DOM parts of the `<vaadin-login-form-wrapper>` are available for styling:
51+
* The following shadow DOM parts are available for styling:
5552
*
5653
* Part name | Description
5754
* ---------------|---------------------------------------------------------|

0 commit comments

Comments
 (0)