Skip to content

Commit a579646

Browse files
authored
Navigation Link: Clarify Link To invalid and draft state messages (WordPress#74054)
1 parent 61417d8 commit a579646

File tree

6 files changed

+105
-105
lines changed

6 files changed

+105
-105
lines changed

packages/block-library/src/navigation-link/edit.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {
3737
Controls,
3838
LinkUI,
3939
useEntityBinding,
40-
MissingEntityHelpText,
40+
getInvalidLinkHelpText,
4141
useHandleLinkChange,
4242
useIsInvalidLink,
4343
InvalidDraftDisplay,
@@ -366,6 +366,7 @@ export default function NavigationLinkEdit( {
366366
} );
367367

368368
const missingText = getMissingText( type );
369+
const invalidLinkHelpText = getInvalidLinkHelpText();
369370

370371
return (
371372
<>
@@ -400,7 +401,7 @@ export default function NavigationLinkEdit( {
400401
<div { ...blockProps }>
401402
{ hasMissingEntity && (
402403
<VisuallyHidden id={ missingEntityDescriptionId }>
403-
<MissingEntityHelpText type={ type } kind={ kind } />
404+
{ invalidLinkHelpText }
404405
</VisuallyHidden>
405406
) }
406407
{ /* eslint-disable jsx-a11y/anchor-is-valid */ }

packages/block-library/src/navigation-link/shared/controls.js

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { useHandleLinkChange } from './use-handle-link-change';
2222
import { useEntityBinding } from './use-entity-binding';
2323
import { getSuggestionsQuery } from '../link-ui';
2424
import { useLinkPreview } from './use-link-preview';
25+
import { useIsInvalidLink } from './use-is-invalid-link';
2526
import { unlock } from '../../lock-unlock';
2627

2728
const { LinkPicker } = unlock( blockEditorPrivateApis );
@@ -79,17 +80,26 @@ export function Controls( { attributes, setAttributes, clientId } ) {
7980
attributes,
8081
} );
8182

82-
const needsHelpText = hasUrlBinding;
83-
const helpText = isBoundEntityAvailable
84-
? BindingHelpText( {
85-
type: attributes.type,
86-
kind: attributes.kind,
87-
} )
88-
: MissingEntityHelpText( {
89-
type: attributes.type,
90-
kind: attributes.kind,
91-
} );
83+
const [ isInvalid, isDraft ] = useIsInvalidLink(
84+
attributes.kind,
85+
attributes.type,
86+
entityRecord?.id,
87+
hasUrlBinding
88+
);
89+
90+
let helpText = '';
9291

92+
if ( isInvalid || ( hasUrlBinding && ! isBoundEntityAvailable ) ) {
93+
// Show invalid link help text for:
94+
// 1. Invalid post-type links (trashed/deleted posts/pages) - via useIsInvalidLink
95+
// 2. Missing bound taxonomy entities (deleted categories/tags) - useIsInvalidLink only checks post-types
96+
helpText = getInvalidLinkHelpText();
97+
} else if ( isDraft ) {
98+
helpText = getDraftHelpText( {
99+
type: attributes.type,
100+
kind: attributes.kind,
101+
} );
102+
}
93103
// Get the link change handler with built-in binding management
94104
const handleLinkChange = useHandleLinkChange( {
95105
clientId,
@@ -184,7 +194,7 @@ export function Controls( { attributes, setAttributes, clientId } ) {
184194
attributes.kind
185195
) }
186196
label={ __( 'Link to' ) }
187-
help={ needsHelpText ? helpText : undefined }
197+
help={ helpText ? helpText : undefined }
188198
/>
189199
</ToolsPanelItem>
190200

@@ -243,37 +253,32 @@ export function Controls( { attributes, setAttributes, clientId } ) {
243253
</ToolsPanel>
244254
);
245255
}
246-
247256
/**
248-
* Component to display help text for bound URL attributes.
257+
* Returns help text for invalid links.
249258
*
250-
* @param {Object} props - Component props
251-
* @param {string} props.type - The entity type
252-
* @param {string} props.kind - The entity kind
253-
* @return {string} Help text for the bound URL
259+
* @return {string} Error help text string (empty string if valid).
254260
*/
255-
export function BindingHelpText( { type, kind } ) {
256-
const entityType = getEntityTypeName( type, kind );
257-
return sprintf(
258-
/* translators: %s is the entity type (e.g., "page", "post", "category") */
259-
__( 'Synced with the selected %s.' ),
260-
entityType
261+
export function getInvalidLinkHelpText() {
262+
return __(
263+
'This link is invalid and will not appear on your site. Please update the link.'
261264
);
262265
}
263266

264267
/**
265-
* Component to display error help text for missing entity bindings.
268+
* Returns the help text for links to draft entities
266269
*
267-
* @param {Object} props - Component props
270+
* @param {Object} props - Function props
268271
* @param {string} props.type - The entity type
269272
* @param {string} props.kind - The entity kind
270-
* @return {JSX.Element} Error help text component
273+
* @return {string} Draft help text
271274
*/
272-
export function MissingEntityHelpText( { type, kind } ) {
275+
function getDraftHelpText( { type, kind } ) {
273276
const entityType = getEntityTypeName( type, kind );
274277
return sprintf(
275-
/* translators: %s is the entity type (e.g., "page", "post", "category") */
276-
__( 'Synced %s is missing. Please update or remove this link.' ),
278+
/* translators: %1$s is the entity type (e.g., "page", "post", "category") */
279+
__(
280+
'This link is to a draft %1$s and will not appear on your site until the %1$s is published.'
281+
),
277282
entityType
278283
);
279284
}

packages/block-library/src/navigation-link/shared/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* to reduce code duplication and ensure consistent behavior.
66
*/
77

8-
export { Controls, BindingHelpText, MissingEntityHelpText } from './controls';
8+
export { Controls, getInvalidLinkHelpText } from './controls';
99
export { updateAttributes } from './update-attributes';
1010
export {
1111
useEntityBinding,

packages/block-library/src/navigation-link/shared/test/controls.js

Lines changed: 52 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ jest.mock( '../use-entity-binding', () => ( {
3232
} ) ),
3333
} ) );
3434

35+
// Mock the useIsInvalidLink hook
36+
jest.mock( '../use-is-invalid-link', () => ( {
37+
useIsInvalidLink: jest.fn( () => [ false, false ] ),
38+
} ) );
39+
3540
describe( 'Controls', () => {
3641
// Initialize the mock function
3742
beforeAll( () => {
@@ -54,6 +59,18 @@ describe( 'Controls', () => {
5459
beforeEach( () => {
5560
jest.clearAllMocks();
5661
mockUpdateAttributes.mockClear();
62+
63+
// Reset useEntityBinding mock to default
64+
const { useEntityBinding } = require( '../use-entity-binding' );
65+
useEntityBinding.mockReturnValue( {
66+
hasUrlBinding: false,
67+
isBoundEntityAvailable: false,
68+
clearBinding: jest.fn(),
69+
} );
70+
71+
// Reset useIsInvalidLink mock to default
72+
const { useIsInvalidLink } = require( '../use-is-invalid-link' );
73+
useIsInvalidLink.mockReturnValue( [ false, false ] );
5774
} );
5875

5976
it( 'renders all form controls', () => {
@@ -119,77 +136,59 @@ describe( 'Controls', () => {
119136
} );
120137

121138
describe( 'URL binding help text', () => {
122-
it( 'shows help text when URL is bound to an entity', () => {
139+
it( 'shows invalid link help text when bound entity is not available', () => {
123140
const { useEntityBinding } = require( '../use-entity-binding' );
124141
useEntityBinding.mockReturnValue( {
125142
hasUrlBinding: true,
126-
isBoundEntityAvailable: true,
143+
isBoundEntityAvailable: false,
127144
clearBinding: jest.fn(),
128145
} );
129146

130-
const propsWithBinding = {
147+
const propsWithCategoryBinding = {
131148
...defaultProps,
132149
attributes: {
133150
...defaultProps.attributes,
134-
type: 'page',
135-
kind: 'post-type',
151+
type: 'category',
152+
kind: 'taxonomy',
136153
},
137154
};
138155

139-
render( <Controls { ...propsWithBinding } /> );
156+
render( <Controls { ...propsWithCategoryBinding } /> );
140157

141158
expect(
142-
screen.getByText( 'Synced with the selected page.' )
159+
screen.getByText(
160+
'This link is invalid and will not appear on your site. Please update the link.'
161+
)
143162
).toBeInTheDocument();
144163
} );
145164

146-
it( 'shows help text for different entity types', () => {
147-
const { useEntityBinding } = require( '../use-entity-binding' );
148-
useEntityBinding.mockReturnValue( {
149-
hasUrlBinding: true,
150-
isBoundEntityAvailable: true,
151-
clearBinding: jest.fn(),
152-
} );
165+
it( 'shows draft help text for draft entities', () => {
166+
const { useIsInvalidLink } = require( '../use-is-invalid-link' );
167+
useIsInvalidLink.mockReturnValue( [ false, true ] ); // isInvalid: false, isDraft: true
153168

154-
const propsWithCategoryBinding = {
169+
const propsWithDraftPage = {
155170
...defaultProps,
156171
attributes: {
157172
...defaultProps.attributes,
158-
type: 'category',
159-
kind: 'taxonomy',
173+
type: 'page',
174+
kind: 'post-type',
160175
},
161176
};
162177

163-
render( <Controls { ...propsWithCategoryBinding } /> );
178+
render( <Controls { ...propsWithDraftPage } /> );
164179

165180
expect(
166-
screen.getByText( 'Synced with the selected category.' )
181+
screen.getByText(
182+
'This link is to a draft page and will not appear on your site until the page is published.'
183+
)
167184
).toBeInTheDocument();
168185
} );
169186

170-
it( 'does not show help text when URL is not bound', () => {
171-
const { useEntityBinding } = require( '../use-entity-binding' );
172-
useEntityBinding.mockReturnValue( {
173-
hasUrlBinding: false,
174-
clearBinding: jest.fn(),
175-
} );
176-
177-
render( <Controls { ...defaultProps } /> );
178-
179-
expect(
180-
screen.queryByText( /Synced with the selected/ )
181-
).not.toBeInTheDocument();
182-
} );
183-
184-
it( 'shows help text for post entity type', () => {
185-
const { useEntityBinding } = require( '../use-entity-binding' );
186-
useEntityBinding.mockReturnValue( {
187-
hasUrlBinding: true,
188-
isBoundEntityAvailable: true,
189-
clearBinding: jest.fn(),
190-
} );
187+
it( 'shows draft help text for different entity types', () => {
188+
const { useIsInvalidLink } = require( '../use-is-invalid-link' );
189+
useIsInvalidLink.mockReturnValue( [ false, true ] );
191190

192-
const propsWithPostBinding = {
191+
const propsWithDraftPost = {
193192
...defaultProps,
194193
attributes: {
195194
...defaultProps.attributes,
@@ -198,35 +197,30 @@ describe( 'Controls', () => {
198197
},
199198
};
200199

201-
render( <Controls { ...propsWithPostBinding } /> );
200+
render( <Controls { ...propsWithDraftPost } /> );
202201

203202
expect(
204-
screen.getByText( 'Synced with the selected post.' )
203+
screen.getByText(
204+
'This link is to a draft post and will not appear on your site until the post is published.'
205+
)
205206
).toBeInTheDocument();
206207
} );
207208

208-
it( 'shows help text for tag entity type', () => {
209-
const { useEntityBinding } = require( '../use-entity-binding' );
210-
useEntityBinding.mockReturnValue( {
211-
hasUrlBinding: true,
212-
isBoundEntityAvailable: true,
213-
clearBinding: jest.fn(),
214-
} );
215-
216-
const propsWithTagBinding = {
209+
it( 'does not show help text for valid link', () => {
210+
const propsWithValidLink = {
217211
...defaultProps,
218212
attributes: {
219213
...defaultProps.attributes,
220-
type: 'tag',
221-
kind: 'taxonomy',
214+
url: 'https://example.com',
215+
type: 'page',
216+
kind: 'post-type',
222217
},
223218
};
224219

225-
render( <Controls { ...propsWithTagBinding } /> );
220+
render( <Controls { ...propsWithValidLink } /> );
226221

227-
expect(
228-
screen.getByText( 'Synced with the selected tag.' )
229-
).toBeInTheDocument();
222+
// When link is valid (not invalid, not draft, no binding issues), no help text should be shown
223+
expect( screen.queryByText( /This link/ ) ).not.toBeInTheDocument();
230224
} );
231225
} );
232226
} );

packages/block-library/src/navigation-link/shared/use-link-preview.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* WordPress dependencies
33
*/
4-
import { __ } from '@wordpress/i18n';
4+
import { __, sprintf } from '@wordpress/i18n';
55
import { safeDecodeURI } from '@wordpress/url';
66
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
77

@@ -93,10 +93,17 @@ function computeBadges( {
9393
}
9494

9595
// Status badge
96-
if ( ! url ) {
96+
if ( hasBinding && ! isEntityAvailable ) {
97+
badges.push( {
98+
label: sprintf(
99+
/* translators: %s is the entity type (e.g., "page", "post", "category") */
100+
__( 'Missing %s' ),
101+
type
102+
),
103+
intent: 'error',
104+
} );
105+
} else if ( ! url ) {
97106
badges.push( { label: __( 'No link selected' ), intent: 'error' } );
98-
} else if ( hasBinding && ! isEntityAvailable ) {
99-
badges.push( { label: __( 'Deleted' ), intent: 'error' } );
100107
} else if ( entityStatus ) {
101108
const statusMap = {
102109
publish: { label: __( 'Published' ), intent: 'success' },

test/e2e/specs/editor/blocks/navigation.spec.js

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,13 +1202,6 @@ test.describe( 'Navigation block', () => {
12021202
await expect( linkButton ).toContainText(
12031203
url.pathname.replace( /\/$/, '' )
12041204
);
1205-
1206-
// Verify help text
1207-
await expect(
1208-
settingsControls.getByText(
1209-
'Synced with the selected page.'
1210-
)
1211-
).toBeVisible();
12121205
} );
12131206

12141207
await test.step( 'Verify bound link works correctly on frontend', async () => {
@@ -1633,14 +1626,14 @@ test.describe( 'Navigation block', () => {
16331626

16341627
// With LinkControlInspector, unavailable entities show a button with error badge
16351628
const linkButton = settingsControls.getByRole( 'button', {
1636-
name: /No link selected/i,
1629+
name: /Missing page/i,
16371630
} );
16381631

16391632
// Button is enabled (can click to fix the link)
16401633
await expect( linkButton ).toBeEnabled();
16411634

1642-
// Button should show "No link selected" for unavailable entity
1643-
await expect( linkButton ).toContainText( 'No link selected' );
1635+
// Button should show "Missing page" for unavailable entity
1636+
await expect( linkButton ).toContainText( 'Missing page' );
16441637
} );
16451638

16461639
await test.step( 'Verify clicking button with error opens link control for fixing', async () => {
@@ -1649,7 +1642,7 @@ test.describe( 'Navigation block', () => {
16491642
.getByRole( 'tabpanel', { name: 'Settings' } );
16501643

16511644
const linkButton = settingsControls.getByRole( 'button', {
1652-
name: /No link selected/i,
1645+
name: /Missing page/i,
16531646
} );
16541647

16551648
// Click the button to open the link control and fix the link

0 commit comments

Comments
 (0)