-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[RNMobile] Limit inner blocks nesting depth to avoid call stack size …
…exceeded crash (#54382) * Prevent rendering deeply nested block list * Add `BlockFallbackWebVersion` component * Add `WarningMaxDepthExceeded` component * Add max depth warning in inner blocks component * Update title of max depth exceeded warning Co-authored-by: David Calhoun <[email protected]> * Update description of max depth exceeded warning * Add constants file to `InnerBlocks` component `MAX_NESTING_DEPTH` has been moved to the constants file. * Revert `BlockFallbackWebVersion` export * Rename `BlockFallbackWebVersion` component to `UnsupportedBlockDetails` * Rename `UnsupportedBlockDetails` styles * Update default web editor action label via WP hooks * Allow inserting extra notes to description via WP hooks * Extract UBE settings fetch to a hook * Update description of max depth exceeded warning based on UBE support * Update `react-native-editor` changelog --------- Co-authored-by: David Calhoun <[email protected]>
- Loading branch information
Showing
7 changed files
with
419 additions
and
0 deletions.
There are no files selected for viewing
5 changes: 5 additions & 0 deletions
5
packages/block-editor/src/components/inner-blocks/constants.native.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// Hermes has a limit for the call stack depth to avoid infinite recursion. | ||
// When creating a deep nested structure of inner blocks, the editor might exceed | ||
// this limit and crash. In order to avoid this, we set a maximum depth level where | ||
// we stop rendering blocks. | ||
export const MAX_NESTING_DEPTH = 10; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
packages/block-editor/src/components/inner-blocks/warning-max-depth-exceeded.native.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { TouchableWithoutFeedback, View } from 'react-native'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __, sprintf } from '@wordpress/i18n'; | ||
import { useState } from '@wordpress/element'; | ||
import { useDispatch, useSelect } from '@wordpress/data'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import Warning from '../warning'; | ||
import UnsupportedBlockDetails from '../unsupported-block-details'; | ||
import { store as blockEditorStore } from '../../store'; | ||
import { MAX_NESTING_DEPTH } from './constants'; | ||
import useUnsupportedBlockEditor from '../use-unsupported-block-editor'; | ||
|
||
const WarningMaxDepthExceeded = ( { clientId } ) => { | ||
const [ showDetails, setShowDetails ] = useState( false ); | ||
|
||
const { isSelected, innerBlocks } = useSelect( | ||
( select ) => { | ||
const { getBlock, isBlockSelected } = select( blockEditorStore ); | ||
return { | ||
innerBlocks: getBlock( clientId )?.innerBlocks || [], | ||
isSelected: isBlockSelected( clientId ), | ||
}; | ||
}, | ||
[ clientId ] | ||
); | ||
const { replaceBlocks } = useDispatch( blockEditorStore ); | ||
|
||
const { | ||
isUnsupportedBlockEditorSupported, | ||
canEnableUnsupportedBlockEditor, | ||
} = useUnsupportedBlockEditor( clientId ); | ||
|
||
const onUngroup = () => { | ||
if ( ! innerBlocks.length ) { | ||
return; | ||
} | ||
|
||
replaceBlocks( clientId, innerBlocks ); | ||
}; | ||
|
||
let description; | ||
// When UBE can't be used, the description mentions using the web browser to edit the block. | ||
if ( | ||
! isUnsupportedBlockEditorSupported && | ||
! canEnableUnsupportedBlockEditor | ||
) { | ||
/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ | ||
const descriptionFormat = __( | ||
'Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser.' | ||
); | ||
description = sprintf( descriptionFormat, MAX_NESTING_DEPTH ); | ||
} | ||
// Otherwise, the description mentions using the web editor (i.e. UBE). | ||
else { | ||
/* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ | ||
const descriptionFormat = __( | ||
'Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor.' | ||
); | ||
description = sprintf( descriptionFormat, MAX_NESTING_DEPTH ); | ||
} | ||
|
||
return ( | ||
<TouchableWithoutFeedback | ||
disabled={ ! isSelected } | ||
accessibilityLabel={ __( 'Warning message' ) } | ||
accessibilityRole={ 'button' } | ||
accessibilityHint={ __( 'Tap here to show more details.' ) } | ||
onPress={ () => setShowDetails( true ) } | ||
> | ||
<View> | ||
<Warning | ||
message={ __( | ||
'Block cannot be rendered because it is deeply nested. Tap here for more details.' | ||
) } | ||
/> | ||
<UnsupportedBlockDetails | ||
clientId={ clientId } | ||
showSheet={ showDetails } | ||
onCloseSheet={ () => setShowDetails( false ) } | ||
title={ __( 'Deeply nested block' ) } | ||
description={ description } | ||
customActions={ [ | ||
{ label: __( 'Ungroup block' ), onPress: onUngroup }, | ||
] } | ||
/> | ||
</View> | ||
</TouchableWithoutFeedback> | ||
); | ||
}; | ||
|
||
export default WarningMaxDepthExceeded; |
183 changes: 183 additions & 0 deletions
183
packages/block-editor/src/components/unsupported-block-details/index.native.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { View, Text } from 'react-native'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { BottomSheet, Icon, TextControl } from '@wordpress/components'; | ||
import { | ||
requestUnsupportedBlockFallback, | ||
sendActionButtonPressedAction, | ||
actionButtons, | ||
} from '@wordpress/react-native-bridge'; | ||
import { help } from '@wordpress/icons'; | ||
import { __ } from '@wordpress/i18n'; | ||
import { usePreferredColorSchemeStyle } from '@wordpress/compose'; | ||
import { getBlockType } from '@wordpress/blocks'; | ||
import { useCallback, useState } from '@wordpress/element'; | ||
import { applyFilters } from '@wordpress/hooks'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import styles from './style.scss'; | ||
import useUnsupportedBlockEditor from '../use-unsupported-block-editor'; | ||
|
||
const EMPTY_ARRAY = []; | ||
|
||
const UnsupportedBlockDetails = ( { | ||
clientId, | ||
showSheet, | ||
onCloseSheet, | ||
customBlockTitle = '', | ||
icon, | ||
title, | ||
description, | ||
actionButtonLabel, | ||
customActions = EMPTY_ARRAY, | ||
} ) => { | ||
const [ sendFallbackMessage, setSendFallbackMessage ] = useState( false ); | ||
const [ sendButtonPressMessage, setSendButtonPressMessage ] = | ||
useState( false ); | ||
|
||
const { | ||
blockName, | ||
blockContent, | ||
isUnsupportedBlockEditorSupported, | ||
canEnableUnsupportedBlockEditor, | ||
isEditableInUnsupportedBlockEditor, | ||
} = useUnsupportedBlockEditor( clientId ); | ||
|
||
// Styles | ||
const textStyle = usePreferredColorSchemeStyle( | ||
styles[ 'unsupported-block-details__text' ], | ||
styles[ 'unsupported-block-details__text--dark' ] | ||
); | ||
const titleStyle = usePreferredColorSchemeStyle( | ||
styles[ 'unsupported-block-details__title' ], | ||
styles[ 'unsupported-block-details__title--dark' ] | ||
); | ||
const descriptionStyle = usePreferredColorSchemeStyle( | ||
styles[ 'unsupported-block-details__description' ], | ||
styles[ 'unsupported-block-details__description--dark' ] | ||
); | ||
const iconStyle = usePreferredColorSchemeStyle( | ||
styles[ 'unsupported-block-details__icon' ], | ||
styles[ 'unsupported-block-details__icon--dark' ] | ||
); | ||
const actionButtonStyle = usePreferredColorSchemeStyle( | ||
styles[ 'unsupported-block-details__action-button' ], | ||
styles[ 'unsupported-block-details__action-button--dark' ] | ||
); | ||
|
||
const blockTitle = | ||
customBlockTitle || getBlockType( blockName )?.title || blockName; | ||
|
||
const requestFallback = useCallback( () => { | ||
if ( | ||
canEnableUnsupportedBlockEditor && | ||
isUnsupportedBlockEditorSupported === false | ||
) { | ||
onCloseSheet(); | ||
setSendButtonPressMessage( true ); | ||
} else { | ||
onCloseSheet(); | ||
setSendFallbackMessage( true ); | ||
} | ||
}, [ | ||
canEnableUnsupportedBlockEditor, | ||
isUnsupportedBlockEditorSupported, | ||
onCloseSheet, | ||
] ); | ||
|
||
// The description can include extra notes via WP hooks. | ||
const descriptionWithNotes = applyFilters( | ||
'native.unsupported_block_details_extra_note', | ||
description, | ||
blockName | ||
); | ||
|
||
const webEditorDefaultLabel = applyFilters( | ||
'native.unsupported_block_details_web_editor_action', | ||
__( 'Edit using web editor' ) | ||
); | ||
|
||
const canUseWebEditor = | ||
( isUnsupportedBlockEditorSupported || | ||
canEnableUnsupportedBlockEditor ) && | ||
isEditableInUnsupportedBlockEditor; | ||
const actions = [ | ||
...[ | ||
canUseWebEditor && { | ||
label: actionButtonLabel || webEditorDefaultLabel, | ||
onPress: requestFallback, | ||
}, | ||
], | ||
...customActions, | ||
].filter( Boolean ); | ||
|
||
return ( | ||
<BottomSheet | ||
isVisible={ showSheet } | ||
hideHeader | ||
onClose={ onCloseSheet } | ||
onModalHide={ () => { | ||
if ( sendFallbackMessage ) { | ||
// On iOS, onModalHide is called when the controller is still part of the hierarchy. | ||
// A small delay will ensure that the controller has already been removed. | ||
this.timeout = setTimeout( () => { | ||
// For the Classic block, the content is kept in the `content` attribute. | ||
requestUnsupportedBlockFallback( | ||
blockContent, | ||
clientId, | ||
blockName, | ||
blockTitle | ||
); | ||
}, 100 ); | ||
setSendFallbackMessage( false ); | ||
} else if ( sendButtonPressMessage ) { | ||
this.timeout = setTimeout( () => { | ||
sendActionButtonPressedAction( | ||
actionButtons.missingBlockAlertActionButton | ||
); | ||
}, 100 ); | ||
setSendButtonPressMessage( false ); | ||
} | ||
} } | ||
> | ||
<View style={ styles[ 'unsupported-block-details__container' ] }> | ||
<Icon | ||
icon={ icon || help } | ||
color={ iconStyle.color } | ||
size={ iconStyle.size } | ||
/> | ||
<Text style={ [ textStyle, titleStyle ] }>{ title }</Text> | ||
{ isEditableInUnsupportedBlockEditor && | ||
descriptionWithNotes && ( | ||
<Text style={ [ textStyle, descriptionStyle ] }> | ||
{ descriptionWithNotes } | ||
</Text> | ||
) } | ||
</View> | ||
{ actions.map( ( { label, onPress }, index ) => ( | ||
<TextControl | ||
key={ `${ label } - ${ index }` } | ||
label={ label } | ||
separatorType="topFullWidth" | ||
onPress={ onPress } | ||
labelStyle={ actionButtonStyle } | ||
/> | ||
) ) } | ||
<TextControl | ||
label={ __( 'Dismiss' ) } | ||
separatorType="topFullWidth" | ||
onPress={ onCloseSheet } | ||
labelStyle={ actionButtonStyle } | ||
/> | ||
</BottomSheet> | ||
); | ||
}; | ||
|
||
export default UnsupportedBlockDetails; |
56 changes: 56 additions & 0 deletions
56
packages/block-editor/src/components/unsupported-block-details/style.native.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
.unsupported-block-details__container { | ||
flex-direction: column; | ||
align-items: center; | ||
justify-content: flex-end; | ||
} | ||
|
||
.unsupported-block-details__icon { | ||
size: 36; | ||
height: 36; | ||
padding-top: 8; | ||
padding-bottom: 8; | ||
color: $gray; | ||
} | ||
|
||
.unsupported-block-details__icon--dark { | ||
color: $gray-20; | ||
} | ||
|
||
.unsupported-block-details__text { | ||
text-align: center; | ||
color: $gray-dark; | ||
} | ||
|
||
.unsupported-block-details__text--dark { | ||
color: $white; | ||
} | ||
|
||
.unsupported-block-details__title { | ||
padding-top: 8; | ||
padding-bottom: 12; | ||
font-size: 20; | ||
font-weight: bold; | ||
color: $gray-dark; | ||
} | ||
|
||
.unsupported-block-details__title--dark { | ||
color: $white; | ||
} | ||
|
||
.unsupported-block-details__description { | ||
padding-bottom: 24; | ||
font-size: 16; | ||
color: $gray-darken-20; | ||
} | ||
|
||
.unsupported-block-details__description--dark { | ||
color: $gray-20; | ||
} | ||
|
||
.unsupported-block-details__action-button { | ||
color: $blue-50; | ||
} | ||
|
||
.unsupported-block-details__action-button--dark { | ||
color: $blue-30; | ||
} |
Oops, something went wrong.