-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cfa1e98
commit 2dbf9b8
Showing
15 changed files
with
1,291 additions
and
14 deletions.
There are no files selected for viewing
81 changes: 81 additions & 0 deletions
81
client/a8c-for-agencies/components/guided-tour-step/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { Button, Popover } from '@automattic/components'; | ||
import classNames from 'classnames'; | ||
import { useTranslate } from 'i18n-calypso'; | ||
import { LegacyRef, useContext, useEffect } from 'react'; | ||
import { GuidedTourContext } from 'calypso/a8c-for-agencies/data/guided-tours/guided-tour-context'; | ||
|
||
type Props = { | ||
id: string; | ||
context: LegacyRef< HTMLSpanElement | null >; | ||
className?: string; | ||
hideSteps?: boolean; | ||
}; | ||
|
||
/** | ||
* Renders a single step in a guided tour. | ||
*/ | ||
export function GuidedTourStep( { id, context, hideSteps, className }: Props ) { | ||
const translate = useTranslate(); | ||
|
||
const { currentStep, currentStepCount, stepsCount, nextStep, endTour, tourDismissed } = | ||
useContext( GuidedTourContext ); | ||
|
||
const lastTourLabel = stepsCount === 1 ? translate( 'Got it' ) : translate( 'Done' ); | ||
|
||
// Keep track of which tour steps are currently rendered/mounted. | ||
useEffect( () => { | ||
// Register on mount. | ||
registerRenderedStep( id ); | ||
// Unregister on unmount. | ||
return () => unregisterRenderedStep( id ); | ||
}, [ id, registerRenderedStep, unregisterRenderedStep ] ); | ||
|
||
// Do not render unless this is the current step in the active tour. | ||
if ( ! currentStep || id !== currentStep.id || tourDismissed ) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<Popover | ||
isVisible={ true } | ||
className={ classNames( className, 'guided-tour__popover' ) } | ||
context={ context } | ||
position={ currentStep.popoverPosition } | ||
> | ||
<h2 className="guided-tour__popover-heading">{ currentStep.title }</h2> | ||
<p className="guided-tour__popover-description">{ currentStep.description }</p> | ||
<div className="guided-tour__popover-footer"> | ||
<div> | ||
{ | ||
// Show the step count if there are multiple steps and we're not on the last step, unless we explicitly choose to hide them | ||
stepsCount > 1 && ! hideSteps && ( | ||
<span className="guided-tour__popover-step-count"> | ||
{ translate( 'Step %(currentStep)d of %(totalSteps)d', { | ||
args: { currentStep: currentStepCount, totalSteps: stepsCount }, | ||
} ) } | ||
</span> | ||
) | ||
} | ||
</div> | ||
<div className="guided-tour__popover-footer-right-content"> | ||
<> | ||
{ ( ( ! currentStep.nextStepOnTargetClick && | ||
stepsCount > 1 && | ||
currentStepCount < stepsCount ) || | ||
currentStep.forceShowSkipButton ) && ( | ||
// Show the skip button if there are multiple steps and we're not on the last step, unless we explicitly choose to add them | ||
<Button borderless onClick={ endTour }> | ||
{ translate( 'Skip' ) } | ||
</Button> | ||
) } | ||
{ ! currentStep.nextStepOnTargetClick && ( | ||
<Button onClick={ nextStep }> | ||
{ currentStepCount === stepsCount ? lastTourLabel : translate( 'Next' ) } | ||
</Button> | ||
) } | ||
</> | ||
</div> | ||
</div> | ||
</Popover> | ||
); | ||
} |
166 changes: 166 additions & 0 deletions
166
client/a8c-for-agencies/components/guided-tour/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import page from '@automattic/calypso-router'; | ||
import { Popover, Button } from '@automattic/components'; | ||
import classNames from 'classnames'; | ||
import { useTranslate } from 'i18n-calypso'; | ||
import { useState, useEffect, useCallback, useContext } from 'react'; | ||
import { GuidedTourContext } from 'calypso/a8c-for-agencies/data/guided-tours/guided-tour-context'; | ||
import { useDispatch, useSelector } from 'calypso/state'; | ||
import { recordTracksEvent } from 'calypso/state/analytics/actions'; | ||
import { getJetpackDashboardPreference as getPreference } from 'calypso/state/jetpack-agency-dashboard/selectors'; | ||
import { savePreference } from 'calypso/state/preferences/actions'; | ||
import { preferencesLastFetchedTimestamp } from 'calypso/state/preferences/selectors'; | ||
|
||
import './style.scss'; | ||
|
||
export interface Tour { | ||
id: string; | ||
target: string; | ||
title: string; | ||
description: string | JSX.Element; | ||
popoverPosition?: | ||
| 'top' | ||
| 'top right' | ||
| 'right' | ||
| 'bottom right' | ||
| 'bottom' | ||
| 'bottom left' | ||
| 'left' | ||
| 'top left'; | ||
nextStepOnTargetClick?: string; | ||
forceShowSkipButton?: boolean; | ||
} | ||
|
||
type Props = { | ||
className?: string; | ||
}; | ||
|
||
const GuidedTour = ( { className }: Props ) => { | ||
const translate = useTranslate(); | ||
const dispatch = useDispatch(); | ||
|
||
const { currentTourId, currentStep, currentStepCount, nextStep, stepsCount, targetElement } = | ||
useContext( GuidedTourContext ); | ||
|
||
if ( ! currentTourId || ! currentStep ) { | ||
throw new Error( 'GuidedTour requires a current tour ID to be available in context.' ); | ||
} | ||
|
||
const preference = useSelector( ( state ) => getPreference( state, currentTourId ) ); | ||
const hasFetched = !! useSelector( preferencesLastFetchedTimestamp ); | ||
|
||
const [ isVisible, setIsVisible ] = useState( false ); | ||
|
||
const isDismissed = preference?.dismiss; | ||
|
||
const { | ||
title, | ||
description, | ||
popoverPosition, | ||
nextStepOnTargetClick, | ||
forceShowSkipButton = false, | ||
} = currentStep; | ||
|
||
const redirectAfterTourEnds = false; | ||
const hideSteps = false; | ||
|
||
useEffect( () => { | ||
if ( targetElement && ! isDismissed && hasFetched ) { | ||
setIsVisible( true ); | ||
dispatch( | ||
recordTracksEvent( 'calypso_a4a_start_tour', { | ||
tour: currentTourId, | ||
} ) | ||
); | ||
} | ||
}, [ dispatch, isDismissed, currentTourId, targetElement, hasFetched ] ); | ||
|
||
const endTour = useCallback( () => { | ||
dispatch( savePreference( currentTourId, { ...preference, dismiss: true } ) ); | ||
dispatch( | ||
recordTracksEvent( 'calypso_a4a_end_tour', { | ||
tour: currentTourId, | ||
} ) | ||
); | ||
if ( redirectAfterTourEnds ) { | ||
page.redirect( redirectAfterTourEnds ); | ||
} | ||
}, [ dispatch, currentTourId, preference, redirectAfterTourEnds ] ); | ||
|
||
useEffect( () => { | ||
if ( nextStepOnTargetClick && targetElement && ! isDismissed && hasFetched ) { | ||
// Find the target element using the nextStepOnTargetClick selector | ||
targetElement.addEventListener( 'click', nextStep ); | ||
} | ||
|
||
// Cleanup function to remove the event listener | ||
return () => { | ||
if ( targetElement ) { | ||
targetElement.removeEventListener( 'click', nextStep ); | ||
} | ||
}; | ||
}, [ nextStepOnTargetClick, nextStep, targetElement, isDismissed, hasFetched ] ); | ||
|
||
const lastTourLabel = stepsCount === 1 ? translate( 'Got it' ) : translate( 'Done' ); | ||
|
||
return ( | ||
<Popover | ||
isVisible={ isVisible } | ||
className={ classNames( className, 'guided-tour__popover' ) } | ||
context={ targetElement } | ||
position={ popoverPosition } | ||
> | ||
<h2 className="guided-tour__popover-heading">{ title }</h2> | ||
<p className="guided-tour__popover-description">{ description }</p> | ||
<div className="guided-tour__popover-footer"> | ||
<div> | ||
{ | ||
// Show the step count if there are multiple steps and we're not on the last step, unless we explicitly choose to hide them | ||
stepsCount > 1 && ! hideSteps && ( | ||
<span className="guided-tour__popover-step-count"> | ||
{ translate( 'Step %(currentStep)d of %(totalSteps)d', { | ||
args: { currentStep: currentStepCount + 1, totalSteps: stepsCount }, | ||
} ) } | ||
</span> | ||
) | ||
} | ||
</div> | ||
<div className="guided-tour__popover-footer-right-content"> | ||
<> | ||
{ ( ( ! nextStepOnTargetClick && | ||
stepsCount > 1 && | ||
currentStepCount < stepsCount - 1 ) || | ||
forceShowSkipButton ) && ( | ||
// Show the skip button if there are multiple steps and we're not on the last step, unless we explicitly choose to add them | ||
<Button borderless onClick={ endTour }> | ||
{ translate( 'Skip' ) } | ||
</Button> | ||
) } | ||
{ ! nextStepOnTargetClick && ( | ||
<Button onClick={ nextStep }> | ||
{ currentStepCount === stepsCount - 1 ? lastTourLabel : translate( 'Next' ) } | ||
</Button> | ||
) } | ||
</> | ||
</div> | ||
</div> | ||
</Popover> | ||
); | ||
}; | ||
|
||
const GuidedTourSlot = ( { defaultTourId } ) => { | ||
const { currentTourId, currentStep, startTour } = useContext( GuidedTourContext ); | ||
|
||
useEffect( () => { | ||
if ( ! currentTourId && defaultTourId ) { | ||
startTour( defaultTourId ); | ||
} | ||
}, [ currentTourId, defaultTourId, startTour ] ); | ||
|
||
if ( ! currentTourId || ! currentStep ) { | ||
return null; | ||
} | ||
|
||
return <GuidedTour />; | ||
}; | ||
|
||
export default GuidedTourSlot; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
.guided-tour__popover { | ||
.popover__inner { | ||
display: flex; | ||
gap: 16px; | ||
padding: 16px; | ||
flex-direction: column; | ||
align-items: flex-start; | ||
/* stylelint-disable-next-line scales/radii */ | ||
border-radius: 8px; | ||
} | ||
} | ||
|
||
.guided-tour__popover-heading { | ||
color: var(--studio-gray-100); | ||
font-size: rem(14px); | ||
line-height: 1.5; | ||
font-weight: 500; | ||
} | ||
|
||
.guided-tour__popover-description { | ||
color: var(--studio-gray-80); | ||
font-size: rem(12px); | ||
line-height: 1.5; | ||
max-width: 220px; | ||
text-align: left; | ||
margin-block-end: 0; | ||
} | ||
|
||
.guided-tour__popover-footer { | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
width: 100%; | ||
|
||
.guided-tour__popover-step-count { | ||
color: var(--studio-gray-60); | ||
font-size: rem(12px); | ||
} | ||
|
||
.guided-tour__popover-footer-right-content { | ||
display: flex; | ||
gap: 8px; | ||
|
||
button { | ||
padding: 4px 8px; | ||
font-size: rem(12px); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.