@@ -42,6 +42,7 @@ import {
4242 DropdownMenuTrigger ,
4343} from "@deco/ui/components/dropdown-menu.tsx" ;
4444import { useDecofile } from "@/web/components/sections-editor/use-decofile" ;
45+ import { withVariantMatcherOverride } from "@/web/components/sections-editor/variant-matcher-override" ;
4546import { parseSections } from "@/web/components/sections-editor/parse-sections" ;
4647import { getPageVariantSectionsAt } from "@/web/components/sections-editor/page-variants" ;
4748import { useLiveMeta } from "@/web/components/sections-editor/use-live-meta" ;
@@ -209,6 +210,11 @@ export function PreviewContent() {
209210 const [ cmsSelectedSectionIndex , setCmsSelectedSectionIndex ] = useState <
210211 number | null
211212 > ( null ) ;
213+ // `x-deco-matchers-override` params forcing the preview to render the section
214+ // variant currently selected in the sections editor (CMS mode only).
215+ const [ variantOverrideParams , setVariantOverrideParams ] = useState <
216+ string [ ] | null
217+ > ( null ) ;
212218 const [ panelWidth , setPanelWidth ] = useState ( 384 ) ;
213219 const isResizingRef = useRef ( false ) ;
214220 const resizeStartXRef = useRef ( 0 ) ;
@@ -358,10 +364,13 @@ export function PreviewContent() {
358364
359365 const iframeSrc =
360366 previewState . kind === "iframe"
361- ? withDeviceHint (
362- directPreviewUrl ??
363- new URL ( currentPath , previewState . previewUrl ) . href ,
364- previewDeviceSize ,
367+ ? withVariantMatcherOverride (
368+ withDeviceHint (
369+ directPreviewUrl ??
370+ new URL ( currentPath , previewState . previewUrl ) . href ,
371+ previewDeviceSize ,
372+ ) ,
373+ sectionsOpen && variantOverrideParams ? variantOverrideParams : [ ] ,
365374 )
366375 : null ;
367376
@@ -533,7 +542,10 @@ export function PreviewContent() {
533542 // Leaving code mode clears the "View JSON" deep-link so re-entering code
534543 // mode later opens the file tree, not the previously-viewed page JSON.
535544 if ( mode !== "code" ) setCodeFilePath ( null ) ;
536- if ( mode !== "cms" ) setCmsSelectedSectionIndex ( null ) ;
545+ if ( mode !== "cms" ) {
546+ setCmsSelectedSectionIndex ( null ) ;
547+ setVariantOverrideParams ( null ) ;
548+ }
537549 if ( prev === "visual" ) deactivateVisualEditor ( ) ;
538550 if ( prev === "cms" ) deactivateCmsEditor ( ) ;
539551 if ( mode === "visual" ) injectVisualEditor ( ) ;
@@ -548,6 +560,40 @@ export function PreviewContent() {
548560 iframe . src = iframeSrc ;
549561 } ;
550562
563+ // Selecting/reordering a variant changes `iframeSrc` (override params), which
564+ // re-navigates the iframe. Scroll position is preserved entirely inside the
565+ // injected CMS script (it persists scroll to sessionStorage and restores it
566+ // after the reload, surviving re-hydration), so nothing to capture here.
567+ const handleVariantPreviewOverride = ( params : string [ ] | null ) => {
568+ setVariantOverrideParams ( params ) ;
569+ } ;
570+
571+ // Reload the preview after a save while preventing the iframe from stealing
572+ // focus. Scroll is restored by the injected CMS script (see above).
573+ const reloadPreviewPreservingScroll = ( ) => {
574+ const iframe = previewIframeRef . current ;
575+ if ( ! iframe ) return ;
576+ const focused = document . activeElement as HTMLElement | null ;
577+ const prevTabIndex = iframe . tabIndex ;
578+ iframe . tabIndex = - 1 ;
579+ iframe . style . pointerEvents = "none" ;
580+ iframe . blur ( ) ;
581+ try {
582+ iframe . contentWindow ?. location . reload ( ) ;
583+ } catch {
584+ const src = iframeSrcRef . current ;
585+ if ( src ) iframe . src = src ;
586+ }
587+ const restore = ( ) => {
588+ iframe . tabIndex = prevTabIndex ;
589+ iframe . style . pointerEvents = "" ;
590+ focused ?. focus ( ) ;
591+ iframe . removeEventListener ( "load" , restore ) ;
592+ } ;
593+ iframe . addEventListener ( "load" , restore ) ;
594+ setTimeout ( restore , 3000 ) ;
595+ } ;
596+
551597 const handleHardReload = ( ) => {
552598 if ( ! previewIframeRef . current || ! iframeSrc ) return ;
553599 const sep = iframeSrc . includes ( "?" ) ? "&" : "?" ;
@@ -1084,31 +1130,10 @@ export function PreviewContent() {
10841130 activeGlobalSection ? null : cmsSelectedSectionIndex
10851131 }
10861132 onSaved = { ( ) => {
1087- setTimeout ( ( ) => {
1088- const iframe = previewIframeRef . current ;
1089- if ( ! iframe ) return ;
1090- // Prevent iframe from stealing focus during reload
1091- const focused =
1092- document . activeElement as HTMLElement | null ;
1093- const prevTabIndex = iframe . tabIndex ;
1094- iframe . tabIndex = - 1 ;
1095- iframe . style . pointerEvents = "none" ;
1096- iframe . blur ( ) ;
1097- try {
1098- iframe . contentWindow ?. location . reload ( ) ;
1099- } catch {
1100- const src = iframeSrcRef . current ;
1101- if ( src ) iframe . src = src ;
1102- }
1103- const restore = ( ) => {
1104- iframe . tabIndex = prevTabIndex ;
1105- iframe . style . pointerEvents = "" ;
1106- focused ?. focus ( ) ;
1107- iframe . removeEventListener ( "load" , restore ) ;
1108- } ;
1109- iframe . addEventListener ( "load" , restore ) ;
1110- setTimeout ( restore , 3000 ) ;
1111- } , DEV_SERVER_SETTLE_MS ) ;
1133+ setTimeout (
1134+ reloadPreviewPreservingScroll ,
1135+ DEV_SERVER_SETTLE_MS ,
1136+ ) ;
11121137 } }
11131138 initialEditSeo = { cmsInitialEditSeo }
11141139 onExitSeo = { ( ) => setCmsInitialEditSeo ( false ) }
@@ -1120,6 +1145,7 @@ export function PreviewContent() {
11201145 toast . error ( "Invalid page block key" ) ;
11211146 }
11221147 } }
1148+ onVariantPreviewOverride = { handleVariantPreviewOverride }
11231149 />
11241150 </ Suspense >
11251151 </ div >
0 commit comments