diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js
index 697a9ffc19daa6..bdea027b116f14 100644
--- a/packages/block-editor/src/components/block-list/index.native.js
+++ b/packages/block-editor/src/components/block-list/index.native.js
@@ -24,7 +24,7 @@ import { __ } from '@wordpress/i18n';
*/
import styles from './style.scss';
import BlockListAppender from '../block-list-appender';
-import BlockListItem from './block-list-item.native';
+import BlockListItem from './block-list-item';
import { store as blockEditorStore } from '../../store';
const BlockListContext = createContext();
diff --git a/packages/block-editor/src/components/block-settings/container.native.js b/packages/block-editor/src/components/block-settings/container.native.js
index 7f9d8a9073d398..0ebc357e55b97d 100644
--- a/packages/block-editor/src/components/block-settings/container.native.js
+++ b/packages/block-editor/src/components/block-settings/container.native.js
@@ -6,6 +6,7 @@ import {
BottomSheet,
ColorSettings,
FocalPointSettingsPanel,
+ ImageLinkDestinationsScreen,
LinkPickerScreen,
} from '@wordpress/components';
import { compose } from '@wordpress/compose';
@@ -21,6 +22,7 @@ export const blockSettingsScreens = {
color: 'Color',
focalPoint: 'FocalPoint',
linkPicker: 'linkPicker',
+ imageLinkDestinations: 'imageLinkDestinations',
};
function BottomSheetSettings( {
@@ -75,6 +77,11 @@ function BottomSheetSettings( {
returnScreenName={ blockSettingsScreens.settings }
/>
+
+
+
);
diff --git a/packages/block-editor/src/components/block-styles/preview.native.js b/packages/block-editor/src/components/block-styles/preview.native.js
index c017331ad38a32..1fce3ddc0acc5e 100644
--- a/packages/block-editor/src/components/block-styles/preview.native.js
+++ b/packages/block-editor/src/components/block-styles/preview.native.js
@@ -67,11 +67,11 @@ function StylePreview( { onPress, isActive, style, url } ) {
);
const getOutline = ( outlineStyles ) =>
- outlineStyles.map( ( outlineStyle ) => {
+ outlineStyles.map( ( outlineStyle, index ) => {
return (
);
} );
diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js
index 6468b9f7198622..82fd9f638a20c0 100644
--- a/packages/block-library/src/image/edit.native.js
+++ b/packages/block-library/src/image/edit.native.js
@@ -2,11 +2,12 @@
* External dependencies
*/
import { View, TouchableWithoutFeedback } from 'react-native';
+import { useRoute } from '@react-navigation/native';
/**
* WordPress dependencies
*/
-import { Component } from '@wordpress/element';
+import { Component, useEffect } from '@wordpress/element';
import {
requestMediaImport,
mediaUploadSync,
@@ -41,6 +42,7 @@ import {
BlockAlignmentToolbar,
BlockStyles,
store as blockEditorStore,
+ blockSettingsScreens,
} from '@wordpress/block-editor';
import { __, _x, sprintf } from '@wordpress/i18n';
import { getProtocol, hasQueryArg } from '@wordpress/url';
@@ -63,6 +65,7 @@ import styles from './styles.scss';
import { getUpdatedLinkTargetSettings } from './utils';
import {
+ LINK_DESTINATION_NONE,
LINK_DESTINATION_CUSTOM,
LINK_DESTINATION_ATTACHMENT,
LINK_DESTINATION_MEDIA,
@@ -76,6 +79,101 @@ const getUrlForSlug = ( image, sizeSlug ) => {
return image?.media_details?.sizes?.[ sizeSlug ]?.source_url;
};
+function LinkSettings( {
+ attributes,
+ image,
+ isLinkSheetVisible,
+ setMappedAttributes,
+} ) {
+ const route = useRoute();
+ const { href: url, label, linkDestination, linkTarget, rel } = attributes;
+
+ // Persist attributes passed from child screen
+ useEffect( () => {
+ const { inputValue: newUrl } = route.params || {};
+
+ let newLinkDestination;
+ switch ( newUrl ) {
+ case attributes.url:
+ newLinkDestination = LINK_DESTINATION_MEDIA;
+ break;
+ case image?.link:
+ newLinkDestination = LINK_DESTINATION_ATTACHMENT;
+ break;
+ case '':
+ newLinkDestination = LINK_DESTINATION_NONE;
+ break;
+ default:
+ newLinkDestination = LINK_DESTINATION_CUSTOM;
+ break;
+ }
+
+ setMappedAttributes( {
+ url: newUrl,
+ linkDestination: newLinkDestination,
+ } );
+ }, [ route.params?.inputValue ] );
+
+ let valueMask;
+ switch ( linkDestination ) {
+ case LINK_DESTINATION_MEDIA:
+ valueMask = __( 'Media File' );
+ break;
+ case LINK_DESTINATION_ATTACHMENT:
+ valueMask = __( 'Attachment Page' );
+ break;
+ case LINK_DESTINATION_CUSTOM:
+ valueMask = __( 'Custom URL' );
+ break;
+ default:
+ valueMask = __( 'None' );
+ break;
+ }
+
+ const linkSettingsOptions = {
+ url: {
+ valueMask,
+ autoFocus: false,
+ autoFill: true,
+ },
+ openInNewTab: {
+ label: __( 'Open in new tab' ),
+ },
+ linkRel: {
+ label: __( 'Link Rel' ),
+ placeholder: _x( 'None', 'Link rel attribute value placeholder' ),
+ },
+ };
+
+ return (
+
+ {
+ navigation.navigate(
+ blockSettingsScreens.imageLinkDestinations,
+ {
+ inputValue: attributes.href,
+ linkDestination: attributes.linkDestination,
+ imageUrl: attributes.url,
+ attachmentPageUrl: image?.link,
+ }
+ );
+ } }
+ />
+
+ );
+}
+
export class ImageEdit extends Component {
constructor( props ) {
super( props );
@@ -96,7 +194,6 @@ export class ImageEdit extends Component {
);
this.updateMediaProgress = this.updateMediaProgress.bind( this );
this.updateImageURL = this.updateImageURL.bind( this );
- this.onSetLinkDestination = this.onSetLinkDestination.bind( this );
this.onSetNewTab = this.onSetNewTab.bind( this );
this.onSetSizeSlug = this.onSetSizeSlug.bind( this );
this.onImagePressed = this.onImagePressed.bind( this );
@@ -108,25 +205,6 @@ export class ImageEdit extends Component {
);
this.setMappedAttributes = this.setMappedAttributes.bind( this );
this.onSizeChangeValue = this.onSizeChangeValue.bind( this );
-
- this.linkSettingsOptions = {
- url: {
- label: __( 'Image Link URL' ),
- placeholder: __( 'Add URL' ),
- autoFocus: false,
- autoFill: true,
- },
- openInNewTab: {
- label: __( 'Open in new tab' ),
- },
- linkRel: {
- label: __( 'Link Rel' ),
- placeholder: _x(
- 'None',
- 'Link rel attribute value placeholder'
- ),
- },
- };
}
componentDidMount() {
@@ -282,13 +360,6 @@ export class ImageEdit extends Component {
} );
}
- onSetLinkDestination( href ) {
- this.props.setAttributes( {
- linkDestination: LINK_DESTINATION_CUSTOM,
- href,
- } );
- }
-
onSetNewTab( value ) {
const updatedLinkTarget = getUpdatedLinkTargetSettings(
value,
@@ -383,45 +454,23 @@ export class ImageEdit extends Component {
: width;
}
- setMappedAttributes( { url: href, ...restAttributes } ) {
+ setMappedAttributes( { url: href, linkDestination, ...restAttributes } ) {
const { setAttributes } = this.props;
+ if ( ! href && ! linkDestination ) {
+ linkDestination = LINK_DESTINATION_NONE;
+ } else if ( ! linkDestination ) {
+ linkDestination = LINK_DESTINATION_CUSTOM;
+ }
- return href === undefined
- ? setAttributes( {
- ...restAttributes,
- linkDestination: LINK_DESTINATION_CUSTOM,
- } )
+ return href === undefined || href === this.props.attributes.href
+ ? setAttributes( restAttributes )
: setAttributes( {
...restAttributes,
+ linkDestination,
href,
- linkDestination: LINK_DESTINATION_CUSTOM,
} );
}
- getLinkSettings() {
- const { isLinkSheetVisible } = this.state;
- const {
- attributes: { href: url, ...unMappedAttributes },
- } = this.props;
- const mappedAttributes = { ...unMappedAttributes, url };
-
- return (
-
- );
- }
-
getAltTextSettings() {
const {
attributes: { alt },
@@ -583,9 +632,12 @@ export class ImageEdit extends Component {
) }
{ this.getAltTextSettings() }
-
- { this.getLinkSettings( true ) }
-
+
{
+ const dataControls = jest.requireActual( '@wordpress/data-controls' );
+ return {
+ ...dataControls,
+ apiFetch: jest.fn(),
+ };
+} );
-const getStylesFromColorScheme = () => {
- return { color: 'white' };
-};
+const apiFetchPromise = Promise.resolve( {} );
+apiFetch.mockImplementation( () => apiFetchPromise );
-const setAttributes = jest.fn();
+beforeAll( () => {
+ registerCoreBlocks();
+
+ // Mock Image.getSize to avoid failed attempt to size non-existant image
+ const getSizeSpy = jest.spyOn( Image, 'getSize' );
+ getSizeSpy.mockImplementation( ( _url, callback ) => callback( 300, 200 ) );
+} );
+
+afterAll( () => {
+ getBlockTypes().forEach( ( { name } ) => {
+ unregisterBlockType( name );
+ } );
-const getImageComponent = ( attributes = {} ) => (
-
-);
+ // Restore mocks
+ Image.getSize.mockRestore();
+} );
describe( 'Image Block', () => {
- beforeEach( () => {
- setAttributes.mockReset();
+ it( 'sets link to None', async () => {
+ const initialHtml = `
+
+
+ `;
+ const screen = await initializeEditor( { initialHtml } );
+ // We must await the image fetch via `getMedia`
+ await act( () => apiFetchPromise );
+
+ fireEvent.press( screen.getByA11yLabel( /Image Block/ ) );
+ // Awaiting navigation event seemingly required due to React Navigation bug
+ // https://git.io/Ju35Z
+ await act( () =>
+ fireEvent.press( screen.getByA11yLabel( 'Open Settings' ) )
+ );
+ fireEvent.press( screen.getByText( 'Media File' ) );
+ fireEvent.press( screen.getByText( 'None' ) );
+
+ const expectedHtml = `
+
+`;
+ expect( getEditorHtml() ).toBe( expectedHtml );
+ } );
+
+ it( 'sets link to Media File', async () => {
+ const initialHtml = `
+
+
+ `;
+ const screen = await initializeEditor( { initialHtml } );
+ // We must await the image fetch via `getMedia`
+ await act( () => apiFetchPromise );
+
+ fireEvent.press( screen.getByA11yLabel( /Image Block/ ) );
+ // Awaiting navigation event seemingly required due to React Navigation bug
+ // https://git.io/Ju35Z
+ await act( () =>
+ fireEvent.press( screen.getByA11yLabel( 'Open Settings' ) )
+ );
+ fireEvent.press( screen.getByText( 'None' ) );
+ fireEvent.press( screen.getByText( 'Media File' ) );
+
+ const expectedHtml = `
+
+`;
+ expect( getEditorHtml() ).toBe( expectedHtml );
} );
- it( 'renders without crashing', () => {
- const component = renderer.create( getImageComponent() );
- const rendered = component.toJSON();
- expect( rendered ).toBeTruthy();
+ it( 'sets link to Custom URL', async () => {
+ const initialHtml = `
+
+
+ `;
+ const screen = await initializeEditor( { initialHtml } );
+ // We must await the image fetch via `getMedia`
+ await act( () => apiFetchPromise );
+
+ fireEvent.press( screen.getByA11yLabel( /Image Block/ ) );
+ // Awaiting navigation event seemingly required due to React Navigation bug
+ // https://git.io/Ju35Z
+ await act( () =>
+ fireEvent.press( screen.getByA11yLabel( 'Open Settings' ) )
+ );
+ fireEvent.press( screen.getByText( 'None' ) );
+ fireEvent.press( screen.getByText( 'Custom URL' ) );
+ fireEvent.changeText(
+ screen.getByPlaceholderText( 'Search or type URL' ),
+ 'wordpress.org'
+ );
+ fireEvent.press( screen.getByA11yLabel( 'Apply' ) );
+
+ const expectedHtml = `
+
+`;
+ expect( getEditorHtml() ).toBe( expectedHtml );
} );
- it( 'sets link target', () => {
- const component = renderer.create( getImageComponent() );
- const instance = component.getInstance();
+ it( 'swaps the link between destinations', async () => {
+ const initialHtml = `
+
+
+ `;
+ const screen = await initializeEditor( { initialHtml } );
+ // We must await the image fetch via `getMedia`
+ await act( () => apiFetchPromise );
- instance.onSetNewTab( true );
+ fireEvent.press( screen.getByA11yLabel( /Image Block/ ) );
+ // Awaiting navigation event seemingly required due to React Navigation bug
+ // https://git.io/Ju35Z
+ await act( () =>
+ fireEvent.press( screen.getByA11yLabel( 'Open Settings' ) )
+ );
+ fireEvent.press( screen.getByText( 'None' ) );
+ fireEvent.press( screen.getByText( 'Media File' ) );
+ fireEvent.press( screen.getByText( 'Custom URL' ) );
+ fireEvent.changeText(
+ screen.getByPlaceholderText( 'Search or type URL' ),
+ 'wordpress.org'
+ );
+ fireEvent.press( screen.getByA11yLabel( 'Apply' ) );
+ fireEvent.press( screen.getByText( 'Custom URL' ) );
+ fireEvent.press( screen.getByText( 'Media File' ) );
- expect( setAttributes ).toHaveBeenCalledWith( {
- linkTarget: '_blank',
- rel: undefined,
- } );
+ const expectedHtml = `
+
+`;
+ expect( getEditorHtml() ).toBe( expectedHtml );
} );
- it( 'unset link target', () => {
- const component = renderer.create(
- getImageComponent( {
- linkTarget: '_blank',
- rel: NEW_TAB_REL.join( ' ' ),
- } )
+ it( 'does not display the Link To URL within the Custom URL input when set to Media File and query parameters are present', async () => {
+ const initialHtml = `
+
+
+ `;
+ const screen = await initializeEditor( { initialHtml } );
+ // We must await the image fetch via `getMedia`
+ await act( () => apiFetchPromise );
+
+ fireEvent.press( screen.getByA11yLabel( /Image Block/ ) );
+ // Awaiting navigation event seemingly required due to React Navigation bug
+ // https://git.io/Ju35Z
+ await act( () =>
+ fireEvent.press( screen.getByA11yLabel( 'Open Settings' ) )
);
- const instance = component.getInstance();
+ fireEvent.press( screen.getByText( 'Media File' ) );
+
+ expect( screen.queryByA11yLabel( /https:\/\/cldup\.com/ ) ).toBeNull();
+ } );
+
+ it( 'sets link target', async () => {
+ const initialHtml = `
+
+
+ `;
+ const screen = await initializeEditor( { initialHtml } );
+ // We must await the image fetch via `getMedia`
+ await act( () => apiFetchPromise );
+
+ const imageBlock = screen.getByA11yLabel( /Image Block/ );
+ fireEvent.press( imageBlock );
+
+ const settingsButton = screen.getByA11yLabel( 'Open Settings' );
+ // Awaiting navigation event seemingly required due to React Navigation bug
+ // https://git.io/Ju35Z
+ await act( () => fireEvent.press( settingsButton ) );
+
+ const linkTargetButton = screen.getByText( 'Open in new tab' );
+ fireEvent.press( linkTargetButton );
+
+ const expectedHtml = `
+
+`;
+ expect( getEditorHtml() ).toBe( expectedHtml );
+ } );
+
+ it( 'unset link target', async () => {
+ const initialHtml = `
+
+
+ `;
+ const screen = await initializeEditor( { initialHtml } );
+ // We must await the image fetch via `getMedia`
+ await act( () => apiFetchPromise );
+
+ const imageBlock = screen.getByA11yLabel( /Image Block/ );
+ fireEvent.press( imageBlock );
+
+ const settingsButton = screen.getByA11yLabel( 'Open Settings' );
+ // Awaiting navigation event seemingly required due to React Navigation bug
+ // https://git.io/Ju35Z
+ await act( () => fireEvent.press( settingsButton ) );
- instance.onSetNewTab( false );
+ const linkTargetButton = screen.getByText( 'Open in new tab' );
+ fireEvent.press( linkTargetButton );
- expect( setAttributes ).toHaveBeenCalledWith( {
- linkTarget: undefined,
- rel: undefined,
- } );
+ const expectedHtml = `
+
+`;
+ expect( getEditorHtml() ).toBe( expectedHtml );
} );
} );
diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js
index d733021cba85dc..5b42724130a97f 100644
--- a/packages/components/src/index.native.js
+++ b/packages/components/src/index.native.js
@@ -103,6 +103,7 @@ export { default as LinkPickerScreen } from './mobile/link-picker/link-picker-sc
export { default as LinkSettings } from './mobile/link-settings';
export { default as LinkSettingsScreen } from './mobile/link-settings/link-settings-screen';
export { default as LinkSettingsNavigation } from './mobile/link-settings/link-settings-navigation';
+export { default as ImageLinkDestinationsScreen } from './mobile/link-settings/image-link-destinations-screen';
export { default as SegmentedControl } from './mobile/segmented-control';
export { default as Image, IMAGE_DEFAULT_FOCAL_POINT } from './mobile/image';
export { default as ImageEditingButton } from './mobile/image/image-editing-button';
diff --git a/packages/components/src/mobile/bottom-sheet/cell.native.js b/packages/components/src/mobile/bottom-sheet/cell.native.js
index 136e4daef1b220..ac780d9e0f65d5 100644
--- a/packages/components/src/mobile/bottom-sheet/cell.native.js
+++ b/packages/components/src/mobile/bottom-sheet/cell.native.js
@@ -106,6 +106,7 @@ class BottomSheetCell extends Component {
valuePlaceholder = '',
icon,
leftAlign,
+ iconStyle = {},
labelStyle = {},
valueStyle = {},
cellContainerStyle = {},
@@ -307,7 +308,7 @@ class BottomSheetCell extends Component {
);
};
- const iconStyle = getStylesFromColorScheme(
+ const iconStyleBase = getStylesFromColorScheme(
styles.icon,
styles.iconDark
);
@@ -362,7 +363,11 @@ class BottomSheetCell extends Component {
diff --git a/packages/components/src/mobile/link-settings/image-link-destinations-screen.native.js b/packages/components/src/mobile/link-settings/image-link-destinations-screen.native.js
new file mode 100644
index 00000000000000..6cdfc5a781c6a0
--- /dev/null
+++ b/packages/components/src/mobile/link-settings/image-link-destinations-screen.native.js
@@ -0,0 +1,148 @@
+/**
+ * External dependencies
+ */
+import { useNavigation, useRoute } from '@react-navigation/native';
+import { StyleSheet } from 'react-native';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, check, chevronRight } from '@wordpress/icons';
+import { blockSettingsScreens } from '@wordpress/block-editor';
+import { usePreferredColorSchemeStyle } from '@wordpress/compose';
+
+/**
+ * Internal dependencies
+ */
+import styles from './style.scss';
+import PanelBody from '../../panel/body';
+import BottomSheet from '../bottom-sheet';
+
+const LINK_DESTINATION_NONE = 'none';
+const LINK_DESTINATION_MEDIA = 'media';
+const LINK_DESTINATION_ATTACHMENT = 'attachment';
+const LINK_DESTINATION_CUSTOM = 'custom';
+
+function LinkDestination( {
+ children,
+ isSelected,
+ label,
+ onPress,
+ value,
+ valueStyle,
+} ) {
+ const optionIcon = usePreferredColorSchemeStyle(
+ styles.optionIcon,
+ styles.optionIconDark
+ );
+ return (
+
+ { children }
+
+ );
+}
+
+function ImageLinkDestinationsScreen( props ) {
+ const navigation = useNavigation();
+ const route = useRoute();
+ const { url = '' } = props;
+ const { inputValue = url, imageUrl, attachmentPageUrl, linkDestination } =
+ route.params || {};
+
+ function goToLinkPicker() {
+ navigation.navigate( blockSettingsScreens.linkPicker, {
+ inputValue:
+ linkDestination === LINK_DESTINATION_CUSTOM ? inputValue : '',
+ } );
+ }
+
+ const setLinkDestination = ( newLinkDestination ) => () => {
+ let newUrl;
+ switch ( newLinkDestination ) {
+ case LINK_DESTINATION_MEDIA:
+ newUrl = imageUrl;
+ break;
+ case LINK_DESTINATION_ATTACHMENT:
+ newUrl = attachmentPageUrl;
+ break;
+ default:
+ newUrl = '';
+ break;
+ }
+
+ navigation.navigate( blockSettingsScreens.settings, {
+ // The `inputValue` name is reused from LinkPicker, as it helps avoid
+ // bugs from stale values remaining in the React Navigation route
+ // parameters
+ inputValue: newUrl,
+ // Clear link text value that may be set from LinkPicker
+ text: '',
+ } );
+ };
+
+ return (
+ <>
+
+
+
+ { __( 'Link To' ) }
+
+
+
+
+
+ { !! attachmentPageUrl && (
+
+ ) }
+
+
+
+
+ >
+ );
+}
+
+export default ImageLinkDestinationsScreen;
diff --git a/packages/components/src/mobile/link-settings/index.native.js b/packages/components/src/mobile/link-settings/index.native.js
index eaa00bf26ba259..70b5a3b6e9201f 100644
--- a/packages/components/src/mobile/link-settings/index.native.js
+++ b/packages/components/src/mobile/link-settings/index.native.js
@@ -86,9 +86,9 @@ function LinkSettings( {
urlValue,
// Attributes properties
url,
- label,
+ label = '',
linkTarget,
- rel,
+ rel = '',
} ) {
const [ urlInputValue, setUrlInputValue ] = useState( '' );
const [ labelInputValue, setLabelInputValue ] = useState( '' );
@@ -226,6 +226,7 @@ function LinkSettings( {
) : (
diff --git a/packages/components/src/mobile/link-settings/link-settings-screen.native.js b/packages/components/src/mobile/link-settings/link-settings-screen.native.js
index cf81843d6ee16f..65c95187d2fcb8 100644
--- a/packages/components/src/mobile/link-settings/link-settings-screen.native.js
+++ b/packages/components/src/mobile/link-settings/link-settings-screen.native.js
@@ -20,17 +20,21 @@ const LinkSettingsScreen = ( props ) => {
const { inputValue = url } = route.params || {};
const onLinkCellPressed = () => {
- navigation.navigate( 'linkPicker', { inputValue } );
+ if ( props.onLinkCellPressed ) {
+ props.onLinkCellPressed( { navigation } );
+ } else {
+ navigation.navigate( 'linkPicker', { inputValue } );
+ }
};
return useMemo( () => {
return (
);
}, [ props, inputValue, navigation, route ] );
diff --git a/packages/components/src/mobile/link-settings/style.native.scss b/packages/components/src/mobile/link-settings/style.native.scss
index b9545b80ec4ab1..137c2e32dfc768 100644
--- a/packages/components/src/mobile/link-settings/style.native.scss
+++ b/packages/components/src/mobile/link-settings/style.native.scss
@@ -2,3 +2,20 @@
padding-left: 0;
padding-right: 0;
}
+
+// used in both light and dark modes
+.placeholderTextColor {
+ color: #87a6bc;
+}
+
+.optionIcon {
+ color: $blue-50;
+}
+
+.optionIconDark {
+ color: $blue-30;
+}
+
+.unselectedOptionIcon {
+ opacity: 0;
+}
diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md
index 27442a817942b2..a055276d776242 100644
--- a/packages/react-native-editor/CHANGELOG.md
+++ b/packages/react-native-editor/CHANGELOG.md
@@ -10,6 +10,7 @@ For each user feature we should also add a importance categorization label to i
-->
## Unreleased
+- [**] [Image block] Add ability to quickly link images to Media Files and Attachment Pages [#34846]
## 1.65.0
- [**] Search block - Text and background color support [#35511]
diff --git a/test/native/setup.js b/test/native/setup.js
index 318fbe03ff46ba..ffd651680b45b2 100644
--- a/test/native/setup.js
+++ b/test/native/setup.js
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+import 'react-native-gesture-handler/jestSetup';
import { NativeModules as RNNativeModules } from 'react-native';
RNNativeModules.UIManager = RNNativeModules.UIManager || {};