diff --git a/src/App.web.tsx b/src/App.web.tsx
index b706774fdc..ec57e1a406 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -60,6 +60,7 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo
import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
+import {Provider as TooltipProvider} from '#/components/Tooltip'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
@@ -194,9 +195,11 @@ function App() {
-
-
-
+
+
+
+
+
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index 79ec41679a..440ac16ac9 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -983,6 +983,10 @@ export const atoms = {
transition_none: web({
transitionProperty: 'none',
}),
+ transition_timing_default: web({
+ transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
+ transitionDuration: '100ms',
+ }),
transition_all: web({
transitionProperty: 'all',
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
diff --git a/src/components/Tooltip/const.ts b/src/components/Tooltip/const.ts
index d4756760be..75e2603052 100644
--- a/src/components/Tooltip/const.ts
+++ b/src/components/Tooltip/const.ts
@@ -1 +1,4 @@
+import {atoms as a} from '#/alf'
+
export const TIP_SIZE = 12
+export const MIN_X_SPACE = a.px_lg.paddingLeft
diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx
index 4d00eb7673..95329ab4a8 100644
--- a/src/components/Tooltip/index.tsx
+++ b/src/components/Tooltip/index.tsx
@@ -14,12 +14,19 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {atoms as a, select, useTheme} from '#/alf'
import {useOnGesture} from '#/components/hooks/useOnGesture'
import {Portal} from '#/components/Portal'
-import {TIP_SIZE} from '#/components/Tooltip/const'
+import {MIN_EDGE_SPACE, TIP_SIZE} from '#/components/Tooltip/const'
import {Text} from '#/components/Typography'
const HALF_TIP = TIP_SIZE / 2
const TIP_VISUAL_OFFSET = TIP_SIZE / 3
+/**
+ * Only needed on web
+ */
+export function Provider({children}: {children: React.ReactNode}) {
+ return children
+}
+
type TooltipContextType = {
position: 'top' | 'bottom'
ready: boolean
@@ -205,7 +212,7 @@ function Bubble({
const maxTop = insets.top
const maxBottom = wh - insets.bottom
const {width: cw, height: ch} = bubbleMeasurements
- const minLeft = a.px_xl.paddingLeft
+ const minLeft = MIN_EDGE_SPACE
const maxLeft = ww - minLeft
let computedPosition: 'top' | 'bottom' = position
diff --git a/src/components/Tooltip/index.web.tsx b/src/components/Tooltip/index.web.tsx
new file mode 100644
index 0000000000..8093e6f27b
--- /dev/null
+++ b/src/components/Tooltip/index.web.tsx
@@ -0,0 +1,157 @@
+import {Children, createContext, useContext, useMemo, useState} from 'react'
+import {View} from 'react-native'
+import {Popover} from 'radix-ui'
+
+import {atoms as a, flatten, select, useTheme} from '#/alf'
+import {transparentifyColor} from '#/alf/util/colorGeneration'
+import {MIN_EDGE_SPACE, TIP_SIZE} from '#/components/Tooltip/const'
+import {Text} from '#/components/Typography'
+
+const TIP_VISUAL_OFFSET = TIP_SIZE / 3
+
+type TooltipContextType = {
+ position: 'top' | 'bottom'
+}
+
+const TooltipContext = createContext({
+ position: 'bottom',
+})
+
+export function Provider({children}: {children: React.ReactNode}) {
+ return children
+}
+
+export function Outer({
+ children,
+ position = 'bottom',
+ visible,
+ onVisibleChange,
+}: {
+ children: React.ReactNode
+ position?: 'top' | 'bottom'
+ visible: boolean
+ onVisibleChange: (visible: boolean) => void
+}) {
+ const ctx = useMemo(() => ({position}), [position])
+ return (
+
+ {children}
+
+ )
+}
+
+export function Target({
+ children,
+}: {
+ children: (props: object) => React.ReactNode
+}) {
+ const child = useMemo(() => children({}), [children])
+ return {child}
+}
+
+export function Content({
+ children,
+ label,
+}: {
+ children: React.ReactNode
+ label: string
+}) {
+ const t = useTheme()
+ const {position} = useContext(TooltipContext)
+ const [bubbleMeasurements, setBubbleMeasurements] = useState<
+ | {
+ width: number
+ height: number
+ }
+ | undefined
+ >(undefined)
+ return (
+
+
+
+ {
+ setBubbleMeasurements({
+ width: Math.ceil(e.nativeEvent.layout.width),
+ height: Math.ceil(e.nativeEvent.layout.height),
+ })
+ }}>
+ {children}
+
+
+
+ )
+}
+
+export function TextBubble({children}: {children: React.ReactNode}) {
+ const c = Children.toArray(children)
+ return (
+
+
+ {c.map((child, i) => (
+
+ {child}
+
+ ))}
+
+
+ )
+}
diff --git a/src/style.css b/src/style.css
index a4a2c455f7..35ffe0d3a2 100644
--- a/src/style.css
+++ b/src/style.css
@@ -334,3 +334,38 @@ input[type='range'][orient='vertical']::-moz-range-thumb {
min-width: var(--radix-select-trigger-width);
max-height: var(--radix-select-content-available-height);
}
+
+/*
+ * #/components/Tooltip/index.web.tsx
+ */
+.radix-popover-content {
+ animation-duration: 300ms;
+ animation-timing-function: cubic-bezier(0.17, 0.73, 0.14, 1);
+ will-change: transform, opacity;
+}
+.radix-popover-content[data-state='open'][data-side='top'] {
+ animation-name: radixPopoverSlideUpAndFade;
+}
+.radix-popover-content[data-state='open'][data-side='bottom'] {
+ animation-name: radixPopoverSlideDownAndFade;
+}
+@keyframes radixPopoverSlideUpAndFade {
+ from {
+ opacity: 0;
+ transform: translateY(2px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+@keyframes radixPopoverSlideDownAndFade {
+ from {
+ opacity: 0;
+ transform: translateY(-2px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}