Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,210 @@ describe('c-common-carousel', () => {
expect(global.__ioObserved.length).toBe(0);
});

/**
* Test suite for showMoreProducts functionality
*/
describe('Show More Products Functionality', () => {
beforeEach(() => {
element.productData = mockProductCards;
});

it('does not render show more button when showMoreProducts is false', async () => {
element.showMoreProducts = false;
await Promise.resolve();

const showMoreButton = element.querySelector('.show-more-products');
expect(showMoreButton).toBeNull();
});

it('renders show more button with correct content and attributes', async () => {
element.showMoreProducts = true;
await Promise.resolve();

const showMoreButton = element.querySelector('.show-more-products');
expect(showMoreButton).toBeTruthy();

// Check aria-label
expect(showMoreButton.getAttribute('aria-label')).toBeTruthy();

// Check name attribute
expect(showMoreButton.getAttribute('name')).toBeTruthy();

// Check icon is present (lightning-icon attributes may not be accessible in tests)
const icon = showMoreButton.querySelector('lightning-icon');
expect(icon).toBeTruthy();
// Note: icon-name attribute may not be accessible in Jest tests

// Check text content
const textSpan = showMoreButton.querySelector('.show-more-text');
expect(textSpan).toBeTruthy();
expect(textSpan.textContent).toBeTruthy();
});

it('dispatches showmoreclicked event when show more button is clicked', async () => {
const mockShowMoreProducts = jest.fn();
element.addEventListener('showmoreproducts', mockShowMoreProducts);

element.showMoreProducts = true;
await Promise.resolve();

const showMoreButton = element.querySelector('.show-more-products');
expect(showMoreButton).toBeTruthy();

showMoreButton.click();
await Promise.resolve();

expect(mockShowMoreProducts).toHaveBeenCalledWith(
expect.objectContaining({
detail: expect.objectContaining({
showMoreProducts: true,
productIds: ['1', '2', '3'],
}),
})
);
});

it('renders additional dot for show more button when showMoreProducts is true', async () => {
element.showMoreProducts = true;
await Promise.resolve();

const dots = element.querySelectorAll('.indicator-dot');
const productCards = element.querySelectorAll('.product-card:not(.show-more-products)');

// Should have one additional dot for the show more button
expect(dots.length).toBe(productCards.length + 1);
});

it('does not render additional dot when showMoreProducts is false', async () => {
element.showMoreProducts = false;
await Promise.resolve();

const dots = element.querySelectorAll('.indicator-dot');
const productCards = element.querySelectorAll('.product-card:not(.show-more-products)');

// Should have same number of dots as product cards
expect(dots.length).toBe(productCards.length);
});

it('handles navigation correctly with show more button present', async () => {
element.showMoreProducts = true;
await Promise.resolve();

const rightButton = element.querySelector('.carousel-nav-right');
const totalItems = mockProductCards.length;

// Navigate to the show more button (last item)
for (let i = 0; i < totalItems; i++) {
rightButton.click();
}
await Promise.resolve();

// Right button should be disabled when on show more button
expect(rightButton.disabled).toBe(true);

// Left button should be enabled
const leftButton = element.querySelector('.carousel-nav-left');
expect(leftButton.disabled).toBe(false);
});

it('updates dots correctly when showMoreProducts changes from false to true', async () => {
// Initially no show more button
element.showMoreProducts = false;
await Promise.resolve();

let dots = element.querySelectorAll('.indicator-dot');
let initialDotCount = dots.length;

// Enable show more button
element.showMoreProducts = true;
await Promise.resolve();

dots = element.querySelectorAll('.indicator-dot');
expect(dots.length).toBe(initialDotCount + 1);
});

it('handles dot click navigation to show more button', async () => {
element.showMoreProducts = true;
await Promise.resolve();

const dots = element.querySelectorAll('.indicator-dot');
const showMoreDotIndex = dots.length - 1; // Last dot should be for show more
const showMoreDot = dots[showMoreDotIndex];

// Click the show more dot
showMoreDot.click();
await Promise.resolve();

// Verify the show more dot is active
expect(showMoreDot.getAttribute('data-active')).toBe('true');

// Verify scrollIntoView was called
expect(Element.prototype.scrollIntoView).toHaveBeenCalledWith({
behavior: 'smooth',
block: 'nearest',
inline: 'start',
});
});

it('maintains correct active dot state when navigating with show more button', async () => {
element.showMoreProducts = true;
await Promise.resolve();

const dots = element.querySelectorAll('.indicator-dot');
const rightButton = element.querySelector('.carousel-nav-right');

// Navigate through all items including show more
for (let i = 0; i < dots.length; i++) {
rightButton.click();
}
await Promise.resolve();

// Check that the correct dot is active
const activeDot = element.querySelector('.indicator-dot[data-active="true"]');
expect(activeDot).toBeTruthy();
});

it.each([null, undefined, '', 0, false])(
'handles showMoreProducts setter with falsy value %p correctly',
async (falsyValue) => {
element.showMoreProducts = falsyValue;
await Promise.resolve();
expect(element.showMoreProducts).toBe(false);
}
);

it('handles intersection observer with show more button correctly', async () => {
element.showMoreProducts = true;
await Promise.resolve();

const scrollContainer = element.querySelector('.carousel-scroll-container');
Object.defineProperty(scrollContainer, 'offsetWidth', { value: 100, configurable: true });

// Trigger setup
element.productData = [...mockProductCards];
await Promise.resolve();

// Should observe all product cards including show more button
const allCards = element.querySelectorAll('.product-card');
expect(global.__ioObserved.length).toBe(allCards.length);

// Simulate show more button intersecting
const showMoreButton = element.querySelector('.show-more-products');
global.__ioCallback([
{
target: showMoreButton,
isIntersecting: true,
intersectionRatio: 0.8,
},
]);
await Promise.resolve();

// Verify dots are updated correctly
const activeDot = element.querySelector('.indicator-dot[data-active="true"]');
expect(activeDot).toBeTruthy();
});
});

