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

Navigation: Allow Search block to be added alongside links #22656

Merged
merged 5 commits into from
Jul 3, 2020

Conversation

noisysocks
Copy link
Member

@noisysocks noisysocks commented May 27, 2020

This is a quick proof of concept for how I was thinking we could support saving non-core/navigation-link blocks in the Navigation Screen. I've demonstrated the approach by allowing Search to be added.

Being able to add different kinds of blocks to a site's existing menu is the primary reason we're building out a block-based Navigation Screen as it will allow more WordPress users to access blocks.

I'm focused only on a technical proof of concept here. See #22096 for discussion on the UX of adding different types of blocks to Navigation.

Implementation notes

  • The nav_menu_item CPT now supports $menu_item->type = 'html'. This accompanies the existing allowed types: taxonomy, post_type, post_type_archive and custom. When type is html it signifies that the menu item will render custom HTML, stored in a new $menu_item->content field, in place of the <a> link. The custom HTML may contain block markup.

  • $menu_item->content is stored in post meta. I wanted to use post_content but this is already taken by $menu_item->description. I thought about using post_content only if $menu_item→type === 'html' but decided against it as this may break third party code that reads $menu_item->post_content to access an item's description.

  • The experimental /menu-items REST API endpoint supports type=html and a new content property as above. The content property is split into content.raw and content.rendered so that it closely matches the /posts endpoint. content.raw may only be accessed when context=edit.

  • The approach requires minimal changes to Core that are easy for the Gutenberg plugin to shim.

    • wp_setup_nav_menu_item and wp_update_nav_menu_item must be be updated to save and populate the new $menu_item->content field.

    • Walker_Nav_Menu::start_el must be updated to display $menu_item->content instead of the <a>, $args→link_before, and $args->link_after.

  • I made no effort to make these new changes display nicely in nav-menus.php. I don't think it's important that we do this as these changes would likely land in Core alongside a nav-menus.php replacement.

  • Non-core/navigation-link blocks do not display in Navigation blocks that are added to a post or to a FSE template. This is because core/navigation overrides the link's render_callback. Extract navigation link rendering code from the navigation block #21075 will address this.

Outstanding questions

  • The resultant markup on the frontend looks like this:

    <ul id="primary-menu" class="menu nav-menu" aria-expanded="false">
        <li id="menu-item-118" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-118">
     	   <a href="#">A menu item</a>
        </li>
        <li id="menu-item-128" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children menu-item-128">
     	   <a href="#">A menu item with children</a>
     	   <ul class="sub-menu">
     		   <li id="menu-item-161" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-161">
     			   <a href="#">A child menu item</a>
     		   </li>
     	   </ul>
        </li>
        <li id="menu-item-193" class="menu-item menu-item-type-html menu-item-object- menu-item-193">
     	   <form class="wp-block-search" role="search" method="get" action="http://localhost:8888/">
     		   <label for="wp-block-search__input-1" class="wp-block-search__label">Search</label>
     		   <input type="search" id="wp-block-search__input-1" class="wp-block-search__input" name="s" value="" placeholder="" required="">
     		   <button type="submit" class="wp-block-search__button">Search</button>
     	   </form>
        </li>
    </ul>

    I suspect a great many themes will not work nicely with something other than an <a> element inside the <li>. How do we ease this transition?

    We definitely will want to add a filter which allows developers to disable html menu items. In addition to this, we could explore making html menu items opt-in using add_theme_support().

  • When $menu_item->type === 'html', $menu_item->title and $menu_item->url will both be set to ''. This will break any third party code that blindly expects a non-empty value. I don't think this would be very common, but it's worth thinking through.

  • I had to remove parent from core/navigation-link so that the Inserter appears when adding to a Navigation block. This means that the Navigation Link block appears in the Inserter when adding a block to a post or page, though. How do we make it so that Navigation Link can only be added to a Navigation block?

Tasks

Before merge

After merge

