Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2c7e674
close SearchMenu on client navigation
reidbarber Nov 5, 2025
5cb61c3
add footer
reidbarber Nov 6, 2025
33f0724
made Nav and ToC widths static
reidbarber Nov 6, 2025
35069c4
fix min-height issue on mobile
reidbarber Nov 6, 2025
5e84727
show Toast if route can't load
reidbarber Nov 6, 2025
9122573
add toast for clipboard copy failures
reidbarber Nov 6, 2025
45ec5f7
fix React insertion effect
reidbarber Nov 6, 2025
81f27fb
improve markdown menu
reidbarber Nov 6, 2025
c35adb7
add skeleton loading for client routing
reidbarber Nov 6, 2025
c7b2c20
copy update
reidbarber Nov 6, 2025
217f3df
Merge remote-tracking branch 'origin/main' into s2-docs-general-impro…
reidbarber Nov 6, 2025
df6fe71
remote extra startTransitions and use use hook
reidbarber Nov 7, 2025
2bbe51f
improve skeleton loading and optimistic render link selection + ToC
reidbarber Nov 7, 2025
93dca54
add delay to showing skeleton
reidbarber Nov 7, 2025
9338497
fix getPageInfo logic
reidbarber Nov 7, 2025
dc3309a
try fixing getPageInfo again
reidbarber Nov 7, 2025
25297d3
try fixing normalizePathname on build
reidbarber Nov 7, 2025
aa6ba15
add prefetch onPressStart
reidbarber Nov 7, 2025
9cd4e02
revert optimistic UI and show error toast if fetch fails
reidbarber Nov 7, 2025
fbd63dd
fix skeleton title
reidbarber Nov 10, 2025
31683a3
don't clear targetPathname until new navigation
reidbarber Nov 10, 2025
4970161
move prefetch to a global pointerover listener
reidbarber Nov 10, 2025
cf27fd3
close search menu when navigation starts
reidbarber Nov 10, 2025
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
10 changes: 7 additions & 3 deletions packages/dev/s2-docs/src/CodePlatter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import {ActionButton, ActionButtonGroup, Button, ButtonGroup, Content, createIcon, Dialog, DialogContainer, Heading, Link, Menu, MenuItem, MenuTrigger, Text, Tooltip, TooltipTrigger} from '@react-spectrum/s2';
import {ActionButton, ActionButtonGroup, Button, ButtonGroup, Content, createIcon, Dialog, DialogContainer, Heading, Link, Menu, MenuItem, MenuTrigger, Text, UNSTABLE_ToastQueue as ToastQueue, Tooltip, TooltipTrigger} from '@react-spectrum/s2';
import {CopyButton} from './CopyButton';
import {createCodeSandbox, getCodeSandboxFiles} from './CodeSandbox';
import {createStackBlitz} from './StackBlitz';
Expand Down Expand Up @@ -97,7 +97,9 @@ export function CodePlatter({children, shareUrl, files, type, registryUrl, showC
if (node instanceof HTMLHeadingElement && node.id) {
url.hash = '#' + node.id;
}
navigator.clipboard.writeText(url.toString());
navigator.clipboard.writeText(url.toString()).catch(() => {
ToastQueue.negative('Failed to copy link.');
});
}}>
<LinkIcon />
<Text slot="label">Copy link</Text>
Expand Down Expand Up @@ -265,7 +267,9 @@ function ShadcnDialog({registryUrl}) {
<Button
variant="accent"
onPress={() => {
navigator.clipboard.writeText(preRef.current!.textContent!);
navigator.clipboard.writeText(preRef.current!.textContent!).catch(() => {
ToastQueue.negative('Failed to copy command. Please try again.');
});
close();
}}>
Copy and close
Expand Down
4 changes: 2 additions & 2 deletions packages/dev/s2-docs/src/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import {ActionButton, Tooltip, TooltipTrigger} from '@react-spectrum/s2';
import {ActionButton, UNSTABLE_ToastQueue as ToastQueue, Tooltip, TooltipTrigger} from '@react-spectrum/s2';
import CheckmarkCircle from '@react-spectrum/s2/icons/CheckmarkCircle';
import Copy from '@react-spectrum/s2/icons/Copy';
import React, {useEffect, useRef, useState} from 'react';
Expand Down Expand Up @@ -44,7 +44,7 @@ export function CopyButton({text, getText, ariaLabel = 'Copy', tooltip = 'Copy',
setIsCopied(true);
timeout.current = setTimeout(() => setIsCopied(false), 2000);
}).catch(() => {
// noop
ToastQueue.negative('Failed to copy.');
});
};

