From 7d0bddc2fb88e7d07c570738a73d93882229cdcd Mon Sep 17 00:00:00 2001 From: Olivier Lamothe Date: Fri, 7 Jun 2024 11:30:31 -0400 Subject: [PATCH] fix(atomic): fix grid system for commerce when no results or error (#4058) Grid system (search-layout/commerce-layout) relies on specific css classes being toggled on the container interface when there are no products/results or when there is an error, in order to align the columns properly. https://coveord.atlassian.net/browse/KIT-3239 --- .../atomic-commerce-interface.tsx | 58 +++++++++++++++---- .../atomic-commerce-layout/commerce-layout.ts | 12 +++- .../atomic-layout-section/search-layout.ts | 6 +- .../search/atomic-layout/search-layout.ts | 12 +++- .../atomic-search-interface.tsx | 11 +++- 5 files changed, 80 insertions(+), 19 deletions(-) diff --git a/packages/atomic/src/components/commerce/atomic-commerce-interface/atomic-commerce-interface.tsx b/packages/atomic/src/components/commerce/atomic-commerce-interface/atomic-commerce-interface.tsx index 6629a8a08a2..25649ef9134 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-interface/atomic-commerce-interface.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-interface/atomic-commerce-interface.tsx @@ -12,6 +12,10 @@ import { ProductListing, Context, buildContext, + buildSearchSummary, + buildListingSummary, + SearchSummary, + ListingSummary, } from '@coveo/headless/commerce'; import { Component, @@ -36,6 +40,11 @@ import { BaseAtomicInterface, CommonAtomicInterfaceHelper, } from '../../common/interface/interface-common'; +import { + errorSelector, + firstSearchExecutedSelector, + noProductsSelector, +} from '../atomic-commerce-layout/commerce-layout'; import {getAnalyticsConfig} from './analytics-config'; import {AtomicCommerceStore, createAtomicCommerceStore} from './store'; @@ -66,10 +75,12 @@ export class AtomicCommerceInterface implements BaseAtomicInterface { private urlManager!: UrlManager; - private searchStatus!: Search | ProductListing; + private searchOrListing!: Search | ProductListing; + private summary!: SearchSummary | ListingSummary; private context!: Context; private unsubscribeUrlManager: Unsubscribe = () => {}; private unsubscribeSearchStatus: Unsubscribe = () => {}; + private unsubscribeSummary: Unsubscribe = () => {}; private initialized = false; private store: AtomicCommerceStore; private commonInterfaceHelper: CommonAtomicInterfaceHelper; @@ -210,6 +221,7 @@ export class AtomicCommerceInterface public disconnectedCallback() { this.unsubscribeUrlManager(); this.unsubscribeSearchStatus(); + this.unsubscribeSummary(); window.removeEventListener('hashchange', this.onHashChange); } @@ -339,7 +351,7 @@ export class AtomicCommerceInterface } private initUrlManager() { - this.urlManager = this.searchStatus.urlManager({ + this.urlManager = this.searchOrListing.urlManager({ initialState: {fragment: this.fragment}, }); @@ -351,14 +363,14 @@ export class AtomicCommerceInterface } private initSearchStatus() { - if (this.type === 'product-listing') { - this.searchStatus = buildProductListing(this.engine!); - } else { - this.searchStatus = buildSearch(this.engine!); - } - this.unsubscribeSearchStatus = this.searchStatus.subscribe(() => { + this.searchOrListing = + this.type === 'product-listing' + ? buildProductListing(this.engine!) + : buildSearch(this.engine!); + + this.unsubscribeSearchStatus = this.searchOrListing.subscribe(() => { if ( - !this.searchStatus.state.isLoading && + !this.searchOrListing.state.isLoading && this.store.hasLoadingFlag(FirstSearchExecutedFlag) ) { this.store.unsetLoadingFlag(FirstSearchExecutedFlag); @@ -366,6 +378,31 @@ export class AtomicCommerceInterface }); } + private initSummary() { + this.summary = + this.type === 'product-listing' + ? buildListingSummary(this.engine!) + : buildSearchSummary(this.engine!); + + this.unsubscribeSummary = this.summary.subscribe(() => { + const {firstSearchExecuted, hasProducts, hasError} = this.summary.state; + const hasNoProductsAfterInitialSearch = + firstSearchExecuted && !hasError && !hasProducts; + + this.host.classList.toggle( + noProductsSelector, + hasNoProductsAfterInitialSearch + ); + + this.host.classList.toggle(errorSelector, hasError); + + this.host.classList.toggle( + firstSearchExecutedSelector, + firstSearchExecuted + ); + }); + } + private initContext() { this.context = buildContext(this.engine!); } @@ -373,7 +410,7 @@ export class AtomicCommerceInterface private updateHash() { const newFragment = this.urlManager.state.fragment; - if (!this.searchStatus.state.isLoading) { + if (!this.searchOrListing.state.isLoading) { history.replaceState(null, document.title, `#${newFragment}`); this.bindings.engine.logger.info(`History replaceState #${newFragment}`); @@ -392,6 +429,7 @@ export class AtomicCommerceInterface await this.commonInterfaceHelper.onInitialization(initEngine); this.initSearchStatus(); + this.initSummary(); this.initUrlManager(); this.initContext(); this.initialized = true; diff --git a/packages/atomic/src/components/commerce/atomic-commerce-layout/commerce-layout.ts b/packages/atomic/src/components/commerce/atomic-commerce-layout/commerce-layout.ts index 957b3f159a4..8f0996fa1da 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-layout/commerce-layout.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-layout/commerce-layout.ts @@ -1,5 +1,11 @@ import {buildSearchLayoutCommon} from '../../common/atomic-layout-section/search-layout'; +export const layoutWebComponentTagName = 'atomic-commerce-layout'; +export const containerWebComponentTagName = 'atomic-commerce-interface'; +export const noProductsSelector = `${containerWebComponentTagName}-no-results`; +export const errorSelector = `${containerWebComponentTagName}-error`; +export const firstSearchExecutedSelector = `${containerWebComponentTagName}-search-executed`; + export function makeDesktopQuery(mobileBreakpoint: string) { return `only screen and (min-width: ${mobileBreakpoint})`; } @@ -10,7 +16,9 @@ export function buildCommerceLayout( return buildSearchLayoutCommon( element, mobileBreakpoint, - 'atomic-commerce-layout', - 'atomic-commerce-interface' + layoutWebComponentTagName, + containerWebComponentTagName, + noProductsSelector, + errorSelector ); } diff --git a/packages/atomic/src/components/common/atomic-layout-section/search-layout.ts b/packages/atomic/src/components/common/atomic-layout-section/search-layout.ts index b5bd3bbd0e3..34e5cb55640 100644 --- a/packages/atomic/src/components/common/atomic-layout-section/search-layout.ts +++ b/packages/atomic/src/components/common/atomic-layout-section/search-layout.ts @@ -10,11 +10,13 @@ export function buildSearchLayoutCommon( element: HTMLElement, mobileBreakpoint: string, layoutWebComponentTagName: string, - containerWebComponentTagName: string + containerWebComponentTagName: string, + noItemsSelector: string, + errorSelector: string ) { const id = element.id; const layoutSelector = `${layoutWebComponentTagName}#${id}`; - const cleanStatusSelector = `${containerWebComponentTagName}:not(.${containerWebComponentTagName}-no-results, .${containerWebComponentTagName}-error)`; + const cleanStatusSelector = `${containerWebComponentTagName}:not(.${noItemsSelector}, .${errorSelector})`; const mediaQuerySelector = `@media ${makeDesktopQuery(mobileBreakpoint)}`; const display = `${layoutSelector} { display: grid }`; diff --git a/packages/atomic/src/components/search/atomic-layout/search-layout.ts b/packages/atomic/src/components/search/atomic-layout/search-layout.ts index 9664ea3ae01..619bd11bcac 100644 --- a/packages/atomic/src/components/search/atomic-layout/search-layout.ts +++ b/packages/atomic/src/components/search/atomic-layout/search-layout.ts @@ -1,5 +1,11 @@ import {buildSearchLayoutCommon} from '../../common/atomic-layout-section/search-layout'; +export const layoutWebComponentTagName = 'atomic-search-layout'; +export const containerWebComponentTagName = 'atomic-search-interface'; +export const noResultsSelector = `${containerWebComponentTagName}-no-results`; +export const errorSelector = `${containerWebComponentTagName}-error`; +export const firstSearchExecutedSelector = `${containerWebComponentTagName}-search-executed`; + export function makeDesktopQuery(mobileBreakpoint: string) { return `only screen and (min-width: ${mobileBreakpoint})`; } @@ -10,7 +16,9 @@ export function buildSearchLayout( return buildSearchLayoutCommon( element, mobileBreakpoint, - 'atomic-search-layout', - 'atomic-search-interface' + layoutWebComponentTagName, + containerWebComponentTagName, + noResultsSelector, + errorSelector ); } diff --git a/packages/atomic/src/components/search/atomic-search-interface/atomic-search-interface.tsx b/packages/atomic/src/components/search/atomic-search-interface/atomic-search-interface.tsx index e8affd66849..6a29a7ad48b 100644 --- a/packages/atomic/src/components/search/atomic-search-interface/atomic-search-interface.tsx +++ b/packages/atomic/src/components/search/atomic-search-interface/atomic-search-interface.tsx @@ -41,6 +41,11 @@ import { CommonAtomicInterfaceHelper, mismatchedInterfaceAndEnginePropError, } from '../../common/interface/interface-common'; +import { + errorSelector, + firstSearchExecutedSelector, + noResultsSelector, +} from '../atomic-layout/search-layout'; import {getAnalyticsConfig} from './analytics-config'; import {AtomicStore, createAtomicStore} from './store'; @@ -538,17 +543,17 @@ export class AtomicSearchInterface !this.searchStatus.state.hasError; this.host.classList.toggle( - 'atomic-search-interface-no-results', + noResultsSelector, hasNoResultsAfterInitialSearch ); this.host.classList.toggle( - 'atomic-search-interface-error', + errorSelector, this.searchStatus.state.hasError ); this.host.classList.toggle( - 'atomic-search-interface-search-executed', + firstSearchExecutedSelector, this.searchStatus.state.firstSearchExecuted );