Skip to content

Commit ea3b68b

Browse files
authored
docs: S2 docs fixes from testing (#9144)
* fix llms.txt link * hide mobile ToC if there is just one section * add copy feedback to icons search * add message to press icon items to copy * lint * fix llms.txt link to open in new tab * remove Archived releases header link * fix overflow on useDrop page * increase text size for info message * dedupe icons search code where possible
1 parent f1aafc6 commit ea3b68b

File tree

7 files changed

+120
-89
lines changed

7 files changed

+120
-89
lines changed

packages/dev/s2-docs/pages/react-aria/getting-started.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {ShadcnCommand} from '../../src/ShadcnCommand';
99
import {StarterKits} from '../../src/StarterKits';
1010
import docs from 'docs:react-aria-components';
1111
import '../../tailwind/tailwind.css';
12+
import {Link} from '@react-spectrum/s2';
1213

1314
export const section = 'Getting started';
1415
export const tags = ['introduction', 'installation'];
@@ -73,7 +74,7 @@ If you're building a full component library, download a pre-built [Storybook](ht
7374

7475
### Working with AI
7576

76-
Use the menu at the top of each page in the docs to open or copy it into your favorite AI assistant. We also have an [MCP server](mcp.html) which can be used directly in your IDE, and [llms.txt](../llms.txt) which can help AI agents navigate the docs.
77+
Use the menu at the top of each page in the docs to open or copy it into your favorite AI assistant. We also have an [MCP server](mcp.html) which can be used directly in your IDE, and <Link href="llms.txt" target="_blank">lms.txt</Link> which can help AI agents navigate the docs.
7778

7879
## Build a component from scratch
7980

packages/dev/s2-docs/pages/s2/Icons.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Layout} from '../../src/Layout';
22
import {InstallCommand} from '../../src/InstallCommand';
33
import {Command} from '../../src/Command';
4-
import {IconCards} from '../../src/IconSearchView';
4+
import {IconsPageSearch} from '../../src/IconSearchView';
55
import {IconColors} from '../../src/IconColors';
66
import {IconSizes} from '../../src/IconSizes';
77
import {InlineAlert, Heading, Content} from '@react-spectrum/s2';
@@ -24,7 +24,7 @@ import Edit from "@react-spectrum/s2/icons/Edit";
2424

2525
## Available icons
2626

27-
<IconCards />
27+
<IconsPageSearch />
2828

2929
## API
3030

packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -727,13 +727,13 @@ function remarkDocsComponentsToMarkdown() {
727727
}
728728

