Skip to content

Commit

Permalink
Merge pull request #6028 from WordPress/update/link-interface
Browse files Browse the repository at this point in the history
Iterate on 'Insert link' interface
  • Loading branch information
noisysocks authored Apr 10, 2018
2 parents 410c387 + 0bb6c85 commit 43a58b2
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 64 deletions.
156 changes: 104 additions & 52 deletions blocks/rich-text/format-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { IconButton, Toolbar, withSpokenMessages, Fill } from '@wordpress/components';
import {
Fill,
IconButton,
ToggleControl,
Toolbar,
withSpokenMessages,
} from '@wordpress/components';
import { keycodes } from '@wordpress/utils';

/**
Expand Down Expand Up @@ -32,8 +38,6 @@ const FORMATTING_CONTROLS = [
format: 'strikethrough',
},
{
icon: 'admin-links',
title: __( 'Link' ),
format: 'link',
},
];
Expand All @@ -47,10 +51,11 @@ const stopKeyPropagation = ( event ) => event.stopPropagation();
class FormatToolbar extends Component {
constructor() {
super( ...arguments );

this.state = {
isAddingLink: false,
isEditingLink: false,
settingsVisible: false,
opensInNewWindow: false,
newLinkValue: '',
};

Expand All @@ -60,6 +65,8 @@ class FormatToolbar extends Component {
this.submitLink = this.submitLink.bind( this );
this.onKeyDown = this.onKeyDown.bind( this );
this.onChangeLinkValue = this.onChangeLinkValue.bind( this );
this.toggleLinkSettingsVisibility = this.toggleLinkSettingsVisibility.bind( this );
this.setLinkTarget = this.setLinkTarget.bind( this );
}

onKeyDown( event ) {
Expand All @@ -79,6 +86,8 @@ class FormatToolbar extends Component {
this.setState( {
isAddingLink: false,
isEditingLink: false,
settingsVisible: false,
opensInNewWindow: !! nextProps.formats.link && !! nextProps.formats.link.target,
newLinkValue: '',
} );
}
Expand All @@ -96,6 +105,17 @@ class FormatToolbar extends Component {
};
}

toggleLinkSettingsVisibility() {
this.setState( ( state ) => ( { settingsVisible: ! state.settingsVisible } ) );
}

setLinkTarget( opensInNewWindow ) {
this.setState( { opensInNewWindow } );
if ( this.props.formats.link ) {
this.props.onChange( { link: { value: this.props.formats.link.value, target: opensInNewWindow ? '_blank' : '' } } );
}
}

addLink() {
this.setState( { isEditingLink: false, isAddingLink: true, newLinkValue: '' } );
}
Expand All @@ -112,7 +132,8 @@ class FormatToolbar extends Component {

submitLink( event ) {
event.preventDefault();
this.props.onChange( { link: { value: this.state.newLinkValue } } );
this.setState( { isEditingLink: false, isAddingLink: false, newLinkValue: '' } );
this.props.onChange( { link: { value: this.state.newLinkValue, target: this.state.opensInNewWindow ? '_blank' : '' } } );
if ( this.state.isAddingLink ) {
this.props.speak( __( 'Link added.' ), 'assertive' );
}
Expand All @@ -124,70 +145,101 @@ class FormatToolbar extends Component {

render() {
const { formats, focusPosition, enabledControls = DEFAULT_CONTROLS, customControls = [] } = this.props;
const { isAddingLink, isEditingLink, newLinkValue } = this.state;
const linkStyle = focusPosition ?
{ position: 'absolute', ...focusPosition } :
null;
const { isAddingLink, isEditingLink, newLinkValue, settingsVisible, opensInNewWindow } = this.state;

const toolbarControls = FORMATTING_CONTROLS.concat( customControls )
.filter( control => enabledControls.indexOf( control.format ) !== -1 )
.map( ( control ) => {
const isLink = control.format === 'link';
if ( control.format === 'link' ) {
const isFormatActive = this.isFormatActive( 'link' );
const isActive = isFormatActive || isAddingLink;
return {
...control,
icon: isFormatActive ? 'editor-unlink' : 'admin-links', // TODO: Need proper unlink icon
title: isFormatActive ? __( 'Unlink' ) : __( 'Link' ),
onClick: isActive ? this.dropLink : this.addLink,
isActive,
};
}

return {
...control,
onClick: isLink ? this.addLink : this.toggleFormat( control.format ),
isActive: this.isFormatActive( control.format ) || ( isLink && isAddingLink ),
onClick: this.toggleFormat( control.format ),
isActive: this.isFormatActive( control.format ),
};
} );

const linkSettings = settingsVisible && (
<div className="blocks-format-toolbar__link-modal-line blocks-format-toolbar__link-settings">
<ToggleControl
label={ __( 'Open in new window' ) }
checked={ opensInNewWindow }
onChange={ this.setLinkTarget } />
</div>
);

return (
<div className="blocks-format-toolbar">
<Toolbar controls={ toolbarControls } />

{ ( isAddingLink || isEditingLink ) &&
// Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
<Fill name="RichText.Siblings">
<form
className="blocks-format-toolbar__link-modal"
style={ linkStyle }
onKeyPress={ stopKeyPropagation }
onKeyDown={ this.onKeyDown }
onSubmit={ this.submitLink }>
<div className="blocks-format-toolbar__link-modal-line">
<UrlInput value={ newLinkValue } onChange={ this.onChangeLinkValue } />
<IconButton icon="editor-break" label={ __( 'Apply' ) } type="submit" />
<IconButton icon="editor-unlink" label={ __( 'Remove link' ) } onClick={ this.dropLink } />
</div>
</form>
</Fill>
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions */
}

