Add tooltips capabilities to buttons (#5562)
Add tooltips for buttons on ActionBar if text not visible
This commit is contained in:
parent
3e86b2e666
commit
eff4cde738
@ -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;
|
||||
};
|
||||
}
|
||||
|
20
js/src/ui/Button/button.css
Normal file
20
js/src/ui/Button/button.css
Normal 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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
44
js/src/util/messages.js
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user