Create primary tooltip variant (#11140)

This commit is contained in:
DS Boyce
2026-07-14 05:57:11 -07:00
committed by GitHub
parent ac0a249721
commit 4778142ee8
9 changed files with 190 additions and 109 deletions
-10
View File
@@ -406,16 +406,6 @@
"count": 1 "count": 1
} }
}, },
"src/components/Tooltip/index.tsx": {
"typescript/no-base-to-string": {
"count": 1
}
},
"src/components/Tooltip/index.web.tsx": {
"typescript/no-base-to-string": {
"count": 1
}
},
"src/components/WhoCanReply.tsx": { "src/components/WhoCanReply.tsx": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 1 "count": 1
+36 -1
View File
@@ -1,6 +1,41 @@
import {atoms as a} from '#/alf' import {atoms as a, select, type Theme} from '#/alf'
/**
* Visual variant for the tooltip surface. `default` is a neutral floating card;
* `primary` is a subtle blue surface matching the `primary_subtle` button.
*/
export type TooltipColor = 'default' | 'primary'
export const BUBBLE_MAX_WIDTH = 240 export const BUBBLE_MAX_WIDTH = 240
export const ARROW_SIZE = 12 export const ARROW_SIZE = 12
export const ARROW_HALF_SIZE = ARROW_SIZE / 2 export const ARROW_HALF_SIZE = ARROW_SIZE / 2
export const MIN_EDGE_SPACE = a.px_lg.paddingLeft export const MIN_EDGE_SPACE = a.px_lg.paddingLeft
/**
* Resolves the surface (background/arrow fill) and text colors for a tooltip
* variant. Kept here so the native and web implementations stay in sync.
*/
export function getTooltipStyle(t: Theme, color: TooltipColor) {
if (color === 'primary') {
return {
surface: t.palette.primary_50,
text: t.palette.primary_600,
border: {
color: t.atoms.border_contrast_low.borderColor,
width: 1,
},
}
}
return {
surface: select(t.name, {
light: t.atoms.bg.backgroundColor,
dark: t.atoms.bg_contrast_100.backgroundColor,
dim: t.atoms.bg_contrast_100.backgroundColor,
}),
text: t.atoms.text.color,
border: {
color: undefined,
width: 0,
},
}
}
+1 -1
View File
@@ -14,6 +14,6 @@ export function Content() {
return null return null
} }
export function TextBubble() { export function BubbleText() {
return null return null
} }
+44 -40
View File
@@ -1,5 +1,4 @@
import { import {
Children,
createContext, createContext,
useCallback, useCallback,
useContext, useContext,
@@ -14,14 +13,16 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
import {GlobalGestureEventsProvider} from '#/state/global-gesture-events' import {GlobalGestureEventsProvider} from '#/state/global-gesture-events'
import {atoms as a, select, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {useOnGesture} from '#/components/hooks/useOnGesture' import {useOnGesture} from '#/components/hooks/useOnGesture'
import {createPortalGroup, Portal as RootPortal} from '#/components/Portal' import {createPortalGroup, Portal as RootPortal} from '#/components/Portal'
import { import {
ARROW_HALF_SIZE, ARROW_HALF_SIZE,
ARROW_SIZE, ARROW_SIZE,
BUBBLE_MAX_WIDTH, BUBBLE_MAX_WIDTH,
getTooltipStyle,
MIN_EDGE_SPACE, MIN_EDGE_SPACE,
type TooltipColor,
} from '#/components/Tooltip/const' } from '#/components/Tooltip/const'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -56,10 +57,10 @@ SheetCompatProvider.displayName = 'TooltipSheetCompatProvider'
* These are native specific values, not shared with web * These are native specific values, not shared with web
*/ */
const ARROW_VISUAL_OFFSET = ARROW_SIZE / 1.25 // vibes-based, slightly off the target const ARROW_VISUAL_OFFSET = ARROW_SIZE / 1.25 // vibes-based, slightly off the target
const BUBBLE_SHADOW_OFFSET = ARROW_SIZE / 3 // vibes-based, provide more shadow beneath tip
type TooltipContextType = { type TooltipContextType = {
position: 'top' | 'bottom' position: 'top' | 'bottom'
color: TooltipColor
visible: boolean visible: boolean
onVisibleChange: (visible: boolean) => void onVisibleChange: (visible: boolean) => void
} }
@@ -79,6 +80,7 @@ type TargetContextType = {
const TooltipContext = createContext<TooltipContextType>({ const TooltipContext = createContext<TooltipContextType>({
position: 'bottom', position: 'bottom',
color: 'default',
visible: false, visible: false,
onVisibleChange: () => {}, onVisibleChange: () => {},
}) })
@@ -94,11 +96,13 @@ TargetContext.displayName = 'TargetContext'
export function Outer({ export function Outer({
children, children,
position = 'bottom', position = 'bottom',
color = 'default',
visible: requestVisible, visible: requestVisible,
onVisibleChange, onVisibleChange,
}: { }: {
children: React.ReactNode children: React.ReactNode
position?: 'top' | 'bottom' position?: 'top' | 'bottom'
color?: TooltipColor
visible: boolean visible: boolean
onVisibleChange: (visible: boolean) => void onVisibleChange: (visible: boolean) => void
}) { }) {
@@ -126,8 +130,8 @@ export function Outer({
} }
const ctx = useMemo( const ctx = useMemo(
() => ({position, visible, onVisibleChange}), () => ({position, color, visible, onVisibleChange}),
[position, visible, onVisibleChange], [position, color, visible, onVisibleChange],
) )
const targetCtx = useMemo( const targetCtx = useMemo(
() => ({ () => ({
@@ -149,13 +153,13 @@ export function Outer({
export function Target({children}: {children: React.ReactNode}) { export function Target({children}: {children: React.ReactNode}) {
const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext) const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext)
const [hasLayedOut, setHasLayedOut] = useState(false) const [hasLaidOut, setHasLaidOut] = useState(false)
const targetRef = useRef<View>(null) const targetRef = useRef<View>(null)
const containerRef = useContext(TooltipProviderContext) const containerRef = useContext(TooltipProviderContext)
const keyboardIsOpen = useIsKeyboardVisible() const keyboardIsOpen = useIsKeyboardVisible()
useEffect(() => { useEffect(() => {
if (!shouldMeasure || !hasLayedOut) return if (!shouldMeasure || !hasLaidOut) return
/* /*
* Once opened, measure the dimensions and position of the target * Once opened, measure the dimensions and position of the target
*/ */
@@ -179,7 +183,7 @@ export function Target({children}: {children: React.ReactNode}) {
}, [ }, [
shouldMeasure, shouldMeasure,
setTargetMeasurements, setTargetMeasurements,
hasLayedOut, hasLaidOut,
containerRef, containerRef,
keyboardIsOpen, keyboardIsOpen,
]) ])
@@ -188,7 +192,7 @@ export function Target({children}: {children: React.ReactNode}) {
<View <View
collapsable={false} collapsable={false}
ref={targetRef} ref={targetRef}
onLayout={() => setHasLayedOut(true)}> onLayout={() => setHasLaidOut(true)}>
{children} {children}
</View> </View>
) )
@@ -201,7 +205,7 @@ export function Content({
children: React.ReactNode children: React.ReactNode
label: string label: string
}) { }) {
const {position, visible, onVisibleChange} = useContext(TooltipContext) const {position, color, visible, onVisibleChange} = useContext(TooltipContext)
const {targetMeasurements} = useContext(TargetContext) const {targetMeasurements} = useContext(TargetContext)
const isWithinProvider = !!useContext(TooltipProviderContext) const isWithinProvider = !!useContext(TooltipProviderContext)
const requestClose = useCallback(() => { const requestClose = useCallback(() => {
@@ -217,8 +221,9 @@ export function Content({
<Bubble <Bubble
label={label} label={label}
position={position} position={position}
color={color}
/* /*
* Gotta pass these in here. Inside the Bubble, we're Potal-ed outside * Gotta pass these in here. Inside the Bubble, we're Portal-ed outside
* the context providers. * the context providers.
*/ */
targetMeasurements={targetMeasurements} targetMeasurements={targetMeasurements}
@@ -233,12 +238,14 @@ function Bubble({
children, children,
label, label,
position, position,
color,
requestClose, requestClose,
targetMeasurements, targetMeasurements,
}: { }: {
children: React.ReactNode children: React.ReactNode
label: string label: string
position: TooltipContextType['position'] position: TooltipContextType['position']
color: TooltipColor
requestClose: () => void requestClose: () => void
targetMeasurements: Exclude< targetMeasurements: Exclude<
TargetContextType['targetMeasurements'], TargetContextType['targetMeasurements'],
@@ -246,6 +253,7 @@ function Bubble({
> >
}) { }) {
const t = useTheme() const t = useTheme()
const style = getTooltipStyle(t, color)
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const dimensions = useWindowDimensions() const dimensions = useWindowDimensions()
const [bubbleMeasurements, setBubbleMeasurements] = useState< const [bubbleMeasurements, setBubbleMeasurements] = useState<
@@ -383,21 +391,19 @@ function Bubble({
]}> ]}>
<Animated.View <Animated.View
entering={ZoomIn.easing(Easing.out(Easing.exp))} entering={ZoomIn.easing(Easing.out(Easing.exp))}
style={{transformOrigin: oppposite(position)}}> style={{transformOrigin: opposite(position)}}>
<View <View
style={[ style={[
a.absolute, a.absolute,
a.top_0, a.top_0,
a.z_10, a.z_10,
t.atoms.bg,
select(t.name, {
light: t.atoms.bg,
dark: t.atoms.bg_contrast_100,
dim: t.atoms.bg_contrast_100,
}),
{ {
backgroundColor: style.surface,
borderTopLeftRadius: a.rounded_2xs.borderRadius, borderTopLeftRadius: a.rounded_2xs.borderRadius,
borderBottomRightRadius: a.rounded_2xs.borderRadius, borderBottomRightRadius: a.rounded_2xs.borderRadius,
borderColor: style.border.color,
borderTopWidth: style.border.width,
borderLeftWidth: style.border.width,
width: ARROW_SIZE, width: ARROW_SIZE,
height: ARROW_SIZE, height: ARROW_SIZE,
transform: [{rotate: '45deg'}], transform: [{rotate: '45deg'}],
@@ -410,21 +416,12 @@ function Bubble({
style={[ style={[
a.px_md, a.px_md,
a.py_sm, a.py_sm,
a.rounded_sm, a.rounded_md,
select(t.name, { t.atoms.shadow_xs,
light: t.atoms.bg,
dark: t.atoms.bg_contrast_100,
dim: t.atoms.bg_contrast_100,
}),
t.atoms.shadow_md,
{ {
shadowOpacity: 0.2, backgroundColor: style.surface,
shadowOffset: { borderColor: style.border.color,
width: 0, borderWidth: style.border.width,
height:
BUBBLE_SHADOW_OFFSET *
(coords.computedPosition === 'bottom' ? -1 : 1),
},
}, },
]} ]}
onLayout={e => { onLayout={e => {
@@ -440,7 +437,7 @@ function Bubble({
) )
} }
function oppposite(position: 'top' | 'bottom') { function opposite(position: 'top' | 'bottom') {
switch (position) { switch (position) {
case 'top': case 'top':
return 'center bottom' return 'center bottom'
@@ -451,16 +448,23 @@ function oppposite(position: 'top' | 'bottom') {
} }
} }
export function TextBubble({children}: {children: React.ReactNode}) { export function BubbleText({
const c = Children.toArray(children) children,
label,
}: {
children: React.ReactNode
label: string
}) {
const t = useTheme()
const {color} = useContext(TooltipContext)
const style = getTooltipStyle(t, color)
// eslint-disable-next-line bsky-internal/avoid-unwrapped-text
return ( return (
<Content label={c.join(' ')}> <Content label={label}>
<View style={[a.gap_xs]}> <View style={[a.gap_xs]}>
{c.map((child, i) => ( <Text style={[a.text_sm, a.leading_snug, {color: style.text}]}>
<Text key={i} style={[a.text_sm, a.leading_snug]}> {children}
{child}
</Text> </Text>
))}
</View> </View>
</Content> </Content>
) )
+34 -27
View File
@@ -1,13 +1,15 @@
import {Children, createContext, useContext, useMemo} from 'react' import {createContext, useContext, useMemo} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {utils} from '@bsky.app/alf' import {utils} from '@bsky.app/alf'
import {Popover} from 'radix-ui' import {Popover} from 'radix-ui'
import {atoms as a, flatten, select, useTheme} from '#/alf' import {atoms as a, flatten, useTheme} from '#/alf'
import { import {
ARROW_SIZE, ARROW_SIZE,
BUBBLE_MAX_WIDTH, BUBBLE_MAX_WIDTH,
getTooltipStyle,
MIN_EDGE_SPACE, MIN_EDGE_SPACE,
type TooltipColor,
} from '#/components/Tooltip/const' } from '#/components/Tooltip/const'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -19,26 +21,32 @@ Provider.displayName = 'TooltipProvider'
type TooltipContextType = { type TooltipContextType = {
position: 'top' | 'bottom' position: 'top' | 'bottom'
color: TooltipColor
onVisibleChange: (open: boolean) => void onVisibleChange: (open: boolean) => void
} }
const TooltipContext = createContext<Pick<TooltipContextType, 'position'>>({ const TooltipContext = createContext<
Pick<TooltipContextType, 'position' | 'color'>
>({
position: 'bottom', position: 'bottom',
color: 'default',
}) })
TooltipContext.displayName = 'TooltipContext' TooltipContext.displayName = 'TooltipContext'
export function Outer({ export function Outer({
children, children,
position = 'bottom', position = 'bottom',
color = 'default',
visible, visible,
onVisibleChange, onVisibleChange,
}: { }: {
children: React.ReactNode children: React.ReactNode
position?: 'top' | 'bottom' position?: 'top' | 'bottom'
color?: TooltipColor
visible: boolean visible: boolean
onVisibleChange: (visible: boolean) => void onVisibleChange: (visible: boolean) => void
}) { }) {
const ctx = useMemo(() => ({position}), [position]) const ctx = useMemo(() => ({position, color}), [position, color])
return ( return (
<Popover.Root open={visible} onOpenChange={onVisibleChange}> <Popover.Root open={visible} onOpenChange={onVisibleChange}>
<TooltipContext.Provider value={ctx}>{children}</TooltipContext.Provider> <TooltipContext.Provider value={ctx}>{children}</TooltipContext.Provider>
@@ -62,7 +70,8 @@ export function Content({
label: string label: string
}) { }) {
const t = useTheme() const t = useTheme()
const {position} = useContext(TooltipContext) const {position, color} = useContext(TooltipContext)
const style = getTooltipStyle(t, color)
return ( return (
<Popover.Portal> <Popover.Portal>
<Popover.Content <Popover.Content
@@ -78,28 +87,19 @@ export function Content({
}} }}
style={flatten([ style={flatten([
a.rounded_sm, a.rounded_sm,
select(t.name, {
light: t.atoms.bg,
dark: t.atoms.bg_contrast_100,
dim: t.atoms.bg_contrast_100,
}),
{ {
backgroundColor: style.surface,
borderColor: style.border.color,
borderWidth: style.border.width,
borderStyle: 'solid',
minWidth: 'max-content', minWidth: 'max-content',
boxShadow: select(t.name, { boxShadow: `0 0 24px ${utils.alpha(t.palette.black, 0.2)}`,
light: `0 0 24px ${utils.alpha(t.palette.black, 0.2)}`,
dark: `0 0 24px ${utils.alpha(t.palette.black, 0.2)}`,
dim: `0 0 24px ${utils.alpha(t.palette.black, 0.2)}`,
}),
}, },
])}> ])}>
<Popover.Arrow <Popover.Arrow
width={ARROW_SIZE} width={ARROW_SIZE}
height={ARROW_SIZE / 2} height={ARROW_SIZE / 2}
fill={select(t.name, { fill={style.surface}
light: t.atoms.bg.backgroundColor,
dark: t.atoms.bg_contrast_100.backgroundColor,
dim: t.atoms.bg_contrast_100.backgroundColor,
})}
/> />
<View style={[a.px_md, a.py_sm, {maxWidth: BUBBLE_MAX_WIDTH}]}> <View style={[a.px_md, a.py_sm, {maxWidth: BUBBLE_MAX_WIDTH}]}>
{children} {children}
@@ -109,16 +109,23 @@ export function Content({
) )
} }
export function TextBubble({children}: {children: React.ReactNode}) { export function BubbleText({
const c = Children.toArray(children) children,
label,
}: {
children: React.ReactNode
label: string
}) {
const t = useTheme()
const {color} = useContext(TooltipContext)
const style = getTooltipStyle(t, color)
// eslint-disable-next-line bsky-internal/avoid-unwrapped-text
return ( return (
<Content label={c.join(' ')}> <Content label={label}>
<View style={[a.gap_xs]}> <View style={[a.gap_xs]}>
{c.map((child, i) => ( <Text style={[a.text_sm, a.leading_snug, {color: style.text}]}>
<Text key={i} style={[a.text_sm, a.leading_snug]}> {children}
{child}
</Text> </Text>
))}
</View> </View>
</Content> </Content>
) )
@@ -1,8 +1,6 @@
import {useCallback, useEffect, useState} from 'react' import {useCallback, useEffect, useState} from 'react'
import {type ModerationOpts} from '@atproto/api' import {type ModerationOpts} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
@@ -11,7 +9,6 @@ import {useDialogControl} from '#/components/Dialog'
import {BellPlus_Stroke2_Corner0_Rounded as BellPlusIcon} from '#/components/icons/BellPlus' import {BellPlus_Stroke2_Corner0_Rounded as BellPlusIcon} from '#/components/icons/BellPlus'
import {BellRinging_Filled_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging' import {BellRinging_Filled_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
import * as Tooltip from '#/components/Tooltip' import * as Tooltip from '#/components/Tooltip'
import {Text} from '#/components/Typography'
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
import {SubscribeProfileDialog} from './SubscribeProfileDialog' import {SubscribeProfileDialog} from './SubscribeProfileDialog'
@@ -25,7 +22,7 @@ export function SubscribeProfileButton({
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
disableHint?: boolean disableHint?: boolean
}) { }) {
const {_} = useLingui() const {t: l} = useLingui()
const requireEmailVerification = useRequireEmailVerification() const requireEmailVerification = useRequireEmailVerification()
const subscribeDialogControl = useDialogControl() const subscribeDialogControl = useDialogControl()
const [activitySubscriptionsNudged, setActivitySubscriptionsNudged] = const [activitySubscriptionsNudged, setActivitySubscriptionsNudged] =
@@ -84,18 +81,15 @@ export function SubscribeProfileButton({
size="small" size="small"
color={tooltipVisible ? 'primary_subtle' : 'secondary'} color={tooltipVisible ? 'primary_subtle' : 'secondary'}
shape="round" shape="round"
label={_(msg`Get notified when ${name} posts`)} label={l`Get notified when ${name} posts`}
onPress={wrappedOnPress}> onPress={wrappedOnPress}>
<ButtonIcon icon={Icon} size="md" /> <ButtonIcon icon={Icon} size="md" />
</Button> </Button>
</Tooltip.Target> </Tooltip.Target>
<Tooltip.TextBubble> <Tooltip.BubbleText label={l`Get notified about new posts`}>
<Text>
<Trans>Get notified about new posts</Trans> <Trans>Get notified about new posts</Trans>
</Text> </Tooltip.BubbleText>
</Tooltip.TextBubble>
</Tooltip.Outer> </Tooltip.Outer>
<SubscribeProfileDialog <SubscribeProfileDialog
control={subscribeDialogControl} control={subscribeDialogControl}
profile={profile} profile={profile}
@@ -2,9 +2,7 @@ import {useEffect, useMemo, useState} from 'react'
import {Keyboard, type StyleProp, type ViewStyle} from 'react-native' import {Keyboard, type StyleProp, type ViewStyle} from 'react-native'
import {type AnimatedStyle} from 'react-native-reanimated' import {type AnimatedStyle} from 'react-native-reanimated'
import {type AppBskyFeedPostgate} from '@atproto/api' import {type AppBskyFeedPostgate} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import deepEqual from 'fast-deep-equal' import deepEqual from 'fast-deep-equal'
import {isNetworkError} from '#/lib/strings/errors' import {isNetworkError} from '#/lib/strings/errors'
@@ -24,7 +22,6 @@ import {TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronIcon} from '#/co
import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe' import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe'
import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group' import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
import * as Tooltip from '#/components/Tooltip' import * as Tooltip from '#/components/Tooltip'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged' import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged'
@@ -43,11 +40,12 @@ export function ThreadgateBtn({
style?: StyleProp<AnimatedStyle<ViewStyle>> style?: StyleProp<AnimatedStyle<ViewStyle>>
}) { }) {
const {_} = useLingui() const {t: l} = useLingui()
const ax = useAnalytics() const ax = useAnalytics()
const control = Dialog.useDialogControl() const control = Dialog.useDialogControl()
const [threadgateNudged, setThreadgateNudged] = useThreadgateNudged() const [threadgateNudged, setThreadgateNudged] = useThreadgateNudged()
const [showTooltip, setShowTooltip] = useState(false) const [showTooltip, setShowTooltip] = useState(false)
// eslint-disable-next-line react/hook-use-state
const [tooltipWasShown] = useState(!threadgateNudged) const [tooltipWasShown] = useState(!threadgateNudged)
useEffect(() => { useEffect(() => {
@@ -134,8 +132,8 @@ export function ThreadgateBtn({
!postgate.embeddingRules || postgate.embeddingRules.length === 0 !postgate.embeddingRules || postgate.embeddingRules.length === 0
const anyoneCanInteract = anyoneCanReply && anyoneCanQuote const anyoneCanInteract = anyoneCanReply && anyoneCanQuote
const label = anyoneCanInteract const label = anyoneCanInteract
? _(msg`Anyone can interact`) ? l`Anyone can interact`
: _(msg`Interaction limited`) : l`Interaction limited`
return ( return (
<> <>
@@ -150,9 +148,7 @@ export function ThreadgateBtn({
testID="openReplyGateButton" testID="openReplyGateButton"
onPress={onPress} onPress={onPress}
label={label} label={label}
accessibilityHint={_( accessibilityHint={l`Opens a dialog to choose who can interact with this post`}>
msg`Opens a dialog to choose who can interact with this post`,
)}>
<ButtonIcon icon={anyoneCanInteract ? EarthIcon : GroupIcon} /> <ButtonIcon icon={anyoneCanInteract ? EarthIcon : GroupIcon} />
<ButtonText numberOfLines={1} maxFontSizeMultiplier={2}> <ButtonText numberOfLines={1} maxFontSizeMultiplier={2}>
{label} {label}
@@ -160,13 +156,11 @@ export function ThreadgateBtn({
<ButtonIcon icon={TinyChevronIcon} size="2xs" /> <ButtonIcon icon={TinyChevronIcon} size="2xs" />
</Button> </Button>
</Tooltip.Target> </Tooltip.Target>
<Tooltip.TextBubble> <Tooltip.BubbleText
<Text> label={l`Psst! You can edit who can interact with this post.`}>
<Trans>Psst! You can edit who can interact with this post.</Trans> <Trans>Psst! You can edit who can interact with this post.</Trans>
</Text> </Tooltip.BubbleText>
</Tooltip.TextBubble>
</Tooltip.Outer> </Tooltip.Outer>
<PostInteractionSettingsControlledDialog <PostInteractionSettingsControlledDialog
control={control} control={control}
onSave={() => { onSave={() => {
+2
View File
@@ -26,6 +26,7 @@ import {Shadows} from './Shadows'
import {Spacing} from './Spacing' import {Spacing} from './Spacing'
import {Theming} from './Theming' import {Theming} from './Theming'
import {Toasts} from './Toasts' import {Toasts} from './Toasts'
import {Tooltips} from './Tooltips'
import {Typography} from './Typography' import {Typography} from './Typography'
export default function Storybook() { export default function Storybook() {
@@ -127,6 +128,7 @@ export default function Storybook() {
<Shadows /> <Shadows />
<Icons /> <Icons />
<Links /> <Links />
<Tooltips />
<Dialogs /> <Dialogs />
<Menus /> <Menus />
<Breakpoints /> <Breakpoints />
+55
View File
@@ -0,0 +1,55 @@
import {useState} from 'react'
import {View} from 'react-native'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Tooltip from '#/components/Tooltip'
import {H1} from '#/components/Typography'
export function Tooltips() {
const [defaultVisible, setDefaultVisible] = useState(false)
const [primaryVisible, setPrimaryVisible] = useState(false)
return (
<View style={[a.gap_md, a.align_start]}>
<H1>Tooltips</H1>
<View style={[a.flex_row, a.gap_md, a.align_start]}>
<Tooltip.Outer
visible={defaultVisible}
onVisibleChange={setDefaultVisible}>
<Tooltip.Target>
<Button
color="secondary"
size="small"
label="Toggle default tooltip"
onPress={() => setDefaultVisible(v => !v)}>
<ButtonText>Default</ButtonText>
</Button>
</Tooltip.Target>
<Tooltip.BubbleText label="This is a default tooltip.">
This is a default tooltip.
</Tooltip.BubbleText>
</Tooltip.Outer>
<Tooltip.Outer
color="primary"
visible={primaryVisible}
onVisibleChange={setPrimaryVisible}>
<Tooltip.Target>
<Button
color="primary_subtle"
size="small"
label="Toggle primary tooltip"
onPress={() => setPrimaryVisible(v => !v)}>
<ButtonText>Primary</ButtonText>
</Button>
</Tooltip.Target>
<Tooltip.BubbleText label="This is a primary tooltip.">
This is a primary tooltip.
</Tooltip.BubbleText>
</Tooltip.Outer>
</View>
</View>
)
}