@dhis2-ui/popover 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 +6 -5
- package/src/arrow.js +87 -0
- package/src/features/arrow_position/index.js +121 -0
- package/src/features/arrow_position.feature +27 -0
- package/src/features/clicking_outside/index.js +16 -0
- package/src/features/clicking_outside.feature +6 -0
- package/src/features/conditional_arrow/index.js +14 -0
- package/src/features/conditional_arrow.feature +9 -0
- package/src/features/position/index.js +117 -0
- package/src/features/position.feature +35 -0
- package/src/get-arrow-position.js +19 -0
- package/src/index.js +1 -0
- package/src/modifiers.js +62 -0
- package/src/popover.e2e.stories.js +133 -0
- package/src/popover.js +92 -0
- package/src/popover.prod.stories.js +116 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dhis2-ui/popover",
|
|
3
|
-
"version": "10.16.1",
|
|
3
|
+
"version": "10.16.3-alpha.1",
|
|
4
4
|
"description": "UI Popover",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,15 +33,16 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@dhis2/prop-types": "^3.1.2",
|
|
36
|
-
"@dhis2-ui/layer": "10.16.1",
|
|
37
|
-
"@dhis2-ui/popper": "10.16.1",
|
|
38
|
-
"@dhis2/ui-constants": "10.16.1",
|
|
36
|
+
"@dhis2-ui/layer": "10.16.3-alpha.1",
|
|
37
|
+
"@dhis2-ui/popper": "10.16.3-alpha.1",
|
|
38
|
+
"@dhis2/ui-constants": "10.16.3-alpha.1",
|
|
39
39
|
"classnames": "^2.3.1",
|
|
40
40
|
"prop-types": "^15.7.2"
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"build",
|
|
44
|
-
"types"
|
|
44
|
+
"types",
|
|
45
|
+
"src"
|
|
45
46
|
],
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"react": "^18.3.1",
|
package/src/arrow.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { colors } from '@dhis2/ui-constants'
|
|
2
|
+
import cx from 'classnames'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import React, { forwardRef } from 'react'
|
|
5
|
+
import { getArrowPosition } from './get-arrow-position.js'
|
|
6
|
+
|
|
7
|
+
const ARROW_SIZE = 8
|
|
8
|
+
|
|
9
|
+
const Arrow = forwardRef(({ hidden, popperPlacement, styles }, ref) => (
|
|
10
|
+
<div
|
|
11
|
+
data-test="dhis2-uicore-popoverarrow"
|
|
12
|
+
className={cx(getArrowPosition(popperPlacement), { hidden })}
|
|
13
|
+
style={styles}
|
|
14
|
+
ref={ref}
|
|
15
|
+
>
|
|
16
|
+
<style jsx>{`
|
|
17
|
+
div {
|
|
18
|
+
width: ${ARROW_SIZE}px;
|
|
19
|
+
height: ${ARROW_SIZE}px;
|
|
20
|
+
position: absolute;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
div.top {
|
|
24
|
+
top: -${ARROW_SIZE / 2}px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
div.right {
|
|
28
|
+
right: -${ARROW_SIZE / 2}px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
div.bottom {
|
|
32
|
+
bottom: -${ARROW_SIZE / 2}px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
div.left {
|
|
36
|
+
left: -${ARROW_SIZE / 2}px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
div.hidden {
|
|
40
|
+
visibility: hidden;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
div::after {
|
|
44
|
+
content: '';
|
|
45
|
+
position: absolute;
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
border-style: solid;
|
|
49
|
+
border-width: ${ARROW_SIZE / 2}px;
|
|
50
|
+
border-color: transparent transparent ${colors.white}
|
|
51
|
+
${colors.white};
|
|
52
|
+
box-shadow: -1px 1px 1px 0 rgba(64, 75, 90, 0.08),
|
|
53
|
+
-3px 3px 8px -6px rgba(64, 75, 90, 0.15);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
div.bottom::after {
|
|
57
|
+
transform: rotate(-45deg);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
div.top::after {
|
|
61
|
+
transform: rotate(135deg);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
div.right::after {
|
|
65
|
+
transform: rotate(-135deg);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
div.left::after {
|
|
69
|
+
transform: rotate(45deg);
|
|
70
|
+
}
|
|
71
|
+
`}</style>
|
|
72
|
+
</div>
|
|
73
|
+
))
|
|
74
|
+
Arrow.displayName = 'Arrow'
|
|
75
|
+
|
|
76
|
+
Arrow.propTypes = {
|
|
77
|
+
hidden: PropTypes.bool,
|
|
78
|
+
popperPlacement: PropTypes.string,
|
|
79
|
+
styles: PropTypes.shape({
|
|
80
|
+
left: PropTypes.string,
|
|
81
|
+
position: PropTypes.string,
|
|
82
|
+
top: PropTypes.string,
|
|
83
|
+
transform: PropTypes.string,
|
|
84
|
+
}),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { Arrow, ARROW_SIZE }
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a Popover is rendered with placement top', () => {
|
|
4
|
+
cy.visitStory('Popover', 'Placement Top')
|
|
5
|
+
})
|
|
6
|
+
Given('a Popover is rendered with placement right', () => {
|
|
7
|
+
cy.visitStory('Popover', 'Placement Right')
|
|
8
|
+
})
|
|
9
|
+
Given('a Popover is rendered with placement bottom', () => {
|
|
10
|
+
cy.visitStory('Popover', 'Placement Bottom')
|
|
11
|
+
})
|
|
12
|
+
Given('a Popover is rendered with placement left', () => {
|
|
13
|
+
cy.visitStory('Popover', 'Placement Left')
|
|
14
|
+
})
|
|
15
|
+
Given('a Popover with position left is shifted into view', () => {
|
|
16
|
+
cy.visitStory('Popover', 'Shifted Arrow')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
Then('the Arrow is horizontally aligned with the Popper', () => {
|
|
20
|
+
cy.all(
|
|
21
|
+
() => cy.get('[data-test="dhis2-uicore-popover"]'),
|
|
22
|
+
() => cy.get('[data-test="dhis2-uicore-popoverarrow"]')
|
|
23
|
+
).should(([$popover, $arrow]) => {
|
|
24
|
+
const popoverRect = $popover.get(0).getBoundingClientRect()
|
|
25
|
+
const arrowRect = $arrow.get(0).getBoundingClientRect()
|
|
26
|
+
const popoverCenterX = popoverRect.left + popoverRect.width / 2
|
|
27
|
+
const arrowCenterX = arrowRect.left + arrowRect.width / 2
|
|
28
|
+
|
|
29
|
+
expect(popoverCenterX).to.equal(arrowCenterX)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
Then('the Arrow is vertically aligned with the Popper', () => {
|
|
33
|
+
cy.all(
|
|
34
|
+
() => cy.get('[data-test="dhis2-uicore-popover"]'),
|
|
35
|
+
() => cy.get('[data-test="dhis2-uicore-popoverarrow"]')
|
|
36
|
+
).should(([$popover, $arrow]) => {
|
|
37
|
+
const popoverRect = $popover.get(0).getBoundingClientRect()
|
|
38
|
+
const arrowRect = $arrow.get(0).getBoundingClientRect()
|
|
39
|
+
const popoverCenterY = popoverRect.top + popoverRect.height / 2
|
|
40
|
+
const arrowCenterY = arrowRect.top + arrowRect.height / 2
|
|
41
|
+
|
|
42
|
+
expect(popoverCenterY).to.equal(arrowCenterY)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
Then('the Arrow is vertically aligned with the reference element', () => {
|
|
47
|
+
cy.all(
|
|
48
|
+
() => cy.get('[data-test="reference-element"]'),
|
|
49
|
+
() => cy.get('[data-test="dhis2-uicore-popoverarrow"]')
|
|
50
|
+
).should(([$reference, $arrow]) => {
|
|
51
|
+
const referenceRect = $reference.get(0).getBoundingClientRect()
|
|
52
|
+
const arrowRect = $arrow.get(0).getBoundingClientRect()
|
|
53
|
+
const referenceCenterY = referenceRect.top + referenceRect.height / 2
|
|
54
|
+
const arrowCenterY = arrowRect.top + arrowRect.height / 2
|
|
55
|
+
|
|
56
|
+
expect(referenceCenterY).to.equal(arrowCenterY)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
Then('the Arrow is at the bottom of the Popper', () => {
|
|
61
|
+
cy.all(
|
|
62
|
+
() => cy.get('[data-test="dhis2-uicore-popover"]'),
|
|
63
|
+
() => cy.get('[data-test="dhis2-uicore-popoverarrow"]')
|
|
64
|
+
).should(([$popover, $arrow]) => {
|
|
65
|
+
const popoverRect = $popover.get(0).getBoundingClientRect()
|
|
66
|
+
const arrowRect = $arrow.get(0).getBoundingClientRect()
|
|
67
|
+
const arrowCenterY = arrowRect.top + arrowRect.height / 2
|
|
68
|
+
|
|
69
|
+
expect(popoverRect.bottom).to.equal(arrowCenterY)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
Then('the Arrow is at the left of the Popper', () => {
|
|
73
|
+
cy.all(
|
|
74
|
+
() => cy.get('[data-test="dhis2-uicore-popover"]'),
|
|
75
|
+
() => cy.get('[data-test="dhis2-uicore-popoverarrow"]')
|
|
76
|
+
).should(([$popover, $arrow]) => {
|
|
77
|
+
const popoverRect = $popover.get(0).getBoundingClientRect()
|
|
78
|
+
const arrowRect = $arrow.get(0).getBoundingClientRect()
|
|
79
|
+
const arrowCenterX = arrowRect.left + arrowRect.width / 2
|
|
80
|
+
|
|
81
|
+
expect(popoverRect.left).to.equal(arrowCenterX)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
Then('the Arrow is at the top of the Popper', () => {
|
|
85
|
+
cy.all(
|
|
86
|
+
() => cy.get('[data-test="dhis2-uicore-popover"]'),
|
|
87
|
+
() => cy.get('[data-test="dhis2-uicore-popoverarrow"]')
|
|
88
|
+
).should(([$popover, $arrow]) => {
|
|
89
|
+
const popoverRect = $popover.get(0).getBoundingClientRect()
|
|
90
|
+
const arrowRect = $arrow.get(0).getBoundingClientRect()
|
|
91
|
+
const arrowCenterY = arrowRect.top + arrowRect.height / 2
|
|
92
|
+
|
|
93
|
+
expect(popoverRect.top).to.equal(arrowCenterY)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
Then('the Arrow is at the right of the Popper', () => {
|
|
97
|
+
cy.all(
|
|
98
|
+
() => cy.get('[data-test="dhis2-uicore-popover"]'),
|
|
99
|
+
() => cy.get('[data-test="dhis2-uicore-popoverarrow"]')
|
|
100
|
+
).should(([$popover, $arrow]) => {
|
|
101
|
+
const popoverRect = $popover.get(0).getBoundingClientRect()
|
|
102
|
+
const arrowRect = $arrow.get(0).getBoundingClientRect()
|
|
103
|
+
const arrowCenterX = arrowRect.left + arrowRect.width / 2
|
|
104
|
+
|
|
105
|
+
expect(popoverRect.right).to.equal(arrowCenterX)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
Then('the Arrow is at the top half of the Popper', () => {
|
|
110
|
+
cy.all(
|
|
111
|
+
() => cy.get('[data-test="dhis2-uicore-popover"]'),
|
|
112
|
+
() => cy.get('[data-test="dhis2-uicore-popoverarrow"]')
|
|
113
|
+
).should(([$popover, $arrow]) => {
|
|
114
|
+
const popoverRect = $popover.get(0).getBoundingClientRect()
|
|
115
|
+
const arrowRect = $arrow.get(0).getBoundingClientRect()
|
|
116
|
+
const popoverCenterY = popoverRect.top + popoverRect.height / 2
|
|
117
|
+
const arrowCenterY = arrowRect.top + arrowRect.height / 2
|
|
118
|
+
|
|
119
|
+
expect(popoverCenterY).to.greaterThan(arrowCenterY)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Feature: Popover arrow positions
|
|
2
|
+
|
|
3
|
+
Scenario: The Arrow is at the bottom of a Popper with placement top
|
|
4
|
+
Given a Popover is rendered with placement top
|
|
5
|
+
Then the Arrow is at the bottom of the Popper
|
|
6
|
+
And the Arrow is horizontally aligned with the Popper
|
|
7
|
+
|
|
8
|
+
Scenario: The Arrow is at the left of a Popper with placement right
|
|
9
|
+
Given a Popover is rendered with placement right
|
|
10
|
+
Then the Arrow is at the left of the Popper
|
|
11
|
+
And the Arrow is vertically aligned with the Popper
|
|
12
|
+
|
|
13
|
+
Scenario: The Arrow is at the top of a Popper with placement bottom
|
|
14
|
+
Given a Popover is rendered with placement bottom
|
|
15
|
+
Then the Arrow is at the top of the Popper
|
|
16
|
+
And the Arrow is horizontally aligned with the Popper
|
|
17
|
+
|
|
18
|
+
Scenario: The Arrow is at the right of a Popper with placement left
|
|
19
|
+
Given a Popover is rendered with placement left
|
|
20
|
+
Then the Arrow is at the right of the Popper
|
|
21
|
+
And the Arrow is vertically aligned with the Popper
|
|
22
|
+
|
|
23
|
+
Scenario: The Arrow is shifted along the Popper to align with the reference element
|
|
24
|
+
Given a Popover with position left is shifted into view
|
|
25
|
+
Then the Arrow is at the right of the Popper
|
|
26
|
+
And the Arrow is at the top half of the Popper
|
|
27
|
+
And the Arrow is vertically aligned with the reference element
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a default Popper is rendered with arrow set to true', () => {
|
|
4
|
+
cy.visitStory('Popover', 'Default')
|
|
5
|
+
})
|
|
6
|
+
Given('a default Popover is rendered with an onClickOutside handler', () => {
|
|
7
|
+
cy.visitStory('Popover', 'With On Click Outside')
|
|
8
|
+
})
|
|
9
|
+
When('the user clicks outside of the Popover', () => {
|
|
10
|
+
cy.get('[data-test="dhis2-uicore-layer"]').click()
|
|
11
|
+
})
|
|
12
|
+
Then('the clickOutside handler is called', () => {
|
|
13
|
+
cy.window().should((win) => {
|
|
14
|
+
expect(win.onClickOutside).to.be.calledOnce
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
Given('a default Popper is rendered with arrow set to true', () => {
|
|
4
|
+
cy.visitStory('Popover', 'Default')
|
|
5
|
+
})
|
|
6
|
+
Given('a Popover is rendered with the arrow prop set to false', () => {
|
|
7
|
+
cy.visitStory('Popover', 'No Arrow')
|
|
8
|
+
})
|
|
9
|
+
Then('there is an arrow element in the Popover', () => {
|
|
10
|
+
cy.get('[data-test="dhis2-uicore-popoverarrow"]').should('exist')
|
|
11
|
+
})
|
|
12
|
+
Then('there is no arrow element in the Popover', () => {
|
|
13
|
+
cy.get('[data-test="dhis2-uicore-popoverarrow"]').should('not.exist')
|
|
14
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Feature: Popover conditional arrow
|
|
2
|
+
|
|
3
|
+
Scenario: With arrow
|
|
4
|
+
Given a default Popper is rendered with arrow set to true
|
|
5
|
+
Then there is an arrow element in the Popover
|
|
6
|
+
|
|
7
|
+
Scenario: Without arrow
|
|
8
|
+
Given a Popover is rendered with the arrow prop set to false
|
|
9
|
+
Then there is no arrow element in the Popover
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
|
|
2
|
+
|
|
3
|
+
const CLOSE_TO_DELTA = 1
|
|
4
|
+
|
|
5
|
+
// Stories
|
|
6
|
+
Given(
|
|
7
|
+
'there is sufficient space to place the Popover above the reference element',
|
|
8
|
+
() => {
|
|
9
|
+
cy.visitStory('Popover', 'Default')
|
|
10
|
+
}
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
Given(
|
|
14
|
+
'there is not enough space between the reference element top and the body top to fit the Popover',
|
|
15
|
+
() => {
|
|
16
|
+
cy.visitStory('Popover', 'Flipped')
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
Given(
|
|
21
|
+
'there is very little space between the top of the reference element and the top of the body',
|
|
22
|
+
() => {
|
|
23
|
+
cy.visitStory('Popover', 'Hidden Arrow')
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// Window height manipulation to control the space below the reference element
|
|
28
|
+
Given(
|
|
29
|
+
'there is sufficient space between the reference element bottom and the body bottom to fit the Popover',
|
|
30
|
+
() => {
|
|
31
|
+
cy.viewport(1000, 660)
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
Given(
|
|
36
|
+
'there is not enough space between the reference element bottom and the body bottom to fit the Popover',
|
|
37
|
+
() => {
|
|
38
|
+
cy.viewport(1000, 325)
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
Given(
|
|
43
|
+
'there is not enough space between the top of the reference element and the top of the Popover to show the arrow',
|
|
44
|
+
() => {
|
|
45
|
+
cy.viewport(1000, 380)
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// Assertions
|
|
50
|
+
Given(
|
|
51
|
+
'there is sufficient space between the bottom of the reference element and the bottom of the Popover to show the arrow',
|
|
52
|
+
() => {
|
|
53
|
+
compareRefAndPopoverPositions((refPos, popoverPos) => {
|
|
54
|
+
expect(refPos.bottom).to.be.greaterThan(popoverPos.bottom + 8)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
Then('the arrow is hiding', () => {
|
|
60
|
+
cy.get('[data-test="dhis2-uicore-popoverarrow"]').should('not.be.visible')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
Then('the arrow is showing', () => {
|
|
64
|
+
cy.get('[data-test="dhis2-uicore-popoverarrow"]').should('be.visible')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
Then(
|
|
68
|
+
'the horizontal center of the popover is aligned with the horizontal center of the reference element',
|
|
69
|
+
() => {
|
|
70
|
+
compareRefAndPopoverPositions((refPos, popoverPos) => {
|
|
71
|
+
const refCenter = refPos.left + refPos.width / 2
|
|
72
|
+
const popoverCenter = popoverPos.left + popoverPos.width / 2
|
|
73
|
+
expect(refCenter).to.be.closeTo(popoverCenter, CLOSE_TO_DELTA)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
Then('the popover is placed above the reference element', () => {
|
|
79
|
+
compareRefAndPopoverPositions((refPos, popoverPos) => {
|
|
80
|
+
expect(refPos.top).to.be.greaterThan(popoverPos.bottom)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
Then('the popover is placed below the reference element', () => {
|
|
85
|
+
compareRefAndPopoverPositions((refPos, popoverPos) => {
|
|
86
|
+
expect(popoverPos.top).to.be.greaterThan(refPos.bottom)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
Then('the popover is placed op top of the reference element', () => {
|
|
91
|
+
compareRefAndPopoverPositions((refPos, popoverPos) => {
|
|
92
|
+
expect(popoverPos.bottom).to.be.greaterThan(refPos.top)
|
|
93
|
+
expect(refPos.top).to.be.greaterThan(popoverPos.top)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
Then('there is some space between the anchor and the popover', () => {
|
|
98
|
+
compareRefAndPopoverPositions((refPos, popoverPos) => {
|
|
99
|
+
expect(popoverPos.bottom + 8).to.be.closeTo(refPos.top, CLOSE_TO_DELTA)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// helper
|
|
104
|
+
const compareRefAndPopoverPositions = (callback) => {
|
|
105
|
+
// this needs to be done as the cypress reference to the popover is lost
|
|
106
|
+
// once react re-renders it to adjust to the changed viewport size,
|
|
107
|
+
// which happens async
|
|
108
|
+
cy.get('body').should(($body) => {
|
|
109
|
+
const body = $body.get(0)
|
|
110
|
+
const ref = body.querySelector('[data-test="reference-element"]')
|
|
111
|
+
const popover = body.querySelector('[data-test="dhis2-uicore-popover"]')
|
|
112
|
+
const refPos = ref.getBoundingClientRect()
|
|
113
|
+
const popoverPos = popover.getBoundingClientRect()
|
|
114
|
+
|
|
115
|
+
callback(refPos, popoverPos)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Feature: Popover positioning
|
|
2
|
+
|
|
3
|
+
Scenario: Spacing
|
|
4
|
+
Given there is sufficient space to place the Popover above the reference element
|
|
5
|
+
Then there is some space between the anchor and the popover
|
|
6
|
+
And the arrow is showing
|
|
7
|
+
|
|
8
|
+
Scenario: Default positioning
|
|
9
|
+
Given there is sufficient space to place the Popover above the reference element
|
|
10
|
+
Then the popover is placed above the reference element
|
|
11
|
+
And the horizontal center of the popover is aligned with the horizontal center of the reference element
|
|
12
|
+
And the arrow is showing
|
|
13
|
+
|
|
14
|
+
Scenario: Flipped vertical
|
|
15
|
+
Given there is not enough space between the reference element top and the body top to fit the Popover
|
|
16
|
+
And there is sufficient space between the reference element bottom and the body bottom to fit the Popover
|
|
17
|
+
Then the popover is placed below the reference element
|
|
18
|
+
And the horizontal center of the popover is aligned with the horizontal center of the reference element
|
|
19
|
+
And the arrow is showing
|
|
20
|
+
|
|
21
|
+
Scenario: On top of the reference element
|
|
22
|
+
Given there is not enough space between the reference element top and the body top to fit the Popover
|
|
23
|
+
And there is not enough space between the reference element bottom and the body bottom to fit the Popover
|
|
24
|
+
But there is sufficient space between the bottom of the reference element and the bottom of the Popover to show the arrow
|
|
25
|
+
Then the popover is placed op top of the reference element
|
|
26
|
+
And the horizontal center of the popover is aligned with the horizontal center of the reference element
|
|
27
|
+
And the arrow is showing
|
|
28
|
+
|
|
29
|
+
Scenario: Hiding the arrow
|
|
30
|
+
Given there is very little space between the top of the reference element and the top of the body
|
|
31
|
+
And there is not enough space between the reference element bottom and the body bottom to fit the Popover
|
|
32
|
+
And there is not enough space between the top of the reference element and the top of the Popover to show the arrow
|
|
33
|
+
Then the popover is placed op top of the reference element
|
|
34
|
+
And the horizontal center of the popover is aligned with the horizontal center of the reference element
|
|
35
|
+
And the arrow is hiding
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const getArrowPosition = (popperPlacement) => {
|
|
2
|
+
const direction =
|
|
3
|
+
typeof popperPlacement === 'string' ? popperPlacement.split('-')[0] : ''
|
|
4
|
+
|
|
5
|
+
switch (direction) {
|
|
6
|
+
case 'top':
|
|
7
|
+
return 'bottom'
|
|
8
|
+
case 'right':
|
|
9
|
+
return 'left'
|
|
10
|
+
case 'bottom':
|
|
11
|
+
return 'top'
|
|
12
|
+
case 'left':
|
|
13
|
+
return 'right'
|
|
14
|
+
default:
|
|
15
|
+
return ''
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { getArrowPosition }
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Popover } from './popover.js'
|
package/src/modifiers.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getBaseModifiers } from '@dhis2-ui/popper'
|
|
2
|
+
import { ARROW_SIZE } from './arrow.js'
|
|
3
|
+
|
|
4
|
+
const BORDER_RADIUS = 4
|
|
5
|
+
|
|
6
|
+
const computeArrowPadding = () => {
|
|
7
|
+
// pythagoras
|
|
8
|
+
const diagonal = Math.sqrt(2 * Math.pow(ARROW_SIZE, 2))
|
|
9
|
+
const overflowInPx = (diagonal - ARROW_SIZE) / 2
|
|
10
|
+
const padding = BORDER_RADIUS + overflowInPx
|
|
11
|
+
|
|
12
|
+
return Math.ceil(padding)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const hideArrowWhenDisplaced = ({ state }) => {
|
|
16
|
+
const halfArrow = ARROW_SIZE / 2
|
|
17
|
+
const displacement = state.modifiersData.preventOverflow
|
|
18
|
+
const referenceRect = state.rects.reference
|
|
19
|
+
const shouldHideArrow =
|
|
20
|
+
Math.abs(displacement.x) >= referenceRect.width + halfArrow ||
|
|
21
|
+
Math.abs(displacement.y) >= referenceRect.height + halfArrow
|
|
22
|
+
|
|
23
|
+
if (typeof state.attributes.arrow !== 'object') {
|
|
24
|
+
state.attributes.arrow = {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
state.attributes.arrow['data-arrow-hidden'] = shouldHideArrow
|
|
28
|
+
|
|
29
|
+
return state
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const combineModifiers = (arrow, arrowElement, resizeObservers) => {
|
|
33
|
+
const baseModifiers = getBaseModifiers(resizeObservers)
|
|
34
|
+
|
|
35
|
+
if (!arrow) {
|
|
36
|
+
return baseModifiers
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
...baseModifiers,
|
|
41
|
+
{
|
|
42
|
+
name: 'offset',
|
|
43
|
+
options: {
|
|
44
|
+
offset: [0, ARROW_SIZE],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'arrow',
|
|
49
|
+
options: {
|
|
50
|
+
padding: computeArrowPadding(),
|
|
51
|
+
element: arrowElement,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'hideArrowWhenDisplaced',
|
|
56
|
+
enabled: true,
|
|
57
|
+
phase: 'main',
|
|
58
|
+
fn: hideArrowWhenDisplaced,
|
|
59
|
+
requires: ['preventOverflow'],
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import PropTypes from 'prop-types'
|
|
2
|
+
import React, { Component, createRef } from 'react'
|
|
3
|
+
import { Popover } from './popover.js'
|
|
4
|
+
|
|
5
|
+
const boxStyle = {
|
|
6
|
+
display: 'flex',
|
|
7
|
+
justifyContent: 'center',
|
|
8
|
+
width: 400,
|
|
9
|
+
backgroundColor: 'aliceblue',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const referenceElementStyle = {
|
|
13
|
+
width: 100,
|
|
14
|
+
height: 50,
|
|
15
|
+
backgroundColor: 'cadetblue',
|
|
16
|
+
textAlign: 'center',
|
|
17
|
+
padding: 6,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class PopperInBoxWithCenteredReferenceElement extends Component {
|
|
21
|
+
ref = createRef()
|
|
22
|
+
static defaultProps = {
|
|
23
|
+
paddingTop: 220,
|
|
24
|
+
popoverHeight: 200,
|
|
25
|
+
popoverWidth: 336,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render() {
|
|
29
|
+
const {
|
|
30
|
+
paddingTop = 220,
|
|
31
|
+
popoverHeight = 200,
|
|
32
|
+
popoverWidth = 336,
|
|
33
|
+
...popoverProps
|
|
34
|
+
} = this.props
|
|
35
|
+
return (
|
|
36
|
+
<div style={{ ...boxStyle, paddingTop, height: paddingTop + 100 }}>
|
|
37
|
+
<div
|
|
38
|
+
style={referenceElementStyle}
|
|
39
|
+
ref={this.ref}
|
|
40
|
+
data-test="reference-element"
|
|
41
|
+
>
|
|
42
|
+
Reference element
|
|
43
|
+
</div>
|
|
44
|
+
<Popover reference={this.ref} {...popoverProps}>
|
|
45
|
+
<div
|
|
46
|
+
data-test="popover-content"
|
|
47
|
+
style={{ width: popoverWidth, height: popoverHeight }}
|
|
48
|
+
>
|
|
49
|
+
I am in a box with width: {popoverWidth}px and height:{' '}
|
|
50
|
+
{popoverHeight}px
|
|
51
|
+
</div>
|
|
52
|
+
</Popover>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
PopperInBoxWithCenteredReferenceElement.propTypes = {
|
|
58
|
+
paddingTop: PropTypes.number,
|
|
59
|
+
popoverHeight: PropTypes.number,
|
|
60
|
+
popoverWidth: PropTypes.number,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
window.onClickOutside = window.Cypress && window.Cypress.cy.stub()
|
|
64
|
+
|
|
65
|
+
export default { title: 'Popover', component: Popover }
|
|
66
|
+
|
|
67
|
+
export const Default = () => <PopperInBoxWithCenteredReferenceElement />
|
|
68
|
+
|
|
69
|
+
export const Flipped = () => (
|
|
70
|
+
// default viewport-height for flipped popover
|
|
71
|
+
// viePort height 400px for diplaced with arrow
|
|
72
|
+
<PopperInBoxWithCenteredReferenceElement paddingTop={160} />
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
export const HiddenArrow = () => (
|
|
76
|
+
// viewPort height 325px
|
|
77
|
+
<PopperInBoxWithCenteredReferenceElement paddingTop={110} />
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
export const NoArrow = () => (
|
|
81
|
+
<PopperInBoxWithCenteredReferenceElement arrow={false} />
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
export const WithOnClickOutside = () => (
|
|
85
|
+
<PopperInBoxWithCenteredReferenceElement
|
|
86
|
+
onClickOutside={window.onClickOutside}
|
|
87
|
+
/>
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
export const PlacementTop = () => (
|
|
91
|
+
<PopperInBoxWithCenteredReferenceElement
|
|
92
|
+
popoverHeight={40}
|
|
93
|
+
popoverWidth={180}
|
|
94
|
+
paddingTop={50}
|
|
95
|
+
placement="top"
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
export const PlacementRight = () => (
|
|
100
|
+
<PopperInBoxWithCenteredReferenceElement
|
|
101
|
+
popoverHeight={60}
|
|
102
|
+
popoverWidth={130}
|
|
103
|
+
paddingTop={50}
|
|
104
|
+
placement="right"
|
|
105
|
+
/>
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
export const PlacementBottom = () => (
|
|
109
|
+
<PopperInBoxWithCenteredReferenceElement
|
|
110
|
+
popoverHeight={40}
|
|
111
|
+
popoverWidth={180}
|
|
112
|
+
paddingTop={50}
|
|
113
|
+
placement="bottom"
|
|
114
|
+
/>
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
export const PlacementLeft = () => (
|
|
118
|
+
<PopperInBoxWithCenteredReferenceElement
|
|
119
|
+
popoverHeight={60}
|
|
120
|
+
popoverWidth={130}
|
|
121
|
+
paddingTop={50}
|
|
122
|
+
placement="left"
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
export const ShiftedArrow = () => (
|
|
127
|
+
<PopperInBoxWithCenteredReferenceElement
|
|
128
|
+
popoverHeight={160}
|
|
129
|
+
popoverWidth={130}
|
|
130
|
+
paddingTop={1}
|
|
131
|
+
placement="left"
|
|
132
|
+
/>
|
|
133
|
+
)
|
package/src/popover.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { colors, elevations, sharedPropTypes } from '@dhis2/ui-constants'
|
|
2
|
+
import { Layer } from '@dhis2-ui/layer'
|
|
3
|
+
import { getReferenceElement, usePopper } from '@dhis2-ui/popper'
|
|
4
|
+
import PropTypes from 'prop-types'
|
|
5
|
+
import React, { useState, useMemo } from 'react'
|
|
6
|
+
import { Arrow } from './arrow.js'
|
|
7
|
+
import { combineModifiers } from './modifiers.js'
|
|
8
|
+
|
|
9
|
+
const Popover = ({
|
|
10
|
+
children,
|
|
11
|
+
reference,
|
|
12
|
+
arrow = true,
|
|
13
|
+
className,
|
|
14
|
+
dataTest = 'dhis2-uicore-popover',
|
|
15
|
+
elevation = elevations.popover,
|
|
16
|
+
maxWidth = 360,
|
|
17
|
+
observePopperResize,
|
|
18
|
+
observeReferenceResize,
|
|
19
|
+
placement = 'top',
|
|
20
|
+
onClickOutside,
|
|
21
|
+
}) => {
|
|
22
|
+
const referenceElement = getReferenceElement(reference)
|
|
23
|
+
const [popperElement, setPopperElement] = useState(null)
|
|
24
|
+
const [arrowElement, setArrowElement] = useState(null)
|
|
25
|
+
const modifiers = useMemo(
|
|
26
|
+
() =>
|
|
27
|
+
combineModifiers(arrow, arrowElement, {
|
|
28
|
+
observePopperResize,
|
|
29
|
+
observeReferenceResize,
|
|
30
|
+
}),
|
|
31
|
+
[arrow, arrowElement, observePopperResize, observeReferenceResize]
|
|
32
|
+
)
|
|
33
|
+
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
|
34
|
+
placement,
|
|
35
|
+
modifiers,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Layer onBackdropClick={onClickOutside}>
|
|
40
|
+
<div
|
|
41
|
+
data-test={dataTest}
|
|
42
|
+
className={className}
|
|
43
|
+
ref={setPopperElement}
|
|
44
|
+
style={styles.popper}
|
|
45
|
+
{...attributes.popper}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
{arrow && (
|
|
49
|
+
<Arrow
|
|
50
|
+
hidden={
|
|
51
|
+
attributes.arrow &&
|
|
52
|
+
attributes.arrow['data-arrow-hidden']
|
|
53
|
+
}
|
|
54
|
+
popperPlacement={
|
|
55
|
+
attributes.popper &&
|
|
56
|
+
attributes.popper['data-popper-placement']
|
|
57
|
+
}
|
|
58
|
+
ref={setArrowElement}
|
|
59
|
+
styles={styles.arrow}
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
<style jsx>{`
|
|
63
|
+
div {
|
|
64
|
+
max-width: ${maxWidth}px;
|
|
65
|
+
box-shadow: ${elevation};
|
|
66
|
+
background-color: ${colors.white};
|
|
67
|
+
border-radius: 4px;
|
|
68
|
+
}
|
|
69
|
+
`}</style>
|
|
70
|
+
</div>
|
|
71
|
+
</Layer>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Popover.propTypes = {
|
|
76
|
+
children: PropTypes.node.isRequired,
|
|
77
|
+
/** Show or hide the arrow */
|
|
78
|
+
arrow: PropTypes.bool,
|
|
79
|
+
className: PropTypes.string,
|
|
80
|
+
dataTest: PropTypes.string,
|
|
81
|
+
/** Box-shadow to create appearance of elevation. Use `elevations` constants from the UI library. */
|
|
82
|
+
elevation: PropTypes.string,
|
|
83
|
+
maxWidth: PropTypes.number,
|
|
84
|
+
observePopperResize: PropTypes.bool,
|
|
85
|
+
observeReferenceResize: PropTypes.bool,
|
|
86
|
+
placement: sharedPropTypes.popperPlacementPropType,
|
|
87
|
+
/** A React ref that refers to the element the Popover should position against */
|
|
88
|
+
reference: sharedPropTypes.popperReferencePropType,
|
|
89
|
+
onClickOutside: PropTypes.func,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { Popover }
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { elevations, sharedPropTypes } from '@dhis2/ui-constants'
|
|
2
|
+
import React, { useRef } from 'react'
|
|
3
|
+
import { Popover } from './popover.js'
|
|
4
|
+
|
|
5
|
+
const subtitle = `Useful to give a user more information or possible actions without disrupting a page or flow`
|
|
6
|
+
|
|
7
|
+
const description = `
|
|
8
|
+
Popovers are similar to tooltips: they are for displaying extra information, but popovers are intended for richer information and actions.
|
|
9
|
+
|
|
10
|
+
Popovers are triggered by hovering or tapping on an element. Clicking on a element keeps the popover open until the user clicks or interacts elsewhere on the page.
|
|
11
|
+
|
|
12
|
+
Popovers can contain information in the form of rich markup, as well as actions. Critical actions, or the only action on a page, should not be hidden inside a popover.
|
|
13
|
+
|
|
14
|
+
Before using a popover, consider that some users may never see the information contained within. If that is a problem, display the information right on the page instead. Do not use a popover for content that is essential to the user experience or application.
|
|
15
|
+
|
|
16
|
+
See more about Popovers at [Design System: Popover](https://github.com/dhis2/design-system/blob/master/molecules/popover.md).
|
|
17
|
+
|
|
18
|
+
\`\`\`js
|
|
19
|
+
import { Popover } from '@dhis2/ui'
|
|
20
|
+
\`\`\`
|
|
21
|
+
|
|
22
|
+
_**Note**: Due to the full-page interaction of this component, only one representative example in an iframe sandbox is shown here. See more (interactive) examples in the 'Canvas' tab._
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
title: 'Popover',
|
|
27
|
+
component: Popover,
|
|
28
|
+
parameters: {
|
|
29
|
+
componentSubtitle: subtitle,
|
|
30
|
+
docs: {
|
|
31
|
+
description: { component: description },
|
|
32
|
+
// Contain the popovers in iframes in the docs page
|
|
33
|
+
inlineStories: false,
|
|
34
|
+
iframeHeight: '500px',
|
|
35
|
+
// Disable stories in docs page by default to use one representative example
|
|
36
|
+
disable: true,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
// Handles weird treatment of non-literal args (`elevation: elevations.e200`)
|
|
40
|
+
args: {
|
|
41
|
+
arrow: true,
|
|
42
|
+
dataTest: 'dhis2-uicore-popover',
|
|
43
|
+
maxWidth: 360,
|
|
44
|
+
placement: 'top',
|
|
45
|
+
},
|
|
46
|
+
argTypes: {
|
|
47
|
+
reference: { ...sharedPropTypes.popperReferenceArgType },
|
|
48
|
+
placement: { ...sharedPropTypes.popperPlacementArgType },
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const boxStyle = {
|
|
53
|
+
display: 'flex',
|
|
54
|
+
justifyContent: 'center',
|
|
55
|
+
width: 400,
|
|
56
|
+
paddingTop: 280,
|
|
57
|
+
backgroundColor: 'aliceblue',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const referenceElementStyle = {
|
|
61
|
+
width: 100,
|
|
62
|
+
height: 50,
|
|
63
|
+
backgroundColor: 'cadetblue',
|
|
64
|
+
textAlign: 'center',
|
|
65
|
+
padding: 6,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const Template = (args) => {
|
|
69
|
+
const ref = useRef(null)
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div style={boxStyle}>
|
|
73
|
+
<div style={referenceElementStyle} ref={ref}>
|
|
74
|
+
Reference element
|
|
75
|
+
</div>
|
|
76
|
+
<Popover {...args} reference={ref}>
|
|
77
|
+
<div>
|
|
78
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
|
|
79
|
+
do eiusmod tempor incididunt ut labore et dolore magna
|
|
80
|
+
aliqua. Consectetur purus ut faucibus pulvinar elementum.
|
|
81
|
+
Dignissim diam quis enim lobortis scelerisque fermentum dui
|
|
82
|
+
faucibus. Rhoncus aenean vel elit scelerisque mauris
|
|
83
|
+
pellentesque. Non sodales neque sodales ut etiam sit amet.
|
|
84
|
+
Volutpat sed cras ornare arcu dui. Quis imperdiet massa
|
|
85
|
+
tincidunt nunc pulvinar sapien et ligula. Convallis posuere
|
|
86
|
+
morbi leo urna molestie at. Mauris cursus mattis molestie a
|
|
87
|
+
iaculis at.
|
|
88
|
+
</div>
|
|
89
|
+
</Popover>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const Default = Template.bind({})
|
|
95
|
+
Default.parameters = {
|
|
96
|
+
docs: {
|
|
97
|
+
// Enable this story for the docs page
|
|
98
|
+
disable: false,
|
|
99
|
+
// Show source, including 'ref' hooks
|
|
100
|
+
source: { type: 'code' },
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const NoArrow = Template.bind({})
|
|
105
|
+
NoArrow.args = { arrow: false }
|
|
106
|
+
|
|
107
|
+
export const Customization = Template.bind({})
|
|
108
|
+
Customization.args = {
|
|
109
|
+
arrow: true,
|
|
110
|
+
className: 'custom-classname',
|
|
111
|
+
dataTest: 'custom-data-test-id',
|
|
112
|
+
elevation: elevations.e200,
|
|
113
|
+
maxWidth: 400,
|
|
114
|
+
placement: 'bottom-start',
|
|
115
|
+
onClickOutside: () => console.log('backdrop was clicked...'),
|
|
116
|
+
}
|