@tamagui/popover 1.0.1-beta.59 → 1.0.1-beta.60
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 +12 -11
- package/src/Popover.tsx +552 -0
- package/src/index.tsx +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamagui/popover",
|
|
3
|
-
"version": "1.0.1-beta.
|
|
3
|
+
"version": "1.0.1-beta.60",
|
|
4
4
|
"sideEffects": true,
|
|
5
5
|
"source": "src/index.ts",
|
|
6
6
|
"types": "./types/index.d.ts",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"module": "dist/esm",
|
|
9
9
|
"module:jsx": "dist/jsx",
|
|
10
10
|
"files": [
|
|
11
|
+
"src",
|
|
11
12
|
"types",
|
|
12
13
|
"dist"
|
|
13
14
|
],
|
|
@@ -19,15 +20,15 @@
|
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
22
|
"@floating-ui/react-dom-interactions": "^0.5.0",
|
|
22
|
-
"@tamagui/animate-presence": "^1.0.1-beta.
|
|
23
|
-
"@tamagui/compose-refs": "^1.0.1-beta.
|
|
24
|
-
"@tamagui/core": "^1.0.1-beta.
|
|
25
|
-
"@tamagui/create-context": "^1.0.1-beta.
|
|
26
|
-
"@tamagui/polyfill-dev": "^1.0.1-beta.
|
|
27
|
-
"@tamagui/popper": "^1.0.1-beta.
|
|
28
|
-
"@tamagui/portal": "^1.0.1-beta.
|
|
29
|
-
"@tamagui/stacks": "^1.0.1-beta.
|
|
30
|
-
"@tamagui/use-controllable-state": "^1.0.1-beta.
|
|
23
|
+
"@tamagui/animate-presence": "^1.0.1-beta.60",
|
|
24
|
+
"@tamagui/compose-refs": "^1.0.1-beta.60",
|
|
25
|
+
"@tamagui/core": "^1.0.1-beta.60",
|
|
26
|
+
"@tamagui/create-context": "^1.0.1-beta.60",
|
|
27
|
+
"@tamagui/polyfill-dev": "^1.0.1-beta.60",
|
|
28
|
+
"@tamagui/popper": "^1.0.1-beta.60",
|
|
29
|
+
"@tamagui/portal": "^1.0.1-beta.60",
|
|
30
|
+
"@tamagui/stacks": "^1.0.1-beta.60",
|
|
31
|
+
"@tamagui/use-controllable-state": "^1.0.1-beta.60"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
33
34
|
"react": "*",
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"react-native": "*"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
|
-
"@tamagui/build": "^1.0.1-beta.
|
|
39
|
+
"@tamagui/build": "^1.0.1-beta.60",
|
|
39
40
|
"@types/react-dom": "^18.0.3",
|
|
40
41
|
"@types/react-native": "^0.67.3",
|
|
41
42
|
"react": "*",
|
package/src/Popover.tsx
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
// adapted from radix-ui popover
|
|
2
|
+
|
|
3
|
+
import '@tamagui/polyfill-dev'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
useDismiss,
|
|
7
|
+
useFloating,
|
|
8
|
+
useFocus,
|
|
9
|
+
useInteractions,
|
|
10
|
+
useRole,
|
|
11
|
+
} from '@floating-ui/react-dom-interactions'
|
|
12
|
+
import { AnimatePresence } from '@tamagui/animate-presence'
|
|
13
|
+
import { useComposedRefs } from '@tamagui/compose-refs'
|
|
14
|
+
import {
|
|
15
|
+
Theme,
|
|
16
|
+
composeEventHandlers,
|
|
17
|
+
useId,
|
|
18
|
+
useThemeName,
|
|
19
|
+
withStaticProperties,
|
|
20
|
+
} from '@tamagui/core'
|
|
21
|
+
import type { Scope } from '@tamagui/create-context'
|
|
22
|
+
import { createContextScope } from '@tamagui/create-context'
|
|
23
|
+
// import { DismissableLayer } from '@tamagui/react-dismissable-layer'
|
|
24
|
+
// import { useFocusGuards } from '@tamagui/react-focus-guards'
|
|
25
|
+
// import { FocusScope } from '@tamagui/react-focus-scope'
|
|
26
|
+
import {
|
|
27
|
+
FloatingOverrideContext,
|
|
28
|
+
Popper,
|
|
29
|
+
PopperAnchor,
|
|
30
|
+
PopperArrow,
|
|
31
|
+
PopperContent,
|
|
32
|
+
PopperContentProps,
|
|
33
|
+
PopperProps,
|
|
34
|
+
UseFloatingFn,
|
|
35
|
+
createPopperScope,
|
|
36
|
+
} from '@tamagui/popper'
|
|
37
|
+
import { Portal } from '@tamagui/portal'
|
|
38
|
+
import { YStack, YStackProps } from '@tamagui/stacks'
|
|
39
|
+
import { useControllableState } from '@tamagui/use-controllable-state'
|
|
40
|
+
// import { hideOthers } from 'aria-hidden'
|
|
41
|
+
import * as React from 'react'
|
|
42
|
+
import { GestureResponderEvent, View } from 'react-native'
|
|
43
|
+
|
|
44
|
+
// import { RemoveScroll } from 'react-remove-scroll'
|
|
45
|
+
|
|
46
|
+
const POPOVER_NAME = 'Popover'
|
|
47
|
+
|
|
48
|
+
type ScopedProps<P> = P & { __scopePopover?: Scope }
|
|
49
|
+
const [createPopoverContext, createPopoverScopeInternal] = createContextScope(POPOVER_NAME, [
|
|
50
|
+
createPopperScope,
|
|
51
|
+
])
|
|
52
|
+
export const usePopoverScope = createPopperScope()
|
|
53
|
+
export const createPopoverScope = createPopoverScopeInternal
|
|
54
|
+
|
|
55
|
+
type PopoverContextValue = {
|
|
56
|
+
triggerRef: React.RefObject<HTMLButtonElement>
|
|
57
|
+
contentId?: string
|
|
58
|
+
open: boolean
|
|
59
|
+
onOpenChange(open: boolean): void
|
|
60
|
+
onOpenToggle(): void
|
|
61
|
+
hasCustomAnchor: boolean
|
|
62
|
+
onCustomAnchorAdd(): void
|
|
63
|
+
onCustomAnchorRemove(): void
|
|
64
|
+
modal: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const [PopoverProviderInternal, usePopoverInternalContext] =
|
|
68
|
+
createPopoverContext<PopoverContextValue>(POPOVER_NAME)
|
|
69
|
+
|
|
70
|
+
export const __PopoverProviderInternal = PopoverProviderInternal
|
|
71
|
+
|
|
72
|
+
/* -------------------------------------------------------------------------------------------------
|
|
73
|
+
* PopoverAnchor
|
|
74
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
75
|
+
|
|
76
|
+
const ANCHOR_NAME = 'PopoverAnchor'
|
|
77
|
+
|
|
78
|
+
type PopoverAnchorElement = HTMLElement | View
|
|
79
|
+
export type PopoverAnchorProps = YStackProps
|
|
80
|
+
|
|
81
|
+
export const PopoverAnchor = React.forwardRef<PopoverAnchorElement, PopoverAnchorProps>(
|
|
82
|
+
(props: ScopedProps<PopoverAnchorProps>, forwardedRef) => {
|
|
83
|
+
const { __scopePopover, ...anchorProps } = props
|
|
84
|
+
const context = usePopoverInternalContext(ANCHOR_NAME, __scopePopover)
|
|
85
|
+
const popperScope = usePopoverScope(__scopePopover)
|
|
86
|
+
const { onCustomAnchorAdd, onCustomAnchorRemove } = context
|
|
87
|
+
|
|
88
|
+
React.useEffect(() => {
|
|
89
|
+
onCustomAnchorAdd()
|
|
90
|
+
return () => onCustomAnchorRemove()
|
|
91
|
+
}, [onCustomAnchorAdd, onCustomAnchorRemove])
|
|
92
|
+
|
|
93
|
+
return <PopperAnchor {...popperScope} {...anchorProps} ref={forwardedRef} />
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
PopoverAnchor.displayName = ANCHOR_NAME
|
|
98
|
+
|
|
99
|
+
/* -------------------------------------------------------------------------------------------------
|
|
100
|
+
* PopoverTrigger
|
|
101
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
102
|
+
|
|
103
|
+
const TRIGGER_NAME = 'PopoverTrigger'
|
|
104
|
+
|
|
105
|
+
type PopoverTriggerElement = HTMLElement | View
|
|
106
|
+
export type PopoverTriggerProps = YStackProps
|
|
107
|
+
|
|
108
|
+
export const PopoverTrigger = React.forwardRef<PopoverTriggerElement, PopoverTriggerProps>(
|
|
109
|
+
(props: ScopedProps<PopoverTriggerProps>, forwardedRef) => {
|
|
110
|
+
const { __scopePopover, ...triggerProps } = props
|
|
111
|
+
const context = usePopoverInternalContext(TRIGGER_NAME, __scopePopover)
|
|
112
|
+
const popperScope = usePopoverScope(__scopePopover)
|
|
113
|
+
const composedTriggerRef = useComposedRefs(forwardedRef, context.triggerRef)
|
|
114
|
+
|
|
115
|
+
const trigger = (
|
|
116
|
+
<YStack
|
|
117
|
+
aria-haspopup="dialog"
|
|
118
|
+
aria-expanded={context.open}
|
|
119
|
+
aria-controls={context.contentId}
|
|
120
|
+
data-state={getState(context.open)}
|
|
121
|
+
{...triggerProps}
|
|
122
|
+
ref={composedTriggerRef}
|
|
123
|
+
onPress={composeEventHandlers(props.onPress, context.onOpenToggle)}
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return context.hasCustomAnchor ? (
|
|
128
|
+
trigger
|
|
129
|
+
) : (
|
|
130
|
+
<PopperAnchor asChild {...popperScope}>
|
|
131
|
+
{trigger}
|
|
132
|
+
</PopperAnchor>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
PopoverTrigger.displayName = TRIGGER_NAME
|
|
138
|
+
|
|
139
|
+
/* -------------------------------------------------------------------------------------------------
|
|
140
|
+
* PopoverContent
|
|
141
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
142
|
+
|
|
143
|
+
const CONTENT_NAME = 'PopoverContent'
|
|
144
|
+
|
|
145
|
+
export interface PopoverContentTypeProps
|
|
146
|
+
extends Omit<PopoverContentImplProps, 'trapFocus' | 'disableOutsidePointerEvents'> {
|
|
147
|
+
/**
|
|
148
|
+
* @see https://github.com/theKashey/react-remove-scroll#usage
|
|
149
|
+
*/
|
|
150
|
+
allowPinchZoom?: RemoveScrollProps['allowPinchZoom']
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type PopoverContentProps = PopoverContentTypeProps
|
|
154
|
+
|
|
155
|
+
export const PopoverContent = React.forwardRef<PopoverContentTypeElement, PopoverContentProps>(
|
|
156
|
+
(props: ScopedProps<PopoverContentProps>, forwardedRef) => {
|
|
157
|
+
const context = usePopoverInternalContext(CONTENT_NAME, props.__scopePopover)
|
|
158
|
+
return context.modal ? (
|
|
159
|
+
<PopoverContentModal {...props} ref={forwardedRef} />
|
|
160
|
+
) : (
|
|
161
|
+
<PopoverContentNonModal {...props} ref={forwardedRef} />
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
PopoverContent.displayName = CONTENT_NAME
|
|
167
|
+
|
|
168
|
+
/* -----------------------------------------------------------------------------------------------*/
|
|
169
|
+
|
|
170
|
+
type RemoveScrollProps = any //React.ComponentProps<typeof RemoveScroll>
|
|
171
|
+
type PopoverContentTypeElement = PopoverContentImplElement
|
|
172
|
+
|
|
173
|
+
const PopoverContentModal = React.forwardRef<PopoverContentTypeElement, PopoverContentTypeProps>(
|
|
174
|
+
(props: ScopedProps<PopoverContentTypeProps>, forwardedRef) => {
|
|
175
|
+
const { allowPinchZoom, ...contentModalProps } = props
|
|
176
|
+
const context = usePopoverInternalContext(CONTENT_NAME, props.__scopePopover)
|
|
177
|
+
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
178
|
+
const composedRefs = useComposedRefs(forwardedRef, contentRef)
|
|
179
|
+
const isRightClickOutsideRef = React.useRef(false)
|
|
180
|
+
const themeName = useThemeName()
|
|
181
|
+
|
|
182
|
+
// aria-hide everything except the content (better supported equivalent to setting aria-modal)
|
|
183
|
+
// React.useEffect(() => {
|
|
184
|
+
// const content = contentRef.current
|
|
185
|
+
// if (content) return hideOthers(content)
|
|
186
|
+
// }, [])
|
|
187
|
+
|
|
188
|
+
const PortalWrapper = context.modal ? Portal : React.Fragment
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<PortalWrapper>
|
|
192
|
+
<Theme name={themeName}>
|
|
193
|
+
{/* <RemoveScroll allowPinchZoom={allowPinchZoom}> */}
|
|
194
|
+
<PopoverContentImpl
|
|
195
|
+
{...contentModalProps}
|
|
196
|
+
ref={composedRefs}
|
|
197
|
+
// we make sure we're not trapping once it's been closed
|
|
198
|
+
// (closed !== unmounted when animating out)
|
|
199
|
+
trapFocus={context.open}
|
|
200
|
+
disableOutsidePointerEvents
|
|
201
|
+
onCloseAutoFocus={composeEventHandlers(props.onCloseAutoFocus, (event) => {
|
|
202
|
+
event.preventDefault()
|
|
203
|
+
if (!isRightClickOutsideRef.current) context.triggerRef.current?.focus()
|
|
204
|
+
})}
|
|
205
|
+
onPointerDownOutside={composeEventHandlers(
|
|
206
|
+
props.onPointerDownOutside,
|
|
207
|
+
(event) => {
|
|
208
|
+
// @ts-expect-error
|
|
209
|
+
const originalEvent = event.detail.originalEvent
|
|
210
|
+
const ctrlLeftClick = originalEvent.button === 0 && originalEvent.ctrlKey === true
|
|
211
|
+
const isRightClick = originalEvent.button === 2 || ctrlLeftClick
|
|
212
|
+
isRightClickOutsideRef.current = isRightClick
|
|
213
|
+
},
|
|
214
|
+
{ checkDefaultPrevented: false }
|
|
215
|
+
)}
|
|
216
|
+
// When focus is trapped, a `focusout` event may still happen.
|
|
217
|
+
// We make sure we don't trigger our `onDismiss` in such case.
|
|
218
|
+
onFocusOutside={composeEventHandlers(
|
|
219
|
+
props.onFocusOutside,
|
|
220
|
+
(event) => event.preventDefault(),
|
|
221
|
+
{ checkDefaultPrevented: false }
|
|
222
|
+
)}
|
|
223
|
+
/>
|
|
224
|
+
</Theme>
|
|
225
|
+
{/* </RemoveScroll> */}
|
|
226
|
+
</PortalWrapper>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const PopoverContentNonModal = React.forwardRef<PopoverContentTypeElement, PopoverContentTypeProps>(
|
|
232
|
+
(props: ScopedProps<PopoverContentTypeProps>, forwardedRef) => {
|
|
233
|
+
const { ...contentNonModalProps } = props
|
|
234
|
+
const context = usePopoverInternalContext(CONTENT_NAME, props.__scopePopover)
|
|
235
|
+
const hasInteractedOutsideRef = React.useRef(false)
|
|
236
|
+
const PortalWrapper = context.modal ? Portal : React.Fragment
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<PortalWrapper>
|
|
240
|
+
<PopoverContentImpl
|
|
241
|
+
{...contentNonModalProps}
|
|
242
|
+
ref={forwardedRef}
|
|
243
|
+
trapFocus={false}
|
|
244
|
+
disableOutsidePointerEvents={false}
|
|
245
|
+
onCloseAutoFocus={(event) => {
|
|
246
|
+
props.onCloseAutoFocus?.(event)
|
|
247
|
+
|
|
248
|
+
if (!event.defaultPrevented) {
|
|
249
|
+
if (!hasInteractedOutsideRef.current) context.triggerRef.current?.focus()
|
|
250
|
+
// Always prevent auto focus because we either focus manually or want user agent focus
|
|
251
|
+
event.preventDefault()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
hasInteractedOutsideRef.current = false
|
|
255
|
+
}}
|
|
256
|
+
onInteractOutside={(event) => {
|
|
257
|
+
props.onInteractOutside?.(event)
|
|
258
|
+
|
|
259
|
+
if (!event.defaultPrevented) hasInteractedOutsideRef.current = true
|
|
260
|
+
|
|
261
|
+
// Prevent dismissing when clicking the trigger.
|
|
262
|
+
// As the trigger is already setup to close, without doing so would
|
|
263
|
+
// cause it to close and immediately open.
|
|
264
|
+
//
|
|
265
|
+
// We use `onInteractOutside` as some browsers also
|
|
266
|
+
// focus on pointer down, creating the same issue.
|
|
267
|
+
// @ts-ignore
|
|
268
|
+
const target = event.target as HTMLElement
|
|
269
|
+
const targetIsTrigger = context.triggerRef.current?.contains(target)
|
|
270
|
+
if (targetIsTrigger) event.preventDefault()
|
|
271
|
+
}}
|
|
272
|
+
/>
|
|
273
|
+
</PortalWrapper>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
/* -----------------------------------------------------------------------------------------------*/
|
|
279
|
+
|
|
280
|
+
type PopoverContentImplElement = React.ElementRef<typeof PopperContent>
|
|
281
|
+
|
|
282
|
+
// TODO
|
|
283
|
+
type FocusScopeProps = {
|
|
284
|
+
/**
|
|
285
|
+
* Whether focus should be trapped within the FocusScope
|
|
286
|
+
* (default: false)
|
|
287
|
+
*/
|
|
288
|
+
trapped?: boolean
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Event handler called when auto-focusing on mount.
|
|
292
|
+
* Can be prevented.
|
|
293
|
+
*/
|
|
294
|
+
onMountAutoFocus?: (event: GestureResponderEvent) => void
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Event handler called when auto-focusing on unmount.
|
|
298
|
+
* Can be prevented.
|
|
299
|
+
*/
|
|
300
|
+
onUnmountAutoFocus?: (event: GestureResponderEvent) => void
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
type DismissableLayerProps = {
|
|
304
|
+
/**
|
|
305
|
+
* When `true`, hover/focus/click interactions will be disabled on elements outside the `DismissableLayer`.
|
|
306
|
+
* Users will need to click twice on outside elements to interact with them:
|
|
307
|
+
* Once to close the `DismissableLayer`, and again to trigger the element.
|
|
308
|
+
*/
|
|
309
|
+
disableOutsidePointerEvents?: boolean
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Event handler called when the escape key is down.
|
|
313
|
+
* Can be prevented.
|
|
314
|
+
*/
|
|
315
|
+
onEscapeKeyDown?: (event: KeyboardEvent) => void
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Event handler called when the a pointer event happens outside of the `DismissableLayer`.
|
|
319
|
+
* Can be prevented.
|
|
320
|
+
*/
|
|
321
|
+
onPointerDownOutside?: (event: GestureResponderEvent) => void
|
|
322
|
+
// onPointerDownOutside?: (event: MouseEvent | TouchEvent) => void
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Event handler called when the focus moves outside of the `DismissableLayer`.
|
|
326
|
+
* Can be prevented.
|
|
327
|
+
*/
|
|
328
|
+
onFocusOutside?: (event: React.FocusEvent) => void
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Event handler called when an interaction happens outside the `DismissableLayer`.
|
|
332
|
+
* Specifically, when a pointer event happens outside of the `DismissableLayer` or focus moves outside of it.
|
|
333
|
+
* Can be prevented.
|
|
334
|
+
*/
|
|
335
|
+
onInteractOutside?: (event: GestureResponderEvent) => void
|
|
336
|
+
// onInteractOutside?: (event: MouseEvent | TouchEvent | React.FocusEvent) => void
|
|
337
|
+
|
|
338
|
+
/** Callback called when the `DismissableLayer` should be dismissed */
|
|
339
|
+
onDismiss?: () => void
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export interface PopoverContentImplProps
|
|
343
|
+
extends PopperContentProps,
|
|
344
|
+
Omit<DismissableLayerProps, 'onDismiss'> {
|
|
345
|
+
/**
|
|
346
|
+
* Whether focus should be trapped within the `Popover`
|
|
347
|
+
* (default: false)
|
|
348
|
+
*/
|
|
349
|
+
trapFocus?: FocusScopeProps['trapped']
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Event handler called when auto-focusing on open.
|
|
353
|
+
* Can be prevented.
|
|
354
|
+
*/
|
|
355
|
+
onOpenAutoFocus?: FocusScopeProps['onMountAutoFocus']
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Event handler called when auto-focusing on close.
|
|
359
|
+
* Can be prevented.
|
|
360
|
+
*/
|
|
361
|
+
onCloseAutoFocus?: FocusScopeProps['onUnmountAutoFocus']
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const PopoverContentImpl = React.forwardRef<PopoverContentImplElement, PopoverContentImplProps>(
|
|
365
|
+
(props: ScopedProps<PopoverContentImplProps>, forwardedRef) => {
|
|
366
|
+
const {
|
|
367
|
+
__scopePopover,
|
|
368
|
+
trapFocus,
|
|
369
|
+
onOpenAutoFocus,
|
|
370
|
+
onCloseAutoFocus,
|
|
371
|
+
disableOutsidePointerEvents,
|
|
372
|
+
onEscapeKeyDown,
|
|
373
|
+
onPointerDownOutside,
|
|
374
|
+
onFocusOutside,
|
|
375
|
+
onInteractOutside,
|
|
376
|
+
...contentProps
|
|
377
|
+
} = props
|
|
378
|
+
const context = usePopoverInternalContext(CONTENT_NAME, __scopePopover)
|
|
379
|
+
const popperScope = usePopoverScope(__scopePopover)
|
|
380
|
+
|
|
381
|
+
// Make sure the whole tree has focus guards as our `Popover` may be
|
|
382
|
+
// the last element in the DOM (beacuse of the `Portal`)
|
|
383
|
+
// useFocusGuards()
|
|
384
|
+
|
|
385
|
+
// <FocusScope
|
|
386
|
+
// asChild
|
|
387
|
+
// loop
|
|
388
|
+
// trapped={trapFocus}
|
|
389
|
+
// onMountAutoFocus={onOpenAutoFocus}
|
|
390
|
+
// onUnmountAutoFocus={onCloseAutoFocus}
|
|
391
|
+
// >
|
|
392
|
+
// <DismissableLayer
|
|
393
|
+
// asChild
|
|
394
|
+
// disableOutsidePointerEvents={disableOutsidePointerEvents}
|
|
395
|
+
// onInteractOutside={onInteractOutside}
|
|
396
|
+
// onEscapeKeyDown={onEscapeKeyDown}
|
|
397
|
+
// onPointerDownOutside={onPointerDownOutside}
|
|
398
|
+
// onFocusOutside={onFocusOutside}
|
|
399
|
+
// onDismiss={() => context.onOpenChange(false)}
|
|
400
|
+
// >
|
|
401
|
+
return (
|
|
402
|
+
<AnimatePresence>
|
|
403
|
+
{!!context.open && (
|
|
404
|
+
<PopperContent
|
|
405
|
+
data-state={getState(context.open)}
|
|
406
|
+
// role="dialog"
|
|
407
|
+
id={context.contentId}
|
|
408
|
+
pointerEvents="auto"
|
|
409
|
+
{...popperScope}
|
|
410
|
+
{...contentProps}
|
|
411
|
+
ref={forwardedRef}
|
|
412
|
+
/>
|
|
413
|
+
)}
|
|
414
|
+
</AnimatePresence>
|
|
415
|
+
)
|
|
416
|
+
// </DismissableLayer>
|
|
417
|
+
// </FocusScope>
|
|
418
|
+
}
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
/* -------------------------------------------------------------------------------------------------
|
|
422
|
+
* PopoverClose
|
|
423
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
424
|
+
|
|
425
|
+
const CLOSE_NAME = 'PopoverClose'
|
|
426
|
+
|
|
427
|
+
type PopoverCloseElement = HTMLElement | View
|
|
428
|
+
export type PopoverCloseProps = YStackProps
|
|
429
|
+
|
|
430
|
+
export const PopoverClose = React.forwardRef<PopoverCloseElement, PopoverCloseProps>(
|
|
431
|
+
(props: ScopedProps<PopoverCloseProps>, forwardedRef) => {
|
|
432
|
+
const { __scopePopover, ...closeProps } = props
|
|
433
|
+
const context = usePopoverInternalContext(CLOSE_NAME, __scopePopover)
|
|
434
|
+
return (
|
|
435
|
+
<YStack
|
|
436
|
+
{...closeProps}
|
|
437
|
+
ref={forwardedRef}
|
|
438
|
+
onPress={composeEventHandlers(props.onPress, () => context.onOpenChange(false))}
|
|
439
|
+
/>
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
PopoverClose.displayName = CLOSE_NAME
|
|
445
|
+
|
|
446
|
+
/* -------------------------------------------------------------------------------------------------
|
|
447
|
+
* PopoverArrow
|
|
448
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
449
|
+
|
|
450
|
+
const ARROW_NAME = 'PopoverArrow'
|
|
451
|
+
|
|
452
|
+
type PopoverArrowElement = HTMLElement | View
|
|
453
|
+
export type PopoverArrowProps = YStackProps
|
|
454
|
+
|
|
455
|
+
export const PopoverArrow = React.forwardRef<PopoverArrowElement, PopoverArrowProps>(
|
|
456
|
+
(props: ScopedProps<PopoverArrowProps>, forwardedRef) => {
|
|
457
|
+
const { __scopePopover, ...arrowProps } = props
|
|
458
|
+
const popperScope = usePopoverScope(__scopePopover)
|
|
459
|
+
return <PopperArrow {...popperScope} {...arrowProps} ref={forwardedRef} />
|
|
460
|
+
}
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
PopoverArrow.displayName = ARROW_NAME
|
|
464
|
+
|
|
465
|
+
/* -------------------------------------------------------------------------------------------------
|
|
466
|
+
* Popover
|
|
467
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
468
|
+
|
|
469
|
+
export type PopoverProps = PopperProps & {
|
|
470
|
+
open?: boolean
|
|
471
|
+
defaultOpen?: boolean
|
|
472
|
+
onOpenChange?: (open: boolean) => void
|
|
473
|
+
modal?: boolean
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export const Popover = withStaticProperties(
|
|
477
|
+
((props: ScopedProps<PopoverProps>) => {
|
|
478
|
+
const {
|
|
479
|
+
__scopePopover,
|
|
480
|
+
children,
|
|
481
|
+
open: openProp,
|
|
482
|
+
defaultOpen,
|
|
483
|
+
onOpenChange,
|
|
484
|
+
modal = true,
|
|
485
|
+
...restProps
|
|
486
|
+
} = props
|
|
487
|
+
const popperScope = usePopoverScope(__scopePopover)
|
|
488
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
489
|
+
const [hasCustomAnchor, setHasCustomAnchor] = React.useState(false)
|
|
490
|
+
const [open, setOpen] = useControllableState({
|
|
491
|
+
prop: openProp,
|
|
492
|
+
defaultProp: defaultOpen || false,
|
|
493
|
+
onChange: onOpenChange,
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
const useFloatingFn: UseFloatingFn = (props) => {
|
|
497
|
+
const floating = useFloating({
|
|
498
|
+
...props,
|
|
499
|
+
open,
|
|
500
|
+
onOpenChange: setOpen,
|
|
501
|
+
})
|
|
502
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([
|
|
503
|
+
useFocus(floating.context),
|
|
504
|
+
useRole(floating.context, { role: 'dialog' }),
|
|
505
|
+
useDismiss(floating.context),
|
|
506
|
+
])
|
|
507
|
+
return {
|
|
508
|
+
...floating,
|
|
509
|
+
getReferenceProps,
|
|
510
|
+
getFloatingProps,
|
|
511
|
+
} as any
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const useFloatingContext = React.useCallback(useFloatingFn, [open])
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<FloatingOverrideContext.Provider value={useFloatingContext}>
|
|
518
|
+
<Popper {...popperScope} {...restProps}>
|
|
519
|
+
<PopoverProviderInternal
|
|
520
|
+
scope={__scopePopover}
|
|
521
|
+
contentId={useId()}
|
|
522
|
+
triggerRef={triggerRef}
|
|
523
|
+
open={open}
|
|
524
|
+
onOpenChange={setOpen}
|
|
525
|
+
onOpenToggle={React.useCallback(() => setOpen((prevOpen) => !prevOpen), [setOpen])}
|
|
526
|
+
hasCustomAnchor={hasCustomAnchor}
|
|
527
|
+
onCustomAnchorAdd={React.useCallback(() => setHasCustomAnchor(true), [])}
|
|
528
|
+
onCustomAnchorRemove={React.useCallback(() => setHasCustomAnchor(false), [])}
|
|
529
|
+
modal={modal}
|
|
530
|
+
>
|
|
531
|
+
{children}
|
|
532
|
+
</PopoverProviderInternal>
|
|
533
|
+
</Popper>
|
|
534
|
+
</FloatingOverrideContext.Provider>
|
|
535
|
+
)
|
|
536
|
+
}) as React.FC<PopoverProps>,
|
|
537
|
+
{
|
|
538
|
+
Anchor: PopoverAnchor,
|
|
539
|
+
Arrow: PopoverArrow,
|
|
540
|
+
Trigger: PopoverTrigger,
|
|
541
|
+
Content: PopoverContent,
|
|
542
|
+
Close: PopoverClose,
|
|
543
|
+
}
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
Popover.displayName = POPOVER_NAME
|
|
547
|
+
|
|
548
|
+
/* -----------------------------------------------------------------------------------------------*/
|
|
549
|
+
|
|
550
|
+
function getState(open: boolean) {
|
|
551
|
+
return open ? 'open' : 'closed'
|
|
552
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Popover'
|