@dhis2-ui/intersection-detector 10.16.1 → 10.16.3-alpha.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhis2-ui/intersection-detector",
3
- "version": "10.16.1",
3
+ "version": "10.16.3-alpha.1",
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.1",
36
+ "@dhis2/ui-constants": "10.16.3-alpha.1",
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 } }