diff --git a/.eslintrc.js b/.eslintrc.js index 9240b96c033b4..e997e7804beac 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -253,6 +253,24 @@ module.exports = { ], }, }, + { + files: [ + 'packages/*/src/**/*.[tj]s?(x)', + 'storybook/stories/**/*.[tj]s?(x)', + ], + excludedFiles: [ '**/*.native.js' ], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: + 'JSXOpeningElement[name.name="Button"]:not(:has(JSXAttribute[name.name="__experimentalIsFocusable"])) JSXAttribute[name.name="disabled"]', + message: + '`disabled` used without the `__experimentalIsFocusable` prop. Disabling a control without maintaining focusability can cause accessibility issues, by hiding their presence from screen reader users, or preventing focus from returning to a trigger element. (Ignore this error if you truly mean to disable.)', + }, + ], + }, + }, { files: [ // Components package. diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 862a8b2d8a06a..f687eb79732b5 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -1439,7 +1439,7 @@ wp.data.dispatch( 'core/block-editor' ).registerInserterMediaCategory( { per_page: 'page_size', search: 'q', }; - const url = new URL( 'https://api.openverse.engineering/v1/images/' ); + const url = new URL( 'https://api.openverse.org/v1/images/' ); Object.entries( finalQuery ).forEach( ( [ key, value ] ) => { const queryKey = mapFromInserterMediaRequest[ key ] || key; url.searchParams.set( queryKey, value ); diff --git a/lib/block-template-utils.php b/lib/block-template-utils.php new file mode 100644 index 0000000000000..a644047d3cfdc --- /dev/null +++ b/lib/block-template-utils.php @@ -0,0 +1,114 @@ +open( $filename, ZipArchive::CREATE | ZipArchive::OVERWRITE ) ) { + return new WP_Error( 'unable_to_create_zip', __( 'Unable to open export file (archive) for writing.', 'gutenberg' ) ); + } + + $zip->addEmptyDir( 'templates' ); + $zip->addEmptyDir( 'parts' ); + + // Get path of the theme. + $theme_path = wp_normalize_path( get_stylesheet_directory() ); + + // Create recursive directory iterator. + $theme_files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $theme_path ), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + // Make a copy of the current theme. + foreach ( $theme_files as $file ) { + // Skip directories as they are added automatically. + if ( ! $file->isDir() ) { + // Get real and relative path for current file. + $file_path = wp_normalize_path( $file ); + $relative_path = substr( $file_path, strlen( $theme_path ) + 1 ); + + if ( ! wp_is_theme_directory_ignored( $relative_path ) ) { + $zip->addFile( $file_path, $relative_path ); + } + } + } + + // Load templates into the zip file. + $templates = gutenberg_get_block_templates(); + foreach ( $templates as $template ) { + $template->content = traverse_and_serialize_blocks( + parse_blocks( $template->content ), + '_remove_theme_attribute_from_template_part_block' + ); + + $zip->addFromString( + 'templates/' . $template->slug . '.html', + $template->content + ); + } + + // Load template parts into the zip file. + $template_parts = gutenberg_get_block_templates( array(), 'wp_template_part' ); + foreach ( $template_parts as $template_part ) { + $zip->addFromString( + 'parts/' . $template_part->slug . '.html', + $template_part->content + ); + } + + // Load theme.json into the zip file. + $tree = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data( array(), array( 'with_supports' => false ) ); + // Merge with user data. + $tree->merge( WP_Theme_JSON_Resolver_Gutenberg::get_user_data() ); + + $theme_json_raw = $tree->get_data(); + // If a version is defined, add a schema. + if ( $theme_json_raw['version'] ) { + $theme_json_version = 'wp/' . substr( $wp_version, 0, 3 ); + $schema = array( '$schema' => 'https://schemas.wp.org/' . $theme_json_version . '/theme.json' ); + $theme_json_raw = array_merge( $schema, $theme_json_raw ); + } + + // Convert to a string. + $theme_json_encoded = wp_json_encode( $theme_json_raw, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); + + // Replace 4 spaces with a tab. + $theme_json_tabbed = preg_replace( '~(?:^|\G)\h{4}~m', "\t", $theme_json_encoded ); + + // Add the theme.json file to the zip. + $zip->addFromString( + 'theme.json', + $theme_json_tabbed + ); + + // Save changes to the zip file. + $zip->close(); + + return $filename; +} diff --git a/lib/class-wp-rest-edit-site-export-controller-gutenberg.php b/lib/class-wp-rest-edit-site-export-controller-gutenberg.php new file mode 100644 index 0000000000000..b05de230dd0cc --- /dev/null +++ b/lib/class-wp-rest-edit-site-export-controller-gutenberg.php @@ -0,0 +1,46 @@ +add_data( array( 'status' => 500 ) ); + + return $filename; + } + + $theme_name = basename( get_stylesheet() ); + header( 'Content-Type: application/zip' ); + header( 'Content-Disposition: attachment; filename=' . $theme_name . '.zip' ); + header( 'Content-Length: ' . filesize( $filename ) ); + flush(); + readfile( $filename ); + unlink( $filename ); + exit; + } +} diff --git a/lib/compat/wordpress-6.6/rest-api.php b/lib/compat/wordpress-6.6/rest-api.php index 54796685f45ab..2cf026cc817c1 100644 --- a/lib/compat/wordpress-6.6/rest-api.php +++ b/lib/compat/wordpress-6.6/rest-api.php @@ -87,3 +87,73 @@ function gutenberg_register_global_styles_revisions_endpoints() { } add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' ); + +if ( ! function_exists( 'gutenberg_register_wp_rest_themes_stylesheet_directory_uri_field' ) ) { + /** + * Adds `stylesheet_uri` fields to WP_REST_Themes_Controller class. + */ + function gutenberg_register_wp_rest_themes_stylesheet_directory_uri_field() { + register_rest_field( + 'theme', + 'stylesheet_uri', + array( + 'get_callback' => function ( $item ) { + if ( ! empty( $item['stylesheet'] ) ) { + $theme = wp_get_theme( $item['stylesheet'] ); + $current_theme = wp_get_theme(); + if ( $theme->get_stylesheet() === $current_theme->get_stylesheet() ) { + return get_stylesheet_directory_uri(); + } else { + return $theme->get_stylesheet_directory_uri(); + } + } + + return null; + }, + 'schema' => array( + 'type' => 'string', + 'description' => __( 'The uri for the theme\'s stylesheet directory.', 'gutenberg' ), + 'format' => 'uri', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + ) + ); + } +} +add_action( 'rest_api_init', 'gutenberg_register_wp_rest_themes_stylesheet_directory_uri_field' ); + +if ( ! function_exists( 'gutenberg_register_wp_rest_themes_template_directory_uri_field' ) ) { + /** + * Adds `template_uri` fields to WP_REST_Themes_Controller class. + */ + function gutenberg_register_wp_rest_themes_template_directory_uri_field() { + register_rest_field( + 'theme', + 'template_uri', + array( + 'get_callback' => function ( $item ) { + if ( ! empty( $item['stylesheet'] ) ) { + $theme = wp_get_theme( $item['stylesheet'] ); + $current_theme = wp_get_theme(); + if ( $theme->get_stylesheet() === $current_theme->get_stylesheet() ) { + return get_template_directory_uri(); + } else { + return $theme->get_template_directory_uri(); + } + } + + return null; + }, + 'schema' => array( + 'type' => 'string', + 'description' => __( 'The uri for the theme\'s template directory. If this is a child theme, this refers to the parent theme, otherwise this is the same as the theme\'s stylesheet directory.', 'gutenberg' ), + 'format' => 'uri', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + ) + ); + } +} +add_action( 'rest_api_init', 'gutenberg_register_wp_rest_themes_template_directory_uri_field' ); diff --git a/lib/load.php b/lib/load.php index 23985f9c8a92e..1f63c816f8173 100644 --- a/lib/load.php +++ b/lib/load.php @@ -53,6 +53,7 @@ function gutenberg_is_experiment_enabled( $name ) { // Plugin specific code. require_once __DIR__ . '/class-wp-rest-global-styles-controller-gutenberg.php'; + require_once __DIR__ . '/class-wp-rest-edit-site-export-controller-gutenberg.php'; require_once __DIR__ . '/rest-api.php'; // Experimental. @@ -206,6 +207,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/demo.php'; require __DIR__ . '/experiments-page.php'; require __DIR__ . '/interactivity-api.php'; +require __DIR__ . '/block-template-utils.php'; if ( gutenberg_is_experiment_enabled( 'gutenberg-full-page-client-side-navigation' ) ) { require __DIR__ . '/experimental/full-page-client-side-navigation.php'; } diff --git a/lib/rest-api.php b/lib/rest-api.php index 04f521d132c46..fedd75151584d 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -18,3 +18,15 @@ function gutenberg_register_global_styles_endpoints() { $global_styles_controller->register_routes(); } add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' ); + +if ( ! function_exists( 'gutenberg_register_edit_site_export_controller_endpoints' ) ) { + /** + * Registers the Edit Site Export REST API routes. + */ + function gutenberg_register_edit_site_export_controller_endpoints() { + $edit_site_export_controller = new WP_REST_Edit_Site_Export_Controller_Gutenberg(); + $edit_site_export_controller->register_routes(); + } +} + +add_action( 'rest_api_init', 'gutenberg_register_edit_site_export_controller_endpoints' ); diff --git a/packages/block-directory/src/plugins/get-install-missing/install-button.js b/packages/block-directory/src/plugins/get-install-missing/install-button.js index 2dc01184bdeb4..075fed360c14c 100644 --- a/packages/block-directory/src/plugins/get-install-missing/install-button.js +++ b/packages/block-directory/src/plugins/get-install-missing/install-button.js @@ -42,6 +42,7 @@ export default function InstallButton( { attributes, block, clientId } ) { } } ) } + __experimentalIsFocusable disabled={ isInstallingBlock } isBusy={ isInstallingBlock } variant="primary" diff --git a/packages/block-editor/src/components/button-block-appender/index.js b/packages/block-editor/src/components/button-block-appender/index.js index 974f48e61bc28..cd1289c897824 100644 --- a/packages/block-editor/src/components/button-block-appender/index.js +++ b/packages/block-editor/src/components/button-block-appender/index.js @@ -60,6 +60,8 @@ function ButtonBlockAppender( onClick={ onToggle } aria-haspopup={ isToggleButton ? 'true' : undefined } aria-expanded={ isToggleButton ? isOpen : undefined } + // Disable reason: There shouldn't be a case where this button is disabled but not visually hidden. + // eslint-disable-next-line no-restricted-syntax disabled={ disabled } label={ label } > diff --git a/packages/block-editor/src/components/link-control/link-preview.js b/packages/block-editor/src/components/link-control/link-preview.js index 867b69356eb9d..fb4b3658e2a4f 100644 --- a/packages/block-editor/src/components/link-control/link-preview.js +++ b/packages/block-editor/src/components/link-control/link-preview.js @@ -149,6 +149,7 @@ export default function LinkPreview( { isEmptyURL || showIconLabels ? '' : ': ' + value.url ) } ref={ ref } + __experimentalIsFocusable disabled={ isEmptyURL } size="compact" /> diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index db2c615dd5d6c..d44fa3e69f86a 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -2010,7 +2010,7 @@ export function __unstableSetTemporarilyEditingAsBlocks( * per_page: 'page_size', * search: 'q', * }; - * const url = new URL( 'https://api.openverse.engineering/v1/images/' ); + * const url = new URL( 'https://api.openverse.org/v1/images/' ); * Object.entries( finalQuery ).forEach( ( [ key, value ] ) => { * const queryKey = mapFromInserterMediaRequest[ key ] || key; * url.searchParams.set( queryKey, value ); diff --git a/packages/block-library/src/gallery/v1/gallery-image.js b/packages/block-library/src/gallery/v1/gallery-image.js index 368d5da55c4ac..5384944b2335d 100644 --- a/packages/block-library/src/gallery/v1/gallery-image.js +++ b/packages/block-library/src/gallery/v1/gallery-image.js @@ -222,6 +222,8 @@ class GalleryImage extends Component { onClick={ isFirstItem ? undefined : onMoveBackward } label={ __( 'Move image backward' ) } aria-disabled={ isFirstItem } + // Disable reason: Truly disable when image is not selected. + // eslint-disable-next-line no-restricted-syntax disabled={ ! isSelected } /> diff --git a/packages/dataviews/src/item-actions.tsx b/packages/dataviews/src/item-actions.tsx index 8a4fcf1b19f8d..90ae74b5f74ea 100644 --- a/packages/dataviews/src/item-actions.tsx +++ b/packages/dataviews/src/item-actions.tsx @@ -254,6 +254,7 @@ function CompactItemActions< Item extends AnyItem >( { size="compact" icon={ moreVertical } label={ __( 'Actions' ) } + __experimentalIsFocusable disabled={ ! actions.length } className="dataviews-all-actions-button" /> diff --git a/packages/dataviews/src/view-list.tsx b/packages/dataviews/src/view-list.tsx index 9468d6aa2d058..0721a9b5d8ffe 100644 --- a/packages/dataviews/src/view-list.tsx +++ b/packages/dataviews/src/view-list.tsx @@ -255,6 +255,7 @@ function ListItem< Item extends AnyItem >( { size="compact" icon={ moreVertical } label={ __( 'Actions' ) } + __experimentalIsFocusable disabled={ ! actions.length } onKeyDown={ ( event: { key: string; diff --git a/packages/edit-post/src/components/preferences-modal/enable-custom-fields.js b/packages/edit-post/src/components/preferences-modal/enable-custom-fields.js index e3ba4a1568420..e655a7300c37e 100644 --- a/packages/edit-post/src/components/preferences-modal/enable-custom-fields.js +++ b/packages/edit-post/src/components/preferences-modal/enable-custom-fields.js @@ -42,6 +42,7 @@ export function CustomFieldsConfirmation( { willEnable } ) { className="edit-post-preferences-modal__custom-fields-confirmation-button" variant="secondary" isBusy={ isReloading } + __experimentalIsFocusable disabled={ isReloading } onClick={ () => { setIsReloading( true ); diff --git a/packages/edit-site/src/components/block-editor/inserter-media-categories.js b/packages/edit-site/src/components/block-editor/inserter-media-categories.js index af591d1fa2468..7ebc771126122 100644 --- a/packages/edit-site/src/components/block-editor/inserter-media-categories.js +++ b/packages/edit-site/src/components/block-editor/inserter-media-categories.js @@ -191,9 +191,7 @@ const inserterMediaCategories = [ per_page: 'page_size', search: 'q', }; - const url = new URL( - 'https://api.openverse.engineering/v1/images/' - ); + const url = new URL( 'https://api.openverse.org/v1/images/' ); Object.entries( finalQuery ).forEach( ( [ key, value ] ) => { const queryKey = mapFromInserterMediaRequest[ key ] || key; url.searchParams.set( queryKey, value ); diff --git a/packages/edit-site/src/components/global-styles-sidebar/index.js b/packages/edit-site/src/components/global-styles-sidebar/index.js index 436762d6bcf94..f57cc8c417f41 100644 --- a/packages/edit-site/src/components/global-styles-sidebar/index.js +++ b/packages/edit-site/src/components/global-styles-sidebar/index.js @@ -152,6 +152,7 @@ export default function GlobalStylesSidebar() { isPressed={ isStyleBookOpened || isRevisionsStyleBookOpened } + __experimentalIsFocusable disabled={ shouldClearCanvasContainerView } onClick={ toggleStyleBook } size="compact" @@ -162,6 +163,7 @@ export default function GlobalStylesSidebar() { label={ __( 'Revisions' ) } icon={ backup } onClick={ toggleRevisions } + __experimentalIsFocusable disabled={ ! hasRevisions } isPressed={ isRevisionsOpened || isRevisionsStyleBookOpened diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js index 9c4280f2b1eb5..7f4f9896344b4 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js @@ -163,6 +163,7 @@ function RevisionsButtons( { >