fix: harden selection handling and clamp the highlight button

Per review:
- Guard sel.rangeCount === 0 before calling getRangeAt(0), which
  throws otherwise.
- Scope containment by the range's commonAncestorContainer instead of
  just anchorNode, so a selection that starts inside the article but
  is dragged out past its boundary is correctly rejected.
- Clamp the floating button's top so a selection near the top of the
  viewport doesn't push it off-screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
This commit is contained in:
2026-09-06 18:06:30 +02:00
parent c56e4fbf02
commit 3d77b0516c

View File

@@ -37,7 +37,15 @@ export function HighlightLayer({
function handleSelectionChange() {
const sel = window.getSelection();
const container = containerRef.current;
if (!sel || sel.isCollapsed || !container || !container.contains(sel.anchorNode)) {
if (!sel || sel.isCollapsed || sel.rangeCount === 0 || !container) {
setSelection(null);
return;
}
const range = sel.getRangeAt(0);
// commonAncestorContainer, not anchorNode: anchorNode is only where the
// selection *started*, so a selection that starts inside the article
// and is dragged out past its boundary would otherwise still pass.
if (!container.contains(range.commonAncestorContainer)) {
setSelection(null);
return;
}
@@ -46,7 +54,7 @@ export function HighlightLayer({
setSelection(null);
return;
}
const rect = sel.getRangeAt(0).getBoundingClientRect();
const rect = range.getBoundingClientRect();
setSelection({ text, top: rect.top, left: rect.left + rect.width / 2 });
}
@@ -84,7 +92,14 @@ export function HighlightLayer({
// the instant it's pressed.
onMouseDown={(event) => event.preventDefault()}
onClick={handleHighlight}
style={{ position: 'fixed', top: selection.top - 40, left: selection.left, transform: 'translateX(-50%)' }}
style={{
position: 'fixed',
// Clamped so a selection near the top of the viewport doesn't
// push the button off-screen and out of reach.
top: Math.max(8, selection.top - 40),
left: selection.left,
transform: 'translateX(-50%)',
}}
className="z-50 flex items-center gap-1.5 rounded-full bg-foreground px-3 py-1.5 text-xs font-medium text-background shadow-lg"
>
<Highlighter className="size-3.5" aria-hidden />