Skip to content

EP Catalog Search: tiered, progressively-disclosed designer data surface (ADR-0011) #355

Description

@field123

Parent PRD

Part of #304 (catalog-search component coverage gaps).

Spec: ADR-0011 — "Catalog-search designer ergonomics: a tiered, progressively-disclosed data surface" (lives in the ISO storefront repo, which has no remote; decisions quoted inline below). Companion to the ADR-0006 field-display and ADR-0007 curated-extensions-map work already shipped via #339 / #350.

What to build

Bring the catalog-search components up to the ADR-0006/0007 insulation floor, applying one governing principle — progressive disclosure: make the common case a discoverable, zero-lookup, zero-wiring action; keep every advanced path reachable underneath.

Governing guarantee — no enhancement removes a capability. Every convenience is opt-out to the exact prior behaviour, and every underlying value/function (raw extensions/rawData, toggle/goTo, EPClearRefinements, raw pages) remains. The simple path is added beside the powerful one, never on top of it.

The work is two independent, end-to-end vertical slices (each cuts normalizer → component API → mock/defaults → demoable on a search page). Either may be grabbed first; they touch different components.

Slice 1 — Discoverable, tiered hit data surface

Today a designer must type ($ctx.currentProduct.extensions["products(iso-standard)"] || {}).title — a throw-guarded, slug-bracketed lookup that doesn't autocomplete in the data picker. Replace with a tiered surface:

  • (D7, prerequisite) Shared base Product contract both normalizers (normalizeHitToCurrentProduct and the PDP shopper-SDK path) produce identically via shared utils/ primitives (extensions map, formatCurrency, image/highlight resolution). Each source extends it with a typed superset (SearchHitProduct extends Product adds _highlighted*/_score/_rawTypesenseHit). Input-key mapping stays per-source. A hit that lacks a base field omits it (presence reads absent) — never synthesises an empty.
  • (D1) EPSearchHits publishes per hit: a flat, null-safe, slug-free currentProduct.fields.<key> namespace flattening a configurable primaryExtensionTemplate prop (Tier 1, discoverable), and the ADR-0007 slug-keyed $ctx.productExtensions Proxy map (Tier 2). Raw extensions/rawData retained (Tier 3).
  • (D3, fields) Add highlight: auto | on | off and a titlecase option to EPProductField's format enum. auto renders the highlighted/snippet variant when present (in a hit) and the plain value otherwise (inert on the PDP). The ADR-0006 field components are the Tier-0 GUI-selector for hit fields — they already resolve in hit cards via the rawData bridge.
  • (D8/D9) EPSearchHits ships a batteries-included-but-unstyled default card built from the field components; collapse its divergent mock tree to a single render (data wrapper holds the hook, pure inner takes items) per the PRD: EP catalog-search components must honour designer styling (headless styling contract) #305 contract.

Slice 2 — Zero-wiring facets & pagination

Today scope pills and pagination are hand-wired with customFunction calls ($ctx.currentRefinement.toggle(), $ctx.searchPaginationData.goTo()) and the "All" pill is a second glued-on component.

  • (D2) Auto-wire interactive items via the prop-getter pattern the autocomplete already uses (getItemProps() spreads onClick + role + aria-*): the component owns click + active/current state + a11y; the designer only styles the slot. Auto-wiring is a defaulted-on prop whose OFF state is byte-for-byte today's behaviour; toggle/goTo remain on context as the Tier-1 escape. (Supersedes the earlier "rename goTo vs goToPage" idea.)
  • (D5) Split single-select out of EPRefinementList into a dedicated EPSingleSelectFacet (useMenu-backed, replace semantics, radio a11y). Per-item context stays identical (value/label/count/isRefined/toggle) so slot content ports across a mode change. Generic name (no ISO knowledge in the package).
  • (D6) EPSingleSelectFacet gains opt-in includeAllOption emitting an "All" pseudo-item (isRefined when nothing refined; toggle clears), flagged isAllOption. Default off; EPRefinementList + EPClearRefinements two-component path stays working.
  • (D4) Enrich EPSearchPagination's exposed page-item model with ellipsis sentinels and isFirst/isLast/isCurrent flags (window size as a prop). Keep raw pages: number[] + goTo.
  • (D8/D9) Working unstyled defaults; collapse EPRefinementList's divergent mock tree per the PRD: EP catalog-search components must honour designer styling (headless styling contract) #305 contract.

Cross-cutting (do in whichever slice lands first)

  • (D3, provider) EPCatalogSearchProvider gains excludeVariationChildren (default true) emitting meta.product_types:!=child, AND'd with any baseFilter. Deliberate non-additive default — the common case is near-universally correct; no external consumers depend on the old behaviour.
  • (D8) Add a single flat "EP Catalog Search" section to group the family in the insert menu (parentComponentName already handles in-context surfacing).

Acceptance criteria

Slice 1

  • Both normalizers produce an identical shared base Product; SearchHitProduct superset is typed; a base field absent from a hit reads as absent (presence false), never throws.
  • A designer binds currentProduct.fields.<key> (autocompletes in the picker) with no slug bracket and no (… || {}).
  • EPProductField with highlight: auto renders the <mark> name/abstract in a hit and the plain value on a PDP; titlecase renders product_kind/lifecycle_status without a ternary.
  • EPSearchHits default slot renders a working card from the field components against mock data; mock and runtime produce the same DOM tree (assertHeadlessStylingContract passes).

Slice 2

  • Refinement items and page items refine/navigate on click with no customFunction wired; active/current state and ARIA come for free; auto-wiring disables to today's function-on-context behaviour.
  • EPSingleSelectFacet exists (radio/menu semantics) with opt-in includeAllOption; EPRefinementList + EPClearRefinements still work unchanged.
  • EPSearchPagination exposes windowed page items (ellipsis + first/last/current); raw pages/goTo retained.
  • EPRefinementList mock collapses to a single render; assertHeadlessStylingContract passes.

Cross-cutting

  • excludeVariationChildren defaults on, emits the correct filter, AND's with baseFilter; documented as a behaviour change.
  • Catalog-search components group under one "EP Catalog Search" insert-menu section.
  • All existing package tests pass; existing component instances render without re-authoring (all enhancements opt-out to prior behaviour).

Blocked by

None — can start immediately. The two slices are independent (different components); if doing one first, Slice 1 is the higher-value foundation.

ADR decisions addressed

  • Slice 1: D1, D3 (fields), D7, D8/D9 (hits)
  • Slice 2: D2, D4, D5, D6, D8/D9 (refinement list)
  • Cross-cutting: D3 (provider), D8 (insert section)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions