Skip to content

Commit 376fdfb

Browse files
jeryjjeryjmikachangetdavescruffian
authored
Informational Parity between Inspector Link Preview and on Canvas Link Preview (#75399)
* Move featured image and title fetching into useLinkPreview * Pass useLinkPreview values to LinkUI for LinkControl to output featured image * Apply badges to link control link preview * Always show url to match inspector link preview * Only fetch useLinkPreview info if popover is open * Make entity data optional for LinkUI and add full feature to navigation-submenu * Add tests * fix: ensure unique badge keys in LinkPreview --------- Co-authored-by: jeryj <jeryj@git.wordpress.org> Co-authored-by: mikachan <mikachan@git.wordpress.org> Co-authored-by: getdave <get_dave@git.wordpress.org> Co-authored-by: scruffian <scruffian@git.wordpress.org>
1 parent 65e3bc8 commit 376fdfb

10 files changed

Lines changed: 523 additions & 89 deletions

File tree

‎packages/block-editor/src/components/link-control/link-preview.js‎

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import {
1111
Button,
1212
ExternalLink,
1313
__experimentalTruncate as Truncate,
14+
__experimentalHStack as HStack,
1415
Flex,
16+
privateApis as componentsPrivateApis,
1517
} from '@wordpress/components';
1618
import { useCopyToClipboard } from '@wordpress/compose';
1719
import { filterURLForDisplay, safeDecodeURI } from '@wordpress/url';
@@ -31,23 +33,12 @@ import { store as preferencesStore } from '@wordpress/preferences';
3133
/**
3234
* Internal dependencies
3335
*/
36+
import { unlock } from '../../lock-unlock';
3437
import { ViewerSlot } from './viewer-slot';
3538

36-
import useRichUrlData from './use-rich-url-data';
39+
const { Badge } = unlock( componentsPrivateApis );
3740

38-
/**
39-
* Filters the title for display. Removes the protocol and www prefix.
40-
*
41-
* @param {string} title The title to be filtered.
42-
*
43-
* @return {string} The filtered title.
44-
*/
45-
function filterTitleForDisplay( title ) {
46-
// Derived from `filterURLForDisplay` in `@wordpress/url`.
47-
return title
48-
.replace( /^[a-z\-.\+]+[0-9]*:(\/\/)?/i, '' )
49-
.replace( /^www\./i, '' );
50-
}
41+
import useRichUrlData from './use-rich-url-data';
5142

5243
export default function LinkPreview( {
5344
value,
@@ -81,9 +72,6 @@ export default function LinkPreview( {
8172
! isEmptyURL &&
8273
stripHTML( richData?.title || value?.title || displayURL );
8374

84-
const isUrlRedundant =
85-
! value?.url || filterTitleForDisplay( displayTitle ) === displayURL;
86-
8775
let icon;
8876

8977
if ( richData?.icon ) {
@@ -115,7 +103,7 @@ export default function LinkPreview( {
115103
'is-url-title': displayTitle === displayURL,
116104
} ) }
117105
>
118-
<Flex gap={ 0 }>
106+
<Flex gap={ 0 } align="flex-start">
119107
<Flex
120108
className="block-editor-link-control__link-information"
121109
role="figure"
@@ -124,22 +112,32 @@ export default function LinkPreview( {
124112
__( 'Link information' )
125113
}
126114
justify="start"
115+
align="flex-start"
127116
>
128-
<Flex
129-
className={ clsx(
130-
'block-editor-link-control__preview-icon',
131-
{
132-
'is-image': richData?.icon,
133-
}
134-
) }
135-
justify="center"
136-
>
137-
{ icon }
138-
</Flex>
117+
{ value?.image ? (
118+
<Flex
119+
className="block-editor-link-control__preview-image"
120+
justify="center"
121+
>
122+
<img src={ value?.image } alt="" />
123+
</Flex>
124+
) : (
125+
<Flex
126+
className={ clsx(
127+
'block-editor-link-control__preview-icon',
128+
{
129+
'is-image': richData?.icon,
130+
}
131+
) }
132+
justify="center"
133+
>
134+
{ icon }
135+
</Flex>
136+
) }
139137
<Flex
140138
className="block-editor-link-control__preview-details"
141139
direction="column"
142-
gap={ 1 }
140+
gap={ 2 }
143141
>
144142
{ ! isEmptyURL ? (
145143
<>
@@ -151,12 +149,28 @@ export default function LinkPreview( {
151149
{ displayTitle }
152150
</Truncate>
153151
</ExternalLink>
154-
{ ! isUrlRedundant && (
155-
<span className="block-editor-link-control__preview-info">
156-
<Truncate numberOfLines={ 1 }>
157-
{ displayURL }
158-
</Truncate>
159-
</span>
152+
<span className="block-editor-link-control__preview-info">
153+
<Truncate numberOfLines={ 1 }>
154+
{ displayURL }
155+
</Truncate>
156+
</span>
157+
{ value?.badges?.length > 0 && (
158+
<HStack
159+
className="block-editor-link-control__preview-badges"
160+
alignment="left"
161+
gap={ 1 }
162+
>
163+
{ value.badges.map(
164+
( badge, index ) => (
165+
<Badge
166+
key={ `${ badge.label }|${ badge.intent }|${ index }` }
167+
intent={ badge.intent }
168+
>
169+
{ badge.label }
170+
</Badge>
171+
)
172+
) }
173+
</HStack>
160174
) }
161175
</>
162176
) : (

‎packages/block-editor/src/components/link-control/style.scss‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,17 +192,18 @@ $block-editor-link-control-number-of-actions: 1;
192192
word-break: break-all;
193193
}
194194

195+
.block-editor-link-control__preview-image,
195196
.block-editor-link-control__preview-icon {
196197
position: relative;
197198
flex-shrink: 0;
198199
background-color: $gray-100;
199200
width: $grid-unit-40;
200201
height: $grid-unit-40;
201202
border-radius: $radius-small;
203+
}
202204

203-
img {
204-
width: $grid-unit-20; // favicons often have a source of 32px
205-
}
205+
.block-editor-link-control__preview-icon img {
206+
width: $grid-unit-20; // favicons often have a source of 32px
206207
}
207208

208209
&.is-error .block-editor-link-control__preview-icon {

‎packages/block-editor/src/components/link-control/test/index.js‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3622,3 +3622,119 @@ async function toggleSettingsDrawer( user ) {
36223622

36233623
await user.click( settingsToggle );
36243624
}
3625+
3626+
describe( 'Link preview with entity data from navigation blocks', () => {
3627+
describe( 'Featured image display', () => {
3628+
it( 'should display featured image in link preview when entity provides image', () => {
3629+
const linkWithImage = {
3630+
url: 'https://example.com/my-page',
3631+
title: 'My Test Page',
3632+
image: 'https://example.com/featured.jpg',
3633+
};
3634+
3635+
render( <LinkControl value={ linkWithImage } /> );
3636+
3637+
const linkPreview = screen.getByRole( 'group', {
3638+
name: 'Manage link',
3639+
} );
3640+
3641+
// eslint-disable-next-line testing-library/no-node-access
3642+
const imagePreview = linkPreview.querySelector(
3643+
'.block-editor-link-control__preview-image img'
3644+
);
3645+
3646+
expect( imagePreview ).toBeInTheDocument();
3647+
expect( imagePreview ).toHaveAttribute(
3648+
'src',
3649+
'https://example.com/featured.jpg'
3650+
);
3651+
} );
3652+
3653+
it( 'should not display featured image section when entity has no image', () => {
3654+
const linkWithoutImage = {
3655+
url: 'https://example.com/my-page',
3656+
title: 'My Test Page',
3657+
};
3658+
3659+
render( <LinkControl value={ linkWithoutImage } /> );
3660+
3661+
const linkPreview = screen.getByRole( 'group', {
3662+
name: 'Manage link',
3663+
} );
3664+
3665+
// eslint-disable-next-line testing-library/no-node-access
3666+
const imagePreview = linkPreview.querySelector(
3667+
'.block-editor-link-control__preview-image'
3668+
);
3669+
3670+
expect( imagePreview ).not.toBeInTheDocument();
3671+
} );
3672+
} );
3673+
3674+
describe( 'Entity status badges', () => {
3675+
it( 'should display badges in link preview when entity provides badges', () => {
3676+
const linkWithBadges = {
3677+
url: 'https://example.com/my-page',
3678+
title: 'My Test Page',
3679+
badges: [
3680+
{ label: 'Draft', intent: 'warning' },
3681+
{ label: 'Page', intent: 'default' },
3682+
],
3683+
};
3684+
3685+
render( <LinkControl value={ linkWithBadges } /> );
3686+
3687+
const linkPreview = screen.getByRole( 'group', {
3688+
name: 'Manage link',
3689+
} );
3690+
3691+
// eslint-disable-next-line testing-library/no-node-access
3692+
const badgesContainer = linkPreview.querySelector(
3693+
'.block-editor-link-control__preview-badges'
3694+
);
3695+
3696+
expect( badgesContainer ).toBeInTheDocument();
3697+
} );
3698+
3699+
it( 'should not display badges section when entity provides no badges', () => {
3700+
const linkWithoutBadges = {
3701+
url: 'https://example.com/my-page',
3702+
title: 'My Test Page',
3703+
};
3704+
3705+
render( <LinkControl value={ linkWithoutBadges } /> );
3706+
3707+
const linkPreview = screen.getByRole( 'group', {
3708+
name: 'Manage link',
3709+
} );
3710+
3711+
// eslint-disable-next-line testing-library/no-node-access
3712+
const badgesContainer = linkPreview.querySelector(
3713+
'.block-editor-link-control__preview-badges'
3714+
);
3715+
3716+
expect( badgesContainer ).not.toBeInTheDocument();
3717+
} );
3718+
3719+
it( 'should not display badges section when entity provides empty badges array', () => {
3720+
const linkWithEmptyBadges = {
3721+
url: 'https://example.com/my-page',
3722+
title: 'My Test Page',
3723+
badges: [],
3724+
};
3725+
3726+
render( <LinkControl value={ linkWithEmptyBadges } /> );
3727+
3728+
const linkPreview = screen.getByRole( 'group', {
3729+
name: 'Manage link',
3730+
} );
3731+
3732+
// eslint-disable-next-line testing-library/no-node-access
3733+
const badgesContainer = linkPreview.querySelector(
3734+
'.block-editor-link-control__preview-badges'
3735+
);
3736+
3737+
expect( badgesContainer ).not.toBeInTheDocument();
3738+
} );
3739+
} );
3740+
} );

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,11 @@ export default function NavigationLinkEdit( {
167167
const { getBlocks } = useSelect( blockEditorStore );
168168

169169
// URL binding logic
170-
const { hasUrlBinding, isBoundEntityAvailable } = useEntityBinding( {
171-
clientId,
172-
attributes,
173-
} );
170+
const { hasUrlBinding, isBoundEntityAvailable, entityRecord } =
171+
useEntityBinding( {
172+
clientId,
173+
attributes,
174+
} );
174175

175176
const handleLinkChange = useHandleLinkChange( {
176177
clientId,
@@ -462,6 +463,11 @@ export default function NavigationLinkEdit( {
462463
ref={ linkUIref }
463464
clientId={ clientId }
464465
link={ attributes }
466+
entity={ {
467+
entityRecord,
468+
hasBinding: hasUrlBinding,
469+
isEntityAvailable: isBoundEntityAvailable,
470+
} }
465471
onClose={ () => {
466472
setIsLinkOpen( false );
467473
// If there is no link and no binding, remove the auto-inserted block.

‎packages/block-library/src/navigation-link/link-ui/index.js‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { isURL } from '@wordpress/url';
2727
*/
2828
import { LinkUIPageCreator } from './page-creator';
2929
import LinkUIBlockInserter from './block-inserter';
30-
import { useEntityBinding } from '../shared/use-entity-binding';
30+
import { useEntityBinding, useLinkPreview } from '../shared';
3131

3232
/**
3333
* Given the Link block's type attribute, return the query params to give to
@@ -72,6 +72,17 @@ export function getSuggestionsQuery( type, kind ) {
7272

7373
function UnforwardedLinkUI( props, ref ) {
7474
const { label, url, opensInNewTab, type, kind, id } = props.link;
75+
76+
const { entityRecord, hasBinding, isEntityAvailable } = props.entity || {};
77+
78+
const { image, badges } = useLinkPreview( {
79+
url,
80+
entityRecord,
81+
type,
82+
hasBinding,
83+
isEntityAvailable,
84+
} );
85+
7586
const { clientId } = props;
7687
const postType = type || 'page';
7788

@@ -106,8 +117,10 @@ function UnforwardedLinkUI( props, ref ) {
106117
kind,
107118
type,
108119
id,
120+
image,
121+
badges,
109122
} ),
110-
[ label, opensInNewTab, url, kind, type, id ]
123+
[ label, opensInNewTab, url, kind, type, id, image, badges ]
111124
);
112125

113126
const handlePageCreated = ( pageLink ) => {

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

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -115,38 +115,6 @@ export function Controls( { attributes, setAttributes, clientId } ) {
115115
setAttributes,
116116
} );
117117

118-
const linkTitle =
119-
entityRecord?.title?.rendered ||
120-
entityRecord?.title ||
121-
entityRecord?.name;
122-
123-
const linkImage = useSelect(
124-
( select ) => {
125-
// Only fetch for post-type entities with featured media
126-
if ( ! entityRecord?.featured_media ) {
127-
return null;
128-
}
129-
130-
const { getEntityRecord } = select( coreStore );
131-
132-
// Get the media entity to fetch the image URL
133-
const media = getEntityRecord(
134-
'postType',
135-
'attachment',
136-
entityRecord.featured_media
137-
);
138-
139-
// Return the thumbnail or medium size URL, fallback to source_url
140-
return (
141-
media?.media_details?.sizes?.thumbnail?.source_url ||
142-
media?.media_details?.sizes?.medium?.source_url ||
143-
media?.source_url ||
144-
null
145-
);
146-
},
147-
[ entityRecord?.featured_media ]
148-
);
149-
150118
const onNavigateToEntityRecord = useSelect(
151119
( select ) =>
152120
select( blockEditorStore ).getSettings().onNavigateToEntityRecord,
@@ -168,10 +136,8 @@ export function Controls( { attributes, setAttributes, clientId } ) {
168136

169137
const preview = useLinkPreview( {
170138
url,
171-
title: linkTitle,
172-
image: linkImage,
139+
entityRecord,
173140
type: attributes.type,
174-
entityStatus: entityRecord?.status,
175141
hasBinding: hasUrlBinding,
176142
isEntityAvailable: isBoundEntityAvailable,
177143
} );

0 commit comments

Comments
 (0)