Skip to content

Commit

Permalink
feat(atomic,headless): support for atomic-commerce-did-you-mean (#4029)
Browse files Browse the repository at this point in the history
https://coveord.atlassian.net/browse/KIT-3167

---------

Co-authored-by: GitHub Actions Bot <>
  • Loading branch information
olamothe authored Jun 4, 2024
1 parent c25a4bc commit 5e860a5
Show file tree
Hide file tree
Showing 19 changed files with 505 additions and 139 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { JSX } from '@coveo/atomic';
import { defineCustomElements } from '@coveo/atomic/loader';

defineCustomElements();
export const AtomicCommerceDidYouMean = /*@__PURE__*/createReactComponent<JSX.AtomicCommerceDidYouMean, HTMLAtomicCommerceDidYouMeanElement>('atomic-commerce-did-you-mean');
export const AtomicCommerceFacet = /*@__PURE__*/createReactComponent<JSX.AtomicCommerceFacet, HTMLAtomicCommerceFacetElement>('atomic-commerce-facet');
export const AtomicCommerceFacets = /*@__PURE__*/createReactComponent<JSX.AtomicCommerceFacets, HTMLAtomicCommerceFacetsElement>('atomic-commerce-facets');
export const AtomicCommerceInterface = /*@__PURE__*/createReactComponent<JSX.AtomicCommerceInterface, HTMLAtomicCommerceInterfaceElement>('atomic-commerce-interface');
Expand Down
13 changes: 13 additions & 0 deletions packages/atomic/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ export namespace Components {
*/
"summary": SearchSummary | ListingSummary;
}
interface AtomicCommerceDidYouMean {
}
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
*/
Expand Down Expand Up @@ -3448,6 +3450,12 @@ declare global {
prototype: HTMLAtomicCommerceCategoryFacetElement;
new (): HTMLAtomicCommerceCategoryFacetElement;
};
interface HTMLAtomicCommerceDidYouMeanElement extends Components.AtomicCommerceDidYouMean, HTMLStencilElement {
}
var HTMLAtomicCommerceDidYouMeanElement: {
prototype: HTMLAtomicCommerceDidYouMeanElement;
new (): HTMLAtomicCommerceDidYouMeanElement;
};
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
*/
Expand Down Expand Up @@ -5275,6 +5283,7 @@ declare global {
"atomic-citation": HTMLAtomicCitationElement;
"atomic-color-facet": HTMLAtomicColorFacetElement;
"atomic-commerce-category-facet": HTMLAtomicCommerceCategoryFacetElement;
"atomic-commerce-did-you-mean": HTMLAtomicCommerceDidYouMeanElement;
"atomic-commerce-facet": HTMLAtomicCommerceFacetElement;
"atomic-commerce-facet-number-input": HTMLAtomicCommerceFacetNumberInputElement;
"atomic-commerce-facets": HTMLAtomicCommerceFacetsElement;
Expand Down Expand Up @@ -5665,6 +5674,8 @@ declare namespace LocalJSX {
*/
"summary": SearchSummary | ListingSummary;
}
interface AtomicCommerceDidYouMean {
}
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
*/
Expand Down Expand Up @@ -8507,6 +8518,7 @@ declare namespace LocalJSX {
"atomic-citation": AtomicCitation;
"atomic-color-facet": AtomicColorFacet;
"atomic-commerce-category-facet": AtomicCommerceCategoryFacet;
"atomic-commerce-did-you-mean": AtomicCommerceDidYouMean;
"atomic-commerce-facet": AtomicCommerceFacet;
"atomic-commerce-facet-number-input": AtomicCommerceFacetNumberInput;
"atomic-commerce-facets": AtomicCommerceFacets;
Expand Down Expand Up @@ -8732,6 +8744,7 @@ declare module "@stencil/core" {
* An `atomic-commerce-category-facet` displays a facet of values in a browsable, hierarchical fashion.
*/
"atomic-commerce-category-facet": LocalJSX.AtomicCommerceCategoryFacet & JSXBase.HTMLAttributes<HTMLAtomicCommerceCategoryFacetElement>;
"atomic-commerce-did-you-mean": LocalJSX.AtomicCommerceDidYouMean & JSXBase.HTMLAttributes<HTMLAtomicCommerceDidYouMeanElement>;
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import '../../../global/global.pcss';
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
DidYouMeanState,
DidYouMean,
buildSearch,
QueryTrigger,
buildQueryTrigger,
QueryTriggerState,
} from '@coveo/headless/commerce';
import {Component, State, h} from '@stencil/core';
import {
BindStateToController,
InitializableComponent,
InitializeBindings,
} from '../../../utils/initialization-utils';
import {AutoCorrection} from '../../common/query-correction/auto-correction';
import {Correction} from '../../common/query-correction/correction';
import {QueryCorrectionGuard} from '../../common/query-correction/guard';
import {TriggerCorrection} from '../../common/query-correction/trigger-correction';
import {CommerceBindings} from '../atomic-commerce-interface/atomic-commerce-interface';

/**
* @internal
*
* The `atomic-commerce-query-correction` component is responsible for handling query corrections. When a query returns no products but finds a possible query correction, the component either suggests the correction or automatically triggers a new query with the suggested term.
*
* @part no-results - The text displayed when there are no results.
* @part auto-corrected - The text displayed for the automatically corrected query.
* @part showing-results-for - The first paragraph of the text displayed when a query trigger changes a query.
* @part search-instead-for - The second paragraph of the text displayed when a query trigger changes a query.
* @part did-you-mean - The text displayed around the button to manually correct a query.
* @part correction-btn - The button used to manually correct a query.
* @part undo-btn - The button used to undo a query changed by a query trigger.
* @part highlight - The query highlights.
*/
@Component({
tag: 'atomic-commerce-did-you-mean',
styleUrl: 'atomic-commerce-did-you-mean.pcss',
shadow: true,
})
export class AtomicCommerceDidYouMean
implements InitializableComponent<CommerceBindings>
{
@InitializeBindings() public bindings!: CommerceBindings;
didYouMean!: DidYouMean;
queryTrigger!: QueryTrigger;

@BindStateToController('didYouMean')
@State()
private didYouMeanState?: DidYouMeanState;
@BindStateToController('queryTrigger')
@State()
private queryTriggerState?: QueryTriggerState;
@State()
public error!: Error;

public initialize() {
if (this.bindings.interfaceElement.type !== 'search') {
this.error = new Error(
'atomic-commerce-did-you-mean is only usable with an atomic-commerce-interface of type "search"'
);
}

this.didYouMean = buildSearch(this.bindings.engine).didYouMean();
this.queryTrigger = buildQueryTrigger(this.bindings.engine);
}

private get content() {
if (!this.didYouMeanState || !this.queryTriggerState) {
return;
}

const {hasQueryCorrection, wasAutomaticallyCorrected} =
this.didYouMeanState;
const hasTrigger = this.queryTriggerState.wasQueryModified;

if (hasQueryCorrection && wasAutomaticallyCorrected) {
return (
<AutoCorrection
correctedTo={this.didYouMeanState.wasCorrectedTo}
originalQuery={this.didYouMeanState.originalQuery}
i18n={this.bindings.i18n}
/>
);
}
if (hasQueryCorrection) {
return (
<Correction
correctedQuery={this.didYouMeanState.queryCorrection.correctedQuery}
i18n={this.bindings.i18n}
onClick={() => {}}
/>
);
}
if (hasTrigger) {
return (
<TriggerCorrection
i18n={this.bindings.i18n}
correctedQuery={this.queryTriggerState.newQuery}
originalQuery={this.queryTriggerState.originalQuery}
onClick={() => this.queryTrigger.undo()}
/>
);
}
}

public render() {
if (!this.didYouMeanState || !this.queryTriggerState) {
return;
}

return (
<QueryCorrectionGuard
hasCorrection={
this.didYouMeanState.hasQueryCorrection ||
this.queryTriggerState.wasQueryModified
}
>
{this.content}
</QueryCorrectionGuard>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {FunctionalComponent, h, Fragment} from '@stencil/core';
import {i18n} from 'i18next';
import {LocalizedString} from '../../../utils/jsx-utils';

interface AutoCorrectionProps {
i18n: i18n;
originalQuery: string;
correctedTo: string;
}
export const AutoCorrection: FunctionalComponent<AutoCorrectionProps> = ({
i18n,
correctedTo,
originalQuery,
}) => {
return (
<Fragment>
<p class="text-on-background mb-1" part="no-results">
<LocalizedString
i18n={i18n}
key={'no-results-for-did-you-mean'}
params={{query: <b part="highlight">{originalQuery}</b>}}
/>
</p>
<p class="text-on-background" part="auto-corrected">
<LocalizedString
i18n={i18n}
key={'query-auto-corrected-to'}
params={{query: <b part="highlight">{correctedTo}</b>}}
/>
</p>
</Fragment>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {FunctionalComponent, h} from '@stencil/core';
import {i18n} from 'i18next';
import {LocalizedString} from '../../../utils/jsx-utils';

interface CorrectionProps {
i18n: i18n;
onClick: () => void;
correctedQuery: string;
}
export const Correction: FunctionalComponent<CorrectionProps> = ({
i18n,
onClick,
correctedQuery,
}) => {
return (
<p class="text-on-background" part="did-you-mean">
<LocalizedString
i18n={i18n}
key="did-you-mean"
params={{
query: (
<button
class="link py-1"
part="correction-btn"
onClick={() => onClick()}
>
{correctedQuery}
</button>
),
}}
/>
</p>
);
};
13 changes: 13 additions & 0 deletions packages/atomic/src/components/common/query-correction/guard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Fragment, FunctionalComponent, h} from '@stencil/core';

interface QueryCorrectionGuardProps {
hasCorrection: boolean;
}
export const QueryCorrectionGuard: FunctionalComponent<
QueryCorrectionGuardProps
> = ({hasCorrection}, children) => {
if (!hasCorrection) {
return;
}
return <Fragment>{children}</Fragment>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {FunctionalComponent, Fragment, h} from '@stencil/core';
import {i18n} from 'i18next';
import {LocalizedString} from '../../../utils/jsx-utils';

interface TriggerCorrectionProps {
i18n: i18n;
correctedQuery: string;
originalQuery: string;
onClick: () => void;
}
export const TriggerCorrection: FunctionalComponent<TriggerCorrectionProps> = ({
i18n,
correctedQuery,
originalQuery,
onClick,
}) => {
return (
<Fragment>
<p
class="text-on-background leading-6 text-lg"
part="showing-results-for"
>
<LocalizedString
i18n={i18n}
key={'showing-results-for'}
params={{query: <b part="highlight">{correctedQuery}</b>}}
/>
</p>
<p
class="text-on-background leading-5 text-base"
part="search-instead-for"
>
<LocalizedString
i18n={i18n}
key="search-instead-for"
params={{
query: (
<button
class="link py-1"
part="undo-btn"
onClick={() => onClick()}
>
{originalQuery}
</button>
),
}}
/>
</p>
</Fragment>
);
};
Loading

0 comments on commit 5e860a5

Please sign in to comment.