Skip to content
Merged
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
12 changes: 7 additions & 5 deletions packages/block-library/src/navigation-link/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -553,17 +553,19 @@ export default function NavigationLinkEdit( {
anchor={ popoverAnchor }
onRemove={ removeLink }
onChange={ ( updatedValue ) => {
updateAttributes(
const { isEntityLink } = updateAttributes(
updatedValue,
setAttributes,
attributes
);

// Handle URL binding
if ( ! updatedValue?.id ) {
clearBinding();
} else {
// Handle URL binding based on the final computed state
// Only create bindings for entity links (posts, pages, taxonomies)
// Never create bindings for custom links (manual URLs)
if ( isEntityLink ) {
createBinding();
} else {
clearBinding();
}
} }
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1133,4 +1133,175 @@ describe( 'updateAttributes', () => {
);
} );
} );

describe( 'Return value metadata', () => {
describe( 'isEntityLink', () => {
it( 'should return true for entity links with id and non-custom kind', () => {
const setAttributes = jest.fn();
const linkSuggestion = {
id: 123,
kind: 'post-type',
type: 'page',
url: 'https://example.com/page',
title: 'Test Page',
};

const result = updateAttributes(
linkSuggestion,
setAttributes
);

expect( result ).toEqual( {
isEntityLink: true,
} );
} );

it( 'should return false for custom links even with id', () => {
const setAttributes = jest.fn();
const linkSuggestion = {
id: 123,
kind: 'custom',
type: 'custom',
url: 'https://example.com/custom',
title: 'Custom Link',
};

const result = updateAttributes(
linkSuggestion,
setAttributes
);

expect( result ).toEqual( {
isEntityLink: false,
} );
} );

it( 'should return false for links without id', () => {
const setAttributes = jest.fn();
const linkSuggestion = {
url: 'https://example.com',
title: 'Example',
};

const result = updateAttributes(
linkSuggestion,
setAttributes
);

expect( result ).toEqual( {
isEntityLink: false,
} );
} );

it( 'should return false when entity link is severed', () => {
const setAttributes = jest.fn();
const blockAttributes = {
id: 123,
type: 'page',
kind: 'post-type',
url: 'https://example.com/original-page',
};

const updatedValue = {
url: 'https://example.com/different-page',
};

const result = updateAttributes(
updatedValue,
setAttributes,
blockAttributes
);

// Should return false because the link was severed and converted to custom
expect( result ).toEqual( {
isEntityLink: false,
} );
} );

it( 'should return true when entity link is preserved through query string change', () => {
const setAttributes = jest.fn();
const blockAttributes = {
id: 123,
type: 'page',
kind: 'post-type',
url: 'https://example.com/page',
};

const updatedValue = {
url: 'https://example.com/page?foo=bar',
};

const result = updateAttributes(
updatedValue,
setAttributes,
blockAttributes
);

// Should return true because entity link is preserved
expect( result ).toEqual( {
isEntityLink: true,
} );
} );

it( 'should return false for mailto links', () => {
const setAttributes = jest.fn();
const linkSuggestion = {
id: 'mailto:test@example.com',
type: 'mailto',
url: 'mailto:test@example.com',
title: 'mailto:test@example.com',
};

const result = updateAttributes(
linkSuggestion,
setAttributes
);

// mailto links have kind: 'custom', so isEntityLink should be false
expect( result ).toEqual( {
isEntityLink: false,
} );
} );

it( 'should return false for tel links', () => {
const setAttributes = jest.fn();
const linkSuggestion = {
id: 'tel:5555555',
type: 'tel',
url: 'tel:5555555',
title: 'tel:5555555',
};

const result = updateAttributes(
linkSuggestion,
setAttributes
);

// tel links have kind: 'custom', so isEntityLink should be false
expect( result ).toEqual( {
isEntityLink: false,
} );
} );

it( 'should return true for taxonomy links', () => {
const setAttributes = jest.fn();
const linkSuggestion = {
id: 5,
kind: 'taxonomy',
type: 'category',
url: 'https://example.com/category/news',
title: 'News',
};

const result = updateAttributes(
linkSuggestion,
setAttributes
);

expect( result ).toEqual( {
isEntityLink: true,
} );
} );
} );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,19 @@ export const updateAttributes = (
}

setAttributes( attributes );

// Return metadata about the final state for binding decisions.
// We need to distinguish between:
// 1. Property not set in attributes (use blockAttributes fallback)
// 2. Property explicitly set to undefined (means "remove this")
// Using 'in' operator checks if property exists, even if undefined.
// This is critical for severing: attributes.id = undefined means "remove the ID",
// not "keep the old ID from blockAttributes".
const finalId = 'id' in attributes ? attributes.id : blockAttributes.id;
const finalKind =
'kind' in attributes ? attributes.kind : blockAttributes.kind;

return {
isEntityLink: !! finalId && finalKind !== 'custom',
};
};
13 changes: 8 additions & 5 deletions packages/block-library/src/navigation-submenu/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,17 +435,20 @@ export default function NavigationSubmenuEdit( {
speak( __( 'Link removed.' ), 'assertive' );
} }
onChange={ ( updatedValue ) => {
updateAttributes(
// updateAttributes determines the final state and returns metadata
const { isEntityLink } = updateAttributes(
updatedValue,
setAttributes,
attributes
);

// Handle URL binding
if ( ! updatedValue?.id ) {
clearBinding();
} else {
// Handle URL binding based on the final computed state
// Only create bindings for entity links (posts, pages, taxonomies)
// Never create bindings for custom links (manual URLs)
if ( isEntityLink ) {
createBinding();
} else {
clearBinding();
}
} }
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import { unlock } from '../../lock-unlock';
import DeletedNavigationWarning from './deleted-navigation-warning';
import useNavigationMenu from '../use-navigation-menu';
import LeafMoreMenu from './leaf-more-menu';
import { LinkUI, updateAttributes } from '../../navigation-link/shared';
import {
LinkUI,
updateAttributes,
useEntityBinding,
} from '../../navigation-link/shared';

const actionLabel =
/* translators: %s: The name of a menu. */ __( "Switch to '%s'" );
Expand All @@ -43,6 +47,12 @@ function AdditionalBlockContent( { block, insertedBlock, setInsertedBlock } ) {
const blockWasJustInserted = insertedBlock?.clientId === block.clientId;
const showLinkControls = supportsLinkControls && blockWasJustInserted;

// Get binding utilities for the inserted block
const { createBinding, clearBinding } = useEntityBinding( {
clientId: insertedBlock?.clientId,
attributes: insertedBlock?.attributes || {},
} );

if ( ! showLinkControls ) {
return null;
}
Expand Down Expand Up @@ -100,11 +110,22 @@ function AdditionalBlockContent( { block, insertedBlock, setInsertedBlock } ) {
cleanupInsertedBlock();
} }
onChange={ ( updatedValue ) => {
updateAttributes(
// updateAttributes determines the final state and returns metadata
const { isEntityLink } = updateAttributes(
updatedValue,
setInsertedBlockAttributes( insertedBlock?.clientId ),
insertedBlock?.attributes
);

// Handle URL binding based on the final computed state
// Only create bindings for entity links (posts, pages, taxonomies)
// Never create bindings for custom links (manual URLs)
if ( isEntityLink ) {
createBinding();
} else {
clearBinding();
}

setInsertedBlock( null );
} }
/>
Expand Down
Loading