Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/tabView.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function TabView({ id, disableSwipe = false }, children) {
if (!activeRect.width) return;
const targetLeft = activeRect.left - optionsRect.left;
const targetWidth = activeRect.width;
const targetTransform = `translate3d(${targetLeft}px, 0, 0)`;
const targetTransform = `translateX(${targetLeft}px)`;
$indicator.style.width = `${targetWidth}px`;
if (document.body.classList.contains("no-animation")) {
$indicator.style.transform = targetTransform;
Expand Down
50 changes: 43 additions & 7 deletions src/components/tooltip/index.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,78 @@
import "./style.scss";
import { animate } from "motion";
import { installIconTooltips } from "./longPress";

let tooltip;
let rafId = null;
let animation;
let icons;

export function initIconTooltips() {
icons ||= installIconTooltips(document, showTooltip, hideTooltip);
return icons.dismiss;
}

function createTooltip() {
if (tooltip) return tooltip;

tooltip = document.createElement("div");
tooltip.className = "acode-tooltip";
tooltip.setAttribute("role", "tooltip");
document.body.appendChild(tooltip);

return tooltip;
}

export function showTooltip(target, text) {
export function showTooltip(target, text, description) {
if (!target || !text) return;

const $tooltip = createTooltip();

$tooltip.textContent = text;
animation?.stop();
$tooltip.replaceChildren();
const label = document.createElement("div");
label.textContent = text;
$tooltip.append(label);
if (description) {
const detail = document.createElement("div");
detail.className = "acode-tooltip-description";
detail.textContent = description;
$tooltip.append(detail);
}
$tooltip.removeAttribute("aria-hidden");

const rect = target.getBoundingClientRect();

if (rafId !== null) {
cancelAnimationFrame(rafId);
}
rafId = requestAnimationFrame(() => {
const viewport = window.visualViewport;
const x = viewport?.offsetLeft || 0,
y = viewport?.offsetTop || 0;
const visibleWidth = viewport?.width || window.innerWidth;
const visibleHeight = viewport?.height || window.innerHeight;
$tooltip.style.maxWidth = `${Math.max(0, visibleWidth - 16)}px`;
$tooltip.style.maxHeight = `${Math.max(0, visibleHeight - 16)}px`;
const width = $tooltip.offsetWidth;
const height = $tooltip.offsetHeight;

const left = Math.max(
8,
x + 8,
Math.min(
window.innerWidth - width - 8,
x + visibleWidth - width - 8,
rect.left + rect.width / 2 - width / 2,
),
);

const top = Math.max(8, rect.top - height - 10);
const above = rect.top - height - 10;
const top = Math.max(
y + 8,
Math.min(
y + visibleHeight - height - 8,
above >= y + 8 ? above : rect.bottom + 10,
),
);

$tooltip.style.left = `${left}px`;
$tooltip.style.top = `${top}px`;
Expand All @@ -48,7 +82,7 @@ export function showTooltip(target, text) {
rafId = null;
return;
}
animate(
animation = animate(
$tooltip,
{
opacity: 1,
Expand All @@ -64,6 +98,8 @@ export function showTooltip(target, text) {

export function hideTooltip() {
if (!tooltip) return;
animation?.stop();
tooltip.setAttribute("aria-hidden", "true");

if (rafId !== null) {
cancelAnimationFrame(rafId);
Expand All @@ -74,7 +110,7 @@ export function hideTooltip() {
tooltip.style.transform = "translateY(5px)";
return;
}
animate(
animation = animate(
tooltip,
{
opacity: 0,
Expand Down
126 changes: 126 additions & 0 deletions src/components/tooltip/longPress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/** One delegated listener set, including icon controls inside open ShadowRoots. */
export function installIconTooltips(root, show, hide) {
let press, timer, suppressed, observer;
let shadowRoots = [];
const pointers = new Set();
const labelOf = (node) =>
["data-label", "aria-label", "title"]
.map((name) => node.getAttribute(name)?.trim())
.find(Boolean);
const targetOf = (event) => {
const path = event.composedPath();
// Quicktools already own their tooltip, repeat and modifier gestures.
if (path.some((node) => node?.id === "quick-tools")) return;
return path.find(
(node) =>
node?.matches?.("button, [role='button'], .icon") && labelOf(node),
);
};
function dismiss() {
clearTimeout(timer);
press = undefined;
observer?.disconnect();
observer = undefined;
for (const shadow of shadowRoots)
shadow.removeEventListener("scroll", dismiss, true);
shadowRoots = [];
hide();
}
function down(event) {
pointers.add(event.pointerId);
dismiss();
suppressed = undefined;
if (pointers.size !== 1 || event.button !== 0) return;
const target = targetOf(event);
if (!target) return;
// Element scroll events do not cross a shadow boundary.
shadowRoots = event
.composedPath()
.filter((node) => node?.nodeType === 11 && node.host);
for (const shadow of shadowRoots)
shadow.addEventListener("scroll", dismiss, true);
press = { target, x: event.clientX, y: event.clientY, held: false };
timer = setTimeout(() => {
if (!press || !target.isConnected) return;
press.held = true;
show(target, labelOf(target), target.dataset.description);
observer = new MutationObserver(() => {
if (!target.isConnected) dismiss();
});
// Also observe removal of the tab's host, including nested shadow hosts.
for (const boundary of [root, ...shadowRoots])
observer.observe(boundary, { childList: true, subtree: true });
}, 500);
}
function move(event) {
if (
press &&
Math.hypot(event.clientX - press.x, event.clientY - press.y) > 10
)
dismiss();
}
function up(event) {
pointers.delete(event.pointerId);
clearTimeout(timer);
if (press?.held)
suppressed = { target: press.target, until: Date.now() + 750 };
press = undefined;
}
function cancel(event) {
pointers.delete(event.pointerId);
dismiss();
}
function click(event) {
if (
event.detail &&
suppressed &&
suppressed.target === targetOf(event) &&
Date.now() < suppressed.until
) {
event.preventDefault();
event.stopImmediatePropagation();
}
suppressed = undefined;
}
function contextmenu(event) {
// A control's own menu wins over the shared tooltip.
if (event.defaultPrevented) {
dismiss();
return;
}
if (press?.held && press.target === targetOf(event)) event.preventDefault();
}
function reset() {
pointers.clear();
suppressed = undefined;
dismiss();
}
const events = {
pointerdown: down,
pointermove: move,
pointerup: up,
pointercancel: cancel,
click,
scroll: dismiss,
keydown: dismiss,
};
for (const [name, handler] of Object.entries(events))
root.addEventListener(name, handler, true);
root.addEventListener("contextmenu", contextmenu);
const viewport = root.defaultView?.visualViewport;
root.defaultView?.addEventListener("blur", reset);
viewport?.addEventListener("resize", dismiss);
viewport?.addEventListener("scroll", dismiss);
return {
dismiss: reset,
dispose() {
reset();
for (const [name, handler] of Object.entries(events))
root.removeEventListener(name, handler, true);
root.removeEventListener("contextmenu", contextmenu);
root.defaultView?.removeEventListener("blur", reset);
viewport?.removeEventListener("resize", dismiss);
viewport?.removeEventListener("scroll", dismiss);
},
};
}
22 changes: 16 additions & 6 deletions src/components/tooltip/style.scss
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
.acode-tooltip {
position: fixed;

padding: 6px 10px;
padding: 8px 12px;

background: var(--popup-background-color);
color: var(--popup-text-color);
border: 1px solid var(--popup-border-color);
box-shadow: 0 0 4px var(--box-shadow-color);
border: 1px solid
color-mix(in srgb, var(--popup-text-color) 30%, var(--popup-background-color));
box-shadow: 0 4px 12px var(--box-shadow-color);
border-radius: var(--popup-border-radius);

font-size: 12px;
font-size: 13px;
font-weight: 500;

white-space: nowrap;
white-space: normal;
overflow-wrap: anywhere;
overflow: hidden;
box-sizing: border-box;
line-height: 1.4;

pointer-events: none;

Expand All @@ -21,4 +27,8 @@

z-index: 999999;
}

.acode-tooltip-description {
margin-top: 3px;
font-size: 12px;
font-weight: 400;
}
3 changes: 3 additions & 0 deletions src/fileSystem/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export default function fsOperation(...url) {
return fsList.find((fs) => fs.test(url))?.fs(url);
}

// Check registration without constructing a transport or opening a file.
fsOperation.hasProvider = (url) => fsList.some((fs) => fs.test(url));

fsOperation.extend = (test, fs) => {
fsList.push({ test, fs });
};
Expand Down
Loading