Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,42 @@ import {keyed} from 'lit/directives/keyed.js';
import {map} from 'lit/directives/map.js';
import {ref} from 'lit/directives/ref.js';
import {when} from 'lit/directives/when.js';
import {bindStateToController} from '@/src/decorators/bind-state.js';
import {bindingGuard} from '@/src/decorators/binding-guard.js';
import {bindings} from '@/src/decorators/bindings.js';
import {errorGuard} from '@/src/decorators/error-guard.js';
import type {InitializableComponent} from '@/src/decorators/types.js';
import {withTailwindStyles} from '@/src/decorators/with-tailwind-styles.js';
import {ChildrenUpdateCompleteMixin} from '@/src/mixins/children-update-complete-mixin.js';
import {FocusTargetController} from '@/src/utils/accessibility-utils.js';
import {randomID} from '@/src/utils/utils.js';
import {renderItemPlaceholders} from '../../common/atomic-result-placeholder/item-placeholders.js';
import {createAppLoadedListener} from '../../common/interface/store.js';
import {renderDisplayWrapper} from '../../common/item-list/display-wrapper.js';
import {renderGridLayout} from '../../common/item-list/grid-layout.js';
import type {CommerceBindings} from '@/src/components/commerce/atomic-commerce-interface/atomic-commerce-interface';
import type {SelectChildProductEventArgs} from '@/src/components/commerce/atomic-product-children/select-child-product-event';
import {ProductTemplateProvider} from '@/src/components/commerce/product-list/product-template-provider';
import {renderItemPlaceholders} from '@/src/components/common/atomic-result-placeholder/item-placeholders';
import {createAppLoadedListener} from '@/src/components/common/interface/store';
import {renderDisplayWrapper} from '@/src/components/common/item-list/display-wrapper';
import {renderGridLayout} from '@/src/components/common/item-list/grid-layout';
import {renderItemList} from '@/src/components/common/item-list/item-list';
import {
ItemListCommon,
type ItemRenderingFunction,
} from '../../common/item-list/item-list-common.js';
import gridDisplayStyles from '../../common/item-list/styles/grid-display.tw.css';
import listDisplayStyles from '../../common/item-list/styles/list-display.tw.css';
import placeholderStyles from '../../common/item-list/styles/placeholders.tw.css';
import tableDisplayStyles from '../../common/item-list/styles/table-display.tw.css';
} from '@/src/components/common/item-list/item-list-common';
import gridDisplayStyles from '@/src/components/common/item-list/styles/grid-display.tw.css';
import listDisplayStyles from '@/src/components/common/item-list/styles/list-display.tw.css';
import placeholderStyles from '@/src/components/common/item-list/styles/placeholders.tw.css';
import tableDisplayStyles from '@/src/components/common/item-list/styles/table-display.tw.css';
import {
renderTableData,
renderTableLayout,
renderTableRow,
} from '../../common/item-list/table-layout.js';
} from '@/src/components/common/item-list/table-layout';
import {
getItemListDisplayClasses,
type ItemDisplayDensity,
type ItemDisplayImageSize,
type ItemDisplayLayout,
} from '../../common/layout/display-options.js';
import type {CommerceBindings} from '../atomic-commerce-interface/atomic-commerce-interface.js';
import type {SelectChildProductEventArgs} from '../atomic-product-children/select-child-product-event.js';
import {ProductTemplateProvider} from '../product-list/product-template-provider.js';
} from '@/src/components/common/layout/display-options';
import {bindStateToController} from '@/src/decorators/bind-state';
import {bindingGuard} from '@/src/decorators/binding-guard';
import {bindings} from '@/src/decorators/bindings';
import {errorGuard} from '@/src/decorators/error-guard';
import type {InitializableComponent} from '@/src/decorators/types';
import {withTailwindStyles} from '@/src/decorators/with-tailwind-styles';
import {ChildrenUpdateCompleteMixin} from '@/src/mixins/children-update-complete-mixin';
import {FocusTargetController} from '@/src/utils/accessibility-utils';
import {randomID} from '@/src/utils/utils';

