diff --git a/packages/format-library/src/link/index.js b/packages/format-library/src/link/index.js
index 7455c75b55809..fdb62566ad99e 100644
--- a/packages/format-library/src/link/index.js
+++ b/packages/format-library/src/link/index.js
@@ -92,6 +92,7 @@ export const link = {
render() {
const { isActive, activeAttributes, value, onChange } = this.props;
+ const { addingLink } = this.state;
return (
<>
@@ -123,15 +124,16 @@ export const link = {
shortcutType="primary"
shortcutCharacter="k"
/> }
-
+ { ( isActive || addingLink ) && (
+
+ ) }
>
);
}
diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js
index 34c05ad432c0c..673f9d7667b6e 100644
--- a/packages/format-library/src/link/inline.js
+++ b/packages/format-library/src/link/inline.js
@@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { Component, createRef, useMemo } from '@wordpress/element';
+import { useRef, useState, useMemo, useEffect } from '@wordpress/element';
import {
ToggleControl,
withSpokenMessages,
@@ -26,11 +26,7 @@ import { createLinkFormat, isValidHref } from './utils';
const stopKeyPropagation = ( event ) => event.stopPropagation();
-function isShowingInput( props, state ) {
- return props.addingLink || state.editLink;
-}
-
-const URLPopoverAtLink = ( { isActive, addingLink, value, ...props } ) => {
+export const URLPopoverAtLink = ( { isActive, addingLink, value, ...props } ) => {
const anchorRef = useMemo( () => {
const selection = window.getSelection();
@@ -63,84 +59,62 @@ const URLPopoverAtLink = ( { isActive, addingLink, value, ...props } ) => {
return ;
};
-class InlineLinkUI extends Component {
- constructor() {
- super( ...arguments );
-
- this.editLink = this.editLink.bind( this );
- this.submitLink = this.submitLink.bind( this );
- this.onKeyDown = this.onKeyDown.bind( this );
- this.onChangeInputValue = this.onChangeInputValue.bind( this );
- this.setLinkTarget = this.setLinkTarget.bind( this );
- this.onFocusOutside = this.onFocusOutside.bind( this );
- this.resetState = this.resetState.bind( this );
- this.autocompleteRef = createRef();
-
- this.state = {
- opensInNewWindow: false,
- inputValue: '',
- };
- }
-
- static getDerivedStateFromProps( props, state ) {
- const { activeAttributes: { url, target } } = props;
- const opensInNewWindow = target === '_blank';
-
- if ( ! isShowingInput( props, state ) ) {
- const update = {};
- if ( url !== state.inputValue ) {
- update.inputValue = url;
- }
-
- if ( opensInNewWindow !== state.opensInNewWindow ) {
- update.opensInNewWindow = opensInNewWindow;
- }
- return Object.keys( update ).length ? update : null;
+function InlineLinkUI( {
+ isActive,
+ activeAttributes,
+ addingLink,
+ value,
+ onChange,
+ stopAddingLink,
+ speak,
+} ) {
+ const [ opensInNewWindow, setOpensInNewWindow ] = useState( false );
+ const [ inputValue, setInputValue ] = useState( '' );
+ const [ isEditingLink, setIsEditingLink ] = useState( false );
+ const autocompleteRef = useRef();
+
+ const { url, target } = activeAttributes;
+ const isShowingInput = addingLink || isEditingLink;
+
+ useEffect( () => {
+ if ( ! isShowingInput ) {
+ setInputValue( url );
+ setOpensInNewWindow( target === '_blank' );
}
+ } );
- return null;
- }
-
- onKeyDown( event ) {
+ function onKeyDown( event ) {
if ( [ LEFT, DOWN, RIGHT, UP, BACKSPACE, ENTER ].indexOf( event.keyCode ) > -1 ) {
// Stop the key event from propagating up to ObserveTyping.startTypingInTextField.
event.stopPropagation();
}
}
- onChangeInputValue( inputValue ) {
- this.setState( { inputValue } );
- }
-
- setLinkTarget( opensInNewWindow ) {
- const { activeAttributes: { url = '' }, value, onChange } = this.props;
-
- this.setState( { opensInNewWindow } );
+ function setLinkTarget( nextOpensInNewWindow ) {
+ setOpensInNewWindow( nextOpensInNewWindow );
// Apply now if URL is not being edited.
- if ( ! isShowingInput( this.props, this.state ) ) {
+ if ( ! isShowingInput ) {
const selectedText = getTextContent( slice( value ) );
onChange( applyFormat( value, createLinkFormat( {
url,
- opensInNewWindow,
+ opensInNewWindow: nextOpensInNewWindow,
text: selectedText,
} ) ) );
}
}
- editLink( event ) {
- this.setState( { editLink: true } );
+ function editLink( event ) {
+ setIsEditingLink( true );
event.preventDefault();
}
- submitLink( event ) {
- const { isActive, value, onChange, speak } = this.props;
- const { inputValue, opensInNewWindow } = this.state;
- const url = prependHTTP( inputValue );
+ function submitLink( event ) {
+ const nextURL = prependHTTP( inputValue );
const selectedText = getTextContent( slice( value ) );
const format = createLinkFormat( {
- url,
+ url: nextURL,
opensInNewWindow,
text: selectedText,
} );
@@ -148,15 +122,15 @@ class InlineLinkUI extends Component {
event.preventDefault();
if ( isCollapsed( value ) && ! isActive ) {
- const toInsert = applyFormat( create( { text: url } ), format, 0, url.length );
+ const toInsert = applyFormat( create( { text: nextURL } ), format, 0, nextURL.length );
onChange( insert( value, toInsert ) );
} else {
onChange( applyFormat( value, format ) );
}
- this.resetState();
+ resetState();
- if ( ! isValidHref( url ) ) {
+ if ( ! isValidHref( nextURL ) ) {
speak( __( 'Warning: the link has been inserted but may have errors. Please test it.' ), 'assertive' );
} else if ( isActive ) {
speak( __( 'Link edited.' ), 'assertive' );
@@ -165,72 +139,61 @@ class InlineLinkUI extends Component {
}
}
- onFocusOutside() {
+ function onFocusOutside() {
// The autocomplete suggestions list renders in a separate popover (in a portal),
// so onFocusOutside fails to detect that a click on a suggestion occurred in the
// LinkContainer. Detect clicks on autocomplete suggestions using a ref here, and
// return to avoid the popover being closed.
- const autocompleteElement = this.autocompleteRef.current;
+ const autocompleteElement = autocompleteRef.current;
if ( autocompleteElement && autocompleteElement.contains( document.activeElement ) ) {
return;
}
- this.resetState();
+ resetState();
}
- resetState() {
- this.props.stopAddingLink();
- this.setState( { editLink: false } );
+ function resetState() {
+ stopAddingLink();
+ setIsEditingLink( false );
}
- render() {
- const { isActive, activeAttributes: { url }, addingLink, value } = this.props;
-
- if ( ! isActive && ! addingLink ) {
- return null;
- }
-
- const { inputValue, opensInNewWindow } = this.state;
- const showInput = isShowingInput( this.props, this.state );
-
- return (
- (
-
- ) }
- >
- { showInput ? (
-
- ) : (
-
- ) }
-
- );
- }
+ return (
+ (
+
+ ) }
+ >
+ { isShowingInput ? (
+
+ ) : (
+
+ ) }
+
+ );
}
export default withSpokenMessages( InlineLinkUI );
diff --git a/packages/format-library/src/link/test/inline.js b/packages/format-library/src/link/test/inline.js
index fa5201992c10b..18928a94cbc8f 100644
--- a/packages/format-library/src/link/test/inline.js
+++ b/packages/format-library/src/link/test/inline.js
@@ -1,38 +1,70 @@
/**
- * Internal dependencies
+ * External dependencies
*/
-import InlineLinkUI from '../inline';
+import TestRenderer, { act } from 'react-test-renderer';
+
/**
- * External dependencies
+ * WordPress dependencies
*/
-import { shallow } from 'enzyme';
+import { create } from '@wordpress/rich-text';
+
+/**
+ * Internal dependencies
+ */
+import InlineLinkUI, { URLPopoverAtLink } from '../inline';
describe( 'InlineLinkUI', () => {
- it( 'InlineLinkUI renders', () => {
- const wrapper = shallow(
-
- );
- expect( wrapper ).toBeTruthy();
+ const defaultProps = {
+ value: create( { text: '' } ),
+ activeAttributes: {},
+ };
+
+ // TODO: At the time of writing, JSDOM has just implemented Selection APIs,
+ // but they are not yet published in a publicly-available release. This
+ // `window.getSelection` assignment should be non-harmful but also redundant
+ // once this becomes available. In other words, once the next JSDOM feature
+ // release is published, this lifecycle code should be able to be removed.
+ //
+ // See: https://github.com/jsdom/jsdom/pull/2719
+ let originalWindowGetSelection;
+ beforeAll( () => {
+ originalWindowGetSelection = window.getSelection;
+ window.getSelection = () => ( { rangeCount: 0 } );
+ } );
+
+ afterAll( () => {
+ window.getSelection = originalWindowGetSelection;
} );
- it( 'should set state.opensInNewWindow to false by default', () => {
- const wrapper = shallow(
-
- ).dive();
+ it( 'should set "Opens in New Tab" toggle to unchecked by default', () => {
+ let renderer;
+ act( () => {
+ renderer = TestRenderer.create( );
+ } );
- expect( wrapper.state( 'opensInNewWindow' ) ).toEqual( false );
+ const openInNewTabToggle = renderer.root.findByType( URLPopoverAtLink ).props.renderSettings();
+ expect( openInNewTabToggle.props.checked ).toBe( false );
} );
- it( 'should set state.opensInNewWindow to true if props.activeAttributes.target is _blank', () => {
- const givenProps = {
- addingLink: false,
- activeAttributes: { url: 'http://www.google.com', target: '_blank' },
- };
-
- const wrapper = shallow(
-
- ).dive();
- wrapper.setProps( givenProps );
- expect( wrapper.state( 'opensInNewWindow' ) ).toEqual( true );
+ it( 'should set "Opens in New Tab" toggle to checked if props.activeAttributes.target is _blank', () => {
+ let renderer;
+ act( () => {
+ renderer = TestRenderer.create( );
+ } );
+
+ act( () => {
+ renderer.update(
+
+ );
+ } );
+
+ const openInNewTabToggle = renderer.root.findByType( URLPopoverAtLink ).props.renderSettings();
+ expect( openInNewTabToggle.props.checked ).toBe( true );
} );
} );