Skip to content

Commit

Permalink
Merge pull request #2274 from graphcommerce-org/feature/GCOM-1382
Browse files Browse the repository at this point in the history
Feature/gcom 1382
  • Loading branch information
paales authored Jun 6, 2024
2 parents 49d4a4a + e451395 commit 563329f
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/honest-dingos-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphcommerce/next-ui': patch
---

Created a cssFlags functionality to allow for conditional rendering based on stored flags in the localStorage
5 changes: 5 additions & 0 deletions .changeset/rotten-steaks-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphcommerce/next-ui": minor
---

Add props to DarkLightModeThemeProvider to disable dark/light mode or to change the default ssr mode. Save user chosen mode in localStorage
6 changes: 6 additions & 0 deletions docs/framework/theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ width, borderRadius and margin. Performance-wise, font-size and line-height
should not be scaled with responsiveVal. To learn more, look into
[responsive font sizes](../framework/typography.md).
## Disabling darkmode or lightmode site wide
Remove light={lightTheme} or dark={darkTheme} from the
`<DarkLightModeThemeProvider />` in \_app.tsx to disable darkmode or lightmode
site wide.
## Next steps
- Learn about [icons](../framework/icons.md) in GraphCommerce
Expand Down
3 changes: 2 additions & 1 deletion examples/magento-graphcms/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { normalizeLocale } from '@graphcommerce/lingui-next'
import { withLingui } from '@graphcommerce/lingui-next/document/withLingui'
import type { LinguiDocumentProps } from '@graphcommerce/lingui-next/document/withLingui'
import { EmotionCacheProps, withEmotionCache } from '@graphcommerce/next-ui'
import { EmotionCacheProps, getCssFlagsInitScript, withEmotionCache } from '@graphcommerce/next-ui'
import NextDocument, { Html, Head, Main, NextScript } from 'next/document'

class Document extends NextDocument<EmotionCacheProps & LinguiDocumentProps> {
render() {
return (
<Html lang={normalizeLocale(this.props.locale)}>
<Head>
{getCssFlagsInitScript()}
{/* Inject MUI styles first to match with the prepend: true configuration. */}
<meta name='emotion-insertion-point' content='' />
{this.props.emotionStyleTags}
Expand Down
2 changes: 2 additions & 0 deletions packages/magento-customer/components/SignOutForm/signOut.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ApolloClient } from '@graphcommerce/graphql'
import { removeCssFlag } from '@graphcommerce/next-ui'

export function signOut(client: ApolloClient<object>) {
removeCssFlag('signed-in')
client.cache.evict({ fieldName: 'currentCartId' })
client.cache.evict({ fieldName: 'cart' })
client.cache.evict({ fieldName: 'customerToken' })
Expand Down
5 changes: 5 additions & 0 deletions packages/magento-customer/hooks/useSignInForm.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useApolloClient } from '@graphcommerce/graphql'
import { setCssFlag } from '@graphcommerce/next-ui'
import { UseFormGraphQlOptions, useFormGqlMutation } from '@graphcommerce/react-hook-form'
import {
SignInDocument,
Expand Down Expand Up @@ -38,6 +39,10 @@ export function useSignInForm({ email, ...options }: UseSignInFormProps) {
? options.onBeforeSubmit({ ...values, email })
: { ...values, email }
},
onComplete: (...args) => {
setCssFlag('signed-in', true)
return options.onComplete?.(...args)
},
},
{ errorPolicy: 'all' },
)
Expand Down
60 changes: 41 additions & 19 deletions packages/next-ui/Theme/DarkLightModeThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useRouter } from 'next/router'
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
import { IconSvg } from '../IconSvg'
import { iconMoon, iconSun } from '../icons'
import { getCssFlag, setCssFlag } from '../utils/cssFlags'

type Mode = 'dark' | 'light'
type UserMode = 'auto' | Mode
Expand All @@ -22,55 +23,67 @@ type ColorModeContext = {
userMode: UserMode
browserMode: Mode
currentMode: Mode
isSingleMode: boolean
toggle: () => void
}

export const colorModeContext = createContext(undefined as unknown as ColorModeContext)
colorModeContext.displayName = 'ColorModeContext'

type ThemeProviderProps = {
// eslint-disable-next-line react/no-unused-prop-types
light: Theme
// eslint-disable-next-line react/no-unused-prop-types
dark: Theme

children: React.ReactNode
}
ssrMode?: Mode
listenToBrowser?: boolean
} & (
| { light: Theme; dark?: undefined }
| { light?: undefined; dark: Theme }
| { light: Theme; dark: Theme }
)

