Skip to content

Commit 5d111e9

Browse files
committed
dedupe icons search code where possible
1 parent 9f3373c commit 5d111e9

File tree

4 files changed

+100
-120
lines changed

4 files changed

+100
-120
lines changed

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: 91 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,85 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
1616

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

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+
1998
const itemStyle = style({
2099
...focusRing(),
21100
size: 'full',
@@ -49,52 +128,12 @@ interface IconSearchViewProps {
49128
}
50129

51130
export function IconSearchView({filteredItems}: IconSearchViewProps) {
52-
let [copiedId, setCopiedId] = useState<string | null>(null);
53-
let timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
54-
55-
useEffect(() => {
56-
return () => {
57-
if (timeout.current) {
58-
clearTimeout(timeout.current);
59-
}
60-
};
61-
}, []);
62-
63-
let handleCopyImport = useCallback((id: string) => {
64-
if (timeout.current) {
65-
clearTimeout(timeout.current);
66-
}
67-
navigator.clipboard.writeText(`import ${id} from '@react-spectrum/s2/icons/${id}';`).then(() => {
68-
setCopiedId(id);
69-
timeout.current = setTimeout(() => setCopiedId(null), 2000);
70-
}).catch(() => {
71-
// noop
72-
});
73-
}, []);
131+
let {copiedId, handleCopyImport} = useCopyImport();
74132

75133
return (
76134
<>
77-
<div className={style({display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4})}>
78-
<InfoCircle styles={iconStyle({size: 'XS'})} />
79-
<span className={style({font: 'ui'})}>Press an item to copy its import statement</span>
80-
</div>
81-
<Virtualizer layout={GridLayout} layoutOptions={{minItemSize: new Size(64, 64), maxItemSize: new Size(64, 64), minSpace: new Size(12, 12), preserveAspectRatio: true}}>
82-
<ListBox
83-
onAction={(item) => handleCopyImport(item.toString())}
84-
items={filteredItems}
85-
layout="grid"
86-
className={style({width: '100%', scrollPaddingY: 4})}
87-
dependencies={[copiedId]}
88-
renderEmptyState={() => (
89-
<IllustratedMessage styles={style({marginX: 'auto', marginY: 32})}>
90-
<NoSearchResults />
91-
<Heading>No results</Heading>
92-
<Content>Try a different search term.</Content>
93-
</IllustratedMessage>
94-
)}>
95-
{item => <IconItem item={item} isCopied={copiedId === item.id} />}
96-
</ListBox>
97-
</Virtualizer>
135+
<CopyInfoMessage />
136+
<IconListBox items={filteredItems} copiedId={copiedId} onAction={handleCopyImport} />
98137
</>
99138
);
100139
}
@@ -187,68 +226,21 @@ export function IconSearchSkeleton() {
187226
);
188227
}
189228

190-
export function IconCards() {
191-
let {contains} = useFilter({sensitivity: 'base'});
192-
let filter = useCallback((textValue, inputValue) => {
193-
// check if we're typing part of a category alias
194-
for (const alias of Object.keys(iconAliases)) {
195-
if (contains(alias, inputValue) && iconAliases[alias].includes(textValue)) {
196-
return true;
197-
}
198-
}
199-
// also compare for substrings in the icon's actual name
200-
return textValue != null && contains(textValue, inputValue);
201-
}, [contains]);
202-
203-
let [copiedId, setCopiedId] = useState<string | null>(null);
204-
let timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
205-
206-
useEffect(() => {
207-
return () => {
208-
if (timeout.current) {
209-
clearTimeout(timeout.current);
210-
}
211-
};
212-
}, []);
213-
214-
let handleCopyImport = useCallback((id: string) => {
215-
if (timeout.current) {
216-
clearTimeout(timeout.current);
217-
}
218-
navigator.clipboard.writeText(`import ${id} from '@react-spectrum/s2/icons/${id}';`).then(() => {
219-
setCopiedId(id);
220-
timeout.current = setTimeout(() => setCopiedId(null), 2000);
221-
}).catch(() => {
222-
// noop
223-
});
224-
}, []);
229+
export function IconsPageSearch() {
230+
let filter = useIconFilter();
231+
let {copiedId, handleCopyImport} = useCopyImport();
225232

226233
return (
227234
<>
228235
<Autocomplete filter={filter}>
229236
<div className={style({display: 'flex', flexDirection: 'column', gap: 8})}>
230237
<SearchField size="L" aria-label="Search icons" placeholder="Search icons" />
231-
<div className={style({display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4})}>
232-
<InfoCircle styles={iconStyle({size: 'XS'})} />
233-
<span className={style({font: 'ui'})}>Press an item to copy its import statement</span>
234-
</div>
235-
<Virtualizer layout={GridLayout} layoutOptions={{minItemSize: new Size(64, 64), maxItemSize: new Size(64, 64), minSpace: new Size(12, 12), preserveAspectRatio: true}}>
236-
<ListBox
237-
onAction={(item) => handleCopyImport(item.toString())}
238-
items={iconList}
239-
layout="grid"
240-
className={style({height: 440, width: '100%', maxHeight: '100%', overflow: 'auto', scrollPaddingY: 4})}
241-
dependencies={[copiedId]}
242-
renderEmptyState={() => (
243-
<IllustratedMessage styles={style({marginX: 'auto', marginY: 32})}>
244-
<NoSearchResults />
245-
<Heading>No results</Heading>
246-
<Content>Try a different search term.</Content>
247-
</IllustratedMessage>
248-
)}>
249-
{item => <IconItem item={item} isCopied={copiedId === item.id} />}
250-
</ListBox>
251-
</Virtualizer>
238+
<CopyInfoMessage />
239+
<IconListBox
240+
items={iconList}
241+
copiedId={copiedId}
242+
onAction={handleCopyImport}
243+
listBoxClassName={style({height: 440, width: '100%', maxHeight: '100%', overflow: 'auto', scrollPaddingY: 4})} />
252244
</div>
253245
</Autocomplete>
254246
</>

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';
@@ -141,18 +140,7 @@ export function SearchMenu(props: SearchMenuProps) {
141140
const [selectedSectionId, setSelectedSectionId] = useState<string>(() => currentPage.exports?.section?.toLowerCase() || 'components');
142141
const prevSearchWasEmptyRef = useRef<boolean>(true);
143142

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

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

0 commit comments

Comments
 (0)