diff --git a/src/blocks/carousel/__tests__/edit.test.tsx b/src/blocks/carousel/__tests__/edit.test.tsx index fc90528..80ebf80 100644 --- a/src/blocks/carousel/__tests__/edit.test.tsx +++ b/src/blocks/carousel/__tests__/edit.test.tsx @@ -407,7 +407,9 @@ describe( 'useTabs toggle', () => { toggleCall[ 0 ].onChange( true ); - expect( setAttributes ).toHaveBeenCalledWith( expect.objectContaining( { useTabs: true } ) ); + expect( setAttributes ).toHaveBeenCalledWith( + expect.objectContaining( { useTabs: true, transition: 'slide', autoScroll: false } ), + ); expect( mockInsertBlock ).toHaveBeenCalledWith( expect.objectContaining( { name: 'rt-carousel/carousel-tab-list' } ), 0, @@ -436,18 +438,92 @@ describe( 'useTabs toggle', () => { toggleCall[ 0 ].onChange( true ); - expect( setAttributes ).toHaveBeenCalledWith( expect.objectContaining( { useTabs: true } ) ); + expect( setAttributes ).toHaveBeenCalledWith( + expect.objectContaining( { useTabs: true, transition: 'slide', autoScroll: false } ), + ); expect( mockInsertBlock ).not.toHaveBeenCalled(); } ); - it( 'removes all tab list blocks (including nested ones) when toggling Use as Tabs OFF', () => { + it( 'removes tab list blocks and re-inserts full nav group container when toggling Use as Tabs OFF and no nav blocks exist', () => { mockBlocks = [ { name: 'rt-carousel/carousel-tab-list', clientId: 'top-tab-list', innerBlocks: [] }, + { name: 'rt-carousel/carousel-viewport', clientId: 'viewport-1', innerBlocks: [] }, + ]; + const setAttributes = jest.fn(); + + render( + , + ); + + const toggleCall = ( ToggleControl as unknown as jest.Mock ).mock.calls.find( + ( [ props ] ) => props.label === 'Use as Tabs', + ); + + toggleCall[ 0 ].onChange( false ); + + expect( setAttributes ).toHaveBeenCalledWith( { useTabs: false } ); + expect( mockRemoveBlocks ).toHaveBeenCalledWith( [ 'top-tab-list' ] ); + expect( mockInsertBlock ).toHaveBeenCalledWith( + expect.objectContaining( { name: 'core/group' } ), + undefined, + 'test-client-id', + ); + } ); + + it( 'removes navigation group row containers and navigation blocks when toggling Use as Tabs ON', () => { + mockBlocks = [ + { name: 'rt-carousel/carousel-viewport', clientId: 'viewport-1', innerBlocks: [] }, { name: 'core/group', clientId: 'group-1', innerBlocks: [ - { name: 'rt-carousel/carousel-tab-list', clientId: 'nested-tab-list', innerBlocks: [] }, + { name: 'rt-carousel/carousel-controls', clientId: 'nested-controls', innerBlocks: [] }, + { name: 'rt-carousel/carousel-counter', clientId: 'nested-counter', innerBlocks: [] }, + { name: 'rt-carousel/carousel-dots', clientId: 'nested-dots', innerBlocks: [] }, + ], + }, + ]; + const setAttributes = jest.fn(); + + render( + , + ); + + const toggleCall = ( ToggleControl as unknown as jest.Mock ).mock.calls.find( + ( [ props ] ) => props.label === 'Use as Tabs', + ); + + toggleCall[ 0 ].onChange( true ); + + expect( setAttributes ).toHaveBeenCalledWith( expect.objectContaining( { useTabs: true } ) ); + const removedIds = mockRemoveBlocks.mock.calls[ 0 ][ 0 ]; + expect( removedIds ).toHaveLength( 4 ); + expect( removedIds ).toEqual( + expect.arrayContaining( [ + 'group-1', + 'nested-controls', + 'nested-counter', + 'nested-dots', + ] ), + ); + } ); + + it( 'restores missing nav blocks into the core/group that actually contains nav elements, ignoring unrelated groups', () => { + mockBlocks = [ + { name: 'core/group', clientId: 'unrelated-group', innerBlocks: [] }, + { + name: 'core/group', + clientId: 'nav-group-1', + innerBlocks: [ + { name: 'rt-carousel/carousel-dots', clientId: 'existing-dots', innerBlocks: [] }, ], }, ]; @@ -467,7 +543,61 @@ describe( 'useTabs toggle', () => { toggleCall[ 0 ].onChange( false ); - expect( setAttributes ).toHaveBeenCalledWith( { useTabs: false } ); - expect( mockRemoveBlocks ).toHaveBeenCalledWith( [ 'top-tab-list', 'nested-tab-list' ] ); + expect( mockInsertBlock ).toHaveBeenCalledWith( + expect.objectContaining( { name: 'rt-carousel/carousel-controls' } ), + undefined, + 'nav-group-1', + ); + expect( mockInsertBlock ).toHaveBeenCalledWith( + expect.objectContaining( { name: 'rt-carousel/carousel-counter' } ), + undefined, + 'nav-group-1', + ); + } ); + + it( 're-inserts only missing nav blocks (counter and dots) when controls already exist in a nav group', () => { + mockBlocks = [ + { + name: 'core/group', + clientId: 'nav-group-1', + innerBlocks: [ + { name: 'rt-carousel/carousel-controls', clientId: 'existing-controls', innerBlocks: [] }, + ], + }, + ]; + const setAttributes = jest.fn(); + + render( + , + ); + + const toggleCall = ( ToggleControl as unknown as jest.Mock ).mock.calls.find( + ( [ props ] ) => props.label === 'Use as Tabs', + ); + + toggleCall[ 0 ].onChange( false ); + + // Controls already exist — should NOT be inserted again + expect( mockInsertBlock ).not.toHaveBeenCalledWith( + expect.objectContaining( { name: 'rt-carousel/carousel-controls' } ), + expect.anything(), + expect.anything(), + ); + + // Missing counter and dots SHOULD be inserted into nav-group-1 + expect( mockInsertBlock ).toHaveBeenCalledWith( + expect.objectContaining( { name: 'rt-carousel/carousel-counter' } ), + undefined, + 'nav-group-1', + ); + expect( mockInsertBlock ).toHaveBeenCalledWith( + expect.objectContaining( { name: 'rt-carousel/carousel-dots' } ), + undefined, + 'nav-group-1', + ); } ); } ); diff --git a/src/blocks/carousel/edit.tsx b/src/blocks/carousel/edit.tsx index 08a2cd6..9232051 100644 --- a/src/blocks/carousel/edit.tsx +++ b/src/blocks/carousel/edit.tsx @@ -342,12 +342,14 @@ export default function Edit( { if ( value ) { setAttributes( { useTabs: true, + transition: 'slide', loop: false, dragFree: false, carouselAlign: 'start', containScroll: 'trimSnaps', slidesToScroll: '1', autoplay: false, + autoScroll: false, axis: 'x', } ); const tabListExists = findBlockDeep( innerBlocks, 'rt-carousel/carousel-tab-list' ); @@ -359,6 +361,29 @@ export default function Edit( { clientId, ); } + // Find nav row containers (core/group containing navigation blocks) as well as any standalone nav blocks + const navGroupBlocks = innerBlocks.filter( + ( b ) => + b.name === 'core/group' && + ( findBlockDeep( b.innerBlocks ?? [], 'rt-carousel/carousel-controls' ) || + findBlockDeep( b.innerBlocks ?? [], 'rt-carousel/carousel-counter' ) || + findBlockDeep( b.innerBlocks ?? [], 'rt-carousel/carousel-dots' ) ), + ); + const looseNavBlocks = [ + ...findAllBlocksDeep( innerBlocks, 'rt-carousel/carousel-controls' ), + ...findAllBlocksDeep( innerBlocks, 'rt-carousel/carousel-counter' ), + ...findAllBlocksDeep( innerBlocks, 'rt-carousel/carousel-dots' ), + ]; + + const idsToRemove = Array.from( + new Set( [ + ...navGroupBlocks.map( ( b ) => b.clientId ), + ...looseNavBlocks.map( ( b ) => b.clientId ), + ] ), + ); + if ( idsToRemove.length > 0 ) { + removeBlocks( idsToRemove ); + } } else { setAttributes( { useTabs: false } ); const tabListBlocks = findAllBlocksDeep( innerBlocks, 'rt-carousel/carousel-tab-list' ); @@ -366,6 +391,46 @@ export default function Edit( { if ( tabListIds.length > 0 ) { removeBlocks( tabListIds ); } + + const controlsExist = findBlockDeep( innerBlocks, 'rt-carousel/carousel-controls' ); + const counterExist = findBlockDeep( innerBlocks, 'rt-carousel/carousel-counter' ); + const dotsExist = findBlockDeep( innerBlocks, 'rt-carousel/carousel-dots' ); + + if ( ! controlsExist && ! counterExist && ! dotsExist ) { + // No navigation blocks exist — insert full nav group row container + insertBlock( createNavGroup(), undefined, clientId ); + } else if ( ! controlsExist || ! counterExist || ! dotsExist ) { + const navGroup = innerBlocks.find( + ( b ) => + b.name === 'core/group' && + ( findBlockDeep( b.innerBlocks ?? [], 'rt-carousel/carousel-controls' ) || + findBlockDeep( b.innerBlocks ?? [], 'rt-carousel/carousel-counter' ) || + findBlockDeep( b.innerBlocks ?? [], 'rt-carousel/carousel-dots' ) ), + ); + const targetParentId = navGroup ? navGroup.clientId : clientId; + + if ( ! controlsExist ) { + insertBlock( + createBlock( 'rt-carousel/carousel-controls', {} ), + undefined, + targetParentId, + ); + } + if ( ! counterExist ) { + insertBlock( + createBlock( 'rt-carousel/carousel-counter', {} ), + undefined, + targetParentId, + ); + } + if ( ! dotsExist ) { + insertBlock( + createBlock( 'rt-carousel/carousel-dots', {} ), + undefined, + targetParentId, + ); + } + } } }; @@ -373,26 +438,6 @@ export default function Edit( { <> - - setAttributes( { transition: value as CarouselAttributes[ 'transition' ] } ) - } - help={ - autoScroll - ? __( 'Auto Scroll does not support transitions.', 'rt-carousel' ) - : __( - 'Choose how slides transition: sliding horizontally or cross-fading.', - 'rt-carousel', - ) - } - /> { ! useTabs && ( <> + + setAttributes( { transition: value as CarouselAttributes[ 'transition' ] } ) + } + help={ + autoScroll + ? __( 'Auto Scroll does not support transitions.', 'rt-carousel' ) + : __( + 'Choose how slides transition: sliding horizontally or cross-fading.', + 'rt-carousel', + ) + } + />