Skip to content

Commit b0cf807

Browse files
theMasudRanaCopilot
andauthored
feat: Terms Query support for the carousel block (#131)
* feat: add Terms Query carousel block and update dynamic list selectors Co-authored-by: Copilot <copilot@github.com> * docs: update README and usage guide to include support for Terms Query block * refactor: remove unused style imports and enhance containScroll handling in carousel edit component * feat: Enhance documentation and add dynamic content insertion options for carousel viewport --------- Co-authored-by: Copilot <copilot@github.com>
1 parent dd90468 commit b0cf807

16 files changed

Lines changed: 526 additions & 100 deletions

File tree

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Easily create dynamic, accessible, and customizable carousels for any content ty
1212
- **Flexible Compound Block Architecture**: Mix and match any blocks inside the carousel.
1313
- **High Performance**: Viewport & Slide Engine powered by Embla Carousel.
1414
- **Interactivity API**: Reactive state management with `data-wp-interactive`.
15-
- **Dynamic Content**: Full support for WordPress **Query Loop** block.
15+
- **Dynamic Content**: Full support for WordPress **Query Loop** and **Terms Query** blocks.
1616
- **Accessibility**: W3C-compliant roles, labels, and keyboard navigation.
1717
- **RTL Support**: Built-in support for Right-to-Left languages.
1818

@@ -68,9 +68,15 @@ Yes! rtCarousel is fully compatible with Full Site Editing. You can use the caro
6868

6969
Absolutely. Each slide is a container that accepts any WordPress block—images, paragraphs, groups, columns, and even other third-party blocks.
7070

71-
### Does it support the Query Loop block?
71+
### How do I add content to an empty Carousel Viewport?
7272

73-
Yes. Simply add a Query Loop block inside the Carousel Viewport, and each post in the loop becomes a slide automatically. No special configuration needed.
73+
Use **Add Slide** for static/manual slides, **Add Query Loop** for dynamic post slides, or **Add Terms Query** for dynamic taxonomy term slides.
74+
75+
### Does it support the Query Loop and Terms Query blocks?
76+
77+
Yes. Add a Query Loop or Terms Query block inside the Carousel Viewport, and each post or term becomes a slide automatically. You can also start from the bundled Query Loop Carousel or Terms Query Carousel patterns.
78+
79+
Do not place Query Loop or Terms Query inside a Carousel Slide block. Their generated posts or terms are used as the carousel slides automatically; Carousel Slide is intended for static or manually created slide content.
7480

7581
### Is it accessible?
7682

docs/USAGE.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ To create a specialized carousel (e.g., testimonials only), set the `allowedSlid
4444

4545
---
4646

47+
## Adding Content to an Empty Viewport
48+
49+
When the **Carousel Viewport** is empty, rtCarousel shows three insertion actions:
50+
51+
| Action | Use When |
52+
| :--- | :--- |
53+
| **Add Slide** | You want static or manually created slide content. |
54+
| **Add Query Loop** | You want dynamic post, page, product, or custom post type slides. |
55+
| **Add Terms Query** | You want dynamic category, tag, or custom taxonomy term slides. |
56+
57+
Use **Add Query Loop** or **Add Terms Query** for dynamic content so the generated posts or terms become the carousel slides automatically. Use **Add Slide** only for static/manual slides.
58+
59+
---
60+
4761
## Using Query Loop with Carousel
4862

4963
You can create dynamic post sliders or content carousels using the WordPress Query Loop block.
@@ -55,12 +69,45 @@ You can create dynamic post sliders or content carousels using the WordPress Que
5569
4. Configure your query (post type, category, order, etc). **Disable** "Inherit query from template" if using on single posts/pages to avoid loop conflicts.
5670
5. Design your slide inside the Query Loop's **Post Template**.
5771

58-
**Note:** Each post generated by the Query Loop becomes an individual slide. The system automatically detects `.wp-block-post-template` and forces horizontal flex row display. The `slideGap` attribute controls spacing.
72+
**Important:** Do not wrap Query Loop in a Carousel Slide block. Each post generated by the Query Loop becomes an individual carousel slide, so the Query Loop block must be placed directly inside the Carousel Viewport. The Carousel Slide block is for static or manually created slides.
73+
74+
**Note:** The system automatically detects `.wp-block-post-template` and forces horizontal flex row display. The `slideGap` attribute controls spacing.
75+
76+
You can also start from the bundled **rtCarousel: Query Loop Carousel** pattern.
77+
78+
---
79+
80+
## Using Terms Query with Carousel
81+
82+
You can create dynamic taxonomy sliders using the WordPress Terms Query block (`core/terms-query`). This supports categories, tags, and custom taxonomies.
83+
84+
### Setup Steps
85+
1. Add the **Carousel** block to your page.
86+
2. Select the inner **Carousel Viewport** block.
87+
3. Insert a **Terms Query** block directly inside the Viewport, not inside a Carousel Slide.
88+
4. Configure the taxonomy, order, visibility, and included terms in the Terms Query settings.
89+
5. Design your slide inside the Terms Query's **Term Template**.
90+
91+
**Important:** Do not wrap Terms Query in a Carousel Slide block. Each term generated by the Terms Query becomes an individual carousel slide, so the Terms Query block must be placed directly inside the Carousel Viewport. The Carousel Slide block is for static or manually created slides.
92+
93+
**Note:** The system automatically detects `.wp-block-term-template` and applies the same carousel layout behavior used for Query Loop. The `slideGap` attribute controls spacing.
94+
95+
You can also start from the bundled **rtCarousel: Terms Query Carousel** pattern.
5996

6097
### Block Selection Guide
6198

6299
| Use Case | Recommended Block |
63100
| :--- | :--- |
64101
| Dynamic Content (Posts, Pages, Products, Custom Post Types) | Query Loop (`core/query`) |
102+
| Dynamic Taxonomy Content (Categories, Tags, Custom Taxonomies) | Terms Query (`core/terms-query`) |
65103
| Static Content (Hero Slider, Logo Showcase, Manual Testimonials) | Carousel Slide (`rt-carousel/carousel-slide`) |
66104
| Mixed Content (Slide 1 is a Video, Slide 2 is Text) | Carousel Slide (`rt-carousel/carousel-slide`) |
105+
106+
### Terms Query Selection Guide
107+
108+
| Taxonomy Use Case | Recommended Setup |
109+
| :--- | :--- |
110+
| All categories | Terms Query (`core/terms-query`) with `taxonomy: category` |
111+
| All tags | Terms Query (`core/terms-query`) with `taxonomy: post_tag` |
112+
| Custom taxonomy terms | Terms Query (`core/terms-query`) with your taxonomy slug |
113+
| Hand-picked static term cards | Carousel Slide (`rt-carousel/carousel-slide`) |

examples/patterns/terms-query.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
/**
3+
* Title: rtCarousel: Terms Query Carousel
4+
* Slug: rt-carousel/terms-query-carousel
5+
* Categories: rt-carousel
6+
* Description: A carousel block containing a Terms Query displaying taxonomy terms (categories, tags, custom taxonomies) as slides.
7+
*/
8+
9+
// Exit if accessed directly.
10+
if ( ! defined( 'ABSPATH' ) ) {
11+
exit;
12+
}
13+
?>
14+
15+
<!-- wp:rt-carousel/carousel {"ariaLabel":"rtCarousel: Terms Query Carousel","slideGap":16,"metadata":{"name":"rtCarousel: Terms Query Carousel","categories":["rt-carousel"],"patternName":"rt-carousel/terms-query-carousel"},"align":"wide","className":"wp-block-carousel-carousel is-style-columns-3"} -->
16+
<div class="wp-block-rt-carousel-carousel alignwide rt-carousel wp-block-carousel-carousel is-style-columns-3" role="region" aria-roledescription="carousel" aria-label="rtCarousel: Terms Query Carousel" dir="ltr" data-axis="x" data-wp-interactive="rt-carousel/carousel" data-wp-context="{&quot;options&quot;:{&quot;loop&quot;:false,&quot;dragFree&quot;:false,&quot;align&quot;:&quot;start&quot;,&quot;containScroll&quot;:&quot;trimSnaps&quot;,&quot;direction&quot;:&quot;ltr&quot;,&quot;axis&quot;:&quot;x&quot;,&quot;slidesToScroll&quot;:1},&quot;autoplay&quot;:false,&quot;isPlaying&quot;:false,&quot;timerIterationId&quot;:0,&quot;selectedIndex&quot;:-1,&quot;scrollSnaps&quot;:[],&quot;canScrollPrev&quot;:false,&quot;canScrollNext&quot;:false,&quot;scrollProgress&quot;:0,&quot;slideCount&quot;:0,&quot;ariaLabelPattern&quot;:&quot;Go to slide %d&quot;}" data-wp-init="callbacks.initCarousel" style="--rt-carousel-gap:16px"><!-- wp:rt-carousel/carousel-viewport {"className":"wp-block-carousel-carousel-viewport"} -->
17+
<div class="wp-block-rt-carousel-carousel-viewport embla wp-block-carousel-carousel-viewport">
18+
<div class="embla__container"><!-- wp:terms-query {"termQuery":{"perPage":10,"taxonomy":"category","order":"asc","orderBy":"name","include":[],"hideEmpty":false,"showNested":false,"inherit":false}} -->
19+
<div class="wp-block-terms-query"><!-- wp:term-template -->
20+
<!-- wp:group {"className":"is-style-section-1","style":{"spacing":{"padding":{"top":"30px","right":"30px","bottom":"30px","left":"30px"}},"border":{"radius":{"topLeft":"10px","topRight":"10px","bottomLeft":"10px","bottomRight":"10px"}},"color":{"background":"#f6f6f6"}},"layout":{"inherit":false}} -->
21+
<div class="wp-block-group is-style-section-1 has-background" style="border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-left-radius:10px;border-bottom-right-radius:10px;background-color:#f6f6f6;padding-top:30px;padding-right:30px;padding-bottom:30px;padding-left:30px"><!-- wp:term-name {"isLink":true,"fontSize":"x-large"} /-->
22+
23+
<!-- wp:term-description /-->
24+
25+
<!-- wp:term-count /-->
26+
</div>
27+
<!-- /wp:group -->
28+
<!-- /wp:term-template -->
29+
</div>
30+
<!-- /wp:terms-query -->
31+
</div>
32+
</div>
33+
<!-- /wp:rt-carousel/carousel-viewport -->
34+
35+
<!-- wp:group {"style":{"spacing":{"margin":{"top":"var:preset|spacing|30","bottom":"0"}}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
36+
<div class="wp-block-group" style="margin-top:var(--wp--preset--spacing--30);margin-bottom:0"><!-- wp:rt-carousel/carousel-controls {"className":"wp-block-carousel-carousel-controls"} -->
37+
<div class="wp-block-rt-carousel-carousel-controls rt-carousel-controls wp-block-carousel-carousel-controls"><button type="button" class="rt-carousel-controls__btn rt-carousel-controls__btn--prev" data-wp-on--click="actions.scrollPrev" data-wp-bind--disabled="!state.canScrollPrev" aria-label="Previous Slide"><svg class="rt-carousel-controls__icon" width="13" height="8" viewBox="0 0 13 8" fill="none" xmlns="http://www.w3.org/2000/svg">
38+
<path d="M0 3.55371L3.55371 7.10742V4.26562H12.7861V2.84375H3.55371V0L0 3.55371Z" fill="#1C1C1C"></path>
39+
</svg></button><button type="button" class="rt-carousel-controls__btn rt-carousel-controls__btn--next" data-wp-on--click="actions.scrollNext" data-wp-bind--disabled="!state.canScrollNext" aria-label="Next Slide"><svg class="rt-carousel-controls__icon" width="13" height="8" viewBox="0 0 13 8" fill="none" xmlns="http://www.w3.org/2000/svg">
40+
<path d="M12.7861 3.55371L9.23242 7.10742V4.26562H0V2.84375H9.23242V0L12.7861 3.55371Z" fill="#1C1C1C"></path>
41+
</svg></button></div>
42+
<!-- /wp:rt-carousel/carousel-controls -->
43+
</div>
44+
<!-- /wp:group -->
45+
</div>
46+
<!-- /wp:rt-carousel/carousel -->

src/blocks/carousel/__tests__/edit.test.tsx

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* @package
55
*/
66

7+
import '@testing-library/jest-dom';
8+
import type { ReactNode } from 'react';
79
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
810
import Edit from '../edit';
911
import type { CarouselAttributes } from '../types';
@@ -19,15 +21,22 @@ jest.mock( '@wordpress/block-editor', () => ( {
1921
} ) );
2022

2123
jest.mock( '@wordpress/components', () => {
22-
const React = jest.requireActual( 'react' );
23-
24-
const Button = ( { children, onClick, className, ...rest }: any ) => (
24+
const Button = ( {
25+
children,
26+
onClick,
27+
className,
28+
...rest
29+
}: {
30+
children?: ReactNode;
31+
onClick?: () => void;
32+
className?: string;
33+
} ) => (
2534
<button type="button" className={ className } onClick={ onClick } { ...rest }>
2635
{ children }
2736
</button>
2837
);
2938

30-
const Passthrough = ( { children }: any ) => <>{ children }</>;
39+
const Passthrough = ( { children }: { children?: ReactNode } ) => <>{ children }</>;
3140

3241
return {
3342
PanelBody: Passthrough,
@@ -37,7 +46,15 @@ jest.mock( '@wordpress/components', () => {
3746
BaseControl: Passthrough,
3847
TextControl: jest.fn( () => null ),
3948
RangeControl: jest.fn( () => null ),
40-
Placeholder: ( { children, instructions, className }: any ) => (
49+
Placeholder: ( {
50+
children,
51+
instructions,
52+
className,
53+
}: {
54+
children?: ReactNode;
55+
instructions?: string;
56+
className?: string;
57+
} ) => (
4158
<div className={ className }>
4259
<p>{ instructions }</p>
4360
{ children }
@@ -53,7 +70,7 @@ jest.mock( '@wordpress/data', () => ( {
5370
replaceInnerBlocks: jest.fn(),
5471
insertBlock: jest.fn(),
5572
} ) ),
56-
useSelect: jest.fn( ( selector: any ) =>
73+
useSelect: jest.fn( ( selector: ( select: ( storeName: string ) => unknown ) => unknown ) =>
5774
selector( ( storeName: string ) => {
5875
if ( storeName === 'core/block-editor' ) {
5976
return {

src/blocks/carousel/__tests__/view.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,26 @@ describe( 'Carousel View Module', () => {
436436

437437
expect( result ).toBe( true );
438438
} );
439+
440+
it( 'should work with Terms Query terms (.wp-block-term)', () => {
441+
const container = document.createElement( 'div' );
442+
443+
const term1 = document.createElement( 'li' );
444+
term1.className = 'wp-block-term';
445+
const term2 = document.createElement( 'li' );
446+
term2.className = 'wp-block-term';
447+
448+
container.appendChild( term1 );
449+
container.appendChild( term2 );
450+
451+
const mockContext = createMockContext( { selectedIndex: 1, initialized: true } );
452+
( getContext as jest.Mock ).mockReturnValue( mockContext );
453+
( getElement as jest.Mock ).mockReturnValue( { ref: term2 } );
454+
455+
const result = storeConfig.callbacks.isSlideActive();
456+
457+
expect( result ).toBe( true );
458+
} );
439459
} );
440460

441461
describe( 'isDotActive', () => {

0 commit comments

Comments
 (0)