Skip to content

Commit

Permalink
Merge pull request #7538 from Automattic/add/interaction-on-lesson-step
Browse files Browse the repository at this point in the history
Add interactivity to lesson steps
  • Loading branch information
renatho authored Mar 19, 2024
2 parents 414a627 + da17671 commit 6190ee1
Show file tree
Hide file tree
Showing 10 changed files with 1,266 additions and 199 deletions.
24 changes: 18 additions & 6 deletions assets/admin/tour/components/sensei-tour-kit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,16 @@ import { WpcomTourKit } from '@automattic/tour-kit';
* @param {string} props.tourName The unique name of the tour.
* @param {string} props.trackId ID of tracking event (optional). Tracking will be enabled only when provided.
* @param {TourStep[]} props.steps An array of steps to include in the tour.
* @param {Function} [props.beforeEach] A function to run before each step.
* @param {Object} [props.extraConfig={}] Additional configuration options for the tour kit.
*/
function SenseiTourKit( { tourName, trackId, steps, extraConfig = {} } ) {
function SenseiTourKit( {
tourName,
trackId,
steps,
beforeEach = () => {},
extraConfig = {},
} ) {
const { showTour } = useSelect( ( select ) => {
const { shouldShowTour } = select( SENSEI_TOUR_STORE );
return {
Expand All @@ -52,6 +59,11 @@ function SenseiTourKit( { tourName, trackId, steps, extraConfig = {} } ) {
[ trackId, steps ]
);

const runAction = ( index ) => {
beforeEach( steps[ index ] );
performStepAction( index, steps );
};

const config = {
steps,
closeHandler: () => setTourShowStatus( false, true, tourName ),
Expand All @@ -71,27 +83,27 @@ function SenseiTourKit( { tourName, trackId, steps, extraConfig = {} } ) {
},
callbacks: {
onNextStep: ( index ) => {
performStepAction( index + 1, steps );
runAction( index + 1 );
},
onPreviousStep: ( index ) => {
performStepAction( index - 1, steps );
runAction( index - 1 );
},
onGoToStep: ( index ) => {
if ( index === steps.length - 1 ) {
performStepAction( 0, steps );
runAction( 0 );
} else {
removeHighlightClasses();
}
},
onMaximize: ( index ) => {
performStepAction( index, steps );
runAction( index );
},
onMinimize: () => {
removeHighlightClasses();
},
onStepViewOnce: ( index ) => {
if ( index === 0 ) {
performStepAction( index, steps );
runAction( index );
}
trackTourStepView( index );
},
Expand Down
43 changes: 40 additions & 3 deletions assets/admin/tour/components/sensei-tour-kit/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,17 @@ describe( 'SenseiTourKit', () => {
Close
</button>
<button
data-testid="nextButton"
data-testid="stepViewOnceButton"
onClick={ () =>
props.config.options.callbacks.onStepViewOnce( 1 )
}
></button>
<button
data-testid="nextButton"
onClick={ () =>
props.config.options.callbacks.onNextStep( 1 )
}
></button>
<h1>WpcomTourKit output</h1>
</div>
);
Expand Down Expand Up @@ -139,7 +145,7 @@ describe( 'SenseiTourKit', () => {
/>
);

fireEvent.click( getByTestId( 'nextButton' ) );
fireEvent.click( getByTestId( 'stepViewOnceButton' ) );

expect(
window.sensei_log_event
Expand Down Expand Up @@ -169,8 +175,39 @@ describe( 'SenseiTourKit', () => {
/>
);

fireEvent.click( getByTestId( 'nextButton' ) );
fireEvent.click( getByTestId( 'stepViewOnceButton' ) );

expect( window.sensei_log_event ).not.toHaveBeenCalled();
} );

test( 'should call the beforeEach for every step', () => {
useSelect.mockImplementation( () => ( {
showTour: true,
} ) );
const beforeEachMock = jest.fn();

const nextStep = {
slug: 'step-2',
};

const { getByTestId } = render(
<SenseiTourKit
tourName="test-tour"
steps={ [
{
slug: 'step-1',
},
nextStep,
{
slug: 'step-2',
},
] }
beforeEach={ beforeEachMock }
/>
);

fireEvent.click( getByTestId( 'nextButton' ) );

expect( beforeEachMock ).toHaveBeenCalledWith( nextStep );
} );
} );
13 changes: 8 additions & 5 deletions assets/admin/tour/course-tour/steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { __ } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import { ExternalLink } from '@wordpress/components';
import { select, dispatch } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { store as editorStore } from '@wordpress/editor';

/**
* Internal dependencies
*/
Expand All @@ -18,16 +21,16 @@ import {
waitForElement,
} from '../helper';

const getCourseOutlineBlock = () =>
export const getCourseOutlineBlock = () =>
getFirstBlockByName(
'sensei-lms/course-outline',
select( 'core/block-editor' ).getBlocks()
select( blockEditorStore ).getBlocks()
);

function insertLessonBlock( lessonTitle ) {
const courseOutlineBlock = getCourseOutlineBlock();
if ( courseOutlineBlock ) {
const { insertBlock } = dispatch( 'core/block-editor' );
const { insertBlock } = dispatch( blockEditorStore );

insertBlock(
createBlock( 'sensei-lms/course-outline-lesson', {
Expand All @@ -44,7 +47,7 @@ function focusOnCourseOutlineBlock() {
if ( ! courseOutlineBlock ) {
return;
}
dispatch( 'core/editor' ).selectBlock( courseOutlineBlock.clientId );
dispatch( editorStore ).selectBlock( courseOutlineBlock.clientId );
}

async function ensureLessonBlocksIsInEditor() {
Expand Down Expand Up @@ -486,7 +489,7 @@ function getTourSteps() {
);

if ( ! savedLesson ) {
const { savePost } = dispatch( 'core/editor' );
const { savePost } = dispatch( editorStore );
savePost();
await waitForElement( savedlessonSelector, 15 );
}
Expand Down
46 changes: 35 additions & 11 deletions assets/admin/tour/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,18 @@ export function performStepAction( index, steps ) {
/**
* Highlights the elements with a border.
*
* @param {Array} selectors An array of selectors to highlight.
* @param {Array} selectors An array of selectors to highlight.
* @param {string} modifier A modifier to add to the highlight class.
*/
export function highlightElementsWithBorders( selectors ) {
export function highlightElementsWithBorders( selectors, modifier = '' ) {
selectors.forEach( function ( selector ) {
const element = document.querySelector( selector );
if ( element ) {
element.classList.add( HIGHLIGHT_CLASS );

if ( modifier ) {
element.classList.add( HIGHLIGHT_CLASS + '--' + modifier );
}
}
} );
}
Expand All @@ -42,10 +47,18 @@ export function removeHighlightClasses() {
'.sensei-tour-highlight'
);
highlightedElements.forEach( function ( element ) {
element.classList.remove( HIGHLIGHT_CLASS );
// Remove class and modifiers.
[ ...element.classList ].forEach( ( className ) => {
if ( className.startsWith( HIGHLIGHT_CLASS ) ) {
element.classList.remove( className );
}
} );
} );
}

let stepActionTimeout = null;
let rejectLastPromise = null;

/**
* Performs step actions one after another.
*
Expand All @@ -54,15 +67,26 @@ export function removeHighlightClasses() {
export async function performStepActionsAsync( stepActions ) {
removeHighlightClasses();

for ( const stepAction of stepActions ) {
if ( stepAction ) {
await new Promise( ( resolve ) =>
setTimeout( () => {
stepAction.action();
resolve();
}, stepAction.delay ?? 0 )
);
// Clear the timeout and reject the last promise if it exists, so it stops the step if actions from another step started.
clearTimeout( stepActionTimeout );
if ( rejectLastPromise ) {
rejectLastPromise();
}

try {
for ( const stepAction of stepActions ) {
if ( stepAction ) {
await new Promise( ( resolve, reject ) => {
rejectLastPromise = reject;
stepActionTimeout = setTimeout( () => {
stepAction.action();
resolve();
}, stepAction.delay ?? 0 );
} );
}
}
} catch ( e ) {
// Do nothing.
}
}

Expand Down
55 changes: 55 additions & 0 deletions assets/admin/tour/helper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ describe( 'highlightElementsWithBorders', () => {
expect( document.querySelector( selector ) ).toBeNull();
} );
} );

it( 'should add highlight class to the element with a modifier', () => {
const element = document.createElement( 'div' );

mockQuerySelector.mockImplementation( () => {
return element;
} );

highlightElementsWithBorders( [ 'div' ], 'modifier' );

expect( element.className ).toBe(
`${ HIGHLIGHT_CLASS } ${ HIGHLIGHT_CLASS }--modifier`
);
} );
} );

describe( 'removeHighlightClasses', () => {
Expand Down Expand Up @@ -137,6 +151,30 @@ describe( 'removeHighlightClasses', () => {
);
} );
} );

it( 'should add remove modifier classNames', () => {
const mockedElement = document.createElement( 'div' );

mockedElement.classList.add(
'any-other-class',
HIGHLIGHT_CLASS,
HIGHLIGHT_CLASS + '--modifier'
);

mockQuerySelectorAll.mockReturnValue( [ mockedElement ] );

removeHighlightClasses();

expect( mockedElement.classList.contains( HIGHLIGHT_CLASS ) ).toBe(
false
);
expect(
mockedElement.classList.contains( HIGHLIGHT_CLASS + '--modifier' )
).toBe( false );
expect( mockedElement.classList.contains( 'any-other-class' ) ).toBe(
true
);
} );
} );

describe( 'performStepActionsAsync', () => {
Expand Down Expand Up @@ -199,6 +237,23 @@ describe( 'performStepActionsAsync', () => {
expect( stepActions[ 2 ].action ).toHaveBeenCalledTimes( 1 );
expect( setTimeout ).toHaveBeenCalledTimes( 3 );
} );

it( 'should stop previous step actions if starting a new one', async () => {
jest.useFakeTimers();
jest.spyOn( global, 'setTimeout' );
const stepActions = [
{ action: jest.fn(), delay: 100 },
{ action: jest.fn(), delay: 200 },
];

performStepActionsAsync( stepActions );

await jest.runAllTimers();
expect( stepActions[ 0 ].action ).toHaveBeenCalledTimes( 1 );
performStepActionsAsync( [] );
await jest.runAllTimers();
expect( stepActions[ 1 ].action ).not.toHaveBeenCalled();
} );
} );

describe( 'waitForElement', () => {
Expand Down
3 changes: 2 additions & 1 deletion assets/admin/tour/lesson-tour/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { registerPlugin } from '@wordpress/plugins';
*/
import { getFirstBlockByName } from '../../../blocks/course-outline/data';
import SenseiTourKit from '../components/sensei-tour-kit';
import getTourSteps from './steps';
import getTourSteps, { beforeEach } from './steps';
import { useState } from '@wordpress/element';

const tourName = 'sensei-lesson-tour';
Expand All @@ -33,6 +33,7 @@ export default function LessonTour() {
trackId="lesson_quiz_onboarding_step_complete"
tourName={ tourName }
steps={ tourSteps }
beforeEach={ beforeEach }
/>
);
}
Expand Down
Loading

0 comments on commit 6190ee1

Please sign in to comment.