Skip to content

Commit 99e6051

Browse files
SaraVieiraCompuIves
andcommitted
Add Google fonts picker (#2328)
* start * add google font * readd button * remove console.log * style fixes * fix paddinhg * redo react component * pr review * pr review * remove suffix * add font preview * add tringle * Update packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/FontPicker/List.tsx Co-Authored-By: Michaël De Boey <[email protected]> * make it functional component * fix light themes * fix safari * fix imports * render in portal * fix click * make it lighter * fix small a11y * Update packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/FontPicker/index.tsx Co-Authored-By: Ives van Hoorne <[email protected]>
1 parent 1ee17f7 commit 99e6051

File tree

13 files changed

+451
-31
lines changed

13 files changed

+451
-31
lines changed

packages/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"@codesandbox/executors": "^0.1.0",
7373
"@codesandbox/template-icons": "^1.0.1",
7474
"@emmetio/codemirror-plugin": "^0.3.5",
75+
"@samuelmeuli/font-manager": "^1.2.0",
7576
"@sentry/webpack-plugin": "^1.8.0",
7677
"@styled-system/css": "^5.0.23",
7778
"@svgr/core": "^2.4.1",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React, { FunctionComponent, useState, useMemo } from 'react';
2+
import { Font } from '@samuelmeuli/font-manager';
3+
import { List, SearchFonts, FontLI, FontFamily, Arrow } from './elements';
4+
5+
type Props = {
6+
fonts: Font[];
7+
onSelection: (e: any) => void;
8+
activeFontFamily: string;
9+
expanded: boolean;
10+
};
11+
12+
export const FontList: FunctionComponent<Props> = ({
13+
fonts,
14+
onSelection,
15+
activeFontFamily,
16+
expanded,
17+
}) => {
18+
const [searchTerm, setSearchTerm] = useState('');
19+
20+
const updateSearch = (e: any) => setSearchTerm(e.target.value);
21+
22+
const getFontId = (fontFamily: string): string =>
23+
fontFamily.replace(/\s+/g, '-').toLowerCase();
24+
25+
const getFonts: Font[] = useMemo(
26+
() =>
27+
fonts.filter(f =>
28+
f.family.toLowerCase().includes(searchTerm.trim().toLowerCase())
29+
),
30+
[fonts, searchTerm]
31+
);
32+
return (
33+
<>
34+
<Arrow />
35+
<List expanded={expanded}>
36+
<SearchFonts
37+
type="text"
38+
value={searchTerm}
39+
onChange={updateSearch}
40+
placeholder="Search Typefaces"
41+
/>
42+
{getFonts.map((font: Font) => (
43+
<FontLI
44+
key={font.family}
45+
onClick={onSelection}
46+
onKeyPress={onSelection}
47+
>
48+
<FontFamily
49+
type="button"
50+
id={`font-button-${getFontId(font.family)}`}
51+
active={font.family === activeFontFamily}
52+
>
53+
{font.family}
54+
</FontFamily>
55+
</FontLI>
56+
))}
57+
</List>
58+
</>
59+
);
60+
};
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import styled, { css } from 'styled-components';
2+
import Color from 'color';
3+
4+
const makeDarker = ({ theme }) =>
5+
Color(theme['input.background'])
6+
.darken(theme.light ? 0.1 : 0.3)
7+
.rgbString();
8+
9+
const makeLighter = ({ theme }) =>
10+
Color(theme['sideBar.background'])
11+
.lighten(0.3)
12+
.rgbString();
13+
14+
export const SearchFonts = styled.input`
15+
border: 1px solid ${props => makeDarker(props)};
16+
box-sizing: border-box;
17+
border-radius: 2px;
18+
width: 100%;
19+
margin-bottom: 0.5rem;
20+
padding: 0.5rem;
21+
background: transparent;
22+
color: ${props =>
23+
props.theme['input.foreground'] ||
24+
(props.theme.light ? '#636363' : 'white')};
25+
26+
::-webkit-input-placeholder {
27+
color: ${props =>
28+
props.theme['input.foreground'] ||
29+
(props.theme.light ? '#636363' : 'white')};
30+
}
31+
::-moz-placeholder {
32+
color: ${props =>
33+
props.theme['input.foreground'] ||
34+
(props.theme.light ? '#636363' : 'white')};
35+
}
36+
:-ms-input-placeholder {
37+
color: ${props =>
38+
props.theme['input.foreground'] ||
39+
(props.theme.light ? '#636363' : 'white')};
40+
}
41+
`;
42+
43+
export const FontFamily = styled.button<{ active?: boolean }>`
44+
margin: 0;
45+
padding: 0;
46+
background-color: ${props => makeLighter(props)};
47+
width: 100%;
48+
padding-left: 0.25rem;
49+
border: none;
50+
text-align: left;
51+
color: ${props => props.theme['sideBar.foreground'] || 'inherit'};
52+
cursor: pointer;
53+
54+
&:focus {
55+
border-color: ${props => props.theme.secondary.clearer(0.6)};
56+
}
57+
`;
58+
59+
export const FontLI = styled.li`
60+
color: ${props => props.theme['sideBar.foreground'] || 'inherit'};
61+
padding: 0.5rem;
62+
text-align: left;
63+
cursor: pointer;
64+
`;
65+
66+
export const List = styled.ul<{ expanded?: boolean }>`
67+
font-size: 13px;
68+
list-style: none;
69+
border: 1px solid ${props => makeDarker(props)};
70+
box-sizing: border-box;
71+
padding: 0.5rem;
72+
margin: 0;
73+
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.24), 0px 4px 4px rgba(0, 0, 0, 0.12);
74+
max-height: 0;
75+
overflow: scroll;
76+
transition: all 200ms ease;
77+
text-align: left;
78+
display: none;
79+
margin-top: 0.5rem;
80+
background-color: ${props => makeLighter(props)};
81+
width: 240px;
82+
z-index: 10;
83+
84+
${props =>
85+
props.expanded &&
86+
css`
87+
max-height: 130px;
88+
display: block;
89+
`}
90+
`;
91+
92+
export const SelectedFont = styled.button<{ done?: boolean }>`
93+
background-color: ${props =>
94+
props.theme['input.background'] || 'rgba(0, 0, 0, 0.3)'};
95+
color: ${props =>
96+
props.theme['input.foreground'] ||
97+
(props.theme.light ? '#636363' : 'white')};
98+
border: 1px solid ${props => makeDarker(props)};
99+
100+
box-shadow: none;
101+
text-align: left;
102+
appearance: none;
103+
width: 100%;
104+
padding: 0.5rem 0.75rem;
105+
position: relative;
106+
box-sizing: border-box;
107+
outline: none;
108+
109+
${props =>
110+
props.done &&
111+
css`
112+
:after {
113+
content: '';
114+
background-image: url("${`data:image/svg+xml,%3Csvg width='7' height='4' viewBox='0 0 7 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.50007 4L1.27146e-07 1.35122e-07L7 -4.76837e-07L3.50007 4Z' fill='${
115+
props.theme.light ? 'black' : 'white'
116+
}'/%3E%3C/svg%3E%0A`}");
117+
width: 7px;
118+
height: 4px;
119+
position: absolute;
120+
right: 0.5rem;
121+
top: 50%;
122+
transform: translateY(-50%);
123+
}
124+
`}
125+
`;
126+
127+
export const Arrow = styled.div`
128+
width: 0;
129+
height: 0;
130+
border-style: solid;
131+
border-width: 0 6px 6px 6px;
132+
border-color: transparent transparent
133+
${props => props.theme['sideBar.background']} transparent;
134+
position: absolute;
135+
margin-top: 2px;
136+
left: 50%;
137+
margin-left: -6px;
138+
`;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import {
2+
FontManager,
3+
Font,
4+
FONT_FAMILY_DEFAULT,
5+
OPTIONS_DEFAULTS,
6+
} from '@samuelmeuli/font-manager';
7+
import React, { useState, useEffect, useRef } from 'react';
8+
import OutsideClickHandler from 'react-outside-click-handler';
9+
import { Portal } from 'reakit';
10+
import { SelectedFont } from './elements';
11+
import { FontList } from './List';
12+
13+
export const FontPicker = ({
14+
activeFontFamily = FONT_FAMILY_DEFAULT,
15+
onChange,
16+
apiKey,
17+
}) => {
18+
const [expanded, setExpanded] = useState(false);
19+
const [loadingStatus, setLoadingStatus] = useState('loading');
20+
const [fontManager, setFontManager] = useState();
21+
const [style, setStyle] = useState({ x: 0, y: 0 });
22+
const ref = useRef(null);
23+
24+
useEffect(() => {
25+
const manager = new FontManager(
26+
apiKey,
27+
activeFontFamily,
28+
{ ...OPTIONS_DEFAULTS, limit: 200 },
29+
onChange
30+
);
31+
setFontManager(manager);
32+
33+
manager
34+
.init()
35+
.then(() => setLoadingStatus('finished'))
36+
.catch((err: Error) => {
37+
setLoadingStatus('error');
38+
console.error(err);
39+
});
40+
// eslint-disable-next-line
41+
}, []);
42+
43+
useEffect(() => {
44+
if (ref && ref.current) {
45+
const styles = ref.current.getBoundingClientRect();
46+
setStyle({
47+
x: styles.x,
48+
y: styles.y,
49+
});
50+
}
51+
}, [ref]);
52+
53+
const onSelection = (e: any): void => {
54+
const active = e.target.textContent;
55+
if (!active) {
56+
throw Error(`Missing font family in clicked font button`);
57+
}
58+
fontManager.setActiveFont(active);
59+
toggleExpanded();
60+
};
61+
62+
const toggleExpanded = () => setExpanded(exp => !exp);
63+
64+
const fonts: Font[] =
65+
fontManager && Array.from(fontManager.getFonts().values());
66+
67+
return (
68+
<>
69+
<SelectedFont
70+
ref={ref}
71+
type="button"
72+
done={loadingStatus === 'finished'}
73+
onClick={toggleExpanded}
74+
onKeyPress={toggleExpanded}
75+
disabled={loadingStatus === 'loading'}
76+
>
77+
{loadingStatus === 'loading' ? 'Loading Typefaces' : activeFontFamily}
78+
</SelectedFont>
79+
{expanded && loadingStatus === 'finished' && (
80+
<Portal>
81+
<div
82+
style={{
83+
position: 'fixed',
84+
zIndex: 11,
85+
top: style.y + 201,
86+
left: style.x,
87+
}}
88+
>
89+
<OutsideClickHandler onOutsideClick={() => setExpanded(false)}>
90+
<FontList
91+
fonts={fonts}
92+
onSelection={onSelection}
93+
activeFontFamily={activeFontFamily}
94+
expanded={expanded}
95+
/>
96+
</OutsideClickHandler>
97+
</div>
98+
</Portal>
99+
)}
100+
</>
101+
);
102+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import styled from 'styled-components';
2+
3+
export const Container = styled.div`
4+
margin: 0.5rem 1rem;
5+
position: relative;
6+
`;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React, { useState } from 'react';
2+
import { Button } from '@codesandbox/common/lib/components/Button';
3+
import { FontPicker } from './FontPicker/index';
4+
import { Container } from './elements';
5+
6+
export const AddFont = ({ addResource }) => {
7+
const [activeFontFamily, setActiveFontFamily] = useState('Roboto');
8+
9+
const addFont = async () => {
10+
if (activeFontFamily) {
11+
const font = activeFontFamily.trim().replace(/ /g, '+');
12+
const link = `https://fonts.googleapis.com/css?family=${font}&display=swap`;
13+
await addResource(link);
14+
}
15+
};
16+
17+
return (
18+
<>
19+
<Container>
20+
<FontPicker
21+
apiKey="AIzaSyDQ9HOzvLFchvhfDG9MR0UeLpF8ScJshxU"
22+
activeFontFamily={activeFontFamily}
23+
onChange={nextFont => setActiveFontFamily(nextFont.family)}
24+
/>
25+
</Container>
26+
<Container>
27+
<Button disabled={!activeFontFamily} block small onClick={addFont}>
28+
Add Typeface
29+
</Button>
30+
</Container>
31+
</>
32+
);
33+
};

packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddResource/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const initialState = {
99
name: '',
1010
};
1111

12-
export default class AddVersion extends React.PureComponent {
12+
export class AddResource extends React.PureComponent {
1313
state = initialState;
1414

1515
setName = e => {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
3+
import CrossIcon from 'react-icons/lib/md/clear';
4+
5+
import { EntryContainer, IconArea, Icon } from '../../elements';
6+
import { Link } from '../elements';
7+
8+
const getFamily = search => {
9+
const hashes = search.slice(search.indexOf('?') + 1).split('&');
10+
const family = hashes
11+
.find(hash => hash.split('=')[0] === 'family')
12+
.split('=')[1];
13+
14+
return {
15+
name: family.split('+').join(' '),
16+
id: family
17+
.split('+')
18+
.join('-')
19+
.toLowerCase(),
20+
};
21+
};
22+
23+
export const ExternalFonts = ({ removeResource, resource }) => (
24+
<EntryContainer as="li">
25+
<Link id={`font-button-${getFamily(resource).id}`} href={resource}>
26+
{getFamily(resource).name}
27+
</Link>
28+
<IconArea>
29+
<Icon
30+
ariaLabel="Remove Resource"
31+
onClick={() => removeResource(resource)}
32+
>
33+
<CrossIcon />
34+
</Icon>
35+
</IconArea>
36+
</EntryContainer>
37+
);

0 commit comments

Comments
 (0)