Skip to content

Commit

Permalink
Fixed reading progress indicator for very short articles (#22036)
Browse files Browse the repository at this point in the history
ref https://linear.app/ghost/issue/AP-653/scroll-percentage-remains-at-0percent-when-no-content-to-scroll

- When an entire article fits into the viewport height, we used to
show`0%` in the reading progress indicator. Now we check if that's the
case, and then show `100%` if it is.
  • Loading branch information
djordjevlais authored Jan 22, 2025
1 parent 5409ae1 commit 2d0f656
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 9 deletions.
2 changes: 1 addition & 1 deletion apps/admin-x-activitypub/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryghost/admin-x-activitypub",
"version": "0.3.52",
"version": "0.3.53",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
58 changes: 50 additions & 8 deletions apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import APAvatar from '../global/APAvatar';
import APReplyBox from '../global/APReplyBox';
import TableOfContents, {TOCItem} from './TableOfContents';
import getReadingTime from '../../utils/get-reading-time';
import {useDebounce} from 'use-debounce';

interface ArticleModalProps {
activityId: string;
Expand Down Expand Up @@ -48,6 +49,7 @@ const ArticleBody: React.FC<{
fontFamily: SelectOption;
onHeadingsExtracted?: (headings: TOCItem[]) => void;
onIframeLoad?: (iframe: HTMLIFrameElement) => void;
onLoadingChange?: (isLoading: boolean) => void;
}> = ({
heading,
image,
Expand All @@ -57,7 +59,8 @@ const ArticleBody: React.FC<{
lineHeight,
fontFamily,
onHeadingsExtracted,
onIframeLoad
onIframeLoad,
onLoadingChange
}) => {
const site = useBrowseSite();
const siteData = site.data?.site;
Expand Down Expand Up @@ -213,7 +216,6 @@ const ArticleBody: React.FC<{
if (iframeWindow && typeof iframeWindow.resizeIframe === 'function') {
iframeWindow.resizeIframe();
} else {
// Fallback: trigger a resize event
const resizeEvent = new Event('resize');
iframeDocument.dispatchEvent(resizeEvent);
}
Expand Down Expand Up @@ -269,6 +271,11 @@ const ArticleBody: React.FC<{
return () => iframe.removeEventListener('load', handleLoad);
}, [onHeadingsExtracted, onIframeLoad]);

// Update parent when loading state changes
useEffect(() => {
onLoadingChange?.(isLoading);
}, [isLoading, onLoadingChange]);

return (
<div className='w-full pb-6'>
<div className='relative'>
Expand Down Expand Up @@ -526,30 +533,66 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
const currentGridWidth = `${parseInt(currentMaxWidth) - 64}px`;

const [readingProgress, setReadingProgress] = useState(0);
const [isLoading, setIsLoading] = useState(true);

// Add debounced version of setReadingProgress
const [debouncedSetReadingProgress] = useDebounce(setReadingProgress, 100);

const PROGRESS_INCREMENT = 5; // Progress is shown in 5% increments (0%, 5%, 10%, etc.)

useEffect(() => {
const container = document.querySelector('.overflow-y-auto');
const article = document.getElementById('object-content');

const handleScroll = () => {
if (isLoading) {
return;
}

if (!container || !article) {
return;
}

const articleRect = article.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();

const isContentShorterThanViewport = articleRect.height <= containerRect.height;

if (isContentShorterThanViewport) {
debouncedSetReadingProgress(100);
return;
}

const scrolledPast = Math.max(0, containerRect.top - articleRect.top);
const totalHeight = (article as HTMLElement).offsetHeight - (container as HTMLElement).offsetHeight;

const rawProgress = Math.min(Math.max((scrolledPast / totalHeight) * 100, 0), 100);
const progress = Math.round(rawProgress / 5) * 5;
const progress = Math.round(rawProgress / PROGRESS_INCREMENT) * PROGRESS_INCREMENT;

setReadingProgress(progress);
debouncedSetReadingProgress(progress);
};

if (isLoading) {
return;
}

const observer = new MutationObserver(handleScroll);
if (article) {
observer.observe(article, {
childList: true,
subtree: true,
characterData: true
});
}

container?.addEventListener('scroll', handleScroll);
return () => container?.removeEventListener('scroll', handleScroll);
}, []);
handleScroll();

return () => {
container?.removeEventListener('scroll', handleScroll);
observer.disconnect();
};
}, [isLoading, debouncedSetReadingProgress]);

const [tocItems, setTocItems] = useState<TOCItem[]>([]);
const [activeHeadingId, setActiveHeadingId] = useState<string | null>(null);
Expand All @@ -575,7 +618,6 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
return;
}

// Use offsetTop for absolute position within the document
const headingOffset = heading.offsetTop;

container.scrollTo({
Expand All @@ -602,7 +644,6 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
return;
}

// Get all heading elements and their positions
const headings = tocItems
.map(item => doc.getElementById(item.id))
.filter((el): el is HTMLElement => el !== null)
Expand Down Expand Up @@ -845,6 +886,7 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
lineHeight={LINE_HEIGHTS[currentLineHeightIndex]}
onHeadingsExtracted={handleHeadingsExtracted}
onIframeLoad={handleIframeLoad}
onLoadingChange={setIsLoading}
/>
<div className='ml-[-7px]'>
<FeedItemStats
Expand Down

0 comments on commit 2d0f656

Please sign in to comment.