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 0ebc357e55b97d..540bbc270a9ed0 100644 --- a/packages/block-editor/src/components/block-settings/container.native.js +++ b/packages/block-editor/src/components/block-settings/container.native.js @@ -43,6 +43,7 @@ function BottomSheetSettings( { hideHeader contentStyle={ styles.content } hasNavigation + testID="block-settings-modal" { ...props } > diff --git a/packages/block-library/src/embed/edit.native.js b/packages/block-library/src/embed/edit.native.js index b9ff3350f71141..2c7ca52f28b868 100644 --- a/packages/block-library/src/embed/edit.native.js +++ b/packages/block-library/src/embed/edit.native.js @@ -13,7 +13,7 @@ import { embedContentIcon } from './icons'; import EmbedLoading from './embed-loading'; import EmbedPlaceholder from './embed-placeholder'; import EmbedPreview from './embed-preview'; -import EmbedBottomSheet from './embed-bottom-sheet'; +import EmbedLinkSettings from './embed-link-settings'; /** * External dependencies @@ -24,7 +24,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { _x } from '@wordpress/i18n'; -import { useState, useEffect } from '@wordpress/element'; +import { useCallback, useState, useEffect } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { useBlockProps, @@ -196,6 +196,14 @@ const EmbedEdit = ( props ) => { isEditingURL, ] ); + const onEditURL = useCallback( ( value ) => { + // The order of the following calls is important, we need to update the URL attribute before changing `isEditingURL`, + // otherwise the side-effect that potentially replaces the block when updating the local state won't use the new URL + // for creating the new block. + setAttributes( { url: value } ); + setIsEditingURL( false ); + }, [] ); + const blockProps = useBlockProps(); if ( fetching ) { @@ -230,16 +238,12 @@ const EmbedEdit = ( props ) => { ( WP_EMBED_TYPE === type && ! NOT_PREVIEWABLE_WP_EMBED_PROVIDERS.includes( providerNameSlug ) ); - const bottomSheetLabel = WP_EMBED_TYPE === type ? 'WordPress' : title; + const linkLabel = WP_EMBED_TYPE === type ? 'WordPress' : title; return ( <> { showEmbedPlaceholder ? ( <> - setIsEditingURL( true ) } - /> { url, ] ); } } + openEmbedLinkSettings={ () => + setShowEmbedBottomSheet( true ) + } /> ) : ( <> setIsEditingURL( true ) } + url={ url } + linkLabel={ linkLabel } + onEditURL={ onEditURL } /> { ) } - setShowEmbedBottomSheet( false ) } - onSubmit={ ( value ) => { - // The order of the following calls is important, we need to update the URL attribute before changing `isEditingURL`, - // otherwise the side-effect that potentially replaces the block when updating the local state won't use the new URL - // for creating the new block. - setAttributes( { url: value } ); - setIsEditingURL( false ); - } } + onSubmit={ onEditURL } + withBottomSheet /> ); diff --git a/packages/block-library/src/embed/embed-bottom-sheet.native.js b/packages/block-library/src/embed/embed-bottom-sheet.native.js deleted file mode 100644 index f525d22d23ad98..00000000000000 --- a/packages/block-library/src/embed/embed-bottom-sheet.native.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { - LinkSettingsNavigation, - FooterMessageLink, -} from '@wordpress/components'; -import { isURL } from '@wordpress/url'; -import { useDispatch } from '@wordpress/data'; -import { store as noticesStore } from '@wordpress/notices'; -import { useCallback, useState } from '@wordpress/element'; - -const EmbedBottomSheet = ( { value, label, isVisible, onClose, onSubmit } ) => { - const [ url, setURL ] = useState( value ); - const { createErrorNotice } = useDispatch( noticesStore ); - - const linkSettingsOptions = { - url: { - label: sprintf( - // translators: %s: embed block variant's label e.g: "Twitter". - __( '%s link' ), - label - ), - placeholder: __( 'Add link' ), - autoFocus: true, - autoFill: true, - }, - footer: { - label: ( - - ), - separatorType: 'topFullWidth', - }, - }; - - const onDismiss = useCallback( () => { - if ( url !== '' ) { - if ( isURL( url ) ) { - onSubmit( url ); - } else { - createErrorNotice( - __( 'Invalid URL. Please enter a valid URL.' ) - ); - // If the URL was already defined, we submit it to stop showing the embed placeholder. - onSubmit( value ); - } - } else { - // Resets the URL when new value is empty - onSubmit( '' ); - } - }, [ url, onSubmit, value ] ); - - function setAttributes( attributes ) { - setURL( attributes.url ); - } - - return ( - - ); -}; - -export default EmbedBottomSheet; diff --git a/packages/block-library/src/embed/embed-controls.native.js b/packages/block-library/src/embed/embed-controls.native.js new file mode 100644 index 00000000000000..5361e4da108282 --- /dev/null +++ b/packages/block-library/src/embed/embed-controls.native.js @@ -0,0 +1,65 @@ +/** + * Internal dependencies + */ +import EmbedLinkSettings from './embed-link-settings'; +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { PanelBody, ToggleControl } from '@wordpress/components'; +import { InspectorControls } from '@wordpress/block-editor'; +import { useDispatch } from '@wordpress/data'; +import { store as editPostStore } from '@wordpress/edit-post'; + +function getResponsiveHelp( checked ) { + return checked + ? __( + 'This embed will preserve its aspect ratio when the browser is resized.' + ) + : __( + 'This embed may not preserve its aspect ratio when the browser is resized.' + ); +} + +const EmbedControls = ( { + blockSupportsResponsive, + themeSupportsResponsive, + allowResponsive, + toggleResponsive, + url, + linkLabel, + onEditURL, +} ) => { + const { closeGeneralSidebar: closeSettingsBottomSheet } = useDispatch( + editPostStore + ); + + return ( + <> + + { themeSupportsResponsive && blockSupportsResponsive && ( + + + + ) } + + { + closeSettingsBottomSheet(); + onEditURL( value ); + } } + /> + + + + ); +}; + +export default EmbedControls; diff --git a/packages/block-library/src/embed/embed-link-settings.native.js b/packages/block-library/src/embed/embed-link-settings.native.js new file mode 100644 index 00000000000000..dcacde56aa05ea --- /dev/null +++ b/packages/block-library/src/embed/embed-link-settings.native.js @@ -0,0 +1,100 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { + LinkSettingsNavigation, + FooterMessageLink, +} from '@wordpress/components'; +import { isURL } from '@wordpress/url'; +import { useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; +import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; + +const EmbedLinkSettings = ( { + autoFocus, + value, + label, + isVisible, + onClose, + onSubmit, + withBottomSheet, +} ) => { + const url = useRef( value ); + const [ inputURL, setInputURL ] = useState( value ); + const { createErrorNotice } = useDispatch( noticesStore ); + + const linkSettingsOptions = { + url: { + label: sprintf( + // translators: %s: embed block variant's label e.g: "Twitter". + __( '%s link' ), + label + ), + placeholder: __( 'Add link' ), + autoFocus, + autoFill: true, + }, + footer: { + label: ( + + ), + separatorType: 'topFullWidth', + }, + }; + + const onDismiss = useCallback( () => { + if ( ! isURL( url.current ) && url.current !== '' ) { + createErrorNotice( __( 'Invalid URL. Please enter a valid URL.' ) ); + // If the URL was already defined, we submit it to stop showing the embed placeholder. + onSubmit( value ); + return; + } + onSubmit( url.current ); + }, [ onSubmit, value ] ); + + useEffect( () => { + url.current = value; + setInputURL( value ); + }, [ value ] ); + + /** + * If the Embed Bottom Sheet component does not utilize a bottom sheet then the onDismiss action is not + * called. Here we are wiring the onDismiss to the onClose callback that gets triggered when input is submitted. + */ + const performOnCloseOperations = useCallback( () => { + if ( onClose ) { + onClose(); + } + + if ( ! withBottomSheet ) { + onDismiss(); + } + }, [ onClose ] ); + + const onSetAttributes = useCallback( ( attributes ) => { + url.current = attributes.url; + setInputURL( attributes.url ); + }, [] ); + + return ( + + ); +}; + +export default EmbedLinkSettings; diff --git a/packages/block-library/src/embed/embed-placeholder.native.js b/packages/block-library/src/embed/embed-placeholder.native.js index 66c42533585e5d..aca1acee5125fb 100644 --- a/packages/block-library/src/embed/embed-placeholder.native.js +++ b/packages/block-library/src/embed/embed-placeholder.native.js @@ -27,6 +27,7 @@ const EmbedPlaceholder = ( { cannotEmbed, fallback, tryAgain, + openEmbedLinkSettings, } ) => { const containerStyle = usePreferredColorSchemeStyle( styles.embed__container, @@ -59,11 +60,18 @@ const EmbedPlaceholder = ( { value: 'convertToLinkOption', onSelect: fallback, }, + editLink: { + id: 'editLinkOption', + label: __( 'Edit link' ), + value: 'editLinkOption', + onSelect: openEmbedLinkSettings, + }, }; const options = compact( [ cannotEmbed && errorPickerOptions.retry, cannotEmbed && errorPickerOptions.convertToLink, + cannotEmbed && errorPickerOptions.editLink, ] ); function onPickerSelect( value ) { diff --git a/packages/block-library/src/embed/test/__snapshots__/index.native.js.snap b/packages/block-library/src/embed/test/__snapshots__/index.native.js.snap index d1e287c1168d36..0e85219afb31a1 100644 --- a/packages/block-library/src/embed/test/__snapshots__/index.native.js.snap +++ b/packages/block-library/src/embed/test/__snapshots__/index.native.js.snap @@ -102,6 +102,8 @@ https://www.youtube.com/watch?v=lXMskKTw3Bc " `; +exports[`Embed block edit URL sets empty state when setting an empty URL 1`] = `""`; + exports[`Embed block insert via slash inserter insert generic embed block 1`] = `""`; exports[`Embed block insert via slash inserter inserts Twitter embed block 1`] = `""`; @@ -122,6 +124,14 @@ exports[`Embed block insertion inserts YouTube embed block 1`] = `""`; +exports[`Embed block retry allows editing link if request failed 1`] = ` +" +
+https://twitter.com/notnownikki +
+" +`; + exports[`Embed block retry converts to link if preview request failed 1`] = ` "

https://twitter.com/notnownikki

diff --git a/packages/block-library/src/embed/test/index.native.js b/packages/block-library/src/embed/test/index.native.js index fb37b437b9c840..0f6c466004959b 100644 --- a/packages/block-library/src/embed/test/index.native.js +++ b/packages/block-library/src/embed/test/index.native.js @@ -185,10 +185,10 @@ beforeAll( () => { } ); beforeEach( () => { - // Invalidate embed preview resolutions - dispatch( coreStore ).invalidateResolutionForStoreSelector( - 'getEmbedPreview' - ); + // Invalidate all resolutions of core-data to prevent + // caching embed preview and theme supports requests. + dispatch( coreStore ).invalidateResolutionForStore(); + // Mock embed responses mockEmbedResponses( [ RICH_TEXT_EMBED_SUCCESS_RESPONSE, @@ -246,7 +246,6 @@ describe( 'Embed block', () => { const expectedURL = 'https://twitter.com/notnownikki'; const { - getByA11yLabel, getByPlaceholderText, getByTestId, } = await insertEmbedBlock(); @@ -264,12 +263,15 @@ describe( 'Embed block', () => { fireEvent( embedEditURLModal, 'backdropPress' ); fireEvent( embedEditURLModal, MODAL_DISMISS_EVENT ); - // Wait for block settings button to be present - const settingsButton = await waitFor( () => - getByA11yLabel( 'Open Settings' ) + const blockSettingsModal = await waitFor( () => + getByTestId( 'block-settings-modal' ) ); + // Get Twitter link field + const twitterLinkField = within( + blockSettingsModal + ).getByA11yLabel( `Twitter link, ${ expectedURL }` ); - expect( settingsButton ).toBeDefined(); + expect( twitterLinkField ).toBeDefined(); expect( getEditorHtml() ).toMatchSnapshot(); } ); @@ -279,30 +281,31 @@ describe( 'Embed block', () => { // Mock clipboard Clipboard.getString.mockResolvedValue( clipboardURL ); - const { - getByA11yLabel, - getByTestId, - getByText, - } = await insertEmbedBlock(); + const { getByTestId, getByText } = await insertEmbedBlock(); // Wait for edit URL modal to be visible const embedEditURLModal = getByTestId( 'embed-edit-url-modal' ); await waitFor( () => embedEditURLModal.props.isVisible ); - // Get embed link - const embedLink = await waitFor( () => getByText( clipboardURL ) ); + // Get embed link with auto-pasted URL + const autopastedLinkField = await waitFor( () => + getByText( clipboardURL ) + ); // Dismiss the edit URL modal fireEvent( embedEditURLModal, 'backdropPress' ); fireEvent( embedEditURLModal, MODAL_DISMISS_EVENT ); - // Wait for block settings button to be present - const settingsButton = await waitFor( () => - getByA11yLabel( 'Open Settings' ) + const blockSettingsModal = await waitFor( () => + getByTestId( 'block-settings-modal' ) ); + // Get Twitter link field + const twitterLinkField = within( + blockSettingsModal + ).getByA11yLabel( `Twitter link, ${ clipboardURL }` ); - expect( embedLink ).toBeDefined(); - expect( settingsButton ).toBeDefined(); + expect( autopastedLinkField ).toBeDefined(); + expect( twitterLinkField ).toBeDefined(); expect( getEditorHtml() ).toMatchSnapshot(); Clipboard.getString.mockReset(); @@ -333,7 +336,6 @@ describe( 'Embed block', () => { const expectedURL = 'https://twitter.com/notnownikki'; const { - getByA11yLabel, getByPlaceholderText, getByTestId, getByText, @@ -355,12 +357,15 @@ describe( 'Embed block', () => { fireEvent( embedEditURLModal, 'backdropPress' ); fireEvent( embedEditURLModal, MODAL_DISMISS_EVENT ); - // Wait for block settings button to be present - const settingsButton = await waitFor( () => - getByA11yLabel( 'Open Settings' ) + const blockSettingsModal = await waitFor( () => + getByTestId( 'block-settings-modal' ) ); + // Get Twitter link field + const twitterLinkField = within( + blockSettingsModal + ).getByA11yLabel( `Twitter link, ${ expectedURL }` ); - expect( settingsButton ).toBeDefined(); + expect( twitterLinkField ).toBeDefined(); expect( getEditorHtml() ).toMatchSnapshot(); } ); @@ -370,11 +375,9 @@ describe( 'Embed block', () => { // Mock clipboard Clipboard.getString.mockResolvedValue( clipboardURL ); - const { - getByA11yLabel, - getByTestId, - getByText, - } = await initializeWithEmbedBlock( EMPTY_EMBED_HTML ); + const { getByTestId, getByText } = await initializeWithEmbedBlock( + EMPTY_EMBED_HTML + ); // Edit URL fireEvent.press( getByText( 'ADD LINK' ) ); @@ -390,13 +393,16 @@ describe( 'Embed block', () => { fireEvent( embedEditURLModal, 'backdropPress' ); fireEvent( embedEditURLModal, MODAL_DISMISS_EVENT ); - // Wait for block settings button to be present - const settingsButton = await waitFor( () => - getByA11yLabel( 'Open Settings' ) + const blockSettingsModal = await waitFor( () => + getByTestId( 'block-settings-modal' ) ); + // Get Twitter link field + const twitterLinkField = within( + blockSettingsModal + ).getByA11yLabel( `Twitter link, ${ clipboardURL }` ); expect( embedLink ).toBeDefined(); - expect( settingsButton ).toBeDefined(); + expect( twitterLinkField ).toBeDefined(); expect( getEditorHtml() ).toMatchSnapshot(); Clipboard.getString.mockReset(); @@ -410,18 +416,18 @@ describe( 'Embed block', () => { getByTestId, } = await initializeWithEmbedBlock( RICH_TEXT_EMBED_HTML ); - // Edit URL + // Open Block Settings fireEvent.press( - await waitFor( () => getByA11yLabel( 'Edit URL' ) ) + await waitFor( () => getByA11yLabel( 'Open Settings' ) ) ); - // Wait for edit URL modal to be visible - const embedEditURLModal = getByTestId( 'embed-edit-url-modal' ); - await waitFor( () => embedEditURLModal.props.isVisible ); + // Wait for Block Settings to be visible + const blockSettingsModal = getByTestId( 'block-settings-modal' ); + await waitFor( () => blockSettingsModal.props.isVisible ); - // Dismiss the edit URL modal - fireEvent( embedEditURLModal, 'backdropPress' ); - fireEvent( embedEditURLModal, MODAL_DISMISS_EVENT ); + // Dismiss the Block Settings modal + fireEvent( blockSettingsModal, 'backdropPress' ); + fireEvent( blockSettingsModal, MODAL_DISMISS_EVENT ); expect( getEditorHtml() ).toMatchSnapshot(); } ); @@ -436,18 +442,20 @@ describe( 'Embed block', () => { getByTestId, } = await initializeWithEmbedBlock( RICH_TEXT_EMBED_HTML ); - // Edit URL + // Open Block Settings fireEvent.press( - await waitFor( () => getByA11yLabel( 'Edit URL' ) ) + await waitFor( () => getByA11yLabel( 'Open Settings' ) ) ); - // Wait for edit URL modal to be visible - const embedEditURLModal = getByTestId( 'embed-edit-url-modal' ); - await waitFor( () => embedEditURLModal.props.isVisible ); + // Wait for Block Settings to be visible + const blockSettingsModal = getByTestId( 'block-settings-modal' ); + await waitFor( () => blockSettingsModal.props.isVisible ); // Start editing link fireEvent.press( - getByA11yLabel( `Twitter link, ${ initialURL }` ) + within( blockSettingsModal ).getByA11yLabel( + `Twitter link, ${ initialURL }` + ) ); // Replace URL @@ -455,13 +463,15 @@ describe( 'Embed block', () => { fireEvent( linkTextInput, 'focus' ); fireEvent.changeText( linkTextInput, expectedURL ); - // Dismiss the edit URL modal - fireEvent( embedEditURLModal, 'backdropPress' ); - fireEvent( embedEditURLModal, MODAL_DISMISS_EVENT ); + // Dismiss the Block Settings modal + fireEvent( blockSettingsModal, 'backdropPress' ); + fireEvent( blockSettingsModal, MODAL_DISMISS_EVENT ); // Get YouTube link field const youtubeLinkField = await waitFor( () => - getByA11yLabel( `YouTube link, ${ expectedURL }` ) + within( blockSettingsModal ).getByA11yLabel( + `YouTube link, ${ expectedURL }` + ) ); expect( youtubeLinkField ).toBeDefined(); @@ -479,18 +489,20 @@ describe( 'Embed block', () => { getByText, } = await initializeWithEmbedBlock( RICH_TEXT_EMBED_HTML ); - // Edit URL + // Open Block Settings fireEvent.press( - await waitFor( () => getByA11yLabel( 'Edit URL' ) ) + await waitFor( () => getByA11yLabel( 'Open Settings' ) ) ); - // Wait for edit URL modal to be visible - const embedEditURLModal = getByTestId( 'embed-edit-url-modal' ); - await waitFor( () => embedEditURLModal.props.isVisible ); + // Wait for Block Settings to be visible + const blockSettingsModal = getByTestId( 'block-settings-modal' ); + await waitFor( () => blockSettingsModal.props.isVisible ); // Start editing link fireEvent.press( - getByA11yLabel( `Twitter link, ${ previousURL }` ) + within( blockSettingsModal ).getByA11yLabel( + `Twitter link, ${ previousURL }` + ) ); // Replace URL @@ -498,9 +510,9 @@ describe( 'Embed block', () => { fireEvent( linkTextInput, 'focus' ); fireEvent.changeText( linkTextInput, invalidURL ); - // Dismiss the edit URL modal - fireEvent( embedEditURLModal, 'backdropPress' ); - fireEvent( embedEditURLModal, MODAL_DISMISS_EVENT ); + // Dismiss the Block Settings modal + fireEvent( blockSettingsModal, 'backdropPress' ); + fireEvent( blockSettingsModal, MODAL_DISMISS_EVENT ); const errorNotice = await waitFor( () => getByText( 'Invalid URL. Please enter a valid URL.' ) @@ -510,6 +522,49 @@ describe( 'Embed block', () => { expect( getEditorHtml() ).toMatchSnapshot(); } ); + it( 'sets empty state when setting an empty URL', async () => { + const previousURL = 'https://twitter.com/notnownikki'; + + const { + getByA11yLabel, + getByDisplayValue, + getByTestId, + getByPlaceholderText, + } = await initializeWithEmbedBlock( RICH_TEXT_EMBED_HTML ); + + // Open Block Settings + fireEvent.press( + await waitFor( () => getByA11yLabel( 'Open Settings' ) ) + ); + + // Get Block Settings modal + const blockSettingsModal = getByTestId( 'block-settings-modal' ); + + // Start editing link + fireEvent.press( + within( blockSettingsModal ).getByA11yLabel( + `Twitter link, ${ previousURL }` + ) + ); + + // Replace URL with empty value + const linkTextInput = getByDisplayValue( previousURL ); + fireEvent( linkTextInput, 'focus' ); + fireEvent.changeText( linkTextInput, '' ); + + // Dismiss the Block Settings modal + fireEvent( blockSettingsModal, 'backdropPress' ); + fireEvent( blockSettingsModal, MODAL_DISMISS_EVENT ); + + // Get empty embed link + const emptyLinkTextInput = await waitFor( () => + getByPlaceholderText( 'Add link' ) + ); + + expect( emptyLinkTextInput ).toBeDefined(); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + // This test case covers the bug fixed in PR #35460 it( 'edits URL after dismissing two times the edit URL bottom sheet with empty value', async () => { const { block, getByTestId, getByText } = await insertEmbedBlock(); @@ -559,7 +614,7 @@ describe( 'Embed block', () => { } = await insertEmbedBlock(); // Wait for edit URL modal to be visible - let embedEditURLModal = getByTestId( 'embed-edit-url-modal' ); + const embedEditURLModal = getByTestId( 'embed-edit-url-modal' ); await waitFor( () => embedEditURLModal.props.isVisible ); // Set an bad URL @@ -571,30 +626,36 @@ describe( 'Embed block', () => { fireEvent( embedEditURLModal, 'backdropPress' ); fireEvent( embedEditURLModal, MODAL_DISMISS_EVENT ); - // Edit URL + // Open Block Settings fireEvent.press( - await waitFor( () => getByA11yLabel( 'Edit URL' ) ) + await waitFor( () => getByA11yLabel( 'Open Settings' ) ) ); - // Wait for edit URL modal to be visible - embedEditURLModal = getByTestId( 'embed-edit-url-modal' ); - await waitFor( () => embedEditURLModal.props.isVisible ); + // Wait for Block Settings to be visible + const blockSettingsModal = getByTestId( 'block-settings-modal' ); + await waitFor( () => blockSettingsModal.props.isVisible ); // Start editing link - fireEvent.press( getByA11yLabel( `Embed link, ${ badURL }` ) ); + fireEvent.press( + within( blockSettingsModal ).getByA11yLabel( + `Embed link, ${ badURL }` + ) + ); // Replace URL linkTextInput = getByDisplayValue( badURL ); fireEvent( linkTextInput, 'focus' ); fireEvent.changeText( linkTextInput, expectedURL ); - // Dismiss the edit URL modal - fireEvent( embedEditURLModal, 'backdropPress' ); - fireEvent( embedEditURLModal, MODAL_DISMISS_EVENT ); + // Dismiss the Block Settings modal + fireEvent( blockSettingsModal, 'backdropPress' ); + fireEvent( blockSettingsModal, MODAL_DISMISS_EVENT ); // Get Twitter link field const twitterLinkField = await waitFor( () => - getByA11yLabel( `Twitter link, ${ expectedURL }` ) + within( blockSettingsModal ).getByA11yLabel( + `Twitter link, ${ expectedURL }` + ) ); expect( twitterLinkField ).toBeDefined(); @@ -633,6 +694,8 @@ describe( 'Embed block', () => { describe( 'retry', () => { it( 'retries loading the preview if initial request failed', async () => { + const expectedURL = 'https://twitter.com/notnownikki'; + // Return bad response for the first request to oembed endpoint // and success response for the rest of requests. let isFirstEmbedRequest = true; @@ -650,23 +713,26 @@ describe( 'Embed block', () => { return Promise.resolve( response ); } ); - const { - getByA11yLabel, - getByText, - } = await initializeWithEmbedBlock( RICH_TEXT_EMBED_HTML ); + const { getByTestId, getByText } = await initializeWithEmbedBlock( + RICH_TEXT_EMBED_HTML + ); // Retry request fireEvent.press( getByText( 'More options' ) ); fireEvent.press( getByText( 'Retry' ) ); - // Wait for edit URL button to be present - const editURLButton = await waitFor( () => - getByA11yLabel( 'Edit URL' ) + const blockSettingsModal = await waitFor( () => + getByTestId( 'block-settings-modal' ) ); + // Get Twitter link field + const twitterLinkField = within( + blockSettingsModal + ).getByA11yLabel( `Twitter link, ${ expectedURL }` ); - expect( editURLButton ).toBeDefined(); + expect( twitterLinkField ).toBeDefined(); expect( getEditorHtml() ).toMatchSnapshot(); } ); + it( 'converts to link if preview request failed', async () => { // Return bad response for requests to oembed endpoint. fetchRequest.mockImplementation( ( { path } ) => { @@ -693,6 +759,61 @@ describe( 'Embed block', () => { expect( paragraphBlock ).toBeDefined(); expect( getEditorHtml() ).toMatchSnapshot(); } ); + + it( 'allows editing link if request failed', async () => { + const failURL = 'https://wordpress.org/news/2021/07/tatum/'; + const successURL = 'https://twitter.com/notnownikki'; + + // Return bad response for WordPress URL and success for Twitter URL. + fetchRequest.mockImplementation( ( { path } ) => { + const matchesPath = ( url ) => + path === + `/oembed/1.0/proxy?url=${ encodeURIComponent( url ) }`; + + let response = {}; + if ( matchesPath( failURL ) ) { + response = MOCK_BAD_WORDPRESS_RESPONSE; + } else if ( matchesPath( successURL ) ) { + response = RICH_TEXT_EMBED_SUCCESS_RESPONSE; + } + + return Promise.resolve( response ); + } ); + + const { + getByA11yLabel, + getByText, + getByTestId, + getByDisplayValue, + } = await initializeWithEmbedBlock( WP_EMBED_HTML ); + + fireEvent.press( getByText( 'More options' ) ); + fireEvent.press( getByText( 'Edit link' ) ); + + // Start editing link + fireEvent.press( getByA11yLabel( `WordPress link, ${ failURL }` ) ); + + // Set an URL + const linkTextInput = getByDisplayValue( failURL ); + fireEvent( linkTextInput, 'focus' ); + fireEvent.changeText( linkTextInput, successURL ); + + // Dismiss the edit URL modal + const embedEditURLModal = getByTestId( 'embed-edit-url-modal' ); + fireEvent( embedEditURLModal, 'backdropPress' ); + fireEvent( embedEditURLModal, MODAL_DISMISS_EVENT ); + + const blockSettingsModal = await waitFor( () => + getByTestId( 'block-settings-modal' ) + ); + // Get Twitter link field + const twitterLinkField = within( + blockSettingsModal + ).getByA11yLabel( `Twitter link, ${ successURL }` ); + + expect( twitterLinkField ).toBeDefined(); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); } ); describe( 'preview coming soon', () => { @@ -970,10 +1091,12 @@ describe( 'Embed block', () => { getByText, } = await initializeWithEmbedBlock( RICH_TEXT_EMBED_HTML ); + // Open Block Settings fireEvent.press( await waitFor( () => getByA11yLabel( 'Open Settings' ) ) ); + // Untoggle resize for smaller devices fireEvent.press( await waitFor( () => getByText( /Resize for smaller devices/ ) ) ); @@ -981,21 +1104,28 @@ describe( 'Embed block', () => { expect( getEditorHtml() ).toMatchSnapshot(); } ); - it( 'does not show settings button if responsive is not supported', async () => { - const { getByA11yLabel } = await initializeWithEmbedBlock( - WP_EMBED_HTML + it( 'does not show media settings panel if responsive is not supported', async () => { + const { + getByA11yLabel, + getByText, + } = await initializeWithEmbedBlock( WP_EMBED_HTML ); + + // Open Block Settings + fireEvent.press( + await waitFor( () => getByA11yLabel( 'Open Settings' ) ) ); - let settingsButton; + // Wait for media settings panel + let mediaSettingsPanel; try { - settingsButton = await waitFor( () => - getByA11yLabel( 'Open Settings' ) + mediaSettingsPanel = await waitFor( () => + getByText( 'Media settings' ) ); } catch ( e ) { // NOOP } - expect( settingsButton ).not.toBeDefined(); + expect( mediaSettingsPanel ).not.toBeDefined(); } ); } ); } ); diff --git a/packages/components/src/mobile/link-settings/index.native.js b/packages/components/src/mobile/link-settings/index.native.js index 70b5a3b6e9201f..0abebbdeb3fad6 100644 --- a/packages/components/src/mobile/link-settings/index.native.js +++ b/packages/components/src/mobile/link-settings/index.native.js @@ -93,6 +93,7 @@ function LinkSettings( { const [ urlInputValue, setUrlInputValue ] = useState( '' ); const [ labelInputValue, setLabelInputValue ] = useState( '' ); const [ linkRelInputValue, setLinkRelInputValue ] = useState( '' ); + const onCloseSettingsSheetConsumed = useRef( false ); const prevEditorSidebarOpenedRef = useRef(); const { onHandleClosingBottomSheet } = useContext( BottomSheetContext ); @@ -123,6 +124,10 @@ function LinkSettings( { useEffect( () => { const isSettingSheetOpen = isVisible || editorSidebarOpened; + if ( isSettingSheetOpen ) { + onCloseSettingsSheetConsumed.current = false; + } + if ( options.url.autoFill && isSettingSheetOpen && ! url ) { getURLFromClipboard(); } @@ -174,6 +179,12 @@ function LinkSettings( { }, [ urlInputValue, labelInputValue, linkRelInputValue, setAttributes ] ); const onCloseSettingsSheet = useCallback( () => { + if ( onCloseSettingsSheetConsumed.current ) { + return; + } + + onCloseSettingsSheetConsumed.current = true; + onSetAttributes(); if ( onClose ) { diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 7553204ad4cb16..e9a02b8bba43d1 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 +- [*] [Embed block] Included Link in Block Settings [#36099] ## 1.66.0 - [**] [Image block] Add ability to quickly link images to Media Files and Attachment Pages [#34846]