Add basic styling and display behavior
authorAlexander Ebert <ebert@woltlab.com>
Wed, 24 Jan 2024 17:59:25 +0000 (18:59 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Thu, 25 Jan 2024 15:29:59 +0000 (16:29 +0100)
ts/WoltLabSuite/Core/Component/Popover.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Popover.js
wcfsetup/install/files/lib/action/UserPopoverAction.class.php
wcfsetup/install/files/style/ui/popover.scss

index e3af09779f2a78033983ac74e3f4a9910912fa75..659307e0ab59d591e3d531e8a4829fb38d830b95 100644 (file)
@@ -2,6 +2,13 @@ import { prepareRequest } from "../Ajax/Backend";
 import DomUtil from "../Dom/Util";
 import { getPageOverlayContainer } from "../Helper/PageOverlay";
 import { wheneverFirstSeen } from "../Helper/Selector";
+import RepeatingTimer from "../Timer/Repeating";
+import * as UiAlignment from "../Ui/Alignment";
+
+const enum Delay {
+  Hide = 500,
+  Show = 800,
+}
 
 class Popover {
   readonly #cache = new Map<number, string>();
@@ -10,6 +17,10 @@ class Popover {
   readonly #endpoint: URL;
   #enabled = true;
   readonly #identifier: string;
+  #pendingElement: HTMLElement | undefined = undefined;
+  #pendingObjectId: number | undefined = undefined;
+  #timerStart: RepeatingTimer | undefined = undefined;
+  #timerHide: RepeatingTimer | undefined = undefined;
 
   constructor(selector: string, endpoint: string, identifier: string) {
     this.#identifier = identifier;
@@ -17,7 +28,7 @@ class Popover {
 
     wheneverFirstSeen(selector, (element) => {
       element.addEventListener("mouseenter", () => {
-        void this.#hoverStart(element);
+        this.#hoverStart(element);
       });
       element.addEventListener("mouseleave", () => {
         this.#hoverEnd(element);
@@ -36,29 +47,63 @@ class Popover {
     });
   }
 
-  async #hoverStart(element: HTMLElement): Promise<void> {
+  #hoverStart(element: HTMLElement): void {
     const objectId = this.#getObjectId(element);
 
-    let content = this.#cache.get(objectId);
-    if (content === undefined) {
-      content = await this.#fetch(objectId);
-      this.#cache.set(objectId, content);
+    this.#pendingObjectId = objectId;
+    if (this.#timerStart === undefined) {
+      this.#timerStart = new RepeatingTimer((timer) => {
+        timer.stop();
+
+        const objectId = this.#pendingObjectId!;
+        void this.#getContent(objectId).then((content) => {
+          if (objectId !== this.#pendingObjectId) {
+            return;
+          }
+
+          const container = this.#getContainer();
+          DomUtil.setInnerHtml(container, content);
+
+          UiAlignment.set(container, element, { vertical: "top" });
+
+          container.setAttribute("aria-hidden", "false");
+        });
+      }, Delay.Show);
+    } else {
+      this.#timerStart.restart();
     }
+  }
+
+  #hoverEnd(element: HTMLElement): void {
+    this.#timerStart?.stop();
+    this.#pendingObjectId = undefined;
 
-    DomUtil.setInnerHtml(this.#getContainer(), content);
+    if (this.#timerHide === undefined) {
+      this.#timerHide = new RepeatingTimer(() => {
+        // do something
+      }, Delay.Hide);
+    } else {
+      this.#timerHide.restart();
+    }
   }
 
-  #hoverEnd(element: HTMLElement): void {}
+  async #getContent(objectId: number): Promise<string> {
+    let content = this.#cache.get(objectId);
+    if (content !== undefined) {
+      return content;
+    }
 
-  async #fetch(objectId: number): Promise<string> {
     this.#endpoint.searchParams.set("id", objectId.toString());
 
     const response = await prepareRequest(this.#endpoint).get().fetchAsResponse();
-    if (response?.ok) {
-      return await response.text();
+    if (!response?.ok) {
+      return "";
     }
 
-    return "";
+    content = await response.text();
+    this.#cache.set(objectId, content);
+
+    return content;
   }
 
   #setEnabled(enabled: boolean): void {
@@ -72,7 +117,9 @@ class Popover {
   #getContainer(): HTMLElement {
     if (this.#container === undefined) {
       this.#container = document.createElement("div");
+      this.#container.classList.add("popoverContainer");
       this.#container.dataset.identifier = this.#identifier;
+      this.#container.setAttribute("aria-hidden", "true");
     }
 
     this.#container.remove();
index e090260ca47037c8886db3089f147cb282e31131..64917fd0d5bd26e59cec71287fef20d712f286dd 100644 (file)
@@ -1,8 +1,10 @@
-define(["require", "exports", "tslib", "../Ajax/Backend", "../Dom/Util", "../Helper/PageOverlay", "../Helper/Selector"], function (require, exports, tslib_1, Backend_1, Util_1, PageOverlay_1, Selector_1) {
+define(["require", "exports", "tslib", "../Ajax/Backend", "../Dom/Util", "../Helper/PageOverlay", "../Helper/Selector", "../Timer/Repeating", "../Ui/Alignment"], function (require, exports, tslib_1, Backend_1, Util_1, PageOverlay_1, Selector_1, Repeating_1, UiAlignment) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.setupFor = void 0;
     Util_1 = tslib_1.__importDefault(Util_1);
+    Repeating_1 = tslib_1.__importDefault(Repeating_1);
+    UiAlignment = tslib_1.__importStar(UiAlignment);
     class Popover {
         #cache = new Map();
         #currentElement = undefined;
@@ -10,12 +12,16 @@ define(["require", "exports", "tslib", "../Ajax/Backend", "../Dom/Util", "../Hel
         #endpoint;
         #enabled = true;
         #identifier;
+        #pendingElement = undefined;
+        #pendingObjectId = undefined;
+        #timerStart = undefined;
+        #timerHide = undefined;
         constructor(selector, endpoint, identifier) {
             this.#identifier = identifier;
             this.#endpoint = new URL(endpoint);
             (0, Selector_1.wheneverFirstSeen)(selector, (element) => {
                 element.addEventListener("mouseenter", () => {
-                    void this.#hoverStart(element);
+                    this.#hoverStart(element);
                 });
                 element.addEventListener("mouseleave", () => {
                     this.#hoverEnd(element);
@@ -30,23 +36,53 @@ define(["require", "exports", "tslib", "../Ajax/Backend", "../Dom/Util", "../Hel
                 this.#setEnabled(false);
             });
         }
-        async #hoverStart(element) {
+        #hoverStart(element) {
             const objectId = this.#getObjectId(element);
-            let content = this.#cache.get(objectId);
-            if (content === undefined) {
-                content = await this.#fetch(objectId);
-                this.#cache.set(objectId, content);
+            this.#pendingObjectId = objectId;
+            if (this.#timerStart === undefined) {
+                this.#timerStart = new Repeating_1.default((timer) => {
+                    timer.stop();
+                    const objectId = this.#pendingObjectId;
+                    void this.#getContent(objectId).then((content) => {
+                        if (objectId !== this.#pendingObjectId) {
+                            return;
+                        }
+                        const container = this.#getContainer();
+                        Util_1.default.setInnerHtml(container, content);
+                        UiAlignment.set(container, element, { vertical: "top" });
+                        container.setAttribute("aria-hidden", "false");
+                    });
+                }, 800 /* Delay.Show */);
+            }
+            else {
+                this.#timerStart.restart();
             }
-            Util_1.default.setInnerHtml(this.#getContainer(), content);
         }
-        #hoverEnd(element) { }
-        async #fetch(objectId) {
+        #hoverEnd(element) {
+            this.#timerStart?.stop();
+            this.#pendingObjectId = undefined;
+            if (this.#timerHide === undefined) {
+                this.#timerHide = new Repeating_1.default(() => {
+                    // do something
+                }, 500 /* Delay.Hide */);
+            }
+            else {
+                this.#timerHide.restart();
+            }
+        }
+        async #getContent(objectId) {
+            let content = this.#cache.get(objectId);
+            if (content !== undefined) {
+                return content;
+            }
             this.#endpoint.searchParams.set("id", objectId.toString());
             const response = await (0, Backend_1.prepareRequest)(this.#endpoint).get().fetchAsResponse();
-            if (response?.ok) {
-                return await response.text();
+            if (!response?.ok) {
+                return "";
             }
-            return "";
+            content = await response.text();
+            this.#cache.set(objectId, content);
+            return content;
         }
         #setEnabled(enabled) {
             this.#enabled = enabled;
@@ -57,7 +93,9 @@ define(["require", "exports", "tslib", "../Ajax/Backend", "../Dom/Util", "../Hel
         #getContainer() {
             if (this.#container === undefined) {
                 this.#container = document.createElement("div");
+                this.#container.classList.add("popoverContainer");
                 this.#container.dataset.identifier = this.#identifier;
+                this.#container.setAttribute("aria-hidden", "true");
             }
             this.#container.remove();
             (0, PageOverlay_1.getPageOverlayContainer)().append(this.#container);
index 8e5a2cc1fbc82fa0c734df8698c2bb24b031fe04..be24b6c8fff15757f4612f4f2036ddc63a93a437 100644 (file)
@@ -3,13 +3,9 @@
 namespace wcf\action;
 
 use Laminas\Diactoros\Response\HtmlResponse;
-use Laminas\Diactoros\Response\JsonResponse;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\RequestHandlerInterface;
-use wcf\data\user\group\UserGroup;
-use wcf\data\user\UserProfile;
-use wcf\data\user\UserProfileList;
 use wcf\http\Helper;
 use wcf\system\cache\runtime\UserProfileRuntimeCache;
 use wcf\system\WCF;
index 760ca85614c75fa9493a0c20d3fa565f4a642879..77c374faccfbbf2fb16ea0a3da66f4c9425cf35f 100644 (file)
                display: none;
        }
 }
+
+/* @since 6.1 */
+.popoverContainer {
+       --padding: 20px;
+
+       background-color: var(--wcfContentContainerBackground);
+       border: 1px solid var(--wcfContentBorderInner);
+       border-radius: var(--wcfBorderRadius);
+       box-shadow: var(--wcfBoxShadow);
+       color: var(--wcfContentText);
+       max-height: 300px;
+       max-width: 500px;
+       opacity: 0;
+       padding: var(--padding);
+       position: absolute;
+       transform: translateY(-20px);
+       transition:
+               opacity 0.12s linear,
+               transform 0.12s linear;
+
+       a {
+               color: var(--wcfContentLink);
+
+               &:hover {
+                       color: var(--wcfContentLinkActive);
+               }
+       }
+
+       &[aria-hidden="false"] {
+               opacity: 1;
+               transform: translateY(0);
+       }
+}