/**
* Wrapper around `import { ThemeProvider } from '@mui/material'`
*
* The multi DarkLightModeThemeProvider allows switching between light and dark mode based on URL
* and on user input.
*
* If you _just_ wan't a single theme, use the import { ThemeProvider } from '@mui/material' instead.
*/
export function DarkLightModeThemeProvider(props: ThemeProviderProps) {
const { children, light, dark } = props
const { children, light, dark, ssrMode = 'light', listenToBrowser = true } = props
const [configuredMode, setConfiguredMode] = useState<UserMode>(listenToBrowser ? 'auto' : ssrMode)
const setThemeMode = (mode: UserMode) => {
setConfiguredMode(mode)
setCssFlag('color-scheme', mode)
}

// todo: Save this in local storage
const [userMode, setUserMode] = useState<UserMode>('auto')
const browserMode: Mode = useMediaQuery('(prefers-color-scheme: dark)') ? 'dark' : 'light'

// If the user has set a mode, use that. Otherwise, use the browser mode.
const currentMode = userMode === 'auto' ? browserMode : userMode
const theme = currentMode === 'light' ? light : dark
const currentMode = configuredMode === 'auto' ? browserMode : configuredMode

let theme: Theme = light || dark // Default
if (light && currentMode === 'light') theme = light
else if (dark) theme = dark

useEffect(() => {
const flag = getCssFlag('color-scheme') as Mode
if (flag) setConfiguredMode(flag)
}, [setConfiguredMode])

// If a URL parameter is present, switch from auto to light or dark mode
const { asPath } = useRouter()
useEffect(() => {
if (asPath.includes('darkmode')) setUserMode('dark')
if (asPath.includes('darkmode')) setConfiguredMode('dark')
}, [asPath])

// Create the context
const colorContext: ColorModeContext = useMemo(
() => ({
browserMode,
userMode,
userMode: configuredMode,
currentMode,
toggle: () => setUserMode(currentMode === 'light' ? 'dark' : 'light'),
isSingleMode: !light || !dark,
toggle: () => setThemeMode(currentMode === 'light' ? 'dark' : 'light'),
}),
[browserMode, currentMode, userMode],
[browserMode, configuredMode, currentMode, light, dark],
)

return (
Expand All @@ -85,7 +98,12 @@ export function useColorMode() {
}

export function DarkLightModeToggleFab(props: Omit<FabProps, 'onClick'>) {
const { currentMode, toggle } = useColorMode()
const { currentMode, isSingleMode, toggle } = useColorMode()

if (isSingleMode) {
return null
}

return (
<Fab size='large' color='inherit' onClick={toggle} {...props}>
<IconSvg src={currentMode === 'light' ? iconMoon : iconSun} size='large' />
Expand All @@ -100,7 +118,11 @@ export function DarkLightModeToggleFab(props: Omit<FabProps, 'onClick'>) {
*/
export function DarkLightModeMenuSecondaryItem(props: ListItemButtonProps) {
const { sx = [] } = props
const { currentMode, toggle } = useColorMode()
const { currentMode, isSingleMode, toggle } = useColorMode()

if (isSingleMode) {
return null
}

return (
<ListItemButton {...props} sx={[{}, ...(Array.isArray(sx) ? sx : [sx])]} dense onClick={toggle}>
Expand Down
1 change: 1 addition & 0 deletions packages/next-ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@ export * from './icons'
export * from './utils/cookie'
export * from './utils/sitemap'
export * from './utils/robots'
export * from './utils/cssFlags'
76 changes: 76 additions & 0 deletions packages/next-ui/utils/cssFlags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
export const FLAGS_STORAGE_KEY = 'gc-flags'
export const FLAG_PREFIX = 'data'

export function getCssFlagsInitScript() {
return (
<script
id='init-gc-flags'
key='mui-color-scheme-init'
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: `(function() {
try {
const flags = JSON.parse(localStorage.getItem('${FLAGS_STORAGE_KEY}') || '{}')
Object.entries(flags).forEach(([key, val]) => {
document.documentElement.setAttribute('data-' +key, typeof val === 'boolean' ? '' : val)
})
} catch(e){}})();`,
}}
/>
)
}

function loadFlags() {
const flags = JSON.parse(localStorage.getItem(FLAGS_STORAGE_KEY) || '{}')
if (typeof flags !== 'object' && flags !== null) return {}
return flags as Record<string, true | string>
}

function saveFlags(flags: Record<string, true | string>) {
window.localStorage?.setItem(FLAGS_STORAGE_KEY, JSON.stringify(flags))
}

export function removeCssFlag(flagName: string) {
const flags = loadFlags()
delete flags[flagName]
document.documentElement.removeAttribute(`data-${flagName}`)
saveFlags(flags)
}

export function setCssFlag(flagName: string, val: true | string) {
document.documentElement.setAttribute(`data-${flagName}`, typeof val === 'boolean' ? '' : val)

const flags = loadFlags()
flags[flagName] = val
saveFlags(flags)
}

/**
* @deprecated flags are not intendend to be used in JS, so this should only be used for debugging purposes.
*/
export function getCssFlag(flagName: string) {
return loadFlags()[flagName]
}

/**
* Easily create a CSS selector that only applies when a flag is set.
*
* Example:
*
* ```tsx
* <Box sx={{ [cssFlagSelector('dark')]: { color: 'white' } }} />
* ```
*/
export const cssFlag = <T extends string>(flagName: T) => `html[data-${flagName}] &` as const

/**
* Easily create a CSS selector that only applies when a flag is not set.
*
* Example:
*
* ```tsx
* <Box sx={{ [cssNotFlagSelector('dark')]: { color: 'black' } }} />
* ```
*/
export const cssNotFlag = <T extends string>(flagName: T) =>
`html:not([data-${flagName}]) &` as const

0 comments on commit 563329f

Please sign in to comment.