Skip to content

Commit

Permalink
feat(commerce): create PLP v2 interactive result controller (#3129)
Browse files Browse the repository at this point in the history
  • Loading branch information
Spuffynism authored Sep 8, 2023
1 parent 1d63e43 commit 4d3c39d
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 2 deletions.
4 changes: 2 additions & 2 deletions packages/atomic/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1656,7 +1656,7 @@ export namespace Components {
*/
"suggestionTimeout": number;
/**
* Whether to render the search box using a [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) element. The resulting component will expand to support multi-line queries. When customizing the dimensions of the textarea element using the `"textarea"` CSS part, it is important to also apply the styling to its ::after pseudo-element as well as the `"textarea-spacer"` part. The buttons within the search box are likely to need adjusting as well. Example: ```css <style> atomic-search-box::part(textarea), atomic-search-box::part(textarea)::after, atomic-search-box::part(textarea-spacer) { font-size: x-large; } atomic-search-box::part(submit-button-wrapper), atomic-search-box::part(clear-button-wrapper) { padding-top: 0.75rem; } </style> ```
* Whether to render the search box using a [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) element. The resulting component will expand to support multi-line queries. When customizing the dimensions of the textarea element using the `"textarea"` CSS part, it is important to also apply the styling to its container's ::after pseudo-element as well as the `"textarea-spacer"` part. The buttons within the search box are likely to need adjusting as well. Example: ```css <style> atomic-search-box::part(textarea), atomic-search-box::part(textarea-expander)::after, atomic-search-box::part(textarea-spacer) { font-size: x-large; } atomic-search-box::part(submit-button-wrapper), atomic-search-box::part(clear-button-wrapper) { padding-top: 0.75rem; } </style> ```
*/
"textarea": boolean;
}
Expand Down Expand Up @@ -4588,7 +4588,7 @@ declare namespace LocalJSX {
*/
"suggestionTimeout"?: number;
/**
* Whether to render the search box using a [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) element. The resulting component will expand to support multi-line queries. When customizing the dimensions of the textarea element using the `"textarea"` CSS part, it is important to also apply the styling to its ::after pseudo-element as well as the `"textarea-spacer"` part. The buttons within the search box are likely to need adjusting as well. Example: ```css <style> atomic-search-box::part(textarea), atomic-search-box::part(textarea)::after, atomic-search-box::part(textarea-spacer) { font-size: x-large; } atomic-search-box::part(submit-button-wrapper), atomic-search-box::part(clear-button-wrapper) { padding-top: 0.75rem; } </style> ```
* Whether to render the search box using a [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) element. The resulting component will expand to support multi-line queries. When customizing the dimensions of the textarea element using the `"textarea"` CSS part, it is important to also apply the styling to its container's ::after pseudo-element as well as the `"textarea-spacer"` part. The buttons within the search box are likely to need adjusting as well. Example: ```css <style> atomic-search-box::part(textarea), atomic-search-box::part(textarea-expander)::after, atomic-search-box::part(textarea-spacer) { font-size: x-large; } atomic-search-box::part(submit-button-wrapper), atomic-search-box::part(clear-button-wrapper) { padding-top: 0.75rem; } </style> ```
*/
"textarea"?: boolean;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/headless/src/commerce.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ export type {
Subscribable,
} from './controllers/controller/headless-controller';
export {buildController} from './controllers/controller/headless-controller';

export type {
InteractiveResult,
InteractiveResultOptions,
InteractiveResultProps,
} from './controllers/commerce/product-listing/result-list/headless-product-listing-interactive-result';
export {buildInteractiveResult} from './controllers/commerce/product-listing/result-list/headless-product-listing-interactive-result';
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {ProductRecommendation} from '../../../../api/search/search/product-recommendation';
import {configuration} from '../../../../app/common-reducers';
import {logProductRecommendationOpen} from '../../../../features/product-listing/product-listing-analytics';
import {pushRecentResult} from '../../../../features/recent-results/recent-results-actions';
import {
buildMockCommerceEngine,
MockCommerceEngine,
} from '../../../../test/mock-engine';
import {buildMockProductRecommendation} from '../../../../test/mock-product-recommendation';
import {
buildInteractiveResult,
InteractiveResult,
} from './headless-product-listing-interactive-result';

describe('InteractiveResult', () => {
let engine: MockCommerceEngine;
let mockProductRec: ProductRecommendation;
let interactiveResult: InteractiveResult;
let logDocumentOpenPendingActionType: string;

const productRecStringParams = {
permanentid: 'permanentid',
documentUri: 'documentUri',
clickUri: 'clickUri',
};

function initializeInteractiveResult(delay?: number) {
const productRec = (mockProductRec = buildMockProductRecommendation(
productRecStringParams
));
logDocumentOpenPendingActionType =
logProductRecommendationOpen(mockProductRec).pending.type;
interactiveResult = buildInteractiveResult(engine, {
options: {result: productRec, selectionDelay: delay},
});
}

function findLogDocumentAction() {
return (
engine.actions.find(
(action) => action.type === logDocumentOpenPendingActionType
) ?? null
);
}

function expectLogDocumentActionPending() {
const action = findLogDocumentAction();
expect(action).toEqual(
logProductRecommendationOpen(mockProductRec).pending(
action!.meta.requestId
)
);
}

beforeEach(() => {
engine = buildMockCommerceEngine();
initializeInteractiveResult();
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

it('adds the correct reducers to engine', () => {
expect(engine.addReducers).toHaveBeenCalledWith({configuration});
});

it('when calling select() should add the result to recent results list', () => {
interactiveResult.select();
jest.runAllTimers();

expect(
engine.actions.find((a) => a.type === pushRecentResult.type)
).toBeDefined();
});

it('when calling select(), logs documentOpen', () => {
interactiveResult.select();
expectLogDocumentActionPending();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {ProductRecommendation} from '../../../../api/search/search/product-recommendation';
import {CommerceEngine} from '../../../../app/commerce-engine/commerce-engine';
import {logProductRecommendationOpen} from '../../../../features/product-listing/product-listing-analytics';
import {pushRecentResult} from '../../../../features/product-listing/product-listing-recent-results';
import {
buildInteractiveResultCore,
InteractiveResultCore,
InteractiveResultCoreOptions,
InteractiveResultCoreProps,
} from '../../../core/interactive-result/headless-core-interactive-result';

export interface InteractiveResultOptions extends InteractiveResultCoreOptions {
/**
* The product.
*/
result: ProductRecommendation;
}

export interface InteractiveResultProps extends InteractiveResultCoreProps {
/**
* The options for the `InteractiveResult` controller.
* */
options: InteractiveResultOptions;
}

/**
* The `InteractiveProduct` controller provides an interface for handling long presses, multiple clicks, etc. to ensure that Coveo usage analytics events are logged properly when a user selects a product.
*/
export interface InteractiveResult extends InteractiveResultCore {}

/**
* Creates an `InteractiveResult` controller instance.
*
* @param engine - The headless commerce engine.
* @param props - The configurable `InteractiveResult` properties.
* @returns An `InteractiveResult` controller instance.
*/
export function buildInteractiveResult(
engine: CommerceEngine,
props: InteractiveResultProps
): InteractiveResult {
let wasOpened = false;

const logAnalyticsIfNeverOpened = () => {
if (wasOpened) {
return;
}
wasOpened = true;
engine.dispatch(logProductRecommendationOpen(props.options.result));
};

const action = () => {
logAnalyticsIfNeverOpened();
engine.dispatch(pushRecentResult(props.options.result));
};

return buildInteractiveResultCore(engine, props, action);
}

0 comments on commit 4d3c39d

Please sign in to comment.