feat(trading): store current tab selection (#3148)

This commit is contained in:
Bartłomiej Głownia 2023-03-21 15:50:31 +01:00 committed by GitHub
parent b2dd053621
commit 1d178b940f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 131 additions and 27 deletions

View File

@ -282,7 +282,7 @@ export const LiquidityViewContainer = ({
<div className="break-word">{marketId}</div> <div className="break-word">{marketId}</div>
</HeaderStat> </HeaderStat>
</Header> </Header>
<Tabs active={getActiveDefaultId()}> <Tabs defaultValue={getActiveDefaultId()}>
<Tab <Tab
id={LiquidityTabs.MyLiquidityProvision} id={LiquidityTabs.MyLiquidityProvision}
name={t('My liquidity provision')} name={t('My liquidity provision')}

View File

@ -14,7 +14,7 @@ import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart'; import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
import { import {
Tab, Tab,
Tabs, LocalStoragePersistTabs as Tabs,
ResizableGrid, ResizableGrid,
ResizableGridPanel, ResizableGridPanel,
Splash, Splash,
@ -96,7 +96,7 @@ const MarketBottomPanel = memo(
minSize={50} minSize={50}
> >
<TradeGridChild> <TradeGridChild>
<Tabs> <Tabs storageKey="console-trade-grid-orders">
<Tab id="orders" name={t('Orders')}> <Tab id="orders" name={t('Orders')}>
<VegaWalletContainer> <VegaWalletContainer>
<TradingViews.Orders <TradingViews.Orders
@ -122,7 +122,7 @@ const MarketBottomPanel = memo(
minSize={50} minSize={50}
> >
<TradeGridChild> <TradeGridChild>
<Tabs> <Tabs storageKey="console-trade-grid-positions">
<Tab id="positions" name={t('Positions')}> <Tab id="positions" name={t('Positions')}>
<VegaWalletContainer> <VegaWalletContainer>
<TradingViews.Positions <TradingViews.Positions
@ -146,7 +146,7 @@ const MarketBottomPanel = memo(
</ResizableGrid> </ResizableGrid>
) : ( ) : (
<TradeGridChild> <TradeGridChild>
<Tabs> <Tabs storageKey="console-trade-grid-positions">
<Tab id="positions" name={t('Positions')}> <Tab id="positions" name={t('Positions')}>
<VegaWalletContainer> <VegaWalletContainer>
<TradingViews.Positions onMarketClick={onMarketClick} /> <TradingViews.Positions onMarketClick={onMarketClick} />
@ -201,7 +201,7 @@ const MainGrid = memo(
preferredSize="50%" preferredSize="50%"
> >
<TradeGridChild> <TradeGridChild>
<Tabs> <Tabs storageKey="console-trade-grid-chart">
<Tab id="chart" name={t('Chart')}> <Tab id="chart" name={t('Chart')}>
<TradingViews.Candles marketId={marketId} /> <TradingViews.Candles marketId={marketId} />
</Tab> </Tab>
@ -220,7 +220,7 @@ const MainGrid = memo(
minSize={300} minSize={300}
> >
<TradeGridChild> <TradeGridChild>
<Tabs> <Tabs storageKey="console-trade-grid-ticket">
<Tab id="ticket" name={t('Ticket')}> <Tab id="ticket" name={t('Ticket')}>
<TradingViews.Ticket <TradingViews.Ticket
marketId={marketId} marketId={marketId}
@ -244,7 +244,7 @@ const MainGrid = memo(
minSize={200} minSize={200}
> >
<TradeGridChild> <TradeGridChild>
<Tabs> <Tabs storageKey="console-trade-grid-orderbook">
<Tab id="orderbook" name={t('Orderbook')}> <Tab id="orderbook" name={t('Orderbook')}>
<TradingViews.Orderbook marketId={marketId} /> <TradingViews.Orderbook marketId={marketId} />
</Tab> </Tab>

View File

@ -1,7 +1,7 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils'; import { titlefy } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Tabs, Tab } from '@vegaprotocol/ui-toolkit'; import { LocalStoragePersistTabs as Tabs, Tab } from '@vegaprotocol/ui-toolkit';
import { Markets } from './markets'; import { Markets } from './markets';
import { Proposed } from './proposed'; import { Proposed } from './proposed';
import { usePageTitleStore } from '../../stores'; import { usePageTitleStore } from '../../stores';
@ -14,7 +14,7 @@ export const MarketsPage = () => {
updateTitle(titlefy(['Markets'])); updateTitle(titlefy(['Markets']));
}, [updateTitle]); }, [updateTitle]);
return ( return (
<Tabs> <Tabs storageKey="console-markets">
<Tab id="all-markets" name={t('All markets')}> <Tab id="all-markets" name={t('All markets')}>
<Markets /> <Markets />
</Tab> </Tab>

View File

@ -2,7 +2,11 @@ import { titlefy } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { PositionsContainer } from '@vegaprotocol/positions'; import { PositionsContainer } from '@vegaprotocol/positions';
import { OrderListContainer } from '@vegaprotocol/orders'; import { OrderListContainer } from '@vegaprotocol/orders';
import { ResizableGridPanel, Tab, Tabs } from '@vegaprotocol/ui-toolkit'; import {
ResizableGridPanel,
Tab,
LocalStoragePersistTabs as Tabs,
} from '@vegaprotocol/ui-toolkit';
import { WithdrawalsContainer } from './withdrawals-container'; import { WithdrawalsContainer } from './withdrawals-container';
import { FillsContainer } from '@vegaprotocol/fills'; import { FillsContainer } from '@vegaprotocol/fills';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
@ -41,7 +45,7 @@ export const Portfolio = () => {
<ResizableGrid vertical> <ResizableGrid vertical>
<ResizableGridPanel minSize={75}> <ResizableGridPanel minSize={75}>
<PortfolioGridChild> <PortfolioGridChild>
<Tabs> <Tabs storageKey="console-portfolio-account-history">
<Tab id="account-history" name={t('Account history')}> <Tab id="account-history" name={t('Account history')}>
<VegaWalletContainer> <VegaWalletContainer>
<AccountHistoryContainer /> <AccountHistoryContainer />
@ -79,7 +83,7 @@ export const Portfolio = () => {
minSize={50} minSize={50}
> >
<PortfolioGridChild> <PortfolioGridChild>
<Tabs> <Tabs storageKey="console-portfolio-collateral">
<Tab id="collateral" name={t('Collateral')}> <Tab id="collateral" name={t('Collateral')}>
<VegaWalletContainer> <VegaWalletContainer>
<AccountsContainer /> <AccountsContainer />

View File

@ -1,5 +1,5 @@
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
import { useLocalStorage } from './use-local-storage'; import { useLocalStorage, useLocalStorageSnapshot } from './use-local-storage';
describe('useLocalStorage', () => { describe('useLocalStorage', () => {
afterEach(() => window.localStorage.clear()); afterEach(() => window.localStorage.clear());
@ -50,3 +50,51 @@ describe('useLocalStorage', () => {
expect(B.current[0]).toBeNull(); expect(B.current[0]).toBeNull();
}); });
}); });
describe('useLocalStorageSnapshot', () => {
afterEach(() => window.localStorage.clear());
it("should return null if there's no value set", () => {
const { result } = renderHook(() => useLocalStorageSnapshot('test'));
const [value] = result.current;
expect(value).toBeNull();
});
it('should return already saved value', () => {
window.localStorage.setItem('test', '123');
const { result } = renderHook(() => useLocalStorageSnapshot('test'));
const [value] = result.current;
expect(value).toEqual('123');
});
it('should save given value', () => {
const { result } = renderHook(() => useLocalStorageSnapshot('test'));
const setValue = result.current[1];
expect(result.current[0]).toBeNull();
act(() => setValue('123'));
expect(result.current[0]).toEqual('123');
act(() => setValue('456'));
expect(result.current[0]).toEqual('456');
});
it('should remove given value', () => {
const { result } = renderHook(() => useLocalStorageSnapshot('test'));
const setValue = result.current[1];
const removeValue = result.current[2];
act(() => setValue('123'));
expect(result.current[0]).toEqual('123');
act(() => removeValue());
expect(result.current[0]).toBeNull();
});
it('should not return value set by storage event (by another tab)', () => {
const { result: A } = renderHook(() => useLocalStorageSnapshot('test-a'));
act(() => {
window.localStorage.setItem('test-a', '123');
window.dispatchEvent(
new StorageEvent('storage', {
key: 'test-a',
oldValue: window.localStorage.getItem('test-a'),
storageArea: window.localStorage,
newValue: '123',
})
);
});
expect(A.current[0]).toBeNull();
});
});

View File

@ -1,4 +1,10 @@
import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'; import {
useCallback,
useEffect,
useMemo,
useSyncExternalStore,
useState,
} from 'react';
import { LocalStorage } from '@vegaprotocol/utils'; import { LocalStorage } from '@vegaprotocol/utils';
type LocalStorageCallback = (key: string) => void; type LocalStorageCallback = (key: string) => void;
@ -10,6 +16,12 @@ const unregisterCallback = (callback: LocalStorageCallback) =>
const triggerCallbacks = (key: string) => const triggerCallbacks = (key: string) =>
LOCAL_STORAGE_CALLBACKS.forEach((cb) => cb(key)); LOCAL_STORAGE_CALLBACKS.forEach((cb) => cb(key));
type UseLocalStorageHook = [
string | null | undefined,
(value: string) => void,
() => void
];
export const useLocalStorage = (key: string) => { export const useLocalStorage = (key: string) => {
const subscribe = useCallback( const subscribe = useCallback(
(onStoreChange: () => void) => { (onStoreChange: () => void) => {
@ -52,7 +64,27 @@ export const useLocalStorage = (key: string) => {
return () => window.removeEventListener('storage', onStorage); return () => window.removeEventListener('storage', onStorage);
}, [key, onStorage]); }, [key, onStorage]);
return useMemo< return useMemo<UseLocalStorageHook>(
[string | null | undefined, (value: string) => void, () => void] () => [value, setValue, removeValue],
>(() => [value, setValue, removeValue], [removeValue, setValue, value]); [removeValue, setValue, value]
);
};
export const useLocalStorageSnapshot = (key: string) => {
const [value, setStoredValue] = useState(LocalStorage.getItem(key));
const setValue = useCallback(
(value: string) => {
LocalStorage.setItem(key, value);
setStoredValue(value);
},
[key]
);
const removeValue = useCallback(() => {
LocalStorage.removeItem(key);
setStoredValue(null);
}, [key]);
return useMemo<UseLocalStorageHook>(
() => [value, setValue, removeValue],
[removeValue, setValue, value]
);
}; };

View File

@ -1,23 +1,31 @@
import * as TabsPrimitive from '@radix-ui/react-tabs'; import * as TabsPrimitive from '@radix-ui/react-tabs';
import { useLocalStorageSnapshot } from '@vegaprotocol/react-helpers';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactElement, ReactNode } from 'react'; import type { ReactElement, ReactNode } from 'react';
import { Children, isValidElement, useState } from 'react'; import { Children, isValidElement, useState } from 'react';
export interface TabsProps extends TabsPrimitive.TabsProps {
interface TabsProps {
children: ReactElement<TabProps>[]; children: ReactElement<TabProps>[];
active?: string;
} }
export const Tabs = ({ children, active: activeDefaultId }: TabsProps) => { export const Tabs = ({
children,
defaultValue,
value,
onValueChange,
...props
}: TabsProps) => {
const [activeTab, setActiveTab] = useState<string>(() => { const [activeTab, setActiveTab] = useState<string>(() => {
return activeDefaultId ?? children[0].props.id; if (defaultValue) {
return defaultValue;
}
return children[0].props.id;
}); });
return ( return (
<TabsPrimitive.Root <TabsPrimitive.Root
value={activeTab} {...props}
value={value || activeTab}
onValueChange={onValueChange || setActiveTab}
className="h-full grid grid-rows-[min-content_1fr]" className="h-full grid grid-rows-[min-content_1fr]"
onValueChange={(value) => setActiveTab(value)}
> >
<div className="border-b border-default"> <div className="border-b border-default">
<TabsPrimitive.List <TabsPrimitive.List
@ -26,7 +34,7 @@ export const Tabs = ({ children, active: activeDefaultId }: TabsProps) => {
> >
{Children.map(children, (child) => { {Children.map(children, (child) => {
if (!isValidElement(child) || child.props.hidden) return null; if (!isValidElement(child) || child.props.hidden) return null;
const isActive = child.props.id === activeTab; const isActive = child.props.id === (value || activeTab);
const triggerClass = classNames( const triggerClass = classNames(
'relative px-4 py-1 border-r border-default', 'relative px-4 py-1 border-r border-default',
'uppercase', 'uppercase',
@ -82,3 +90,15 @@ interface TabProps {
export const Tab = ({ children, ...props }: TabProps) => { export const Tab = ({ children, ...props }: TabProps) => {
return <div {...props}>{children}</div>; return <div {...props}>{children}</div>;
}; };
export const LocalStoragePersistTabs = ({
storageKey,
...props
}: Omit<TabsProps, 'value' | 'onValueChange'> & { storageKey: string }) => {
const [value, onValueChange] = useLocalStorageSnapshot(
`active-tab-${storageKey}`
);
return (
<Tabs {...props} value={value || undefined} onValueChange={onValueChange} />
);
};