forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {Children, createContext, useContext, useMemo} from 'react'
2import {View} from 'react-native'
3import {utils} from '@bsky.app/alf'
4import {Popover} from 'radix-ui'
5
6import {atoms as a, flatten, select, useTheme} from '#/alf'
7import {
8 ARROW_SIZE,
9 BUBBLE_MAX_WIDTH,
10 MIN_EDGE_SPACE,
11} from '#/components/Tooltip/const'
12import {Text} from '#/components/Typography'
13
14// Portal Provider on native, but we actually don't need to do anything here
15export function Provider({children}: {children: React.ReactNode}) {
16 return <>{children}</>
17}
18Provider.displayName = 'TooltipProvider'
19
20type TooltipContextType = {
21 position: 'top' | 'bottom'
22 onVisibleChange: (open: boolean) => void
23}
24
25const TooltipContext = createContext<Pick<TooltipContextType, 'position'>>({
26 position: 'bottom',
27})
28TooltipContext.displayName = 'TooltipContext'
29
30export function Outer({
31 children,
32 position = 'bottom',
33 visible,
34 onVisibleChange,
35}: {
36 children: React.ReactNode
37 position?: 'top' | 'bottom'
38 visible: boolean
39 onVisibleChange: (visible: boolean) => void
40}) {
41 const ctx = useMemo(() => ({position}), [position])
42 return (
43 <Popover.Root open={visible} onOpenChange={onVisibleChange}>
44 <TooltipContext.Provider value={ctx}>{children}</TooltipContext.Provider>
45 </Popover.Root>
46 )
47}
48
49export function Target({children}: {children: React.ReactNode}) {
50 return (
51 <Popover.Trigger asChild>
52 <View collapsable={false}>{children}</View>
53 </Popover.Trigger>
54 )
55}
56
57export function Content({
58 children,
59 label,
60}: {
61 children: React.ReactNode
62 label: string
63}) {
64 const t = useTheme()
65 const {position} = useContext(TooltipContext)
66 return (
67 <Popover.Portal>
68 <Popover.Content
69 className="radix-popover-content"
70 aria-label={label}
71 side={position}
72 sideOffset={4}
73 collisionPadding={MIN_EDGE_SPACE}
74 onInteractOutside={evt => {
75 if (evt.type === 'dismissableLayer.focusOutside') {
76 evt.preventDefault()
77 }
78 }}
79 style={flatten([
80 a.rounded_sm,
81 select(t.name, {
82 light: t.atoms.bg,
83 dark: t.atoms.bg_contrast_100,
84 dim: t.atoms.bg_contrast_100,
85 }),
86 {
87 minWidth: 'max-content',
88 boxShadow: select(t.name, {
89 light: `0 0 24px ${utils.alpha(t.palette.black, 0.2)}`,
90 dark: `0 0 24px ${utils.alpha(t.palette.black, 0.2)}`,
91 dim: `0 0 24px ${utils.alpha(t.palette.black, 0.2)}`,
92 }),
93 },
94 ])}>
95 <Popover.Arrow
96 width={ARROW_SIZE}
97 height={ARROW_SIZE / 2}
98 fill={select(t.name, {
99 light: t.atoms.bg.backgroundColor,
100 dark: t.atoms.bg_contrast_100.backgroundColor,
101 dim: t.atoms.bg_contrast_100.backgroundColor,
102 })}
103 />
104 <View style={[a.px_md, a.py_sm, {maxWidth: BUBBLE_MAX_WIDTH}]}>
105 {children}
106 </View>
107 </Popover.Content>
108 </Popover.Portal>
109 )
110}
111
112export function TextBubble({children}: {children: React.ReactNode}) {
113 const c = Children.toArray(children)
114 return (
115 <Content label={c.join(' ')}>
116 <View style={[a.gap_xs]}>
117 {c.map((child, i) => (
118 <Text key={i} style={[a.text_sm, a.leading_snug]}>
119 {child}
120 </Text>
121 ))}
122 </View>
123 </Content>
124 )
125}