From 6c85286938dafc49e7f7580a6f9bc481b25ef645 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 13 Nov 2025 10:36:32 -0600 Subject: [PATCH 1/9] fix client listeners Element assumption --- packages/dev/s2-docs/src/client.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/dev/s2-docs/src/client.tsx b/packages/dev/s2-docs/src/client.tsx index 0fe33508021..4a01b62c548 100644 --- a/packages/dev/s2-docs/src/client.tsx +++ b/packages/dev/s2-docs/src/client.tsx @@ -154,7 +154,7 @@ function clearPrefetchTimeout() { } document.addEventListener('pointerover', e => { - let link = (e.target as Element).closest('a'); + let link = e.target instanceof Element ? e.target.closest('a') : null; let publicUrl = process.env.PUBLIC_URL || '/'; let publicUrlPathname = publicUrl.startsWith('http') ? new URL(publicUrl).pathname : publicUrl; @@ -172,14 +172,14 @@ document.addEventListener('pointerover', e => { // Clear prefetch timeout when pointer leaves a link document.addEventListener('pointerout', e => { - let link = (e.target as Element).closest('a'); + let link = e.target instanceof Element ? e.target.closest('a') : null; if (link && link === currentPrefetchLink) { clearPrefetchTimeout(); } }, true); document.addEventListener('focus', e => { - let link = (e.target as Element).closest('a'); + let link = e.target instanceof Element ? e.target.closest('a') : null; let publicUrl = process.env.PUBLIC_URL || '/'; let publicUrlPathname = publicUrl.startsWith('http') ? new URL(publicUrl).pathname : publicUrl; @@ -197,7 +197,7 @@ document.addEventListener('focus', e => { // Clear prefetch timeout when focus leaves a link document.addEventListener('blur', e => { - let link = (e.target as Element).closest('a'); + let link = e.target instanceof Element ? e.target.closest('a') : null; if (link && link === currentPrefetchLink) { clearPrefetchTimeout(); } @@ -205,7 +205,7 @@ document.addEventListener('blur', e => { // Intercept link clicks to perform RSC navigation. document.addEventListener('click', e => { - let link = (e.target as Element).closest('a'); + let link = e.target instanceof Element ? e.target.closest('a') : null; let publicUrl = process.env.PUBLIC_URL || '/'; let publicUrlPathname = publicUrl.startsWith('http') ? new URL(publicUrl).pathname : publicUrl; if ( From 89836ddbf8fa1b56830f9e89f887f6266eda2c7d Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 13 Nov 2025 11:07:12 -0600 Subject: [PATCH 2/9] improve Illustrations search (copy feedback + message) --- .../dev/s2-docs/src/IllustrationCards.tsx | 68 ++++++++++++++----- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/packages/dev/s2-docs/src/IllustrationCards.tsx b/packages/dev/s2-docs/src/IllustrationCards.tsx index be0a82112a9..0367909f749 100644 --- a/packages/dev/s2-docs/src/IllustrationCards.tsx +++ b/packages/dev/s2-docs/src/IllustrationCards.tsx @@ -1,15 +1,18 @@ 'use client'; import {Autocomplete, GridLayout, ListBox, ListBoxItem, Size, useFilter, Virtualizer} from 'react-aria-components'; +// eslint-disable-next-line monorepo/no-internal-import +import Checkmark from '@react-spectrum/s2/illustrations/gradient/generic1/Checkmark'; import {Content, Heading, IllustratedMessage, pressScale, ProgressCircle, Radio, RadioGroup, SearchField, SegmentedControl, SegmentedControlItem, Text, UNSTABLE_ToastQueue as ToastQueue} from '@react-spectrum/s2'; -import {focusRing, style} from '@react-spectrum/s2/style' with {type: 'macro'}; +import {focusRing, iconStyle, style} from '@react-spectrum/s2/style' with {type: 'macro'}; // @ts-ignore import Gradient from '@react-spectrum/s2/icons/Gradient'; import {illustrationAliases} from './illustrationAliases.js'; +import InfoCircle from '@react-spectrum/s2/icons/InfoCircle'; // eslint-disable-next-line monorepo/no-internal-import import NoSearchResults from '@react-spectrum/s2/illustrations/linear/NoSearchResults'; import Polygon4 from '@react-spectrum/s2/icons/Polygon4'; -import React, {Suspense, use, useCallback, useRef, useState} from 'react'; +import React, {Suspense, use, useCallback, useEffect, useRef, useState} from 'react'; type IllustrationItemType = { id: string, @@ -77,6 +80,7 @@ export function IllustrationCards() { Generic 2 )} + }> @@ -93,19 +97,48 @@ function Loading() { ); } -let handleCopyImport = (id: string, variant: string, gradientStyle: string) => { - let importText = variant === 'gradient' ? - `import ${id} from '@react-spectrum/s2/illustrations/gradient/${gradientStyle}/${id}';` : - `import ${id} from '@react-spectrum/s2/illustrations/linear/${id}';`; - navigator.clipboard.writeText(importText).then(() => { - // noop - }).catch(() => { - ToastQueue.negative('Failed to copy import statement.'); - }); -}; +function CopyInfoMessage() { + return ( +
+ + Press an item to copy its import statement +
+ ); +} + +function useCopyImport(variant: string, gradientStyle: string) { + let [copiedId, setCopiedId] = useState(null); + let timeout = useRef | null>(null); + + useEffect(() => { + return () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + }; + }, []); + + let handleCopyImport = useCallback((id: string) => { + if (timeout.current) { + clearTimeout(timeout.current); + } + let importText = variant === 'gradient' ? + `import ${id} from '@react-spectrum/s2/illustrations/gradient/${gradientStyle}/${id}';` : + `import ${id} from '@react-spectrum/s2/illustrations/linear/${id}';`; + navigator.clipboard.writeText(importText).then(() => { + setCopiedId(id); + timeout.current = setTimeout(() => setCopiedId(null), 2000); + }).catch(() => { + ToastQueue.negative('Failed to copy import statement.'); + }); + }, [variant, gradientStyle]); + + return {copiedId, handleCopyImport}; +} function IllustrationList({variant, gradientStyle}) { let items = use(loadIllustrations(variant, gradientStyle)); + let {copiedId, handleCopyImport} = useCopyImport(variant, gradientStyle); return ( handleCopyImport(item.toString(), variant, gradientStyle)} + onAction={(item) => handleCopyImport(item.toString())} + dependencies={[copiedId]} className={style({height: 560, width: '100%', maxHeight: '100%', overflow: 'auto', scrollPaddingY: 4})} renderEmptyState={() => ( @@ -132,25 +166,25 @@ function IllustrationList({variant, gradientStyle}) { )}> - {(item: IllustrationItemType) => } + {(item: IllustrationItemType) => } ); } -function IllustrationItem({item}: {item: IllustrationItemType}) { +function IllustrationItem({item, isCopied = false}: {item: IllustrationItemType, isCopied?: boolean}) { let Illustration = item.Component; let ref = useRef(null); return ( - + {isCopied ? : }
- {item.id} + {isCopied ? 'Copied!' : item.id}
); From 094eff0c153a4513ad91798d0dc20854aa6374c6 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 13 Nov 2025 11:19:04 -0600 Subject: [PATCH 3/9] dynamic search placeholder if resources tag selected --- packages/dev/s2-docs/src/SearchMenu.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/dev/s2-docs/src/SearchMenu.tsx b/packages/dev/s2-docs/src/SearchMenu.tsx index e1674bceb79..640c2b7b256 100644 --- a/packages/dev/s2-docs/src/SearchMenu.tsx +++ b/packages/dev/s2-docs/src/SearchMenu.tsx @@ -275,6 +275,10 @@ export function SearchMenu(props: SearchMenuProps) { {orderedTabs.map((tab, i) => { const tabResourceTags = getResourceTags(tab.id); + const selectedResourceTag = tabResourceTags.find(tag => tag.id === selectedTagId); + const placeholderText = selectedResourceTag + ? `Search ${selectedResourceTag.name}` + : `Search ${tab.label}`; return ( @@ -286,7 +290,7 @@ export function SearchMenu(props: SearchMenuProps) { ref={searchRef} size="L" aria-label={`Search ${tab.label}`} - placeholder={`Search ${tab.label}`} + placeholder={placeholderText} UNSAFE_style={{marginInlineEnd: 296, viewTransitionName: i === 0 ? 'search-menu-search-field' : 'none'} as CSSProperties} styles={style({width: 500})} /> From 5c008faaa0e4bdc43029d3ef414bc81ba5e1be3b Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 13 Nov 2025 12:13:54 -0600 Subject: [PATCH 4/9] silently handle prefetch failures --- packages/dev/s2-docs/src/prefetch.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/dev/s2-docs/src/prefetch.ts b/packages/dev/s2-docs/src/prefetch.ts index fe3425e5e31..033e123c89f 100644 --- a/packages/dev/s2-docs/src/prefetch.ts +++ b/packages/dev/s2-docs/src/prefetch.ts @@ -25,6 +25,9 @@ export function prefetchRoute(pathname: string) { prefetchPromises.delete(rscPath); return Promise.reject(new Error('Prefetch failed')); }); + + // Silently handle prefetch failures + prefetchPromise.catch(() => {}); prefetchPromises.set(rscPath, prefetchPromise); } From 7cf24e022834598d130e941439144bb745377f4d Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 13 Nov 2025 11:24:50 -0800 Subject: [PATCH 5/9] fix focus visible style so it doesnt linger on click --- packages/dev/s2-docs/src/IllustrationCards.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/s2-docs/src/IllustrationCards.tsx b/packages/dev/s2-docs/src/IllustrationCards.tsx index 0367909f749..f475fbd058b 100644 --- a/packages/dev/s2-docs/src/IllustrationCards.tsx +++ b/packages/dev/s2-docs/src/IllustrationCards.tsx @@ -25,7 +25,7 @@ const itemStyle = style({ backgroundColor: { default: 'gray-50', isHovered: 'gray-100', - isFocused: 'gray-100', + isFocusVisible: 'gray-100', isSelected: 'neutral' }, font: 'ui-sm', @@ -122,7 +122,7 @@ function useCopyImport(variant: string, gradientStyle: string) { if (timeout.current) { clearTimeout(timeout.current); } - let importText = variant === 'gradient' ? + let importText = variant === 'gradient' ? `import ${id} from '@react-spectrum/s2/illustrations/gradient/${gradientStyle}/${id}';` : `import ${id} from '@react-spectrum/s2/illustrations/linear/${id}';`; navigator.clipboard.writeText(importText).then(() => { From 9ad5e4cc4a75cbade83ea107cb45b5214ea0dadf Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 13 Nov 2025 14:32:30 -0600 Subject: [PATCH 6/9] fix anchor links --- packages/dev/s2-docs/src/client.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/dev/s2-docs/src/client.tsx b/packages/dev/s2-docs/src/client.tsx index 4a01b62c548..17ba85fdcc0 100644 --- a/packages/dev/s2-docs/src/client.tsx +++ b/packages/dev/s2-docs/src/client.tsx @@ -14,7 +14,7 @@ let isClientLink = (link: HTMLAnchorElement, pathname: string) => { link.href && (!link.target || link.target === '_self') && link.origin === location.origin && - link.pathname !== location.pathname && + (link.pathname !== location.pathname || link.hash) && !link.hasAttribute('download') && link.pathname.startsWith(pathname) ); @@ -36,7 +36,23 @@ let currentAbortController: AbortController | null = null; // and in a React transition, stream in the new page. Once complete, we'll pushState to // update the URL in the browser. async function navigate(pathname: string, push = false) { - let [basePath] = pathname.split('#'); + let [basePath, pathAnchor] = pathname.split('#'); + let currentPath = location.pathname; + let isSamePageAnchor = (!basePath || basePath === currentPath) && pathAnchor; + + if (isSamePageAnchor) { + if (push) { + history.pushState(null, '', pathname); + } + + // Scroll to the anchor + let element = document.getElementById(pathAnchor); + if (element) { + element.scrollIntoView(); + } + return; + } + let rscPath = basePath.replace('.html', '.rsc'); // Cancel any in-flight navigation From 0b1a3094df4099aa38baec8f1b42c316d86a5bd5 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 13 Nov 2025 15:09:59 -0600 Subject: [PATCH 7/9] don't clear search field when switching library tabs --- packages/dev/s2-docs/src/SearchMenu.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/dev/s2-docs/src/SearchMenu.tsx b/packages/dev/s2-docs/src/SearchMenu.tsx index 640c2b7b256..46d1ba3d306 100644 --- a/packages/dev/s2-docs/src/SearchMenu.tsx +++ b/packages/dev/s2-docs/src/SearchMenu.tsx @@ -186,9 +186,6 @@ export function SearchMenu(props: SearchMenuProps) { ); const handleTabSelectionChange = React.useCallback((key: Key) => { - if (searchValue) { - setSearchValue(''); - } setSelectedLibrary(key as typeof selectedLibrary); // Focus main search field of the newly selected tab setTimeout(() => { @@ -198,7 +195,7 @@ export function SearchMenu(props: SearchMenuProps) { searchRef.current.focus(); } }, 10); - }, [searchValue]); + }, []); const handleSectionSelectionChange = React.useCallback((keys: Iterable) => { const firstKey = Array.from(keys)[0] as string; From de9f7652d2dc5ea55c8f8f3830fb56ab83f26c17 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 13 Nov 2025 15:25:47 -0600 Subject: [PATCH 8/9] fix anchor links on page load --- packages/dev/s2-docs/src/client.tsx | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/dev/s2-docs/src/client.tsx b/packages/dev/s2-docs/src/client.tsx index 17ba85fdcc0..d58afb25a64 100644 --- a/packages/dev/s2-docs/src/client.tsx +++ b/packages/dev/s2-docs/src/client.tsx @@ -243,3 +243,35 @@ document.addEventListener('click', e => { window.addEventListener('popstate', () => { navigate(location.pathname + location.search + location.hash); }); + +function scrollToCurrentHash() { + if (!location.hash || location.hash === '#') { + return; + } + + let anchorId = location.hash.slice(1); + try { + anchorId = decodeURIComponent(anchorId); + } catch { + // Fall back to raw hash + } + + if (!anchorId) { + return; + } + + requestAnimationFrame(() => { + let element = document.getElementById(anchorId); + if (element) { + element.scrollIntoView(); + } + }); +} + +if (document.readyState === 'complete' || document.readyState === 'interactive') { + scrollToCurrentHash(); +} else { + window.addEventListener('DOMContentLoaded', () => { + scrollToCurrentHash(); + }, {once: true}); +} From 7d81fcfb2f25b1dedbeea65e8415dc6f21a99e6b Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 13 Nov 2025 17:16:46 -0600 Subject: [PATCH 9/9] better cards for releases --- packages/dev/s2-docs/src/ComponentCard.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/dev/s2-docs/src/ComponentCard.tsx b/packages/dev/s2-docs/src/ComponentCard.tsx index 407bd5eb7dc..91f6afaa63c 100644 --- a/packages/dev/s2-docs/src/ComponentCard.tsx +++ b/packages/dev/s2-docs/src/ComponentCard.tsx @@ -266,6 +266,14 @@ function getDefaultIllustration(href: string) { return AdobeDefaultSvg; } +function getReleaseVersionLabel(href: string) { + let match = href.match(/releases\/(v[\w-]+)\.html$/i); + if (!match) { + return null; + } + return match[1].replace(/-/g, '.'); +} + interface ComponentCardProps extends Omit { name: string, href: string, @@ -276,7 +284,14 @@ export function ComponentCard({id, name, href, description, size, ...otherProps} let IllustrationComponent = componentIllustrations[name] || getDefaultIllustration(href); let overrides = propOverrides[name] || {}; let preview; - if (href.includes('react-aria/examples/') && !href.endsWith('index.html')) { + let releaseVersion = getReleaseVersionLabel(href); + if (releaseVersion) { + preview = ( +
+ {releaseVersion} +
+ ); + } else if (href.includes('react-aria/examples/') && !href.endsWith('index.html')) { preview = ; } else { preview = (