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 { useLocation } from 'react-router-dom';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
import {
VegaConnectDialog,
@ -18,8 +16,7 @@ import useLocalValues from './hooks/use-local-values';
import type { InMemoryCacheConfig } from '@apollo/client';
function App() {
const [theme, toggleTheme] = useThemeSwitcher();
const localValues = useLocalValues(theme, toggleTheme);
const localValues = useLocalValues();
const {
vegaWalletDialog,
menu: { setMenuOpen },
@ -56,25 +53,23 @@ function App() {
return (
<EnvironmentProvider>
<ThemeContext.Provider value={theme}>
<NetworkLoader cache={cacheConfig}>
<VegaWalletProvider>
<LocalContext.Provider value={localValues}>
<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]">
<Header />
<Main />
<VegaConnectDialog connectors={Connectors} />
<VegaManageDialog
dialogOpen={vegaWalletDialog.manage}
setDialogOpen={vegaWalletDialog.setManage}
/>
</div>
</AppLoader>
</LocalContext.Provider>
</VegaWalletProvider>
</NetworkLoader>
</ThemeContext.Provider>
<NetworkLoader cache={cacheConfig}>
<VegaWalletProvider>
<LocalContext.Provider value={localValues}>
<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]">
<Header />
<Main />
<VegaConnectDialog connectors={Connectors} />
<VegaManageDialog
dialogOpen={vegaWalletDialog.manage}
setDialogOpen={vegaWalletDialog.setManage}
/>
</div>
</AppLoader>
</LocalContext.Provider>
</VegaWalletProvider>
</NetworkLoader>
</EnvironmentProvider>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,15 @@
import { useMemo, useState } from 'react';
import type { LocalValues } from '../context/local-context';
const useLocalValues = (theme: 'light' | 'dark', toggleTheme: () => void) => {
const useLocalValues = () => {
const [manage, setManage] = useState<boolean>(false);
const [menuOpen, setMenuOpen] = useState(false);
return useMemo<LocalValues>(
() => ({
vegaWalletDialog: { manage, setManage },
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 { BrowserTracing } from '@sentry/tracing';
import { useLocation } from 'react-router-dom';
import { ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers';
import {
EnvironmentProvider,
NetworkLoader,
@ -19,7 +18,6 @@ import type { InMemoryCacheConfig } from '@apollo/client';
function App() {
const { VEGA_ENV } = useEnvironment();
const [theme, toggleTheme] = useThemeSwitcher();
const [menuOpen, setMenuOpen] = useState(false);
const location = useLocation();
@ -57,25 +55,18 @@ function App() {
);
return (
<ThemeContext.Provider value={theme}>
<TendermintWebsocketProvider>
<NetworkLoader cache={cacheConfig}>
<div className={layoutClasses}>
<Header
theme={theme}
toggleTheme={toggleTheme}
menuOpen={menuOpen}
setMenuOpen={setMenuOpen}
/>
<Nav menuOpen={menuOpen} />
<Main />
<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>
<TendermintWebsocketProvider>
<NetworkLoader cache={cacheConfig}>
<div className={layoutClasses}>
<Header menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
<Nav menuOpen={menuOpen} />
<Main />
<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>
);
}

View File

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

View File

@ -8,18 +8,11 @@ import type { Dispatch, SetStateAction } from 'react';
import { NetworkSwitcher } from '@vegaprotocol/environment';
interface ThemeToggleProps {
theme: 'light' | 'dark';
toggleTheme: () => void;
menuOpen: boolean;
setMenuOpen: Dispatch<SetStateAction<boolean>>;
}
export const Header = ({
theme,
toggleTheme,
menuOpen,
setMenuOpen,
}: ThemeToggleProps) => {
export const Header = ({ menuOpen, setMenuOpen }: ThemeToggleProps) => {
const headerClasses = classnames(
'md:col-span-2',
'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'} />
</button>
<Search />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} className="-my-4" />
<ThemeSwitcher className="-my-4" />
</header>
);
};

View File

@ -88,7 +88,7 @@ describe('NestedDataList', () => {
for (let i = 0; i < 8; 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);
}
});

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

View File

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

View File

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

View File

@ -4,19 +4,14 @@ import {
VegaLogo,
} from '@vegaprotocol/ui-toolkit';
interface HeaderProps {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
export const Header = ({ theme, toggleTheme }: HeaderProps) => {
export const Header = () => {
return (
<header className="relative overflow-hidden py-2 mb-8">
<BackgroundVideo />
<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">
<VegaLogo />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} />
<ThemeSwitcher />
</div>
</div>
</header>

View File

@ -15,7 +15,6 @@
<ul>
<li><a href="./fonts.css">AlphaLyrae font</a></li>
<li><a href="./favicon.ico">Favicon</a></li>
<li><a href="./theme.js">Theme</a></li>
</ul>
</body>
</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 { Header } from './components/header';
import { StatsManager } from '@vegaprotocol/network-stats';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
function App() {
const [theme, toggleTheme] = useThemeSwitcher();
return (
<ThemeContext.Provider value={theme}>
<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="layout-grid w-screen justify-self-center">
<Header theme={theme} toggleTheme={toggleTheme} />
<StatsManager className="max-w-3xl px-6" />
</div>
<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="layout-grid w-screen justify-self-center">
<Header />
<StatsManager className="max-w-3xl px-6" />
</div>
</NetworkLoader>
</ThemeContext.Provider>
</div>
</NetworkLoader>
);
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import Head from 'next/head';
import type { AppProps } from 'next/app';
import { Navbar } from '../components/navbar';
import { t, ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/react-helpers';
import {
useEagerConnect as useVegaEagerConnect,
VegaWalletProvider,
@ -48,10 +48,9 @@ const Title = () => {
function AppBody({ Component }: AppProps) {
const location = useLocation();
const { VEGA_ENV } = useEnvironment();
const [theme, toggleTheme] = useThemeSwitcher();
return (
<ThemeContext.Provider value={theme}>
<>
<Head>
{/* 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" />
@ -62,8 +61,6 @@ function AppBody({ Component }: AppProps) {
<Web3Provider>
<div className="h-full relative dark:bg-black dark:text-white z-0 grid grid-rows-[min-content,1fr,min-content]">
<Navbar
theme={theme}
toggleTheme={toggleTheme}
navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'}
/>
<main data-testid={location.pathname}>
@ -76,7 +73,7 @@ function AppBody({ Component }: AppProps) {
</Web3Provider>
</AppLoader>
</VegaWalletProvider>
</ThemeContext.Provider>
</>
);
}

View File

@ -17,11 +17,6 @@ export default function Document() {
type="image/x-icon"
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'] || '') ? (
/* eslint-disable-next-line @next/next/no-sync-scripts */
<script src="/assets/env-config.js" type="text/javascript" />

View File

@ -1,6 +1,5 @@
import './styles.scss';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useEffect, useState } from 'react';
import { useStorybookThemeObserver } from '@vegaprotocol/react-helpers';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
backgrounds: { disable: true },
@ -14,36 +13,12 @@ export const parameters = {
};
export const decorators = [
(Story, context) => {
// storybook-addon-themes doesn't seem to provide the current selected
// 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();
};
}, []);
(Story) => {
useStorybookThemeObserver();
return (
<div style={{ width: '100%', height: 500 }}>
<ThemeContext.Provider value={theme}>
<Story />
</ThemeContext.Provider>
<Story />
</div>
);
},

View File

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

View File

@ -1,6 +1,5 @@
import './styles.css';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useEffect, useState } from 'react';
import { useStorybookThemeObserver } from '@vegaprotocol/react-helpers';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
@ -15,36 +14,12 @@ export const parameters = {
};
export const decorators = [
(Story, context) => {
// storybook-addon-themes doesnt seem to provide the current selected
// 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();
};
}, []);
(Story) => {
useStorybookThemeObserver();
return (
<div style={{ width: '100%', height: 500 }}>
<ThemeContext.Provider value={theme}>
<Story />
</ThemeContext.Provider>
<Story />
</div>
);
},

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import './styles.scss';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { useEffect, useState } from 'react';
import { useStorybookThemeObserver } from '@vegaprotocol/react-helpers';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
backgrounds: { disable: true },
@ -14,36 +13,12 @@ export const parameters = {
};
export const decorators = [
(Story, context) => {
// storybook-addon-themes doesnt seem to provide the current selected
// 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();
};
}, []);
(Story) => {
useStorybookThemeObserver();
return (
<div style={{ width: '100%', height: 500 }}>
<ThemeContext.Provider value={theme}>
<Story />
</ThemeContext.Provider>
<Story />
</div>
);
},

View File

@ -9,4 +9,5 @@ export * from './use-resize-observer';
export * from './use-resize';
export * from './use-screen-dimensions';
export * from './use-theme-switcher';
export * from './use-storybook-theme-observer';
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';
const darkTheme = 'dark';
const lightTheme = 'light';
type themeVariant = typeof darkTheme | typeof lightTheme;
const THEME_STORAGE_KEY = 'theme';
const Themes = {
DARK: 'dark',
LIGHT: 'light',
} as const;
const darkThemeCssClass = darkTheme;
type Theme = typeof Themes[keyof typeof Themes];
const getCurrentTheme = () => {
const theme = LocalStorage.getItem('theme');
if (
theme === darkTheme ||
(!theme &&
typeof window !== 'undefined' &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
return darkTheme;
}
return lightTheme;
const validateTheme = (theme: string): theme is Theme => {
if (Object.values(Themes).includes(theme as Theme)) return true;
LocalStorage.removeItem(THEME_STORAGE_KEY);
return false;
};
const toggleTheme = () => {
const theme = document.documentElement.classList.contains(darkThemeCssClass)
? lightTheme
: darkTheme;
LocalStorage.setItem('theme', theme);
const setThemeClassName = (theme: Theme) => {
if (typeof window !== 'undefined') {
if (theme === Themes.DARK) {
document.documentElement.classList.add(Themes.DARK);
} 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;
};
export function useThemeSwitcher(): [themeVariant, () => void] {
const [theme, setTheme] = useState<themeVariant>(lightTheme);
useEffect(() => setTheme(getCurrentTheme()), []);
useEffect(() => {
if (theme === darkTheme) {
document.documentElement.classList.add(darkThemeCssClass);
} else {
document.documentElement.classList.remove(darkThemeCssClass);
}
}, [theme]);
return [theme, () => setTheme(toggleTheme)];
type ThemeStore = {
theme: Theme;
setTheme: (theme?: Theme) => void;
};
const useThemeStore = create<ThemeStore>((set) => ({
theme: getCurrentTheme(),
setTheme: (newTheme) => {
set((state) => {
let theme: Theme =
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 './lib/context';
export * from './lib/format';
export * from './lib/generic-data-provider';
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 './grid';
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';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
@ -38,47 +41,50 @@ export const globalTypes = {
},
};
const StoryWrapper = ({ children, className, style }) => (
<div style={style} className={className}>
<div className="p-4">
<div className="dark:bg-black dark:text-neutral-200 bg-white text-neutral-800">
{children}
</div>
</div>
</div>
);
const StoryWrapper = ({ children, fill }) => {
const classes = classNames(
'p-4',
'bg-white dark:bg-black',
'text-neutral-800 dark:text-neutral-200',
{
'w-screen h-screen': fill,
}
);
return <div className={classes}>{children}</div>;
};
const lightThemeClasses = 'bg-white text-black';
const darkThemeClasses = 'dark bg-black text-white';
const withTheme = (Story, context) => {
const ThemeWrapper = (Story, context) => {
const theme = context.parameters.theme || context.globals.theme;
const storyClasses = `h-[100vh] w-[100vw] ${
theme === 'dark' ? darkThemeClasses : lightThemeClasses
}`;
const { setTheme } = useThemeSwitcher();
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' ? (
<>
<div className={lightThemeClasses}>
<div className="bg-white text-black">
<StoryWrapper>
<Story />
</StoryWrapper>
</div>
<div className={darkThemeClasses}>
<div className="dark bg-black text-white">
<StoryWrapper>
<Story />
</StoryWrapper>
</div>
</>
) : (
<div className={storyClasses}>
<StoryWrapper>
<Story />
</StoryWrapper>
</div>
<StoryWrapper fill={true}>
<Story />
</StoryWrapper>
);
};
export const decorators = [withTheme];
export const decorators = [ThemeWrapper];

View File

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

View File

@ -1,7 +1,7 @@
import * as React from 'react';
import type { AgGridReactProps, AgReactUiProps } 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';
const AgGridLightTheme = React.lazy(() =>
@ -23,7 +23,7 @@ export const AgGridThemed = React.forwardRef<
className?: string;
}
>(({ style, className, ...props }, ref) => {
const theme = React.useContext(ThemeContext);
const { theme } = useThemeSwitcher();
return (
<div
className={`${className ?? ''} ${

View File

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

View File

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

View File

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