Feat/83 switching vega key (#156)
* add manage dialog to wallet lib, add it to trading app * add test for wallet button * add tests for manage dialog * move tooltip to ui-toolkit, add copy with tooltip component for manage dialog * add better labelling * add tooltip story * add story for copy-with-tooltip * add tests for tooltip and copy-with-tooltip * move useFakeTimers call to beforeAll * adjust design of manage dialog * fix linting issues
This commit is contained in:
parent
e5b2c360ce
commit
15551b65e5
@ -1,7 +1,6 @@
|
||||
import { truncateByChars } from '@vegaprotocol/react-helpers';
|
||||
import * as React from 'react';
|
||||
|
||||
const ELLIPSIS = '\u2026';
|
||||
|
||||
interface TruncateInlineProps {
|
||||
text: string | null;
|
||||
className?: string;
|
||||
@ -41,16 +40,3 @@ export function TruncateInline({
|
||||
return <span {...wrapperProps}>{truncatedText}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
export function truncateByChars(s: string, startChars = 6, endChars = 6) {
|
||||
// if the text is shorted than the total number of chars to show
|
||||
// no truncation is needed. Plus one is to account for the ellipsis
|
||||
if (s.length <= startChars + endChars + 1) {
|
||||
return s;
|
||||
}
|
||||
|
||||
const start = s.slice(0, startChars);
|
||||
const end = s.slice(-endChars);
|
||||
|
||||
return start + ELLIPSIS + end;
|
||||
}
|
||||
|
@ -1,30 +1 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
interface VegaWalletButtonProps {
|
||||
setConnectDialog: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const VegaWalletButton = ({
|
||||
setConnectDialog,
|
||||
}: VegaWalletButtonProps) => {
|
||||
const { disconnect, keypairs } = useVegaWallet();
|
||||
const isConnected = keypairs !== null;
|
||||
|
||||
const handleClick = () => {
|
||||
if (isConnected) {
|
||||
disconnect();
|
||||
} else {
|
||||
setConnectDialog(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="ml-auto inline-block text-ui sm:text-body-large"
|
||||
>
|
||||
{isConnected ? t('Disconnect Vega wallet') : t('Connect Vega wallet')}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
export { VegaWalletConnectButton } from './vega-wallet-connect-button';
|
||||
|
@ -0,0 +1,51 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import type { VegaWalletConnectButtonProps } from './vega-wallet-connect-button';
|
||||
import { VegaWalletConnectButton } from './vega-wallet-connect-button';
|
||||
|
||||
let props: VegaWalletConnectButtonProps;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
setConnectDialog: jest.fn(),
|
||||
setManageDialog: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const generateJsx = (
|
||||
context: VegaWalletContextShape,
|
||||
props: VegaWalletConnectButtonProps
|
||||
) => {
|
||||
return (
|
||||
<VegaWalletContext.Provider value={context}>
|
||||
<VegaWalletConnectButton {...props} />
|
||||
</VegaWalletContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
test('Not connected', () => {
|
||||
render(generateJsx({ keypair: null } as VegaWalletContextShape, props));
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveTextContent('Connect Vega wallet');
|
||||
fireEvent.click(button);
|
||||
expect(props.setConnectDialog).toHaveBeenCalledWith(true);
|
||||
expect(props.setManageDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Connected', () => {
|
||||
render(
|
||||
generateJsx(
|
||||
{ keypair: { pub: '123456__123456' } } as VegaWalletContextShape,
|
||||
props
|
||||
)
|
||||
);
|
||||
|
||||
expect(screen.getByText('Vega key:')).toBeInTheDocument();
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveTextContent('123456\u2026123456');
|
||||
fireEvent.click(button);
|
||||
expect(props.setManageDialog).toHaveBeenCalledWith(true);
|
||||
expect(props.setConnectDialog).not.toHaveBeenCalled();
|
||||
});
|
@ -0,0 +1,37 @@
|
||||
import { truncateByChars } from '@vegaprotocol/react-helpers';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
export interface VegaWalletConnectButtonProps {
|
||||
setConnectDialog: (isOpen: boolean) => void;
|
||||
setManageDialog: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const VegaWalletConnectButton = ({
|
||||
setConnectDialog,
|
||||
setManageDialog,
|
||||
}: VegaWalletConnectButtonProps) => {
|
||||
const { keypair } = useVegaWallet();
|
||||
const isConnected = keypair !== null;
|
||||
|
||||
const handleClick = () => {
|
||||
if (isConnected) {
|
||||
setManageDialog(true);
|
||||
} else {
|
||||
setConnectDialog(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span>
|
||||
{isConnected && (
|
||||
<span className="text-ui-small font-mono mr-2">Vega key:</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="ml-auto inline-block text-ui-small font-mono hover:underline"
|
||||
>
|
||||
{isConnected ? truncateByChars(keypair.pub) : 'Connect Vega wallet'}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
};
|
@ -1,31 +1,29 @@
|
||||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { Navbar } from '../components/navbar';
|
||||
import { t, ThemeContext } from '@vegaprotocol/react-helpers';
|
||||
import { VegaConnectDialog, VegaWalletProvider } from '@vegaprotocol/wallet';
|
||||
import { t, ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
VegaConnectDialog,
|
||||
VegaManageDialog,
|
||||
VegaWalletProvider,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { Connectors } from '../lib/vega-connectors';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { createClient } from '../lib/apollo-client';
|
||||
import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import { AppLoader } from '../components/app-loader';
|
||||
import { VegaWalletButton } from '../components/vega-wallet-connect-button';
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
|
||||
import { VegaWalletConnectButton } from '../components/vega-wallet-connect-button';
|
||||
import './styles.css';
|
||||
|
||||
function VegaTradingApp({ Component, pageProps }: AppProps) {
|
||||
const client = useMemo(() => createClient(process.env['NX_VEGA_URL']), []);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [vegaWallet, setVegaWallet] = useState({
|
||||
connect: false,
|
||||
manage: false,
|
||||
});
|
||||
const [theme, toggleTheme] = useThemeSwitcher();
|
||||
|
||||
const setConnectDialog = useCallback((isOpen?: boolean) => {
|
||||
setDialogOpen((curr) => {
|
||||
if (isOpen === undefined) return !curr;
|
||||
return isOpen;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<ApolloProvider client={client}>
|
||||
@ -42,8 +40,15 @@ function VegaTradingApp({ Component, pageProps }: AppProps) {
|
||||
<div className="h-full dark:bg-black dark:text-white-60 bg-white text-black-60 grid grid-rows-[min-content,1fr]">
|
||||
<div className="flex items-stretch border-b-[7px] border-vega-yellow">
|
||||
<Navbar />
|
||||
<div className="flex items-center ml-auto mr-8">
|
||||
<VegaWalletButton setConnectDialog={setConnectDialog} />
|
||||
<div className="flex items-center gap-4 ml-auto mr-8">
|
||||
<VegaWalletConnectButton
|
||||
setConnectDialog={(open) =>
|
||||
setVegaWallet((x) => ({ ...x, connect: open }))
|
||||
}
|
||||
setManageDialog={(open) =>
|
||||
setVegaWallet((x) => ({ ...x, manage: open }))
|
||||
}
|
||||
/>
|
||||
<ThemeSwitcher onToggle={toggleTheme} className="-my-4" />
|
||||
</div>
|
||||
</div>
|
||||
@ -52,8 +57,16 @@ function VegaTradingApp({ Component, pageProps }: AppProps) {
|
||||
</main>
|
||||
<VegaConnectDialog
|
||||
connectors={Connectors}
|
||||
dialogOpen={dialogOpen}
|
||||
setDialogOpen={setDialogOpen}
|
||||
dialogOpen={vegaWallet.connect}
|
||||
setDialogOpen={(open) =>
|
||||
setVegaWallet((x) => ({ ...x, connect: open }))
|
||||
}
|
||||
/>
|
||||
<VegaManageDialog
|
||||
dialogOpen={vegaWallet.manage}
|
||||
setDialogOpen={(open) =>
|
||||
setVegaWallet((x) => ({ ...x, manage: open }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</AppLoader>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Tooltip } from '../tooltip';
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import type { StatFields } from '../../config/types';
|
||||
import { defaultFieldFormatter } from '../table-row';
|
||||
import { GoodThresholdIndicator } from '../good-threshold-indicator';
|
||||
@ -11,7 +11,7 @@ export const PromotedStatsItem = ({
|
||||
description,
|
||||
}: StatFields) => {
|
||||
return (
|
||||
<Tooltip description={description}>
|
||||
<Tooltip description={description} align="start">
|
||||
<div className="px-24 py-16 pr-64 border items-center">
|
||||
<div className="uppercase text-[0.9375rem]">
|
||||
<GoodThresholdIndicator goodThreshold={goodThreshold} value={value} />
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Tooltip } from '../tooltip';
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import type { StatFields } from '../../config/types';
|
||||
import { GoodThresholdIndicator } from '../good-threshold-indicator';
|
||||
|
||||
@ -13,7 +13,7 @@ export const TableRow = ({
|
||||
description,
|
||||
}: StatFields) => {
|
||||
return (
|
||||
<Tooltip description={description}>
|
||||
<Tooltip description={description} align="start">
|
||||
<tr className="border">
|
||||
<td className="py-4 px-8">{title}</td>
|
||||
<td className="py-4 px-8 text-right">
|
||||
|
@ -1,6 +1,4 @@
|
||||
export * from './lib/context';
|
||||
export * from './lib/datetime';
|
||||
export * from './lib/decimals';
|
||||
export * from './lib/format';
|
||||
export * from './lib/grid-cells';
|
||||
export * from './lib/storage';
|
||||
|
@ -1,13 +0,0 @@
|
||||
/** Returns date in a format suitable for input[type=date] elements */
|
||||
export const formatForInput = (date: Date) => {
|
||||
const padZero = (num: number) => num.toString().padStart(2, '0');
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = padZero(date.getMonth() + 1);
|
||||
const day = padZero(date.getDate());
|
||||
const hours = padZero(date.getHours());
|
||||
const minutes = padZero(date.getMinutes());
|
||||
const secs = padZero(date.getSeconds());
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}:${secs}`;
|
||||
};
|
@ -1 +0,0 @@
|
||||
export * from './datetime';
|
@ -1,12 +0,0 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
|
||||
export function addDecimal(value: string, decimals: number): string {
|
||||
if (!decimals) return value;
|
||||
return new BigNumber(value || 0)
|
||||
.dividedBy(Math.pow(10, decimals))
|
||||
.toFixed(decimals);
|
||||
}
|
||||
export function removeDecimal(value: string, decimals: number): string {
|
||||
if (!decimals) return value;
|
||||
return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0);
|
||||
}
|
@ -1,11 +1,5 @@
|
||||
import once from 'lodash/once';
|
||||
import memoize from 'lodash/memoize';
|
||||
import { addDecimal } from '../decimals';
|
||||
|
||||
const getUserLocale = () => 'default';
|
||||
|
||||
export const splitAt = (index: number) => (x: string) =>
|
||||
[x.slice(0, index), x.slice(index)];
|
||||
import { getUserLocale } from './utils';
|
||||
|
||||
/**
|
||||
* Returns a number prefixed with either a '-' or a '+'. The open volume field
|
||||
@ -50,18 +44,20 @@ export const getDateTimeFormat = once(
|
||||
})
|
||||
);
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
|
||||
export const getNumberFormat = memoize(
|
||||
(minimumFractionDigits: number) =>
|
||||
new Intl.NumberFormat(getUserLocale(), { minimumFractionDigits })
|
||||
);
|
||||
|
||||
export const getRelativeTimeFormat = once(
|
||||
() => new Intl.RelativeTimeFormat(getUserLocale())
|
||||
);
|
||||
|
||||
export const formatNumber = (rawValue: string, decimalPlaces: number) => {
|
||||
const x = addDecimal(rawValue, decimalPlaces);
|
||||
/** Returns date in a format suitable for input[type=date] elements */
|
||||
export const formatForInput = (date: Date) => {
|
||||
const padZero = (num: number) => num.toString().padStart(2, '0');
|
||||
|
||||
return getNumberFormat(decimalPlaces).format(Number(x));
|
||||
const year = date.getFullYear();
|
||||
const month = padZero(date.getMonth() + 1);
|
||||
const day = padZero(date.getDate());
|
||||
const hours = padZero(date.getHours());
|
||||
const minutes = padZero(date.getMinutes());
|
||||
const secs = padZero(date.getSeconds());
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}:${secs}`;
|
||||
};
|
@ -1 +1,4 @@
|
||||
export * from './format';
|
||||
export * from './date';
|
||||
export * from './number';
|
||||
export * from './truncate';
|
||||
export * from './utils';
|
||||
|
27
libs/react-helpers/src/lib/format/number.ts
Normal file
27
libs/react-helpers/src/lib/format/number.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import memoize from 'lodash/memoize';
|
||||
import { getUserLocale } from './utils';
|
||||
|
||||
export function addDecimal(value: string, decimals: number): string {
|
||||
if (!decimals) return value;
|
||||
return new BigNumber(value || 0)
|
||||
.dividedBy(Math.pow(10, decimals))
|
||||
.toFixed(decimals);
|
||||
}
|
||||
|
||||
export function removeDecimal(value: string, decimals: number): string {
|
||||
if (!decimals) return value;
|
||||
return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0);
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
|
||||
export const getNumberFormat = memoize(
|
||||
(minimumFractionDigits: number) =>
|
||||
new Intl.NumberFormat(getUserLocale(), { minimumFractionDigits })
|
||||
);
|
||||
|
||||
export const formatNumber = (rawValue: string, decimalPlaces: number) => {
|
||||
const x = addDecimal(rawValue, decimalPlaces);
|
||||
|
||||
return getNumberFormat(decimalPlaces).format(Number(x));
|
||||
};
|
14
libs/react-helpers/src/lib/format/truncate.ts
Normal file
14
libs/react-helpers/src/lib/format/truncate.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export function truncateByChars(s: string, startChars = 6, endChars = 6) {
|
||||
const ELLIPSIS = '\u2026';
|
||||
|
||||
// if the text is shorted than the total number of chars to show
|
||||
// no truncation is needed. Plus one is to account for the ellipsis
|
||||
if (s.length <= startChars + endChars + 1) {
|
||||
return s;
|
||||
}
|
||||
|
||||
const start = s.slice(0, startChars);
|
||||
const end = s.slice(-endChars);
|
||||
|
||||
return start + ELLIPSIS + end;
|
||||
}
|
4
libs/react-helpers/src/lib/format/utils.ts
Normal file
4
libs/react-helpers/src/lib/format/utils.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const getUserLocale = () => 'default';
|
||||
|
||||
export const splitAt = (index: number) => (x: string) =>
|
||||
[x.slice(0, index), x.slice(index)];
|
@ -151,7 +151,7 @@ module.exports = {
|
||||
body: ['14px', '20px'],
|
||||
|
||||
ui: ['14px', '20px'],
|
||||
'ui-small': ['10px', '16px'],
|
||||
'ui-small': ['12px', '16px'],
|
||||
},
|
||||
|
||||
boxShadow: {
|
||||
|
@ -0,0 +1,29 @@
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
import { CopyWithTooltip } from './copy-with-tooltip';
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('CopyWithTooltip', async () => {
|
||||
const copyText = 'Text to be copied';
|
||||
render(
|
||||
<CopyWithTooltip text={copyText}>
|
||||
<button>Copy</button>
|
||||
</CopyWithTooltip>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Copy'));
|
||||
|
||||
expect(screen.getByRole('tooltip')).toBeInTheDocument();
|
||||
await waitForElementToBeRemoved(() => screen.queryByRole('tooltip'));
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import type { CopyWithTooltipProps } from './copy-with-tooltip';
|
||||
import { CopyWithTooltip } from './copy-with-tooltip';
|
||||
|
||||
export default {
|
||||
component: CopyWithTooltip,
|
||||
title: 'CopyWithTooltip',
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<CopyWithTooltipProps> = (args) => (
|
||||
<div>
|
||||
<p>
|
||||
<span>{args.text}</span>
|
||||
{' | '}
|
||||
<CopyWithTooltip {...args} />
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
children: <button className="underline">Copy</button>,
|
||||
text: 'Lorem ipsum dolor sit',
|
||||
};
|
@ -0,0 +1,41 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Tooltip } from '../tooltip';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
|
||||
export const TOOLTIP_TIMEOUT = 800;
|
||||
|
||||
export interface CopyWithTooltipProps {
|
||||
children: ReactElement;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function CopyWithTooltip({ children, text }: CopyWithTooltipProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line
|
||||
let timeout: any;
|
||||
|
||||
if (copied) {
|
||||
timeout = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, TOOLTIP_TIMEOUT);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [copied]);
|
||||
|
||||
return (
|
||||
<CopyToClipboard text={text} onCopy={() => setCopied(true)}>
|
||||
{/* Needs this wrapping div as tooltip component interfers with element used to capture click for copy */}
|
||||
<span>
|
||||
<Tooltip description="Copied" open={copied} align="center">
|
||||
{children}
|
||||
</Tooltip>
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { CopyWithTooltip } from './copy-with-tooltip';
|
@ -12,6 +12,8 @@ interface IconProps {
|
||||
|
||||
export const Icon = ({ size = 16, name, className }: IconProps) => {
|
||||
const effectiveClassName = classNames(
|
||||
'inline-block',
|
||||
'fill-current',
|
||||
{
|
||||
'w-20': size === 20,
|
||||
'h-20': size === 20,
|
||||
|
28
libs/ui-toolkit/src/components/tooltip/tooltip.spec.tsx
Normal file
28
libs/ui-toolkit/src/components/tooltip/tooltip.spec.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { Tooltip } from './tooltip';
|
||||
|
||||
test('Renders a tooltip', async () => {
|
||||
const props = {
|
||||
description: 'description',
|
||||
children: <button>Tooltip</button>,
|
||||
};
|
||||
render(<Tooltip {...props} />);
|
||||
// radix applies the data-state attribute
|
||||
expect(screen.getByRole('button')).toHaveAttribute('data-state', 'closed');
|
||||
fireEvent.mouseOver(screen.getByRole('button'));
|
||||
expect(await screen.findByRole('tooltip')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Doesnt render a tooltip if no description provided', () => {
|
||||
const props = {
|
||||
description: undefined,
|
||||
children: <button>Tooltip</button>,
|
||||
};
|
||||
render(<Tooltip {...props} />);
|
||||
expect(screen.getByRole('button')).not.toHaveAttribute(
|
||||
'data-state',
|
||||
'closed'
|
||||
);
|
||||
fireEvent.mouseOver(screen.getByRole('button'));
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
||||
});
|
23
libs/ui-toolkit/src/components/tooltip/tooltip.stories.tsx
Normal file
23
libs/ui-toolkit/src/components/tooltip/tooltip.stories.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import type { TooltipProps } from './tooltip';
|
||||
import { Tooltip } from './tooltip';
|
||||
|
||||
export default {
|
||||
component: Tooltip,
|
||||
title: 'Tooltip',
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<TooltipProps> = (args) => <Tooltip {...args} />;
|
||||
|
||||
export const Uncontrolled = Template.bind({});
|
||||
Uncontrolled.args = {
|
||||
children: <button>Click me!</button>,
|
||||
description: 'Tooltip content!',
|
||||
};
|
||||
|
||||
export const Controlled = Template.bind({});
|
||||
Controlled.args = {
|
||||
children: <button>Open me using the 'open' prop</button>,
|
||||
description: 'Tooltip content!',
|
||||
open: false,
|
||||
};
|
@ -7,18 +7,20 @@ import {
|
||||
Arrow,
|
||||
} from '@radix-ui/react-tooltip';
|
||||
|
||||
interface TooltipProps {
|
||||
export interface TooltipProps {
|
||||
children: React.ReactElement;
|
||||
description?: string;
|
||||
open?: boolean;
|
||||
align?: 'start' | 'center' | 'end';
|
||||
}
|
||||
|
||||
// Conditionally rendered tooltip if description content is provided.
|
||||
export const Tooltip = ({ children, description }: TooltipProps) =>
|
||||
export const Tooltip = ({ children, description, open, align }: TooltipProps) =>
|
||||
description ? (
|
||||
<Provider delayDuration={200} skipDelayDuration={100}>
|
||||
<Root>
|
||||
<Root open={open}>
|
||||
<Trigger asChild>{children}</Trigger>
|
||||
<Content align={'start'} alignOffset={5}>
|
||||
<Content align={align} alignOffset={5}>
|
||||
<Arrow
|
||||
width={10}
|
||||
height={5}
|
@ -5,6 +5,7 @@ export { AgGridLazy, AgGridDynamic } from './components/ag-grid';
|
||||
export { AsyncRenderer } from './components/async-renderer';
|
||||
export { Button, AnchorButton } from './components/button';
|
||||
export { Callout } from './components/callout';
|
||||
export { CopyWithTooltip } from './components/copy-with-tooltip';
|
||||
export { EthereumUtils };
|
||||
export { EtherscanLink } from './components/etherscan-link';
|
||||
export { FormGroup } from './components/form-group';
|
||||
@ -19,6 +20,7 @@ export { TextArea } from './components/text-area';
|
||||
export { ThemeSwitcher } from './components/theme-switcher';
|
||||
export { Dialog } from './components/dialog/dialog';
|
||||
export { VegaLogo } from './components/vega-logo';
|
||||
export { Tooltip } from './components/tooltip';
|
||||
|
||||
// Utils
|
||||
export * from './utils/intent';
|
||||
|
@ -3,3 +3,32 @@
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
// copy-to-clipboard used byreact-copy-to-clipboard falls back to
|
||||
// window.prompt if more modern methods of accessing the clipboard api fail
|
||||
window.prompt = jest.fn();
|
||||
|
||||
// Required by radix-ui/react-tooltip
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
// Required by radix-ui/react-tooltip
|
||||
global.DOMRect = class DOMRect {
|
||||
bottom = 0;
|
||||
left = 0;
|
||||
right = 0;
|
||||
top = 0;
|
||||
|
||||
constructor(
|
||||
public x = 0,
|
||||
public y = 0,
|
||||
public width = 0,
|
||||
public height = 0
|
||||
) {}
|
||||
static fromRect(other?: DOMRectInit): DOMRect {
|
||||
return new DOMRect(other?.x, other?.y, other?.width, other?.height);
|
||||
}
|
||||
toJSON() {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
};
|
||||
|
@ -6,4 +6,5 @@ module.exports = {
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/wallet',
|
||||
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||
};
|
||||
|
@ -6,3 +6,4 @@ export * from './connectors';
|
||||
export * from './storage-keys';
|
||||
export * from './types';
|
||||
export * from './use-vega-transaction';
|
||||
export * from './manage-dialog';
|
||||
|
82
libs/wallet/src/manage-dialog.spec.tsx
Normal file
82
libs/wallet/src/manage-dialog.spec.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { VegaWalletContext } from './context';
|
||||
import type { VegaWalletContextShape, VegaKeyExtended } from './context';
|
||||
import type { VegaManageDialogProps } from './manage-dialog';
|
||||
import { VegaManageDialog } from './manage-dialog';
|
||||
|
||||
let props: VegaManageDialogProps;
|
||||
let context: Partial<VegaWalletContextShape>;
|
||||
let keypair1: VegaKeyExtended;
|
||||
let keypair2: VegaKeyExtended;
|
||||
|
||||
beforeEach(() => {
|
||||
keypair1 = {
|
||||
pub: '111111__111111',
|
||||
name: 'keypair1-name',
|
||||
} as VegaKeyExtended;
|
||||
keypair2 = {
|
||||
pub: '222222__222222',
|
||||
name: 'keypair2-name',
|
||||
} as VegaKeyExtended;
|
||||
props = {
|
||||
dialogOpen: true,
|
||||
setDialogOpen: jest.fn(),
|
||||
};
|
||||
context = {
|
||||
keypair: keypair1,
|
||||
keypairs: [keypair1, keypair2],
|
||||
selectPublicKey: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const generateJsx = (
|
||||
context: VegaWalletContextShape,
|
||||
props: VegaManageDialogProps
|
||||
) => {
|
||||
return (
|
||||
<VegaWalletContext.Provider value={context}>
|
||||
<VegaManageDialog {...props} />
|
||||
</VegaWalletContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
test('Shows list of available keys and can disconnect', () => {
|
||||
render(generateJsx(context as VegaWalletContextShape, props));
|
||||
|
||||
const list = screen.getByTestId('keypair-list');
|
||||
expect(list).toBeInTheDocument();
|
||||
// eslint-disable-next-line
|
||||
expect(list.children).toHaveLength(context.keypairs!.length);
|
||||
|
||||
// eslint-disable-next-line
|
||||
context.keypairs!.forEach((kp, i) => {
|
||||
const keyListItem = within(screen.getByTestId(`key-${kp.pub}`));
|
||||
expect(
|
||||
keyListItem.getByText(kp.name, { selector: 'h2' })
|
||||
).toBeInTheDocument();
|
||||
expect(keyListItem.getByText('Copy')).toBeInTheDocument();
|
||||
|
||||
// Active
|
||||
// eslint-disable-next-line
|
||||
if (kp.pub === context.keypair!.pub) {
|
||||
expect(keyListItem.getByTestId('selected-key')).toBeInTheDocument();
|
||||
expect(
|
||||
keyListItem.queryByTestId('select-keypair-button')
|
||||
).not.toBeInTheDocument();
|
||||
}
|
||||
// Inactive
|
||||
else {
|
||||
const selectButton = keyListItem.getByTestId('select-keypair-button');
|
||||
expect(selectButton).toBeInTheDocument();
|
||||
expect(keyListItem.queryByTestId('selected-key')).not.toBeInTheDocument();
|
||||
fireEvent.click(selectButton);
|
||||
expect(context.selectPublicKey).toHaveBeenCalledWith(kp.pub);
|
||||
}
|
||||
});
|
||||
|
||||
// Disconnect
|
||||
fireEvent.click(screen.getByTestId('disconnect'));
|
||||
expect(context.disconnect).toHaveBeenCalled();
|
||||
expect(props.setDialogOpen).toHaveBeenCalledWith(false);
|
||||
});
|
90
libs/wallet/src/manage-dialog.tsx
Normal file
90
libs/wallet/src/manage-dialog.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { t, truncateByChars } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
CopyWithTooltip,
|
||||
Intent,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '.';
|
||||
|
||||
export interface VegaManageDialogProps {
|
||||
dialogOpen: boolean;
|
||||
setDialogOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const VegaManageDialog = ({
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
}: VegaManageDialogProps) => {
|
||||
const { keypair, keypairs, selectPublicKey, disconnect } = useVegaWallet();
|
||||
return (
|
||||
<Dialog
|
||||
title={t('SELECT A VEGA KEY')}
|
||||
open={dialogOpen}
|
||||
onChange={setDialogOpen}
|
||||
intent={Intent.Prompt}
|
||||
>
|
||||
<div className="text-ui">
|
||||
{keypairs ? (
|
||||
<ul className="mb-12" data-testid="keypair-list">
|
||||
{keypairs.map((kp) => {
|
||||
return (
|
||||
<li
|
||||
key={kp.pub}
|
||||
data-testid={`key-${kp.pub}`}
|
||||
className="mb-24 last:mb-0"
|
||||
>
|
||||
<h2 className="mb-8 text-h5 capitalize">{kp.name}</h2>
|
||||
{kp.pub === keypair?.pub ? (
|
||||
<p
|
||||
className="uppercase mb-8 font-bold"
|
||||
data-testid="selected-key"
|
||||
>
|
||||
{t('Selected key')}
|
||||
</p>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
selectPublicKey(kp.pub);
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
disabled={kp.pub === keypair?.pub}
|
||||
className="mb-8"
|
||||
data-testid="select-keypair-button"
|
||||
>
|
||||
{t('Select this key')}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex justify-between text-ui-small">
|
||||
<p className="font-mono">
|
||||
{truncateByChars(kp.pub, 23, 23)}
|
||||
</p>
|
||||
<CopyWithTooltip text={kp.pub}>
|
||||
<button className="underline">
|
||||
<Icon name="duplicate" className="mr-4" />
|
||||
{t('Copy')}
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : null}
|
||||
<div className="mt-24">
|
||||
<Button
|
||||
data-testid="disconnect"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
disconnect();
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('Disconnect all keys')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
1
libs/wallet/src/setup-tests.ts
Normal file
1
libs/wallet/src/setup-tests.ts
Normal file
@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
@ -46,6 +46,7 @@
|
||||
"nx": "^13.8.3",
|
||||
"postcss": "^8.4.6",
|
||||
"react": "17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.4",
|
||||
"react-dom": "17.0.2",
|
||||
"react-hook-form": "^7.27.0",
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
@ -88,6 +89,7 @@
|
||||
"@types/node": "16.11.7",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/react": "17.0.30",
|
||||
"@types/react-copy-to-clipboard": "^5.0.2",
|
||||
"@types/react-dom": "17.0.9",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "~5.10.0",
|
||||
@ -111,6 +113,7 @@
|
||||
"nx": "^13.8.3",
|
||||
"prettier": "^2.5.1",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sass": "1.43.2",
|
||||
"storybook-addon-themes": "^6.1.0",
|
||||
"ts-jest": "27.0.5",
|
||||
|
24
yarn.lock
24
yarn.lock
@ -5386,6 +5386,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/react-copy-to-clipboard@^5.0.2":
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz#c29690b472a54edff35916f0d1c6c797ad0fd34b"
|
||||
integrity sha512-O29AThfxrkUFRsZXjfSWR2yaWo0ppB1yLEnHA+Oh24oNetjBAwTDu1PmolIqdJKzsZiO4J1jn6R6TmO96uBvGg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-dom@17.0.9":
|
||||
version "17.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
|
||||
@ -8966,7 +8973,7 @@ copy-descriptor@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
||||
|
||||
copy-to-clipboard@^3.3.1:
|
||||
copy-to-clipboard@^3, copy-to-clipboard@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
|
||||
integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
|
||||
@ -16584,7 +16591,7 @@ prompts@^2.0.1, prompts@^2.4.0:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
prop-types@^15.0.0, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@ -16834,6 +16841,14 @@ react-colorful@^5.1.2:
|
||||
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784"
|
||||
integrity sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg==
|
||||
|
||||
react-copy-to-clipboard@^5.0.4:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz#42ec519b03eb9413b118af92d1780c403a5f19bf"
|
||||
integrity sha512-IeVAiNVKjSPeGax/Gmkqfa/+PuMTBhutEvFUaMQLwE2tS0EXrAdgOpWDX26bWTXF3HrioorR7lr08NqeYUWQCQ==
|
||||
dependencies:
|
||||
copy-to-clipboard "^3"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-docgen-typescript@^2.0.0:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz#4611055e569edc071204aadb20e1c93e1ab1659c"
|
||||
@ -17425,6 +17440,11 @@ requires-port@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||
|
||||
resize-observer-polyfill@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
||||
|
||||
resolve-cwd@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
|
||||
|
Loading…
Reference in New Issue
Block a user