diff --git a/blocks/rich-text/format-toolbar/index.js b/blocks/rich-text/format-toolbar/index.js index edc1c54a61b749..b53cf3cd3336c0 100644 --- a/blocks/rich-text/format-toolbar/index.js +++ b/blocks/rich-text/format-toolbar/index.js @@ -11,9 +11,10 @@ import { keycodes } from '@wordpress/utils'; */ import './style.scss'; import UrlInput from '../../url-input'; +import { LINK_PLACEHOLDER_VALUE } from '../'; import { filterURLForDisplay } from '../../../editor/utils/url'; -const { ESCAPE, LEFT, RIGHT, UP, DOWN, BACKSPACE, ENTER } = keycodes; +const { ESCAPE } = keycodes; const FORMATTING_CONTROLS = [ { @@ -32,8 +33,6 @@ const FORMATTING_CONTROLS = [ format: 'strikethrough', }, { - icon: 'admin-links', - title: __( 'Link' ), format: 'link', }, ]; @@ -49,7 +48,6 @@ class FormatToolbar extends Component { super( ...arguments ); this.state = { - isAddingLink: false, isEditingLink: false, newLinkValue: '', }; @@ -63,21 +61,20 @@ class FormatToolbar extends Component { } onKeyDown( event ) { + event.stopPropagation(); + if ( event.keyCode === ESCAPE ) { - if ( this.state.isEditingLink ) { - event.stopPropagation(); + this.setState( { isEditingLink: false, newLinkValue: '' } ); + + if ( this.props.formats.link.value === LINK_PLACEHOLDER_VALUE ) { this.dropLink(); } } - if ( [ LEFT, DOWN, RIGHT, UP, BACKSPACE, ENTER ].indexOf( event.keyCode ) > -1 ) { - stopKeyPropagation( event ); - } } componentWillReceiveProps( nextProps ) { if ( this.props.selectedNodeId !== nextProps.selectedNodeId ) { this.setState( { - isAddingLink: false, isEditingLink: false, newLinkValue: '', } ); @@ -97,25 +94,28 @@ class FormatToolbar extends Component { } addLink() { - this.setState( { isEditingLink: false, isAddingLink: true, newLinkValue: '' } ); + this.props.onChange( { link: { value: LINK_PLACEHOLDER_VALUE } } ); } dropLink() { this.props.onChange( { link: undefined } ); - this.setState( { isEditingLink: false, isAddingLink: false, newLinkValue: '' } ); } editLink( event ) { event.preventDefault(); - this.setState( { isEditingLink: false, isAddingLink: true, newLinkValue: this.props.formats.link.value } ); + this.setState( { isEditingLink: true, newLinkValue: this.props.formats.link.value } ); } submitLink( event ) { event.preventDefault(); - this.props.onChange( { link: { value: this.state.newLinkValue } } ); - if ( this.state.isAddingLink ) { + + this.setState( { isEditingLink: false, newLinkValue: '' } ); + + if ( this.props.formats.link.value === LINK_PLACEHOLDER_VALUE ) { this.props.speak( __( 'Link added.' ), 'assertive' ); } + + this.props.onChange( { link: { value: this.state.newLinkValue } } ); } isFormatActive( format ) { @@ -124,69 +124,79 @@ 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 { isEditingLink, newLinkValue } = this.state; const toolbarControls = FORMATTING_CONTROLS.concat( customControls ) - .filter( control => enabledControls.indexOf( control.format ) !== -1 ) + .filter( control => enabledControls.includes( control.format ) ) .map( ( control ) => { - const isLink = control.format === 'link'; + const isActive = this.isFormatActive( control.format ); + + if ( control.format === 'link' ) { + return { + ...control, + icon: isActive ? 'editor-unlink' : 'admin-links', // TODO: proper unlink icon + title: isActive ? __( 'Unlink' ) : __( 'Link' ), + isActive, + onClick: isActive ? this.dropLink : this.addLink, + }; + } + return { ...control, - onClick: isLink ? this.addLink : this.toggleFormat( control.format ), - isActive: this.isFormatActive( control.format ) || ( isLink && isAddingLink ), + isActive, + onClick: this.toggleFormat( control.format ), }; } ); + const hasLinkUI = !! formats.link; + const hasEditLinkUI = hasLinkUI && ( isEditingLink || formats.link.value === LINK_PLACEHOLDER_VALUE ); + const hasViewLinkUI = hasLinkUI && ! isEditingLink && formats.link.value !== LINK_PLACEHOLDER_VALUE; + return (
- { ( isAddingLink || isEditingLink ) && - // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar - /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ - -
-
- - - -
-
-
- /* 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 */ + { hasLinkUI && -
-
- + { hasEditLinkUI && + // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ +
+
+ + +
+
+ /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ + } + + { hasViewLinkUI && + // Disable reason: KeyPress must be suppressed so the block doesn't hide the toolbar + /* eslint-disable jsx-a11y/no-static-element-interactions */ +
+ +
+ /* eslint-enable jsx-a11y/no-static-element-interactions */ + }
- /* eslint-enable jsx-a11y/no-static-element-interactions */ }
); diff --git a/blocks/rich-text/format-toolbar/style.scss b/blocks/rich-text/format-toolbar/style.scss index 609255d2e3ffc8..7f761c080a66c9 100644 --- a/blocks/rich-text/format-toolbar/style.scss +++ b/blocks/rich-text/format-toolbar/style.scss @@ -3,11 +3,11 @@ } .blocks-format-toolbar__link-modal { - position: absolute; + position: relative; + left: -50%; box-shadow: 0px 3px 20px rgba( 18, 24, 30, .1 ), 0px 1px 3px rgba( 18, 24, 30, .1 ); border: 1px solid #e0e5e9; background: #fff; - width: 300px; display: flex; flex-direction: column; font-family: $default-font; @@ -37,8 +37,4 @@ position: relative; white-space: nowrap; min-width: 0; - - &:after { - @include long-content-fade( $size: 40% ); - } } diff --git a/blocks/rich-text/index.js b/blocks/rich-text/index.js index eeeebff9581f13..8d7a783142a9e5 100644 --- a/blocks/rich-text/index.js +++ b/blocks/rich-text/index.js @@ -72,6 +72,12 @@ export function getFormatProperties( formatName, parents ) { const DEFAULT_FORMATS = [ 'bold', 'italic', 'strikethrough', 'link' ]; +/** + * When inserting a new link, we insert an tag with this placeholder href + * so that there is a visual indication of which text will be made into a link. + */ +export const LINK_PLACEHOLDER_VALUE = '_wp_link_placeholder'; + export default class RichText extends Component { constructor( props ) { super( ...arguments ); @@ -428,11 +434,10 @@ export default 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, }; } @@ -656,22 +661,41 @@ export default class RichText extends Component { ); } - onNodeChange( { parents } ) { + removePlaceholderLinks() { + this.editor.$( 'a' ).each( ( index, element ) => { + if ( element.getAttribute( 'href' ) === LINK_PLACEHOLDER_VALUE ) { + this.editor.dom.remove( element, true ); + } + } ); + } + + getFormats( parents ) { + return this.editor.formatter.matchAll( this.props.formattingControls ).reduce( ( formats, format ) => { + formats[ format ] = { + isActive: true, + ...getFormatProperties( format, parents ), + }; + return formats; + }, {} ); + } + + onNodeChange( event ) { if ( document.activeElement !== this.editor.getBody() ) { return; } - const formatNames = this.props.formattingControls; - const formats = this.editor.formatter.matchAll( formatNames ).reduce( ( accFormats, activeFormat ) => { - accFormats[ activeFormat ] = { - isActive: true, - ...getFormatProperties( activeFormat, parents ), - }; - return accFormats; - }, {} ); + const formats = this.getFormats( event.parents ); + + // Remove all placeholder links if selection moves away from a placeholder link + if ( ! formats.link || formats.link.value !== LINK_PLACEHOLDER_VALUE ) { + this.removePlaceholderLinks(); + } - const focusPosition = this.getFocusPosition(); - this.setState( { formats, focusPosition, selectedNodeId: this.state.selectedNodeId + 1 } ); + this.setState( { + formats, + focusPosition: this.getFocusPosition(), + selectedNodeId: this.state.selectedNodeId + 1, + } ); } updateContent() { diff --git a/blocks/url-input/index.js b/blocks/url-input/index.js index a98d0df14025aa..755707fae0b2c2 100644 --- a/blocks/url-input/index.js +++ b/blocks/url-input/index.js @@ -177,11 +177,9 @@ class UrlInput extends Component { render() { const { value, instanceId } = this.props; const { showSuggestions, posts, selectedSuggestion, loading } = this.state; - /* eslint-disable jsx-a11y/no-autofocus */ return (
); - /* eslint-enable jsx-a11y/no-autofocus */ } }