@noisysocks noisysocks added [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible [Status] In Progress Tracking issues with work in progress [Block] Navigation Affects the Navigation Block [Feature] Navigation Screen labels May 27, 2020
@github-actions
Copy link

github-actions bot commented May 27, 2020

Size Change: +560 B (0%)

Total Size: 1.13 MB

Filename Size Change
build/api-fetch/index.js 3.4 kB +1 B
build/autop/index.js 2.82 kB -3 B (0%)
build/block-directory/index.js 7.16 kB -255 B (3%)
build/block-directory/style-rtl.css 952 B +11 B (1%)
build/block-directory/style.css 951 B +9 B (0%)
build/block-editor/index.js 109 kB +63 B (0%)
build/block-editor/style-rtl.css 10.7 kB +28 B (0%)
build/block-editor/style.css 10.7 kB +26 B (0%)
build/block-library/editor-rtl.css 7.63 kB +7 B (0%)
build/block-library/editor.css 7.64 kB +7 B (0%)
build/block-library/index.js 130 kB +251 B (0%)
build/block-serialization-default-parser/index.js 1.88 kB -1 B
build/blocks/index.js 48.2 kB +3 B (0%)
build/components/index.js 198 kB +254 B (0%)
build/components/style-rtl.css 15.9 kB -47 B (0%)
build/components/style.css 15.8 kB -48 B (0%)
build/compose/index.js 9.65 kB +3 B (0%)
build/data/index.js 8.44 kB -1 B
build/date/index.js 5.47 kB -3 B (0%)
build/edit-navigation/index.js 9.97 kB +94 B (0%)
build/edit-post/index.js 304 kB +4 B (0%)
build/edit-post/style-rtl.css 5.57 kB +54 B (0%)
build/edit-post/style.css 5.56 kB +53 B (0%)
build/edit-site/index.js 16.6 kB +3 B (0%)
build/edit-site/style-rtl.css 3.02 kB +25 B (0%)
build/edit-site/style.css 3.02 kB +26 B (0%)
build/edit-widgets/index.js 9.32 kB +5 B (0%)
build/edit-widgets/style-rtl.css 2.45 kB +26 B (1%)
build/edit-widgets/style.css 2.45 kB +27 B (1%)
build/editor/index.js 44.8 kB -42 B (0%)
build/editor/style-rtl.css 3.82 kB -36 B (0%)
build/editor/style.css 3.82 kB -40 B (1%)
build/element/index.js 4.65 kB +1 B
build/format-library/index.js 7.73 kB +4 B (0%)
build/keyboard-shortcuts/index.js 2.51 kB -5 B (0%)
build/list-reusable-blocks/style-rtl.css 476 B +26 B (5%) 🔍
build/list-reusable-blocks/style.css 476 B +25 B (5%) 🔍
build/media-utils/index.js 5.3 kB +5 B (0%)
build/plugins/index.js 2.56 kB -1 B
build/server-side-render/index.js 2.68 kB +1 B
build/token-list/index.js 1.28 kB -1 B
build/url/index.js 4.06 kB +1 B
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 1.14 kB 0 B
build/annotations/index.js 3.62 kB 0 B
build/blob/index.js 620 B 0 B
build/block-library/style-rtl.css 7.79 kB 0 B
build/block-library/style.css 7.79 kB 0 B
build/block-library/theme-rtl.css 730 B 0 B
build/block-library/theme.css 732 B 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/core-data/index.js 11.4 kB 0 B
build/data-controls/index.js 1.29 kB 0 B
build/deprecated/index.js 772 B 0 B
build/dom-ready/index.js 569 B 0 B
build/dom/index.js 3.19 kB 0 B
build/edit-navigation/style-rtl.css 1.02 kB 0 B
build/edit-navigation/style.css 1.02 kB 0 B
build/editor/editor-styles-rtl.css 537 B 0 B
build/editor/editor-styles.css 539 B 0 B
build/escape-html/index.js 733 B 0 B
build/format-library/style-rtl.css 547 B 0 B
build/format-library/style.css 548 B 0 B
build/hooks/index.js 2.13 kB 0 B
build/html-entities/index.js 622 B 0 B
build/i18n/index.js 3.56 kB 0 B
build/is-shallow-equal/index.js 710 B 0 B
build/keycodes/index.js 1.94 kB 0 B
build/list-reusable-blocks/index.js 3.12 kB 0 B
build/notices/index.js 1.79 kB 0 B
build/nux/index.js 3.4 kB 0 B
build/nux/style-rtl.css 671 B 0 B
build/nux/style.css 668 B 0 B
build/primitives/index.js 1.5 kB 0 B
build/priority-queue/index.js 788 B 0 B
build/redux-routine/index.js 2.85 kB 0 B
build/rich-text/index.js 14 kB 0 B
build/shortcode/index.js 1.69 kB 0 B
build/viewport/index.js 1.85 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

@draganescu
Copy link
Contributor

I have tested this and it works quite well. I like the approach as it is using the full extent of our current data structures and data flow, without any new additions to them, which is great!

I support the proposal to make this opt in with add_theme_support(). Current testing with TwentyTwenty and TwentyFourteen showed the markup but were visually broken by it. Opting in will be beneficial even more considering that new blocks will be supported in the html content and those will break themes even worse than the current text with a form.

Opting in will also solve the problem of emptu title and url for html content type.

I had to remove parent from core/navigation-link so that the Inserter appears when adding to a Navigation block

I stumbled upon this while exploring additional blocks as children of Navigation, it feels like a bug that specifying parent has this effect. I expect that the inserter shows when the allowed blocks of InnerBlocks is more than one block type.

@adamziel
Copy link
Contributor

cc @TimothyBJacobs and @spacedmonkey as it touches the rest api endpoint

@adamziel
Copy link
Contributor

When $menu_item->type === 'html', $menu_item->title and $menu_item->url will both be set to ''. This will break any third party code that blindly expects a non-empty value. I don't think this would be very common, but it's worth thinking through.

I support what @draganescu said about opt-in capabilities. I also don't necessarily think this is a bad thing - conceptually it adds a new type of data which breaks BC a little bit, let's make it clear, easily debuggable, and apparent in the changelog. Concealing it by keeping title and url non-empty would likely introduce hard to find bugs in third party code.

@adamziel
Copy link
Contributor

adamziel commented May 27, 2020

I had to remove parent from core/navigation-link so that the Inserter appears when adding to a Navigation block. This means that the Navigation Link block appears in the Inserter when adding a block to a post or page, though. How do we make it so that Navigation Link can only be added to a Navigation block?

@noisysocks My understanding is that the way it currently work is this:

  1. InnerBlocks receives a list of allowedBlocks like this:
<InnerBlocks
	allowedBlocks={ [
		'core/navigation-link',
		'core/search',
	] }
	/>
  1. InserterBlockList grabs that list here:
    items: getInserterItems( rootClientId ),
  2. InserterBlockList also grabs the list of all blocks with matching parent: [] declaration here:
    rootChildBlocks: getChildBlockNames( rootBlockName ),
  3. Then, the InserterBlockList displays block choices using a logic similar to:
const allowedChildBlocks = intersection(allowedBlocks, childBlocks);
render( allowedChildBlocks.length ? allowedChildBlocks : allowedBlocks );

This means that declaring a parent: ["X"] in block-Y/block.json does not mean "I only want this block Y to be used as a child of block X" - which is what I would expect. Instead, it means "I only ever want block X to receive children where it's explicitly listed as parent". So specifying parent does not change the behavior of the current block (Y), but really the behavior of the parent block X. Once that declaration is present, the parent block X no longer supports any child blocks without the explicit parent declaration. It seems like a tradeoff that makes core blocks more extensible by third party libraries, but maybe we can separate these two concepts and have more granular control?

@adamziel
Copy link
Contributor

adamziel commented May 27, 2020

Maybe we could separate "supported children" from "supported parents" like this:

// navigation/block.json
{
	"name": "core/navigation",
	// Restrict possible children to the following ones only
	"children": [
		 "core/navigation-link",
		 "core/search"
	]
}
// navigation-link/block.json
{
	"name": "core/navigation-link",
	// Restrict possible parents to the following ones only
	"parent": [
		 "core/navigation"
	]
}

And then make it extensible with something like:

// third-party-search/block.json
{
	"name": "third-party/search",
	"extend": {
		"core/navigation": {
			"children": [
				"third-party/search"
			]
		}
	}
}

Although this would break BC and also involve changing the core heavily so maybe there's a simpler way?

@noisysocks
Copy link
Member Author

This means that declaring a parent: ["X"] in block-Y/block.json does not mean "I only want this block Y to be used as a child of block X" - which is what I would expect.

This is definitely how it used to work (#6753). Do you know why it was changed and if there's an alternative, @youknowriad?

@noisysocks
Copy link
Member Author

I support the proposal to make this opt in with add_theme_support(). Current testing with TwentyTwenty and TwentyFourteen showed the markup but were visually broken by it. Opting in will be beneficial even more considering that new blocks will be supported in the html content and those will break themes even worse than the current text with a form.

The problem with making it opt-in is that it means that the vast majority of themes and therefore the vast majority users will not gain access to blocks in this new context. If we go this route we will want to heavily publicise and document how theme authors can easily adapt their exiting themes to support blocks in navigation.

I wonder, though, if there's a way to make the new markup more compatible with existing theme stylesheets? I'm not sure if it's an impossible problem or if I'm just fresh out of ideas 😀

What do you think, @mtias?

@TimothyBJacobs
Copy link
Member

This approach looks good to me, we need to get test coverage on the new endpoint field.

Copy link
Contributor

@tellthemachines tellthemachines left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect a great many themes will not work nicely with something other than an element inside the

  • .

  • What might go wrong, apart from minor styling breakage? It's not looking too bad with the default themes. Using add_theme_support() sounds good though.

    We might also need to get rid of some default search block styles: (but that shouldn't be a matter for this PR)

    Screen Shot 2020-05-29 at 1 36 47 pm

    packages/block-library/src/navigation-link/block.json Outdated Show resolved Hide resolved
    @noisysocks
    Copy link
    Member Author

    noisysocks commented Jun 1, 2020

    Thanks for the feedback everyone! I've added a summary of next steps as I see them to the PR description.

    @adamziel
    Copy link
    Contributor

    adamziel commented Jun 1, 2020

    How should this behave in the navigator?

    @mtias
    Copy link
    Member

    mtias commented Jun 4, 2020

    How should this behave in the navigator?

    I'd expect it to just render like the main navigator (it's own block type with icon). Did you have any concerns in mind?

    @adamziel
    Copy link
    Contributor

    adamziel commented Jun 9, 2020

    I'd expect it to just render like the main navigator (it's own block type with icon). Did you have any concerns in mind?

    Not as much concerns as I wanted to get on the same page. So it would be just a regular navigator item labeled e.g. "Search"?

    @noisysocks
    Copy link
    Member Author

    This means that declaring a parent: ["X"] in block-Y/block.json does not mean "I only want this block Y to be used as a child of block X" - which is what I would expect.

    This is definitely how it used to work (#6753). Do you know why it was changed and if there's an alternative, @youknowriad?

    I'm working on #23231 which will fix this.

    @mtias
    Copy link
    Member

    mtias commented Jun 17, 2020

    So it would be just a regular navigator item labeled e.g. "Search"?

    Indeed:

    image

    @shaunandrews
    Copy link
    Contributor

    The inserter that appears when adding a new block to the Navigation has some inconsistent spacing, and perhaps a redundant "Navigation" icon+label:

    image

    I'd expect it to look something more like this:

    image

    --

    In general, the Search block leaves a lot to be desired; Its very restrictive in its current state. I realize this is unlikely to be resolved in this PR, but we do need to address it at some point.

    --

    In the editor I have two navigations, both with a Search block, and it mostly looks as I would expect:

    image

    But when I preview I just see a general "Search" label that doesn't do anything:

    image

    @shaunandrews
    Copy link
    Contributor

    Oh, and the search box on the inserter seems entirely useless. I get why its there, and I didn't even question it at first, but now I realize it doesn't really offer anything helpful.

    @noisysocks noisysocks marked this pull request as ready for review June 22, 2020 06:17
    @noisysocks
    Copy link
    Member Author

    I've rebased this and it's ready for review! 🙏

    Worth noting that #22789 means that the inserter flow is now a little nicer.

    2020-07-02 14 59 30

    Adds support for nav menu items with `type` set to `'html'`. These are
    nav menu items that can contain HTML or block markup which is rendered
    in place of the link. This allows the Navigation screen to use `'html`'
    menu items to support the Search block alongside links.
    if ( menuItem.type === 'html' ) {
    const parsedBlocks = parse( menuItem.content.raw );

    if ( parsedBlocks.length !== 1 ) {
    Copy link
    Contributor

    @adamziel adamziel Jul 2, 2020

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    This will kick in once a block plugin gets disabled. I don't think there's a good way of addressing it, but just wanted to double check - is this behavior consistent with the post editor?

    Copy link
    Member Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    If a block plugin is disabled then parse() will return an array containing one core/missing block. The body of this if() won't be executed and so a core/missing is what will be used for the menu item. This matches the behaviour of the post editor.

    The if() here is to guard against the case where, for whatever reason, there is 0 or >1 blocks in the menu item's HTML. I'm not really sure what to do in this case—it's undefined behaviour and probably indicates a bug somewhere. Perhaps failing loudly would be better?

    Copy link
    Member Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    I put it on my todo list to come back to this as will need to think on it further.

    Comment on lines +204 to +216
    if ( block.name === 'core/navigation-link' ) {
    attributes = {
    type: 'custom',
    title: block.attributes?.label,
    original_title: '',
    url: block.attributes.url,
    };
    } else {
    attributes = {
    type: 'html',
    content: serialize( block ),
    };
    }
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    It's totally too early for that, but a future refactor into a factory-like structure for transform/reverse transform could be a good idea to avoid a huge if/else mess.

    Copy link
    Member Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    I'm so excited because I love mess.

    lib/compat.php Outdated Show resolved Hide resolved
    function gutenberg_setup_html_nav_menu_item( $menu_item ) {
    if ( 'html' === $menu_item->type ) {
    $menu_item->type_label = __( 'HTML', 'gutenberg' );
    $menu_item->content = ! isset( $menu_item->content ) ? get_post_meta( $menu_item->db_id, '_menu_item_content', true ) : $menu_item->content;
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Not a blocker but just flagging this means plenty of DB roundtrips if the object cache happens to be disabled.

    Copy link
    Member Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    It's pretty par the course for wp_setup_nav_menu_item() which is where this code will eventually live.

    https://github.com/WordPress/wordpress-develop/blob/master/src/wp-includes/nav-menu.php#L806

    Copy link
    Contributor

    @adamziel adamziel left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    @noisysocks amazing job, I think we're almost there - this PR looks great to me! I found a single blocker which seems pretty easy to address. I am going to approve in advance, feel free to merge once that note is addressed!

    Also - this could be an addition to this PR or a follow-up exploration: Should the inserter in the navigator also offer the search block?

    Zrzut ekranu 2020-07-2 o 12 49 08

    Co-authored-by: Adam Zielinski <adam@adamziel.com>
    @noisysocks
    Copy link
    Member Author

    Also - this could be an addition to this PR or a follow-up exploration: Should the inserter in the navigator also offer the search block?

    Yes, good catch. Added to my list! 🙂

    @noisysocks noisysocks merged commit 0db58dd into master Jul 3, 2020
    @noisysocks noisysocks deleted the try/search-in-navigation branch July 3, 2020 02:08
    @github-actions github-actions bot added this to the Gutenberg 8.5 milestone Jul 3, 2020
    @noisysocks
    Copy link
    Member Author

    Thanks @adamziel! I'm happy with this as a foundation to build on.

    @noisysocks
    Copy link
    Member Author

    noisysocks commented Jul 7, 2020

    Also - this could be an addition to this PR or a follow-up exploration: Should the inserter in the navigator also offer the search block?

    Yes, good catch. Added to my list! 🙂

    @adamziel: This already works. The problem, looking at your screenshot, is that you can't add a Search block underneath a Link block. Unsure if we should allow that.

    @adamziel
    Copy link
    Contributor

    adamziel commented Jul 7, 2020

    This already works. The problem, looking at your screenshot, is that you can't add a Search block underneath a Link block. Unsure if we should allow that.

    Right - that's a really good point! I don't think it makes sense under a Link block specifically, BUT I think it could make sense in the dropdown menu in general - just like plenty of other blocks such as images, columns, and so on. I'll find or create a new issue for that.

    @adamziel
    Copy link
    Contributor

    adamziel commented Jul 7, 2020

    I just created this one: #23745

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    [Block] Navigation Affects the Navigation Block REST API Interaction Related to REST API
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    8 participants