Expand Down
2 changes: 0 additions & 2 deletions packages/dev/s2-docs/src/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import BetaApp from '@react-spectrum/s2/icons/BetaApp';
import {flushSync} from 'react-dom';
import {getLibraryFromPage, getLibraryIcon, getLibraryLabel} from './library';
import GithubLogo from './icons/GithubLogo';
import {MarkdownMenu} from './MarkdownMenu';
// @ts-ignore
import {PageProps} from '@parcel/rsc';
import React, {CSSProperties, useId, useState} from 'react';
Expand Down Expand Up @@ -115,7 +114,6 @@ export default function Header(props: PageProps) {
<BetaApp />
<Text>Beta Preview</Text>
</Badge>
<MarkdownMenu url={currentPage.url} />
<ActionButton aria-label="React Spectrum GitHub repo" size="L" isQuiet onPress={() => window.open('https://github.com/adobe/react-spectrum', '_blank', 'noopener,noreferrer')}>
<GithubLogo />
</ActionButton>
Expand Down
4 changes: 2 additions & 2 deletions packages/dev/s2-docs/src/IconSearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import {Autocomplete, GridLayout, ListBox, ListBoxItem, Size, useFilter, Virtualizer} from 'react-aria-components';
import CheckmarkCircle from '@react-spectrum/s2/icons/CheckmarkCircle';
import Close from '@react-spectrum/s2/icons/Close';
import {Content, Heading, IllustratedMessage, pressScale, SearchField, Skeleton, Text} from '@react-spectrum/s2';
import {Content, Heading, IllustratedMessage, pressScale, SearchField, Skeleton, Text, UNSTABLE_ToastQueue as ToastQueue} from '@react-spectrum/s2';
import {focusRing, iconStyle, style} from '@react-spectrum/s2/style' with {type: 'macro'};
import {iconAliases} from './iconAliases.js';
// @ts-ignore
Expand Down Expand Up @@ -50,7 +50,7 @@ export function useCopyImport() {
setCopiedId(id);
timeout.current = setTimeout(() => setCopiedId(null), 2000);
}).catch(() => {
// noop
ToastQueue.negative('Failed to copy import statement.');
});
}, []);

Expand Down
4 changes: 2 additions & 2 deletions packages/dev/s2-docs/src/IllustrationCards.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import {Autocomplete, GridLayout, ListBox, ListBoxItem, Size, useFilter, Virtualizer} from 'react-aria-components';
import {Content, Heading, IllustratedMessage, pressScale, ProgressCircle, Radio, RadioGroup, SearchField, SegmentedControl, SegmentedControlItem, Text} from '@react-spectrum/s2';
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'};
// @ts-ignore
import Gradient from '@react-spectrum/s2/icons/Gradient';
Expand Down Expand Up @@ -100,7 +100,7 @@ let handleCopyImport = (id: string, variant: string, gradientStyle: string) => {
navigator.clipboard.writeText(importText).then(() => {
// noop
}).catch(() => {
// noop
ToastQueue.negative('Failed to copy import statement.');
});
};

