Skip to content

feat(ep-commerce): add EPSearchEmpty for zero-result state#329

Open
field123 wants to merge 1 commit into
masterfrom
feat/ep-search-empty
Open

feat(ep-commerce): add EPSearchEmpty for zero-result state#329
field123 wants to merge 1 commit into
masterfrom
feat/ep-search-empty

Conversation

@field123
Copy link
Copy Markdown
Collaborator

@field123 field123 commented May 8, 2026

Part of #304 — ships item 9 from the catalog-search coverage gaps roadmap (Suggested Sequencing #1).

Summary

Designers drop EPSearchEmpty as a sibling of EPSearchHits and it renders heading + body copy when (and only when) a real InstantSearch response lands with zero hits.

  • Visibility gate is useInstantSearch().results !== null && results.nbHits === 0 — deliberately stricter than useStats().nbHits. Stays silent during the SSR-to-first-response window (results is null) and during search-time errors (results keeps last successful value). Adapter-init failures remain the Provider's errorContent concern.
  • Mock + Inner split per L7: editor branch renders the wrapper unconditionally without ever calling useInstantSearch; runtime branch gates and shares the same JSX wrapper so the headless-styling contract sees structurally equivalent leaves either way.
  • role=\"status\" on the wrapper — implicitly aria-live=\"polite\" per ARIA spec — so screen readers announce the slot's content when results flip from N hits to 0.
  • data-ep-search-empty is the documented leaf for designer className forwarding and the headless :where() block (width: 100%; align-self: stretch; text-align: center).
  • Default slot ships heading + body in a centered vbox (h2: \"No results found\", p: \"Try clearing your filters or searching for something else.\") — recognisable shape on fresh drop, all values overridable in Studio.
  • Sibling visibility is designer's call via dataCond: $ctx.searchStatsData.nbHits > 0; Empty stays single-purpose and does not publish a redundant searchIsEmpty flag.

Design decisions resolved (/grill-me walkthrough)

# Decision
Visibility gate results !== null && results.nbHits === 0 (no flicker, no error masking)
Errors Out of scope — Provider's errorContent handles them
Sibling coordination Designer-wired via existing searchStatsData.nbHits context
DOM ownership Single wrapper <div data-ep-search-empty role=\"status\">
Default copy Heading + body, no embedded CTA, no dynamic query echo
Preview state L7 \"auto\" | \"withData\" with Mock + Inner split
A11y role=\"status\" baked, no explicit aria-live (implied)
Headless CSS width: 100%; align-self: stretch; text-align: center
Props API Minimal — children, className, previewState; parent enforced
Default tree h2 + p in centered vbox, gray-500 body

Test plan

  • 193/193 catalog-search jest tests pass (yarn test plasmicpkgs/commerce-providers/elastic-path/src/catalog-search/__tests__/)
  • 16 new EPSearchEmpty cases: meta enforcement, gate (null / 0 / >0), wrapper role + data attrs, mock branch (Inner never mounts in canvas), previewState=\"withData\" override, lifecycle flips (5→0 mounts, 0→5 unmounts cleanly), default-slot shape, registration
  • Reviewer: drop EP Search Empty next to EP Search Hits on a search page, verify it renders only on zero-hit responses and shows the default heading + body
  • Reviewer: verify role=\"status\" is announced by VoiceOver / NVDA when filters narrow to zero
  • Reviewer: confirm fresh drop styling matches storefront retail conventions (centered, readable hierarchy)

What's next from #304

Per the issue's Suggested Sequencing, after this lands the next item is #7 EPToggleRefinement (single-slot wrapper-onClick for boolean filters like "In stock only").

Closes the no-results UX gap from #304 / Suggested-Sequencing #1.
Designers drop EPSearchEmpty as a sibling of EPSearchHits and it renders
heading + body copy when (and only when) a real InstantSearch response
lands with zero hits.

Visibility gate is `useInstantSearch().results !== null && results.nbHits
=== 0` — deliberately stricter than `useStats().nbHits`. It stays silent
during the SSR-to-first-response window (`results` is null until the
first response) and during search-time errors (`results` keeps the last
successful value), so designers never see Empty flash on a fresh page or
hide a real error behind a "no results" message. Adapter-init failures
remain the Provider's `errorContent` concern.

Component splits into Mock + Inner per L7: editor branch renders the
wrapper unconditionally without ever calling `useInstantSearch`, runtime
branch gates and shares the same JSX wrapper so the headless-styling
contract sees structurally equivalent leaves either way.

Wrapper carries `role="status"`, which is implicitly `aria-live="polite"`
per the ARIA spec, so screen readers announce the slot's content when
results flip from N hits to 0. `data-ep-search-empty` is the documented
leaf for designer className forwarding and the headless `:where()` block
(width: 100%; align-self: stretch; text-align: center).

Default slot ships heading + body in a centered vbox — recognisable
"no results" shape on fresh drop, all values overridable in Studio.
Sibling visibility (Stats / Pagination) is the designer's call via
`dataCond: $ctx.searchStatsData.nbHits > 0`; Empty stays single-purpose
and does not publish a redundant `searchIsEmpty` flag.

Tests: 16 new cases covering visibility gate, role/data attributes,
mock-vs-runtime split (Inner never mounts in canvas), preview-state
overrides, lifecycle flips (5 → 0 mounts, 0 → 5 unmounts), default-slot
shape, registration. 193/193 catalog-search jest tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant