a tool for shared writing and social publishing

scroll pages and comments into view properly

+91 -32
+3 -3
app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
··· 13 13 comments: Comment[]; 14 14 did: string; 15 15 }) => { 16 - let drawer = useDrawerOpen(); 16 + let drawer = useDrawerOpen(props.document_uri); 17 17 if (!drawer) return null; 18 18 return ( 19 19 <> ··· 36 36 ); 37 37 }; 38 38 39 - export const useDrawerOpen = () => { 39 + export const useDrawerOpen = (uri: string) => { 40 40 let params = useSearchParams(); 41 41 let interactionDrawerSearchParam = params.get("interactionDrawer"); 42 - let { drawerOpen: open, drawer } = useInteractionState(); 42 + let { drawerOpen: open, drawer } = useInteractionState(uri); 43 43 if (open === false || (open === undefined && !interactionDrawerSearchParam)) 44 44 return null; 45 45 return drawer || interactionDrawerSearchParam;
+4 -17
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 8 8 import { QuotePosition } from "../quotePosition"; 9 9 import { useContext } from "react"; 10 10 import { PostPageContext } from "../PostPageContext"; 11 + import { scrollIntoView } from "src/utils/scrollIntoView"; 11 12 12 13 type InteractionState = { 13 14 drawerOpen: undefined | boolean; ··· 27 28 [document_uri: string]: InteractionState; 28 29 }>(() => ({})); 29 30 30 - export function useInteractionState(document_uri?: string) { 31 + export function useInteractionState(document_uri: string) { 31 32 return useInteractionStateStore((state) => { 32 - if (!document_uri || !state[document_uri]) { 33 + if (!state[document_uri]) { 33 34 return defaultInteractionState; 34 35 } 35 36 return state[document_uri]; ··· 87 88 flushSync(() => { 88 89 setInteractionState(document_uri, { drawerOpen: true, drawer }); 89 90 }); 90 - let el = document.getElementById("interaction-drawer"); 91 - let isOffscreen = false; 92 - if (el) { 93 - const rect = el.getBoundingClientRect(); 94 - const windowWidth = 95 - window.innerWidth || document.documentElement.clientWidth; 96 - isOffscreen = rect.right > windowWidth - 64; 97 - } 98 - 99 - if (el && isOffscreen) 100 - el.scrollIntoView({ 101 - behavior: "smooth", 102 - block: "center", 103 - inline: "center", 104 - }); 91 + scrollIntoView("interaction-drawer"); 105 92 } 106 93 107 94 export const Interactions = (props: {
+21 -12
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 9 9 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 10 10 import { SubscribeWithBluesky } from "app/lish/Subscribe"; 11 11 import { EditTiny } from "components/Icons/EditTiny"; 12 - import { Interactions, useInteractionState } from "./Interactions/Interactions"; 12 + import { Interactions } from "./Interactions/Interactions"; 13 13 import { PostContent } from "./PostContent"; 14 14 import { PostHeader } from "./PostHeader/PostHeader"; 15 15 import { useIdentityData } from "components/IdentityProvider"; ··· 25 25 import { CloseTiny } from "components/Icons/CloseTiny"; 26 26 import { PageWrapper } from "components/Pages/Page"; 27 27 import { Fragment } from "react"; 28 + import { flushSync } from "react-dom"; 29 + import { scrollIntoView } from "src/utils/scrollIntoView"; 28 30 export const usePostPageUIState = create(() => ({ 29 31 pages: [] as string[], 30 32 })); 31 33 32 - export const openPage = (parent: string | undefined, page: string) => 33 - usePostPageUIState.setState((state) => { 34 - let parentPosition = state.pages.findIndex((s) => s == parent); 35 - return { 36 - pages: 37 - parentPosition === -1 38 - ? [page] 39 - : [...state.pages.slice(0, parentPosition + 1), page], 40 - }; 34 + export const openPage = (parent: string | undefined, page: string) => { 35 + flushSync(() => { 36 + usePostPageUIState.setState((state) => { 37 + let parentPosition = state.pages.findIndex((s) => s == parent); 38 + return { 39 + pages: 40 + parentPosition === -1 41 + ? [page] 42 + : [...state.pages.slice(0, parentPosition + 1), page], 43 + }; 44 + }); 41 45 }); 42 46 47 + scrollIntoView(`post-page-${page}`); 48 + }; 49 + 43 50 export const closePage = (page: string) => 44 51 usePostPageUIState.setState((state) => { 45 52 let parentPosition = state.pages.findIndex((s) => s == page); ··· 58 65 pubRecord, 59 66 prerenderedCodeBlocks, 60 67 bskyPostData, 68 + document_uri, 61 69 }: { 70 + document_uri: string; 62 71 document: PostPageData; 63 72 blocks: PubLeafletPagesLinearDocument.Block[]; 64 73 name: string; ··· 70 79 preferences: { showComments?: boolean }; 71 80 }) { 72 81 let { identity } = useIdentityData(); 73 - let drawerOpen = useDrawerOpen(); 82 + let drawerOpen = useDrawerOpen(document_uri); 74 83 let pages = usePostPageUIState((s) => s.pages); 75 84 if (!document || !document.documents_in_publications[0].publications) 76 85 return null; ··· 154 163 <SandwichSpacer /> 155 164 <PageWrapper 156 165 cardBorderHidden={!hasPageBackground} 157 - id={"post-page"} 166 + id={`post-page-${p}`} 158 167 fullPageScroll={false} 159 168 pageOptions={ 160 169 <PageOptions
+1
app/lish/[did]/[publication]/[rkey]/page.tsx
··· 154 154 */} 155 155 <LeafletLayout> 156 156 <PostPages 157 + document_uri={document.uri} 157 158 preferences={pubRecord.preferences || {}} 158 159 pubRecord={pubRecord} 159 160 profile={JSON.parse(JSON.stringify(profile.data))}
+62
src/utils/scrollIntoView.ts
··· 1 + // Generated with claude code, sonnet 4.5 2 + /** 3 + * Scrolls an element into view within a scrolling container using Intersection Observer 4 + * and the scrollTo API, instead of the native scrollIntoView. 5 + * 6 + * @param elementId - The ID of the element to scroll into view 7 + * @param scrollContainerId - The ID of the scrolling container (defaults to "pages") 8 + * @param threshold - Intersection observer threshold (0-1, defaults to 0.2 for 20%) 9 + */ 10 + export function scrollIntoView( 11 + elementId: string, 12 + scrollContainerId: string = "pages", 13 + threshold: number = 0.2, 14 + ) { 15 + const element = document.getElementById(elementId); 16 + const scrollContainer = document.getElementById(scrollContainerId); 17 + 18 + if (!element || !scrollContainer) { 19 + console.warn(`scrollIntoView: element or container not found`, { 20 + elementId, 21 + scrollContainerId, 22 + element, 23 + scrollContainer, 24 + }); 25 + return; 26 + } 27 + 28 + // Create an intersection observer to check if element is visible 29 + const observer = new IntersectionObserver( 30 + (entries) => { 31 + const entry = entries[0]; 32 + 33 + // If element is not sufficiently visible, scroll to it 34 + if (!entry.isIntersecting || entry.intersectionRatio < threshold) { 35 + const elementRect = element.getBoundingClientRect(); 36 + const containerRect = scrollContainer.getBoundingClientRect(); 37 + 38 + // Calculate the target scroll position 39 + // We want to center the element horizontally in the container 40 + const targetScrollLeft = 41 + scrollContainer.scrollLeft + 42 + elementRect.left - 43 + containerRect.left - 44 + (containerRect.width - elementRect.width) / 2; 45 + 46 + scrollContainer.scrollTo({ 47 + left: targetScrollLeft, 48 + behavior: "smooth", 49 + }); 50 + } 51 + 52 + // Disconnect after checking once 53 + observer.disconnect(); 54 + }, 55 + { 56 + root: scrollContainer, 57 + threshold: threshold, 58 + }, 59 + ); 60 + 61 + observer.observe(element); 62 + }