{ !! formats.link && ! isAddingLink && ! isEditingLink &&
// Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar
/* eslint-disable jsx-a11y/no-static-element-interactions */
{ ( isAddingLink || isEditingLink || formats.link ) && (
<Fill name="RichText.Siblings">
<div
className="blocks-format-toolbar__link-modal"
style={ linkStyle }
onKeyPress={ stopKeyPropagation }
>
<div className="blocks-format-toolbar__link-modal-line">
<a
className="blocks-format-toolbar__link-value"
href={ formats.link.value }
target="_blank"
<div style={ { position: 'absolute', ...focusPosition } }>
{ ( isAddingLink || isEditingLink ) && (
// Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
<form
className="blocks-format-toolbar__link-modal"
onKeyPress={ stopKeyPropagation }
onKeyDown={ this.onKeyDown }
onSubmit={ this.submitLink }>
<div className="blocks-format-toolbar__link-modal-line">
<UrlInput value={ newLinkValue } onChange={ this.onChangeLinkValue } />
<IconButton icon="editor-break" label={ __( 'Apply' ) } type="submit" />
<IconButton
className="blocks-format-toolbar__link-settings-toggle"
icon="ellipsis"
label={ __( 'Link Settings' ) }
onClick={ this.toggleLinkSettingsVisibility }
aria-expanded={ settingsVisible }
/>
</div>
{ linkSettings }
</form>
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions */
) }

{ formats.link && ! isAddingLink && ! isEditingLink && (
// Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar
/* eslint-disable jsx-a11y/no-static-element-interactions */
<div
className="blocks-format-toolbar__link-modal"
onKeyPress={ stopKeyPropagation }
>
{ formats.link.value && filterURLForDisplay( decodeURI( formats.link.value ) ) }
</a>
<IconButton icon="edit" label={ __( 'Edit' ) } onClick={ this.editLink } />
<IconButton icon="editor-unlink" label={ __( 'Remove link' ) } onClick={ this.dropLink } />
</div>
<div className="blocks-format-toolbar__link-modal-line">
<a
className="blocks-format-toolbar__link-value"
href={ formats.link.value }
target="_blank"
>
{ formats.link.value && filterURLForDisplay( decodeURI( formats.link.value ) ) }
</a>
<IconButton icon="edit" label={ __( 'Edit' ) } onClick={ this.editLink } />
<IconButton
className="blocks-format-toolbar__link-settings-toggle"
icon="ellipsis"
label={ __( 'Link Settings' ) }
onClick={ this.toggleLinkSettingsVisibility }
aria-expanded={ settingsVisible }
/>
</div>
{ linkSettings }
</div>
/* eslint-enable jsx-a11y/no-static-element-interactions */
) }
</div>
</Fill>
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
) }
</div>
);
}
Expand Down
53 changes: 47 additions & 6 deletions blocks/rich-text/format-toolbar/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
}

.blocks-format-toolbar__link-modal {
position: absolute;
position: relative;
left: -50%;
box-shadow: 0 3px 20px rgba( 18, 24, 30, .1 ), 0 1px 3px rgba( 18, 24, 30, .1 );
border: 1px solid #e0e5e9;
background: #fff;
width: 300px;
display: flex;
flex-direction: column;
font-family: $default-font;
Expand All @@ -17,28 +17,69 @@
}

