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

Make Nav Link Block text also editable from within the hyperlinklink creation UI #19413

Closed
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
57 changes: 4 additions & 53 deletions packages/block-editor/src/components/link-control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
- Type: `String`
- Required: Yes

### currentLink
### value

- Type: `Object`
- Required: Yes

### currentSettings
### settings

- Type: `Array`
- Required: No
Expand All @@ -22,12 +22,11 @@
{
id: 'newTab',
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

Expand All @@ -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
<LinkControl
onChangeMode={ ( mode ) => { 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
Expand All @@ -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.

```
<LinkControl
currentSettings={ [
{
id: 'opensInNewTab',
title: __( 'Open in New Tab' ),
checked: attributes.opensInNewTab, // Block attributes persist control state
},
] }
onSettingsChange={ ( setting, value ) => setAttributes( { [ setting ]: value } ) }
/>
```

144 changes: 83 additions & 61 deletions packages/block-editor/src/components/link-control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import {
Button,
ExternalLink,
Popover,
TextControl,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';

import {
useCallback,
useState,
useEffect,
Fragment,
} from '@wordpress/element';

Expand All @@ -31,7 +31,14 @@ import {

import { withInstanceId, compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';

import {
LEFT,
RIGHT,
UP,
DOWN,
BACKSPACE,
ENTER,
} from '@wordpress/keycodes';
/**
* Internal dependencies
*/
Expand All @@ -44,48 +51,35 @@ 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,
text = '',
onTextChange = 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

/**
* 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`.
*/
Expand All @@ -94,12 +88,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 );
}
};

Expand All @@ -112,10 +102,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';
Expand All @@ -125,44 +115,57 @@ 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
// have a URL option to fallback on.
return couldBeURL ? results[ 0 ].concat( results[ 1 ] ) : results[ 0 ];
};

const handleTextControlOnKeyDown = ( 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 handleTextControlOnKeyPress = ( event ) => {
event.stopPropagation();
};

// 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
Expand All @@ -181,7 +184,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 }
Expand All @@ -202,7 +208,7 @@ function LinkControl( {
<div className="block-editor-link-control__popover-inner">
<div className="block-editor-link-control__search">

{ ( ! isEditingLink && currentLink ) && (
{ ( ! isEditingLink ) && (
<Fragment>
<p className="screen-reader-text" id={ `current-link-label-${ instanceId }` }>
{ __( 'Currently selected' ) }:
Expand All @@ -215,14 +221,13 @@ function LinkControl( {
} ) }
>
<span className="block-editor-link-control__search-item-header">

<ExternalLink
className="block-editor-link-control__search-item-title"
href={ currentLink.url }
href={ value.url }
>
{ currentLink.title }
{ value.title }
</ExternalLink>
<span className="block-editor-link-control__search-item-info">{ filterURLForDisplay( safeDecodeURI( currentLink.url ) ) || '' }</span>
<span className="block-editor-link-control__search-item-info">{ filterURLForDisplay( safeDecodeURI( value.url ) ) || '' }</span>
</span>

<Button isSecondary onClick={ setMode( MODE_EDIT ) } className="block-editor-link-control__search-item-action block-editor-link-control__search-item-action--edit">
Expand All @@ -233,20 +238,37 @@ function LinkControl( {
) }

{ isEditingLink && (
<LinkControlSearchInput
value={ inputValue }
onChange={ onInputChange }
onSelect={ onLinkChange }
renderSuggestions={ renderSearchResults }
fetchSuggestions={ getSearchHandler }
onReset={ resetInput }
onKeyDown={ onKeyDown }
onKeyPress={ onKeyPress }
/>
<>
<LinkControlSearchInput
value={ inputValue }
onChange={ onInputChange }
onSelect={ ( suggestion ) => {
setIsEditingLink( false );
onChange( { ...value, ...suggestion } );
} }
renderSuggestions={ renderSearchResults }
fetchSuggestions={ getSearchHandler }
onReset={ resetInput }
/>

<TextControl
className="block-editor-link-control__text-content"
label="Text content"
value={ text }
onChange={ onTextChange }
onKeyDown={ ( event ) => {
if ( event.keyCode === ENTER ) {
return;
}
handleTextControlOnKeyDown( event );
} }
onKeyPress={ handleTextControlOnKeyPress }
/>
</>
) }

{ ! isEditingLink && (
<LinkControlSettingsDrawer settings={ currentSettings } onSettingChange={ onSettingsChange } />
<LinkControlSettingsDrawer value={ value } settings={ settings } onChange={ onChange } />
) }
</div>
</div>
Expand Down
Loading