Merge pull request #20467 from calixteman/make_sidebar

Create a sidebar object
This commit is contained in:
calixteman 2025-11-28 16:27:44 +01:00 committed by GitHub
commit 925fc3d8f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 252 additions and 85 deletions

View File

@ -22,6 +22,8 @@ import {
getEditorSelector, getEditorSelector,
getRect, getRect,
highlightSpan, highlightSpan,
kbModifierDown,
kbModifierUp,
loadAndWait, loadAndWait,
scrollIntoView, scrollIntoView,
selectEditor, selectEditor,
@ -546,6 +548,59 @@ describe("Comment", () => {
); );
}); });
it("must check that the comment sidebar is resizable with the keyboard", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToComment(page);
const sidebarSelector = "#editorCommentParamsToolbar";
const handle = await createPromise(page, resolve => {
document
.getElementById("editorCommentsSidebarResizer")
.addEventListener("focus", () => resolve(), { once: true });
});
await page.focus(`${sidebarSelector} #editorCommentsSidebarResizer`);
await awaitPromise(handle);
// Use Ctrl+ArrowLeft/Right to resize the sidebar.
for (const extraWidth of [10, -10]) {
const rect = await getRect(page, sidebarSelector);
const arrowKey = extraWidth > 0 ? "ArrowLeft" : "ArrowRight";
for (let i = 0; i < Math.abs(extraWidth); i++) {
await kbModifierDown(page);
await page.keyboard.press(arrowKey);
await kbModifierUp(page);
}
const rectAfter = await getRect(page, sidebarSelector);
expect(Math.abs(rectAfter.width - (rect.width + 10 * extraWidth)))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
expect(Math.abs(rectAfter.x - (rect.x - 10 * extraWidth)))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
}
// Use ArrowLeft/Right to resize the sidebar.
for (const extraWidth of [10, -10]) {
const rect = await getRect(page, sidebarSelector);
const arrowKey = extraWidth > 0 ? "ArrowLeft" : "ArrowRight";
for (let i = 0; i < Math.abs(extraWidth); i++) {
await page.keyboard.press(arrowKey);
}
const rectAfter = await getRect(page, sidebarSelector);
expect(Math.abs(rectAfter.width - (rect.width + extraWidth)))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
expect(Math.abs(rectAfter.x - (rect.x - extraWidth)))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
}
})
);
});
it("must check that comments are in chronological order", async () => { it("must check that comments are in chronological order", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {

View File

@ -27,6 +27,7 @@ import {
Util, Util,
} from "pdfjs-lib"; } from "pdfjs-lib";
import { binarySearchFirstItem } from "./ui_utils.js"; import { binarySearchFirstItem } from "./ui_utils.js";
import { Sidebar } from "./sidebar.js";
class CommentManager { class CommentManager {
#dialog; #dialog;
@ -141,7 +142,7 @@ class CommentManager {
} }
} }
class CommentSidebar { class CommentSidebar extends Sidebar {
#annotations = null; #annotations = null;
#eventBus; #eventBus;
@ -150,8 +151,6 @@ class CommentSidebar {
#boundCommentKeydown = this.#commentKeydown.bind(this); #boundCommentKeydown = this.#commentKeydown.bind(this);
#sidebar;
#closeButton; #closeButton;
#commentsList; #commentsList;
@ -174,16 +173,6 @@ class CommentSidebar {
#uiManager = null; #uiManager = null;
#minWidth = 0;
#maxWidth = 0;
#initialWidth = 0;
#width = 0;
#ltr;
constructor( constructor(
{ {
learnMoreUrl, learnMoreUrl,
@ -201,7 +190,11 @@ class CommentSidebar {
dateFormat, dateFormat,
ltr ltr
) { ) {
this.#sidebar = sidebar; super(
{ sidebar, resizer: sidebarResizer, toggleButton: commentToolbarButton },
ltr,
/* isResizerOnTheLeft = */ true
);
this.#sidebarTitle = sidebarTitle; this.#sidebarTitle = sidebarTitle;
this.#commentsList = commentsList; this.#commentsList = commentsList;
this.#commentCount = commentCount; this.#commentCount = commentCount;
@ -210,17 +203,8 @@ class CommentSidebar {
this.#closeButton = closeButton; this.#closeButton = closeButton;
this.#popup = popup; this.#popup = popup;
this.#dateFormat = dateFormat; this.#dateFormat = dateFormat;
this.#ltr = ltr;
this.#eventBus = eventBus; this.#eventBus = eventBus;
const style = window.getComputedStyle(sidebar);
this.#minWidth = parseFloat(style.getPropertyValue("--sidebar-min-width"));
this.#maxWidth = parseFloat(style.getPropertyValue("--sidebar-max-width"));
this.#initialWidth = this.#width = parseFloat(
style.getPropertyValue("--sidebar-width")
);
this.#makeSidebarResizable(sidebarResizer);
closeButton.addEventListener("click", () => { closeButton.addEventListener("click", () => {
eventBus.dispatch("switchannotationeditormode", { eventBus.dispatch("switchannotationeditormode", {
source: this, source: this,
@ -238,64 +222,6 @@ class CommentSidebar {
}; };
commentToolbarButton.addEventListener("keydown", keyDownCallback); commentToolbarButton.addEventListener("keydown", keyDownCallback);
sidebar.addEventListener("keydown", keyDownCallback); sidebar.addEventListener("keydown", keyDownCallback);
this.#sidebar.hidden = true;
}
#makeSidebarResizable(resizer) {
let pointerMoveAC;
const cancelResize = () => {
this.#width = MathClamp(this.#width, this.#minWidth, this.#maxWidth);
this.#sidebar.classList.remove("resizing");
pointerMoveAC?.abort();
pointerMoveAC = null;
};
resizer.addEventListener("pointerdown", e => {
if (pointerMoveAC) {
cancelResize();
return;
}
const { clientX } = e;
stopEvent(e);
let prevX = clientX;
pointerMoveAC = new AbortController();
const { signal } = pointerMoveAC;
const sign = this.#ltr ? -1 : 1;
const sidebar = this.#sidebar;
const sidebarStyle = sidebar.style;
sidebar.classList.add("resizing");
const parentStyle = sidebar.parentElement.style;
parentStyle.minWidth = 0;
window.addEventListener("contextmenu", noContextMenu, { signal });
window.addEventListener(
"pointermove",
ev => {
if (!pointerMoveAC) {
return;
}
stopEvent(ev);
const { clientX: x } = ev;
const newWidth = (this.#width += sign * (x - prevX));
prevX = x;
if (newWidth > this.#maxWidth || newWidth < this.#minWidth) {
return;
}
sidebarStyle.width = `${newWidth.toFixed(3)}px`;
parentStyle.insetInlineStart = `${(this.#initialWidth - newWidth).toFixed(3)}px`;
},
{ signal, capture: true }
);
window.addEventListener("blur", cancelResize, { signal });
window.addEventListener(
"pointerup",
ev => {
if (pointerMoveAC) {
cancelResize();
stopEvent(ev);
}
},
{ signal }
);
});
} }
setUIManager(uiManager) { setUIManager(uiManager) {
@ -318,7 +244,7 @@ class CommentSidebar {
} else { } else {
this.#setCommentsCount(); this.#setCommentsCount();
} }
this.#sidebar.hidden = false; this._sidebar.hidden = false;
this.#eventBus.dispatch("reporttelemetry", { this.#eventBus.dispatch("reporttelemetry", {
source: this, source: this,
details: { details: {
@ -329,7 +255,7 @@ class CommentSidebar {
} }
hide() { hide() {
this.#sidebar.hidden = true; this._sidebar.hidden = true;
this.#commentsList.replaceChildren(); this.#commentsList.replaceChildren();
this.#elementsToAnnotations = null; this.#elementsToAnnotations = null;
this.#idsToElements = null; this.#idsToElements = null;
@ -356,7 +282,7 @@ class CommentSidebar {
if (!element) { if (!element) {
return; return;
} }
this.#sidebar.scrollTop = element.offsetTop - this.#sidebar.offsetTop; this._sidebar.scrollTop = element.offsetTop - this._sidebar.offsetTop;
for (const el of this.#commentsList.children) { for (const el of this.#commentsList.children) {
el.classList.toggle("selected", el === element); el.classList.toggle("selected", el === element);
} }

View File

@ -22,6 +22,7 @@
--sidebar-box-shadow: --sidebar-box-shadow:
0 0.25px 0.75px light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)), 0 0.25px 0.75px light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)),
0 2px 6px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4)); 0 2px 6px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4));
--sidebar-backdrop-filter: none;
--sidebar-border-radius: 8px; --sidebar-border-radius: 8px;
--sidebar-padding: 5px; --sidebar-padding: 5px;
--sidebar-min-width: 180px; --sidebar-min-width: 180px;
@ -46,6 +47,7 @@
width: var(--sidebar-width); width: var(--sidebar-width);
min-width: var(--sidebar-min-width); min-width: var(--sidebar-min-width);
max-width: var(--sidebar-max-width); max-width: var(--sidebar-max-width);
backdrop-filter: var(--sidebar-backdrop-filter);
.sidebarResizer { .sidebarResizer {
width: var(--resizer-width); width: var(--resizer-width);
@ -64,6 +66,10 @@
&:hover { &:hover {
background-color: var(--resizer-hover-bg-color); background-color: var(--resizer-hover-bg-color);
} }
&:focus-visible {
background-color: var(--resizer-hover-bg-color);
outline: none;
}
} }
&.resizing { &.resizing {

180
web/sidebar.js Normal file
View File

@ -0,0 +1,180 @@
/* 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 { MathClamp, noContextMenu, stopEvent } from "pdfjs-lib";
/**
* Viewer control to display a sidebar with resizer functionality.
*/
class Sidebar {
#minWidth = 0;
#maxWidth = 0;
#initialWidth = 0;
#width = 0;
#coefficient;
#visible = false;
/**
* @typedef {Object} SidebarElements
* @property {HTMLElement} sidebar - The sidebar element.
* @property {HTMLElement} resizer - The sidebar resizer element.
* @property {HTMLElement} toggleButton - The button used to toggle the
* sidebar.
*/
/**
* Create a sidebar with resizer functionality.
* @param {SidebarElements} sidebarElements
* @param {boolean} ltr
* @param {boolean} isResizerOnTheLeft
*/
constructor({ sidebar, resizer, toggleButton }, ltr, isResizerOnTheLeft) {
this._sidebar = sidebar;
this.#coefficient = ltr === isResizerOnTheLeft ? -1 : 1;
const style = window.getComputedStyle(sidebar);
this.#minWidth = parseFloat(style.getPropertyValue("--sidebar-min-width"));
this.#maxWidth = parseFloat(style.getPropertyValue("--sidebar-max-width"));
this.#initialWidth = this.#width = parseFloat(
style.getPropertyValue("--sidebar-width")
);
this.#makeSidebarResizable(resizer, isResizerOnTheLeft);
toggleButton.addEventListener("click", this.toggle.bind(this));
sidebar.hidden = true;
}
#makeSidebarResizable(resizer, isResizerOnTheLeft) {
resizer.ariaValueMin = this.#minWidth;
resizer.ariaValueMax = this.#maxWidth;
resizer.ariaValueNow = this.#width;
let pointerMoveAC;
const cancelResize = () => {
this.#width = MathClamp(this.#width, this.#minWidth, this.#maxWidth);
this._sidebar.classList.remove("resizing");
pointerMoveAC?.abort();
pointerMoveAC = null;
};
resizer.addEventListener("pointerdown", e => {
if (pointerMoveAC) {
cancelResize();
return;
}
const { clientX } = e;
stopEvent(e);
let prevX = clientX;
pointerMoveAC = new AbortController();
const { signal } = pointerMoveAC;
const sidebar = this._sidebar;
const sidebarStyle = sidebar.style;
sidebar.classList.add("resizing");
const parentStyle = sidebar.parentElement.style;
parentStyle.minWidth = 0;
window.addEventListener("contextmenu", noContextMenu, { signal });
window.addEventListener(
"pointermove",
ev => {
if (!pointerMoveAC) {
return;
}
stopEvent(ev);
const { clientX: x } = ev;
this.#setNewWidth(
x - prevX,
parentStyle,
resizer,
sidebarStyle,
isResizerOnTheLeft,
/* isFromKeyboard */ false
);
prevX = x;
},
{ signal, capture: true }
);
window.addEventListener("blur", cancelResize, { signal });
window.addEventListener(
"pointerup",
ev => {
if (pointerMoveAC) {
cancelResize();
stopEvent(ev);
}
},
{ signal }
);
});
resizer.addEventListener("keydown", e => {
const { key } = e;
const isArrowLeft = key === "ArrowLeft";
if (isArrowLeft || key === "ArrowRight") {
const base = e.ctrlKey || e.metaKey ? 10 : 1;
const dx = base * (isArrowLeft ? -1 : 1);
this.#setNewWidth(
dx,
this._sidebar.parentElement.style,
resizer,
this._sidebar.style,
isResizerOnTheLeft,
/* isFromKeyboard */ true
);
stopEvent(e);
}
});
}
#setNewWidth(
dx,
parentStyle,
resizer,
sidebarStyle,
isResizerOnTheLeft,
isFromKeyboard
) {
let newWidth = this.#width + this.#coefficient * dx;
if (!isFromKeyboard) {
this.#width = newWidth;
}
if (
(newWidth > this.#maxWidth || newWidth < this.#minWidth) &&
(this.#width === this.#maxWidth || this.#width === this.#minWidth)
) {
return;
}
newWidth = MathClamp(newWidth, this.#minWidth, this.#maxWidth);
if (isFromKeyboard) {
this.#width = newWidth;
}
resizer.ariaValueNow = Math.round(newWidth);
sidebarStyle.width = `${newWidth.toFixed(3)}px`;
if (isResizerOnTheLeft) {
parentStyle.insetInlineStart = `${(this.#initialWidth - newWidth).toFixed(3)}px`;
}
}
/**
* Toggle the sidebar's visibility.
*/
toggle() {
this._sidebar.hidden = !(this.#visible = !this.#visible);
}
}
export { Sidebar };

View File

@ -335,7 +335,7 @@ See https://github.com/adobe-type-tools/cmap-resources
</button> </button>
<div class="editorParamsToolbar hidden menu" id="editorCommentParamsToolbar"> <div class="editorParamsToolbar hidden menu" id="editorCommentParamsToolbar">
<div id="editorCommentsSidebar" class="menuContainer comment sidebar" role="landmark" aria-labelledby="editorCommentsSidebarHeader"> <div id="editorCommentsSidebar" class="menuContainer comment sidebar" role="landmark" aria-labelledby="editorCommentsSidebarHeader">
<div id="editorCommentsSidebarResizer" class="sidebarResizer"></div> <div id="editorCommentsSidebarResizer" class="sidebarResizer" role="separator" aria-controls="editorCommentsSidebar" tabindex="0"></div>
<div id="editorCommentsSidebarHeader" role="heading" aria-level="2"> <div id="editorCommentsSidebarHeader" role="heading" aria-level="2">
<span class="commentCount"> <span class="commentCount">
<span id="editorCommentsSidebarTitle" data-l10n-id="pdfjs-editor-comments-sidebar-title" data-l10n-args='{ "count": 0 }'></span> <span id="editorCommentsSidebarTitle" data-l10n-id="pdfjs-editor-comments-sidebar-title" data-l10n-args='{ "count": 0 }'></span>