Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions .changeset/recent-queries-clear-button.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@coveo/atomic': minor
---

Move the option to clear recent queries out of the list of suggestions in `atomic-search-box` and `atomic-commerce-search-box`. It is now a "Clear recent searches" button below the suggestions that is reached with the Tab key, instead of the first item reached with the Down arrow key. While this button is displayed, pressing Tab no longer closes the suggestions.

The `recent-query-title-item`, `recent-query-title-content` and `recent-query-title` parts were removed, and the `recent-query-clear` part now targets the button.
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,7 @@ describe('atomic-commerce-search-box-recent-queries', () => {
it('should have the correct properties on the query clear item', () => {
expect(items[0]).toEqual(
expect.objectContaining({
part: 'recent-query-title-item suggestion-divider',
hideIfLast: true,
part: 'recent-query-clear',
ariaLabel: 'Clear recent searches',
key: 'recent-query-clear',
})
Expand Down Expand Up @@ -249,41 +248,6 @@ describe('atomic-commerce-search-box-recent-queries', () => {
expect(buildFakeRecentQueriesList().executeRecentQuery).toHaveBeenCalled();
});

describe('when rendering the query clear item', () => {
const setupContent = () => {
return items[0].content as HTMLElement;
};

it('should have the correct part on the container', () => {
const content = setupContent();
expect(content).toHaveAttribute('part', 'recent-query-title-content');
});

it('should have the correct part on the recent searches span ', () => {
const content = setupContent();
const recentSearchesSpan = content.querySelector('span');
expect(recentSearchesSpan).toHaveAttribute('part', 'recent-query-title');
});

it('should have the correct text on the recent searches span', () => {
const content = setupContent();
const recentSearchesSpan = content.querySelector('span');
expect(recentSearchesSpan).toHaveTextContent('Recent searches');
});

it('should have the correct part on the recent query clear span', () => {
const content = setupContent();
const recentQueryClearSpan = content.querySelector('span[part="recent-query-clear"]');
expect(recentQueryClearSpan).toHaveAttribute('part', 'recent-query-clear');
});

it('should have the correct text on the recent query clear span', () => {
const content = setupContent();
const recentQueryClearSpan = content.querySelector('span[part="recent-query-clear"]');
expect(recentQueryClearSpan).toHaveTextContent('Clear');
});
});

describe('when rendering the recent query item', () => {
const setupContent = async (bindings: {} = {}) => {
({element} = await renderElements(bindings));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ export class AtomicCommerceSearchBoxRecentQueriesPageObject extends BasePageObje
}

get clearButton() {
return this.page.getByLabel('Clear recent searches.');
return this.page.getByRole('button', {name: 'Clear recent searches'});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,46 @@ describe('atomic-commerce-search-box', () => {
});
});

describe('when there are recent queries', () => {
const renderWithRecentQueries = async () => {
const searchBox = await renderSearchBox({noSuggestions: true});
await userEvent.click(searchBox.element);

return {
...searchBox,
clearRecentQueriesButton: () =>
searchBox.suggestionsContainer.querySelector<HTMLButtonElement>(
'button[part="recent-query-clear"]'
)!,
};
};

it('should render the clear button below the suggestions', async () => {
const {suggestionsContainer, clearRecentQueriesButton} = await renderWithRecentQueries();

expect(clearRecentQueriesButton()).toHaveTextContent('Clear recent searches');
expect(suggestionsContainer.lastElementChild).toContainElement(clearRecentQueriesButton());
});

it('should clear the recent queries and focus the search box when the clear button is clicked', async () => {
const {element, textArea, clearRecentQueriesButton} = await renderWithRecentQueries();

await userEvent.click(clearRecentQueriesButton());

expect(buildFakeRecentQueriesList().clear).toHaveBeenCalledOnce();
expect(element.shadowRoot!.activeElement).toBe(textArea);
});

it('should keep the suggestions when the Tab key is pressed', async () => {
const {suggestions} = await renderWithRecentQueries();
const suggestionCount = suggestions().length;

await userEvent.keyboard('{Tab}');

expect(suggestions()).toHaveLength(suggestionCount);
});
});

describe('when clicking the clear button', () => {
it('should call the clear method of the search box controller', async () => {
const {clearButton} = await renderSearchBox({searchBoxValue: 'test'});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type StandaloneSearchBox,
type StandaloneSearchBoxState,
} from '@coveo/headless/commerce';
import {type CSSResultGroup, css, html, LitElement} from 'lit';
import {type CSSResultGroup, css, html, LitElement, nothing} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {classMap} from 'lit/directives/class-map.js';
import {createRef, type RefOrCallback, ref} from 'lit/directives/ref.js';
Expand All @@ -31,10 +31,12 @@ import {
import {updateBreakpoints} from '@/src/utils/replace-breakpoint-utils';
import {getDefaultSlotContent} from '@/src/utils/slot-utils';
import {isFocusingOut, once, randomID, spreadProperties} from '../../../utils/utils';
import {renderButton} from '../../common/button';
import type {RedirectionPayload} from '../../common/search-box/redirection-payload';
import {renderSearchBoxWrapper} from '../../common/search-box/search-box-wrapper';
import {renderSearchBoxTextArea} from '../../common/search-box/search-text-area';
import {renderSubmitButton} from '../../common/search-box/submit-button';
import {isRecentQueryClearElement} from '../../common/suggestions/recent-queries';
import {SuggestionManager} from '../../common/suggestions/suggestion-manager';
import type {
SearchBoxSuggestionElement,
Expand Down Expand Up @@ -82,10 +84,7 @@ import '@coveo/atomic-legacy/atomic-suggestion-renderer';
* @part recent-query-icon - The icon of a suggestion from the `atomic-commerce-search-box-recent-queries` component.
* @part recent-query-text - The text of a suggestion from the `atomic-commerce-search-box-recent-queries` component.
* @part recent-query-text-highlight - The highlighted portion of the text of a suggestion from the `atomic-commerce-search-box-recent-queries` component.
* @part recent-query-title-item - The clear button above suggestions from the `atomic-commerce-search-box-recent-queries` component.
* @part recent-query-title-content - The contents of the clear button above suggestions from the `atomic-commerce-search-box-recent-queries` component.
* @part recent-query-title - The "recent searches" text of the clear button above suggestions from the `atomic-commerce-search-box-recent-queries` component.
* @part recent-query-clear - The "clear" text of the clear button above suggestions from the `atomic-commerce-search-box-recent-queries` component.
* @part recent-query-clear - The clear button below suggestions from the `atomic-commerce-search-box-recent-queries` component.
*
* @part instant-results-item - An instant product rendered by an `atomic-commerce-search-box-instant-products` component.
* @part instant-results-show-all - The clickable suggestion to show all items for the current instant product search rendered by an `atomic-commerce-search-box-instant-products` component.
Expand Down Expand Up @@ -473,7 +472,9 @@ export class AtomicCommerceSearchBox
}
break;
case 'Tab':
this.suggestionManager.clearSuggestions();
if (!this.clearRecentQueriesElement) {
this.suggestionManager.clearSuggestions();
}
break;
default:
if (this.suggestionManager.keyboardActiveDescendant) {
Expand Down Expand Up @@ -559,6 +560,34 @@ export class AtomicCommerceSearchBox
})}`;
}

private get clearRecentQueriesElement() {
return this.suggestionManager.allSuggestionElements.find(isRecentQueryClearElement);
}

private renderClearRecentQueries() {
const element = this.clearRecentQueriesElement;
if (!element) {
return nothing;
}

return html`<div
class="border-neutral basis-full border-t px-2 py-1"
@mousedown=${(e: MouseEvent) => e.preventDefault()}
>
${renderButton({
props: {
style: 'text-primary',
part: element.part,
class: 'focus-visible:ring-ring-primary px-2 py-1 focus-visible:ring-2',
onClick: (e) => {
element.onSelect?.(e!);
this.textAreaRef.value?.focus();
},
},
})(html`${element.content}`)}
Comment thread
y-lakhdar marked this conversation as resolved.
Outdated
</div>`;
}

private renderSuggestions() {
const part = `suggestions-wrapper ${
this.suggestionManager.isDoubleList ? 'suggestions-double-list' : 'suggestions-single-list'
Expand All @@ -569,6 +598,7 @@ export class AtomicCommerceSearchBox

const classes = {
'bg-background border-neutral absolute top-full left-0 z-10 flex w-full rounded-md border': true,
'flex-wrap': !!this.clearRecentQueriesElement,
hidden: !isVisible,
};

Expand Down Expand Up @@ -600,15 +630,17 @@ export class AtomicCommerceSearchBox
},
() => this.suggestionManager.rightPanel
)}
${this.renderClearRecentQueries()}
</div>`;
}

private renderPanel(
side: 'left' | 'right',
elements: SearchBoxSuggestionElement[],
panelElements: SearchBoxSuggestionElement[],
setRef: (el: HTMLElement | undefined) => void,
getRef: () => HTMLElement | undefined
) {
const elements = panelElements.filter((element) => !isRecentQueryClearElement(element));
if (!elements.length) {
return null;
}
Expand Down Expand Up @@ -654,10 +686,6 @@ export class AtomicCommerceSearchBox
.isDoubleList=${this.suggestionManager.isDoubleList}
.onClick=${async (e: Event) => {
await this.suggestionManager.onSuggestionClick(item, e);
if (item.key === 'recent-query-clear') {
return;
}

this.isExpanded = false;
}}
.onMouseEnter=${async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ test.describe('atomic-commerce-search-box', () => {
test('should clear recent queries when pressing enter while the clear button is focused', async ({
searchBox,
}) => {
await searchBox.clearRecentQueriesButton.hover();
await searchBox.searchInput.press('Enter');
await searchBox.clearRecentQueriesButton.focus();
await searchBox.clearRecentQueriesButton.press('Enter');
await expect(searchBox.recentQueries().first()).not.toBeVisible();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class SearchBoxPageObject extends BasePageObject {
}

get clearRecentQueriesButton() {
return this.component.getByLabel('Clear recent searches.').first();
return this.component.getByRole('button', {name: 'Clear recent searches'}).first();
}

get numberOfQueries() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {createTestI18n} from '@/vitest-utils/testing-helpers/i18n-utils';
import {
getPartialRecentQueryClearElement,
getPartialRecentQueryElement,
isRecentQueryClearElement,
type RecentQueriesContainerProps,
renderRecentQuery,
renderRecentQueryClear,
Expand Down Expand Up @@ -43,10 +44,10 @@ describe('#getPartialRecentQueryClearElement', () => {
i18n = await createTestI18n();
});

it('should return the "recent-query-title-item suggestion-divider" part', () => {
it('should return the "recent-query-clear" part', () => {
const result = getPartialRecentQueryClearElement(i18n);

expect(result.part).toBe('recent-query-title-item suggestion-divider');
expect(result.part).toBe('recent-query-clear');
});

it('should return the correct aria-label', () => {
Expand All @@ -62,6 +63,26 @@ describe('#getPartialRecentQueryClearElement', () => {
});
});

describe('#isRecentQueryClearElement', () => {
const content = document.createElement('div');

it('should return true for the recent query clear element', async () => {
const i18n = await createTestI18n();

expect(isRecentQueryClearElement({...getPartialRecentQueryClearElement(i18n), content})).toBe(
true
);
});

it('should return false for a recent query element', async () => {
const i18n = await createTestI18n();

expect(
isRecentQueryClearElement({...getPartialRecentQueryElement('test', i18n), content})
).toBe(false);
});
});

describe('#renderRecentQuery', () => {
const renderQuery = async (overrides: Partial<RecentQueriesContainerProps> = {}) => {
const props = {
Expand Down Expand Up @@ -138,38 +159,8 @@ describe('#renderRecentQueryClear', () => {
return await fixture(html`${renderRecentQueryClear({i18n})}`);
};

it('should have the "recent-query-title-content" part on the container', async () => {
const element = await renderQueryClear();
expect(element).toHaveAttribute('part', 'recent-query-title-content');
});

it('should have the "recent-query-title" part on the first span', async () => {
const element = await renderQueryClear();
const span = element.querySelector('span');
expect(span).toHaveAttribute('part', 'recent-query-title');
});

it('should have the proper content on the first span', async () => {
const element = await renderQueryClear();
const span = element.querySelector('span');
expect(span).toHaveTextContent('Recent searches');
});

it('should have the "recent-query-clear" part on the second span', async () => {
const element = await renderQueryClear();
const span = element.querySelector('span:nth-of-type(2)');
expect(span).toHaveAttribute('part', 'recent-query-clear');
});

it('should have the proper content on the second span', async () => {
it('should have the "Clear recent searches" text', async () => {
const element = await renderQueryClear();
const span = element.querySelector('span:nth-of-type(2)');
expect(span).toHaveTextContent('Clear');
});

it('container should have pointer-events-none class to allow clicks on parent', async () => {
const element = await renderQueryClear();

expect(element).toHaveClass('pointer-events-none');
expect(element).toHaveTextContent('Clear recent searches');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@ export const getPartialRecentQueryElement = (
};
};

const recentQueryClearKey = 'recent-query-clear';

export const isRecentQueryClearElement = (element: SearchBoxSuggestionElement) =>
element.key === recentQueryClearKey;

export const getPartialRecentQueryClearElement = (
i18n: i18n
): Pick<SearchBoxSuggestionElement, 'ariaLabel' | 'key' | 'part' | 'hideIfLast'> => {
): Pick<SearchBoxSuggestionElement, 'ariaLabel' | 'key' | 'part'> => {
return {
key: 'recent-query-clear',
key: recentQueryClearKey,
ariaLabel: i18n.t('clear-recent-searches', {
interpolation: {escapeValue: false},
}),
part: 'recent-query-title-item suggestion-divider',
hideIfLast: true,
part: 'recent-query-clear',
};
};

Expand Down Expand Up @@ -81,13 +85,7 @@ export const renderRecentQuery = ({
};

export const renderRecentQueryClear = ({i18n}: {i18n: i18n}): HTMLElement => {
const template = html` <div
part="recent-query-title-content"
class="pointer-events-none flex w-full justify-between"
>
<span class="font-bold" part="recent-query-title"> ${i18n.t('recent-searches')} </span>
<span part="recent-query-clear">${i18n.t('clear')}</span>
</div>`;
const template = html`<span>${i18n.t('clear-recent-searches')}</span>`;

const container = document.createElement('div');
render(template, container);
Expand Down
Loading
Loading