setFormToggled(false)}
- onCancel={() => {
- setFormToggled(false);
- if (activeThreads.length === 0 && onDismiss) onDismiss();
- }} />}
+ onSubmit={() => setFormToggled(false)} />}
{noThreads && !showNewForm &&
diff --git a/entry_types/scrolled/package/src/review/ThreadsBadge.js b/entry_types/scrolled/package/src/review/ThreadsBadge.js
index 2b7f255adb..dea6be35b1 100644
--- a/entry_types/scrolled/package/src/review/ThreadsBadge.js
+++ b/entry_types/scrolled/package/src/review/ThreadsBadge.js
@@ -1,46 +1,14 @@
-import React, {useCallback, useRef} from 'react';
+import React, {useCallback} from 'react';
-import {usePostMessageListener} from '../shared/usePostMessageListener';
import {useCommentThreads} from './ReviewStateProvider';
import {Badge} from './Badge';
-export function ThreadsBadge({subjectType, subjectId, subjectRange, onClick, onSelectThread, mode}) {
+export function ThreadsBadge({subjectType, subjectId, subjectRange, onClick, mode}) {
const threads = useCommentThreads({subjectType, subjectId, subjectRange}, {resolved: false});
- const ref = useRef();
const handleClick = useCallback(() => {
if (onClick) onClick(threads);
}, [onClick, threads]);
- return (
- <>
- {threads.length > 0 &&
- }
-
- >
- );
-}
-
-// Side-effect-only host for the SELECT_COMMENT_THREAD listener. Only
-// rendered while the badge has threads, so the listener never attaches
-// for empty badges. Returns null because all the work happens in the
-// effect.
-function SelectThreadListener({threads, badgeRef, onSelectThread}) {
- usePostMessageListener(useCallback(data => {
- if (data.type !== 'SELECT_COMMENT_THREAD') return;
- const threadId = data.payload.threadId;
- if (!threads.some(t => t.id === threadId)) return;
-
- // Prefer scrolling an enclosing selectable (e.g. SelectionRect with
- // aria-selected) into view so the user sees surrounding context,
- // not just the badge anchored at the side.
- const scrollTarget = badgeRef.current?.closest('[aria-selected]') || badgeRef.current;
- if (scrollTarget) scrollTarget.scrollIntoView({block: 'nearest', behavior: 'smooth'});
-
- if (onSelectThread) onSelectThread(threadId);
- }, [threads, badgeRef, onSelectThread]));
-
- return null;
+ return ;
}
diff --git a/entry_types/scrolled/package/src/review/commentHighlights.js b/entry_types/scrolled/package/src/review/commentHighlights.js
index 977c86907f..2e9e6f3b6c 100644
--- a/entry_types/scrolled/package/src/review/commentHighlights.js
+++ b/entry_types/scrolled/package/src/review/commentHighlights.js
@@ -31,13 +31,19 @@ export function decorateCommentHighlights(editor, highlights) {
Range.start(intersection),
Range.start(highlight.range)
);
+ const isLast = Point.equals(
+ Range.end(intersection),
+ Range.end(highlight.range)
+ );
decorations.push({
...intersection,
commentHighlight: true,
subjectRange: highlight.range,
rangeKey: highlight.key,
- ...(isFirst && {firstInRange: true})
+ resolved: !!highlight.thread?.resolvedAt,
+ ...(isFirst && {firstInRange: true}),
+ ...(isLast && {lastInRange: true})
});
}
}
diff --git a/entry_types/scrolled/package/src/review/commentHighlights.module.css b/entry_types/scrolled/package/src/review/commentHighlights.module.css
index 25c3ba5806..ca1390995d 100644
--- a/entry_types/scrolled/package/src/review/commentHighlights.module.css
+++ b/entry_types/scrolled/package/src/review/commentHighlights.module.css
@@ -7,3 +7,11 @@
background-color: var(--ui-accent-color-lighter);
border-bottom: 2px solid var(--ui-accent-color);
}
+
+/* A resolved thread shown only because it is the highlighted thread is
+ muted to grey, matching how the comments sidebar de-emphasizes
+ resolved threads. */
+.resolved {
+ background-color: var(--ui-accent-grey-color-lighter);
+ border-bottom: 2px solid var(--ui-accent-grey-color);
+}
diff --git a/entry_types/scrolled/package/src/review/rangeAnchors.js b/entry_types/scrolled/package/src/review/rangeAnchors.js
index 41b7587cf5..bb597e6327 100644
--- a/entry_types/scrolled/package/src/review/rangeAnchors.js
+++ b/entry_types/scrolled/package/src/review/rangeAnchors.js
@@ -82,19 +82,21 @@ export function alignToContainerEdge(containerRef, {
} = {}) {
return {
name: 'alignToContainerEdge',
- fn({rects, placement}) {
+ fn(state) {
const containerEl = containerRef.current;
if (!containerEl) return {};
+ const {rects, placement} = state;
const containerRect = containerEl.getBoundingClientRect();
+ const toLocalX = viewportToLocalX(state);
let x;
if (placement.startsWith('right')) {
- x = containerRect.right + mainAxisOffset;
+ x = toLocalX(containerRect.right + mainAxisOffset);
}
else if (placement.startsWith('left')) {
- x = containerRect.left - rects.floating.width - mainAxisOffset;
+ x = toLocalX(containerRect.left - mainAxisOffset) - rects.floating.width;
}
else {
return {};
@@ -118,12 +120,32 @@ export function alignToContainerEdge(containerRef, {
function clampXToViewport({viewportPadding = 8} = {}) {
return {
name: 'clampXToViewport',
- fn({x, rects}) {
+ fn(state) {
+ const {x, rects} = state;
+ const toLocalX = viewportToLocalX(state);
const viewportWidth = document.documentElement.clientWidth;
- const maxX = viewportWidth - rects.floating.width - viewportPadding;
- const minX = viewportPadding;
+
+ const minX = toLocalX(viewportPadding);
+ const maxX = toLocalX(viewportWidth - viewportPadding) - rects.floating.width;
return {x: Math.max(minX, Math.min(x, maxX))};
}
};
}
+
+// Floating UI applies `x`/`y` relative to the floating element's offsetParent
+// and reports `rects.reference` in that same (unscaled) space, while
+// getBoundingClientRect speaks viewport coordinates. As long as the badges
+// were portaled to the document root those spaces coincided, but rendered
+// inside the (relatively positioned, transform-scaled) main storyline sheet
+// they no longer do. Derive the offsetParent's current scale from the
+// reference and build a converter from viewport into the local space the
+// middleware must return.
+function viewportToLocalX({rects, elements}) {
+ const referenceRect = elements.reference.getBoundingClientRect();
+ const scale = rects.reference.width ?
+ referenceRect.width / rects.reference.width :
+ 1;
+
+ return viewportX => rects.reference.x + (viewportX - referenceRect.left) / scale;
+}
diff --git a/entry_types/scrolled/package/src/review/submitShortcut.js b/entry_types/scrolled/package/src/review/submitShortcut.js
new file mode 100644
index 0000000000..26856a68d0
--- /dev/null
+++ b/entry_types/scrolled/package/src/review/submitShortcut.js
@@ -0,0 +1,3 @@
+export function isSubmitShortcut(event) {
+ return (event.metaKey || event.ctrlKey) && event.key === 'Enter';
+}