From 9ad6fa973a866435e695c4807f7724017e73fb56 Mon Sep 17 00:00:00 2001
From: Jochen Roth <jochen.roth@b13.com>
Date: Wed, 12 Jun 2024 14:28:21 +0200
Subject: [PATCH] [FEATURE] Improve backend notifications display and handling

Currently, the notification/alert container does not allow scrolling
when multiple notifications exceed the viewport height and
it is not possible to clear all displayed notifications.

This has been changed to allow scrolling within the
container. On top of this, it is now also possible
to focus the container using tab and scroll up and down
using the arrow keys and clear all notifications with a
single click.

Resolves: #104069
Releases: main
Change-Id: I6f4c27bff84942a3f161310da85a04957648218d
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/84659
Reviewed-by: Garvin Hicking <gh@faktor-e.de>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Garvin Hicking <gh@faktor-e.de>
---
 .../element/typo3-notification-message.scss   | 15 ++++
 .../TypeScript/backend/notification.ts        | 84 +++++++++++++++++--
 .../backend/tests/notification-test.ts        |  2 +-
 .../Resources/Private/Language/locallang.xlf  |  3 +
 .../backend/Resources/Public/Css/backend.css  |  3 +
 .../Public/JavaScript/notification.js         |  8 +-
 ...BackendNotificationsDisplayAndHandling.rst | 27 ++++++
 .../Styleguide/NotificationCest.php           | 50 +++++++++++
 8 files changed, 183 insertions(+), 9 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/13.2/Feature-104069-ImprovedBackendNotificationsDisplayAndHandling.rst
 create mode 100644 typo3/sysext/core/Tests/Acceptance/Application/Styleguide/NotificationCest.php

diff --git a/Build/Sources/Sass/element/typo3-notification-message.scss b/Build/Sources/Sass/element/typo3-notification-message.scss
index 40bb4363dd67..d2427e8d692f 100644
--- a/Build/Sources/Sass/element/typo3-notification-message.scss
+++ b/Build/Sources/Sass/element/typo3-notification-message.scss
@@ -22,9 +22,24 @@
         right: calc(var(--typo3-spacing) * 1.5);
     }
 
+    .alert-list {
+        display: block;
+        max-height: calc(100vh - $toolbar-height - var(--typo3-spacing) - var(--module-docheader-height) * 2);
+        overflow-y: auto;
+    }
+
     typo3-notification-message {
         margin-top: 5px;
     }
