chore(#2412): refactor theme context (#2435)

* chore: switch theme to zustand, delete context

* chore: switch apps/componenets to consume the hook

* chore: update storybook theme usage to use documentElement

* chore: dry up theme switcher listener for storybooks

* feat: optional theme param to allow toggling

* chore: add additional check for matchMedia function

* chore: change block explorer test to use light theme as its the default

* chore: remove unused headerprops for multisig-signer

* chore: remove unused props from theme switcher component

* chore: dry up validateTheme func

* chore: remove unused props from explorer header test

* chore: use new theme switcher in account history container
This commit is contained in:
Matthew Russell 2022-12-20 20:55:35 -06:00 committed by GitHub
parent df609d442c
commit 0b4f918208
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 240 additions and 368 deletions

View File

@ -1,7 +1,5 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment'; import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
import { import {
VegaConnectDialog, VegaConnectDialog,
@ -18,8 +16,7 @@ import useLocalValues from './hooks/use-local-values';
import type { InMemoryCacheConfig } from '@apollo/client'; import type { InMemoryCacheConfig } from '@apollo/client';
function App() { function App() {
const [theme, toggleTheme] = useThemeSwitcher(); const localValues = useLocalValues();
const localValues = useLocalValues(theme, toggleTheme);
const { const {
vegaWalletDialog, vegaWalletDialog,
menu: { setMenuOpen }, menu: { setMenuOpen },
@ -56,25 +53,23 @@ function App() {
return ( return (
<EnvironmentProvider> <EnvironmentProvider>
<ThemeContext.Provider value={theme}> <NetworkLoader cache={cacheConfig}>
<NetworkLoader cache={cacheConfig}> <VegaWalletProvider>
<VegaWalletProvider> <LocalContext.Provider value={localValues}>
<LocalContext.Provider value={localValues}> <AppLoader>
<AppLoader> <div className="max-h-full min-h-full dark:bg-lite-black dark:text-neutral-200 bg-white text-neutral-800 grid grid-rows-[min-content,1fr]">
<div className="max-h-full min-h-full dark:bg-lite-black dark:text-neutral-200 bg-white text-neutral-800 grid grid-rows-[min-content,1fr]"> <Header />
<Header /> <Main />
<Main /> <VegaConnectDialog connectors={Connectors} />
<VegaConnectDialog connectors={Connectors} /> <VegaManageDialog
<VegaManageDialog dialogOpen={vegaWalletDialog.manage}
dialogOpen={vegaWalletDialog.manage} setDialogOpen={vegaWalletDialog.setManage}
setDialogOpen={vegaWalletDialog.setManage} />
/> </div>
</div> </AppLoader>
</AppLoader> </LocalContext.Provider>
</LocalContext.Provider> </VegaWalletProvider>
</VegaWalletProvider> </NetworkLoader>
</NetworkLoader>
</ThemeContext.Provider>
</EnvironmentProvider> </EnvironmentProvider>
); );
} }

View File

@ -1,14 +1,11 @@
import React, { import React, { forwardRef, useCallback, useMemo, useRef } from 'react';
forwardRef,
useCallback,
useContext,
useMemo,
useRef,
} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { ThemeContext, useScreenDimensions } from '@vegaprotocol/react-helpers'; import {
useScreenDimensions,
useThemeSwitcher,
} from '@vegaprotocol/react-helpers';
import type { import type {
GridOptions, GridOptions,
GetRowIdParams, GetRowIdParams,
@ -43,7 +40,7 @@ const ConsoleLiteGrid = <T extends { id?: string }>(
) => { ) => {
const { isMobile, screenSize } = useScreenDimensions(); const { isMobile, screenSize } = useScreenDimensions();
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const theme = useContext(ThemeContext); const { theme } = useThemeSwitcher();
const handleOnGridReady = useCallback(() => { const handleOnGridReady = useCallback(() => {
( (
(ref as React.RefObject<AgGridReact>) || gridRef (ref as React.RefObject<AgGridReact>) || gridRef

View File

@ -11,8 +11,6 @@ const Header = () => {
})); }));
const { const {
vegaWalletDialog: { setManage }, vegaWalletDialog: { setManage },
theme,
toggleTheme,
} = useContext(LocalContext); } = useContext(LocalContext);
return ( return (
<div <div
@ -25,7 +23,7 @@ const Header = () => {
setConnectDialog={updateVegaWalletDialog} setConnectDialog={updateVegaWalletDialog}
setManageDialog={setManage} setManageDialog={setManage}
/> />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} className="-my-4" /> <ThemeSwitcher className="-my-4" />
</div> </div>
</div> </div>
); );

View File

@ -14,8 +14,6 @@ interface MenuState {
export interface LocalValues { export interface LocalValues {
vegaWalletDialog: VegaWalletDialogState; vegaWalletDialog: VegaWalletDialogState;
menu: MenuState; menu: MenuState;
theme: 'light' | 'dark';
toggleTheme: () => void;
} }
const LocalContext = createContext<LocalValues>({} as LocalValues); const LocalContext = createContext<LocalValues>({} as LocalValues);

View File

@ -3,8 +3,7 @@ import useLocalValues from './use-local-values';
describe('local values hook', () => { describe('local values hook', () => {
it('state of wallet dialog should be properly handled', () => { it('state of wallet dialog should be properly handled', () => {
const setTheme = jest.fn(); const { result } = renderHook(() => useLocalValues());
const { result } = renderHook(() => useLocalValues('light', setTheme));
expect(result.current.vegaWalletDialog).toBeDefined(); expect(result.current.vegaWalletDialog).toBeDefined();
expect(result.current.vegaWalletDialog.manage).toBe(false); expect(result.current.vegaWalletDialog.manage).toBe(false);
act(() => { act(() => {

View File

@ -1,17 +1,15 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import type { LocalValues } from '../context/local-context'; import type { LocalValues } from '../context/local-context';
const useLocalValues = (theme: 'light' | 'dark', toggleTheme: () => void) => { const useLocalValues = () => {
const [manage, setManage] = useState<boolean>(false); const [manage, setManage] = useState<boolean>(false);
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
return useMemo<LocalValues>( return useMemo<LocalValues>(
() => ({ () => ({
vegaWalletDialog: { manage, setManage }, vegaWalletDialog: { manage, setManage },
menu: { menuOpen, setMenuOpen, onToggle: () => setMenuOpen(!menuOpen) }, menu: { menuOpen, setMenuOpen, onToggle: () => setMenuOpen(!menuOpen) },
theme,
toggleTheme,
}), }),
[manage, theme, toggleTheme, menuOpen] [manage, menuOpen]
); );
}; };

View File

@ -3,7 +3,6 @@ import { useState, useEffect } from 'react';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing'; import { BrowserTracing } from '@sentry/tracing';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { import {
EnvironmentProvider, EnvironmentProvider,
NetworkLoader, NetworkLoader,
@ -19,7 +18,6 @@ import type { InMemoryCacheConfig } from '@apollo/client';
function App() { function App() {
const { VEGA_ENV } = useEnvironment(); const { VEGA_ENV } = useEnvironment();
const [theme, toggleTheme] = useThemeSwitcher();
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const location = useLocation(); const location = useLocation();
@ -57,25 +55,18 @@ function App() {
); );
return ( return (
<ThemeContext.Provider value={theme}> <TendermintWebsocketProvider>
<TendermintWebsocketProvider> <NetworkLoader cache={cacheConfig}>
<NetworkLoader cache={cacheConfig}> <div className={layoutClasses}>
<div className={layoutClasses}> <Header menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
<Header <Nav menuOpen={menuOpen} />
theme={theme} <Main />
toggleTheme={toggleTheme} <footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-sm md:text-md md:flex md:col-span-2 p-4 gap-4 border-t border-neutral-700 dark:border-neutral-300">
menuOpen={menuOpen} <NetworkInfo />
setMenuOpen={setMenuOpen} </footer>
/> </div>
<Nav menuOpen={menuOpen} /> </NetworkLoader>
<Main /> </TendermintWebsocketProvider>
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-sm md:text-md md:flex md:col-span-2 p-4 gap-4 border-t border-neutral-700 dark:border-neutral-300">
<NetworkInfo />
</footer>
</div>
</NetworkLoader>
</TendermintWebsocketProvider>
</ThemeContext.Provider>
); );
} }

View File

@ -14,12 +14,7 @@ jest.mock('../search', () => ({
const renderComponent = () => ( const renderComponent = () => (
<MemoryRouter> <MemoryRouter>
<Header <Header menuOpen={false} setMenuOpen={jest.fn()} />
theme="dark"
toggleTheme={jest.fn()}
menuOpen={false}
setMenuOpen={jest.fn()}
/>
</MemoryRouter> </MemoryRouter>
); );

View File

@ -8,18 +8,11 @@ import type { Dispatch, SetStateAction } from 'react';
import { NetworkSwitcher } from '@vegaprotocol/environment'; import { NetworkSwitcher } from '@vegaprotocol/environment';
interface ThemeToggleProps { interface ThemeToggleProps {
theme: 'light' | 'dark';
toggleTheme: () => void;
menuOpen: boolean; menuOpen: boolean;
setMenuOpen: Dispatch<SetStateAction<boolean>>; setMenuOpen: Dispatch<SetStateAction<boolean>>;
} }
export const Header = ({ export const Header = ({ menuOpen, setMenuOpen }: ThemeToggleProps) => {
theme,
toggleTheme,
menuOpen,
setMenuOpen,
}: ThemeToggleProps) => {
const headerClasses = classnames( const headerClasses = classnames(
'md:col-span-2', 'md:col-span-2',
'grid grid-rows-2 md:grid-rows-1 grid-cols-[1fr_auto] md:grid-cols-[auto_1fr_auto] items-center', 'grid grid-rows-2 md:grid-rows-1 grid-cols-[1fr_auto] md:grid-cols-[auto_1fr_auto] items-center',
@ -48,7 +41,7 @@ export const Header = ({
<Icon name={menuOpen ? 'cross' : 'menu'} /> <Icon name={menuOpen ? 'cross' : 'menu'} />
</button> </button>
<Search /> <Search />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} className="-my-4" /> <ThemeSwitcher className="-my-4" />
</header> </header>
); );
}; };

View File

@ -88,7 +88,7 @@ describe('NestedDataList', () => {
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
const item = getByTestId(`T${i}`); const item = getByTestId(`T${i}`);
const expected = BORDER_COLOURS.dark[i % 5]; const expected = BORDER_COLOURS.light[i % 5];
expect(item.style.borderColor.toUpperCase()).toBe(expected); expect(item.style.borderColor.toUpperCase()).toBe(expected);
} }
}); });

View File

@ -1,7 +1,7 @@
import React, { useCallback, useContext, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import isObject from 'lodash/isObject'; import isObject from 'lodash/isObject';
import { ThemeContext } from '@vegaprotocol/react-helpers'; import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { Icon } from '@vegaprotocol/ui-toolkit'; import { Icon } from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import { VegaColours } from '@vegaprotocol/tailwindcss-config'; import { VegaColours } from '@vegaprotocol/tailwindcss-config';
@ -65,7 +65,7 @@ const NestedDataListItem = ({
); );
const hasChildren = isObject(value) && !!Object.keys(value).length; const hasChildren = isObject(value) && !!Object.keys(value).length;
const title = useMemo(() => camelToTitle(label), [label]); const title = useMemo(() => camelToTitle(label), [label]);
const theme = useContext(ThemeContext); const { theme } = useThemeSwitcher();
const currentLevelBorder = useMemo( const currentLevelBorder = useMemo(
() => getBorderColour(index, theme), () => getBorderColour(index, theme),
[index, theme] [index, theme]

View File

@ -1,7 +1,6 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment'; import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
import App from './app/app'; import App from './app/app';
@ -36,11 +35,9 @@ root?.render(
<StrictMode> <StrictMode>
<BrowserRouter> <BrowserRouter>
<EnvironmentProvider> <EnvironmentProvider>
<ThemeContext.Provider value="light"> <NetworkLoader cache={cache}>
<NetworkLoader cache={cache}> <App />
<App /> </NetworkLoader>
</NetworkLoader>
</ThemeContext.Provider>
</EnvironmentProvider> </EnvironmentProvider>
</BrowserRouter> </BrowserRouter>
</StrictMode> </StrictMode>

View File

@ -10,7 +10,7 @@ import {
import { AsyncRenderer, Button, Lozenge } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Button, Lozenge } from '@vegaprotocol/ui-toolkit';
import type { EthereumConfig } from '@vegaprotocol/web3'; import type { EthereumConfig } from '@vegaprotocol/web3';
import { useEthereumConfig, Web3Provider } from '@vegaprotocol/web3'; import { useEthereumConfig, Web3Provider } from '@vegaprotocol/web3';
import { ThemeContext, useThemeSwitcher, t } from '@vegaprotocol/react-helpers'; import { t } from '@vegaprotocol/react-helpers';
import { ENV } from './config/env'; import { ENV } from './config/env';
import { ContractsProvider } from './config/contracts/contracts-provider'; import { ContractsProvider } from './config/contracts/contracts-provider';
import { import {
@ -55,7 +55,6 @@ function App() {
const { VEGA_ENV, ETHEREUM_PROVIDER_URL } = useEnvironment(); const { VEGA_ENV, ETHEREUM_PROVIDER_URL } = useEnvironment();
const { config, loading, error } = useEthereumConfig(); const { config, loading, error } = useEthereumConfig();
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [theme, toggleTheme] = useThemeSwitcher();
useEffect(() => { useEffect(() => {
Sentry.init({ Sentry.init({
@ -73,25 +72,23 @@ function App() {
}, [config?.chain_id, ETHEREUM_PROVIDER_URL]); }, [config?.chain_id, ETHEREUM_PROVIDER_URL]);
return ( return (
<ThemeContext.Provider value={theme}> <AsyncRenderer loading={loading} data={config} error={error}>
<AsyncRenderer loading={loading} data={config} error={error}> <Web3Provider connectors={Connectors}>
<Web3Provider connectors={Connectors}> <Web3Connector dialogOpen={dialogOpen} setDialogOpen={setDialogOpen}>
<Web3Connector dialogOpen={dialogOpen} setDialogOpen={setDialogOpen}> <EthWalletContainer
<EthWalletContainer dialogOpen={dialogOpen}
dialogOpen={dialogOpen} setDialogOpen={setDialogOpen}
setDialogOpen={setDialogOpen} >
> <ContractsProvider>
<ContractsProvider> <div className={pageWrapperClasses}>
<div className={pageWrapperClasses}> <Header />
<Header theme={theme} toggleTheme={toggleTheme} /> <ConnectedApp config={config} />
<ConnectedApp config={config} /> </div>
</div> </ContractsProvider>
</ContractsProvider> </EthWalletContainer>
</EthWalletContainer> </Web3Connector>
</Web3Connector> </Web3Provider>
</Web3Provider> </AsyncRenderer>
</AsyncRenderer>
</ThemeContext.Provider>
); );
} }

View File

@ -4,19 +4,14 @@ import {
VegaLogo, VegaLogo,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
interface HeaderProps { export const Header = () => {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
export const Header = ({ theme, toggleTheme }: HeaderProps) => {
return ( return (
<header className="relative overflow-hidden py-2 mb-8"> <header className="relative overflow-hidden py-2 mb-8">
<BackgroundVideo /> <BackgroundVideo />
<div className="relative flex justify-center px-2 dark:bg-black bg-white"> <div className="relative flex justify-center px-2 dark:bg-black bg-white">
<div className="w-full max-w-3xl p-5 flex items-center justify-between"> <div className="w-full max-w-3xl p-5 flex items-center justify-between">
<VegaLogo /> <VegaLogo />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} /> <ThemeSwitcher />
</div> </div>
</div> </div>
</header> </header>

View File

@ -15,7 +15,6 @@
<ul> <ul>
<li><a href="./fonts.css">AlphaLyrae font</a></li> <li><a href="./fonts.css">AlphaLyrae font</a></li>
<li><a href="./favicon.ico">Favicon</a></li> <li><a href="./favicon.ico">Favicon</a></li>
<li><a href="./theme.js">Theme</a></li>
</ul> </ul>
</body> </body>
</html> </html>

View File

@ -1,9 +0,0 @@
(function () {
var storedTheme = window.localStorage.getItem('theme');
if (
storedTheme === 'dark' ||
(!storedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
}
})();

View File

@ -1,23 +1,17 @@
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment'; import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
import { Header } from './components/header'; import { Header } from './components/header';
import { StatsManager } from '@vegaprotocol/network-stats'; import { StatsManager } from '@vegaprotocol/network-stats';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
function App() { function App() {
const [theme, toggleTheme] = useThemeSwitcher();
return ( return (
<ThemeContext.Provider value={theme}> <NetworkLoader>
<NetworkLoader> <div className="w-screen min-h-screen grid pb-6 bg-white text-neutral-900 dark:bg-black dark:text-neutral-100">
<div className="w-screen min-h-screen grid pb-6 bg-white text-neutral-900 dark:bg-black dark:text-neutral-100"> <div className="layout-grid w-screen justify-self-center">
<div className="layout-grid w-screen justify-self-center"> <Header />
<Header theme={theme} toggleTheme={toggleTheme} /> <StatsManager className="max-w-3xl px-6" />
<StatsManager className="max-w-3xl px-6" />
</div>
</div> </div>
</NetworkLoader> </div>
</ThemeContext.Provider> </NetworkLoader>
); );
} }

View File

@ -4,12 +4,7 @@ import {
ThemeSwitcher, ThemeSwitcher,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
interface ThemeToggleProps { export const Header = () => {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
export const Header = ({ theme, toggleTheme }: ThemeToggleProps) => {
return ( return (
<header className="relative overflow-hidden py-2 mb-10 md:mb-16"> <header className="relative overflow-hidden py-2 mb-10 md:mb-16">
<BackgroundVideo /> <BackgroundVideo />
@ -17,7 +12,7 @@ export const Header = ({ theme, toggleTheme }: ThemeToggleProps) => {
<div className="relative flex justify-center px-2 dark:bg-black bg-white"> <div className="relative flex justify-center px-2 dark:bg-black bg-white">
<div className="w-full max-w-3xl p-5 flex items-center justify-between"> <div className="w-full max-w-3xl p-5 flex items-center justify-between">
<VegaLogo /> <VegaLogo />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} /> <ThemeSwitcher />
</div> </div>
</div> </div>
</header> </header>

View File

@ -2,12 +2,11 @@ import {
addDecimal, addDecimal,
fromNanoSeconds, fromNanoSeconds,
t, t,
ThemeContext, useThemeSwitcher,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import { useContext } from 'react';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import type { AccountHistoryQuery } from './__generated__/AccountHistory'; import type { AccountHistoryQuery } from './__generated__/AccountHistory';
import { useAccountsWithBalanceQuery } from './__generated__/AccountHistory'; import { useAccountsWithBalanceQuery } from './__generated__/AccountHistory';
@ -209,7 +208,7 @@ export const AccountHistoryChart = ({
accountType: Schema.AccountType; accountType: Schema.AccountType;
asset: AssetFieldsFragment; asset: AssetFieldsFragment;
}) => { }) => {
const theme = useContext(ThemeContext); const { theme } = useThemeSwitcher();
const values: { cols: string[]; rows: [Date, ...number[]][] } | null = const values: { cols: string[]; rows: [Date, ...number[]][] } | null =
useMemo(() => { useMemo(() => {
if (!data?.balanceChanges.edges.length) { if (!data?.balanceChanges.edges.length) {

View File

@ -16,16 +16,10 @@ import {
type NavbarTheme = 'inherit' | 'dark' | 'yellow'; type NavbarTheme = 'inherit' | 'dark' | 'yellow';
interface NavbarProps { interface NavbarProps {
theme: 'light' | 'dark';
toggleTheme: () => void;
navbarTheme?: NavbarTheme; navbarTheme?: NavbarTheme;
} }
export const Navbar = ({ export const Navbar = ({ navbarTheme = 'inherit' }: NavbarProps) => {
theme,
toggleTheme,
navbarTheme = 'inherit',
}: NavbarProps) => {
const { VEGA_TOKEN_URL } = useEnvironment(); const { VEGA_TOKEN_URL } = useEnvironment();
const { marketId } = useGlobalStore((store) => ({ const { marketId } = useGlobalStore((store) => ({
marketId: store.marketId, marketId: store.marketId,
@ -71,7 +65,7 @@ export const Navbar = ({
</a> </a>
<div className="flex items-center gap-2 ml-auto"> <div className="flex items-center gap-2 ml-auto">
<VegaWalletConnectButton /> <VegaWalletConnectButton />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} /> <ThemeSwitcher />
</div> </div>
</Nav> </Nav>
); );

View File

@ -1,7 +1,7 @@
import Head from 'next/head'; import Head from 'next/head';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import { Navbar } from '../components/navbar'; import { Navbar } from '../components/navbar';
import { t, ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers'; import { t } from '@vegaprotocol/react-helpers';
import { import {
useEagerConnect as useVegaEagerConnect, useEagerConnect as useVegaEagerConnect,
VegaWalletProvider, VegaWalletProvider,
@ -48,10 +48,9 @@ const Title = () => {
function AppBody({ Component }: AppProps) { function AppBody({ Component }: AppProps) {
const location = useLocation(); const location = useLocation();
const { VEGA_ENV } = useEnvironment(); const { VEGA_ENV } = useEnvironment();
const [theme, toggleTheme] = useThemeSwitcher();
return ( return (
<ThemeContext.Provider value={theme}> <>
<Head> <Head>
{/* Cannot use meta tags in _document.page.tsx see https://nextjs.org/docs/messages/no-document-viewport-meta */} {/* Cannot use meta tags in _document.page.tsx see https://nextjs.org/docs/messages/no-document-viewport-meta */}
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@ -62,8 +61,6 @@ function AppBody({ Component }: AppProps) {
<Web3Provider> <Web3Provider>
<div className="h-full relative dark:bg-black dark:text-white z-0 grid grid-rows-[min-content,1fr,min-content]"> <div className="h-full relative dark:bg-black dark:text-white z-0 grid grid-rows-[min-content,1fr,min-content]">
<Navbar <Navbar
theme={theme}
toggleTheme={toggleTheme}
navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'}
/> />
<main data-testid={location.pathname}> <main data-testid={location.pathname}>
@ -76,7 +73,7 @@ function AppBody({ Component }: AppProps) {
</Web3Provider> </Web3Provider>
</AppLoader> </AppLoader>
</VegaWalletProvider> </VegaWalletProvider>
</ThemeContext.Provider> </>
); );
} }

View File

@ -17,11 +17,6 @@ export default function Document() {
type="image/x-icon" type="image/x-icon"
href="https://static.vega.xyz/favicon.ico" href="https://static.vega.xyz/favicon.ico"
/> />
<script
src="https://static.vega.xyz/theme.js"
type="text/javascript"
async
/>
{['1', 'true'].includes(process.env['NX_USE_ENV_OVERRIDES'] || '') ? ( {['1', 'true'].includes(process.env['NX_USE_ENV_OVERRIDES'] || '') ? (
/* eslint-disable-next-line @next/next/no-sync-scripts */ /* eslint-disable-next-line @next/next/no-sync-scripts */
<script src="/assets/env-config.js" type="text/javascript" /> <script src="/assets/env-config.js" type="text/javascript" />

View File

@ -1,6 +1,5 @@
import './styles.scss'; import './styles.scss';
import { ThemeContext } from '@vegaprotocol/react-helpers'; import { useStorybookThemeObserver } from '@vegaprotocol/react-helpers';
import { useEffect, useState } from 'react';
export const parameters = { export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' }, actions: { argTypesRegex: '^on[A-Z].*' },
backgrounds: { disable: true }, backgrounds: { disable: true },
@ -14,36 +13,12 @@ export const parameters = {
}; };
export const decorators = [ export const decorators = [
(Story, context) => { (Story) => {
// storybook-addon-themes doesn't seem to provide the current selected useStorybookThemeObserver();
// theme in context, we need to provide it in JS as some components
// rely on it for rendering
const [theme, setTheme] = useState(context.parameters.themes.default);
useEffect(() => {
const observer = new MutationObserver((mutationList) => {
if (mutationList.length) {
const body = mutationList[0].target;
if (body.classList.contains('dark')) {
setTheme('dark');
} else {
setTheme('light');
}
}
});
observer.observe(document.body, { attributes: true });
return () => {
observer.disconnect();
};
}, []);
return ( return (
<div style={{ width: '100%', height: 500 }}> <div style={{ width: '100%', height: 500 }}>
<ThemeContext.Provider value={theme}> <Story />
<Story />
</ThemeContext.Provider>
</div> </div>
); );
}, },

View File

@ -13,9 +13,9 @@ import {
} from 'pennant'; } from 'pennant';
import { VegaDataSource } from './data-source'; import { VegaDataSource } from './data-source';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useContext, useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { ThemeContext } from '@vegaprotocol/react-helpers'; import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
@ -46,7 +46,7 @@ export const CandlesChartContainer = ({
}: CandlesChartContainerProps) => { }: CandlesChartContainerProps) => {
const client = useApolloClient(); const client = useApolloClient();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const theme = useContext(ThemeContext); const { theme } = useThemeSwitcher();
const [interval, setInterval] = useState<Interval>(Interval.I15M); const [interval, setInterval] = useState<Interval>(Interval.I15M);
const [chartType, setChartType] = useState<ChartType>(ChartType.CANDLE); const [chartType, setChartType] = useState<ChartType>(ChartType.CANDLE);

View File

@ -1,6 +1,5 @@
import './styles.css'; import './styles.css';
import { ThemeContext } from '@vegaprotocol/react-helpers'; import { useStorybookThemeObserver } from '@vegaprotocol/react-helpers';
import { useEffect, useState } from 'react';
export const parameters = { export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' }, actions: { argTypesRegex: '^on[A-Z].*' },
@ -15,36 +14,12 @@ export const parameters = {
}; };
export const decorators = [ export const decorators = [
(Story, context) => { (Story) => {
// storybook-addon-themes doesnt seem to provide the current selected useStorybookThemeObserver();
// theme in context, we need to provid it in JS as some components
// rely on it for rendering
const [theme, setTheme] = useState(context.parameters.themes.default);
useEffect(() => {
const observer = new MutationObserver((mutationList) => {
if (mutationList.length) {
const body = mutationList[0].target;
if (body.classList.contains('dark')) {
setTheme('dark');
} else {
setTheme('light');
}
}
});
observer.observe(document.body, { attributes: true });
return () => {
observer.disconnect();
};
}, []);
return ( return (
<div style={{ width: '100%', height: 500 }}> <div style={{ width: '100%', height: 500 }}>
<ThemeContext.Provider value={theme}> <Story />
<Story />
</ThemeContext.Provider>
</div> </div>
); );
}, },

View File

@ -4,18 +4,11 @@ import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { import {
useDataProvider, useDataProvider,
addDecimal, addDecimal,
ThemeContext,
getNumberFormat, getNumberFormat,
useThemeSwitcher,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { marketDepthProvider } from './market-depth-provider'; import { marketDepthProvider } from './market-depth-provider';
import { import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
useCallback,
useEffect,
useMemo,
useRef,
useState,
useContext,
} from 'react';
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list'; import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
import type { MarketData } from '@vegaprotocol/market-list'; import type { MarketData } from '@vegaprotocol/market-list';
import type { import type {
@ -35,7 +28,7 @@ const formatMidPrice = (midPrice: string, decimalPlaces: number) =>
type DepthData = Pick<DepthChartProps, 'data' | 'midPrice'>; type DepthData = Pick<DepthChartProps, 'data' | 'midPrice'>;
export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => { export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
const theme = useContext(ThemeContext); const { theme } = useThemeSwitcher();
const variables = useMemo(() => ({ marketId }), [marketId]); const variables = useMemo(() => ({ marketId }), [marketId]);
const [depthData, setDepthData] = useState<DepthData | null>(null); const [depthData, setDepthData] = useState<DepthData | null>(null);
const dataRef = useRef<DepthData | null>(null); const dataRef = useRef<DepthData | null>(null);

View File

@ -8,7 +8,6 @@ import {
useState, useState,
useMemo, useMemo,
useCallback, useCallback,
useContext,
Fragment, Fragment,
} from 'react'; } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
@ -16,9 +15,9 @@ import classNames from 'classnames';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
t, t,
ThemeContext,
useResizeObserver, useResizeObserver,
formatNumberFixed, formatNumberFixed,
useThemeSwitcher,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { OrderbookRow } from './orderbook-row'; import { OrderbookRow } from './orderbook-row';
@ -281,7 +280,7 @@ export const Orderbook = ({
fillGaps: initialFillGaps, fillGaps: initialFillGaps,
onResolutionChange, onResolutionChange,
}: OrderbookProps) => { }: OrderbookProps) => {
const theme = useContext(ThemeContext); const { theme } = useThemeSwitcher();
const scrollElement = useRef<HTMLDivElement>(null); const scrollElement = useRef<HTMLDivElement>(null);
const rootElement = useRef<HTMLDivElement>(null); const rootElement = useRef<HTMLDivElement>(null);
const gridElement = useRef<HTMLDivElement>(null); const gridElement = useRef<HTMLDivElement>(null);

View File

@ -1,6 +1,5 @@
import './styles.scss'; import './styles.scss';
import { ThemeContext } from '@vegaprotocol/react-helpers'; import { useStorybookThemeObserver } from '@vegaprotocol/react-helpers';
import { useEffect, useState } from 'react';
export const parameters = { export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' }, actions: { argTypesRegex: '^on[A-Z].*' },
backgrounds: { disable: true }, backgrounds: { disable: true },
@ -14,36 +13,12 @@ export const parameters = {
}; };
export const decorators = [ export const decorators = [
(Story, context) => { (Story) => {
// storybook-addon-themes doesnt seem to provide the current selected useStorybookThemeObserver();
// theme in context, we need to provid it in JS as some components
// rely on it for rendering
const [theme, setTheme] = useState(context.parameters.themes.default);
useEffect(() => {
const observer = new MutationObserver((mutationList) => {
if (mutationList.length) {
const body = mutationList[0].target;
if (body.classList.contains('dark')) {
setTheme('dark');
} else {
setTheme('light');
}
}
});
observer.observe(document.body, { attributes: true });
return () => {
observer.disconnect();
};
}, []);
return ( return (
<div style={{ width: '100%', height: 500 }}> <div style={{ width: '100%', height: 500 }}>
<ThemeContext.Provider value={theme}> <Story />
<Story />
</ThemeContext.Provider>
</div> </div>
); );
}, },

View File

@ -9,4 +9,5 @@ export * from './use-resize-observer';
export * from './use-resize'; export * from './use-resize';
export * from './use-screen-dimensions'; export * from './use-screen-dimensions';
export * from './use-theme-switcher'; export * from './use-theme-switcher';
export * from './use-storybook-theme-observer';
export * from './use-yesterday'; export * from './use-yesterday';

View File

@ -0,0 +1,25 @@
import { useMutationObserver } from './use-mutation-observer';
import { useThemeSwitcher } from './use-theme-switcher';
/**
* Listens for theme classname changes on the body tag and applies the
* same theme to the theme store. This is required as some components
* (EG AgGrid) rely on the theme as a JS variable rather than purely a className
*
* Additionally storybook-addon-themes doesn't seem to provide the current selected
* theme in context so we listen for changes on the body tag
*/
export const useStorybookThemeObserver = () => {
const { setTheme } = useThemeSwitcher();
useMutationObserver(document.body, (mutationList) => {
if (mutationList.length) {
const body = mutationList[0].target as HTMLElement;
if (body && body.classList.contains('dark')) {
setTheme('dark');
} else {
setTheme('light');
}
}
});
};

View File

@ -1,42 +1,77 @@
import { useEffect, useState } from 'react'; import create from 'zustand';
import { LocalStorage } from '../lib/storage'; import { LocalStorage } from '../lib/storage';
const darkTheme = 'dark'; const THEME_STORAGE_KEY = 'theme';
const lightTheme = 'light'; const Themes = {
type themeVariant = typeof darkTheme | typeof lightTheme; DARK: 'dark',
LIGHT: 'light',
} as const;
const darkThemeCssClass = darkTheme; type Theme = typeof Themes[keyof typeof Themes];
const getCurrentTheme = () => { const validateTheme = (theme: string): theme is Theme => {
const theme = LocalStorage.getItem('theme'); if (Object.values(Themes).includes(theme as Theme)) return true;
if ( LocalStorage.removeItem(THEME_STORAGE_KEY);
theme === darkTheme || return false;
(!theme &&
typeof window !== 'undefined' &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
return darkTheme;
}
return lightTheme;
}; };
const toggleTheme = () => { const setThemeClassName = (theme: Theme) => {
const theme = document.documentElement.classList.contains(darkThemeCssClass) if (typeof window !== 'undefined') {
? lightTheme if (theme === Themes.DARK) {
: darkTheme; document.documentElement.classList.add(Themes.DARK);
LocalStorage.setItem('theme', theme); } else {
document.documentElement.classList.remove(Themes.DARK);
}
}
};
const getCurrentTheme = () => {
const storedTheme = LocalStorage.getItem(THEME_STORAGE_KEY);
if (storedTheme && validateTheme(storedTheme)) {
setThemeClassName(storedTheme);
return storedTheme;
}
const theme =
typeof window !== 'undefined' &&
typeof window.matchMedia === 'function' && // jest test environment matchMedia is undefined
window.matchMedia('(prefers-color-scheme: dark)').matches
? Themes.DARK
: Themes.LIGHT;
setThemeClassName(theme);
return theme; return theme;
}; };
export function useThemeSwitcher(): [themeVariant, () => void] { type ThemeStore = {
const [theme, setTheme] = useState<themeVariant>(lightTheme); theme: Theme;
useEffect(() => setTheme(getCurrentTheme()), []); setTheme: (theme?: Theme) => void;
useEffect(() => { };
if (theme === darkTheme) {
document.documentElement.classList.add(darkThemeCssClass); const useThemeStore = create<ThemeStore>((set) => ({
} else { theme: getCurrentTheme(),
document.documentElement.classList.remove(darkThemeCssClass); setTheme: (newTheme) => {
} set((state) => {
}, [theme]); let theme: Theme =
return [theme, () => setTheme(toggleTheme)]; state.theme === Themes.LIGHT ? Themes.DARK : Themes.LIGHT;
if (newTheme) {
theme = newTheme;
}
LocalStorage.setItem(THEME_STORAGE_KEY, theme);
setThemeClassName(theme);
return {
theme,
};
});
},
}));
export function useThemeSwitcher(): ThemeStore {
const { theme, setTheme } = useThemeStore();
return { theme, setTheme };
} }

View File

@ -1,5 +1,4 @@
export * from './hooks'; export * from './hooks';
export * from './lib/context';
export * from './lib/format'; export * from './lib/format';
export * from './lib/generic-data-provider'; export * from './lib/generic-data-provider';
export * from './lib/get-nodes'; export * from './lib/get-nodes';

View File

@ -1,3 +0,0 @@
import * as React from 'react';
export const ThemeContext = React.createContext<'light' | 'dark'>('dark');

View File

@ -1 +0,0 @@
export * from './context';

View File

@ -1,4 +1,3 @@
export * from './context';
export * from './format'; export * from './format';
export * from './grid'; export * from './grid';
export * from './storage'; export * from './storage';

View File

@ -1,3 +1,6 @@
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import classNames from 'classnames';
import { useEffect } from 'react';
import '../src/styles.css'; import '../src/styles.css';
export const parameters = { export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' }, actions: { argTypesRegex: '^on[A-Z].*' },
@ -38,47 +41,50 @@ export const globalTypes = {
}, },
}; };
const StoryWrapper = ({ children, className, style }) => ( const StoryWrapper = ({ children, fill }) => {
<div style={style} className={className}> const classes = classNames(
<div className="p-4"> 'p-4',
<div className="dark:bg-black dark:text-neutral-200 bg-white text-neutral-800"> 'bg-white dark:bg-black',
{children} 'text-neutral-800 dark:text-neutral-200',
</div> {
</div> 'w-screen h-screen': fill,
</div> }
); );
return <div className={classes}>{children}</div>;
};
const lightThemeClasses = 'bg-white text-black'; const ThemeWrapper = (Story, context) => {
const darkThemeClasses = 'dark bg-black text-white';
const withTheme = (Story, context) => {
const theme = context.parameters.theme || context.globals.theme; const theme = context.parameters.theme || context.globals.theme;
const storyClasses = `h-[100vh] w-[100vw] ${ const { setTheme } = useThemeSwitcher();
theme === 'dark' ? darkThemeClasses : lightThemeClasses
}`;
document.body.classList.toggle('dark', theme === 'dark'); useEffect(() => {
// in side by side mode a 'dark' class on the html tag will interfere
// making the light 'side' dark, so remove it in that case
if (theme === 'sideBySide') {
document.documentElement.classList.remove('dark');
} else {
setTheme(theme);
}
}, [setTheme, theme]);
return theme === 'sideBySide' ? ( return theme === 'sideBySide' ? (
<> <>
<div className={lightThemeClasses}> <div className="bg-white text-black">
<StoryWrapper> <StoryWrapper>
<Story /> <Story />
</StoryWrapper> </StoryWrapper>
</div> </div>
<div className={darkThemeClasses}> <div className="dark bg-black text-white">
<StoryWrapper> <StoryWrapper>
<Story /> <Story />
</StoryWrapper> </StoryWrapper>
</div> </div>
</> </>
) : ( ) : (
<div className={storyClasses}> <StoryWrapper fill={true}>
<StoryWrapper> <Story />
<Story /> </StoryWrapper>
</StoryWrapper>
</div>
); );
}; };
export const decorators = [withTheme]; export const decorators = [ThemeWrapper];

View File

@ -1,9 +1,8 @@
import type { ReactNode, FunctionComponent } from 'react'; import type { ReactNode, FunctionComponent } from 'react';
import { useContext } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react'; import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react';
import { AgGridReact } from 'ag-grid-react'; import { AgGridReact } from 'ag-grid-react';
import { ThemeContext } from '@vegaprotocol/react-helpers'; import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import 'ag-grid-community/dist/styles/ag-grid.css'; import 'ag-grid-community/dist/styles/ag-grid.css';
interface GridProps { interface GridProps {
@ -33,7 +32,7 @@ export const AgGridThemed = ({
gridRef?: React.ForwardedRef<AgGridReact>; gridRef?: React.ForwardedRef<AgGridReact>;
customThemeParams?: string; customThemeParams?: string;
}) => { }) => {
const theme = useContext(ThemeContext); const { theme } = useThemeSwitcher();
const defaultProps = { rowHeight: 22, headerHeight: 22 }; const defaultProps = { rowHeight: 22, headerHeight: 22 };
return ( return (
<div <div

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react'; import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react';
import { AgGridReact } from 'ag-grid-react'; import { AgGridReact } from 'ag-grid-react';
import { ThemeContext } from '@vegaprotocol/react-helpers'; import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import 'ag-grid-community/dist/styles/ag-grid.css'; import 'ag-grid-community/dist/styles/ag-grid.css';
const AgGridLightTheme = React.lazy(() => const AgGridLightTheme = React.lazy(() =>
@ -23,7 +23,7 @@ export const AgGridThemed = React.forwardRef<
className?: string; className?: string;
} }
>(({ style, className, ...props }, ref) => { >(({ style, className, ...props }, ref) => {
const theme = React.useContext(ThemeContext); const { theme } = useThemeSwitcher();
return ( return (
<div <div
className={`${className ?? ''} ${ className={`${className ?? ''} ${

View File

@ -4,14 +4,7 @@ import { ThemeSwitcher } from './theme-switcher';
describe('ThemeSwitcher', () => { describe('ThemeSwitcher', () => {
it('should render successfully', () => { it('should render successfully', () => {
const { baseElement } = render( const { baseElement } = render(<ThemeSwitcher />);
<ThemeSwitcher
theme="dark"
onToggle={() => {
return;
}}
/>
);
expect(baseElement).toBeTruthy(); expect(baseElement).toBeTruthy();
}); });
}); });

View File

@ -1,5 +1,4 @@
import type { Story, Meta } from '@storybook/react'; import type { Story, Meta } from '@storybook/react';
import { useState } from 'react';
import { ThemeSwitcher } from './theme-switcher'; import { ThemeSwitcher } from './theme-switcher';
export default { export default {
@ -8,16 +7,9 @@ export default {
} as Meta; } as Meta;
const Template: Story = () => { const Template: Story = () => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return ( return (
<div className="p-4"> <div className="p-4">
<ThemeSwitcher <ThemeSwitcher />
theme={theme}
onToggle={() => {
setTheme((curr) => (curr === 'light' ? 'dark' : 'light'));
document.body.classList.toggle('dark');
}}
/>
</div> </div>
); );
}; };

View File

@ -1,19 +1,12 @@
import React from 'react'; import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { SunIcon, MoonIcon } from './icons'; import { SunIcon, MoonIcon } from './icons';
export const ThemeSwitcher = ({ export const ThemeSwitcher = ({ className }: { className?: string }) => {
theme, const { theme, setTheme } = useThemeSwitcher();
onToggle,
className,
}: {
theme: 'light' | 'dark';
onToggle: () => void;
className?: string;
}) => {
return ( return (
<button <button
type="button" type="button"
onClick={onToggle} onClick={() => setTheme()}
className={className} className={className}
data-testid="theme-switcher" data-testid="theme-switcher"
> >