729729
// Render an unordered list of icon names.
730-
if (name === 'IconCards') {
730+
if (name === 'IconsPageSearch') {
731731
const iconList = getIconNames();
732732
const listMarkdown = iconList.length
733733
? iconList.map(iconName => `- ${iconName}`).join('\n')
734734
: '> Icon list could not be generated.';
735-
const iconCardsNode = unified().use(remarkParse).parse(listMarkdown);
736-
parent.children.splice(index, 1, ...iconCardsNode.children);
735+
const iconListNode = unified().use(remarkParse).parse(listMarkdown);
736+
parent.children.splice(index, 1, ...iconListNode.children);
737737
return index;
738738
}
739739

packages/dev/s2-docs/src/IconSearchView.tsx

Lines changed: 107 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,99 @@
22
'use client';
33

44
import {Autocomplete, GridLayout, ListBox, ListBoxItem, Size, useFilter, Virtualizer} from 'react-aria-components';
5+
import CheckmarkCircle from '@react-spectrum/s2/icons/CheckmarkCircle';
56
import Close from '@react-spectrum/s2/icons/Close';
67
import {Content, Heading, IllustratedMessage, pressScale, SearchField, Skeleton, Text} from '@react-spectrum/s2';
78
import {focusRing, iconStyle, style} from '@react-spectrum/s2/style' with {type: 'macro'};
89
import {iconAliases} from './iconAliases.js';
910
// @ts-ignore
1011
import icons from '/packages/@react-spectrum/s2/s2wf-icons/*.svg';
12+
import InfoCircle from '@react-spectrum/s2/icons/InfoCircle';
1113
// eslint-disable-next-line monorepo/no-internal-import
1214
import NoSearchResults from '@react-spectrum/s2/illustrations/linear/NoSearchResults';
13-
import React, {useCallback, useMemo, useRef} from 'react';
15+
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
1416

1517
export const iconList = Object.keys(icons).map(name => ({id: name.replace(/^S2_Icon_(.*?)(Size\d+)?_2.*/, '$1'), icon: icons[name].default}));
1618

19+
export function useIconFilter() {
20+
let {contains} = useFilter({sensitivity: 'base'});
21+
return useCallback((textValue: string, inputValue: string) => {
22+
// Check for alias matches
23+
for (const alias of Object.keys(iconAliases)) {
24+
if (contains(alias, inputValue) && iconAliases[alias].includes(textValue)) {
25+
return true;
26+
}
27+
}
28+
// Also compare for substrings in the icon's actual name
29+
return textValue != null && contains(textValue, inputValue);
30+
}, [contains]);
31+
}
32+
33+
export function useCopyImport() {
34+
let [copiedId, setCopiedId] = useState<string | null>(null);
35+
let timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
36+
37+
useEffect(() => {
38+
return () => {
39+
if (timeout.current) {
40+
clearTimeout(timeout.current);
41+
}
42+
};
43+
}, []);
44+
45+
let handleCopyImport = useCallback((id: string) => {
46+
if (timeout.current) {
47+
clearTimeout(timeout.current);
48+
}
49+
navigator.clipboard.writeText(`import ${id} from '@react-spectrum/s2/icons/${id}';`).then(() => {
50+
setCopiedId(id);
51+
timeout.current = setTimeout(() => setCopiedId(null), 2000);
52+
}).catch(() => {
53+
// noop
54+
});
55+
}, []);
56+
57+
return {copiedId, handleCopyImport};
58+
}
59+
60+
function CopyInfoMessage() {
61+
return (
62+
<div className={style({display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4})}>
63+
<InfoCircle styles={iconStyle({size: 'XS'})} />
64+
<span className={style({font: 'ui'})}>Press an item to copy its import statement</span>
65+
</div>
66+
);
67+
}
68+
69+
interface IconListBoxProps {
70+
items: typeof iconList,
71+
copiedId: string | null,
72+
onAction: (item: string) => void,
73+
listBoxClassName?: string
74+
}
75+
76+
function IconListBox({items, copiedId, onAction, listBoxClassName}: IconListBoxProps) {
77+
return (
78+
<Virtualizer layout={GridLayout} layoutOptions={{minItemSize: new Size(64, 64), maxItemSize: new Size(64, 64), minSpace: new Size(12, 12), preserveAspectRatio: true}}>
79+
<ListBox
80+
onAction={(item) => onAction(item.toString())}
81+
items={items}
82+
layout="grid"
83+
className={listBoxClassName || style({width: '100%', scrollPaddingY: 4})}
84+
dependencies={[copiedId]}
85+
renderEmptyState={() => (
86+
<IllustratedMessage styles={style({marginX: 'auto', marginY: 32})}>
87+
<NoSearchResults />
88+
<Heading>No results</Heading>
89+
<Content>Try a different search term.</Content>
90+
</IllustratedMessage>
91+
)}>
92+
{item => <IconItem item={item} isCopied={copiedId === item.id} />}
93+
</ListBox>
94+
</Virtualizer>
95+
);
96+
}
97+
1798
const itemStyle = style({
1899
...focusRing(),
19100
size: 'full',
@@ -42,53 +123,35 @@ const itemStyle = style({
42123
cursor: 'default'
43124
});
44125

45-
let handleCopyImport = (id: string) => {
46-
navigator.clipboard.writeText(`import ${id} from '@react-spectrum/s2/icons/${id}';`).then(() => {
47-
// noop
48-
}).catch(() => {
49-
// noop
50-
});
51-
};
52-
53126
interface IconSearchViewProps {
54127
filteredItems: typeof iconList
55128
}
56129

57130
export function IconSearchView({filteredItems}: IconSearchViewProps) {
131+
let {copiedId, handleCopyImport} = useCopyImport();
132+
58133
return (
59-
<Virtualizer layout={GridLayout} layoutOptions={{minItemSize: new Size(64, 64), maxItemSize: new Size(64, 64), minSpace: new Size(12, 12), preserveAspectRatio: true}}>
60-
<ListBox
61-
onAction={(item) => handleCopyImport(item.toString())}
62-
items={filteredItems}
63-
layout="grid"
64-
className={style({width: '100%', scrollPaddingY: 4})}
65-
renderEmptyState={() => (
66-
<IllustratedMessage styles={style({marginX: 'auto', marginY: 32})}>
67-
<NoSearchResults />
68-
<Heading>No results</Heading>
69-
<Content>Try a different search term.</Content>
70-
</IllustratedMessage>
71-
)}>
72-
{item => <IconItem item={item} />}
73-
</ListBox>
74-
</Virtualizer>
134+
<>
135+
<CopyInfoMessage />
136+
<IconListBox items={filteredItems} copiedId={copiedId} onAction={handleCopyImport} />
137+
</>
75138
);
76139
}
77140

78-
function IconItem({item}) {
141+
function IconItem({item, isCopied = false}: {item: typeof iconList[number], isCopied?: boolean}) {
79142
let Icon = item.icon;
80143
let ref = useRef(null);
81144
return (
82145
<ListBoxItem id={item.id} value={item} textValue={item.id} className={itemStyle} ref={ref} style={pressScale(ref)}>
83-
<Icon styles={iconStyle({size: 'XL'})} />
146+
{isCopied ? <CheckmarkCircle styles={iconStyle({size: 'XL'})} /> : <Icon styles={iconStyle({size: 'XL'})} />}
84147
<div
85148
className={style({
86149
maxWidth: '100%',
87150
textOverflow: 'ellipsis',
88151
overflow: 'hidden',
89152
whiteSpace: 'nowrap'
90153
})}>
91-
{item.id}
154+
{isCopied ? 'Copied!' : item.id}
92155
</div>
93156
</ListBoxItem>
94157
);
@@ -163,39 +226,23 @@ export function IconSearchSkeleton() {
163226
);
164227
}
165228

166-
export function IconCards() {
167-
let {contains} = useFilter({sensitivity: 'base'});
168-
let filter = useCallback((textValue, inputValue) => {
169-
// check if we're typing part of a category alias
170-
for (const alias of Object.keys(iconAliases)) {
171-
if (contains(alias, inputValue) && iconAliases[alias].includes(textValue)) {
172-
return true;
173-
}
174-
}
175-
// also compare for substrings in the icon's actual name
176-
return textValue != null && contains(textValue, inputValue);
177-
}, [contains]);
229+
export function IconsPageSearch() {
230+
let filter = useIconFilter();
231+
let {copiedId, handleCopyImport} = useCopyImport();
232+
178233
return (
179-
<Autocomplete filter={filter}>
180-
<div className={style({display: 'flex', flexDirection: 'column', gap: 8})}>
181-
<SearchField size="L" aria-label="Search icons" placeholder="Search icons" />
182-
<Virtualizer layout={GridLayout} layoutOptions={{minItemSize: new Size(64, 64), maxItemSize: new Size(64, 64), minSpace: new Size(12, 12), preserveAspectRatio: true}}>
183-
<ListBox
184-
onAction={(item) => handleCopyImport(item.toString())}
234+
<>
235+
<Autocomplete filter={filter}>
236+
<div className={style({display: 'flex', flexDirection: 'column', gap: 8})}>
237+
<SearchField size="L" aria-label="Search icons" placeholder="Search icons" />
238+
<CopyInfoMessage />
239+
<IconListBox
185240
items={iconList}
186-
layout="grid"
187-
className={style({height: 440, width: '100%', maxHeight: '100%', overflow: 'auto', scrollPaddingY: 4})}
188-
renderEmptyState={() => (
189-
<IllustratedMessage styles={style({marginX: 'auto', marginY: 32})}>
190-
<NoSearchResults />
191-
<Heading>No results</Heading>
192-
<Content>Try a different search term.</Content>
193-
</IllustratedMessage>
194-
)}>
195-
{item => <IconItem item={item} />}
196-
</ListBox>
197-
</Virtualizer>
198-
</div>
199-
</Autocomplete>
241+
copiedId={copiedId}
242+
onAction={handleCopyImport}
243+
listBoxClassName={style({height: 440, width: '100%', maxHeight: '100%', overflow: 'auto', scrollPaddingY: 4})} />
244+
</div>
245+
</Autocomplete>
246+
</>
200247
);
201248
}

packages/dev/s2-docs/src/Layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export function Layout(props: PageProps & {children: ReactElement<any>}) {
222222
})}>
223223
<Header pages={pages} currentPage={currentPage} />
224224
<MobileHeader
225-
toc={(currentPage.tableOfContents?.[0]?.children?.length ?? 0) > 0 ? <MobileToc key="toc" toc={currentPage.tableOfContents ?? []} currentPage={currentPage} /> : null}
225+
toc={(currentPage.tableOfContents?.[0]?.children?.length ?? 0) > 1 ? <MobileToc key="toc" toc={currentPage.tableOfContents ?? []} currentPage={currentPage} /> : null}
226226
pages={pages}
227227
currentPage={currentPage} />
228228
<div className={style({display: 'flex', width: 'full'})}>

packages/dev/s2-docs/src/ReleasesList.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,7 @@ export function ReleasesList({pages}: {pages: Page[]}) {
1919
<p className={style({font: 'body', margin: 0})}>{renderHTMLfromMarkdown(release.exports?.description, {})}</p>
2020
</div>
2121
))}
22-
<div>
23-
<header className={style({marginBottom: 12})}>
24-
<h2 className={style({font: 'heading-lg', margin: 0})}><Link href="https://react-spectrum.adobe.com/releases/index.html">Archived releases</Link></h2>
25-
</header>
26-
<p className={style({font: 'body', margin: 0})}>For all previous releases or React Spectrum v3, see the <Link href="https://react-spectrum.adobe.com/releases/index.html">Archived releases</Link> page.</p>
27-
</div>
22+
<p className={style({font: 'body', margin: 0})}>For all previous releases or React Spectrum v3, see the <Link href="https://react-spectrum.adobe.com/releases/index.html">Archived releases</Link> page.</p>
2823
</article>
2924
);
3025
}

