Skip to content

Search: jetpack-search/filter-static block (SEARCH-219, 3/3)#49040

Open
kangzj wants to merge 3 commits into
trunkfrom
search-219-static-filter-block
Open

Search: jetpack-search/filter-static block (SEARCH-219, 3/3)#49040
kangzj wants to merge 3 commits into
trunkfrom
search-219-static-filter-block

Conversation

@kangzj
Copy link
Copy Markdown
Contributor

@kangzj kangzj commented May 21, 2026

Third of three sequential PRs for SEARCH-219. Base branch: #49039 (review parts 1 and 2 first).

Part What ships Status
1/3 #49038 — JS store plumbing. open
2/3 #49039 — PHP helper + REST endpoint + state seed. open
3/3 (this PR) The user-facing jetpack-search/filter-static block + editor wiring + changelog. ⏳ this PR

Proposed changes

  • block.json — three attributes: variation (sidebar/tabbed scopes which filters render), filterId (optionally narrow to one registered filter; empty renders all matching the variation), label (override the server-supplied name). No parent/ancestor constraint — works bare or inside filters / filters-popover.
  • render.php — pulls entries via Filter_Static::filters_for_variation() (from part 2), merges the URL-seeded selection (falls back to the entry's selected default), pushes filterConfigs[$id] + staticFilterSelections[$id] into the shared Interactivity state, and emits one <fieldset> per entry with <input type="radio" name="<filter_id>"> for native single-select grouping. data-wp-on--change="actions.onStaticFilterChange" wires each input to the store action (from part 1).
  • edit.js — inspector exposes the three attributes; the filter picker is populated via the REST endpoint from part 2. Empty-state Placeholder when no static filters are registered. Sample radios so the layout previews in place.
  • view.js — bare import 'jetpack-search/store' (mandatory per search-blocks/AGENTS.md).
  • style.scss — minimal styles, reuses existing filter list/label partials.
  • editor/register-blocks.js + editor/icons.js — register the edit component and assign the brand-green postTerms icon.
  • Changelog entry (minor, type=added).

Falls under the existing jetpack_search_blocks_enabled gate; no new flag.

Related product discussion/links

Does this pull request change what data or activity we track or use?

No.

Testing instructions

End-to-end smoke test on a Search 3.0 blocks-enabled site:

  1. Register a static filter:
add_filter( 'jetpack_search_static_filters', function () {
    return [ [
        'filter_id' => 'section',
        'name'      => 'Section',
        'type'      => 'group',
        'variation' => 'sidebar',
        'selected'  => '',
        'values'    => [
            [ 'name' => 'News',   'value' => 'news' ],
            [ 'name' => 'Guides', 'value' => 'guides' ],
        ],
    ] ];
} );
  • Build the search package: pnpm jetpack build search --deps.
  • Insert Static Filter on a page that also has search-results. Inspector lists "Section" in the Filter picker (REST round-trip from part 2).
  • Front end: two radios render (News / Guides), none checked initially.
  • Click Guides → URL becomes ?section=guides (scalar — no []). Network shows one search request with bool.filter carrying { term: { section: 'guides' } }.
  • Reload ?section=guides → radio stays checked (server-rendered checked attribute via Filter_Static::parse_url_selections()).
  • Pick News → URL section replaces (does not append).
  • Reload the page mid-search: state survives via the URL, the radio re-checks server-side, and the JS reactive binding takes over after hydration.
  • Disable the host filter (return []) → block renders nothing on the front end; editor Placeholder shows the empty-state message.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the search-219-static-filter-block branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack search-219-static-filter-block

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions github-actions Bot added the [Package] Search Contains core Search functionality for Jetpack and Search plugins label May 21, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@github-actions github-actions Bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label May 21, 2026
@jp-launch-control
Copy link
Copy Markdown

jp-launch-control Bot commented May 21, 2026

Code Coverage Summary

3 files are newly checked for coverage.

File Coverage
projects/packages/search/src/search-blocks/blocks/filter-static/render.php 0/64 (0.00%) 💔
projects/packages/search/src/search-blocks/blocks/filter-static/edit.js 2/35 (5.71%) 💔
projects/packages/search/src/search-blocks/blocks/filter-static/view.js 0/0 (—%) 🤷

Full summary · PHP report · JS report

If appropriate, add one of these labels to override the failing coverage check: Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR I don't care about code coverage for this PR Use this label to ignore the check for insufficient code coveage.

@kangzj kangzj force-pushed the search-219-static-filter-helper branch from a45f65d to ea9574c Compare May 21, 2026 05:26
@kangzj kangzj force-pushed the search-219-static-filter-block branch from 58dccee to eaa5154 Compare May 21, 2026 06:19
@kangzj

This comment has been minimized.

@kangzj

This comment has been minimized.

@claude

This comment has been minimized.

This comment has been minimized.

Copilot finished work on behalf of kangzj May 21, 2026 06:28
kangzj added a commit that referenced this pull request May 21, 2026
…on, normalizeVariation test (PR #49040)

Addresses copilot-swe-agent's review:

- Changelog: 'Search: new `...` block' → 'Search: add `...` block'
  (imperative mood per the changelogger convention).
- Accessibility: <fieldset> now always emits a <legend> so the radio
  group has a programmatic name. When both the block-level label
  override and the server-supplied name are empty, the legend falls
  back to the filter_id — that case is host-site misconfiguration but
  the markup degrades gracefully instead of leaving the fieldset
  unnamed for screen readers.
- Icon collision: filter-static was using `postTerms`, which already
  belongs to the filter-checkbox 'post_tag' variation. The
  FILTER_CHECKBOX_VARIATION_ICONS docblock guarantees no-duplicates
  across the two icon maps. Switched filter-static to
  `formatListNumbered` (still semantically fitting: a numbered/preset
  radio list of server-defined choices, visually distinct from
  filter-checkbox's `formatListBullets`).
- Test coverage: new tests/js/search-blocks/filter-static-edit.test.js
  pins `normalizeVariation` — anything other than the literal string
  'tabbed' collapses to 'sidebar', matching
  `Filter_Static::normalize_variation` on the PHP side. A drift
  between these two would scope the editor preview to a different
  filter subset than the front-end render.

Skipped:
- The `__()` third-arg-0 pattern in edit.js intentionally mirrors the
  same convention in filter-checkbox/edit.js (4 sites). Diverging here
  alone would create drift; leaving as a future repo-wide cleanup if
  the underlying minification issue is ever documented.

<!-- jp-loop -->
@kangzj

This comment has been minimized.

@kangzj

This comment has been minimized.

@kangzj

This comment has been minimized.

@claude

This comment has been minimized.

This comment has been minimized.

Copilot finished work on behalf of kangzj May 21, 2026 06:39
kangzj added a commit that referenced this pull request May 21, 2026
…g (PR #49038)

Block-side static-filter radios need a reactive checked binding so they
stay in sync with the store across paths that mutate
state.staticFilterSelections outside the radio's own change event — the
clearFilters() reset and the popstate-driven URL rewind. Without this,
radios visually keep their last clicked state even after the store has
cleared the slice.

The callback reads the per-<li> context (filterKey + optionValue) that
the filter-static block's render.php will emit, and compares against
state.staticFilterSelections[filterKey]. Each radio in render.php then
binds:
  data-wp-bind--checked="callbacks.isStaticFilterSelected"

Server-rendered `checked` still seeds first paint; this takes over
after hydration.

Surfaced during the review cycle on PR #49040.

<!-- jp-loop -->
@kangzj kangzj force-pushed the search-219-static-filter-helper branch from 308ad57 to 7da8d9d Compare May 21, 2026 06:48
kangzj added a commit that referenced this pull request May 21, 2026
…on, normalizeVariation test (PR #49040)

Addresses copilot-swe-agent's review:

- Changelog: 'Search: new `...` block' → 'Search: add `...` block'
  (imperative mood per the changelogger convention).
- Accessibility: <fieldset> now always emits a <legend> so the radio
  group has a programmatic name. When both the block-level label
  override and the server-supplied name are empty, the legend falls
  back to the filter_id — that case is host-site misconfiguration but
  the markup degrades gracefully instead of leaving the fieldset
  unnamed for screen readers.
- Icon collision: filter-static was using `postTerms`, which already
  belongs to the filter-checkbox 'post_tag' variation. The
  FILTER_CHECKBOX_VARIATION_ICONS docblock guarantees no-duplicates
  across the two icon maps. Switched filter-static to
  `formatListNumbered` (still semantically fitting: a numbered/preset
  radio list of server-defined choices, visually distinct from
  filter-checkbox's `formatListBullets`).
- Test coverage: new tests/js/search-blocks/filter-static-edit.test.js
  pins `normalizeVariation` — anything other than the literal string
  'tabbed' collapses to 'sidebar', matching
  `Filter_Static::normalize_variation` on the PHP side. A drift
  between these two would scope the editor preview to a different
  filter subset than the front-end render.

Skipped:
- The `__()` third-arg-0 pattern in edit.js intentionally mirrors the
  same convention in filter-checkbox/edit.js (4 sites). Diverging here
  alone would create drift; leaving as a future repo-wide cleanup if
  the underlying minification issue is ever documented.

<!-- jp-loop -->
kangzj added a commit that referenced this pull request May 21, 2026
…nt (PR #49040)

Two findings from claude[bot]'s re-review on PR #49040:

- Bug: Radio state drifted from the store after clearFilters() or
  handlePopState() because the radios had no reactive checked binding
  — the change event only fires on user-initiated transitions, so any
  state mutation through other paths left the DOM stale. Fix:
  - Add per-<li> data-wp-context with the option's value so the new
    callbacks.isStaticFilterSelected (landed in the foundation PR
    #49038) can compare it against state.staticFilterSelections.
  - Add data-wp-bind--checked="callbacks.isStaticFilterSelected" to
    each radio input. Server-rendered `checked` still seeds first
    paint; the reactive binding takes over after IA hydration.
- Doc nit (copilot): FILTER_CHECKBOX_VARIATION_ICONS docblock count
  was '19 + 8'; updated to '20 + 8' to reflect the new filter-static
  entry in BLOCK_ICONS.

Also deferred: the 're-pick a checked radio to deselect' line in the
prior testing instructions is unreachable (browser change event
semantics) — removing it from the PR body in a follow-up edit instead
of carrying a separate code change.

<!-- jp-loop -->
@kangzj kangzj force-pushed the search-219-static-filter-block branch from eb464f6 to 17e8213 Compare May 21, 2026 06:49
@kangzj

This comment has been minimized.

@kangzj

This comment has been minimized.

@kangzj

This comment has been minimized.

@claude

This comment has been minimized.

This comment has been minimized.

Copilot finished work on behalf of kangzj May 21, 2026 06:55
@kangzj kangzj force-pushed the search-219-static-filter-helper branch from 7da8d9d to 350ab3d Compare May 21, 2026 07:02
kangzj added a commit that referenced this pull request May 21, 2026
…on, normalizeVariation test (PR #49040)

Addresses copilot-swe-agent's review:

- Changelog: 'Search: new `...` block' → 'Search: add `...` block'
  (imperative mood per the changelogger convention).
- Accessibility: <fieldset> now always emits a <legend> so the radio
  group has a programmatic name. When both the block-level label
  override and the server-supplied name are empty, the legend falls
  back to the filter_id — that case is host-site misconfiguration but
  the markup degrades gracefully instead of leaving the fieldset
  unnamed for screen readers.
- Icon collision: filter-static was using `postTerms`, which already
  belongs to the filter-checkbox 'post_tag' variation. The
  FILTER_CHECKBOX_VARIATION_ICONS docblock guarantees no-duplicates
  across the two icon maps. Switched filter-static to
  `formatListNumbered` (still semantically fitting: a numbered/preset
  radio list of server-defined choices, visually distinct from
  filter-checkbox's `formatListBullets`).
- Test coverage: new tests/js/search-blocks/filter-static-edit.test.js
  pins `normalizeVariation` — anything other than the literal string
  'tabbed' collapses to 'sidebar', matching
  `Filter_Static::normalize_variation` on the PHP side. A drift
  between these two would scope the editor preview to a different
  filter subset than the front-end render.

Skipped:
- The `__()` third-arg-0 pattern in edit.js intentionally mirrors the
  same convention in filter-checkbox/edit.js (4 sites). Diverging here
  alone would create drift; leaving as a future repo-wide cleanup if
  the underlying minification issue is ever documented.

<!-- jp-loop -->
@kangzj kangzj force-pushed the search-219-static-filter-block branch from 17e8213 to b10667e Compare May 21, 2026 07:02
kangzj added a commit that referenced this pull request May 21, 2026
…nt (PR #49040)

Two findings from claude[bot]'s re-review on PR #49040:

- Bug: Radio state drifted from the store after clearFilters() or
  handlePopState() because the radios had no reactive checked binding
  — the change event only fires on user-initiated transitions, so any
  state mutation through other paths left the DOM stale. Fix:
  - Add per-<li> data-wp-context with the option's value so the new
    callbacks.isStaticFilterSelected (landed in the foundation PR
    #49038) can compare it against state.staticFilterSelections.
  - Add data-wp-bind--checked="callbacks.isStaticFilterSelected" to
    each radio input. Server-rendered `checked` still seeds first
    paint; the reactive binding takes over after IA hydration.
- Doc nit (copilot): FILTER_CHECKBOX_VARIATION_ICONS docblock count
  was '19 + 8'; updated to '20 + 8' to reflect the new filter-static
  entry in BLOCK_ICONS.

Also deferred: the 're-pick a checked radio to deselect' line in the
prior testing instructions is unreachable (browser change event
semantics) — removing it from the PR body in a follow-up edit instead
of carrying a separate code change.

<!-- jp-loop -->
@kangzj kangzj force-pushed the search-219-static-filter-helper branch from 350ab3d to 0c55d44 Compare May 21, 2026 07:28
kangzj added a commit that referenced this pull request May 21, 2026
…on, normalizeVariation test (PR #49040)

Addresses copilot-swe-agent's review:

- Changelog: 'Search: new `...` block' → 'Search: add `...` block'
  (imperative mood per the changelogger convention).
- Accessibility: <fieldset> now always emits a <legend> so the radio
  group has a programmatic name. When both the block-level label
  override and the server-supplied name are empty, the legend falls
  back to the filter_id — that case is host-site misconfiguration but
  the markup degrades gracefully instead of leaving the fieldset
  unnamed for screen readers.
- Icon collision: filter-static was using `postTerms`, which already
  belongs to the filter-checkbox 'post_tag' variation. The
  FILTER_CHECKBOX_VARIATION_ICONS docblock guarantees no-duplicates
  across the two icon maps. Switched filter-static to
  `formatListNumbered` (still semantically fitting: a numbered/preset
  radio list of server-defined choices, visually distinct from
  filter-checkbox's `formatListBullets`).
- Test coverage: new tests/js/search-blocks/filter-static-edit.test.js
  pins `normalizeVariation` — anything other than the literal string
  'tabbed' collapses to 'sidebar', matching
  `Filter_Static::normalize_variation` on the PHP side. A drift
  between these two would scope the editor preview to a different
  filter subset than the front-end render.

Skipped:
- The `__()` third-arg-0 pattern in edit.js intentionally mirrors the
  same convention in filter-checkbox/edit.js (4 sites). Diverging here
  alone would create drift; leaving as a future repo-wide cleanup if
  the underlying minification issue is ever documented.

<!-- jp-loop -->
kangzj added a commit that referenced this pull request May 21, 2026
…nt (PR #49040)

Two findings from claude[bot]'s re-review on PR #49040:

- Bug: Radio state drifted from the store after clearFilters() or
  handlePopState() because the radios had no reactive checked binding
  — the change event only fires on user-initiated transitions, so any
  state mutation through other paths left the DOM stale. Fix:
  - Add per-<li> data-wp-context with the option's value so the new
    callbacks.isStaticFilterSelected (landed in the foundation PR
    #49038) can compare it against state.staticFilterSelections.
  - Add data-wp-bind--checked="callbacks.isStaticFilterSelected" to
    each radio input. Server-rendered `checked` still seeds first
    paint; the reactive binding takes over after IA hydration.
- Doc nit (copilot): FILTER_CHECKBOX_VARIATION_ICONS docblock count
  was '19 + 8'; updated to '20 + 8' to reflect the new filter-static
  entry in BLOCK_ICONS.

Also deferred: the 're-pick a checked radio to deselect' line in the
prior testing instructions is unreachable (browser change event
semantics) — removing it from the PR body in a follow-up edit instead
of carrying a separate code change.

<!-- jp-loop -->
@kangzj kangzj force-pushed the search-219-static-filter-block branch from b10667e to 63d0b9c Compare May 21, 2026 07:28
@kangzj kangzj added [Status] Needs Team Review Obsolete. Use Needs Review instead. and removed [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. labels May 21, 2026
@kangzj
Copy link
Copy Markdown
Contributor Author

kangzj commented May 21, 2026

🤖 Review-cycle summary — initial → 63d0b9c46d

3 rounds; 57 functional CI checks pass; 1 informational Code coverage requirement pending (non-blocking); 7 review items addressed (5 from copilot's first review, 2 bugs from claude); 2 AI reviewers approved.

What changed during the cycle

Commits added on top of the initial PR:

  • eb464f6b91 — Round 2: changelog wording, a11y legend fallback, postTerms icon collision, normalizeVariation unit test.
  • 17e8213b45 — Round 3: reactive radio binding (data-wp-context + data-wp-bind--checked) + icon docblock count fix.

Review items addressed:

Source Item Resolution
copilot-swe-agent Changelog imperative mood ("new" → "add") (eb464f6b91)
copilot-swe-agent missing for a11y when label + name empty Always emits , falls back to filter_id (eb464f6b91)
copilot-swe-agent Non-standard __() third arg Acknowledged — kept consistent with filter-checkbox; deferred as repo-wide cleanup
copilot-swe-agent postTerms icon collision with FILTER_CHECKBOX_VARIATION_ICONS.post_tag Switched to formatListNumbered (eb464f6b91)
copilot-swe-agent No unit test for normalizeVariation Added filter-static-edit.test.js (eb464f6b91)
claude[bot] Bug: radio state drifts from store after clearFilters() / handlePopState() Added per-
  • data-wp-context + data-wp-bind--checked binding (17e8213b45); pairs with isStaticFilterSelected callback added in PR Search: store foundation for static-filter URL contract (SEARCH-219, 1/3) #49038
  • claude[bot] Bug: testing instruction claimed re-pick deselects (unreachable via radio change event) Removed from PR body; setStaticFilter toggle branch stays as defensive no-op
    copilot-swe-agent Icon docblock count "19 + 8" → "20 + 8" (17e8213b45)

    New file added: tests/js/search-blocks/filter-static-edit.test.js (3 tests for normalizeVariation).

    Reviewer outcome (last review):

    • claude[bot]: "Ready to merge. All previously reported bugs are fixed and verified. No new issues found."
    • copilot-swe-agent: "LGTM — no findings."

    CI: 57 functional checks pass; Code coverage requirement pending — non-blocking.

    kangzj added a commit that referenced this pull request May 22, 2026
    …1/3) (#49038)
    
    * Search: store foundation for static-filter URL contract (SEARCH-219)
    
    Adds the JS store plumbing the upcoming `jetpack-search/filter-static`
    block depends on. Foundation-only — inert until a block contributes a
    `kind: 'static'` filterConfig entry.
    
    - `store/url-state.js`: `stateToUrlParams` accepts a
      `staticFilterSelections` slice and serializes each `kind === 'static'`
      entry as a scalar `?filter_id=value` param (no `[]` suffix).
      `urlParamsToState` mirrors the parse direction — scalar params land in
      `staticFilterSelections` only when their key is registered as
      kind=static, so plugin-emitted scalar params can't pollute state.
    - `store/index.js`: new `staticFilterSelections` state slice,
      `setStaticFilter` (replace, not toggle) and `onStaticFilterChange`
      actions, `gateStaticFilterSelections` helper. Slice threaded through
      `syncToUrl`, `handlePopState`, `clearFilters`, `initialize()`
      re-gating, the search-request builder, and the `hasActiveFilters` /
      `activeFilterCount` getters.
    - `store/api.js`: `buildStaticFilterClauses` emits ES `term` clauses
      keyed by the static filter id; folded into the existing `bool.must`
      pipeline alongside dynamic facet filters, static post-type clauses,
      and the price range.
    
    URL contract matches the legacy instant-search overlay's static-filter
    shape (scalar, single value per key) so deep links round-trip between
    the two surfaces. Precedent: `priceRange` is a sibling slice with the
    same shape (see `search-blocks/AGENTS.md` "URL format").
    
    Test coverage:
    - url-state.test.js (+8 cases): serialize gated on kind=static; parse
      pulls only configured keys; ignores unrelated scalar params; empty
      values dropped; last-write-wins on duplicate params.
    - store.test.js (+7 cases): setStaticFilter replace/clear; clearFilters
      resets the slice; gateStaticFilterSelections rule set including a
      prototype-pollution defence.
    - api.test.js (+6 cases): buildStaticFilterClauses gating + empty-value
      drop; buildSearchUrl integration alone and combined with dynamic
      filters.
    
    * Address review: orphaned setFilter JSDoc, RESERVED_PARAMS guard, gateStaticFilterSelections return shape (PR #49038)
    
    - Move the orphaned setFilter() JSDoc block back to sit immediately above
      the setFilter() declaration. The insertion of setStaticFilter() pushed
      the block out of place; JSDoc processors attach to the next decl, so
      setFilter() was effectively undocumented.
    - Add RESERVED_PARAMS guard to stateToUrlParams' static-filter
      serializer so a misconfigured filter whose filter_id matches a
      reserved param (s / orderby / min_price / max_price) can't silently
      overwrite the search query / sort / price params. Mirrors
      urlParamsToState which already filtered these.
    - Align gateStaticFilterSelections' return shape with gateActiveFilters:
      now returns { gated, droppedAny } so callers in handlePopState() and
      initialize() can skip the state write when the gate was a no-op. The
      prior length-comparison fallback in initialize() collapses to a single
      boolean check.
    
    Addresses claude[bot]'s review on PR #49038.
    
    <!-- jp-loop -->
    
    * Search: add isStaticFilterSelected callback for reactive radio binding (PR #49038)
    
    Block-side static-filter radios need a reactive checked binding so they
    stay in sync with the store across paths that mutate
    state.staticFilterSelections outside the radio's own change event — the
    clearFilters() reset and the popstate-driven URL rewind. Without this,
    radios visually keep their last clicked state even after the store has
    cleared the slice.
    
    The callback reads the per-<li> context (filterKey + optionValue) that
    the filter-static block's render.php will emit, and compares against
    state.staticFilterSelections[filterKey]. Each radio in render.php then
    binds:
      data-wp-bind--checked="callbacks.isStaticFilterSelected"
    
    Server-rendered `checked` still seeds first paint; this takes over
    after hydration.
    
    Surfaced during the review cycle on PR #49040.
    
    <!-- jp-loop -->
    
    * Address re-review nits: afterEach reset + undefined-slice test (PR #49038)
    
    Two non-blocker nits from claude[bot]'s re-review:
    
    - afterEach in the 'store callbacks' describe block now resets
      state.staticFilterSelections to {} so the assignment at the end of
      the isStaticFilterSelected test doesn't leak into later cases in the
      same block. Currently benign (no downstream reader), but defensive.
    - Add a test pinning the ?. guard on state.staticFilterSelections: when
      the slice is undefined (PHP seed hasn't populated it yet, e.g. on a
      page with no filter-static block), the callback returns false instead
      of crashing.
    
    <!-- jp-loop -->
    
    * Add integration coverage for staticFilterSelections threading (PR #49038)
    
    Pre-existing 'Code coverage requirement' check flagged that the threading
    paths through hasActiveFilters / activeFilterCount / syncToUrl weren't
    exercised end-to-end. Added:
    
    - hasActiveFilters + activeFilterCount include staticFilterSelections —
      counts entries with non-empty values, treats empty-string ('cleared')
      entries as inactive.
    - syncToUrl writes static-filter selections as scalar params alongside
      other state — regression guard against a future change forgetting to
      thread the slice through pushStateToUrl.
    - beforeEach now resets state.staticFilterSelections so subsequent
      tests don't inherit leftover values from earlier ones (the new
      syncToUrl test would otherwise leak its assignment forward).
    
    <!-- jp-loop -->
    @kangzj kangzj force-pushed the search-219-static-filter-helper branch from 0c55d44 to 20da295 Compare May 22, 2026 01:00
    Base automatically changed from search-219-static-filter-helper to trunk May 22, 2026 02:23
    kangzj added 3 commits May 22, 2026 14:25
    The user-facing block, completing the three-part SEARCH-219 series.
    Mirrors the legacy instant-search overlay's static-filter widget as a
    single-select radio list whose options come from server config (no
    per-instance editor UI for values). Selections round-trip as scalar
    `?filter_id=value` URL params via the JS plumbing landed in part 1/3
    and the server-side helper landed in part 2/3.
    
    - `blocks/filter-static/block.json` — three attributes:
      - `variation` (sidebar | tabbed) scopes the block to filters
        registered with the matching variation.
      - `filterId` optionally narrows to a single registered filter; empty
        renders every filter for the variation.
      - `label` overrides the server-supplied filter name.
    - `blocks/filter-static/render.php` — pulls the configured entries via
      `Filter_Static::filters_for_variation()`, merges the URL-seeded
      selection (falls back to the entry's `selected` default), pushes
      `filterConfigs[$id]` + `staticFilterSelections[$id]` into the shared
      Interactivity state, and emits one `<fieldset>` per entry with
      `<input type="radio" name="<filter_id>">` for native single-select
      grouping. `data-wp-on--change="actions.onStaticFilterChange"` wires
      each input to the store action.
    - `blocks/filter-static/edit.js` — editor preview. Inspector exposes
      variation, label override, and a filter picker populated via the
      REST endpoint from part 2/3. Empty-state Placeholder when no static
      filters are registered. Sample radios so the layout previews in
      place.
    - `blocks/filter-static/view.js` — bare `import 'jetpack-search/store'`
      to register the shared store module dependency.
    - `blocks/filter-static/style.scss` — minimal styles that reuse the
      existing filter list/label partials.
    - `editor/register-blocks.js` + `editor/icons.js` — register the
      block's edit component and assign the brand-green `postTerms` icon.
    - Changelog entry (minor, type=added).
    
    Falls under the existing `jetpack_search_blocks_enabled` gate; no new
    flag.
    …on, normalizeVariation test (PR #49040)
    
    Addresses copilot-swe-agent's review:
    
    - Changelog: 'Search: new `...` block' → 'Search: add `...` block'
      (imperative mood per the changelogger convention).
    - Accessibility: <fieldset> now always emits a <legend> so the radio
      group has a programmatic name. When both the block-level label
      override and the server-supplied name are empty, the legend falls
      back to the filter_id — that case is host-site misconfiguration but
      the markup degrades gracefully instead of leaving the fieldset
      unnamed for screen readers.
    - Icon collision: filter-static was using `postTerms`, which already
      belongs to the filter-checkbox 'post_tag' variation. The
      FILTER_CHECKBOX_VARIATION_ICONS docblock guarantees no-duplicates
      across the two icon maps. Switched filter-static to
      `formatListNumbered` (still semantically fitting: a numbered/preset
      radio list of server-defined choices, visually distinct from
      filter-checkbox's `formatListBullets`).
    - Test coverage: new tests/js/search-blocks/filter-static-edit.test.js
      pins `normalizeVariation` — anything other than the literal string
      'tabbed' collapses to 'sidebar', matching
      `Filter_Static::normalize_variation` on the PHP side. A drift
      between these two would scope the editor preview to a different
      filter subset than the front-end render.
    
    Skipped:
    - The `__()` third-arg-0 pattern in edit.js intentionally mirrors the
      same convention in filter-checkbox/edit.js (4 sites). Diverging here
      alone would create drift; leaving as a future repo-wide cleanup if
      the underlying minification issue is ever documented.
    
    <!-- jp-loop -->
    …nt (PR #49040)
    
    Two findings from claude[bot]'s re-review on PR #49040:
    
    - Bug: Radio state drifted from the store after clearFilters() or
      handlePopState() because the radios had no reactive checked binding
      — the change event only fires on user-initiated transitions, so any
      state mutation through other paths left the DOM stale. Fix:
      - Add per-<li> data-wp-context with the option's value so the new
        callbacks.isStaticFilterSelected (landed in the foundation PR
        #49038) can compare it against state.staticFilterSelections.
      - Add data-wp-bind--checked="callbacks.isStaticFilterSelected" to
        each radio input. Server-rendered `checked` still seeds first
        paint; the reactive binding takes over after IA hydration.
    - Doc nit (copilot): FILTER_CHECKBOX_VARIATION_ICONS docblock count
      was '19 + 8'; updated to '20 + 8' to reflect the new filter-static
      entry in BLOCK_ICONS.
    
    Also deferred: the 're-pick a checked radio to deselect' line in the
    prior testing instructions is unreachable (browser change event
    semantics) — removing it from the PR body in a follow-up edit instead
    of carrying a separate code change.
    
    <!-- jp-loop -->
    @kangzj kangzj self-assigned this May 22, 2026
    @kangzj kangzj force-pushed the search-219-static-filter-block branch from 63d0b9c to 6db2dc5 Compare May 22, 2026 02:36
    @kangzj
    Copy link
    Copy Markdown
    Contributor Author

    kangzj commented May 22, 2026

    This needs to be tested on p2s before shipping... 👷

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    [Package] Search Contains core Search functionality for Jetpack and Search plugins [Status] Needs Team Review Obsolete. Use Needs Review instead. [Tests] Includes Tests

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    2 participants