@dhis2-ui/intersection-detector 10.16.2 → 10.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/src/features/Visibility_notification/index.js +48 -0
- package/src/features/Visibility_notification.feature +13 -0
- package/src/index.js +1 -0
- package/src/intersection-detector.e2e.stories.js +61 -0
- package/src/intersection-detector.js +97 -0
- package/src/intersection-detector.prod.stories.js +121 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dhis2-ui/intersection-detector",
|
|
3
|
-
"version": "10.16.
|
|
3
|
+
"version": "10.16.3",
|
|
4
4
|
"description": "UI IntersectionDetector",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,13 +33,14 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@dhis2/prop-types": "^3.1.2",
|
|
36
|
-
"@dhis2/ui-constants": "10.16.
|
|
36
|
+
"@dhis2/ui-constants": "10.16.3",
|
|
37
37
|
"classnames": "^2.3.1",
|
|
38
38
|
"prop-types": "^15.7.2"
|
|
39
39
|
},
|
|
40
40
|
"files": [
|
|
41
41
|
"build",
|
|
42
|
-
"types"
|
|
42
|
+
"types",
|
|
43
|
+
"src"
|
|
43
44
|
],
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"react": "^18.3.1",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('the detector is not intersecting with the root', () => {
|
|
4
|
+
cy.visitStory('IntersectionDetector', 'Out Of View')
|
|
5
|
+
cy.window().should((win) => expect(win.onChange).to.not.be.undefined)
|
|
6
|
+
cy.window()
|
|
7
|
+
.its('onChange')
|
|
8
|
+
.as('onChangeStub', { static: true })
|
|
9
|
+
.should('be.calledOnce')
|
|
10
|
+
.should('be.calledWith', { isIntersecting: false })
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
Given('the detector is intersecting with the root', () => {
|
|
14
|
+
cy.visitStory('IntersectionDetector', 'In View')
|
|
15
|
+
cy.window().should((win) => expect(win.onChange).to.not.be.undefined)
|
|
16
|
+
cy.window()
|
|
17
|
+
.its('onChange')
|
|
18
|
+
.as('onChangeStub', { static: true })
|
|
19
|
+
.should('be.calledOnce')
|
|
20
|
+
.should('be.calledWith', { isIntersecting: true })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
When('the user scrolls the detector into view', () => {
|
|
24
|
+
// using 311 to scroll 1px more than "just enough"
|
|
25
|
+
cy.get('[data-test="scroll-container"]').scrollTo(0, 311)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
When('the user scrolls the detector out of view', () => {
|
|
29
|
+
// Scroll 1px more than "just enough" to make sure
|
|
30
|
+
// the detector is completely hidden
|
|
31
|
+
cy.get('[data-test="scroll-container"]').scrollTo(0, 201)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
Then('the callback passed to onChange should be called', () => {
|
|
35
|
+
cy.get('@onChangeStub').should((stub) => {
|
|
36
|
+
expect(stub).to.be.calledTwice
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
Then('the isIntersecting prop of its payload should be true', () => {
|
|
41
|
+
cy.get('@onChangeStub').should('be.calledWith', { isIntersecting: true })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
Then('the isIntersecting prop of its payload should be false', () => {
|
|
45
|
+
cy.get('@onChangeStub').should('be.calledWith', {
|
|
46
|
+
isIntersecting: false,
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Feature: The IntersectionDetector notifies the consumer when becoming visible
|
|
2
|
+
|
|
3
|
+
Scenario: The user scrolls the IntersectionDetector into view
|
|
4
|
+
Given the detector is not intersecting with the root
|
|
5
|
+
When the user scrolls the detector into view
|
|
6
|
+
Then the callback passed to onChange should be called
|
|
7
|
+
And the isIntersecting prop of its payload should be true
|
|
8
|
+
|
|
9
|
+
Scenario: The user scrolls the IntersectionDetector out of view
|
|
10
|
+
Given the detector is intersecting with the root
|
|
11
|
+
When the user scrolls the detector out of view
|
|
12
|
+
Then the callback passed to onChange should be called
|
|
13
|
+
And the isIntersecting prop of its payload should be false
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { IntersectionDetector } from './intersection-detector.js'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { useRef } from 'react'
|
|
2
|
+
import { IntersectionDetector } from './intersection-detector.js'
|
|
3
|
+
|
|
4
|
+
window.onChange = window.Cypress ? window.Cypress.cy.stub() : () => null
|
|
5
|
+
|
|
6
|
+
export default { title: 'IntersectionDetector' }
|
|
7
|
+
|
|
8
|
+
export const OutOfView = () => {
|
|
9
|
+
const rootRef = useRef()
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
ref={rootRef}
|
|
14
|
+
data-test="scroll-container"
|
|
15
|
+
style={{ width: 200, height: 300, overflow: 'auto' }}
|
|
16
|
+
>
|
|
17
|
+
<div
|
|
18
|
+
// spacer to push indicator out of the intersecting area
|
|
19
|
+
style={{
|
|
20
|
+
width: 200,
|
|
21
|
+
height: 310,
|
|
22
|
+
overflow: 'auto',
|
|
23
|
+
background: 'blue',
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
|
|
27
|
+
<IntersectionDetector rootRef={rootRef} onChange={window.onChange}>
|
|
28
|
+
<div style={{ background: 'red', height: 200 }} />
|
|
29
|
+
</IntersectionDetector>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const InView = () => {
|
|
35
|
+
const rootRef = useRef()
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
ref={rootRef}
|
|
40
|
+
data-test="scroll-container"
|
|
41
|
+
style={{ width: 200, height: 300, overflow: 'auto' }}
|
|
42
|
+
>
|
|
43
|
+
<div style={{ background: 'red', height: 200 }}>
|
|
44
|
+
<IntersectionDetector
|
|
45
|
+
rootRef={rootRef}
|
|
46
|
+
onChange={window.onChange}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div
|
|
51
|
+
// spacer to push indicator out of the intersecting area
|
|
52
|
+
style={{
|
|
53
|
+
width: 200,
|
|
54
|
+
height: 310,
|
|
55
|
+
overflow: 'auto',
|
|
56
|
+
background: 'blue',
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import PropTypes from 'prop-types'
|
|
2
|
+
import React, { useEffect, useRef } from 'react'
|
|
3
|
+
|
|
4
|
+
export const IntersectionDetector = ({
|
|
5
|
+
threshold = 0,
|
|
6
|
+
onChange,
|
|
7
|
+
children,
|
|
8
|
+
className,
|
|
9
|
+
dataTest = 'dhis2-uicore-intersectiondetector',
|
|
10
|
+
rootRef,
|
|
11
|
+
}) => {
|
|
12
|
+
// Use useRef instead of useState to prevent unnecessary re-render:
|
|
13
|
+
// The state changes won't be reflected in what this component renders,
|
|
14
|
+
// so there's no need for re-rendering the (potentially computational
|
|
15
|
+
// heavy) children. Also: If the parent re-renders (e. g. due to a state
|
|
16
|
+
// change), then this component will re-render as well.
|
|
17
|
+
|
|
18
|
+
// @var {Object}
|
|
19
|
+
// @prop {bool} current
|
|
20
|
+
const isIntersecting = useRef()
|
|
21
|
+
|
|
22
|
+
// @var {Object}
|
|
23
|
+
// @prop {HTMLElement} current
|
|
24
|
+
const intersectionRef = useRef()
|
|
25
|
+
|
|
26
|
+
const onChangeRef = useRef(onChange)
|
|
27
|
+
onChangeRef.current = onChange
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const rootEl = rootRef.current
|
|
31
|
+
const intersectionEl = intersectionRef.current
|
|
32
|
+
|
|
33
|
+
if (rootEl && intersectionEl) {
|
|
34
|
+
const onIntersection = (entries) => {
|
|
35
|
+
// Currently there's no way to supply multiple thresholds,
|
|
36
|
+
// so a single entry can be assumed safely
|
|
37
|
+
const [entry] = entries
|
|
38
|
+
|
|
39
|
+
// Make sure the callback is not called multiple times
|
|
40
|
+
// if there is no change
|
|
41
|
+
const intersectionChange =
|
|
42
|
+
entry.isIntersecting !== isIntersecting.current
|
|
43
|
+
|
|
44
|
+
if (intersectionChange) {
|
|
45
|
+
isIntersecting.current = entry.isIntersecting
|
|
46
|
+
onChangeRef.current({
|
|
47
|
+
isIntersecting: entry.isIntersecting,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const observerOptions = { root: rootEl, threshold }
|
|
53
|
+
const intersectionObserver = new IntersectionObserver(
|
|
54
|
+
onIntersection,
|
|
55
|
+
observerOptions
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
intersectionObserver.observe(intersectionEl)
|
|
59
|
+
|
|
60
|
+
// Make sure to clean up everything when un-mounting.
|
|
61
|
+
// Using an arrow function instead of just returning
|
|
62
|
+
// the disconnect function for better readability.
|
|
63
|
+
return () => intersectionObserver.disconnect()
|
|
64
|
+
}
|
|
65
|
+
}, [rootRef, threshold])
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div ref={intersectionRef} className={className} data-test={dataTest}>
|
|
69
|
+
{children}
|
|
70
|
+
|
|
71
|
+
<style jsx>{`
|
|
72
|
+
// Enables compatibility with "position: absolute;" containers.
|
|
73
|
+
// If the parent does not have a height, this will have no
|
|
74
|
+
// effect, which makes it work with standard containers.
|
|
75
|
+
div {
|
|
76
|
+
height: 100%;
|
|
77
|
+
}
|
|
78
|
+
`}</style>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
IntersectionDetector.propTypes = {
|
|
84
|
+
/** React ref on other component to detect intersections with */
|
|
85
|
+
rootRef: PropTypes.shape({
|
|
86
|
+
// not required so `current` can be `null`
|
|
87
|
+
current: PropTypes.instanceOf(HTMLElement),
|
|
88
|
+
}).isRequired,
|
|
89
|
+
/** Called with signature `({ isIntersecting: bool })` */
|
|
90
|
+
onChange: PropTypes.func.isRequired,
|
|
91
|
+
|
|
92
|
+
children: PropTypes.any,
|
|
93
|
+
className: PropTypes.string,
|
|
94
|
+
dataTest: PropTypes.string,
|
|
95
|
+
/** The [threshold](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Intersection_observer_options) value: a value from 0.0 to 1.0 that controls the point at which an intersecting component is considered 'intersected' or 'visible' and the onChange callback triggers */
|
|
96
|
+
threshold: PropTypes.number,
|
|
97
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import PropTypes from 'prop-types'
|
|
2
|
+
import React, { useState, useRef } from 'react'
|
|
3
|
+
import { IntersectionDetector } from './intersection-detector.js'
|
|
4
|
+
|
|
5
|
+
const description = `
|
|
6
|
+
A tool to performantly detect when two components intersect, which is often better to use than scroll listeners. A common use case is to detect when a child component is in view inside a scrolling parent. Uses an [\`IntersectionObserver\`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) object internally.
|
|
7
|
+
|
|
8
|
+
See the code examples below for examples on how to implement it.
|
|
9
|
+
|
|
10
|
+
\`\`\`js
|
|
11
|
+
import { IntersectionDetector } from '@dhis2/ui'
|
|
12
|
+
\`\`\`
|
|
13
|
+
`
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
title: 'Intersection Observer',
|
|
17
|
+
component: IntersectionDetector,
|
|
18
|
+
parameters: {
|
|
19
|
+
docs: {
|
|
20
|
+
description: { component: description },
|
|
21
|
+
source: { type: 'code' },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
// Hide 'container styles' from Template in args table
|
|
25
|
+
argTypes: { containerStyles: { table: { disable: true } } },
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Text = () => (
|
|
29
|
+
<p>
|
|
30
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras tempor
|
|
31
|
+
venenatis hendrerit. Donec dictum sed ligula id efficitur. Suspendisse
|
|
32
|
+
feugiat, elit in dictum imperdiet, mi tellus euismod nibh, vitae
|
|
33
|
+
hendrerit turpis odio ut mauris. Vestibulum rhoncus interdum nunc eu
|
|
34
|
+
eleifend. Aenean viverra nibh hendrerit nulla iaculis, vitae tincidunt
|
|
35
|
+
erat ullamcorper. Donec tempus mattis faucibus. Donec nec lacus vitae
|
|
36
|
+
elit aliquet pharetra. Cras vitae odio eu lorem euismod malesuada. Nunc
|
|
37
|
+
eu rhoncus mauris. Nullam vehicula elit id vehicula maximus. Phasellus
|
|
38
|
+
gravida tincidunt mauris, vitae laoreet erat commodo id. Nullam vitae
|
|
39
|
+
erat ante. Proin id ultricies risus, in ultricies mauris. Vivamus lectus
|
|
40
|
+
enim, ultricies vel egestas nec, tempor a magna. Nam sed fermentum
|
|
41
|
+
ipsum, a ullamcorper felis. Aenean finibus erat elit, at eleifend nulla
|
|
42
|
+
rutrum at.
|
|
43
|
+
<style jsx>{`
|
|
44
|
+
p {
|
|
45
|
+
margin: 0;
|
|
46
|
+
}
|
|
47
|
+
`}</style>
|
|
48
|
+
</p>
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const Template = ({ containerStyles, ...args }) => {
|
|
52
|
+
const rootRef = useRef()
|
|
53
|
+
const [isIntersecting, setIsIntersecting] = useState(false)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="container">
|
|
57
|
+
<p>Is intersecting: {` ${isIntersecting}`}</p>
|
|
58
|
+
|
|
59
|
+
<div className="scrollContainer" ref={rootRef}>
|
|
60
|
+
<div className="contentContainer">
|
|
61
|
+
<Text />
|
|
62
|
+
|
|
63
|
+
<div style={containerStyles}>
|
|
64
|
+
<IntersectionDetector
|
|
65
|
+
{...args}
|
|
66
|
+
onChange={({ isIntersecting }) =>
|
|
67
|
+
setIsIntersecting(isIntersecting)
|
|
68
|
+
}
|
|
69
|
+
rootRef={rootRef}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<style jsx>{`
|
|
76
|
+
.scrollContainer {
|
|
77
|
+
width: 200px;
|
|
78
|
+
height: 300px;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
overflow-y: auto;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.contentContainer {
|
|
84
|
+
position: relative;
|
|
85
|
+
}
|
|
86
|
+
`}</style>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
Template.propTypes = { containerStyles: PropTypes.shape({}) }
|
|
91
|
+
|
|
92
|
+
const boxStyles = {
|
|
93
|
+
boxSizing: 'border-box',
|
|
94
|
+
border: '1px solid #f76a8c',
|
|
95
|
+
background: 'rgba(246,172,200,0.4)',
|
|
96
|
+
width: '100%',
|
|
97
|
+
height: '100px',
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const AbsoluteBottomArea = Template.bind({})
|
|
101
|
+
AbsoluteBottomArea.args = {
|
|
102
|
+
containerStyles: {
|
|
103
|
+
...boxStyles,
|
|
104
|
+
position: 'absolute',
|
|
105
|
+
bottom: '0',
|
|
106
|
+
left: '0',
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const AbsoluteTopArea = Template.bind({})
|
|
111
|
+
AbsoluteTopArea.args = {
|
|
112
|
+
containerStyles: {
|
|
113
|
+
...boxStyles,
|
|
114
|
+
position: 'absolute',
|
|
115
|
+
top: '0',
|
|
116
|
+
left: '0',
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const BottomArea = Template.bind({})
|
|
121
|
+
BottomArea.args = { containerStyles: { ...boxStyles } }
|