Bounties - list layout improvements (#4644)

This commit is contained in:
Ewa Kowalska 2021-02-11 11:03:46 +01:00 committed by GitHub
parent a180bbc4ff
commit a580ac4d5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 238 additions and 176 deletions

View File

@ -101,6 +101,7 @@
"Title too long": "Title too long",
"Unassign curator": "Unassign curator",
"Unassign curator under voting": "Unassign curator under voting",
"Update overdue": "Update overdue",
"Voting": "Voting",
"Voting results": "Voting results",
"Waiting for Bounty Funding": "Waiting for Bounty Funding",

View File

@ -21,7 +21,7 @@ import { keyring } from '@polkadot/ui-keyring';
import { defaultBountyUpdatePeriod, mockBountyHooks } from '../test/hooks/defaults';
import { BountiesPage } from '../test/pages/bountiesPage';
import { BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING } from './BountyInfos';
import { BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING } from './BountyNextActionInfo/BountyActionMessage';
jest.mock('@polkadot/react-hooks/useTreasury', () => ({
useTreasury: () => mockHooks.treasury
@ -100,7 +100,7 @@ describe('Bounties', () => {
bountiesPage.renderOne(bounty, proposals);
await bountiesPage.expectText('Curator proposal under voting');
await bountiesPage.expectVotingDescription('Curator proposal under voting');
});
it('when voting on bounty approval', async () => {
@ -109,7 +109,7 @@ describe('Bounties', () => {
bountiesPage.renderOne(bounty, proposals);
await bountiesPage.expectText('Bounty approval under voting');
await bountiesPage.expectVotingDescription('Bounty approval under voting');
});
it('when simultaneous close and approve motions exist, show approved', async () => {
@ -121,7 +121,7 @@ describe('Bounties', () => {
bountiesPage.renderOne(bounty, proposals);
await bountiesPage.expectText('Bounty approval under voting');
await bountiesPage.expectVotingDescription('Bounty approval under voting');
});
it('when voting on close bounty', async () => {
@ -130,7 +130,7 @@ describe('Bounties', () => {
bountiesPage.renderOne(bounty, proposals);
await bountiesPage.expectText('Bounty rejection under voting');
await bountiesPage.expectVotingDescription('Bounty rejection under voting');
});
it('when voting on unassign curator', async () => {
@ -139,7 +139,7 @@ describe('Bounties', () => {
bountiesPage.renderOne(bounty, proposals);
await bountiesPage.expectText('Curator slash under voting');
await bountiesPage.expectVotingDescription('Curator slash under voting');
});
it('when a motion exists that would fail on execution, show nothing', async () => {
@ -148,14 +148,7 @@ describe('Bounties', () => {
const { findByTestId } = bountiesPage.renderOne(bounty, proposals);
await expect(findByTestId('extendedVotingStatus')).rejects.toThrow();
});
it('when bounty is claimable', async () => {
const bounty = bountyWith({ status: 'PendingPayout' });
const { findByTestId } = bountiesPage.renderOne(bounty);
expect((await findByTestId('extendedActionStatus')).textContent).toEqual('Claimable');
await expect(findByTestId('voting-description')).rejects.toThrow();
});
});
@ -215,7 +208,7 @@ describe('Bounties', () => {
const { findByTestId } = bountiesPage.renderOne(bounty, proposals);
expect((await findByTestId('voting-summary')).textContent).toEqual('Aye 2/4Nay 0/0Voting results');
expect((await findByTestId('voting-summary')).textContent).toEqual('Aye 2/4Nay 0/0Voting');
});
it('is not displayed when not voting', async () => {
@ -457,16 +450,28 @@ describe('Bounties', () => {
bountiesPage.renderOne(bounty);
await bountiesPage.expectText('Warning');
await bountiesPage.expectText('Close deadline');
});
it('warning when update time is overdue', async () => {
const bounty = aBounty({ status: bountyStatusWith(
{
curator: alice,
status: 'Active',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
updateDue: mockBountyHooks.bountyApi.bestNumber!.toNumber()
}) });
bountiesPage.renderOne(bounty);
await bountiesPage.expectText('Update overdue');
});
it('info when waiting for bounty funding', async () => {
const bounty = bountyWith({ status: 'Approved' });
bountiesPage.renderOne(bounty);
await bountiesPage.expectText('Info');
await bountiesPage.expectText('Waiting for Bounty Funding');
});
@ -475,10 +480,17 @@ describe('Bounties', () => {
bountiesPage.renderOne(bounty);
await bountiesPage.expectText('Info');
await bountiesPage.expectText('Waiting for Curator\'s acceptance');
});
it('info when bounty is claimable', async () => {
const bounty = bountyWith({ status: 'PendingPayout' });
bountiesPage.renderOne(bounty);
await bountiesPage.expectText('Waiting for implementer to claim');
});
it('no warning or info when requirements are not met', async () => {
const bounty = aBounty({ status: bountyStatusWith({
curator: alice,
@ -489,11 +501,11 @@ describe('Bounties', () => {
bountiesPage.renderOne(bounty);
await bountiesPage.rendered();
bountiesPage.expectTextAbsent('Warning');
bountiesPage.expectTextAbsent('Close deadline');
bountiesPage.expectTextAbsent('Info');
bountiesPage.expectTextAbsent('Update overdue');
bountiesPage.expectTextAbsent('Waiting for Bounty Funding');
bountiesPage.expectTextAbsent("Waiting for Curator's acceptance");
bountiesPage.expectTextAbsent('Waiting for implementer to claim');
});
});
});

View File

@ -13,8 +13,8 @@ import { ThemeProps } from '@polkadot/react-components/types';
import { BlockToTime, FormatBalance } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';
import BountyActionMessage from './BountyNextActionInfo/BountyActionMessage';
import { getProposalToDisplay } from './helpers/extendedStatuses';
import VotersColumn from './Voting/VotersColumn';
import { BountyActions } from './BountyActions';
import BountyExtraActions from './BountyExtraActions';
import BountyInfos from './BountyInfos';
@ -23,6 +23,7 @@ import Curator from './Curator';
import { useBountyStatus } from './hooks';
import { bountyBorderColor, bountyLabelColor } from './theme';
import { useTranslation } from './translate';
import VotersColumn from './VotersColumn';
interface Props {
bestNumber: BlockNumber;
@ -71,12 +72,7 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
<>
<tr className={className}>
<td>
<BountyStatusView
blocksUntilPayout={blocksUntilPayout}
bountyStatus={bountyStatus}
proposals={proposals}
status={status}
/>
<BountyStatusView bountyStatus={bountyStatus}/>
</td>
<td
className='description-column'
@ -109,12 +105,16 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
until={'update'}
/>
: ''}
<BountyActionMessage
bestNumber={bestNumber}
blocksUntilUpdate={blocksUntilUpdate}
status={status}
/>
</td>
<td>
<td className='td-info-action-row'>
<div className='td-row'>
<BountyInfos
beneficiary={beneficiary}
blocksUntilUpdate={blocksUntilUpdate}
proposals={proposals}
status={status}
/>
@ -240,7 +240,10 @@ export default React.memo(styled(Bounty)(({ theme }: ThemeProps) => `
display: inline-flex;
}
& .fast-actions {
& td.fast-actions {
padding-left: 0.2rem;
width: 1%;
.fast-actions-row {
display: flex;
align-items: center;
@ -296,17 +299,24 @@ export default React.memo(styled(Bounty)(({ theme }: ThemeProps) => `
padding: 0 0 1.3rem;
}
& .td-info-action-row {
padding-right: 0;
}
.td-row {
display: flex;
justify-content: space-between;
align-items: center;
& :only-child {
margin-left: auto;
}
}
.bounty-action-row {
display: flex;
justify-content: flex-end;
align-items: center;
margin-left: auto;
& > * + * {
margin-left: 0.6rem;
@ -314,9 +324,8 @@ export default React.memo(styled(Bounty)(({ theme }: ThemeProps) => `
}
.block-to-time {
margin-top: 0.28rem;
font-size: 0.7rem;
line-height: 0.85rem;
line-height: 1.5rem;
color: ${bountyLabelColor[theme.theme]};
}

View File

@ -4,24 +4,22 @@
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { BountyStatus } from '@polkadot/types/interfaces';
import BN from 'bn.js';
import React, { useRef } from 'react';
import styled from 'styled-components';
import { BN_ZERO } from '@polkadot/util';
import { LabelHelp } from '@polkadot/react-components';
import Description from '../Description';
import { proposalNameToDisplay } from '../helpers/extendedStatuses';
import { useTranslation } from '../translate';
interface Props {
blocksUntilPayout?: BN ;
className?: string;
proposals?: DeriveCollectiveProposal[];
proposal: DeriveCollectiveProposal;
status: BountyStatus;
}
function ExtendedStatus ({ blocksUntilPayout, className = '', proposals, status }: Props): React.ReactElement<Props> {
const bestProposalName = proposals ? proposalNameToDisplay(proposals, status) : null;
function VotingDescriptionInfo ({ className, proposal, status }: Props): React.ReactElement<Props> {
const bestProposalName = proposalNameToDisplay(proposal, status);
const { t } = useTranslation();
const votingDescriptions = useRef<Record<string, string>>({
approveBounty: t('Bounty approval under voting'),
@ -32,23 +30,17 @@ function ExtendedStatus ({ blocksUntilPayout, className = '', proposals, status
});
return (
<>
<div
className={className}
data-testid='voting-description'
>
{bestProposalName && votingDescriptions.current[bestProposalName] &&
<Description
className={className}
dataTestId='extendedVotingStatus'
description={votingDescriptions.current[bestProposalName]}
/>
<LabelHelp help={votingDescriptions.current[bestProposalName]}/>
}
{blocksUntilPayout?.lt(BN_ZERO) &&
<Description
className={className}
dataTestId='extendedActionStatus'
description={t<string>('Claimable')}
/>
}
</>
</div>
);
}
export default React.memo(ExtendedStatus);
export default React.memo(styled(VotingDescriptionInfo)`
margin-left: 0.2rem;
`);

View File

@ -4,7 +4,6 @@
import React from 'react';
import styled from 'styled-components';
import Description from '../Description';
import { useTranslation } from '../translate';
interface Props {
@ -15,13 +14,15 @@ function VotingLink ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<div className={className}>
<a href='#/council/motions'>{t<string>('Voting')}</a>
<Description description={t<string>('Go to motions panel')} />
</div>
<a className={className}
href='#/council/motions'>
{t<string>('Voting')}
</a>
);
}
export default React.memo(styled(VotingLink)`
margin: 0 1rem 0 0
line-height: 0.85rem;
font-size: 0.7rem;
text-decoration: underline;
`);

View File

@ -2,23 +2,26 @@
// SPDX-License-Identifier: Apache-2.0
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { BountyStatus } from '@polkadot/types/interfaces';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import VotingDescriptionInfo from '@polkadot/app-bounties/BountyInfos/VotingDescriptionInfo';
import { ThemeProps } from '@polkadot/react-components/types';
import { useMembers } from '@polkadot/react-hooks';
import Description from '../Description';
import { bountyLabelColor } from '../theme';
import { useTranslation } from '../translate';
import VotingLink from './VotingLink';
interface Props {
className?: string;
proposal: DeriveCollectiveProposal;
status: BountyStatus;
}
function VotingSummary ({ className, proposal }: Props): JSX.Element {
function VotingSummary ({ className, proposal, status }: Props): JSX.Element {
const { members } = useMembers();
const { t } = useTranslation();
const ayes = useMemo(() => proposal?.votes?.ayes?.length, [proposal]);
@ -33,10 +36,14 @@ function VotingSummary ({ className, proposal }: Props): JSX.Element {
className={className}
data-testid='voting-summary'
>
<div className='votes'>
<p className='voting-summary-text'><span>{t('Aye')}</span> <b>{ayes}/{threshold}</b></p>
<p className='voting-summary-text'><span>{t('Nay')}</span> <b>{nays}/{nayThreshold}</b></p>
<Description description={t<string>('Voting results')} />
<div className='voting-summary-text'><span>{t('Aye')}</span> <b>{ayes}/{threshold}</b></div>
<div className='voting-summary-text'><span>{t('Nay')}</span> <b>{nays}/{nayThreshold}</b></div>
<div className='link-info'>
<VotingLink/>
<VotingDescriptionInfo
proposal={proposal}
status={status}
/>
</div>
</div>
)}
@ -45,11 +52,9 @@ function VotingSummary ({ className, proposal }: Props): JSX.Element {
}
export default React.memo(styled(VotingSummary)(({ theme }: ThemeProps) => `
margin: 0 1rem 0 0;
.voting-summary-text {
font-size: 0.85rem;
line-height: 0.5rem;
line-height: 1.5rem;
color: ${bountyLabelColor[theme.theme]}
span {
@ -57,4 +62,11 @@ export default React.memo(styled(VotingSummary)(({ theme }: ThemeProps) => `
margin-right: 0.5rem;
}
}
.link-info {
display: flex;
justify-content: space-between;
align-items: center;
line-height: 1.5rem;
}
`));

View File

@ -4,75 +4,42 @@
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { AccountId, BountyStatus } from '@polkadot/types/interfaces';
import BN from 'bn.js';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { AddressSmall } from '@polkadot/react-components';
import { BN_HUNDRED } from '@polkadot/util';
import Description from '../Description';
import { getProposalToDisplay } from '../helpers/extendedStatuses';
import { useBounties } from '../hooks';
import { useTranslation } from '../translate';
import VotingLink from '../Voting/VotingLink';
import BountyInfo from './BountyInfo';
import VotingSummary from './VotingSummary';
interface Props {
beneficiary?: AccountId;
blocksUntilUpdate?: BN;
className?: string;
proposals?: DeriveCollectiveProposal[];
status: BountyStatus;
}
export const BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING = 10;
const BLOCKS_LEFT_TO_SHOW_WARNING = new BN('10000');
function BountyInfos ({ beneficiary, blocksUntilUpdate, className, proposals, status }: Props): JSX.Element {
function BountyInfos ({ beneficiary, proposals, status }: Props): JSX.Element {
const { t } = useTranslation();
const { bountyUpdatePeriod } = useBounties();
const blocksPercentageLeftToShowWarning = bountyUpdatePeriod?.muln(BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING).div(BN_HUNDRED);
const blocksToShowWarning = blocksPercentageLeftToShowWarning ?? BLOCKS_LEFT_TO_SHOW_WARNING;
const proposalToDisplay = useMemo(() => proposals && getProposalToDisplay(proposals, status), [proposals, status]);
return (
<div className={className}>
{proposalToDisplay && <VotingSummary proposal={proposalToDisplay}/>}
{proposalToDisplay && <VotingLink />}
<>
{proposalToDisplay &&
<VotingSummary
proposal={proposalToDisplay}
status={status}
/>
}
{beneficiary && (
<div>
<AddressSmall value={beneficiary} />
<Description description={t<string>('Beneficiary')} />
</div>
)}
{blocksUntilUpdate?.lt(blocksToShowWarning) && (
<BountyInfo
description={t<string>('Close deadline')}
type='warning'
/>
)}
{status.isApproved && (
<BountyInfo
description={t<string>('Waiting for Bounty Funding')}
type='info'
/>
)}
{status.isCuratorProposed && (
<BountyInfo
description={t<string>("Waiting for Curator's acceptance")}
type='info'
/>
)}
</div>
</>
);
}
export default React.memo(styled(BountyInfos)`
display: flex;
justify-content: space-between;
align-items: center;
`);
export default React.memo(BountyInfos);

View File

@ -0,0 +1,71 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BlockNumber, BountyStatus } from '@polkadot/types/interfaces';
import BN from 'bn.js';
import React, { useMemo } from 'react';
import { useBountyStatus } from '@polkadot/app-bounties/hooks';
import { BN_HUNDRED, BN_ZERO } from '@polkadot/util';
import { useBounties } from '../hooks';
import { useTranslation } from '../translate';
import BountyInfo from './BountyInfo';
interface Props {
bestNumber: BlockNumber;
blocksUntilUpdate?: BN;
status: BountyStatus;
}
export const BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING = 10;
const BLOCKS_LEFT_TO_SHOW_WARNING = new BN('10000');
function BountyActionMessage ({ bestNumber, blocksUntilUpdate, status }: Props): JSX.Element {
const { t } = useTranslation();
const { unlockAt } = useBountyStatus(status);
const { bountyUpdatePeriod } = useBounties();
const blocksUntilPayout = useMemo(() => unlockAt?.sub(bestNumber), [bestNumber, unlockAt]);
const blocksPercentageLeftToShowWarning = bountyUpdatePeriod?.muln(BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING).div(BN_HUNDRED);
const blocksToShowWarning = blocksPercentageLeftToShowWarning ?? BLOCKS_LEFT_TO_SHOW_WARNING;
return (
<div>
{blocksUntilUpdate?.lte(BN_ZERO) && (
<BountyInfo
description={t<string>('Update overdue')}
type='warning'
/>
)}
{blocksUntilUpdate?.lt(blocksToShowWarning) && blocksUntilUpdate?.gt(BN_ZERO) && (
<BountyInfo
description={t<string>('Close deadline')}
type='warning'
/>
)}
{status.isApproved && (
<BountyInfo
description={t<string>('Waiting for Bounty Funding')}
type='info'
/>
)}
{status.isCuratorProposed && (
<BountyInfo
description={t<string>("Waiting for Curator's acceptance")}
type='info'
/>
)}
{blocksUntilPayout?.lt(BN_ZERO) &&
<BountyInfo
description={t<string>('Waiting for implementer to claim')}
type='info'
/>
}
</div>
);
}
export default React.memo(BountyActionMessage);

View File

@ -8,7 +8,6 @@ import { Icon } from '@polkadot/react-components';
import { ThemeProps } from '@polkadot/react-components/types';
import { bountyColor, bountyLabelColor, bountySvgColor } from '../theme';
import { useTranslation } from '../translate';
interface Props {
className: '';
@ -17,13 +16,13 @@ interface Props {
}
function BountyInfo ({ className = '', description, type = 'info' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<div className={className}>
<div className='title'>
{ type === 'warning' && <Icon icon={'exclamation-triangle'}/> } {type === 'info' ? t('Info') : t('Warning')}
</div>
{type === 'warning' && (
<div className='info-icon'>
<Icon icon={'exclamation-triangle'}/>
</div>
)}
<div className='description'>
{description}
</div>
@ -32,13 +31,15 @@ function BountyInfo ({ className = '', description, type = 'info' }: Props): Rea
}
export default React.memo(styled(BountyInfo)(({ theme }: ThemeProps) => `
display: flex;
align-items: center;
font-weight: 500;
font-size: 0.857rem;
line-height: 1.714rem;
line-height: 1.5rem;
color: ${bountyColor[theme.theme]};
.title{
margin-bottom: 0.285rem;
.info-icon{
margin-right: 0.2rem;
svg {
color: ${bountySvgColor[theme.theme]};
}
@ -49,5 +50,6 @@ export default React.memo(styled(BountyInfo)(({ theme }: ThemeProps) => `
font-size: 0.714rem;
line-height: 0.864rem;
color: ${bountyLabelColor[theme.theme]};
word-wrap: break-word;
}
`));

View File

@ -1,29 +1,21 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { BountyStatus } from '@polkadot/types/interfaces';
import BN from 'bn.js';
import React, { useRef } from 'react';
import styled from 'styled-components';
import { LabelHelp } from '@polkadot/react-components';
import ExtendedStatus from './Voting/ExtendedStatus';
import { insertSpaceBeforeCapitalLetter } from './helpers';
import { useTranslation } from './translate';
import { HelpMessages, StatusName } from './types';
interface Props {
blocksUntilPayout?: BN
bountyStatus: StatusName;
className?: string;
proposals?: DeriveCollectiveProposal[];
status: BountyStatus;
}
function BountyStatusView ({ blocksUntilPayout, bountyStatus, className = '', proposals, status }: Props): React.ReactElement<Props> {
function BountyStatusView ({ bountyStatus, className = '' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const statusHelpMessages = useRef<HelpMessages>({
@ -36,30 +28,19 @@ function BountyStatusView ({ blocksUntilPayout, bountyStatus, className = '', pr
});
return (
<div className={className}>
<div className='bountyStatus'
data-testid={'bountyStatus'}>
{insertSpaceBeforeCapitalLetter(bountyStatus)}
<LabelHelp
help={statusHelpMessages.current[bountyStatus]}
/>
</div>
{proposals && (
<div>
<ExtendedStatus
blocksUntilPayout={blocksUntilPayout}
proposals={proposals}
status={status}
/>
</div>
)}
<div
className={className}
data-testid={'bountyStatus'}
>
{insertSpaceBeforeCapitalLetter(bountyStatus)}
<LabelHelp
help={statusHelpMessages.current[bountyStatus]}
/>
</div>
);
}
export default React.memo(styled(BountyStatusView)`
.bountyStatus {
display: flex;
align-items: center;
}
display: flex;
align-items: center;
`);

View File

@ -10,9 +10,9 @@ import styled from 'styled-components';
import { AddressSmall, Icon } from '@polkadot/react-components';
import { ThemeProps } from '@polkadot/react-components/types';
import { getProposalToDisplay } from '../helpers/extendedStatuses';
import { bountyLabelColor } from '../theme';
import { useTranslation } from '../translate';
import { getProposalToDisplay } from './helpers/extendedStatuses';
import { bountyLabelColor } from './theme';
import { useTranslation } from './translate';
interface Props {
className?: string;

View File

@ -29,10 +29,8 @@ function bestValidProposalName (bountyProposals: DeriveCollectiveProposal[], sta
return validMethods(status).find((method) => methods.includes(method));
}
export function proposalNameToDisplay (bountyProposals: DeriveCollectiveProposal[], status: BountyStatus): string | undefined {
const bestProposalName = bestValidProposalName(bountyProposals, status);
if (bestProposalName !== 'unassignCurator') { return bestProposalName; }
export function proposalNameToDisplay (bountyProposal: DeriveCollectiveProposal, status: BountyStatus): string | undefined {
if (bountyProposal.proposal.method !== 'unassignCurator') { return bountyProposal.proposal.method; }
return status.isCuratorProposed ? 'unassignCurator' : 'slashCurator';
}

View File

@ -114,17 +114,20 @@ export class BountiesPage {
} as QueueProps;
return render(
<Suspense fallback='...'>
<QueueProvider value={queue}>
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<ApiContext.Provider value={mockApi}>
<Bounties/>
</ApiContext.Provider>
</ThemeProvider>
</MemoryRouter>
</QueueProvider>
</Suspense>
<>
<div id='tooltips'/>
<Suspense fallback='...'>
<QueueProvider value={queue}>
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<ApiContext.Provider value={mockApi}>
<Bounties/>
</ApiContext.Provider>
</ThemeProvider>
</MemoryRouter>
</QueueProvider>
</Suspense>
</>
);
}
@ -305,4 +308,13 @@ export class BountiesPage {
fireEvent.change(beneficiaryAccountInput, { target: { value: beneficiary } });
fireEvent.keyDown(beneficiaryAccountInput, { code: 'Enter', key: 'Enter' });
}
async expectVotingDescription (description: string): Promise<void> {
this.assertRendered();
const votingInfo = await this.findByTestId('voting-description');
const icon = await within(votingInfo).findByTestId('question-circle');
fireEvent.mouseEnter(icon);
expect(await this.findByText(description)).toBeVisible();
}
}

View File

@ -24,7 +24,7 @@ library.add(fas);
function Icon ({ className = '', color = 'normal', icon, isSpinning, onClick, size = '1x', tooltip }: Props): React.ReactElement<Props> {
const extraProps = tooltip
? { 'data-for': tooltip, 'data-tip': true }
? { 'data-for': tooltip, 'data-testid': icon, 'data-tip': true }
: {};
return (

View File

@ -6,9 +6,11 @@ import ReactDOM from 'react-dom';
import ReactTooltip from 'react-tooltip';
import styled from 'styled-components';
const rootElement = typeof document === 'undefined'
? null // This hack is required for server side rendering
: document.getElementById('tooltips');
function rootElement () {
return typeof document === 'undefined'
? null // This hack is required for server side rendering
: document.getElementById('tooltips');
}
interface Props {
className?: string;
@ -33,10 +35,12 @@ function Tooltip ({ className = '', effect = 'solid', offset, place = 'top', tex
);
useEffect((): () => void => {
rootElement && rootElement.appendChild(tooltipContainer);
const root = rootElement();
root && root.appendChild(tooltipContainer);
return (): void => {
rootElement && rootElement.removeChild(tooltipContainer);
root && root.removeChild(tooltipContainer);
};
}, [tooltipContainer]);