Skip to content

Commit

Permalink
feat(atomic): add ipx facets (#2565)
Browse files Browse the repository at this point in the history
* fix(atomic): remove dangling div

* feat(atomic): Add facets in IPX

* Add generated files

* fix(atomic): remove unused style + story

* feat(atomic): add customizable class to atomic-icon-button

* fix(atomic): Use part to style icon

* fix(ipx): move facets to facets section

Co-authored-by: GitHub Actions Bot <>
  • Loading branch information
dguerinCoveo committed Nov 25, 2022
1 parent dece950 commit d58e5df
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 2 deletions.
30 changes: 30 additions & 0 deletions packages/atomic/src/components.d.ts
Expand Up @@ -676,6 +676,12 @@ export namespace Components {
}
interface AtomicIpxLayout {
}
interface AtomicIpxRefineModal {
"isOpen": boolean;
"openButton"?: HTMLElement;
}
interface AtomicIpxRefineToggle {
}
interface AtomicLayoutSection {
/**
* For column sections, the maximum horizontal space it should take. E.g. '300px'
Expand Down Expand Up @@ -2010,6 +2016,18 @@ declare global {
prototype: HTMLAtomicIpxLayoutElement;
new (): HTMLAtomicIpxLayoutElement;
};
interface HTMLAtomicIpxRefineModalElement extends Components.AtomicIpxRefineModal, HTMLStencilElement {
}
var HTMLAtomicIpxRefineModalElement: {
prototype: HTMLAtomicIpxRefineModalElement;
new (): HTMLAtomicIpxRefineModalElement;
};
interface HTMLAtomicIpxRefineToggleElement extends Components.AtomicIpxRefineToggle, HTMLStencilElement {
}
var HTMLAtomicIpxRefineToggleElement: {
prototype: HTMLAtomicIpxRefineToggleElement;
new (): HTMLAtomicIpxRefineToggleElement;
};
interface HTMLAtomicLayoutSectionElement extends Components.AtomicLayoutSection, HTMLStencilElement {
}
var HTMLAtomicLayoutSectionElement: {
Expand Down Expand Up @@ -2485,6 +2503,8 @@ declare global {
"atomic-insight-tabs": HTMLAtomicInsightTabsElement;
"atomic-insight-timeframe-facet": HTMLAtomicInsightTimeframeFacetElement;
"atomic-ipx-layout": HTMLAtomicIpxLayoutElement;
"atomic-ipx-refine-modal": HTMLAtomicIpxRefineModalElement;
"atomic-ipx-refine-toggle": HTMLAtomicIpxRefineToggleElement;
"atomic-layout-section": HTMLAtomicLayoutSectionElement;
"atomic-load-more-children-results": HTMLAtomicLoadMoreChildrenResultsElement;
"atomic-load-more-results": HTMLAtomicLoadMoreResultsElement;
Expand Down Expand Up @@ -3184,6 +3204,12 @@ declare namespace LocalJSX {
}
interface AtomicIpxLayout {
}
interface AtomicIpxRefineModal {
"isOpen"?: boolean;
"openButton"?: HTMLElement;
}
interface AtomicIpxRefineToggle {
}
interface AtomicLayoutSection {
/**
* For column sections, the maximum horizontal space it should take. E.g. '300px'
Expand Down Expand Up @@ -4223,6 +4249,8 @@ declare namespace LocalJSX {
"atomic-insight-tabs": AtomicInsightTabs;
"atomic-insight-timeframe-facet": AtomicInsightTimeframeFacet;
"atomic-ipx-layout": AtomicIpxLayout;
"atomic-ipx-refine-modal": AtomicIpxRefineModal;
"atomic-ipx-refine-toggle": AtomicIpxRefineToggle;
"atomic-layout-section": AtomicLayoutSection;
"atomic-load-more-children-results": AtomicLoadMoreChildrenResults;
"atomic-load-more-results": AtomicLoadMoreResults;
Expand Down Expand Up @@ -4343,6 +4371,8 @@ declare module "@stencil/core" {
"atomic-insight-tabs": LocalJSX.AtomicInsightTabs & JSXBase.HTMLAttributes<HTMLAtomicInsightTabsElement>;
"atomic-insight-timeframe-facet": LocalJSX.AtomicInsightTimeframeFacet & JSXBase.HTMLAttributes<HTMLAtomicInsightTimeframeFacetElement>;
"atomic-ipx-layout": LocalJSX.AtomicIpxLayout & JSXBase.HTMLAttributes<HTMLAtomicIpxLayoutElement>;
"atomic-ipx-refine-modal": LocalJSX.AtomicIpxRefineModal & JSXBase.HTMLAttributes<HTMLAtomicIpxRefineModalElement>;
"atomic-ipx-refine-toggle": LocalJSX.AtomicIpxRefineToggle & JSXBase.HTMLAttributes<HTMLAtomicIpxRefineToggleElement>;
"atomic-layout-section": LocalJSX.AtomicLayoutSection & JSXBase.HTMLAttributes<HTMLAtomicLayoutSectionElement>;
"atomic-load-more-children-results": LocalJSX.AtomicLoadMoreChildrenResults & JSXBase.HTMLAttributes<HTMLAtomicLoadMoreChildrenResultsElement>;
"atomic-load-more-results": LocalJSX.AtomicLoadMoreResults & JSXBase.HTMLAttributes<HTMLAtomicLoadMoreResultsElement>;
Expand Down
Expand Up @@ -43,6 +43,7 @@ export class AtomicIconButton {
icon={this.icon}
class="w-4 h-4 shrink-0"
aria-hidden="true"
part="icon"
></atomic-icon>
</Button>
{this.badge && <span part="badge">{this.badge}</span>}
Expand Down
Expand Up @@ -23,6 +23,10 @@ atomic-ipx-layout {
box-sizing: border-box;
}

atomic-layout-section[section='facets'] {
display: none;
}

atomic-layout-section[section='status'] {
padding: 1.5rem;
}
Expand Down
@@ -0,0 +1 @@
@import '../../common/refine-modal/refine-modal-common.pcss';
@@ -0,0 +1,170 @@
import {
BreadcrumbManager,
BreadcrumbManagerState,
buildBreadcrumbManager,
buildFacetManager,
buildQuerySummary,
FacetManager,
QuerySummary,
QuerySummaryState,
} from '@coveo/headless';
import {Component, h, State, Prop, Element, Watch, Host} from '@stencil/core';
import {rectEquals} from '../../../utils/dom-utils';
import {
BindStateToController,
InitializableComponent,
InitializeBindings,
} from '../../../utils/initialization-utils';
import {Button} from '../../common/button';
import {Hidden} from '../../common/hidden';
import {
getClonedFacetElements,
RefineModalCommon,
} from '../../common/refine-modal/refine-modal-common';
import {Bindings} from '../../search/atomic-search-interface/atomic-search-interface';

/**
* @internal
*/
@Component({
tag: 'atomic-ipx-refine-modal',
styleUrl: 'atomic-ipx-refine-modal.pcss',
shadow: true,
})
export class AtomicIPXRefineModal implements InitializableComponent {
@InitializeBindings() public bindings!: Bindings;
@Element() public host!: HTMLElement;

@BindStateToController('querySummary')
@State()
public querySummaryState!: QuerySummaryState;

@BindStateToController('breadcrumbManager')
@State()
public breadcrumbManagerState!: BreadcrumbManagerState;

@State()
public error!: Error;

@State()
public interfaceDimensions?: DOMRect;

@Prop({mutable: true}) openButton?: HTMLElement;

@Prop({reflect: true, mutable: true}) isOpen = false;

private breadcrumbManager!: BreadcrumbManager;
public querySummary!: QuerySummary;
private facetManager!: FacetManager;

@Watch('isOpen')
watchEnabled(isOpen: boolean) {
if (isOpen) {
if (!this.host.querySelector('div[slot="facets"]')) {
this.host.append(
getClonedFacetElements(
this.bindings.store.getFacetElements(),
this.facetManager
)
);
}
this.onAnimationFrame();
}
}

private onAnimationFrame() {
if (!this.isOpen) {
return;
}
if (this.dimensionChanged()) {
this.updateDimensions();
}
window.requestAnimationFrame(() => this.onAnimationFrame());
}

private dimensionChanged() {
if (!this.interfaceDimensions) {
return true;
}

return !rectEquals(
this.interfaceDimensions,
this.bindings.interfaceElement.getBoundingClientRect()
);
}

public updateDimensions() {
this.interfaceDimensions = this.bindings.interfaceElement
.getElementsByTagName('atomic-ipx-layout')[0]
.getBoundingClientRect();
}

public initialize() {
this.breadcrumbManager = buildBreadcrumbManager(this.bindings.engine);
this.facetManager = buildFacetManager(this.bindings.engine);
this.querySummary = buildQuerySummary(this.bindings.engine);
}

private renderHeader() {
return (
<div class="w-full flex justify-between mb-3">
<h2 class="text-2xl font-bold truncate">
{this.bindings.i18n.t('filters')}
</h2>
{this.breadcrumbManagerState.hasBreadcrumbs && (
<Button
onClick={() => this.breadcrumbManager.deselectAll()}
style="text-primary"
text={this.bindings.i18n.t('clear-all-filters')}
class="px-2 py-1"
></Button>
)}
</div>
);
}

private renderBody() {
if (!this.bindings.store.getFacetElements().length) {
return <Hidden></Hidden>;
}

return (
<aside slot="body" class="flex flex-col w-full adjust-for-scroll-bar">
{this.renderHeader()}
<slot name="facets"></slot>
</aside>
);
}

public render() {
return (
<Host>
{this.interfaceDimensions && (
<style>
{`atomic-modal::part(backdrop) {
top: ${this.interfaceDimensions.top}px;
left: ${this.interfaceDimensions.left}px;
width: ${this.interfaceDimensions.width}px;
height: ${this.interfaceDimensions.height}px;
}`}
</style>
)}
<RefineModalCommon
bindings={this.bindings}
host={this.host}
isOpen={this.isOpen}
onClose={() => (this.isOpen = false)}
querySummaryState={this.querySummaryState}
title={this.bindings.i18n.t('filters')}
openButton={this.openButton}
>
{this.renderBody()}
</RefineModalCommon>
</Host>
);
}

public componentDidLoad() {
this.host.style.display = '';
}
}
@@ -0,0 +1,20 @@
@import '../../../global/global.pcss';

atomic-icon-button {
height: 100%;
&::part(button) {
height: 100%;
width: auto;
}
&::part(badge) {
width: 1.25rem;
height: 1.25rem;
line-height: 1.25rem;
font-size: 0.7rem;
}

&::part(icon) {
width: 1.25rem;
height: 1.25rem;
}
}
@@ -0,0 +1,105 @@
import {
BreadcrumbManager,
BreadcrumbManagerState,
buildBreadcrumbManager,
buildSearchStatus,
SearchStatus,
SearchStatusState,
} from '@coveo/headless';
import {Component, h, State, Element} from '@stencil/core';
import FilterIcon from '../../../images/filter.svg';
import {
InitializeBindings,
BindStateToController,
} from '../../../utils/initialization-utils';
import {Bindings} from '../../search/atomic-search-interface/atomic-search-interface';

/**
* @internal
*/
@Component({
tag: 'atomic-ipx-refine-toggle',
styleUrl: 'atomic-ipx-refine-toggle.pcss',
shadow: true,
})
export class AtomicIPXRefineToggle {
@InitializeBindings() public bindings!: Bindings;
@Element() public host!: HTMLElement;

@State() public error!: Error;

@BindStateToController('breadcrumbManager')
@State()
private breadcrumbManagerState!: BreadcrumbManagerState;

@BindStateToController('searchStatus')
@State()
private searchStatusState!: SearchStatusState;

public breadcrumbManager!: BreadcrumbManager;
public searchStatus!: SearchStatus;
private modalRef?: HTMLAtomicIpxRefineModalElement;
private buttonRef?: HTMLButtonElement;

private get numberOfBreadcrumbs(): number {
return [
...this.breadcrumbManagerState.facetBreadcrumbs,
...this.breadcrumbManagerState.categoryFacetBreadcrumbs,
...this.breadcrumbManagerState.numericFacetBreadcrumbs,
...this.breadcrumbManagerState.dateFacetBreadcrumbs,
...this.breadcrumbManagerState.staticFilterBreadcrumbs,
].length;
}

public initialize() {
this.breadcrumbManager = buildBreadcrumbManager(this.bindings.engine);
this.searchStatus = buildSearchStatus(this.bindings.engine);
}

private enableModal() {
this.modalRef && (this.modalRef.isOpen = true);
}

private loadModal() {
if (this.modalRef) {
return;
}

this.modalRef = document.createElement('atomic-ipx-refine-modal');
this.host.parentElement?.insertAdjacentElement(
'beforebegin',
this.modalRef
);
this.modalRef.openButton = this.buttonRef;
}

public render() {
return (
<atomic-icon-button
tooltip={this.bindings.i18n.t('filters')}
icon={FilterIcon}
disabled={
!this.searchStatusState.hasResults && !this.numberOfBreadcrumbs
}
labelI18nKey="sort"
clickCallback={() => {
this.bindings.store.waitUntilAppLoaded(() => {
this.enableModal();
});
}}
buttonRef={(button?: HTMLButtonElement) => {
if (!button) {
return;
}
this.buttonRef = button;
this.loadModal();
}}
badge={
this.breadcrumbManagerState.hasBreadcrumbs ? (
<slot>{this.numberOfBreadcrumbs.toString()}</slot>
) : undefined
}
/>
);
}
}

0 comments on commit d58e5df

Please sign in to comment.