Skip to content

Commit 453edcb

Browse files
chaoxiaochechaoxiaoche
authored andcommitted
Keep comment popovers anchored
1 parent b771313 commit 453edcb

4 files changed

Lines changed: 101 additions & 3 deletions

File tree

apps/web/src/components/BoardComposerPopover.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ export function BoardComposerPopover({
174174
bounds,
175175
offset,
176176
docked = false,
177+
targetVisible = true,
177178
}: {
178179
target: PreviewCommentSnapshot;
179180
existing: PreviewComment | null;
@@ -194,13 +195,14 @@ export function BoardComposerPopover({
194195
bounds?: PopoverBounds;
195196
offset?: PopoverOffset;
196197
docked?: boolean;
198+
targetVisible?: boolean;
197199
}) {
198200
const pendingCount = notes.length + (draft.trim() ? 1 : 0);
199201
const hasCommentChange = !existing || draft.trim() !== existing.note.trim();
200202
const podMembers = target.podMembers ?? [];
201203
return (
202204
<div
203-
className={`comment-popover${docked ? ' comment-popover-docked' : ''}`}
205+
className={`comment-popover${docked ? ' comment-popover-docked' : ''}${targetVisible ? '' : ' is-target-offscreen'}`}
204206
data-testid="comment-popover"
205207
role="dialog"
206208
aria-modal="false"

apps/web/src/components/FileViewer.tsx

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1994,6 +1994,23 @@ function commentActivityAt(comment: PreviewComment): number {
19941994
);
19951995
}
19961996

1997+
function commentTargetIntersectsPreview(
1998+
target: PreviewCommentSnapshot | null,
1999+
scale: number,
2000+
offset: { x: number; y: number },
2001+
bounds?: PreviewCanvasSize,
2002+
): boolean {
2003+
if (!target || !bounds?.width || !bounds.height) return true;
2004+
const rect = overlayBoundsFromSnapshot(target, scale, offset);
2005+
const margin = 8;
2006+
return (
2007+
rect.left + rect.width > margin &&
2008+
rect.top + rect.height > margin &&
2009+
rect.left < bounds.width - margin &&
2010+
rect.top < bounds.height - margin
2011+
);
2012+
}
2013+
19972014
function commentDisplayLabel(comment: PreviewComment, t: TranslateFn): string {
19982015
if (comment.elementId.startsWith('pin-')) return t('chat.comments.pin');
19992016
const label = String(comment.label || '').trim().toLowerCase();
@@ -4956,7 +4973,7 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
49564973
current
49574974
? current.selectionKind === 'pod'
49584975
? current
4959-
: next.get(current.elementId) ?? null
4976+
: next.get(current.elementId) ?? current
49604977
: null
49614978
));
49624979
setHoveredCommentTarget((current) => (
@@ -6017,6 +6034,16 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
60176034
void exitManualEditModeAfterFlush();
60186035
}
60196036

6037+
function returnToActiveCommentTarget() {
6038+
if (!activeCommentTarget) return;
6039+
iframeRef.current?.contentWindow?.postMessage({
6040+
type: 'od:comment-scroll-to-target',
6041+
elementId: activeCommentTarget.elementId,
6042+
selector: activeCommentTarget.selector,
6043+
}, '*');
6044+
setHoveredCommentTarget(activeCommentTarget);
6045+
}
6046+
60206047
function queueCurrentDraft() {
60216048
const note = commentDraft.trim();
60226049
if (!note) return;
@@ -6129,6 +6156,16 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
61296156
? visibleSideComments.find((comment) => comment.elementId === activeCommentTarget.elementId)?.id ?? null
61306157
: null
61316158
);
6159+
const activeCommentTargetVisible = commentTargetIntersectsPreview(
6160+
activeCommentTarget,
6161+
overlayPreviewScale,
6162+
{ x: overlayPreviewTransform.offsetX, y: overlayPreviewTransform.offsetY },
6163+
previewBodySize,
6164+
);
6165+
const hoveredTargetHasSavedComment = Boolean(
6166+
hoveredCommentTarget &&
6167+
visibleSideComments.some((comment) => comment.elementId === hoveredCommentTarget.elementId),
6168+
);
61326169
useEffect(() => {
61336170
if (!boardMode || !activePreviewCommentId) return;
61346171
const stillOpen = visibleSideComments.some((comment) => comment.id === activePreviewCommentId);
@@ -6332,6 +6369,7 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
63326369
offset={{ x: overlayPreviewTransform.offsetX, y: overlayPreviewTransform.offsetY }}
63336370
bounds={previewBodySize}
63346371
docked={false}
6372+
targetVisible={activeCommentTargetVisible}
63356373
/>
63366374
) : null;
63376375
const commentSidePanel = commentPanelOpen ? (
@@ -7023,7 +7061,18 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
70237061
</div>
70247062
) : null}
70257063
{commentComposer}
7026-
{boardMode && !commentCreateMode && hoveredCommentTarget && (!activeCommentTarget || commentPortalHost) ? (
7064+
{boardMode && activeCommentTarget && !activeCommentTargetVisible ? (
7065+
<button
7066+
type="button"
7067+
className="comment-return-anchor"
7068+
data-testid="comment-return-anchor"
7069+
onClick={returnToActiveCommentTarget}
7070+
>
7071+
<span aria-hidden="true">📍</span>
7072+
<span>Return to element</span>
7073+
</button>
7074+
) : null}
7075+
{boardMode && !commentCreateMode && hoveredCommentTarget && !hoveredTargetHasSavedComment && (!activeCommentTarget || commentPortalHost) ? (
70277076
<AnnotationHoverPopover
70287077
target={hoveredCommentTarget}
70297078
scale={overlayPreviewScale}

apps/web/src/runtime/srcdoc.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,24 @@ function meaningfulDomFallbackTarget(el) {
11271127
function requestPreviewScrollRestore(){
11281128
window.parent.postMessage({ type: 'od:preview-scroll-request' }, '*');
11291129
}
1130+
function scrollCommentTargetIntoView(data){
1131+
var el = null;
1132+
if (data.selector) {
1133+
try { el = document.querySelector(String(data.selector)); } catch (_) { el = null; }
1134+
}
1135+
if (!el && data.elementId) {
1136+
try {
1137+
var id = String(data.elementId).replace(/"/g, '\\"');
1138+
el = document.querySelector('[data-od-id="' + id + '"], [data-screen-label="' + id + '"]');
1139+
} catch (_) { el = null; }
1140+
}
1141+
if (!el || typeof el.scrollIntoView !== 'function') return;
1142+
el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' });
1143+
window.setTimeout(function(){
1144+
schedulePostTargets();
1145+
schedulePostPreviewScroll();
1146+
}, 120);
1147+
}
11301148
function postTargets(){
11311149
if (!active()) return;
11321150
window.parent.postMessage({ type: 'od:comment-targets', targets: allTargets() }, '*');
@@ -1217,6 +1235,10 @@ function meaningfulDomFallbackTarget(el) {
12171235
setTimeout(postPreviewScroll, 0);
12181236
return;
12191237
}
1238+
if (data.type === 'od:comment-scroll-to-target') {
1239+
scrollCommentTargetIntoView(data);
1240+
return;
1241+
}
12201242
if (data.type === 'od:inspect-mode') {
12211243
inspectEnabled = !!data.enabled;
12221244
document.documentElement.toggleAttribute('data-od-inspect-mode', inspectEnabled);

apps/web/src/styles/viewer/core.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,31 @@
11271127
background: var(--bg-panel);
11281128
box-shadow: var(--shadow-lg);
11291129
}
1130+
.comment-popover.is-target-offscreen {
1131+
opacity: 0.42;
1132+
}
1133+
.comment-return-anchor {
1134+
position: absolute;
1135+
left: 50%;
1136+
bottom: 16px;
1137+
z-index: 45;
1138+
display: inline-flex;
1139+
align-items: center;
1140+
gap: 8px;
1141+
transform: translateX(-50%);
1142+
padding: 8px 12px;
1143+
border: 1px solid var(--border);
1144+
border-radius: var(--radius-pill);
1145+
background: var(--bg-panel);
1146+
color: var(--text);
1147+
box-shadow: var(--shadow-lg);
1148+
font-size: 13px;
1149+
font-weight: 650;
1150+
}
1151+
.comment-return-anchor:hover {
1152+
border-color: var(--accent-soft);
1153+
color: var(--accent-strong);
1154+
}
11301155
.annotation-hover-popover {
11311156
pointer-events: none;
11321157
}

0 commit comments

Comments
 (0)