Create primary tooltip variant (#11140)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ export function Content() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TextBubble() {
|
export function BubbleText() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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={() => {
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user