diff --git a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/atomic-angular.module.ts b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/atomic-angular.module.ts
index 5bb94f659f9..9f806236047 100644
--- a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/atomic-angular.module.ts
+++ b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/atomic-angular.module.ts
@@ -11,6 +11,7 @@ AtomicAutomaticFacetGenerator,
AtomicBreadbox,
AtomicCategoryFacet,
AtomicColorFacet,
+AtomicCommerceFacet,
AtomicCommerceFacets,
AtomicCommerceLoadMoreProducts,
AtomicCommerceQuerySummary,
@@ -112,6 +113,7 @@ AtomicAutomaticFacetGenerator,
AtomicBreadbox,
AtomicCategoryFacet,
AtomicColorFacet,
+AtomicCommerceFacet,
AtomicCommerceFacets,
AtomicCommerceLoadMoreProducts,
AtomicCommerceQuerySummary,
diff --git a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts
index 80eefff5d7c..fab9b6297e0 100644
--- a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts
+++ b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts
@@ -138,6 +138,27 @@ export class AtomicColorFacet {
export declare interface AtomicColorFacet extends Components.AtomicColorFacet {}
+@ProxyCmp({
+})
+@Component({
+ selector: 'atomic-commerce-facet',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: '',
+ // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
+ inputs: [],
+})
+export class AtomicCommerceFacet {
+ protected el: HTMLElement;
+ constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
+ c.detach();
+ this.el = r.nativeElement;
+ }
+}
+
+
+export declare interface AtomicCommerceFacet extends Components.AtomicCommerceFacet {}
+
+
@ProxyCmp({
inputs: ['collapseFacetsAfter']
})
diff --git a/packages/atomic/src/components.d.ts b/packages/atomic/src/components.d.ts
index b3f1a7b6e5e..08f0495f3e8 100644
--- a/packages/atomic/src/components.d.ts
+++ b/packages/atomic/src/components.d.ts
@@ -291,10 +291,12 @@ export namespace Components {
}
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
+ * @alpha
*/
interface AtomicCommerceFacet {
/**
* The facet controller instance.
+ * @
*/
"facet": RegularFacet;
/**
@@ -3495,6 +3497,7 @@ declare global {
};
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
+ * @alpha
*/
interface HTMLAtomicCommerceFacetElement extends Components.AtomicCommerceFacet, HTMLStencilElement {
}
@@ -5726,10 +5729,12 @@ declare namespace LocalJSX {
}
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
+ * @alpha
*/
interface AtomicCommerceFacet {
/**
* The facet controller instance.
+ * @
*/
"facet": RegularFacet;
/**
@@ -8830,6 +8835,7 @@ declare module "@stencil/core" {
"atomic-commerce-did-you-mean": LocalJSX.AtomicCommerceDidYouMean & JSXBase.HTMLAttributes;
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
+ * @alpha
*/
"atomic-commerce-facet": LocalJSX.AtomicCommerceFacet & JSXBase.HTMLAttributes;
/**
diff --git a/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/atomic-commerce-facet.new.stories.tsx b/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/atomic-commerce-facet.new.stories.tsx
new file mode 100644
index 00000000000..0958f8a521a
--- /dev/null
+++ b/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/atomic-commerce-facet.new.stories.tsx
@@ -0,0 +1,39 @@
+import {
+ wrapInCommerceInterface,
+ playExecuteFirstSearch,
+ playKeepOnlyFirstFacetOfType,
+} from '@coveo/atomic/storybookUtils/commerce-interface-wrapper';
+import {parameters} from '@coveo/atomic/storybookUtils/common-meta-parameters';
+import {renderComponent} from '@coveo/atomic/storybookUtils/render-component';
+import type {Meta, StoryObj as Story} from '@storybook/web-components';
+import {html} from 'lit-html';
+
+const {play, decorator} = wrapInCommerceInterface({skipFirstSearch: true});
+
+const meta: Meta = {
+ component: 'atomic-commerce-facet',
+ title: 'Atomic-Commerce/Facet',
+ id: 'atomic-commerce-facet',
+ render: renderComponent,
+ decorators: [decorator],
+ parameters,
+ play,
+};
+
+export default meta;
+
+export const Default: Story = {
+ name: 'atomic-commerce-facet',
+ decorators: [
+ (_) => {
+ return html``;
+ },
+ ],
+ play: async (context) => {
+ await play(context);
+ await playExecuteFirstSearch(context);
+ playKeepOnlyFirstFacetOfType('atomic-commerce-facet', context);
+ },
+};
diff --git a/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/atomic-commerce-facet.tsx b/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/atomic-commerce-facet.tsx
index b13cfe5fd05..57813635e7c 100644
--- a/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/atomic-commerce-facet.tsx
+++ b/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/atomic-commerce-facet.tsx
@@ -48,7 +48,42 @@ import {CommerceBindings as Bindings} from '../../atomic-commerce-interface/atom
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
*
- * @internal
+ * @part facet - The wrapper for the entire facet.
+ * @part placeholder - The placeholder shown before the first search is executed.
+ *
+ * @part label-button - The button that displays the label and allows to expand/collapse the facet.
+ * @part label-button-icon - The label button icon.
+ * @part clear-button - The button that resets the actively selected facet values.
+ * @part clear-button-icon - The clear button icon.
+ *
+ * @part search-wrapper - The search box wrapper.
+ * @part search-input - The search box input.
+ * @part search-icon - The search box submit button.
+ * @part search-clear-button - The button to clear the search box of input.
+ * @part more-matches - The label indicating there are more matches for the current facet search query.
+ * @part no-matches - The label indicating there are no matches for the current facet search query.
+ * @part matches-query - The highlighted query inside the matches labels.
+ * @part search-highlight - The highlighted query inside the facet values.
+ *
+ * @part values - The facet values container.
+ * @part value-label - The facet value label, common for all displays.
+ * @part value-count - The facet value count, common for all displays.
+ *
+ * @part value-checkbox - The facet value checkbox, available when display is 'checkbox'.
+ * @part value-checkbox-checked - The checked facet value checkbox, available when display is 'checkbox'.
+ * @part value-checkbox-label - The facet value checkbox clickable label, available when display is 'checkbox'.
+ * @part value-checkbox-icon - The facet value checkbox icon, available when display is 'checkbox'.
+ * @part value-link - The facet value when display is 'link'.
+ * @part value-link-selected - The selected facet value when display is 'link'.
+ * @part value-box - The facet value when display is 'box'.
+ * @part value-box-selected - The selected facet value when display is 'box'.
+ * @part value-exclude-button - The button to exclude a facet value, available when display is 'checkbox'.
+ *
+ * @part show-more - The show more results button.
+ * @part show-less - The show less results button.
+ * @part show-more-less-icon - The icons of the show more & show less buttons.
+ *
+ * @alpha
*/
@Component({
tag: 'atomic-commerce-facet',
@@ -61,18 +96,26 @@ export class AtomicCommerceFacet implements InitializableComponent {
/**
* The Summary controller instance.
+ *
+ * @internal
*/
@Prop() summary!: Summary;
/**
* The facet controller instance.
+ *
+ * @@internal
*/
@Prop() public facet!: RegularFacet;
/**
* Specifies whether the facet is collapsed.
+ *
+ * @internal
*/
@Prop({reflect: true, mutable: true}) public isCollapsed = false;
/**
* The field identifier for this facet.
+ *
+ * @internal
*/
@Prop({reflect: true}) field?: string;
@@ -90,6 +133,9 @@ export class AtomicCommerceFacet implements InitializableComponent {
protected facetSearchAriaMessage!: string;
public initialize() {
+ if (!this.facet) {
+ return;
+ }
this.initAriaLive();
this.initPopover();
this.registerFacet();
@@ -113,6 +159,9 @@ export class AtomicCommerceFacet implements InitializableComponent {
}
public render() {
+ if (!this.facet) {
+ return;
+ }
const {hasError, firstRequestExecuted} = this.summary.state;
return (
{
+ test.beforeEach(async ({facet}) => {
+ await facet.load();
+ });
+
+ test('should be A11y compliant', async ({facet, makeAxeBuilder}) => {
+ await facet.hydrated.waitFor();
+ const accessibilityResults = await makeAxeBuilder().analyze();
+ expect(accessibilityResults.violations).toEqual([]);
+ });
+
+ test('should allow to filter by selecting and deselecting a value', async ({
+ facet,
+ }) => {
+ const facetValueLabel = facet.getFacetValue('Nike');
+ const facetValueBtn = facet.getFacetValueButton('Nike');
+
+ await expect(facetValueBtn).not.toBeChecked();
+ await facetValueLabel.click();
+
+ await expect(facetValueBtn).toBeChecked();
+
+ await facetValueLabel.click();
+ await expect(facetValueBtn).not.toBeChecked();
+ });
+
+ test('should allow to filter by selecting multiple values', async ({
+ facet,
+ page,
+ }) => {
+ const firstValueBtn = facet.getFacetValueButton('Nike');
+ const secondValueBtn = facet.getFacetValueButton('Adidas');
+
+ await expect(firstValueBtn).not.toBeChecked();
+ await expect(secondValueBtn).not.toBeChecked();
+ await firstValueBtn.click();
+ await secondValueBtn.click();
+
+ await expect(firstValueBtn).toBeChecked();
+ await expect(secondValueBtn).toBeChecked();
+ await expect(page.getByText('Clear 2 filters')).toBeVisible();
+ });
+
+ test('should allow to deselect a filter with the clear button', async ({
+ facet,
+ }) => {
+ const facetValueLabel = facet.getFacetValue('Nike');
+
+ await expect(facet.clearFilter).toHaveCount(0);
+
+ await facetValueLabel.click();
+
+ await expect(facet.clearFilter).toBeVisible();
+ await facet.clearFilter.click();
+
+ await expect(facet.clearFilter).toHaveCount(0);
+ });
+
+ test('should allow to show more values and show less values', async ({
+ facet,
+ page,
+ }) => {
+ await expect(facet.showMore).toBeVisible();
+ await expect(facet.showLess).not.toBeVisible();
+
+ await expect(page.getByRole('listitem')).toHaveCount(8);
+
+ await facet.showMore.click();
+
+ await expect(facet.showLess).toBeVisible();
+ await expect(page.getByRole('listitem')).toHaveCount(16);
+ await facet.showLess.click();
+ await expect(page.getByRole('listitem')).toHaveCount(8);
+ await expect(facet.showLess).not.toBeVisible();
+ });
+
+ test('allow to search for a value', async ({facet, page}) => {
+ await facet.searchInput.fill('n');
+
+ expect(await page.getByRole('listitem').count()).toBeGreaterThanOrEqual(8);
+ await expect(page.getByText('More matches for n')).toBeVisible();
+ await facet.searchInput.fill('nike');
+
+ await facet.getFacetValue('Nike').click();
+
+ await expect(facet.getFacetValueButton('Nike')).toBeChecked();
+ });
+
+ test('allow to clear the search input', async ({facet}) => {
+ await facet.searchInput.fill('nike');
+ await expect(facet.clearSearchInput).toBeVisible();
+
+ await facet.clearSearchInput.click();
+ await expect(facet.clearSearchInput).not.toBeVisible();
+ await expect(facet.searchInput).toBeEmpty();
+ });
+
+ test('behave correct when searching for a value that does not exist', async ({
+ facet,
+ page,
+ }) => {
+ await facet.searchInput.fill('non-existing-value');
+
+ await expect(
+ page.getByText('No matches found for non-existing-value')
+ ).toBeVisible();
+ });
+});
diff --git a/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/e2e/fixture.ts b/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/e2e/fixture.ts
new file mode 100644
index 00000000000..8e5adb37bfb
--- /dev/null
+++ b/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/e2e/fixture.ts
@@ -0,0 +1,19 @@
+import {test as base} from '@playwright/test';
+import {
+ AxeFixture,
+ makeAxeBuilder,
+} from '../../../../../../playwright-utils/base-fixture';
+import {FacetPageObject} from './page-object';
+
+type MyFixtures = {
+ facet: FacetPageObject;
+};
+
+export const test = base.extend({
+ makeAxeBuilder,
+ facet: async ({page}, use) => {
+ await use(new FacetPageObject(page));
+ },
+});
+
+export {expect} from '@playwright/test';
diff --git a/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/e2e/page-object.ts b/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/e2e/page-object.ts
new file mode 100644
index 00000000000..7ad4f0c0cc1
--- /dev/null
+++ b/packages/atomic/src/components/commerce/facets/atomic-commerce-facet/e2e/page-object.ts
@@ -0,0 +1,40 @@
+import type {Page} from '@playwright/test';
+import {BasePageObject} from '../../../../../../playwright-utils/base-page-object';
+
+export class FacetPageObject extends BasePageObject<'atomic-commerce-facet'> {
+ constructor(page: Page) {
+ super(page, 'atomic-commerce-facet');
+ }
+
+ get title() {
+ return this.page.getByText('Brand');
+ }
+
+ get searchInput() {
+ return this.page.getByPlaceholder('Search');
+ }
+
+ get clearSearchInput() {
+ return this.page.getByRole('button', {name: 'Clear'});
+ }
+
+ getFacetValue(value: string) {
+ return this.page.getByRole('listitem').filter({hasText: value});
+ }
+
+ getFacetValueButton(value: string) {
+ return this.page.getByLabel(`Inclusion filter on ${value}`);
+ }
+
+ get clearFilter() {
+ return this.page.getByRole('button').filter({hasText: /Clear.*filter/});
+ }
+
+ get showMore() {
+ return this.page.getByLabel('Show more values');
+ }
+
+ get showLess() {
+ return this.page.getByLabel('Show less values');
+ }
+}
diff --git a/packages/atomic/storybookUtils/commerce-interface-wrapper.tsx b/packages/atomic/storybookUtils/commerce-interface-wrapper.tsx
index a2af87835bc..47b6a5d6c06 100644
--- a/packages/atomic/storybookUtils/commerce-interface-wrapper.tsx
+++ b/packages/atomic/storybookUtils/commerce-interface-wrapper.tsx
@@ -107,3 +107,35 @@ export const playExecuteFirstSearch: (
await searchInterface!.executeFirstSearch();
});
};
+
+export const playKeepOnlyFirstFacetOfType = (
+ facetType: string,
+ context: StoryContext
+) => {
+ const observer = new MutationObserver(() => {
+ const childNodes = Array.from(
+ context.canvasElement.querySelector('atomic-commerce-facets')
+ ?.childNodes || []
+ );
+
+ const allFacetsMatching = childNodes.filter((node) => {
+ return node.nodeName.toLowerCase() === facetType;
+ });
+
+ const allAtomicElementNotOfType = childNodes.filter((node) => {
+ return (
+ node.nodeName.toLowerCase().indexOf('atomic') !== -1 &&
+ node.nodeName.toLowerCase() !== facetType
+ );
+ });
+
+ allAtomicElementNotOfType.forEach((node) => node.remove());
+ allFacetsMatching.slice(1).forEach((node) => node.remove());
+ });
+
+ observer.observe(context.canvasElement, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ });
+};