Skip to content

Commit

Permalink
feat: resizable bottom panel (#1385)
Browse files Browse the repository at this point in the history
  • Loading branch information
tyleroooo authored Jan 6, 2025
1 parent 31b015d commit 4508235
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 29 deletions.
1 change: 1 addition & 0 deletions src/components/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ const $Header = styled.header<{ $side: 'top' | 'bottom' }>`
flex: 1;
${layoutMixins.row}
justify-content: space-between;
background-color: var(--trigger-backgroundColor);
z-index: var(--stickyHeader-zIndex);
`;

Expand Down
3 changes: 3 additions & 0 deletions src/constants/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ export enum TradeLayouts {
Default = 'Default',
Reverse = 'Reverse',
}

export const HORIZONTAL_PANEL_MIN_HEIGHT = 250;
export const HORIZONTAL_PANEL_MAX_HEIGHT = 700;
31 changes: 16 additions & 15 deletions src/pages/trade/HorizontalPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ enum InfoSection {
type ElementProps = {
isOpen?: boolean;
setIsOpen?: (isOpen: boolean) => void;
handleStartResize?: (e: React.MouseEvent<HTMLElement>) => void;
};

export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: ElementProps) => {
const stringGetter = useStringGetter();
const navigate = useNavigate();
const { isTablet } = useBreakpoints();
Expand Down Expand Up @@ -92,7 +93,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
showCurrentMarket ? numOpenOrders : numTotalOpenOrders
);

const initialPageSize = 10;
const initialPageSize = 20;

const onViewOrders = useCallback(
(market: string) => {
Expand Down Expand Up @@ -264,19 +265,6 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
]
);

// TODO - TRCL-1693 - re-enable when funding payments are supported
// const paymentsTabItem = {
// value: InfoSection.Payments,
// label: stringGetter({ key: STRING_KEYS.PAYMENTS }),

// tag: shortenNumberForDisplay(
// showCurrentMarket ? numFundingPayments : numTotalFundingPayments
// ),
// content: (
// <FundingPaymentsTable currentMarket={showCurrentMarket ? currentMarket?.id : undefined} />
// ),
// },

const tabItems = useMemo(
() => [positionTabItem, fillsTabItem, ordersTabItem],
[positionTabItem, fillsTabItem, ordersTabItem]
Expand All @@ -295,6 +283,9 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
<MobileTabs defaultValue={InfoSection.Position} items={tabItems} />
) : (
<>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<$DragHandle onMouseDown={handleStartResize} />

<$CollapsibleTabs
defaultTab={InfoSection.Position}
tab={tab}
Expand All @@ -318,6 +309,16 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
);
};

const $DragHandle = styled.div`
width: 100%;
height: 0.5rem;
cursor: ns-resize;
position: absolute;
top: 0;
left: 0;
z-index: 2;
`;