.blocks-format-toolbar__link-modal-line {
$button-size: 36px;
$input-padding: 10px;
$input-size: 230px;

display: flex;
flex-direction: row;
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
align-items: center;
align-items: flex-start;

.components-button {
flex-shrink: 0;
width: $button-size;
height: $button-size;
}

.blocks-url-input {
width: $input-size;

input {
padding: $input-padding;
}
}

.blocks-url-input__suggestions {
border-top: 1px solid $light-gray-500;
box-shadow: none;
padding: 4px 0;
position: relative;
width: $input-size + $button-size * 2;
}

.blocks-url-input__suggestion {
color: $dark-gray-100;
padding: 4px ( $button-size + $input-padding );
}
}

.blocks-format-toolbar__link-settings-toggle .dashicon {
transform: rotate(90deg);
}

.blocks-format-toolbar__link-value {
margin: 10px;
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
white-space: nowrap;
min-width: 0;
min-width: 150px;
max-width: 500px;
}

.blocks-format-toolbar__link-settings {
padding: 7px 8px;
border-top: 1px solid $light-gray-500;
padding-top: 8px; // add 1px for the border

&:after {
@include long-content-fade( $size: 40% );
.components-base-control {
margin: 0;
flex-grow: 1;
flex-shrink: 1;
}
}
7 changes: 3 additions & 4 deletions blocks/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export function getFormatProperties( formatName, parents ) {
switch ( formatName ) {
case 'link' : {
const anchor = find( parents, node => node.nodeName.toLowerCase() === 'a' );
return !! anchor ? { value: anchor.getAttribute( 'href' ) || '', node: anchor } : {};
return !! anchor ? { value: anchor.getAttribute( 'href' ) || '', target: anchor.getAttribute( 'target' ) || '', node: anchor } : {};
}
default:
return {};
Expand Down Expand Up @@ -432,11 +432,10 @@ export class RichText extends Component {
const container = findRelativeParent( this.editor.getBody() );
const containerPosition = container.getBoundingClientRect();
const toolbarOffset = { top: 10, left: 0 };
const linkModalWidth = 298;

return {
top: position.top - containerPosition.top + ( position.height ) + toolbarOffset.top,
left: position.left - containerPosition.left - ( linkModalWidth / 2 ) + ( position.width / 2 ) + toolbarOffset.left,
left: position.left - containerPosition.left + ( position.width / 2 ) + toolbarOffset.left,
};
}

Expand Down Expand Up @@ -753,7 +752,7 @@ export class RichText extends Component {
if ( ! anchor ) {
this.removeFormat( 'link' );
}
this.applyFormat( 'link', { href: formatValue.value }, anchor );
this.applyFormat( 'link', { href: formatValue.value, target: formatValue.target }, anchor );
} else {
this.editor.execCommand( 'Unlink' );
}
Expand Down
7 changes: 5 additions & 2 deletions blocks/rich-text/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ describe( 'getFormatProperties', () => {
nodeName: 'A',
attributes: {
href: 'https://www.testing.com',
target: '_blank',
},
};

Expand All @@ -156,7 +157,7 @@ describe( 'getFormatProperties', () => {
expect( getFormatProperties( formatName, [ { ...node, nodeName: 'P' } ] ) ).toEqual( {} );
} );

test( 'should return an object of value and node for a link', () => {
test( 'should return a populated object', () => {
const mockNode = {
...node,
getAttribute: jest.fn().mockImplementation( ( attr ) => mockNode.attributes[ attr ] ),
Expand All @@ -168,11 +169,12 @@ describe( 'getFormatProperties', () => {

expect( getFormatProperties( formatName, parents ) ).toEqual( {
value: 'https://www.testing.com',
target: '_blank',
node: mockNode,
} );
} );

test( 'should return an object of value and node of empty values when no values are found.', () => {
test( 'should return an object with empty values when no link is found', () => {
const mockNode = {
...node,
attributes: {},
Expand All @@ -185,6 +187,7 @@ describe( 'getFormatProperties', () => {

expect( getFormatProperties( formatName, parents ) ).toEqual( {
value: '',
target: '',
node: mockNode,
} );
} );
Expand Down

0 comments on commit 43a58b2

Please sign in to comment.