From 446af23d9cac1cc8a2cf3e046cac5a8710d30f42 Mon Sep 17 00:00:00 2001 From: Art Date: Thu, 29 Sep 2022 14:23:14 +0200 Subject: [PATCH] fix: orderbook sticky behaviour and precision options (#1519) * fix: orderbook sticky behaviour and precision options * chore: add handling of y-axis resize * fix: addressed PR comments * fix: addressed PR comments * added cy.log * Update apps/trading-e2e/src/support/vega-wallet.ts --- apps/trading-e2e/src/support/vega-wallet.ts | 1 - libs/market-depth/src/lib/orderbook.tsx | 179 ++++++++++++------ libs/market-depth/src/setup-tests.ts | 10 + libs/react-helpers/src/hooks/index.ts | 2 + .../src/hooks/use-mutation-observer.ts | 41 ++++ .../src/hooks/use-resize-observer.ts | 39 ++++ 6 files changed, 210 insertions(+), 62 deletions(-) create mode 100644 libs/react-helpers/src/hooks/use-mutation-observer.ts create mode 100644 libs/react-helpers/src/hooks/use-resize-observer.ts diff --git a/apps/trading-e2e/src/support/vega-wallet.ts b/apps/trading-e2e/src/support/vega-wallet.ts index c1095a11c..97a66ca39 100644 --- a/apps/trading-e2e/src/support/vega-wallet.ts +++ b/apps/trading-e2e/src/support/vega-wallet.ts @@ -3,7 +3,6 @@ export const connectVegaWallet = () => { const manageVegaBtn = 'manage-vega-wallet'; const walletName = Cypress.env('TRADING_TEST_VEGA_WALLET_NAME'); const walletPassphrase = Cypress.env('TRADING_TEST_VEGA_WALLET_PASSPHRASE'); - cy.getByTestId('connect-vega-wallet').click(); cy.getByTestId('connectors-list').find('button').click(); cy.getByTestId(form).find('#wallet').click().type(walletName); diff --git a/libs/market-depth/src/lib/orderbook.tsx b/libs/market-depth/src/lib/orderbook.tsx index 809b77293..e3d225dd3 100644 --- a/libs/market-depth/src/lib/orderbook.tsx +++ b/libs/market-depth/src/lib/orderbook.tsx @@ -12,7 +12,12 @@ import { } from 'react'; import classNames from 'classnames'; -import { formatNumber, t, ThemeContext } from '@vegaprotocol/react-helpers'; +import { + formatNumber, + t, + ThemeContext, + useResizeObserver, +} from '@vegaprotocol/react-helpers'; import { MarketTradingMode } from '@vegaprotocol/types'; import { OrderbookRow } from './orderbook-row'; import { createRow, getPriceLevel } from './orderbook-data'; @@ -109,6 +114,9 @@ export const Orderbook = ({ }: OrderbookProps) => { const theme = useContext(ThemeContext); const scrollElement = useRef(null); + const gridElement = useRef(null); + const headerElement = useRef(null); + const footerElement = useRef(null); // scroll offset for which rendered rows are selected, will change after user will scroll to margin of rendered data const [scrollOffset, setScrollOffset] = useState(0); // actual scrollTop of scrollElement current element @@ -249,7 +257,7 @@ export const Orderbook = ({ } }, [scrollToMidPrice, scrollToPrice, resolution]); - // handles viewport resize + // handles window resize useEffect(() => { function handleResize() { if (scrollElement.current) { @@ -262,6 +270,50 @@ export const Orderbook = ({ handleResize(); return () => window.removeEventListener('resize', handleResize); }, []); + // sets the correct width of header and footer + useLayoutEffect(() => { + if ( + !gridElement.current || + !headerElement.current || + !footerElement.current + ) { + return; + } + const gridWidth = gridElement.current.clientWidth; + headerElement.current.style.width = `${gridWidth}px`; + footerElement.current.style.width = `${gridWidth}px`; + }, [headerElement, footerElement, gridElement]); + // handles resizing of the Allotment.Pane (x-axis) + // adjusts the header and footer width + const gridResizeHandler: ResizeObserverCallback = useCallback( + (entries) => { + if ( + !headerElement.current || + !footerElement.current || + entries.length === 0 + ) { + return; + } + const { + contentRect: { width, height }, + } = entries[0]; + headerElement.current.style.width = `${width}px`; + footerElement.current.style.width = `${width}px`; + setViewportHeight(height); + }, + [headerElement, footerElement] + ); + // handles resizing of the Allotment.Pane (y-axis) + // adjusts the scroll height + const scrollElementResizeHandler: ResizeObserverCallback = useCallback( + (entries) => { + if (!scrollElement.current || entries.length === 0) return; + setViewportHeight(entries[0].contentRect.height); + }, + [setViewportHeight, scrollElement] + ); + useResizeObserver(gridElement.current, gridResizeHandler); + useResizeObserver(scrollElement.current, scrollElementResizeHandler); let offset = Math.max(0, Math.round(scrollOffset / rowHeight)); const prependingBufferSize = Math.min(bufferSize, offset); @@ -313,16 +365,16 @@ export const Orderbook = ({ const c = theme === 'dark' ? colors.neutral[600] : colors.neutral[300]; const gradientStyles = `linear-gradient(${c},${c}) 24.6% 0/1px 100% no-repeat, linear-gradient(${c},${c}) 50% 0/1px 100% no-repeat, linear-gradient(${c},${c}) 75.2% 0/1px 100% no-repeat`; + const resolutions = new Array(decimalPlaces + 1) + .fill(null) + .map((v, i) => Math.pow(10, i)); + return ( -
+
{t('Bid vol')}
{t('Price')}
@@ -330,23 +382,61 @@ export const Orderbook = ({
{t('Cumulative vol')}
- {tableBody || ( -
- {t('No data')} -
- )} +
+ {tableBody || ( +
+ {t('No data')} +
+ )} +
+ {maxPriceLevel && + bestStaticBidPrice && + BigInt(bestStaticBidPrice) < BigInt(maxPriceLevel) && + BigInt(bestStaticBidPrice) > minPriceLevel && ( + + )} + {maxPriceLevel && + bestStaticOfferPrice && + BigInt(bestStaticOfferPrice) <= BigInt(maxPriceLevel) && + BigInt(bestStaticOfferPrice) > minPriceLevel && ( + + )}
@@ -374,43 +461,13 @@ export const Orderbook = ({ })} data-testid="scroll-to-midprice" > - Go to mid + {t('Go to mid')}
- {maxPriceLevel && - bestStaticBidPrice && - BigInt(bestStaticBidPrice) < BigInt(maxPriceLevel) && - BigInt(bestStaticBidPrice) > minPriceLevel && ( - - )} - {maxPriceLevel && - bestStaticOfferPrice && - BigInt(bestStaticOfferPrice) <= BigInt(maxPriceLevel) && - BigInt(bestStaticOfferPrice) > minPriceLevel && ( - - )}
); }; diff --git a/libs/market-depth/src/setup-tests.ts b/libs/market-depth/src/setup-tests.ts index 143438dc4..97c567326 100644 --- a/libs/market-depth/src/setup-tests.ts +++ b/libs/market-depth/src/setup-tests.ts @@ -14,3 +14,13 @@ Object.defineProperty(window, 'matchMedia', { dispatchEvent: jest.fn(), })), }); + +Object.defineProperty(window, 'ResizeObserver', { + writable: false, + value: jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + connect: jest.fn(), + disconnect: jest.fn(), + })), +}); diff --git a/libs/react-helpers/src/hooks/index.ts b/libs/react-helpers/src/hooks/index.ts index 387bfe3ed..d8452d8b2 100644 --- a/libs/react-helpers/src/hooks/index.ts +++ b/libs/react-helpers/src/hooks/index.ts @@ -6,3 +6,5 @@ export * from './use-outside-click'; export * from './use-resize'; export * from './use-screen-dimensions'; export * from './use-theme-switcher'; +export * from './use-mutation-observer'; +export * from './use-resize-observer'; diff --git a/libs/react-helpers/src/hooks/use-mutation-observer.ts b/libs/react-helpers/src/hooks/use-mutation-observer.ts new file mode 100644 index 000000000..d2c93ccde --- /dev/null +++ b/libs/react-helpers/src/hooks/use-mutation-observer.ts @@ -0,0 +1,41 @@ +import { captureException } from '@sentry/react'; +import debounce from 'lodash/debounce'; +import { useEffect, useMemo } from 'react'; + +type MutationObserverConfiguration = { + debounceTime: number; + config: MutationObserverInit; +}; + +const DEFAULT_OPTIONS: MutationObserverConfiguration = { + debounceTime: 0, + config: { + attributes: true, + childList: false, + subtree: false, + }, +}; + +export function useMutationObserver( + target: Node | null, + callback: MutationCallback, + options: MutationObserverConfiguration = DEFAULT_OPTIONS +) { + const observer = useMemo(() => { + return new MutationObserver( + options.debounceTime > 0 + ? debounce(callback, options.debounceTime) + : callback + ); + }, [callback, options.debounceTime]); + + useEffect(() => { + if (!observer || !target) return; + try { + observer.observe(target, options.config); + } catch (err) { + captureException(err); + } + return () => observer?.disconnect(); + }, [observer, options.config, target]); +} diff --git a/libs/react-helpers/src/hooks/use-resize-observer.ts b/libs/react-helpers/src/hooks/use-resize-observer.ts new file mode 100644 index 000000000..fb492ef36 --- /dev/null +++ b/libs/react-helpers/src/hooks/use-resize-observer.ts @@ -0,0 +1,39 @@ +import { captureException } from '@sentry/react'; +import debounce from 'lodash/debounce'; +import { useEffect, useMemo } from 'react'; + +type ResizeObserverConfiguration = { + debounceTime: number; + config: ResizeObserverOptions; +}; + +const DEFAULT_OPTIONS: ResizeObserverConfiguration = { + debounceTime: 0, + config: { + box: 'border-box', + }, +}; + +export function useResizeObserver( + target: Element | null, + callback: ResizeObserverCallback, + options: ResizeObserverConfiguration = DEFAULT_OPTIONS +) { + const observer = useMemo(() => { + return new ResizeObserver( + options.debounceTime > 0 + ? debounce(callback, options.debounceTime) + : callback + ); + }, [callback, options.debounceTime]); + + useEffect(() => { + if (!observer || !target) return; + try { + observer.observe(target, options.config); + } catch (err) { + captureException(err); + } + return () => observer?.disconnect(); + }, [observer, options.config, target]); +}