-
Notifications
You must be signed in to change notification settings - Fork 114
/
SearchResults.tsx
171 lines (159 loc) · 6.35 KB
/
SearchResults.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import React from 'react';
import useDarkMode from 'use-dark-mode';
import * as Icons from '@radix-ui/react-icons';
import { Box, Flex, Grid, Text, darkTheme } from '@modulz/design-system';
import { CopyToastVisibility } from './CopyToast';
import { ChromelessButton } from './ChromelessButton';
type SearchResultsProps = {
value: string;
};
const GhostButton = (props: React.ComponentProps<typeof ChromelessButton>) => {
return (
<ChromelessButton
css={{
width: '100%',
display: 'block',
cursor: 'pointer',
padding: '$2',
borderRadius: '$2',
'&:hover': {
backgroundColor: '$mintA4',
},
'&:active, &:focus': {
boxShadow: '0 0 0 2px $colors$mintA8',
},
}}
{...props}
/>
);
};
const iconNames = Object.keys(Icons).map((key) => {
switch (key) {
// Logos using original PascalCase naming can't be automated
case 'LinkedInLogoIcon':
return 'LinkedIn Logo';
case 'GitHubLogoIcon':
return 'GitHub Logo';
case 'IconJarLogoIcon':
return 'IconJar Logo';
case 'CodeSandboxLogoIcon':
return 'CodeSandbox Logo';
case 'CounterClockwiseClockIcon':
return 'Counter-Clockwise Clock';
case 'RotateCounterClockwiseIcon':
return 'Rotate Counter-Clockwise';
// Naïve UpperCamelCaseIcon to Title Case conversion otherwise
default:
return key.replace(/Icon$/, '').replace(/(.)([0-9A-Z])/g, '$1 $2');
}
});
export const SearchResults = ({ value }: SearchResultsProps) => {
const cleanValue = escapeStringRegexp(value.trim().replace(/\s/g, ' '));
const matchingNames = iconNames.filter((name) => new RegExp(`\\b${cleanValue}`, 'gi').test(name));
return (
<CopyToastVisibility.Consumer>
{({ setIcon, setIsVisible }) => (
<Box>
{value && matchingNames.length > 0 && (
<Grid
css={{
rowGap: '$2',
alignContent: 'start',
padding: '$2 $3',
'@bp1': {
columnGap: '$6',
padding: '$5 $6',
gridAutoFlow: 'column',
gridTemplateColumns: 'repeat(2, 1fr)',
gridTemplateRows: `repeat(${Math.max(Math.ceil(matchingNames.length / 2), 3)}, auto)`,
},
'@bp2': {
// Place icons on rows first, then add more columns as needed, up to 4 total.
// And make sure there's at least 3 rows, so it looks nice.
// If only there was something like a multi-column layout feature in CSS... 🙃
gridAutoFlow: 'column',
gridTemplateColumns: 'repeat(4, 1fr)',
gridTemplateRows: `repeat(${Math.max(Math.ceil(matchingNames.length / 4), 3)}, auto)`,
columnGap: '$2',
},
}}
>
{matchingNames.map((name) => (
<Box css={{ minWidth: 0 }} key={name}>
<GhostButton
onClick={(event: React.MouseEvent) => {
const svg = event.currentTarget.querySelector('svg');
const code = svg && svg.parentElement ? svg.parentElement.innerHTML : null;
// Copy code to clipboard via a hidden textarea element
if (code) {
// Temporary shim until a proper focus-visible handler is added
if (document.activeElement instanceof HTMLButtonElement) {
document.activeElement.blur();
}
const textarea = document.createElement('textarea');
textarea.value = code.toString();
textarea.setAttribute('readonly', '');
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
// Show CopyToast and set latest icon
setIsVisible();
setIcon(code);
}
}}
>
<Flex
as="span"
css={{
alignItems: 'center',
justifyContent: 'start',
whiteSpace: 'nowrap',
minWidth: 0,
}}
>
<Flex as="span" css={{ mr: '$1', p: '$1', flex: '0' }}>
{React.createElement(Object.values(Icons)[iconNames.indexOf(name)] as any)}
</Flex>
<Text
size="2"
css={{
flexGrow: 0,
flexShrink: 1,
textOverflow: 'ellipsis',
overflow: 'hidden',
minWidth: 0,
lineHeight: '25px',
}}
>
{name}
</Text>
</Flex>
</GhostButton>
</Box>
))}
</Grid>
)}
{!matchingNames.length && (
<Flex css={{ alignItems: 'center', justifyContent: 'center', minHeight: 300, padding: '$5 $6' }}>
<Text size="2" css={{ textAlign: 'center', lineHeight: '20px' }}>
No icons for <span style={{ fontWeight: 500 }}>{value}</span>
</Text>
</Flex>
)}
</Box>
)}
</CopyToastVisibility.Consumer>
);
};
// https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js
function escapeStringRegexp(string) {
if (typeof string !== 'string') {
throw new TypeError('Expected a string');
}
// Escape characters with special meaning either inside or outside character sets.
// Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar.
return string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d');
}