fix(hover-card): maiden name false positive, placeholder on non-empty editor, card persistence
- PersonHoverCard: alias is compared against both `lastName` and `displayName` before showing as maiden name — prevents false positive when alias is stored as the full current name (e.g. "Maria Schmidt" ≠ "Schmidt" but name unchanged) - PersonMentionEditor: data-placeholder was set statically so the CSS ::before rule showed the placeholder on any blur even with content; now a $effect toggles the attribute based on editor.isEmpty - TranscriptionReadView: hovering onto the card itself cancels the 150ms close timer so the card stays open while reading it; leaving the card closes it immediately — onmouseenter/onmouseleave wired through PersonHoverCard props - hoverCardPosition: removed scrollX/scrollY offset since the card is now position:fixed (scroll is already baked into getBoundingClientRect coords) - MentionDropdown: raised z-index from z-20 to z-50 to render above the hover card - vite.config.ts: pre-bundle Tiptap packages to avoid HMR waterfall on first load Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,8 @@ const makeRect = (overrides: Partial<DOMRect> = {}): DOMRect => {
|
||||
} as DOMRect;
|
||||
};
|
||||
|
||||
const vp = { viewportWidth: 1440, viewportHeight: 900 };
|
||||
|
||||
describe('computeHoverCardPosition', () => {
|
||||
it('exports the spec constants used by the spec/CSS layer', () => {
|
||||
// Pin the values the design spec calls out — if these drift, the design spec
|
||||
@@ -33,12 +35,7 @@ describe('computeHoverCardPosition', () => {
|
||||
describe('default placement (below-right)', () => {
|
||||
it('positions the card below the rect with a small gap', () => {
|
||||
const rect = makeRect({ top: 100, bottom: 120, left: 200 });
|
||||
const result = computeHoverCardPosition(rect, {
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
scrollX: 0,
|
||||
scrollY: 0
|
||||
});
|
||||
const result = computeHoverCardPosition(rect, vp);
|
||||
expect(result.top).toBe(120 + CARD_GAP_PX);
|
||||
expect(result.left).toBe(200);
|
||||
});
|
||||
@@ -48,24 +45,14 @@ describe('computeHoverCardPosition', () => {
|
||||
it('flips up when the card would overflow the bottom edge', () => {
|
||||
// Mention sits 50px above the viewport bottom — card is 180px tall, can't fit below
|
||||
const rect = makeRect({ top: 800, bottom: 850 });
|
||||
const result = computeHoverCardPosition(rect, {
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
scrollX: 0,
|
||||
scrollY: 0
|
||||
});
|
||||
const result = computeHoverCardPosition(rect, vp);
|
||||
expect(result.top).toBe(800 - CARD_HEIGHT_PX - CARD_GAP_PX);
|
||||
});
|
||||
|
||||
it('flips up when the mention sits in the bottom 30% of the viewport (BOTTOM_BAND_RATIO)', () => {
|
||||
// rect.top is at 80% of viewport — fits below numerically, but poor UX
|
||||
const rect = makeRect({ top: 720, bottom: 740 });
|
||||
const result = computeHoverCardPosition(rect, {
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
scrollX: 0,
|
||||
scrollY: 0
|
||||
});
|
||||
const result = computeHoverCardPosition(rect, vp);
|
||||
expect(result.top).toBe(720 - CARD_HEIGHT_PX - CARD_GAP_PX);
|
||||
});
|
||||
});
|
||||
@@ -74,12 +61,7 @@ describe('computeHoverCardPosition', () => {
|
||||
it('flips left when the rect is within RIGHT_FLIP_THRESHOLD_PX of the right edge', () => {
|
||||
// vw - rect.left = 1440 - 1200 = 240 < 300, so flip
|
||||
const rect = makeRect({ left: 1200, right: 1300, top: 100, bottom: 120 });
|
||||
const result = computeHoverCardPosition(rect, {
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
scrollX: 0,
|
||||
scrollY: 0
|
||||
});
|
||||
const result = computeHoverCardPosition(rect, { viewportWidth: 1440, viewportHeight: 900 });
|
||||
// left = right - CARD_WIDTH = 1300 - 320 = 980
|
||||
expect(result.left).toBe(980);
|
||||
});
|
||||
@@ -87,12 +69,7 @@ describe('computeHoverCardPosition', () => {
|
||||
it('does not flip left when the rect has plenty of right-side room', () => {
|
||||
// vw - rect.left = 1440 - 200 = 1240 >> 300 → no flip
|
||||
const rect = makeRect({ left: 200, right: 300 });
|
||||
const result = computeHoverCardPosition(rect, {
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
scrollX: 0,
|
||||
scrollY: 0
|
||||
});
|
||||
const result = computeHoverCardPosition(rect, vp);
|
||||
expect(result.left).toBe(200);
|
||||
});
|
||||
});
|
||||
@@ -103,50 +80,32 @@ describe('computeHoverCardPosition', () => {
|
||||
// Without clamping the card would be at left=0 but extend to 320 — fine.
|
||||
// At viewport=400px with rect.left=200, flip puts left=300-320=-20, clamped to 0.
|
||||
const rect = makeRect({ left: 200, right: 300, top: 100, bottom: 120 });
|
||||
const result = computeHoverCardPosition(rect, {
|
||||
viewportWidth: 400,
|
||||
viewportHeight: 900,
|
||||
scrollX: 0,
|
||||
scrollY: 0
|
||||
});
|
||||
const result = computeHoverCardPosition(rect, { viewportWidth: 400, viewportHeight: 900 });
|
||||
expect(result.left).toBeGreaterThanOrEqual(0);
|
||||
expect(result.left + CARD_WIDTH_PX).toBeLessThanOrEqual(400);
|
||||
});
|
||||
|
||||
it('never returns a negative top or left', () => {
|
||||
const rect = makeRect({ top: -50, left: -100, bottom: -30, right: 0 });
|
||||
const result = computeHoverCardPosition(rect, {
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
scrollX: 0,
|
||||
scrollY: 0
|
||||
});
|
||||
const result = computeHoverCardPosition(rect, vp);
|
||||
expect(result.top).toBeGreaterThanOrEqual(0);
|
||||
expect(result.left).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scroll offset', () => {
|
||||
it('adds window.scrollY to the absolute-positioned top', () => {
|
||||
describe('position: fixed (viewport-relative coordinates)', () => {
|
||||
it('returns viewport-relative top — does not add scroll offset', () => {
|
||||
// getBoundingClientRect values are already viewport-relative; with position:fixed
|
||||
// we use them directly without adding scrollY.
|
||||
const rect = makeRect({ top: 100, bottom: 120 });
|
||||
const result = computeHoverCardPosition(rect, {
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
scrollX: 0,
|
||||
scrollY: 500
|
||||
});
|
||||
expect(result.top).toBe(120 + CARD_GAP_PX + 500);
|
||||
const result = computeHoverCardPosition(rect, vp);
|
||||
expect(result.top).toBe(120 + CARD_GAP_PX);
|
||||
});
|
||||
|
||||
it('adds window.scrollX to the absolute-positioned left', () => {
|
||||
it('returns viewport-relative left — does not add scroll offset', () => {
|
||||
const rect = makeRect({ top: 100, bottom: 120, left: 200, right: 300 });
|
||||
const result = computeHoverCardPosition(rect, {
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
scrollX: 50,
|
||||
scrollY: 0
|
||||
});
|
||||
expect(result.left).toBe(200 + 50);
|
||||
const result = computeHoverCardPosition(rect, vp);
|
||||
expect(result.left).toBe(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user