Skip to content
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

Display tooltip on button hover and focus #839

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
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
131 changes: 115 additions & 16 deletions components/button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,122 @@
*/
import './style.scss';
import classnames from 'classnames';
import { compact, over } from 'lodash';

function Button( { href, isPrimary, isLarge, isToggled, className, ...additionalProps } ) {
const classes = classnames( 'components-button', className, {
button: ( isPrimary || isLarge ),
'button-primary': isPrimary,
'button-large': isLarge,
'is-toggled': isToggled,
} );

const tag = href !== undefined ? 'a' : 'button';
const tagProps = tag === 'a' ? { href } : { type: 'button' };

return wp.element.createElement( tag, {
...tagProps,
...additionalProps,
className: classes,
} );
/**
* WordPress dependencies
*/
import { Component } from 'element';

class Button extends Component {
constructor() {
super( ...arguments );

this.setFocused = this.setFocused.bind( this );
this.checkTooltipBounds = this.checkTooltipBounds.bind( this );
this.resetBoundedTooltip = this.resetBoundedTooltip.bind( this );

this.state = {
hasFocus: false,
bounded: [],
};
}

setFocused() {
this.setState( { hasFocus: true } );
}

checkTooltipBounds( event ) {
// Prevent calculating bounded restrictions if the button has no title
// or if bounds have already been calculated in current hover/focus
if ( ! this.props.title || this.state.bounded.length ) {
return;
}

const { currentTarget } = event;

// TODO: Find a better way to determine bounds of the content area,
// This is implemented with padding on the content element, meaning
// it's not as simple as referencing the root or body nodes. A better
// solution would not be tied to specific element IDs but instead
// offet values calculated from the editor's root layout element.
const contentTop = document.getElementById( 'wpbody-content' ).getBoundingClientRect().top;

// Because tooltip is rendered as a pseudo-element, positional offsets
// are calculated relative the button
const targetRect = currentTarget.getBoundingClientRect();
const targetCenter = targetRect.left + ( targetRect.width / 2 );
const tooltipStyles = window.getComputedStyle( currentTarget, ':after' );
const tooltipWidth = parseInt( tooltipStyles.width, 10 );
const tooltipTop = targetRect.top + parseInt( tooltipStyles.top, 10 );
const tooltipLeft = targetCenter - ( tooltipWidth / 2 );
const tooltipRight = targetCenter + ( tooltipWidth / 2 );

this.setState( {
// If the default position of the tooltip exceeds any bounds of the
// page content, assign into state so it can be flipped by styling
bounded: compact( [
tooltipTop < contentTop ? 'top' : null,
tooltipLeft < 0 ? 'left' : null,
tooltipRight > document.documentElement.clientWidth ? 'right' : null,
] ),
} );
}

resetBoundedTooltip( event ) {
// If button currently has focus, only reset flipped state when focus
// leaves, not on mouse out.
if ( this.state.hasFocus && 'blur' !== event.type ) {
return;
}

this.setState( {
bounded: [],
hasFocus: false,
} );
}

render() {
const { href, isPrimary, isLarge, isToggled, disabled, title, className, ...additionalProps } = this.props;
const { bounded } = this.state;
const classes = classnames( 'components-button', className, {
button: ( isPrimary || isLarge ),
'button-primary': isPrimary,
'button-large': isLarge,
'is-toggled': isToggled,
'is-disabled': disabled,
}, bounded.map( ( bound ) => 'is-tooltip-bounded-' + bound ) );

let tag;
if ( href ) {
tag = 'a';
} else if ( disabled ) {
// Treat disabled button as styled static element, to avoid needing
// to handle focus events since we can't assign disabled attribute
tag = 'span';
} else {
tag = 'button';
}

const tagProps = tag === 'a' ? { href } : { type: 'button' };

return wp.element.createElement( tag, {
...tagProps,
...additionalProps,
'aria-label': title,
// We can't assign a disabled attribute, both because it's invalid
// for anchor elements, and also because it interferes with mouse
// events on tooltip bounds check
'aria-disabled': disabled,
role: 'button',
className: classes,
onClick: disabled ? null : this.props.onClick,
onFocus: over( this.setFocused, this.checkTooltipBounds, this.props.onFocus ),
onBlur: over( this.resetBoundedTooltip, this.props.onBlur ),
onMouseEnter: over( this.checkTooltipBounds, this.props.onMouseEnter ),
onMouseLeave: over( this.resetBoundedTooltip, this.props.onMouseLeave ),
} );
}
}

export default Button;
97 changes: 96 additions & 1 deletion components/button/style.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,105 @@
.components-button {
display: inline-block;
position: relative;
background: none;
border: none;
outline: none;
text-decoration: none;

&:disabled {
&.is-toggled {
background: $dark-gray-500;
color: $white;
}

&.is-toggled:hover {
color: $white;
background: $dark-gray-400;
}

&.is-disabled {
opacity: 0.6;
cursor: default;
}

&:focus {
box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba( 30, 140, 190, .8 );
}

&[aria-label]:focus,
&[aria-label]:hover {
&:before,
&:after {
box-sizing: border-box;
z-index: z-index( '.components-button[aria-label]:after' );
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX( -50% );
pointer-events: none;
text-shadow: none;
font-family: $default-font;
font-size: $default-font-size;
}

&:before {
content: '';
width: 0;
height: 0;
margin-bottom: -6px;
border: 6px solid transparent;
border-bottom-width: 0;
border-top-color: $dark-gray-400;
}

&:after {
content: attr( aria-label );
padding: 4px 12px;
background: $dark-gray-400;
white-space: nowrap;
font-size: $default-font-size;
color: white;
}
}

&.is-tooltip-bounded-top {
&[aria-label]:focus,
&[aria-label]:hover {
&:before,
&:after {
top: 100%;
bottom: auto;
}

&:before {
margin-top: -6px;
margin-bottom: 0;
border-top-width: 0;
border-bottom-width: 6px;
border-top-color: transparent;
border-bottom-color: $dark-gray-400;
}
}
}

&.is-tooltip-bounded-right {
&[aria-label]:focus,
&[aria-label]:hover {
&:after {
left: auto;
right: 50%;
margin-right: -12px;
transform: none;
}
}
}

&.is-tooltip-bounded-left {
&[aria-label]:focus,
&[aria-label]:hover {
&:after {
margin-left: -12px;
transform: none;
}
}
}
}
4 changes: 2 additions & 2 deletions components/icon-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import './style.scss';
import Button from '../button';
import Dashicon from '../dashicon';

function IconButton( { icon, children, label, className, ...additionalProps } ) {
function IconButton( { icon, children, className, ...additionalProps } ) {
const classes = classnames( 'components-icon-button', className );

return (
<Button { ...additionalProps } aria-label={ label } className={ classes }>
<Button { ...additionalProps } className={ classes }>
<Dashicon icon={ icon } />
{ children }
</Button>
Expand Down
14 changes: 1 addition & 13 deletions components/icon-button/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,9 @@
color: $dark-gray-500;
position: relative;

&:not( :disabled ) {
cursor: pointer;

&:not( .is-disabled ) {
&:hover {
color: $blue-medium;
}
}

&:focus:before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
}
}
2 changes: 1 addition & 1 deletion components/toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function Toolbar( { controls } ) {
<IconButton
key={ index }
icon={ control.icon }
label={ control.title }
title={ control.title }
data-subscript={ control.subscript }
onClick={ ( event ) => {
event.stopPropagation();
Expand Down
7 changes: 0 additions & 7 deletions components/toolbar/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@
margin-left: 3px;
}

&:focus:before {
top: -4px;
right: -4px;
bottom: -4px;
left: -4px;
}

&.is-active,
&:hover,
&:not(:disabled):hover {
Expand Down
1 change: 1 addition & 0 deletions editor/assets/stylesheets/_z-index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ $z-layers: (
'.editor-visual-editor__block {core/image aligned left or right}': 10,
'.editor-visual-editor__block-controls': 1,
'.editor-header': 20,
'.components-button[aria-label]:before': 30,
);

@function z-index( $key ) {
Expand Down
2 changes: 1 addition & 1 deletion editor/block-switcher/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class BlockSwitcher extends wp.element.Component {
onClick={ this.toggleMenu }
aria-haspopup="true"
aria-expanded={ this.state.open }
label={ wp.i18n.__( 'Change block type' ) }
title={ wp.i18n.__( 'Change block type' ) }
>
<Dashicon icon="arrow-down" />
</IconButton>
Expand Down
7 changes: 0 additions & 7 deletions editor/block-switcher/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@
width: auto;
margin: 3px;
padding: 6px;

&:focus:before {
top: -3px;
right: -3px;
bottom: -3px;
left: -3px;
}
}

.editor-block-switcher__menu {
Expand Down
4 changes: 2 additions & 2 deletions editor/header/tools/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ function Tools( { undo, redo, hasUndo, hasRedo, isSidebarOpened, toggleSidebar }
<IconButton
className="editor-tools__undo"
icon="undo"
label={ wp.i18n.__( 'Undo' ) }
title={ wp.i18n.__( 'Undo' ) }
disabled={ ! hasUndo }
onClick={ undo } />
<IconButton
className="editor-tools__redo"
icon="redo"
label={ wp.i18n.__( 'Redo' ) }
title={ wp.i18n.__( 'Redo' ) }
disabled={ ! hasRedo }
onClick={ redo } />
<Inserter position="bottom" />
Expand Down
12 changes: 0 additions & 12 deletions editor/header/tools/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,4 @@
margin-right: 4px;
}
}

.components-button {
&.is-toggled {
background: $dark-gray-500;
color: $white;
}

&.is-toggled:hover {
color: $white;
background: $dark-gray-400;
}
}
}
2 changes: 1 addition & 1 deletion editor/inserter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class Inserter extends wp.element.Component {
<div className="editor-inserter">
<IconButton
icon="insert"
label={ wp.i18n.__( 'Insert block' ) }
title={ wp.i18n.__( 'Insert block' ) }
onClick={ this.toggle }
className="editor-inserter__toggle"
aria-haspopup="true"
Expand Down