diff --git a/.eslintrc.js b/.eslintrc.js index 9240b96c033b48..e997e7804beac4 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/block-api/block-styles.md b/docs/reference-guides/block-api/block-styles.md index 90b6c06d18f59d..b47b1a76a71f68 100644 --- a/docs/reference-guides/block-api/block-styles.md +++ b/docs/reference-guides/block-api/block-styles.md @@ -1,6 +1,6 @@ # Styles -Block Styles allow alternative styles to be applied to existing blocks. They work by adding a className to the block's wrapper. This className can be used to provide an alternative styling for the block if the block style is selected. See the [Getting Started with JavaScript tutorial](/docs/how-to-guides/javascript/) for a full example. +Block Styles allow alternative styles to be applied to existing blocks. They work by adding a className to the block's wrapper. This className can be used to provide an alternative styling for the block if the block style is selected. See the [Use styles and stylesheets](/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md) for a full example on how to apply styles to a block. _Example:_ diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 862a8b2d8a06aa..f687eb79732b5a 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/docs/reference-guides/slotfills/README.md b/docs/reference-guides/slotfills/README.md index 043a50cb5186e6..ea324f71b25e83 100644 --- a/docs/reference-guides/slotfills/README.md +++ b/docs/reference-guides/slotfills/README.md @@ -33,7 +33,7 @@ registerPlugin( 'post-status-info-test', { render: PluginPostStatusInfoTest } ); SlotFills are created using `createSlotFill`. This creates two components, `Slot` and `Fill` which are then used to create a new component that is exported on the `wp.plugins` global. -**Definition of the `PluginPostStatusInfo` SlotFill** ([see core code](https://github.com/WordPress/gutenberg/blob/HEAD/packages/edit-post/src/components/sidebar/plugin-post-status-info/index.js#L54)) +**Definition of the `PluginPostStatusInfo` SlotFill** ([see core code](https://github.com/WordPress/gutenberg/blob/HEAD/packages/editor/src/components/plugin-post-status-info/index.js#L55)) ```js /** @@ -61,34 +61,70 @@ export default PluginPostStatusInfo; This new Slot is then exposed in the editor. The example below is from core and represents the Summary panel. As we can see, the `` is wrapping all of the items that will appear in the panel. -Any items that have been added via the SlotFill ( see the example above ), will be included in the `fills` parameter and be displayed between the `` and `` components. +Any items that have been added via the SlotFill ( see the example above ), will be included in the `fills` parameter and be displayed in the end of the component. -See [core code](https://github.com/WordPress/gutenberg/tree/HEAD/packages/edit-post/src/components/sidebar/post-status/index.js#L26). +See [core code](https://github.com/WordPress/gutenberg/tree/HEAD/packages/editor/src/components/sidebar/post-summary.js#L39). ```js -const PostStatus = ( { isOpened, onTogglePanel } ) => ( - - - { ( fills ) => ( - <> - - - - - - - { fills } - - - ) } - - -); +export default function PostSummary( { onActionPerformed } ) { + const { isRemovedPostStatusPanel } = useSelect( ( select ) => { + // We use isEditorPanelRemoved to hide the panel if it was programatically removed. We do + // not use isEditorPanelEnabled since this panel should not be disabled through the UI. + const { isEditorPanelRemoved, getCurrentPostType } = + select( editorStore ); + return { + isRemovedPostStatusPanel: isEditorPanelRemoved( PANEL_NAME ), + postType: getCurrentPostType(), + }; + }, [] ); + + return ( + + + { ( fills ) => ( + <> + + + } + /> + + + + + + + { ! isRemovedPostStatusPanel && ( + + + + + + + + + + + + + + + + + + { fills } + + ) } + + + ) } + + + ); +} ``` ## Currently available SlotFills and examples diff --git a/lib/block-template-utils.php b/lib/block-template-utils.php new file mode 100644 index 00000000000000..a644047d3cfdc1 --- /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 00000000000000..b05de230dd0ccd --- /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 54796685f45ab8..2cf026cc817c15 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/experimental/script-modules.php b/lib/experimental/script-modules.php index a9d5540e56dc55..709ab322f63a3e 100644 --- a/lib/experimental/script-modules.php +++ b/lib/experimental/script-modules.php @@ -205,10 +205,10 @@ function gutenberg_dequeue_module( $module_identifier ) { * This embeds data in the page HTML so that it is available on page load. * * Data can be associated with a given Script Module by using the - * `scriptmoduledata_{$module_id}` filter. + * `script_module_data_{$module_id}` filter. * * The data for a given Script Module will be JSON serialized in a script tag with an ID - * like `wp-scriptmodule-data_{$module_id}`. + * like `wp-script-module-data-{$module_id}`. */ function gutenberg_print_script_module_data(): void { $get_marked_for_enqueue = new ReflectionMethod( 'WP_Script_Modules', 'get_marked_for_enqueue' ); @@ -236,14 +236,14 @@ function gutenberg_print_script_module_data(): void { * If the filter returns no data (an empty array), nothing will be embedded in the page. * * The data for a given Script Module, if provided, will be JSON serialized in a script tag - * with an ID like `wp-scriptmodule-data_{$module_id}`. + * with an ID like `wp-script-module-data-{$module_id}`. * * The dynamic portion of the hook name, `$module_id`, refers to the Script Module ID that * the data is associated with. * * @param array $data The data that should be associated with the array. */ - $data = apply_filters( "scriptmoduledata_{$module_id}", array() ); + $data = apply_filters( "script_module_data_{$module_id}", array() ); if ( is_array( $data ) && ! empty( $data ) ) { /* @@ -281,7 +281,7 @@ function gutenberg_print_script_module_data(): void { wp_json_encode( $data, $json_encode_flags ), array( 'type' => 'application/json', - 'id' => "wp-scriptmodule-data_{$module_id}", + 'id' => "wp-script-module-data-{$module_id}", ) ); } diff --git a/lib/load.php b/lib/load.php index 23985f9c8a92e9..1f63c816f8173d 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 04f521d132c461..fedd75151584d5 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 2dc01184bdeb4a..075fed360c14c8 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 974f48e61bc287..cd1289c897824c 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/inserter/hooks/use-block-types-state.js b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js index 6b9e694c1cdf8f..8db23267eee8f4 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js @@ -8,7 +8,7 @@ import { parse, } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; -import { useCallback } from '@wordpress/element'; +import { useCallback, useMemo } from '@wordpress/element'; /** * Internal dependencies @@ -25,13 +25,18 @@ import { withRootClientIdOptionKey } from '../../../store/utils'; * @return {Array} Returns the block types state. (block types, categories, collections, onSelect handler) */ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => { + const options = useMemo( + () => ( { [ withRootClientIdOptionKey ]: ! isQuick } ), + [ isQuick ] + ); const [ items ] = useSelect( ( select ) => [ - select( blockEditorStore ).getInserterItems( rootClientId, { - [ withRootClientIdOptionKey ]: ! isQuick, - } ), + select( blockEditorStore ).getInserterItems( + rootClientId, + options + ), ], - [ rootClientId, isQuick ] + [ rootClientId, options ] ); const [ categories, collections ] = useSelect( ( select ) => { 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 3a9ced64d9d614..44751351d30f29 100644 --- a/packages/block-editor/src/components/link-control/link-preview.js +++ b/packages/block-editor/src/components/link-control/link-preview.js @@ -166,6 +166,7 @@ export default function LinkPreview( { isEmptyURL || showIconLabels ? '' : ': ' + value.url ) } ref={ ref } + __experimentalIsFocusable disabled={ isEmptyURL } size="compact" /> diff --git a/packages/block-editor/src/components/link-control/search-input.js b/packages/block-editor/src/components/link-control/search-input.js index 238b2d6b3acc7c..78730463389e26 100644 --- a/packages/block-editor/src/components/link-control/search-input.js +++ b/packages/block-editor/src/components/link-control/search-input.js @@ -125,7 +125,7 @@ const LinkControlSearchInput = forwardRef( className={ className } value={ value } onChange={ onInputChange } - placeholder={ placeholder ?? __( 'Search or type url' ) } + placeholder={ placeholder ?? __( 'Search or type URL' ) } __experimentalRenderSuggestions={ showSuggestions ? handleRenderSuggestions : null } diff --git a/packages/block-editor/src/components/provider/use-block-sync.js b/packages/block-editor/src/components/provider/use-block-sync.js index 300c108a70cf1a..4e9cc9784554f5 100644 --- a/packages/block-editor/src/components/provider/use-block-sync.js +++ b/packages/block-editor/src/components/provider/use-block-sync.js @@ -9,7 +9,6 @@ import { cloneBlock } from '@wordpress/blocks'; * Internal dependencies */ import { store as blockEditorStore } from '../../store'; -import { undoIgnoreBlocks } from '../../store/undo-ignore'; const noop = () => {}; @@ -274,10 +273,6 @@ export default function useBlockSync( { const updateParent = isPersistent ? onChangeRef.current : onInputRef.current; - const undoIgnore = undoIgnoreBlocks.has( blocks ); - if ( undoIgnore ) { - undoIgnoreBlocks.delete( blocks ); - } updateParent( blocks, { selection: { selectionStart: getSelectionStart(), @@ -285,7 +280,6 @@ export default function useBlockSync( { initialPosition: getSelectedBlocksInitialCaretPosition(), }, - undoIgnore, } ); } previousAreBlocksDifferent = areBlocksDifferent; diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index db2c615dd5d6ca..d44fa3e69f86ad 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-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index 28a7b1da98f73f..5e4e0c7a222b0b 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -6,7 +6,6 @@ import { Platform } from '@wordpress/element'; /** * Internal dependencies */ -import { undoIgnoreBlocks } from './undo-ignore'; import { store as blockEditorStore } from './index'; import { unlock } from '../lock-unlock'; @@ -292,34 +291,6 @@ export function deleteStyleOverride( id ) { }; } -/** - * A higher-order action that mark every change inside a callback as "non-persistent" - * and ignore pushing to the undo history stack. It's primarily used for synchronized - * derived updates from the block editor without affecting the undo history. - * - * @param {() => void} callback The synchronous callback to derive updates. - */ -export function syncDerivedUpdates( callback ) { - return ( { dispatch, select, registry } ) => { - registry.batch( () => { - // Mark every change in the `callback` as non-persistent. - dispatch( { - type: 'SET_EXPLICIT_PERSISTENT', - isPersistentChange: false, - } ); - callback(); - dispatch( { - type: 'SET_EXPLICIT_PERSISTENT', - isPersistentChange: undefined, - } ); - - // Ignore pushing undo stack for the updated blocks. - const updatedBlocks = select.getBlocks(); - undoIgnoreBlocks.add( updatedBlocks ); - } ); - }; -} - /** * Action that sets the element that had focus when focus leaves the editor canvas. * diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index bf7b5125a770e6..bf98521dfe9b65 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -76,6 +76,8 @@ const EMPTY_ARRAY = []; */ const EMPTY_SET = new Set(); +const EMPTY_OBJECT = {}; + /** * Returns a block's name given its client ID, or null if no block exists with * the client ID. @@ -1996,7 +1998,7 @@ const buildBlockTypeItem = */ export const getInserterItems = createRegistrySelector( ( select ) => createSelector( - ( state, rootClientId = null, options = {} ) => { + ( state, rootClientId = null, options = EMPTY_OBJECT ) => { const buildReusableBlockInserterItem = ( reusableBlock ) => { const icon = ! reusableBlock.wp_pattern_sync_status ? { diff --git a/packages/block-editor/src/store/undo-ignore.js b/packages/block-editor/src/store/undo-ignore.js deleted file mode 100644 index f0a64428ea7c26..00000000000000 --- a/packages/block-editor/src/store/undo-ignore.js +++ /dev/null @@ -1,4 +0,0 @@ -// Keep track of the blocks that should not be pushing an additional -// undo stack when editing the entity. -// See the implementation of `syncDerivedUpdates` and `useBlockSync`. -export const undoIgnoreBlocks = new WeakSet(); diff --git a/packages/block-library/src/gallery/v1/gallery-image.js b/packages/block-library/src/gallery/v1/gallery-image.js index 368d5da55c4ac9..5384944b2335d9 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/blocks/src/api/raw-handling/image-corrector.native.js b/packages/blocks/src/api/raw-handling/image-corrector.native.js index c6a9288ede2d3b..550c2e0e6e1537 100644 --- a/packages/blocks/src/api/raw-handling/image-corrector.native.js +++ b/packages/blocks/src/api/raw-handling/image-corrector.native.js @@ -10,7 +10,10 @@ export default function imageCorrector( node ) { return; } - if ( node.src.indexOf( 'file:' ) === 0 ) { + // For local files makes sure the path doesn't end with an invalid extension. + // This scenario often happens with content from MS Word and similar text apps. + // We still need to support local files pasted from the users Media library. + if ( node.src.startsWith( 'file:' ) && node.src.slice( -1 ) === '/' ) { node.setAttribute( 'src', '' ); } diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 4ad4dd4ec63125..e8d65cfd9ce837 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- Add `text-wrap: balance` fallback to all instances of `text-wrap: pretty` for greater cross browser compatibility. ([#62233](https://github.com/WordPress/gutenberg/pull/62233)) + ## 28.0.0 (2024-05-31) ### Breaking Changes diff --git a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap index 73c8ebcfb49276..608cadbcf544f2 100644 --- a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap +++ b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap @@ -68,6 +68,7 @@ exports[`DimensionControl rendering renders with custom sizes 1`] = ` color: #1e1e1e; line-height: 1.4; margin: 0; + text-wrap: balance; text-wrap: pretty; font-size: calc((13 / 13) * 13px); font-weight: normal; @@ -347,6 +348,7 @@ exports[`DimensionControl rendering renders with defaults 1`] = ` color: #1e1e1e; line-height: 1.4; margin: 0; + text-wrap: balance; text-wrap: pretty; font-size: calc((13 / 13) * 13px); font-weight: normal; @@ -636,6 +638,7 @@ exports[`DimensionControl rendering renders with icon and custom icon label 1`] color: #1e1e1e; line-height: 1.4; margin: 0; + text-wrap: balance; text-wrap: pretty; font-size: calc((13 / 13) * 13px); font-weight: normal; @@ -937,6 +940,7 @@ exports[`DimensionControl rendering renders with icon and default icon label 1`] color: #1e1e1e; line-height: 1.4; margin: 0; + text-wrap: balance; text-wrap: pretty; font-size: calc((13 / 13) * 13px); font-weight: normal; diff --git a/packages/components/src/heading/test/__snapshots__/index.tsx.snap b/packages/components/src/heading/test/__snapshots__/index.tsx.snap index 9a779613f56c22..cf863c4b2bb2ef 100644 --- a/packages/components/src/heading/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/heading/test/__snapshots__/index.tsx.snap @@ -5,6 +5,7 @@ exports[`props should render correctly 1`] = ` color: #1e1e1e; line-height: 1.4; margin: 0; + text-wrap: balance; text-wrap: pretty; color: #1e1e1e; font-size: calc(1.95 * 13px); diff --git a/packages/components/src/text/styles.ts b/packages/components/src/text/styles.ts index 1a0a6383363df8..c7d48552795938 100644 --- a/packages/components/src/text/styles.ts +++ b/packages/components/src/text/styles.ts @@ -12,6 +12,7 @@ export const Text = css` color: ${ COLORS.gray[ 900 ] }; line-height: ${ CONFIG.fontLineHeightBase }; margin: 0; + text-wrap: balance; /* Fallback for Safari. */ text-wrap: pretty; `; diff --git a/packages/components/src/text/test/__snapshots__/index.tsx.snap b/packages/components/src/text/test/__snapshots__/index.tsx.snap index d2caecd9bf0593..1b98c0853ac549 100644 --- a/packages/components/src/text/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/text/test/__snapshots__/index.tsx.snap @@ -22,6 +22,7 @@ exports[`Text should render highlighted words with highlightCaseSensitive 1`] = color: #1e1e1e; line-height: 1.4; margin: 0; + text-wrap: balance; text-wrap: pretty; font-size: calc((13 / 13) * 13px); font-weight: normal; @@ -54,6 +55,7 @@ exports[`Text snapshot tests should render correctly 1`] = ` color: #1e1e1e; line-height: 1.4; margin: 0; + text-wrap: balance; text-wrap: pretty; font-size: calc((13 / 13) * 13px); font-weight: normal; diff --git a/packages/dataviews/src/item-actions.tsx b/packages/dataviews/src/item-actions.tsx index 8a4fcf1b19f8d8..90ae74b5f74ea6 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 9468d6aa2d0585..0721a9b5d8ffe7 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 e3ba4a15684200..e655a7300c37ee 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-post/src/test/__snapshots__/editor.native.js.snap b/packages/edit-post/src/test/__snapshots__/editor.native.js.snap index 76bb42d5a2ccea..4a88b249a1db66 100644 --- a/packages/edit-post/src/test/__snapshots__/editor.native.js.snap +++ b/packages/edit-post/src/test/__snapshots__/editor.native.js.snap @@ -1,5 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Editor adds empty image block when pasting unsupported HTML local image path 1`] = ` +" +
+" +`; + +exports[`Editor adds image block when pasting HTML local image path 1`] = ` +" +
+" +`; + exports[`Editor appends media correctly for allowed types 1`] = ` "
diff --git a/packages/edit-post/src/test/editor.native.js b/packages/edit-post/src/test/editor.native.js index acafc4d68d42a5..8fe116758608b5 100644 --- a/packages/edit-post/src/test/editor.native.js +++ b/packages/edit-post/src/test/editor.native.js @@ -9,8 +9,10 @@ import { getEditorHtml, getEditorTitle, initializeEditor, + pasteIntoRichText, screen, setupCoreBlocks, + within, } from 'test/helpers'; import { BackHandler } from 'react-native'; @@ -98,6 +100,38 @@ describe( 'Editor', () => { } ); } ); + it( 'adds empty image block when pasting unsupported HTML local image path', async () => { + await initializeEditor(); + await addBlock( screen, 'Paragraph' ); + + const paragraphBlock = getBlock( screen, 'Paragraph' ); + fireEvent.press( paragraphBlock ); + const paragraphTextInput = + within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); + + pasteIntoRichText( paragraphTextInput, { + text: '
', + } ); + + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + + it( 'adds image block when pasting HTML local image path', async () => { + await initializeEditor(); + await addBlock( screen, 'Paragraph' ); + + const paragraphBlock = getBlock( screen, 'Paragraph' ); + fireEvent.press( paragraphBlock ); + const paragraphTextInput = + within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); + + pasteIntoRichText( paragraphTextInput, { + files: [ 'file:///path/to/file.png' ], + } ); + + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + it( 'appends media correctly for allowed types', async () => { // Arrange requestMediaImport 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 af591d1fa24688..7ebc7711261223 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 436762d6bcf94f..f57cc8c417f410 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 9c4280f2b1eb51..7f4f9896344b4f 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( { >