From d0010217cd98c1289ca06730324431ae059ab6cb Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Thu, 22 May 2025 11:13:42 +0100 Subject: [PATCH] Merge pull request #25613 from overleaf/td-bs5-sp-login Migrate SP/CE login page to Bootstrap 5 GitOrigin-RevId: 37fc7cbb453bfef93abde2080faaa0a88116d1f4 --- .../web/app/views/_mixins/formMessages.pug | 37 ++++---- services/web/app/views/user/login.pug | 88 +++++++++---------- .../js/features/form-helpers/create-icon.js | 7 ++ .../js/features/form-helpers/hydrate-form.js | 13 ++- .../features/form-helpers/input-validator.js | 20 ++++- .../bootstrap-5/components/notifications.scss | 5 ++ 6 files changed, 99 insertions(+), 71 deletions(-) create mode 100644 services/web/frontend/js/features/form-helpers/create-icon.js diff --git a/services/web/app/views/_mixins/formMessages.pug b/services/web/app/views/_mixins/formMessages.pug index 9ea239277a..9a76d118c7 100644 --- a/services/web/app/views/_mixins/formMessages.pug +++ b/services/web/app/views/_mixins/formMessages.pug @@ -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 diff --git a/services/web/app/views/user/login.pug b/services/web/app/views/user/login.pug index 9185b0b14b..1ad77cb8b4 100644 --- a/services/web/app/views/user/login.pug +++ b/services/web/app/views/user/login.pug @@ -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} + diff --git a/services/web/frontend/js/features/form-helpers/create-icon.js b/services/web/frontend/js/features/form-helpers/create-icon.js new file mode 100644 index 0000000000..fc26724bee --- /dev/null +++ b/services/web/frontend/js/features/form-helpers/create-icon.js @@ -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 +} diff --git a/services/web/frontend/js/features/form-helpers/hydrate-form.js b/services/web/frontend/js/features/form-helpers/hydrate-form.js index ed7b9fc26e..89bd1a657d 100644 --- a/services/web/frontend/js/features/form-helpers/hydrate-form.js +++ b/services/web/frontend/js/features/form-helpers/hydrate-form.js @@ -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) diff --git a/services/web/frontend/js/features/form-helpers/input-validator.js b/services/web/frontend/js/features/form-helpers/input-validator.js index 411c6c0e83..f01c4af3da 100644 --- a/services/web/frontend/js/features/form-helpers/input-validator.js +++ b/services/web/frontend/js/features/form-helpers/input-validator.js @@ -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 } } diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/notifications.scss b/services/web/frontend/stylesheets/bootstrap-5/components/notifications.scss index 2dc789ad44..ece1a465a4 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/notifications.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/notifications.scss @@ -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); +}