diff --git a/packages/block-editor/src/components/link-control/README.md b/packages/block-editor/src/components/link-control/README.md index af25dfc2f42fe..2fb785798ab1e 100644 --- a/packages/block-editor/src/components/link-control/README.md +++ b/packages/block-editor/src/components/link-control/README.md @@ -7,12 +7,12 @@ - Type: `String` - Required: Yes -### currentLink +### value - Type: `Object` - Required: Yes -### currentSettings +### settings - Type: `Array` - Required: No @@ -20,14 +20,13 @@ ``` [ { - id: 'newTab', + id: 'opensInNewTab', title: 'Open in New Tab', - checked: false, }, ]; ``` -An array of settings objects. Each object will used to render a `ToggleControl` for that setting. See also `onSettingsChange`. +An array of settings objects. Each object will used to render a `ToggleControl` for that setting. ### fetchSearchSuggestions @@ -36,36 +35,12 @@ An array of settings objects. Each object will used to render a `ToggleControl` ## Event handlers -### onChangeMode - -- Type: `Function` -- Required: No - -Use this callback to know when the LinkControl component changes its mode to `edit` or `show` -through of its function parameter. - -```es6 - { console.log( `Mode change to ${ mode } mode.` ) } -/> -``` - ### onClose - Type: `Function` - Required: No -### onKeyDown - -- Type: `Function` -- Required: No - -### onKeyPress - -- Type: `Function` -- Required: No - -### onLinkChange +### onChange - Type: `Function` - Required: No @@ -81,29 +56,5 @@ The function callback will receive the selected item, or Null. : console.warn( 'No Item selected.' ); } /> -``` - -### onSettingsChange - -- Type: `Function` -- Required: No -- Args: - - `id` - the `id` property of the setting that changed (eg: `newTab`). - - `value` - the `checked` value of the control. - - `settings` - the current settings object. - -Called when any of the settings supplied as `currentSettings` are changed/toggled. May be used to attribute a Block's `attributes` with the current state of the control. - -``` - setAttributes( { [ setting ]: value } ) } -/> ``` diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js index 18ae9da7c5401..ac69db9b655fe 100644 --- a/packages/block-editor/src/components/link-control/index.js +++ b/packages/block-editor/src/components/link-control/index.js @@ -17,7 +17,6 @@ import { __ } from '@wordpress/i18n'; import { useCallback, useState, - useEffect, Fragment, } from '@wordpress/element'; @@ -44,40 +43,26 @@ const MODE_EDIT = 'edit'; function LinkControl( { className, - currentLink, - currentSettings, + value, + settings, fetchSearchSuggestions, instanceId, onClose = noop, - onChangeMode = noop, - onKeyDown = noop, - onKeyPress = noop, - onLinkChange = noop, - onSettingsChange = noop, + onChange = noop, } ) { // State const [ inputValue, setInputValue ] = useState( '' ); - const [ isEditingLink, setIsEditingLink ] = useState( false ); - - // Effects - useEffect( () => { - // If we have a link then stop editing mode - if ( currentLink ) { - setIsEditingLink( false ); - } else { - setIsEditingLink( true ); - } - }, [ currentLink ] ); + const [ isEditingLink, setIsEditingLink ] = useState( ! value || ! value.url ); // Handlers /** * onChange LinkControlSearchInput event handler * - * @param {string} value Current value returned by the search. + * @param {string} val Current value returned by the search. */ - const onInputChange = ( value = '' ) => { - setInputValue( value ); + const onInputChange = ( val = '' ) => { + setInputValue( val ); }; // Utils @@ -85,7 +70,6 @@ function LinkControl( { /** * Handler function which switches the mode of the component, * between `edit` and `show` mode. - * Also, it calls `onChangeMode` callback function. * * @param {string} mode Component mode: `show` or `edit`. */ @@ -94,12 +78,8 @@ function LinkControl( { // Populate input searcher whether // the current link has a title. - if ( currentLink && currentLink.title ) { - setInputValue( currentLink.title ); - } - - if ( isFunction( onChangeMode ) ) { - onChangeMode( mode ); + if ( value && value.title && mode === 'edit' ) { + setInputValue( value.title ); } }; @@ -112,10 +92,10 @@ function LinkControl( { setInputValue( '' ); }; - const handleDirectEntry = ( value ) => { + const handleDirectEntry = ( val ) => { let type = 'URL'; - const protocol = getProtocol( value ) || ''; + const protocol = getProtocol( val ) || ''; if ( protocol.includes( 'mailto' ) ) { type = 'mailto'; @@ -125,27 +105,27 @@ function LinkControl( { type = 'tel'; } - if ( startsWith( value, '#' ) ) { + if ( startsWith( val, '#' ) ) { type = 'internal'; } return Promise.resolve( [ { id: '-1', - title: value, - url: type === 'URL' ? prependHTTP( value ) : value, + title: val, + url: type === 'URL' ? prependHTTP( val ) : val, type, } ] ); }; - const handleEntitySearch = async ( value ) => { + const handleEntitySearch = async ( val ) => { const results = await Promise.all( [ - fetchSearchSuggestions( value ), - handleDirectEntry( value ), + fetchSearchSuggestions( val ), + handleDirectEntry( val ), ] ); - const couldBeURL = ! value.includes( ' ' ); + const couldBeURL = ! val.includes( ' ' ); // If it's potentially a URL search then concat on a URL search suggestion // just for good measure. That way once the actual results run out we always @@ -154,15 +134,15 @@ function LinkControl( { }; // Effects - const getSearchHandler = useCallback( ( value ) => { - const protocol = getProtocol( value ) || ''; + const getSearchHandler = useCallback( ( val ) => { + const protocol = getProtocol( val ) || ''; const isMailto = protocol.includes( 'mailto' ); - const isInternal = startsWith( value, '#' ); + const isInternal = startsWith( val, '#' ); const isTel = protocol.includes( 'tel' ); - const handleManualEntry = isInternal || isMailto || isTel || isURL( value ) || ( value && value.includes( 'www.' ) ); + const handleManualEntry = isInternal || isMailto || isTel || isURL( val ) || ( val && val.includes( 'www.' ) ); - return ( handleManualEntry ) ? handleDirectEntry( value ) : handleEntitySearch( value ); + return ( handleManualEntry ) ? handleDirectEntry( val ) : handleEntitySearch( val ); }, [ handleDirectEntry, fetchSearchSuggestions ] ); // Render Components @@ -181,7 +161,10 @@ function LinkControl( { key={ `${ suggestion.id }-${ suggestion.type }` } itemProps={ buildSuggestionItemProps( suggestion, index ) } suggestion={ suggestion } - onClick={ () => onLinkChange( suggestion ) } + onClick={ () => { + setIsEditingLink( false ); + onChange( { ...value, ...suggestion } ); + } } isSelected={ index === selectedSuggestion } isURL={ manualLinkEntryTypes.includes( suggestion.type.toLowerCase() ) } searchTerm={ inputValue } @@ -202,7 +185,7 @@ function LinkControl( {
- { ( ! isEditingLink && currentLink ) && ( + { ( ! isEditingLink ) && (

{ __( 'Currently selected' ) }: @@ -215,14 +198,13 @@ function LinkControl( { } ) } > - - { currentLink.title } + { value.title } - { filterURLForDisplay( safeDecodeURI( currentLink.url ) ) || '' } + { filterURLForDisplay( safeDecodeURI( value.url ) ) || '' }

diff --git a/packages/block-editor/src/components/link-control/search-input.js b/packages/block-editor/src/components/link-control/search-input.js index 615f062b3fe01..9360ef0080137 100644 --- a/packages/block-editor/src/components/link-control/search-input.js +++ b/packages/block-editor/src/components/link-control/search-input.js @@ -4,13 +4,32 @@ */ import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; -import { ENTER } from '@wordpress/keycodes'; +import { LEFT, + RIGHT, + UP, + DOWN, + BACKSPACE, + ENTER, +} from '@wordpress/keycodes'; /** * Internal dependencies */ import { URLInput } from '../'; +const handleLinkControlOnKeyDown = ( event ) => { + const { keyCode } = event; + + if ( [ LEFT, DOWN, RIGHT, UP, BACKSPACE, ENTER ].indexOf( keyCode ) > -1 ) { + // Stop the key event from propagating up to ObserveTyping.startTypingInTextField. + event.stopPropagation(); + } +}; + +const handleLinkControlOnKeyPress = ( event ) => { + event.stopPropagation(); +}; + const LinkControlSearchInput = ( { value, onChange, @@ -18,8 +37,6 @@ const LinkControlSearchInput = ( { renderSuggestions, fetchSuggestions, onReset, - onKeyDown, - onKeyPress, } ) => { const selectItemHandler = ( selection, suggestion ) => { onChange( selection ); @@ -44,9 +61,9 @@ const LinkControlSearchInput = ( { if ( event.keyCode === ENTER ) { return; } - onKeyDown( event ); + handleLinkControlOnKeyDown( event ); } } - onKeyPress={ onKeyPress } + onKeyPress={ handleLinkControlOnKeyPress } placeholder={ __( 'Search or type url' ) } __experimentalRenderSuggestions={ renderSuggestions } __experimentalFetchLinkSuggestions={ fetchSuggestions } diff --git a/packages/block-editor/src/components/link-control/settings-drawer.js b/packages/block-editor/src/components/link-control/settings-drawer.js index 3d9f957e207a8..3100ea87ef661 100644 --- a/packages/block-editor/src/components/link-control/settings-drawer.js +++ b/packages/block-editor/src/components/link-control/settings-drawer.js @@ -13,19 +13,21 @@ import { const defaultSettings = [ { - id: 'newTab', + id: 'opensInNewTab', title: __( 'Open in New Tab' ), - checked: false, }, ]; -const LinkControlSettingsDrawer = ( { settings = defaultSettings, onSettingChange = noop } ) => { +const LinkControlSettingsDrawer = ( { value, onChange = noop, settings = defaultSettings } ) => { if ( ! settings || ! settings.length ) { return null; } - const handleSettingChange = ( setting ) => ( value ) => { - onSettingChange( setting.id, value, settings ); + const handleSettingChange = ( setting ) => ( newValue ) => { + onChange( { + ...value, + [ setting.id ]: newValue, + } ); }; const theSettings = settings.map( ( setting ) => ( @@ -34,7 +36,7 @@ const LinkControlSettingsDrawer = ( { settings = defaultSettings, onSettingChang key={ setting.id } label={ setting.title } onChange={ handleSettingChange( setting ) } - checked={ setting.checked } /> + checked={ value ? value[ setting.id ] : false } /> ) ); return ( diff --git a/packages/block-editor/src/components/link-control/test/index.js b/packages/block-editor/src/components/link-control/test/index.js index 5a69142b5f495..ca91c9791384c 100644 --- a/packages/block-editor/src/components/link-control/test/index.js +++ b/packages/block-editor/src/components/link-control/test/index.js @@ -318,7 +318,7 @@ describe( 'Selecting links', () => { return ( ); @@ -343,17 +343,15 @@ describe( 'Selecting links', () => { it( 'should hide "selected" link UI and display search UI prepopulated with previously selected link title when "Change" button is clicked', () => { const selectedLink = first( fauxEntitySuggestions ); - const spyOnEditMode = jest.fn(); const LinkControlConsumer = () => { const [ link, setLink ] = useState( selectedLink ); return ( setLink( suggestion ) } + value={ link } + onChange={ ( suggestion ) => setLink( suggestion ) } fetchSearchSuggestions={ fetchFauxEntitySuggestions } - onChangeMode={ spyOnEditMode( 'edit' ) } /> ); }; @@ -380,7 +378,6 @@ describe( 'Selecting links', () => { expect( searchInput ).not.toBeNull(); expect( searchInput.value ).toBe( selectedLink.title ); // prepopulated with previous link's title expect( currentLinkUI ).toBeNull(); - expect( spyOnEditMode ).toHaveBeenCalled(); } ); describe( 'Selection using mouse click', () => { @@ -398,8 +395,8 @@ describe( 'Selecting links', () => { return ( setLink( suggestion ) } + value={ link } + onChange={ ( suggestion ) => setLink( suggestion ) } fetchSearchSuggestions={ fetchFauxEntitySuggestions } /> ); @@ -458,8 +455,8 @@ describe( 'Selecting links', () => { return ( setLink( suggestion ) } + value={ link } + onChange={ ( suggestion ) => setLink( suggestion ) } fetchSearchSuggestions={ fetchFauxEntitySuggestions } /> ); @@ -547,7 +544,7 @@ describe( 'Addition Settings UI', () => { return ( ); @@ -577,12 +574,10 @@ describe( 'Addition Settings UI', () => { { id: 'newTab', title: 'Open in New Tab', - checked: false, }, { id: 'noFollow', title: 'No follow', - checked: true, }, ]; @@ -593,9 +588,9 @@ describe( 'Addition Settings UI', () => { return ( ); }; diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index 927308b6a9d02..52b69d1184c49 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -37,12 +37,6 @@ import { __experimentalLinkControl as LinkControl, } from '@wordpress/block-editor'; import { - LEFT, - RIGHT, - UP, - DOWN, - BACKSPACE, - ENTER, rawShortcut, displayShortcut, } from '@wordpress/keycodes'; @@ -88,19 +82,6 @@ function BorderPanel( { borderRadius = '', setAttributes } ) { ); } -const handleLinkControlOnKeyDown = ( event ) => { - const { keyCode } = event; - - if ( [ LEFT, DOWN, RIGHT, UP, BACKSPACE, ENTER ].indexOf( keyCode ) > -1 ) { - // Stop the key event from propagating up to ObserveTyping.startTypingInTextField. - event.stopPropagation(); - } -}; - -const handleLinkControlOnKeyPress = ( event ) => { - event.stopPropagation(); -}; - function URLPicker( { isSelected, url, title, setAttributes, opensInNewTab, onToggleOpenInNewTab } ) { const [ isURLPickerOpen, setIsURLPickerOpen ] = useState( false ); useEffect( @@ -117,25 +98,15 @@ function URLPicker( { isSelected, url, title, setAttributes, opensInNewTab, onTo const linkControl = isURLPickerOpen && ( { + value={ { url, title, opensInNewTab } } + onChange={ ( { title: newTitle = '', url: newURL = '', opensInNewTab: newOpensInNewTab } ) => { setAttributes( { title: escape( newTitle ), url: newURL, } ); - } } - currentSettings={ [ - { - id: 'opensInNewTab', - title: __( 'Open in new tab' ), - checked: opensInNewTab, - }, - ] } - onSettingsChange={ ( setting, value ) => { - if ( setting === 'opensInNewTab' ) { - onToggleOpenInNewTab( value ); + + if ( opensInNewTab !== newOpensInNewTab ) { + onToggleOpenInNewTab( newOpensInNewTab ); } } } onClose={ () => { diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index 5356a873d7493..179ba9451c87e 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -23,12 +23,6 @@ import { ToolbarGroup, } from '@wordpress/components'; import { - LEFT, - RIGHT, - UP, - DOWN, - BACKSPACE, - ENTER, rawShortcut, displayShortcut, } from '@wordpress/keycodes'; @@ -42,35 +36,6 @@ import { } from '@wordpress/block-editor'; import { Fragment, useState, useEffect } from '@wordpress/element'; -/** - * It updates the link attribute when the - * link settings changes. - * - * @param {Function} setter Setter attribute function. - */ -const updateLinkSetting = ( setter ) => ( setting, value ) => { - setter( { [ setting ]: value } ); -}; - -/** - * Updates the link attribute when it changes - * through of the `onLinkChange` LinkControl callback. - * - * @param {Function} setter Setter attribute function. - * @param {string} label Link label. - */ -const updateLink = ( setter, label ) => ( { title: newTitle = '', url: newURL = '' } = {} ) => { - setter( { - title: escape( newTitle ), - url: newURL, - } ); - - // Set the item label as well if it isn't already defined. - if ( ! label ) { - setter( { label: escape( newTitle ) } ); - } -}; - function NavigationLinkEdit( { attributes, hasDescendants, @@ -80,59 +45,34 @@ function NavigationLinkEdit( { insertLinkBlock, } ) { const { label, opensInNewTab, title, url, nofollow, description } = attributes; - const link = title ? { title: unescape( title ), url } : null; - const [ isLinkOpen, setIsLinkOpen ] = useState( ! label && isSelected ); + const link = { + title: title ? unescape( title ) : '', + url, + opensInNewTab, + }; + const [ isLinkOpen, setIsLinkOpen ] = useState( false ); + const itemLabelPlaceholder = __( 'Add link…' ); - let onCloseTimerId = null; + // Show the LinkControl on mount if the URL is empty + // ( When adding a new menu item) + // This can't be done in the useState call because it cconflicts + // with the autofocus behavior of the BlockListBlock component. + useEffect( () => { + if ( ! url ) { + setIsLinkOpen( true ); + } + }, [] ); /** - * It's a kind of hack to handle closing the LinkControl popover - * clicking on the ToolbarButton link. + * The hook shouldn't be necessary but due to a focus loss happening + * when selecting a suggestion in the link popover, we force close on block unselection. */ useEffect( () => { if ( ! isSelected ) { setIsLinkOpen( false ); } - - return () => { - // Clear LinkControl.OnClose timeout. - if ( onCloseTimerId ) { - clearTimeout( onCloseTimerId ); - } - }; }, [ isSelected ] ); - /** - * Opens the LinkControl popup - */ - const openLinkControl = () => { - if ( isLinkOpen ) { - return; - } - - setIsLinkOpen( ! isLinkOpen ); - }; - - /** - * `onKeyDown` LinkControl handler. - * It takes over to stop the event propagation to make the - * navigation work, avoiding undesired behaviors. - * For instance, it will block to move between link blocks - * when the LinkControl is focused. - * - * @param {Event} event - */ - const handleLinkControlOnKeyDown = ( event ) => { - const { keyCode } = event; - - if ( [ LEFT, DOWN, RIGHT, UP, BACKSPACE, ENTER ].indexOf( keyCode ) > -1 ) { - // Stop the key event from propagating up to ObserveTyping.startTypingInTextField. - event.stopPropagation(); - } - }; - - const itemLabelPlaceholder = __( 'Add link…' ); - return ( @@ -140,7 +80,7 @@ function NavigationLinkEdit( { setIsLinkOpen( true ), } } /> setIsLinkOpen( true ) } /> event.stopPropagation() } - currentLink={ link } - onLinkChange={ updateLink( setAttributes, label ) } - onClose={ () => { - onCloseTimerId = setTimeout( () => setIsLinkOpen( false ), 100 ); - } } - currentSettings={ [ - { - id: 'opensInNewTab', - title: __( 'Open in new tab' ), - checked: opensInNewTab, - }, - ] } - onSettingsChange={ updateLinkSetting( setAttributes ) } + value={ link } + onChange={ ( { + title: newTitle = '', + url: newURL = '', + opensInNewTab: newOpensInNewTab, + } = {} ) => setAttributes( { + title: escape( newTitle ), + url: newURL, + label: label || escape( newTitle ), + opensInNewTab: newOpensInNewTab, + } ) } + onClose={ () => setIsLinkOpen( false ) } /> ) }