summaryrefslogtreecommitdiffhomepage
path: root/client/web/src/ui/popover.tsx
blob: 0139894bb5e4ad613089f6a5848ce095d7fc4ae6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

import * as PopoverPrimitive from "@radix-ui/react-popover"
import cx from "classnames"
import React, { ReactNode } from "react"
import PortalContainerContext from "src/ui/portal-container-context"

type Props = {
  className?: string
  content: ReactNode
  children: ReactNode

  /**
   * asChild renders the trigger element without wrapping it in a button. Use
   * this when you want to use a `button` element as the trigger.
   */
  asChild?: boolean
  /**
   * side is the side of the direction from the target element to render the
   * popover.
   */
  side?: "top" | "bottom" | "left" | "right"
  /**
   * sideOffset is how far from a give side to render the popover.
   */
  sideOffset?: number
  /**
   * align is how to align the popover with the target element.
   */
  align?: "start" | "center" | "end"
  /**
   * alignOffset is how far off of the alignment point to render the popover.
   */
  alignOffset?: number

  open?: boolean
  onOpenChange?: (open: boolean) => void
}

/**
 * Popover is a UI component that allows rendering unique controls in a floating
 * popover, attached to a trigger element. It appears on click and manages focus
 * on its own behalf.
 *
 * To use the Popover, pass the content as children, and give it a `trigger`:
 *
 *    <Popover trigger={<span>Open popover</span>}>
 *      <p>Hello world!</p>
 *    </Popover>
 *
 * By default, the toggle is wrapped in an accessible <button> tag. You can
 * customize by providing your own button and using the `asChild` prop.
 *
 *    <Popover trigger={<Button>Hello</Button>} asChild>
 *      <p>Hello world!</p>
 *    </Popover>
 *
 * The former style is recommended whenever possible.
 */
export default function Popover(props: Props) {
  const {
    children,
    className,
    content,
    side,
    sideOffset,
    align,
    alignOffset,
    asChild,
    open,
    onOpenChange,
  } = props

  return (
    <PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
      <PopoverPrimitive.Trigger asChild={asChild}>
        {children}
      </PopoverPrimitive.Trigger>
      <PortalContainerContext.Consumer>
        {(portalContainer) => (
          <PopoverPrimitive.Portal container={portalContainer}>
            <PopoverPrimitive.Content
              className={cx(
                "origin-radix-popover shadow-popover bg-white rounded-md z-50",
                "state-open:animate-scale-in state-closed:animate-scale-out",
                className
              )}
              side={side}
              sideOffset={sideOffset}
              align={align}
              alignOffset={alignOffset}
              collisionPadding={12}
            >
              {content}
            </PopoverPrimitive.Content>
          </PopoverPrimitive.Portal>
        )}
      </PortalContainerContext.Consumer>
    </PopoverPrimitive.Root>
  )
}

Popover.defaultProps = {
  sideOffset: 10,
}