You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Shoppers on EP-powered storefronts built with Plasmic cannot get search-as-you-type suggestions. The current EPSearchBox only refines the catalog-search query on debounced keystrokes, so the experience is "type, wait, see results in the grid below" — there is no live dropdown of matching query strings or recent searches anchored to the input. This makes the storefront feel slow on long queries and discoverable categories, and forces shoppers who don't know the exact product term to commit a query before getting any feedback.
Plasmic designers cannot reach for an InstantSearch autocomplete primitive when assembling a search page; they have to either build one themselves (no headless component primitive ships in this package) or ignore the feature altogether. From the package's component coverage perspective this is item #3 of the catalog-search gaps tracked in #304.
The EP catalog-search service exposes a dedicated type=autocomplete endpoint backed by a Typesense collection containing query-suggestion documents (with a q field) — but the EP catalog-search-instantsearch-adapter README and the existing demo at field123/ep-search-instantsearch-demo are the only existing surfaces consuming it, and neither composes with the rest of the EP commerce Plasmic component family.
Solution
Ship a four-component compound EPSearchAutocomplete in plasmicpkgs/commerce-providers/elastic-path/src/catalog-search/, wrapping @algolia/autocomplete-core (the headless package, not autocomplete-js).
The compound exposes Algolia's prop-getter pattern through Plasmic-friendly bridge components that own one DOM element each, spread the right prop-getter onto it, and slot designer-authored content for visuals. State, keyboard navigation, accessibility, and plugin support come from autocomplete-core; chrome and per-suggestion rendering come from the slot.
A shopper drops focus into the input, sees a panel of matching query-string suggestions sourced from EP's type=autocomplete endpoint, can navigate with arrow keys, can pick one with click or Enter, and the main EPSearchHits grid refreshes against the chosen query. The input value is also live-mirrored (debounced) into the shared catalog-search query so the grid feels responsive while typing.
A designer drops the four components into a Plasmic page (or an opinionated default tree if dropping just the provider seeds them all), gets a working autocomplete on desktop and mobile out of the box, and can restyle every visible element through Plasmic's panel because the components honour the L2 headless-styling contract: only the inner mobile-close button renders chrome the designer didn't author, and that is itself a documented L2 exception with a stable data-attribute hook.
Recent-searches and other autocomplete-core plugins are supported but default-off; v1 ships predictions-only.
User Stories
Shopper-facing
As a shopper, I want a dropdown of suggested queries to appear as I type into the search input, so that I can pick a relevant query without committing a full search.
As a shopper, I want to navigate the suggestion list with the keyboard (Up/Down/Enter/Escape), so that I can pick a suggestion without taking my hands off the keyboard.
As a shopper, I want to click a suggestion and have the storefront's search results update to that query, so that picking a suggestion feels instant and decisive.
As a shopper, I want to press Enter without picking a suggestion and have the storefront search for whatever I typed, so that I am not blocked when none of the suggestions match my intent.
As a shopper, I want my prior URL-state query to be present in the input when I land on the search page, so that the autocomplete works on top of an existing search session.
As a shopper using a mobile device, I want the suggestion panel to be readable and usable on my screen, so that I am not staring at a desktop dropdown that overflows my viewport.
As a shopper using a mobile device, I want a clear way to dismiss the suggestion panel, so that I am never trapped in a full-screen overlay that obscures the rest of the page.
As a shopper, I want the suggestion panel to close when I click outside of it, so that I can return to the rest of the page intuitively.
As a shopper, I want the suggestion panel to never appear when my input is empty, so that the panel's existence is correlated with my typing intent.
As a shopper, I want the storefront's main results grid to update as I type (with a small delay), so that I get immediate feedback even if I do not pick a suggestion.
Designer-facing
As a Plasmic designer, I want to drop a single autocomplete component and have it render a working input + panel + suggestion list on the canvas without further configuration, so that I can iterate on a search page in minutes, not hours.
As a Plasmic designer, I want each of the four bridge components to come with a sensible default slot tree on insert, so that fresh drops render recognisable shapes and I can edit from a working starting point.
As a Plasmic designer, I want every visible element of the autocomplete UI to be a Plasmic-controlled tag in a slot — so that styles I set in the Plasmic panel reach the live DOM unfiltered, per the catalog-search styling contract.
As a Plasmic designer, I want to compose the per-suggestion row from primitives in the slot using the same context-binding pattern I already use for EPCurrentRefinements, so that the autocomplete component does not introduce a foreign authoring model.
As a Plasmic designer, I want the autocomplete panel to anchor below the input on desktop and adapt to a mobile-friendly layout below a 680px breakpoint without me having to author breakpoint CSS, so that mobile is not silently broken on every fresh drop.
As a Plasmic designer, I want the mobile close affordance to be present and stylable via a documented data-attribute, so that I can redesign or hide it but never have to re-add it just to make mobile usable.
As a Plasmic designer, I want a previewState prop with mocked autocomplete state, so that I can lay out the suggestion list against representative data without a live EP backend.
As a Plasmic designer, I want the autocomplete provider to enforce that it must live inside EPCatalogSearchProvider, so that I cannot accidentally compose an autocomplete that has no source of catalog-search state.
As a Plasmic designer, I want each bridge component to enforce that it must live inside the autocomplete provider, so that I get an early error rather than a confusing runtime failure when I drop a panel without its provider.
As a Plasmic designer, I want to opt into recent-search persistence with a single boolean prop, so that customers who do not want browser-localStorage writes get a clean default and customers who do can flip one switch.
As a Plasmic designer building two pages — one for desktop search and one for in-store kiosk where recent searches are inappropriate — I want the recent-searches behaviour to be per-instance, not a package-level global, so that I can configure it differently per page without code changes.
As a Plasmic designer using the multi-source layout, I want to compose two EPSearchAutocompleteList siblings inside one panel — one scoped to predictions, one to recent searches — and have them render only their own source's items, so that I can label and style each section independently.
Developer-facing
As a storefront developer, I want the type=autocomplete request shape (q, include_fields, highlight_full_fields) baked into the provider rather than configured per consumer, so that a misconfigured request does not silently degrade UX in production.
As a storefront developer whose EP autocomplete collection schema names the suggestion field something other than q, I want a predictionsField prop on the provider with a sensible default of "q", so that I can survive customer-tenant schema variation without forking the component.
As a storefront developer, I want the autocomplete component to gracefully handle errors from the EP adapter (missing autocomplete collection, network failure) by returning an empty suggestion list rather than throwing, so that the shopper experience never crashes a search page.
As a storefront developer, I want the autocomplete provider to expose ref-actions (setQuery, focus, clear) so that ad-hoc Plasmic interactions outside the autocomplete tree (like a "Search this category" CTA) can drive it programmatically.
As a storefront developer, I want a plugins escape hatch on the provider for advanced consumers to inject additional autocomplete-core plugins (custom analytics, third-party recent-searches stores), so that the four-component compound is not a closed system.
As a storefront developer, I want the provider's deprecated future props (or any input from a v1.1 schema flip) to remain registered as hidden no-ops per the catalog-search bundle-orphans guidance, so that existing project bundles do not fail validation when I upgrade the package.
As a Plasmic platform developer, I want the autocomplete-core integration encapsulated in a deep module so that I can reason about and test the state-management logic independently of the four React component shells.
Implementation Decisions
Components shipped (four, all in the catalog-search subpackage)
EPSearchAutocomplete — the provider. Wraps createAutocomplete() from @algolia/autocomplete-core. Owns the autocomplete-core instance, the predictions source, optional plugins (including the optional recent-searches plugin), the live-mirror bridge to the surrounding useSearchBox().refine(), and the single render-time wrapper <div> that gives the panel a positioning context. Publishes autocompleteData via Plasmic DataProvider. Exposes ref-actions for setQuery, focus, clear. Renders no visible chrome of its own beyond the wrapper element. Its registration enforces parentComponentName: plasmic-commerce-ep-catalog-search-provider.
EPSearchAutocompleteInput — the input bridge. Spreads getInputProps() onto the designer's slot input element via runtime cloneElement. Single-element-slot semantics with a default of one <input type="search"> so that a fresh drop is functional. Required parent is the autocomplete provider.
EPSearchAutocompletePanel — the panel bridge. Conditionally rendered when state.isOpen. Spreads getPanelProps() onto its outer <div data-ep-autocomplete-panel>. Renders one component-owned chrome element (the mobile-close button as a documented L2 exception, attribute data-ep-autocomplete-close) which is hidden via media query at desktop breakpoints. Designer composes the panel's inner content through the slot. Required parent is the autocomplete provider.
EPSearchAutocompleteList — the list / repeater bridge. Optional sourceId prop scopes the rendered items to one named source (e.g. "predictions", "recent"). Renders <ul {...getListProps()}> and per-item <li {...getItemProps({item, source})}> wrappers around repeatedElement(i, children) so the wrapper element carries the ARIA role="option", aria-selected, mouse handlers, click handler, and ref required by autocomplete-core. Publishes per-iteration currentSuggestion context ({ item, isHighlighted, source, query, refine }). Required parent is the autocomplete provider.
Architectural decisions
Pure-L2 compound, not a single chrome-owning widget. Every visible element except the mobile-close affordance belongs to the slot, not the component. This rejects path X (wrap autocomplete-js), which renders the panel via its own createRoot and bakes @algolia/autocomplete-theme-classic. The headless @algolia/autocomplete-core package supplies the same state-machine, plugin, and accessibility behaviour without owning the DOM, and is the maintainer-blessed path for full custom UI per their "creating a custom renderer" guide.
Predictions source baked into the provider. Designers and consuming developers do not configure how items are fetched. The provider builds a single autocomplete-core source that calls EP's postMultiSearch with type: "autocomplete", requests include_fields and highlight_full_fields for the configured predictionsField, and maps the response into the autocomplete-core source contract. The advanced plugins array remains an escape hatch for power users who need additional sources.
Live mirror to useSearchBox().refine(). On every change to the autocomplete-core input state, the provider debounces (default 300ms, matching EPSearchBox's existing debounce) and pushes the value into the surrounding InstantSearch query. On selection (item click or Enter on a highlighted item) and on submit-without-selection (Enter with no highlight), the refine fires immediately and the panel closes. This matches the demo at field123/ep-search-instantsearch-demo and aligns the autocomplete's input with the rest of the catalog-search components reading the same query state.
Initial input value mirrors useSearchBox().query. URL-state survives page load — the autocomplete-core instance is initialised with the existing InstantSearch query so a navigated-into search page does not blank the input.
Recent searches default off. The provider declares enableRecentSearches?: boolean (default false), recentSearchesKey?: string (default an EP-namespaced key), recentSearchesLimit?: number (default 3, matching demo). When enabled, the provider requires @algolia/autocomplete-plugin-recent-searches as a peer dependency. Disabled-by-default avoids surprise localStorage writes and keeps v1's bundle and test scope minimal. Adding the prop later is purely additive — no breaking change for consumers shipping with default-off behaviour.
Mobile breakpoint defaults baked into headless-styling.ts. New :where() rules for the autocomplete root, panel, and close button give the components a sensible desktop layout (panel anchored absolute below the input, scrollable, shadowed) and a mobile-friendly variant below 680px (full-screen or top-anchored sheet, max-height unset, close button visible). Zero specificity means any Plasmic-set class overrides every property — designers retain full styling control.
Mobile close button as documented L2 exception. A single <button data-ep-autocomplete-close> rendered inside the panel, hidden by media query above 680px. This is the only chrome the component renders without designer authoring; it ships because a fresh drop on mobile would otherwise trap the user in a panel they cannot dismiss. Designer hides it with display: none or restyles via the data attribute.
Provider is the only component that renders an unconditional wrapper. A small departure from EPSearchBox's post-PRD: Make EPSearchBox styleable by lifting its DOM out of the code-component #308 wrapperless pattern, justified because the panel needs a position: relative ancestor for absolute anchoring. The wrapper has data-ep-autocomplete-root.
Per-item DOM ownership via repeater wrappers. The list bridge's <li> is component-rendered (so getItemProps()'s ref, aria-selected, role="option", and mouse handlers all attach correctly) but the visual content of each row is the designer's slot child, repeated via Plasmic's repeatedElement. Designer drives row visuals (text, image, highlight markup) via the per-iteration currentSuggestion context, including the isHighlighted boolean which they bind to a Plasmic conditional style.
Highlight rendering is the designer's responsibility. The provider exposes the autocomplete-core item shape including any _highlightResult / highlights produced by the EP adapter. Designers wire the highlighted snippet through Plasmic's existing dynamic-text + sanitised-HTML primitives — the component does not bake an opinion on highlight rendering.
Empty-query suppression. When the input is empty, no autocomplete request fires and the panel does not open. This is the autocomplete-core default behaviour and is not configurable in v1.
Deep modules
useEPAutocompleteState hook. Encapsulates the autocomplete-core integration. Inputs include the EP shopper client (read from the existing commerce provider context), predictionsField, debounceMs, enableRecentSearches, recent-searches storage config, and the optional plugins array. Outputs the live state, all six prop-getters, an imperative refine for ref-actions, an imperative close, and the published collections array. Hides the createAutocomplete lifecycle, plugin loading, source construction, debounce, mirror-to-useSearchBox wiring, and React state subscription behind a single hook signature. The four React components do not call createAutocomplete directly — they consume this hook and the published context.
predictionsSource factory. A pure function that returns an autocomplete-core source descriptor configured for the EP type=autocomplete endpoint. Owns the request shape (the multi-search body and include_fields parameter), the response mapping (item ↔ field), and graceful error handling (returns an empty list on adapter failure rather than throwing). Tested as a unit independent of React, autocomplete-core, and Plasmic.
Schema and registration changes
New entries in index.ts and the package's src/index.tsx registering all four components, with parentComponentName enforcing the family hierarchy. The four register* functions follow the existing pattern.
New mock fixtures in design-time-data.ts for autocomplete state and prediction items, used by both the editor preview branch and the unit tests.
New :where() blocks in headless-styling.ts for the autocomplete root, panel, close button, and the 680px mobile media-query variant.
package.json declares the @algolia/autocomplete-core runtime dependency. @algolia/autocomplete-plugin-recent-searches is declared as a peerDependency with peerDependenciesMeta.optional = true — required only when consumers enable recent searches.
API contracts
EPSearchAutocomplete props: predictionsField?: string (default "q"), debounceMs?: number (default 300), enableRecentSearches?: boolean (default false), recentSearchesKey?: string, recentSearchesLimit?: number, plugins?: AutocompletePlugin[] (advanced, hidden in the Studio panel), previewState?: "auto" | "withData", plus children + className. Ref-actions: setQuery(value: string), focus(), clear().
EPSearchAutocompleteInput props: children + className + previewState. Single-slot, default content is <input type="search">.
EPSearchAutocompletePanel props: children + className + previewState. Single-slot, default content is one EPSearchAutocompleteList.
EPSearchAutocompleteList props: sourceId?: string (default unset = renders all sources' items), children + className + previewState. Single-slot, default content is a styled suggestion-row template using currentSuggestion bindings.
Published context names: autocompleteData on the provider; currentSuggestion per repeated list item.
Stable data attributes: data-ep-autocomplete-root, data-ep-autocomplete-input, data-ep-autocomplete-panel, data-ep-autocomplete-close, data-ep-autocomplete-list. These are the public surface for designer CSS overrides and for the styling-contract tests.
Testing Decisions
Definition of a good test
A good test asserts external behaviour through observable outputs (rendered DOM, published context values, calls into bridges like useSearchBox().refine) rather than internal implementation details (autocomplete-core's private state shape, the order of operations inside useEPAutocompleteState's effects, the specific React fiber structure). Tests should survive a refactor that swaps the autocomplete-core version, replaces the debounce primitive, or restructures the React tree, as long as the contract — what designers see, what shoppers see, what the catalog-search query state ends up in — is preserved.
State emits when autocomplete-core's state changes, and the published collections shape matches the expected per-source structure.
With enableRecentSearches: false, the collections array contains exactly one source (predictions); with enableRecentSearches: true, it contains both.
A burst of input events produces exactly one debounced call into a mocked useSearchBox().refine, with the most recent value, after the configured debounceMs.
Selection of a prediction item invokes useSearchBox().refine immediately with item[predictionsField], and the panel closes.
Submit-without-selection invokes useSearchBox().refine with the current input value.
Unmount cancels in-flight debounced calls and tears down the autocomplete-core instance.
predictionsSource (deep module, isolated tests):
The source's getItems invokes postMultiSearch with the expected body shape (type: "autocomplete", q, include_fields, highlight_full_fields).
Returned items are mapped from response.results[0].hits and expose the configured field plus the raw hit for downstream highlight rendering.
On adapter error, the source returns [] rather than throwing.
The configured predictionsField is passed through to include_fields.
The four registered components (contract tests, mirroring the describeHeadlessStylingContract precedent):
className reaches the documented data-attribute leaf for each component.
No inline appearance styles are emitted; structural defaults come exclusively from the :where() style block.
previewState: "withData" and previewState: "auto" in editor canvas both render the mock branch with the expected published context.
parentComponentName is enforced for each non-provider component.
Each component's defaultValue produces a recognisable shape on fresh drop.
EPSearchAutocompleteList (behavioural):
Clicking a repeated item calls the autocomplete-core onSelect for that item, which in turn invokes the provider's refine with the right query.
The wrapper <li> receives aria-selected="true" for the highlighted item and aria-selected="false" for the rest.
When sourceId is set, only that source's items are repeated.
Per-iteration currentSuggestion context is published with the expected shape.
Prior art
The existing test suites in plasmicpkgs/commerce-providers/elastic-path/src/catalog-search/__tests__/ are the closest match. catalog-search-components.test.tsx is where the new contract assertions live, alongside the existing 9-component contract suite shipped with #309 / #316. cloneWithInjectedHandlers.test.tsx is the precedent for testing a small extracted helper in isolation; useEPAutocompleteState and predictionsSource follow the same shape — narrow inputs, narrow outputs, no React or DOM mocking beyond what the unit demands. react-instantsearch is mocked at the module level the same way the existing components mock it.
End-to-end verification on examples/ep-commerce-app-router (per L8) is part of the PR's acceptance criteria, not the automated test suite. The verification checklist covers the shopper user stories above on the live storefront against a real EP catalog with the autocomplete collection populated.
Out of Scope
Mobile detached overlay parity with autocomplete-js. v1 ships responsive defaults via the headless-styling block but does not replicate autocomplete-js's full-screen detached mode with its own input element and animation. Designers compose the mobile experience from the slot.
Recent searches default-on. v1 ships the plugin support and the boolean prop but defaults to off. A follow-up issue can flip the default once recent-searches has its own design discussion (key namespacing across multi-instance pages, TTL/limit defaults, storage migration story).
Query-suggestions in the Algolia sense (a separate index of analytics-derived query strings). EP's type=autocomplete is the closest equivalent and is what we wrap; any future analytics-populated suggestions surface would be a separate component or a new source-id added to the existing list.
Prompt suggestions. The EXPERIMENTAL_Autocomplete widget's prompt-suggestions concept assumes infrastructure we do not have. Defer indefinitely.
Submit on blur. Selection happens on click and on Enter; blur closes the panel without refining. This avoids accidental searches when a shopper changes focus to read the page.
Highlight markup helpers. The component does not ship a <Highlight> component or a snippet renderer; designers consume the raw item shape and use Plasmic's existing primitives.
Multi-language / RTL specific defaults. Default styling is LTR-shape; RTL stores override via Plasmic class — out of scope for v1's defaults.
Backporting the bridge-component pattern to existing catalog-search components.EPSearchBox's slot-only model and EPCurrentRefinements' single-component repeater are not refactored to compound shapes.
Further Notes
This PRD applies the catalog-search lessons L1 (per-iteration getItemProps via wrapper-onClick), L2 (compound headless components via path A — every visible element except the documented mobile-close exception lives in slots), L3 (every component ships a recognisable default slot shape), L4 (predictionsField is the ergonomic prop; the raw plugins array is the escape hatch), L5 (any future schema additions stay registered as hidden no-ops), L7 (previewState + MOCK_* design-time data), and L8 (live verification on examples/ep-commerce-app-router) as documented in feat(ep-commerce): catalog-search component coverage gaps #304's "Lessons from shipped work" section.
The q-field schema stability across EP customer tenants is unconfirmed (the demo at field123/ep-search-instantsearch-demo deployed at https://ep-spa-search-instantsearch.vercel.app/ is currently misbehaving, possibly an index issue). The configurable predictionsField prop is the package-side mitigation; the PR's verification step against the EP test catalog will confirm the default of "q" is correct against a freshly-configured tenant before merge.
The cross-cutting MCP gap add enhanced container insights #81 / PRD PRD: Expose Plasmic's invokeRefAction interaction type in the MCP (gap #81) #310 (invokeRefAction interaction wiring from MCP) does not block this PRD because the autocomplete behaviours are pre-wired internally — selection, submit, debounced refine, and panel visibility all happen without designer-side Plasmic interaction wiring. Ref-actions are exposed for advanced use cases (a "Search this category" CTA elsewhere on the page driving the autocomplete) but are not on the critical path for a working v1 drop.
Problem Statement
Shoppers on EP-powered storefronts built with Plasmic cannot get search-as-you-type suggestions. The current
EPSearchBoxonly refines the catalog-search query on debounced keystrokes, so the experience is "type, wait, see results in the grid below" — there is no live dropdown of matching query strings or recent searches anchored to the input. This makes the storefront feel slow on long queries and discoverable categories, and forces shoppers who don't know the exact product term to commit a query before getting any feedback.Plasmic designers cannot reach for an InstantSearch autocomplete primitive when assembling a search page; they have to either build one themselves (no headless component primitive ships in this package) or ignore the feature altogether. From the package's component coverage perspective this is item #3 of the catalog-search gaps tracked in #304.
The EP catalog-search service exposes a dedicated
type=autocompleteendpoint backed by a Typesense collection containing query-suggestion documents (with aqfield) — but the EP catalog-search-instantsearch-adapter README and the existing demo at field123/ep-search-instantsearch-demo are the only existing surfaces consuming it, and neither composes with the rest of the EP commerce Plasmic component family.Solution
Ship a four-component compound
EPSearchAutocompleteinplasmicpkgs/commerce-providers/elastic-path/src/catalog-search/, wrapping@algolia/autocomplete-core(the headless package, notautocomplete-js).The compound exposes Algolia's prop-getter pattern through Plasmic-friendly bridge components that own one DOM element each, spread the right prop-getter onto it, and slot designer-authored content for visuals. State, keyboard navigation, accessibility, and plugin support come from autocomplete-core; chrome and per-suggestion rendering come from the slot.
A shopper drops focus into the input, sees a panel of matching query-string suggestions sourced from EP's
type=autocompleteendpoint, can navigate with arrow keys, can pick one with click or Enter, and the mainEPSearchHitsgrid refreshes against the chosen query. The input value is also live-mirrored (debounced) into the shared catalog-search query so the grid feels responsive while typing.A designer drops the four components into a Plasmic page (or an opinionated default tree if dropping just the provider seeds them all), gets a working autocomplete on desktop and mobile out of the box, and can restyle every visible element through Plasmic's panel because the components honour the L2 headless-styling contract: only the inner mobile-close button renders chrome the designer didn't author, and that is itself a documented L2 exception with a stable data-attribute hook.
Recent-searches and other autocomplete-core plugins are supported but default-off; v1 ships predictions-only.
User Stories
Shopper-facing
Designer-facing
EPCurrentRefinements, so that the autocomplete component does not introduce a foreign authoring model.previewStateprop with mocked autocomplete state, so that I can lay out the suggestion list against representative data without a live EP backend.EPCatalogSearchProvider, so that I cannot accidentally compose an autocomplete that has no source of catalog-search state.EPSearchAutocompleteListsiblings inside one panel — one scoped to predictions, one to recent searches — and have them render only their own source's items, so that I can label and style each section independently.Developer-facing
type=autocompleterequest shape (q,include_fields,highlight_full_fields) baked into the provider rather than configured per consumer, so that a misconfigured request does not silently degrade UX in production.q, I want apredictionsFieldprop on the provider with a sensible default of"q", so that I can survive customer-tenant schema variation without forking the component.setQuery,focus,clear) so that ad-hoc Plasmic interactions outside the autocomplete tree (like a "Search this category" CTA) can drive it programmatically.pluginsescape hatch on the provider for advanced consumers to inject additional autocomplete-core plugins (custom analytics, third-party recent-searches stores), so that the four-component compound is not a closed system.Implementation Decisions
Components shipped (four, all in the catalog-search subpackage)
createAutocomplete()from@algolia/autocomplete-core. Owns the autocomplete-core instance, the predictions source, optional plugins (including the optional recent-searches plugin), the live-mirror bridge to the surroundinguseSearchBox().refine(), and the single render-time wrapper<div>that gives the panel a positioning context. PublishesautocompleteDatavia PlasmicDataProvider. Exposes ref-actions forsetQuery,focus,clear. Renders no visible chrome of its own beyond the wrapper element. Its registration enforcesparentComponentName: plasmic-commerce-ep-catalog-search-provider.getInputProps()onto the designer's slot input element via runtime cloneElement. Single-element-slot semantics with a default of one<input type="search">so that a fresh drop is functional. Required parent is the autocomplete provider.state.isOpen. SpreadsgetPanelProps()onto its outer<div data-ep-autocomplete-panel>. Renders one component-owned chrome element (the mobile-close button as a documented L2 exception, attributedata-ep-autocomplete-close) which is hidden via media query at desktop breakpoints. Designer composes the panel's inner content through the slot. Required parent is the autocomplete provider.sourceIdprop scopes the rendered items to one named source (e.g."predictions","recent"). Renders<ul {...getListProps()}>and per-item<li {...getItemProps({item, source})}>wrappers aroundrepeatedElement(i, children)so the wrapper element carries the ARIArole="option",aria-selected, mouse handlers, click handler, and ref required by autocomplete-core. Publishes per-iterationcurrentSuggestioncontext ({ item, isHighlighted, source, query, refine }). Required parent is the autocomplete provider.Architectural decisions
autocomplete-js), which renders the panel via its owncreateRootand bakes@algolia/autocomplete-theme-classic. The headless@algolia/autocomplete-corepackage supplies the same state-machine, plugin, and accessibility behaviour without owning the DOM, and is the maintainer-blessed path for full custom UI per their "creating a custom renderer" guide.postMultiSearchwithtype: "autocomplete", requestsinclude_fieldsandhighlight_full_fieldsfor the configuredpredictionsField, and maps the response into the autocomplete-core source contract. The advancedpluginsarray remains an escape hatch for power users who need additional sources.useSearchBox().refine(). On every change to the autocomplete-core input state, the provider debounces (default 300ms, matchingEPSearchBox's existing debounce) and pushes the value into the surrounding InstantSearch query. On selection (item click or Enter on a highlighted item) and on submit-without-selection (Enter with no highlight), the refine fires immediately and the panel closes. This matches the demo at field123/ep-search-instantsearch-demo and aligns the autocomplete's input with the rest of the catalog-search components reading the same query state.useSearchBox().query. URL-state survives page load — the autocomplete-core instance is initialised with the existing InstantSearch query so a navigated-into search page does not blank the input.enableRecentSearches?: boolean(default false),recentSearchesKey?: string(default an EP-namespaced key),recentSearchesLimit?: number(default 3, matching demo). When enabled, the provider requires@algolia/autocomplete-plugin-recent-searchesas a peer dependency. Disabled-by-default avoids surprise localStorage writes and keeps v1's bundle and test scope minimal. Adding the prop later is purely additive — no breaking change for consumers shipping with default-off behaviour.headless-styling.ts. New:where()rules for the autocomplete root, panel, and close button give the components a sensible desktop layout (panel anchored absolute below the input, scrollable, shadowed) and a mobile-friendly variant below 680px (full-screen or top-anchored sheet, max-height unset, close button visible). Zero specificity means any Plasmic-set class overrides every property — designers retain full styling control.<button data-ep-autocomplete-close>rendered inside the panel, hidden by media query above 680px. This is the only chrome the component renders without designer authoring; it ships because a fresh drop on mobile would otherwise trap the user in a panel they cannot dismiss. Designer hides it withdisplay: noneor restyles via the data attribute.EPSearchBox's post-PRD: Make EPSearchBox styleable by lifting its DOM out of the code-component #308 wrapperless pattern, justified because the panel needs aposition: relativeancestor for absolute anchoring. The wrapper hasdata-ep-autocomplete-root.<li>is component-rendered (sogetItemProps()'sref,aria-selected,role="option", and mouse handlers all attach correctly) but the visual content of each row is the designer's slot child, repeated via Plasmic'srepeatedElement. Designer drives row visuals (text, image, highlight markup) via the per-iterationcurrentSuggestioncontext, including theisHighlightedboolean which they bind to a Plasmic conditional style._highlightResult/highlightsproduced by the EP adapter. Designers wire the highlighted snippet through Plasmic's existing dynamic-text + sanitised-HTML primitives — the component does not bake an opinion on highlight rendering.Deep modules
useEPAutocompleteStatehook. Encapsulates the autocomplete-core integration. Inputs include the EP shopper client (read from the existing commerce provider context),predictionsField,debounceMs,enableRecentSearches, recent-searches storage config, and the optionalpluginsarray. Outputs the live state, all six prop-getters, an imperativerefinefor ref-actions, an imperativeclose, and the publishedcollectionsarray. Hides thecreateAutocompletelifecycle, plugin loading, source construction, debounce, mirror-to-useSearchBoxwiring, and React state subscription behind a single hook signature. The four React components do not callcreateAutocompletedirectly — they consume this hook and the published context.predictionsSourcefactory. A pure function that returns an autocomplete-core source descriptor configured for the EPtype=autocompleteendpoint. Owns the request shape (the multi-search body andinclude_fieldsparameter), the response mapping (item ↔ field), and graceful error handling (returns an empty list on adapter failure rather than throwing). Tested as a unit independent of React, autocomplete-core, and Plasmic.Schema and registration changes
index.tsand the package'ssrc/index.tsxregistering all four components, withparentComponentNameenforcing the family hierarchy. The fourregister*functions follow the existing pattern.design-time-data.tsfor autocomplete state and prediction items, used by both the editor preview branch and the unit tests.:where()blocks inheadless-styling.tsfor the autocomplete root, panel, close button, and the 680px mobile media-query variant.package.jsondeclares the@algolia/autocomplete-coreruntime dependency.@algolia/autocomplete-plugin-recent-searchesis declared as a peerDependency withpeerDependenciesMeta.optional = true— required only when consumers enable recent searches.API contracts
EPSearchAutocompleteprops:predictionsField?: string(default"q"),debounceMs?: number(default 300),enableRecentSearches?: boolean(default false),recentSearchesKey?: string,recentSearchesLimit?: number,plugins?: AutocompletePlugin[](advanced, hidden in the Studio panel),previewState?: "auto" | "withData", plus children + className. Ref-actions:setQuery(value: string),focus(),clear().EPSearchAutocompleteInputprops: children + className + previewState. Single-slot, default content is<input type="search">.EPSearchAutocompletePanelprops: children + className + previewState. Single-slot, default content is oneEPSearchAutocompleteList.EPSearchAutocompleteListprops:sourceId?: string(default unset = renders all sources' items), children + className + previewState. Single-slot, default content is a styled suggestion-row template usingcurrentSuggestionbindings.autocompleteDataon the provider;currentSuggestionper repeated list item.data-ep-autocomplete-root,data-ep-autocomplete-input,data-ep-autocomplete-panel,data-ep-autocomplete-close,data-ep-autocomplete-list. These are the public surface for designer CSS overrides and for the styling-contract tests.Testing Decisions
Definition of a good test
A good test asserts external behaviour through observable outputs (rendered DOM, published context values, calls into bridges like
useSearchBox().refine) rather than internal implementation details (autocomplete-core's private state shape, the order of operations insideuseEPAutocompleteState's effects, the specific React fiber structure). Tests should survive a refactor that swaps the autocomplete-core version, replaces the debounce primitive, or restructures the React tree, as long as the contract — what designers see, what shoppers see, what the catalog-search query state ends up in — is preserved.Modules and components under test
useEPAutocompleteState(deep module, isolated tests):collectionsshape matches the expected per-source structure.enableRecentSearches: false, thecollectionsarray contains exactly one source (predictions); withenableRecentSearches: true, it contains both.useSearchBox().refine, with the most recent value, after the configureddebounceMs.useSearchBox().refineimmediately withitem[predictionsField], and the panel closes.useSearchBox().refinewith the current input value.predictionsSource(deep module, isolated tests):getItemsinvokespostMultiSearchwith the expected body shape (type: "autocomplete",q,include_fields,highlight_full_fields).response.results[0].hitsand expose the configured field plus the raw hit for downstream highlight rendering.[]rather than throwing.predictionsFieldis passed through toinclude_fields.The four registered components (contract tests, mirroring the
describeHeadlessStylingContractprecedent):classNamereaches the documented data-attribute leaf for each component.:where()style block.previewState: "withData"andpreviewState: "auto"in editor canvas both render the mock branch with the expected published context.parentComponentNameis enforced for each non-provider component.defaultValueproduces a recognisable shape on fresh drop.EPSearchAutocompleteList(behavioural):onSelectfor that item, which in turn invokes the provider's refine with the right query.<li>receivesaria-selected="true"for the highlighted item andaria-selected="false"for the rest.sourceIdis set, only that source's items are repeated.currentSuggestioncontext is published with the expected shape.Prior art
The existing test suites in
plasmicpkgs/commerce-providers/elastic-path/src/catalog-search/__tests__/are the closest match.catalog-search-components.test.tsxis where the new contract assertions live, alongside the existing 9-component contract suite shipped with #309 / #316.cloneWithInjectedHandlers.test.tsxis the precedent for testing a small extracted helper in isolation;useEPAutocompleteStateandpredictionsSourcefollow the same shape — narrow inputs, narrow outputs, no React or DOM mocking beyond what the unit demands.react-instantsearchis mocked at the module level the same way the existing components mock it.End-to-end verification on
examples/ep-commerce-app-router(per L8) is part of the PR's acceptance criteria, not the automated test suite. The verification checklist covers the shopper user stories above on the live storefront against a real EP catalog with theautocompletecollection populated.Out of Scope
type=autocompleteis the closest equivalent and is what we wrap; any future analytics-populated suggestions surface would be a separate component or a new source-id added to the existing list.EXPERIMENTAL_Autocompletewidget's prompt-suggestions concept assumes infrastructure we do not have. Defer indefinitely.<Highlight>component or a snippet renderer; designers consume the raw item shape and use Plasmic's existing primitives.EPSearchBox's slot-only model andEPCurrentRefinements' single-component repeater are not refactored to compound shapes.Further Notes
getItemPropsvia wrapper-onClick), L2 (compound headless components via path A — every visible element except the documented mobile-close exception lives in slots), L3 (every component ships a recognisable default slot shape), L4 (predictionsFieldis the ergonomic prop; the rawpluginsarray is the escape hatch), L5 (any future schema additions stay registered as hidden no-ops), L7 (previewState+MOCK_*design-time data), and L8 (live verification onexamples/ep-commerce-app-router) as documented in feat(ep-commerce): catalog-search component coverage gaps #304's "Lessons from shipped work" section.q-field schema stability across EP customer tenants is unconfirmed (the demo at field123/ep-search-instantsearch-demo deployed at https://ep-spa-search-instantsearch.vercel.app/ is currently misbehaving, possibly an index issue). The configurablepredictionsFieldprop is the package-side mitigation; the PR's verification step against the EP test catalog will confirm the default of"q"is correct against a freshly-configured tenant before merge.invokeRefActioninteraction type in the MCP (gap #81) #310 (invokeRefActioninteraction wiring from MCP) does not block this PRD because the autocomplete behaviours are pre-wired internally — selection, submit, debounced refine, and panel visibility all happen without designer-side Plasmic interaction wiring. Ref-actions are exposed for advanced use cases (a "Search this category" CTA elsewhere on the page driving the autocomplete) but are not on the critical path for a working v1 drop.