From dca1af7391e01cd7684a105a3020c614665b4a3d Mon Sep 17 00:00:00 2001 From: John Walley Date: Fri, 13 May 2022 14:08:32 +0100 Subject: [PATCH] Feat/246 Add candle chart controls (#281) * Add candle chart controls * Improve dropdown menu styling * Use latest pennant version * Use translation helper * Square off highlighted corners --- libs/candles-chart/src/lib/candles-chart.tsx | 174 +++++++++++++++++- libs/candles-chart/src/lib/data-source.ts | 2 +- .../dropdown-menu/dropdown-menu.stories.tsx | 93 ++++++++++ .../dropdown-menu/dropdown-menu.tsx | 145 +++++++++++++++ .../src/components/dropdown-menu/index.ts | 1 + libs/ui-toolkit/src/components/index.ts | 1 + package.json | 3 +- yarn.lock | 53 +++++- 8 files changed, 463 insertions(+), 9 deletions(-) create mode 100644 libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.stories.tsx create mode 100644 libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.tsx create mode 100644 libs/ui-toolkit/src/components/dropdown-menu/index.ts diff --git a/libs/candles-chart/src/lib/candles-chart.tsx b/libs/candles-chart/src/lib/candles-chart.tsx index a74567ac5..720d7ee6a 100644 --- a/libs/candles-chart/src/lib/candles-chart.tsx +++ b/libs/candles-chart/src/lib/candles-chart.tsx @@ -1,11 +1,42 @@ import 'pennant/dist/style.css'; -import { Chart, Interval } from 'pennant'; +import { + Chart, + ChartType, + Interval, + Overlay, + Study, + chartTypeLabels, + intervalLabels, + overlayLabels, + studyLabels, +} from 'pennant'; import { VegaDataSource } from './data-source'; import { useApolloClient } from '@apollo/client'; -import { useContext, useMemo } from 'react'; +import { useContext, useMemo, useState } from 'react'; import { useVegaWallet } from '@vegaprotocol/wallet'; import { ThemeContext } from '@vegaprotocol/react-helpers'; +import { + Button, + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItemIndicator, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, + Icon, +} from '@vegaprotocol/ui-toolkit'; +import type { IconName } from '@blueprintjs/icons'; +import { IconNames } from '@blueprintjs/icons'; +import { t } from '@vegaprotocol/react-helpers'; + +const chartTypeIcon = new Map([ + [ChartType.AREA, IconNames.TIMELINE_AREA_CHART], + [ChartType.CANDLE, IconNames.WATERFALL_CHART], + [ChartType.LINE, IconNames.TIMELINE_LINE_CHART], + [ChartType.OHLC, IconNames.WATERFALL_CHART], +]); export type CandlesChartContainerProps = { marketId: string; @@ -18,11 +49,148 @@ export const CandlesChartContainer = ({ const { keypair } = useVegaWallet(); const theme = useContext(ThemeContext); + const [interval, setInterval] = useState(Interval.I15M); + const [chartType, setChartType] = useState(ChartType.CANDLE); + const [overlays, setOverlays] = useState([]); + const [studies, setStudies] = useState([]); + const dataSource = useMemo(() => { return new VegaDataSource(client, marketId, keypair?.pub); }, [client, marketId, keypair]); return ( - +
+
+ + + + + + { + setInterval(value as Interval); + }} + > + {Object.values(Interval).map((timeInterval) => ( + + + + + {intervalLabels[timeInterval]} + + ))} + + + + + + + + + { + setChartType(value as ChartType); + }} + > + {Object.values(ChartType).map((type) => ( + + + + + {chartTypeLabels[type]} + + ))} + + + + + + + + + {Object.values(Overlay).map((overlay) => ( + { + const newOverlays = [...overlays]; + const index = overlays.findIndex((item) => item === overlay); + + index !== -1 + ? newOverlays.splice(index, 1) + : newOverlays.push(overlay); + + setOverlays(newOverlays); + }} + > + + + + {overlayLabels[overlay]} + + ))} + + + + + + + + {Object.values(Study).map((study) => ( + { + const newStudies = [...studies]; + const index = studies.findIndex((item) => item === study); + + index !== -1 + ? newStudies.splice(index, 1) + : newStudies.push(study); + + setStudies(newStudies); + }} + > + + + + {studyLabels[study]} + + ))} + + +
+
+ { + setStudies(options.studies ?? []); + }} + /> +
+
); }; diff --git a/libs/candles-chart/src/lib/data-source.ts b/libs/candles-chart/src/lib/data-source.ts index 22d73df7e..88b5d8f6e 100644 --- a/libs/candles-chart/src/lib/data-source.ts +++ b/libs/candles-chart/src/lib/data-source.ts @@ -1,7 +1,6 @@ import type { ApolloClient } from '@apollo/client'; import { gql } from '@apollo/client'; import type { Candle, DataSource } from 'pennant'; -import { Interval } from 'pennant'; import { addDecimal } from '@vegaprotocol/react-helpers'; import type { Chart, ChartVariables } from './__generated__/Chart'; @@ -12,6 +11,7 @@ import type { CandlesSubVariables, } from './__generated__/CandlesSub'; import type { Subscription } from 'zen-observable-ts'; +import { Interval } from '@vegaprotocol/types'; export const CANDLE_FRAGMENT = gql` fragment CandleFields on Candle { diff --git a/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.stories.tsx b/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.stories.tsx new file mode 100644 index 000000000..bc2fa9746 --- /dev/null +++ b/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.stories.tsx @@ -0,0 +1,93 @@ +import { useState } from 'react'; +import type { ComponentMeta } from '@storybook/react'; + +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuItemIndicator, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from './dropdown-menu'; +import { Button } from '../button'; +import { Icon } from '../icon'; + +export default { + title: 'DropdownMenu', +} as ComponentMeta; + +export const CheckboxItems = () => { + const checkboxItems = [ + { label: 'Bollinger bands', state: useState(false) }, + { label: 'Envelope', state: useState(true) }, + { label: 'EMA', state: useState(false) }, + { label: 'Moving average', state: useState(false) }, + { label: 'Price monitoring bands', state: useState(false) }, + ]; + + return ( +
+ + + + + + {checkboxItems.map(({ label, state: [checked, setChecked] }) => ( + + + + + {label} + + ))} + + +
+ ); +}; + +export const RadioItems = () => { + const files = ['README.md', 'index.js', 'page.css']; + const [file, setFile] = useState(files[1]); + + return ( +
+ + + + + + console.log('minimize')}> + Minimize window + + console.log('zoom')}> + Zoom + + console.log('smaller')}> + Smaller + + + + {files.map((file) => ( + + {file} + + + + + ))} + + + +

Selected file: {file}

+
+ ); +}; diff --git a/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.tsx b/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.tsx new file mode 100644 index 000000000..7073e7e77 --- /dev/null +++ b/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.tsx @@ -0,0 +1,145 @@ +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import classNames from 'classnames'; +import { forwardRef } from 'react'; + +const itemStyles = classNames([ + 'text-ui', + 'text-black', + 'dark:text-white', + 'flex', + 'items-center', + 'justify-between', + 'leading-1', + 'cursor-default', + 'select-none', + 'whitespace-nowrap', + 'h-[25px]', + 'py-0', + 'pr-8', + 'color-black', +]); + +const itemClass = classNames(itemStyles, [ + 'focus:bg-vega-yellow', + 'dark:focus:bg-vega-yellow', + 'focus:text-black', + 'dark:focus:text-black', + 'focus:outline-none', +]); + +function getItemClasses(inset: boolean) { + return classNames(itemClass, inset ? 'pl-28' : 'pl-4', 'relative'); +} + +/** + * Contains all the parts of a dropdown menu. + */ +export const DropdownMenu = DropdownMenuPrimitive.Root; + +/** + * The button that toggles the dropdown menu. + * By default, the {@link DropdownMenuContent} will position itself against the trigger. + */ +export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +/** + * Used to group multiple {@link DropdownMenuRadioItem}s. + */ +export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +/** + * The component that pops out when the dropdown menu is open. + */ +export const DropdownMenuContent = forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, ...contentProps }, forwardedRef) => ( + +)); + +/** + * The component that contains the dropdown menu items. + */ +export const DropdownMenuItem = forwardRef< + React.ElementRef, + React.ComponentProps & { + inset?: boolean; + } +>(({ className, inset = false, ...itemProps }, forwardedRef) => ( + +)); + +/** + * An item that can be controlled and rendered like a checkbox. + */ +export const DropdownMenuCheckboxItem = forwardRef< + React.ElementRef, + React.ComponentProps & { + inset?: boolean; + } +>(({ className, inset = false, ...checkboxItemProps }, forwardedRef) => ( + +)); + +/** + * An item that can be controlled and rendered like a radio. + */ +export const DropdownMenuRadioItem = forwardRef< + React.ElementRef, + React.ComponentProps & { + inset?: boolean; + } +>(({ className, inset = false, ...radioItemprops }, forwardedRef) => ( + +)); + +/** + * Renders when the parent {@link DropdownMenuCheckboxItem} or {@link DropdownMenuRadioItem} is checked. + * You can style this element directly, or you can use it as a wrapper to put an icon into, or both. + */ +export const DropdownMenuItemIndicator = forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, ...itemIndicatorProps }, forwardedRef) => ( + +)); + +/** + * Used to visually separate items in the dropdown menu. + */ +export const DropdownMenuSeparator = forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, ...separatorProps }, forwardedRef) => ( + +)); diff --git a/libs/ui-toolkit/src/components/dropdown-menu/index.ts b/libs/ui-toolkit/src/components/dropdown-menu/index.ts new file mode 100644 index 000000000..2759d3ce6 --- /dev/null +++ b/libs/ui-toolkit/src/components/dropdown-menu/index.ts @@ -0,0 +1 @@ +export * from './dropdown-menu'; diff --git a/libs/ui-toolkit/src/components/index.ts b/libs/ui-toolkit/src/components/index.ts index ed1eb418c..ee05debea 100644 --- a/libs/ui-toolkit/src/components/index.ts +++ b/libs/ui-toolkit/src/components/index.ts @@ -5,6 +5,7 @@ export * from './callout'; export * from './card'; export * from './copy-with-tooltip'; export * from './dialog'; +export * from './dropdown-menu'; export * from './etherscan-link'; export * from './form-group'; export * from './icon'; diff --git a/package.json b/package.json index 97d342f33..6c5675452 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@blueprintjs/select": "^3.16.6", "@nrwl/next": "13.10.3", "@radix-ui/react-dialog": "^0.1.5", + "@radix-ui/react-dropdown-menu": "^0.1.6", "@radix-ui/react-tabs": "^0.1.5", "@radix-ui/react-tooltip": "^0.1.7", "@sentry/nextjs": "^6.19.3", @@ -53,7 +54,7 @@ "immer": "^9.0.12", "lodash": "^4.17.21", "next": "^12.0.7", - "pennant": "0.4.7", + "pennant": "0.4.8", "postcss": "^8.4.6", "react": "17.0.2", "react-copy-to-clipboard": "^5.0.4", diff --git a/yarn.lock b/yarn.lock index 9861e1fc5..f74e4f55c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3307,6 +3307,20 @@ "@radix-ui/react-use-callback-ref" "0.1.0" "@radix-ui/react-use-escape-keydown" "0.1.0" +"@radix-ui/react-dropdown-menu@^0.1.6": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz#3203229788cd57e552c9f19dcc7008e2b545919c" + integrity sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.1.0" + "@radix-ui/react-compose-refs" "0.1.0" + "@radix-ui/react-context" "0.1.1" + "@radix-ui/react-id" "0.1.5" + "@radix-ui/react-menu" "0.1.6" + "@radix-ui/react-primitive" "0.1.4" + "@radix-ui/react-use-controllable-state" "0.1.0" + "@radix-ui/react-focus-guards@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-0.1.0.tgz#ba3b6f902cba7826569f8edc21ff8223dece7def" @@ -3332,6 +3346,30 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "0.1.0" +"@radix-ui/react-menu@0.1.6": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-0.1.6.tgz#7f9521a10f6a9cd819b33b33d5ed9538d79b2e75" + integrity sha512-ho3+bhpr3oAFkOBJ8VkUb1BcGoiZBB3OmcWPqa6i5RTUKrzNX/d6rauochu2xDlWjiRtpVuiAcsTVOeIC4FbYQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.1.0" + "@radix-ui/react-collection" "0.1.4" + "@radix-ui/react-compose-refs" "0.1.0" + "@radix-ui/react-context" "0.1.1" + "@radix-ui/react-dismissable-layer" "0.1.5" + "@radix-ui/react-focus-guards" "0.1.0" + "@radix-ui/react-focus-scope" "0.1.4" + "@radix-ui/react-id" "0.1.5" + "@radix-ui/react-popper" "0.1.4" + "@radix-ui/react-portal" "0.1.4" + "@radix-ui/react-presence" "0.1.2" + "@radix-ui/react-primitive" "0.1.4" + "@radix-ui/react-roving-focus" "0.1.5" + "@radix-ui/react-use-callback-ref" "0.1.0" + "@radix-ui/react-use-direction" "0.1.0" + aria-hidden "^1.1.1" + react-remove-scroll "^2.4.0" + "@radix-ui/react-popper@0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-0.1.4.tgz#dfc055dcd7dfae6a2eff7a70d333141d15a5d029" @@ -3453,6 +3491,13 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-callback-ref" "0.1.0" +"@radix-ui/react-use-direction@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-direction/-/react-use-direction-0.1.0.tgz#97ac1d52e497c974389e7988f809238ed72e7df7" + integrity sha512-NajpY/An9TCPSfOVkgWIdXJV+VuWl67PxB6kOKYmtNAFHvObzIoh8o0n9sAuwSAyFCZVq211FEf9gvVDRhOyiA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-escape-keydown@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-0.1.0.tgz#dc80cb3753e9d1bd992adbad9a149fb6ea941874" @@ -16634,10 +16679,10 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -pennant@0.4.7: - version "0.4.7" - resolved "https://registry.yarnpkg.com/pennant/-/pennant-0.4.7.tgz#b1541f57d563c8a14f026292f6f88c059938f31a" - integrity sha512-HKpQS9wUMdH7aqdiOTwWwXAd2GhxKT1RF//f9utYhTuWFK5dK44EvWiaOTol+ddpN30GStK4KxO8s2OkeYLvPw== +pennant@0.4.8: + version "0.4.8" + resolved "https://registry.yarnpkg.com/pennant/-/pennant-0.4.8.tgz#a19a5aa862621f1ac868a44c1e62fc4db1250d95" + integrity sha512-/vk81reu4643Ol4C7sZ+SaJujpT8eHQmmETb/HmqzMR9pOY5rqMpOTzY5OMQO4SM8amvO40BkOaHt1HSnANcKA== dependencies: "@babel/runtime" "^7.13.10" "@d3fc/d3fc-technical-indicator" "^8.0.1"