-
Notifications
You must be signed in to change notification settings - Fork 0
feat: onboarding tour with name changes #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # How to create new tours | ||
|
|
||
| ## 1. Create a unique tourID for the tour | ||
|
|
||
| Let's say the tourId is `interaction-library` | ||
|
|
||
| ## 2. Add the GuidedModalTour component | ||
|
|
||
| Add the GuidedModalTour component to where we want to show the tour. This tour | ||
| will be visible only once when the component is rendered. | ||
|
|
||
| For example in `interaction-library` tour, we can add it in the actual interaction library | ||
| modal render: | ||
|
|
||
| ```js | ||
| <GuidedModalTour tourId="interaction-library" /> | ||
| ``` | ||
|
|
||
| ## 3. Add initialization code to trigger the tour if needed | ||
|
|
||
| Since the `GuidedModalTour` component is only rendered when the tour is actually | ||
| shown, if we are coming from the Getting Started screen, and we want to force | ||
| the tour to open and need to perform any code for this, then we can add them in: | ||
|
|
||
| `src/editor/components/guided-modal-tour/index.js` | ||
|
|
||
| ## 4. Add the show condition for the tour | ||
|
|
||
| By default, `GuidedModalTour` only shows once. When it's finished, it will not | ||
| show again. We can change this behavior e.g. stop the tour from showing, by | ||
| adding a condition in `./tour-conditions.js`. | ||
|
|
||
| ## 5. Create the tour steps | ||
|
|
||
| Create the tour steps in `src/editor/components/modal-tour/tours/`, refer to | ||
| `src/editor/components/modal-tour/tours/README.md` for more details | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| .interact-tour-modal--overlay { | ||
| z-index: 1000002; | ||
| background-color: transparent !important; | ||
| pointer-events: none; | ||
| } | ||
|
|
||
| .interact-tour-modal { | ||
| pointer-events: all; | ||
| position: absolute; | ||
| --offset-x: 0px; | ||
| --offset-y: 0px; | ||
| --left: 50%; | ||
| --top: 50%; | ||
| left: var(--left); | ||
| top: var(--top); | ||
| margin-left: var(--offset-x); | ||
| margin-top: var(--offset-y); | ||
| overflow: visible; | ||
| border-radius: 16px; | ||
|
|
||
| --wp-admin-theme-color: #05f; | ||
| --wp-admin-theme-color-darker-10: #0044cc; | ||
| --wp-admin-theme-color-darker-20: #0036a1; | ||
|
|
||
| // Smoothly transition moving top & left. | ||
| transition: | ||
| max-width 0.4s cubic-bezier(0.4, 0, 0.2, 1), | ||
| left 0.2s cubic-bezier(0.4, 0, 0.2, 1), | ||
| top 0.2s cubic-bezier(0.4, 0, 0.2, 1), | ||
| margin-left 0.2s cubic-bezier(0.4, 0, 0.2, 1), | ||
| margin-top 0.2s cubic-bezier(0.4, 0, 0.2, 1), | ||
| opacity 0.4s ease-in-out, | ||
| transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), | ||
| box-shadow 0.2s ease-in-out; | ||
| will-change: left, top, max-width; | ||
|
|
||
| display: none; | ||
| &.interact-tour-modal--visible { | ||
| display: block !important; | ||
| opacity: 0 !important; | ||
| transform: scale(0.4); | ||
| } | ||
| &.interact-tour-modal--visible-delayed { | ||
| opacity: 1 !important; | ||
| transform: scale(1); | ||
| } | ||
|
|
||
| .components-modal__header-heading { | ||
| line-height: 1.2; | ||
| } | ||
|
|
||
| .components-button { | ||
| border-radius: 4px; | ||
| } | ||
|
|
||
| .components-modal__content { | ||
| padding: 2em; | ||
| margin: 0; | ||
| position: relative; | ||
| overflow: visible; | ||
| z-index: 1; | ||
| box-shadow: 0 22px 200px 4px #0005; | ||
| border-radius: 16px; | ||
| // border: 1px solid #f00069ad; | ||
| } | ||
| .components-modal__header { | ||
| position: relative; | ||
| padding: 0; | ||
| margin-bottom: 8px; | ||
| height: auto; | ||
| line-height: 1.2; | ||
| } | ||
| .interact-tour-modal__footer { | ||
| margin-top: 16px; | ||
| justify-content: flex-end; | ||
| } | ||
|
|
||
| .interact-tour-modal__help { | ||
| position: relative; | ||
| background: #f4fbff; | ||
| padding: 8px 12px; | ||
| padding-inline-start: 30px; | ||
| border-radius: 8px; | ||
| border: 1px solid rgba(0, 0, 0, 0.1); | ||
| margin-block: 16px; | ||
| svg { | ||
| vertical-align: text-top; | ||
| margin-inline-end: 8px; | ||
| margin-top: 1px; | ||
| position: absolute; | ||
| inset-inline-start: 9px; | ||
| } | ||
| } | ||
|
|
||
| .interact-tour-modal__cta { | ||
| width: 100%; | ||
| justify-content: center; | ||
| margin: 16px 0 8px; | ||
| } | ||
|
|
||
| &.interact-tour-modal--right, | ||
| &.interact-tour-modal--left, | ||
| &.interact-tour-modal--top, | ||
| &.interact-tour-modal--bottom { | ||
| .components-modal__content { | ||
| box-shadow: rgba(0, 0, 0, 0.2) -20px 22px 60px -4px; | ||
| &::after { | ||
| content: ""; | ||
| position: absolute; | ||
| top: 50%; | ||
| left: -10px; | ||
| width: 30px; | ||
| height: 30px; | ||
| transform: translateY(-50%) rotate(45deg); | ||
| border-radius: 4px; | ||
| background-color: #fff; | ||
| z-index: -1; | ||
| } | ||
| } | ||
| } | ||
| &.interact-tour-modal--left { | ||
| .components-modal__content { | ||
| box-shadow: rgba(0, 0, 0, 0.2) 20px 22px 60px -4px; | ||
| &::after { | ||
| left: auto; | ||
| right: -10px; | ||
| } | ||
| } | ||
| } | ||
| &.interact-tour-modal--left-top { | ||
| .components-modal__content { | ||
| &::after { | ||
| top: 30px; | ||
| } | ||
| } | ||
| } | ||
| &.interact-tour-modal--top { | ||
| .components-modal__content { | ||
| box-shadow: rgba(0, 0, 0, 0.2) 0px 22px 60px -4px; | ||
| &::after { | ||
| top: auto; | ||
| left: 50%; | ||
| bottom: -10px; | ||
| transform: translateX(-50%) rotate(45deg); | ||
| } | ||
| } | ||
| } | ||
| &.interact-tour-modal--top-right { | ||
| .components-modal__content { | ||
| &::after { | ||
| left: auto; | ||
| right: 16px; | ||
| } | ||
| } | ||
| } | ||
| &.interact-tour-modal--bottom { | ||
| .components-modal__content { | ||
| box-shadow: rgba(0, 0, 0, 0.2) 0px -22px 60px -4px; | ||
| &::after { | ||
| left: 50%; | ||
| top: -10px; | ||
| transform: translateX(-50%) rotate(45deg); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| .interact-tour-modal__steps { | ||
| display: flex; | ||
| gap: 6px; | ||
| margin-inline-end: auto; | ||
| } | ||
| .interact-tour-modal__step { | ||
| width: 8px; | ||
| height: 8px; | ||
| border-radius: 20px; | ||
| background-color: #e1e1e1; | ||
| // cursor: pointer; | ||
| padding: 0 !important; | ||
| margin: 0 !important; | ||
|
|
||
| &--active { | ||
| background: #05f; | ||
| width: 24px; | ||
| border-radius: 20px; | ||
| } | ||
|
|
||
| // &:hover { | ||
| // background-color: #aaa; | ||
| // } | ||
| } | ||
|
|
||
| .interact-tour-modal__glow { | ||
| position: absolute; | ||
| z-index: 1000001; | ||
| box-shadow: 0 0 20px #05f; | ||
| border-radius: 8px; | ||
| pointer-events: none; | ||
| animation: tour-modal-glow 0.7s infinite alternate; | ||
| mix-blend-mode: multiply; | ||
| will-change: transform, box-shadow; | ||
| transition: opacity 0.2s ease-in-out; | ||
| opacity: 1; | ||
| &.interact-tour-modal__glow--hidden { | ||
| opacity: 0; | ||
| } | ||
| } | ||
|
|
||
| .interact-tour-modal__glow--medium, | ||
| .interact-tour-modal__glow--large { | ||
| animation: tour-modal-glow-small 0.7s infinite alternate; | ||
| } | ||
|
|
||
| // Animation keyframes to grow the box-shadow like it's glowing | ||
| @keyframes tour-modal-glow { | ||
| 0% { | ||
| box-shadow: 0 0 20px #05f, 0 0 5px #0036a1; | ||
| transform: scaleX(1) scaleY(1); | ||
| } | ||
| 100% { | ||
| box-shadow: 0 0 50px #05f, 0 0 5px #0036a1; | ||
| transform: scaleX(1.05) scaleY(1.12); | ||
| } | ||
| } | ||
|
|
||
| // Animation keyframes for small glow | ||
| @keyframes tour-modal-glow-small { | ||
| 0% { | ||
| box-shadow: 0 0 20px #05f, 0 0 5px #0036a1; | ||
| transform: scaleX(1) scaleY(1); | ||
| } | ||
| 100% { | ||
| box-shadow: 0 0 50px #05f, 0 0 5px #0036a1; | ||
| transform: scaleX(1.02) scaleY(1.02); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,108 @@ | ||||||||||
| /** | ||||||||||
| * Internal dependencies | ||||||||||
| */ | ||||||||||
| // import { TOUR_STEPS } from './tour-steps' | ||||||||||
| import { TOUR_CONDITIONS } from './tour-conditions' | ||||||||||
| import { | ||||||||||
| clearActiveTour, | ||||||||||
| isTourActive, | ||||||||||
| getActiveTourId, | ||||||||||
| addTourStateListener, | ||||||||||
| } from './util' | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * External dependencies | ||||||||||
| */ | ||||||||||
| import { guidedTourStates } from 'interactions' | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * WordPress dependencies | ||||||||||
| */ | ||||||||||
| import { models } from '@wordpress/api' | ||||||||||
| import { | ||||||||||
| useEffect, useState, lazy, Suspense, memo, | ||||||||||
| } from '@wordpress/element' | ||||||||||
|
|
||||||||||
| // The main tour component. | ||||||||||
| const GuidedModalTour = memo( props => { | ||||||||||
| const { | ||||||||||
| tourId = '', // This is the ID of the tour, this will be used to store the tour state in the database and to get the steps. | ||||||||||
| } = props | ||||||||||
|
|
||||||||||
| // On mount, check if the tour has been completed, if so, don't show it. | ||||||||||
| const [ isDone, setIsDone ] = useState( guidedTourStates.includes( tourId ) ) | ||||||||||
|
|
||||||||||
| // We need this to prevent the tour from being shown again if it's just completed. | ||||||||||
| const [ justCompleted, setJustCompleted ] = useState( false ) | ||||||||||
|
|
||||||||||
| // Check if another tour is already active | ||||||||||
| const [ isAnotherTourActive, setIsAnotherTourActive ] = useState( isTourActive() && getActiveTourId() !== tourId ) | ||||||||||
|
|
||||||||||
| // Listen for tour state changes | ||||||||||
| useEffect( () => { | ||||||||||
| const removeListener = addTourStateListener( activeId => { | ||||||||||
| setIsAnotherTourActive( activeId !== null && activeId !== tourId ) | ||||||||||
| } ) | ||||||||||
| return removeListener | ||||||||||
| }, [ tourId ] ) | ||||||||||
|
|
||||||||||
| if ( justCompleted ) { | ||||||||||
| return null | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // If another tour is already active, don't show this tour | ||||||||||
| if ( isAnotherTourActive ) { | ||||||||||
| return null | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // If there is a condition, check if it's met, if not, don't show the tour. | ||||||||||
| // condition can be true, false, or null. true will show the tour (even if | ||||||||||
| // it's already done), false will not show the tour, null will show the tour | ||||||||||
| // only once (normal behavior). | ||||||||||
| const condition = TOUR_CONDITIONS[ tourId ] | ||||||||||
| const conditionResult = condition ? condition() : null | ||||||||||
| if ( conditionResult === false ) { | ||||||||||
| return null | ||||||||||
| } else if ( conditionResult === null ) { | ||||||||||
| if ( isDone ) { | ||||||||||
| return null | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Only lazy-load ModalTour when we're actually going to render it | ||||||||||
| const ModalTour = lazy( () => import( /* webpackChunkName: "modal-tour" */ '../modal-tour' ) ) | ||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <Suspense fallback={ null }> | ||||||||||
| <ModalTour | ||||||||||
| tourId={ tourId } | ||||||||||
| onClose={ () => { | ||||||||||
| setIsDone( true ) | ||||||||||
| setJustCompleted( true ) | ||||||||||
|
|
||||||||||
| // Clear the active tour | ||||||||||
| clearActiveTour() | ||||||||||
|
|
||||||||||
| // Update the interact_guided_tour_states setting | ||||||||||
| if ( ! guidedTourStates.includes( tourId ) ) { | ||||||||||
| // eslint-disable-next-line camelcase | ||||||||||
| const settings = new models.Settings( { interact_guided_tour_states: [ ...guidedTourStates, tourId ] } ) | ||||||||||
| settings.save().catch( error => { | ||||||||||
| console.error( 'Error saving guided tour state:', error ) // eslint-disable-line no-console | ||||||||||
| } ) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Soft update the global variable to prevent the tour from being shown again. | ||||||||||
| guidedTourStates.push( tourId ) | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Avoid direct mutation of the imported array. Directly mutating If // Soft update the global variable to prevent the tour from being shown again.
+ // Note: This mutates the global cache to prevent re-renders in other components
guidedTourStates.push( tourId )Based on learnings: In React, direct mutations of imported data structures should be avoided or clearly documented as intentional cache synchronization. 📝 Committable suggestion
Suggested change
|
||||||||||
|
|
||||||||||
| // Remove the "tour" GET parameter from the URL so conditions won't get triggered again. | ||||||||||
| const url = new URL( window.location.href ) | ||||||||||
| url.searchParams.delete( 'tour' ) | ||||||||||
| window.history.replaceState( null, '', url.toString() ) | ||||||||||
| } } | ||||||||||
| /> | ||||||||||
| </Suspense> | ||||||||||
| ) | ||||||||||
| } ) | ||||||||||
|
|
||||||||||
| export default GuidedModalTour | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| // For each condition, true will show the tour (even if it's already done), false will not show the tour, null will show the tour only once. | ||
| export const TOUR_CONDITIONS = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move
lazy()call to module scope.The
lazy()call is currently inside the component's render path, which means it's re-invoked on every render. React'slazy()must be called once at the module level to properly enable code-splitting and avoid remounting issues.Apply this diff:
📝 Committable suggestion
🤖 Prompt for AI Agents