From 31ab47a5345e0bf245c82bd6b512fc3ffb881e95 Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Thu, 8 Jan 2026 16:31:14 +0100 Subject: [PATCH] [web] Make CIAM login notification dismissible (with cookies) (#30251) * Extend notifications so they can be dismissed The dismissal is stored in a cookie for a year * Extend CIAM notifications so they can be dismissed * Make the CIAM notification dismissable * Style the close button so it is in the corner of the notification * Add e2e test * [e2e] Replace 'not.exist' by 'not.be.visible' * Set cookie with the same pattern as the "cookie-banner cookie" * Hide notification in the frontend * Revert "Hide notification in the frontend" This reverts commit b5d205f3e3a4e2555be038eb3b7561761a2cde59. * Add `data-ol-dismiss-cookie-paths` to the notification * Add `data-ol-dismiss-cookie-paths` to the notification (CIAM variant) * Shorten cookie name * Revert: remove the .corner class * Apply design from Figma for CIAM notifications GitOrigin-RevId: 4070715c6a63d0497b7a41c343c3f943ced4bfef --- .../app/src/infrastructure/ExpressLocals.mjs | 12 +++++ .../web/app/views/_mixins/ciam_mixins.pug | 30 +++++++---- .../web/app/views/_mixins/notification.pug | 52 +++++++++++-------- services/web/frontend/js/bootstrap.ts | 1 + .../form-helpers/form-phosphor-icons.ts | 1 + .../dismissible-notifications.ts | 44 ++++++++++++++++ .../ds/components/notification.scss | 28 ++++++++++ 7 files changed, 136 insertions(+), 32 deletions(-) create mode 100644 services/web/frontend/js/features/notifications/dismissible-notifications.ts diff --git a/services/web/app/src/infrastructure/ExpressLocals.mjs b/services/web/app/src/infrastructure/ExpressLocals.mjs index 8c9adbad27..dede4db7be 100644 --- a/services/web/app/src/infrastructure/ExpressLocals.mjs +++ b/services/web/app/src/infrastructure/ExpressLocals.mjs @@ -337,6 +337,18 @@ export default async function (webRouter, privateApiRouter, publicApiRouter) { next() }) + webRouter.use(function (req, res, next) { + const KEY_PREFIX = 'readnotif-' + const dismissedNotifications = [] + for (const cookieName in req.cookies) { + if (cookieName.startsWith(KEY_PREFIX)) { + dismissedNotifications.push(cookieName.slice(KEY_PREFIX.length)) + } + } + res.locals.dismissedNotifications = dismissedNotifications + next() + }) + webRouter.use(function (req, res, next) { res.locals.ExposedSettings = { isOverleaf: Settings.overleaf != null, diff --git a/services/web/app/views/_mixins/ciam_mixins.pug b/services/web/app/views/_mixins/ciam_mixins.pug index 8fba22064d..8ade02511d 100644 --- a/services/web/app/views/_mixins/ciam_mixins.pug +++ b/services/web/app/views/_mixins/ciam_mixins.pug @@ -109,16 +109,26 @@ mixin ciamErrorNotification .notification-content block -mixin ciamInfoNotification - .notification.notification-ds.notification-type-info( - role='alert' - aria-live='polite' - ) - .notification-icon - ph-info(aria-hidden='true') - .notification-content-and-cta - .notification-content - block +mixin ciamInfoNotification(options) + - var {dismissId, dismissCookiePaths = ['/']} = options + if dismissId && dismissedNotifications.includes(dismissId) + // Notification has been dismissed, do not render + else + .notification.notification-ds.notification-type-info( + role='alert' + aria-live='polite' + data-ol-dismiss-id=dismissId + data-ol-dismiss-cookie-paths=dismissCookiePaths.join(',') + ) + .notification-icon + ph-info(aria-hidden='true') + .notification-content-and-cta + .notification-content + block + if dismissId + .notification-close-btn + button(aria-label=translate('close') data-ol-dismiss-button) + ph-x mixin ciamOrDivider p.ciam-login-register-or-text-container= translate('login_register_or').toUpperCase() diff --git a/services/web/app/views/_mixins/notification.pug b/services/web/app/views/_mixins/notification.pug index 807e13ab60..f17b0f32a7 100644 --- a/services/web/app/views/_mixins/notification.pug +++ b/services/web/app/views/_mixins/notification.pug @@ -12,27 +12,35 @@ mixin notificationIcon(type) +material-symbol('warning') mixin notification(options) - - var {ariaLive, id, type, title, content, disclaimer, className} = options + - var {ariaLive, id, type, title, content, disclaimer, className, dismissId, dismissCookiePaths = ['/']} = options - var classNames = `notification notification-type-${type} ${className ? className : ''} ${isActionBelowContent ? 'notification-cta-below-content' : ''}` - div(aria-live=ariaLive role='alert' id=id class=classNames) - .notification-icon - +notificationIcon(type) - .notification-content-and-cta - .notification-content - if title - p - b #{title} - | !{content} - block - //- TODO: handle action - //- if action - //- .notification-cta - if disclaimer - .notification-disclaimer #{disclaimer} - //- TODO: handle dismissible notifications - //- TODO: handle onDismiss - //- if isDismissible - //- .notification-close-btn - //- button(aria-label=translate('close')) - //- +material-symbol("close") + if dismissId && dismissedNotifications.includes(dismissId) + //- Notification has been dismissed, do not render + else + div( + aria-live=ariaLive + role='alert' + id=id + class=classNames + data-ol-dismiss-id=dismissId + data-ol-dismiss-cookie-paths=dismissCookiePaths.join(',') + ) + .notification-icon + +notificationIcon(type) + .notification-content-and-cta + .notification-content + if title + p + b #{title} + | !{content} + block + //- TODO: handle action + //- if action + //- .notification-cta + if disclaimer + .notification-disclaimer #{disclaimer} + if dismissId + .notification-close-btn + button(aria-label=translate('close') data-ol-dismiss-button) + +material-symbol('close') diff --git a/services/web/frontend/js/bootstrap.ts b/services/web/frontend/js/bootstrap.ts index 2525e080d2..cd80f4818d 100644 --- a/services/web/frontend/js/bootstrap.ts +++ b/services/web/frontend/js/bootstrap.ts @@ -1,4 +1,5 @@ import './features/bookmarkable-tab/index' import './features/tooltip/index' import './features/navbar/index' +import './features/notifications/dismissible-notifications' import 'bootstrap' diff --git a/services/web/frontend/js/features/form-helpers/form-phosphor-icons.ts b/services/web/frontend/js/features/form-helpers/form-phosphor-icons.ts index 3ebe910702..8503b8a3f2 100644 --- a/services/web/frontend/js/features/form-helpers/form-phosphor-icons.ts +++ b/services/web/frontend/js/features/form-helpers/form-phosphor-icons.ts @@ -4,3 +4,4 @@ import '@phosphor-icons/webcomponents/PhEye' import '@phosphor-icons/webcomponents/PhInfo' import '@phosphor-icons/webcomponents/PhEyeSlash' import '@phosphor-icons/webcomponents/PhWarningCircle' +import '@phosphor-icons/webcomponents/PhX' diff --git a/services/web/frontend/js/features/notifications/dismissible-notifications.ts b/services/web/frontend/js/features/notifications/dismissible-notifications.ts new file mode 100644 index 0000000000..cee34f12fd --- /dev/null +++ b/services/web/frontend/js/features/notifications/dismissible-notifications.ts @@ -0,0 +1,44 @@ +import getMeta from '@/utils/meta' + +const KEY_PREFIX = 'readnotif-' + +function setDismissedNotification(dismissId: string, cookiePaths: string[]) { + const name = `${KEY_PREFIX}${dismissId}` + const cookieDomain = getMeta('ol-ExposedSettings').cookieDomain + const oneYearInSeconds = 60 * 60 * 24 * 365 + + for (const path of cookiePaths) { + const cookieAttributes = + `; path=${path}` + + '; domain=' + + cookieDomain + + '; max-age=' + + oneYearInSeconds + + '; SameSite=Lax; Secure' + document.cookie = `${name}=1;${cookieAttributes}` + } +} + +function hydrateDismissibleNotification(notification: HTMLElement) { + const dismissId = notification.dataset.olDismissId + if (!dismissId) return + + const dismissCookiePaths = notification.dataset.olDismissCookiePaths + ? notification.dataset.olDismissCookiePaths.split(',') + : ['/'] + + const dismissButton = notification.querySelector( + '[data-ol-dismiss-button]' + ) + + if (dismissButton) { + dismissButton.addEventListener('click', () => { + setDismissedNotification(dismissId, dismissCookiePaths) + notification.hidden = true + }) + } +} + +document + .querySelectorAll('[data-ol-dismiss-id]') + .forEach(hydrateDismissibleNotification) diff --git a/services/web/frontend/stylesheets/ds/components/notification.scss b/services/web/frontend/stylesheets/ds/components/notification.scss index f462adc62c..6086d6f84a 100644 --- a/services/web/frontend/stylesheets/ds/components/notification.scss +++ b/services/web/frontend/stylesheets/ds/components/notification.scss @@ -27,4 +27,32 @@ &.notification-type-info { background-color: var(--ds-color-blue-50); } + + .notification-close-btn { + align-self: flex-start; + padding: var(--ds-spacing-400) 0 0; + height: auto; + + button { + @include ds-heading-sm-regular; + + color: var(--ds-color-neutral-950); + padding: var(--ds-spacing-150); + + &:hover, + &:focus { + background-color: var(--ds-color-blue-100); + } + + &:active { + background-color: var(--ds-color-blue-200); + } + + &:focus-visible { + background: transparent; + + @include ds-focus-outline; + } + } + } }