feat: synchronized platform tabs (#165)

* feat: synchronized platform tabs

* chore: rename enums to pascal case

* chore: fixed typo
This commit is contained in:
Vojtech Simetka
2021-08-11 19:46:09 +02:00
committed by GitHub
parent f90778d338
commit ec42eafc2b
4 changed files with 112 additions and 157 deletions
+3
View File
@@ -9,6 +9,7 @@ import { SnackbarProvider } from 'notistack'
import BaseRouter from './routes/routes' import BaseRouter from './routes/routes'
import { lightTheme, darkTheme } from './theme' import { lightTheme, darkTheme } from './theme'
import { Provider as StampsProvider } from './providers/Stamps' import { Provider as StampsProvider } from './providers/Stamps'
import { Provider as PlatformProvider } from './providers/Platform'
const App = (): ReactElement => { const App = (): ReactElement => {
const [themeMode, toggleThemeMode] = useState('light') const [themeMode, toggleThemeMode] = useState('light')
@@ -36,6 +37,7 @@ const App = (): ReactElement => {
<div className="App"> <div className="App">
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}> <ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
<StampsProvider> <StampsProvider>
<PlatformProvider>
<SnackbarProvider> <SnackbarProvider>
<> <>
<CssBaseline /> <CssBaseline />
@@ -44,6 +46,7 @@ const App = (): ReactElement => {
</Router> </Router>
</> </>
</SnackbarProvider> </SnackbarProvider>
</PlatformProvider>
</StampsProvider> </StampsProvider>
</ThemeProvider> </ThemeProvider>
</div> </div>
+16 -132
View File
@@ -1,13 +1,7 @@
import React, { ReactElement, useEffect } from 'react' import { ReactElement, useContext } from 'react'
import { withStyles, Theme, createStyles } from '@material-ui/core/styles' import TabsContainer from './TabsContainer'
import { Tabs, Tab, Box, Typography } from '@material-ui/core'
import CodeBlock from './CodeBlock' import CodeBlock from './CodeBlock'
import { Context } from '../providers/Platform'
interface TabPanelProps {
children?: React.ReactNode
index: number
value: number
}
interface Props { interface Props {
linux: string linux: string
@@ -15,133 +9,23 @@ interface Props {
showLineNumbers?: boolean showLineNumbers?: boolean
} }
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
}
}
function getOS() {
const userAgent = window.navigator.userAgent
const platform = window.navigator.platform
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
if (macosPlatforms.includes(platform)) return 'macOS'
if (iosPlatforms.includes(platform)) return 'iOS'
if (windowsPlatforms.includes(platform)) return 'windows'
if (/Android/.test(userAgent)) return 'android'
if (/Linux/.test(platform)) return 'linux'
return null
}
export default function CodeBlockTabs(props: Props): ReactElement { export default function CodeBlockTabs(props: Props): ReactElement {
const [value, setValue] = React.useState(0) const { platform, setPlatform } = useContext(Context)
const handleChange = (event: React.ChangeEvent<unknown>, newValue: number) => {
setValue(newValue)
}
useEffect(() => {
const os = getOS()
if (os === 'windows') {
setValue(0)
} else if (os === 'linux') {
setValue(0)
} else if (os === 'macOS') {
setValue(1)
}
}, [])
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props
return ( return (
<div <TabsContainer
role="tabpanel" index={platform}
hidden={value !== index} indexChanged={setPlatform}
id={`simple-tabpanel-${index}`} values={[
aria-labelledby={`simple-tab-${index}`} {
{...other} label: 'Linux',
> component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} />,
{value === index && (
<Box style={{ marginTop: '-12px' }}>
<Typography component="div">{children}</Typography>
</Box>
)}
</div>
)
}
const AntTabs = withStyles({
root: {
borderBottom: '1px solid #e8e8e8',
}, },
indicator: { {
backgroundColor: '#3f51b5', label: 'macOS',
component: <CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} />,
}, },
})(Tabs) ]}
/>
interface StyledTabProps {
label: string
}
const AntTab = withStyles((theme: Theme) =>
createStyles({
root: {
textTransform: 'none',
minWidth: 72,
backgroundColor: 'transparent',
fontWeight: theme.typography.fontWeightRegular,
marginRight: theme.spacing(4),
fontFamily: [
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'Arial',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
].join(','),
'&:hover': {
color: '#3f51b5',
opacity: 1,
},
'&$selected': {
color: '#3f51b5',
fontWeight: theme.typography.fontWeightMedium,
},
'&:focus': {
color: '#3f51b5',
},
},
selected: {},
}),
)((props: StyledTabProps) => <Tab disableRipple {...props} />)
return (
<div>
<AntTabs style={{ marginTop: '12px' }} value={value} onChange={handleChange} aria-label="ant example">
<AntTab label="Linux" {...a11yProps(0)} />
<AntTab label="MacOS" {...a11yProps(1)} />
</AntTabs>
<TabPanel value={value} index={0}>
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.linux} />
</TabPanel>
<TabPanel value={value} index={1}>
<CodeBlock showLineNumbers={props.showLineNumbers} language="bash" code={props.mac} />
</TabPanel>
</div>
) )
} }
+14 -15
View File
@@ -15,13 +15,7 @@ function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props const { children, value, index, ...other } = props
return ( return (
<div <div role="tabpanel" hidden={value !== index} {...other}>
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && ( {value === index && (
<Box p={3}> <Box p={3}>
<Typography>{children}</Typography> <Typography>{children}</Typography>
@@ -44,25 +38,30 @@ interface TabsValues {
interface Props { interface Props {
values: TabsValues[] values: TabsValues[]
index?: number
indexChanged?: (index: number) => void
} }
export default function SimpleTabs({ values }: Props): ReactElement { export default function SimpleTabs({ values, index, indexChanged }: Props): ReactElement {
const classes = useStyles() const classes = useStyles()
const [value, setValue] = React.useState<number>(0) const [value, setValue] = React.useState<number>(index || 0)
const handleChange = (event: React.ChangeEvent<Record<string, never>>, newValue: number) => { const handleChange = (event: React.ChangeEvent<Record<string, never>>, newValue: number) => {
setValue(newValue) if (indexChanged) indexChanged(newValue)
else setValue(newValue)
} }
const v = index !== undefined ? index : value
return ( return (
<div className={classes.root}> <div className={classes.root}>
<Tabs value={value} onChange={handleChange} aria-label="simple tabs example"> <Tabs value={v} onChange={handleChange}>
{values.map(({ label }, index) => ( {values.map(({ label }, idx) => (
<Tab key={index} label={label} /> <Tab key={idx} label={label} />
))} ))}
</Tabs> </Tabs>
{values.map(({ component }, index) => ( {values.map(({ component }, idx) => (
<TabPanel key={index} value={value} index={index}> <TabPanel key={idx} value={v} index={idx}>
{component} {component}
</TabPanel> </TabPanel>
))} ))}
+69
View File
@@ -0,0 +1,69 @@
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
// These need to be numeric values as they are used as indexes in the TabsContainer
export enum Platforms {
macOS = 0,
Linux,
Windows,
iOS,
Android,
}
export enum SupportedPlatforms {
macOS = Platforms.macOS,
Linux = Platforms.Windows,
}
interface ContextInterface {
platform: SupportedPlatforms
setPlatform: (platform: SupportedPlatforms) => void
}
const initialValues: ContextInterface = {
platform: SupportedPlatforms.macOS,
setPlatform: () => {}, // eslint-disable-line
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
function isSupportedPlatform(platform: unknown): platform is SupportedPlatforms {
return Object.keys(SupportedPlatforms).includes(platform as string)
}
function getOS(): Platforms | null {
const userAgent = window.navigator.userAgent
const platform = window.navigator.platform
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
if (macosPlatforms.includes(platform)) return Platforms.macOS
if (iosPlatforms.includes(platform)) return Platforms.iOS
if (windowsPlatforms.includes(platform)) return Platforms.Windows
if (/Android/.test(userAgent)) return Platforms.Android
if (/Linux/.test(platform)) return Platforms.Linux
return null
}
export function Provider({ children }: Props): ReactElement {
const [platform, setPlatform] = useState<SupportedPlatforms>(SupportedPlatforms.Linux)
// This is in useEffect as it really just needs to run once and not on each re-render
useEffect(() => {
const os = getOS()
setPlatform(isSupportedPlatform(os) ? os : SupportedPlatforms.Linux)
}, [])
return <Context.Provider value={{ platform, setPlatform }}>{children}</Context.Provider>
}