Skip to content

Add Tooltip component #360

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 25, 2018
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions app/renderer/components/CopyButton.js
Original file line number Diff line number Diff line change
@@ -2,13 +2,17 @@ import {clipboard} from 'electron';
import React from 'react';
import './CopyButton.scss';

const CopyButton = ({className, value, ...props}) => {
const CopyButton = ({className, onClick, value, ...props}) => {
return (
<button
{...props}
type="button"
className={`${className} CopyButton`}
onClick={() => {
onClick={event => {
if (typeof onClick === 'function') {
onClick(event);
}

clipboard.writeText(value);
}}
/>
114 changes: 114 additions & 0 deletions app/renderer/components/Tooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
import {classNames} from 'react-extras';
import CSSTransition from 'react-transition-group/CSSTransition';
import {Manager, Popper, Reference, placements} from 'react-popper';
import './Tooltip.scss';

class Tooltip extends React.PureComponent {
static propTypes = {
animationDuration: PropTypes.number,
children: PropTypes.node,
content: PropTypes.node,
margin: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
onClose: PropTypes.func,
position: PropTypes.oneOf(placements),
}

static defaultProps = {
animationDuration: 300,
margin: 0,
position: 'top',
}

state = {
isOpen: false,
}

componentWillReceiveProps({content}) {
if (typeof this.updatePopper === 'function' && this.props.content !== content) {
this.updatePopper();
}
}

handleMouseEnter = () => {
this.setState({isOpen: true});
}

handleMouseLeave = () => {
this.setState({isOpen: false});
}

render() {
const {animationDuration, children, content, margin, onClose, position} = this.props;
const {isOpen} = this.state;
const tooltip = (
<Popper placement={position} modifiers={{offset: {offset: `0, ${margin}`}}}>
{({arrowProps, placement, ref, scheduleUpdate, style}) => {
this.updatePopper = scheduleUpdate;

return (
<div ref={ref} className="Tooltip__container" style={style}>
<CSSTransition
classNames="Tooltip"
in={isOpen}
mountOnEnter
timeout={{
enter: 0, // Start animation immediately
exit: animationDuration,
}}
onExited={onClose}
>
<div
className={classNames(
'Tooltip',
`Tooltip--${placement}`,
)}
>
<div className="Tooltip__content">
{content}
</div>
<div
ref={arrowProps.ref}
className="Tooltip__arrow"
style={arrowProps.style}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would've wanted to put these into styled-jsx too, but that'd require converting them to a CSS string. Decided not to bother with it.

/>
</div>
</CSSTransition>
<style jsx>
{`
.Tooltip {
transition-duration: ${animationDuration}ms;
}
`}
</style>
</div>
);
}}
</Popper>
);

return (
<Manager>
<Reference>
{({ref}) => (
<div
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really bothers me that we have to wrap the children in a div since it won't wrap elements with absolute position correctly. I'd really like to be able to pass a ref to children directly. React.forwardRef() only works for components.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's kinda cool. Doesn't seem to work though. See the screenshot below (don't mind the positioning of the elements inside the seed phrase box, it's just for testing):

screen shot 2018-06-21 at 16 28 56

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it's only available in Chrome 65, but Electron 2 is at 61. We'll be able to use this when Electron 3 is stable. #374

Copy link
Contributor Author

@kevva kevva Jun 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the flag to blinkFeatures, but it didn't work regardless, i.e. instead of getting the wrong position it got no position at all (which the screenshot shows).

ref={ref}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
{children}
</div>
)}
</Reference>
{ReactDOM.createPortal(tooltip, document.body)}
</Manager>
);
}
}

export default Tooltip;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add prop-types too? I've been too lazy to do this on the other components, but in hindsight, I should have.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I didn't because no other components had it. Will add!

122 changes: 122 additions & 0 deletions app/renderer/components/Tooltip.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
.Tooltip {
--color: var(--border-color);
--arrow-height: 7px;
--arrow-width: 12px;
opacity: 0;
transition-property: opacity, transform;
transition-timing-function: ease-out;

&__target {
overflow: hidden;
}

&__container {
z-index: 1100;
}

&__content {
background-color: var(--color);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.1);
border-radius: 4px;
font-size: 13px;
padding: 8px 10px;
text-align: center;
}

&__arrow {
display: block;
height: var(--arrow-height);
position: absolute;
width: var(--arrow-width);

&::before {
border-color: transparent;
border-style: solid;
content: '';
position: absolute;
}
}

&--top {
padding-bottom: var(--arrow-height);
transform: translate3d(0, -10%, 0);

.Tooltip__arrow {
bottom: 0;
left: 0;

&::before {
border-top-color: var(--color);
border-width: var(--arrow-height) calc(var(--arrow-width) / 2) 0;
filter:
drop-shadow(0 calc(var(--arrow-height) / 2) 2px rgba(0, 0, 0, 0.1))
drop-shadow(0 calc(var(--arrow-height) / 2) 1px rgba(0, 0, 0, 0.1));
top: 0;
}
}
}

&--right {
padding-left: var(--arrow-height);
transform: translate3d(5%, 0, 0);

.Tooltip__arrow {
height: var(--arrow-width);
left: 0;
width: var(--arrow-height);

&::before {
border-right-color: var(--color);
border-width: calc(var(--arrow-width) / 2) var(--arrow-height) calc(var(--arrow-width) / 2) 0;
filter:
drop-shadow(calc(var(--arrow-height) / 2 * -1) 0 2px rgba(0, 0, 0, 0.1))
drop-shadow(calc(var(--arrow-height) / 2 * -1) 0 1px rgba(0, 0, 0, 0.1));
right: 0;
}
}
}

&--bottom {
padding-top: var(--arrow-height);
transform: translate3d(0, 10%, 0);

.Tooltip__arrow {
left: 0;
top: 0;

&::before {
border-bottom-color: var(--color);
border-width: 0 calc(var(--arrow-width) / 2) var(--arrow-height);
filter:
drop-shadow(0 calc(var(--arrow-height) / 2 * -1) 2px rgba(0, 0, 0, 0.1))
drop-shadow(0 calc(var(--arrow-height) / 2 * -1) 1px rgba(0, 0, 0, 0.1));
bottom: 0;
}
}
}

&--left {
padding-right: var(--arrow-height);
transform: translate3d(-5%, 0, 0);

.Tooltip__arrow {
height: var(--arrow-width);
right: 0;
width: var(--arrow-height);

&::before {
border-left-color: var(--color);
border-width: calc(var(--arrow-width) / 2) 0 calc(var(--arrow-width) / 2) var(--arrow-height);
filter:
drop-shadow(calc(var(--arrow-height) / 2) 0 2px rgba(0, 0, 0, 0.1))
drop-shadow(calc(var(--arrow-height) / 2) 0 1px rgba(0, 0, 0, 0.1));
left: 0;
}
}
}

&-enter-done {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
25 changes: 16 additions & 9 deletions app/renderer/views/CreatePortfolio/CreatePortfolio.scss
Original file line number Diff line number Diff line change
@@ -2,26 +2,33 @@

.CreatePortfolio {
.generated-seed-phrase-container {
align-items: center;
background-color: rgba(248, 7, 89, 0.05);
border: 1px solid var(--error-color);
color: var(--error-color);
display: flex;
border-radius: 4px;
position: relative;
padding: 10px 60px;
padding: 10px 0;

.button {
display: flex;
flex-basis: 60px;
padding: 0 12px;
}

.ReloadButton {
@include center-vertically;
left: 12px;
.button--reload {
justify-content: flex-start;
}

.CopyButton {
@include center-vertically;
right: 12px;
.button--copy {
justify-content: flex-end;
}

.seed-phrase {
margin: 0 auto;
display: flex;
flex: 1;
font-size: 17px;
justify-content: center;
}
}

38 changes: 31 additions & 7 deletions app/renderer/views/CreatePortfolio/Step2.js
Original file line number Diff line number Diff line change
@@ -5,12 +5,15 @@ import ReloadButton from 'components/ReloadButton';
import CopyButton from 'components/CopyButton';
Copy link
Contributor Author

@kevva kevva Jun 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to rewrite this file to use flex inside generated-seed-phrase-container because we currently can't use absolutely positioned targets for our tooltips. flex is nicer than using position: absolute anyway :).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better! No idea why I didn't use flexbox in the first place...

import WrapWidth from 'components/WrapWidth';
import ExternalLink from 'components/ExternalLink';
import Tooltip from 'components/Tooltip';
import container from 'containers/CreatePortfolio';
import {withState} from 'containers/SuperContainer';
import './CreatePortfolio.scss';

const CreatePortfolioStep2 = () => {
const CreatePortfolioStep2 = ({setState, ...props}) => {
// TODO(sindresorhus): Fill in the link to security best practices

const {isCopied} = props.state;
const {state} = container;

return (
@@ -19,11 +22,32 @@ const CreatePortfolioStep2 = () => {
<h1>Seed Phrase for Your Portfolio</h1>
<div className="form-group" style={{width: '460px', marginTop: '20px'}}>
<div className="generated-seed-phrase-container">
<ReloadButton onClick={container.generateSeedPhrase}/>
<WrapWidth wordsPerLine={6} className="seed-phrase">
{state.generatedSeedPhrase}
</WrapWidth>
<CopyButton value={state.generatedSeedPhrase}/>
<div className="button button--reload">
<ReloadButton onClick={() => {
container.generateSeedPhrase();
setState({isCopied: false});
}}/>
</div>
<div className="seed-phrase">
<WrapWidth wordsPerLine={6}>
{state.generatedSeedPhrase}
</WrapWidth>
</div>
<div className="button button--copy">
<Tooltip
content={isCopied ? 'Copied' : 'Copy'}
onClose={() => {
setState({isCopied: false});
}}
>
<CopyButton
value={state.generatedSeedPhrase}
onClick={() => {
setState({isCopied: true});
}}
/>
</Tooltip>
</div>
</div>
<div className="warning-box">
<img className="icon" src="/assets/warning-icon.svg" width="30" height="30"/>
@@ -44,4 +68,4 @@ const CreatePortfolioStep2 = () => {
);
};

export default CreatePortfolioStep2;
export default withState(CreatePortfolioStep2, {isCopied: false});
26 changes: 22 additions & 4 deletions app/renderer/views/Dashboard/DepositModal.js
Original file line number Diff line number Diff line change
@@ -4,13 +4,31 @@ import Modal from 'components/Modal';
import Button from 'components/Button';
import CopyButton from 'components/CopyButton';
import Input from 'components/Input';
import Tooltip from 'components/Tooltip';
import dashboardContainer from 'containers/Dashboard';
import {withState} from 'containers/SuperContainer';
import './DepositModal.scss';

const CopyIconButton = props => (
<CopyButton {...props} value={dashboardContainer.activeCurrency.address}>
<img src="/assets/copy-icon.svg"/>
</CopyButton>
const CopyIconButton = withState(
({setState, state, ...props}) => (
<Tooltip
content={state.isCopied ? 'Copied' : 'Copy'}
onClose={() => {
setState({isCopied: false});
}}
>
<CopyButton
{...props}
value={dashboardContainer.activeCurrency.address}
onClick={() => {
setState({isCopied: true});
}}
>
<img src="/assets/copy-icon.svg"/>
</CopyButton>
</Tooltip>
),
{isCopied: false}
);

class DepositModal extends React.Component {
Loading