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>
</HeaderStat>
</Header>
<Tabs active={getActiveDefaultId()}>
<Tabs defaultValue={getActiveDefaultId()}>
<Tab
id={LiquidityTabs.MyLiquidityProvision}
name={t('My liquidity provision')}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { act, renderHook } from '@testing-library/react';
import { useLocalStorage } from './use-local-storage';
import { useLocalStorage, useLocalStorageSnapshot } from './use-local-storage';
describe('useLocalStorage', () => {
afterEach(() => window.localStorage.clear());
@ -50,3 +50,51 @@ describe('useLocalStorage', () => {
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';
type LocalStorageCallback = (key: string) => void;
@ -10,6 +16,12 @@ const unregisterCallback = (callback: LocalStorageCallback) =>
const triggerCallbacks = (key: string) =>
LOCAL_STORAGE_CALLBACKS.forEach((cb) => cb(key));
type UseLocalStorageHook = [
string | null | undefined,
(value: string) => void,
() => void
];
export const useLocalStorage = (key: string) => {
const subscribe = useCallback(
(onStoreChange: () => void) => {
@ -52,7 +64,27 @@ export const useLocalStorage = (key: string) => {
return () => window.removeEventListener('storage', onStorage);
}, [key, onStorage]);
return useMemo<
[string | null | undefined, (value: string) => void, () => void]
>(() => [value, setValue, removeValue], [removeValue, setValue, value]);
return useMemo<UseLocalStorageHook>(
() => [value, setValue, removeValue],
[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 { useLocalStorageSnapshot } from '@vegaprotocol/react-helpers';
import classNames from 'classnames';
import type { ReactElement, ReactNode } from 'react';
import { Children, isValidElement, useState } from 'react';
interface TabsProps {
export interface TabsProps extends TabsPrimitive.TabsProps {
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>(() => {
return activeDefaultId ?? children[0].props.id;
if (defaultValue) {
return defaultValue;
}
return children[0].props.id;
});
return (
<TabsPrimitive.Root
value={activeTab}
{...props}
value={value || activeTab}
onValueChange={onValueChange || setActiveTab}
className="h-full grid grid-rows-[min-content_1fr]"
onValueChange={(value) => setActiveTab(value)}
>
<div className="border-b border-default">
<TabsPrimitive.List
@ -26,7 +34,7 @@ export const Tabs = ({ children, active: activeDefaultId }: TabsProps) => {
>
{Children.map(children, (child) => {
if (!isValidElement(child) || child.props.hidden) return null;
const isActive = child.props.id === activeTab;
const isActive = child.props.id === (value || activeTab);
const triggerClass = classNames(
'relative px-4 py-1 border-r border-default',
'uppercase',
@ -82,3 +90,15 @@ interface TabProps {
export const Tab = ({ children, ...props }: TabProps) => {
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} />
);
};