Add a menu class in order to be used in the new UI for the merge feature

This commit is contained in:
Calixte Denizet 2025-12-04 19:33:06 +01:00 committed by calixteman
parent 615965f3d9
commit 4c6cc0a042
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
3 changed files with 394 additions and 0 deletions

177
web/menu.css Normal file
View File

@ -0,0 +1,177 @@
/* Copyright 2025 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.popupMenu {
--menuitem-checkmark-icon: url(images/checkmark.svg);
--menu-mark-icon-size: 0;
--menu-icon-size: 16px;
--menuitem-gap: 5px;
--menuitem-border-color: transparent;
--menuitem-active-bg: color-mix(
in srgb,
var(--menu-text-color),
transparent 79%
);
--menuitem-text-active-fg: var(--menu-text-color);
--menuitem-focus-bg: color-mix(
in srgb,
var(--menu-text-color),
transparent 93%
);
--menuitem-focus-outline-color: light-dark(#0062fa, #00cadb);
--menuitem-focus-border-color: light-dark(white, black);
@media screen and (forced-colors: active) {
--menu-bg: Canvas;
--menu-background-blend-mode: normal;
--menu-box-shadow: none;
--menu-backdrop-filter: none;
--menu-text-color: ButtonText;
--menu-border-color: CanvasText;
--menuitem-border-color: none;
--menuitem-hover-bg: SelectedItemText;
--menuitem-text-hover-fg: SelectedItem;
--menuitem-active-bg: SelectedItemText;
--menuitem-text-active-fg: SelectedItem;
--menuitem-focus-outline-color: CanvasText;
--menuitem-focus-border-color: none;
}
display: flex;
flex-direction: column;
width: max-content;
height: auto;
position: relative;
left: 0;
top: 1px;
margin: 0;
padding: 5px;
background: var(--menu-bg);
background-blend-mode: var(--menu-background-blend-mode);
box-shadow: var(--menu-box-shadow);
border-radius: 6px;
border: 1px solid var(--menu-border-color);
backdrop-filter: var(--menu-backdrop-filter);
&.withMark {
--menu-mark-icon-size: 16px;
}
> li {
display: flex;
align-items: center;
list-style: none;
width: 100%;
height: 24px;
padding-inline: calc(var(--menu-mark-icon-size) + var(--menuitem-gap))
var(--menuitem-gap);
gap: var(--menuitem-gap);
box-sizing: border-box;
border-radius: var(--menuitem-border-radius);
border: 1px solid var(--menuitem-border-color);
background: transparent;
&:has(button.selected)::before {
content: "";
display: inline-block;
width: 11px;
height: 11px;
mask-repeat: no-repeat;
mask-position: center;
mask-image: var(--menuitem-checkmark-icon);
background-color: var(--menu-text-color);
position: absolute;
margin-left: -16px;
}
&:has(button:disabled) {
opacity: 0.62;
pointer-events: none;
}
&:hover {
background: var(--menuitem-hover-bg);
background-blend-mode: var(--menuitem-hover-background-blend-mode);
> button {
&:not(.noIcon)::before {
background-color: var(--menuitem-text-hover-fg);
}
> span {
color: var(--menuitem-text-hover-fg);
}
}
&:has(button.selected)::before {
background-color: var(--menuitem-text-hover-fg);
}
}
&:active {
background-color: var(--menuitem-active-bg);
> button > span {
color: var(--menuitem-text-active-fg);
}
}
&:has(> button:focus-visible) {
border-color: var(--menuitem-focus-border-color);
background-color: var(--menuitem-focus-bg);
outline: 2px solid var(--menuitem-focus-outline-color);
outline-offset: 2px;
}
> button {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
height: auto;
padding: var(--menuitem-gap);
gap: var(--menuitem-gap);
background: transparent;
border: none;
&:not(.noIcon)::before {
display: inline-block;
width: var(--menu-icon-size);
height: var(--menu-icon-size);
content: "";
mask-size: cover;
mask-position: center;
background-color: var(--menu-text-color);
}
&:focus-visible {
outline: none;
}
> span {
display: inline-block;
width: max-content;
height: auto;
text-align: left;
color: var(--menu-text-color);
user-select: none;
padding-inline-start: 6px;
font: menu;
font-size: 13px;
font-style: normal;
font-weight: 510;
line-height: normal;
}
}
}
}

216
web/menu.js Normal file
View File

@ -0,0 +1,216 @@
/* Copyright 2025 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { noContextMenu, stopEvent } from "pdfjs-lib";
class Menu {
#triggeringButton;
#menu;
#menuItems;
#openMenuAC = null;
#menuAC = new AbortController();
#lastIndex = -1;
/**
* Create a menu for the given button.
* @param {HTMLElement} menuContainer
* @param {HTMLElement} triggeringButton
* @param {Array<HTMLElement>|null} menuItems
*/
constructor(menuContainer, triggeringButton, menuItems) {
this.#menu = menuContainer;
this.#triggeringButton = triggeringButton;
if (Array.isArray(menuItems)) {
this.#menuItems = menuItems;
} else {
this.#menuItems = [];
for (const button of this.#menu.querySelectorAll("button")) {
this.#menuItems.push(button);
}
}
this.#setUpMenu();
}
/**
* Close the menu.
*/
#closeMenu() {
if (!this.#openMenuAC) {
return;
}
const menu = this.#menu;
menu.classList.toggle("hidden", true);
this.#triggeringButton.ariaExpanded = "false";
this.#openMenuAC.abort();
this.#openMenuAC = null;
if (menu.contains(document.activeElement)) {
// If the menu is closed while focused, focus the actions button.
setTimeout(() => {
if (!menu.contains(document.activeElement)) {
this.#triggeringButton.focus();
}
}, 0);
}
this.#lastIndex = -1;
}
/**
* Set up the menu.
*/
#setUpMenu() {
this.#triggeringButton.addEventListener("click", e => {
if (this.#openMenuAC) {
this.#closeMenu();
return;
}
const menu = this.#menu;
menu.classList.toggle("hidden", false);
this.#triggeringButton.ariaExpanded = "true";
this.#openMenuAC = new AbortController();
const signal = AbortSignal.any([
this.#menuAC.signal,
this.#openMenuAC.signal,
]);
window.addEventListener(
"pointerdown",
({ target }) => {
if (target !== this.#triggeringButton && !menu.contains(target)) {
this.#closeMenu();
}
},
{ signal }
);
window.addEventListener("blur", this.#closeMenu.bind(this), { signal });
});
const { signal } = this.#menuAC;
this.#menu.addEventListener(
"keydown",
e => {
switch (e.key) {
case "Escape":
this.#closeMenu();
stopEvent(e);
break;
case "ArrowDown":
case "Tab":
this.#goToNextItem(e.target, true);
stopEvent(e);
break;
case "ArrowUp":
case "ShiftTab":
this.#goToNextItem(e.target, false);
stopEvent(e);
break;
case "Home":
this.#menuItems
.find(
item => !item.disabled && !item.classList.contains("hidden")
)
.focus();
stopEvent(e);
break;
case "End":
this.#menuItems
.findLast(
item => !item.disabled && !item.classList.contains("hidden")
)
.focus();
stopEvent(e);
break;
}
},
{ signal, capture: true }
);
this.#menu.addEventListener("contextmenu", noContextMenu, { signal });
this.#menu.addEventListener("click", this.#closeMenu.bind(this), {
signal,
capture: true,
});
this.#triggeringButton.addEventListener(
"keydown",
ev => {
if (!this.#openMenuAC) {
return;
}
switch (ev.key) {
case "ArrowDown":
case "Home":
this.#menuItems
.find(
item => !item.disabled && !item.classList.contains("hidden")
)
.focus();
stopEvent(ev);
break;
case "ArrowUp":
case "End":
this.#menuItems
.findLast(
item => !item.disabled && !item.classList.contains("hidden")
)
.focus();
stopEvent(ev);
break;
case "Escape":
this.#closeMenu();
stopEvent(ev);
}
},
{ signal }
);
}
/**
* Go to the next/previous menu item.
* @param {HTMLElement} element
* @param {boolean} forward
*/
#goToNextItem(element, forward) {
const index =
this.#lastIndex === -1
? this.#menuItems.indexOf(element)
: this.#lastIndex;
const len = this.#menuItems.length;
const increment = forward ? 1 : len - 1;
for (
let i = (index + increment) % len;
i !== index;
i = (i + increment) % len
) {
const menuItem = this.#menuItems[i];
if (!menuItem.disabled && !menuItem.classList.contains("hidden")) {
menuItem.focus();
this.#lastIndex = i;
break;
}
}
}
destroy() {
this.#closeMenu();
this.#menuAC?.abort();
this.#menuAC = null;
}
}
export { Menu };

View File

@ -20,6 +20,7 @@
/* Ignored in GECKOVIEW builds: */
@import url(annotation_editor_layer_builder.css);
@import url(sidebar.css);
@import url(menu.css);
:root {
color-scheme: light dark;