pdf.js.mirror/web/sidebar.js
Calixte Denizet 956c2a051a
Fix the sidebar resizer accessibility
It's width was a bit wrong because of its box-sizing property and it was causing some issues when resizing it with the keyboard.
And for the thumbnails sidebar, the tabindex was missing and some aria properties too.
2026-01-27 21:41:11 +01:00

220 lines
6.1 KiB
JavaScript

/* 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";
// Timeout before ending resize operation.
const RESIZE_TIMEOUT = 400; // ms
/**
* Viewer control to display a sidebar with resizer functionality.
*/
class Sidebar {
#initialWidth = 0;
#width = 0;
#coefficient;
#resizeTimeout = null;
#resizer;
#isResizerOnTheLeft;
#isKeyboardResizing = false;
#resizeObserver;
#prevX = 0;
/**
* @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;
this.#resizer = resizer;
this.#isResizerOnTheLeft = isResizerOnTheLeft;
const style = window.getComputedStyle(sidebar);
this.#initialWidth = this.#width = parseFloat(
style.getPropertyValue("--sidebar-width")
);
resizer.ariaValueMin =
parseFloat(style.getPropertyValue("--sidebar-min-width")) || 0;
resizer.ariaValueMax =
parseFloat(style.getPropertyValue("--sidebar-max-width")) || Infinity;
resizer.ariaValueNow = this.#width;
this.#makeSidebarResizable();
toggleButton.addEventListener("click", this.toggle.bind(this));
this._isOpen = false;
sidebar.hidden = true;
this.#resizeObserver = new ResizeObserver(
([
{
borderBoxSize: [{ inlineSize }],
},
]) => {
if (!isNaN(this.#prevX)) {
this.#prevX += this.#coefficient * (inlineSize - this.#width);
}
this.#setWidth(inlineSize);
}
);
this.#resizeObserver.observe(sidebar);
}
#makeSidebarResizable() {
const sidebarStyle = this._sidebar.style;
let pointerMoveAC;
const cancelResize = () => {
this.#resizeTimeout = null;
this._sidebar.classList.remove("resizing");
pointerMoveAC?.abort();
pointerMoveAC = null;
this.#isKeyboardResizing = false;
this.onStopResizing();
this.#prevX = NaN;
};
this.#resizer.addEventListener("pointerdown", e => {
if (pointerMoveAC) {
cancelResize();
return;
}
this.onStartResizing();
const { clientX } = e;
stopEvent(e);
this.#prevX = clientX;
pointerMoveAC = new AbortController();
const { signal } = pointerMoveAC;
const sidebar = this._sidebar;
sidebar.classList.add("resizing");
const parentStyle = sidebar.parentElement.style;
parentStyle.minWidth = 0;
window.addEventListener("contextmenu", noContextMenu, { signal });
window.addEventListener(
"pointermove",
ev => {
if (!pointerMoveAC || Math.abs(ev.clientX - this.#prevX) < 1) {
return;
}
stopEvent(ev);
sidebarStyle.width = `${Math.round(this.#width + this.#coefficient * (ev.clientX - this.#prevX))}px`;
},
{ signal, capture: true }
);
window.addEventListener("blur", cancelResize, { signal });
window.addEventListener(
"pointerup",
ev => {
if (pointerMoveAC) {
cancelResize();
stopEvent(ev);
}
},
{ signal }
);
});
this.#resizer.addEventListener("keydown", e => {
const { key } = e;
const isArrowLeft = key === "ArrowLeft";
if (isArrowLeft || key === "ArrowRight") {
if (!this.#isKeyboardResizing) {
this._sidebar.classList.add("resizing");
this.#isKeyboardResizing = true;
this.onStartResizing();
}
const base = e.ctrlKey || e.metaKey ? 10 : 1;
const dx = base * (isArrowLeft ? -1 : 1);
clearTimeout(this.#resizeTimeout);
this.#resizeTimeout = setTimeout(cancelResize, RESIZE_TIMEOUT);
sidebarStyle.width = `${Math.round(this.#width + this.#coefficient * dx)}px`;
stopEvent(e);
}
});
}
#setWidth(newWidth) {
this.#width = newWidth;
this.#resizer.ariaValueNow = Math.round(newWidth);
if (this.#isResizerOnTheLeft) {
this._sidebar.parentElement.style.insetInlineStart = `${(this.#initialWidth - newWidth).toFixed(3)}px`;
}
this.onResizing(newWidth);
}
/**
* Get the current width of the sidebar in pixels.
* @returns {number}
*/
get width() {
return this.#width;
}
/**
* Set the width of the sidebar in pixels.
* @param {number} newWidth
*/
set width(newWidth) {
this._sidebar.style.width = `${newWidth}px`;
}
/**
* Callback to be executed when the user starts resizing the sidebar.
*/
onStartResizing() {}
/**
* Callback to be executed when the user stops resizing the sidebar.
*/
onStopResizing() {}
/**
* Callback to be executed when the sidebar is being resized.
* @param {number} newWidth - The new width of the sidebar in pixels.
*/
onResizing(_newWidth) {}
/**
* Toggle the sidebar's visibility.
* @param {boolean} [visibility] - The visibility state to set.
*/
toggle(visibility = !this._isOpen) {
this._sidebar.hidden = !(this._isOpen = visibility);
}
destroy() {
this.#resizeObserver?.disconnect();
this.#resizeObserver = null;
}
}
export { Sidebar };