@tamagui/popover 1.0.1-beta.21 → 1.0.1-beta.211

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