Skip to content

Commit

Permalink
feat(website): add theme switcher and theme switching (#82)
Browse files Browse the repository at this point in the history
* feat(website): add website theme switcher

* chore(node): lock down the node version for build tools

* fix: pr feedback

* fix(screen reader only): fix typings for the as prop

* fix(screen-reader-only): swap back to a default span

* fix: adjust the site header to accomodate the theme switcher
  • Loading branch information
SiTaggart committed Sep 11, 2019
1 parent ed7d2da commit a3b400d
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 128 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v10.16
v10.16
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import styled from '@emotion/styled';

export const ScreenReaderOnly = styled.span<{}>`
interface ScreenReaderOnlyProps {
// as prop isn't typed correctly from styled package https://github.com/emotion-js/emotion/issues/1137
as?: keyof JSX.IntrinsicElements;
}

export const ScreenReaderOnly = styled.span<ScreenReaderOnlyProps>`
position: absolute;
margin: -1px;
border: 0;
Expand Down
72 changes: 69 additions & 3 deletions packages/paste-website/src/components/ThemeSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,79 @@
import * as React from 'react';
import styled from '@emotion/styled';
import {themeGet} from 'styled-system';
import {useUID} from 'react-uid';
import {ScreenReaderOnly} from '@twilio-paste/screen-reader-only';
import {Box} from '@twilio-paste/box';
import {Themes, ThemesType} from '../constants';
import {useActiveSiteTheme} from '../context/ActiveSiteThemeContext';

interface ThemeSwitcherProps {
children?: React.ReactElement;
}

const StyledThemeSwitcherLabel = styled.label<{}>(props => ({
cursor: 'pointer',
display: 'inline-block',
padding: `${themeGet('space.space20')(props)} ${themeGet('space.space40')(props)}`,
'&:hover': {
textDecoration: 'underline',
},
}));

const StyledThemeSwitcherRadio = styled.input<{}>(props => ({
opacity: 0,
position: 'absolute',
[`&:focus + ${StyledThemeSwitcherLabel}`]: {
boxShadow: themeGet('shadows.shadowFocus')(props),
textDecoration: 'underline',
},
[`&:checked + ${StyledThemeSwitcherLabel}`]: {
backgroundColor: themeGet('backgroundColors.colorBackgroundPrimary')(props),
borderRadius: themeGet('radii.borderRadius10')(props),
color: themeGet('textColors.colorTextInverse')(props),
},
}));

export const ThemeSwitcher: React.FC<ThemeSwitcherProps> = () => {
const {theme, updateActiveSiteTheme} = useActiveSiteTheme();

const handleChange: React.ChangeEventHandler<HTMLInputElement> = event => {
updateActiveSiteTheme(event.currentTarget.value as ThemesType);
};

const consoleID = useUID();
const sendGridID = useUID();

return (
<div>
<ScreenReaderOnly>Theme Switcher Placeholder</ScreenReaderOnly>
</div>
<Box
as="form"
borderColor="colorBorderLight"
borderRadius="borderRadius20"
borderStyle="solid"
borderWidth="borderWidth10"
padding="space10"
>
<Box as="fieldset" borderWidth="borderWidth0" padding="space0" margin="space0">
<ScreenReaderOnly as="legend">Change the site theme</ScreenReaderOnly>
<StyledThemeSwitcherRadio
checked={theme === Themes.CONSOLE}
id={consoleID}
name="sitetheme"
onChange={handleChange}
type="radio"
value={Themes.CONSOLE}
/>
<StyledThemeSwitcherLabel htmlFor={consoleID}>Console</StyledThemeSwitcherLabel>
<StyledThemeSwitcherRadio
checked={theme === Themes.SENDGRID}
id={sendGridID}
name="sitetheme"
onChange={handleChange}
type="radio"
value={Themes.SENDGRID}
/>
<StyledThemeSwitcherLabel htmlFor={sendGridID}>SendGrid</StyledThemeSwitcherLabel>
</Box>
</Box>
);
};
1 change: 0 additions & 1 deletion packages/paste-website/src/components/callout/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,5 @@ export interface CalloutTitleProps {
export type CalloutTextProps = Pick<TextProps, 'marginTop' | 'marginBottom' | 'as'>;

export interface CalloutProps extends BoxProps {
children?: React.ReactNode;
variant?: CalloutVariants;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// https://github.com/FormidableLabs/react-live
import * as React from 'react';
import {Box} from '@twilio-paste/box';
import {Theme} from '@twilio-paste/theme';
import {LiveProvider, LiveEditor, LiveError, LivePreview as ReactLivePreview} from 'react-live';
import styled from '@emotion/styled';
import {themeGet} from 'styled-system';
import {CodeblockTheme} from './theme';
import {Language} from '../../codeblock';
import {useActiveSiteTheme} from '../../../context/ActiveSiteThemeContext';

interface CodeblockProps {
children: string;
Expand All @@ -20,28 +22,31 @@ const StyledPreviewWrapper = styled.div(props => ({

// FIXME use tokens for theme and LiveEditor
const LivePreview: React.FC<CodeblockProps> = ({children, language = 'jsx', scope, disabled = false}) => {
const {theme} = useActiveSiteTheme();
return (
<StyledPreviewWrapper>
<LiveProvider code={children} scope={scope} language={language} theme={CodeblockTheme} disabled={disabled}>
<Box
px="space50"
py="space80"
backgroundColor="colorBackground"
borderTopLeftRadius="borderRadius20"
borderTopRightRadius="borderRadius20"
>
<ReactLivePreview />
</Box>
<Box
css={{padding: '2px 10px', backgroundColor: '#011627'}}
borderBottomLeftRadius="borderRadius20"
borderBottomRightRadius="borderRadius20"
>
<LiveEditor />
</Box>
<LiveError />
</LiveProvider>
</StyledPreviewWrapper>
<Theme.Provider theme={theme}>
<StyledPreviewWrapper>
<LiveProvider code={children} scope={scope} language={language} theme={CodeblockTheme} disabled={disabled}>
<Box
px="space50"
py="space80"
backgroundColor="colorBackground"
borderTopLeftRadius="borderRadius20"
borderTopRightRadius="borderRadius20"
>
<ReactLivePreview />
</Box>
<Box
css={{padding: '2px 10px', backgroundColor: '#011627'}}
borderBottomLeftRadius="borderRadius20"
borderBottomRightRadius="borderRadius20"
>
<LiveEditor />
</Box>
<LiveError />
</LiveProvider>
</StyledPreviewWrapper>
</Theme.Provider>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const SiteHeader: React.FC<{}> = () => {
borderBottomWidth="borderWidth10"
pl="space200"
pr="space80"
py="space60"
py="space50"
mb="space140"
css={{
left: SIDEBAR_WIDTH,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const SIDEBAR_WIDTH = '240px';
export const HEADER_HEIGHT = '64px';
export const HEADER_HEIGHT = '70px';
23 changes: 13 additions & 10 deletions packages/paste-website/src/components/site-wrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Theme} from '@twilio-paste/theme';
import {SiteBody} from './SiteBody';
import {Sidebar} from './sidebar';
import {SiteHeader} from './SiteHeader';
import {ActiveSiteThemeProvider} from '../../context/ActiveSiteThemeContext';
import {SiteMain, SiteMainInner} from './SiteMain';
import {SiteFooter} from './SiteFooter';
import {ScrollAnchorIntoView} from './ScrollAnchorIntoView';
Expand All @@ -24,16 +25,18 @@ const globalStyles = css`
const SiteWrapper: React.FC = ({children}) => {
return (
<Theme.Provider theme="sendgrid">
<Global styles={globalStyles} />
<SiteBody>
<Sidebar />
<SiteHeader />
<SiteMain>
<ScrollAnchorIntoView />
<SiteMainInner>{children}</SiteMainInner>
<SiteFooter />
</SiteMain>
</SiteBody>
<ActiveSiteThemeProvider>
<Global styles={globalStyles} />
<SiteBody>
<Sidebar />
<SiteHeader />
<SiteMain>
<ScrollAnchorIntoView />
<SiteMainInner>{children}</SiteMainInner>
<SiteFooter />
</SiteMain>
</SiteBody>
</ActiveSiteThemeProvider>
</Theme.Provider>
);
};
Expand Down
2 changes: 1 addition & 1 deletion packages/paste-website/src/components/table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const Tbody = styled.tbody(props => ({
}));

export const Tr = styled.tr(props => ({
'&:nth-child(even)': {
'&:nth-of-type(even)': {
background: themeGet('backgroundColors.colorBackgroundRowStriped')(props),
},
}));
Expand Down
111 changes: 66 additions & 45 deletions packages/paste-website/src/components/tokens-list/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import * as React from 'react';
import {Box} from '@twilio-paste/box';
import {Text} from '@twilio-paste/text';
import {Theme} from '@twilio-paste/theme';
import {useUID} from 'react-uid';
import {Table, Tr, Th, Td, Tbody} from '../table';
import {TokenExample} from '../tokens-example';
import {Input} from '../input';
import {Label} from '../label';
import {InlineCode} from '../Typography';
import {AnchoredHeading} from '../Heading';
import {useActiveSiteTheme} from '../../context/ActiveSiteThemeContext';
import {Themes, ThemesType} from '../../constants';

const sentenceCase = (catName: string): string => {
return catName
Expand Down Expand Up @@ -39,27 +42,38 @@ interface TokensShape {

interface TokensListProps {
children?: React.ReactElement;
default: TokensShape[];
sendgrid: TokensShape[];
consoleTokens: TokensShape[];
sendgridTokens: TokensShape[];
}

const setInitialState = (data: TokensShape[]): TokenCategory[] | null => {
if (data != null) {
const {tokens} = data[0].node;
return tokens;
const getTokensByTheme = (theme: ThemesType, props: TokensListProps): TokenCategory[] => {
let tokens = [] as TokenCategory[];
if (theme === Themes.CONSOLE) {
if (props.consoleTokens != null) {
// eslint-disable-next-line prefer-destructuring
tokens = props.consoleTokens[0].node.tokens;
}
}
return null;
if (theme === Themes.SENDGRID) {
if (props.sendgridTokens != null) {
// eslint-disable-next-line prefer-destructuring
tokens = props.sendgridTokens[0].node.tokens;
}
}
return tokens;
};

export const TokensList: React.FC<TokensListProps> = props => {
const [tokens, setTokens] = React.useState(setInitialState(props.sendgrid));
const {theme} = useActiveSiteTheme();
const [tokens, setTokens] = React.useState<TokenCategory[] | null>(getTokensByTheme(theme, props));
const [filterString, setFilterString] = React.useState('');

const filterTokenList = (filter: string): void => {
const filterTokenList = (): void => {
setTokens(() => {
const newTokenCategories = props.sendgrid[0].node.tokens.map(
const newTokenCategories = getTokensByTheme(theme, props).map(
(category): TokenCategory => {
const newTokens = category.tokens.filter(token => {
return token.name.includes(filter) || token.value.includes(filter);
return token.name.includes(filterString) || token.value.includes(filterString);
});
return {...category, tokens: newTokens};
}
Expand All @@ -74,9 +88,14 @@ export const TokensList: React.FC<TokensListProps> = props => {
});
};

React.useEffect((): void => {
filterTokenList();
}, [theme]);

const handleInput = (e: React.FormEvent<HTMLInputElement>): void => {
const filter = e.currentTarget.value;
filterTokenList(filter);
setFilterString(filter);
filterTokenList();
};

const uid = useUID();
Expand All @@ -101,39 +120,41 @@ export const TokensList: React.FC<TokensListProps> = props => {
<AnchoredHeading as="h2" headingStyle="headingStyle20">
{sentenceCase(cat.categoryName)}
</AnchoredHeading>
<Box mb="space160">
<Table>
<thead>
<Tr>
<Th>Token</Th>
<Th style={{width: '250px'}}>Value</Th>
<Th style={{width: '250px'}}>Example</Th>
</Tr>
</thead>
<Tbody>
{cat.tokens.map((token: Token) => {
return (
<Tr key={`token${token.name}`}>
<Td>
<Text mb="space30" lineHeight="lineHeight40">
<InlineCode>${token.name}</InlineCode>
</Text>
<Text>{token.comment}</Text>
</Td>
<Td>{token.type === 'color' ? token.value.toUpperCase() : token.value}</Td>
<Td
css={{
position: 'relative',
}}
>
<TokenExample token={token} />
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</Box>
<Theme.Provider theme={theme}>
<Box mb="space160">
<Table>
<thead>
<Tr>
<Th>Token</Th>
<Th style={{width: '250px'}}>Value</Th>
<Th style={{width: '250px'}}>Example</Th>
</Tr>
</thead>
<Tbody>
{cat.tokens.map((token: Token) => {
return (
<Tr key={`token${token.name}`}>
<Td>
<Text mb="space30" lineHeight="lineHeight40">
<InlineCode>${token.name}</InlineCode>
</Text>
<Text>{token.comment}</Text>
</Td>
<Td>{token.type === 'color' ? token.value.toUpperCase() : token.value}</Td>
<Td
css={{
position: 'relative',
}}
>
<TokenExample token={token} />
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</Box>
</Theme.Provider>
</React.Fragment>
);
})}
Expand Down
Loading

1 comment on commit a3b400d

@vercel
Copy link

@vercel vercel bot commented on a3b400d Sep 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.