From 7a035069d155ec83f347eb33c69284e3817e9bc5 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Wed, 6 May 2026 20:08:15 +1200 Subject: [PATCH 1/2] Search dashboard: hide the Off row on WordPress.com (RSM-2400) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the legacy ``'s `! isWpcom` gate around its "Enable Jetpack Search" toggle. On WordPress.com, search activation is managed from the .com side, so the new `` shouldn't expose Off as a save target either. We were rendering all four rows regardless of platform, which would let a .com admin pick Off, hit Save, and dispatch `Modules::deactivate( 'search' )` — the exact action the legacy UI deliberately hides. Reads `isWpcom` via the existing store selector (seeded from `Helper::is_wpcom()` in `class-initial-state.php`), filters `EXPERIENCE_ORDER` to drop `EXPERIENCE.OFF` when true, and renders the filtered list. The legacy `` continues to render alongside the new selector while RSM-2291 lands, so admins on .com still see the instant-search-vs-classic toggle that ModuleControl already renders unconditionally. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...sm-2400-feature-selector-hide-off-on-wpcom | 4 ++++ .../components/feature-selector/index.jsx | 12 ++++++++-- .../dashboard/feature-selector/index.test.jsx | 24 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 projects/packages/search/changelog/rsm-2400-feature-selector-hide-off-on-wpcom diff --git a/projects/packages/search/changelog/rsm-2400-feature-selector-hide-off-on-wpcom b/projects/packages/search/changelog/rsm-2400-feature-selector-hide-off-on-wpcom new file mode 100644 index 000000000000..da8a2f0e1fcb --- /dev/null +++ b/projects/packages/search/changelog/rsm-2400-feature-selector-hide-off-on-wpcom @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Search dashboard: hide the Off row of the new feature-selector on WordPress.com, matching the legacy module control's behaviour. diff --git a/projects/packages/search/src/dashboard/components/feature-selector/index.jsx b/projects/packages/search/src/dashboard/components/feature-selector/index.jsx index 3d00991ef967..0f5e0ff03dd0 100644 --- a/projects/packages/search/src/dashboard/components/feature-selector/index.jsx +++ b/projects/packages/search/src/dashboard/components/feature-selector/index.jsx @@ -2,7 +2,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { Button, Stack } from '@wordpress/ui'; import { STORE_ID } from 'store'; -import { EXPERIENCE_ORDER } from './constants'; +import { EXPERIENCE, EXPERIENCE_ORDER } from './constants'; import ExperienceOption from './experience-option'; import './style.scss'; @@ -25,8 +25,16 @@ export default function FeatureSelector() { select => select( STORE_ID ).supportsOnlyClassicSearch(), [] ); + // On WordPress.com, search activation is managed from the .com side, so we + // hide the Off row here to mirror the legacy ``'s + // `! isWpcom` gate around the "Enable Jetpack Search" toggle. + const isWpcom = useSelect( select => select( STORE_ID ).isWpcom(), [] ); const { saveExperience } = useDispatch( STORE_ID ); + const visibleExperiences = isWpcom + ? EXPERIENCE_ORDER.filter( experience => experience !== EXPERIENCE.OFF ) + : EXPERIENCE_ORDER; + const isExperienceDisabled = experience => supportsOnlyClassicSearch && ( experience === 'embedded' || experience === 'overlay' ); @@ -51,7 +59,7 @@ export default function FeatureSelector() { aria-labelledby="jp-search-feature-selector-heading" > - { EXPERIENCE_ORDER.map( experience => ( + { visibleExperiences.map( experience => ( ', () => { expect( screen.getByRole( 'radio', { name: /theme search/i } ) ).toBeEnabled(); expect( screen.getByRole( 'radio', { name: /off/i } ) ).toBeEnabled(); } ); + + test( 'hides the Off row on WordPress.com (parity with legacy ModuleControl)', () => { + const registry = createRegistry(); + const store = createReduxStore( STORE_ID, { + ...storeConfig, + initialState: { + ...( storeConfig.initialState || {} ), + jetpackSettings: baseSettings, + siteData: { isWpcom: true }, + }, + } ); + registry.register( store ); + render( + + + + ); + const radios = screen.getAllByRole( 'radio' ); + expect( radios ).toHaveLength( 3 ); + expect( screen.getByRole( 'radio', { name: /embedded search/i } ) ).toBeInTheDocument(); + expect( screen.getByRole( 'radio', { name: /overlay search/i } ) ).toBeInTheDocument(); + expect( screen.getByRole( 'radio', { name: /theme search/i } ) ).toBeInTheDocument(); + expect( screen.queryByRole( 'radio', { name: /^off$/i } ) ).not.toBeInTheDocument(); + } ); } ); From a5cb5c6e9aacfd28aac62248c20776380de8fb09 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Thu, 7 May 2026 12:57:46 +1200 Subject: [PATCH 2/2] Search feature selector: address PR review feedback - Batch the five useSelect calls into one returning an object, matching the pattern already used in experience-option.jsx. - Clarify the WPcom comment: only Simple sites (where IS_WPCOM is defined) hit this gate, not Atomic. - Drop the anchors on the negative Off-row matcher so the regex actually matches the row's accessible name; the toHaveLength(3) above is the load-bearing check, but the assertion now pulls its weight too. --- .../components/feature-selector/index.jsx | 21 +++++++++++-------- .../dashboard/feature-selector/index.test.jsx | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/projects/packages/search/src/dashboard/components/feature-selector/index.jsx b/projects/packages/search/src/dashboard/components/feature-selector/index.jsx index 0f5e0ff03dd0..fddaef0fbd35 100644 --- a/projects/packages/search/src/dashboard/components/feature-selector/index.jsx +++ b/projects/packages/search/src/dashboard/components/feature-selector/index.jsx @@ -18,17 +18,20 @@ import './style.scss'; * @return {import('react').Element} - The selector. */ export default function FeatureSelector() { - const isDirty = useSelect( select => select( STORE_ID ).isDirty(), [] ); - const isUpdating = useSelect( select => select( STORE_ID ).isUpdatingJetpackSettings(), [] ); - const pendingExperience = useSelect( select => select( STORE_ID ).getPendingExperience(), [] ); - const supportsOnlyClassicSearch = useSelect( - select => select( STORE_ID ).supportsOnlyClassicSearch(), + // On WordPress.com Simple sites (where `IS_WPCOM` is defined), search + // activation is managed from the .com side, so we hide the Off row here to + // mirror the legacy ``'s `! isWpcom` gate around the + // "Enable Jetpack Search" toggle. + const { isDirty, isUpdating, pendingExperience, supportsOnlyClassicSearch, isWpcom } = useSelect( + select => ( { + isDirty: select( STORE_ID ).isDirty(), + isUpdating: select( STORE_ID ).isUpdatingJetpackSettings(), + pendingExperience: select( STORE_ID ).getPendingExperience(), + supportsOnlyClassicSearch: select( STORE_ID ).supportsOnlyClassicSearch(), + isWpcom: select( STORE_ID ).isWpcom(), + } ), [] ); - // On WordPress.com, search activation is managed from the .com side, so we - // hide the Off row here to mirror the legacy ``'s - // `! isWpcom` gate around the "Enable Jetpack Search" toggle. - const isWpcom = useSelect( select => select( STORE_ID ).isWpcom(), [] ); const { saveExperience } = useDispatch( STORE_ID ); const visibleExperiences = isWpcom diff --git a/projects/packages/search/tests/js/dashboard/feature-selector/index.test.jsx b/projects/packages/search/tests/js/dashboard/feature-selector/index.test.jsx index e65211074b67..fda22bf540d5 100644 --- a/projects/packages/search/tests/js/dashboard/feature-selector/index.test.jsx +++ b/projects/packages/search/tests/js/dashboard/feature-selector/index.test.jsx @@ -113,6 +113,6 @@ describe( '', () => { expect( screen.getByRole( 'radio', { name: /embedded search/i } ) ).toBeInTheDocument(); expect( screen.getByRole( 'radio', { name: /overlay search/i } ) ).toBeInTheDocument(); expect( screen.getByRole( 'radio', { name: /theme search/i } ) ).toBeInTheDocument(); - expect( screen.queryByRole( 'radio', { name: /^off$/i } ) ).not.toBeInTheDocument(); + expect( screen.queryByRole( 'radio', { name: /off/i } ) ).not.toBeInTheDocument(); } ); } );