diff --git a/packages/dev/s2-docs/pages/error.mdx b/packages/dev/s2-docs/pages/error.mdx new file mode 100644 index 00000000000..82201a6dc99 --- /dev/null +++ b/packages/dev/s2-docs/pages/error.mdx @@ -0,0 +1,9 @@ +import Error from '../src/Error'; +import {Layout} from '../src/Layout'; +export default Layout; + +import docs from 'docs:@react-spectrum/s2'; + +export const hideFromSearch = true; + + diff --git a/packages/dev/s2-docs/pages/s2/DropZone.mdx b/packages/dev/s2-docs/pages/s2/DropZone.mdx index b5c3612a2ff..bd60edf18d4 100644 --- a/packages/dev/s2-docs/pages/s2/DropZone.mdx +++ b/packages/dev/s2-docs/pages/s2/DropZone.mdx @@ -21,13 +21,13 @@ function Example(props) { return ( ( ['text/plain', 'image/jpeg', 'image/png', 'image/gif'].some(t => types.has(t)) - ? 'copy' + ? 'copy' : 'cancel' )} onDrop={async (event) => { diff --git a/packages/dev/s2-docs/pages/s2/Image.mdx b/packages/dev/s2-docs/pages/s2/Image.mdx index 4134048208e..cee1784e3d8 100644 --- a/packages/dev/s2-docs/pages/s2/Image.mdx +++ b/packages/dev/s2-docs/pages/s2/Image.mdx @@ -56,7 +56,7 @@ import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
diff --git a/packages/dev/s2-docs/pages/s2/Popover.mdx b/packages/dev/s2-docs/pages/s2/Popover.mdx index 637d4955be2..ae163d379f7 100644 --- a/packages/dev/s2-docs/pages/s2/Popover.mdx +++ b/packages/dev/s2-docs/pages/s2/Popover.mdx @@ -11,34 +11,27 @@ export const tags = ['popup', 'overlay']; ```tsx render docs={docs.exports.Popover} links={docs.links} props={['placement', 'size', 'offset', 'crossOffset', 'shouldFlip', 'hideArrow']} type="s2" initialProps={{size: 'S'}} "use client"; -import {Popover, DialogTrigger, ActionButton, CheckboxGroup, Checkbox, Form} from '@react-spectrum/s2'; -import Filter from '@react-spectrum/s2/icons/Filter'; +import {Popover, DialogTrigger, ActionButton, Form, TextField, Switch, Button} from '@react-spectrum/s2'; +import Feedback from '@react-spectrum/s2/icons/Feedback'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; function Example(props) { return ( - - + + {/*- begin focus -*/} -
- - Non-stop - 1 stop - 2+ stops - - - Carry on - Checked bag - - - Morning - Afternoon - Evening - -
+
+

How are we doing? Share your feedback here.

+
+ + + Adobe can contact me for further questions concerning this feedback + + +
{/*- end focus -*/}
@@ -65,17 +58,17 @@ function Example() { -
Popover appears here
- + + + Error 404: Page not found + This page isn't available. Try checking the URL or visit a different page. + +
+ ); +} diff --git a/packages/dev/s2-docs/src/Layout.tsx b/packages/dev/s2-docs/src/Layout.tsx index bd0df3ec68c..4a1d57ec982 100644 --- a/packages/dev/s2-docs/src/Layout.tsx +++ b/packages/dev/s2-docs/src/Layout.tsx @@ -74,8 +74,19 @@ const getDescription = (currentPage: Page): string => { return library ? `Documentation for ${pageTitle} in ${library}.` : `Documentation for ${pageTitle}.`; }; +let articleStyles = style({ + maxWidth: { + default: 'none', + isWithToC: 768 + }, + width: 'full', + height: 'fit' +}); + + export function Layout(props: PageProps & {children: ReactElement}) { let {pages, currentPage, children} = props; + let hasToC = currentPage.tableOfContents?.[0]?.children && currentPage.tableOfContents[0].children.length > 0; return ( @@ -120,12 +131,12 @@ export function Layout(props: PageProps & {children: ReactElement}) { })}>
0 ? : null} + toc={(currentPage.tableOfContents?.[0]?.children?.length ?? 0) > 0 ? : null} pages={pages} currentPage={currentPage} />
@@ -205,9 +212,9 @@ function Toc({toc}) { ); } -function MobileToc({toc}) { +function MobileToc({toc, currentPage}) { return ( - + {renderMobileToc(toc)} ); diff --git a/packages/dev/s2-docs/src/MobileHeader.tsx b/packages/dev/s2-docs/src/MobileHeader.tsx index 9a4733b4690..dc8e34a8c6f 100644 --- a/packages/dev/s2-docs/src/MobileHeader.tsx +++ b/packages/dev/s2-docs/src/MobileHeader.tsx @@ -1,12 +1,13 @@ 'use client'; import {ActionButton, DialogTrigger} from '@react-spectrum/s2'; -import {AdobeLogo} from './icons/AdobeLogo'; +import {getLibraryFromPage} from './library'; import {keyframes} from '../../../@react-spectrum/s2/style/style-macro' with {type: 'macro'}; import MenuHamburger from '@react-spectrum/s2/icons/MenuHamburger'; import {Modal} from '../../../@react-spectrum/s2/src/Modal'; import React, {CSSProperties, lazy, useEffect, useRef} from 'react'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; +import {TAB_DEFS} from './constants'; const MobileSearchMenu = lazy(() => import('./SearchMenu').then(({MobileSearchMenu}) => ({default: MobileSearchMenu}))); @@ -80,6 +81,9 @@ export function MobileHeader({toc, pages, currentPage}) { } }, []); + let currentLibrary = getLibraryFromPage(currentPage); + let icon = TAB_DEFS[currentLibrary].icon; + return (
- -

- React Aria + {TAB_DEFS[currentLibrary].label}

{toc && ( diff --git a/packages/dev/s2-docs/src/MobileSearchMenu.tsx b/packages/dev/s2-docs/src/MobileSearchMenu.tsx index eb340150f44..33051304804 100644 --- a/packages/dev/s2-docs/src/MobileSearchMenu.tsx +++ b/packages/dev/s2-docs/src/MobileSearchMenu.tsx @@ -9,9 +9,10 @@ import {type Library, TAB_DEFS} from './constants'; import NoSearchResults from '@react-spectrum/s2/illustrations/linear/NoSearchResults'; import {OverlayTriggerStateContext, Provider, Dialog as RACDialog, DialogProps as RACDialogProps, Tab as RACTab, TabList as RACTabList, TabPanel as RACTabPanel, TabPanelProps as RACTabPanelProps, TabProps as RACTabProps, Tabs as RACTabs, SelectionIndicator, TabRenderProps} from 'react-aria-components'; import type {PageProps} from '@parcel/rsc'; -import React, {ReactNode, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {useId} from '@react-aria/utils'; + interface MobileDialogProps extends Omit { size?: 'S' | 'M' | 'L' | 'fullscreen' | 'fullscreenTakeover', isDismissible?: boolean, @@ -208,7 +209,7 @@ const MobileCustomDialog = function MobileCustomDialog(props: MobileDialogProps) }; function MobileNav({pages, currentPage}: PageProps) { - let overlayTriggerState = React.useContext(OverlayTriggerStateContext); + let overlayTriggerState = useContext(OverlayTriggerStateContext); let [searchFocused, setSearchFocused] = useState(false); let [searchValue, setSearchValue] = useState(''); let [selectedSection, setSelectedSection] = useState(undefined); @@ -218,7 +219,7 @@ function MobileNav({pages, currentPage}: PageProps) { let getSectionsForLibrary = useCallback((libraryId: string) => { let sectionsMap = new Map(); - let filteredPages = pages.filter(page => getLibraryFromPage(page) === libraryId); + let filteredPages = pages.filter(page => getLibraryFromPage(page) === libraryId && !page.exports?.hideFromSearch); for (let page of filteredPages) { let section = page.exports?.section ?? 'Components'; let sectionPages = sectionsMap.get(section) ?? []; @@ -243,7 +244,7 @@ function MobileNav({pages, currentPage}: PageProps) { }); return sectionArray; }, [getSectionsForLibrary, selectedLibrary]); - + useEffect(() => { // Auto-select first section initially or when library changes @@ -296,9 +297,9 @@ function MobileNav({pages, currentPage}: PageProps) { if (!searchValue.trim()) { return pages; } - + let searchLower = searchValue.toLowerCase(); - + // Filter items where name or tags start with search value let matchedPages = pages.filter(page => { let pageTitle = title(page).toLowerCase(); @@ -307,19 +308,19 @@ function MobileNav({pages, currentPage}: PageProps) { let tagMatch = tags.some(tag => tag.toLowerCase().startsWith(searchLower)); return nameMatch || tagMatch; }); - + // Sort to prioritize name matches over tag matches return matchedPages.sort((a, b) => { let aNameMatch = title(a).toLowerCase().startsWith(searchLower); let bNameMatch = title(b).toLowerCase().startsWith(searchLower); - + if (aNameMatch && !bNameMatch) { return -1; } if (!aNameMatch && bNameMatch) { return 1; } - + // If both match by name or both match by tag, maintain original order return 0; }); @@ -328,9 +329,9 @@ function MobileNav({pages, currentPage}: PageProps) { let getSectionContent = (sectionName: string, libraryId: string, searchValue: string = ''): ComponentCardItem[] => { let librarySections = getSectionsForLibrary(libraryId); let pages = librarySections.get(sectionName) ?? []; - + let filteredPages = filterPages(pages, searchValue); - + return filteredPages .sort((a, b) => title(a).localeCompare(title(b))) .map(page => ({id: page.url.replace(/^\//, ''), name: title(page), href: page.url})); @@ -355,13 +356,13 @@ function MobileNav({pages, currentPage}: PageProps) { } else { items = getSectionContent(section, libraryId, searchValue); } - + // Sort to show "Introduction" first when search is empty if (searchValue.trim().length === 0) { items = [...items].sort((a, b) => { const aIsIntro = a.name === 'Introduction'; const bIsIntro = b.name === 'Introduction'; - + if (aIsIntro && !bIsIntro) { return -1; } @@ -371,14 +372,14 @@ function MobileNav({pages, currentPage}: PageProps) { return 0; }); } - + return items; }; let getSectionNamesForLibrary = (libraryId: string) => { let librarySections = getSectionsForLibrary(libraryId); let sectionArray = [...librarySections.keys()]; - + // Show 'Components' first sectionArray.sort((a, b) => { if (a === 'Components') { @@ -389,7 +390,7 @@ function MobileNav({pages, currentPage}: PageProps) { } return a.localeCompare(b); }); - + return sectionArray; }; @@ -464,8 +465,8 @@ function MobileNav({pages, currentPage}: PageProps) { {libraries.map(library => (
-
{ let elements = Array.from(document.querySelectorAll('article > :is(h1,h2,h3,h4,h5)')); elements.reverse(); - let visible = new Set(); let observer = new IntersectionObserver(entries => { for (let entry of entries) { @@ -225,7 +223,7 @@ export function MobileOnPageNav({children}) { visible.delete(entry.target); } } - + let lastVisible = elements.find(e => visible.has(e)); if (lastVisible) { setSelected('#' + lastVisible.id!); @@ -244,7 +242,7 @@ export function MobileOnPageNav({children}) { } return () => observer.disconnect(); - }, []); + }, [currentPage]); return ( diff --git a/packages/dev/s2-docs/src/SearchMenu.tsx b/packages/dev/s2-docs/src/SearchMenu.tsx index e49cd6ff416..313550cfdfa 100644 --- a/packages/dev/s2-docs/src/SearchMenu.tsx +++ b/packages/dev/s2-docs/src/SearchMenu.tsx @@ -71,7 +71,7 @@ export function SearchMenu(props: SearchMenuProps) { } const components = pages - .filter(page => page.url && page.url.endsWith('.html') && getLibraryFromUrl(page.url) === selectedLibrary) + .filter(page => page.url && page.url.endsWith('.html') && getLibraryFromUrl(page.url) === selectedLibrary && !page.exports?.hideFromSearch) .map(page => { const name = page.url.replace(/^\//, '').replace(/\.html$/, ''); const title = page.tableOfContents?.[0]?.title || name; diff --git a/packages/dev/s2-docs/src/VisualExample.tsx b/packages/dev/s2-docs/src/VisualExample.tsx index ca002640e26..c22d18533de 100644 --- a/packages/dev/s2-docs/src/VisualExample.tsx +++ b/packages/dev/s2-docs/src/VisualExample.tsx @@ -56,7 +56,7 @@ const exampleStyle = style({ const controlsStyle = style({ display: 'grid', gridTemplateColumns: { - default: 'repeat(auto-fit, minmax(130px, 1fr))', + default: 'repeat(auto-fit, minmax(200px, 1fr))', lg: ['1fr'] }, gridAutoFlow: 'dense', diff --git a/packages/dev/s2-docs/src/client.tsx b/packages/dev/s2-docs/src/client.tsx index 4471c218992..a7b923e4f01 100644 --- a/packages/dev/s2-docs/src/client.tsx +++ b/packages/dev/s2-docs/src/client.tsx @@ -12,16 +12,38 @@ let updateRoot = hydrate({ }); // A very simple router. When we navigate, we'll fetch a new RSC payload from the server, -// and in a React transition, stream in the new page. Once complete, we'll pushState to +// 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 res = fetchRSC(pathname.replace('.html', '.rsc')); - updateRoot(res, () => { - if (push) { - history.pushState(null, '', pathname); - push = false; - } - }); + try { + let res = await fetchRSC(pathname.replace('.html', '.rsc')); + let currentPath = location.pathname; + let [newBasePath, newPathAnchor] = pathname.split('#'); + + updateRoot(res, () => { + if (push) { + history.pushState(null, '', pathname); + push = false; + } + + // Reset scroll if navigating to a different page without an anchor + if (currentPath !== newBasePath && !newPathAnchor) { + window.scrollTo(0, 0); + } else if (newPathAnchor) { + let element = document.getElementById(newPathAnchor); + if (element) { + element.scrollIntoView(); + } + } + }); + } catch { + let errorRes = await fetchRSC('/error.rsc'); + updateRoot(errorRes, () => { + if (push) { + history.pushState(null, '', '/error.html'); + } + }); + } } // Intercept link clicks to perform RSC navigation. diff --git a/packages/dev/s2-docs/src/icons/InternationalizedLogo.tsx b/packages/dev/s2-docs/src/icons/InternationalizedLogo.tsx index 5669b147dec..85969944894 100644 --- a/packages/dev/s2-docs/src/icons/InternationalizedLogo.tsx +++ b/packages/dev/s2-docs/src/icons/InternationalizedLogo.tsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, {useId} from 'react'; export const InternationalizedLogo = ({size = 32}) => { + const clipPathId = `internationalized-logo-clip-${useId()}`; return ( { viewBox="15 17 135 128" style={{width: size, height: size}}> - + - + @@ -22,7 +23,7 @@ export const InternationalizedLogo = ({size = 32}) => { { { + const clipPathId = `react-aria-logo-clip-${useId()}`; + return ( - + { }} />