From 87f475bcecbefaa6e0574d783c4517122433e6a9 Mon Sep 17 00:00:00 2001 From: Milind More Date: Thu, 30 Jul 2026 14:26:34 +0530 Subject: [PATCH 1/4] refactor: automate management of carousel navigation and tab blocks when toggling Use as Tabs setting --- src/blocks/carousel/__tests__/edit.test.tsx | 57 ++++++++++-- src/blocks/carousel/edit.tsx | 98 ++++++++++++++++----- 2 files changed, 127 insertions(+), 28 deletions(-) diff --git a/src/blocks/carousel/__tests__/edit.test.tsx b/src/blocks/carousel/__tests__/edit.test.tsx index fc90528..c441061 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' } ), + ); expect( mockInsertBlock ).toHaveBeenCalledWith( expect.objectContaining( { name: 'rt-carousel/carousel-tab-list' } ), 0, @@ -436,18 +438,52 @@ describe( 'useTabs toggle', () => { toggleCall[ 0 ].onChange( true ); - expect( setAttributes ).toHaveBeenCalledWith( expect.objectContaining( { useTabs: true } ) ); + expect( setAttributes ).toHaveBeenCalledWith( + expect.objectContaining( { useTabs: true, transition: 'slide' } ), + ); 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: [] }, ], }, ]; @@ -455,7 +491,7 @@ describe( 'useTabs toggle', () => { render( , @@ -465,9 +501,14 @@ describe( 'useTabs toggle', () => { ( [ props ] ) => props.label === 'Use as Tabs', ); - toggleCall[ 0 ].onChange( false ); + toggleCall[ 0 ].onChange( true ); - expect( setAttributes ).toHaveBeenCalledWith( { useTabs: false } ); - expect( mockRemoveBlocks ).toHaveBeenCalledWith( [ 'top-tab-list', 'nested-tab-list' ] ); + expect( setAttributes ).toHaveBeenCalledWith( expect.objectContaining( { useTabs: true } ) ); + expect( mockRemoveBlocks ).toHaveBeenCalledWith( [ + 'group-1', + 'nested-controls', + 'nested-counter', + 'nested-dots', + ] ); } ); } ); diff --git a/src/blocks/carousel/edit.tsx b/src/blocks/carousel/edit.tsx index 08a2cd6..d675284 100644 --- a/src/blocks/carousel/edit.tsx +++ b/src/blocks/carousel/edit.tsx @@ -342,6 +342,7 @@ export default function Edit( { if ( value ) { setAttributes( { useTabs: true, + transition: 'slide', loop: false, dragFree: false, carouselAlign: 'start', @@ -359,6 +360,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 +390,40 @@ 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' ); + const targetParentId = navGroup ? navGroup.clientId : clientId; + + if ( ! controlsExist ) { + insertBlock( + createBlock( 'rt-carousel/carousel-controls', {} ), + 0, + targetParentId, + ); + } + if ( ! counterExist ) { + insertBlock( + createBlock( 'rt-carousel/carousel-counter', {} ), + 1, + targetParentId, + ); + } + if ( ! dotsExist ) { + insertBlock( + createBlock( 'rt-carousel/carousel-dots', {} ), + 2, + targetParentId, + ); + } + } } }; @@ -373,26 +431,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', + ) + } + /> Date: Thu, 30 Jul 2026 14:28:01 +0530 Subject: [PATCH 2/4] fix: add null coalescing operators to innerBlocks in block filter logic --- src/blocks/carousel/edit.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/blocks/carousel/edit.tsx b/src/blocks/carousel/edit.tsx index d675284..a7297c0 100644 --- a/src/blocks/carousel/edit.tsx +++ b/src/blocks/carousel/edit.tsx @@ -364,9 +364,9 @@ export default function Edit( { 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' ) ), + ( 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' ), From 5dac541c8838e40cf3fe19480ccc5dc381ffca5d Mon Sep 17 00:00:00 2001 From: Milind More Date: Thu, 30 Jul 2026 15:08:43 +0530 Subject: [PATCH 3/4] fix: disable autoScroll when using tabs and improve navigation block restoration logic in edit component --- src/blocks/carousel/__tests__/edit.test.tsx | 43 ++++++++++++++++++++- src/blocks/carousel/edit.tsx | 9 ++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/blocks/carousel/__tests__/edit.test.tsx b/src/blocks/carousel/__tests__/edit.test.tsx index c441061..46dfbb1 100644 --- a/src/blocks/carousel/__tests__/edit.test.tsx +++ b/src/blocks/carousel/__tests__/edit.test.tsx @@ -408,7 +408,7 @@ describe( 'useTabs toggle', () => { toggleCall[ 0 ].onChange( true ); expect( setAttributes ).toHaveBeenCalledWith( - expect.objectContaining( { useTabs: true, transition: 'slide' } ), + expect.objectContaining( { useTabs: true, transition: 'slide', autoScroll: false } ), ); expect( mockInsertBlock ).toHaveBeenCalledWith( expect.objectContaining( { name: 'rt-carousel/carousel-tab-list' } ), @@ -439,7 +439,7 @@ describe( 'useTabs toggle', () => { toggleCall[ 0 ].onChange( true ); expect( setAttributes ).toHaveBeenCalledWith( - expect.objectContaining( { useTabs: true, transition: 'slide' } ), + expect.objectContaining( { useTabs: true, transition: 'slide', autoScroll: false } ), ); expect( mockInsertBlock ).not.toHaveBeenCalled(); } ); @@ -511,4 +511,43 @@ describe( 'useTabs toggle', () => { '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: [] }, + ], + }, + ]; + 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( mockInsertBlock ).toHaveBeenCalledWith( + expect.objectContaining( { name: 'rt-carousel/carousel-controls' } ), + 0, + 'nav-group-1', + ); + expect( mockInsertBlock ).toHaveBeenCalledWith( + expect.objectContaining( { name: 'rt-carousel/carousel-counter' } ), + 1, + 'nav-group-1', + ); + } ); } ); diff --git a/src/blocks/carousel/edit.tsx b/src/blocks/carousel/edit.tsx index a7297c0..7dd9c84 100644 --- a/src/blocks/carousel/edit.tsx +++ b/src/blocks/carousel/edit.tsx @@ -349,6 +349,7 @@ export default function Edit( { containScroll: 'trimSnaps', slidesToScroll: '1', autoplay: false, + autoScroll: false, axis: 'x', } ); const tabListExists = findBlockDeep( innerBlocks, 'rt-carousel/carousel-tab-list' ); @@ -399,7 +400,13 @@ export default function Edit( { // 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' ); + 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 ) { From 050a1e20fb25eeaf15cb282aa48f1a42f1c09f8e Mon Sep 17 00:00:00 2001 From: Milind More Date: Thu, 30 Jul 2026 15:13:33 +0530 Subject: [PATCH 4/4] refactor: update nav block insertion logic to append at default index and verify existing blocks during tab toggling --- src/blocks/carousel/__tests__/edit.test.tsx | 66 ++++++++++++++++++--- src/blocks/carousel/edit.tsx | 6 +- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/blocks/carousel/__tests__/edit.test.tsx b/src/blocks/carousel/__tests__/edit.test.tsx index 46dfbb1..80ebf80 100644 --- a/src/blocks/carousel/__tests__/edit.test.tsx +++ b/src/blocks/carousel/__tests__/edit.test.tsx @@ -504,12 +504,16 @@ describe( 'useTabs toggle', () => { toggleCall[ 0 ].onChange( true ); expect( setAttributes ).toHaveBeenCalledWith( expect.objectContaining( { useTabs: true } ) ); - expect( mockRemoveBlocks ).toHaveBeenCalledWith( [ - 'group-1', - 'nested-controls', - 'nested-counter', - 'nested-dots', - ] ); + 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', () => { @@ -541,12 +545,58 @@ describe( 'useTabs toggle', () => { expect( mockInsertBlock ).toHaveBeenCalledWith( expect.objectContaining( { name: 'rt-carousel/carousel-controls' } ), - 0, + undefined, 'nav-group-1', ); expect( mockInsertBlock ).toHaveBeenCalledWith( expect.objectContaining( { name: 'rt-carousel/carousel-counter' } ), - 1, + 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 7dd9c84..9232051 100644 --- a/src/blocks/carousel/edit.tsx +++ b/src/blocks/carousel/edit.tsx @@ -412,21 +412,21 @@ export default function Edit( { if ( ! controlsExist ) { insertBlock( createBlock( 'rt-carousel/carousel-controls', {} ), - 0, + undefined, targetParentId, ); } if ( ! counterExist ) { insertBlock( createBlock( 'rt-carousel/carousel-counter', {} ), - 1, + undefined, targetParentId, ); } if ( ! dotsExist ) { insertBlock( createBlock( 'rt-carousel/carousel-dots', {} ), - 2, + undefined, targetParentId, ); }