Skip to content

Commit 4004f3f

Browse files
created modal tour component
1 parent 887566a commit 4004f3f

File tree

4 files changed

+369
-0
lines changed

4 files changed

+369
-0
lines changed

src/components/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export { default as AdminSelectSetting } from './admin-select-setting'
5656
export { default as TaxonomyControl } from './taxonomy-control'
5757
export { default as Tooltip } from './tooltip'
5858
export { default as BlockStyles } from './block-styles'
59+
export { default as ModalTour } from './modal-tour'
5960

6061
// V2 only Components, for deprecation
6162
export { default as BlockContainer } from './block-container'

src/components/modal-design-library/modal.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import BlockList from './block-list'
66
import Button from '../button'
77
// import AdvancedToolbarControl from '../advanced-toolbar-control'
88
import DesignLibraryList from '~stackable/components/design-library-list'
9+
import { ModalTour } from '~stackable/components'
910
import { getDesigns, filterDesigns } from '~stackable/design-library'
1011

1112
/**
@@ -215,6 +216,10 @@ export const ModalDesignLibrary = props => {
215216
onRequestClose={ props.onClose }
216217
>
217218
<div className="ugb-modal-design-library__wrapper">
219+
220+
{ /* DEV NOTE: this is just a test */ }
221+
<ModalTour />
222+
218223
<aside className="ugb-modal-design-library__sidebar">
219224
<div className="ugb-modal-design-library__filters">
220225
<BlockList

src/components/modal-tour/editor.scss

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
.ugb-tour-modal--overlay {
2+
z-index: 1000000;
3+
background-color: transparent !important;
4+
pointer-events: none;
5+
}
6+
7+
.ugb-tour-modal {
8+
pointer-events: all;
9+
position: absolute;
10+
--offset-x: 0;
11+
--offset-y: 0;
12+
--left: 50%;
13+
--top: 50%;
14+
left: var(--left);
15+
top: var(--top);
16+
overflow: visible;
17+
18+
--wp-admin-theme-color: #f00069;
19+
--wp-admin-theme-color-darker-10: #e0003c;
20+
--wp-admin-theme-color-darker-20: #cb0044;
21+
22+
// Smoothly transition moving top & left.
23+
transition: max-width 0.2s 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), opacity 0.2s ease-in-out, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s ease-in-out;
24+
will-change: left, top, max-width;
25+
26+
display: none;
27+
&.ugb-tour-modal--visible {
28+
display: block !important;
29+
opacity: 0 !important;
30+
transform: scale(0.4);
31+
}
32+
&.ugb-tour-modal--visible-delayed {
33+
opacity: 1 !important;
34+
transform: scale(1);
35+
}
36+
37+
.components-modal__content {
38+
padding: 20px;
39+
margin: 0;
40+
position: relative;
41+
overflow: visible;
42+
z-index: 1;
43+
box-shadow: 0 22px 200px 4px #0005;
44+
border-radius: 4px;
45+
// border: 1px solid #f00069ad;
46+
}
47+
.components-modal__header {
48+
position: relative;
49+
padding: 0;
50+
margin-bottom: 8px;
51+
height: auto;
52+
line-height: 1.2;
53+
}
54+
.ugb-tour-modal__footer {
55+
margin-top: 16px;
56+
justify-content: flex-end;
57+
}
58+
59+
&.ugb-tour-modal--right,
60+
&.ugb-tour-modal--left,
61+
&.ugb-tour-modal--top,
62+
&.ugb-tour-modal--bottom {
63+
.components-modal__content {
64+
box-shadow: rgba(0, 0, 0, 0.3) -20px 22px 70px 4px;
65+
&::after {
66+
content: "";
67+
position: absolute;
68+
top: 50%;
69+
left: -10px;
70+
width: 30px;
71+
height: 30px;
72+
transform: translateY(-50%) rotate(45deg);
73+
border-radius: 4px;
74+
background-color: #fff;
75+
z-index: -1;
76+
}
77+
}
78+
}
79+
&.ugb-tour-modal--left {
80+
.components-modal__content {
81+
box-shadow: rgba(0, 0, 0, 0.3) 20px 22px 70px 4px;
82+
&::after {
83+
left: auto;
84+
right: -10px;
85+
}
86+
}
87+
}
88+
&.ugb-tour-modal--top {
89+
.components-modal__content {
90+
box-shadow: rgba(0, 0, 0, 0.3) 0px 22px 70px 4px;
91+
&::after {
92+
top: auto;
93+
left: 50%;
94+
bottom: -10px;
95+
transform: translateX(-50%) rotate(45deg);
96+
}
97+
}
98+
}
99+
&.ugb-tour-modal--bottom {
100+
.components-modal__content {
101+
box-shadow: rgba(0, 0, 0, 0.3) 0px -22px 70px 4px;
102+
&::after {
103+
left: 50%;
104+
top: -10px;
105+
transform: translateX(-50%) rotate(45deg);
106+
}
107+
}
108+
}
109+
}
110+
111+
.ugb-tour-modal__steps {
112+
display: flex;
113+
gap: 6px;
114+
margin-inline-end: auto;
115+
}
116+
.ugb-tour-modal__step {
117+
width: 8px;
118+
height: 8px;
119+
border-radius: 20px;
120+
background-color: #e1e1e1;
121+
// cursor: pointer;
122+
padding: 0 !important;
123+
margin: 0 !important;
124+
125+
&--active {
126+
background: #f00069;
127+
width: 24px;
128+
border-radius: 20px;
129+
}
130+
131+
// &:hover {
132+
// background-color: #aaa;
133+
// }
134+
}

