diff --git a/.changeset/quantic-load-more-results.md b/.changeset/quantic-load-more-results.md
new file mode 100644
index 00000000000..52d0e91b55c
--- /dev/null
+++ b/.changeset/quantic-load-more-results.md
@@ -0,0 +1,5 @@
+---
+'@coveo/quantic': minor
+---
+
+Added the `quantic-load-more-results` component, allowing users to load additional results, if more are available, by clicking a button. It displays a running count of results shown versus the total available, and a progress bar, and announces through an aria-live region when the last batch of results has been loaded. This is an alternative to `quantic-pager`; use one or the other, not both on the same page.
diff --git a/packages/quantic/force-app/examples/main/lwc/exampleQuanticLoadMoreResults/exampleQuanticLoadMoreResults.html b/packages/quantic/force-app/examples/main/lwc/exampleQuanticLoadMoreResults/exampleQuanticLoadMoreResults.html
new file mode 100644
index 00000000000..8e4bdb1c1d7
--- /dev/null
+++ b/packages/quantic/force-app/examples/main/lwc/exampleQuanticLoadMoreResults/exampleQuanticLoadMoreResults.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/quantic/force-app/examples/main/lwc/exampleQuanticLoadMoreResults/exampleQuanticLoadMoreResults.js b/packages/quantic/force-app/examples/main/lwc/exampleQuanticLoadMoreResults/exampleQuanticLoadMoreResults.js
new file mode 100644
index 00000000000..9e888f038ce
--- /dev/null
+++ b/packages/quantic/force-app/examples/main/lwc/exampleQuanticLoadMoreResults/exampleQuanticLoadMoreResults.js
@@ -0,0 +1,25 @@
+import {api, LightningElement, track} from 'lwc';
+
+export default class ExampleQuanticLoadMoreResults extends LightningElement {
+ @api engineId = 'quantic-load-more-results-engine';
+ @track config = {};
+ isConfigured = false;
+
+ pageTitle = 'Quantic Load More Results';
+ pageDescription =
+ 'The Quantic Load More Results component allows users to load additional results, if more are available, by clicking a button.';
+ options = [
+ {
+ attribute: 'useCase',
+ label: 'Use Case',
+ description:
+ 'Define which use case to test. Possible values are: search, insight',
+ defaultValue: 'search',
+ },
+ ];
+
+ handleTryItNow(evt) {
+ this.config = evt.detail;
+ this.isConfigured = true;
+ }
+}
diff --git a/packages/quantic/force-app/examples/main/lwc/exampleQuanticLoadMoreResults/exampleQuanticLoadMoreResults.js-meta.xml b/packages/quantic/force-app/examples/main/lwc/exampleQuanticLoadMoreResults/exampleQuanticLoadMoreResults.js-meta.xml
new file mode 100644
index 00000000000..44f9004fefc
--- /dev/null
+++ b/packages/quantic/force-app/examples/main/lwc/exampleQuanticLoadMoreResults/exampleQuanticLoadMoreResults.js-meta.xml
@@ -0,0 +1,9 @@
+
+
+ 64.0
+ true
+
+ lightningCommunity__Page
+ lightningCommunity__Default
+
+
diff --git a/packages/quantic/force-app/main/default/labels/CustomLabels.labels-meta.xml b/packages/quantic/force-app/main/default/labels/CustomLabels.labels-meta.xml
index bdecb826e6c..7c2f98618d4 100644
--- a/packages/quantic/force-app/main/default/labels/CustomLabels.labels-meta.xml
+++ b/packages/quantic/force-app/main/default/labels/CustomLabels.labels-meta.xml
@@ -56,6 +56,34 @@
false
No Results for potato
+
+ quantic_LoadMoreResults
+ Load more results
+ en_US
+ false
+ Label of the load more results button
+
+
+ quantic_ShowingResultsOfLoadMore
+ Showing {{0}} of {{1}} result
+ en_US
+ false
+ ex: Showing 1 of 123 result
+
+
+ quantic_ShowingResultsOfLoadMore_plural
+ Showing {{0}} of {{1}} results
+ en_US
+ false
+ ex: Showing 10 of 123 results
+
+
+ quantic_AllResultsLoaded
+ All results have been loaded
+ en_US
+ false
+ Aria-live announcement when the last batch of results has been loaded
+
quantic_Clear
Clear
diff --git a/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/__tests__/quanticLoadMoreResults.test.js b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/__tests__/quanticLoadMoreResults.test.js
new file mode 100644
index 00000000000..767bb0285fa
--- /dev/null
+++ b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/__tests__/quanticLoadMoreResults.test.js
@@ -0,0 +1,241 @@
+jest.mock('c/quanticHeadlessLoader');
+jest.mock('c/quanticUtils');
+
+import QuanticLoadMoreResults from 'c/quanticLoadMoreResults';
+import {buildCreateTestComponent, cleanup, flushPromises} from 'c/testUtils';
+import * as quanticHeadlessLoader from 'c/quanticHeadlessLoader';
+import * as quanticUtils from 'c/quanticUtils';
+
+const headlessLoaderMock = jest.mocked(quanticHeadlessLoader);
+const engineMock = {
+ id: 'mockEngine',
+ dispatch: jest.fn(),
+};
+
+const functionsMocks = {
+ resultListSubscribe: jest.fn((cb) => {
+ cb();
+ return functionsMocks.resultListUnsubscribe;
+ }),
+ resultListUnsubscribe: jest.fn(() => {}),
+ querySummarySubscribe: jest.fn((cb) => {
+ cb();
+ return functionsMocks.querySummaryUnsubscribe;
+ }),
+ querySummaryUnsubscribe: jest.fn(() => {}),
+ fetchMoreResults: jest.fn(),
+ dispatchMessage: jest.fn(),
+};
+
+const resultListControllerMock = {
+ subscribe: functionsMocks.resultListSubscribe,
+ fetchMoreResults: functionsMocks.fetchMoreResults,
+};
+
+const querySummaryControllerMock = {
+ subscribe: functionsMocks.querySummarySubscribe,
+};
+
+const headlessMock = {
+ buildResultList: jest.fn().mockReturnValue(resultListControllerMock),
+ buildQuerySummary: jest.fn().mockReturnValue(querySummaryControllerMock),
+};
+headlessLoaderMock.getHeadlessBundle.mockReturnValue(headlessMock);
+headlessLoaderMock.initializeWithHeadless.mockImplementation(
+ async (element, _, initialize) => {
+ if (element instanceof QuanticLoadMoreResults) {
+ initialize(engineMock);
+ }
+ }
+);
+
+const quanticUtilsMock = jest.mocked(quanticUtils);
+quanticUtilsMock.AriaLiveRegion.mockReturnValue({
+ dispatchMessage: functionsMocks.dispatchMessage,
+ registerRegion: undefined,
+});
+quanticUtilsMock.I18nUtils.getLabelNameWithCount.mockImplementation(
+ (baseName, count) => {
+ if (!count) {
+ return `${baseName}_zero`;
+ }
+ if (count > 1) {
+ return `${baseName}_plural`;
+ }
+ return baseName;
+ }
+);
+quanticUtilsMock.I18nUtils.format.mockImplementation(
+ (str, ...args) => `${str} ${args.join(' ')}`
+);
+
+const selectors = {
+ componentError: 'c-quantic-component-error',
+ summary: 'lightning-formatted-rich-text',
+ progressBar: '.load-more-results__progress-bar-fill',
+ loadMoreButton: 'button.load-more-results__button',
+};
+
+const createTestComponent = buildCreateTestComponent(
+ QuanticLoadMoreResults,
+ 'c-quantic-load-more-results',
+ {engineId: engineMock.id}
+);
+
+function setState({
+ moreResultsAvailable = true,
+ hasResults = true,
+ lastResult = 10,
+ total = 100,
+ searchResponseId = 'response-1',
+} = {}) {
+ resultListControllerMock.state = {
+ moreResultsAvailable,
+ searchResponseId,
+ };
+ querySummaryControllerMock.state = {
+ hasResults,
+ lastResult,
+ total,
+ };
+}
+
+describe('c-quantic-load-more-results', () => {
+ beforeEach(() => {
+ setState();
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it('should build the result list and query summary controllers and subscribe to their state', async () => {
+ createTestComponent();
+ await flushPromises();
+
+ expect(headlessLoaderMock.getHeadlessBundle).toHaveBeenCalledWith(
+ engineMock.id
+ );
+ expect(headlessMock.buildResultList).toHaveBeenCalledWith(engineMock);
+ expect(headlessMock.buildQuerySummary).toHaveBeenCalledWith(engineMock);
+ expect(resultListControllerMock.subscribe).toHaveBeenCalledWith(
+ expect.any(Function)
+ );
+ expect(querySummaryControllerMock.subscribe).toHaveBeenCalledWith(
+ expect.any(Function)
+ );
+ });
+
+ describe('when there is an initialization error', () => {
+ it('should display the initialization error component', async () => {
+ headlessLoaderMock.initializeWithHeadless.mockImplementationOnce(
+ async (element) => {
+ if (element instanceof QuanticLoadMoreResults) {
+ element.setInitializationError();
+ }
+ }
+ );
+
+ const element = createTestComponent();
+ await flushPromises();
+
+ const error = element.shadowRoot.querySelector(selectors.componentError);
+ expect(error).not.toBeNull();
+ });
+ });
+
+ describe('when there are no results', () => {
+ it('should render nothing', async () => {
+ setState({hasResults: false});
+ const element = createTestComponent();
+ await flushPromises();
+
+ const summary = element.shadowRoot.querySelector(selectors.summary);
+ const button = element.shadowRoot.querySelector(selectors.loadMoreButton);
+ expect(summary).toBeNull();
+ expect(button).toBeNull();
+ });
+ });
+
+ describe('when more results are available', () => {
+ it('should render the summary, progress bar, and load more button', async () => {
+ setState({moreResultsAvailable: true, lastResult: 10, total: 100});
+ const element = createTestComponent();
+ await flushPromises();
+
+ const summary = element.shadowRoot.querySelector(selectors.summary);
+ const progressBar = element.shadowRoot.querySelector(
+ selectors.progressBar
+ );
+ const button = element.shadowRoot.querySelector(selectors.loadMoreButton);
+ expect(summary).not.toBeNull();
+ expect(progressBar).not.toBeNull();
+ expect(progressBar.style.width).toBe('10%');
+ expect(button).not.toBeNull();
+ });
+
+ it('should call fetchMoreResults when the button is clicked', async () => {
+ setState({moreResultsAvailable: true});
+ const element = createTestComponent();
+ await flushPromises();
+
+ const button = element.shadowRoot.querySelector(selectors.loadMoreButton);
+ button.click();
+
+ expect(functionsMocks.fetchMoreResults).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when no more results are available', () => {
+ it('should not render the load more button', async () => {
+ setState({moreResultsAvailable: false, lastResult: 100, total: 100});
+ const element = createTestComponent();
+ await flushPromises();
+
+ const button = element.shadowRoot.querySelector(selectors.loadMoreButton);
+ expect(button).toBeNull();
+ });
+ });
+
+ describe('when the last batch of results has just been loaded', () => {
+ it('should dispatch the all-results-loaded aria-live message', async () => {
+ setState({
+ moreResultsAvailable: true,
+ searchResponseId: 'response-1',
+ });
+ createTestComponent();
+ await flushPromises();
+
+ functionsMocks.dispatchMessage.mockClear();
+
+ resultListControllerMock.state = {
+ moreResultsAvailable: false,
+ searchResponseId: 'response-1',
+ };
+ functionsMocks.resultListSubscribe.mock.calls[0][0]();
+
+ expect(functionsMocks.dispatchMessage).toHaveBeenCalledWith(
+ 'c.quantic_AllResultsLoaded'
+ );
+ });
+
+ it('should not dispatch the message when the transition is caused by a new search', async () => {
+ setState({
+ moreResultsAvailable: true,
+ searchResponseId: 'response-1',
+ });
+ createTestComponent();
+ await flushPromises();
+
+ functionsMocks.dispatchMessage.mockClear();
+
+ resultListControllerMock.state = {
+ moreResultsAvailable: false,
+ searchResponseId: 'response-2',
+ };
+ functionsMocks.resultListSubscribe.mock.calls[0][0]();
+
+ expect(functionsMocks.dispatchMessage).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/e2e/fixture.ts b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/e2e/fixture.ts
new file mode 100644
index 00000000000..c714a7154bb
--- /dev/null
+++ b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/e2e/fixture.ts
@@ -0,0 +1,59 @@
+import {LoadMoreResultsObject} from './pageObject';
+import {quanticBase} from '../../../../../../playwright/fixtures/baseFixture';
+import {InsightSetupObject} from '../../../../../../playwright/page-object/insightSetupObject';
+import {SearchObject} from '../../../../../../playwright/page-object/searchObject';
+import {
+ searchRequestRegex,
+ insightSearchRequestRegex,
+} from '../../../../../../playwright/utils/requests';
+import {useCaseEnum} from '../../../../../../playwright/utils/useCase';
+
+const loadMoreResultsUrl = 's/quantic-load-more-results';
+
+type QuanticLoadMoreResultsE2EFixtures = {
+ loadMoreResults: LoadMoreResultsObject;
+ search: SearchObject;
+};
+
+type QuanticLoadMoreResultsE2EInsightFixtures =
+ QuanticLoadMoreResultsE2EFixtures & {
+ insightSetup: InsightSetupObject;
+ };
+
+export const testSearch =
+ quanticBase.extend({
+ search: async ({page}, use) => {
+ await use(new SearchObject(page, searchRequestRegex));
+ },
+ loadMoreResults: async ({page, configuration, search}, use) => {
+ await search.mockSearchWithLoadMoreSequence();
+ await page.goto(loadMoreResultsUrl);
+ configuration.configure({});
+ await search.waitForSearchResponse();
+ await use(new LoadMoreResultsObject(page));
+ },
+ });
+
+export const testInsight =
+ quanticBase.extend({
+ search: async ({page}, use) => {
+ await use(new SearchObject(page, insightSearchRequestRegex));
+ },
+ insightSetup: async ({page}, use) => {
+ await use(new InsightSetupObject(page));
+ },
+ loadMoreResults: async (
+ {page, search, configuration, insightSetup},
+ use
+ ) => {
+ await search.mockSearchWithLoadMoreSequence();
+ await page.goto(loadMoreResultsUrl);
+ configuration.configure({useCase: useCaseEnum.insight});
+ await insightSetup.waitForInsightInterfaceInitialization();
+ await Promise.all([
+ search.waitForSearchResponse(),
+ search.performSearch(),
+ ]);
+ await use(new LoadMoreResultsObject(page));
+ },
+ });
diff --git a/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/e2e/pageObject.ts b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/e2e/pageObject.ts
new file mode 100644
index 00000000000..6271a85e856
--- /dev/null
+++ b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/e2e/pageObject.ts
@@ -0,0 +1,44 @@
+import type {Locator, Page} from '@playwright/test';
+
+const loadMoreResultsElementsSelectors = {
+ component: 'c-quantic-load-more-results',
+ componentError: 'c-quantic-component-error',
+ summary: 'c-quantic-load-more-results lightning-formatted-rich-text',
+ progressBar: 'c-quantic-load-more-results .load-more-results__progress-bar',
+ loadMoreButton: 'c-quantic-load-more-results button',
+ results: 'c-quantic-result',
+};
+
+export class LoadMoreResultsObject {
+ constructor(public page: Page) {
+ this.page = page;
+ }
+
+ get component(): Locator {
+ return this.page.locator(loadMoreResultsElementsSelectors.component);
+ }
+
+ get componentError(): Locator {
+ return this.page.locator(loadMoreResultsElementsSelectors.componentError);
+ }
+
+ get summary(): Locator {
+ return this.page.locator(loadMoreResultsElementsSelectors.summary);
+ }
+
+ get progressBar(): Locator {
+ return this.page.locator(loadMoreResultsElementsSelectors.progressBar);
+ }
+
+ get loadMoreButton(): Locator {
+ return this.page.locator(loadMoreResultsElementsSelectors.loadMoreButton);
+ }
+
+ get results(): Locator {
+ return this.page.locator(loadMoreResultsElementsSelectors.results);
+ }
+
+ async clickLoadMoreButton(): Promise {
+ await this.loadMoreButton.click();
+ }
+}
diff --git a/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/e2e/quanticLoadMoreResults.e2e.ts b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/e2e/quanticLoadMoreResults.e2e.ts
new file mode 100644
index 00000000000..2ff1eaf657c
--- /dev/null
+++ b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/e2e/quanticLoadMoreResults.e2e.ts
@@ -0,0 +1,70 @@
+import {testSearch, testInsight} from './fixture';
+import {
+ useCaseEnum,
+ useCaseTestCases,
+} from '../../../../../../playwright/utils/useCase';
+
+const fixtures = {
+ search: testSearch,
+ insight: testInsight,
+};
+
+useCaseTestCases.forEach((useCase) => {
+ let test = fixtures[useCase.value];
+
+ test.describe(`quantic load more results ${useCase.label}`, () => {
+ test('should load as expected', async ({loadMoreResults}) => {
+ await test.expect(loadMoreResults.component).toBeVisible();
+ await test.expect(loadMoreResults.summary).toBeVisible();
+ await test.expect(loadMoreResults.progressBar).toBeVisible();
+ await test.expect(loadMoreResults.loadMoreButton).toBeVisible();
+ });
+
+ test.describe('when clicking the load more button', () => {
+ test('should append results and hide the button once all results are loaded', async ({
+ loadMoreResults,
+ search,
+ }) => {
+ const initialResultsCount = await loadMoreResults.results.count();
+
+ const searchResponsePromise = search.waitForSearchResponse();
+ await loadMoreResults.clickLoadMoreButton();
+ await searchResponsePromise;
+
+ await test
+ .expect(loadMoreResults.results)
+ .toHaveCount(initialResultsCount * 2);
+ await test.expect(loadMoreResults.loadMoreButton).toBeHidden();
+ });
+
+ test('should announce that all results have been loaded', async ({
+ loadMoreResults,
+ page,
+ search,
+ }) => {
+ const ariaLiveRegion = page.locator('[aria-live]');
+
+ const searchResponsePromise = search.waitForSearchResponse();
+ await loadMoreResults.clickLoadMoreButton();
+ await searchResponsePromise;
+
+ await test
+ .expect(ariaLiveRegion)
+ .toContainText('All results have been loaded');
+ });
+ });
+
+ if (useCase.value === useCaseEnum.search) {
+ test.describe('when there are no results', () => {
+ test.beforeEach(async ({search}) => {
+ await search.mockEmptySearchResponse();
+ });
+
+ test('should not render anything', async ({page}) => {
+ const component = page.locator('c-quantic-load-more-results');
+ await test.expect(component).toBeEmpty();
+ });
+ });
+ }
+ });
+});
diff --git a/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/quanticLoadMoreResults.css b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/quanticLoadMoreResults.css
new file mode 100644
index 00000000000..94b4a873a9c
--- /dev/null
+++ b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/quanticLoadMoreResults.css
@@ -0,0 +1,13 @@
+.load-more-results__progress-bar {
+ width: 100%;
+ height: 0.25rem;
+ border-radius: var(--lwc-borderRadiusMedium, 0.25rem);
+ background-color: var(--lwc-colorBackgroundRowHover, #f3f3f3);
+ overflow: hidden;
+ margin: var(--lwc-spacingSmall, 0.5rem) 0;
+}
+
+.load-more-results__progress-bar-fill {
+ height: 100%;
+ background-color: var(--lwc-brandPrimary, #1b96ff);
+}
diff --git a/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/quanticLoadMoreResults.html b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/quanticLoadMoreResults.html
new file mode 100644
index 00000000000..b312a97f58f
--- /dev/null
+++ b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/quanticLoadMoreResults.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {labels.loadMoreResults}
+
+
+
+
+
diff --git a/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/quanticLoadMoreResults.js b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/quanticLoadMoreResults.js
new file mode 100644
index 00000000000..606a289bf69
--- /dev/null
+++ b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/quanticLoadMoreResults.js
@@ -0,0 +1,163 @@
+import allResultsLoaded from '@salesforce/label/c.quantic_AllResultsLoaded';
+import loadMoreResults from '@salesforce/label/c.quantic_LoadMoreResults';
+import showingResultsOfLoadMore from '@salesforce/label/c.quantic_ShowingResultsOfLoadMore';
+import showingResultsOfLoadMore_plural from '@salesforce/label/c.quantic_ShowingResultsOfLoadMore_plural';
+import {
+ registerComponentForInit,
+ initializeWithHeadless,
+ getHeadlessBundle,
+} from 'c/quanticHeadlessLoader';
+import {AriaLiveRegion, I18nUtils} from 'c/quanticUtils';
+import {LightningElement, api, track} from 'lwc';
+
+/** @typedef {import("coveo").SearchEngine} SearchEngine */
+/** @typedef {import("coveo").ResultList} ResultList */
+/** @typedef {import("coveo").ResultListState} ResultListState */
+/** @typedef {import("coveo").QuerySummary} QuerySummary */
+/** @typedef {import("coveo").QuerySummaryState} QuerySummaryState */
+
+/**
+ * The `QuanticLoadMoreResults` component allows the user to load additional results if more are available.
+ *
+ * This component is an alternative to the `quantic-pager` component: use one or the other, not both on the same page.
+ * @category Search
+ * @category Insight Panel
+ * @example
+ *
+ */
+export default class QuanticLoadMoreResults extends LightningElement {
+ /**
+ * The ID of the engine instance the component registers to.
+ * @api
+ * @type {string}
+ */
+ @api engineId;
+
+ /** @type {ResultListState} */
+ @track resultListState;
+ /** @type {QuerySummaryState} */
+ @track querySummaryState;
+
+ /** @type {ResultList} */
+ resultList;
+ /** @type {QuerySummary} */
+ querySummary;
+ /** @type {Function} */
+ unsubscribeResultList;
+ /** @type {Function} */
+ unsubscribeQuerySummary;
+ /** @type {AnyHeadless} */
+ headless;
+ /** @type {import('c/quanticUtils').AriaLiveUtils} */
+ allResultsLoadedAriaMessage;
+ /** @type {boolean} */
+ hasInitializationError = false;
+ /** @type {ResultListState} */
+ previousResultListState;
+
+ labels = {
+ allResultsLoaded,
+ loadMoreResults,
+ showingResultsOfLoadMore,
+ showingResultsOfLoadMore_plural,
+ };
+
+ connectedCallback() {
+ registerComponentForInit(this, this.engineId);
+ }
+
+ renderedCallback() {
+ initializeWithHeadless(this, this.engineId, this.initialize);
+ }
+
+ /**
+ * @param {SearchEngine} engine
+ */
+ initialize = (engine) => {
+ this.headless = getHeadlessBundle(this.engineId);
+ this.resultList = this.headless.buildResultList(engine);
+ this.querySummary = this.headless.buildQuerySummary(engine);
+ this.allResultsLoadedAriaMessage = AriaLiveRegion(
+ 'load-more-results',
+ this
+ );
+ this.unsubscribeResultList = this.resultList.subscribe(() =>
+ this.updateResultListState()
+ );
+ this.unsubscribeQuerySummary = this.querySummary.subscribe(() =>
+ this.updateQuerySummaryState()
+ );
+ };
+
+ disconnectedCallback() {
+ this.unsubscribeResultList?.();
+ this.unsubscribeQuerySummary?.();
+ }
+
+ updateResultListState() {
+ this.previousResultListState = this.resultListState;
+ this.resultListState = this.resultList.state;
+ this.announceWhenAllResultsAreLoaded();
+ }
+
+ updateQuerySummaryState() {
+ this.querySummaryState = this.querySummary.state;
+ }
+
+ announceWhenAllResultsAreLoaded() {
+ const justLoadedLastBatch =
+ this.previousResultListState?.moreResultsAvailable === true &&
+ this.resultListState?.moreResultsAvailable === false &&
+ this.previousResultListState?.searchResponseId ===
+ this.resultListState?.searchResponseId;
+
+ if (justLoadedLastBatch) {
+ this.allResultsLoadedAriaMessage.dispatchMessage(
+ this.labels.allResultsLoaded
+ );
+ }
+ }
+
+ get hasResults() {
+ return this.querySummaryState?.hasResults;
+ }
+
+ get moreResultsAvailable() {
+ return this.resultListState?.moreResultsAvailable;
+ }
+
+ get progressPercentage() {
+ const total = this.querySummaryState?.total;
+ if (!total) {
+ return 0;
+ }
+ return Math.min((this.querySummaryState.lastResult / total) * 100, 100);
+ }
+
+ get progressBarStyle() {
+ return `width: ${this.progressPercentage}%`;
+ }
+
+ get summaryLabel() {
+ const labelName = I18nUtils.getLabelNameWithCount(
+ 'showingResultsOfLoadMore',
+ this.querySummaryState?.lastResult
+ );
+ return I18nUtils.format(
+ this.labels[labelName],
+ Intl.NumberFormat().format(this.querySummaryState?.lastResult),
+ Intl.NumberFormat().format(this.querySummaryState?.total)
+ );
+ }
+
+ fetchMoreResults() {
+ this.resultList.fetchMoreResults();
+ }
+
+ /**
+ * Sets the component in the initialization error state.
+ */
+ setInitializationError() {
+ this.hasInitializationError = true;
+ }
+}
diff --git a/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/quanticLoadMoreResults.js-meta.xml b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/quanticLoadMoreResults.js-meta.xml
new file mode 100644
index 00000000000..9176009d178
--- /dev/null
+++ b/packages/quantic/force-app/main/default/lwc/quanticLoadMoreResults/quanticLoadMoreResults.js-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 64.0
+ false
+
diff --git a/packages/quantic/force-app/main/default/lwc/quanticPager/quanticPager.js b/packages/quantic/force-app/main/default/lwc/quanticPager/quanticPager.js
index 380005f4a1e..bd5455f8ee2 100644
--- a/packages/quantic/force-app/main/default/lwc/quanticPager/quanticPager.js
+++ b/packages/quantic/force-app/main/default/lwc/quanticPager/quanticPager.js
@@ -14,6 +14,8 @@ import {LightningElement, api, track} from 'lwc';
/**
* The `QuanticPager` provides buttons that allow the end user to navigate through the different result pages.
+ *
+ * This component is an alternative to the `quantic-load-more-results` component: use one or the other, not both on the same page.
* @category Search
* @category Insight Panel
* @example
diff --git a/packages/quantic/force-app/main/translations/es.translation-meta.xml b/packages/quantic/force-app/main/translations/es.translation-meta.xml
index cf234cc0464..9e6fab9a8d1 100644
--- a/packages/quantic/force-app/main/translations/es.translation-meta.xml
+++ b/packages/quantic/force-app/main/translations/es.translation-meta.xml
@@ -32,6 +32,22 @@
Ningún resultado para {{0}}
quantic_NoResultsFor
+
+ Cargar más resultados
+ quantic_LoadMoreResults
+
+
+ Mostrando {{0}} de {{1}} resultado
+ quantic_ShowingResultsOfLoadMore
+
+
+ Mostrando {{0}} de {{1}} resultados
+ quantic_ShowingResultsOfLoadMore_plural
+
+
+ Se han cargado todos los resultados
+ quantic_AllResultsLoaded
+
Eliminar todos los filtros
quantic_ClearAllFilters
diff --git a/packages/quantic/force-app/main/translations/fr.translation-meta.xml b/packages/quantic/force-app/main/translations/fr.translation-meta.xml
index 6edfa7311eb..e54420d9991 100644
--- a/packages/quantic/force-app/main/translations/fr.translation-meta.xml
+++ b/packages/quantic/force-app/main/translations/fr.translation-meta.xml
@@ -32,6 +32,22 @@
Aucun résultat pour {{0}}
quantic_NoResultsFor
+
+ Charger plus de résultats
+ quantic_LoadMoreResults
+
+
+ {{0}} résultat affiché sur {{1}}
+ quantic_ShowingResultsOfLoadMore
+
+
+ {{0}} résultats affichés sur {{1}}
+ quantic_ShowingResultsOfLoadMore_plural
+
+
+ Tous les résultats ont été chargés
+ quantic_AllResultsLoaded
+
Enlever tous les filtres
quantic_ClearAllFilters
diff --git a/packages/quantic/playwright/page-object/searchObject.ts b/packages/quantic/playwright/page-object/searchObject.ts
index 62e7ef54a9f..f06fa2c1ee0 100644
--- a/packages/quantic/playwright/page-object/searchObject.ts
+++ b/packages/quantic/playwright/page-object/searchObject.ts
@@ -178,6 +178,46 @@ export class SearchObject {
});
}
+ /**
+ * Mocks the search endpoint so that the first response reports more
+ * results are available, and every subsequent response (i.e. after a
+ * "load more" fetch) reports all results have been loaded. Each response
+ * beyond the first returns a distinct batch of results (`uniqueId`s
+ * suffixed by response index), matching how a real "load more" batch
+ * differs from the initial page. `searchUid` is kept constant across the
+ * sequence, matching headless's `fetchMoreResults` behavior, which does
+ * not update `searchResponseId` on subsequent batches.
+ */
+ async mockSearchWithLoadMoreSequence(): Promise {
+ await this.page.unroute(this.searchRequestRegex);
+ let responseIndex = 0;
+ await this.page.route(this.searchRequestRegex, async (route) => {
+ const originalBody = searchResponses.richResponse;
+ const totalCount =
+ responseIndex === 0
+ ? originalBody.results.length * 2
+ : originalBody.results.length;
+ const results = originalBody.results.map((result) => ({
+ ...result,
+ uniqueId: `${result.uniqueId}-batch${responseIndex}`,
+ }));
+ responseIndex += 1;
+
+ await route.fulfill({
+ body: JSON.stringify({
+ ...originalBody,
+ results,
+ totalCount,
+ totalCountFiltered: totalCount,
+ }),
+ status: 200,
+ headers: {
+ 'content-type': 'application/json',
+ },
+ });
+ });
+ }
+
extractDataFromResponse(response: Response) {
return response.request().postDataJSON();
}