Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(refine-modal): include facets from atomic-external #4219

Merged
merged 10 commits into from
Aug 14, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -473,14 +473,14 @@ export declare interface AtomicDidYouMean extends Components.AtomicDidYouMean {}


@ProxyCmp({
inputs: ['selector']
inputs: ['boundInterface', 'selector']
})
@Component({
selector: 'atomic-external',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['selector'],
inputs: ['boundInterface', 'selector'],
})
export class AtomicExternal {
protected el: HTMLElement;
Expand Down
10 changes: 10 additions & 0 deletions packages/atomic/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ItemDisplayBasicLayout, ItemDisplayDensity, ItemDisplayImageSize, ItemD
import { ItemRenderingFunction } from "./components/common/item-list/item-list-common";
import { RedirectionPayload } from "./components/search/atomic-search-box/redirection-payload";
import { AriaLabelGenerator } from "./components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/atomic-commerce-search-box-instant-products";
import { AtomicInterface } from "./utils/initialization-utils";
import { unknown as AnyBindings, i18nCompatibilityVersion as i18nCompatibilityVersion1, ItemDisplayBasicLayout as ItemDisplayBasicLayout1, ItemDisplayDensity as ItemDisplayDensity1, ItemDisplayImageSize as ItemDisplayImageSize1, ItemRenderingFunction as ItemRenderingFunction1, ItemTarget as ItemTarget1 } from "./components";
import { AnyBindings as AnyBindings1 } from "./components/common/interface/bindings";
import { NumberInputType } from "./components/common/facets/facet-number-input/number-input-type";
Expand Down Expand Up @@ -46,6 +47,7 @@ export { ItemDisplayBasicLayout, ItemDisplayDensity, ItemDisplayImageSize, ItemD
export { ItemRenderingFunction } from "./components/common/item-list/item-list-common";
export { RedirectionPayload } from "./components/search/atomic-search-box/redirection-payload";
export { AriaLabelGenerator } from "./components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/atomic-commerce-search-box-instant-products";
export { AtomicInterface } from "./utils/initialization-utils";
export { unknown as AnyBindings, i18nCompatibilityVersion as i18nCompatibilityVersion1, ItemDisplayBasicLayout as ItemDisplayBasicLayout1, ItemDisplayDensity as ItemDisplayDensity1, ItemDisplayImageSize as ItemDisplayImageSize1, ItemRenderingFunction as ItemRenderingFunction1, ItemTarget as ItemTarget1 } from "./components";
export { AnyBindings as AnyBindings1 } from "./components/common/interface/bindings";
export { NumberInputType } from "./components/common/facets/facet-number-input/number-input-type";
Expand Down Expand Up @@ -800,6 +802,10 @@ export namespace Components {
* The `atomic-external` component allows components defined outside of the `atomic-search-interface` to initialize.
*/
interface AtomicExternal {
/**
* Represents the bound interface for the AtomicExternal component.
*/
"boundInterface"?: AtomicInterface;
/**
* The CSS selector that identifies the `atomic-search-interface` component with which to initialize the external components.
*/
Expand Down Expand Up @@ -6466,6 +6472,10 @@ declare namespace LocalJSX {
* The `atomic-external` component allows components defined outside of the `atomic-search-interface` to initialize.
*/
interface AtomicExternal {
/**
* Represents the bound interface for the AtomicExternal component.
*/
"boundInterface"?: AtomicInterface;
/**
* The CSS selector that identifies the `atomic-search-interface` component with which to initialize the external components.
*/
Expand Down
19 changes: 9 additions & 10 deletions packages/atomic/src/components/common/facets/facet-common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,16 @@ export function getAutomaticFacetGenerator(
);
}

const get2DMatrix = (xSize: number, ySize: number = 0) =>
new Array(xSize).fill(null).map(() => new Array(ySize));

function findIndiceOfParent(
function findFacetParent(
louis-bompart marked this conversation as resolved.
Show resolved Hide resolved
facet: BaseFacetElement,
parents: (HTMLElement | null)[]
) {
for (let i = 0; i < parents.length; i++) {
if (parents[i]?.contains(facet)) {
return i;
return parents[i];
}
}
return parents.length;
return null;
}

/**
Expand All @@ -131,11 +128,13 @@ function findIndiceOfParent(
export function triageFacetsByParents(
facets: BaseFacetElement[],
...parents: (HTMLElement | null)[]
) {
const sortedFacets: BaseFacetElement[][] = get2DMatrix(parents.length + 1);
): Map<HTMLElement | null, BaseFacetElement[]> {
const sortedFacets: Map<HTMLElement | null, BaseFacetElement[]> = new Map(
parents.concat([null]).map((parent) => [parent, []])
);
for (const facet of facets) {
const indice = findIndiceOfParent(facet, parents);
sortedFacets[indice].push(facet);
const parent = findFacetParent(facet, parents);
sortedFacets.get(parent)!.push(facet);
}
return sortedFacets;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const FacetHeader: FunctionalComponent<FacetHeaderProps> = (props) => {
style="text-transparent"
part="label-button"
class="flex w-full justify-between rounded-none px-2 py-1 text-lg font-bold"
title={props.isCollapsed ? expandFacet : collapseFacet}
ariaLabel={props.isCollapsed ? expandFacet : collapseFacet}
louis-bompart marked this conversation as resolved.
Show resolved Hide resolved
onClick={() => props.onToggleCollapse()}
ariaExpanded={(!props.isCollapsed).toString()}
ref={props.headerRef}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Component, Prop, Listen} from '@stencil/core';
import {buildCustomEvent} from '../../../utils/event-utils';
import {
AtomicInterface,
InitializeEvent,
initializeEventName,
} from '../../../utils/initialization-utils';
Expand All @@ -21,7 +22,7 @@ export class AtomicExternal {
public handleInitialization(event: InitializeEvent) {
event.preventDefault();
event.stopPropagation();
this.interface.dispatchEvent(
this.#interface.dispatchEvent(
buildCustomEvent(initializeEventName, event.detail)
);
}
Expand All @@ -30,19 +31,26 @@ export class AtomicExternal {
public handleScrollToTop(event: CustomEvent) {
event.preventDefault();
event.stopPropagation();
this.interface.dispatchEvent(
this.#interface.dispatchEvent(
buildCustomEvent('atomic/scrollToTop', event.detail)
);
}

private get interface() {
const element = document.querySelector(this.selector);
if (!element) {
throw new Error(
`Cannot find interface element with selector "${this.selector}"`
);
/**
* Represents the bound interface for the AtomicExternal component.
*/
@Prop({mutable: true}) boundInterface?: AtomicInterface;
louis-bompart marked this conversation as resolved.
Show resolved Hide resolved

get #interface() {
louis-bompart marked this conversation as resolved.
Show resolved Hide resolved
if (!this.boundInterface) {
this.boundInterface = document.querySelector(this.selector) ?? undefined;
if (!this.boundInterface) {
throw new Error(
`Cannot find interface element with selector "${this.selector}"`
);
}
}

return element;
return this.boundInterface!;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '@coveo/headless';
import {Component, h, State, Prop, Element, Watch} from '@stencil/core';
import {
AtomicInterface,
BindStateToController,
InitializableComponent,
InitializeBindings,
Expand Down Expand Up @@ -144,26 +145,41 @@ export class AtomicRefineModal implements InitializableComponent {
this.addFacetColumnStyling(divSlot);

const facets = this.bindings.store.getFacetElements() as BaseFacetElement[];
const atomicSearchInterface = this.host.closest('atomic-search-interface')!;
const facetsSection = findSection(atomicSearchInterface, 'facets');
const horizontalFacetsSection = findSection(
atomicSearchInterface,
'horizontal-facets'
const boundInterfaces = this.getBoundInterfaces().sort(
sortByDocumentPosition
);
const facetsSection = [];
const horizontalFacetsSection = [];
for (const boundInterface of boundInterfaces) {
const facetSection = findSection(boundInterface, 'facets');
if (facetSection) {
facetsSection.push(facetSection);
}
const horizontalFacetSection = findSection(
boundInterface,
'horizontal-facets'
);
if (horizontalFacetSection) {
horizontalFacetsSection.push(horizontalFacetSection);
}
}
const triagedFacets = triageFacetsByParents(
facets,
horizontalFacetsSection,
facetsSection
...horizontalFacetsSection,
...facetsSection
);
const [horizontalFacetsSectionFacets, facetsSectionFacets, orphanedFacets] =
triagedFacets.map((facetsArray) =>
facetsArray.sort(sortByDocumentPosition)
for (const triagedFacet of triagedFacets.values()) {
triagedFacet.sort(sortByDocumentPosition);
}

const sortedFacets = [];
for (let i = 0; i < boundInterfaces.length; i++) {
sortedFacets.push(...(triagedFacets.get(facetsSection[i]) || []));
sortedFacets.push(
...(triagedFacets.get(horizontalFacetsSection[i]) || [])
);
const sortedFacets = [
...facetsSectionFacets,
...horizontalFacetsSectionFacets,
...orphanedFacets,
];
}
sortedFacets.push(...(triagedFacets.get(null) || []));

const {visibleFacets, invisibleFacets} = sortFacetVisibility(
sortedFacets,
Expand All @@ -190,6 +206,22 @@ export class AtomicRefineModal implements InitializableComponent {
return divSlot;
}

private getBoundInterfaces(): AtomicInterface[] {
const mainInterface: AtomicInterface | null =
this.host.closest('atomic-search-interface') ??
this.host.closest('atomic-external')?.boundInterface ??
null;
if (!mainInterface) {
throw new Error('Cannot find bound interface');
}
const boundExternalInterfaces = Array.from(
document.querySelectorAll('atomic-external')
).filter(
(atomicExternal) => atomicExternal.boundInterface === mainInterface
);
return [...boundExternalInterfaces, mainInterface];
}

private cloneFacets(facets: BaseFacetElement[]): BaseFacetElement[] {
return facets.map((facet, i) => {
facet.classList.remove(popoverClass);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type {Page} from '@playwright/test';
import {BasePageObject} from '../../../../../playwright-utils/base-page-object';

export class RefineModalPageObject extends BasePageObject<'atomic-refine-toggle'> {
constructor(page: Page) {
super(page, 'atomic-refine-toggle');
}

get modal() {
return this.page.getByRole('dialog', {name: 'Sort & Filter'});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const meta: Meta = {
title: 'Atomic/RefineToggle',
id: 'atomic-refine-toggle',
render: renderComponent,
decorators: [decorator],
parameters,
play,
};
Expand All @@ -38,5 +37,59 @@ export const Default: Story = {
></atomic-facet>
</div>
`,
decorator,
],
};

export const WithAtomicExternals: Story = {
name: 'With multiple atomic-external',
decorators: [
(story) => html`
<atomic-search-interface id="foo" data-testid="root-interface">
louis-bompart marked this conversation as resolved.
Show resolved Hide resolved
<h1>Search Interface</h1>
<atomic-layout-section section="horizontal-facets">
<atomic-facet field="author" label="facet2"></atomic-facet>
</atomic-layout-section>
<atomic-layout-section section="facets">
<atomic-facet field="author" label="facet1"></atomic-facet>
</atomic-layout-section>
<atomic-facet field="author" label="facet7"></atomic-facet>
</atomic-search-interface>
<atomic-external selector="#foo">
<h1>External 1</h1>
<atomic-layout-section section="horizontal-facets">
<atomic-facet field="author" label="facet4"></atomic-facet>
</atomic-layout-section>
<atomic-layout-section section="facets">
<atomic-facet field="author" label="facet3"></atomic-facet>
</atomic-layout-section>
<atomic-facet field="author" label="facet8"></atomic-facet>
</atomic-external>
<atomic-external selector="#bar">
<h1>External 2</h1>
<p>Not bound to the search interface</p>
<atomic-layout-section section="horizontal-facets">
<atomic-facet field="author" label="facet01"></atomic-facet>
</atomic-layout-section>
<atomic-layout-section section="facets">
<atomic-facet field="author" label="facet02"></atomic-facet>
</atomic-layout-section>
<atomic-facet field="author" label="facet03"></atomic-facet>
</atomic-external>
<atomic-external selector="#foo">
<h1>External 3</h1>
<atomic-layout-section section="horizontal-facets">
<atomic-facet field="author" label="facet6"></atomic-facet>
</atomic-layout-section>
<atomic-layout-section section="facets">
<atomic-facet field="author" label="facet5"></atomic-facet>
</atomic-layout-section>
<atomic-facet field="author" label="facet9"></atomic-facet>
</atomic-external>
<atomic-external selector="#foo">
<h1>External 4</h1>
${story()}
</atomic-external>
`,
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {test, expect} from './fixture';

test.describe('atomic-refine-toggle', () => {
test.beforeEach(async ({refineToggle, page}) => {
await refineToggle.load({
story: 'with-atomic-externals',
});
await page.setViewportSize({width: 400, height: 845});
});
test.describe('when the button is clicked', () => {
test.beforeEach(async ({refineToggle}) => {
await refineToggle.button.click();
});

test('should open the refine modal', async ({refineModal}) => {
await expect(refineModal.modal).toBeVisible();
});

test('should display the facets in the right order', async ({facet}) => {
await expect(facet.expandButtons).toHaveText([
'facet1',
'facet2',
'facet3',
'facet4',
'facet5',
'facet6',
'facet7',
'facet8',
'facet9',
]);
louis-bompart marked this conversation as resolved.
Show resolved Hide resolved
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {test as base} from '@playwright/test';
import {
AxeFixture,
makeAxeBuilder,
} from '../../../../../playwright-utils/base-fixture';
import {RefineModalPageObject} from '../../atomic-refine-modal/e2e/page-object';
import {AtomicFacetPageObject} from '../../facets/atomic-facet/e2e/page-object';
import {RefineTogglePageObject} from './page-object';

type MyFixtures = {
refineToggle: RefineTogglePageObject;
refineModal: RefineModalPageObject;
facet: AtomicFacetPageObject;
};

export const test = base.extend<MyFixtures & AxeFixture>({
makeAxeBuilder,
refineToggle: async ({page}, use) => {
await use(new RefineTogglePageObject(page));
},
refineModal: async ({page}, use) => {
await use(new RefineModalPageObject(page));
},
facet: async ({page}, use) => {
await use(new AtomicFacetPageObject(page));
},
});

export {expect} from '@playwright/test';
Loading
Loading