src/components/modal-tour/index.js

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { i18n } from 'stackable'
2+
import classNames from 'classnames'
3+
4+
import {
5+
Modal, Flex, Button,
6+
} from '@wordpress/components'
7+
import { __ } from '@wordpress/i18n'
8+
import {
9+
useEffect, useState, useCallback, useRef, useMemo,
10+
} from '@wordpress/element'
11+
12+
const STEPS = [
13+
{
14+
title: '👋 ' + __( 'Welcome to Your Design Library', i18n ),
15+
description: __( 'These are pre-built designs that are style-matched to your block theme. You can insert one or more patterns to quickly build your page.', i18n ),
16+
size: 'medium',
17+
},
18+
{
19+
title: __( 'Pick Styling Options', i18n ),
20+
description: __( 'Turn on backgrounds, change color schemes, to customize the library. Go ahead and click on "Section Background" and see your changes in real-time.', i18n ),
21+
anchor: '.ugb-modal-design-library__enable-background',
22+
position: 'right',
23+
nextEventTarget: '.ugb-modal-design-library__enable-background',
24+
// showNext: false,
25+
},
26+
{
27+
title: __( 'Patterns and Full-Pages', i18n ),
28+
description: __( 'Click here to switch between patterns and full-page layouts.', i18n ),
29+
anchor: '.ugb-modal-design-library .components-modal__header',
30+
position: 'bottom',
31+
},
32+
]
33+
34+
const NOOP = () => {}
35+
36+
const ModalTour = props => {
37+
const {
38+
steps = STEPS,
39+
onClose = NOOP,
40+
} = props
41+
42+
const [ currentStep, setCurrentStep ] = useState( 0 )
43+
const [ isVisible, setIsVisible ] = useState( false )
44+
const [ isVisibleDelayed, setIsVisibleDelayed ] = useState( false )
45+
const [ forceRefresh, setForceRefresh ] = useState( 0 )
46+
const modalRef = useRef( null )
47+
48+
const {
49+
title,
50+
description,
51+
size = 'small',
52+
anchor = null, // This is a selector for the element to anchor the modal to. Defaults to middle of the screen.
53+
position = 'center', // This is the position to place the modal relative to the anchor. Can be 'left', 'right', 'top', 'bottom', 'center'.
54+
offsetX = 0,
55+
offsetY = 0,
56+
showNext = true,
57+
nextEvent = 'click',
58+
nextEventTarget = null, // This is a selector for the element to trigger the next event if there is one.
59+
} = steps[ currentStep ]
60+
61+
// Create a stable function reference for the event listener
62+
const handleNextEvent = useCallback( () => {
63+
setCurrentStep( currentStep + 1 )
64+
setTimeout( () => {
65+
setForceRefresh( forceRefresh + 1 )
66+
}, 50 )
67+
}, [ currentStep ] )
68+
69+
// Show modal after 1 second delay
70+
useEffect( () => {
71+
const timer = setTimeout( () => {
72+
setIsVisible( true )
73+
setTimeout( () => {
74+
setIsVisibleDelayed( true )
75+
}, 30 )
76+
}, 1000 )
77+
78+
return () => clearTimeout( timer )
79+
}, [] )
80+
81+
useEffect( () => {
82+
if ( nextEventTarget ) {
83+
const element = document.querySelector( nextEventTarget )
84+
element?.addEventListener( nextEvent, handleNextEvent )
85+
}
86+
87+
return () => {
88+
if ( nextEventTarget ) {
89+
const element = document.querySelector( nextEventTarget )
90+
element?.removeEventListener( nextEvent, handleNextEvent )
91+
}
92+
}
93+
}, [ currentStep, nextEventTarget, nextEvent, handleNextEvent ] )
94+
95+
// These are the X and Y offsets of the modal relative to the anchor. This will be
96+
const [ modalOffsetX, modalOffsetY ] = useMemo( () => {
97+
if ( ! modalRef.current ) {
98+
return [ '', '' ] // This is for the entire screen.
99+
}
100+
101+
const modalRect = modalRef.current.querySelector( '.ugb-tour-modal' ).getBoundingClientRect()
102+
const defaultOffset = [ `${ ( window.innerWidth / 2 ) - ( modalRect.width / 2 ) }px`, `${ ( window.innerHeight / 2 ) - ( modalRect.height / 2 ) }px` ]
103+
104+
if ( ! anchor ) {
105+
return defaultOffset // This is for the entire screen.
106+
}
107+
108+
// Based on the anchor and position, calculate the X and Y offsets of the modal relative to the anchor.
109+
// We have the modalRef.current which we can use to get the modal's bounding client rect.
110+
const anchorRect = document.querySelector( anchor )?.getBoundingClientRect()
111+
112+
switch ( position ) {
113+
case 'left':
114+
// Left, middle
115+
return [ `${ anchorRect.left - modalRect.width - 24 }px`, `${ anchorRect.top + ( anchorRect.height / 2 ) - ( modalRect.height / 2 ) }px` ]
116+
case 'right':
117+
// Right, middle
118+
return [ `${ anchorRect.right + 24 }px`, `${ anchorRect.top + ( anchorRect.height / 2 ) - ( modalRect.height / 2 ) }px` ]
119+
case 'top':
120+
// Center, top
121+
return [ `${ anchorRect.left + ( anchorRect.width / 2 ) - ( modalRect.width / 2 ) }px`, `${ anchorRect.top - modalRect.height - 24 }px` ]
122+
case 'bottom':
123+
// Center, bottom
124+
return [ `${ anchorRect.left + ( anchorRect.width / 2 ) - ( modalRect.width / 2 ) }px`, `${ anchorRect.bottom + 24 }px` ]
125+
case 'center':
126+
return [
127+
`${ anchorRect.left + ( anchorRect.width / 2 ) - ( modalRect.width / 2 ) }px`,
128+
`${ anchorRect.top + ( anchorRect.height / 2 ) - ( modalRect.height / 2 ) }px`,
129+
]
130+
default:
131+
return defaultOffset
132+
}
133+
}, [ anchor, position, modalRef.current, isVisible, isVisibleDelayed, forceRefresh ] )
134+
135+
if ( ! isVisible ) {
136+
return null
137+
}
138+
139+
return (
140+
<Modal
141+
title={ title }
142+
overlayClassName="ugb-tour-modal--overlay"
143+
shouldCloseOnClickOutside={ false }
144+
size={ size }
145+
onRequestClose={ onClose }
146+
className={ classNames( 'ugb-tour-modal', `ugb-tour-modal--${ position }`, {
147+
'ugb-tour-modal--visible': isVisible,
148+
'ugb-tour-modal--visible-delayed': isVisibleDelayed,
149+
} ) }
150+
ref={ modalRef }
151+
>
152+
<style>
153+
{ `.ugb-tour-modal {
154+
--offset-x: ${ offsetX }px;
155+
--offset-y: ${ offsetY }px;
156+
--left: ${ modalOffsetX };
157+
--top: ${ modalOffsetY };
158+
}` }
159+
</style>
160+
{ description }
161+
<Flex className="ugb-tour-modal__footer">
162+
<Steps
163+
numSteps={ steps.length }
164+
currentStep={ currentStep }
165+
onClickStep={ setCurrentStep }
166+
/>
167+
{ currentStep > 0 && (
168+
<Button
169+
variant="secondary"
170+
onClick={ () => {
171+
setCurrentStep( currentStep - 1 )
172+
setTimeout( () => {
173+
setForceRefresh( forceRefresh + 1 )
174+
}, 50 )
175+
} }
176+
>
177+
{ __( 'Previous', i18n ) }
178+
</Button>
179+
) }
180+
{ showNext && (
181+
<Button
182+
variant="primary"
183+
onClick={ () => {
184+
if ( currentStep === steps.length - 1 ) {
185+
props.onClose()
186+
} else {
187+
setCurrentStep( currentStep + 1 )
188+
setTimeout( () => {
189+
setForceRefresh( forceRefresh + 1 )
190+
}, 50 )
191+
}
192+
} }
193+
>
194+
{ currentStep === steps.length - 1 ? __( 'Finish', i18n ) : __( 'Next', i18n ) }
195+
</Button>
196+
) }
197+
</Flex>
198+
</Modal>
199+
)
200+
}
201+
202+
export default ModalTour
203+
204+
const Steps = props => {
205+
const {
206+
numSteps = 3,
207+
currentStep = 0,
208+
// onClickStep = NOOP,
209+
} = props
210+
211+
return (
212+
<div className="ugb-tour-modal__steps">
213+
{ Array.from( { length: numSteps } ).map( ( _, index ) => {
214+
const classes = classNames( [
215+
'ugb-tour-modal__step',
216+
currentStep === index && 'ugb-tour-modal__step--active',
217+
] )
218+
219+
return (
220+
<div
221+
className={ classes }
222+
// onClick={ () => onClickStep( index ) }
223+
key={ index }
224+
/>
225+
)
226+
} ) }
227+
</div>
228+
)
229+
}

0 commit comments

Comments
 (0)