const $CollapsibleTabs = styled(CollapsibleTabs)`
header {
background-color: var(--color-layer-2);
Expand Down
49 changes: 43 additions & 6 deletions src/pages/trade/LaunchableMarket.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';

import { useMatch } from 'react-router-dom';
import styled, { css } from 'styled-components';

import { AnalyticsEvents } from '@/constants/analytics';
import { TradeLayouts } from '@/constants/layout';
import {
HORIZONTAL_PANEL_MAX_HEIGHT,
HORIZONTAL_PANEL_MIN_HEIGHT,
TradeLayouts,
} from '@/constants/layout';
import { AppRoute } from '@/constants/routes';

import { useBreakpoints } from '@/hooks/useBreakpoints';
Expand All @@ -16,7 +20,9 @@ import { DetachedSection } from '@/components/ContentSection';
import { AccountInfo } from '@/views/AccountInfo';
import { LaunchMarketSidePanel } from '@/views/LaunchMarketSidePanel';

import { useAppSelector } from '@/state/appTypes';
import { useAppDispatch, useAppSelector } from '@/state/appTypes';
import { setHorizontalPanelHeightPx } from '@/state/appUiConfigs';
import { getHorizontalPanelHeightPx } from '@/state/appUiConfigsSelectors';
import { getSelectedTradeLayout } from '@/state/layoutSelectors';

import { track } from '@/lib/analytics/analytics';
Expand All @@ -27,6 +33,7 @@ import { MarketSelectorAndStats } from './MarketSelectorAndStats';
import { MobileBottomPanel } from './MobileBottomPanel';
import { MobileTopPanel } from './MobileTopPanel';
import { TradeHeaderMobile } from './TradeHeaderMobile';
import { useResizablePanel } from './useResizablePanel';

const LaunchableMarket = () => {
const tradePageRef = useRef<HTMLDivElement>(null);
Expand All @@ -36,7 +43,22 @@ const LaunchableMarket = () => {
const { marketId } = match?.params ?? {};

const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true);

const horizontalPanelHeightPxBase = useAppSelector(getHorizontalPanelHeightPx);
const dispatch = useAppDispatch();
const setPanelHeight = useCallback(
(h: number) => {
dispatch(setHorizontalPanelHeightPx(h));
},
[dispatch]
);
const {
handleMouseDown,
panelHeight: horizontalPanelHeight,
isDragging,
} = useResizablePanel(horizontalPanelHeightPxBase, setPanelHeight, {
min: HORIZONTAL_PANEL_MIN_HEIGHT,
max: HORIZONTAL_PANEL_MAX_HEIGHT,
});
useEffect(() => {
if (marketId) {
track(
Expand Down Expand Up @@ -70,6 +92,7 @@ const LaunchableMarket = () => {
ref={tradePageRef}
tradeLayout={tradeLayout}
isHorizontalPanelOpen={isHorizontalPanelOpen}
horizontalPanelHeightPx={horizontalPanelHeight}
>
<header tw="[grid-area:Top]">
<MarketSelectorAndStats launchableMarketId={marketId} />
Expand All @@ -82,10 +105,15 @@ const LaunchableMarket = () => {

<$GridSection gridArea="Inner">
<InnerPanel launchableMarketId={marketId} />
{isDragging && <$CoverUpTradingView />}
</$GridSection>

<$GridSection gridArea="Horizontal">
<HorizontalPanel isOpen={isHorizontalPanelOpen} setIsOpen={setIsHorizontalPanelOpen} />
<HorizontalPanel
isOpen={isHorizontalPanelOpen}
setIsOpen={setIsHorizontalPanelOpen}
handleStartResize={handleMouseDown}
/>
</$GridSection>
</$TradeLayout>
);
Expand All @@ -96,8 +124,9 @@ export default LaunchableMarket;
const $TradeLayout = styled.article<{
tradeLayout: TradeLayouts;
isHorizontalPanelOpen: boolean;
horizontalPanelHeightPx: number;
}>`
--horizontalPanel-height: 18rem;
--horizontalPanel-height: ${({ horizontalPanelHeightPx }) => `${horizontalPanelHeightPx}px`};
// Constants
/* prettier-ignore */
Expand Down Expand Up @@ -198,3 +227,11 @@ const $LaunchMarketSidePanel = styled(LaunchMarketSidePanel)`
overflow: auto;
border-top: var(--border-width) solid var(--color-border);
`;

const $CoverUpTradingView = styled.div`
width: 100%;
height: 100%;
position: absolute;
z-index: 2;
background: rgba(0, 0, 0, 0.2);
`;
54 changes: 46 additions & 8 deletions src/pages/trade/Trade.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';

import styled, { css } from 'styled-components';

import { TradeLayouts } from '@/constants/layout';
import {
HORIZONTAL_PANEL_MAX_HEIGHT,
HORIZONTAL_PANEL_MIN_HEIGHT,
TradeLayouts,
} from '@/constants/layout';

import { useBreakpoints } from '@/hooks/useBreakpoints';
import { useCurrentMarketId } from '@/hooks/useCurrentMarketId';
Expand All @@ -17,7 +21,9 @@ import { AccountInfo } from '@/views/AccountInfo';
import { TradeBox } from '@/views/TradeBox';

import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { useAppSelector } from '@/state/appTypes';
import { useAppDispatch, useAppSelector } from '@/state/appTypes';
import { setHorizontalPanelHeightPx } from '@/state/appUiConfigs';
import { getHorizontalPanelHeightPx } from '@/state/appUiConfigsSelectors';
import { getSelectedTradeLayout } from '@/state/layoutSelectors';

import { HorizontalPanel } from './HorizontalPanel';
Expand All @@ -29,6 +35,7 @@ import { MobileTopPanel } from './MobileTopPanel';
import { TradeDialogTrigger } from './TradeDialogTrigger';
import { TradeHeaderMobile } from './TradeHeaderMobile';
import { VerticalPanel } from './VerticalPanel';
import { useResizablePanel } from './useResizablePanel';

