@wordpress/block-library 9.33.6 → 9.33.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/heading/index.js +3 -1
- package/build/heading/index.js.map +3 -3
- package/build/heading/variations.js +53 -0
- package/build/heading/variations.js.map +7 -0
- package/build/navigation-link/edit.js +31 -10
- package/build/navigation-link/edit.js.map +2 -2
- package/build/navigation-link/link-ui/index.js +8 -3
- package/build/navigation-link/link-ui/index.js.map +2 -2
- package/build/navigation-link/shared/controls.js +74 -23
- package/build/navigation-link/shared/controls.js.map +2 -2
- package/build/navigation-link/shared/index.js +4 -0
- package/build/navigation-link/shared/index.js.map +2 -2
- package/build/navigation-link/shared/use-entity-binding.js +32 -2
- package/build/navigation-link/shared/use-entity-binding.js.map +3 -3
- package/build/paragraph/index.js +3 -1
- package/build/paragraph/index.js.map +3 -3
- package/build/paragraph/variations.js +55 -0
- package/build/paragraph/variations.js.map +7 -0
- package/build/social-links/edit.js +1 -1
- package/build/social-links/edit.js.map +1 -1
- package/build-module/heading/index.js +3 -1
- package/build-module/heading/index.js.map +2 -2
- package/build-module/heading/variations.js +33 -0
- package/build-module/heading/variations.js.map +7 -0
- package/build-module/navigation-link/edit.js +45 -14
- package/build-module/navigation-link/edit.js.map +2 -2
- package/build-module/navigation-link/link-ui/index.js +8 -3
- package/build-module/navigation-link/link-ui/index.js.map +2 -2
- package/build-module/navigation-link/shared/controls.js +72 -23
- package/build-module/navigation-link/shared/controls.js.map +2 -2
- package/build-module/navigation-link/shared/index.js +3 -1
- package/build-module/navigation-link/shared/index.js.map +2 -2
- package/build-module/navigation-link/shared/use-entity-binding.js +36 -3
- package/build-module/navigation-link/shared/use-entity-binding.js.map +2 -2
- package/build-module/paragraph/index.js +3 -1
- package/build-module/paragraph/index.js.map +2 -2
- package/build-module/paragraph/variations.js +35 -0
- package/build-module/paragraph/variations.js.map +7 -0
- package/build-module/social-links/edit.js +1 -1
- package/build-module/social-links/edit.js.map +1 -1
- package/build-style/accordion-heading/style-rtl.css +19 -3
- package/build-style/accordion-heading/style.css +19 -3
- package/build-style/accordion-panel/style-rtl.css +4 -1
- package/build-style/accordion-panel/style.css +4 -1
- package/build-style/editor-rtl.css +7 -0
- package/build-style/editor.css +7 -0
- package/build-style/navigation-link/editor-rtl.css +7 -0
- package/build-style/navigation-link/editor.css +7 -0
- package/build-style/style-rtl.css +23 -4
- package/build-style/style.css +23 -4
- package/package.json +11 -11
- package/src/accordion-heading/style.scss +40 -7
- package/src/accordion-panel/style.scss +6 -1
- package/src/heading/index.js +2 -0
- package/src/heading/variations.js +37 -0
- package/src/navigation-link/edit.js +71 -17
- package/src/navigation-link/editor.scss +7 -0
- package/src/navigation-link/link-ui/index.js +9 -8
- package/src/navigation-link/shared/controls.js +120 -26
- package/src/navigation-link/shared/index.js +1 -1
- package/src/navigation-link/shared/test/controls.js +19 -9
- package/src/navigation-link/shared/test/use-entity-binding.js +14 -1
- package/src/navigation-link/shared/use-entity-binding.js +60 -9
- package/src/paragraph/index.js +2 -0
- package/src/paragraph/variations.js +39 -0
- package/src/social-links/edit.js +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
import { Path, SVG } from '@wordpress/primitives';
|
|
6
|
+
import { heading } from '@wordpress/icons';
|
|
7
|
+
|
|
8
|
+
const variations = [
|
|
9
|
+
{
|
|
10
|
+
name: 'heading',
|
|
11
|
+
title: __( 'Heading' ),
|
|
12
|
+
description: __(
|
|
13
|
+
'Introduce new sections and organize content to help visitors (and search engines) understand the structure of your content.'
|
|
14
|
+
),
|
|
15
|
+
isDefault: true,
|
|
16
|
+
scope: [ 'inserter', 'transform' ],
|
|
17
|
+
attributes: { fitText: undefined },
|
|
18
|
+
icon: heading,
|
|
19
|
+
},
|
|
20
|
+
// There is a hardcoded workaround in packages/block-editor/src/store/selectors.js
|
|
21
|
+
// to make Stretchy variations appear as the last of their sections in the inserter.
|
|
22
|
+
{
|
|
23
|
+
name: 'stretchy-heading',
|
|
24
|
+
title: __( 'Stretchy Heading' ),
|
|
25
|
+
description: __( 'Heading that resizes to fit its container.' ),
|
|
26
|
+
icon: (
|
|
27
|
+
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
28
|
+
<Path d="m3 18.6 6-4.7 6 4.7V5H3v13.6Zm16.2-9.8v1.5h2.2L17.7 14l1.1 1.1 3.7-3.7v2.2H24V8.8h-4.8Z" />
|
|
29
|
+
</SVG>
|
|
30
|
+
),
|
|
31
|
+
attributes: { fitText: true },
|
|
32
|
+
scope: [ 'inserter', 'transform' ],
|
|
33
|
+
isActive: ( blockAttributes ) => blockAttributes.fitText === true,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export default variations;
|
|
@@ -8,9 +8,13 @@ import clsx from 'clsx';
|
|
|
8
8
|
*/
|
|
9
9
|
import { createBlock } from '@wordpress/blocks';
|
|
10
10
|
import { useSelect, useDispatch } from '@wordpress/data';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ToolbarButton,
|
|
13
|
+
ToolbarGroup,
|
|
14
|
+
VisuallyHidden,
|
|
15
|
+
} from '@wordpress/components';
|
|
12
16
|
import { displayShortcut, isKeyboardEvent } from '@wordpress/keycodes';
|
|
13
|
-
import { __ } from '@wordpress/i18n';
|
|
17
|
+
import { __, sprintf } from '@wordpress/i18n';
|
|
14
18
|
import {
|
|
15
19
|
BlockControls,
|
|
16
20
|
InspectorControls,
|
|
@@ -26,13 +30,19 @@ import { useState, useEffect, useRef, useCallback } from '@wordpress/element';
|
|
|
26
30
|
import { decodeEntities } from '@wordpress/html-entities';
|
|
27
31
|
import { link as linkIcon, addSubmenu } from '@wordpress/icons';
|
|
28
32
|
import { store as coreStore } from '@wordpress/core-data';
|
|
29
|
-
import { useMergeRefs, usePrevious } from '@wordpress/compose';
|
|
33
|
+
import { useMergeRefs, usePrevious, useInstanceId } from '@wordpress/compose';
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* Internal dependencies
|
|
33
37
|
*/
|
|
34
38
|
import { getColors } from '../navigation/edit/utils';
|
|
35
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
Controls,
|
|
41
|
+
LinkUI,
|
|
42
|
+
updateAttributes,
|
|
43
|
+
useEntityBinding,
|
|
44
|
+
MissingEntityHelpText,
|
|
45
|
+
} from './shared';
|
|
36
46
|
|
|
37
47
|
const DEFAULT_BLOCK = { name: 'core/navigation-link' };
|
|
38
48
|
const NESTING_BLOCK_NAMES = [
|
|
@@ -97,21 +107,35 @@ const useIsInvalidLink = ( kind, type, id, enabled ) => {
|
|
|
97
107
|
const hasId = Number.isInteger( id );
|
|
98
108
|
const blockEditingMode = useBlockEditingMode();
|
|
99
109
|
|
|
100
|
-
const postStatus = useSelect(
|
|
110
|
+
const { postStatus, isDeleted } = useSelect(
|
|
101
111
|
( select ) => {
|
|
102
112
|
if ( ! isPostType ) {
|
|
103
|
-
return null;
|
|
113
|
+
return { postStatus: null, isDeleted: false };
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
// Fetching the posts status is an "expensive" operation. Especially for sites with large navigations.
|
|
107
117
|
// When the block is rendered in a template or other disabled contexts we can skip this check in order
|
|
108
118
|
// to avoid all these additional requests that don't really add any value in that mode.
|
|
109
119
|
if ( blockEditingMode === 'disabled' || ! enabled ) {
|
|
110
|
-
return null;
|
|
120
|
+
return { postStatus: null, isDeleted: false };
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
const { getEntityRecord } =
|
|
114
|
-
|
|
123
|
+
const { getEntityRecord, hasFinishedResolution } =
|
|
124
|
+
select( coreStore );
|
|
125
|
+
const entityRecord = getEntityRecord( 'postType', type, id );
|
|
126
|
+
const hasResolved = hasFinishedResolution( 'getEntityRecord', [
|
|
127
|
+
'postType',
|
|
128
|
+
type,
|
|
129
|
+
id,
|
|
130
|
+
] );
|
|
131
|
+
|
|
132
|
+
// If resolution has finished and entityRecord is undefined, the entity was deleted.
|
|
133
|
+
const deleted = hasResolved && entityRecord === undefined;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
postStatus: entityRecord?.status,
|
|
137
|
+
isDeleted: deleted,
|
|
138
|
+
};
|
|
115
139
|
},
|
|
116
140
|
[ isPostType, blockEditingMode, enabled, type, id ]
|
|
117
141
|
);
|
|
@@ -121,11 +145,13 @@ const useIsInvalidLink = ( kind, type, id, enabled ) => {
|
|
|
121
145
|
// 2. It has an id.
|
|
122
146
|
// 3. It's neither null, nor undefined, as valid items might be either of those while loading.
|
|
123
147
|
// If those conditions are met, check if
|
|
124
|
-
// 1. The post status is
|
|
125
|
-
// 2. The
|
|
148
|
+
// 1. The post status is trash (trashed).
|
|
149
|
+
// 2. The entity doesn't exist (deleted).
|
|
126
150
|
// If either of those is true, invalidate.
|
|
127
151
|
const isInvalid =
|
|
128
|
-
isPostType &&
|
|
152
|
+
isPostType &&
|
|
153
|
+
hasId &&
|
|
154
|
+
( isDeleted || ( postStatus && 'trash' === postStatus ) );
|
|
129
155
|
const isDraft = 'draft' === postStatus;
|
|
130
156
|
|
|
131
157
|
return [ isInvalid, isDraft ];
|
|
@@ -248,7 +274,12 @@ export default function NavigationLinkEdit( {
|
|
|
248
274
|
const { getBlocks } = useSelect( blockEditorStore );
|
|
249
275
|
|
|
250
276
|
// URL binding logic
|
|
251
|
-
const {
|
|
277
|
+
const {
|
|
278
|
+
clearBinding,
|
|
279
|
+
createBinding,
|
|
280
|
+
hasUrlBinding,
|
|
281
|
+
isBoundEntityAvailable,
|
|
282
|
+
} = useEntityBinding( {
|
|
252
283
|
clientId,
|
|
253
284
|
attributes,
|
|
254
285
|
} );
|
|
@@ -373,6 +404,12 @@ export default function NavigationLinkEdit( {
|
|
|
373
404
|
}
|
|
374
405
|
}
|
|
375
406
|
|
|
407
|
+
const instanceId = useInstanceId( NavigationLinkEdit );
|
|
408
|
+
const hasMissingEntity = hasUrlBinding && ! isBoundEntityAvailable;
|
|
409
|
+
const missingEntityDescriptionId = hasMissingEntity
|
|
410
|
+
? sprintf( 'navigation-link-edit-%d-desc', instanceId )
|
|
411
|
+
: undefined;
|
|
412
|
+
|
|
376
413
|
const blockProps = useBlockProps( {
|
|
377
414
|
ref: useMergeRefs( [ setPopoverAnchor, listItemRef ] ),
|
|
378
415
|
className: clsx( 'wp-block-navigation-item', {
|
|
@@ -386,6 +423,8 @@ export default function NavigationLinkEdit( {
|
|
|
386
423
|
[ getColorClassName( 'background-color', backgroundColor ) ]:
|
|
387
424
|
!! backgroundColor,
|
|
388
425
|
} ),
|
|
426
|
+
'aria-describedby': missingEntityDescriptionId,
|
|
427
|
+
'aria-invalid': hasMissingEntity,
|
|
389
428
|
style: {
|
|
390
429
|
color: ! textColor && customTextColor,
|
|
391
430
|
backgroundColor: ! backgroundColor && customBackgroundColor,
|
|
@@ -405,14 +444,23 @@ export default function NavigationLinkEdit( {
|
|
|
405
444
|
}
|
|
406
445
|
);
|
|
407
446
|
|
|
408
|
-
if (
|
|
447
|
+
if (
|
|
448
|
+
! url ||
|
|
449
|
+
isInvalid ||
|
|
450
|
+
isDraft ||
|
|
451
|
+
( hasUrlBinding && ! isBoundEntityAvailable )
|
|
452
|
+
) {
|
|
409
453
|
blockProps.onClick = () => {
|
|
410
454
|
setIsLinkOpen( true );
|
|
411
455
|
};
|
|
412
456
|
}
|
|
413
457
|
|
|
414
458
|
const classes = clsx( 'wp-block-navigation-item__content', {
|
|
415
|
-
'wp-block-navigation-link__placeholder':
|
|
459
|
+
'wp-block-navigation-link__placeholder':
|
|
460
|
+
! url ||
|
|
461
|
+
isInvalid ||
|
|
462
|
+
isDraft ||
|
|
463
|
+
( hasUrlBinding && ! isBoundEntityAvailable ),
|
|
416
464
|
} );
|
|
417
465
|
|
|
418
466
|
const missingText = getMissingText( type );
|
|
@@ -452,6 +500,11 @@ export default function NavigationLinkEdit( {
|
|
|
452
500
|
/>
|
|
453
501
|
</InspectorControls>
|
|
454
502
|
<div { ...blockProps }>
|
|
503
|
+
{ hasMissingEntity && (
|
|
504
|
+
<VisuallyHidden id={ missingEntityDescriptionId }>
|
|
505
|
+
<MissingEntityHelpText type={ type } kind={ kind } />
|
|
506
|
+
</VisuallyHidden>
|
|
507
|
+
) }
|
|
455
508
|
{ /* eslint-disable jsx-a11y/anchor-is-valid */ }
|
|
456
509
|
<a className={ classes }>
|
|
457
510
|
{ /* eslint-enable */ }
|
|
@@ -531,9 +584,10 @@ export default function NavigationLinkEdit( {
|
|
|
531
584
|
link={ attributes }
|
|
532
585
|
onClose={ () => {
|
|
533
586
|
setIsLinkOpen( false );
|
|
534
|
-
// If there is no link
|
|
587
|
+
// If there is no link and no binding, remove the auto-inserted block.
|
|
535
588
|
// This avoids empty blocks which can provided a poor UX.
|
|
536
|
-
if (
|
|
589
|
+
// Don't remove if binding exists (even if entity is unavailable) so user can fix it.
|
|
590
|
+
if ( ! url && ! hasUrlBinding ) {
|
|
537
591
|
onReplace( [] );
|
|
538
592
|
} else if ( isNewLink.current ) {
|
|
539
593
|
// If we just created a new link, select it
|
|
@@ -26,6 +26,7 @@ import { useInstanceId } from '@wordpress/compose';
|
|
|
26
26
|
*/
|
|
27
27
|
import { LinkUIPageCreator } from './page-creator';
|
|
28
28
|
import LinkUIBlockInserter from './block-inserter';
|
|
29
|
+
import { useEntityBinding } from '../shared/use-entity-binding';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Given the Link block's type attribute, return the query params to give to
|
|
@@ -66,7 +67,8 @@ export function getSuggestionsQuery( type, kind ) {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
function UnforwardedLinkUI( props, ref ) {
|
|
69
|
-
const { label, url, opensInNewTab, type, kind, id
|
|
70
|
+
const { label, url, opensInNewTab, type, kind, id } = props.link;
|
|
71
|
+
const { clientId } = props;
|
|
70
72
|
const postType = type || 'page';
|
|
71
73
|
|
|
72
74
|
const [ addingBlock, setAddingBlock ] = useState( false );
|
|
@@ -78,12 +80,11 @@ function UnforwardedLinkUI( props, ref ) {
|
|
|
78
80
|
name: postType,
|
|
79
81
|
} );
|
|
80
82
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
!! id;
|
|
83
|
+
// Use the entity binding hook to get binding status
|
|
84
|
+
const { isBoundEntityAvailable } = useEntityBinding( {
|
|
85
|
+
clientId,
|
|
86
|
+
attributes: props.link,
|
|
87
|
+
} );
|
|
87
88
|
|
|
88
89
|
// Memoize link value to avoid overriding the LinkControl's internal state.
|
|
89
90
|
// This is a temporary fix. See https://github.com/WordPress/gutenberg/issues/50976#issuecomment-1568226407.
|
|
@@ -152,7 +153,7 @@ function UnforwardedLinkUI( props, ref ) {
|
|
|
152
153
|
onChange={ props.onChange }
|
|
153
154
|
onRemove={ props.onRemove }
|
|
154
155
|
onCancel={ props.onCancel }
|
|
155
|
-
handleEntities={
|
|
156
|
+
handleEntities={ isBoundEntityAvailable }
|
|
156
157
|
renderControlBottom={ () => {
|
|
157
158
|
// Don't show the tools when there is submitted link (preview state).
|
|
158
159
|
if ( link?.url?.length ) {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
TextareaControl,
|
|
12
12
|
} from '@wordpress/components';
|
|
13
13
|
import { __, sprintf } from '@wordpress/i18n';
|
|
14
|
-
import { useRef } from '@wordpress/element';
|
|
14
|
+
import { useRef, useEffect, useState } from '@wordpress/element';
|
|
15
15
|
import { useInstanceId } from '@wordpress/compose';
|
|
16
16
|
import { safeDecodeURI } from '@wordpress/url';
|
|
17
17
|
import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
|
|
@@ -72,19 +72,31 @@ export function Controls( { attributes, setAttributes, clientId } ) {
|
|
|
72
72
|
const { label, url, description, rel, opensInNewTab } = attributes;
|
|
73
73
|
const lastURLRef = useRef( url );
|
|
74
74
|
const dropdownMenuProps = useToolsPanelDropdownMenuProps();
|
|
75
|
+
const urlInputRef = useRef();
|
|
76
|
+
const shouldFocusURLInputRef = useRef( false );
|
|
75
77
|
const inputId = useInstanceId( Controls, 'link-input' );
|
|
76
78
|
const helpTextId = `${ inputId }__help`;
|
|
77
79
|
|
|
80
|
+
// Local state to control the input value
|
|
81
|
+
const [ inputValue, setInputValue ] = useState( url );
|
|
82
|
+
|
|
83
|
+
// Sync local state when url prop changes (e.g., from undo/redo or external updates)
|
|
84
|
+
useEffect( () => {
|
|
85
|
+
setInputValue( url );
|
|
86
|
+
lastURLRef.current = url;
|
|
87
|
+
}, [ url ] );
|
|
88
|
+
|
|
78
89
|
// Use the entity binding hook internally
|
|
79
|
-
const { hasUrlBinding, clearBinding } =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
const { hasUrlBinding, isBoundEntityAvailable, clearBinding } =
|
|
91
|
+
useEntityBinding( {
|
|
92
|
+
clientId,
|
|
93
|
+
attributes,
|
|
94
|
+
} );
|
|
83
95
|
|
|
84
96
|
// Get direct store dispatch to bypass setBoundAttributes wrapper
|
|
85
97
|
const { updateBlockAttributes } = useDispatch( blockEditorStore );
|
|
86
98
|
|
|
87
|
-
const
|
|
99
|
+
const unsyncBoundLink = () => {
|
|
88
100
|
// Clear the binding first
|
|
89
101
|
clearBinding();
|
|
90
102
|
|
|
@@ -93,9 +105,22 @@ export function Controls( { attributes, setAttributes, clientId } ) {
|
|
|
93
105
|
// setAttributes is actually setBoundAttributes, a wrapper function that
|
|
94
106
|
// processes attributes through the binding system.
|
|
95
107
|
// See: packages/block-editor/src/components/block-edit/edit.js
|
|
96
|
-
updateBlockAttributes( clientId, {
|
|
108
|
+
updateBlockAttributes( clientId, {
|
|
109
|
+
url: lastURLRef.current, // set the lastURLRef as the new editable value so we avoid bugs from empty link states
|
|
110
|
+
id: undefined,
|
|
111
|
+
} );
|
|
97
112
|
};
|
|
98
113
|
|
|
114
|
+
useEffect( () => {
|
|
115
|
+
// Only want to focus the input if the url is not bound to an entity.
|
|
116
|
+
if ( ! hasUrlBinding && shouldFocusURLInputRef.current ) {
|
|
117
|
+
// focuses and highlights the url input value, giving the user
|
|
118
|
+
// the ability to delete the value quickly or edit it.
|
|
119
|
+
urlInputRef.current?.select();
|
|
120
|
+
}
|
|
121
|
+
shouldFocusURLInputRef.current = false;
|
|
122
|
+
}, [ hasUrlBinding ] );
|
|
123
|
+
|
|
99
124
|
return (
|
|
100
125
|
<ToolsPanel
|
|
101
126
|
label={ __( 'Settings' ) }
|
|
@@ -135,56 +160,100 @@ export function Controls( { attributes, setAttributes, clientId } ) {
|
|
|
135
160
|
isShownByDefault
|
|
136
161
|
>
|
|
137
162
|
<InputControl
|
|
163
|
+
ref={ urlInputRef }
|
|
138
164
|
__nextHasNoMarginBottom
|
|
139
165
|
__next40pxDefaultSize
|
|
140
166
|
id={ inputId }
|
|
141
167
|
label={ __( 'Link' ) }
|
|
142
|
-
value={
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return; // Prevent editing when URL is bound
|
|
168
|
+
value={ ( () => {
|
|
169
|
+
if ( hasUrlBinding && ! isBoundEntityAvailable ) {
|
|
170
|
+
return '';
|
|
146
171
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
} );
|
|
150
|
-
} }
|
|
172
|
+
return inputValue ? safeDecodeURI( inputValue ) : '';
|
|
173
|
+
} )() }
|
|
151
174
|
autoComplete="off"
|
|
152
175
|
type="url"
|
|
153
176
|
disabled={ hasUrlBinding }
|
|
177
|
+
aria-invalid={
|
|
178
|
+
hasUrlBinding && ! isBoundEntityAvailable
|
|
179
|
+
? 'true'
|
|
180
|
+
: undefined
|
|
181
|
+
}
|
|
182
|
+
aria-describedby={ helpTextId }
|
|
183
|
+
className={
|
|
184
|
+
hasUrlBinding && ! isBoundEntityAvailable
|
|
185
|
+
? 'navigation-link-control__input-with-error-suffix'
|
|
186
|
+
: undefined
|
|
187
|
+
}
|
|
188
|
+
onChange={ ( newValue ) => {
|
|
189
|
+
if ( isBoundEntityAvailable ) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Defer updating the url attribute until onBlur to prevent the canvas from
|
|
194
|
+
// treating a temporary empty value as a committed value, which replaces the
|
|
195
|
+
// label with placeholder text.
|
|
196
|
+
setInputValue( newValue );
|
|
197
|
+
} }
|
|
154
198
|
onFocus={ () => {
|
|
155
|
-
if (
|
|
199
|
+
if ( isBoundEntityAvailable ) {
|
|
156
200
|
return;
|
|
157
201
|
}
|
|
158
202
|
lastURLRef.current = url;
|
|
159
203
|
} }
|
|
160
204
|
onBlur={ () => {
|
|
161
|
-
if (
|
|
205
|
+
if ( isBoundEntityAvailable ) {
|
|
162
206
|
return;
|
|
163
207
|
}
|
|
208
|
+
|
|
209
|
+
const finalValue = ! inputValue
|
|
210
|
+
? lastURLRef.current
|
|
211
|
+
: inputValue;
|
|
212
|
+
|
|
213
|
+
// Update local state immediately so input reflects the reverted value if the value was cleared
|
|
214
|
+
setInputValue( finalValue );
|
|
215
|
+
|
|
164
216
|
// Defer the updateAttributes call to ensure entity connection isn't severed by accident.
|
|
165
|
-
updateAttributes(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
);
|
|
217
|
+
updateAttributes( { url: finalValue }, setAttributes, {
|
|
218
|
+
...attributes,
|
|
219
|
+
url: lastURLRef.current,
|
|
220
|
+
} );
|
|
170
221
|
} }
|
|
171
222
|
help={
|
|
172
|
-
hasUrlBinding && (
|
|
173
|
-
<
|
|
223
|
+
hasUrlBinding && ! isBoundEntityAvailable ? (
|
|
224
|
+
<MissingEntityHelp
|
|
225
|
+
id={ helpTextId }
|
|
174
226
|
type={ attributes.type }
|
|
175
227
|
kind={ attributes.kind }
|
|
176
228
|
/>
|
|
229
|
+
) : (
|
|
230
|
+
isBoundEntityAvailable && (
|
|
231
|
+
<BindingHelpText
|
|
232
|
+
type={ attributes.type }
|
|
233
|
+
kind={ attributes.kind }
|
|
234
|
+
/>
|
|
235
|
+
)
|
|
177
236
|
)
|
|
178
237
|
}
|
|
179
238
|
suffix={
|
|
180
239
|
hasUrlBinding && (
|
|
181
240
|
<Button
|
|
182
241
|
icon={ unlinkIcon }
|
|
183
|
-
onClick={
|
|
242
|
+
onClick={ () => {
|
|
243
|
+
unsyncBoundLink();
|
|
244
|
+
// Focus management to send focus to the URL input
|
|
245
|
+
// on next render after disabled state is removed.
|
|
246
|
+
shouldFocusURLInputRef.current = true;
|
|
247
|
+
} }
|
|
184
248
|
aria-describedby={ helpTextId }
|
|
185
249
|
showTooltip
|
|
186
250
|
label={ __( 'Unsync and edit' ) }
|
|
187
251
|
__next40pxDefaultSize
|
|
252
|
+
className={
|
|
253
|
+
hasUrlBinding && ! isBoundEntityAvailable
|
|
254
|
+
? 'navigation-link-control__error-suffix-button'
|
|
255
|
+
: undefined
|
|
256
|
+
}
|
|
188
257
|
/>
|
|
189
258
|
)
|
|
190
259
|
}
|
|
@@ -258,7 +327,7 @@ export function Controls( { attributes, setAttributes, clientId } ) {
|
|
|
258
327
|
* @param {string} props.kind - The entity kind
|
|
259
328
|
* @return {string} Help text for the bound URL
|
|
260
329
|
*/
|
|
261
|
-
function BindingHelpText( { type, kind } ) {
|
|
330
|
+
export function BindingHelpText( { type, kind } ) {
|
|
262
331
|
const entityType = getEntityTypeName( type, kind );
|
|
263
332
|
return sprintf(
|
|
264
333
|
/* translators: %s is the entity type (e.g., "page", "post", "category") */
|
|
@@ -266,3 +335,28 @@ function BindingHelpText( { type, kind } ) {
|
|
|
266
335
|
entityType
|
|
267
336
|
);
|
|
268
337
|
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Component to display error help text for missing entity bindings.
|
|
341
|
+
*
|
|
342
|
+
* @param {Object} props - Component props
|
|
343
|
+
* @param {string} props.type - The entity type
|
|
344
|
+
* @param {string} props.kind - The entity kind
|
|
345
|
+
* @return {JSX.Element} Error help text component
|
|
346
|
+
*/
|
|
347
|
+
export function MissingEntityHelpText( { type, kind } ) {
|
|
348
|
+
const entityType = getEntityTypeName( type, kind );
|
|
349
|
+
return sprintf(
|
|
350
|
+
/* translators: %s is the entity type (e.g., "page", "post", "category") */
|
|
351
|
+
__( 'Synced %s is missing. Please update or remove this link.' ),
|
|
352
|
+
entityType
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function MissingEntityHelp( { id, type, kind } ) {
|
|
357
|
+
return (
|
|
358
|
+
<span id={ id } className="navigation-link-control__error-text">
|
|
359
|
+
<MissingEntityHelpText type={ type } kind={ kind } />
|
|
360
|
+
</span>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* to reduce code duplication and ensure consistent behavior.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export { Controls } from './controls';
|
|
8
|
+
export { Controls, BindingHelpText, MissingEntityHelpText } from './controls';
|
|
9
9
|
export { updateAttributes } from './update-attributes';
|
|
10
10
|
export {
|
|
11
11
|
useEntityBinding,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* External dependencies
|
|
7
7
|
*/
|
|
8
8
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
9
|
+
import userEvent from '@testing-library/user-event';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Internal dependencies
|
|
@@ -27,6 +28,7 @@ jest.mock( '../../../utils/hooks', () => ( {
|
|
|
27
28
|
jest.mock( '../use-entity-binding', () => ( {
|
|
28
29
|
useEntityBinding: jest.fn( () => ( {
|
|
29
30
|
hasUrlBinding: false,
|
|
31
|
+
isBoundEntityAvailable: false,
|
|
30
32
|
clearBinding: jest.fn(),
|
|
31
33
|
} ) ),
|
|
32
34
|
} ) );
|
|
@@ -95,18 +97,22 @@ describe( 'Controls', () => {
|
|
|
95
97
|
expect( urlInput.value ).toBe( 'https://example.com/test page' );
|
|
96
98
|
} );
|
|
97
99
|
|
|
98
|
-
it( '
|
|
100
|
+
it( 'calls updateAttributes with new URL on blur', async () => {
|
|
101
|
+
const user = userEvent.setup();
|
|
99
102
|
render( <Controls { ...defaultProps } /> );
|
|
100
103
|
|
|
101
104
|
const urlInput = screen.getByLabelText( 'Link' );
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
await user.click( urlInput );
|
|
107
|
+
await user.clear( urlInput );
|
|
108
|
+
await user.type( urlInput, 'https://example.com/test page' );
|
|
109
|
+
await user.tab();
|
|
106
110
|
|
|
107
|
-
expect(
|
|
108
|
-
url: 'https://example.com/test
|
|
109
|
-
|
|
111
|
+
expect( mockUpdateAttributes ).toHaveBeenCalledWith(
|
|
112
|
+
{ url: 'https://example.com/test page' },
|
|
113
|
+
defaultProps.setAttributes,
|
|
114
|
+
{ ...defaultProps.attributes, url: 'https://example.com' }
|
|
115
|
+
);
|
|
110
116
|
} );
|
|
111
117
|
|
|
112
118
|
it( 'calls updateAttributes on URL blur', () => {
|
|
@@ -143,11 +149,11 @@ describe( 'Controls', () => {
|
|
|
143
149
|
target: { value: 'https://new.com' },
|
|
144
150
|
} );
|
|
145
151
|
|
|
146
|
-
// Blur should call updateAttributes with the
|
|
152
|
+
// Blur should call updateAttributes with the new URL value from the input
|
|
147
153
|
fireEvent.blur( urlInput );
|
|
148
154
|
|
|
149
155
|
expect( mockUpdateAttributes ).toHaveBeenCalledWith(
|
|
150
|
-
{ url: 'https://
|
|
156
|
+
{ url: 'https://new.com' }, // New URL from input value
|
|
151
157
|
defaultProps.setAttributes,
|
|
152
158
|
{
|
|
153
159
|
...propsWithDifferentUrl.attributes,
|
|
@@ -197,6 +203,7 @@ describe( 'Controls', () => {
|
|
|
197
203
|
const { useEntityBinding } = require( '../use-entity-binding' );
|
|
198
204
|
useEntityBinding.mockReturnValue( {
|
|
199
205
|
hasUrlBinding: true,
|
|
206
|
+
isBoundEntityAvailable: true,
|
|
200
207
|
clearBinding: jest.fn(),
|
|
201
208
|
} );
|
|
202
209
|
|
|
@@ -220,6 +227,7 @@ describe( 'Controls', () => {
|
|
|
220
227
|
const { useEntityBinding } = require( '../use-entity-binding' );
|
|
221
228
|
useEntityBinding.mockReturnValue( {
|
|
222
229
|
hasUrlBinding: true,
|
|
230
|
+
isBoundEntityAvailable: true,
|
|
223
231
|
clearBinding: jest.fn(),
|
|
224
232
|
} );
|
|
225
233
|
|
|
@@ -257,6 +265,7 @@ describe( 'Controls', () => {
|
|
|
257
265
|
const { useEntityBinding } = require( '../use-entity-binding' );
|
|
258
266
|
useEntityBinding.mockReturnValue( {
|
|
259
267
|
hasUrlBinding: true,
|
|
268
|
+
isBoundEntityAvailable: true,
|
|
260
269
|
clearBinding: jest.fn(),
|
|
261
270
|
} );
|
|
262
271
|
|
|
@@ -280,6 +289,7 @@ describe( 'Controls', () => {
|
|
|
280
289
|
const { useEntityBinding } = require( '../use-entity-binding' );
|
|
281
290
|
useEntityBinding.mockReturnValue( {
|
|
282
291
|
hasUrlBinding: true,
|
|
292
|
+
isBoundEntityAvailable: true,
|
|
283
293
|
clearBinding: jest.fn(),
|
|
284
294
|
} );
|
|
285
295
|
|
|
@@ -18,12 +18,23 @@ import {
|
|
|
18
18
|
// Mock the entire @wordpress/block-editor module
|
|
19
19
|
jest.mock( '@wordpress/block-editor', () => ( {
|
|
20
20
|
useBlockBindingsUtils: jest.fn(),
|
|
21
|
+
useBlockEditingMode: jest.fn(),
|
|
21
22
|
} ) );
|
|
22
23
|
|
|
24
|
+
// Mock useSelect specifically to avoid needing to set up full data store
|
|
25
|
+
jest.mock( '@wordpress/data/src/components/use-select', () => {
|
|
26
|
+
const mock = jest.fn();
|
|
27
|
+
return mock;
|
|
28
|
+
} );
|
|
29
|
+
|
|
23
30
|
/**
|
|
24
31
|
* WordPress dependencies
|
|
25
32
|
*/
|
|
26
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
useBlockBindingsUtils,
|
|
35
|
+
useBlockEditingMode,
|
|
36
|
+
} from '@wordpress/block-editor';
|
|
37
|
+
import { useSelect } from '@wordpress/data';
|
|
27
38
|
|
|
28
39
|
describe( 'useEntityBinding', () => {
|
|
29
40
|
const mockUpdateBlockBindings = jest.fn();
|
|
@@ -33,6 +44,8 @@ describe( 'useEntityBinding', () => {
|
|
|
33
44
|
useBlockBindingsUtils.mockReturnValue( {
|
|
34
45
|
updateBlockBindings: mockUpdateBlockBindings,
|
|
35
46
|
} );
|
|
47
|
+
useBlockEditingMode.mockReturnValue( 'default' );
|
|
48
|
+
useSelect.mockReturnValue( true );
|
|
36
49
|
} );
|
|
37
50
|
|
|
38
51
|
describe( 'hasUrlBinding', () => {
|