This commit is contained in:
Eric Bailey
2025-06-22 18:11:32 -05:00
parent 2b23499a15
commit dbe86dde43
2 changed files with 105 additions and 41 deletions
+76 -15
View File
@@ -7,14 +7,18 @@ import {
useState, useState,
} from 'react' } from 'react'
import {Dimensions, View} from 'react-native' import {Dimensions, View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {useOnGesture} from '#/components/hooks/useOnGesture' import {useOnGesture} from '#/components/hooks/useOnGesture'
// import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {TIP_SIZE} from '#/components/Tooltip/const' import {TIP_SIZE} from '#/components/Tooltip/const'
const HALF_TIP = TIP_SIZE / 2
const TIP_VISUAL_OFFSET = TIP_SIZE / 3
type TooltipContextType = { type TooltipContextType = {
position: 'top' | 'bottom'
ready: boolean ready: boolean
onVisibleChange: (visible: boolean) => void onVisibleChange: (visible: boolean) => void
} }
@@ -32,6 +36,7 @@ type TargetContextType = {
} }
const TooltipContext = createContext<TooltipContextType>({ const TooltipContext = createContext<TooltipContextType>({
position: 'bottom',
ready: false, ready: false,
onVisibleChange: () => {}, onVisibleChange: () => {},
}) })
@@ -43,10 +48,12 @@ const TargetContext = createContext<TargetContextType>({
export function Outer({ export function Outer({
children, children,
position = 'bottom',
visible: requestVisible, visible: requestVisible,
onVisibleChange, onVisibleChange,
}: { }: {
children: React.ReactNode children: React.ReactNode
position?: 'top' | 'bottom'
visible: boolean visible: boolean
onVisibleChange: (visible: boolean) => void onVisibleChange: (visible: boolean) => void
}) { }) {
@@ -94,8 +101,8 @@ export function Outer({
} }
const ctx = useMemo( const ctx = useMemo(
() => ({ready, onVisibleChange}), () => ({position, ready, onVisibleChange}),
[ready, onVisibleChange], [position, ready, onVisibleChange],
) )
const targetCtx = useMemo( const targetCtx = useMemo(
() => ({targetMeasurements, targetRef}), () => ({targetMeasurements, targetRef}),
@@ -124,7 +131,7 @@ export function Target({
} }
export function Content({children}: {children: React.ReactNode}) { export function Content({children}: {children: React.ReactNode}) {
const {ready, onVisibleChange} = useContext(TooltipContext) const {position, ready, onVisibleChange} = useContext(TooltipContext)
const {targetMeasurements} = useContext(TargetContext) const {targetMeasurements} = useContext(TargetContext)
const requestClose = useCallback(() => { const requestClose = useCallback(() => {
onVisibleChange(false) onVisibleChange(false)
@@ -135,6 +142,7 @@ export function Content({children}: {children: React.ReactNode}) {
return ( return (
<Portal> <Portal>
<Bubble <Bubble
position={position}
/* /*
* Gotta pass these in here. Inside the Bubble, we're Potal-ed outside * Gotta pass these in here. Inside the Bubble, we're Potal-ed outside
* the context providers. * the context providers.
@@ -149,10 +157,12 @@ export function Content({children}: {children: React.ReactNode}) {
function Bubble({ function Bubble({
children, children,
position,
requestClose, requestClose,
targetMeasurements, targetMeasurements,
}: { }: {
children: React.ReactNode children: React.ReactNode
position: TooltipContextType['position']
requestClose: () => void requestClose: () => void
targetMeasurements: Exclude< targetMeasurements: Exclude<
TargetContextType['targetMeasurements'], TargetContextType['targetMeasurements'],
@@ -160,7 +170,7 @@ function Bubble({
> >
}) { }) {
const t = useTheme() const t = useTheme()
// const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const [bubbleMeasurements, setBubbleMeasurements] = useState< const [bubbleMeasurements, setBubbleMeasurements] = useState<
| { | {
width: number width: number
@@ -179,39 +189,74 @@ function Bubble({
tipLeft: 0, tipLeft: 0,
} }
const win = Dimensions.get('window') const {width: ww, height: wh} = Dimensions.get('window')
const {width: ww, height: _wh} = win const maxTop = insets.top
// const maxTop = insets.top const maxBottom = wh - insets.bottom
// const maxBottom = wh - insets.bottom
const {width: cw, height: ch} = bubbleMeasurements const {width: cw, height: ch} = bubbleMeasurements
const minLeft = a.px_xl.paddingLeft const minLeft = a.px_xl.paddingLeft
const maxLeft = ww - minLeft const maxLeft = ww - minLeft
let computedPosition: 'top' | 'bottom' = position
let top = targetMeasurements.y + targetMeasurements.height let top = targetMeasurements.y + targetMeasurements.height
let left = Math.max( let left = Math.max(
minLeft, minLeft,
targetMeasurements.x + targetMeasurements.width / 2 - cw / 2, targetMeasurements.x + targetMeasurements.width / 2 - cw / 2,
) )
let tipTop = (TIP_SIZE / 2) * -1 const tipTranslate = HALF_TIP * -1
let tipTop = tipTranslate
if (left + cw > maxLeft) { if (left + cw > maxLeft) {
left -= left + cw - maxLeft left -= left + cw - maxLeft
} }
let tipLeft = let tipLeft =
targetMeasurements.x - left + targetMeasurements.width / 2 - TIP_SIZE / 2 targetMeasurements.x - left + targetMeasurements.width / 2 - HALF_TIP
top += TIP_SIZE / 3 let bottom = top + ch
function positionTop() {
top = top - ch - targetMeasurements.height
bottom = top + ch
tipTop = tipTop + ch
computedPosition = 'top'
}
function positionBottom() {
top = targetMeasurements.y + targetMeasurements.height
bottom = top + ch
tipTop = tipTranslate
computedPosition = 'bottom'
}
if (position === 'top') {
positionTop()
if (top < maxTop) {
positionBottom()
}
} else {
if (bottom > maxBottom) {
positionTop()
}
}
if (computedPosition === 'bottom') {
top += TIP_VISUAL_OFFSET
bottom += TIP_VISUAL_OFFSET
} else {
top -= TIP_VISUAL_OFFSET
bottom -= TIP_VISUAL_OFFSET
}
return { return {
computedPosition,
top, top,
bottom: top + ch, bottom,
left, left,
right: left + cw, right: left + cw,
tipTop, tipTop,
tipLeft, tipLeft,
} }
}, [targetMeasurements, bubbleMeasurements]) }, [position, targetMeasurements, bubbleMeasurements, insets])
const requestCloseWrapped = useCallback(() => { const requestCloseWrapped = useCallback(() => {
setBubbleMeasurements(undefined) setBubbleMeasurements(undefined)
@@ -265,7 +310,23 @@ function Bubble({
]} ]}
/> />
<View <View
style={[a.px_md, a.py_sm, a.rounded_sm, t.atoms.bg, t.atoms.shadow_sm]} style={[
a.px_md,
a.py_sm,
a.rounded_sm,
t.atoms.bg,
t.atoms.shadow_md,
{
shadowOpacity: 0.2,
shadowOffset: {
width: 0,
// provide more shadow beneath tip
height:
TIP_VISUAL_OFFSET *
(coords.computedPosition === 'bottom' ? -1 : 1),
},
},
]}
onLayout={e => { onLayout={e => {
setBubbleMeasurements({ setBubbleMeasurements({
width: e.nativeEvent.layout.width, width: e.nativeEvent.layout.width,
@@ -34,13 +34,13 @@ import {
} from '#/components/KnownFollowers' } from '#/components/KnownFollowers'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import * as Tooltip from '#/components/Tooltip'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
import {EditProfileDialog} from './EditProfileDialog' import {EditProfileDialog} from './EditProfileDialog'
import {ProfileHeaderHandle} from './Handle' import {ProfileHeaderHandle} from './Handle'
import {ProfileHeaderMetrics} from './Metrics' import {ProfileHeaderMetrics} from './Metrics'
import {ProfileHeaderShell} from './Shell' import {ProfileHeaderShell} from './Shell'
import * as Tooltip from '#/components/Tooltip'
interface Props { interface Props {
profile: AppBskyActorDefs.ProfileViewDetailed profile: AppBskyActorDefs.ProfileViewDetailed
@@ -202,16 +202,37 @@ let ProfileHeaderStandard = ({
) : !profile.viewer?.blockedBy ? ( ) : !profile.viewer?.blockedBy ? (
<> <>
<Button <Button
label='test' label="test"
size='small' size="small"
variant='solid' variant="solid"
color='secondary' color="secondary"
shape='round' shape="round"
onPress={() => setVisible(!visible)} onPress={() => setVisible(!visible)}>
>
<ButtonIcon position="left" icon={Plus} /> <ButtonIcon position="left" icon={Plus} />
</Button> </Button>
{hasSession && (
<>
<Tooltip.Outer
position="top"
visible={visible}
onVisibleChange={setVisible}>
<Tooltip.Target>
{({ref}) => (
<View ref={ref}>
<MessageProfileButton profile={profile} />
</View>
)}
</Tooltip.Target>
<Tooltip.Content>
<Text selectable>
The quick brown fox jumps over the lazy dog
</Text>
</Tooltip.Content>
</Tooltip.Outer>
</>
)}
<Button <Button
testID={profile.viewer?.following ? 'unfollowBtn' : 'followBtn'} testID={profile.viewer?.following ? 'unfollowBtn' : 'followBtn'}
size="small" size="small"
@@ -239,24 +260,6 @@ let ProfileHeaderStandard = ({
)} )}
</ButtonText> </ButtonText>
</Button> </Button>
{hasSession && (
<>
<Tooltip.Outer visible={visible} onVisibleChange={setVisible}>
<Tooltip.Target>
{({ref}) => (
<View ref={ref}>
<MessageProfileButton profile={profile} />
</View>
)}
</Tooltip.Target>
<Tooltip.Content>
<Text>The quick brown fox jumps over the lazy dog</Text>
</Tooltip.Content>
</Tooltip.Outer>
</>
)}
</> </>
) : null} ) : null}
<ProfileMenu profile={profile} /> <ProfileMenu profile={profile} />