diff --git a/packages/block-editor/src/components/block-types-list/README.md b/packages/block-editor/src/components/block-types-list/README.md index f93352f371f2b6..68e89573447db8 100644 --- a/packages/block-editor/src/components/block-types-list/README.md +++ b/packages/block-editor/src/components/block-types-list/README.md @@ -35,6 +35,25 @@ The blocks that will be displayed in the block list. - Type: `Array` - Required: Yes +- Platform: Web | Mobile + +#### name + +Name of the list to be used as part of component's key. + +- Type: `String` +- Required: Yes +- Platform: Mobile + +#### listProps + +Extra `FlatList` props for customizing the list. + +On Mobile usually this component is rendered inside `BottomSheet` component, which already [generates these props](<(https://github.com/WordPress/gutenberg/blob/1ca1fe0c64dfe1a385221399fc94b0fb14f34199/packages/components/src/mobile/bottom-sheet/index.native.js#L355-L372)>) for this component. + +- Type: `String` +- Required: No +- Platform: Mobile ## Related components diff --git a/packages/block-editor/src/components/block-types-list/index.native.js b/packages/block-editor/src/components/block-types-list/index.native.js new file mode 100644 index 00000000000000..9c330a38ac66b2 --- /dev/null +++ b/packages/block-editor/src/components/block-types-list/index.native.js @@ -0,0 +1,112 @@ +/** + * External dependencies + */ +import { + Dimensions, + FlatList, + StyleSheet, + TouchableWithoutFeedback, + View, +} from 'react-native'; + +/** + * WordPress dependencies + */ +import { useState, useEffect } from '@wordpress/element'; +import { BottomSheet, InserterButton } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import styles from './style.scss'; + +const MIN_COL_NUM = 3; + +export default function BlockTypesList( { name, items, onSelect, listProps } ) { + const [ numberOfColumns, setNumberOfColumns ] = useState( MIN_COL_NUM ); + const [ itemWidth, setItemWidth ] = useState(); + const [ maxWidth, setMaxWidth ] = useState(); + + useEffect( () => { + Dimensions.addEventListener( 'change', onLayout ); + onLayout(); + return () => { + Dimensions.removeEventListener( 'change', onLayout ); + }; + }, [] ); + + function calculateItemWidth() { + const { + paddingLeft: itemPaddingLeft, + paddingRight: itemPaddingRight, + } = InserterButton.Styles.modalItem; + const { width } = InserterButton.Styles.modalIconWrapper; + return width + itemPaddingLeft + itemPaddingRight; + } + + function onLayout() { + const columnStyle = styles[ 'block-types-list__column' ]; + const sumLeftRightPadding = + columnStyle.paddingLeft + columnStyle.paddingRight; + + const bottomSheetWidth = BottomSheet.getWidth(); + const containerTotalWidth = bottomSheetWidth - sumLeftRightPadding; + const itemTotalWidth = calculateItemWidth(); + + const columnsFitToWidth = Math.floor( + containerTotalWidth / itemTotalWidth + ); + + const numColumns = Math.max( MIN_COL_NUM, columnsFitToWidth ); + + setNumberOfColumns( numColumns ); + setMaxWidth( containerTotalWidth / numColumns ); + + if ( columnsFitToWidth < MIN_COL_NUM ) { + const updatedItemWidth = + ( bottomSheetWidth - 2 * sumLeftRightPadding ) / MIN_COL_NUM; + setItemWidth( updatedItemWidth ); + } + } + + const contentContainerStyle = StyleSheet.flatten( + listProps.contentContainerStyle + ); + + return ( + ( + + + + ) } + keyExtractor={ ( item ) => item.id } + renderItem={ ( { item } ) => ( + + ) } + { ...listProps } + contentContainerStyle={ { + ...contentContainerStyle, + paddingBottom: Math.max( + listProps.safeAreaBottomInset, + contentContainerStyle.paddingBottom + ), + } } + /> + ); +} diff --git a/packages/block-editor/src/components/block-types-list/style.native.scss b/packages/block-editor/src/components/block-types-list/style.native.scss new file mode 100644 index 00000000000000..a9a3b4171ba18b --- /dev/null +++ b/packages/block-editor/src/components/block-types-list/style.native.scss @@ -0,0 +1,7 @@ +.block-types-list__row-separator { + height: 12px; +} + +.block-types-list__column { + padding: $grid-unit-20; +} diff --git a/packages/block-editor/src/components/inserter/block-types-tab.native.js b/packages/block-editor/src/components/inserter/block-types-tab.native.js new file mode 100644 index 00000000000000..1ae14c2e962182 --- /dev/null +++ b/packages/block-editor/src/components/inserter/block-types-tab.native.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import BlockTypesList from '../block-types-list'; +import useClipboardBlock from './hooks/use-clipboard-block'; +import { store as blockEditorStore } from '../../store'; + +const NON_BLOCK_CATEGORIES = [ 'reusable' ]; + +function BlockTypesTab( { onSelect, rootClientId, listProps } ) { + const clipboardBlock = useClipboardBlock( rootClientId ); + + const { items } = useSelect( + ( select ) => { + const { getInserterItems } = select( blockEditorStore ); + + const allItems = getInserterItems( rootClientId ); + const blockItems = allItems.filter( + ( { category } ) => ! NON_BLOCK_CATEGORIES.includes( category ) + ); + + return { + items: clipboardBlock + ? [ clipboardBlock, ...blockItems ] + : blockItems, + }; + }, + [ rootClientId ] + ); + + return ( + + ); +} + +export default BlockTypesTab; diff --git a/packages/block-editor/src/components/inserter/hooks/use-clipboard-block.native.js b/packages/block-editor/src/components/inserter/hooks/use-clipboard-block.native.js new file mode 100644 index 00000000000000..c86dce77847745 --- /dev/null +++ b/packages/block-editor/src/components/inserter/hooks/use-clipboard-block.native.js @@ -0,0 +1,39 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { rawHandler, store as blocksStore } from '@wordpress/blocks'; +import { getClipboard } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../../store'; + +export default function useClipboardBlock( destinationRootClientId ) { + const { canInsertBlockType } = useSelect( blockEditorStore ); + const { getBlockType } = useSelect( blocksStore ); + + const clipboard = getClipboard(); + const clipboardBlock = rawHandler( { HTML: clipboard } )[ 0 ]; + + const canAddClipboardBlock = canInsertBlockType( + clipboardBlock?.name, + destinationRootClientId + ); + + if ( ! canAddClipboardBlock ) { + return undefined; + } + + const { icon, name } = getBlockType( clipboardBlock.name ); + const { attributes: initialAttributes, innerBlocks } = clipboardBlock; + + return { + id: 'clipboard', + name, + icon, + initialAttributes, + innerBlocks, + }; +} diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js index 45a1695ac94d3a..ccbe387e4b8373 100644 --- a/packages/block-editor/src/components/inserter/menu.native.js +++ b/packages/block-editor/src/components/inserter/menu.native.js @@ -1,33 +1,27 @@ /** * External dependencies */ -import { View } from 'react-native'; +import { LayoutAnimation, TouchableHighlight } from 'react-native'; + /** * WordPress dependencies */ import { useEffect, useState, useCallback } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; -import { - createBlock, - rawHandler, - store as blocksStore, -} from '@wordpress/blocks'; -import { - BottomSheet, - BottomSheetConsumer, - getClipboard, -} from '@wordpress/components'; +import { createBlock } from '@wordpress/blocks'; +import { BottomSheet, BottomSheetConsumer } from '@wordpress/components'; /** * Internal dependencies */ - import InserterSearchResults from './search-results'; import InserterSearchForm from './search-form'; import { store as blockEditorStore } from '../../store'; -import { searchItems } from './search-items'; +import InserterTabs from './tabs'; +import styles from './style.scss'; const MIN_ITEMS_FOR_SEARCH = 2; +const REUSABLE_BLOCKS_CATEGORY = 'reusable'; function InserterMenu( { onSelect, @@ -39,8 +33,10 @@ function InserterMenu( { insertionIndex, } ) { const [ filterValue, setFilterValue ] = useState( '' ); + const [ showTabs, setShowTabs ] = useState( true ); // eslint-disable-next-line no-undef const [ showSearchForm, setShowSearchForm ] = useState( __DEV__ ); + const [ tabIndex, setTabIndex ] = useState( 0 ); const { showInsertionPoint, @@ -52,30 +48,37 @@ function InserterMenu( { insertDefaultBlock, } = useDispatch( blockEditorStore ); - const { items, destinationRootClientId } = useSelect( ( select ) => { - const { - getInserterItems, - getBlockRootClientId, - getBlockSelectionEnd, - } = select( blockEditorStore ); - - let targetRootClientId = rootClientId; - if ( ! targetRootClientId && ! clientId && ! isAppender ) { - const end = getBlockSelectionEnd(); - if ( end ) { - targetRootClientId = getBlockRootClientId( end ) || undefined; + const { items, destinationRootClientId, showReusableBlocks } = useSelect( + ( select ) => { + const { + getInserterItems, + getBlockRootClientId, + getBlockSelectionEnd, + } = select( blockEditorStore ); + + let targetRootClientId = rootClientId; + if ( ! targetRootClientId && ! clientId && ! isAppender ) { + const end = getBlockSelectionEnd(); + if ( end ) { + targetRootClientId = + getBlockRootClientId( end ) || undefined; + } } - } - return { - items: getInserterItems( targetRootClientId ), - destinationRootClientId: targetRootClientId, - }; - } ); - const { getBlockOrder, getBlockCount, canInsertBlockType } = useSelect( - blockEditorStore + const allItems = getInserterItems( targetRootClientId ); + const reusableBlockItems = allItems.filter( + ( { category } ) => category === REUSABLE_BLOCKS_CATEGORY + ); + + return { + items: allItems, + destinationRootClientId: targetRootClientId, + showReusableBlocks: !! reusableBlockItems.length, + }; + } ); - const { getBlockType } = useSelect( blocksStore ); + + const { getBlockOrder, getBlockCount } = useSelect( blockEditorStore ); useEffect( () => { // Show/Hide insertion point on Mount/Dismount @@ -98,7 +101,7 @@ function InserterMenu( { showInsertionPoint( destinationRootClientId, insertionIndex ); // Show search form if there are enough items to filter. - if ( getItems()?.length < MIN_ITEMS_FOR_SEARCH ) { + if ( items.length < MIN_ITEMS_FOR_SEARCH ) { setShowSearchForm( false ); } @@ -135,77 +138,80 @@ function InserterMenu( { [ insertBlock, destinationRootClientId, insertionIndex ] ); - /** - * Processes the inserter items to check - * if there's any copied block in the clipboard - * to add it as an extra item - */ - function getItems() { - // Filter out reusable blocks (they will be added in another tab) - let itemsToDisplay = items.filter( - ( { name } ) => name !== 'core/block' - ); - - itemsToDisplay = searchItems( itemsToDisplay, filterValue ); - - const clipboard = getClipboard(); - let clipboardBlock = rawHandler( { HTML: clipboard } )[ 0 ]; - - const canAddClipboardBlock = canInsertBlockType( - clipboardBlock?.name, - destinationRootClientId - ); - - if ( ! canAddClipboardBlock ) { - return itemsToDisplay; - } + const onSelectItem = useCallback( + ( item ) => { + onInsert( item ); + onSelect( item ); + }, + [ onInsert, onSelect ] + ); - const { icon, name } = getBlockType( clipboardBlock.name ); - const { attributes: initialAttributes, innerBlocks } = clipboardBlock; + const onChangeSearch = useCallback( + ( value ) => { + if ( ! value ) { + LayoutAnimation.configureNext( + LayoutAnimation.Presets.easeInEaseOut + ); + } + setFilterValue( value ); + }, + [ setFilterValue ] + ); - clipboardBlock = { - id: 'clipboard', - name, - icon, - initialAttributes, - innerBlocks, - }; + const onKeyboardShow = useCallback( () => setShowTabs( false ), [ + setShowTabs, + ] ); - return [ clipboardBlock, ...itemsToDisplay ]; - } + const onKeyboardHide = useCallback( () => setShowTabs( true ), [ + setShowTabs, + ] ); return ( { - setFilterValue( value ); - } } - value={ filterValue } - /> - ) + <> + { showSearchForm && ( + + ) } + { showTabs && ! filterValue && ( + + ) } + } hasNavigation setMinHeightToMaxHeight={ showSearchForm } + contentStyle={ styles.list } > - { ( { listProps, safeAreaBottomInset } ) => ( - - { - onInsert( item ); - onSelect( item ); - } } - { ...{ - listProps, - safeAreaBottomInset, - } } - /> - + { ( { listProps } ) => ( + + { ! showTabs || filterValue ? ( + + ) : ( + + ) } + ) } diff --git a/packages/block-editor/src/components/inserter/reusable-blocks-tab.native.js b/packages/block-editor/src/components/inserter/reusable-blocks-tab.native.js new file mode 100644 index 00000000000000..337c08de739e74 --- /dev/null +++ b/packages/block-editor/src/components/inserter/reusable-blocks-tab.native.js @@ -0,0 +1,38 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import BlockTypesList from '../block-types-list'; +import { store as blockEditorStore } from '../../store'; + +const REUSABLE_BLOCKS_CATEGORY = 'reusable'; + +function ReusableBlocksTab( { onSelect, rootClientId, listProps } ) { + const { items } = useSelect( + ( select ) => { + const { getInserterItems } = select( blockEditorStore ); + const allItems = getInserterItems( rootClientId ); + const reusableBlockItems = allItems.filter( + ( { category } ) => category === REUSABLE_BLOCKS_CATEGORY + ); + + return { items: reusableBlockItems }; + }, + [ rootClientId ] + ); + + return ( + + ); +} + +export default ReusableBlocksTab; diff --git a/packages/block-editor/src/components/inserter/search-results.native.js b/packages/block-editor/src/components/inserter/search-results.native.js index feecdf0981265a..fd556f008fd195 100644 --- a/packages/block-editor/src/components/inserter/search-results.native.js +++ b/packages/block-editor/src/components/inserter/search-results.native.js @@ -1,113 +1,35 @@ -/** - * External dependencies - */ -import { - FlatList, - View, - TouchableHighlight, - TouchableWithoutFeedback, - Dimensions, -} from 'react-native'; - /** * WordPress dependencies */ -import { useState, useEffect } from '@wordpress/element'; -import { BottomSheet, InserterButton } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import styles from './style.scss'; - -const MIN_COL_NUM = 3; +import { searchItems } from './search-items'; +import BlockTypesList from '../block-types-list'; +import { store as blockEditorStore } from '../../store'; function InserterSearchResults( { - items, + filterValue, onSelect, listProps, - safeAreaBottomInset, + rootClientId, } ) { - const [ numberOfColumns, setNumberOfColumns ] = useState( MIN_COL_NUM ); - const [ itemWidth, setItemWidth ] = useState(); - const [ maxWidth, setMaxWidth ] = useState(); - - useEffect( () => { - Dimensions.addEventListener( 'change', onLayout ); - return () => { - Dimensions.removeEventListener( 'change', onLayout ); - }; - }, [] ); - - function calculateItemWidth() { - const { - paddingLeft: itemPaddingLeft, - paddingRight: itemPaddingRight, - } = InserterButton.Styles.modalItem; - const { width } = InserterButton.Styles.modalIconWrapper; - return width + itemPaddingLeft + itemPaddingRight; - } - - function onLayout() { - const sumLeftRightPadding = - styles.columnPadding.paddingLeft + - styles.columnPadding.paddingRight; - - const bottomSheetWidth = BottomSheet.getWidth(); - const containerTotalWidth = bottomSheetWidth - sumLeftRightPadding; - const itemTotalWidth = calculateItemWidth(); - - const columnsFitToWidth = Math.floor( - containerTotalWidth / itemTotalWidth - ); - - const numColumns = Math.max( MIN_COL_NUM, columnsFitToWidth ); - - setNumberOfColumns( numColumns ); - setMaxWidth( containerTotalWidth / numColumns ); - - if ( columnsFitToWidth < MIN_COL_NUM ) { - const updatedItemWidth = - ( bottomSheetWidth - 2 * sumLeftRightPadding ) / MIN_COL_NUM; - setItemWidth( updatedItemWidth ); - } - } + const { items } = useSelect( + ( select ) => { + const allItems = select( blockEditorStore ).getInserterItems( + rootClientId + ); + const filteredItems = searchItems( allItems, filterValue ); + + return { items: filteredItems }; + }, + [ rootClientId, filterValue ] + ); return ( - - ( - - - - ) } - keyExtractor={ ( item ) => item.name } - renderItem={ ( { item } ) => ( - - ) } - { ...listProps } - contentContainerStyle={ [ - ...listProps.contentContainerStyle, - { - paddingBottom: - safeAreaBottomInset || styles.list.paddingBottom, - }, - ] } - /> - + ); } diff --git a/packages/block-editor/src/components/inserter/style.native.scss b/packages/block-editor/src/components/inserter/style.native.scss index 55063f811cc784..a0f017931a62aa 100644 --- a/packages/block-editor/src/components/inserter/style.native.scss +++ b/packages/block-editor/src/components/inserter/style.native.scss @@ -1,9 +1,5 @@ /** @format */ -.rowSeparator { - height: 12px; -} - .addBlockButton { color: $blue-wordpress; } @@ -12,12 +8,9 @@ color: $blue-30; } -.columnPadding { - padding: $grid-unit-20; -} - .list { padding-bottom: 20; + padding-top: 8; } .searchForm { @@ -50,3 +43,16 @@ .searchFormPlaceholderDark { color: rgba($white, 0.8); } + +.inserter-tabs__wrapper { + overflow: hidden; +} + +.inserter-tabs__container { + height: 100%; + width: 100%; +} + +.inserter-tabs__item { + position: absolute; +} diff --git a/packages/block-editor/src/components/inserter/tabs.native.js b/packages/block-editor/src/components/inserter/tabs.native.js new file mode 100644 index 00000000000000..f0c82a9d5cb0d6 --- /dev/null +++ b/packages/block-editor/src/components/inserter/tabs.native.js @@ -0,0 +1,153 @@ +/** + * External dependencies + */ +import { Animated, View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { SegmentedControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import BlockTypesTab from './block-types-tab'; +import ReusableBlocksTab from './reusable-blocks-tab'; +import styles from './style.scss'; + +const TAB_ANIMATION_DURATION = 250; + +function InserterTabs( { + listProps, + onSelect, + rootClientId, + showReusableBlocks, + tabIndex, +} ) { + const tabAnimation = useRef( new Animated.Value( 0 ) ).current; + const lastScrollEvents = useRef( [] ).current; + const [ wrapperWidth, setWrapperWidth ] = useState( 0 ); + + function onScroll( event ) { + lastScrollEvents[ tabIndex ] = event.nativeEvent; + listProps.onScroll( event ); + } + + const onWrapperLayout = useCallback( + ( { nativeEvent } ) => { + setWrapperWidth( nativeEvent.layout.width ); + }, + [ setWrapperWidth ] + ); + + useEffect( () => { + Animated.timing( tabAnimation, { + duration: TAB_ANIMATION_DURATION, + toValue: tabIndex, + useNativeDriver: true, + } ).start(); + + // Notify upstream with the last scroll event of the current tab. + const lastScrollEvent = lastScrollEvents[ tabIndex ]; + if ( lastScrollEvent ) { + listProps.onScroll( { nativeEvent: lastScrollEvent } ); + } + }, [ tabIndex ] ); + + const { tabs, tabKeys } = useMemo( () => { + const filteredTabs = InserterTabs.TABS.filter( + ( { name } ) => showReusableBlocks || name !== 'reusable' + ); + return { + tabs: filteredTabs, + tabKeys: [ ...filteredTabs.keys() ], + }; + }, [ showReusableBlocks ] ); + + const translateX = useMemo( + () => + tabKeys.length > 1 + ? tabAnimation.interpolate( { + inputRange: tabKeys, + outputRange: tabKeys.map( + ( key ) => key * -wrapperWidth + ), + } ) + : tabAnimation, + [ tabAnimation, tabKeys, wrapperWidth ] + ); + + const containerStyle = [ + styles[ 'inserter-tabs__container' ], + { + width: wrapperWidth * tabKeys.length, + transform: [ { translateX } ], + }, + ]; + + return ( + + + { tabs.map( ( tab, index ) => ( + + { tab.component( { + rootClientId, + onSelect, + listProps: { ...listProps, onScroll }, + } ) } + + ) ) } + + + ); +} + +function TabsControl( { onChangeTab, showReusableBlocks } ) { + const segments = useMemo( () => { + const filteredTabs = InserterTabs.TABS.filter( + ( { name } ) => showReusableBlocks || name !== 'reusable' + ); + return filteredTabs.map( ( { title } ) => title ); + }, [ showReusableBlocks ] ); + + const segmentHandler = useCallback( + ( selectedTab ) => { + const tabTitles = InserterTabs.TABS.map( ( { title } ) => title ); + onChangeTab( tabTitles.indexOf( selectedTab ) ); + }, + [ onChangeTab ] + ); + + return segments.length > 1 ? ( + + ) : null; +} + +InserterTabs.Control = TabsControl; + +InserterTabs.TABS = [ + { name: 'blocks', title: __( 'Blocks' ), component: BlockTypesTab }, + { name: 'reusable', title: __( 'Reusable' ), component: ReusableBlocksTab }, +]; + +export default InserterTabs; diff --git a/packages/block-editor/src/components/inserter/test/block-types-tab.native.js b/packages/block-editor/src/components/inserter/test/block-types-tab.native.js new file mode 100644 index 00000000000000..311bcba757d255 --- /dev/null +++ b/packages/block-editor/src/components/inserter/test/block-types-tab.native.js @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import items from './fixtures'; +import BlockTypesTab from '../block-types-tab'; +import BlockTypesList from '../../block-types-list'; + +jest.mock( '../../block-types-list' ); +jest.mock( '../hooks/use-clipboard-block' ); +jest.mock( '@wordpress/data/src/components/use-select' ); + +const selectMock = { + getInserterItems: jest.fn().mockReturnValue( [] ), + canInsertBlockType: jest.fn(), + getBlockType: jest.fn(), + getClipboard: jest.fn(), +}; + +describe( 'BlockTypesTab component', () => { + beforeEach( () => { + useSelect.mockImplementation( ( callback ) => + callback( () => selectMock ) + ); + } ); + + it( 'renders without crashing', () => { + const component = shallow( + + ); + expect( component ).toBeTruthy(); + } ); + + it( 'shows block items', () => { + selectMock.getInserterItems.mockReturnValue( items ); + + const blockItems = items.filter( + ( { category } ) => category !== 'reusable' + ); + const component = shallow( + + ); + expect( component.find( BlockTypesList ).prop( 'items' ) ).toEqual( + blockItems + ); + } ); +} ); diff --git a/packages/block-editor/src/components/inserter/test/reusable-blocks-tab.native.js b/packages/block-editor/src/components/inserter/test/reusable-blocks-tab.native.js new file mode 100644 index 00000000000000..f203e9eac1ffb7 --- /dev/null +++ b/packages/block-editor/src/components/inserter/test/reusable-blocks-tab.native.js @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import items from './fixtures'; +import ReusableBlocksTab from '../reusable-blocks-tab'; +import BlockTypesList from '../../block-types-list'; + +jest.mock( '../../block-types-list' ); +jest.mock( '@wordpress/data/src/components/use-select' ); + +const fetchReusableBlocks = jest.fn(); +const selectMock = { + getInserterItems: jest.fn().mockReturnValue( [] ), + getSettings: jest.fn().mockReturnValue( { + __experimentalFetchReusableBlocks: fetchReusableBlocks, + } ), +}; + +describe( 'ReusableBlocksTab component', () => { + beforeEach( () => { + useSelect.mockImplementation( ( callback ) => + callback( () => selectMock ) + ); + } ); + + it( 'renders without crashing', () => { + const component = shallow( + + ); + expect( component ).toBeTruthy(); + } ); + + it( 'shows reusable block items', () => { + selectMock.getInserterItems.mockReturnValue( items ); + + const reusableBlockItems = items.filter( + ( { category } ) => category === 'reusable' + ); + const component = shallow( + + ); + expect( component.find( BlockTypesList ).prop( 'items' ) ).toEqual( + reusableBlockItems + ); + } ); +} ); diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 143e92f7354957..2ff204cd832efc 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -86,6 +86,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 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'; export { default as InserterButton } from './mobile/inserter-button'; diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index d13c361f9c8979..38d8087f5a8ce5 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -299,7 +299,7 @@ class BottomSheet extends Component { onScroll( { nativeEvent } ) { if ( this.isCloseToTop( nativeEvent ) ) { this.setState( { bounces: false } ); - } else if ( this.isCloseToBottom( nativeEvent ) ) { + } else { this.setState( { bounces: true } ); } } diff --git a/packages/components/src/mobile/inserter-button/index.native.js b/packages/components/src/mobile/inserter-button/index.native.js index 013266a4b3d3d1..8ca9750b30adc5 100644 --- a/packages/components/src/mobile/inserter-button/index.native.js +++ b/packages/components/src/mobile/inserter-button/index.native.js @@ -88,7 +88,9 @@ class MenuItem extends Component { /> - { blockTitle } + + { blockTitle } + ); diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 412cd30fc0ac3b..0e7fd0209dde3c 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -49,7 +49,7 @@ function useBlockEditorSettings( settings, hasTemplate ) { * Unbounded queries are not supported on native so as a workaround, we set per_page with the maximum value that native version can handle. * Related issue: https://github.com/wordpress-mobile/gutenberg-mobile/issues/2661 */ - { per_page: Platform.select( { web: -1, native: 10 } ) } + { per_page: Platform.select( { web: -1, native: 100 } ) } ), hasUploadPermissions: defaultTo( canUser( 'create', 'media' ), diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 1563796bc1153a..870492b38bb60f 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -14,6 +14,7 @@ For each user feature we should also add a importance categorization label to i - [*] Fixes color picker rendering bug when scrolling [#30994] - [*] Add enableCaching param to fetch request on Android [#31186] - [*] Disabled featured image banner on iOS. [#31681] +- [***] Add reusable blocks to the inserter menu. [#28495] ## 1.52.1