Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/components/SearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(null)
const bodyPointerEventsRef = React.useRef('')
const [shouldRenderSearch, setShouldRenderSearch] = React.useState(isOpen)
Expand Down Expand Up @@ -399,6 +399,10 @@ export function SearchModal() {
<DialogPrimitive.Content
forceMount
ref={contentRef}
onCloseAutoFocus={(event) => {
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',
Expand Down
32 changes: 25 additions & 7 deletions src/components/ds/ui/BlogPostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export function BlogPostCard({
post,
size = 'sm',
featured = false,
imageLoading,
imageSizes,
showLibraryBadges = true,
showCategory = false,
}: {
Expand All @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -75,14 +96,11 @@ export function BlogPostCard({
>
{post.headerImage ? (
<img
src={getOptimizedImageUrl(post.headerImage, {
fit: 'cover',
format: 'auto',
quality: 80,
width: featured ? 1200 : isLarge ? 900 : 640,
})}
src={imageSrc}
srcSet={imageSrcSet}
sizes={imageSizes}
alt=""
loading={featured ? 'eager' : 'lazy'}
loading={imageLoading ?? (featured ? 'eager' : 'lazy')}
decoding="async"
className={
featured ? 'block h-auto w-full' : 'h-full w-full object-cover'
Expand Down
30 changes: 30 additions & 0 deletions src/contexts/SearchContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface SearchContextType {
askAiDock: (question: string) => void
clearAiDockAskRequest: (id: number) => void
closeSearch: () => void
restoreSearchFocus: () => void
closeAiDock: () => void
cancelAiDockHoverClose: () => void
scheduleAiDockHoverClose: () => void
Expand Down Expand Up @@ -45,6 +46,7 @@ export function SearchProvider({
question: string
} | null>(null)
const aiDockCloseTimerRef = React.useRef<number | null>(null)
const searchTriggerRef = React.useRef<HTMLElement | null>(null)

const cancelAiDockHoverClose = React.useCallback(() => {
if (!aiDockCloseTimerRef.current) {
Expand All @@ -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(() => {
Expand Down Expand Up @@ -128,6 +156,7 @@ export function SearchProvider({
askAiDock,
clearAiDockAskRequest,
closeSearch,
restoreSearchFocus,
closeAiDock,
cancelAiDockHoverClose,
scheduleAiDockHoverClose,
Expand All @@ -140,6 +169,7 @@ export function SearchProvider({
clearAiDockAskRequest,
closeAiDock,
closeSearch,
restoreSearchFocus,
isAiDockDirty,
isAiDockOpen,
isOpen,
Expand Down
5 changes: 5 additions & 0 deletions src/routes/blog.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ function BlogIndex() {
<BlogPostCard
post={featuredPost}
featured
imageSizes="(min-width: 1344px) 837px, (min-width: 768px) calc((100vw - 88px) * 2 / 3), calc(100vw - 32px)"
showCategory
showLibraryBadges={false}
/>
Expand All @@ -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}
Expand Down
16 changes: 14 additions & 2 deletions src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,20 @@ function Index() {
light-mode selves (no per-button color overrides). */}
<div className="ds-mode-light flex flex-wrap items-center gap-2">
<Button
type="button"
onClick={() => openLibraries()}
as="a"
href="/libraries"
onClick={(event) => {
if (
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
)
return
event.preventDefault()
openLibraries()
}}
variant="primary"
size="md"
>
Expand Down
2 changes: 1 addition & 1 deletion src/utils/optimizedImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function shouldTransformImage(src: string) {
const normalized = src.toLowerCase()

return (
src.startsWith('/') &&
(src.startsWith('/') || src.startsWith('https://media.tanstack.com/')) &&
!src.startsWith('/cdn-cgi/image/') &&
!normalized.startsWith('data:') &&
!normalized.endsWith('.svg')
Expand Down
41 changes: 41 additions & 0 deletions tests/optimized-image.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/// <reference types="node" />

import assert from 'node:assert/strict'
import { beforeEach, test } from 'node:test'
import { getOptimizedImageUrl } from '../src/utils/optimizedImage'

beforeEach(() => {
Object.assign(globalThis, {
__TANSTACK_ENABLE_IMAGE_TRANSFORMATIONS__: true,
__TANSTACK_SITE_URL__: 'https://tanstack.com',
})
})

test('resizes media-host posters through the existing image service', () => {
assert.equal(
getOptimizedImageUrl(
'https://media.tanstack.com/blog/tanstack-ai-1-0/v9-title-poster.jpg',
{ width: 640, format: 'auto' },
),
'https://tanstack.com/cdn-cgi/image/width=640,quality=80,format=auto/https://media.tanstack.com/blog/tanstack-ai-1-0/v9-title-poster.jpg',
)
})

test('does not proxy other hosts or transform an image twice', () => {
for (const src of [
'https://example.com/image.jpg',
'https://media.tanstack.com.example.com/image.jpg',
'/cdn-cgi/image/width=640,format=auto/images/image.jpg',
]) {
assert.equal(getOptimizedImageUrl(src, { width: 400 }), src)
}
})

test('keeps original media URLs when transformations are disabled locally', () => {
Object.assign(globalThis, {
__TANSTACK_ENABLE_IMAGE_TRANSFORMATIONS__: false,
})
const src =
'https://media.tanstack.com/blog/tanstack-ai-1-0/v9-title-poster.jpg'
assert.equal(getOptimizedImageUrl(src, { width: 640 }), src)
})
Loading