packages/dev/s2-docs/src/SearchMenu.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
'use client';
22

33
import {ActionButton, Content, Heading, IllustratedMessage, SearchField, Tag, TagGroup} from '@react-spectrum/s2';
4-
import {Autocomplete, Dialog, Key, OverlayTriggerStateContext, Provider, Separator as RACSeparator, useFilter} from 'react-aria-components';
4+
import {Autocomplete, Dialog, Key, OverlayTriggerStateContext, Provider, Separator as RACSeparator} from 'react-aria-components';
55
import Close from '@react-spectrum/s2/icons/Close';
66
import {ComponentCardView} from './ComponentCardView';
77
import {getLibraryFromPage, getLibraryFromUrl} from './library';
8-
import {iconAliases} from './iconAliases.js';
9-
import {iconList, IconSearchSkeleton} from './IconSearchView';
8+
import {iconList, IconSearchSkeleton, useIconFilter} from './IconSearchView';
109
import {type Library, TAB_DEFS} from './constants';
1110
// eslint-disable-next-line monorepo/no-internal-import
1211
import NoSearchResults from '@react-spectrum/s2/illustrations/linear/NoSearchResults';
1312
// @ts-ignore
1413
import {Page} from '@parcel/rsc';
15-
import React, {CSSProperties, lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState} from 'react';
14+
import React, {CSSProperties, lazy, Suspense, useEffect, useMemo, useRef, useState} from 'react';
1615
import {SelectableCollectionContext} from '../../../react-aria-components/src/RSPContexts';
1716
import {style} from '@react-spectrum/s2/style' with { type: 'macro' };
1817
import {Tab, TabList, TabPanel, Tabs} from './Tabs';
@@ -142,18 +141,7 @@ export function SearchMenu(props: SearchMenuProps) {
142141
const [selectedSectionId, setSelectedSectionId] = useState<string>(() => currentPage.exports?.section?.toLowerCase() || 'components');
143142
const prevSearchWasEmptyRef = useRef<boolean>(true);
144143

145-
// Icon filter function
146-
const {contains} = useFilter({sensitivity: 'base'});
147-
const iconFilter = useCallback((textValue, inputValue) => {
148-
// check if we're typing part of a category alias
149-
for (const alias of Object.keys(iconAliases)) {
150-
if (contains(alias, inputValue) && iconAliases[alias].includes(textValue)) {
151-
return true;
152-
}
153-
}
154-
// also compare for substrings in the icon's actual name
155-
return textValue != null && contains(textValue, inputValue);
156-
}, [contains]);
144+
const iconFilter = useIconFilter();
157145

158146
const filteredIcons = useMemo(() => {
159147
if (!searchValue.trim()) {

0 commit comments

Comments
 (0)