From f47094fed3474d3eb48ec03f216ea4ac66052712 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Fri, 17 Jul 2026 16:23:20 +0200 Subject: [PATCH] Prefer left side for receipt hover preview when there is room --- .../ReceiptPreview/getAnchoredPreviewPosition.ts | 13 +++++++------ .../unit/components/ReceiptPreviewPositionTest.ts | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/components/TransactionItemRow/ReceiptPreview/getAnchoredPreviewPosition.ts b/src/components/TransactionItemRow/ReceiptPreview/getAnchoredPreviewPosition.ts index 9eee1e74de7c..f42d6102ab97 100644 --- a/src/components/TransactionItemRow/ReceiptPreview/getAnchoredPreviewPosition.ts +++ b/src/components/TransactionItemRow/ReceiptPreview/getAnchoredPreviewPosition.ts @@ -15,10 +15,10 @@ const RECEIPT_PREVIEW_EDGE_MARGIN = 24; const RECEIPT_PREVIEW_MIN_VISIBLE_HEIGHT = 160; /** - * Anchors the preview's bottom-left corner to the right of the hovered thumbnail, so the preview grows upward - * from the row. If there isn't room on the right, it flips to the left of the thumbnail. The top is clamped to a - * viewport margin so a tall receipt near the top of the screen never runs off the top edge. Returns undefined - * when there is no anchor, so the caller keeps the static style. + * Anchors the preview's bottom-left corner beside the hovered thumbnail, so the preview grows upward from the + * row. It prefers the left of the thumbnail and only flips to the right when there isn't room on the left. The + * top is clamped to a viewport margin so a tall receipt near the top of the screen never runs off the top edge. + * Returns undefined when there is no anchor, so the caller keeps the static style. * * @param previewHeight Measured height of the preview. 0 means "not measured yet" — we can't bottom-align without * it, so we fall back to aligning the top with the row (keeping a minimum slice on-screen) for the first frame. @@ -28,9 +28,10 @@ function getAnchoredPreviewPosition(anchorPosition: AnchorPosition | undefined, return undefined; } + const leftOfThumbnail = anchorPosition.left - RECEIPT_PREVIEW_WIDTH - RECEIPT_PREVIEW_GAP; const rightOfThumbnail = anchorPosition.left + anchorPosition.width + RECEIPT_PREVIEW_GAP; - const overflowsRight = windowWidth > 0 && rightOfThumbnail + RECEIPT_PREVIEW_WIDTH + RECEIPT_PREVIEW_EDGE_MARGIN > windowWidth; - const left = overflowsRight ? Math.max(RECEIPT_PREVIEW_EDGE_MARGIN, anchorPosition.left - RECEIPT_PREVIEW_WIDTH - RECEIPT_PREVIEW_GAP) : rightOfThumbnail; + const fitsLeft = leftOfThumbnail >= RECEIPT_PREVIEW_EDGE_MARGIN; + const left = fitsLeft ? leftOfThumbnail : rightOfThumbnail; // Before it's measured we can't bottom-align, so keep a minimum slice on-screen relative to the row top. if (previewHeight <= 0) { diff --git a/tests/unit/components/ReceiptPreviewPositionTest.ts b/tests/unit/components/ReceiptPreviewPositionTest.ts index dba8a39f89ab..b84724a691bf 100644 --- a/tests/unit/components/ReceiptPreviewPositionTest.ts +++ b/tests/unit/components/ReceiptPreviewPositionTest.ts @@ -13,7 +13,17 @@ describe('getAnchoredPreviewPosition', () => { expect(getAnchoredPreviewPosition(undefined, WINDOW_WIDTH, WINDOW_HEIGHT)).toBeUndefined(); }); - it('places the preview just to the right of the hovered thumbnail', () => { + it('places the preview just to the left of the hovered thumbnail when there is room', () => { + const anchor = {top: 300, left: 500, width: 68, height: 64}; + + const position = getAnchoredPreviewPosition(anchor, WINDOW_WIDTH, WINDOW_HEIGHT); + + expect(position?.left).toBe(anchor.left - RECEIPT_PREVIEW_WIDTH - RECEIPT_PREVIEW_GAP); + expect(position?.left).toBeLessThan(anchor.left); + }); + + it('flips to the right of the thumbnail when there is not enough room on the left', () => { + // A thumbnail hugging the left edge leaves no room for the preview to its left. const anchor = {top: 300, left: 120, width: 68, height: 64}; const position = getAnchoredPreviewPosition(anchor, WINDOW_WIDTH, WINDOW_HEIGHT); @@ -27,8 +37,7 @@ describe('getAnchoredPreviewPosition', () => { expect(getAnchoredPreviewPosition(anchor, WINDOW_WIDTH, WINDOW_HEIGHT)?.top).toBe(420); }); - it('flips to the left of the thumbnail when there is not enough room on the right', () => { - // A thumbnail hugging the right edge leaves no room for a 380px-wide preview to its right. + it('keeps the preview to the left of a thumbnail near the right edge', () => { const anchor = {top: 300, left: WINDOW_WIDTH - 80, width: 68, height: 64}; const position = getAnchoredPreviewPosition(anchor, WINDOW_WIDTH, WINDOW_HEIGHT);