Add tooltips capabilities to buttons (#5562)

Add tooltips for buttons on ActionBar if text not visible
This commit is contained in:
Nicolas Gotchac 2017-05-10 16:19:01 +02:00 committed by Jaco Greeff
parent 3e86b2e666
commit eff4cde738
6 changed files with 244 additions and 22 deletions

View File

@ -14,7 +14,9 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { isEqual } from 'lodash';
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar';
import { nodeOrStringProptype } from '~/util/proptypes';
@ -22,6 +24,9 @@ import { nodeOrStringProptype } from '~/util/proptypes';
import styles from './actionbar.css';
export default class Actionbar extends Component {
buttons = {};
buttonsTooltip = {};
static propTypes = {
title: nodeOrStringProptype(),
buttons: PropTypes.array,
@ -29,6 +34,30 @@ export default class Actionbar extends Component {
className: PropTypes.string
};
static defaultProps = {
buttons: []
};
state = {
buttons: []
};
componentWillMount () {
this.setButtons(this.props);
window.addEventListener('resize', this.checkButtonsTooltip);
}
componentWillUnmount () {
window.removeEventListener('resize', this.checkButtonsTooltip);
}
componentWillReceiveProps (nextProps) {
if (!isEqual(this.props.buttons, nextProps.buttons)) {
this.setButtons(nextProps);
}
}
render () {
const { children, className } = this.props;
const classes = `${styles.actionbar} ${className}`;
@ -43,9 +72,9 @@ export default class Actionbar extends Component {
}
renderButtons () {
const { buttons } = this.props;
const { buttons } = this.state;
if (!buttons || !buttons.length) {
if (buttons.length === 0) {
return null;
}
@ -65,4 +94,109 @@ export default class Actionbar extends Component {
</h3>
);
}
checkButtonsTooltip = () => {
const buttonsTooltip = Object.keys(this.buttons)
.reduce((buttonsTooltip, index) => {
buttonsTooltip[index] = this.checkButtonTooltip(this.buttons[index]);
return buttonsTooltip;
}, {});
if (isEqual(buttonsTooltip, this.buttonsTooltip)) {
return;
}
this.buttonsTooltip = buttonsTooltip;
this.setButtons(this.props);
}
checkButtonTooltip = (button) => {
const { icon, text } = button;
const iconBoundings = icon.getBoundingClientRect();
const textBoundings = text.getBoundingClientRect();
// Visible if the bottom of the text is above the bottom of the
// button (text is v-aligned on top)
const isTextVisible = textBoundings.top + textBoundings.height < iconBoundings.top + iconBoundings.height;
return !isTextVisible;
}
/**
* Return the icon and text nodes of a Button
* (and SVG/IMG for the icon next to a span node)
*/
getIconAndTextNodes (element) {
if (!element || !element.children || element.children.length === 0) {
return null;
}
const children = Array.slice(element.children);
const text = children.find((child) => child.nodeName.toLowerCase() === 'span');
const icon = children.find((child) => {
const nodeName = child.nodeName.toLowerCase();
return nodeName === 'svg' || nodeName === 'img';
});
if (icon && text) {
return { icon, text };
}
const result = children
.map((child) => {
return this.getIconAndTextNodes(child);
})
.filter((result) => result);
return result.length > 0
? result[0]
: null;
}
/**
* Add tooltip to all Buttons
*/
patchButton (element, extraProps) {
if (element.type.displayName !== 'Button') {
if (!element.props.children) {
return element;
}
const children = this.patchButton(element.props.children);
return React.cloneElement(element, {}, children);
}
return React.cloneElement(element, extraProps);
}
setButtons (props) {
const buttons = props.buttons
.filter((button) => button)
.map((button, index) => {
const ref = this.setButtonRef.bind(this, index);
const showTooltip = this.buttonsTooltip[index];
return this.patchButton(button, { tooltip: showTooltip, ref });
});
this.setState({ buttons });
}
setButtonRef = (index, element) => {
const node = ReactDOM.findDOMNode(element);
const iconAndText = this.getIconAndTextNodes(node);
if (!iconAndText) {
return;
}
if (!this.buttons[index]) {
this.buttonsTooltip[index] = this.checkButtonTooltip(iconAndText);
this.setButtons(this.props);
}
this.buttons[index] = iconAndText;
};
}

View File

@ -0,0 +1,20 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.tooltip {
text-transform: uppercase;
}

View File

@ -15,10 +15,15 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import ReactTooltip from 'react-tooltip';
import { FlatButton } from 'material-ui';
import { nodeOrStringProptype } from '~/util/proptypes';
import styles from './button.css';
let id = 0;
export default class Button extends Component {
static propTypes = {
backgroundColor: PropTypes.string,
@ -27,26 +32,53 @@ export default class Button extends Component {
icon: PropTypes.node,
label: nodeOrStringProptype(),
onClick: PropTypes.func,
primary: PropTypes.bool
}
primary: PropTypes.bool,
tooltip: PropTypes.bool
};
static defaultProps = {
primary: true
primary: true,
tooltip: false
};
componentWillMount () {
this.id = id++;
}
render () {
const { className, backgroundColor, disabled, icon, label, primary, onClick } = this.props;
return (
const { className, backgroundColor, disabled, icon, label, primary, onClick, tooltip } = this.props;
const button = (
<FlatButton
className={ className }
backgroundColor={ backgroundColor }
className={ className }
disabled={ disabled }
icon={ icon }
label={ label }
primary={ primary }
onTouchTap={ onClick }
primary={ primary }
/>
);
if (!tooltip) {
return button;
}
return (
<div>
<div
data-tip
data-for={ `button_${this.id}` }
data-effect='solid'
data-place='bottom'
>
{ button }
</div>
<ReactTooltip id={ `button_${this.id}` }>
<div className={ styles.tooltip }>
{ label }
</div>
</ReactTooltip>
</div>
);
}
}

View File

@ -31,6 +31,7 @@ import Loading from '~/ui/Loading';
import Portal from '~/ui/Portal';
import { nodeOrStringProptype } from '~/util/proptypes';
import { validateAddress } from '~/util/validation';
import { toString } from '~/util/messages';
import AddressSelectStore from './addressSelectStore';
import styles from './addressSelect.css';
@ -186,12 +187,7 @@ class AddressSelect extends Component {
}
const id = `addressSelect_${++currentId}`;
const ilHint = typeof hint === 'string' || !(hint && hint.props)
? (hint || '')
: this.context.intl.formatMessage(
hint.props,
hint.props.values || {}
);
const ilHint = toString(this.context, hint);
return (
<Portal

View File

@ -20,6 +20,7 @@ import { noop } from 'lodash';
import keycode from 'keycode';
import { nodeOrStringProptype } from '~/util/proptypes';
import { toString } from '~/util/messages';
import CopyToClipboard from '../../CopyToClipboard';
@ -149,12 +150,7 @@ export default class Input extends Component {
? UNDERLINE_FOCUSED
: readOnly && typeof focused !== 'boolean' ? { display: 'none' } : null;
const textValue = typeof value !== 'string' && (value && value.props)
? this.context.intl.formatMessage(
value.props,
value.props.values || {}
)
: value;
const textValue = toString(this.context, value);
return (
<div className={ styles.container } style={ style }>

44
js/src/util/messages.js Normal file
View File

@ -0,0 +1,44 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
/**
* Convert a String or a FormattedMessage
* element into a string
*
* @param {Object} context - The React `context`
* @param {String|Object} value - A String or a FormattedMessage
* element
* @return {String}
*/
export function toString (context, value) {
if (!context.intl) {
console.warn(`remember to add:
static contextTypes = {
intl: React.PropTypes.object.isRequired
};
to your component`);
return value;
}
const textValue = typeof value !== 'string' && (value && value.props)
? context.intl.formatMessage(
value.props,
value.props.values || {}
)
: value || '';
return textValue;
}