Expand Down
230 changes: 121 additions & 109 deletions packages/dev/s2-docs/src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {ExampleList} from './ExampleList';
import {MobileOnPageNav, Nav, OnPageNav, SideNav, SideNavItem, SideNavLink} from '../src/Nav';
import type {Page, PageProps, TocNode} from '@parcel/rsc';
import {Nav, PendingPageProvider} from '../src/Nav';
import {OptimisticMobileToc, OptimisticToc} from './OptimisticToc';
import type {Page, PageProps} from '@parcel/rsc';
import React, {ReactElement} from 'react';
// @ts-ignore
import '../src/client';
Expand All @@ -9,11 +10,13 @@ import internationalizedFavicon from 'url:../assets/internationalized.ico';
// @ts-ignore
import reactAriaFavicon from 'url:../assets/react-aria.ico';
import './anatomy.css';
import './footer.css';
import ChevronRightIcon from '@react-spectrum/s2/icons/ChevronRight';
import {ClassAPI} from './ClassAPI';
import {Code} from './Code';
import {CodeBlock} from './CodeBlock';
import {CodePlatterProvider} from './CodePlatter';
import {Divider, Provider, UNSTABLE_ToastContainer as ToastContainer} from '@react-spectrum/s2';
import {ExampleSwitcher} from './ExampleSwitcher';
import {getLibraryFromPage, getLibraryFromUrl, getLibraryLabel} from './library';
import {getTextWidth} from './textWidth';
Expand All @@ -22,7 +25,7 @@ import Header from './Header';
import {iconStyle, style} from '@react-spectrum/s2/style' with {type: 'macro'};
import {Link, TitleLink} from './Link';
import {MobileHeader} from './MobileHeader';
import {PickerItem, Provider} from '@react-spectrum/s2';
import {NavigationSuspense} from './NavigationSuspense';
import {PropTable} from './PropTable';
import {StateTable} from './StateTable';
import {TypeLink} from './types';
Expand Down Expand Up @@ -77,10 +80,6 @@ const subPageComponents = (previousPage?: Page) => ({
)
});

function anchorId(children) {
return children.replace(/\s/g, '-').replace(/[^a-zA-Z0-9-_]/g, '').toLowerCase();
}

