diff --git a/app/javascript/mastodon/components/button.js b/app/javascript/mastodon/components/button.js index eb8dd7dc8eb9c9501c0ce0be83e33f41d5c35c81..ac336bfbbf30e264ba5b4f02fd0fa16d0bf574f8 100644 --- a/app/javascript/mastodon/components/button.js +++ b/app/javascript/mastodon/components/button.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import Tooltip from 'mastodon/components/tooltip'; export default class Button extends React.PureComponent { @@ -49,16 +50,17 @@ export default class Button extends React.PureComponent { }); return ( - <button - className={className} - disabled={this.props.disabled} - onClick={this.handleClick} - ref={this.setRef} - style={style} - title={this.props.title} - > - {this.props.text || this.props.children} - </button> + <Tooltip placement='bottom' overlay={this.props.title}> + <button + className={className} + disabled={this.props.disabled} + onClick={this.handleClick} + ref={this.setRef} + style={style} + > + {this.props.text || this.props.children} + </button> + </Tooltip> ); } diff --git a/app/javascript/mastodon/components/icon.js b/app/javascript/mastodon/components/icon.js index d8a17722fed2a7bbcaf4060d8587f71ebdd29524..073bd17cffc070b59369f8172a1965f3efab3552 100644 --- a/app/javascript/mastodon/components/icon.js +++ b/app/javascript/mastodon/components/icon.js @@ -1,20 +1,24 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import Tooltip from 'mastodon/components/tooltip'; export default class Icon extends React.PureComponent { static propTypes = { id: PropTypes.string.isRequired, className: PropTypes.string, + title: PropTypes.node, fixedWidth: PropTypes.bool, }; render () { - const { id, className, fixedWidth, ...other } = this.props; + const { id, className, fixedWidth, title, ...other } = this.props; return ( - <i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} /> + <Tooltip placement='top' overlay={title}> + <i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} /> + </Tooltip> ); } diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index fd715bc3c83ac83fadc5fb9de2fee1b74fdff3d5..d56d117b4507b13975eb1bdbe1d91a42563706e3 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; +import Tooltip from 'mastodon/components/tooltip'; export default class IconButton extends React.PureComponent { @@ -114,22 +115,23 @@ export default class IconButton extends React.PureComponent { }); return ( - <button - aria-label={title} - aria-pressed={pressed} - aria-expanded={expanded} - title={title} - className={classes} - onClick={this.handleClick} - onMouseDown={this.handleMouseDown} - onKeyDown={this.handleKeyDown} - onKeyPress={this.handleKeyPress} - style={style} - tabIndex={tabIndex} - disabled={disabled} - > - <Icon id={icon} fixedWidth aria-hidden='true' /> - </button> + <Tooltip placement='bottom' overlay={title}> + <button + aria-label={title} + aria-pressed={pressed} + aria-expanded={expanded} + className={classes} + onClick={this.handleClick} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleKeyDown} + onKeyPress={this.handleKeyPress} + style={style} + tabIndex={tabIndex} + disabled={disabled} + > + <Icon id={icon} fixedWidth aria-hidden='true' /> + </button> + </Tooltip> ); } diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index a31de206b43f7e0926c656f05fa829b584f71451..93c8e2abe32b032efc151ae1090bfe6f5c784b2d 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -8,6 +8,7 @@ import { isIOS } from '../is_mobile'; import classNames from 'classnames'; import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state'; import { decode } from 'blurhash'; +import Tooltip from 'mastodon/components/tooltip'; const messages = defineMessages({ toggle_visible: { id: 'media_gallery.toggle_visible', @@ -165,9 +166,11 @@ class Item extends React.PureComponent { if (attachment.get('type') === 'unknown') { return ( <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'> - <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> - </a> + <Tooltip placement='bottom' overlay={attachment.get('description')}> + <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} aria-label={attachment.get('description')} target='_blank' rel='noopener noreferrer'> + <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> + </a> + </Tooltip> </div> ); } else if (attachment.get('type') === 'image') { @@ -188,42 +191,44 @@ class Item extends React.PureComponent { const y = ((focusY / -2) + .5) * 100; thumbnail = ( - <a - className='media-gallery__item-thumbnail' - href={attachment.get('remote_url') || originalUrl} - onClick={this.handleClick} - target='_blank' - rel='noopener noreferrer' - > - <img - src={previewUrl} - srcSet={srcSet} - sizes={sizes} - alt={attachment.get('description')} - title={attachment.get('description')} - style={{ objectPosition: `${x}% ${y}%` }} - onLoad={this.handleImageLoad} - /> - </a> + <Tooltip placement='bottom' overlay={attachment.get('description')}> + <a + className='media-gallery__item-thumbnail' + href={attachment.get('remote_url') || originalUrl} + onClick={this.handleClick} + target='_blank' + rel='noopener noreferrer' + > + <img + src={previewUrl} + srcSet={srcSet} + sizes={sizes} + alt={attachment.get('description')} + style={{ objectPosition: `${x}% ${y}%` }} + onLoad={this.handleImageLoad} + /> + </a> + </Tooltip> ); } else if (attachment.get('type') === 'gifv') { const autoPlay = !isIOS() && this.getAutoPlay(); thumbnail = ( <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> - <video - className='media-gallery__item-gifv-thumbnail' - aria-label={attachment.get('description')} - title={attachment.get('description')} - role='application' - src={attachment.get('url')} - onClick={this.handleClick} - onMouseEnter={this.handleMouseEnter} - onMouseLeave={this.handleMouseLeave} - autoPlay={autoPlay} - loop - muted - /> + <Tooltip placement='bottom' overlay={attachment.get('description')}> + <video + className='media-gallery__item-gifv-thumbnail' + aria-label={attachment.get('description')} + role='application' + src={attachment.get('url')} + onClick={this.handleClick} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + autoPlay={autoPlay} + loop + muted + /> + </Tooltip> <span className='media-gallery__gifv__label'>GIF</span> </div> diff --git a/app/javascript/mastodon/components/permalink.js b/app/javascript/mastodon/components/permalink.js index b369e98126d36d0d713dc49cb3c31cc44158dedd..4e48419884335008f23608c1b01d161f90b7de48 100644 --- a/app/javascript/mastodon/components/permalink.js +++ b/app/javascript/mastodon/components/permalink.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Tooltip from 'mastodon/components/tooltip'; export default class Permalink extends React.PureComponent { @@ -28,12 +29,14 @@ export default class Permalink extends React.PureComponent { } render () { - const { href, children, className, onInterceptClick, ...other } = this.props; + const { href, children, className, onInterceptClick, title, ...other } = this.props; return ( - <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}> - {children} - </a> + <Tooltip placement='top' overlay={title}> + <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}> + {children} + </a> + </Tooltip> ); } diff --git a/app/javascript/mastodon/components/relative_timestamp.js b/app/javascript/mastodon/components/relative_timestamp.js index 711181dcdbec0ba5cf0f76639a90915b983be66b..4c2fca1bbed8eb28ac0bac660e7c21f669858c01 100644 --- a/app/javascript/mastodon/components/relative_timestamp.js +++ b/app/javascript/mastodon/components/relative_timestamp.js @@ -1,6 +1,7 @@ import React from 'react'; import { injectIntl, defineMessages } from 'react-intl'; import PropTypes from 'prop-types'; +import Tooltip from 'mastodon/components/tooltip'; const messages = defineMessages({ today: { id: 'relative_time.today', defaultMessage: 'today' }, @@ -181,11 +182,14 @@ class RelativeTimestamp extends React.Component { const timeGiven = timestamp.includes('T'); const date = new Date(timestamp); const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven); + const formatted = intl.formatDate(date, dateFormatOptions); return ( - <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}> - {relativeTime} - </time> + <Tooltip placement='top' overlay={formatted}> + <time dateTime={timestamp} aria-label={formatted}> + {relativeTime} + </time> + </Tooltip> ); } diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index f99ccd39a633803d5f42e18684330b32e1a2f557..e94df4c27577f30e357e0f2299b37e4647c78492 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -16,6 +16,7 @@ import { MediaGallery, Video, Audio } from '../features/ui/util/async-components import { HotKeys } from 'react-hotkeys'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; +import Tooltip from 'mastodon/components/tooltip'; import { displayMedia } from '../initial_state'; // We use the component (and not the container) since we do not want @@ -424,13 +425,15 @@ class Status extends ImmutablePureComponent { <div className='status__info'> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a> - <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'> - <div className='status__avatar'> - {statusAvatar} - </div> + <Tooltip placement='top' overlay={status.getIn(['account', 'acct'])}> + <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name' target='_blank' rel='noopener noreferrer'> + <div className='status__avatar'> + {statusAvatar} + </div> - <DisplayName account={status.get('account')} others={otherAccounts} /> - </a> + <DisplayName account={status.get('account')} others={otherAccounts} /> + </a> + </Tooltip> </div> <StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} showThread={showThread} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} /> diff --git a/app/javascript/mastodon/components/tooltip.js b/app/javascript/mastodon/components/tooltip.js new file mode 100644 index 0000000000000000000000000000000000000000..16f3e78aab1f8c6f9618011b2d3e7ccdf9149c39 --- /dev/null +++ b/app/javascript/mastodon/components/tooltip.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Tooltip from 'rc-tooltip'; + +const OptionalTooltip = ({ overlay, children, ...other }) => { + if (overlay) { + return ( + <Tooltip animation='zoom' mouseEnterDelay={0.2} destroyTooltipOnHide {...other} overlay={overlay}> + {children} + </Tooltip> + ); + } else { + return children; + } +}; + +OptionalTooltip.propTypes = { + children: PropTypes.node, + overlay: PropTypes.node, +}; + +export default OptionalTooltip; diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 8c85bbc393d819250b0bdff4f4949229700573be..9775fac8401a3ce72bf73eeb14b22cde8072c4ba 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -11,6 +11,7 @@ import Avatar from 'mastodon/components/avatar'; import { shortNumberFormat } from 'mastodon/utils/numbers'; import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; +import Tooltip from 'mastodon/components/tooltip'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -293,19 +294,34 @@ class Header extends ImmutablePureComponent { <dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} /> <dd className='verified'> - <a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}> - <Icon id='check' className='verified__mark' /> - </span></a> + <Tooltip placement='top' overlay={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}> + <a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'> + <Icon id='check' className='verified__mark' /> + </a> + </Tooltip> + <a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a> </dd> </dl> ))} {fields.map((pair, i) => ( <dl key={i}> - <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> - - <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}> - {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> + <dt> + <Tooltip placement='top' overlay={pair.get('name')}> + <span dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} /> + </Tooltip> + </dt> + + <dd className={pair.get('verified_at') && 'verified'}> + {pair.get('verified_at') && ( + <Tooltip placement='top' overlay={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}> + <Icon id='check' className='verified__mark' /> + </Tooltip> + )} + {' '} + <Tooltip placement='top' overlay={pair.get('value_plain')}> + <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> + </Tooltip> </dd> </dl> ))} @@ -316,17 +332,23 @@ class Header extends ImmutablePureComponent { </div> <div className='account__header__extra__links'> - <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}> - <strong>{shortNumberFormat(account.get('statuses_count'))}</strong> <FormattedMessage id='account.posts' defaultMessage='Toots' /> - </NavLink> - - <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}> - <strong>{shortNumberFormat(account.get('following_count'))}</strong> <FormattedMessage id='account.follows' defaultMessage='Follows' /> - </NavLink> - - <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}> - <strong>{shortNumberFormat(account.get('followers_count'))}</strong> <FormattedMessage id='account.followers' defaultMessage='Followers' /> - </NavLink> + <Tooltip placement='bottom' overlay={intl.formatNumber(account.get('statuses_count'))}> + <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`}> + <strong>{shortNumberFormat(account.get('statuses_count'))}</strong> <FormattedMessage id='account.posts' defaultMessage='Toots' /> + </NavLink> + </Tooltip> + + <Tooltip placement='bottom' overlay={intl.formatNumber(account.get('following_count'))}> + <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`}> + <strong>{shortNumberFormat(account.get('following_count'))}</strong> <FormattedMessage id='account.follows' defaultMessage='Follows' /> + </NavLink> + </Tooltip> + + <Tooltip placement='bottom' overlay={intl.formatNumber(account.get('followers_count'))}> + <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`}> + <strong>{shortNumberFormat(account.get('followers_count'))}</strong> <FormattedMessage id='account.followers' defaultMessage='Followers' /> + </NavLink> + </Tooltip> </div> </div> </div> diff --git a/app/javascript/mastodon/features/compose/components/autosuggest_account.js b/app/javascript/mastodon/features/compose/components/autosuggest_account.js index 1451be0e6467634f4515fcc1aeca3d8d61bf82de..b144a2c3b669c6cace8b1a7a9840c1dbe56b5a8d 100644 --- a/app/javascript/mastodon/features/compose/components/autosuggest_account.js +++ b/app/javascript/mastodon/features/compose/components/autosuggest_account.js @@ -3,6 +3,7 @@ import Avatar from '../../../components/avatar'; import DisplayName from '../../../components/display_name'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import Tooltip from 'mastodon/components/tooltip'; export default class AutosuggestAccount extends ImmutablePureComponent { @@ -14,10 +15,12 @@ export default class AutosuggestAccount extends ImmutablePureComponent { const { account } = this.props; return ( - <div className='autosuggest-account' title={account.get('acct')}> - <div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div> - <DisplayName account={account} /> - </div> + <Tooltip placement='top' overlay={account.get('acct')}> + <div className='autosuggest-account'> + <div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div> + <DisplayName account={account} /> + </div> + </Tooltip> ); } diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index a6186010b4f7be9dbfcdd9791c59b02e4dfcf55f..ed61c904fac580da5c96112bad537bee8557898a 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -7,6 +7,7 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import detectPassiveEvents from 'detect-passive-events'; import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; +import Tooltip from 'mastodon/components/tooltip'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -358,13 +359,15 @@ class EmojiPickerDropdown extends React.PureComponent { return ( <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}> - <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}> - {button || <img - className={classNames('emojione', { 'pulse-loading': active && loading })} - alt='🙂' - src={`${assetHost}/emoji/1f602.svg`} - />} - </div> + <Tooltip placement='top' overlay={title}> + <div ref={this.setTargetRef} className='emoji-button' aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}> + {button || <img + className={classNames('emojione', { 'pulse-loading': active && loading })} + alt='🙂' + src={`${assetHost}/emoji/1f602.svg`} + />} + </div> + </Tooltip> <Overlay show={active} placement={placement} target={this.findTarget}> <EmojiPickerMenu diff --git a/app/javascript/mastodon/features/compose/components/text_icon_button.js b/app/javascript/mastodon/features/compose/components/text_icon_button.js index f0b1335386ded402665a932603829dfa50b616b9..e9be26cfccc6b95f88165faa0d1868244923b7ad 100644 --- a/app/javascript/mastodon/features/compose/components/text_icon_button.js +++ b/app/javascript/mastodon/features/compose/components/text_icon_button.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Tooltip from 'mastodon/components/tooltip'; const iconStyle = { height: null, @@ -26,16 +27,17 @@ export default class TextIconButton extends React.PureComponent { const { label, title, active, ariaControls } = this.props; return ( - <button - title={title} - aria-label={title} - className={`text-icon-button ${active ? 'active' : ''}`} - aria-expanded={active} - onClick={this.handleClick} - aria-controls={ariaControls} style={iconStyle} - > - {label} - </button> + <Tooltip placement='bottom' overlay={title}> + <button + aria-label={title} + className={`text-icon-button ${active ? 'active' : ''}`} + aria-expanded={active} + onClick={this.handleClick} + aria-controls={ariaControls} style={iconStyle} + > + {label} + </button> + </Tooltip> ); } diff --git a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js index 7073f76c2e8f81f81189f7003e22edc5f882594e..bd83a034b064967f9d91a956a0d036266a5be09c 100644 --- a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js +++ b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { changeComposeSensitivity } from 'mastodon/actions/compose'; import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; +import Tooltip from 'mastodon/components/tooltip'; const messages = defineMessages({ marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' }, @@ -37,19 +38,21 @@ class SensitiveButton extends React.PureComponent { return ( <div className='compose-form__sensitive-button'> - <label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}> - <input - name='mark-sensitive' - type='checkbox' - checked={active} - onChange={onClick} - disabled={disabled} - /> - - <span className={classNames('checkbox', { active })} /> - - <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' /> - </label> + <Tooltip placement='top' overlay={intl.formatMessage(active ? messages.marked : messages.unmarked)}> + <label className={classNames('icon-button', { active })} aria-label={intl.formatMessage(active ? messages.marked : messages.unmarked)}> + <input + name='mark-sensitive' + type='checkbox' + checked={active} + onChange={onClick} + disabled={disabled} + /> + + <span className={classNames('checkbox', { active })} /> + + <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' /> + </label> + </Tooltip> </div> ); } diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index e2de8b0e6a20d1da4479c19a77dd201c434af682..133e15f4193257d4d2ee56e9d260ed3088c93335 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -16,6 +16,7 @@ import { openModal } from 'mastodon/actions/modal'; import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; import { mascot } from '../../initial_state'; import Icon from 'mastodon/components/icon'; +import Tooltip from 'mastodon/components/tooltip'; import { logOut } from 'mastodon/utils/log_out'; const messages = defineMessages({ @@ -97,21 +98,55 @@ class Compose extends React.PureComponent { const { columns } = this.props; header = ( <nav className='drawer__header'> - <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' fixedWidth /></Link> + <Tooltip placement='bottom' overlay={intl.formatMessage(messages.start)}> + <Link to='/getting-started' className='drawer__tab' aria-label={intl.formatMessage(messages.start)}> + <Icon id='bars' fixedWidth /> + </Link> + </Tooltip> + {!columns.some(column => column.get('id') === 'HOME') && ( - <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link> + <Tooltip placement='bottom' overlay={intl.formatMessage(messages.home_timeline)}> + <Link to='/timelines/home' className='drawer__tab' aria-label={intl.formatMessage(messages.home_timeline)}> + <Icon id='home' fixedWidth /> + </Link> + </Tooltip> )} + {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( - <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link> + <Tooltip placement='bottom' overlay={intl.formatMessage(messages.notifications)}> + <Link to='/notifications' className='drawer__tab' aria-label={intl.formatMessage(messages.notifications)}> + <Icon id='bell' fixedWidth /> + </Link> + </Tooltip> )} + {!columns.some(column => column.get('id') === 'COMMUNITY') && ( - <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link> + <Tooltip placement='bottom' overlay={intl.formatMessage(messages.community)}> + <Link to='/timelines/public/local' className='drawer__tab' aria-label={intl.formatMessage(messages.community)}> + <Icon id='users' fixedWidth /> + </Link> + </Tooltip> )} + {!columns.some(column => column.get('id') === 'PUBLIC') && ( - <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link> + <Tooltip placement='bottom' overlay={intl.formatMessage(messages.public)}> + <Link to='/timelines/public' className='drawer__tab' aria-label={intl.formatMessage(messages.public)}> + <Icon id='globe' fixedWidth /> + </Link> + </Tooltip> )} - <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a> - <a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' fixedWidth /></a> + + <Tooltip placement='bottom' overlay={intl.formatMessage(messages.preferences)}> + <a href='/settings/preferences' className='drawer__tab' aria-label={intl.formatMessage(messages.preferences)}> + <Icon id='cog' fixedWidth /> + </a> + </Tooltip> + + <Tooltip placement='bottom' overlay={intl.formatMessage(messages.logout)}> + <a href='/auth/sign_out' className='drawer__tab' aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}> + <Icon id='sign-out' fixedWidth /> + </a> + </Tooltip> </nav> ); } diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 8ebc45b62d1ae84b3ba34d9323e718160352f52b..03f23cfcf0cf60ed57a9e16e7d73880423a9abca 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -24,5 +24,6 @@ @import 'mastodon/tables'; @import 'mastodon/admin'; @import 'mastodon/dashboard'; +@import 'mastodon/tooltips'; @import 'mastodon/rtl'; @import 'mastodon/accessibility'; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 5d725b908f3dd9d9ea328e923537a599d64d30be..340537dae2a778bab18656a6de20efebfeee3434 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1076,8 +1076,7 @@ } .status__info .status__display-name { - display: block; - max-width: 100%; + display: inline-flex; padding-right: 25px; } diff --git a/app/javascript/styles/mastodon/tooltips.scss b/app/javascript/styles/mastodon/tooltips.scss new file mode 100644 index 0000000000000000000000000000000000000000..84eca211c49578553d29c136923c7bbf19f8b338 --- /dev/null +++ b/app/javascript/styles/mastodon/tooltips.scss @@ -0,0 +1,233 @@ +// +// Tooltips +// -------------------------------------------------- +$font-size-base: 14px; +$line-height-base: 1.5; +$border-radius-base: 5px; +$overlay-shadow: 0 0 4px rgba(0, 0, 0, 0.17); +//** Tooltip text color +$tooltip-color: #fff; +//** Tooltip background color +$tooltip-bg: #000; +$tooltip-opacity: 0.9; + +//** Tooltip arrow width +$tooltip-arrow-width: 5px; +//** Tooltip distance with trigger +$tooltip-distance: $tooltip-arrow-width + 4; +//** Tooltip arrow color +$tooltip-arrow-color: $tooltip-bg; + +// Base class +.rc-tooltip { + position: absolute; + z-index: 1070; + display: block; + visibility: visible; + // remove left/top by yiminghe + // left: -9999px; + // top: -9999px; + font-size: $font-size-base; + line-height: $line-height-base; + font-weight: 500; + opacity: $tooltip-opacity; + will-change: opacity, transform; + pointer-events: none; + + &-hidden { + display: none; + } + + &-placement-top, &-placement-topLeft, &-placement-topRight { + padding: $tooltip-arrow-width 0 $tooltip-distance 0; + } + &-placement-right, &-placement-rightTop, &-placement-rightBottom { + padding: 0 $tooltip-arrow-width 0 $tooltip-distance; + } + &-placement-bottom, &-placement-bottomLeft, &-placement-bottomRight { + padding: $tooltip-distance 0 $tooltip-arrow-width 0; + } + &-placement-left, &-placement-leftTop, &-placement-leftBottom { + padding: 0 $tooltip-distance 0 $tooltip-arrow-width; + } +} + +// Wrapper for the tooltip content +.rc-tooltip-inner { + padding: 8px 12px; + color: $tooltip-color; + text-align: left; + text-decoration: none; + background-color: $tooltip-bg; + border-radius: $border-radius-base; + box-shadow: $overlay-shadow; + max-width: 290px; + box-sizing: border-box; + min-height: 34px; +} + +// Arrows +.rc-tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.rc-tooltip { + &-placement-top &-arrow, + &-placement-topLeft &-arrow, + &-placement-topRight &-arrow { + bottom: $tooltip-distance - $tooltip-arrow-width; + margin-left: -$tooltip-arrow-width; + border-width: $tooltip-arrow-width $tooltip-arrow-width 0; + border-top-color: $tooltip-arrow-color; + } + + &-placement-top &-arrow { + left: 50%; + } + + &-placement-topLeft &-arrow { + left: 15%; + } + + &-placement-topRight &-arrow { + right: 15%; + } + + &-placement-right &-arrow, + &-placement-rightTop &-arrow, + &-placement-rightBottom &-arrow { + left: $tooltip-distance - $tooltip-arrow-width; + margin-top: -$tooltip-arrow-width; + border-width: $tooltip-arrow-width $tooltip-arrow-width $tooltip-arrow-width 0; + border-right-color: $tooltip-arrow-color; + } + + &-placement-right &-arrow { + top: 50%; + } + + &-placement-rightTop &-arrow { + top: 15%; + margin-top: 0; + } + + &-placement-rightBottom &-arrow { + bottom: 15%; + } + + &-placement-left &-arrow, + &-placement-leftTop &-arrow, + &-placement-leftBottom &-arrow { + right: $tooltip-distance - $tooltip-arrow-width; + margin-top: -$tooltip-arrow-width; + border-width: $tooltip-arrow-width 0 $tooltip-arrow-width $tooltip-arrow-width; + border-left-color: $tooltip-arrow-color; + } + + &-placement-left &-arrow { + top: 50%; + } + + &-placement-leftTop &-arrow { + top: 15%; + margin-top: 0; + } + + &-placement-leftBottom &-arrow { + bottom: 15%; + } + + &-placement-bottom &-arrow, + &-placement-bottomLeft &-arrow, + &-placement-bottomRight &-arrow { + top: $tooltip-distance - $tooltip-arrow-width; + margin-left: -$tooltip-arrow-width; + border-width: 0 $tooltip-arrow-width $tooltip-arrow-width; + border-bottom-color: $tooltip-arrow-color; + } + + &-placement-bottom &-arrow { + left: 50%; + } + + &-placement-bottomLeft &-arrow { + left: 15%; + } + + &-placement-bottomRight &-arrow { + right: 15%; + } + + @mixin effect(){ + animation-duration: 50ms; + animation-fill-mode: both; + } + + & { + &-zoom-enter, + &-zoom-leave { + display: block; + } + } + + &-zoom-enter, + &-zoom-appear { + opacity: 0; + @include effect(); + animation-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28); + animation-play-state: paused; + } + + &-zoom-leave { + @include effect(); + animation-timing-function: cubic-bezier(0.6, -0.3, 0.74, 0.05); + animation-play-state: paused; + } + + &-zoom-enter, + &-zoom-appear { + &-active { + animation-name: rcToolTipZoomIn; + animation-play-state: running; + } + } + + &-zoom-leave { + &-active { + animation-name: rcToolTipZoomOut; + animation-play-state: running; + } + } + + @keyframes rcToolTipZoomIn { + 0% { + opacity: 0; + transform-origin: 50% 50%; + transform: scale(0, 0); + } + + 100% { + opacity: 1; + transform-origin: 50% 50%; + transform: scale(1, 1); + } + } + + @keyframes rcToolTipZoomOut { + 0% { + opacity: 1; + transform-origin: 50% 50%; + transform: scale(1, 1); + } + + 100% { + opacity: 0; + transform-origin: 50% 50%; + transform: scale(0, 0); + } + } +} diff --git a/package.json b/package.json index 65be5a0410ed0730959038535b408388c2862d31..9b162677dad89a25cdec055d0e12f2f4f96a9da8 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "promise.prototype.finally": "^3.1.2", "prop-types": "^15.5.10", "punycode": "^2.1.0", + "rc-tooltip": "^4.2.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-hotkeys": "^1.1.4", diff --git a/yarn.lock b/yarn.lock index 06b9c5b189ca9ea8bc1634f2ec2a9eaa8b67459f..73e8fb805909cd9d1cb0e87697d8da5fbf94bb2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@ant-design/css-animation@^1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@ant-design/css-animation/-/css-animation-1.7.2.tgz#4ee5d2ec0fb7cc0a78b44e1c82628bd4621ac7e3" + integrity sha512-bvVOe7A+r7lws58B7r+fgnQDK90cV45AXuvGx6i5CCSX1W/M3AJnHsNggDANBxEtWdNdFWcDd5LorB+RdSIlBw== + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff" @@ -988,7 +993,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.10.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839" integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg== @@ -2840,7 +2845,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.2.5: +classnames@2.x, classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -3744,6 +3749,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-align@^1.7.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.0.tgz#56fb7156df0b91099830364d2d48f88963f5a29c" + integrity sha512-YkoezQuhp3SLFGdOlr5xkqZ640iXrnHAwVYcDg8ZKRUtO7mSzSC2BA5V0VuyAwPSJA4CLIc6EDDJh4bEsD2+zA== + dom-helpers@^3.2.1, dom-helpers@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" @@ -8769,7 +8779,7 @@ quote@^0.4.0: resolved "https://registry.yarnpkg.com/quote/-/quote-0.4.0.tgz#10839217f6c1362b89194044d29b233fd7f32f01" integrity sha1-EIOSF/bBNiuJGUBE0psjP9fzLwE= -raf@^3.1.0, raf@^3.4.1: +raf@^3.1.0, raf@^3.4.0, raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -8819,6 +8829,54 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" +rc-align@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.0.tgz#7a5b212051bdd840b406a6ad547076534a843691" + integrity sha512-0mKKfiZGo7VNiRCmnI4MTOG72pBFF0H08zebqcJyXcAm2hgAqTUtvt4I0pjMHh1WdYg+iQDjowpB5X8mZTN2vw== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + dom-align "^1.7.0" + rc-util "^5.0.1" + resize-observer-polyfill "^1.5.1" + +rc-animate@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-3.1.0.tgz#051b689c2c7194e4c8ae016d32a0e5f9de6c8baa" + integrity sha512-8FsM+3B1H+0AyTyGggY6JyVldHTs1CyYT8CfTmG/nGHHXlecvSLeICJhcKgRLjUiQlctNnRtB1rwz79cvBVmrw== + dependencies: + "@ant-design/css-animation" "^1.7.2" + classnames "^2.2.6" + raf "^3.4.0" + rc-util "^5.0.1" + +rc-tooltip@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-4.2.1.tgz#c1a2d5017ee03a771a9301c0dfdb46dfdf8fef94" + integrity sha512-oykuaGsHg7RFvPUaxUpxo7ScEqtH61C66x4JUmjlFlSS8gSx2L8JFtfwM1D68SLBxUqGqJObtxj4TED75gQTiA== + dependencies: + rc-trigger "^4.2.1" + +rc-trigger@^4.2.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-4.3.0.tgz#94ea1851d123359716d1dc3030083c015a92ecfb" + integrity sha512-jnGNzosXmDdivMBjPCYe/AfOXTpJU2/xQ9XukgoXDQEoZq/9lcI1r7eUIfq70WlWpLxlUEqQktiV3hwyy6Nw9g== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.6" + raf "^3.4.1" + rc-align "^4.0.0" + rc-animate "^3.0.0" + rc-util "^5.0.1" + +rc-util@^5.0.1: + version "5.0.4" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.0.4.tgz#297bd719b1bd00b3c947a884ab7ef0a07c55dce6" + integrity sha512-cd19RCrE0DJH6UcJ9+V3eaXA/5sNWyVKOKkWl8ZM2OqgNzVb8fv0obf/TkuvSN43tmTsgqY8k7OqpFYHhmef8g== + dependencies: + react-is "^16.12.0" + shallowequal "^1.1.0" + react-dom@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" @@ -9425,6 +9483,11 @@ reselect@^4.0.0: resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -9872,6 +9935,11 @@ shallow-equal@^1.2.1: resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da" integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA== +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"