it('observes image panels and uses image-based counts in IO (image mode branches)', async () => {
element.displayMode = 'productDetailImageCarousel';
const mockImages = productData.imgGroups[0].imgs;
Expand Down
13 changes: 13 additions & 0 deletions force-app/main/default/lwc/commonCarousel/commonCarousel.html
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,19 @@
</div>
</button>
</template>
<!-- Show more products button -->
<button
lwc:if={showMoreProducts}
class="product-card show-more-products"
aria-label={i18n.showMoreProducts}
name={i18n.showMoreProducts}
onclick={handleShowMoreProducts}>
<lightning-icon
icon-name="utility:add"
size="large"
class="show-more-icon"></lightning-icon>
<span class="show-more-text">{i18n.showMoreProducts}</span>
</button>
</div>

<!-- Right navigation arrow -->
Expand Down
61 changes: 59 additions & 2 deletions force-app/main/default/lwc/commonCarousel/commonCarousel.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,35 @@ export default class CommonCarousel extends LightningElement {
nextProduct: Labels.nextProduct(language),
productPrice: Labels.productPrice(language),
outOfStock: Labels.outOfStock(language),
showMoreProducts: Labels.showMoreProducts(language),
};
}

// private property for showMoreProducts flag
_showMoreProducts = false;

/**
* Sets the showMoreProducts property.
* @param {boolean} value - The value to set for showMoreProducts.
*/
@api
set showMoreProducts(value) {
this._showMoreProducts = value || false;
// Initialize dots array when showMoreProducts changes
if (this._showMoreProducts) {
// calling update dots to render the additional dot for the "Show More" card
this.updateDots();
}
}

/**
* Gets the showMoreProducts property. This is a flag to show the "Show More" card.
* @returns {boolean} The value of showMoreProducts.
*/
get showMoreProducts() {
return this.hasProductData && this._showMoreProducts;
}

/**
* Updates the dots array based on the number of images
*/
Expand All @@ -119,10 +145,17 @@ export default class CommonCarousel extends LightningElement {
isActive: index === this.activeImageIndex,
}));
} else if (this.isProductCardsCarousel && this.hasProductData) {
this.dots = this.productData?.map((product, index) => ({
this.dots = this.productData?.map((_, index) => ({
id: `product-${index}`,
isActive: index === this.activeImageIndex,
}));
// Add an additional dot for the "Show More" card if needed
if (this.showMoreProducts) {
this.dots.push({
id: `show-more-card`,
isActive: this.activeImageIndex === this.productData.length,
});
}
}
}

Expand Down Expand Up @@ -206,6 +239,27 @@ export default class CommonCarousel extends LightningElement {
}
}

/**
* Handles the "Show More Products" action.
* It dispatches an event 'showmoreproducts' with the product ids, indicating that the user wants to see more products.
* @param {CustomEvent} event - The click event from the show more button.
*/
handleShowMoreProducts(event) {
event.stopPropagation();
const showMoreProducts = true;
const productIds = this.productData.map((product) => product.id);
this.dispatchEvent(
new CustomEvent('showmoreproducts', {
detail: {
showMoreProducts,
productIds,
},
bubbles: true,
composed: true,
})
);
}

/**
* Handles click on indicator dots to navigate to specific image
* @param {Event} event - Click event from indicator dot
Expand Down Expand Up @@ -253,6 +307,9 @@ export default class CommonCarousel extends LightningElement {
*/
get isLastImage() {
const total = this.isImageCarousel ? this.filteredProductImageLinks.length : this.productData.length;
if (this.showMoreProducts) {
return this.activeImageIndex === total;
}
return this.activeImageIndex === total - 1;
}

Expand Down Expand Up @@ -288,7 +345,7 @@ export default class CommonCarousel extends LightningElement {
*/
handleNextImage() {
const total = this.isImageCarousel ? this.filteredProductImageLinks.length : this.productData.length;
if (this.activeImageIndex < total - 1) {
if (this.activeImageIndex < (this.showMoreProducts ? total : total - 1)) {
this.activeImageIndex++;
this.updateDots();
this.scrollToIndex(this.activeImageIndex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,25 @@
line-height: 1.25rem;
}

/* ===== SHOW MORE PRODUCTS ===== */
.show-more-products {
justify-content: center;
align-items: center;
gap: 1rem;
}

.show-more-icon {
--sds-c-icon-color-foreground-default: #1c1c1c;
}

.show-more-text {
font-size: 1rem;
font-weight: 500;
color: #1c1c1c;
text-align: center;
line-height: 1.25rem;
}

/* ===== IMAGE INDICATOR DOTS ===== */
.image-indicator-dots {
display: flex;
Expand Down
1 change: 1 addition & 0 deletions force-app/main/default/lwc/commonCarousel/labelUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ export const viewImageAriaLabel = (locale, current, total) =>
getTranslatedLabelWithParams('viewImageAriaLabel', locale, current, total);
export const viewProductAriaLabel = (locale, current, total) =>
getTranslatedLabelWithParams('viewProductAriaLabel', locale, current, total);
export const showMoreProducts = (locale) => getTranslatedLabel('showMoreProducts', locale);
20 changes: 20 additions & 0 deletions force-app/main/default/lwc/commonCarousel/labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,24 @@ export const LABEL_DATA = {
zh_CN: '缺货',
zh_TW: '缺貨',
},
showMoreProducts: {
en_US: 'Show more',
en_GB: 'Show more',
en: 'Show more',
de: 'Mehr anzeigen',
es: 'Mostrar más',
fr: 'Afficher plus',
it: 'Mostra altro',
ja: 'もっと表示',
ko: '더 보기',
nl: 'Meer tonen',
no: 'Vis flere',
pl: 'Pokaż więcej',
pt_BR: 'Mostrar mais',
sv: 'Visa fler',
da: 'Vis flere',
fi: 'Näytä lisää',
zh_CN: '显示更多',
zh_TW: '顯示更多',
},
};
Loading