Merge pull request #25613 from overleaf/td-bs5-sp-login

Migrate SP/CE login page to Bootstrap 5

GitOrigin-RevId: 37fc7cbb453bfef93abde2080faaa0a88116d1f4
This commit is contained in:
Tim Down
2025-05-22 11:13:42 +01:00
committed by Copybot
parent a77f218a77
commit d0010217cd
6 changed files with 99 additions and 71 deletions

View File

@@ -4,11 +4,12 @@ mixin formMessages()
role="alert"
)
mixin formMessagesNewStyle()
mixin formMessagesNewStyle(extraClass = 'form-messages-bottom-margin')
- const attrs = extraClass ? { 'class': extraClass } : {}
div(
data-ol-form-messages-new-style='',
role="alert"
)
)&attributes(attrs)
mixin customFormMessage(key, kind)
if kind === 'success'
@@ -36,20 +37,23 @@ mixin customFormMessage(key, kind)
)
block
mixin customFormMessageNewStyle(key, kind)
mixin customFormMessageNewStyle(key, kind, extraClass = 'mb-3')
- extraClass = extraClass ? ' ' + extraClass : ''
if kind === 'success'
div.notification.notification-type-success(
hidden,
data-ol-custom-form-message=key,
role="alert"
aria-live="polite"
)
div(
class="notification notification-type-success" + extraClass,
hidden,
data-ol-custom-form-message=key,
role="alert"
aria-live="polite"
)
div.notification-icon
span.material-symbols(aria-hidden="true") check_circle
div.notification-content.text-left
block
else if kind === 'danger'
div.notification.notification-type-error(
div(
class="notification notification-type-error" + extraClass,
hidden,
data-ol-custom-form-message=key,
role="alert"
@@ -60,12 +64,13 @@ mixin customFormMessageNewStyle(key, kind)
div.notification-content.text-left
block
else
div.notification.notification-type-warning(
hidden,
data-ol-custom-form-message=key,
role="alert"
aria-live="polite"
)
div(
class="notification notification-type-warning" + extraClass,
hidden,
data-ol-custom-form-message=key,
role="alert"
aria-live="polite"
)
div.notification-icon
span.material-symbols(aria-hidden="true") warning
div.notification-content.text-left

View File

@@ -1,52 +1,50 @@
extends ../layout-marketing
block vars
- bootstrap5PageStatus = 'disabled'
block content
main.content.content-alt#main-content
.container
.row
.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4
.col-lg-6.offset-lg-3.col-xl-4.offset-xl-4
.card
.page-header
if login_support_title
h1 !{login_support_title}
else
h1 #{translate("log_in")}
form(data-ol-async-form, name="loginForm", action='/login', method="POST")
input(name='_csrf', type='hidden', value=csrfToken)
+formMessages()
+customFormMessage('invalid-password-retry-or-reset', 'danger')
| !{translate('email_or_password_wrong_try_again_or_reset', {}, [{ name: 'a', attrs: { href: '/user/password/reset', 'aria-describedby': 'resetPasswordDescription' } }])}
span.sr-only(id='resetPasswordDescription')
| #{translate('reset_password_link')}
+customValidationMessage('password-compromised')
| !{translate('password_compromised_try_again_or_use_known_device_or_reset', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}, {name: 'a', attrs: {href: '/user/password/reset', target: '_blank'}}])}.
.form-group
input.form-control(
type='email',
name='email',
required,
placeholder='email@example.com',
autofocus="true"
)
.form-group
input.form-control(
type='password',
name='password',
required,
placeholder='********',
)
.actions
button.btn-primary.btn(
type='submit',
data-ol-disabled-inflight
)
span(data-ol-inflight="idle") #{translate("login")}
span(hidden data-ol-inflight="pending") #{translate("logging_in")}
a.pull-right(href='/user/password/reset') #{translate("forgot_your_password")}?
if login_support_text
hr
p.text-center !{login_support_text}
.card-body
.page-header
if login_support_title
h1 !{login_support_title}
else
h1 #{translate("log_in")}
form(data-ol-async-form, name="loginForm", action='/login', method="POST")
input(name='_csrf', type='hidden', value=csrfToken)
+formMessagesNewStyle()
+customFormMessageNewStyle('invalid-password-retry-or-reset', 'danger')
| !{translate('email_or_password_wrong_try_again_or_reset', {}, [{ name: 'a', attrs: { href: '/user/password/reset', 'aria-describedby': 'resetPasswordDescription' } }])}
span.visually-hidden(id='resetPasswordDescription')
| #{translate('reset_password_link')}
+customFormMessageNewStyle('password-compromised')
| !{translate('password_compromised_try_again_or_use_known_device_or_reset', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}, {name: 'a', attrs: {href: '/user/password/reset', target: '_blank'}}])}.
.form-group
input.form-control(
type='email',
name='email',
required,
placeholder='email@example.com',
autofocus="true"
)
.form-group
input.form-control(
type='password',
name='password',
required,
placeholder='********',
)
.actions
button.btn-primary.btn(
type='submit',
data-ol-disabled-inflight
)
span(data-ol-inflight="idle") #{translate("login")}
span(hidden data-ol-inflight="pending") #{translate("logging_in")}
a.float-end(href='/user/password/reset') #{translate("forgot_your_password")}?
if login_support_text
hr
p.text-center !{login_support_text}

