Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/base-control2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const BaseControl = props => {
{ props.after }
</div>
</div>
<div className="stk-control-content">
<div className="stk-control-content" data-attribute={ props.attribute }>
{ props.children }
</div>
</VisualGuide>
Expand Down
12 changes: 12 additions & 0 deletions src/components/guided-modal-tour/tour-conditions.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,16 @@ export const TOUR_CONDITIONS = {
// Force show the tour if there is a GET parameter tour=site-kit
return window?.location?.search?.includes( 'tour=site-kit' ) ? true : null
},
'block-backgrounds': () => {
// Force show the tour if there is a GET parameter tour=block-backgrounds
return window?.location?.search?.includes( 'tour=block-backgrounds' ) ? true : null
},
'responsive-controls': () => {
// Force show the tour if there is a GET parameter tour=responsive-controls
return window?.location?.search?.includes( 'tour=responsive-controls' ) ? true : null
},
'hover-states': () => {
// Force show the tour if there is a GET parameter tour=hover-states
return window?.location?.search?.includes( 'tour=hover-states' ) ? true : null
},
}
211 changes: 211 additions & 0 deletions src/lazy-components/modal-tour/tours/block-backgrounds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { __ } from '@wordpress/i18n'
import { i18n } from 'stackable'
import { dispatch, select } from '@wordpress/data'
import { createInterpolateElement } from '@wordpress/element'

