@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 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,6 @@
1
+ Feature: Popover clicking outside
2
+
3
+ Scenario: Responds to a click outdside the Popover
4
+ Given a default Popover is rendered with an onClickOutside handler
5
+ When the user clicks outside of the Popover
6
+ Then the clickOutside handler is called
@@ -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'
@@ -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
+ }