feat(ui-toolkit): navigation (#3069)

This commit is contained in:
Art 2023-03-10 16:46:51 +01:00 committed by GitHub
parent b68136ba3f
commit 9d346d7846
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1778 additions and 1387 deletions

View File

@ -1,33 +1,27 @@
context('Blocks page', { tags: '@regression' }, function () {
before('visit token home page', function () {
cy.visit('/');
});
describe('Verify elements on page', function () {
const blockNavigation = 'a[href="/blocks"]';
beforeEach(() => {
cy.visit('/blocks');
});
const blockHeight = '[data-testid="block-height"]';
const blockTime = '[data-testid="block-time"]';
const blockHeader = '[data-testid="block-header"]';
const previousBlockBtn = '[data-testid="previous-block"]';
const infiniteScrollWrapper = '[data-testid="infinite-scroll-wrapper"]';
beforeEach('navigate to blocks page', function () {
cy.get(blockNavigation).click();
});
it('Blocks page is displayed', function () {
validateBlocksDisplayed();
});
it('Blocks page is displayed on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(blockNavigation).click();
cy.switchToMobile();
validateBlocksDisplayed();
});
it('Block validator page is displayed', function () {
waitForBlocksResponse();
cy.get(blockHeight).eq(0).click();
cy.get(blockHeight).eq(0).find('a').click({ force: true });
cy.get('[data-testid="block-validator"]').should('not.be.empty');
cy.get(blockTime).should('not.be.empty');
//TODO: Add assertion for transactions when txs are added
@ -35,7 +29,7 @@ context('Blocks page', { tags: '@regression' }, function () {
it('Navigate to previous block', function () {
waitForBlocksResponse();
cy.get(blockHeight).eq(0).click();
cy.get(blockHeight).eq(0).find('a').click({ force: true });
cy.get(blockHeader)
.invoke('text')
.then(($blockHeaderTxt) => {

View File

@ -2,17 +2,14 @@ context('Network parameters page', { tags: '@smoke' }, function () {
before('navigate to network parameter page', function () {
cy.fixture('net_parameter_format_lookup').as('networkParameterFormat');
});
describe('Verify elements on page', function () {
const networkParametersNavigation = 'a[href="/network-parameters"]';
beforeEach(() => {
cy.visit('/network-parameters');
});
const networkParametersHeader = '[data-testid="network-param-header"]';
const tableRows = '[data-testid="key-value-table-row"]';
before('navigate to network parameter page', function () {
cy.visit('/');
cy.get(networkParametersNavigation).click();
});
it('should show network parameter heading at top of page', function () {
cy.get(networkParametersHeader)
.should('have.text', 'Network Parameters')
@ -201,55 +198,8 @@ context('Network parameters page', { tags: '@smoke' }, function () {
});
});
it('should be able to switch network parameter page - between light and dark mode', function () {
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
const themeSwitcher = '[data-testid="theme-switcher"]';
const jsonFields = '.hljs';
const sideMenuBackground = '.absolute';
// Engage dark mode if not already set
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.then((background_color) => {
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
cy.get(themeSwitcher).click();
});
// Engage white mode
cy.get(themeSwitcher).click();
// White Mode
cy.get(networkParametersNavigation)
.should('have.css', 'background-color')
.and('include', whiteThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', whiteThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', whiteThemeSideMenuBackgroundColor);
// Dark Mode
cy.get(themeSwitcher).click();
cy.get(networkParametersNavigation)
.should('have.css', 'background-color')
.and('include', darkThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', darkThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', darkThemeSideMenuBackgroundColor);
});
it.skip('should be able to see network parameters - on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(networkParametersNavigation).click();
it('should be able to see network parameters - on mobile', function () {
cy.switchToMobile();
cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => {

View File

@ -1,6 +1,4 @@
import classnames from 'classnames';
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
import { Nav } from './components/nav';
import { Header } from './components/header';
import { Main } from './components/main';
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
@ -11,6 +9,7 @@ import {
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
import classNames from 'classnames';
const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
@ -25,19 +24,7 @@ const DialogsContainer = () => {
);
};
function App() {
const layoutClasses = classnames(
'grid grid-rows-[auto_1fr_auto] grid-cols-[1fr] md:grid-rows-[auto_minmax(700px,_1fr)_auto] md:grid-cols-[300px_1fr]',
'min-h-[100vh] mx-auto my-0',
'border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r',
'bg-white dark:bg-black',
'antialiased text-black dark:text-white',
'overflow-hidden relative'
);
return (
<TendermintWebsocketProvider>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
const MainnetSimAd = () => (
<AnnouncementBanner>
<div className="font-alpha calt uppercase text-center text-lg text-white">
<span className="pr-4">Mainnet sim 2 is live!</span>
@ -46,14 +33,33 @@ function App() {
</ExternalLink>
</div>
</AnnouncementBanner>
);
<div className={layoutClasses}>
function App() {
return (
<TendermintWebsocketProvider>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<div
className={classNames(
'max-w-[1500px] min-h-[100vh]',
'mx-auto my-0',
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
'border-vega-light-200 dark:border-vega-dark-200 lg:border-l lg:border-r',
'antialiased text-black dark:text-white',
'overflow-hidden relative'
)}
>
<div>
<Header />
<Nav />
<MainnetSimAd />
</div>
<div>
<Main />
</div>
<div>
<Footer />
</div>
</div>
<DialogsContainer />
</NetworkLoader>
</TendermintWebsocketProvider>

View File

@ -16,7 +16,7 @@ export const Footer = () => {
return (
<>
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-neutral-700 dark:border-neutral-300">
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
<div className="flex justify-between gap-2 align-middle">
{GIT_COMMIT_HASH && (
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">

View File

@ -19,12 +19,10 @@ const renderComponent = () => (
);
describe('Header', () => {
it('should render heading', () => {
it('should render navigation', () => {
render(renderComponent());
expect(screen.getByTestId('explorer-header')).toHaveTextContent(
'Vega Explorer'
);
expect(screen.getByTestId('navigation')).toHaveTextContent('Explorer');
});
it('should render search', () => {
render(renderComponent());

View File

@ -1,43 +1,108 @@
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { ThemeSwitcher, Icon } from '@vegaprotocol/ui-toolkit';
import { matchPath, useLocation } from 'react-router-dom';
import {
ThemeSwitcher,
Navigation,
NavigationList,
NavigationItem,
NavigationLink,
NavigationBreakpoint,
NavigationTrigger,
NavigationContent,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { Search } from '../search';
import { Routes } from '../../routes/route-names';
import { NetworkSwitcher } from '@vegaprotocol/environment';
import { useNavStore } from '../nav';
import type { Navigable } from '../../routes/router-config';
import routerConfig from '../../routes/router-config';
import { useMemo } from 'react';
import compact from 'lodash/compact';
import { Search } from '../search';
const routeToNavigationItem = (r: Navigable) => (
<NavigationItem key={r.name}>
<NavigationLink to={r.path}>{r.text}</NavigationLink>
</NavigationItem>
);
export const Header = () => {
const [open, toggle] = useNavStore((state) => [state.open, state.toggle]);
const headerClasses = classnames(
'md:col-span-2',
'grid grid-rows-2 md:grid-rows-1 grid-cols-[1fr_auto] md:grid-cols-[auto_1fr_auto] items-center',
'p-4 gap-2 md:gap-4',
'border-b border-neutral-700 dark:border-neutral-300 bg-black',
'dark text-white'
const mainItems = compact(
[Routes.TX, Routes.BLOCKS, Routes.ORACLES, Routes.VALIDATORS].map((n) =>
routerConfig.find((r) => r.path === n)
)
);
const groupedItems = compact(
[
Routes.PARTIES,
Routes.ASSETS,
Routes.MARKETS,
Routes.GOVERNANCE,
Routes.NETWORK_PARAMETERS,
Routes.GENESIS,
].map((n) => routerConfig.find((r) => r.path === n))
);
const { pathname } = useLocation();
/**
* Because the grouped items are displayed in a sub menu under an "Other" item
* we need to determine whether any underlying item is active to highlight the
* trigger in the same fashion as any other top-level `NavigationLink`.
* This function checks whether the current location pathname is one of the
* underlying NavigationLinks.
*/
const isOnOther = useMemo(() => {
for (const path of groupedItems.map((r) => r.path)) {
const matched = matchPath(`${path}/*`, pathname);
if (matched) return true;
}
return false;
}, [groupedItems, pathname]);
return (
<header className={headerClasses}>
<div className="flex h-full items-center sm:items-stretch gap-4">
<Link to={Routes.HOME}>
<h1
className="text-white text-3xl font-alpha uppercase calt mb-0"
data-testid="explorer-header"
>
{t('Vega Explorer')}
</h1>
</Link>
<NetworkSwitcher />
</div>
<button
data-testid="open-menu"
className="md:hidden text-white"
onClick={() => toggle()}
>
<Icon name={open ? 'cross' : 'menu'} />
</button>
<Navigation
appName="Explorer"
theme="system"
breakpoints={[490, 900]}
actions={
<>
<ThemeSwitcher />
<Search />
<ThemeSwitcher className="-my-4" />
</header>
</>
}
onResize={(width, el) => {
if (width < 1157) {
// switch to magnifying glass trigger when width < 1157
el.classList.remove('nav-search-full');
el.classList.add('nav-search-compact');
} else {
el.classList.remove('nav-search-compact');
el.classList.add('nav-search-full');
}
}}
>
<NavigationList hide={[NavigationBreakpoint.Small]}>
<NavigationItem>
<NetworkSwitcher />
</NavigationItem>
</NavigationList>
<NavigationList
hide={[NavigationBreakpoint.Small, NavigationBreakpoint.Narrow]}
>
{mainItems.map(routeToNavigationItem)}
{groupedItems && (
<NavigationItem>
<NavigationTrigger isActive={Boolean(isOnOther)}>
{t('Other')}
</NavigationTrigger>
<NavigationContent>
<NavigationList>
{groupedItems.map(routeToNavigationItem)}
</NavigationList>
</NavigationContent>
</NavigationItem>
)}
</NavigationList>
</Navigation>
);
};

View File

@ -1 +0,0 @@
export * from './nav';

View File

@ -1,181 +0,0 @@
import { NavLink, useLocation } from 'react-router-dom';
import type { Navigable } from '../../routes/router-config';
import routerConfig from '../../routes/router-config';
import classnames from 'classnames';
import { create } from 'zustand';
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
} from 'react';
import { Icon } from '@vegaprotocol/ui-toolkit';
import first from 'lodash/first';
import last from 'lodash/last';
import { BREAKPOINT_MD } from '../../config/breakpoints';
type NavStore = {
open: boolean;
toggle: () => void;
hide: () => void;
};
export const useNavStore = create<NavStore>()((set, get) => ({
open: false,
toggle: () => set({ open: !get().open }),
hide: () => set({ open: false }),
}));
const NavLinks = ({ links }: { links: Navigable[] }) => {
const navLinks = links.map((r) => (
<li key={r.name}>
<NavLink
to={r.path}
className={({ isActive }) =>
classnames(
'block mb-2 px-2',
'text-lg hover:bg-vega-pink dark:hover:bg-vega-yellow hover:text-white dark:hover:text-black',
{
'bg-vega-pink text-white dark:bg-vega-yellow dark:text-black':
isActive,
}
)
}
>
{r.text}
</NavLink>
</li>
));
return <ul className="pr-8 md:pr-0">{navLinks}</ul>;
};
export const Nav = () => {
const [open, hide] = useNavStore((state) => [state.open, state.hide]);
const location = useLocation();
const navRef = useRef<HTMLElement>(null);
const btnRef = useRef<HTMLButtonElement>(null);
const focusable = useMemo(
() =>
navRef.current
? [
...(navRef.current.querySelectorAll(
'a, button'
) as NodeListOf<HTMLElement>),
]
: [],
// eslint-disable-next-line react-hooks/exhaustive-deps
[navRef.current] // do not remove `navRef.current` from deps
);
const closeNav = useCallback(() => {
hide();
console.log(focusable);
focusable.forEach((fe) =>
fe.setAttribute(
'tabindex',
window.innerWidth > BREAKPOINT_MD ? '0' : '-1'
)
);
}, [focusable, hide]);
// close navigation when location changes
useEffect(() => {
closeNav();
}, [closeNav, location]);
useLayoutEffect(() => {
if (open) {
focusable.forEach((fe) => fe.setAttribute('tabindex', '0'));
}
document.body.style.overflow = open ? 'hidden' : '';
const offset =
document.querySelector('header')?.getBoundingClientRect().top || 0;
if (navRef.current) {
navRef.current.style.height = `calc(100vh - ${offset}px)`;
}
// focus current by default
if (navRef.current && open) {
(navRef.current.querySelector('a[aria-current]') as HTMLElement)?.focus();
}
const closeOnEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeNav();
}
};
// tabbing loop
const focusLast = (e: FocusEvent) => {
e.preventDefault();
const isNavElement =
e.relatedTarget && navRef.current?.contains(e.relatedTarget as Node);
if (!isNavElement && open) {
last(focusable)?.focus();
}
};
const focusFirst = (e: FocusEvent) => {
e.preventDefault();
const isNavElement =
e.relatedTarget && navRef.current?.contains(e.relatedTarget as Node);
if (!isNavElement && open) {
first(focusable)?.focus();
}
};
const resetOnDesktop = () => {
focusable.forEach((fe) =>
fe.setAttribute(
'tabindex',
window.innerWidth > BREAKPOINT_MD ? '0' : '-1'
)
);
};
window.addEventListener('resize', resetOnDesktop);
first(focusable)?.addEventListener('focusout', focusLast);
last(focusable)?.addEventListener('focusout', focusFirst);
document.addEventListener('keydown', closeOnEsc);
return () => {
window.removeEventListener('resize', resetOnDesktop);
document.removeEventListener('keydown', closeOnEsc);
first(focusable)?.removeEventListener('focusout', focusLast);
last(focusable)?.removeEventListener('focusout', focusFirst);
};
}, [closeNav, focusable, open]);
return (
<nav
ref={navRef}
className={classnames(
'absolute top-0 z-20 overflow-y-auto',
'transition-[right]',
{
'right-[-200vw] h-full': !open,
'right-0 h-[100vh]': open,
},
'w-full p-4 border-neutral-700 dark:border-neutral-300',
'bg-white dark:bg-black',
'md:static md:border-r'
)}
>
<NavLinks links={routerConfig} />
<button
ref={btnRef}
className="absolute top-0 right-0 p-4 md:hidden"
onClick={() => {
closeNav();
}}
>
<Icon name="cross" />
</button>
</nav>
);
};

View File

@ -5,11 +5,31 @@ import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { getSearchType, SearchTypes, toHex } from './detect-search';
import { Routes } from '../../routes/route-names';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
interface FormFields {
search: string;
}
const MagnifyingGlass = () => (
<svg
className="w-4 h-4"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="12.8202"
y1="13.1798"
x2="17.0629"
y2="17.4224"
stroke="currentColor"
/>
<circle cx="8" cy="8" r="7.5" stroke="currentColor" />
</svg>
);
export const Search = () => {
const { register, handleSubmit } = useForm<FormFields>();
const navigate = useNavigate();
@ -49,39 +69,95 @@ export const Search = () => {
[navigate]
);
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="w-full md:max-w-[620px] justify-self-end"
>
const searchForm = (
<form className="block min-w-[290px]" onSubmit={handleSubmit(onSubmit)}>
<div className="flex relative items-stretch gap-2 text-xs">
<label htmlFor="search" className="sr-only">
{t('Search by block number or transaction hash')}
</label>
<div className="flex items-stretch gap-2">
<div className="flex grow relative">
<button
className={classNames(
'absolute top-[50%] translate-y-[-50%] left-2',
'text-vega-light-300 dark:text-vega-dark-300'
)}
>
<MagnifyingGlass />
</button>
<Input
{...register('search')}
id="search"
data-testid="search"
className="text-white"
className={classNames(
'peer',
'pl-8 py-2 text-xs',
'border rounded border-vega-light-200 dark:border-vega-dark-200'
)}
hasError={Boolean(error?.message)}
type="text"
placeholder={t(
'Enter block number, public key or transaction hash'
)}
placeholder={t('Enter block number, public key or transaction hash')}
/>
{error?.message && (
<div className="bg-white border border-t-0 border-accent absolute top-[100%] flex-1 w-full pb-2 px-2 rounded-b text-black">
<InputError data-testid="search-error" intent="danger">
<div
className={classNames(
'hidden peer-focus:block',
'bg-white dark:bg-black',
'border rounded-b border-t-0 border-vega-light-200 dark:border-vega-dark-200',
'absolute top-[100%] flex-1 w-full pb-2 px-2 text-black dark:text-white'
)}
>
<InputError
data-testid="search-error"
intent="danger"
className="text-xs"
>
{error.message}
</InputError>
</div>
)}
</div>
<Button type="submit" size="sm" data-testid="search-button">
<Button
className="hidden [.search-dropdown_&]:block"
type="submit"
size="xs"
data-testid="search-button"
>
{t('Search')}
</Button>
</div>
</form>
);
const searchTrigger = (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="text-vega-light-300 dark:text-vega-dark-300 data-open:text-black dark:data-open:text-white flex items-center">
<MagnifyingGlass />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className={classNames(
'search-dropdown',
'p-2 min-w-[290px] z-20',
'text-vega-light-300 dark:text-vega-dark-300',
'bg-white dark:bg-black',
'border rounded border-vega-light-200 dark:border-vega-dark-200',
'shadow-[8px_8px_16px_0_rgba(0,0,0,0.4)]'
)}
align="end"
sideOffset={10}
>
{searchForm}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
return (
<>
<div className="hidden [.nav-search-full_&]:block">{searchForm}</div>
<div className="hidden [.nav-search-compact_&]:block">
{searchTrigger}
</div>
</>
);
};

View File

@ -23,14 +23,18 @@ import { NetworkParameters } from './network-parameters';
import type { RouteObject } from 'react-router-dom';
import { MarketPage, MarketsPage } from './markets';
export type Navigable = { path: string; name: string; text: string };
export type Navigable = {
path: string;
name: string;
text: string;
};
type Route = RouteObject & Navigable;
const partiesRoutes: Route[] = flags.parties
? [
{
path: Routes.PARTIES,
name: 'Parties',
name: t('Parties'),
text: t('Parties'),
element: <Party />,
children: [
@ -52,7 +56,7 @@ const assetsRoutes: Route[] = flags.assets
{
path: Routes.ASSETS,
text: t('Assets'),
name: 'Assets',
name: t('Assets'),
children: [
{
index: true,
@ -71,7 +75,7 @@ const genesisRoutes: Route[] = flags.genesis
? [
{
path: Routes.GENESIS,
name: 'Genesis',
name: t('Genesis'),
text: t('Genesis Parameters'),
element: <Genesis />,
},
@ -82,7 +86,7 @@ const governanceRoutes: Route[] = flags.governance
? [
{
path: Routes.GOVERNANCE,
name: 'Governance proposals',
name: t('Governance proposals'),
text: t('Governance Proposals'),
element: <Proposals />,
},
@ -93,7 +97,7 @@ const marketsRoutes: Route[] = flags.markets
? [
{
path: Routes.MARKETS,
name: 'Markets',
name: t('Markets'),
text: t('Markets'),
children: [
{
@ -113,17 +117,18 @@ const networkParametersRoutes: Route[] = flags.networkParameters
? [
{
path: Routes.NETWORK_PARAMETERS,
name: 'NetworkParameters',
name: t('NetworkParameters'),
text: t('Network Parameters'),
element: <NetworkParameters />,
},
]
: [];
const validators: Route[] = flags.validators
? [
{
path: Routes.VALIDATORS,
name: 'Validators',
name: t('Validators'),
text: t('Validators'),
element: <ValidatorsPage />,
},
@ -133,14 +138,14 @@ const validators: Route[] = flags.validators
const routerConfig: Route[] = [
{
path: Routes.HOME,
name: 'Home',
name: t('Home'),
text: t('Home'),
element: <Home />,
index: true,
},
{
path: Routes.TX,
name: 'Txs',
name: t('Txs'),
text: t('Transactions'),
element: <Txs />,
children: [
@ -160,7 +165,7 @@ const routerConfig: Route[] = [
},
{
path: Routes.BLOCKS,
name: 'Blocks',
name: t('Blocks'),
text: t('Blocks'),
element: <BlockPage />,
children: [
@ -176,7 +181,7 @@ const routerConfig: Route[] = [
},
{
path: Routes.ORACLES,
name: 'Oracles',
name: t('Oracles'),
text: t('Oracles'),
element: <OraclePage />,
children: [

View File

@ -3,3 +3,13 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
Object.defineProperty(window, 'ResizeObserver', {
writable: false,
value: jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
connect: jest.fn(),
disconnect: jest.fn(),
})),
});

View File

@ -14,13 +14,14 @@ const stakeShare = '[data-testid="stake-percentage"]';
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
const vegaWalletUnstakedBalance =
'[data-testid="vega-wallet-balance-unstaked"]';
'[data-testid="vega-wallet-balance-unstaked"]:visible';
const vegaWalletStakedBalances =
'[data-testid="vega-wallet-balance-staked-validators"]';
const ethWalletAssociatedBalances =
'[data-testid="eth-wallet-associated-balances"]';
const ethWalletTotalAssociatedBalance = '[data-testid="currency-locked"]';
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const ethWalletTotalAssociatedBalance =
'[data-testid="currency-locked"]:visible';
const ethWalletContainer = '[data-testid="ethereum-wallet"]:visible';
const vegaWallet = '[data-testid="vega-wallet"]';
const partValidatorId = '…';
const txTimeout = Cypress.env('txTimeout');

View File

@ -1,60 +1,10 @@
const navSection = 'nav';
const navSupply = '[href="/token/tranches"]';
const navToken = '[href="/token"]';
const navStaking = '[href="/validators"]';
const navRewards = '[href="/rewards"]';
const navWithdraw = '[href="/token/withdraw"]';
const navGovernance = '[href="/proposals"]';
const navRedeem = '[href="/token/redeem"]';
context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
before('visit token home page', function () {
cy.visit('/');
cy.get('nav', { timeout: 10000 }).should('be.visible');
});
describe('with wallets disconnected', function () {
before('wait for page to load', function () {
cy.get(navSection, { timeout: 10000 }).should('be.visible');
});
describe('Navigation tabs', function () {
it('should have proposals tab', function () {
cy.get(navSection).within(() => {
cy.get(navGovernance).should('be.visible');
});
});
it('should have validators tab', function () {
cy.get(navSection).within(() => {
cy.get(navStaking).should('be.visible');
});
});
it('should have rewards tab', function () {
cy.get(navSection).within(() => {
cy.get(navRewards).should('be.visible');
});
});
describe('Token dropdown', function () {
before('click on token dropdown', function () {
cy.get(navSection).within(() => {
cy.getByTestId('state-trigger').realClick();
});
});
it('should have token dropdown', function () {
cy.get(navToken).should('be.visible');
});
it('should have supply & vesting dropdown', function () {
cy.get(navSupply).should('be.visible');
});
it('should have withdraw dropdown', function () {
cy.get(navWithdraw).should('be.visible');
});
it('should have redeem dropdown', function () {
cy.get(navRedeem).should('be.visible');
});
});
});
describe('Links and buttons', function () {
it('should have link for proposal page', function () {
cy.getByTestId('home-proposals').within(() => {

View File

@ -0,0 +1,284 @@
context(
'Landing pages - verifies required elements',
{ tags: '@smoke' },
() => {
const navbar = 'nav .navbar';
const mobileNav = '[data-testid="menu-drawer"]';
const topLevelLinks = [
{
name: 'Proposals',
selector: '[href="/proposals"]',
tests: () => {
it('should be able to see a working link for - find out more about Vega governance', function () {
const governanceDocsUrl = 'https://vega.xyz/governance';
const proposalDocumentationLink =
'[data-testid="proposal-documentation-link"]';
// 3001-VOTE-001
cy.get(proposalDocumentationLink)
.should('be.visible')
.and('have.text', 'Find out more about Vega governance')
.and('have.attr', 'href')
.and('equal', governanceDocsUrl);
// 3002-PROP-001
cy.request(governanceDocsUrl)
.its('body')
.then((body) => {
if (!body.includes('Govern the network')) {
assert.include(
body,
'Govern the network',
`Checking that governance link destination includes 'Govern the network' text`
);
}
});
});
it('should be able to see button for - new proposal', function () {
// 3001-VOTE-002
const newProposalLink = '[data-testid="new-proposal-link"]';
cy.get(newProposalLink)
.should('be.visible')
.and('have.text', 'New proposal')
.and('have.attr', 'href')
.and('equal', '/proposals/propose');
});
},
},
{
name: 'Validators',
selector: '[href="/validators"]',
tests: () => {
it('Should have Staking Guide link visible', function () {
// 2001-STKE-003
cy.get('[data-testid="staking-guide-link"]')
.should('be.visible')
.and('have.text', 'Read more about staking on Vega')
.and(
'have.attr',
'href',
'https://docs.vega.xyz/mainnet/concepts/vega-chain/#staking-on-vega'
);
});
},
},
{
name: 'Rewards',
selector: '[href="/rewards"]',
header: 'Rewards and fees',
tests: () => {
it('should have epoch warning', () => {
cy.get('[data-testid="callout"]')
.should('be.visible')
.and(
'have.text',
'Rewards are credited 5 minutes after the epoch ends.This delay is set by a network parameter'
);
});
it('should have toggle for seeing total vs individual rewards', () => {
cy.get('[data-testid="epoch-reward-view-toggle-total"]').should(
'be.visible'
);
});
},
},
];
const secondLevelLinks = [
{
trigger: true,
name: 'Token',
selector: '[data-testid="state-trigger"]',
},
{
name: 'Token',
selector: '[href="/token"]',
header: 'The $VEGA token',
},
{
name: 'Supply & Vesting',
selector: '[href="/token/tranches"]',
header: 'Vesting tranches',
},
{
name: 'Withdraw',
selector: '[href="/token/withdraw"]',
header: 'Withdrawals',
tests: () => {
it('should have connect Vega wallet button', function () {
cy.get('[data-testid="connect-to-vega-wallet-btn"]')
.should('be.visible')
.and('have.text', 'Connect Vega wallet');
});
},
},
{
name: 'Redeem',
selector: '[href="/token/redeem"]',
header: 'Vesting',
tests: () => {
// 1005-VEST-018
it('should have connect Eth wallet button', function () {
cy.get('[data-testid="connect-to-eth-btn"]')
.should('be.visible')
.and('have.text', 'Connect Ethereum wallet');
});
},
},
{
name: 'Associate',
selector: '[href="/token/associate"]',
header: 'Associate $VEGA tokens with Vega Key',
},
{
name: 'Disassociate',
selector: '[href="/token/disassociate"]',
header: 'Disassociate $VEGA tokens from a Vega key',
},
];
const expand = () => {
const trigger = secondLevelLinks.find((l) => l.trigger).selector;
cy.get(trigger).then((el) => {
if (el.attr('aria-expanded') === 'false') {
el.trigger('click');
}
});
};
const collapse = () => {
const trigger = secondLevelLinks.find((l) => l.trigger).selector;
cy.get(trigger).then((el) => {
if (el.attr('aria-expanded') === 'true') {
el.trigger('click');
}
});
};
const ensureHeader = (text) => {
cy.get('main header h1').should('have.text', text);
};
before(() => {
// goes to HOME
cy.visit('/');
// and waits for it to load
cy.get(navbar, { timeout: 10000 }).should('be.visible');
});
describe('Navigation (desktop)', () => {
for (const { name, selector } of topLevelLinks) {
it(`should have ${name} nav link`, () => {
cy.get(navbar).within(() => {
cy.get(selector).should('be.visible');
cy.get(selector).should('have.text', name);
});
});
}
for (const { name, selector, trigger } of secondLevelLinks) {
it(`should have ${name} ${
trigger ? 'as trigger button' : ''
} second level nav link`, () => {
cy.get(navbar).within(() => {
cy.get(selector).should('be.visible');
cy.get(selector).should('have.text', name);
if (trigger) cy.get(selector).click();
});
});
}
after(() => {
collapse();
});
});
describe('Navigation (mobile)', () => {
beforeEach(() => {
// iphone xr
cy.viewport(414, 896);
});
it('should have burger button', () => {
cy.get('[data-testid="button-menu-drawer"]').should('be.visible');
cy.get('[data-testid="button-menu-drawer"]').click();
cy.get(mobileNav).should('be.visible');
});
for (const { name, selector } of topLevelLinks) {
it(`should have ${name} nav link`, () => {
cy.get(mobileNav).within(() => {
cy.get(selector).should('be.visible');
cy.get(selector).should('have.text', name);
});
});
}
for (const { name, selector, trigger } of secondLevelLinks) {
it(`should have ${name} ${
trigger ? 'as trigger button' : ''
} second level nav link`, () => {
cy.get(mobileNav).within(() => {
cy.get(selector).should('be.visible');
cy.get(selector).should('have.text', name);
});
});
}
after(() => {
cy.get('[data-testid="button-menu-drawer"]').click();
cy.viewport(
Cypress.config('viewportWidth'),
Cypress.config('viewportHeight')
);
});
});
describe('Elements', () => {
for (const { name, selector, header, tests } of topLevelLinks) {
describe(`${name} page`, () => {
it(`navigates to ${name}`, () => {
cy.get(navbar).within(() => {
cy.log(`goes to ${name}`);
cy.get(selector).click();
cy.log(`ensures ${name} is highlighted`);
cy.get(selector).should('have.attr', 'aria-current');
});
});
it('displays header', () => {
ensureHeader(header || name);
});
if (tests) tests.apply(this);
});
}
for (const { name, selector, header, tests } of secondLevelLinks.filter(
(l) => !l.trigger
)) {
describe(`${name} page`, () => {
it(`navigates to ${name}`, () => {
cy.get(navbar).within(() => {
expand();
cy.log(`goes to ${name}`);
cy.get(selector).click();
expand();
cy.log(`ensures ${name} is highlighted`);
cy.get(selector).should('have.attr', 'aria-current');
});
});
it('displays header', () => {
ensureHeader(header || name);
});
if (tests) tests.apply(this);
});
}
after(() => {
collapse();
});
});
}
);

View File

@ -1,68 +0,0 @@
const proposalDocumentationLink = '[data-testid="proposal-documentation-link"]';
const newProposalButton = '[data-testid="new-proposal-link"]';
const newProposalLink = '[data-testid="new-proposal-link"]';
const governanceDocsUrl = 'https://vega.xyz/governance';
const connectToVegaWalletButton = '[data-testid="connect-to-vega-wallet-btn"]';
context(
'Governance Page - verify elements on page',
{ tags: '@smoke' },
function () {
before('navigate to governance page', function () {
cy.visit('/').navigate_to('proposals');
});
describe('with no network change proposals', function () {
it('should have governance tab highlighted', function () {
cy.verify_tab_highlighted('proposals');
});
it('should have GOVERNANCE header visible', function () {
cy.verify_page_header('Proposals');
});
it('should be able to see a working link for - find out more about Vega governance', function () {
// 3001-VOTE-001
cy.get(proposalDocumentationLink)
.should('be.visible')
.and('have.text', 'Find out more about Vega governance')
.and('have.attr', 'href')
.and('equal', governanceDocsUrl);
// 3002-PROP-001
cy.request(governanceDocsUrl)
.its('body')
.then((body) => {
if (!body.includes('Govern the network')) {
assert.include(
body,
'Govern the network',
`Checking that governance link destination includes 'Govern the network' text`
);
}
});
});
it('should be able to see button for - new proposal', function () {
// 3001-VOTE-002
cy.get(newProposalLink)
.should('be.visible')
.and('have.text', 'New proposal')
.and('have.attr', 'href')
.and('equal', '/proposals/propose');
});
// Skipping this test for now, the new proposal button no longer takes a user directly
// to a proposal form, instead it takes them to a page where they can select a proposal type.
// Keeping this test here for now as it can be repurposed to test the new proposal forms.
it.skip('should be able to see a connect wallet button - if vega wallet disconnected and new proposal button selected', function () {
cy.get(newProposalButton).should('be.visible').click();
cy.get(connectToVegaWalletButton)
.should('be.visible')
.and('have.text', 'Connect Vega wallet');
cy.navigate_to('proposals');
cy.wait_for_spinner();
});
});
}
);

View File

@ -50,7 +50,7 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
});
it('Able to disconnect via wallet', function () {
cy.getByTestId('manage-vega-wallet').click();
cy.get('aside [data-testid="manage-vega-wallet"]').click();
cy.getByTestId('disconnect').click();
cy.getByTestId(banner).should('not.exist');
});

View File

@ -1,35 +0,0 @@
const viewToggle = '[data-testid="epoch-reward-view-toggle-total"]';
const warning = '[data-testid="callout"]';
context(
'Rewards Page - verify elements on page',
{ tags: '@regression' },
function () {
before('navigate to rewards page', function () {
cy.visit('/').navigate_to('rewards');
});
describe('with wallets disconnected', function () {
it('should have REWARDS tab highlighted', function () {
cy.verify_tab_highlighted('rewards');
});
it('should have rewards header visible', function () {
cy.verify_page_header('Rewards and fees');
});
it('should have epoch warning', function () {
cy.get(warning)
.should('be.visible')
.and(
'have.text',
'Rewards are credited 5 minutes after the epoch ends.This delay is set by a network parameter'
);
});
it('should have toggle for seeing total vs individual rewards', function () {
cy.get(viewToggle).should('be.visible');
});
});
}
);

View File

@ -17,8 +17,7 @@ const vegaTokenContractAddress = Cypress.env('vegaTokenContractAddress');
context('Verify elements on Token page', { tags: '@smoke' }, function () {
before('Visit token page', function () {
cy.visit('/');
cy.navigate_to('token');
cy.visit('/token');
});
describe('THE $VEGA TOKEN table', function () {
it('should have TOKEN ADDRESS', function () {

View File

@ -8,13 +8,7 @@ context(
function () {
before('visit homepage', function () {
cy.intercept('GET', '**/tranches/stats', { tranches });
cy.visit('/');
});
it('Able to navigate to tranches page', function () {
cy.navigate_to('supply');
cy.url().should('include', '/token/tranches');
cy.get('h1').should('contain.text', 'Vesting tranches');
cy.visit('/token/tranches');
});
// 1005-VEST-001

View File

@ -1,5 +1,4 @@
/// <reference types="cypress" />
const guideLink = '[data-testid="staking-guide-link"]';
const validatorTitle = '[data-testid="validator-node-title"]';
const validatorId = '[data-testid="validator-id"]';
const validatorPubKey = '[data-testid="validator-public-key"]';
@ -25,31 +24,7 @@ const stakeNumberRegex = /^\d*\.?\d*$/;
context('Staking Page - verify elements on page', function () {
before('navigate to staking page', function () {
cy.visit('/').navigate_to('validators');
});
describe('with wallets disconnected', { tags: '@smoke' }, function () {
describe('description section', function () {
it('Should have validators tab highlighted', function () {
cy.verify_tab_highlighted('validators');
});
it('Should have validators ON VEGA header visible', function () {
cy.verify_page_header('Validators');
});
it('Should have Staking Guide link visible', function () {
// 2001-STKE-003
cy.get(guideLink)
.should('be.visible')
.and('have.text', 'Read more about staking on Vega')
.and(
'have.attr',
'href',
'https://docs.vega.xyz/mainnet/concepts/vega-chain/#staking-on-vega'
);
});
});
cy.visit('/validators');
});
describe(

View File

@ -1,4 +1,3 @@
const connectButton = '[data-testid="connect-to-eth-btn"]';
const lockedTokensInVestingContract = '6,499,972.30';
context(
@ -6,24 +5,7 @@ context(
{ tags: '@smoke' },
function () {
before('navigate to vesting page', function () {
cy.visit('/').navigate_to('vesting');
});
describe('with wallets disconnected', function () {
it('should have vesting tab highlighted', function () {
cy.verify_tab_highlighted('token');
});
it('should have VESTING header visible', function () {
cy.verify_page_header('Vesting');
});
// 1005-VEST-018
it('should have connect Eth wallet button', function () {
cy.get(connectButton)
.should('be.visible')
.and('have.text', 'Connect Ethereum wallet');
});
cy.visit('/token/redeem');
});
describe('With Eth wallet connected', function () {
@ -38,15 +20,18 @@ context(
cy.getByTestId('currency-title')
.should('contain.text', 'VEGA')
.and('contain.text', 'In vesting contract');
cy.getByTestId('currency-value').should(
cy.get('[data-testid="currency-value"]:visible').should(
'have.text',
lockedTokensInVestingContract
);
cy.getByTestId('currency-locked').should(
cy.get('[data-testid="currency-locked"]:visible').should(
'have.text',
lockedTokensInVestingContract
);
cy.getByTestId('currency-unlocked').should('have.text', '0.00');
cy.get('[data-testid="currency-unlocked"]:visible').should(
'have.text',
'0.00'
);
});
});
// 1005-VEST-022 1005-VEST-023

View File

@ -1,18 +1,19 @@
const walletContainer = '[data-testid="ethereum-wallet"]';
const walletContainer = 'aside [data-testid="ethereum-wallet"]';
const walletHeader = '[data-testid="wallet-header"] h1';
const connectToEthButton = '[data-testid="connect-to-eth-wallet-button"]';
const connectToEthButton =
'[data-testid="connect-to-eth-wallet-button"]:visible';
const connectorList = '[data-testid="web3-connector-list"]';
const associate = '[href="/token/associate"]';
const disassociate = '[href="/token/disassociate"]';
const disconnect = '[data-testid="disconnect-from-eth-wallet-button"]';
const accountNo = '[data-testid="ethereum-account-truncated"]';
const currencyTitle = '[data-testid="currency-title"]';
const currencyValue = '[data-testid="currency-value"]';
const vegaInVesting = '[data-testid="vega-in-vesting-contract"]';
const vegaInWallet = '[data-testid="vega-in-wallet"]';
const progressBar = '[data-testid="progress-bar"]';
const currencyLocked = '[data-testid="currency-locked"]';
const currencyUnlocked = '[data-testid="currency-unlocked"]';
const currencyTitle = '[data-testid="currency-title"]:visible';
const currencyValue = '[data-testid="currency-value"]:visible';
const vegaInVesting = '[data-testid="vega-in-vesting-contract"]:visible';
const vegaInWallet = '[data-testid="vega-in-wallet"]:visible';
const progressBar = '[data-testid="progress-bar"]:visible';
const currencyLocked = '[data-testid="currency-locked"]:visible';
const currencyUnlocked = '[data-testid="currency-unlocked"]:visible';
const dialog = '[role="dialog"]';
const dialogHeader = '[data-testid="dialog-title"]';
const dialogCloseBtn = '[data-testid="dialog-close"]';

View File

@ -1,6 +1,6 @@
import { truncateByChars } from '@vegaprotocol/utils';
const walletContainer = '[data-testid="vega-wallet"]';
const walletContainer = 'aside [data-testid="vega-wallet"]';
const walletHeader = '[data-testid="wallet-header"] h1';
const connectButton = '[data-testid="connect-vega-wallet"]';
const getVegaLink = '[data-testid="link"]';
@ -14,7 +14,6 @@ const restWallet = '#wallet';
const restPassphrase = '#passphrase';
const restConnectBtn = '[type="submit"]';
const accountNo = '[data-testid="vega-account-truncated"]';
const walletName = '[data-testid="wallet-name"]';
const currencyTitle = '[data-testid="currency-title"]';
const currencyValue = '[data-testid="currency-value"]';
const vegaUnstaked = '[data-testid="vega-wallet-balance-unstaked"] .text-right';
@ -28,44 +27,24 @@ const vegaWalletCurrencyTitle = '[data-testid="currency-title"]';
const vegaWalletPublicKey = Cypress.env('vegaWalletPublicKey');
const txTimeout = Cypress.env('txTimeout');
const faucetAssets = {
BTCFake: 'fBTC',
DAIFake: 'fDAI',
EUROFake: 'fEURO',
USDCFake: 'fUSDC',
};
context(
'Vega Wallet - verify elements on widget',
{ tags: '@regression' },
function () {
before('visit token home page', function () {
() => {
before('visit token home page', () => {
cy.visit('/');
cy.get(walletContainer, { timeout: 60000 }).should('be.visible');
});
describe('with wallets disconnected', function () {
before('wait for widget to load', function () {
cy.get(walletContainer, { timeout: 10000 }).should('be.visible');
});
it('should have VEGA WALLET header visible', function () {
describe('with wallets disconnected', () => {
it('should have required elements visible', function () {
cy.get(walletContainer).within(() => {
cy.get(walletHeader)
.should('be.visible')
.and('have.text', 'Vega Wallet');
});
});
it('should have Connect Vega button visible', function () {
cy.get(walletContainer).within(() => {
cy.get(connectButton)
.should('be.visible')
.and('have.text', 'Connect Vega wallet to use associated $VEGA');
});
});
it('should have Get a Vega wallet link visible', function () {
cy.get(walletContainer).within(() => {
cy.get(getVegaLink)
.should('be.visible')
.and('have.text', 'Get a Vega wallet')
@ -74,14 +53,14 @@ context(
});
});
describe('when connect button clicked', function () {
before('click connect vega wallet button', function () {
describe('when connect button clicked', () => {
before('click connect vega wallet button', () => {
cy.get(walletContainer).within(() => {
cy.get(connectButton).click();
});
});
it('should have Connect Vega header visible', function () {
it('should have Connect Vega header visible', () => {
cy.get(dialog).within(() => {
cy.get(walletDialogHeader)
.should('be.visible')
@ -175,14 +154,6 @@ context(
}
);
it.skip('should have wallet name visible', function () {
cy.get(walletContainer).within(() => {
cy.get(walletName)
.should('be.visible')
.and('have.text', `${Cypress.env('vegaWalletName')} key 1`);
});
});
it('should have Vega Associated currency title visible', function () {
cy.get(walletContainer).within(() => {
cy.get(currencyTitle)
@ -299,113 +270,71 @@ context(
});
// 2002-SINC-016
describe('when assets exist in vegawallet', function () {
before('send-faucet assets to connected vega wallet', function () {
describe('Vega wallet with assets', function () {
const assets = [
{
id: 'fUSDC',
name: 'USDC (fake)',
amount: '1000000',
expectedAmount: '10.00',
},
{
id: 'fDAI',
name: 'DAI (fake)',
amount: '200000',
expectedAmount: '2.00',
},
{
id: 'fBTC',
name: 'BTC (fake)',
amount: '600000',
expectedAmount: '6.00',
},
{
id: 'fEURO',
name: 'EURO (fake)',
amount: '800000',
expectedAmount: '8.00',
},
];
before('faucet assets to connected vega wallet', function () {
for (const { id, amount } of assets) {
cy.vega_wallet_faucet_assets_without_check(
faucetAssets.USDCFake,
'1000000',
vegaWalletPublicKey
);
cy.vega_wallet_faucet_assets_without_check(
faucetAssets.BTCFake,
'600000',
vegaWalletPublicKey
);
cy.vega_wallet_faucet_assets_without_check(
faucetAssets.EUROFake,
'800000',
vegaWalletPublicKey
);
cy.vega_wallet_faucet_assets_without_check(
faucetAssets.DAIFake,
'200000',
id,
amount,
vegaWalletPublicKey
);
}
cy.reload();
cy.wait_for_spinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
});
it('should see fUSDC assets - within vega wallet', function () {
let currency = { id: faucetAssets.USDCFake, name: 'USDC (fake)' };
for (const { id, name, expectedAmount } of assets) {
it(`should see ${id} within vega wallet`, () => {
cy.get(walletContainer).within(() => {
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id, txTimeout)
.contains(id, txTimeout)
.should('be.visible');
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.contains(id)
.parent()
.siblings()
.invoke('text')
.should('not.be.empty');
.within((el) => {
const value = parseFloat(el.text());
cy.wrap(value).should('be.gte', parseFloat(expectedAmount));
});
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.contains(id)
.parent()
.contains(currency.name);
});
});
it('should see fBTC assets - within vega wallet', function () {
let currency = { id: faucetAssets.BTCFake, name: 'BTC (fake)' };
cy.get(walletContainer).within(() => {
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id, txTimeout)
.should('be.visible');
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.siblings()
.within(() => cy.contains_exactly('6.00').should('be.visible'));
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.contains(currency.name);
});
});
it('should see fEURO assets - within vega wallet', function () {
let currency = { id: faucetAssets.EUROFake, name: 'EURO (fake)' };
cy.get(walletContainer).within(() => {
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id, txTimeout)
.should('be.visible');
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.siblings()
.within(() => cy.contains_exactly('8.00').should('be.visible'));
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.contains(currency.name);
});
});
it('should see fDAI assets - within vega wallet', function () {
let currency = { id: faucetAssets.DAIFake, name: 'DAI (fake)' };
cy.get(walletContainer).within(() => {
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id, txTimeout)
.should('be.visible');
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.siblings()
.within(() => cy.contains_exactly('2.00').should('be.visible'));
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.contains(currency.name);
.contains(name);
});
});
}
});
});
}

View File

@ -1,27 +0,0 @@
const connectToVegaBtn = '[data-testid="connect-to-vega-wallet-btn"]';
context(
'Withdraw Page - verify elements on page',
{ tags: '@smoke' },
function () {
before('navigate to withdrawals page', function () {
cy.visit('/').navigate_to('withdraw');
});
describe('with wallets disconnected', function () {
it('should have withdraw tab highlighted', function () {
cy.verify_tab_highlighted('token');
});
it('should have WITHDRAW header visible', function () {
cy.verify_page_header('Withdrawals');
});
it('should have connect Vega wallet button', function () {
cy.get(connectToVegaBtn)
.should('be.visible')
.and('have.text', 'Connect Vega wallet');
});
});
}
);

View File

@ -22,26 +22,33 @@ const navigation = {
};
const topLevelRoutes = ['proposals', 'validators', 'rewards'];
Cypress.Commands.add('navigate_to', (page) => {
const tokenDropDown = 'state-trigger';
Cypress.Commands.add('navigate_to', (page) => {
if (!topLevelRoutes.includes(page)) {
cy.getByTestId(tokenDropDown, { timeout: 10000 }).click();
cy.getByTestId('token-dropdown').within(() => {
cy.get(navigation[page]).click();
// FIXME: Timeout madness
cy.getByTestId(tokenDropDown, { timeout: 60000 }).eq(0).click();
cy.get('[data-testid="token-dropdown"]:visible').within(() => {
cy.get(navigation[page]).eq(0).click();
});
} else {
return cy.get(navigation.section, { timeout: 10000 }).within(() => {
cy.get(navigation[page]).click();
cy.get(navigation[page]).eq(0).click();
});
}
});
Cypress.Commands.add('verify_tab_highlighted', (page) => {
return cy.get(navigation.section).within(() => {
if (!topLevelRoutes.includes(page)) {
cy.getByTestId(tokenDropDown, { timeout: 10000 }).eq(0).click();
cy.get('[data-testid="token-dropdown"]:visible').within(() => {
cy.get(navigation[page]).should('have.attr', 'aria-current');
});
} else {
cy.get(navigation[page]).should('have.attr', 'aria-current');
}
});
});
Cypress.Commands.add('verify_page_header', (text) => {
@ -59,7 +66,7 @@ export function associateTokenStartOfTests() {
cy.highlight(`Associating tokens for first time`);
cy.ethereum_wallet_connect();
cy.connectVegaWallet();
cy.get('[href="/token/associate"]').first().click();
cy.get('[href="/token/associate"]:visible').first().click();
cy.getByTestId('associate-radio-wallet', { timeout: 30000 }).click();
cy.getByTestId('token-amount-input', epochTimeout).type('1');
cy.getByTestId('token-input-submit-button', txTimeout)

View File

@ -3,10 +3,10 @@ const tokenSubmitButton = '[data-testid="token-input-submit-button"]';
const tokenInputApprove = '[data-testid="token-input-approve-button"]';
const addStakeRadioButton = '[data-testid="add-stake-radio"]';
const removeStakeRadioButton = '[data-testid="remove-stake-radio"]';
const ethWalletAssociateButton = '[href="/token/associate"]';
const ethWalletDissociateButton = '[href="/token/disassociate"]';
const ethWalletAssociateButton = '[href="/token/associate"]:visible';
const ethWalletDissociateButton = '[href="/token/disassociate"]:visible';
const vegaWalletUnstakedBalance =
'[data-testid="vega-wallet-balance-unstaked"]';
'[data-testid="vega-wallet-balance-unstaked"]:visible';
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
const associateWalletRadioButton = '[data-testid="associate-radio-wallet"]';
const associateContractRadioButton = '[data-testid="associate-radio-contract"]';

View File

@ -1,5 +1,6 @@
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const connectToEthButton = '[data-testid="connect-to-eth-wallet-button"]';
const ethWalletContainer = '[data-testid="ethereum-wallet"]:visible';
const connectToEthButton =
'[data-testid="connect-to-eth-wallet-button"]:visible';
const capsuleWalletConnectButton = '[data-testid="web3-connector-Unknown"]';
Cypress.Commands.add('ethereum_wallet_connect', () => {

View File

@ -7,7 +7,7 @@ import {
} from '@vegaprotocol/smart-contracts';
import { ethers, Wallet } from 'ethers';
const vegaWalletContainer = '[data-testid="vega-wallet"]';
const vegaWalletContainer = 'aside [data-testid="vega-wallet"]';
const vegaWalletMnemonic = Cypress.env('vegaWalletMnemonic');
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey');
const vegaTokenContractAddress = Cypress.env('vegaTokenContractAddress');
@ -95,7 +95,7 @@ Cypress.Commands.add('faucet_asset', function (assetEthAddress) {
});
Cypress.Commands.add('vega_wallet_teardown', function () {
cy.get('[data-testid="associated-amount"]')
cy.get('aside [data-testid="associated-amount"]')
.should('be.visible')
.invoke('text')
.as('associatedAmount');

View File

@ -1,126 +0,0 @@
import classNames from 'classnames';
import { NavLink } from 'react-router-dom';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import * as Dialog from '@radix-ui/react-dialog';
import { EthWallet } from '../eth-wallet';
import { VegaWallet } from '../vega-wallet';
import { useTranslation } from 'react-i18next';
interface Route {
name: string;
path: string;
}
const DrawerSection = ({ children }: { children: React.ReactNode }) => (
<div className="px-4 my-4">{children}</div>
);
const IconLine = ({ inverted }: { inverted: boolean }) => (
<span className={`block w-6 h-[2px] ${inverted ? 'bg-black' : 'bg-white'}`} />
);
const DrawerNavLinks = ({
isInverted,
routes,
}: {
isInverted?: boolean;
routes: Route[];
}) => {
const { appDispatch } = useAppState();
const { t } = useTranslation();
const linkProps = {
end: true,
onClick: () =>
appDispatch({ type: AppStateActionType.SET_DRAWER, isOpen: false }),
};
const navClasses = classNames('flex flex-col');
return (
<nav className={navClasses}>
{routes.map(({ name, path }) => {
return (
<NavLink
{...linkProps}
to={{ pathname: path }}
className={({ isActive }) =>
classNames({
'bg-vega-yellow text-black': !isInverted && isActive,
'bg-transparent text-white hover:text-vega-yellow':
!isInverted && !isActive,
'bg-black text-white': isInverted && isActive,
'bg-transparent text-black hover:text-white':
isInverted && !isActive,
'border-t border-white p-4': true,
})
}
>
{t(name)}
</NavLink>
);
})}
</nav>
);
};
export const NavDrawer = ({
inverted,
routes,
}: {
inverted: boolean;
routes: Route[];
}) => {
const { appState, appDispatch } = useAppState();
const drawerContentClasses = classNames(
'drawer-content', // needed for css animation
// Positions the modal in the center of screen
'fixed w-[80vw] max-w-[420px] top-0 right-0',
'flex flex-col flex-nowrap justify-between h-full bg-banner overflow-y-scroll border-l border-white',
'bg-black text-neutral-200'
);
return (
<>
<button
onClick={() =>
appDispatch({
type: AppStateActionType.SET_DRAWER,
isOpen: true,
})
}
className="flex flex-col flex-nowrap gap-1"
>
<IconLine inverted={inverted} />
<IconLine inverted={inverted} />
<IconLine inverted={inverted} />
</button>
<Dialog.Root
open={appState.drawerOpen}
onOpenChange={(isOpen) =>
appDispatch({
type: AppStateActionType.SET_DRAWER,
isOpen,
})
}
>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-white/15" />
<Dialog.Content className={drawerContentClasses}>
<div>
<DrawerSection>
<EthWallet />
</DrawerSection>
<DrawerSection>
<VegaWallet />
</DrawerSection>
</div>
<DrawerNavLinks routes={routes} />
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</>
);
};

View File

@ -1,50 +0,0 @@
import { useState } from 'react';
import Routes, { TOKEN_DROPDOWN_ROUTES } from '../../routes/routes';
import { useTranslation } from 'react-i18next';
import type { NavbarTheme } from './nav-link';
import { AppNavLink } from './nav-link';
import {
NavDropdownMenu,
NavDropdownMenuContent,
NavDropdownMenuItem,
NavDropdownMenuTrigger,
} from '@vegaprotocol/ui-toolkit';
export const NavDropDown = ({ navbarTheme }: { navbarTheme: NavbarTheme }) => {
const { t } = useTranslation();
const [isOpen, setOpen] = useState(false);
return (
<NavDropdownMenu open={isOpen} onOpenChange={(open) => setOpen(open)}>
<AppNavLink
name={
<NavDropdownMenuTrigger
className="w-auto flex items-center -m-3 p-3 cursor-pointer"
data-testid="state-trigger"
onClick={() => setOpen(!isOpen)}
>
{t('Token')}
</NavDropdownMenuTrigger>
}
testId="token-dd"
path={Routes.TOKEN}
navbarTheme={navbarTheme}
/>
<NavDropdownMenuContent data-testid="token-dropdown">
{TOKEN_DROPDOWN_ROUTES.map((r) => (
<NavDropdownMenuItem key={r.name} onClick={() => setOpen(false)}>
<AppNavLink
testId={r.name}
name={t(r.name)}
path={r.path}
navbarTheme={'inherit'}
subNav={true}
end={true}
fullWidth={true}
/>
</NavDropdownMenuItem>
))}
</NavDropdownMenuContent>
</NavDropdownMenu>
);
};

View File

@ -1,57 +0,0 @@
import classNames from 'classnames';
import { NavLink } from 'react-router-dom';
import type { HTMLAttributeAnchorTarget, ReactNode } from 'react';
import { getNavLinkClassNames } from '@vegaprotocol/ui-toolkit';
export type NavbarTheme = 'inherit' | 'dark' | 'yellow';
interface AppNavLinkProps {
name: ReactNode | string;
path: string;
navbarTheme: NavbarTheme;
testId?: string;
target?: HTMLAttributeAnchorTarget;
end?: boolean;
fullWidth?: boolean;
subNav?: boolean;
}
export const AppNavLink = ({
name,
path,
navbarTheme,
target,
testId,
end = false,
fullWidth = false,
subNav = false,
}: AppNavLinkProps) => {
const borderClasses = classNames(
'absolute h-0.5 w-full bottom-[-1px] left-0',
{
'bg-black dark:bg-vega-yellow': navbarTheme !== 'yellow',
'bg-black': navbarTheme === 'yellow',
}
);
return (
<NavLink
key={path}
data-testid={testId}
to={{ pathname: path }}
className={getNavLinkClassNames(navbarTheme, fullWidth, subNav)}
target={target}
end={end}
>
{({ isActive }) => {
return (
<div className={subNav ? 'inline-block relative pb-1' : undefined}>
{name}
{isActive && (
<span data-testid="link-active" className={borderClasses} />
)}
</div>
);
}}
</NavLink>
);
};

View File

@ -1,25 +0,0 @@
@keyframes slideIn {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes slideOut {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
.drawer-content[data-state='open'] {
animation: slideIn 150ms ease-out forwards;
}
.drawer-content[data-state='closed'] {
animation: slideOut 150ms ease-in forwards;
}

View File

@ -1,56 +0,0 @@
import { render, screen, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { Nav } from './nav';
jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'),
NetworkSwitcher: () => <div data-testid="network-switcher" />,
useEnvironment: () => ({ VEGA_ENV: 'MAINNET' }),
}));
const renderComponent = (initialEntries?: string[]) => {
return render(
<MemoryRouter initialEntries={initialEntries}>
<Nav />
</MemoryRouter>
);
};
describe('nav', () => {
it('Renders logo with link to home', () => {
renderComponent();
expect(screen.getByTestId('logo-link')).toHaveProperty(
'href',
'http://localhost/'
);
});
it('Renders network switcher', () => {
renderComponent();
expect(screen.getByTestId('network-switcher')).toBeInTheDocument();
});
it('Renders all top level routes', () => {
renderComponent();
expect(screen.getByTestId('Proposals')).toHaveProperty(
'href',
'http://localhost/proposals'
);
expect(screen.getByTestId('Validators')).toHaveProperty(
'href',
'http://localhost/validators'
);
expect(screen.getByTestId('Rewards')).toHaveProperty(
'href',
'http://localhost/rewards'
);
});
it('Shows active state on dropdown trigger when on home route for subroutes', () => {
const { getByTestId } = renderComponent(['/token']);
const dd = getByTestId('token-dd');
expect(within(dd).getByTestId('link-active')).toBeInTheDocument();
});
it('Shows active state on dropdown trigger when on sub route of dropdown', () => {
const { getByTestId } = renderComponent(['/token/withdraw']);
const dd = getByTestId('token-dd');
expect(within(dd).getByTestId('link-active')).toBeInTheDocument();
});
});

View File

@ -1,81 +1,85 @@
import { Link } from 'react-router-dom';
import { NetworkSwitcher } from '@vegaprotocol/environment';
import { useEffect, useState } from 'react';
import { TOP_LEVEL_ROUTES } from '../../routes/routes';
import { TOKEN_DROPDOWN_ROUTES, TOP_LEVEL_ROUTES } from '../../routes/routes';
import { useTranslation } from 'react-i18next';
import logoWhiteText from '../../images/logo-white-text.png';
import logoBlackText from '../../images/logo-black-text.png';
import debounce from 'lodash/debounce';
import { NavDrawer } from './nav-draw';
import { Nav as ToolkitNav } from '@vegaprotocol/ui-toolkit';
import { AppNavLink } from './nav-link';
import { NavDropDown } from './nav-dropdown';
const useDebouncedResize = () => {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResizeDebounced = debounce(() => {
setWindowWidth(window.innerWidth);
}, 300);
window.addEventListener('resize', handleResizeDebounced);
return () => {
window.removeEventListener('resize', handleResizeDebounced);
};
}, []);
return {
windowWidth,
};
};
type NavbarTheme = 'inherit' | 'dark' | 'yellow';
interface NavbarProps {
navbarTheme?: NavbarTheme;
}
export const Nav = ({ navbarTheme = 'inherit' }: NavbarProps) => {
const { windowWidth } = useDebouncedResize();
const isDesktop = windowWidth > 995;
import type { NavigationProps } from '@vegaprotocol/ui-toolkit';
import { useNavigationDrawer } from '@vegaprotocol/ui-toolkit';
import {
Navigation,
NavigationBreakpoint,
NavigationContent,
NavigationItem,
NavigationLink,
NavigationList,
NavigationTrigger,
} from '@vegaprotocol/ui-toolkit';
import { EthWallet } from '../eth-wallet';
import { VegaWallet } from '../vega-wallet';
import { useLocation, useMatch } from 'react-router-dom';
import { useEffect } from 'react';
export const Nav = ({ theme }: Pick<NavigationProps, 'theme'>) => {
const { t } = useTranslation();
const isYellow = navbarTheme === 'yellow';
const setDrawerOpen = useNavigationDrawer((state) => state.setDrawerOpen);
const location = useLocation();
const isOnToken = useMatch('token/*');
useEffect(() => {
setDrawerOpen(false);
}, [location, setDrawerOpen]);
const topLevel = TOP_LEVEL_ROUTES.map(({ name, path }) => (
<NavigationItem key={name}>
<NavigationLink to={path}>{name}</NavigationLink>
</NavigationItem>
));
const secondLevel = TOKEN_DROPDOWN_ROUTES.map(({ name, path, end }) => (
<NavigationItem key={name}>
<NavigationLink to={path} end={Boolean(end)}>
{name}
</NavigationLink>
</NavigationItem>
));
return (
<ToolkitNav
navbarTheme={navbarTheme}
icon={
<Link to="/" data-testid="logo-link">
<img
alt="Vega"
src={navbarTheme === 'yellow' ? logoBlackText : logoWhiteText}
height={30}
width={250}
/>
</Link>
}
title={undefined}
titleContent={<NetworkSwitcher />}
<Navigation appName="Governance" theme={theme} breakpoints={[458, 959]}>
<NavigationList
className="[.drawer-content_&]:border-b [.drawer-content_&]:border-b-vega-light-200 dark:[.drawer-content_&]:border-b-vega-dark-200 [.drawer-content_&]:pb-8 [.drawer-content_&]:mb-2"
hide={[NavigationBreakpoint.Small]}
>
{isDesktop ? (
<nav className="flex items-center flex-1 px-4">
{TOP_LEVEL_ROUTES.map((r) => (
<AppNavLink
key={r.path}
testId={r.name}
name={t(r.name)}
path={r.path}
navbarTheme={navbarTheme}
/>
))}
<NavDropDown navbarTheme={navbarTheme} />
</nav>
) : (
<nav className="flex items-center flex-1 px-2 justify-end">
<NavDrawer inverted={isYellow} routes={TOP_LEVEL_ROUTES} />
</nav>
)}
</ToolkitNav>
<NavigationItem className="[.drawer-content_&]:w-full">
<NetworkSwitcher className="[.drawer-content_&]:w-full" />
</NavigationItem>
</NavigationList>
<NavigationList
hide={[NavigationBreakpoint.Narrow, NavigationBreakpoint.Small]}
>
{topLevel}
<NavigationItem>
<NavigationTrigger
data-testid="state-trigger"
isActive={Boolean(isOnToken)}
>
{t('Token')}
</NavigationTrigger>
<NavigationContent>
<NavigationList data-testid="token-dropdown">
{secondLevel}
</NavigationList>
</NavigationContent>
</NavigationItem>
</NavigationList>
<NavigationList
hide={true}
className="[.drawer-content_&]:border-t [.drawer-content_&]:border-t-vega-light-200 dark:[.drawer-content_&]:border-t-vega-dark-200 [.drawer-content_&]:pt-8 [.drawer-content_&]:mt-4"
>
<NavigationItem className="[.drawer-content_&]:w-full">
<EthWallet />
</NavigationItem>
<NavigationItem className="[.drawer-content_&]:w-full">
<VegaWallet />
</NavigationItem>
</NavigationList>
</Navigation>
);
};

View File

@ -27,7 +27,7 @@ export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
</ExternalLink>
</div>
</AnnouncementBanner>
<Nav navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
<Nav theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
{isReadOnly ? (
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
) : null}

View File

@ -37,9 +37,6 @@ export interface AppState {
/** Whether or not the connect to Ethereum wallet overlay is open */
ethConnectOverlay: boolean;
/** Whether or not the mobile drawer is open. Only relevant on screens smaller than 960 */
drawerOpen: boolean;
/** Whether or not the transaction modal is open */
transactionOverlay: boolean;
/**

View File

@ -17,7 +17,6 @@ const initialAppState: AppState = {
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
drawerOpen: false,
transactionOverlay: false,
bannerMessage: '',
};
@ -36,7 +35,6 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
return {
...state,
vegaWalletOverlay: action.isOpen,
drawerOpen: action.isOpen ? false : state.drawerOpen,
};
}
case AppStateActionType.SET_VEGA_WALLET_MANAGE_OVERLAY: {
@ -44,20 +42,17 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
...state,
vegaWalletManageOverlay: action.isOpen,
vegaWalletOverlay: action.isOpen ? false : state.vegaWalletOverlay,
drawerOpen: action.isOpen ? false : state.drawerOpen,
};
}
case AppStateActionType.SET_ETH_WALLET_OVERLAY: {
return {
...state,
ethConnectOverlay: action.isOpen,
drawerOpen: action.isOpen ? false : state.drawerOpen,
};
}
case AppStateActionType.SET_DRAWER: {
return {
...state,
drawerOpen: action.isOpen,
vegaWalletOverlay: false,
};
}

View File

@ -54,7 +54,6 @@ const mockAppState: AppState = {
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
drawerOpen: false,
transactionOverlay: false,
bannerMessage: '',
};

View File

@ -17,7 +17,6 @@ const mockAppState: AppState = {
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
drawerOpen: false,
transactionOverlay: false,
bannerMessage: '',
};

View File

@ -37,6 +37,7 @@ export const TOKEN_DROPDOWN_ROUTES = [
{
name: 'Token',
path: Routes.TOKEN,
end: true,
},
{
name: 'Supply & Vesting',

View File

@ -17,14 +17,10 @@ describe('Desktop view', { tags: '@smoke' }, () => {
links.forEach((link, index) => {
it(`${link} should be correctly rendered`, () => {
cy.getByTestId('navbar')
.find(`[data-testid="navbar-links"] a[data-testid=${link}]`)
cy.get('nav')
.find(`a[data-testid=${link}]:visible`)
.then((element) => {
cy.contains('Loading...').should('not.exist');
cy.wrap(element).click();
cy.wrap(element)
.get('span.absolute.md\\:h-1.w-full')
.should('exist');
cy.location('hash').should('equal', hashes[index]);
});
});

View File

@ -1,6 +1,4 @@
import { useState } from 'react';
import classNames from 'classnames';
import { NavLink, Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
import {
DApp,
NetworkSwitcher,
@ -11,32 +9,22 @@ import { t } from '@vegaprotocol/i18n';
import { useGlobalStore } from '../../stores';
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
import {
Drawer,
getNavLinkClassNames,
getActiveNavLinkClassNames,
Nav,
NewTab,
ThemeSwitcher,
Navigation,
NavigationList,
NavigationItem,
NavigationLink,
ExternalLink,
Icon,
NavigationBreakpoint,
} from '@vegaprotocol/ui-toolkit';
import { Vega } from '../icons/vega';
import type { HTMLAttributeAnchorTarget } from 'react';
import { Links, Routes } from '../../pages/client-router';
type NavbarTheme = 'inherit' | 'dark' | 'yellow';
interface NavbarProps {
navbarTheme?: NavbarTheme;
}
const LinkList = ({
navbarTheme,
className = 'flex',
dataTestId = 'navbar-links',
onNavigate,
export const Navbar = ({
theme = 'system',
}: {
navbarTheme: NavbarTheme;
className?: string;
dataTestId?: string;
onNavigate?: () => void;
theme: ComponentProps<typeof Navigation>['theme'];
}) => {
const tokenLink = useLinks(DApp.Token);
const { marketId } = useGlobalStore((store) => ({
@ -46,178 +34,67 @@ const LinkList = ({
? Links[Routes.MARKET](marketId)
: Links[Routes.MARKET]();
return (
<div className={className} data-testid={dataTestId}>
<AppNavLink
name={t('Markets')}
path={Links[Routes.MARKETS]()}
navbarTheme={navbarTheme}
onClick={onNavigate}
end
/>
<AppNavLink
name={t('Trading')}
path={tradingPath}
navbarTheme={navbarTheme}
onClick={onNavigate}
end
/>
<AppNavLink
name={t('Portfolio')}
path={Links[Routes.PORTFOLIO]()}
navbarTheme={navbarTheme}
onClick={onNavigate}
/>
<a
href={tokenLink(TOKEN_GOVERNANCE)}
target="_blank"
rel="noreferrer"
className={classNames(
'w-full md:w-auto',
getActiveNavLinkClassNames(false, navbarTheme)
)}
>
<span className="flex items-center justify-between w-full gap-2 pr-3 md:pr-0">
{t('Governance')}
<NewTab />
</span>
</a>
</div>
);
};
const MobileMenuBar = ({ navbarTheme }: { navbarTheme: NavbarTheme }) => {
const [drawerOpen, setDrawerOpen] = useState(false);
const [container, setContainer] = useState<HTMLElement | null>(null);
const menuButton = (
<button
className={classNames(
'flex flex-col justify-around gap-3 p-2 relative z-30 h-[34px]',
{
'z-50': drawerOpen,
}
)}
onClick={() => setDrawerOpen(!drawerOpen)}
data-testid="button-menu-drawer"
>
<div
className={classNames('w-[26px] h-[2px] transition-all', {
'translate-y-0 rotate-0 bg-white': !drawerOpen,
'bg-black': !drawerOpen && navbarTheme === 'yellow',
'translate-y-[7.5px] rotate-45 bg-black dark:bg-white': drawerOpen,
})}
/>
<div
className={classNames('w-[26px] h-[2px] transition-all', {
'translate-y-0 rotate-0 bg-white': !drawerOpen,
'bg-black': !drawerOpen && navbarTheme === 'yellow',
'-translate-y-[7.5px] -rotate-45 bg-black dark:bg-white': drawerOpen,
})}
/>
</button>
);
return (
<div className="flex overflow-hidden md:hidden" ref={setContainer}>
<Drawer
dataTestId="menu-drawer"
open={drawerOpen}
onChange={setDrawerOpen}
container={container}
trigger={menuButton}
>
<div className="border-l border-default px-4 py-2 gap-4 flex flex-col w-full h-full bg-white dark:bg-black dark:text-white justify-start">
<div className="w-full h-1"></div>
<div className="px-2 pt-10 w-full flex flex-col items-stretch">
<NetworkSwitcher />
<div className="w-full pt-8 h-1 border-b border-default"></div>
</div>
<LinkList
className="flex flex-col"
navbarTheme={navbarTheme}
dataTestId="mobile-navbar-links"
onNavigate={() => setDrawerOpen(false)}
/>
<div className="flex flex-col px-2 justify-between">
<div className="w-full h-1 border-t border-default py-5"></div>
<ThemeSwitcher withMobile />
</div>
</div>
</Drawer>
</div>
);
};
export const Navbar = ({ navbarTheme = 'inherit' }: NavbarProps) => {
const titleContent = (
<div className="hidden md:block">
<NetworkSwitcher />
</div>
);
return (
<Nav
navbarTheme={navbarTheme}
title={t('Console')}
titleContent={titleContent}
icon={
<Link to="/">
<Vega className="w-13" />
</Link>
}
>
<LinkList className="hidden md:flex md:px-2" navbarTheme={navbarTheme} />
<div className="flex items-center gap-2 ml-auto overflow-hidden">
<VegaWalletConnectButton />
<ThemeSwitcher className="hidden md:block" />
<MobileMenuBar navbarTheme={navbarTheme} />
</div>
</Nav>
);
};
interface AppNavLinkProps {
name: string;
path: string;
navbarTheme: NavbarTheme;
testId?: string;
target?: HTMLAttributeAnchorTarget;
end?: boolean;
onClick?: () => void;
}
const AppNavLink = ({
name,
path,
navbarTheme,
target,
testId = name,
end,
onClick,
}: AppNavLinkProps) => {
const borderClasses = classNames(
'absolute h-[2px] md:h-1 w-full bottom-[-1px] left-0',
{
'bg-black dark:bg-vega-yellow': navbarTheme !== 'yellow',
'bg-black dark:bg-vega-yellow md:dark:bg-black': navbarTheme === 'yellow',
}
);
return (
<NavLink
data-testid={testId}
to={{ pathname: path }}
className={getNavLinkClassNames(navbarTheme)}
onClick={onClick}
target={target}
end={end}
>
{({ isActive }) => {
return (
<Navigation
appName="Console"
theme={theme}
actions={
<>
{name}
{isActive && <span className={borderClasses} />}
<ThemeSwitcher />
<VegaWalletConnectButton />
</>
);
}}
</NavLink>
}
breakpoints={[521, 1067]}
>
<NavigationList
className="[.drawer-content_&]:border-b [.drawer-content_&]:border-b-vega-light-200 dark:[.drawer-content_&]:border-b-vega-dark-200 [.drawer-content_&]:pb-8 [.drawer-content_&]:mb-2"
hide={[NavigationBreakpoint.Small]}
>
<NavigationItem className="[.drawer-content_&]:w-full">
<NetworkSwitcher className="[.drawer-content_&]:w-full" />
</NavigationItem>
</NavigationList>
<NavigationList
hide={[NavigationBreakpoint.Narrow, NavigationBreakpoint.Small]}
>
<NavigationItem>
<NavigationLink data-testid="Markets" to={Links[Routes.MARKETS]()}>
{t('Markets')}
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink data-testid="Trading" to={tradingPath}>
{t('Trading')}
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink
data-testid="Portfolio"
to={Links[Routes.PORTFOLIO]()}
>
{t('Portfolio')}
</NavigationLink>
</NavigationItem>
<NavigationItem>
<ExternalLink href={tokenLink(TOKEN_GOVERNANCE)}>
<span className="flex items-center gap-2">
<span>{t('Governance')}</span>{' '}
<Icon name="arrow-top-right" size={3} />
</span>
</ExternalLink>
</NavigationItem>
</NavigationList>
<NavigationList
className="[.drawer-content_&]:border-t [.drawer-content_&]:border-t-vega-light-200 dark:[.drawer-content_&]:border-t-vega-dark-200 [.drawer-content_&]:pt-8 [.drawer-content_&]:mt-4"
hide={[
NavigationBreakpoint.Small,
NavigationBreakpoint.Narrow,
NavigationBreakpoint.Full,
]}
>
<NavigationItem className="[.drawer-content_&]:w-full text-black dark:text-white">
<ThemeSwitcher withMobile />
</NavigationItem>
</NavigationList>
</Navigation>
);
};

View File

@ -54,7 +54,7 @@ const MobileWalletButton = ({
? 'hidden'
: isYellow
? 'fill-black'
: 'fill-white';
: 'fill-black dark:fill-white';
const [container, setContainer] = useState<HTMLElement | null>(null);
const walletButton = (

View File

@ -1,6 +1,5 @@
import Head from 'next/head';
import type { AppProps } from 'next/app';
import { Navbar } from '../components/navbar';
import { t } from '@vegaprotocol/i18n';
import {
useEagerConnect as useVegaEagerConnect,
@ -33,6 +32,7 @@ import { ViewingBanner } from '../components/viewing-banner';
import { Banner } from '../components/banner';
import classNames from 'classnames';
import { AppLoader, DynamicLoader } from '../components/app-loader';
import { Navbar } from '../components/navbar';
const DEFAULT_TITLE = t('Welcome to Vega trading!');
@ -83,9 +83,7 @@ function AppBody({ Component }: AppProps) {
</Head>
<Title />
<div className={gridClasses}>
<Navbar
navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'}
/>
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'} />
<Banner />
<ViewingBanner />
<main data-testid={location.pathname}>

View File

@ -12,7 +12,7 @@ export const addConnectPublicKey = () => {
Cypress.Commands.add('connectPublicKey', (publicKey) => {
cy.getByTestId('connect-vega-wallet').then((connectWallet) => {
if (connectWallet.length) {
cy.getByTestId('connect-vega-wallet').click();
cy.get('aside [data-testid="connect-vega-wallet"]').click();
cy.getByTestId('connector-view').should('be.visible').click();
cy.getByTestId('address').click();
cy.getByTestId('address').type(publicKey);

View File

@ -25,7 +25,7 @@ export function addVegaWalletConnect() {
mockConnectWallet();
cy.highlight(`Connecting Vega Wallet`);
cy.get(
`[data-testid=connect-vega-wallet${isMobile ? '-mobile' : ''}]`
`[data-testid=connect-vega-wallet${isMobile ? '-mobile' : ''}]:visible`
).click();
cy.get('[data-testid=connectors-list]')
.find('[data-testid="connector-jsonRpc"]')

View File

@ -11,6 +11,7 @@ import {
import { useEnvironment } from '../../hooks/use-environment';
import { Networks } from '../../types';
import { DApp, TOKEN_NEW_NETWORK_PARAM_PROPOSAL, useLinks } from '../../hooks';
import classNames from 'classnames';
export const envNameMapping: Record<Networks, string> = {
[Networks.VALIDATOR_TESTNET]: t('VALIDATOR_TESTNET'),
@ -80,7 +81,17 @@ const NetworkLabel = ({
</span>
);
export const NetworkSwitcher = () => {
type NetworkSwitcherProps = {
/**
* The current network identifier, defaults to the `VEGA_ENV` if unset.
*/
currentNetwork?: Networks;
className?: string;
};
export const NetworkSwitcher = ({
currentNetwork,
className,
}: NetworkSwitcherProps) => {
const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
const tokenLink = useLinks(DApp.Token);
const [isOpen, setOpen] = useState(false);
@ -97,6 +108,8 @@ export const NetworkSwitcher = () => {
);
const menuRef = useRef<HTMLButtonElement | null>(null);
const current = currentNetwork || VEGA_ENV;
return (
<DropdownMenu
open={isOpen}
@ -104,9 +117,9 @@ export const NetworkSwitcher = () => {
trigger={
<DropdownMenuTrigger
ref={menuRef}
className="flex justify-between items-center"
className={classNames('flex justify-between items-center', className)}
>
{envTriggerMapping[VEGA_ENV]}
{envTriggerMapping[current]}
</DropdownMenuTrigger>
}
>
@ -125,7 +138,7 @@ export const NetworkSwitcher = () => {
<a href={VEGA_NETWORKS[key]}>
{envNameMapping[key]}
<NetworkLabel
isCurrent={VEGA_ENV === key}
isCurrent={current === key}
isAvailable={!!VEGA_NETWORKS[key]}
/>
</a>
@ -150,7 +163,7 @@ export const NetworkSwitcher = () => {
<div className="mr-4">
<Link href={VEGA_NETWORKS[key]}>{envNameMapping[key]}</Link>
<NetworkLabel
isCurrent={VEGA_ENV === key}
isCurrent={current === key}
isAvailable={!!VEGA_NETWORKS[key]}
/>
</div>

View File

@ -165,5 +165,6 @@ module.exports = {
},
data: {
selected: 'state~="checked"',
open: 'state~="open"',
},
};

View File

@ -31,7 +31,7 @@ export const Default = Template.bind({});
Default.args = {
open: false,
children: (
<p className="h-full bg-black dark:bg-white text-white dark:text-black">
<p className="h-full bg-white dark:bg-black text-black dark:text-white">
Some content
</p>
),

View File

@ -27,11 +27,12 @@ export function Drawer({
trigger,
}: DrawerProps) {
const contentClasses = classNames(
'group/drawer',
'h-full max-w-[500px] w-[90vw] z-10 top-0 right-0 fixed transition-transform',
className,
{
'translate-x-[100%]': !open,
'translate-x-0 z-40': open,
'translate-x-0 z-20': open,
}
);

View File

@ -73,7 +73,7 @@ export const DropdownMenuContent = forwardRef<
<DropdownMenuPrimitive.Content
{...contentProps}
ref={forwardedRef}
className="min-w-[290px] bg-neutral-200 dark:bg-white p-2 rounded text-black"
className="min-w-[290px] bg-neutral-200 dark:bg-white p-2 rounded z-20"
align="start"
sideOffset={10}
/>

View File

@ -1,8 +1,8 @@
export * from './accordion';
export * from './announcement-banner';
export * from './arrows';
export * from './async-renderer';
export * from './background-video';
export * from './announcement-banner';
export * from './button';
export * from './callout';
export * from './checkbox';
@ -19,12 +19,16 @@ export * from './key-value-table';
export * from './link';
export * from './loader';
export * from './lozenge';
export * from './nav';
export * from './maintenance-page';
export * from './nav-dropdown';
export * from './nav';
export * from './navigation';
export * from './notification';
export * from './popover';
export * from './progress-bar';
export * from './radio-group';
export * from './resizable-grid';
export * from './rounded-wrapper';
export * from './select';
export * from './simple-grid';
export * from './slider';
@ -35,13 +39,10 @@ export * from './tabs';
export * from './text-area';
export * from './theme-switcher';
export * from './thumbs';
export * from './toast';
export * from './toggle';
export * from './tooltip';
export * from './traffic-light';
export * from './vega-icons';
export * from './vega-logo';
export * from './viewing-as-user';
export * from './traffic-light';
export * from './toast';
export * from './notification';
export * from './rounded-wrapper';
export * from './maintenance-page';

View File

@ -13,6 +13,7 @@ export const InputError = ({
children,
forInput,
testId,
className,
...props
}: InputErrorProps) => {
const effectiveClassName = classNames(
@ -31,7 +32,7 @@ export const InputError = ({
<div
data-testid={testId || 'input-error-text'}
aria-describedby={forInput}
className={effectiveClassName}
className={classNames(effectiveClassName, className)}
{...props}
role="alert"
>

View File

@ -0,0 +1,3 @@
export * from './navigation';
export * from './navigation-utils';
export * from './navigation-drawer';

View File

@ -0,0 +1,136 @@
import classNames from 'classnames';
import type { CSSProperties, HTMLAttributes } from 'react';
import { forwardRef } from 'react';
import { createContext } from 'react';
import { create } from 'zustand';
import type { NavigationProps } from './navigation-utils';
export const NavigationDrawerContext = createContext<true | undefined>(
undefined
);
type NavigationDrawerStore = {
drawerOpen: boolean;
setDrawerOpen: (isOpen: boolean) => void;
};
export const useNavigationDrawer = create<NavigationDrawerStore>((set) => ({
drawerOpen: false,
setDrawerOpen: (isOpen) => {
set({ drawerOpen: isOpen });
},
}));
export const BurgerIcon = ({
variant = 'burger',
className,
}: { variant?: 'burger' | 'close' } & HTMLAttributes<SVGAElement>) => (
<svg
className={classNames('stroke-[1px] transition-transform', className)}
width="16"
height="16"
viewBox="0 0 16 16"
>
<line
x1={0.5}
x2={15.5}
y1={3.5}
y2={3.5}
className={classNames('transition-transform duration-75', {
'rotate-45 translate-y-[4px] origin-[8px_3.5px]': variant === 'close',
})}
/>
<line
x1={0.5}
x2={15.5}
y1={11.5}
y2={11.5}
className={classNames('transition-transform duration-75', {
'rotate-[-45deg] translate-y-[-4px] origin-[8px_11.5px]':
variant === 'close',
})}
/>
</svg>
);
export const NavigationDrawerTrigger = forwardRef<
HTMLButtonElement,
Pick<NavigationProps, 'theme'>
>(({ theme }, ref) => {
const { drawerOpen, setDrawerOpen } = useNavigationDrawer((state) => ({
drawerOpen: state.drawerOpen,
setDrawerOpen: state.setDrawerOpen,
}));
return (
<button
data-testid="button-menu-drawer"
ref={ref}
className={classNames(
'px-2',
`hidden group-[.nav-size-narrow]:block group-[.nav-size-small]:block`,
{
'z-[21]': drawerOpen,
}
)}
onClick={() => {
setDrawerOpen(!drawerOpen);
}}
>
<BurgerIcon
className={classNames({
'stroke-black dark:stroke-white': theme === 'system',
'stroke-black': theme === 'light' || theme === 'yellow',
'stroke-white': theme === 'dark',
'dark:stroke-white': drawerOpen && theme === 'yellow',
})}
variant={drawerOpen ? 'close' : 'burger'}
/>
</button>
);
});
export const NavigationDrawerContent = ({
theme,
children,
style,
}: { style?: CSSProperties } & Pick<NavigationProps, 'theme' | 'children'>) => {
return (
<NavigationDrawerContext.Provider value={true}>
<div
className={classNames(
'drawer-content',
'border-l h-full relative overflow-auto',
'px-4 pb-8 font-alpha',
// text
{
'text-vega-light-300 dark:text-vega-dark-300':
theme === 'system' || theme === 'yellow',
'text-vega-light-300': theme === 'light',
'text-vega-dark-300': theme === 'dark',
},
// border
{
'border-l-vega-light-200 dark:border-l-vega-dark-200':
theme === 'system' || theme === 'yellow',
'border-l-vega-light-200': theme === 'light',
'border-l-vega-dark-200': theme === 'dark',
},
// background
{
'bg-white dark:bg-black': theme === 'system' || theme === 'yellow',
'bg-white': theme === 'light',
'bg-black': theme === 'dark',
}
)}
style={style}
>
<div
data-testid="menu-drawer"
className="flex flex-col gap-2 pr-10 text-lg"
>
{children}
</div>
</div>
</NavigationDrawerContext.Provider>
);
};

View File

@ -0,0 +1,97 @@
import type { ReactNode } from 'react';
import { createContext } from 'react';
export type NavigationProps = {
/**
* The display name of the dApp, e.g. "Console", "Explorer"
*/
appName: string;
/**
* URL pointing to the home page.
*/
homeLink?: string;
/**
* The theme of the navigation.
* @default "system"
*/
theme: 'system' | 'light' | 'dark' | 'yellow';
/**
* The navigation items (links, buttons, dropdowns, etc.)
*/
children?: ReactNode;
/**
* The navigation actions (e.g. theme switcher, wallet connector)
*/
actions?: ReactNode;
/**
* Size variants breakpoints
*/
breakpoints?: [number, number];
fullWidth?: boolean;
onResize?: (width: number, navigationElement: HTMLElement) => void;
};
export enum NavigationBreakpoint {
/**
* Full width navigation
* `width > breakpoints[1]`
*/
Full = 'nav-size-full',
/**
* Narrow variant
* `width > breakpoints[0] && width <= breakpoints[1]`
*/
Narrow = 'nav-size-narrow',
/**
* Small variant
* `width <= breakpoints[0]`
*/
Small = 'nav-size-small',
}
export type NavigationElementProps = {
hide?: NavigationBreakpoint[] | true;
hideInDrawer?: boolean;
};
export const NavigationContext = createContext<{
theme: NavigationProps['theme'];
}>({ theme: 'system' });
export const setSizeVariantClasses = (
breakpoints: [number, number],
currentWidth: number,
target: HTMLElement
) => {
if (
currentWidth <= breakpoints[0] &&
!target.classList.contains(NavigationBreakpoint.Small)
) {
target.classList.remove(
NavigationBreakpoint.Full,
NavigationBreakpoint.Narrow
);
target.classList.add(NavigationBreakpoint.Small);
}
if (
currentWidth > breakpoints[0] &&
currentWidth <= breakpoints[1] &&
!target.classList.contains(NavigationBreakpoint.Narrow)
) {
target.classList.remove(
NavigationBreakpoint.Full,
NavigationBreakpoint.Small
);
target.classList.add(NavigationBreakpoint.Narrow);
}
if (
currentWidth > breakpoints[1] &&
!target.classList.contains(NavigationBreakpoint.Full)
) {
target.classList.remove(
NavigationBreakpoint.Narrow,
NavigationBreakpoint.Small
);
target.classList.add(NavigationBreakpoint.Full);
}
};

View File

@ -0,0 +1,251 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { MemoryRouter, Route, Routes, useMatch } from 'react-router-dom';
import {
Button,
ExternalLink,
Icon,
NavigationBreakpoint,
ThemeSwitcher,
} from '..';
import {
Navigation,
NavigationContent,
NavigationItem,
NavigationLink,
NavigationList,
NavigationTrigger,
} from './navigation';
export default {
title: 'Navigation',
component: Navigation,
} as ComponentMeta<typeof Navigation>;
const Template: ComponentStory<typeof Navigation> = ({
children,
...props
}) => {
const nav = <Navigation {...props}>{children}</Navigation>;
return (
<MemoryRouter>
<div className="h-[300px]">
{nav}
<div className="mt-2">
<Routes>
<Route path="transactions" element={<h1>Transactions</h1>} />
<Route path="blocks" element={<h1>Blocks</h1>} />
<Route path="markets/all" element={<h1>All markets</h1>} />
</Routes>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quis
pariatur a nemo quos sed! Voluptas itaque voluptate dolores minima.
Iste laudantium perspiciatis accusamus facere eius repudiandae sit
odio saepe nisi.
</p>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea facere
ab at incidunt numquam nemo natus eos iure, iste tenetur illo
dolores, commodi magni quam dolor totam quae velit eaque.
</p>
<p>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quaerat
minus perspiciatis quas temporibus odit? Enim ipsam nisi amet
molestias magnam esse blanditiis aperiam sapiente quaerat. Veniam
unde magnam exercitationem distinctio.
</p>
</div>
</div>
</MemoryRouter>
);
};
export const Default = Template.bind({});
Default.args = {
appName: '',
};
const ExplorerNav = () => {
const isOnMarkets = useMatch('markets/*');
return (
<>
<NavigationList>
<NavigationItem hide={[NavigationBreakpoint.Small]}>
{/* <NetworkSwitcher currentNetwork={Networks.CUSTOM} /> */}
<Button size="xs">Network Switcher</Button>
</NavigationItem>
</NavigationList>
<NavigationList
hide={[NavigationBreakpoint.Narrow, NavigationBreakpoint.Small]}
>
<NavigationItem>
<NavigationLink to="transactions">Transactions</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink to="blocks">Blocks</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationTrigger isActive={Boolean(isOnMarkets)}>
Markets
</NavigationTrigger>
<NavigationContent>
<NavigationList>
<NavigationItem>
<NavigationLink to="markets/all">All markets</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink to="markets/proposed">
Proposed markets
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink to="markets/failed">
Failed markets
</NavigationLink>
</NavigationItem>
</NavigationList>
</NavigationContent>
</NavigationItem>
<NavigationItem>
<NavigationLink to="validators">Validators</NavigationLink>
</NavigationItem>
</NavigationList>
</>
);
};
export const Explorer = Template.bind({});
Explorer.args = {
appName: 'Explorer',
theme: 'system',
children: <ExplorerNav />,
actions: (
<>
<ThemeSwitcher />
{/* JUST A PLACEHOLDER */}
<div className="border rounded px-2 py-1 text-xs font-alpha w-60 flex items-center gap-1">
<Icon
name="search"
size={3}
className="text-vega-light-200 dark:text-vega-dark-200"
/>
<input
className="w-full bg-transparent outline-none"
placeholder="Enter block number or transaction hash"
/>
</div>
</>
),
};
const ConsoleNav = () => {
return (
<>
<NavigationList>
<NavigationItem>
<Button size="xs">Network Switcher</Button>
</NavigationItem>
</NavigationList>
<NavigationList
hide={[NavigationBreakpoint.Narrow, NavigationBreakpoint.Small]}
>
<NavigationItem>
<NavigationLink to="markets">Markets</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink to="trading">Trading</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink to="portfolio">Portfolio</NavigationLink>
</NavigationItem>
<NavigationItem>
<ExternalLink>
<span className="flex items-center gap-2">
<span>Governance</span> <Icon name="arrow-top-right" size={3} />
</span>
</ExternalLink>
</NavigationItem>
</NavigationList>
</>
);
};
export const Console = Template.bind({});
Console.args = {
appName: 'Console',
theme: 'system',
children: <ConsoleNav />,
breakpoints: [478, 770],
actions: <ThemeSwitcher />,
};
const GovernanceNav = () => {
const isOnToken = useMatch('token/*');
return (
<>
<NavigationList>
<NavigationItem hide={[NavigationBreakpoint.Small]}>
<Button size="xs">Network Switcher</Button>
</NavigationItem>
</NavigationList>
<NavigationList
hide={[NavigationBreakpoint.Narrow, NavigationBreakpoint.Small]}
>
<NavigationItem>
<NavigationLink to="proposals">Proposals</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink to="validators">Validators</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink to="rewards">Rewards</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationTrigger isActive={Boolean(isOnToken)}>
Token
</NavigationTrigger>
<NavigationContent>
<NavigationList>
<NavigationItem>
<NavigationLink to="token/index">Token</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink to="token/tranches">
Supply & Vesting
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink to="token/withdraw">Withdraw</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink to="token/redeem">Redeem</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink to="token/associate">Associate</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink to="token/disassociate">
Disassociate
</NavigationLink>
</NavigationItem>
</NavigationList>
</NavigationContent>
</NavigationItem>
</NavigationList>
</>
);
};
export const Governance = Template.bind({});
Governance.args = {
appName: 'Governance',
theme: 'dark',
children: <GovernanceNav />,
actions: (
<Button size="sm">
<span className="flex items-center gap-2">
<span>Connect</span> <Icon name="arrow-right" size={3} />
</span>
</Button>
),
};

View File

@ -0,0 +1,388 @@
import classNames from 'classnames';
import type { ComponentProps, ReactNode } from 'react';
import { useLayoutEffect } from 'react';
import { useContext } from 'react';
import { useRef } from 'react';
import { VegaLogo } from '../vega-logo';
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
import { Icon } from '../icon';
import { Drawer } from '../drawer';
import { NavLink } from 'react-router-dom';
import type {
NavigationElementProps,
NavigationProps,
} from './navigation-utils';
import { setSizeVariantClasses } from './navigation-utils';
import { NavigationBreakpoint, NavigationContext } from './navigation-utils';
import {
NavigationDrawerTrigger,
NavigationDrawerContext,
useNavigationDrawer,
NavigationDrawerContent,
} from './navigation-drawer';
const Logo = ({
appName,
homeLink,
}: Pick<NavigationProps, 'theme' | 'appName' | 'homeLink'>) => {
const { theme } = useContext(NavigationContext);
return (
<div className="flex h-full gap-4 items-center">
{homeLink ? (
<NavLink to={homeLink}>
<VegaLogo className="h-4 group-[.nav-size-small]:h-3" />
</NavLink>
) : (
<VegaLogo className="h-4 group-[.nav-size-small]:h-3" />
)}
{appName && (
<span
data-testid="nav-app-name"
className={classNames(
'group-[.nav-size-small]:text-sm',
'font-alpha calt lowercase text-xl tracking-[1px] whitespace-nowrap leading-1',
'border-l pl-4',
{
'border-l-vega-light-200 dark:border-l-vega-dark-200':
theme === 'system',
'border-l-vega-light-200': theme === 'light',
'border-l-vega-dark-200': theme === 'dark' || theme === 'yellow',
}
)}
>
{appName}
</span>
)}
</div>
);
};
const determineIfHidden = ({ hide, hideInDrawer }: NavigationElementProps) => [
{
'[.nav-size-full_.navbar_&]:hidden':
Array.isArray(hide) && hide?.includes(NavigationBreakpoint.Full),
'[.nav-size-narrow_.navbar_&]:hidden':
Array.isArray(hide) && hide?.includes(NavigationBreakpoint.Narrow),
'[.nav-size-small_.navbar_&]:hidden':
Array.isArray(hide) && hide?.includes(NavigationBreakpoint.Small),
'[.drawer-content_&]:hidden': hideInDrawer,
},
];
const Spacer = () => <div className="w-full" aria-hidden="true"></div>;
export const NavigationItem = ({
children,
className,
hide,
hideInDrawer,
...props
}: ComponentProps<typeof NavigationMenu.Item> & NavigationElementProps) => {
const insideDrawer = useContext(NavigationDrawerContext);
return (
<NavigationMenu.Item
className={classNames(
!insideDrawer && ['h-12 [.navigation-content_&]:h-8 flex items-center'],
determineIfHidden({ hide, hideInDrawer }),
className
)}
{...props}
>
{children}
</NavigationMenu.Item>
);
};
export const NavigationList = ({
children,
className,
hide,
hideInDrawer,
...props
}: ComponentProps<typeof NavigationMenu.List> & NavigationElementProps) => {
const insideDrawer = useContext(NavigationDrawerContext);
if (!insideDrawer && hide === true) {
return null;
}
return (
<NavigationMenu.List
className={classNames(
'flex gap-4 items-center',
'[.navigation-content_&]:flex-col [.navigation-content_&]:items-start',
'[.drawer-content_&]:flex-col [.drawer-content_&]:items-start [.drawer-content_&]:gap-6 [.drawer-content_&]:mt-2',
'[.drawer-content_.navigation-content_&]:mt-6',
determineIfHidden({ hide, hideInDrawer }),
className
)}
{...props}
>
{children}
</NavigationMenu.List>
);
};
export const NavigationTrigger = ({
children,
className,
isActive = false,
hide,
hideInDrawer,
...props
}: ComponentProps<typeof NavigationMenu.Trigger> & {
isActive?: boolean;
} & NavigationElementProps) => {
const { theme } = useContext(NavigationContext);
const insideDrawer = useContext(NavigationDrawerContext);
return (
<NavigationMenu.Trigger
className={classNames(
'h-12 [.drawer-content_&]:h-min flex items-center relative gap-2',
{
'text-black dark:text-white': isActive && theme === 'system',
'text-black': isActive && theme === 'light',
'text-white': isActive && theme === 'dark',
'text-black dark:[.drawer-content_&]:text-white':
isActive && theme === 'yellow',
},
determineIfHidden({ hide, hideInDrawer }),
className
)}
onPointerMove={(e) => e.preventDefault()} // disables hover
onPointerLeave={(e) => e.preventDefault()} // disables hover
disabled={insideDrawer}
{...props}
>
<span>{children}</span>
<span className="rotate-90 group-data-open/drawer:hidden">
<Icon name="arrow-right" size={3} />
</span>
<div
aria-hidden="true"
className={classNames(
'absolute bottom-0 left-0 w-full h-[2px] [.navigation-content_&]:hidden [.drawer-content_&]:hidden',
{ hidden: !isActive },
{
'bg-vega-yellow-550 dark:bg-vega-yellow-500': theme === 'system',
'bg-vega-yellow-550': theme === 'light',
'bg-vega-yellow-500': theme === 'dark',
'bg-black': theme === 'yellow',
}
)}
></div>
</NavigationMenu.Trigger>
);
};
export const NavigationContent = ({
children,
className,
...props
}: ComponentProps<typeof NavigationMenu.Content>) => {
const { theme } = useContext(NavigationContext);
const insideDrawer = useContext(NavigationDrawerContext);
const content = (
<NavigationMenu.Content
onPointerLeave={(e) => e.preventDefault()} // disables hover
asChild
{...props}
>
<div
className={classNames(
'navigation-content',
'absolute z-20 top-12 w-max',
'p-2 mt-1',
'text-vega-light-300 dark:text-vega-dark-300',
'border rounded border-vega-light-200 dark:border-vega-dark-200',
'shadow-[8px_8px_16px_0_rgba(0,0,0,0.4)]',
{
'bg-white dark:bg-black': theme === 'system' || theme === 'yellow',
'bg-white': theme === 'light',
'bg-black': theme === 'dark',
}
)}
>
{children}
</div>
</NavigationMenu.Content>
);
const list = (
<div className={classNames('navigation-content', 'border-none pl-4')}>
{children}
</div>
);
return insideDrawer ? list : content;
};
export const NavigationLink = ({
children,
to,
...props
}: ComponentProps<typeof NavLink>) => {
const { theme } = useContext(NavigationContext);
const setDrawerOpen = useNavigationDrawer((state) => state.setDrawerOpen);
return (
<NavigationMenu.Link
asChild
onClick={() => {
setDrawerOpen(false);
}}
>
<NavLink
to={to}
className={classNames(
'h-12 [.navigation-content_&]:h-min [.drawer-content_&]:h-min flex items-center relative'
)}
{...props}
>
{({ isActive }) => (
<>
<span
className={classNames({
'text-black dark:text-white': isActive && theme === 'system',
'text-black': isActive && theme === 'light',
'text-white': isActive && theme === 'dark',
'text-black dark:[.navigation-content_&]:text-white dark:[.drawer-content_&]:text-white':
isActive && theme === 'yellow',
})}
>
{children as ReactNode}
</span>
<div
aria-hidden="true"
className={classNames(
'absolute bottom-0 left-0 w-full h-[2px] [.navigation-content_&]:hidden [.drawer-content_&]:hidden',
{ hidden: !isActive },
{
'bg-vega-yellow-550 dark:bg-vega-yellow-500':
theme === 'system',
'bg-vega-yellow-550': theme === 'light',
'bg-vega-yellow-500': theme === 'dark',
'bg-black': theme === 'yellow',
}
)}
></div>
</>
)}
</NavLink>
</NavigationMenu.Link>
);
};
export const Navigation = ({
appName,
homeLink = '/',
children,
actions,
theme = 'system',
breakpoints = [478, 1000],
onResize,
}: NavigationProps) => {
const navigationRef = useRef<HTMLElement>(null);
const actionsRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (!navigationRef.current) return;
const target = navigationRef.current;
const currentWidth = Math.min(
target.getBoundingClientRect().width,
window.innerWidth
);
setSizeVariantClasses(breakpoints, currentWidth, target);
onResize?.(currentWidth, target);
const handler = () => {
const currentWidth = Math.min(
target.getBoundingClientRect().width,
window.innerWidth
);
setSizeVariantClasses(breakpoints, currentWidth, target);
onResize?.(currentWidth, target);
};
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
};
}, [breakpoints, onResize]);
const { drawerOpen, setDrawerOpen } = useNavigationDrawer((state) => ({
drawerOpen: state.drawerOpen,
setDrawerOpen: state.setDrawerOpen,
}));
return (
<NavigationContext.Provider value={{ theme }}>
<NavigationMenu.Root
ref={navigationRef}
id="navigation"
className={classNames(
'h-12',
'group flex gap-4 items-center',
'border-b px-3 relative',
// text
{
'text-black dark:text-white': theme === 'system',
'text-black': theme === 'light' || theme === 'yellow',
'text-white': theme === 'dark',
},
// border
{
'border-b-vega-light-200 dark:border-b-vega-dark-200':
theme === 'system',
'border-b-vega-light-200': theme === 'light',
'border-b-vega-dark-200': theme === 'dark',
'border-b-black': theme === 'yellow',
},
// background
{
'bg-white dark:bg-black': theme === 'system',
'bg-white': theme === 'light',
'bg-black': theme === 'dark',
'bg-vega-yellow-500': theme === 'yellow',
}
)}
data-testid="navigation"
>
<Logo appName={appName} theme={theme} homeLink={homeLink} />
<div
className={classNames(
'navbar',
'flex gap-4 h-12 items-center font-alpha text-lg calt',
{
'text-vega-light-300 dark:text-vega-dark-300': theme === 'system',
'text-vega-light-300': theme === 'light',
'text-vega-dark-300': theme === 'dark',
'text-vega-dark-200': theme === 'yellow',
}
)}
>
{children}
</div>
<Spacer />
{(actions || children) && (
<div ref={actionsRef} className="flex gap-2 items-center">
{actions}
<Drawer
open={drawerOpen}
onChange={(isOpen) => setDrawerOpen(isOpen)}
trigger={<NavigationDrawerTrigger theme={theme} />}
container={actionsRef.current}
>
<NavigationDrawerContent
theme={theme}
style={{
paddingTop: `${
navigationRef?.current?.getBoundingClientRect().bottom || 0
}px`,
}}
>
{children}
</NavigationDrawerContent>
</Drawer>
</div>
)}
</NavigationMenu.Root>
</NavigationContext.Provider>
);
};

View File

@ -1,5 +1,9 @@
export const SunIcon = () => (
<svg viewBox="0 0 45 45" className="w-8 h-8">
type IconProps = {
className?: string;
};
export const SunIcon = ({ className }: IconProps) => (
<svg viewBox="0 0 45 45" className={className || 'w-8 h-8'}>
<path
d="M22.5 27.79a5.29 5.29 0 1 0 0-10.58 5.29 5.29 0 0 0 0 10.58Z"
fill="currentColor"
@ -13,8 +17,8 @@ export const SunIcon = () => (
</svg>
);
export const MoonIcon = () => (
<svg viewBox="0 0 45 45" className="w-8 h-8">
export const MoonIcon = ({ className }: IconProps) => (
<svg viewBox="0 0 45 45" className={className || 'w-8 h-8'}>
<path
d="M28.75 11.69A12.39 12.39 0 0 0 22.5 10a12.5 12.5 0 1 0 0 25c2.196 0 4.353-.583 6.25-1.69A12.46 12.46 0 0 0 35 22.5a12.46 12.46 0 0 0-6.25-10.81Zm-6.25 22a11.21 11.21 0 0 1-11.2-11.2 11.21 11.21 0 0 1 11.2-11.2c1.246 0 2.484.209 3.66.62a13.861 13.861 0 0 0-5 10.58 13.861 13.861 0 0 0 5 10.58 11.078 11.078 0 0 1-3.66.63v-.01Z"
fill="currentColor"

View File

@ -1,13 +1,16 @@
import { t } from '@vegaprotocol/i18n';
export const VegaLogo = () => {
type VegaLogoProps = {
className?: string;
};
export const VegaLogo = ({ className }: VegaLogoProps) => {
return (
<svg
aria-label={t('Vega logo')}
width="111"
height="24"
className={className || 'h-6'}
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 111 24"
>
<path
fill="currentColor"

View File

@ -25,6 +25,7 @@
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-icons": "^1.1.1",
"@radix-ui/react-navigation-menu": "^1.1.1",
"@radix-ui/react-popover": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.1",
"@radix-ui/react-select": "^1.2.0",

View File

@ -4222,6 +4222,27 @@
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-navigation-menu@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.1.tgz#84f24d90e6448a0c83d3431c6eefbea73dc7522e"
integrity sha512-Khgf+LwqYfUpbFAHcFPDMj6ZrWxnwCgC96liLYwE48x9YJbXGlutOWzZaSzrgl82xS+PwoPLQunfDe/i4ZITRA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-collection" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-direction" "1.0.0"
"@radix-ui/react-dismissable-layer" "1.0.2"
"@radix-ui/react-id" "1.0.0"
"@radix-ui/react-presence" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-controllable-state" "1.0.0"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-use-previous" "1.0.0"
"@radix-ui/react-visually-hidden" "1.0.1"
"@radix-ui/react-popover@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.3.tgz#65ae2ee1fca2d7fd750308549eb8e0857c6160fe"