From 61b81facfed80e29cd2e8540e8a3f92addd3fe21 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Wed, 28 Oct 2026 17:18:05 -0600 Subject: [PATCH] Fix blog image loading and initial navigation accessibility --- src/components/SearchModal.tsx | 6 +++- src/components/ds/ui/BlogPostCard.tsx | 32 ++++++++++++++++----- src/contexts/SearchContext.tsx | 30 ++++++++++++++++++++ src/routes/blog.index.tsx | 5 ++++ src/routes/index.tsx | 16 +++++++++-- src/utils/optimizedImage.ts | 2 +- tests/optimized-image.test.ts | 41 +++++++++++++++++++++++++++ 7 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 tests/optimized-image.test.ts diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 2e6f47a43..41fe2eff6 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -294,7 +294,7 @@ function isSearchModalPortalTarget(target: EventTarget | null) { const searchModalTransitionMs = 140 export function SearchModal() { - const { isOpen, closeSearch } = useSearchContext() + const { isOpen, closeSearch, restoreSearchFocus } = useSearchContext() const contentRef = React.useRef(null) const bodyPointerEventsRef = React.useRef('') const [shouldRenderSearch, setShouldRenderSearch] = React.useState(isOpen) @@ -399,6 +399,10 @@ export function SearchModal() { { + event.preventDefault() + restoreSearchFocus() + }} className={twMerge( 'search-modal-content fixed z-[calc(var(--z-overlay)+2)] inset-0 sm:inset-auto sm:top-4 sm:left-1/2 sm:-translate-x-1/2 sm:w-[96%] xl:w-full sm:max-w-4xl text-left outline-none', isFullHeight && 'sm:bottom-4', diff --git a/src/components/ds/ui/BlogPostCard.tsx b/src/components/ds/ui/BlogPostCard.tsx index c626457c7..824d66581 100644 --- a/src/components/ds/ui/BlogPostCard.tsx +++ b/src/components/ds/ui/BlogPostCard.tsx @@ -21,6 +21,8 @@ export function BlogPostCard({ post, size = 'sm', featured = false, + imageLoading, + imageSizes, showLibraryBadges = true, showCategory = false, }: { @@ -33,6 +35,8 @@ export function BlogPostCard({ /** Hero treatment: image and copy sit side-by-side on wider screens with the * largest type. Used for the single latest post atop the Blog index. */ featured?: boolean + imageLoading?: 'eager' | 'lazy' + imageSizes?: string /** Show the library tag(s) over the image. Off for a single-library blog, * where every card would carry the same redundant badge. */ showLibraryBadges?: boolean @@ -41,6 +45,23 @@ export function BlogPostCard({ showCategory?: boolean }) { const isLarge = featured || size === 'lg' + const headerImage = post.headerImage + const imageSrc = headerImage + ? getOptimizedImageUrl(headerImage, { + format: 'auto', + quality: 80, + width: featured ? 1200 : isLarge ? 900 : 640, + }) + : undefined + const imageSrcSet = + imageSizes && headerImage && imageSrc !== headerImage + ? [400, 640, 900, 1200, 1800] + .map( + (width) => + `${getOptimizedImageUrl(headerImage, { width, quality: 80, format: 'auto' })} ${width}w`, + ) + .join(', ') + : undefined const blogLibraries = showLibraryBadges ? getBlogLibraries(post.library) : [] const primaryLibrary = showCategory ? getBlogLibraries(post.library)[0] @@ -75,14 +96,11 @@ export function BlogPostCard({ > {post.headerImage ? ( void clearAiDockAskRequest: (id: number) => void closeSearch: () => void + restoreSearchFocus: () => void closeAiDock: () => void cancelAiDockHoverClose: () => void scheduleAiDockHoverClose: () => void @@ -45,6 +46,7 @@ export function SearchProvider({ question: string } | null>(null) const aiDockCloseTimerRef = React.useRef(null) + const searchTriggerRef = React.useRef(null) const cancelAiDockHoverClose = React.useCallback(() => { if (!aiDockCloseTimerRef.current) { @@ -56,8 +58,34 @@ export function SearchProvider({ }, []) const openSearch = React.useCallback(() => { + if (!isOpen) { + searchTriggerRef.current = + document.activeElement instanceof HTMLElement + ? document.activeElement + : null + } setHasLoadedSearch(true) setIsOpen(true) + }, [isOpen]) + + const restoreSearchFocus = React.useCallback(() => { + const trigger = searchTriggerRef.current + searchTriggerRef.current = null + if ( + trigger?.isConnected && + trigger.getClientRects().length > 0 && + !trigger.closest('[aria-hidden="true"], [inert]') + ) { + trigger.focus({ preventScroll: true }) + return + } + + const menuTrigger = document.querySelector( + 'button[aria-controls="primary-mobile-menu"]', + ) + if (menuTrigger instanceof HTMLElement) { + menuTrigger.focus({ preventScroll: true }) + } }, []) const openAiDock = React.useCallback(() => { @@ -128,6 +156,7 @@ export function SearchProvider({ askAiDock, clearAiDockAskRequest, closeSearch, + restoreSearchFocus, closeAiDock, cancelAiDockHoverClose, scheduleAiDockHoverClose, @@ -140,6 +169,7 @@ export function SearchProvider({ clearAiDockAskRequest, closeAiDock, closeSearch, + restoreSearchFocus, isAiDockDirty, isAiDockOpen, isOpen, diff --git a/src/routes/blog.index.tsx b/src/routes/blog.index.tsx index bbb5e67de..b5664b627 100644 --- a/src/routes/blog.index.tsx +++ b/src/routes/blog.index.tsx @@ -219,6 +219,7 @@ function BlogIndex() { @@ -245,6 +246,10 @@ function BlogIndex() { key={post.slug} post={post} size="lg" + imageLoading={ + post === visiblePosts[0] ? 'eager' : 'lazy' + } + imageSizes="(min-width: 1344px) 624px, (min-width: 768px) calc((100vw - 96px) / 2), calc(100vw - 32px)" showCategory showLibraryBadges={false} className={enterClass} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index a435afd42..04c764b89 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -110,8 +110,20 @@ function Index() { light-mode selves (no per-button color overrides). */}