diff --git a/changelog.txt b/changelog.txt
index 0b70b68c9377e..58b1b833d2c71 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,22 @@
== Changelog ==
+= 17.2.4 =
+
+## Changelog
+
+### Bug Fixes
+
+- Site editor: fix image upload bug ([57040](https://github.com/WordPress/gutenberg/pull/57040))
+
+## Contributors
+
+The following contributors merged PRs in this release:
+
+@glendaviesnz
+
+
+
+
= 17.3.0 =
diff --git a/docs/assets/text-decoration-component.png b/docs/assets/text-decoration-component.png
new file mode 100644
index 0000000000000..4626f953cf9c1
Binary files /dev/null and b/docs/assets/text-decoration-component.png differ
diff --git a/packages/block-editor/src/components/offline-status/index.native.js b/packages/block-editor/src/components/offline-status/index.native.js
index ae6007e75103c..0447791e69a7e 100644
--- a/packages/block-editor/src/components/offline-status/index.native.js
+++ b/packages/block-editor/src/components/offline-status/index.native.js
@@ -6,11 +6,13 @@ import { Text, View } from 'react-native';
/**
* WordPress dependencies
*/
-import { usePreferredColorSchemeStyle } from '@wordpress/compose';
+import {
+ usePreferredColorSchemeStyle,
+ useNetworkConnectivity,
+} from '@wordpress/compose';
import { Icon } from '@wordpress/components';
import { offline as offlineIcon } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
-import { useIsConnected } from '@wordpress/react-native-bridge';
/**
* Internal dependencies
@@ -18,7 +20,7 @@ import { useIsConnected } from '@wordpress/react-native-bridge';
import styles from './style.native.scss';
const OfflineStatus = () => {
- const { isConnected } = useIsConnected();
+ const { isConnected } = useNetworkConnectivity();
const containerStyle = usePreferredColorSchemeStyle(
styles.offline,
diff --git a/packages/block-editor/src/components/text-decoration-control/README.md b/packages/block-editor/src/components/text-decoration-control/README.md
new file mode 100644
index 0000000000000..a606140baa330
--- /dev/null
+++ b/packages/block-editor/src/components/text-decoration-control/README.md
@@ -0,0 +1,40 @@
+# TextDecorationControl
+
+
+This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
+
+
+
+![TextDecorationControl Element in Inspector Control](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/text-decoration-component.png?raw=true)
+
+
+## Usage
+
+```jsx
+import { __experimentalTextDecorationControl as TextDecorationControl } from '@wordpress/block-editor';
+```
+
+Then, you can use the component in your block editor UI:
+
+```jsx
+ setAttributes({ textDecoration: newValue })}
+/>
+```
+
+### Props
+
+### `value`
+
+- **Type:** `String`
+- **Default:** `none`
+- **Options:** `none`, `underline`, `line-through`
+
+The current value of the Text Decoration setting. You may only choose from the `Options` listed above.
+
+### `onChange`
+
+- **Type:** `Function`
+
+A callback function invoked when the Text Decoration value is changed via an interaction with any of the buttons. Called with the Text Decoration value (`none`, `underline`, `line-through`) as the only argument.
\ No newline at end of file
diff --git a/packages/block-library/src/audio/edit.js b/packages/block-library/src/audio/edit.js
index 13fe6aa7ba53a..773000ad7c152 100644
--- a/packages/block-library/src/audio/edit.js
+++ b/packages/block-library/src/audio/edit.js
@@ -47,9 +47,19 @@ function AudioEdit( {
} ) {
const { id, autoplay, loop, preload, src } = attributes;
const isTemporaryAudio = ! id && isBlobURL( src );
- const mediaUpload = useSelect( ( select ) => {
- const { getSettings } = select( blockEditorStore );
- return getSettings().mediaUpload;
+ const { mediaUpload, multiAudioSelection } = useSelect( ( select ) => {
+ const { getSettings, getMultiSelectedBlockClientIds, getBlockName } =
+ select( blockEditorStore );
+ const multiSelectedClientIds = getMultiSelectedBlockClientIds();
+
+ return {
+ mediaUpload: getSettings().mediaUpload,
+ multiAudioSelection:
+ multiSelectedClientIds.length &&
+ multiSelectedClientIds.every(
+ ( _clientId ) => getBlockName( _clientId ) === 'core/audio'
+ ),
+ };
}, [] );
useEffect( () => {
@@ -146,17 +156,19 @@ function AudioEdit( {
return (
<>
-
-
-
+ { ! multiAudioSelection && (
+
+
+
+ ) }
>
diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js
index 03fc15d19eedc..371a13b1bf5ad 100644
--- a/packages/block-library/src/gallery/edit.js
+++ b/packages/block-library/src/gallery/edit.js
@@ -104,23 +104,38 @@ function GalleryEdit( props ) {
getSettings,
preferredStyle,
innerBlockImages,
- wasBlockJustInserted,
+ blockWasJustInserted,
+ multiGallerySelection,
} = useSelect(
( select ) => {
- const settings = select( blockEditorStore ).getSettings();
+ const {
+ getBlockName,
+ getMultiSelectedBlockClientIds,
+ getSettings: _getSettings,
+ getBlock: _getBlock,
+ wasBlockJustInserted,
+ } = select( blockEditorStore );
const preferredStyleVariations =
- settings.__experimentalPreferredStyleVariations;
+ _getSettings().__experimentalPreferredStyleVariations;
+ const multiSelectedClientIds = getMultiSelectedBlockClientIds();
+
return {
- getBlock: select( blockEditorStore ).getBlock,
- getSettings: select( blockEditorStore ).getSettings,
+ getBlock: _getBlock,
+ getSettings: _getSettings,
preferredStyle:
preferredStyleVariations?.value?.[ 'core/image' ],
innerBlockImages:
- select( blockEditorStore ).getBlock( clientId )
- ?.innerBlocks ?? EMPTY_ARRAY,
- wasBlockJustInserted: select(
- blockEditorStore
- ).wasBlockJustInserted( clientId, 'inserter_menu' ),
+ _getBlock( clientId )?.innerBlocks ?? EMPTY_ARRAY,
+ blockWasJustInserted: wasBlockJustInserted(
+ clientId,
+ 'inserter_menu'
+ ),
+ multiGallerySelection:
+ multiSelectedClientIds.length &&
+ multiSelectedClientIds.every(
+ ( _clientId ) =>
+ getBlockName( _clientId ) === 'core/gallery'
+ ),
};
},
[ clientId ]
@@ -461,7 +476,7 @@ function GalleryEdit( props ) {
( hasImages && ! isSelected ) || imagesUploading,
value: hasImageIds ? images : {},
autoOpenMediaUpload:
- ! hasImages && isSelected && wasBlockJustInserted,
+ ! hasImages && isSelected && blockWasJustInserted,
onFocus,
},
} );
@@ -583,20 +598,22 @@ function GalleryEdit( props ) {
{ Platform.isWeb && (
<>
-
- image.id )
- .map( ( image ) => image.id ) }
- addToGallery={ hasImageIds }
- />
-
+ { ! multiGallerySelection && (
+
+ image.id )
+ .map( ( image ) => image.id ) }
+ addToGallery={ hasImageIds }
+ />
+
+ ) }
>
);
diff --git a/packages/block-library/src/gallery/gallery.js b/packages/block-library/src/gallery/gallery.js
index e898ae2e9fdcb..10c05eb8cc401 100644
--- a/packages/block-library/src/gallery/gallery.js
+++ b/packages/block-library/src/gallery/gallery.js
@@ -24,6 +24,7 @@ export default function Gallery( props ) {
blockProps,
__unstableLayoutClassNames: layoutClassNames,
isContentLocked,
+ multiGallerySelection,
} = props;
const { align, columns, imageCrop } = attributes;
@@ -54,7 +55,9 @@ export default function Gallery( props ) {
setAttributes={ setAttributes }
isSelected={ isSelected }
insertBlocksAfter={ insertBlocksAfter }
- showToolbarButton={ ! isContentLocked }
+ showToolbarButton={
+ ! multiGallerySelection && ! isContentLocked
+ }
className="blocks-gallery-caption"
label={ __( 'Gallery caption text' ) }
placeholder={ __( 'Add gallery caption' ) }
diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js
index e76a99e424a51..ea12457c2585d 100644
--- a/packages/block-library/src/image/image.js
+++ b/packages/block-library/src/image/image.js
@@ -134,17 +134,49 @@ export default function Image( {
const { allowResize = true } = context;
const { getBlock } = useSelect( blockEditorStore );
- const { image, multiImageSelection } = useSelect(
+ const { image } = useSelect(
( select ) => {
const { getMedia } = select( coreStore );
- const { getMultiSelectedBlockClientIds, getBlockName } =
- select( blockEditorStore );
- const multiSelectedClientIds = getMultiSelectedBlockClientIds();
return {
image:
id && isSelected
? getMedia( id, { context: 'view' } )
: null,
+ };
+ },
+ [ id, isSelected ]
+ );
+
+ const {
+ canInsertCover,
+ imageEditing,
+ imageSizes,
+ maxWidth,
+ mediaUpload,
+ multiImageSelection,
+ } = useSelect(
+ ( select ) => {
+ const {
+ getBlockRootClientId,
+ getMultiSelectedBlockClientIds,
+ getBlockName,
+ getSettings,
+ canInsertBlockType,
+ } = select( blockEditorStore );
+
+ const rootClientId = getBlockRootClientId( clientId );
+ const settings = getSettings();
+ const multiSelectedClientIds = getMultiSelectedBlockClientIds();
+
+ return {
+ imageEditing: settings.imageEditing,
+ imageSizes: settings.imageSizes,
+ maxWidth: settings.maxWidth,
+ mediaUpload: settings.mediaUpload,
+ canInsertCover: canInsertBlockType(
+ 'core/cover',
+ rootClientId
+ ),
multiImageSelection:
multiSelectedClientIds.length &&
multiSelectedClientIds.every(
@@ -153,33 +185,8 @@ export default function Image( {
),
};
},
- [ id, isSelected ]
+ [ clientId ]
);
- const { canInsertCover, imageEditing, imageSizes, maxWidth, mediaUpload } =
- useSelect(
- ( select ) => {
- const {
- getBlockRootClientId,
- getSettings,
- canInsertBlockType,
- } = select( blockEditorStore );
-
- const rootClientId = getBlockRootClientId( clientId );
- const settings = getSettings();
-
- return {
- imageEditing: settings.imageEditing,
- imageSizes: settings.imageSizes,
- maxWidth: settings.maxWidth,
- mediaUpload: settings.mediaUpload,
- canInsertCover: canInsertBlockType(
- 'core/cover',
- rootClientId
- ),
- };
- },
- [ clientId ]
- );
const { replaceBlocks, toggleSelection } = useDispatch( blockEditorStore );
const { createErrorNotice, createSuccessNotice } =
@@ -755,7 +762,9 @@ export default function Image( {
isSelected={ isSelected }
insertBlocksAfter={ insertBlocksAfter }
label={ __( 'Image caption text' ) }
- showToolbarButton={ hasNonContentControls }
+ showToolbarButton={
+ ! multiImageSelection && hasNonContentControls
+ }
/>
>
);
diff --git a/packages/block-library/src/social-link/index.php b/packages/block-library/src/social-link/index.php
index b203a662822f5..fe256879fa4ff 100644
--- a/packages/block-library/src/social-link/index.php
+++ b/packages/block-library/src/social-link/index.php
@@ -33,7 +33,7 @@ function render_block_core_social_link( $attributes, $content, $block ) {
* The `is_email` returns false for emails with schema.
*/
if ( is_email( $url ) ) {
- $url = 'mailto:' . $url;
+ $url = 'mailto:' . antispambot( $url );
}
/**
diff --git a/packages/block-library/src/video/edit.js b/packages/block-library/src/video/edit.js
index 88b2669e66f56..db1fbb197126a 100644
--- a/packages/block-library/src/video/edit.js
+++ b/packages/block-library/src/video/edit.js
@@ -75,10 +75,20 @@ function VideoEdit( {
const posterImageButton = useRef();
const { id, controls, poster, src, tracks } = attributes;
const isTemporaryVideo = ! id && isBlobURL( src );
- const mediaUpload = useSelect(
- ( select ) => select( blockEditorStore ).getSettings().mediaUpload,
- []
- );
+ const { mediaUpload, multiVideoSelection } = useSelect( ( select ) => {
+ const { getSettings, getMultiSelectedBlockClientIds, getBlockName } =
+ select( blockEditorStore );
+ const multiSelectedClientIds = getMultiSelectedBlockClientIds();
+
+ return {
+ mediaUpload: getSettings().mediaUpload,
+ multiVideoSelection:
+ multiSelectedClientIds.length &&
+ multiSelectedClientIds.every(
+ ( _clientId ) => getBlockName( _clientId ) === 'core/video'
+ ),
+ };
+ }, [] );
useEffect( () => {
if ( ! id && isBlobURL( src ) ) {
@@ -185,25 +195,29 @@ function VideoEdit( {
return (
<>
-
- {
- setAttributes( { tracks: newTracks } );
- } }
- />
-
-
-
-
+ { ! multiVideoSelection && (
+ <>
+
+ {
+ setAttributes( { tracks: newTracks } );
+ } }
+ />
+
+
+
+
+ >
+ ) }
>
diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index acf5a57bfe6d6..026ad72de90ea 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -15,6 +15,7 @@
- `Truncate`: improve handling of non-string `children` ([#57261](https://github.com/WordPress/gutenberg/pull/57261)).
- `PaletteEdit`: Don't discard colors with default name and slug ([#54332](https://github.com/WordPress/gutenberg/pull/54332)).
- `RadioControl`: Fully encapsulate styles ([#57347](https://github.com/WordPress/gutenberg/pull/57347)).
+- `GradientPicker`: Use slug while iterating over gradient entries to avoid React "duplicated key" warning ([#57361](https://github.com/WordPress/gutenberg/pull/57361)).
### Enhancements
diff --git a/packages/components/src/gradient-picker/index.tsx b/packages/components/src/gradient-picker/index.tsx
index b435ded2bcd0d..52e7e716642da 100644
--- a/packages/components/src/gradient-picker/index.tsx
+++ b/packages/components/src/gradient-picker/index.tsx
@@ -44,9 +44,9 @@ function SingleOrigin( {
...additionalProps
}: PickerProps< GradientObject > ) {
const gradientOptions = useMemo( () => {
- return gradients.map( ( { gradient, name }, index ) => (
+ return gradients.map( ( { gradient, name, slug }, index ) => (
{
- return ( props ) => {
- const { isConnected } = useIsConnected();
- return ;
- };
-}, 'withIsConnected' );
-
-export default withIsConnected;
diff --git a/packages/compose/src/higher-order/with-network-connectivity/README.md b/packages/compose/src/higher-order/with-network-connectivity/README.md
new file mode 100644
index 0000000000000..7ae64c0949973
--- /dev/null
+++ b/packages/compose/src/higher-order/with-network-connectivity/README.md
@@ -0,0 +1,20 @@
+# withNetworkConnectivity
+
+`withNetworkConnectivity` provides a true/false mobile connectivity status based on the `useNetworkConnectivity` hook.
+
+## Usage
+
+```jsx
+/**
+ * WordPress dependencies
+ */
+import { withNetworkConnectivity } from '@wordpress/compose';
+
+export class MyComponent extends Component {
+ if ( this.props.isConnected !== true ) {
+ console.log( 'You are currently offline.' )
+ }
+}
+
+export default withNetworkConnectivity( MyComponent )
+```
diff --git a/packages/compose/src/higher-order/with-network-connectivity/index.native.js b/packages/compose/src/higher-order/with-network-connectivity/index.native.js
new file mode 100644
index 0000000000000..1a416966d45be
--- /dev/null
+++ b/packages/compose/src/higher-order/with-network-connectivity/index.native.js
@@ -0,0 +1,19 @@
+/**
+ * Internal dependencies
+ */
+import { createHigherOrderComponent } from '../../utils/create-higher-order-component';
+import useNetworkConnectivity from '../../hooks/use-network-connectivity';
+
+const withNetworkConnectivity = createHigherOrderComponent(
+ ( WrappedComponent ) => {
+ return ( props ) => {
+ const { isConnected } = useNetworkConnectivity();
+ return (
+
+ );
+ };
+ },
+ 'withNetworkConnectivity'
+);
+
+export default withNetworkConnectivity;
diff --git a/packages/compose/src/hooks/use-network-connectivity/index.native.js b/packages/compose/src/hooks/use-network-connectivity/index.native.js
new file mode 100644
index 0000000000000..1a806cc99a5a7
--- /dev/null
+++ b/packages/compose/src/hooks/use-network-connectivity/index.native.js
@@ -0,0 +1,59 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useState } from '@wordpress/element';
+import {
+ requestConnectionStatus,
+ subscribeConnectionStatus,
+} from '@wordpress/react-native-bridge';
+
+/**
+ * @typedef {Object} NetworkInformation
+ *
+ * @property {boolean} [isConnected] Whether the device is connected to a network.
+ */
+
+/**
+ * Returns the current network connectivity status provided by the native bridge.
+ *
+ * @example
+ *
+ * ```jsx
+ * const { isConnected } = useNetworkConnectivity();
+ * ```
+ *
+ * @return {NetworkInformation} Network information.
+ */
+export default function useNetworkConnectivity() {
+ const [ isConnected, setIsConnected ] = useState( true );
+
+ useEffect( () => {
+ let isCurrent = true;
+
+ requestConnectionStatus( ( isBridgeConnected ) => {
+ if ( ! isCurrent ) {
+ return;
+ }
+
+ setIsConnected( isBridgeConnected );
+ } );
+
+ return () => {
+ isCurrent = false;
+ };
+ }, [] );
+
+ useEffect( () => {
+ const subscription = subscribeConnectionStatus(
+ ( { isConnected: isBridgeConnected } ) => {
+ setIsConnected( isBridgeConnected );
+ }
+ );
+
+ return () => {
+ subscription.remove();
+ };
+ }, [] );
+
+ return { isConnected };
+}
diff --git a/packages/compose/src/hooks/use-network-connectivity/test/index.native.js b/packages/compose/src/hooks/use-network-connectivity/test/index.native.js
new file mode 100644
index 0000000000000..82dce664b8b9c
--- /dev/null
+++ b/packages/compose/src/hooks/use-network-connectivity/test/index.native.js
@@ -0,0 +1,87 @@
+/**
+ * External dependencies
+ */
+import { act, renderHook } from 'test/helpers';
+
+/**
+ * WordPress dependencies
+ */
+import {
+ requestConnectionStatus,
+ subscribeConnectionStatus,
+} from '@wordpress/react-native-bridge';
+
+/**
+ * Internal dependencies
+ */
+import useNetworkConnectivity from '../index';
+
+describe( 'useNetworkConnectivity', () => {
+ it( 'should optimisitically presume network connectivity', () => {
+ const { result } = renderHook( () => useNetworkConnectivity() );
+
+ expect( result.current.isConnected ).toBe( true );
+ } );
+
+ describe( 'when network connectivity is available', () => {
+ beforeAll( () => {
+ requestConnectionStatus.mockImplementation( ( callback ) => {
+ callback( true );
+ return { remove: jest.fn() };
+ } );
+ } );
+
+ it( 'should return true', () => {
+ const { result } = renderHook( () => useNetworkConnectivity() );
+
+ expect( result.current.isConnected ).toBe( true );
+ } );
+
+ it( 'should update the status when network connectivity changes', () => {
+ let subscriptionCallback;
+ subscribeConnectionStatus.mockImplementation( ( callback ) => {
+ subscriptionCallback = callback;
+ return { remove: jest.fn() };
+ } );
+
+ const { result } = renderHook( () => useNetworkConnectivity() );
+
+ expect( result.current.isConnected ).toBe( true );
+
+ act( () => subscriptionCallback( { isConnected: false } ) );
+
+ expect( result.current.isConnected ).toBe( false );
+ } );
+ } );
+
+ describe( 'when network connectivity is unavailable', () => {
+ beforeAll( () => {
+ requestConnectionStatus.mockImplementation( ( callback ) => {
+ callback( false );
+ return { remove: jest.fn() };
+ } );
+ } );
+
+ it( 'should return false', () => {
+ const { result } = renderHook( () => useNetworkConnectivity() );
+
+ expect( result.current.isConnected ).toBe( false );
+ } );
+
+ it( 'should update the status when network connectivity changes', () => {
+ let subscriptionCallback;
+ subscribeConnectionStatus.mockImplementation( ( callback ) => {
+ subscriptionCallback = callback;
+ return { remove: jest.fn() };
+ } );
+
+ const { result } = renderHook( () => useNetworkConnectivity() );
+
+ expect( result.current.isConnected ).toBe( false );
+
+ act( () => subscriptionCallback( { isConnected: true } ) );
+
+ expect( result.current.isConnected ).toBe( true );
+ } );
+ } );
+} );
diff --git a/packages/compose/src/index.native.js b/packages/compose/src/index.native.js
index 00e0a66a36034..8d0953b81a14e 100644
--- a/packages/compose/src/index.native.js
+++ b/packages/compose/src/index.native.js
@@ -17,7 +17,7 @@ export { default as withInstanceId } from './higher-order/with-instance-id';
export { default as withSafeTimeout } from './higher-order/with-safe-timeout';
export { default as withState } from './higher-order/with-state';
export { default as withPreferredColorScheme } from './higher-order/with-preferred-color-scheme';
-export { default as withIsConnected } from './higher-order/with-is-connected';
+export { default as withNetworkConnectivity } from './higher-order/with-network-connectivity';
// Hooks.
export { default as useConstrainedTabbing } from './hooks/use-constrained-tabbing';
@@ -38,3 +38,4 @@ export { default as useDebouncedInput } from './hooks/use-debounced-input';
export { default as useThrottle } from './hooks/use-throttle';
export { default as useMergeRefs } from './hooks/use-merge-refs';
export { default as useRefEffect } from './hooks/use-ref-effect';
+export { default as useNetworkConnectivity } from './hooks/use-network-connectivity';
diff --git a/packages/dataviews/src/add-filter.js b/packages/dataviews/src/add-filter.js
index acc4c86534014..a9d8d78509dc4 100644
--- a/packages/dataviews/src/add-filter.js
+++ b/packages/dataviews/src/add-filter.js
@@ -209,16 +209,14 @@ export default function AddFilter( { filters, view, onChangeView } ) {
disabled={ ! activeElement }
hideOnClick={ false }
onClick={ () => {
- onChangeView( ( currentView ) => ( {
- ...currentView,
+ onChangeView( {
+ ...view,
page: 1,
- filters:
- currentView.filters.filter(
- ( f ) =>
- f.field !==
- filter.field
- ),
- } ) );
+ filters: view.filters.filter(
+ ( f ) =>
+ f.field !== filter.field
+ ),
+ } );
} }
>
@@ -240,11 +238,11 @@ export default function AddFilter( { filters, view, onChangeView } ) {
}
hideOnClick={ false }
onClick={ () => {
- onChangeView( ( currentView ) => ( {
- ...currentView,
+ onChangeView( {
+ ...view,
page: 1,
filters: [],
- } ) );
+ } );
} }
>
diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js
index 6195eefe8fe47..0ba9d1d50a969 100644
--- a/packages/dataviews/src/filters.js
+++ b/packages/dataviews/src/filters.js
@@ -68,7 +68,7 @@ export default function Filters( { fields, view, onChangeView } ) {
return (
{
+export default function ResetFilter( { view, onChangeView } ) {
return (
);
-};
+}
diff --git a/packages/dataviews/src/search.js b/packages/dataviews/src/search.js
index 2e58b721d6e2e..10a578b49aab2 100644
--- a/packages/dataviews/src/search.js
+++ b/packages/dataviews/src/search.js
@@ -18,11 +18,11 @@ export default function Search( { label, view, onChangeView } ) {
onChangeViewRef.current = onChangeView;
}, [ onChangeView ] );
useEffect( () => {
- onChangeViewRef.current( ( currentView ) => ( {
- ...currentView,
+ onChangeViewRef.current( {
+ ...view,
page: 1,
search: debouncedSearch,
- } ) );
+ } );
}, [ debouncedSearch ] );
const searchLabel = label || __( 'Filter list' );
return (
diff --git a/packages/e2e-tests/specs/editor/various/block-switcher.test.js b/packages/e2e-tests/specs/editor/various/block-switcher.test.js
deleted file mode 100644
index 61278881077ba..0000000000000
--- a/packages/e2e-tests/specs/editor/various/block-switcher.test.js
+++ /dev/null
@@ -1,130 +0,0 @@
-/**
- * WordPress dependencies
- */
-import {
- hasBlockSwitcher,
- getAvailableBlockTransforms,
- createNewPost,
- insertBlock,
- pressKeyWithModifier,
-} from '@wordpress/e2e-test-utils';
-
-describe( 'Block Switcher', () => {
- beforeEach( async () => {
- await createNewPost();
- } );
-
- it( 'Should show the expected block transforms on the list block when the blocks are removed', async () => {
- // Insert a list block.
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( '- List content' );
- await page.keyboard.press( 'ArrowUp' );
- await pressKeyWithModifier( 'alt', 'F10' );
-
- // Verify the block switcher exists.
- expect( await hasBlockSwitcher() ).toBeTruthy();
-
- // Verify the correct block transforms appear.
- expect( await getAvailableBlockTransforms() ).toEqual(
- expect.arrayContaining( [
- 'Group',
- 'Paragraph',
- 'Heading',
- 'Quote',
- 'Columns',
- ] )
- );
- } );
-
- it( 'Should show the expected block transforms on the list block when the quote block is removed', async () => {
- // Remove the quote block from the list of registered blocks.
- await page.evaluate( () => {
- wp.blocks.unregisterBlockType( 'core/quote' );
- } );
-
- // Insert a list block.
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( '- List content' );
- await page.keyboard.press( 'ArrowUp' );
- await pressKeyWithModifier( 'alt', 'F10' );
-
- // Verify the block switcher exists.
- expect( await hasBlockSwitcher() ).toBeTruthy();
-
- // Verify the correct block transforms appear.
- expect( await getAvailableBlockTransforms() ).toEqual(
- expect.arrayContaining( [
- 'Group',
- 'Paragraph',
- 'Heading',
- 'Columns',
- ] )
- );
- } );
-
- it( 'Should not show the block switcher if all the blocks the list block transforms into are removed', async () => {
- // Insert a list block.
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( '- List content' );
-
- // Remove the paragraph and quote block from the list of registered blocks.
- await page.evaluate( () => {
- [
- 'core/quote',
- 'core/pullquote',
- 'core/paragraph',
- 'core/group',
- 'core/heading',
- 'core/columns',
- ].forEach( ( block ) => wp.blocks.unregisterBlockType( block ) );
- } );
-
- await page.keyboard.press( 'ArrowUp' );
- await pressKeyWithModifier( 'alt', 'F10' );
-
- // Verify the block switcher exists.
- expect( await hasBlockSwitcher() ).toBeFalsy();
- // Verify the correct block transforms appear.
- expect( await getAvailableBlockTransforms() ).toHaveLength( 0 );
- } );
-
- describe( 'Conditional tranformation options', () => {
- describe( 'Columns tranforms', () => {
- it( 'Should show Columns block only if selected blocks are between limits (1-6)', async () => {
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( '- List content' );
- await page.keyboard.press( 'ArrowUp' );
- await insertBlock( 'Heading' );
- await page.keyboard.type( 'I am a header' );
- await page.keyboard.down( 'Shift' );
- await page.keyboard.press( 'ArrowUp' );
- await page.keyboard.up( 'Shift' );
- expect( await getAvailableBlockTransforms() ).toEqual(
- expect.arrayContaining( [ 'Columns' ] )
- );
- } );
- it( 'Should NOT show Columns transform only if selected blocks are more than max limit(6)', async () => {
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( '- List content' );
- await page.keyboard.press( 'ArrowUp' );
- await insertBlock( 'Heading' );
- await page.keyboard.type( 'I am a header' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( 'First paragraph' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( 'Second paragraph' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( 'Third paragraph' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( 'Fourth paragraph' );
- await page.keyboard.press( 'Enter' );
- await page.keyboard.type( 'Fifth paragraph' );
- await pressKeyWithModifier( 'primary', 'a' );
- await pressKeyWithModifier( 'primary', 'a' );
- expect( await getAvailableBlockTransforms() ).not.toEqual(
- expect.arrayContaining( [ 'Columns' ] )
- );
- } );
- } );
- } );
-} );
diff --git a/packages/e2e-tests/specs/experiments/blocks/post-comments-form.test.js b/packages/e2e-tests/specs/experiments/blocks/post-comments-form.test.js
deleted file mode 100644
index 0a26788b2d442..0000000000000
--- a/packages/e2e-tests/specs/experiments/blocks/post-comments-form.test.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * WordPress dependencies
- */
-import {
- insertBlock,
- activateTheme,
- setOption,
- visitSiteEditor,
- enterEditMode,
- deleteAllTemplates,
- canvas,
-} from '@wordpress/e2e-test-utils';
-
-describe( 'Comments Form', () => {
- let previousCommentStatus;
-
- beforeAll( async () => {
- await activateTheme( 'emptytheme' );
- await deleteAllTemplates( 'wp_template' );
- previousCommentStatus = await setOption(
- 'default_comment_status',
- 'closed'
- );
- } );
-
- afterAll( async () => {
- await setOption( 'default_comment_status', previousCommentStatus );
- } );
-
- describe( 'placeholder', () => {
- it( 'displays in site editor even when comments are closed by default', async () => {
- // Navigate to "Singular" post template
- await visitSiteEditor();
- await expect( page ).toClick(
- '.edit-site-sidebar-navigation-item',
- { text: /templates/i }
- );
- await expect( page ).toClick(
- '.edit-site-sidebar-navigation-item',
- { text: /single entries/i }
- );
- await enterEditMode();
-
- // Insert post comments form
- await insertBlock( 'Comments Form' );
-
- // Ensure the placeholder is there
- await expect( canvas() ).toMatchElement(
- '.wp-block-post-comments-form .comment-form'
- );
- } );
- } );
-} );
diff --git a/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js b/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js
deleted file mode 100644
index 6b589ec7ea33d..0000000000000
--- a/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * WordPress dependencies
- */
-import {
- deleteAllTemplates,
- activateTheme,
- getAllBlocks,
- selectBlockByClientId,
- insertBlock,
- visitSiteEditor,
- enterEditMode,
-} from '@wordpress/e2e-test-utils';
-
-async function toggleSidebar() {
- await page.click(
- '.edit-site-header-edit-mode__actions button[aria-label="Settings"]'
- );
-}
-
-async function getActiveTabLabel() {
- return await page.$eval(
- '.edit-site-sidebar-edit-mode__panel-tab.is-active',
- ( element ) => element.getAttribute( 'aria-label' )
- );
-}
-
-async function getTemplateCard() {
- return {
- title: await page.$eval(
- '.edit-site-sidebar-card__title',
- ( element ) => element.innerText
- ),
- description: await page.$eval(
- '.edit-site-sidebar-card__description',
- ( element ) => element.innerText
- ),
- };
-}
-
-describe( 'Settings sidebar', () => {
- beforeAll( async () => {
- await activateTheme( 'emptytheme' );
- await deleteAllTemplates( 'wp_template' );
- await deleteAllTemplates( 'wp_template_part' );
- } );
- afterAll( async () => {
- await deleteAllTemplates( 'wp_template' );
- await deleteAllTemplates( 'wp_template_part' );
- await activateTheme( 'twentytwentyone' );
- } );
- beforeEach( async () => {
- await visitSiteEditor();
- await enterEditMode();
- } );
-
- describe( 'Template tab', () => {
- it( 'should open template tab by default if no block is selected', async () => {
- await toggleSidebar();
-
- expect( await getActiveTabLabel() ).toEqual(
- 'Template (selected)'
- );
- } );
-
- it( "should show the currently selected template's title and description", async () => {
- await toggleSidebar();
-
- const templateCardBeforeNavigation = await getTemplateCard();
- await visitSiteEditor( {
- postId: 'emptytheme//singular',
- postType: 'wp_template',
- } );
- await enterEditMode();
- const templateCardAfterNavigation = await getTemplateCard();
-
- expect( templateCardBeforeNavigation ).toMatchObject( {
- title: 'Index',
- description:
- 'Used as a fallback template for all pages when a more specific template is not defined.',
- } );
- expect( templateCardAfterNavigation ).toMatchObject( {
- title: 'Single Entries',
- description:
- 'Displays any single entry, such as a post or a page. This template will serve as a fallback when a more specific template (e.g. Single Post, Page, or Attachment) cannot be found.',
- } );
- } );
- } );
-
- describe( 'Block tab', () => {
- it( 'should open block tab by default if a block is selected', async () => {
- const allBlocks = await getAllBlocks();
- await selectBlockByClientId( allBlocks[ 0 ].clientId );
-
- await toggleSidebar();
-
- expect( await getActiveTabLabel() ).toEqual( 'Block (selected)' );
- } );
- } );
-
- describe( 'Tab switch based on selection', () => {
- it( 'should switch to block tab if we select a block, when Template is selected', async () => {
- await toggleSidebar();
- expect( await getActiveTabLabel() ).toEqual(
- 'Template (selected)'
- );
- // By inserting the block is also selected.
- await insertBlock( 'Heading' );
- expect( await getActiveTabLabel() ).toEqual( 'Block (selected)' );
- } );
- it( 'should switch to Template tab when a block was selected and we select the Template', async () => {
- await insertBlock( 'Heading' );
- await toggleSidebar();
- expect( await getActiveTabLabel() ).toEqual( 'Block (selected)' );
- await page.evaluate( () => {
- wp.data.dispatch( 'core/block-editor' ).clearSelectedBlock();
- } );
- expect( await getActiveTabLabel() ).toEqual(
- 'Template (selected)'
- );
- } );
- } );
-} );
diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js
index 56fda6e6154b2..33884d1c5659d 100644
--- a/packages/edit-site/src/components/page-pages/index.js
+++ b/packages/edit-site/src/components/page-pages/index.js
@@ -133,8 +133,10 @@ export default function PagePages() {
const [ pageId, setPageId ] = useState( null );
const history = useHistory();
- const onSelectionChange = ( items ) =>
- setPageId( items?.length === 1 ? items[ 0 ].id : null );
+ const onSelectionChange = useCallback(
+ ( items ) => setPageId( items?.length === 1 ? items[ 0 ].id : null ),
+ [ setPageId ]
+ );
const queryArgs = useMemo( () => {
const filters = {};
@@ -303,23 +305,19 @@ export default function PagePages() {
[ permanentlyDeletePostAction, restorePostAction, editPostAction ]
);
const onChangeView = useCallback(
- ( viewUpdater ) => {
- let updatedView =
- typeof viewUpdater === 'function'
- ? viewUpdater( view )
- : viewUpdater;
- if ( updatedView.type !== view.type ) {
- updatedView = {
- ...updatedView,
+ ( newView ) => {
+ if ( newView.type !== view.type ) {
+ newView = {
+ ...newView,
layout: {
- ...defaultConfigPerViewType[ updatedView.type ],
+ ...defaultConfigPerViewType[ newView.type ],
},
};
}
- setView( updatedView );
+ setView( newView );
},
- [ view, setView ]
+ [ view.type, setView ]
);
// TODO: we need to handle properly `data={ data || EMPTY_ARRAY }` for when `isLoading`.
diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js
index b394ef8eb6e76..bacb0f3190863 100644
--- a/packages/edit-site/src/components/page-patterns/grid-item.js
+++ b/packages/edit-site/src/components/page-patterns/grid-item.js
@@ -200,6 +200,7 @@ function GridItem( { categoryId, item, ...props } ) {
) }
diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js
index d97a5e42fb612..9c52aaa7f12f6 100644
--- a/packages/edit-site/src/components/page-templates/index.js
+++ b/packages/edit-site/src/components/page-templates/index.js
@@ -170,8 +170,11 @@ export default function DataviewsTemplates() {
} );
const history = useHistory();
- const onSelectionChange = ( items ) =>
- setTemplateId( items?.length === 1 ? items[ 0 ].id : null );
+ const onSelectionChange = useCallback(
+ ( items ) =>
+ setTemplateId( items?.length === 1 ? items[ 0 ].id : null ),
+ [ setTemplateId ]
+ );
const authors = useMemo( () => {
if ( ! allTemplates ) {
@@ -341,25 +344,23 @@ export default function DataviewsTemplates() {
],
[ resetTemplateAction ]
);
+
const onChangeView = useCallback(
- ( viewUpdater ) => {
- let updatedView =
- typeof viewUpdater === 'function'
- ? viewUpdater( view )
- : viewUpdater;
- if ( updatedView.type !== view.type ) {
- updatedView = {
- ...updatedView,
+ ( newView ) => {
+ if ( newView.type !== view.type ) {
+ newView = {
+ ...newView,
layout: {
- ...defaultConfigPerViewType[ updatedView.type ],
+ ...defaultConfigPerViewType[ newView.type ],
},
};
}
- setView( updatedView );
+ setView( newView );
},
- [ view, setView ]
+ [ view.type, setView ]
);
+
return (
<>
{
- let isCurrent = true;
-
- RNReactNativeGutenbergBridge.requestConnectionStatus(
- ( isBridgeConnected ) => {
- if ( ! isCurrent ) {
- return;
- }
-
- setIsConnected( isBridgeConnected );
- }
- );
-
- return () => {
- isCurrent = false;
- };
- }, [] );
-
- useEffect( () => {
- const subscription = subscribeConnectionStatus(
- ( { isConnected: isBridgeConnected } ) => {
- setIsConnected( isBridgeConnected );
- }
- );
-
- return () => {
- subscription.remove();
- };
- }, [] );
-
- return { isConnected };
-}
-
-function subscribeConnectionStatus( callback ) {
+export function subscribeConnectionStatus( callback ) {
return gutenbergBridgeEvents.addListener(
'connectionStatusChange',
callback
);
}
+export function requestConnectionStatus( callback ) {
+ return RNReactNativeGutenbergBridge.requestConnectionStatus( callback );
+}
+
/**
* Request media picker for the given media source.
*
diff --git a/test/e2e/specs/editor/blocks/post-comments-form.spec.js b/test/e2e/specs/editor/blocks/post-comments-form.spec.js
new file mode 100644
index 0000000000000..db75771dc0915
--- /dev/null
+++ b/test/e2e/specs/editor/blocks/post-comments-form.spec.js
@@ -0,0 +1,49 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Comments Form', () => {
+ let originalCommentStatus;
+
+ test.beforeAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'emptytheme' );
+ await requestUtils.deleteAllTemplates( 'wp_template' );
+ } );
+
+ test.beforeEach( async ( { requestUtils } ) => {
+ const siteSettings = await requestUtils.getSiteSettings();
+ originalCommentStatus = siteSettings.default_comment_status;
+ } );
+
+ test.afterEach( async ( { requestUtils } ) => {
+ await requestUtils.updateSiteSettings( {
+ default_comment_status: originalCommentStatus,
+ } );
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.activateTheme( 'twentytwentyone' );
+ } );
+
+ test( 'placeholder displays in the site editor even when comments are closed by default', async ( {
+ admin,
+ editor,
+ } ) => {
+ // Navigate to "Singular" post template
+ await admin.visitSiteEditor( {
+ postId: 'emptytheme//singular',
+ postType: 'wp_template',
+ canvas: 'edit',
+ } );
+
+ // Insert post comments form
+ await editor.insertBlock( { name: 'core/post-comments-form' } );
+
+ // Ensure the placeholder is there
+ const postCommentsFormBlock = editor.canvas.locator(
+ 'role=document[name="Block: Comments Form"i]'
+ );
+ await expect( postCommentsFormBlock.locator( 'form' ) ).toBeVisible();
+ } );
+} );
diff --git a/test/e2e/specs/editor/various/block-switcher.spec.js b/test/e2e/specs/editor/various/block-switcher.spec.js
index 12fd843ed2ed1..e68844ac4a9f6 100644
--- a/test/e2e/specs/editor/various/block-switcher.spec.js
+++ b/test/e2e/specs/editor/various/block-switcher.spec.js
@@ -8,7 +8,183 @@ test.describe( 'Block Switcher', () => {
await admin.createNewPost();
} );
- test( 'Block variation transforms', async ( { editor, page } ) => {
+ test( 'Should show the expected block transforms on the list block when the blocks are removed', async ( {
+ editor,
+ page,
+ pageUtils,
+ } ) => {
+ await editor.canvas
+ .getByRole( 'button', { name: 'Add default block' } )
+ .click();
+ await page.keyboard.type( '- List content' );
+ await page.keyboard.press( 'ArrowUp' );
+ await pageUtils.pressKeys( 'alt+F10' );
+
+ const blockSwitcher = page
+ .getByRole( 'toolbar', { name: 'Block tools' } )
+ .getByRole( 'button', { name: 'List' } );
+
+ // Verify the block switcher exists.
+ await expect( blockSwitcher ).toHaveAttribute(
+ 'aria-haspopup',
+ 'true'
+ );
+
+ // Verify the correct block transforms appear.
+ await blockSwitcher.click();
+ await expect(
+ page.getByRole( 'menu', { name: 'List' } ).getByRole( 'menuitem' )
+ ).toHaveText( [ 'Paragraph', 'Heading', 'Quote', 'Columns', 'Group' ] );
+ } );
+
+ test( 'Should show the expected block transforms on the list block when the quote block is removed', async ( {
+ editor,
+ page,
+ pageUtils,
+ } ) => {
+ // Remove the quote block from the list of registered blocks.
+ await page.waitForFunction( () => {
+ try {
+ window.wp.blocks.unregisterBlockType( 'core/quote' );
+ return true;
+ } catch {
+ return false;
+ }
+ } );
+
+ // Insert a list block.
+ await editor.canvas
+ .getByRole( 'button', { name: 'Add default block' } )
+ .click();
+ await page.keyboard.type( '- List content' );
+ await page.keyboard.press( 'ArrowUp' );
+ await pageUtils.pressKeys( 'alt+F10' );
+
+ const blockSwitcher = page
+ .getByRole( 'toolbar', { name: 'Block tools' } )
+ .getByRole( 'button', { name: 'List' } );
+
+ // Verify the block switcher exists.
+ await expect( blockSwitcher ).toHaveAttribute(
+ 'aria-haspopup',
+ 'true'
+ );
+
+ // Verify the correct block transforms appear.
+ await blockSwitcher.click();
+ await expect(
+ page.getByRole( 'menu', { name: 'List' } ).getByRole( 'menuitem' )
+ ).toHaveText( [ 'Paragraph', 'Heading', 'Columns', 'Group' ] );
+ } );
+
+ test( 'Should not show the block switcher if all the blocks the list block transforms into are removed', async ( {
+ editor,
+ page,
+ pageUtils,
+ } ) => {
+ // Insert a list block.
+ await editor.canvas
+ .getByRole( 'button', { name: 'Add default block' } )
+ .click();
+ await page.keyboard.type( '- List content' );
+
+ // Remove blocks.
+ await page.waitForFunction( () => {
+ try {
+ window.wp.data
+ .dispatch( 'core/blocks' )
+ .removeBlockTypes( [
+ 'core/quote',
+ 'core/pullquote',
+ 'core/paragraph',
+ 'core/group',
+ 'core/heading',
+ 'core/columns',
+ ] );
+ return true;
+ } catch {
+ return false;
+ }
+ } );
+
+ await page.keyboard.press( 'ArrowUp' );
+ await pageUtils.pressKeys( 'alt+F10' );
+
+ // Verify the block switcher isn't enabled.
+ await expect(
+ page
+ .getByRole( 'toolbar', { name: 'Block tools' } )
+ .getByRole( 'button', { name: 'List' } )
+ ).toBeDisabled();
+ } );
+
+ test( 'Should show Columns block only if selected blocks are between limits (1-6)', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.canvas
+ .getByRole( 'button', { name: 'Add default block' } )
+ .click();
+ await page.keyboard.type( '- List content' );
+ await page.keyboard.press( 'ArrowUp' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '## I am a header' );
+ await page.keyboard.down( 'Shift' );
+ await page.keyboard.press( 'ArrowUp' );
+ await page.keyboard.up( 'Shift' );
+
+ await page
+ .getByRole( 'toolbar', { name: 'Block tools' } )
+ .getByRole( 'button', { name: 'Multiple blocks selected' } )
+ .click();
+
+ await expect(
+ page
+ .getByRole( 'menu', { name: 'Multiple blocks selected' } )
+ .getByRole( 'menuitem', { name: 'Columns' } )
+ ).toBeVisible();
+ } );
+
+ test( 'Should NOT show Columns transform only if selected blocks are more than max limit(6)', async ( {
+ editor,
+ page,
+ pageUtils,
+ } ) => {
+ await editor.canvas
+ .getByRole( 'button', { name: 'Add default block' } )
+ .click();
+ await page.keyboard.type( '- List content' );
+ await page.keyboard.press( 'ArrowUp' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '## I am a header' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( 'First paragraph' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( 'Second paragraph' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( 'Third paragraph' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( 'Fourth paragraph' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( 'Fifth paragraph' );
+ await pageUtils.pressKeys( 'primary+a', { times: 2 } );
+
+ await page
+ .getByRole( 'toolbar', { name: 'Block tools' } )
+ .getByRole( 'button', { name: 'Multiple blocks selected' } )
+ .click();
+
+ await expect(
+ page
+ .getByRole( 'menu', { name: 'Multiple blocks selected' } )
+ .getByRole( 'menuitem', { name: 'Columns' } )
+ ).toBeHidden();
+ } );
+
+ test( 'should be able to transform to block variations', async ( {
+ editor,
+ page,
+ } ) => {
// This is the `stack` Group variation.
await editor.insertBlock( {
name: 'core/group',
diff --git a/test/e2e/specs/site-editor/settings-sidebar.spec.js b/test/e2e/specs/site-editor/settings-sidebar.spec.js
new file mode 100644
index 0000000000000..f063603deacca
--- /dev/null
+++ b/test/e2e/specs/site-editor/settings-sidebar.spec.js
@@ -0,0 +1,148 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Settings sidebar', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await Promise.all( [
+ requestUtils.activateTheme( 'emptytheme' ),
+ requestUtils.deleteAllTemplates( 'wp_template' ),
+ requestUtils.deleteAllTemplates( 'wp_template_part' ),
+ ] );
+ } );
+
+ test.beforeEach( async ( { admin } ) => {
+ await admin.visitSiteEditor( {
+ postId: 'emptytheme//index',
+ postType: 'wp_template',
+ canvas: 'edit',
+ } );
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await Promise.all( [
+ requestUtils.activateTheme( 'twentytwentyone' ),
+ requestUtils.deleteAllTemplates( 'wp_template' ),
+ requestUtils.deleteAllTemplates( 'wp_template_part' ),
+ ] );
+ } );
+
+ test.describe( 'Template tab', () => {
+ test( 'should open template tab by default if no block is selected', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.openDocumentSettingsSidebar();
+
+ await expect(
+ page
+ .getByRole( 'region', { name: 'Editor settings' } )
+ .getByRole( 'button', { name: 'Template (selected)' } )
+ ).toHaveClass( /is-active/ );
+ } );
+
+ test( `should show the currently selected template's title and description`, async ( {
+ admin,
+ editor,
+ page,
+ } ) => {
+ await editor.openDocumentSettingsSidebar();
+
+ const settingsSideber = page.getByRole( 'region', {
+ name: 'Editor settings',
+ } );
+ const templateTitle = settingsSideber.locator(
+ '.edit-site-sidebar-card__title'
+ );
+ const templateDescription = settingsSideber.locator(
+ '.edit-site-sidebar-card__description'
+ );
+
+ await expect( templateTitle ).toHaveText( 'Index' );
+ await expect( templateDescription ).toHaveText(
+ 'Used as a fallback template for all pages when a more specific template is not defined.'
+ );
+
+ await admin.visitSiteEditor( {
+ postId: 'emptytheme//singular',
+ postType: 'wp_template',
+ canvas: 'edit',
+ } );
+
+ await expect( templateTitle ).toHaveText( 'Single Entries' );
+ await expect( templateDescription ).toHaveText(
+ 'Displays any single entry, such as a post or a page. This template will serve as a fallback when a more specific template (e.g. Single Post, Page, or Attachment) cannot be found.'
+ );
+ } );
+ } );
+
+ test.describe( 'Block tab', () => {
+ test( 'should open block tab by default if a block is selected', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.selectBlocks(
+ editor.canvas.getByRole( 'document', { name: 'Block' } ).first()
+ );
+ await editor.openDocumentSettingsSidebar();
+
+ await expect(
+ page
+ .getByRole( 'region', { name: 'Editor settings' } )
+ .getByRole( 'button', { name: 'Block (selected)' } )
+ ).toHaveClass( /is-active/ );
+ } );
+ } );
+
+ test.describe( 'Tab switch based on selection', () => {
+ test( 'should switch to block tab if we select a block, when Template is selected', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.openDocumentSettingsSidebar();
+
+ await expect(
+ page
+ .getByRole( 'region', { name: 'Editor settings' } )
+ .getByRole( 'button', { name: 'Template (selected)' } )
+ ).toHaveClass( /is-active/ );
+
+ // By inserting the block is also selected.
+ await editor.insertBlock( { name: 'core/heading' } );
+ await expect(
+ page
+ .getByRole( 'region', { name: 'Editor settings' } )
+ .getByRole( 'button', { name: 'Block (selected)' } )
+ ).toHaveClass( /is-active/ );
+ } );
+
+ test( 'should switch to Template tab when a block was selected and we select the Template', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.selectBlocks(
+ editor.canvas.getByRole( 'document', { name: 'Block' } ).first()
+ );
+ await editor.openDocumentSettingsSidebar();
+
+ await expect(
+ page
+ .getByRole( 'region', { name: 'Editor settings' } )
+ .getByRole( 'button', { name: 'Block (selected)' } )
+ ).toHaveClass( /is-active/ );
+
+ await page.evaluate( () => {
+ window.wp.data
+ .dispatch( 'core/block-editor' )
+ .clearSelectedBlock();
+ } );
+
+ await expect(
+ page
+ .getByRole( 'region', { name: 'Editor settings' } )
+ .getByRole( 'button', { name: 'Template (selected)' } )
+ ).toHaveClass( /is-active/ );
+ } );
+ } );
+} );
diff --git a/test/native/setup.js b/test/native/setup.js
index 8bfa8fb0626f2..bf6c6d970aa32 100644
--- a/test/native/setup.js
+++ b/test/native/setup.js
@@ -107,7 +107,8 @@ jest.mock( '@wordpress/react-native-bridge', () => {
subscribeShowEditorHelp: jest.fn(),
subscribeOnUndoPressed: jest.fn(),
subscribeOnRedoPressed: jest.fn(),
- useIsConnected: jest.fn( () => ( { isConnected: true } ) ),
+ subscribeConnectionStatus: jest.fn( () => ( { remove: jest.fn() } ) ),
+ requestConnectionStatus: jest.fn( ( callback ) => callback( true ) ),
editorDidMount: jest.fn(),
showAndroidSoftKeyboard: jest.fn(),
hideAndroidSoftKeyboard: jest.fn(),