@dhis2-ui/popper 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/accepts_different_reference_types/index.js +58 -0
- package/src/features/accepts_different_reference_types.feature +15 -0
- package/src/features/positions/index.js +131 -0
- package/src/features/positions.feature +63 -0
- package/src/get-reference-element.js +16 -0
- package/src/index.js +4 -0
- package/src/modifiers.js +86 -0
- package/src/popper.e2e.stories.js +123 -0
- package/src/popper.js +101 -0
- package/src/popper.prod.stories.js +162 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dhis2-ui/popper",
|
|
3
|
-
"version": "10.16.
|
|
3
|
+
"version": "10.16.3",
|
|
4
4
|
"description": "UI Popper",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
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
|
"@popperjs/core": "^2.11.8",
|
|
38
38
|
"classnames": "^2.3.1",
|
|
39
39
|
"prop-types": "^15.7.2",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"build",
|
|
45
|
-
"types"
|
|
45
|
+
"types",
|
|
46
|
+
"src"
|
|
46
47
|
],
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"react": "^18.3.1",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
Given(
|
|
3
|
+
'a Popper with placement bottom-start has a React Ref as its reference',
|
|
4
|
+
() => {
|
|
5
|
+
cy.visitStory('Popper', 'React Ref As Reference')
|
|
6
|
+
}
|
|
7
|
+
)
|
|
8
|
+
Given(
|
|
9
|
+
'a Popper with placement bottom-start has a DOM node as its reference',
|
|
10
|
+
() => {
|
|
11
|
+
cy.visitStory('Popper', 'DOM Node As Reference')
|
|
12
|
+
}
|
|
13
|
+
)
|
|
14
|
+
Given(
|
|
15
|
+
'a Popper with placement bottom-start has a virtual element as its reference',
|
|
16
|
+
() => {
|
|
17
|
+
cy.visitStory('Popper', 'Virtual Element As Reference')
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
Then(
|
|
21
|
+
'the left of the popper is aligned with the left of the reference element',
|
|
22
|
+
() => {
|
|
23
|
+
cy.all(
|
|
24
|
+
() => cy.get('button:contains("Reference")'),
|
|
25
|
+
() => cy.get('[data-test="dhis2-uicore-popper"]')
|
|
26
|
+
).should(([$reference, $popper]) => {
|
|
27
|
+
const referenceRect = $reference.get(0).getBoundingClientRect()
|
|
28
|
+
const popperRect = $popper.get(0).getBoundingClientRect()
|
|
29
|
+
|
|
30
|
+
expect(referenceRect.left).to.be.closeTo(popperRect.left, 1)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
Then(
|
|
35
|
+
'the top of the popper is adjacent to the bottom of the reference element',
|
|
36
|
+
() => {
|
|
37
|
+
cy.all(
|
|
38
|
+
() => cy.get('button:contains("Reference")'),
|
|
39
|
+
() => cy.get('[data-test="dhis2-uicore-popper"]')
|
|
40
|
+
).should(([$button, $popper]) => {
|
|
41
|
+
const buttonRect = $button.get(0).getBoundingClientRect()
|
|
42
|
+
const popperRect = $popper.get(0).getBoundingClientRect()
|
|
43
|
+
|
|
44
|
+
expect(buttonRect.bottom).to.be.closeTo(popperRect.top, 1)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
Then(
|
|
49
|
+
'the top and left of the popper correspond with the virtualElement',
|
|
50
|
+
() => {
|
|
51
|
+
cy.get('[data-test="dhis2-uicore-popper"]').should(($popper) => {
|
|
52
|
+
const popperRect = $popper.get(0).getBoundingClientRect()
|
|
53
|
+
|
|
54
|
+
expect(popperRect.top).to.be.closeTo(200, 1)
|
|
55
|
+
expect(popperRect.left).to.be.closeTo(200, 1)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Feature: Accept different reference types
|
|
2
|
+
|
|
3
|
+
Scenario: Accepts a React Ref
|
|
4
|
+
Given a Popper with placement bottom-start has a React Ref as its reference
|
|
5
|
+
Then the top of the popper is adjacent to the bottom of the reference element
|
|
6
|
+
And the left of the popper is aligned with the left of the reference element
|
|
7
|
+
|
|
8
|
+
Scenario: Accepts a DOM node
|
|
9
|
+
Given a Popper with placement bottom-start has a DOM node as its reference
|
|
10
|
+
Then the top of the popper is adjacent to the bottom of the reference element
|
|
11
|
+
And the left of the popper is aligned with the left of the reference element
|
|
12
|
+
|
|
13
|
+
Scenario: Accepts a virtual element
|
|
14
|
+
Given a Popper with placement bottom-start has a virtual element as its reference
|
|
15
|
+
Then the top and left of the popper correspond with the virtualElement
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
// Visit stories with different placements
|
|
4
|
+
Given('the Popper is rendered with placement top', () => {
|
|
5
|
+
cy.visitStory('Popper', 'top')
|
|
6
|
+
})
|
|
7
|
+
Given('the Popper is rendered with placement top-start', () => {
|
|
8
|
+
cy.visitStory('Popper', 'top-start')
|
|
9
|
+
})
|
|
10
|
+
Given('the Popper is rendered with placement top-end', () => {
|
|
11
|
+
cy.visitStory('Popper', 'top-end')
|
|
12
|
+
})
|
|
13
|
+
Given('the Popper is rendered with placement right', () => {
|
|
14
|
+
cy.visitStory('Popper', 'right')
|
|
15
|
+
})
|
|
16
|
+
Given('the Popper is rendered with placement right-start', () => {
|
|
17
|
+
cy.visitStory('Popper', 'right-start')
|
|
18
|
+
})
|
|
19
|
+
Given('the Popper is rendered with placement right-end', () => {
|
|
20
|
+
cy.visitStory('Popper', 'right-end')
|
|
21
|
+
})
|
|
22
|
+
Given('the Popper is rendered with placement bottom', () => {
|
|
23
|
+
cy.visitStory('Popper', 'bottom')
|
|
24
|
+
})
|
|
25
|
+
Given('the Popper is rendered with placement bottom-start', () => {
|
|
26
|
+
cy.visitStory('Popper', 'bottom-start')
|
|
27
|
+
})
|
|
28
|
+
Given('the Popper is rendered with placement bottom-end', () => {
|
|
29
|
+
cy.visitStory('Popper', 'bottom-end')
|
|
30
|
+
})
|
|
31
|
+
Given('the Popper is rendered with placement left', () => {
|
|
32
|
+
cy.visitStory('Popper', 'left')
|
|
33
|
+
})
|
|
34
|
+
Given('the Popper is rendered with placement left-start', () => {
|
|
35
|
+
cy.visitStory('Popper', 'left-start')
|
|
36
|
+
})
|
|
37
|
+
Given('the Popper is rendered with placement left-end', () => {
|
|
38
|
+
cy.visitStory('Popper', 'left-end')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Directional assertions
|
|
42
|
+
// top
|
|
43
|
+
Then(
|
|
44
|
+
'the bottom of the popper is adjacent to the top of the reference element',
|
|
45
|
+
() => {
|
|
46
|
+
getRefAndPopperPositions().should(([refPos, popperPos]) => {
|
|
47
|
+
expect(refPos.top).to.equal(popperPos.bottom)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
// right
|
|
52
|
+
Then(
|
|
53
|
+
'the left of the popper is adjacent to the right of the reference element',
|
|
54
|
+
() => {
|
|
55
|
+
getRefAndPopperPositions().should(([refPos, popperPos]) => {
|
|
56
|
+
expect(refPos.right).to.equal(popperPos.left)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
// bottom
|
|
61
|
+
Then(
|
|
62
|
+
'the top of the popper is adjacent to the bottom of the reference element',
|
|
63
|
+
() => {
|
|
64
|
+
getRefAndPopperPositions().should(([refPos, popperPos]) => {
|
|
65
|
+
expect(refPos.bottom).to.equal(popperPos.top)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
// left
|
|
70
|
+
Then(
|
|
71
|
+
'the right of the popper is adjacent to the left of the reference element',
|
|
72
|
+
() => {
|
|
73
|
+
getRefAndPopperPositions().should(([refPos, popperPos]) => {
|
|
74
|
+
expect(refPos.left).to.equal(popperPos.right)
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// Horizontal alignments
|
|
80
|
+
// *-start
|
|
81
|
+
Then('it is horizontally left aligned with the reference element', () => {
|
|
82
|
+
getRefAndPopperPositions().should(([refPos, popperPos]) => {
|
|
83
|
+
expect(refPos.left).to.equal(popperPos.left)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
// * (no suffix)
|
|
87
|
+
Then('it is horizontally center aligned with the reference element', () => {
|
|
88
|
+
getRefAndPopperPositions().should(([refPos, popperPos]) => {
|
|
89
|
+
const refCenterX = refPos.left + refPos.width / 2
|
|
90
|
+
const popperCenterX = popperPos.left + popperPos.width / 2
|
|
91
|
+
|
|
92
|
+
expect(refCenterX).to.equal(popperCenterX)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
// *-end
|
|
96
|
+
Then('it is horizontally right aligned with the reference element', () => {
|
|
97
|
+
getRefAndPopperPositions().should(([refPos, popperPos]) => {
|
|
98
|
+
expect(refPos.right).to.equal(popperPos.right)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Vertical alignments
|
|
103
|
+
// *-start
|
|
104
|
+
Then('it is vertically top aligned with the reference element', () => {
|
|
105
|
+
getRefAndPopperPositions().should(([refPos, popperPos]) => {
|
|
106
|
+
expect(refPos.top).to.equal(popperPos.top)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
// * (no suffix)
|
|
110
|
+
Then('it is vertically center aligned with the reference element', () => {
|
|
111
|
+
getRefAndPopperPositions().should(([refPos, popperPos]) => {
|
|
112
|
+
const refCenterY = refPos.top + refPos.height / 2
|
|
113
|
+
const popperCenterY = popperPos.top + popperPos.height / 2
|
|
114
|
+
|
|
115
|
+
expect(refCenterY).to.equal(popperCenterY)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
// *-end
|
|
119
|
+
Then('it is vertically bottom aligned with the reference element', () => {
|
|
120
|
+
getRefAndPopperPositions().should(([refPos, popperPos]) => {
|
|
121
|
+
expect(refPos.bottom).to.equal(popperPos.bottom)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// helper
|
|
126
|
+
function getRefAndPopperPositions() {
|
|
127
|
+
return cy.getPositionsBySelectors(
|
|
128
|
+
'.reference-element',
|
|
129
|
+
'[data-test="dhis2-uicore-popper"]'
|
|
130
|
+
)
|
|
131
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Feature: Generic position options
|
|
2
|
+
|
|
3
|
+
# the definition of adjacent below: having a common endpoint or border
|
|
4
|
+
|
|
5
|
+
Scenario: Top position
|
|
6
|
+
Given the Popper is rendered with placement top
|
|
7
|
+
Then the bottom of the popper is adjacent to the top of the reference element
|
|
8
|
+
And it is horizontally center aligned with the reference element
|
|
9
|
+
|
|
10
|
+
Scenario: Top-start position
|
|
11
|
+
Given the Popper is rendered with placement top-start
|
|
12
|
+
Then the bottom of the popper is adjacent to the top of the reference element
|
|
13
|
+
And it is horizontally left aligned with the reference element
|
|
14
|
+
|
|
15
|
+
Scenario: Top-end position
|
|
16
|
+
Given the Popper is rendered with placement top-end
|
|
17
|
+
Then the bottom of the popper is adjacent to the top of the reference element
|
|
18
|
+
And it is horizontally right aligned with the reference element
|
|
19
|
+
|
|
20
|
+
Scenario: Right position
|
|
21
|
+
Given the Popper is rendered with placement right
|
|
22
|
+
Then the left of the popper is adjacent to the right of the reference element
|
|
23
|
+
And it is vertically center aligned with the reference element
|
|
24
|
+
|
|
25
|
+
Scenario: Right-start position
|
|
26
|
+
Given the Popper is rendered with placement right-start
|
|
27
|
+
Then the left of the popper is adjacent to the right of the reference element
|
|
28
|
+
And it is vertically top aligned with the reference element
|
|
29
|
+
|
|
30
|
+
Scenario: Right-end position
|
|
31
|
+
Given the Popper is rendered with placement right-end
|
|
32
|
+
Then the left of the popper is adjacent to the right of the reference element
|
|
33
|
+
And it is vertically bottom aligned with the reference element
|
|
34
|
+
|
|
35
|
+
Scenario: Bottom position
|
|
36
|
+
Given the Popper is rendered with placement bottom
|
|
37
|
+
Then the top of the popper is adjacent to the bottom of the reference element
|
|
38
|
+
And it is horizontally center aligned with the reference element
|
|
39
|
+
|
|
40
|
+
Scenario: Bottom-start position
|
|
41
|
+
Given the Popper is rendered with placement bottom-start
|
|
42
|
+
Then the top of the popper is adjacent to the bottom of the reference element
|
|
43
|
+
And it is horizontally left aligned with the reference element
|
|
44
|
+
|
|
45
|
+
Scenario: Bottom-end position
|
|
46
|
+
Given the Popper is rendered with placement bottom-end
|
|
47
|
+
Then the top of the popper is adjacent to the bottom of the reference element
|
|
48
|
+
And it is horizontally right aligned with the reference element
|
|
49
|
+
|
|
50
|
+
Scenario: Left position
|
|
51
|
+
Given the Popper is rendered with placement left
|
|
52
|
+
Then the right of the popper is adjacent to the left of the reference element
|
|
53
|
+
And it is vertically center aligned with the reference element
|
|
54
|
+
|
|
55
|
+
Scenario: Left-start position
|
|
56
|
+
Given the Popper is rendered with placement left-start
|
|
57
|
+
Then the right of the popper is adjacent to the left of the reference element
|
|
58
|
+
And it is vertically top aligned with the reference element
|
|
59
|
+
|
|
60
|
+
Scenario: Left-end position
|
|
61
|
+
Given the Popper is rendered with placement left-end
|
|
62
|
+
Then the right of the popper is adjacent to the left of the reference element
|
|
63
|
+
And it is vertically bottom aligned with the reference element
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const getReferenceElement = (reference) => {
|
|
2
|
+
// Elements or virtualElements
|
|
3
|
+
if (
|
|
4
|
+
reference instanceof Element ||
|
|
5
|
+
(reference && 'getBoundingClientRect' in reference)
|
|
6
|
+
) {
|
|
7
|
+
return reference
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// react refs
|
|
11
|
+
if (reference && 'current' in reference) {
|
|
12
|
+
return reference.current
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return null
|
|
16
|
+
}
|
package/src/index.js
ADDED
package/src/modifiers.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import ResizeObserver from 'resize-observer-polyfill'
|
|
2
|
+
|
|
3
|
+
const attachResizeObservers = ({
|
|
4
|
+
state: { elements },
|
|
5
|
+
options,
|
|
6
|
+
instance: { update },
|
|
7
|
+
}) => {
|
|
8
|
+
const observers = Object.keys(options).reduce((acc, elementKey) => {
|
|
9
|
+
if (options[elementKey]) {
|
|
10
|
+
const observer = new ResizeObserver(update)
|
|
11
|
+
observer.observe(elements[elementKey])
|
|
12
|
+
acc.push(observer)
|
|
13
|
+
}
|
|
14
|
+
return acc
|
|
15
|
+
}, [])
|
|
16
|
+
|
|
17
|
+
return () => {
|
|
18
|
+
observers.forEach((observer) => {
|
|
19
|
+
observer.disconnect()
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const getBaseModifiers = ({
|
|
25
|
+
observePopperResize,
|
|
26
|
+
observeReferenceResize,
|
|
27
|
+
}) => [
|
|
28
|
+
{
|
|
29
|
+
name: 'preventOverflow',
|
|
30
|
+
options: {
|
|
31
|
+
altAxis: true,
|
|
32
|
+
rootBoundary: 'document',
|
|
33
|
+
boundary: document.body,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'flip',
|
|
38
|
+
options: {
|
|
39
|
+
rootBoundary: 'document',
|
|
40
|
+
boundary: document.body,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'resizeObserver',
|
|
45
|
+
enabled: true,
|
|
46
|
+
phase: 'write',
|
|
47
|
+
fn: () => {},
|
|
48
|
+
effect: attachResizeObservers,
|
|
49
|
+
options: {
|
|
50
|
+
popper: observePopperResize,
|
|
51
|
+
reference: observeReferenceResize,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
export const deduplicateModifiers = (modifiers, resizeObservers) => {
|
|
57
|
+
// Deduplicate modifiers from props and baseModifiers,
|
|
58
|
+
// when duplicates are encountered (by name), use the
|
|
59
|
+
// modifier from props so each Popper can be fully custom
|
|
60
|
+
return getBaseModifiers(resizeObservers)
|
|
61
|
+
.filter(({ name }) => !modifiers.some((m) => m.name === name))
|
|
62
|
+
.concat(modifiers)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const resizeObserver = {
|
|
66
|
+
name: 'resizeObserver',
|
|
67
|
+
enabled: true,
|
|
68
|
+
phase: 'write',
|
|
69
|
+
fn: () => {},
|
|
70
|
+
effect: ({ state: { elements }, options, instance: { update } }) => {
|
|
71
|
+
const observers = Object.keys(options).reduce((acc, elementKey) => {
|
|
72
|
+
if (options[elementKey]) {
|
|
73
|
+
const observer = new ResizeObserver(update)
|
|
74
|
+
observer.observe(elements[elementKey])
|
|
75
|
+
acc.push(observer)
|
|
76
|
+
}
|
|
77
|
+
return acc
|
|
78
|
+
}, [])
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
observers.forEach((observer) => {
|
|
82
|
+
observer.disconnect()
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import PropTypes from 'prop-types'
|
|
2
|
+
import React, { useRef, useState } from 'react'
|
|
3
|
+
import { Popper } from './popper.js'
|
|
4
|
+
|
|
5
|
+
const PopperPlacement = ({ placement }) => {
|
|
6
|
+
const ref = useRef()
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="box">
|
|
10
|
+
<div className="reference-element" ref={ref}>
|
|
11
|
+
Reference element
|
|
12
|
+
</div>
|
|
13
|
+
<Popper reference={ref} placement={placement}>
|
|
14
|
+
<div className="popper-content">Popper</div>
|
|
15
|
+
</Popper>
|
|
16
|
+
<style jsx>{`
|
|
17
|
+
.box {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
width: 400px;
|
|
22
|
+
height: 400px;
|
|
23
|
+
margin-bottom: 1000px;
|
|
24
|
+
background-color: aliceblue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.reference-element {
|
|
28
|
+
width: 100px;
|
|
29
|
+
height: 50px;
|
|
30
|
+
background-color: cadetblue;
|
|
31
|
+
text-align: center;
|
|
32
|
+
padding: 6px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.popper-content {
|
|
36
|
+
width: 80px;
|
|
37
|
+
height: 30px;
|
|
38
|
+
background-color: lightblue;
|
|
39
|
+
text-align: center;
|
|
40
|
+
padding: 6px;
|
|
41
|
+
}
|
|
42
|
+
`}</style>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
PopperPlacement.propTypes = {
|
|
48
|
+
placement: PropTypes.string,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default {
|
|
52
|
+
title: 'Popper',
|
|
53
|
+
component: Popper,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const Top = () => <PopperPlacement placement="top" />
|
|
57
|
+
|
|
58
|
+
export const TopStart = () => <PopperPlacement placement="top-start" />
|
|
59
|
+
|
|
60
|
+
export const TopEnd = () => <PopperPlacement placement="top-end" />
|
|
61
|
+
|
|
62
|
+
export const Bottom = () => <PopperPlacement placement="bottom" />
|
|
63
|
+
|
|
64
|
+
export const BottomStart = () => <PopperPlacement placement="bottom-start" />
|
|
65
|
+
|
|
66
|
+
export const BottomEnd = () => <PopperPlacement placement="bottom-end" />
|
|
67
|
+
|
|
68
|
+
export const Right = () => <PopperPlacement placement="right" />
|
|
69
|
+
|
|
70
|
+
export const RightStart = () => <PopperPlacement placement="right-start" />
|
|
71
|
+
|
|
72
|
+
export const RightEnd = () => <PopperPlacement placement="right-end" />
|
|
73
|
+
|
|
74
|
+
export const Left = () => <PopperPlacement placement="left" />
|
|
75
|
+
|
|
76
|
+
export const LeftStart = () => <PopperPlacement placement="left-start" />
|
|
77
|
+
|
|
78
|
+
export const LeftEnd = () => <PopperPlacement placement="left-end" />
|
|
79
|
+
|
|
80
|
+
export const ReactRefAsReference = () => {
|
|
81
|
+
const ref = useRef()
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
<button ref={ref}>Reference</button>
|
|
86
|
+
<Popper placement="bottom-start" reference={ref}>
|
|
87
|
+
Popper
|
|
88
|
+
</Popper>
|
|
89
|
+
</>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const DOMNodeAsReference = () => {
|
|
94
|
+
const [node, setNode] = useState(null)
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
98
|
+
<button ref={setNode}>Reference</button>
|
|
99
|
+
<Popper placement="bottom-start" reference={node}>
|
|
100
|
+
Popper
|
|
101
|
+
</Popper>
|
|
102
|
+
</>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const VirtualElementAsReference = () => {
|
|
107
|
+
const virtualElement = {
|
|
108
|
+
getBoundingClientRect: () => ({
|
|
109
|
+
top: 200,
|
|
110
|
+
left: 200,
|
|
111
|
+
bottom: 'auto',
|
|
112
|
+
right: 'auto',
|
|
113
|
+
width: 0,
|
|
114
|
+
height: 0,
|
|
115
|
+
}),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<Popper placement="bottom-start" reference={virtualElement}>
|
|
120
|
+
Popper
|
|
121
|
+
</Popper>
|
|
122
|
+
)
|
|
123
|
+
}
|
package/src/popper.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { sharedPropTypes } from '@dhis2/ui-constants'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import React, { useState, useMemo, useEffect } from 'react'
|
|
4
|
+
import { usePopper } from 'react-popper'
|
|
5
|
+
import { getReferenceElement } from './get-reference-element.js'
|
|
6
|
+
import { deduplicateModifiers } from './modifiers.js'
|
|
7
|
+
|
|
8
|
+
const flipPlacement = (placement) => {
|
|
9
|
+
if (placement.startsWith('right')) {
|
|
10
|
+
return placement.replace('right', 'left')
|
|
11
|
+
}
|
|
12
|
+
if (placement.startsWith('left')) {
|
|
13
|
+
return placement.replace('left', 'right')
|
|
14
|
+
}
|
|
15
|
+
return placement
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Stable object reference
|
|
19
|
+
const staticArray = []
|
|
20
|
+
const Popper = ({
|
|
21
|
+
children,
|
|
22
|
+
className,
|
|
23
|
+
dataTest = 'dhis2-uicore-popper',
|
|
24
|
+
modifiers = staticArray,
|
|
25
|
+
observePopperResize,
|
|
26
|
+
observeReferenceResize,
|
|
27
|
+
onFirstUpdate,
|
|
28
|
+
placement = 'auto',
|
|
29
|
+
reference,
|
|
30
|
+
strategy,
|
|
31
|
+
}) => {
|
|
32
|
+
const referenceElement = getReferenceElement(reference)
|
|
33
|
+
const [popperElement, setPopperElement] = useState(null)
|
|
34
|
+
|
|
35
|
+
const deduplicatedModifiers = useMemo(
|
|
36
|
+
() =>
|
|
37
|
+
deduplicateModifiers(modifiers, {
|
|
38
|
+
observePopperResize,
|
|
39
|
+
observeReferenceResize,
|
|
40
|
+
}),
|
|
41
|
+
[modifiers, observePopperResize, observeReferenceResize]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
|
45
|
+
strategy,
|
|
46
|
+
onFirstUpdate,
|
|
47
|
+
placement:
|
|
48
|
+
document.documentElement.dir === 'rtl'
|
|
49
|
+
? flipPlacement(placement)
|
|
50
|
+
: placement,
|
|
51
|
+
modifiers: deduplicatedModifiers,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (popperElement) {
|
|
56
|
+
popperElement?.firstElementChild?.focus()
|
|
57
|
+
}
|
|
58
|
+
}, [popperElement])
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
className={className}
|
|
63
|
+
data-test={dataTest}
|
|
64
|
+
ref={setPopperElement}
|
|
65
|
+
style={styles.popper}
|
|
66
|
+
{...attributes.popper}
|
|
67
|
+
tabIndex={0}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Prop names follow the names here: https://popper.js.org/docs/v2/constructors/
|
|
75
|
+
Popper.propTypes = {
|
|
76
|
+
/** Content inside the Popper */
|
|
77
|
+
children: PropTypes.node.isRequired,
|
|
78
|
+
className: PropTypes.string,
|
|
79
|
+
dataTest: PropTypes.string,
|
|
80
|
+
/** A property of the `createPopper` options. See [popper docs](https://popper.js.org/docs/v2/constructors/) */
|
|
81
|
+
modifiers: PropTypes.arrayOf(
|
|
82
|
+
PropTypes.shape({
|
|
83
|
+
name: PropTypes.string,
|
|
84
|
+
options: PropTypes.object,
|
|
85
|
+
})
|
|
86
|
+
),
|
|
87
|
+
/** Makes the Popper update position when the **Popper content** changes size */
|
|
88
|
+
observePopperResize: PropTypes.bool,
|
|
89
|
+
/** Makes the Popper update position when the **reference element** changes size */
|
|
90
|
+
observeReferenceResize: PropTypes.bool,
|
|
91
|
+
/** A property of the `createPopper` options. See [popper docs](https://popper.js.org/docs/v2/constructors/) */
|
|
92
|
+
placement: sharedPropTypes.popperPlacementPropType,
|
|
93
|
+
/** A React ref, DOM node, or [virtual element](https://popper.js.org/docs/v2/virtual-elements/) for the popper to position itself against */
|
|
94
|
+
reference: sharedPropTypes.popperReferencePropType,
|
|
95
|
+
/** A property of the `createPopper` options. See [popper docs](https://popper.js.org/docs/v2/constructors/) */
|
|
96
|
+
strategy: PropTypes.oneOf(['absolute', 'fixed']), // defaults to 'absolute'
|
|
97
|
+
/** A property of the `createPopper` options. See [popper docs](https://popper.js.org/docs/v2/constructors/) */
|
|
98
|
+
onFirstUpdate: PropTypes.func,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { Popper }
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { sharedPropTypes } from '@dhis2/ui-constants'
|
|
2
|
+
import React, { useEffect, useRef } from 'react'
|
|
3
|
+
import { Popper } from './popper.js'
|
|
4
|
+
|
|
5
|
+
const description = `
|
|
6
|
+
A tool for adding additional information or content outside of the document flow, used for example in the Tooltip or Popover components.
|
|
7
|
+
|
|
8
|
+
Since it's built using [Popper.js](https://popper.js.org/docs/v2/) and [react-popper](https://popper.js.org/react-popper/), some of that functionality can be accessed through the props of this component, like modifiers.
|
|
9
|
+
|
|
10
|
+
\`\`\`js
|
|
11
|
+
import { Popper } from '@dhis2/ui'
|
|
12
|
+
\`\`\`
|
|
13
|
+
|
|
14
|
+
_**Note**: Some of the stories may not look right on this page. View those examples in the 'Canvas' tab instead._
|
|
15
|
+
`
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
title: 'Popper',
|
|
19
|
+
component: Popper,
|
|
20
|
+
parameters: { docs: { description: { component: description } } },
|
|
21
|
+
argTypes: {
|
|
22
|
+
placement: { ...sharedPropTypes.popperPlacementArgType },
|
|
23
|
+
reference: { ...sharedPropTypes.popperReferenceArgType },
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const boxStyle = {
|
|
28
|
+
display: 'flex',
|
|
29
|
+
alignItems: 'center',
|
|
30
|
+
justifyContent: 'center',
|
|
31
|
+
width: 400,
|
|
32
|
+
height: 400,
|
|
33
|
+
backgroundColor: 'aliceblue',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const referenceElementStyle = {
|
|
37
|
+
width: 130,
|
|
38
|
+
height: 50,
|
|
39
|
+
backgroundColor: 'cadetblue',
|
|
40
|
+
textAlign: 'center',
|
|
41
|
+
padding: 6,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const popperStyle = {
|
|
45
|
+
width: 110,
|
|
46
|
+
height: 30,
|
|
47
|
+
backgroundColor: 'lightblue',
|
|
48
|
+
textAlign: 'center',
|
|
49
|
+
padding: 6,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const Template = (args) => {
|
|
53
|
+
const ref = useRef(null)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="box" style={boxStyle}>
|
|
57
|
+
<div
|
|
58
|
+
className="reference-element"
|
|
59
|
+
style={referenceElementStyle}
|
|
60
|
+
ref={ref}
|
|
61
|
+
>
|
|
62
|
+
Reference Element
|
|
63
|
+
</div>
|
|
64
|
+
<Popper {...args} reference={ref}>
|
|
65
|
+
<div style={popperStyle}>{args.placement}</div>
|
|
66
|
+
</Popper>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const Top = Template.bind({})
|
|
72
|
+
Top.args = { placement: 'top' }
|
|
73
|
+
|
|
74
|
+
export const TopStart = Template.bind({})
|
|
75
|
+
TopStart.args = { placement: 'top-start' }
|
|
76
|
+
|
|
77
|
+
export const TopEnd = Template.bind({})
|
|
78
|
+
TopEnd.args = { placement: 'top-end' }
|
|
79
|
+
|
|
80
|
+
export const Bottom = Template.bind({})
|
|
81
|
+
Bottom.args = { placement: 'bottom' }
|
|
82
|
+
|
|
83
|
+
export const BottomStart = Template.bind({})
|
|
84
|
+
BottomStart.args = { placement: 'bottom-start' }
|
|
85
|
+
|
|
86
|
+
export const BottomEnd = Template.bind({})
|
|
87
|
+
BottomEnd.args = { placement: 'bottom-end' }
|
|
88
|
+
|
|
89
|
+
export const Right = Template.bind({})
|
|
90
|
+
Right.args = { placement: 'right' }
|
|
91
|
+
|
|
92
|
+
export const RightStart = Template.bind({})
|
|
93
|
+
RightStart.args = { placement: 'right-start' }
|
|
94
|
+
|
|
95
|
+
export const RightEnd = Template.bind({})
|
|
96
|
+
RightEnd.args = { placement: 'right-end' }
|
|
97
|
+
|
|
98
|
+
export const Left = Template.bind({})
|
|
99
|
+
Left.args = { placement: 'left' }
|
|
100
|
+
|
|
101
|
+
export const LeftStart = Template.bind({})
|
|
102
|
+
LeftStart.args = { placement: 'left-start' }
|
|
103
|
+
|
|
104
|
+
export const LeftEnd = Template.bind({})
|
|
105
|
+
LeftEnd.args = { placement: 'left-end' }
|
|
106
|
+
|
|
107
|
+
export const ElementRef = (args) => {
|
|
108
|
+
const anchor = document.createElement('div')
|
|
109
|
+
document.body.appendChild(anchor)
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="box" style={{ ...boxStyle, marginBottom: '500px' }}>
|
|
113
|
+
<Popper {...args} reference={anchor}>
|
|
114
|
+
<div style={popperStyle}>{args.placement}</div>
|
|
115
|
+
</Popper>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
ElementRef.args = { placement: 'left-end' }
|
|
120
|
+
ElementRef.parameters = { docs: { source: { type: 'code' } } }
|
|
121
|
+
|
|
122
|
+
export const VirtualElementRef = (args) => {
|
|
123
|
+
const virtualElement = {
|
|
124
|
+
getBoundingClientRect: () => ({
|
|
125
|
+
width: 0,
|
|
126
|
+
height: 0,
|
|
127
|
+
top: 100,
|
|
128
|
+
right: 0,
|
|
129
|
+
bottom: 0,
|
|
130
|
+
left: 200,
|
|
131
|
+
x: 200,
|
|
132
|
+
y: 100,
|
|
133
|
+
}),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="box" style={{ ...boxStyle, marginBottom: '500px' }}>
|
|
138
|
+
<Popper {...args} reference={virtualElement}>
|
|
139
|
+
<div style={popperStyle}>{args.placement}</div>
|
|
140
|
+
</Popper>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
VirtualElementRef.args = { placement: 'left-end' }
|
|
145
|
+
VirtualElementRef.parameters = { docs: { source: { type: 'code' } } }
|
|
146
|
+
|
|
147
|
+
export const RTL = (args) => {
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
document.documentElement.setAttribute('dir', 'rtl')
|
|
150
|
+
return () => {
|
|
151
|
+
document.documentElement.setAttribute('dir', 'ltr')
|
|
152
|
+
}
|
|
153
|
+
}, [])
|
|
154
|
+
return (
|
|
155
|
+
<div dir="rtl">
|
|
156
|
+
<span>If dir=rtl, `left` and `right` placement are reversed</span>
|
|
157
|
+
<Template {...args} placement="left" />
|
|
158
|
+
<br />
|
|
159
|
+
<Template {...args} placement="right-start" />
|
|
160
|
+
</div>
|
|
161
|
+
)
|
|
162
|
+
}
|