From 18d13000e911bba2c1ab3a063245b0219ac1954a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 20 Mar 2024 18:00:35 +0100
Subject: [PATCH] [PORT] gitea#29831: Prevent layout shift in `<overflow-menu>`
 items

There is a small layout shift in when active tab changes. Notice how the
actions SVG is unstable:

![](https://github.com/go-gitea/gitea/assets/115237/a6928e89-5d47-4a91-8f36-1fa22fddbce7)

This is because the active item with bold text is wider then the
inactive one. I have applied [this
trick](https://stackoverflow.com/a/32570813/808699) to prevent this
layout shift. It's only active inside `<overflow-menu>` because I wanted
to avoid changing HTML and doing it in regular JS would cause a flicker.
I don't expect us to introduce other similar menus without
`<overflow-menu>`, so that place is likely fine.

![after](https://github.com/go-gitea/gitea/assets/115237/d6089924-8de6-4ee0-8db4-15f16069a131)

I also changed the weight from 500 to 600, slightly reduced horizontal
padding, merged some tab-bar related CSS rules and a added a small
margin below repo-header so it does not look so crammed against the
buttons on top.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>

---

Conflict resolution: Moved an `:focus` selector to the new CSS rule.
Ref: https://codeberg.org/forgejo/forgejo/issues/2776
(cherry picked from commit 99d7ef50917e8d61798715e1b0b3dc1a99709f27)
---
 web_src/css/base.css                      | 32 +++++++++++++++--------
 web_src/css/repo/header.css               |  1 +
 web_src/js/webcomponents/overflow-menu.js | 19 ++++++++++++++
 3 files changed, 41 insertions(+), 11 deletions(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index f78fd920e5..67716b273e 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1842,15 +1842,6 @@ table th[data-sortt-desc] .svg {
   border-color: var(--color-secondary);
 }
 
-.ui.tabular.menu .item {
-  padding: 11px 12px;
-  color: var(--color-text-light-2);
-}
-
-.ui.tabular.menu .item:hover {
-  color: var(--color-text);
-}
-
 .ui.tabular.menu .active.item,
 .ui.tabular.menu .active.item:hover {
   background: var(--color-body);
@@ -1867,17 +1858,36 @@ table th[data-sortt-desc] .svg {
   border-color: var(--color-secondary);
 }
 
+.ui.tabular.menu .item,
 .ui.secondary.pointing.menu .item {
+  padding: 11px 12px !important;
   color: var(--color-text-light-2);
 }
 
+.ui.tabular.menu .item:hover,
+.ui.secondary.pointing.menu a.item:hover, .ui.secondary.pointing.menu a.item:focus {
+  color: var(--color-text);
+}
+
 .ui.secondary.pointing.menu .active.item,
 .ui.secondary.pointing.menu .active.item:hover, .ui.secondary.pointing.menu .active.item:focus,
-.ui.secondary.pointing.menu .dropdown.item:hover, .ui.secondary.pointing.menu .dropdown.item:focus,
-.ui.secondary.pointing.menu a.item:hover, .ui.secondary.pointing.menu a.item:focus {
+.ui.secondary.pointing.menu .dropdown.item:hover, .ui.secondary.pointing.menu .dropdown.item:focus {
   color: var(--color-text-dark);
 }
 
+.ui.tabular.menu .active.item,
+.ui.secondary.pointing.menu .active.item,
+.resize-for-semibold::before {
+  font-weight: var(--font-weight-semibold);
+}
+
+.resize-for-semibold::before {
+  content: attr(data-text);
+  visibility: hidden;
+  display: block;
+  height: 0;
+}
+
 .ui.header {
   color: var(--color-text);
 }
diff --git a/web_src/css/repo/header.css b/web_src/css/repo/header.css
index 0a2c0c82ad..13fb40e35d 100644
--- a/web_src/css/repo/header.css
+++ b/web_src/css/repo/header.css
@@ -8,6 +8,7 @@
   flex-flow: row wrap;
   justify-content: space-between;
   gap: 0.5rem;
+  margin-bottom: 4px;
 }
 
 .repo-header .flex-item {
diff --git a/web_src/js/webcomponents/overflow-menu.js b/web_src/js/webcomponents/overflow-menu.js
index 9fa4585567..604fce7d4b 100644
--- a/web_src/js/webcomponents/overflow-menu.js
+++ b/web_src/js/webcomponents/overflow-menu.js
@@ -127,6 +127,25 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
   });
 
   init() {
+    // for horizontal menus where fomantic boldens active items, prevent this bold text from
+    // enlarging the menu's active item replacing the text node with a div that renders a
+    // invisible pseudo-element that enlarges the box.
+    if (this.matches('.ui.secondary.pointing.menu, .ui.tabular.menu')) {
+      for (const item of this.querySelectorAll('.item')) {
+        for (const child of item.childNodes) {
+          if (child.nodeType === Node.TEXT_NODE) {
+            const text = child.textContent.trim(); // whitespace is insignificant inside flexbox
+            if (!text) continue;
+            const span = document.createElement('span');
+            span.classList.add('resize-for-semibold');
+            span.setAttribute('data-text', text);
+            span.textContent = text;
+            child.replaceWith(span);
+          }
+        }
+      }
+    }
+
     // ResizeObserver triggers on initial render, so we don't manually call `updateItems` here which
     // also avoids a full-page FOUC in Firefox that happens when `updateItems` is called too soon.
     this.resizeObserver = new ResizeObserver((entries) => {