+
+    typo3-notification-clear-all {
+        text-align: right;
+
+        div {
+            display: block;
+            padding-bottom: calc(var(--typo3-spacing) / 2);
+        }
+    }
 }
 
 typo3-notification-message {
diff --git a/Build/Sources/TypeScript/backend/notification.ts b/Build/Sources/TypeScript/backend/notification.ts
index a3cf9a20f425..7da60ea7e11f 100644
--- a/Build/Sources/TypeScript/backend/notification.ts
+++ b/Build/Sources/TypeScript/backend/notification.ts
@@ -19,6 +19,7 @@ import { AbstractAction } from './action-button/abstract-action';
 import { SeverityEnum } from './enum/severity';
 import Severity from './severity';
 import '@typo3/backend/element/icon-element';
+import { lll } from '@typo3/core/lit-helper';
 
 interface Action {
   label: string;
@@ -31,7 +32,11 @@ interface Action {
  */
 class Notification {
   private static readonly duration: number = 5;
+  private static readonly showClearAllButtonCount: number = 2;
+  private static totalNotifications: number = 0;
   private static messageContainer: HTMLElement = null;
+  private static notificationList: HTMLElement = null;
+  private static clearAllButton: HTMLElement = null;
 
   /**
    * Show a notice notification
@@ -114,7 +119,30 @@ class Notification {
     if (this.messageContainer === null || document.getElementById('alert-container') === null) {
       this.messageContainer = document.createElement('div');
       this.messageContainer.setAttribute('id', 'alert-container');
+      this.notificationList = document.createElement('div');
+      this.notificationList.setAttribute('class', 'alert-list')
+      // Enable focusing for keyboard scrolling (accessibility)
+      this.notificationList.setAttribute('tabindex', '0')
+      this.messageContainer.appendChild(this.notificationList);
+
+      this.clearAllButton = <ClearNotificationMessages>document.createElement('typo3-notification-clear-all');
+      this.containerItemVisibility();
+      this.messageContainer.prepend(this.clearAllButton);
       document.body.appendChild(this.messageContainer);
+
+      document.addEventListener('typo3-notification-open', () => {
+        this.totalNotifications++;
+        this.containerItemVisibility();
+      })
+
+      document.addEventListener('typo3-notification-clear', () => {
+        // Avoid negative value
+        if(this.totalNotifications > 0) {
+          this.totalNotifications--;
+        }
+
+        this.containerItemVisibility();
+      })
     }
 
     const box = <NotificationMessage>document.createElement('typo3-notification-message');
@@ -126,10 +154,45 @@ class Notification {
     box.setAttribute('notification-severity', severity.toString());
     box.setAttribute('notification-duration', duration.toString());
     box.actions = actions;
-    this.messageContainer.appendChild(box);
+
+    // Wait for the animation to finish, before scrolling into view
+    setTimeout(() => {
+      this.notificationList.querySelector('typo3-notification-message:last-child').scrollIntoView();
+    }, Number(duration))
+
+    this.notificationList.appendChild(box);
+  }
+
+  protected static containerItemVisibility() {
+    this.clearAllButton.hidden = this.totalNotifications < this.showClearAllButtonCount;
+    this.messageContainer.hidden = this.totalNotifications === 0;
   }
 }
 
+@customElement('typo3-notification-clear-all')
+export class ClearNotificationMessages extends LitElement {
+  @property({ type: String, attribute: 'notification-container' }) notificationId: string;
+
+  public async clearAll() {
+    this.dispatchEvent(
+      new CustomEvent('typo3-notification-clear-all', { bubbles: true, composed: true })
+    );
+
+    this.hidden = true
+  }
+
+  protected createRenderRoot(): HTMLElement | DocumentFragment {
+    return this;
+  }
+
+  protected render() {
+    return html`<div><button @click=${() => this.clearAll()} class="btn btn-default">
+      <typo3-backend-icon identifier="actions-close" size="small"></typo3-backend-icon> ${lll('button.clearAll') || 'Clear all'}
+    </button></div>`;
+  }
+
+}
+
 @customElement('typo3-notification-message')
 export class NotificationMessage extends LitElement {
   @property({ type: String, attribute: 'notification-id' }) notificationId: string;
@@ -142,15 +205,26 @@ export class NotificationMessage extends LitElement {
   @state() executingAction: number = -1;
 
   public async firstUpdated(): Promise<void> {
+    document.addEventListener('typo3-notification-clear-all', async () => {
+      this.clear();
+    });
+
+    const event = new CustomEvent('typo3-notification-open', { bubbles: true, composed: true });
+    this.dispatchEvent(event);
+
     await new Promise(resolve => window.setTimeout(resolve, 200));
     await this.requestUpdate();
     if (this.notificationDuration > 0) {
       await new Promise(resolve => window.setTimeout(resolve, this.notificationDuration * 1000));
-      this.close();
+      this.clear();
     }
   }
 
-  public async close(): Promise<void> {
+  public async clear(): Promise<void> {
+    this.dispatchEvent(
+      new CustomEvent('typo3-notification-clear', { bubbles: true, composed: true })
+    );
+
     const onfinish = () => {
       this.parentNode && this.parentNode.removeChild(this);
     };
@@ -209,7 +283,7 @@ export class NotificationMessage extends LitElement {
         aria-labelledby="alert-title-${randomSuffix}"
         aria-describedby="alert-message-${randomSuffix}"
       >
-        <button type="button" class="close" @click="${async () => this.close()}">
+        <button type="button" class="close" @click="${async () => this.clear()}">
           <span aria-hidden="true"><typo3-backend-icon identifier="actions-close" size="small"></typo3-backend-icon></span>
           <span class="visually-hidden">Close</span>
         </button>
@@ -236,7 +310,7 @@ export class NotificationMessage extends LitElement {
                    if ('action' in action) {
                      await action.action.execute(event.currentTarget as HTMLAnchorElement);
                    }
-                   this.close();
+                   this.clear();
                  }}"
                  class="${classMap({
                    executing: this.executingAction === index,
diff --git a/Build/Sources/TypeScript/backend/tests/notification-test.ts b/Build/Sources/TypeScript/backend/tests/notification-test.ts
index f002ba8cf6d8..cbe7dbdf793d 100644
--- a/Build/Sources/TypeScript/backend/tests/notification-test.ts
+++ b/Build/Sources/TypeScript/backend/tests/notification-test.ts
@@ -31,7 +31,7 @@ describe('@typo3/backend/notification:', () => {
   afterEach((): void => {
     getIconStub.restore();
 
-    const alertContainer = document.getElementById('alert-container');
+    const alertContainer = document.querySelector('#alert-container .alert-list');
     while (alertContainer !== null && alertContainer.firstChild) {
       alertContainer.removeChild(alertContainer.firstChild);
     }
diff --git a/typo3/sysext/adminpanel/Resources/Private/Language/locallang.xlf b/typo3/sysext/adminpanel/Resources/Private/Language/locallang.xlf
index 53df11676517..8ca41a7265b8 100644
--- a/typo3/sysext/adminpanel/Resources/Private/Language/locallang.xlf
+++ b/typo3/sysext/adminpanel/Resources/Private/Language/locallang.xlf
@@ -22,6 +22,9 @@
 			<trans-unit id="button.view" resname="button.view">
 				<source>View</source>
 			</trans-unit>
+			<trans-unit id="button.clearAll" resname="button.clearAll">
+				<source>Clear all</source>
+			</trans-unit>
 		</body>
 	</file>
 </xliff>
diff --git a/typo3/sysext/backend/Resources/Public/Css/backend.css b/typo3/sysext/backend/Resources/Public/Css/backend.css
index 0138ff019206..e63a80bab0ea 100644
--- a/typo3/sysext/backend/Resources/Public/Css/backend.css
+++ b/typo3/sysext/backend/Resources/Public/Css/backend.css
@@ -3894,7 +3894,10 @@ typo3-backend-table-wizard{display:inline-block}
 @media (min-width:768px){
 #alert-container{right:calc(var(--typo3-spacing) * 1.5)}
 }
+#alert-container .alert-list{display:block;max-height:calc(100vh - 45px - var(--typo3-spacing) - var(--module-docheader-height) * 2);overflow-y:auto}
 #alert-container typo3-notification-message{margin-top:5px}
+#alert-container typo3-notification-clear-all{text-align:right}
+#alert-container typo3-notification-clear-all div{display:block;padding-bottom:calc(var(--typo3-spacing)/ 2)}
 typo3-notification-message{display:block}
 typo3-notification-message .alert{box-shadow:var(--typo3-component-box-shadow-dialog);margin:0}
 typo3-rte-ckeditor-ckeditor5 .ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled{display:none}
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/notification.js b/typo3/sysext/backend/Resources/Public/JavaScript/notification.js
index 2743f9ebcaa6..f6b3f594db7e 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/notification.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/notification.js
@@ -10,7 +10,9 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-var __decorate=function(t,i,e,o){var n,a=arguments.length,s=a<3?i:null===o?o=Object.getOwnPropertyDescriptor(i,e):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,i,e,o);else for(var c=t.length-1;c>=0;c--)(n=t[c])&&(s=(a<3?n(s):a>3?n(i,e,s):n(i,e))||s);return a>3&&s&&Object.defineProperty(i,e,s),s};import{LitElement,html}from"lit";import{customElement,property,state}from"lit/decorators.js";import{classMap}from"lit/directives/class-map.js";import{ifDefined}from"lit/directives/if-defined.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import Severity from"@typo3/backend/severity.js";import"@typo3/backend/element/icon-element.js";class Notification{static notice(t,i,e,o){Notification.showMessage(t,i,SeverityEnum.notice,e,o)}static info(t,i,e,o){Notification.showMessage(t,i,SeverityEnum.info,e,o)}static success(t,i,e,o){Notification.showMessage(t,i,SeverityEnum.ok,e,o)}static warning(t,i,e,o){Notification.showMessage(t,i,SeverityEnum.warning,e,o)}static error(t,i,e=0,o){Notification.showMessage(t,i,SeverityEnum.error,e,o)}static showMessage(t,i,e=SeverityEnum.info,o,n=[]){void 0===o&&(o=e===SeverityEnum.error?0:this.duration),null!==this.messageContainer&&null!==document.getElementById("alert-container")||(this.messageContainer=document.createElement("div"),this.messageContainer.setAttribute("id","alert-container"),document.body.appendChild(this.messageContainer));const a=document.createElement("typo3-notification-message");a.setAttribute("notification-id","notification-"+Math.random().toString(36).substring(2,6)),a.setAttribute("notification-title",t),i&&a.setAttribute("notification-message",i),a.setAttribute("notification-severity",e.toString()),a.setAttribute("notification-duration",o.toString()),a.actions=n,this.messageContainer.appendChild(a)}}Notification.duration=5,Notification.messageContainer=null;let notificationObject,NotificationMessage=class extends LitElement{constructor(){super(...arguments),this.notificationSeverity=SeverityEnum.info,this.notificationDuration=0,this.actions=[],this.executingAction=-1}async firstUpdated(){await new Promise((t=>window.setTimeout(t,200))),await this.requestUpdate(),this.notificationDuration>0&&(await new Promise((t=>window.setTimeout(t,1e3*this.notificationDuration))),this.close())}async close(){const t=()=>{this.parentNode&&this.parentNode.removeChild(this)};!window.matchMedia("(prefers-reduced-motion: reduce)").matches&&"animate"in this?(this.style.overflow="hidden",this.style.display="block",this.animate([{height:this.getBoundingClientRect().height+"px"},{height:0,opacity:0,marginTop:0}],{duration:400,easing:"cubic-bezier(.02, .01, .47, 1)"}).onfinish=t):t()}createRenderRoot(){return this}render(){const t=Severity.getCssClass(this.notificationSeverity);let i="";switch(this.notificationSeverity){case SeverityEnum.notice:i="actions-lightbulb";break;case SeverityEnum.ok:i="actions-check";break;case SeverityEnum.warning:i="actions-exclamation";break;case SeverityEnum.error:i="actions-close";break;case SeverityEnum.info:default:i="actions-info"}const e=(Math.random()+1).toString(36).substring(2);return html`
+var __decorate=function(t,i,e,o){var n,a=arguments.length,s=a<3?i:null===o?o=Object.getOwnPropertyDescriptor(i,e):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,i,e,o);else for(var c=t.length-1;c>=0;c--)(n=t[c])&&(s=(a<3?n(s):a>3?n(i,e,s):n(i,e))||s);return a>3&&s&&Object.defineProperty(i,e,s),s};import{LitElement,html}from"lit";import{customElement,property,state}from"lit/decorators.js";import{classMap}from"lit/directives/class-map.js";import{ifDefined}from"lit/directives/if-defined.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import Severity from"@typo3/backend/severity.js";import"@typo3/backend/element/icon-element.js";import{lll}from"@typo3/core/lit-helper.js";class Notification{static notice(t,i,e,o){Notification.showMessage(t,i,SeverityEnum.notice,e,o)}static info(t,i,e,o){Notification.showMessage(t,i,SeverityEnum.info,e,o)}static success(t,i,e,o){Notification.showMessage(t,i,SeverityEnum.ok,e,o)}static warning(t,i,e,o){Notification.showMessage(t,i,SeverityEnum.warning,e,o)}static error(t,i,e=0,o){Notification.showMessage(t,i,SeverityEnum.error,e,o)}static showMessage(t,i,e=SeverityEnum.info,o,n=[]){void 0===o&&(o=e===SeverityEnum.error?0:this.duration),null!==this.messageContainer&&null!==document.getElementById("alert-container")||(this.messageContainer=document.createElement("div"),this.messageContainer.setAttribute("id","alert-container"),this.notificationList=document.createElement("div"),this.notificationList.setAttribute("class","alert-list"),this.notificationList.setAttribute("tabindex","0"),this.messageContainer.appendChild(this.notificationList),this.clearAllButton=document.createElement("typo3-notification-clear-all"),this.containerItemVisibility(),this.messageContainer.prepend(this.clearAllButton),document.body.appendChild(this.messageContainer),document.addEventListener("typo3-notification-open",(()=>{this.totalNotifications++,this.containerItemVisibility()})),document.addEventListener("typo3-notification-clear",(()=>{this.totalNotifications>0&&this.totalNotifications--,this.containerItemVisibility()})));const a=document.createElement("typo3-notification-message");a.setAttribute("notification-id","notification-"+Math.random().toString(36).substring(2,6)),a.setAttribute("notification-title",t),i&&a.setAttribute("notification-message",i),a.setAttribute("notification-severity",e.toString()),a.setAttribute("notification-duration",o.toString()),a.actions=n,setTimeout((()=>{this.notificationList.querySelector("typo3-notification-message:last-child").scrollIntoView()}),Number(o)),this.notificationList.appendChild(a)}static containerItemVisibility(){this.clearAllButton.hidden=this.totalNotifications<this.showClearAllButtonCount,this.messageContainer.hidden=0===this.totalNotifications}}Notification.duration=5,Notification.showClearAllButtonCount=2,Notification.totalNotifications=0,Notification.messageContainer=null,Notification.notificationList=null,Notification.clearAllButton=null;let ClearNotificationMessages=class extends LitElement{async clearAll(){this.dispatchEvent(new CustomEvent("typo3-notification-clear-all",{bubbles:!0,composed:!0})),this.hidden=!0}createRenderRoot(){return this}render(){return html`<div><button @click=${()=>this.clearAll()} class="btn btn-default">
+      <typo3-backend-icon identifier="actions-close" size="small"></typo3-backend-icon> ${lll("button.clearAll")||"Clear all"}
+    </button></div>`}};__decorate([property({type:String,attribute:"notification-container"})],ClearNotificationMessages.prototype,"notificationId",void 0),ClearNotificationMessages=__decorate([customElement("typo3-notification-clear-all")],ClearNotificationMessages);export{ClearNotificationMessages};let notificationObject,NotificationMessage=class extends LitElement{constructor(){super(...arguments),this.notificationSeverity=SeverityEnum.info,this.notificationDuration=0,this.actions=[],this.executingAction=-1}async firstUpdated(){document.addEventListener("typo3-notification-clear-all",(async()=>{this.clear()}));const t=new CustomEvent("typo3-notification-open",{bubbles:!0,composed:!0});this.dispatchEvent(t),await new Promise((t=>window.setTimeout(t,200))),await this.requestUpdate(),this.notificationDuration>0&&(await new Promise((t=>window.setTimeout(t,1e3*this.notificationDuration))),this.clear())}async clear(){this.dispatchEvent(new CustomEvent("typo3-notification-clear",{bubbles:!0,composed:!0}));const t=()=>{this.parentNode&&this.parentNode.removeChild(this)};!window.matchMedia("(prefers-reduced-motion: reduce)").matches&&"animate"in this?(this.style.overflow="hidden",this.style.display="block",this.animate([{height:this.getBoundingClientRect().height+"px"},{height:0,opacity:0,marginTop:0}],{duration:400,easing:"cubic-bezier(.02, .01, .47, 1)"}).onfinish=t):t()}createRenderRoot(){return this}render(){const t=Severity.getCssClass(this.notificationSeverity);let i="";switch(this.notificationSeverity){case SeverityEnum.notice:i="actions-lightbulb";break;case SeverityEnum.ok:i="actions-check";break;case SeverityEnum.warning:i="actions-exclamation";break;case SeverityEnum.error:i="actions-close";break;case SeverityEnum.info:default:i="actions-info"}const e=(Math.random()+1).toString(36).substring(2);return html`
       <div
         id="${ifDefined(this.notificationId||void 0)}"
         class="alert alert-${t} alert-dismissible"
@@ -18,7 +20,7 @@ var __decorate=function(t,i,e,o){var n,a=arguments.length,s=a<3?i:null===o?o=Obj
         aria-labelledby="alert-title-${e}"
         aria-describedby="alert-message-${e}"
       >
-        <button type="button" class="close" @click="${async()=>this.close()}">
+        <button type="button" class="close" @click="${async()=>this.clear()}">
           <span aria-hidden="true"><typo3-backend-icon identifier="actions-close" size="small"></typo3-backend-icon></span>
           <span class="visually-hidden">Close</span>
         </button>
@@ -38,7 +40,7 @@ var __decorate=function(t,i,e,o){var n,a=arguments.length,s=a<3?i:null===o?o=Obj
             ${this.actions.map(((t,i)=>html`
               <a href="#"
                  title="${t.label}"
-                 @click="${async e=>{e.preventDefault(),this.executingAction=i,await this.updateComplete,"action"in t&&await t.action.execute(e.currentTarget),this.close()}}"
+                 @click="${async e=>{e.preventDefault(),this.executingAction=i,await this.updateComplete,"action"in t&&await t.action.execute(e.currentTarget),this.clear()}}"
                  class="${classMap({executing:this.executingAction===i,disabled:this.executingAction>=0&&this.executingAction!==i})}"
                 >${t.label}</a>
             `))}
diff --git a/typo3/sysext/core/Documentation/Changelog/13.2/Feature-104069-ImprovedBackendNotificationsDisplayAndHandling.rst b/typo3/sysext/core/Documentation/Changelog/13.2/Feature-104069-ImprovedBackendNotificationsDisplayAndHandling.rst
new file mode 100644
index 000000000000..d4331103e44a
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/13.2/Feature-104069-ImprovedBackendNotificationsDisplayAndHandling.rst
@@ -0,0 +1,27 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-104069-1718551315:
+
+======================================================================
+Feature: #104069 - Improved backend notifications display and handling
+======================================================================
+
+See :issue:`104069`
+
+Description
+===========
+
+The notifications shown on the lower right now have a "Clear all" button to allow the
+user to clear all notifications with a single click. This button is only displayed when
+two or more notifications are on screen.
+
+In case the height of the notification container exceeds the viewport, a scroll bar will
+allow the user to navigate through the notifications.
+
+Impact
+======
+
+Handling of multiple notifications has been improved by allowing to
+scroll and clear all notifications at once.
+
+.. index:: Backend, ext:backend
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Styleguide/NotificationCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Styleguide/NotificationCest.php
new file mode 100644
index 000000000000..78c03f30f169
--- /dev/null
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Styleguide/NotificationCest.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Tests\Acceptance\Application\Styleguide;
+
+use TYPO3\CMS\Core\Tests\Acceptance\Support\ApplicationTester;
+
+final class NotificationCest
+{
+    public function _before(ApplicationTester $I): void
+    {
+        $I->useExistingSession('admin');
+        $I->amOnPage('/typo3/module/system/styleguide?action=notifications');
+    }
+
+    public function seeClearAllButton(ApplicationTester $I): void
+    {
+        $I->amGoingTo('Open a notification');
+        $I->switchToContentFrame();
+        $I->click('.styleguide-content > p [data-severity="error"]');
+        $I->switchToMainFrame();
+        $I->waitForElement('#alert-container');
+        $I->dontSee('#alert-container typo3-notification-message');
+
+        $I->amGoingTo('Open a second notification and expecting to see the "Clear all" button');
+        $I->switchToContentFrame();
+        $I->click('.styleguide-content > p [data-severity="error"]');
+        $I->switchToMainFrame();
+        $I->waitForElement('#alert-container typo3-notification-message');
+        $I->waitForElement('#alert-container typo3-notification-clear-all');
+        $I->click('typo3-notification-clear-all');
+
+        $I->dontSee('typo3-notification-clear-all');
+        $I->dontSee('typo3-notification-message');
+    }
+}
-- 
GitLab