add: partial spotlight for shipping smart defaults tour (#33801)
- added a dynamic floating div to allow tour kit to track just part of the shipping settings table
This commit is contained in:
parent
a3a5168aea
commit
5bb03d0188
|
@ -4,6 +4,13 @@
|
||||||
import { TourKit, TourKitTypes } from '@woocommerce/components';
|
import { TourKit, TourKitTypes } from '@woocommerce/components';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
import {
|
||||||
|
useLayoutEffect,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
createPortal,
|
||||||
|
} from '@wordpress/element';
|
||||||
import { OPTIONS_STORE_NAME, PLUGINS_STORE_NAME } from '@woocommerce/data';
|
import { OPTIONS_STORE_NAME, PLUGINS_STORE_NAME } from '@woocommerce/data';
|
||||||
|
|
||||||
const REVIEWED_DEFAULTS_OPTION =
|
const REVIEWED_DEFAULTS_OPTION =
|
||||||
|
@ -12,6 +19,19 @@ const REVIEWED_DEFAULTS_OPTION =
|
||||||
const CREATED_DEFAULTS_OPTION =
|
const CREATED_DEFAULTS_OPTION =
|
||||||
'woocommerce_admin_created_default_shipping_zones';
|
'woocommerce_admin_created_default_shipping_zones';
|
||||||
|
|
||||||
|
const FLOATER_WRAPPER_CLASS =
|
||||||
|
'woocommerce-settings-shipping-tour-floater-wrapper';
|
||||||
|
|
||||||
|
const FLOATER_CLASS =
|
||||||
|
'woocommerce-settings-smart-defaults-shipping-tour-floater';
|
||||||
|
|
||||||
|
const SHIPPING_ZONES_SETTINGS_TABLE_CLASS = 'table.wc-shipping-zones';
|
||||||
|
|
||||||
|
const WCS_LINK_SELECTOR = 'a[href*="woocommerce-services-settings"]';
|
||||||
|
|
||||||
|
const SHIPPING_RECOMMENDATIONS_SELECTOR =
|
||||||
|
'div.woocommerce-recommended-shipping-extensions';
|
||||||
|
|
||||||
const useShowShippingTour = () => {
|
const useShowShippingTour = () => {
|
||||||
const {
|
const {
|
||||||
hasCreatedDefaultShippingZones,
|
hasCreatedDefaultShippingZones,
|
||||||
|
@ -45,6 +65,138 @@ const useShowShippingTour = () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type NonEmptySelectorArray = readonly [ string, ...string[] ];
|
||||||
|
|
||||||
|
const computeDims = ( elementsSelectors: NonEmptySelectorArray ) => {
|
||||||
|
const rects = elementsSelectors.map( ( elementSelector ) => {
|
||||||
|
const rect = document
|
||||||
|
?.querySelector( elementSelector )
|
||||||
|
?.getBoundingClientRect();
|
||||||
|
|
||||||
|
if ( ! rect ) {
|
||||||
|
throw new Error(
|
||||||
|
"Shipping tour: Couldn't find element with selector: " +
|
||||||
|
elementSelector
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rect;
|
||||||
|
} );
|
||||||
|
|
||||||
|
const originCoords = document
|
||||||
|
.querySelector( `.${ FLOATER_WRAPPER_CLASS }` )
|
||||||
|
?.getBoundingClientRect() || { top: 0, left: 0 };
|
||||||
|
|
||||||
|
const top = Math.min( ...rects.map( ( rect ) => rect.top ) );
|
||||||
|
const left = Math.min( ...rects.map( ( rect ) => rect.left ) );
|
||||||
|
const right = Math.max( ...rects.map( ( rect ) => rect.right ) );
|
||||||
|
const bottom = Math.max( ...rects.map( ( rect ) => rect.bottom ) );
|
||||||
|
const width = right - left;
|
||||||
|
const height = bottom - top;
|
||||||
|
|
||||||
|
// offset top and left from origin
|
||||||
|
const topOffset = top - originCoords.top;
|
||||||
|
const leftOffset = left - originCoords.left;
|
||||||
|
|
||||||
|
return { left: leftOffset, top: topOffset, width, height };
|
||||||
|
};
|
||||||
|
|
||||||
|
const TourFloater = ( { dims }: { dims: Partial< DOMRect > } ) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={ {
|
||||||
|
position: 'relative',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
...dims,
|
||||||
|
} }
|
||||||
|
className={ FLOATER_CLASS }
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// this is defines the elements to be spotlit for each step
|
||||||
|
const spotlitElementsSelectors: Array< NonEmptySelectorArray > = [
|
||||||
|
[
|
||||||
|
// just use bottom right element and top left element instead of all rects
|
||||||
|
// top left = table header cell for sort handles
|
||||||
|
'th.wc-shipping-zone-sort',
|
||||||
|
// bottom right = worldwide region cell
|
||||||
|
'tr.wc-shipping-zone-worldwide > td.wc-shipping-zone-region',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// selectors for rightmost column (shipping methods)
|
||||||
|
'th.wc-shipping-zone-methods',
|
||||||
|
'tr.wc-shipping-zone-worldwide > td.wc-shipping-zone-methods',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const TourFloaterWrapper = ( { step }: { step: number } ) => {
|
||||||
|
const thisRef = useRef< HTMLDivElement >( null );
|
||||||
|
useLayoutEffect( () => {
|
||||||
|
// this moves the element to the correct place which is right before the table element
|
||||||
|
if ( thisRef.current?.parentElement ) {
|
||||||
|
thisRef.current.parentElement.insertBefore(
|
||||||
|
thisRef.current,
|
||||||
|
document.querySelector( 'table.wc-shipping-zones' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [] );
|
||||||
|
|
||||||
|
const currentStepSelectors =
|
||||||
|
spotlitElementsSelectors[ step ] ??
|
||||||
|
spotlitElementsSelectors[ spotlitElementsSelectors.length - 1 ];
|
||||||
|
|
||||||
|
const [ dims, setDims ] = useState( computeDims( currentStepSelectors ) );
|
||||||
|
useEffect( () => {
|
||||||
|
setDims( computeDims( currentStepSelectors ) );
|
||||||
|
const observer = new ResizeObserver( () => {
|
||||||
|
setDims( computeDims( currentStepSelectors ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
const shippingSettingsTableElement = document.querySelector(
|
||||||
|
SHIPPING_ZONES_SETTINGS_TABLE_CLASS
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! shippingSettingsTableElement ) {
|
||||||
|
throw new Error(
|
||||||
|
"Shipping tour: Couldn't find shipping settings table element with selector: " +
|
||||||
|
SHIPPING_ZONES_SETTINGS_TABLE_CLASS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.observe( shippingSettingsTableElement );
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [ currentStepSelectors ] );
|
||||||
|
|
||||||
|
const shippingSettingsTableParentElement = document.querySelector(
|
||||||
|
SHIPPING_ZONES_SETTINGS_TABLE_CLASS
|
||||||
|
)?.parentElement;
|
||||||
|
|
||||||
|
if ( ! shippingSettingsTableParentElement ) {
|
||||||
|
throw new Error(
|
||||||
|
"Shipping tour: Couldn't find shipping settings table parent element with selector: " +
|
||||||
|
SHIPPING_ZONES_SETTINGS_TABLE_CLASS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* use ReactDOM.createPortal to inject our element into non-React controlled DOM
|
||||||
|
* unfortunately createPortal uses .appendChild which puts it in the wrong place,
|
||||||
|
* we want it to be before the table
|
||||||
|
*/
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
ref={ thisRef }
|
||||||
|
className={ FLOATER_WRAPPER_CLASS }
|
||||||
|
style={ { position: 'absolute' } }
|
||||||
|
>
|
||||||
|
{ <TourFloater dims={ dims } /> }
|
||||||
|
</div>,
|
||||||
|
shippingSettingsTableParentElement
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ShippingTour = () => {
|
export const ShippingTour = () => {
|
||||||
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
||||||
const { show: showTour } = useShowShippingTour();
|
const { show: showTour } = useShowShippingTour();
|
||||||
|
@ -52,12 +204,33 @@ export const ShippingTour = () => {
|
||||||
select( PLUGINS_STORE_NAME ).getActivePlugins()
|
select( PLUGINS_STORE_NAME ).getActivePlugins()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [ step, setStepNumber ] = useState( 0 );
|
||||||
|
|
||||||
const tourConfig: TourKitTypes.WooConfig = {
|
const tourConfig: TourKitTypes.WooConfig = {
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
|
options: {
|
||||||
|
effects: {
|
||||||
|
spotlight: {
|
||||||
|
interactivity: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
liveResize: {
|
||||||
|
mutation: true,
|
||||||
|
resize: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
onNextStep: ( currentStepIndex ) =>
|
||||||
|
setStepNumber( currentStepIndex + 1 ),
|
||||||
|
onPreviousStep: ( currentStepIndex ) =>
|
||||||
|
setStepNumber( currentStepIndex - 1 ),
|
||||||
|
},
|
||||||
|
},
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
referenceElements: {
|
referenceElements: {
|
||||||
desktop: 'table.wc-shipping-zones',
|
desktop: `.${ FLOATER_CLASS }`,
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
name: 'shipping-zones',
|
name: 'shipping-zones',
|
||||||
|
@ -85,7 +258,7 @@ export const ShippingTour = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
referenceElements: {
|
referenceElements: {
|
||||||
desktop: 'table.wc-shipping-zones',
|
desktop: `.${ FLOATER_CLASS }`,
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
name: 'shipping-methods',
|
name: 'shipping-methods',
|
||||||
|
@ -106,11 +279,8 @@ export const ShippingTour = () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const WCS_LINK_SELECTOR = 'a[href*="woocommerce-services-settings"]';
|
|
||||||
const isWcsSectionPresent = document.querySelector( WCS_LINK_SELECTOR );
|
const isWcsSectionPresent = document.querySelector( WCS_LINK_SELECTOR );
|
||||||
|
|
||||||
const SHIPPING_RECOMMENDATIONS_SELECTOR =
|
|
||||||
'div.woocommerce-recommended-shipping-extensions';
|
|
||||||
const isShippingRecommendationsPresent = ! activePlugins.includes(
|
const isShippingRecommendationsPresent = ! activePlugins.includes(
|
||||||
'woocommerce-services'
|
'woocommerce-services'
|
||||||
);
|
);
|
||||||
|
@ -154,6 +324,7 @@ export const ShippingTour = () => {
|
||||||
if ( showTour ) {
|
if ( showTour ) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<TourFloaterWrapper step={ step } />
|
||||||
<TourKit config={ tourConfig }></TourKit>
|
<TourKit config={ tourConfig }></TourKit>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
Comment: Added partial spotlight floater to shipping smart defaults tour
|
||||||
|
|
||||||
|
|
|
@ -181,6 +181,8 @@ class Options extends \WC_REST_Data_Controller {
|
||||||
'woocommerce_show_marketplace_suggestions',
|
'woocommerce_show_marketplace_suggestions',
|
||||||
'woocommerce_task_list_reminder_bar_hidden',
|
'woocommerce_task_list_reminder_bar_hidden',
|
||||||
'wc_connect_options',
|
'wc_connect_options',
|
||||||
|
'woocommerce_admin_created_default_shipping_zones',
|
||||||
|
'woocommerce_admin_reviewed_default_shipping_zones',
|
||||||
);
|
);
|
||||||
|
|
||||||
$theme_permissions = array(
|
$theme_permissions = array(
|
||||||
|
|
1622
pnpm-lock.yaml
1622
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue