Skip to content

Commit

Permalink
fix(atomic): search-box suggestions should be resilient to search-box…
Browse files Browse the repository at this point in the history
… redirection-url changes (#4289)

When updating the `redirection-url`, references to the 'old' controller
persisted and broke the suggestions.

To alleviate this issue:
0. Ensure SB ID unicity per component
1. Allow SSB controllers to receive an update to their redirection URL.
2. Clean-up then rebuild controllers when the redirection-url attribute
is updated. When rebuilding a SSB controller, allow the state associated
to the ID to be overwritten.
3. Use 'getter-like' functions instead of direct reference for the
controller in the suggestion-manager.

KIT-3471

---------

Co-authored-by: Alex Prudhomme <[email protected]>
  • Loading branch information
louis-bompart and alexprudhomme authored Aug 23, 2024
1 parent 67d0416 commit 19cabeb
Show file tree
Hide file tree
Showing 30 changed files with 803 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -203,22 +203,46 @@ export class AtomicCommerceSearchBox

@AriaLiveRegion('search-suggestions', true)
protected suggestionsAriaMessage!: string;
public disconnectedCallback = () => {};

private isStandaloneSearchBox(
searchBox: SearchBox | StandaloneSearchBox
): searchBox is StandaloneSearchBox {
return 'redirectTo' in searchBox;
}

public initialize() {
this.id = randomID('atomic-commerce-search-box-');
this.id ??= randomID('atomic-commerce-search-box-');

this.initializeSearchboxController();
this.initializeSuggestionManager();
}

private updateRedirectionUrl() {
if (this.isStandaloneSearchBox(this.searchBox) && this.redirectionUrl) {
this.searchBox.updateRedirectUrl(this.redirectionUrl);
} else {
this.registerNewSearchBoxController();
}
}

private registerNewSearchBoxController() {
this.disconnectedCallback();
this.initialize();
}

private initializeSearchboxController() {
this.searchBox = this.redirectionUrl
? buildStandaloneSearchBox(this.bindings.engine, {
options: {
...this.searchBoxOptions,
redirectionUrl: this.redirectionUrl,
overwrite: true,
},
})
: buildSearchBox(this.bindings.engine, {
options: this.searchBoxOptions,
});

this.initializeSuggestionManager();
}

public componentWillUpdate() {
Expand Down Expand Up @@ -271,7 +295,7 @@ export class AtomicCommerceSearchBox
this.suggestionManager.forceUpdate();
}

public componentWillRender() {
private registerSearchboxSuggestionEvents() {
this.searchBoxSuggestionEventsQueue.forEach((evt) => {
this.suggestionManager.registerSuggestionsFromEvent(
evt,
Expand All @@ -283,7 +307,7 @@ export class AtomicCommerceSearchBox

@Watch('redirectionUrl')
watchRedirectionUrl() {
this.initialize();
this.updateRedirectionUrl();
}

private initializeSuggestionManager() {
Expand Down Expand Up @@ -324,8 +348,8 @@ export class AtomicCommerceSearchBox
return {
...this.bindings,
id: this.id,
isStandalone: !!this.redirectionUrl,
searchBoxController: this.searchBox,
isStandalone: () => !!this.redirectionUrl,
searchBoxController: () => this.searchBox,
numberOfQueries: this.numberOfQueries,
clearFilters: this.clearFilters,
};
Expand Down Expand Up @@ -675,6 +699,9 @@ export class AtomicCommerceSearchBox
const isDisabled = this.isSearchDisabledForEndUser(
this.searchBoxState.value
);
if (!this.suggestionManager.suggestions.length) {
this.registerSearchboxSuggestionEvents();
}

return (
<Host>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,35 @@ test.describe('with instant results & query suggestions', () => {
await expect(searchBox.searchInput).toHaveValue('surf');
});
});

test.describe('after updating the redirect-url attribute', () => {
test.beforeEach(async ({searchBox}) => {
await searchBox.component.evaluate((node) =>
node.setAttribute(
'redirection-url',
'./iframe.html?id=atomic-commerce-search-box--in-page&viewMode=story&args=enable-query-syntax:!true;suggestion-timeout:5000'
)
);
});

test('should redirect to the specified url after selecting a suggestion', async ({
page,
searchBox,
}) => {
const suggestionText = await searchBox
.searchSuggestions()
.first()
.textContent();

expect(suggestionText).not.toBeNull();

await searchBox.searchSuggestions().first().click();
await page.waitForURL(
'**/iframe.html?id=atomic-commerce-search-box--in-page*'
);
await expect(searchBox.searchInput).toHaveValue(suggestionText ?? '');
});
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export class SearchBoxPageObject extends BasePageObject<'atomic-commerce-search-
super(page, 'atomic-commerce-search-box');
}

get component() {
return this.page.locator('atomic-commerce-search-box');
}

get submitButton() {
return this.page.getByLabel('Search', {exact: true});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const InASearchBoxInstantProducts: Story = {
commerceInterfaceDecorator,
],
play: async (context) => {
initializeCommerceInterface(context);
await initializeCommerceInterface(context);
const {canvasElement, step} = context;
const canvas = within(canvasElement);
await step('Click Searchbox', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,10 @@ export class AtomicCommerceSearchBoxInstantProducts
content: <InstantItemShowAllButton i18n={this.bindings.i18n} />,
onSelect: () => {
this.bindings.clearSuggestions();
this.bindings.searchBoxController.updateText(
this.instantProducts.state.query
);
this.bindings.searchBoxController.submit();
this.bindings
.searchBoxController()
.updateText(this.instantProducts.state.query);
this.bindings.searchBoxController().submit();
},
});
}
Expand Down Expand Up @@ -226,7 +226,7 @@ export class AtomicCommerceSearchBoxInstantProducts
private onSuggestedQueryChange() {
if (
!this.bindings.getSuggestionElements().length &&
!this.bindings.searchBoxController.state.value
!this.bindings.searchBoxController().state.value
) {
console.warn(
"There doesn't seem to be any query suggestions configured. Make sure to include either an atomic-commerce-search-box-query-suggestions or atomic-commerce-search-box-recent-queries in your search box in order to see some instant products."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,16 @@ export class AtomicCommerceSearchBoxQuerySuggestions {
}

private renderItems(): SearchBoxSuggestionElement[] {
const hasQuery = this.bindings.searchBoxController.state.value !== '';
const hasQuery = this.bindings.searchBoxController().state.value !== '';
const max = hasQuery ? this.maxWithQuery : this.maxWithoutQuery;
return this.bindings.searchBoxController.state.suggestions
.slice(0, max)
return this.bindings
.searchBoxController()
.state.suggestions.slice(0, max)
.map((suggestion) => this.renderItem(suggestion));
}

private renderItem(suggestion: Suggestion) {
const hasQuery = this.bindings.searchBoxController.state.value !== '';
const hasQuery = this.bindings.searchBoxController().state.value !== '';
const partialItem = getPartialSearchBoxSuggestionElement(
suggestion,
this.bindings.i18n
Expand All @@ -114,7 +115,9 @@ export class AtomicCommerceSearchBoxQuerySuggestions {
</QuerySuggestionContainer>
),
onSelect: () => {
this.bindings.searchBoxController.selectSuggestion(suggestion.rawValue);
this.bindings
.searchBoxController()
.selectSuggestion(suggestion.rawValue);
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export class AtomicCommerceSearchBoxRecentQueries {
return [];
}

const query = this.bindings.searchBoxController.state.value;
const query = this.bindings.searchBoxController().state.value;
const hasQuery = query !== '';
const max = hasQuery ? this.maxWithQuery : this.maxWithoutQuery;
const filteredQueries = this.recentQueriesList.state.queries
Expand Down Expand Up @@ -157,7 +157,7 @@ export class AtomicCommerceSearchBoxRecentQueries {
}

private renderItem(value: string): SearchBoxSuggestionElement {
const query = this.bindings.searchBoxController.state.value;
const query = this.bindings.searchBoxController().state.value;
const partialItem = getPartialRecentQueryElement(value, this.bindings.i18n);
return {
...partialItem,
Expand All @@ -169,9 +169,9 @@ export class AtomicCommerceSearchBoxRecentQueries {
),

onSelect: () => {
if (this.bindings.isStandalone) {
this.bindings.searchBoxController.updateText(value);
this.bindings.searchBoxController.submit();
if (this.bindings.isStandalone()) {
this.bindings.searchBoxController().updateText(value);
this.bindings.searchBoxController().submit();
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,11 @@ export type SearchBoxSuggestionsBindings<
/**
* Whether the search box is [standalone](https://docs.coveo.com/en/atomic/latest/usage/ssb/).
*/
isStandalone: boolean;
isStandalone(): boolean;
/**
* The search box headless controller.
*/
searchBoxController: SearchBoxController;
searchBoxController(): SearchBoxController;
/**
* The number of queries to display when the user interacts with the search box.
*/
Expand Down
Loading

0 comments on commit 19cabeb

Please sign in to comment.