fix: hide popup triangle when inside popup + EPUB image-only paragraph rendering (#4121)
- Popup: hide the inner triangle when its anchor point lands inside the popup body. Extracted as a generic `isPointInRect` helper in `sel.ts` (with a default 1px padding so edge cases stay visible). - style.ts: handle `<p[width][height]><img></p>` (common in some MOBI conversions) — clear hardcoded width/height and apply multiply blend for dark themes so the image doesn't sit on a colored box. - Annotator: shrink dict popup height from 480 to 360 to fit smaller screens. - foliate-js: submodule bump. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,39 @@ import type { Rect, Position } from '@/utils/sel';
|
||||
// constrainPointWithinRect is also non-exported but exercised through getPosition.
|
||||
|
||||
// For getPopupPosition we can test directly.
|
||||
import { getPopupPosition } from '@/utils/sel';
|
||||
import { getPopupPosition, isPointInRect } from '@/utils/sel';
|
||||
|
||||
describe('sel utilities', () => {
|
||||
describe('isPointInRect', () => {
|
||||
const rect: Rect = { left: 100, top: 50, right: 300, bottom: 200 };
|
||||
|
||||
it('returns true for a point strictly inside the rect', () => {
|
||||
expect(isPointInRect({ x: 200, y: 100 }, rect)).toBe(true);
|
||||
});
|
||||
|
||||
it('treats edges as outside with the default 1px padding', () => {
|
||||
expect(isPointInRect({ x: 100, y: 100 }, rect)).toBe(false);
|
||||
expect(isPointInRect({ x: 300, y: 100 }, rect)).toBe(false);
|
||||
expect(isPointInRect({ x: 200, y: 50 }, rect)).toBe(false);
|
||||
expect(isPointInRect({ x: 200, y: 200 }, rect)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true on edges when padding is 0', () => {
|
||||
expect(isPointInRect({ x: 100, y: 50 }, rect, 0)).toBe(true);
|
||||
expect(isPointInRect({ x: 300, y: 200 }, rect, 0)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when x is outside', () => {
|
||||
expect(isPointInRect({ x: 99, y: 100 }, rect)).toBe(false);
|
||||
expect(isPointInRect({ x: 301, y: 100 }, rect)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when y is outside', () => {
|
||||
expect(isPointInRect({ x: 200, y: 49 }, rect)).toBe(false);
|
||||
expect(isPointInRect({ x: 200, y: 201 }, rect)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPopupPosition', () => {
|
||||
const boundingRect: Rect = { top: 0, right: 800, bottom: 600, left: 0 };
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
// Tall enough to fit a header + 2-3 expanded cards comfortably. The popup
|
||||
// shows all enabled providers stacked (no tabs) so it needs more vertical
|
||||
// room than the legacy single-tab layout.
|
||||
const dictPopupHeight = Math.min(480, maxHeight);
|
||||
const dictPopupHeight = Math.min(360, maxHeight);
|
||||
const transPopupWidth = Math.min(480, maxWidth);
|
||||
const transPopupHeight = Math.min(265, maxHeight);
|
||||
const proofreadPopupWidth = Math.min(440, maxWidth);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import { Position } from '@/utils/sel';
|
||||
import { Position, isPointInRect } from '@/utils/sel';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { useKeyDownActions } from '@/hooks/useKeyDownActions';
|
||||
@@ -128,6 +128,18 @@ const Popup = ({
|
||||
const outerTriangleStyles = getTriangleStyles(trianglePosition, 7, 0);
|
||||
const innerTriangleStyles = getTriangleStyles(trianglePosition, 7, -1);
|
||||
|
||||
const popupHeight = height ?? childrenHeight;
|
||||
const triangleHidden = !!(
|
||||
adjustedPosition &&
|
||||
trianglePosition &&
|
||||
isPointInRect(trianglePosition.point, {
|
||||
left: adjustedPosition.point.x,
|
||||
top: adjustedPosition.point.y,
|
||||
right: adjustedPosition.point.x + width,
|
||||
bottom: adjustedPosition.point.y + popupHeight,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
@@ -156,7 +168,11 @@ const Popup = ({
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
className={clsx('popup-triangle-inner text-base-300 absolute z-50', triangleClassName)}
|
||||
className={clsx(
|
||||
'popup-triangle-inner text-base-300 absolute',
|
||||
triangleHidden ? 'z-10' : 'z-50',
|
||||
triangleClassName,
|
||||
)}
|
||||
style={innerTriangleStyles}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -75,6 +75,15 @@ const constrainPointWithinRect = (point: Point, rect: Rect, padding: number) =>
|
||||
};
|
||||
};
|
||||
|
||||
export const isPointInRect = (point: Point, rect: Rect, padding: number = 1): boolean => {
|
||||
return (
|
||||
point.x >= rect.left + padding &&
|
||||
point.x <= rect.right - padding &&
|
||||
point.y >= rect.top + padding &&
|
||||
point.y <= rect.bottom - padding
|
||||
);
|
||||
};
|
||||
|
||||
export const isPointerInsideSelection = (selection: Selection, ev: PointerEvent) => {
|
||||
if (selection.rangeCount === 0) return false;
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
@@ -182,6 +182,9 @@ const getColorStyles = (
|
||||
hr.background-img {
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
p[width][height] > img:only-child {
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
/* inline images */
|
||||
*:has(> img.has-text-siblings):not(body) {
|
||||
${overrideColor ? `background-color: ${bg};` : ''}
|
||||
@@ -349,6 +352,11 @@ const getPageLayoutStyles = (
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
/* some mobi */
|
||||
p[width][height] > img:only-child {
|
||||
width: unset !important;
|
||||
height: unset !important;
|
||||
}
|
||||
|
||||
/* page break */
|
||||
body.paginated-mode div[style*="page-break-after: always"],
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: befe16d47c...a7ecb05a83
Reference in New Issue
Block a user