const getTitle = (currentPage: Page): string => {
const explicitTitle = (currentPage as any).pageTitle || currentPage.exports?.pageTitle;
if (explicitTitle && explicitTitle !== currentPage.tableOfContents?.[0]?.title && explicitTitle !== currentPage.name) {
Expand Down Expand Up @@ -139,6 +138,36 @@ let articleStyles = style({
height: 'fit'
});

function Footer() {
const year = new Date().getFullYear();
return (
<footer
className={style({
marginTop: 32,
paddingY: 12
})}>
<Divider size="S" />
<ul
className={style({
display: 'flex',
justifyContent: 'end',
flexWrap: 'wrap',
paddingX: 12,
margin: 0,
marginTop: 16,
font: 'body-2xs',
listStyleType: 'none'
})}>
<li>Copyright © {year} Adobe. All rights reserved.</li>
<li><Link isQuiet href="//www.adobe.com/privacy.html" variant="secondary">Privacy</Link></li>
<li><Link isQuiet href="//www.adobe.com/legal/terms.html" variant="secondary">Terms of Use</Link></li>
<li><Link isQuiet href="//www.adobe.com/privacy/cookies.html" variant="secondary">Cookies</Link></li>
<li><Link isQuiet href="//www.adobe.com/privacy/ca-rights.html" variant="secondary">Do not sell my personal information</Link></li>
</ul>
</footer>
);
}

export function Layout(props: PageProps & {children: ReactElement<any>}) {
let {pages, currentPage, children} = props;
let hasToC = !currentPage.exports?.hideNav && currentPage.tableOfContents?.[0]?.children && currentPage.tableOfContents?.[0]?.children?.length > 0;
Expand Down Expand Up @@ -218,121 +247,104 @@ export function Layout(props: PageProps & {children: ReactElement<any>}) {
gap: {
default: 0,
lg: 12
},
minHeight: {
default: 'screen',
lg: 'auto'
}
})}>
<Header pages={pages} currentPage={currentPage} />
<MobileHeader
toc={(currentPage.tableOfContents?.[0]?.children?.length ?? 0) > 1 ? <MobileToc key="toc" toc={currentPage.tableOfContents ?? []} currentPage={currentPage} /> : null}
pages={pages}
currentPage={currentPage} />
<div className={style({display: 'flex', width: 'full'})}>
{currentPage.exports?.hideNav ? null : <Nav pages={pages} currentPage={currentPage} />}
<main
key={currentPage.url}
style={{borderBottomLeftRadius: 0, borderBottomRightRadius: 0}}
className={style({
isolation: 'isolate',
backgroundColor: 'base',
padding: {
default: 12,
lg: 40
},
borderRadius: {
default: 'none',
lg: 'xl'
},
boxShadow: {
lg: 'emphasized'
},
width: 'full',
boxSizing: 'border-box',
flexGrow: 1,
display: 'flex',
justifyContent: 'space-between',
position: 'relative',
height: {
lg: '[calc(100vh - 72px)]'
},
overflow: {
lg: 'auto'
}
})}>
<CodePlatterProvider library={getLibraryFromUrl(currentPage.url)}>
<article
className={articleStyles({isWithToC: hasToC})}>
{currentPage.exports?.version && <VersionBadge version={currentPage.exports.version} />}
{React.cloneElement(children, {
components: isSubpage ?
subPageComponents(parentPage) :
components,
pages
})}
</article>
</CodePlatterProvider>
{hasToC && <aside
<PendingPageProvider currentPage={currentPage}>
<MobileHeader
toc={<OptimisticMobileToc currentPage={currentPage} />}
pages={pages}
currentPage={currentPage} />
<div className={style({display: 'flex', width: 'full', flexGrow: {default: 1, lg: 0}})}>
{currentPage.exports?.hideNav ? null : <Nav pages={pages} currentPage={currentPage} />}
<main
key={currentPage.url}
style={{borderBottomLeftRadius: 0, borderBottomRightRadius: 0}}
className={style({
position: 'sticky',
top: 0,
height: 'fit',
maxHeight: 'screen',
overflow: 'auto',
paddingY: 32,
boxSizing: 'border-box',
display: {
isolation: 'isolate',
backgroundColor: 'base',
padding: {
default: 12,
lg: 40
},
borderRadius: {
default: 'none',
lg: 'block'
lg: 'xl'
},
boxShadow: {
lg: 'emphasized'
},
width: 'full',
boxSizing: 'border-box',
flexGrow: 1,
display: 'flex',
justifyContent: 'space-between',
position: 'relative',
height: {
lg: '[calc(100vh - 72px)]'
},
overflow: {
lg: 'auto'
}
})}>
<div className={style({font: 'title', minHeight: 32, paddingX: 12, display: 'flex', alignItems: 'center'})}>Contents</div>
<Toc toc={currentPage.tableOfContents?.[0]?.children ?? []} />
</aside>}
</main>
</div>
<div
className={style({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
width: 'full'
})}>
<CodePlatterProvider library={getLibraryFromUrl(currentPage.url)}>
<NavigationSuspense pages={pages}>
<article
className={articleStyles({isWithToC: hasToC})}>
{currentPage.exports?.version && <VersionBadge version={currentPage.exports.version} />}
{React.cloneElement(children, {
components: isSubpage ?
subPageComponents(parentPage) :
components,
pages
})}
</article>
</NavigationSuspense>
</CodePlatterProvider>
<Footer />
</div>
{hasToC && <aside
className={style({
position: 'sticky',
top: 0,
height: {
default: 'fit',
lg: '[calc(100vh - 72px)]'
},
paddingY: 32,
paddingX: 4,
boxSizing: 'border-box',
width: 180,
flexShrink: 0,
display: {
default: 'none',
lg: 'flex'
},
flexDirection: 'column'
})}>
<OptimisticToc currentPage={currentPage} />
</aside>}
</main>
</div>
</PendingPageProvider>
</div>
<ToastContainer placement="bottom" />
</body>
</Provider>
);
}

function Toc({toc}) {
return (
<OnPageNav>
<SideNav>
{toc.map((c, i) => (
<SideNavItem key={i}>
<SideNavLink href={'#' + anchorId(c.title)}>{c.title}</SideNavLink>
{c.children.length > 0 && <Toc toc={c.children} />}
</SideNavItem>
))}
</SideNav>
</OnPageNav>
);
}

function MobileToc({toc, currentPage}) {
return (
<MobileOnPageNav currentPage={currentPage}>
{renderMobileToc(toc)}
</MobileOnPageNav>
);
}

function renderMobileToc(toc: TocNode[], seen = new Map()) {
return toc.map((c) => {
let href = c.level === 1 ? '#top' : '#' + anchorId(c.title);
if (seen.has(href)) {
seen.set(href, seen.get(href) + 1);
href += '-' + seen.get(href);
} else {
seen.set(href, 1);
}
return (<React.Fragment key={href}>
<PickerItem id={href} href={href}>{c.title}</PickerItem>
{c.children.length > 0 && renderMobileToc(c.children, seen)}
</React.Fragment>);
});
}

export function Time({date}: {date: string}) {
let dateObj = new Date(date);
return (
Expand Down
Loading