@startupjs-ui/abstract-popover 0.1.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/CHANGELOG.md +20 -0
- package/README.mdx +156 -0
- package/constants.ts +35 -0
- package/getGeometry.ts +459 -0
- package/index.cssx.styl +18 -0
- package/index.d.ts +46 -0
- package/index.tsx +230 -0
- package/package.json +20 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @startupjs-ui/abstract-popover
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **abstract-popover:** refactor AbstractPopover component ([4a19018](https://github.com/startupjs/startupjs-ui/commit/4a190183b9e6903b6758d1d006fce7eca39bce0a))
|
|
20
|
+
* add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
|
package/README.mdx
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useRef, useState } from 'react'
|
|
2
|
+
import { Sandbox } from '@startupjs-ui/docs'
|
|
3
|
+
import AbstractPopover, {
|
|
4
|
+
_PropsJsonSchema as AbstractPopoverPropsJsonSchema
|
|
5
|
+
} from './index'
|
|
6
|
+
import Button from '@startupjs-ui/button'
|
|
7
|
+
import Div from '@startupjs-ui/div'
|
|
8
|
+
import Span from '@startupjs-ui/span'
|
|
9
|
+
|
|
10
|
+
# AbstractPopover
|
|
11
|
+
|
|
12
|
+
Low-level primitive for anchoring content to an element (tooltip / popover use-cases).
|
|
13
|
+
|
|
14
|
+
```jsx
|
|
15
|
+
import { AbstractPopover } from 'startupjs-ui'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Basic example
|
|
19
|
+
|
|
20
|
+
```jsx example
|
|
21
|
+
const anchorRef = useRef()
|
|
22
|
+
const [visible, setVisible] = useState(false)
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Div style={{ padding: 20, backgroundColor: '#f6f7f9' }}>
|
|
26
|
+
<Div row gap style={{ alignItems: 'center' }}>
|
|
27
|
+
<Button onPress={() => setVisible(v => !v)}>
|
|
28
|
+
Toggle popover
|
|
29
|
+
</Button>
|
|
30
|
+
<Div
|
|
31
|
+
ref={anchorRef}
|
|
32
|
+
style={{
|
|
33
|
+
padding: 10,
|
|
34
|
+
backgroundColor: 'white',
|
|
35
|
+
borderRadius: 8,
|
|
36
|
+
borderWidth: 1,
|
|
37
|
+
borderColor: '#ddd'
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<Span>Anchor</Span>
|
|
41
|
+
</Div>
|
|
42
|
+
</Div>
|
|
43
|
+
|
|
44
|
+
<AbstractPopover
|
|
45
|
+
anchorRef={anchorRef}
|
|
46
|
+
visible={visible}
|
|
47
|
+
arrow
|
|
48
|
+
position='bottom'
|
|
49
|
+
attachment='start'
|
|
50
|
+
style={{
|
|
51
|
+
backgroundColor: 'white',
|
|
52
|
+
padding: 10,
|
|
53
|
+
borderRadius: 8,
|
|
54
|
+
borderWidth: 1,
|
|
55
|
+
borderColor: '#ddd'
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<Span>Popover content</Span>
|
|
59
|
+
</AbstractPopover>
|
|
60
|
+
</Div>
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Matching anchor width
|
|
65
|
+
|
|
66
|
+
```jsx example
|
|
67
|
+
const anchorRef = useRef()
|
|
68
|
+
const [visible, setVisible] = useState(false)
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Div style={{ padding: 20, backgroundColor: '#f6f7f9' }}>
|
|
72
|
+
<Div
|
|
73
|
+
ref={anchorRef}
|
|
74
|
+
style={{
|
|
75
|
+
width: 240,
|
|
76
|
+
padding: 10,
|
|
77
|
+
backgroundColor: 'white',
|
|
78
|
+
borderRadius: 8,
|
|
79
|
+
borderWidth: 1,
|
|
80
|
+
borderColor: '#ddd'
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<Span>Anchor (240px)</Span>
|
|
84
|
+
<Button pushed onPress={() => setVisible(v => !v)}>
|
|
85
|
+
Toggle
|
|
86
|
+
</Button>
|
|
87
|
+
</Div>
|
|
88
|
+
|
|
89
|
+
<AbstractPopover
|
|
90
|
+
anchorRef={anchorRef}
|
|
91
|
+
visible={visible}
|
|
92
|
+
matchAnchorWidth
|
|
93
|
+
position='bottom'
|
|
94
|
+
attachment='start'
|
|
95
|
+
style={{
|
|
96
|
+
backgroundColor: 'white',
|
|
97
|
+
padding: 10,
|
|
98
|
+
borderRadius: 8,
|
|
99
|
+
borderWidth: 1,
|
|
100
|
+
borderColor: '#ddd'
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<Span>Width matches anchor</Span>
|
|
104
|
+
</AbstractPopover>
|
|
105
|
+
</Div>
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Custom fallback placements
|
|
110
|
+
|
|
111
|
+
```jsx example
|
|
112
|
+
const anchorRef = useRef()
|
|
113
|
+
const [visible, setVisible] = useState(false)
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Div style={{ padding: 20, backgroundColor: '#f6f7f9' }}>
|
|
117
|
+
<Div ref={anchorRef} style={{ padding: 10, backgroundColor: 'white', borderRadius: 8 }}>
|
|
118
|
+
<Span>Anchor</Span>
|
|
119
|
+
<Button pushed onPress={() => setVisible(v => !v)}>
|
|
120
|
+
Toggle
|
|
121
|
+
</Button>
|
|
122
|
+
</Div>
|
|
123
|
+
|
|
124
|
+
<AbstractPopover
|
|
125
|
+
anchorRef={anchorRef}
|
|
126
|
+
visible={visible}
|
|
127
|
+
position='top'
|
|
128
|
+
attachment='center'
|
|
129
|
+
placements={['top-center', 'bottom-center']}
|
|
130
|
+
arrow
|
|
131
|
+
style={{
|
|
132
|
+
backgroundColor: 'white',
|
|
133
|
+
padding: 10,
|
|
134
|
+
borderRadius: 8,
|
|
135
|
+
borderWidth: 1,
|
|
136
|
+
borderColor: '#ddd'
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<Span>Tries top-center, then bottom-center</Span>
|
|
140
|
+
</AbstractPopover>
|
|
141
|
+
</Div>
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Sandbox
|
|
146
|
+
|
|
147
|
+
<Sandbox
|
|
148
|
+
Component={AbstractPopover}
|
|
149
|
+
propsJsonSchema={AbstractPopoverPropsJsonSchema}
|
|
150
|
+
props={{
|
|
151
|
+
anchorRef: { current: { measure: (cb) => cb(0, 0, 120, 32, 40, 120) } },
|
|
152
|
+
style: { backgroundColor: 'white', padding: 10, borderRadius: 8, borderWidth: 1, borderColor: '#ddd' },
|
|
153
|
+
arrow: true,
|
|
154
|
+
children: <Span>Sandbox content</Span>
|
|
155
|
+
}}
|
|
156
|
+
/>
|
package/constants.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const POPOVER_MARGIN = 8 as const
|
|
2
|
+
export const ARROW_SIZE = 8 as const
|
|
3
|
+
|
|
4
|
+
export type AbstractPopoverPosition = 'top' | 'bottom' | 'left' | 'right'
|
|
5
|
+
export type AbstractPopoverAttachment = 'start' | 'center' | 'end'
|
|
6
|
+
export type AbstractPopoverPlacement = `${AbstractPopoverPosition}-${AbstractPopoverAttachment}`
|
|
7
|
+
|
|
8
|
+
export const STEPS = {
|
|
9
|
+
CLOSE: 'close',
|
|
10
|
+
MEASURE: 'measure',
|
|
11
|
+
RENDER: 'render',
|
|
12
|
+
OPEN: 'open'
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
export const PLACEMENTS_ORDER = [
|
|
16
|
+
'top-start',
|
|
17
|
+
'top-center',
|
|
18
|
+
'top-end',
|
|
19
|
+
'right-start',
|
|
20
|
+
'right-center',
|
|
21
|
+
'right-end',
|
|
22
|
+
'bottom-end',
|
|
23
|
+
'bottom-center',
|
|
24
|
+
'bottom-start',
|
|
25
|
+
'left-end',
|
|
26
|
+
'left-center',
|
|
27
|
+
'left-start'
|
|
28
|
+
] as const satisfies readonly AbstractPopoverPlacement[]
|
|
29
|
+
|
|
30
|
+
export const POSITIONS_REVERSE = {
|
|
31
|
+
top: 'bottom',
|
|
32
|
+
bottom: 'top',
|
|
33
|
+
left: 'right',
|
|
34
|
+
right: 'left'
|
|
35
|
+
} as const
|
package/getGeometry.ts
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { Dimensions } from 'react-native'
|
|
2
|
+
import {
|
|
3
|
+
ARROW_SIZE,
|
|
4
|
+
PLACEMENTS_ORDER,
|
|
5
|
+
POPOVER_MARGIN,
|
|
6
|
+
POSITIONS_REVERSE,
|
|
7
|
+
type AbstractPopoverAttachment as Attachment,
|
|
8
|
+
type AbstractPopoverPlacement as Placement,
|
|
9
|
+
type AbstractPopoverPosition as Position
|
|
10
|
+
} from './constants'
|
|
11
|
+
|
|
12
|
+
interface Measures {
|
|
13
|
+
width: number
|
|
14
|
+
height: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface LayoutMeasures {
|
|
18
|
+
x: number
|
|
19
|
+
y: number
|
|
20
|
+
width: number
|
|
21
|
+
height: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface AnchorMeasures extends LayoutMeasures {
|
|
25
|
+
pageX: number
|
|
26
|
+
pageY: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PopoverGeometry {
|
|
30
|
+
placement: Placement
|
|
31
|
+
position: Position
|
|
32
|
+
attachment: Attachment
|
|
33
|
+
top: number
|
|
34
|
+
left: number
|
|
35
|
+
arrowTop: number | string
|
|
36
|
+
arrowLeft: number | string
|
|
37
|
+
width: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function getGeometry (params: {
|
|
41
|
+
placement: Placement
|
|
42
|
+
placements: readonly Placement[]
|
|
43
|
+
arrow?: boolean
|
|
44
|
+
matchAnchorWidth?: boolean
|
|
45
|
+
dimensions: Measures
|
|
46
|
+
tetherMeasures: LayoutMeasures & Measures
|
|
47
|
+
anchorMeasures: AnchorMeasures
|
|
48
|
+
}): PopoverGeometry {
|
|
49
|
+
const {
|
|
50
|
+
placement,
|
|
51
|
+
placements,
|
|
52
|
+
matchAnchorWidth,
|
|
53
|
+
tetherMeasures,
|
|
54
|
+
anchorMeasures
|
|
55
|
+
} = params
|
|
56
|
+
const topPositions = getTopPositions(params)
|
|
57
|
+
const leftPositions = getLeftPositions(params)
|
|
58
|
+
const arrowTopPositions = getTopPositionsArrow(params)
|
|
59
|
+
const arrowLeftPositions = getLeftPositionsArrow(params)
|
|
60
|
+
const preparedPlacements = preparePlacements({ placement, placements })
|
|
61
|
+
const currentPlacement = findAvailablePlacement({
|
|
62
|
+
tetherMeasures,
|
|
63
|
+
placement,
|
|
64
|
+
placements: preparedPlacements,
|
|
65
|
+
leftPositions,
|
|
66
|
+
topPositions,
|
|
67
|
+
initPlacement: placement
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const [currentPosition, currentAttachment] = currentPlacement.split('-') as [Position, Attachment]
|
|
71
|
+
|
|
72
|
+
const geometry = {
|
|
73
|
+
placement: currentPlacement,
|
|
74
|
+
position: currentPosition,
|
|
75
|
+
attachment: currentAttachment,
|
|
76
|
+
top: topPositions[currentPlacement],
|
|
77
|
+
left: leftPositions[currentPlacement],
|
|
78
|
+
arrowTop: arrowTopPositions[currentPlacement],
|
|
79
|
+
arrowLeft: arrowLeftPositions[currentPlacement],
|
|
80
|
+
width: matchAnchorWidth ? anchorMeasures.width : tetherMeasures.width
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return geometry
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getLeftPositions ({
|
|
87
|
+
tetherMeasures,
|
|
88
|
+
anchorMeasures,
|
|
89
|
+
arrow,
|
|
90
|
+
dimensions
|
|
91
|
+
}: {
|
|
92
|
+
tetherMeasures: Measures
|
|
93
|
+
anchorMeasures: AnchorMeasures
|
|
94
|
+
arrow?: boolean
|
|
95
|
+
dimensions: Measures
|
|
96
|
+
}): Record<Placement, number> {
|
|
97
|
+
const leftPositions: Record<string, number> = {}
|
|
98
|
+
|
|
99
|
+
const halfContent = tetherMeasures.width / 2
|
|
100
|
+
const halfCaption = anchorMeasures.width / 2
|
|
101
|
+
|
|
102
|
+
const overRight = dimensions.width - tetherMeasures.width
|
|
103
|
+
|
|
104
|
+
const positionMinorLeft = anchorMeasures.pageX
|
|
105
|
+
if (positionMinorLeft + tetherMeasures.width > dimensions.width) {
|
|
106
|
+
leftPositions['bottom-start'] = overRight
|
|
107
|
+
leftPositions['top-start'] = overRight
|
|
108
|
+
} else {
|
|
109
|
+
leftPositions['bottom-start'] = positionMinorLeft
|
|
110
|
+
leftPositions['top-start'] = positionMinorLeft
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const positionMinorCenter = anchorMeasures.pageX - halfContent + halfCaption
|
|
114
|
+
if (positionMinorCenter < 0) {
|
|
115
|
+
leftPositions['top-center'] = 0
|
|
116
|
+
leftPositions['bottom-center'] = 0
|
|
117
|
+
} else if (positionMinorCenter + tetherMeasures.width > dimensions.width) {
|
|
118
|
+
leftPositions['top-center'] = overRight
|
|
119
|
+
leftPositions['bottom-center'] = overRight
|
|
120
|
+
} else {
|
|
121
|
+
leftPositions['top-center'] = positionMinorCenter
|
|
122
|
+
leftPositions['bottom-center'] = positionMinorCenter
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const positionMinorRight = anchorMeasures.pageX - tetherMeasures.width + anchorMeasures.width
|
|
126
|
+
if (positionMinorRight < 0) {
|
|
127
|
+
leftPositions['bottom-end'] = 0
|
|
128
|
+
leftPositions['top-end'] = 0
|
|
129
|
+
} else {
|
|
130
|
+
leftPositions['bottom-end'] = positionMinorRight
|
|
131
|
+
leftPositions['top-end'] = positionMinorRight
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const positionRootLeft = anchorMeasures.pageX - tetherMeasures.width - (arrow ? ARROW_SIZE + POPOVER_MARGIN : POPOVER_MARGIN)
|
|
135
|
+
leftPositions['left-start'] = positionRootLeft
|
|
136
|
+
leftPositions['left-center'] = positionRootLeft
|
|
137
|
+
leftPositions['left-end'] = positionRootLeft
|
|
138
|
+
|
|
139
|
+
const positionRootRight = anchorMeasures.pageX + anchorMeasures.width + (arrow ? ARROW_SIZE + POPOVER_MARGIN : POPOVER_MARGIN)
|
|
140
|
+
leftPositions['right-start'] = positionRootRight
|
|
141
|
+
leftPositions['right-center'] = positionRootRight
|
|
142
|
+
leftPositions['right-end'] = positionRootRight
|
|
143
|
+
|
|
144
|
+
return leftPositions
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getTopPositions ({
|
|
148
|
+
anchorMeasures,
|
|
149
|
+
tetherMeasures,
|
|
150
|
+
arrow,
|
|
151
|
+
dimensions
|
|
152
|
+
}: {
|
|
153
|
+
tetherMeasures: Measures
|
|
154
|
+
anchorMeasures: AnchorMeasures
|
|
155
|
+
arrow?: boolean
|
|
156
|
+
dimensions: Measures
|
|
157
|
+
}): Record<Placement, number> {
|
|
158
|
+
const topPositions: Record<string, number> = {}
|
|
159
|
+
|
|
160
|
+
const halfCaption = anchorMeasures.height / 2
|
|
161
|
+
const halfContent = tetherMeasures.height / 2
|
|
162
|
+
|
|
163
|
+
const positionRootBottom = anchorMeasures.pageY + anchorMeasures.height + (arrow ? ARROW_SIZE + POPOVER_MARGIN : POPOVER_MARGIN)
|
|
164
|
+
topPositions['bottom-start'] = positionRootBottom
|
|
165
|
+
topPositions['bottom-center'] = positionRootBottom
|
|
166
|
+
topPositions['bottom-end'] = positionRootBottom
|
|
167
|
+
|
|
168
|
+
const positionRootTop = anchorMeasures.pageY - tetherMeasures.height - (arrow ? ARROW_SIZE + POPOVER_MARGIN : POPOVER_MARGIN)
|
|
169
|
+
topPositions['top-start'] = positionRootTop
|
|
170
|
+
topPositions['top-center'] = positionRootTop
|
|
171
|
+
topPositions['top-end'] = positionRootTop
|
|
172
|
+
|
|
173
|
+
const positionMinorCenter = anchorMeasures.pageY + halfCaption - halfContent
|
|
174
|
+
if (positionMinorCenter < 0) {
|
|
175
|
+
topPositions['left-center'] = 0
|
|
176
|
+
topPositions['right-center'] = 0
|
|
177
|
+
} else if (anchorMeasures.pageY + halfContent > dimensions.height) {
|
|
178
|
+
const positionMinorCenterOverBottom = dimensions.height - tetherMeasures.height
|
|
179
|
+
topPositions['left-center'] = positionMinorCenterOverBottom
|
|
180
|
+
topPositions['right-center'] = positionMinorCenterOverBottom
|
|
181
|
+
} else {
|
|
182
|
+
topPositions['left-center'] = positionMinorCenter
|
|
183
|
+
topPositions['right-center'] = positionMinorCenter
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (anchorMeasures.pageY + tetherMeasures.height > dimensions.height) {
|
|
187
|
+
const positionMinorStartOver = dimensions.height - tetherMeasures.height
|
|
188
|
+
topPositions['left-start'] = positionMinorStartOver
|
|
189
|
+
topPositions['right-start'] = positionMinorStartOver
|
|
190
|
+
} else {
|
|
191
|
+
topPositions['left-start'] = anchorMeasures.pageY
|
|
192
|
+
topPositions['right-start'] = anchorMeasures.pageY
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (anchorMeasures.pageY + anchorMeasures.height - tetherMeasures.height < 0) {
|
|
196
|
+
topPositions['left-end'] = 0
|
|
197
|
+
topPositions['right-end'] = 0
|
|
198
|
+
} else {
|
|
199
|
+
const positionMinorEnd = anchorMeasures.pageY + anchorMeasures.height - tetherMeasures.height
|
|
200
|
+
topPositions['left-end'] = positionMinorEnd
|
|
201
|
+
topPositions['right-end'] = positionMinorEnd
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return topPositions
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getLeftPositionsArrow ({
|
|
208
|
+
tetherMeasures,
|
|
209
|
+
anchorMeasures,
|
|
210
|
+
dimensions
|
|
211
|
+
}: {
|
|
212
|
+
tetherMeasures: Measures
|
|
213
|
+
anchorMeasures: AnchorMeasures
|
|
214
|
+
dimensions: Measures
|
|
215
|
+
}): Record<Placement, number | string> {
|
|
216
|
+
const arrowLeftPositions: Record<string, number | string> = {}
|
|
217
|
+
|
|
218
|
+
const halfContent = tetherMeasures.width / 2
|
|
219
|
+
const halfCaption = anchorMeasures.width / 2
|
|
220
|
+
|
|
221
|
+
const overLeft = anchorMeasures.pageX + halfCaption - ARROW_SIZE
|
|
222
|
+
const overRight = tetherMeasures.width - (dimensions.width - anchorMeasures.pageX) + halfCaption - ARROW_SIZE
|
|
223
|
+
const minRight = tetherMeasures.width - (ARROW_SIZE * 3)
|
|
224
|
+
|
|
225
|
+
if (anchorMeasures.pageX + tetherMeasures.width > dimensions.width) {
|
|
226
|
+
if (anchorMeasures.pageX + anchorMeasures.width > dimensions.width) {
|
|
227
|
+
arrowLeftPositions['top-start'] = minRight
|
|
228
|
+
arrowLeftPositions['bottom-start'] = minRight
|
|
229
|
+
} else {
|
|
230
|
+
arrowLeftPositions['top-start'] = overRight
|
|
231
|
+
arrowLeftPositions['bottom-start'] = overRight
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
arrowLeftPositions['top-start'] = ARROW_SIZE
|
|
235
|
+
arrowLeftPositions['bottom-start'] = ARROW_SIZE
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (anchorMeasures.pageX - tetherMeasures.width + anchorMeasures.width < 0) {
|
|
239
|
+
if (anchorMeasures.pageX < 0) {
|
|
240
|
+
arrowLeftPositions['top-end'] = ARROW_SIZE
|
|
241
|
+
arrowLeftPositions['bottom-end'] = ARROW_SIZE
|
|
242
|
+
} else {
|
|
243
|
+
arrowLeftPositions['top-end'] = overLeft
|
|
244
|
+
arrowLeftPositions['bottom-end'] = overLeft
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
const positionEnd = tetherMeasures.width - (ARROW_SIZE * 3)
|
|
248
|
+
arrowLeftPositions['top-end'] = positionEnd
|
|
249
|
+
arrowLeftPositions['bottom-end'] = positionEnd
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (anchorMeasures.pageX - halfContent + halfCaption < 0) {
|
|
253
|
+
if (anchorMeasures.pageX < 0) {
|
|
254
|
+
arrowLeftPositions['top-center'] = ARROW_SIZE
|
|
255
|
+
arrowLeftPositions['bottom-center'] = ARROW_SIZE
|
|
256
|
+
} else {
|
|
257
|
+
arrowLeftPositions['top-center'] = overLeft
|
|
258
|
+
arrowLeftPositions['bottom-center'] = overLeft
|
|
259
|
+
}
|
|
260
|
+
} else if (anchorMeasures.pageX + halfContent > dimensions.width) {
|
|
261
|
+
if (anchorMeasures.pageX + anchorMeasures.width > dimensions.width) {
|
|
262
|
+
arrowLeftPositions['top-center'] = minRight
|
|
263
|
+
arrowLeftPositions['bottom-center'] = minRight
|
|
264
|
+
} else {
|
|
265
|
+
arrowLeftPositions['top-center'] = overRight
|
|
266
|
+
arrowLeftPositions['bottom-center'] = overRight
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
const positionCenter = halfContent - ARROW_SIZE
|
|
270
|
+
arrowLeftPositions['top-center'] = positionCenter
|
|
271
|
+
arrowLeftPositions['bottom-center'] = positionCenter
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
arrowLeftPositions['left-center'] = '100%'
|
|
275
|
+
arrowLeftPositions['left-start'] = '100%'
|
|
276
|
+
arrowLeftPositions['left-end'] = '100%'
|
|
277
|
+
|
|
278
|
+
const dblArrow = ARROW_SIZE * 2
|
|
279
|
+
arrowLeftPositions['right-start'] = -dblArrow
|
|
280
|
+
arrowLeftPositions['right-center'] = -dblArrow
|
|
281
|
+
arrowLeftPositions['right-end'] = -dblArrow
|
|
282
|
+
|
|
283
|
+
return arrowLeftPositions
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getTopPositionsArrow ({
|
|
287
|
+
tetherMeasures,
|
|
288
|
+
anchorMeasures,
|
|
289
|
+
dimensions
|
|
290
|
+
}: {
|
|
291
|
+
tetherMeasures: Measures
|
|
292
|
+
anchorMeasures: AnchorMeasures
|
|
293
|
+
dimensions: Measures
|
|
294
|
+
}): Record<Placement, number> {
|
|
295
|
+
const arrowTopPositions: Record<string, number> = {}
|
|
296
|
+
|
|
297
|
+
const halfCaption = anchorMeasures.height / 2
|
|
298
|
+
const halfContent = tetherMeasures.height / 2
|
|
299
|
+
|
|
300
|
+
const overTop = anchorMeasures.pageX + halfCaption - ARROW_SIZE
|
|
301
|
+
const overBottom = tetherMeasures.height - (dimensions.height - anchorMeasures.pageX) + halfCaption - ARROW_SIZE
|
|
302
|
+
const minBottom = tetherMeasures.height - (ARROW_SIZE * 3)
|
|
303
|
+
|
|
304
|
+
if (anchorMeasures.pageX + halfCaption - halfContent < 0) {
|
|
305
|
+
if (anchorMeasures.pageX < 0) {
|
|
306
|
+
arrowTopPositions['left-center'] = ARROW_SIZE
|
|
307
|
+
arrowTopPositions['right-center'] = ARROW_SIZE
|
|
308
|
+
} else {
|
|
309
|
+
arrowTopPositions['left-center'] = overTop
|
|
310
|
+
arrowTopPositions['right-center'] = overTop
|
|
311
|
+
}
|
|
312
|
+
} else if (anchorMeasures.pageX + halfContent > dimensions.height) {
|
|
313
|
+
if (anchorMeasures.pageX + anchorMeasures.height > dimensions.height) {
|
|
314
|
+
arrowTopPositions['left-center'] = minBottom
|
|
315
|
+
arrowTopPositions['right-center'] = minBottom
|
|
316
|
+
} else {
|
|
317
|
+
arrowTopPositions['left-center'] = overBottom
|
|
318
|
+
arrowTopPositions['right-center'] = overBottom
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
const positionCenter = halfContent - ARROW_SIZE
|
|
322
|
+
arrowTopPositions['left-center'] = positionCenter
|
|
323
|
+
arrowTopPositions['right-center'] = positionCenter
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (anchorMeasures.pageX + tetherMeasures.height > dimensions.height) {
|
|
327
|
+
if (anchorMeasures.pageX + anchorMeasures.height > dimensions.height) {
|
|
328
|
+
arrowTopPositions['left-start'] = minBottom
|
|
329
|
+
arrowTopPositions['right-start'] = minBottom
|
|
330
|
+
} else {
|
|
331
|
+
arrowTopPositions['left-start'] = overBottom
|
|
332
|
+
arrowTopPositions['right-start'] = overBottom
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
const positionStart = ARROW_SIZE
|
|
336
|
+
arrowTopPositions['left-start'] = positionStart
|
|
337
|
+
arrowTopPositions['right-start'] = positionStart
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (anchorMeasures.pageX + anchorMeasures.height - tetherMeasures.height < 0) {
|
|
341
|
+
if (anchorMeasures.pageX < 0) {
|
|
342
|
+
arrowTopPositions['left-end'] = ARROW_SIZE
|
|
343
|
+
arrowTopPositions['right-end'] = ARROW_SIZE
|
|
344
|
+
} else {
|
|
345
|
+
arrowTopPositions['left-end'] = overTop
|
|
346
|
+
arrowTopPositions['right-end'] = overTop
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
const positionEnd = tetherMeasures.height - (ARROW_SIZE * 3)
|
|
350
|
+
arrowTopPositions['left-end'] = positionEnd
|
|
351
|
+
arrowTopPositions['right-end'] = positionEnd
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
arrowTopPositions['top-start'] = tetherMeasures.height
|
|
355
|
+
arrowTopPositions['top-center'] = tetherMeasures.height
|
|
356
|
+
arrowTopPositions['top-end'] = tetherMeasures.height
|
|
357
|
+
|
|
358
|
+
const dblArrow = ARROW_SIZE * 2
|
|
359
|
+
arrowTopPositions['bottom-start'] = -dblArrow
|
|
360
|
+
arrowTopPositions['bottom-center'] = -dblArrow
|
|
361
|
+
arrowTopPositions['bottom-end'] = -dblArrow
|
|
362
|
+
|
|
363
|
+
return arrowTopPositions
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// TODO: This is unlogical behaviour.
|
|
367
|
+
// We can always find a better position if the specified position does not fit
|
|
368
|
+
function preparePlacements ({
|
|
369
|
+
placement,
|
|
370
|
+
placements
|
|
371
|
+
}: {
|
|
372
|
+
placement: Placement
|
|
373
|
+
placements: readonly Placement[]
|
|
374
|
+
}): Placement[] {
|
|
375
|
+
if (placements.length !== PLACEMENTS_ORDER.length) {
|
|
376
|
+
return PLACEMENTS_ORDER.filter(item => {
|
|
377
|
+
return placements.includes(item as Placement)
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let _preparePlacements: Placement[] = [...PLACEMENTS_ORDER]
|
|
382
|
+
|
|
383
|
+
const activeIndexPlacement = _preparePlacements.findIndex(item => {
|
|
384
|
+
return item === placement
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
const activePlacement = _preparePlacements[activeIndexPlacement] ?? placement
|
|
388
|
+
const [position, attachment] = activePlacement.split('-') as [Position, Attachment]
|
|
389
|
+
const reversePlacement = `${POSITIONS_REVERSE[position]}-${attachment}` as Placement
|
|
390
|
+
const reverseIndexPlacement = _preparePlacements.findIndex(item => {
|
|
391
|
+
return item === reversePlacement
|
|
392
|
+
})
|
|
393
|
+
_preparePlacements = _preparePlacements.filter(item => {
|
|
394
|
+
return item !== reversePlacement
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
_preparePlacements = [
|
|
398
|
+
..._preparePlacements.slice(0, activeIndexPlacement),
|
|
399
|
+
(PLACEMENTS_ORDER[reverseIndexPlacement] ?? reversePlacement) as Placement,
|
|
400
|
+
..._preparePlacements.slice(activeIndexPlacement)
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
return _preparePlacements
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// TODO: This is unlogical behaviour.
|
|
407
|
+
// We can always find a better position if the specified position does not fit
|
|
408
|
+
function findAvailablePlacement (options: {
|
|
409
|
+
counter?: number
|
|
410
|
+
tetherMeasures: Measures
|
|
411
|
+
placement: Placement
|
|
412
|
+
placements: Placement[]
|
|
413
|
+
leftPositions: Record<Placement, number>
|
|
414
|
+
topPositions: Record<Placement, number>
|
|
415
|
+
initPlacement: Placement
|
|
416
|
+
}): Placement {
|
|
417
|
+
const {
|
|
418
|
+
counter = 1,
|
|
419
|
+
tetherMeasures,
|
|
420
|
+
placement,
|
|
421
|
+
placements,
|
|
422
|
+
leftPositions,
|
|
423
|
+
topPositions,
|
|
424
|
+
initPlacement
|
|
425
|
+
} = options
|
|
426
|
+
|
|
427
|
+
if (counter > placements.length) {
|
|
428
|
+
return initPlacement
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const activeIndexPlacement = placements.findIndex(item => {
|
|
432
|
+
return item === placement
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
let nextIndex = activeIndexPlacement + 1
|
|
436
|
+
if (nextIndex > placements.length - 1) nextIndex = 0
|
|
437
|
+
|
|
438
|
+
const _leftPosition = leftPositions[placement]
|
|
439
|
+
if (_leftPosition < 0 ||
|
|
440
|
+
_leftPosition + tetherMeasures.width > Dimensions.get('window').width) {
|
|
441
|
+
return findAvailablePlacement({
|
|
442
|
+
...options,
|
|
443
|
+
counter: counter + 1,
|
|
444
|
+
placement: placements[nextIndex]
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const _topPosition = topPositions[placement]
|
|
449
|
+
if (_topPosition < 0 ||
|
|
450
|
+
_topPosition + tetherMeasures.height > Dimensions.get('window').height) {
|
|
451
|
+
return findAvailablePlacement({
|
|
452
|
+
...options,
|
|
453
|
+
counter: counter + 1,
|
|
454
|
+
placement: placements[nextIndex]
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return placements[activeIndexPlacement] ?? initPlacement
|
|
459
|
+
}
|
package/index.cssx.styl
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.root
|
|
2
|
+
position absolute
|
|
3
|
+
z-index 1000
|
|
4
|
+
|
|
5
|
+
.arrow
|
|
6
|
+
position absolute
|
|
7
|
+
border-width 1u
|
|
8
|
+
border-style solid
|
|
9
|
+
border-color transparent
|
|
10
|
+
|
|
11
|
+
&.left
|
|
12
|
+
transform rotate(-90deg)
|
|
13
|
+
|
|
14
|
+
&.right
|
|
15
|
+
transform rotate(90deg)
|
|
16
|
+
|
|
17
|
+
&.bottom
|
|
18
|
+
transform rotate(180deg)
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
|
|
3
|
+
|
|
4
|
+
import { type ReactNode } from 'react';
|
|
5
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
6
|
+
import { type AbstractPopoverAttachment as Attachment, type AbstractPopoverPlacement as Placement, type AbstractPopoverPosition as Position } from './constants';
|
|
7
|
+
import './index.cssx.styl';
|
|
8
|
+
export declare const _PropsJsonSchema: {};
|
|
9
|
+
export interface AbstractPopoverProps {
|
|
10
|
+
/** Additional cssx styleName(s) for the popover root */
|
|
11
|
+
styleName?: any;
|
|
12
|
+
/** Custom styles for popover container */
|
|
13
|
+
style?: StyleProp<ViewStyle>;
|
|
14
|
+
/** Ref to the anchor element (must support `measure`) */
|
|
15
|
+
anchorRef: any;
|
|
16
|
+
/** Show/hide popover */
|
|
17
|
+
visible?: boolean;
|
|
18
|
+
/** Primary position @default 'bottom' */
|
|
19
|
+
position?: Position;
|
|
20
|
+
/** Attachment relative to anchor @default 'start' */
|
|
21
|
+
attachment?: Attachment;
|
|
22
|
+
/** Ordered placements fallback list */
|
|
23
|
+
placements?: readonly Placement[];
|
|
24
|
+
/** Show arrow */
|
|
25
|
+
arrow?: boolean;
|
|
26
|
+
/** Match popover width to anchor */
|
|
27
|
+
matchAnchorWidth?: boolean;
|
|
28
|
+
/** Open animation duration (ms) @default 100 */
|
|
29
|
+
durationOpen?: number;
|
|
30
|
+
/** Close animation duration (ms) @default 50 */
|
|
31
|
+
durationClose?: number;
|
|
32
|
+
/** Wrap rendered popover node */
|
|
33
|
+
renderWrapper?: (node: ReactNode) => ReactNode;
|
|
34
|
+
/** Called right before open animation starts */
|
|
35
|
+
onRequestOpen?: () => void;
|
|
36
|
+
/** Called right before close animation starts */
|
|
37
|
+
onRequestClose?: () => void;
|
|
38
|
+
/** Called after open animation ends */
|
|
39
|
+
onOpenComplete?: (finished?: boolean) => void;
|
|
40
|
+
/** Called after close animation ends */
|
|
41
|
+
onCloseComplete?: (finished?: boolean) => void;
|
|
42
|
+
/** Popover content */
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
declare const _default: import("react").ComponentType<AbstractPopoverProps>;
|
|
46
|
+
export default _default;
|
package/index.tsx
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef, useMemo, type ReactNode } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Easing,
|
|
5
|
+
Dimensions,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
View,
|
|
8
|
+
type LayoutChangeEvent,
|
|
9
|
+
type StyleProp,
|
|
10
|
+
type ViewStyle
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
import { pug, observer } from 'startupjs'
|
|
13
|
+
import { themed } from '@startupjs-ui/core'
|
|
14
|
+
import Portal from '@startupjs-ui/portal'
|
|
15
|
+
import getGeometry, { type PopoverGeometry } from './getGeometry'
|
|
16
|
+
import {
|
|
17
|
+
PLACEMENTS_ORDER,
|
|
18
|
+
type AbstractPopoverAttachment as Attachment,
|
|
19
|
+
type AbstractPopoverPlacement as Placement,
|
|
20
|
+
type AbstractPopoverPosition as Position
|
|
21
|
+
} from './constants'
|
|
22
|
+
import './index.cssx.styl'
|
|
23
|
+
|
|
24
|
+
export const _PropsJsonSchema = {/* AbstractPopoverProps */}
|
|
25
|
+
|
|
26
|
+
export interface AbstractPopoverProps {
|
|
27
|
+
/** Additional cssx styleName(s) for the popover root */
|
|
28
|
+
styleName?: any
|
|
29
|
+
/** Custom styles for popover container */
|
|
30
|
+
style?: StyleProp<ViewStyle>
|
|
31
|
+
/** Ref to the anchor element (must support `measure`) */
|
|
32
|
+
anchorRef: any
|
|
33
|
+
/** Show/hide popover */
|
|
34
|
+
visible?: boolean
|
|
35
|
+
/** Primary position @default 'bottom' */
|
|
36
|
+
position?: Position
|
|
37
|
+
/** Attachment relative to anchor @default 'start' */
|
|
38
|
+
attachment?: Attachment
|
|
39
|
+
/** Ordered placements fallback list */
|
|
40
|
+
placements?: readonly Placement[]
|
|
41
|
+
/** Show arrow */
|
|
42
|
+
arrow?: boolean
|
|
43
|
+
/** Match popover width to anchor */
|
|
44
|
+
matchAnchorWidth?: boolean
|
|
45
|
+
/** Open animation duration (ms) @default 100 */
|
|
46
|
+
durationOpen?: number
|
|
47
|
+
/** Close animation duration (ms) @default 50 */
|
|
48
|
+
durationClose?: number
|
|
49
|
+
/** Wrap rendered popover node */
|
|
50
|
+
renderWrapper?: (node: ReactNode) => ReactNode
|
|
51
|
+
/** Called right before open animation starts */
|
|
52
|
+
onRequestOpen?: () => void
|
|
53
|
+
/** Called right before close animation starts */
|
|
54
|
+
onRequestClose?: () => void
|
|
55
|
+
/** Called after open animation ends */
|
|
56
|
+
onOpenComplete?: (finished?: boolean) => void
|
|
57
|
+
/** Called after close animation ends */
|
|
58
|
+
onCloseComplete?: (finished?: boolean) => void
|
|
59
|
+
/** Popover content */
|
|
60
|
+
children?: ReactNode
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function AbstractPopover ({
|
|
64
|
+
visible: visibleProp,
|
|
65
|
+
onCloseComplete,
|
|
66
|
+
position = 'bottom',
|
|
67
|
+
attachment = 'start',
|
|
68
|
+
placements = PLACEMENTS_ORDER,
|
|
69
|
+
arrow = false,
|
|
70
|
+
matchAnchorWidth = false,
|
|
71
|
+
durationOpen = 100,
|
|
72
|
+
durationClose = 50,
|
|
73
|
+
...props
|
|
74
|
+
}: AbstractPopoverProps): ReactNode {
|
|
75
|
+
const [visible, setVisible] = useState(false)
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (visibleProp) setVisible(true)
|
|
79
|
+
}, [visibleProp])
|
|
80
|
+
|
|
81
|
+
const handleCloseComplete = useCallback((finished?: boolean) => {
|
|
82
|
+
setVisible(false)
|
|
83
|
+
onCloseComplete?.(finished)
|
|
84
|
+
}, [onCloseComplete])
|
|
85
|
+
|
|
86
|
+
if (!visible) return null
|
|
87
|
+
|
|
88
|
+
return pug`
|
|
89
|
+
Tether(
|
|
90
|
+
...props
|
|
91
|
+
visible=visibleProp
|
|
92
|
+
position=position
|
|
93
|
+
attachment=attachment
|
|
94
|
+
placements=placements
|
|
95
|
+
arrow=arrow
|
|
96
|
+
matchAnchorWidth=matchAnchorWidth
|
|
97
|
+
durationOpen=durationOpen
|
|
98
|
+
durationClose=durationClose
|
|
99
|
+
onCloseComplete=handleCloseComplete
|
|
100
|
+
)
|
|
101
|
+
`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const Tether = observer(function TetherComponent ({
|
|
105
|
+
styleName,
|
|
106
|
+
style,
|
|
107
|
+
anchorRef,
|
|
108
|
+
visible,
|
|
109
|
+
position = 'bottom',
|
|
110
|
+
attachment = 'start',
|
|
111
|
+
// IDEA: Is this property is redundant?
|
|
112
|
+
// We can always find a better position if the specified position does not fit
|
|
113
|
+
// Also we can use the same logic like in tether.io
|
|
114
|
+
placements = PLACEMENTS_ORDER,
|
|
115
|
+
arrow = false,
|
|
116
|
+
matchAnchorWidth = false,
|
|
117
|
+
durationOpen = 100,
|
|
118
|
+
durationClose = 50,
|
|
119
|
+
renderWrapper,
|
|
120
|
+
onRequestOpen,
|
|
121
|
+
onRequestClose,
|
|
122
|
+
onOpenComplete,
|
|
123
|
+
onCloseComplete,
|
|
124
|
+
children
|
|
125
|
+
}: AbstractPopoverProps): ReactNode {
|
|
126
|
+
const flattenedStyle = StyleSheet.flatten(style) as ViewStyle | undefined
|
|
127
|
+
if (!renderWrapper) renderWrapper = (node): ReactNode => node
|
|
128
|
+
|
|
129
|
+
const [geometry, setGeometry] = useState<PopoverGeometry>()
|
|
130
|
+
const fadeAnim = useRef(new Animated.Value(0)).current
|
|
131
|
+
const dimensions = useMemo(() => ({
|
|
132
|
+
width: Dimensions.get('window').width,
|
|
133
|
+
height: Dimensions.get('window').height
|
|
134
|
+
}), [])
|
|
135
|
+
|
|
136
|
+
const animateIn = useCallback(() => {
|
|
137
|
+
onRequestOpen?.()
|
|
138
|
+
|
|
139
|
+
Animated.parallel([
|
|
140
|
+
Animated.timing(fadeAnim, {
|
|
141
|
+
toValue: 1,
|
|
142
|
+
duration: durationOpen,
|
|
143
|
+
easing: Easing.linear,
|
|
144
|
+
useNativeDriver: true
|
|
145
|
+
})
|
|
146
|
+
]).start(({ finished }) => {
|
|
147
|
+
onOpenComplete?.(finished)
|
|
148
|
+
})
|
|
149
|
+
}, [durationOpen, fadeAnim, onOpenComplete, onRequestOpen])
|
|
150
|
+
|
|
151
|
+
const animateOut = useCallback(() => {
|
|
152
|
+
onRequestClose?.()
|
|
153
|
+
|
|
154
|
+
Animated.timing(fadeAnim, {
|
|
155
|
+
toValue: 0,
|
|
156
|
+
duration: durationClose,
|
|
157
|
+
easing: Easing.linear,
|
|
158
|
+
useNativeDriver: true
|
|
159
|
+
}).start(({ finished }) => {
|
|
160
|
+
onCloseComplete?.(finished)
|
|
161
|
+
})
|
|
162
|
+
}, [durationClose, fadeAnim, onCloseComplete, onRequestClose])
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (!geometry) return
|
|
166
|
+
if (visible) animateIn()
|
|
167
|
+
else animateOut()
|
|
168
|
+
}, [animateIn, animateOut, geometry, visible])
|
|
169
|
+
|
|
170
|
+
const calculateGeometry = useCallback(({ nativeEvent }: LayoutChangeEvent) => {
|
|
171
|
+
// IDEA: we can pass measures to this component
|
|
172
|
+
// instead of passing ref for the measurement
|
|
173
|
+
// Also, add property that will manage where popover should appear
|
|
174
|
+
// in portal or in at the place where component is called
|
|
175
|
+
// maybe use PortalProvider to render it in the place where component is called
|
|
176
|
+
anchorRef?.current?.measure?.((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => {
|
|
177
|
+
// IDEA: rewrite getGeometry in future
|
|
178
|
+
// we can make geometry behaviout like in tether.js
|
|
179
|
+
const geometry = getGeometry({
|
|
180
|
+
placement: `${position}-${attachment}` as Placement,
|
|
181
|
+
placements,
|
|
182
|
+
arrow,
|
|
183
|
+
matchAnchorWidth,
|
|
184
|
+
dimensions,
|
|
185
|
+
tetherMeasures: nativeEvent.layout,
|
|
186
|
+
anchorMeasures: { x, y, width, height, pageX, pageY }
|
|
187
|
+
})
|
|
188
|
+
setGeometry(geometry)
|
|
189
|
+
})
|
|
190
|
+
}, [anchorRef, arrow, attachment, dimensions, matchAnchorWidth, placements, position])
|
|
191
|
+
|
|
192
|
+
// WORKAROUND
|
|
193
|
+
// the minimum height fixes an issue where the 'onLayout' does not trigger
|
|
194
|
+
// when children are undefined or have no size.
|
|
195
|
+
const rootStyle: any = {
|
|
196
|
+
top: geometry ? geometry.top : -99999,
|
|
197
|
+
left: geometry ? geometry.left : -99999,
|
|
198
|
+
opacity: fadeAnim,
|
|
199
|
+
minHeight: 1
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (geometry) {
|
|
203
|
+
rootStyle.width = geometry.width
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const popover = pug`
|
|
207
|
+
Animated.View.root(
|
|
208
|
+
style=[flattenedStyle, rootStyle]
|
|
209
|
+
styleName=styleName
|
|
210
|
+
onLayout=calculateGeometry
|
|
211
|
+
)
|
|
212
|
+
if arrow && !!geometry
|
|
213
|
+
View.arrow(
|
|
214
|
+
style={
|
|
215
|
+
borderTopColor: flattenedStyle?.backgroundColor,
|
|
216
|
+
left: geometry.arrowLeft,
|
|
217
|
+
top: geometry.arrowTop
|
|
218
|
+
}
|
|
219
|
+
styleName=[geometry.position]
|
|
220
|
+
)
|
|
221
|
+
= children
|
|
222
|
+
`
|
|
223
|
+
|
|
224
|
+
return pug`
|
|
225
|
+
Portal
|
|
226
|
+
= renderWrapper(popover)
|
|
227
|
+
`
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
export default observer(themed('AbstractPopover', AbstractPopover))
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@startupjs-ui/abstract-popover",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"main": "index.tsx",
|
|
8
|
+
"types": "index.d.ts",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@startupjs-ui/core": "^0.1.3",
|
|
12
|
+
"@startupjs-ui/portal": "^0.1.3"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"react": "*",
|
|
16
|
+
"react-native": "*",
|
|
17
|
+
"startupjs": "*"
|
|
18
|
+
},
|
|
19
|
+
"gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
|
|
20
|
+
}
|