const TradePage = () => {
const tradePageRef = useRef<HTMLDivElement>(null);
Expand All @@ -38,6 +45,22 @@ const TradePage = () => {
const tradeLayout = useAppSelector(getSelectedTradeLayout);
const canAccountTrade = useAppSelector(calculateCanAccountTrade);

const horizontalPanelHeightPxBase = useAppSelector(getHorizontalPanelHeightPx);
const dispatch = useAppDispatch();
const setPanelHeight = useCallback(
(h: number) => {
dispatch(setHorizontalPanelHeightPx(h));
},
[dispatch]
);
const {
handleMouseDown,
panelHeight: horizontalPanelHeight,
isDragging,
} = useResizablePanel(horizontalPanelHeightPxBase, setPanelHeight, {
min: HORIZONTAL_PANEL_MIN_HEIGHT,
max: HORIZONTAL_PANEL_MAX_HEIGHT,
});
const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true);

usePageTitlePriceUpdates();
Expand All @@ -57,7 +80,7 @@ const TradePage = () => {
</DetachedSection>

<DetachedSection>
<HorizontalPanel />
<HorizontalPanel handleStartResize={handleMouseDown} />
</DetachedSection>

<DetachedSection>
Expand All @@ -72,6 +95,7 @@ const TradePage = () => {
ref={tradePageRef}
tradeLayout={tradeLayout}
isHorizontalPanelOpen={isHorizontalPanelOpen}
horizontalPanelHeightPx={horizontalPanelHeight}
>
<header tw="[grid-area:Top]">
<MarketSelectorAndStats />
Expand All @@ -88,10 +112,15 @@ const TradePage = () => {

<$GridSection gridArea="Inner">
<InnerPanel />
{isDragging && <$CoverUpTradingView />}
</$GridSection>

<$GridSection gridArea="Horizontal">
<HorizontalPanel isOpen={isHorizontalPanelOpen} setIsOpen={setIsHorizontalPanelOpen} />
<HorizontalPanel
isOpen={isHorizontalPanelOpen}
setIsOpen={setIsHorizontalPanelOpen}
handleStartResize={handleMouseDown}
/>
</$GridSection>
</$TradeLayout>
);
Expand All @@ -101,13 +130,14 @@ export default TradePage;
const $TradeLayout = styled.article<{
tradeLayout: TradeLayouts;
isHorizontalPanelOpen: boolean;
horizontalPanelHeightPx: number;
}>`
--horizontalPanel-height: 18rem;
--horizontalPanel-height: ${({ horizontalPanelHeightPx }) => `${horizontalPanelHeightPx}px`};
// Constants
/* prettier-ignore */
--layout-default:
'Top Top Top' auto
--layout-default:
'Top Top Top' auto
'Inner Vertical Side' minmax(0, 1fr)
'Horizontal Horizontal Side' minmax(var(--tabs-height), var(--horizontalPanel-height))
/ 1fr minmax(0, var(--orderbook-trades-width)) var(--sidebar-width);
Expand Down Expand Up @@ -190,3 +220,11 @@ const $TradeLayoutMobile = styled.article`
const $GridSection = styled.section<{ gridArea: string }>`
grid-area: ${({ gridArea }) => gridArea};
`;

const $CoverUpTradingView = styled.div`
width: 100%;
height: 100%;
position: absolute;
z-index: 2;
background: rgba(0, 0, 0, 0.2);
`;
59 changes: 59 additions & 0 deletions src/pages/trade/useResizablePanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect, useMemo, useRef, useState } from 'react';

import { clamp, debounce } from 'lodash';

import { timeUnits } from '@/constants/time';

export function useResizablePanel(
startSize: number,
syncHeight: (h: number) => void,
bounds: { min: number; max: number }
) {
const [panelHeight, setPanelHeight] = useState(startSize);
const [isDragging, setIsDragging] = useState(false);
const startPosRef = useRef(0);
const startHeightRef = useRef(0);

const debouncedSync = useMemo(() => debounce(syncHeight, timeUnits.second / 2), [syncHeight]);

const handleMove = (clientY: number) => {
const dy = startPosRef.current - clientY;
const newHeight = clamp(startHeightRef.current + dy, bounds.min, bounds.max);
setPanelHeight(newHeight);
debouncedSync(newHeight);
};

const handleMouseDown = (e: React.MouseEvent<HTMLElement>) => {
setIsDragging(true);
startPosRef.current = e.clientY;
startHeightRef.current = panelHeight;
e.preventDefault();
};

const handleMouseMove = (e: MouseEvent) => {
if (isDragging) {
handleMove(e.clientY);
}
};

const handleMouseUp = (e: MouseEvent) => {
setIsDragging(false);
handleMove(e.clientY);
};

// Add event listeners to window to handle mouse moves outside the component
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}
return undefined;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDragging]);

return { handleMouseDown, panelHeight, isDragging };
}
Loading

0 comments on commit 4508235

Please sign in to comment.