View File

@@ -0,0 +1,7 @@
export default function createIcon(type) {
const icon = document.createElement('span')
icon.className = 'material-symbols'
icon.setAttribute('aria-hidden', 'true')
icon.textContent = type
return icon
}

View File

@@ -4,6 +4,7 @@ import { canSkipCaptcha, validateCaptchaV2 } from './captcha'
import inputValidator from './input-validator'
import { disableElement, enableElement } from '../utils/disableElement'
import { isBootstrap5 } from '@/features/utils/bootstrap-5'
import createIcon from '@/features/form-helpers/create-icon'
// Form helper(s) to handle:
// - Attaching to the relevant form elements
@@ -164,10 +165,7 @@ function createNotificationFromMessageBS5(message) {
if (materialIcon) {
const iconEl = document.createElement('div')
iconEl.className = 'notification-icon'
const iconSpan = document.createElement('span')
iconSpan.className = 'material-symbols'
iconSpan.setAttribute('aria-hidden', 'true')
iconSpan.textContent = materialIcon
const iconSpan = createIcon(materialIcon)
iconEl.append(iconSpan)
messageEl.append(iconEl)
}
@@ -315,10 +313,9 @@ function showMessagesNewStyle(formEl, messageBag) {
}
// create the left icon
const icon = document.createElement('span')
icon.className = 'material-symbols'
icon.setAttribute('aria-hidden', 'true')
icon.innerText = message.type === 'error' ? 'error' : 'check_circle'
const icon = createIcon(
message.type === 'error' ? 'error' : 'check_circle'
)
const messageIcon = document.createElement('div')
messageIcon.className = 'notification-icon'
messageIcon.appendChild(icon)

View File

@@ -1,9 +1,25 @@
import { isBootstrap5 } from '@/features/utils/bootstrap-5'
import createIcon from '@/features/form-helpers/create-icon'
export default function inputValidator(inputEl) {
const messageEl = document.createElement('div')
messageEl.className =
inputEl.getAttribute('data-ol-validation-message-classes') ||
'small text-danger mt-2'
'small text-danger mt-2 form-text'
messageEl.hidden = true
const messageInnerEl = messageEl.appendChild(document.createElement('span'))
messageInnerEl.className = 'form-text-inner'
const messageTextNode = document.createTextNode('')
// In Bootstrap 5, add an icon
if (isBootstrap5()) {
const iconEl = createIcon('error')
messageInnerEl.append(iconEl)
}
messageInnerEl.append(messageTextNode)
inputEl.insertAdjacentElement('afterend', messageEl)
// Hide messages until the user leaves the input field or submits the form.
@@ -54,7 +70,7 @@ export default function inputValidator(inputEl) {
// Require another blur before displaying errors again.
canDisplayErrorMessages = false
} else {
messageEl.textContent = inputEl.validationMessage
messageTextNode.data = inputEl.validationMessage
messageEl.hidden = false
}
}

View File

@@ -257,3 +257,8 @@
}
}
}
// Only apply bottom margin to form messages when it is non-empty
.form-messages-bottom-margin > :last-child {
margin-bottom: var(--spacing-06);
}