/**
* The `atomic-commerce-product-list` component is responsible for displaying products.
Expand Down Expand Up @@ -250,55 +251,60 @@ export class AtomicCommerceProductList
@bindingGuard()
@errorGuard()
render() {
return html`${when(
this.shouldRender,
() =>
html`${when(
this.templateHasError,
() => html`<slot></slot>`,
() => {
const listClasses = this.computeListDisplayClasses();
const productClasses = `${listClasses} ${!this.isEveryProductReady && 'hidden'}`;

// Products must be rendered immediately (though hidden) to start their initialization and loading processes.
// If we wait to render products until placeholders are removed, the components won't begin loading until then,
// causing a longer delay. The `isEveryProductsReady` flag hides products while preserving placeholders,
// then removes placeholders once products are fully loaded to prevent content flash.
return html`
${when(this.isAppLoaded, () =>
renderDisplayWrapper({
props: {listClasses: productClasses, display: this.display},
})(
html`${when(
this.display === 'grid',
() => this.renderGrid(),
() =>
html`${when(
this.display === 'list',
() => this.renderList(),
() => this.renderTable()
)}`
)}`
)
)}
${when(!this.isEveryProductReady, () =>
renderDisplayWrapper({
props: {listClasses, display: this.display},
})(
renderItemPlaceholders({
props: {
density: this.density,
display: this.display,
imageSize: this.imageSize,
numberOfPlaceholders: this.numberOfPlaceholders,
},
})
)
)}
`;
}
)}`,
() => nothing
return html`${renderItemList({
props: {
hasError: this.summaryState.hasError,
hasItems: this.summaryState.hasProducts,
hasTemplate: this.resultTemplateRegistered,
firstRequestExecuted: this.summaryState.firstRequestExecuted,
templateHasError: this.templateHasError,
},
})(
html`${when(
this.templateHasError,
() => html`<slot></slot>`,
() => {
const listClasses = this.computeListDisplayClasses();
const productClasses = `${listClasses} ${!this.isEveryProductReady && 'hidden'}`;

// Products must be rendered immediately (though hidden) to start their initialization and loading processes.
// If we wait to render products until placeholders are removed, the components won't begin loading until then,
// causing a longer delay. The `isEveryProductsReady` flag hides products while preserving placeholders,
// then removes placeholders once products are fully loaded to prevent content flash.
return html`
${when(this.isAppLoaded, () =>
renderDisplayWrapper({
props: {listClasses: productClasses, display: this.display},
})(
html`${when(
this.display === 'grid',
() => this.renderGrid(),
() =>
html`${when(
this.display === 'list',
() => this.renderList(),
() => this.renderTable()
)}`
)}`
)
)}
${when(!this.isEveryProductReady, () =>
renderDisplayWrapper({
props: {listClasses, display: this.display},
})(
renderItemPlaceholders({
props: {
density: this.density,
display: this.display,
imageSize: this.imageSize,
numberOfPlaceholders: this.numberOfPlaceholders,
},
})
)
)}
`;
}
)}`
)}`;
}

Expand Down Expand Up @@ -543,14 +549,6 @@ export class AtomicCommerceProductList
}
return this.nextNewResultTarget;
}

private get shouldRender() {
return (
!this.summaryState.hasError &&
this.resultTemplateRegistered &&
(!this.summaryState.firstRequestExecuted || this.summaryState.hasProducts)
);
}
}

declare global {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {html, nothing, render, type TemplateResult} from 'lit';
import {describe, expect, it} from 'vitest';
import {type ItemListProps, renderItemList} from './item-list';

describe('renderItemList', () => {
const renderItemListWithChildren = (
props: Partial<ItemListProps> = {},
children: TemplateResult | typeof nothing = nothing
) => {
const container = document.createElement('div');
document.body.appendChild(container);

render(
renderItemList({
props: {
hasError: false,
hasItems: true,
hasTemplate: true,
firstRequestExecuted: true,
templateHasError: false,
...props,
},
})(children),
container
);

return container;
};

const testChild = html`<div class="child">Test Child</div>`;

it.each([
{
description: 'all conditions are met',
props: {},
},
{
description: '#firstRequestExecuted is false and #hasItems is true',
props: {firstRequestExecuted: false, hasItems: true},
},
{
description: '#firstRequestExecuted is false and #hasItems is false',
props: {firstRequestExecuted: false, hasItems: false},
},
])('should render children when $description', ({props}) => {
const container = renderItemListWithChildren(props, testChild);
const child = container.querySelector('.child');

expect(child).toBeInTheDocument();
expect(child?.textContent).toBe('Test Child');

container.remove();
});

it.each([
{
description: '#hasError is true',
props: {hasError: true},
},
{
description: '#hasTemplate is false',
props: {hasTemplate: false},
},
{
description: '#firstRequestExecuted is true and #hasItems is false',
props: {firstRequestExecuted: true, hasItems: false},
},
])('should not render children when $description', ({props}) => {
const container = renderItemListWithChildren(props, testChild);
const child = container.querySelector('.child');

expect(child).not.toBeInTheDocument();

container.remove();
});
});
22 changes: 22 additions & 0 deletions packages/atomic/src/components/common/item-list/item-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {nothing} from 'lit';
import type {FunctionalComponentWithChildren} from '@/src/utils/functional-component-utils';

export interface ItemListProps {
hasError: boolean;
hasItems: boolean;
hasTemplate: boolean;
firstRequestExecuted: boolean;
templateHasError: boolean;
}

export const renderItemList: FunctionalComponentWithChildren<ItemListProps> =
({props}) =>
(children) => {
const {hasError, hasItems, firstRequestExecuted, hasTemplate} = props;

if (hasError || (firstRequestExecuted && !hasItems) || !hasTemplate) {
return nothing;
}

return children;
};
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -488,5 +488,28 @@ describe('renderTableData', () => {
expect(renderItem).toHaveBeenNthCalledWith(2, tableElement2);
});
});

describe('when #templateContentForFirstItem is null', () => {
it('should not render any td elements', async () => {
const tableData = await tableDataFixture({
templateContentForFirstItem: null as unknown as DocumentFragment,
});

const tdElements = tableData.querySelectorAll('td');

expect(tdElements.length).toBe(0);
});

it('should not call #renderItem', async () => {
const renderItem = vi.fn();

await tableDataFixture({
templateContentForFirstItem: null as unknown as DocumentFragment,
renderItem,
});

expect(renderItem).not.toHaveBeenCalled();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,11 @@ const getFieldTableColumnsFromRenderingFunction = (

const getFieldTableColumnsFromHTMLTemplate = (
props: Pick<TableLayoutProps, 'templateContentForFirstItem'>
): Element[] =>
Array.from(
): Element[] => {
if (!props.templateContentForFirstItem) {
return [];
}
return Array.from(
props.templateContentForFirstItem.querySelectorAll(tableElementTagName)
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import {randomID} from '../../../../utils/utils';
import {ResultsPlaceholdersGuard} from '../../../common/atomic-result-placeholder/stencil-placeholders';
import {extractUnfoldedItem} from '../../../common/item-list/unfolded-item';
import {createAppLoadedListener} from '../../../common/interface/store';
import {ItemDisplayGuard} from '../../../common/item-list/item-display-guard';
import {ItemDisplayGuard} from '../../../common/item-list/stencil-item-display-guard';
import {FoldedItemListStateContextEvent} from '../../../common/item-list/item-list-decorators';
import {ItemListGuard} from '../../../common/item-list/item-list-guard';
import {ItemListGuard} from '../../../common/item-list/stencil-item-list-guard';
import {ResultTemplateProvider} from '../../../common/item-list/result-template-provider';
import {DisplayWrapper} from '../../../common/item-list/stencil-display-wrapper';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import {FocusTargetController} from '../../../../utils/stencil-accessibility-uti
import {randomID} from '../../../../utils/utils';
import {ResultsPlaceholdersGuard} from '../../../common/atomic-result-placeholder/stencil-placeholders';
import {createAppLoadedListener} from '../../../common/interface/store';
import {ItemDisplayGuard} from '../../../common/item-list/item-display-guard';
import {ItemListGuard} from '../../../common/item-list/item-list-guard';
import {ItemDisplayGuard} from '../../../common/item-list/stencil-item-display-guard';
import {ItemListGuard} from '../../../common/item-list/stencil-item-list-guard';
import {ResultTemplateProvider} from '../../../common/item-list/result-template-provider';
import {DisplayWrapper} from '../../../common/item-list/stencil-display-wrapper';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {FocusTargetController} from '../../../../utils/stencil-accessibility-uti
import {randomID} from '../../../../utils/utils';
import {ResultsPlaceholdersGuard} from '../../../common/atomic-result-placeholder/stencil-placeholders';
import {createAppLoadedListener} from '../../../common/interface/store';
import {ItemDisplayGuard} from '../../../common/item-list/item-display-guard';
import {ItemDisplayGuard} from '../../../common/item-list/stencil-item-display-guard';
import {ResultTemplateProvider} from '../../../common/item-list/result-template-provider';
import {DisplayGrid} from '../../../common/item-list/stencil-display-grid';
import {DisplayWrapper} from '../../../common/item-list/stencil-display-wrapper';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {FocusTargetController} from '../../../utils/stencil-accessibility-utils'
import {randomID} from '../../../utils/utils';
import {ResultsPlaceholdersGuard} from '../../common/atomic-result-placeholder/stencil-placeholders';
import {createAppLoadedListener} from '../../common/interface/store';
import {ItemDisplayGuard} from '../../common/item-list/item-display-guard';
import {ItemDisplayGuard} from '../../common/item-list/stencil-item-display-guard';
import {ResultTemplateProvider} from '../../common/item-list/result-template-provider';
import {DisplayGrid} from '../../common/item-list/stencil-display-grid';
import {DisplayWrapper} from '../../common/item-list/stencil-display-wrapper';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ import {randomID} from '../../../../utils/utils';
import {ResultsPlaceholdersGuard} from '../../../common/atomic-result-placeholder/stencil-placeholders';
import {extractUnfoldedItem} from '../../../common/item-list/unfolded-item';
import {createAppLoadedListener} from '../../../common/interface/store';
import {ItemDisplayGuard} from '../../../common/item-list/item-display-guard';
import {ItemListGuard} from '../../../common/item-list/item-list-guard';
import {ItemDisplayGuard} from '../../../common/item-list/stencil-item-display-guard';
import {ItemListGuard} from '../../../common/item-list/stencil-item-list-guard';
import {ResultTemplateProvider} from '../../../common/item-list/result-template-provider';
import {DisplayWrapper} from '../../../common/item-list/stencil-display-wrapper';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import {FocusTargetController} from '../../../../utils/stencil-accessibility-uti
import {randomID} from '../../../../utils/utils';
import {ResultsPlaceholdersGuard} from '../../../common/atomic-result-placeholder/stencil-placeholders';
import {createAppLoadedListener} from '../../../common/interface/store';
import {ItemDisplayGuard} from '../../../common/item-list/item-display-guard';
import {ItemListGuard} from '../../../common/item-list/item-list-guard';
import {ItemDisplayGuard} from '../../../common/item-list/stencil-item-display-guard';
import {ItemListGuard} from '../../../common/item-list/stencil-item-list-guard';
import {ResultTemplateProvider} from '../../../common/item-list/result-template-provider';
import {DisplayGrid} from '../../../common/item-list/stencil-display-grid';
import {
Expand Down
Loading