diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 38ad3e2e11bd1..4cb85864e5719 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -35,6 +35,15 @@ Add a user’s avatar. ([Source](https://github.com/WordPress/gutenberg/tree/tru - **Supports:** align, color (~~background~~, ~~text~~), interactivity (clientNavigation), spacing (margin, padding), ~~alignWide~~, ~~html~~ - **Attributes:** isLink, linkTarget, size, userId +## Back to top + +A link that takes you back to the top. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/back-to-top)) + +- **Name:** core/back-to-top +- **Category:** design +- **Supports:** ~~html~~ +- **Attributes:** text + ## Pattern Reuse this design across your site. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/block)) diff --git a/lib/blocks.php b/lib/blocks.php index e1d4622a0f23d..a6e97f8aa3e92 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -15,6 +15,7 @@ function gutenberg_reregister_core_block_types() { __DIR__ . '/../build/block-library/blocks/' => array( 'block_folders' => array( 'audio', + 'back-to-top', 'button', 'buttons', 'freeform', @@ -49,6 +50,7 @@ function gutenberg_reregister_core_block_types() { 'block_names' => array( 'archives.php' => 'core/archives', 'avatar.php' => 'core/avatar', + 'back-to-top.php' => 'core/back-to-top', 'block.php' => 'core/block', 'calendar.php' => 'core/calendar', 'categories.php' => 'core/categories', diff --git a/packages/block-library/src/back-to-top/block.json b/packages/block-library/src/back-to-top/block.json new file mode 100644 index 0000000000000..28ce04434e997 --- /dev/null +++ b/packages/block-library/src/back-to-top/block.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "core/back-to-top", + "title": "Back to top", + "category": "design", + "description": "A link that takes you back to the top.", + "keywords": [ "top", "skip link" ], + "textdomain": "default", + "attributes": { + "text": { + "type": "string" + } + }, + "supports": { + "html": false + } +} diff --git a/packages/block-library/src/back-to-top/edit.js b/packages/block-library/src/back-to-top/edit.js new file mode 100644 index 0000000000000..e1b1be66cdf87 --- /dev/null +++ b/packages/block-library/src/back-to-top/edit.js @@ -0,0 +1,23 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useBlockProps, RichText } from '@wordpress/block-editor'; + +export default function BackToTopEdit( { attributes, setAttributes } ) { + const { text } = attributes; + return ( +

+ + setAttributes( { text: newLinkText } ) + } + /> +

+ ); +} diff --git a/packages/block-library/src/back-to-top/index.js b/packages/block-library/src/back-to-top/index.js new file mode 100644 index 0000000000000..05e2c05bffe9b --- /dev/null +++ b/packages/block-library/src/back-to-top/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { arrowUp as icon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import initBlock from '../utils/init-block'; +import edit from './edit'; +import metadata from './block.json'; + +const { name } = metadata; +export { metadata, name }; +export const settings = { + icon, + edit, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/back-to-top/index.php b/packages/block-library/src/back-to-top/index.php new file mode 100644 index 0000000000000..39be602f4dbc2 --- /dev/null +++ b/packages/block-library/src/back-to-top/index.php @@ -0,0 +1,89 @@ +%2$s

', + $wrapper_attributes, + wp_kses_post( $link_text ) + ); +} + +/** + * Registers the `core/back_to_top` block on the server. + */ +function register_block_core_back_to_top() { + register_block_type_from_metadata( + __DIR__ . '/back-to-top', + array( + 'render_callback' => 'render_block_core_back_to_top', + ) + ); +} +add_action( 'init', 'register_block_core_back_to_top' ); + +/** + * Adds the target id 'wp-back-to-top' to the top of the page, so that focus can be moved. + * Block themes: Add the target id if the back to top block exists on the page. + * Classic themes with 'wp_body_open()': Always add the target id. + */ +function block_core_back_to_top_target() { + echo '
'; +} +if ( wp_is_block_theme() ) { + add_filter( + 'render_block', + function ( $html, $block ) { + if ( 'core/back-to-top' === $block['blockName'] ) { + add_action( 'wp_body_open', 'block_core_back_to_top_target' ); + } + return $html; + }, + 10, + 2 + ); +} else { + add_action( 'wp_body_open', 'block_core_back_to_top_target' ); +} + +/** + * For classic themes that do not use `wp_body_open()`, + * view.js is needed to move focus to the first focusable element on the top of the page. + */ +function block_core_back_to_top_classic_fallback() { + if ( ! wp_is_block_theme() && 0 === did_action( 'wp_body_open' ) ) { + // If the Gutenberg plugin is active, use the script from the plugin. + if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { + wp_enqueue_script( + 'wp-block-library-back-to-top-fallback', + plugins_url( 'back-to-top/view.min.js', __FILE__ ), + array(), + filemtime( plugin_dir_path( __FILE__ ) . 'back-to-top/view.min.js' ), + true + ); + } else { + wp_enqueue_script( + 'wp-block-library-back-to-top-fallback', + includes_url( 'blocks/back-to-top/view.min.js', __DIR__ ), + array(), + '', + true + ); + } + } +} diff --git a/packages/block-library/src/back-to-top/init.js b/packages/block-library/src/back-to-top/init.js new file mode 100644 index 0000000000000..79f0492c2cb2f --- /dev/null +++ b/packages/block-library/src/back-to-top/init.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import { init } from './'; + +export default init(); diff --git a/packages/block-library/src/back-to-top/view.js b/packages/block-library/src/back-to-top/view.js new file mode 100644 index 0000000000000..ae0b058b1048c --- /dev/null +++ b/packages/block-library/src/back-to-top/view.js @@ -0,0 +1,40 @@ +/** + * This script is needed to move the focus to the first focusable element on the page. + * It is only intended to be loaded on the front of classic themes that does not include wp_body_open(). + */ +window.addEventListener( 'load', () => { + const backToTopBlocks = document.querySelectorAll( + '.wp-block-back-to-top' + ); + function moveFocusToTop() { + const topAnchor = document.getElementById( '#wp-back-to-top' ); + if ( topAnchor ) { + topAnchor.querySelector( 'a' ).focus(); + return; + } + // This list is not exhaustive, but covers common elements that can recieve focus. + const focusable = [ + 'a[href]', + 'audio', + 'button:not([disabled])', + '[contenteditable]', + 'details', + 'embed', + 'iframe', + 'input:not([disabled]):not([type="hidden"])', + 'object', + '[role="button"][tabindex="0"]', + '[role="link"][tabindex="0"]', + 'select:not([disabled])', + 'summary', + 'textarea:not([disabled])', + 'video', + '[tabindex]:not([tabindex="-1"])', + ]; + document.querySelector( focusable.join( ', ' ) ).focus(); + } + + backToTopBlocks.forEach( ( backToTop ) => { + backToTop.addEventListener( 'click', moveFocusToTop ); + } ); +} ); diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index e2e0fd9e414ef..5be66033014d9 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -23,6 +23,7 @@ import { import * as archives from './archives'; import * as avatar from './avatar'; import * as audio from './audio'; +import * as backToTop from './back-to-top'; import * as button from './button'; import * as buttons from './buttons'; import * as calendar from './calendar'; @@ -142,6 +143,7 @@ const getAllBlocks = () => { // Register all remaining core blocks. archives, audio, + backToTop, button, buttons, calendar, diff --git a/test/integration/fixtures/blocks/core__back-to-top.html b/test/integration/fixtures/blocks/core__back-to-top.html new file mode 100644 index 0000000000000..efbe819d59be4 --- /dev/null +++ b/test/integration/fixtures/blocks/core__back-to-top.html @@ -0,0 +1 @@ + diff --git a/test/integration/fixtures/blocks/core__back-to-top.json b/test/integration/fixtures/blocks/core__back-to-top.json new file mode 100644 index 0000000000000..8f8325f34e6d5 --- /dev/null +++ b/test/integration/fixtures/blocks/core__back-to-top.json @@ -0,0 +1,8 @@ +[ + { + "name": "core/back-to-top", + "isValid": true, + "attributes": {}, + "innerBlocks": [] + } +] diff --git a/test/integration/fixtures/blocks/core__back-to-top.parsed.json b/test/integration/fixtures/blocks/core__back-to-top.parsed.json new file mode 100644 index 0000000000000..dbc8929d2bc31 --- /dev/null +++ b/test/integration/fixtures/blocks/core__back-to-top.parsed.json @@ -0,0 +1,9 @@ +[ + { + "blockName": "core/back-to-top", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + } +] diff --git a/test/integration/fixtures/blocks/core__back-to-top.serialized.html b/test/integration/fixtures/blocks/core__back-to-top.serialized.html new file mode 100644 index 0000000000000..efbe819d59be4 --- /dev/null +++ b/test/integration/fixtures/blocks/core__back-to-top.serialized.html @@ -0,0 +1 @@ +