Skip to content

Commit f97a323

Browse files
Merge pull request #266 from qboot/feature/observables
Adding new step props: observables + highlight selectors
2 parents 5077817 + 344f138 commit f97a323

File tree

10 files changed

+376
-33
lines changed

10 files changed

+376
-33
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@ steps: PropTypes.arrayOf(PropTypes.shape({
337337
'stepInteraction': PropTypes.bool,
338338
'navDotAriaLabel': PropTypes.string,
339339
'observe': PropTypes.string,
340+
'highlightedSelectors': PropTypes.array,
341+
'mutationObservables': PropTypes.array,
342+
'resizeObservables': PropTypes.array,
340343
})),
341344
```
342345
@@ -374,6 +377,19 @@ const steps = [
374377
// If a child is added: the highlighted region is redrawn focused on it
375378
// If a child is removed: the highlighted region is redrawn focused on the step selector node
376379
observe: '[data-tour="observable-parent"]',
380+
// Array of selectors, each selected node will be included (by union)
381+
// in the highlighted region of the mask. You don't need to add the
382+
// step selector here as the default highlighted region is focused on it
383+
highlightedSelectors: ['[data-tour="highlighted-element"]'],
384+
// Array of selectors, each selected node DOM addition/removal will triggered a rerender
385+
// of the mask shape. Useful in combinaison with highlightedSelectors when highlighted
386+
// region of mask should be redrawn after a user action
387+
mutationObservables: ['[data-tour="mutable-element"]'],
388+
// Array of selectors, each selected node resize will triggered a rerender of the mask shape.
389+
// Useful in combinaison with highlightedSelectors when highlighted region of mask should
390+
// be redrawn after a user action. You should also add the selector in mutationObservables
391+
// if you want to track DOM addition/removal too
392+
resizeObservables: ['[data-tour="resizable-parent"]'],
377393
},
378394
// ...
379395
]

src/Tour.js

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
Navigation,
1515
Dot,
1616
SvgMask,
17+
ResizeObserver as ReactourResizeObserver,
18+
MutationObserver as ReactourMutationObserver,
1719
} from './components/index'
1820
import Portal from './Portal'
1921
import * as hx from './helpers'
@@ -153,20 +155,15 @@ class Tour extends Component {
153155
) {
154156
const cb = () => stepCallback(mutation.addedNodes[0])
155157
setTimeout(
156-
() =>
157-
this.calculateNode(
158-
mutation.addedNodes[0],
159-
step.position,
160-
cb
161-
),
158+
() => this.calculateNode(mutation.addedNodes[0], step, cb),
162159
100
163160
)
164161
} else if (
165162
mutation.type === 'childList' &&
166163
mutation.removedNodes.length > 0
167164
) {
168165
const cb = () => stepCallback(node)
169-
this.calculateNode(node, step.position, cb)
166+
this.calculateNode(node, step, cb)
170167
}
171168
})
172169
}),
@@ -185,12 +182,9 @@ class Tour extends Component {
185182

186183
if (node) {
187184
const cb = () => stepCallback(node)
188-
this.calculateNode(node, step.position, cb)
185+
this.calculateNode(node, step, cb)
189186
} else {
190-
this.setState(
191-
setNodeState(null, this.helper.current, step.position),
192-
stepCallback
193-
)
187+
this.setState(setNodeState(null, step, this.helper.current), stepCallback)
194188

195189
step.selector &&
196190
console.warn(
@@ -199,9 +193,9 @@ class Tour extends Component {
199193
}
200194
}
201195

202-
calculateNode = (node, stepPosition, cb) => {
196+
calculateNode = (node, step, cb) => {
203197
const { scrollDuration, inViewThreshold, scrollOffset } = this.props
204-
const attrs = hx.getNodeRect(node)
198+
const attrs = hx.getHighlightedRect(node, step)
205199
const w = Math.max(
206200
document.documentElement.clientWidth,
207201
window.innerWidth || 0
@@ -222,14 +216,25 @@ class Tour extends Component {
222216
duration: scrollDuration,
223217
offset,
224218
callback: nd => {
225-
this.setState(setNodeState(nd, this.helper.current, stepPosition), cb)
219+
this.setState(setNodeState(nd, step, this.helper.current), cb)
226220
},
227221
})
228222
} else {
229-
this.setState(setNodeState(node, this.helper.current, stepPosition), cb)
223+
this.setState(setNodeState(node, step, this.helper.current), cb)
230224
}
231225
}
232226

227+
recalculateNode = step => {
228+
const node = document.querySelector(step.selector)
229+
const stepCallback = o => {
230+
if (step.action && typeof step.action === 'function') {
231+
this.unlockFocus(() => step.action(o))
232+
}
233+
}
234+
235+
this.calculateNode(node, step, () => stepCallback(node))
236+
}
237+
233238
close() {
234239
this.setState(prevState => {
235240
if (prevState.observer) {
@@ -401,6 +406,14 @@ class Tour extends Component {
401406
return (
402407
<Portal>
403408
<GlobalStyle />
409+
<ReactourResizeObserver
410+
step={steps[current]}
411+
refresh={() => this.recalculateNode(steps[current])}
412+
/>
413+
<ReactourMutationObserver
414+
step={steps[current]}
415+
refresh={() => this.recalculateNode(steps[current])}
416+
/>
404417
<SvgMask
405418
onClick={this.maskClickHandler}
406419
forwardRef={c => (this.mask = c)}
@@ -567,7 +580,7 @@ class Tour extends Component {
567580
}
568581
}
569582

570-
const setNodeState = (node, helper, position) => {
583+
const setNodeState = (node, step, helper) => {
571584
const w = Math.max(
572585
document.documentElement.clientWidth,
573586
window.innerWidth || 0
@@ -578,26 +591,29 @@ const setNodeState = (node, helper, position) => {
578591
)
579592
const { width: helperWidth, height: helperHeight } = hx.getNodeRect(helper)
580593

581-
const attrs = node
582-
? hx.getNodeRect(node)
583-
: {
584-
top: h + 10,
585-
right: w / 2 + 9,
586-
bottom: h / 2 + 9,
587-
left: w / 2 - helperWidth / 2,
588-
width: 0,
589-
height: 0,
590-
w,
591-
h,
592-
helperPosition: 'center',
593-
}
594+
let attrs = {
595+
top: h + 10,
596+
right: w / 2 + 9,
597+
bottom: h / 2 + 9,
598+
left: w / 2 - helperWidth / 2,
599+
width: 0,
600+
height: 0,
601+
w,
602+
h,
603+
helperPosition: 'center',
604+
}
605+
606+
if (node) {
607+
attrs = hx.getHighlightedRect(node, step)
608+
}
609+
594610
return function update() {
595611
return {
596612
w,
597613
h,
598614
helperWidth,
599615
helperHeight,
600-
helperPosition: position,
616+
helperPosition: step.position,
601617
...attrs,
602618
inDOM: node ? true : false,
603619
}

src/components/MutationObserver.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useEffect } from 'react'
2+
3+
export default ({ step, refresh }) => {
4+
useEffect(() => {
5+
if (!step.mutationObservables) {
6+
return
7+
}
8+
9+
const refreshHighlightedRegionIfObservable = nodes => {
10+
for (const node of nodes) {
11+
if (!node.attributes) {
12+
continue
13+
}
14+
15+
const found = step.mutationObservables.find(observable =>
16+
node.matches(observable)
17+
)
18+
19+
if (found) {
20+
refresh()
21+
}
22+
}
23+
}
24+
25+
const mutationObserver = new MutationObserver(mutationsList => {
26+
for (const mutation of mutationsList) {
27+
if (0 !== mutation.addedNodes.length) {
28+
refreshHighlightedRegionIfObservable(mutation.addedNodes)
29+
}
30+
31+
if (0 !== mutation.removedNodes.length) {
32+
refreshHighlightedRegionIfObservable(mutation.removedNodes)
33+
}
34+
}
35+
})
36+
37+
const observable = document.documentElement || document.body
38+
const config = { childList: true, subtree: true }
39+
40+
mutationObserver.observe(observable, config)
41+
42+
return () => {
43+
mutationObserver.disconnect()
44+
}
45+
}, [step])
46+
47+
return null
48+
}

src/components/ResizeObserver.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useEffect, useState } from 'react'
2+
3+
export default ({ step, refresh }) => {
4+
const [mutationsCounter, setMutationsCounter] = useState(0)
5+
6+
// only use to notify main logic below
7+
// that a resizeObservable has been added to DOM (or removed from it)
8+
useEffect(() => {
9+
if (!step.resizeObservables) {
10+
return
11+
}
12+
13+
const incrementMutationsCounterIfObservable = nodes => {
14+
for (const node of nodes) {
15+
if (!node.attributes) {
16+
continue
17+
}
18+
19+
const found = step.resizeObservables.find(observable =>
20+
node.matches(observable)
21+
)
22+
23+
if (found) {
24+
setMutationsCounter(mutationsCounter + 1)
25+
}
26+
}
27+
}
28+
29+
const mutationObserver = new MutationObserver(mutationsList => {
30+
for (const mutation of mutationsList) {
31+
if (0 !== mutation.addedNodes.length) {
32+
incrementMutationsCounterIfObservable(mutation.addedNodes)
33+
}
34+
35+
if (0 !== mutation.removedNodes.length) {
36+
incrementMutationsCounterIfObservable(mutation.removedNodes)
37+
}
38+
}
39+
})
40+
41+
const observable = document.documentElement || document.body
42+
const config = { childList: true, subtree: true }
43+
44+
mutationObserver.observe(observable, config)
45+
46+
return () => {
47+
mutationObserver.disconnect()
48+
}
49+
}, [step, mutationsCounter])
50+
51+
// the main logic is here
52+
useEffect(() => {
53+
if (!step.resizeObservables) {
54+
return
55+
}
56+
57+
const resizeObserver = new ResizeObserver(entries => {
58+
refresh()
59+
})
60+
61+
for (const observable of step.resizeObservables) {
62+
const element = document.querySelector(observable)
63+
64+
if (element) {
65+
resizeObserver.observe(element)
66+
}
67+
}
68+
69+
return () => {
70+
resizeObserver.disconnect()
71+
}
72+
}, [step, mutationsCounter])
73+
74+
return null
75+
}

src/components/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ export { default as Controls } from './Controls'
66
export { default as Navigation } from './Navigation'
77
export { default as Dot } from './Dot'
88
export { default as SvgMask } from './SvgMask'
9+
export { default as ResizeObserver } from './ResizeObserver'
10+
export { default as MutationObserver } from './MutationObserver'

src/demo/App.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,14 @@ const tourConfig = [
246246
observe: '[data-tut="reactour__state--observe"]',
247247
action: node => node.focus(),
248248
},
249+
{
250+
selector: '[data-tut="reactour__highlighted"]',
251+
content:
252+
'Moreover you can highlight multiple elements and adjust highlighted region depending on DOM resizes and mutations. Try clicking the "?" tooltip and playing with tabs...',
253+
highlightedSelectors: ['[data-tut="reactour__highlighted-absolute-child"]'],
254+
mutationObservables: ['[data-tut="reactour__highlighted-absolute-child"]'],
255+
resizeObservables: ['[data-tut="reactour__highlighted-absolute-child"]'],
256+
},
249257
]
250258

251259
export default App

src/demo/Demo.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react'
2+
import PropTypes from 'prop-types'
23
import Section from './Section'
34
import Logo from './Logo'
45
import Text from './Text'
@@ -9,7 +10,8 @@ import Scrollable from './Scrollable'
910
import Footer from './Footer'
1011
import Image from './Image'
1112
import { Button, Link } from './Button'
12-
import PropTypes from 'prop-types'
13+
import Dropdown from './Dropdown'
14+
import Tabs from './Tabs'
1315

1416
export default function Demo({ openTour, isShowingMore, toggleShowMore }) {
1517
return (
@@ -168,7 +170,38 @@ export default function Demo({ openTour, isShowingMore, toggleShowMore }) {
168170

169171
<Row>
170172
<Box align="right">
171-
<Heading h="2">109 Baptist St</Heading>
173+
<Heading
174+
h="2"
175+
style={{
176+
display: 'flex',
177+
alignItems: 'center',
178+
justifyContent: 'flex-end',
179+
}}
180+
data-tut="reactour__highlighted"
181+
>
182+
109 Baptist St
183+
<Dropdown>
184+
<Tabs>
185+
<Tabs.Tab>
186+
This is a div in absolute position which will be in the
187+
highlighted region of the mask when tour is running.
188+
</Tabs.Tab>
189+
<Tabs.Tab>
190+
This is a long text which demonstrates how resizeObservables
191+
works. Lorem ipsum dolor sit amet, consectetur adipisicing
192+
elit. Optio neque vero consequuntur recusandae, dolore. Aut
193+
molestiae error enim illum odio vero sunt laborum
194+
consectetur minus deleniti pariatur eos quos, earum tenetur
195+
architecto veniam voluptatum sit! Optio similique ducimus
196+
esse vel inventore eaque earum adipisci quo, sit illum
197+
reprehenderit? Fugiat rerum inventore commodi dolores nisi
198+
soluta, nulla velit omnis! Quisquam est illo deserunt.
199+
Consequatur modi voluptatem consectetur nesciunt, eligendi,
200+
natus animi.
201+
</Tabs.Tab>
202+
</Tabs>
203+
</Dropdown>
204+
</Heading>
172205
<Text>
173206
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Optio
174207
neque vero consequuntur recusandae, dolore. Aut molestiae error

0 commit comments

Comments
 (0)