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); + } +}