export const blockBackgrounds = {
initialize: () => {
// Add some default content that we will select

const blockObject = wp.blocks.createBlock(
'stackable/columns',
{
uniqueId: '1dbe04e',
blockMargin: { bottom: '' },
align: 'full',
containerWidth: 50,
containerHorizontalAlign: 'flex-start',
containerWidthUnit: '%',
},
[
wp.blocks.createBlock(
'stackable/column',
{
uniqueId: 'f957abc',
columnSpacing: {
top: '', right: '', bottom: '', left: '',
},
},
[
wp.blocks.createBlock(
'stackable/heading',
{
uniqueId: 'a8ebea7',
// Retain our text
text: 'Explore the World with Us',
textTag: 'h2',
}
),
wp.blocks.createBlock(
'stackable/text',
{
uniqueId: '57e76a1',
// Retain our text
text: 'Discover breathtaking destinations, plan your next adventure, and make unforgettable memories with our travel guides and tips.',
}
),
wp.blocks.createBlock(
'stackable/button-group',
{ uniqueId: 'e063798' },
[
wp.blocks.createBlock(
'stackable/button',
{
uniqueId: '5d04ca8',
// Retain our text
text: 'Start your journey',
url: '',
}
),
]
),
]
),
]
)
Comment on lines +10 to +65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential block ID conflicts with hardcoded uniqueIds.

The hardcoded uniqueId values (e.g., '1dbe04e', 'f957abc') could conflict if blocks with these IDs already exist on the page, potentially causing unexpected behavior or errors.

Consider generating unique IDs dynamically:

 const blockObject = wp.blocks.createBlock(
 	'stackable/columns',
 	{
-		uniqueId: '1dbe04e',
+		uniqueId: `tour-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,

Or use a utility function if one exists in the codebase for generating unique block IDs.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/lazy-components/modal-tour/tours/block-backgrounds.js around lines 10 to
65, the blocks are created with hardcoded uniqueId values which can collide with
existing blocks; update the code to stop using fixed IDs by either removing the
uniqueId property (let Gutenberg generate IDs) or replacing each hardcoded
string with a call to a shared ID generator (e.g., const id =
generateUniqueId(); then use uniqueId: id) — ensure you import or use the
project utility if one exists, or add a small helper that produces RFC4122-like
or sufficiently random IDs and call it for every block creation.

// Insert our block
dispatch( 'core/block-editor' ).insertBlocks( [ blockObject ], 0 )

// Select the inner columns block for the tour
dispatch( 'core/block-editor' ).selectBlock( blockObject.clientId )
},
steps: [
{
title: __( 'Discover Stackable Block Backgrounds & Containers', i18n ),
description: __( 'Welcome! Let\'s enhance your page by first adding a background to the Columns block. The Style Tab lets you tweak backgrounds, colors, borders, and typography for a custom look.', i18n ),
help: createInterpolateElement( __( 'Click the <strong>Style Tab</strong> in the sidebar to get started.', i18n ), {
strong: <strong />,
} ),
anchor: '.edit-post-sidebar__panel-tab.ugb-tab--style',
position: 'left',
glowTarget: '.edit-post-sidebar__panel-tab.ugb-tab--style',
nextEventTarget: '.edit-post-sidebar__panel-tab.ugb-tab--style',
preStep: () => {
// Open the inspector sidebar
dispatch( 'core/edit-post' ).openGeneralSidebar( 'edit-post/block' )

// Look for the first "stackable/columns" block
const allBlocks = select( 'core/block-editor' ).getBlocks()
const columnsBlock = allBlocks.find( block => block.name === 'stackable/columns' )
if ( columnsBlock ) {
dispatch( 'core/block-editor' ).selectBlock( columnsBlock.clientId )
}
},
postStep: () => {
// Click the tab
document.querySelector( '.edit-post-sidebar__panel-tab.ugb-tab--style:not(.is-active)' )?.click()
},
},
{
title: __( 'Enable a Block Background', i18n ),
description: __( 'Turn on the background option to instantly add a background layer to your section. Watch as your design transforms with a single toggle.', i18n ),
help: createInterpolateElement( __( 'Toggle ON the <strong>Background</strong> option to continue.', i18n ), {
strong: <strong />,
} ),
anchor: '.ugb-block-background-panel .components-panel__body-title',
position: 'left',
glowTarget: '.ugb-block-background-panel .components-panel__body-title',
nextEventTarget: '.ugb-block-background-panel .components-panel__body-title input[type="checkbox"]',
nextEvent: 'mousedown',
preStep: () => {
// Open the inspector sidebar
dispatch( 'core/edit-post' ).openGeneralSidebar( 'edit-post/block' )

// Look for the first "stackable/columns" block
const allBlocks = select( 'core/block-editor' ).getBlocks()
const columnsBlock = allBlocks.find( block => block.name === 'stackable/columns' )
if ( columnsBlock ) {
dispatch( 'core/block-editor' ).selectBlock( columnsBlock.clientId )
}

setTimeout( () => {
// Click the tab
document.querySelector( '.edit-post-sidebar__panel-tab.ugb-tab--style:not(.is-active)' )?.click()
}, 100 )
Comment on lines +121 to +124
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Race conditions from hardcoded setTimeout delays.

The 100ms delays are arbitrary and may fail on slower systems, during high CPU load, or in different browser environments, causing the tour to break or behave unpredictably.

Consider using requestAnimationFrame or polling with a timeout for more reliable synchronization:

const waitForElement = (selector, timeout = 2000) => {
	return new Promise((resolve, reject) => {
		const startTime = Date.now()
		const check = () => {
			const element = document.querySelector(selector)
			if (element) {
				resolve(element)
			} else if (Date.now() - startTime > timeout) {
				reject(new Error(`Element ${selector} not found`))
			} else {
				requestAnimationFrame(check)
			}
		}
		check()
	})
}

// Usage in preStep:
await waitForElement('.edit-post-sidebar__panel-tab.ugb-tab--style')
document.querySelector('.edit-post-sidebar__panel-tab.ugb-tab--style:not(.is-active)')?.click()

Also applies to: 184-186


I'll verify the review comment by examining the codebase and understanding the context around those hardcoded delays.


🏁 Script executed:

#!/bin/bash
# Check if file exists and view the code around the mentioned lines
if [ -f "src/lazy-components/modal-tour/tours/block-backgrounds.js" ]; then
  echo "=== File found. Showing lines 115-130 ==="
  sed -n '115,130p' "src/lazy-components/modal-tour/tours/block-backgrounds.js"
  echo ""
  echo "=== Showing lines 180-190 ==="
  sed -n '180,190p' "src/lazy-components/modal-tour/tours/block-backgrounds.js"
else
  echo "File not found at expected path"
  # Try to find it
  find . -name "block-backgrounds.js" -type f 2>/dev/null
fi

Length of output: 1368


🏁 Script executed:

#!/bin/bash
# Search for all setTimeout calls in the file to understand usage patterns
echo "=== All setTimeout calls in block-backgrounds.js ==="
rg -n "setTimeout" "src/lazy-components/modal-tour/tours/block-backgrounds.js" -B2 -A4

Length of output: 641


🏁 Script executed:

#!/bin/bash
# Check if there are utility functions for DOM element waiting in the codebase
echo "=== Searching for existing waitFor, waitForElement patterns ==="
rg -n "waitFor|requestAnimationFrame" --type=js -g "!node_modules" | head -30

Length of output: 299062


🏁 Script executed:

#!/bin/bash
# Understand the structure and purpose of this file
echo "=== File context: imports, function definitions ==="
head -50 "src/lazy-components/modal-tour/tours/block-backgrounds.js"

Length of output: 1372


Address race condition risk from hardcoded setTimeout delays in tour component.

Lines 121-124 and 184-186 use arbitrary 100ms delays before querying and clicking DOM elements. On slower systems or under CPU load, the target elements may not exist in the DOM yet, causing the selector to return null and the tour to fail silently. Replace with polling or requestAnimationFrame to ensure elements are present before interaction.

const waitForElement = (selector, timeout = 2000) => {
	return new Promise((resolve, reject) => {
		const startTime = Date.now()
		const check = () => {
			const element = document.querySelector(selector)
			if (element) {
				resolve(element)
			} else if (Date.now() - startTime > timeout) {
				reject(new Error(`Element ${selector} not found`))
			} else {
				requestAnimationFrame(check)
			}
		}
		check()
	})
}

// Usage in preStep:
await waitForElement('.edit-post-sidebar__panel-tab.ugb-tab--style:not(.is-active)')
document.querySelector('.edit-post-sidebar__panel-tab.ugb-tab--style:not(.is-active)')?.click()

Also applies to: 184-186

🤖 Prompt for AI Agents
In src/lazy-components/modal-tour/tours/block-backgrounds.js around lines
121-124 (and similarly at 184-186), the hardcoded setTimeout(100) introduces a
race on DOM availability; replace these timeouts with a small utility that waits
for the element (polling via requestAnimationFrame or a MutationObserver) with a
reasonable timeout, await that promise before calling .click(), and handle
rejection/logging if the element never appears so the tour fails predictably
instead of silently; add the waitForElement helper once and reuse it for both
places.

},
},
{
title: __( 'Customize the Background Color', i18n ),
description: __( 'Now let\'s personalize your section. Choose a background color to help your content stand out or integrate it seamlessly into your page design.', i18n ),
help: createInterpolateElement( __( 'Open the <strong>Background Color</strong> panel to select a color.', i18n ), {
strong: <strong />,
} ),
anchor: '.stk-color-palette-control',
position: 'left',
glowTarget: '.ugb-block-background-panel .block-editor-panel-color-gradient-settings__dropdown',
nextEventTarget: '.ugb-block-background-panel .block-editor-panel-color-gradient-settings__dropdown',
nextEvent: 'mousedown',
preStep: () => {
// Toggle background on
document.querySelector( '.ugb-block-background-panel:not(is-opened)' )?.click()
document.querySelector( '.ugb-block-background-panel .ugb-toggle-panel-form-toggle:not(is-checked) input' )?.click()
},
postStep: () => {
// Click the tab
document.querySelector( '.ugb-block-background-panel .stk-color-palette-control .block-editor-panel-color-gradient-settings__dropdown:not(is-open)' )?.click()
},
},
{
title: __( 'Pick Your Brand Color', i18n ),
description: __( 'Select a color that matches your brand or mood. Try out different colors and instantly see the changes on your block.', i18n ),
help: createInterpolateElement( __( 'Choose a <strong>Color</strong> from the palette to apply it.', i18n ), {
strong: <strong />,
} ),
anchor: '.stk-color-palette-control__popover-content',
position: 'left',
glowTarget: '.stk-color-palette-control__popover-content',
nextEvent: 'mousedown',
nextEventTarget: '.stk-color-palette-control__popover-content *',
postStep: () => {
// Click the color picker
document.querySelector( '.ugb-block-background-panel .stk-color-palette-control .block-editor-panel-color-gradient-settings__dropdown.is-open' )?.click()
},
},
{
title: __( 'Add a Container for Better Layout', i18n ),
description: __( 'Turn on the Container option to give your columns content a background, padding, and improved alignment for a polished look.', i18n ),
help: createInterpolateElement( __( 'Toggle ON the <strong>Container</strong> to proceed.', i18n ), {
strong: <strong />,
} ),
anchor: '.ugb-panel--container-size .components-panel__body-title',
position: 'left',
glowTarget: '.ugb-panel--container-size .components-panel__body-title',
nextEventTarget: '.ugb-panel--container-size .components-panel__body-title input[type="checkbox"]',
nextEvent: 'mousedown',
preStep: () => {
// Click the tab
document.querySelector( '.ugb-block-background-panel .stk-color-palette-control .block-editor-panel-color-gradient-settings__dropdown.is-open' )?.click()

const allBlocks = select( 'core/block-editor' ).getBlocks()
const columnsBlock = allBlocks.find( block => block.name === 'stackable/columns' )
if ( columnsBlock && columnsBlock.innerBlocks[ 0 ] ) {
dispatch( 'core/block-editor' ).selectBlock( columnsBlock.innerBlocks[ 0 ].clientId )
document.querySelector( '.edit-post-sidebar__panel-tab.ugb-tab--layout:not(.is-active)' )?.click()
setTimeout( () => {
document.querySelector( '.ugb-panel--layout.is-opened .components-panel__body-title button' )?.click()
}, 100 )
}
},
},
{
title: __( 'Block Backgrounds & Containers Recap', i18n ),
description: __( 'Great job! You\'ve learned how to add and customize block backgrounds and containers. These features are available in most Stackable blocks, helping you create beautiful, consistent layouts with ease.', i18n ),
preStep: () => {
// Toggle background on
document.querySelector( '.ugb-panel--container-size:not(is-opened)' )?.click()
document.querySelector( '.ugb-panel--container-size .ugb-toggle-panel-form-toggle:not(is-checked) input' )?.click()
},
},
{
title: __( 'Tip: Style All Blocks Efficiently', i18n ),
description: createInterpolateElement( __( 'Use the <strong>Stackable Design System</strong> to manage styles for all Stackable blocks globally. Perfect for a unified look and quick design changes!', i18n ), {
strong: <strong />,
} ),
anchor: '[aria-controls="stackable-global-settings:sidebar"]',
position: 'left-top',
offsetY: '-30px',
offsetX: '-8px',
glowTarget: '[aria-controls="stackable-global-settings:sidebar"]',
},
],
}
134 changes: 134 additions & 0 deletions src/lazy-components/modal-tour/tours/hover-states.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { __ } from '@wordpress/i18n'
import { i18n } from 'stackable'
import { dispatch } from '@wordpress/data'
import { createInterpolateElement } from '@wordpress/element'

export const hoverStates = {
initialize: () => {
// Add some default content that we will select

const blockObject = wp.blocks.createBlock(
'stackable/image',
{
uniqueId: 'e063798',
imageExternalUrl: 'https://picsum.photos/id/177/500/700.jpg',
}
)
Comment on lines +10 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not reuse hard-coded uniqueId values.

'e063798' is already used by the responsive-controls tour, so running both tours in one editor session produces duplicate block uniqueIds. Gutenberg treats those IDs as primary keys—duplicates trigger invalid-block warnings and can break attribute syncing. Please generate a fresh ID each time you insert the block instead of reusing a constant.

Here is one way to do it:

 export const hoverStates = {
 	initialize: () => {
-		// Add some default content that we will select
-
-		const blockObject = wp.blocks.createBlock(
+		const makeUniqueId = ( prefix = 'tour-' ) => `${ prefix }${ Math.random().toString(36).slice(2, 11) }`
+		const blockObject = wp.blocks.createBlock(
 			'stackable/image',
 			{
-				uniqueId: 'e063798',
+				uniqueId: makeUniqueId( 'hover-image-' ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const blockObject = wp.blocks.createBlock(
'stackable/image',
{
uniqueId: 'e063798',
imageExternalUrl: 'https://picsum.photos/id/177/500/700.jpg',
}
)
const makeUniqueId = ( prefix = 'tour-' ) => `${ prefix }${ Math.random().toString(36).slice(2, 11) }`
const blockObject = wp.blocks.createBlock(
'stackable/image',
{
uniqueId: makeUniqueId( 'hover-image-' ),
imageExternalUrl: 'https://picsum.photos/id/177/500/700.jpg',
}
)
🤖 Prompt for AI Agents
In src/lazy-components/modal-tour/tours/hover-states.js around lines 10-16, the
block is created with a hard-coded uniqueId ('e063798') which collides with
other tours; replace the constant with a freshly generated ID each time you
insert the block. Generate a new uniqueId at runtime (for example using a
UUID/timestamp/random generator or the appropriate WP utility) and pass that
generated value into the block attributes instead of the hard-coded string so
each created block has a distinct uniqueId.


// Insert our block
dispatch( 'core/block-editor' ).insertBlocks( [ blockObject ], 0 )

// Select the inner columns block for the tour
dispatch( 'core/block-editor' ).selectBlock( blockObject.clientId )

setTimeout( () => {
document.querySelector( '.edit-post-sidebar__panel-tab.ugb-tab--style:not(.is-active)' )?.click()
setTimeout( () => {
document.querySelector( '.ugb-panel--image:not(.is-opened)' )?.click()
const target = document.querySelector( '.stk-control:has([data-attribute="imageZoom"])' )
target?.scrollIntoView( { behavior: 'auto', block: 'center' } )
}, 100 )
}, 200 )
Comment on lines +24 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace brittle timeouts with deterministic waits.

The nested setTimeout(200)setTimeout(100) chain is inherently racey: on slower hosts the elements may not exist yet (tour stalls), while on faster hosts the extra delays make the tour feel unresponsive. Please switch to an explicit wait-for-selector helper (e.g., polling with requestAnimationFrame or a small promise that resolves once document.querySelector finds the panel) so the clicks fire exactly when the UI is ready rather than after arbitrary sleeps.

A safer pattern could look like:

-		setTimeout( () => {
-			document.querySelector( '.edit-post-sidebar__panel-tab.ugb-tab--style:not(.is-active)' )?.click()
-			setTimeout( () => {
-				document.querySelector( '.ugb-panel--image:not(.is-opened)' )?.click()
-				const target = document.querySelector( '.stk-control:has([data-attribute="imageZoom"])' )
-				target?.scrollIntoView( { behavior: 'auto', block: 'center' } )
-			}, 100 )
-		}, 200 )
+		const waitForElement = selector => new Promise( resolve => {
+			const tick = () => {
+				const node = document.querySelector( selector )
+				return node ? resolve( node ) : requestAnimationFrame( tick )
+			}
+			tick()
+		} )
+
+		waitForElement( '.edit-post-sidebar__panel-tab.ugb-tab--style' )
+			.then( tab => {
+				if ( ! tab.classList.contains( 'is-active' ) ) {
+					tab.click()
+				}
+				return waitForElement( '.ugb-panel--image' )
+			} )
+			.then( panel => {
+				if ( ! panel.classList.contains( 'is-opened' ) ) {
+					panel.click()
+				}
+				document
+					.querySelector( '.stk-control:has([data-attribute="imageZoom"])' )
+					?.scrollIntoView( { behavior: 'auto', block: 'center' } )
+			} )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setTimeout( () => {
document.querySelector( '.edit-post-sidebar__panel-tab.ugb-tab--style:not(.is-active)' )?.click()
setTimeout( () => {
document.querySelector( '.ugb-panel--image:not(.is-opened)' )?.click()
const target = document.querySelector( '.stk-control:has([data-attribute="imageZoom"])' )
target?.scrollIntoView( { behavior: 'auto', block: 'center' } )
}, 100 )
}, 200 )
const waitForElement = selector => new Promise( resolve => {
const tick = () => {
const node = document.querySelector( selector )
return node ? resolve( node ) : requestAnimationFrame( tick )
}
tick()
} )
waitForElement( '.edit-post-sidebar__panel-tab.ugb-tab--style' )
.then( tab => {
if ( ! tab.classList.contains( 'is-active' ) ) {
tab.click()
}
return waitForElement( '.ugb-panel--image' )
} )
.then( panel => {
if ( ! panel.classList.contains( 'is-opened' ) ) {
panel.click()
}
document
.querySelector( '.stk-control:has([data-attribute="imageZoom"])' )
?.scrollIntoView( { behavior: 'auto', block: 'center' } )
} )
🤖 Prompt for AI Agents
In src/lazy-components/modal-tour/tours/hover-states.js around lines 24 to 31,
replace the nested setTimeout calls with deterministic waits: implement a small
waitForSelector helper that polls (via requestAnimationFrame or setInterval)
until document.querySelector(selector) returns an element or a timeout elapses,
then use it to await the first selector
'.edit-post-sidebar__panel-tab.ugb-tab--style:not(.is-active)' and click it,
then await the second selector '.ugb-panel--image:not(.is-opened)' and click it,
then query '.stk-control:has([data-attribute="imageZoom"])' and scrollIntoView;
remove the arbitrary 200/100ms sleeps so the actions run only once the UI
elements are present.

},
steps: [
{
title: __( 'Using the Hover Effect Controls', i18n ),
description: createInterpolateElement(
__( 'Locate the <strong>hover toggle</strong> next to <strong> Zoom</strong>. Click it to show the different hover state options.', i18n ),
{ strong: <strong /> }
),
anchor: '.stk-control:has([data-attribute="imageZoom"])',
position: 'left',
glowTarget: '.stk-control:has([data-attribute="imageZoom"]) .stk-control-label button[data-value="normal"]',
nextEventTarget: '.stk-control:has([data-attribute="imageZoom"]) .stk-control-label button',
},
{
title: __( 'Different Hover States', i18n ),
description: createInterpolateElement(
__( 'Now select the <strong>hover button</strong>. This lets you adjust how the image will look when hovered.', i18n ),
{ strong: <strong /> }
),
anchor: '.stk-control:has([data-attribute="imageZoom"]) .stk-label-unit-toggle__wrapper',
position: 'left',
glowTarget: '.stk-control:has([data-attribute="imageZoom"]) .stk-label-unit-toggle__wrapper',
nextEventTarget: '.stk-control:has([data-attribute="imageZoom"]) .stk-label-unit-toggle__wrapper button:not(.is-active)[data-value="hover"]',
nextEvent: 'mousedown',
preStep: () => {
document.querySelector( '.stk-control:has([data-attribute="imageZoom"]) .stk-label-unit-toggle__wrapper:not(.is-open) button' )?.click()
},
postStep: () => {
document.querySelector( '.stk-control:has([data-attribute="imageZoom"]) .stk-label-unit-toggle__wrapper button:not(.is-active)[data-value="hover"]' )?.click()
},
},
{
title: __( 'Adjusting Zoom on Hover', i18n ),
description: createInterpolateElement(
__( 'The controls now affect the <strong>Hover</strong> state only. Try adjusting the <strong>Image Zoom</strong> value and see how the preview changes when hovered.', i18n ),
{ strong: <strong /> }
),
anchor: '.stk-control:has([data-attribute="imageZoom"])',
position: 'left',
glowTarget: '.stk-control:has([data-attribute="imageZoom"]) .stk-control-content',
preStep: () => {
document.querySelector( '.stk-control:has([data-attribute="imageZoom"]) .stk-label-unit-toggle__wrapper button:not(.is-active)[data-value="hover"]' )?.click()
},
postStep: () => {
// Update the order of the columns for mobile by dispatching an attribute update.
const allBlocks = wp.data.select( 'core/block-editor' ).getBlocks()
const imageBlock = allBlocks.find( block => block.name === 'stackable/image' )
if ( imageBlock && ! imageBlock.attributes?.imageZoomHover ) {
wp.data.dispatch( 'core/block-editor' ).updateBlockAttributes( imageBlock.clientId, {
imageZoomHover: 1.5,
} )
}
},
},
{
title: __( 'Preview Your Hover Effect', i18n ),
description: createInterpolateElement(
__( 'The image on the canvas is now showing your <strong>new hover zoom effect</strong>. No need to hover—this view already reflects the hover state.', i18n ),
{ strong: <strong /> }
),
offsetX: '300px',
},
{
title: __( 'Return to Normal State', i18n ),
description: createInterpolateElement(
__( 'Switch back to the normal state by clicking the <strong>cursor icon</strong> to test the hover effect in action.', i18n ),
{ strong: <strong /> }
),
anchor: '.stk-control:has([data-attribute="imageZoom"]) .stk-label-unit-toggle__wrapper button[data-value="normal"]',
position: 'left',
glowTarget: '.stk-control:has([data-attribute="imageZoom"]) .stk-label-unit-toggle__wrapper button[data-value="normal"]',
nextEventTarget: '.stk-control:has([data-attribute="imageZoom"]) .stk-label-unit-toggle__wrapper button[data-value="normal"]',
nextEvent: 'mousedown',
preStep: () => {
document.querySelector( '.stk-control:has([data-attribute="imageZoom"]) .stk-label-unit-toggle__wrapper:not(.is-open) button' )?.click()
},
postStep: () => {
document.querySelector( '.stk-control:has([data-attribute="imageZoom"]) .stk-label-unit-toggle__wrapper:not(.is-open) button' )?.click()
setTimeout( () => {
document.querySelector( '.stk-control:has([data-attribute="imageZoom"]) .stk-label-unit-toggle__wrapper button:not(.is-active)[data-value="normal"]' )?.click()
}, 100 )
},
},
{
title: __( 'Back on Normal State', i18n ),
description: createInterpolateElement(
__( 'You are now back on the <strong>normal</strong> state. Notice the hover toggle is <strong>yellow</strong> - that means a custom hover effect has been saved!', i18n ),
{ strong: <strong /> }
),
anchor: '.stk-control:has([data-attribute="imageZoom"]) .stk-control-label button',
position: 'left',
glowTarget: '.stk-control:has([data-attribute="imageZoom"]) .stk-control-label button',
},
{
title: __( 'Pro Tip: Only Some Controls Feature Hover Toggle', i18n ),
description: createInterpolateElement(
__( 'Tip: Watch out—for some controls you <strong>won\'t see the hover state toggle</strong>. Only certain options support hover states. If you don\'t see the toggle, that control doesn\'t have hover customization!', i18n ),
{ strong: <strong /> }
),
offsetX: '300px',
},
],
}
Loading
Loading