Enable React Native strict TypeScript API (#11459)

This commit is contained in:
Samuel Newman
2026-08-25 09:40:14 +03:00
committed by GitHub
parent 5f3d24efab
commit f87fdd2ea2
149 changed files with 506 additions and 623 deletions
+3 -3
View File
@@ -1,9 +1,9 @@
import {Pressable} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
import {HITSLOP_20} from '#/lib/constants'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {atoms as a, useTheme} from '#/alf'
import {Pressable} from '#/components/Pressable'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
@@ -49,7 +49,7 @@ export function AltBadgeWithDialog({
accessibilityHint=""
hitSlop={HITSLOP_20}
onPress={control.open}
style={s => [
style={({pressed, hovered}) => [
a.justify_center,
a.rounded_sm,
a.p_xs,
@@ -62,7 +62,7 @@ export function AltBadgeWithDialog({
opacity: 0.8,
},
pos,
s.hovered || s.pressed
hovered || pressed
? [
{
opacity: 1,
+2 -1
View File
@@ -1,11 +1,12 @@
import {useState} from 'react'
import {type Insets, Pressable, View} from 'react-native'
import {type Insets, View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
import {Pressable} from '#/components/Pressable'
import * as Tooltip from '#/components/Tooltip'
import type * as bsky from '#/types/bsky'
+3 -2
View File
@@ -101,6 +101,8 @@ export type ButtonProps = Pick<
| 'onPressOut'
| 'onFocus'
| 'onBlur'
| 'onAccessibilityAction'
| 'onAccessibilityEscape'
> &
AccessibilityProps &
VariantProps & {
@@ -131,7 +133,7 @@ export function useButtonContext() {
return useContext(Context)
}
export const Button = forwardRef<View, ButtonProps>(
export const Button = forwardRef<React.ComponentRef<typeof View>, ButtonProps>(
(
{
children,
@@ -575,7 +577,6 @@ export const Button = forwardRef<View, ButtonProps>(
role="button"
accessibilityHint={undefined} // optional
{...rest}
// @ts-ignore - this will always be a pressable
ref={ref}
aria-label={label}
aria-pressed={state.pressed}
+13 -6
View File
@@ -39,6 +39,9 @@ import {
import {Span, Text} from '#/components/Typography'
import {IS_IOS, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
type TextInputInstance = React.ComponentRef<typeof TextInput>
type ViewInstance = React.ComponentRef<typeof View>
export type SubmitRequest =
| {
platform: 'web'
@@ -60,7 +63,7 @@ export type ComposerInternalApi = {
input?: ReturnType<typeof useTapper>['input']
clear: () => void
insert(text: string): void
setAutocompleteAnchor: (node: View | null) => void
setAutocompleteAnchor: (node: ViewInstance | null) => void
}
export function useComposerInternalApiRef() {
@@ -82,7 +85,7 @@ export type ComposerProps = Omit<
| 'onSubmitEditing'
> & {
label: string
ref?: React.RefObject<TextInput>
ref?: React.RefObject<TextInputInstance>
internalApiRef?: React.Ref<ComposerInternalApi>
outerStyle?: ViewStyleProp['style']
contentTextStyle?: TextStyleProp['style']
@@ -139,6 +142,11 @@ export function Composer({
placement: autocompletePlacement,
dynamicWidth: IS_WEB,
})
const inputRef = mergeRefs<TextInputInstance>([
ref,
tapper.inputProps.ref as React.Ref<TextInputInstance>,
sift.targetProps.ref as React.Ref<TextInputInstance>,
])
/*
* Active facet state for controlling the visibility of the Autocomplete.
@@ -306,7 +314,7 @@ export function Composer({
style={[a.absolute, a.inset_0, a.z_10, {overflow: 'hidden'}]}
ref={node => {
if (IS_WEB && node) {
// @ts-ignore web only a11y
// @ts-expect-error web only a11y
node.setAttribute('inert', '')
}
}}>
@@ -345,7 +353,7 @@ export function Composer({
{...rest}
{...tapper.inputProps}
{...sift.targetProps}
ref={mergeRefs([ref, tapper.inputProps.ref, sift.targetProps.ref])}
ref={inputRef}
rawValue={tapper.state.text}
onBlur={e => {
rest.onBlur?.(e)
@@ -359,11 +367,10 @@ export function Composer({
inputScrollSharedValue.value = e.nativeEvent.contentOffset.y
}
}}
// @ts-ignore web only
// @ts-expect-error web only
onCompositionStart={() => {
isComposing.current = true
}}
// @ts-ignore web only
onCompositionEnd={() => {
isComposing.current = false
}}
+5 -2
View File
@@ -244,7 +244,7 @@ export function Trigger({
const context = useContextMenuContext()
const playHaptic = useHaptics()
const insets = useSafeAreaInsets()
const ref = useRef<View>(null)
const ref = useRef<React.ComponentRef<typeof View>>(null)
const isFocused = useIsFocused()
const [image, setImage] = useState<string | null>(null)
const [pendingMeasurement, setPendingMeasurement] = useState<{
@@ -971,7 +971,10 @@ export function Divider() {
)
}
function measureView(view: View | null, insets: EdgeInsets) {
function measureView(
view: React.ComponentRef<typeof View> | null,
insets: EdgeInsets,
) {
if (!view) return Promise.resolve(null)
return new Promise<Measurement>(resolve => {
view?.measureInWindow((x, y, width, height) =>
+76 -71
View File
@@ -8,7 +8,7 @@ import {
} from 'react'
import {
Keyboard,
type KeyboardEventListener,
type KeyboardEvent,
type LayoutChangeEvent,
type NativeScrollEvent,
type NativeSyntheticEvent,
@@ -203,80 +203,85 @@ export function Inner(props: DialogInnerProps) {
return <ScrollableInner {...props} />
}
export const ScrollableInner = forwardRef<ScrollView, DialogInnerProps>(
function ScrollableInner(
{children, contentContainerStyle, header, footer, style, ...props},
ref,
) {
const {nativeSnapPoint, disableDrag, setDisableDrag, isHeightConstrained} =
useDialogContext()
const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full
const insets = useSafeAreaInsets()
const [keyboardHeight, setKeyboardHeight] = useState(() =>
IS_ANDROID ? (Keyboard.metrics()?.height ?? 0) : 0,
)
export function ScrollableInner({
ref,
children,
contentContainerStyle,
header,
footer,
style,
...props
}: DialogInnerProps & {
ref?: React.Ref<React.ComponentRef<typeof ScrollView>>
}) {
const {nativeSnapPoint, disableDrag, setDisableDrag, isHeightConstrained} =
useDialogContext()
const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full
const insets = useSafeAreaInsets()
const [keyboardHeight, setKeyboardHeight] = useState(() =>
IS_ANDROID ? (Keyboard.metrics()?.height ?? 0) : 0,
)
const keyboardEventHandler = useCallback<KeyboardEventListener>(e => {
setKeyboardHeight(e.endCoordinates.height)
}, [])
useOnKeyboard('keyboardDidShow', keyboardEventHandler)
useOnKeyboard('keyboardDidHide', keyboardEventHandler)
const keyboardEventHandler = useCallback((e: KeyboardEvent) => {
setKeyboardHeight(e.endCoordinates.height)
}, [])
useOnKeyboard('keyboardDidShow', keyboardEventHandler)
useOnKeyboard('keyboardDidHide', keyboardEventHandler)
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
if (!IS_ANDROID) {
return
}
const {contentOffset} = e.nativeEvent
if (contentOffset.y > 0 && !disableDrag) {
setDisableDrag(true)
} else if (contentOffset.y <= 1 && disableDrag) {
setDisableDrag(false)
}
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
if (!IS_ANDROID) {
return
}
const {contentOffset} = e.nativeEvent
if (contentOffset.y > 0 && !disableDrag) {
setDisableDrag(true)
} else if (contentOffset.y <= 1 && disableDrag) {
setDisableDrag(false)
}
}
return (
<>
<ScrollView
style={[isHeightConstrained && a.flex_1, style]}
contentContainerStyle={[
a.pt_2xl,
IS_LIQUID_GLASS ? a.px_2xl : a.px_xl,
platform({
ios: a.pb_2xl,
android: {
paddingBottom: keyboardHeight + insets.bottom + tokens.space.xl,
},
}),
contentContainerStyle,
]}
ref={ref}
showsVerticalScrollIndicator={IS_ANDROID ? false : undefined}
contentInsetAdjustmentBehavior={
isAtMaxSnapPoint ? 'automatic' : 'never'
}
automaticallyAdjustKeyboardInsets={isAtMaxSnapPoint}
{...props}
bounces={isAtMaxSnapPoint}
scrollEventThrottle={50}
// set drag state based on scroll on android.
// we want to detect if it's at the top or not, so watch
// scrollEndDrag and momentumScrollEnd as well
onScroll={android(onScroll)}
onScrollEndDrag={android(onScroll)}
onMomentumScrollEnd={android(onScroll)}
keyboardShouldPersistTaps="handled"
// TODO: figure out why this positions the header absolutely (rather than stickily)
// on Android. fine to disable for now, because we don't have any
// dialogs that use this that actually scroll -sfn
stickyHeaderIndices={ios(header ? [0] : undefined)}>
{header}
{children}
</ScrollView>
{footer}
</>
)
},
)
return (
<>
<ScrollView
style={[isHeightConstrained && a.flex_1, style]}
contentContainerStyle={[
a.pt_2xl,
IS_LIQUID_GLASS ? a.px_2xl : a.px_xl,
platform({
ios: a.pb_2xl,
android: {
paddingBottom: keyboardHeight + insets.bottom + tokens.space.xl,
},
}),
contentContainerStyle,
]}
ref={ref}
showsVerticalScrollIndicator={IS_ANDROID ? false : undefined}
contentInsetAdjustmentBehavior={
isAtMaxSnapPoint ? 'automatic' : 'never'
}
automaticallyAdjustKeyboardInsets={isAtMaxSnapPoint}
{...props}
bounces={isAtMaxSnapPoint}
scrollEventThrottle={50}
// set drag state based on scroll on android.
// we want to detect if it's at the top or not, so watch
// scrollEndDrag and momentumScrollEnd as well
onScroll={android(onScroll)}
onScrollEndDrag={android(onScroll)}
onMomentumScrollEnd={android(onScroll)}
keyboardShouldPersistTaps="handled"
// TODO: figure out why this positions the header absolutely (rather than stickily)
// on Android. fine to disable for now, because we don't have any
// dialogs that use this that actually scroll -sfn
stickyHeaderIndices={ios(header ? [0] : undefined)}>
{header}
{children}
</ScrollView>
{footer}
</>
)
}
export const InnerFlatList = forwardRef<
ListMethods,
+3 -2
View File
@@ -193,7 +193,6 @@ export function Inner({
aria-label={label}
aria-labelledby={accessibilityLabelledBy}
aria-describedby={accessibilityDescribedBy}
// @ts-expect-error web only -prf
onClick={stopPropagation}
onStartShouldSetResponder={_ => true}
onTouchEnd={stopPropagation}
@@ -239,7 +238,9 @@ export function Inner({
export function ScrollableInner({
ref: _ref,
...props
}: DialogInnerProps & {ref?: React.Ref<ScrollView>}) {
}: DialogInnerProps & {
ref?: React.Ref<React.ComponentRef<typeof ScrollView>>
}) {
return <Inner {...props} />
}
+2 -2
View File
@@ -217,7 +217,7 @@ export function ProfileGrid({
// Track seen profiles
const seenProfilesRef = useRef<Set<string>>(new Set())
const containerRef = useRef<View>(null)
const containerRef = useRef<React.ComponentRef<typeof View>>(null)
const hasTrackedRef = useRef(false)
const logContext: Metrics['suggestedUser:seen']['logContext'] = isFeedContext
? 'DiscoverInterstitial'
@@ -276,7 +276,7 @@ export function ProfileGrid({
},
{threshold: 0.5},
)
// @ts-ignore - web only
// @ts-expect-error - web only
observer.observe(node)
return () => observer.disconnect()
} else {
+11 -8
View File
@@ -41,7 +41,7 @@ export function FocusScope({children}: {children: React.ReactNode}) {
*/
function FocusTrap({children}: {children: React.ReactNode}) {
const {_} = useLingui()
const child = useRef<View>(null)
const child = useRef<React.ComponentRef<typeof View>>(null)
/*
* Here we add a ref to the first child of this component. This currently
@@ -66,13 +66,16 @@ function FocusTrap({children}: {children: React.ReactNode}) {
})
}, [children])
const focusNode = useCallback((ref: View | null) => {
if (!ref) return
const node = findNodeHandle(ref)
if (node) {
AccessibilityInfo.setAccessibilityFocus(node)
}
}, [])
const focusNode = useCallback(
(ref: React.ComponentRef<typeof View> | null) => {
if (!ref) return
const node = findNodeHandle(ref)
if (node) {
AccessibilityInfo.setAccessibilityFocus(node)
}
},
[],
)
useEffect(() => {
setTimeout(() => {
+1 -1
View File
@@ -20,7 +20,7 @@ export const IS_GLASS_AVAILABLE =
*/
export const GlassView = IS_GLASS_AVAILABLE ? InnerGlassView : FallbackView
export type GlassViewProps = ExpoGlassViewProps & {
export type GlassViewProps = Omit<ExpoGlassViewProps, 'ref'> & {
fallbackStyle?: StyleProp<ViewStyle>
}
+1 -1
View File
@@ -47,7 +47,7 @@ export function InterestTabs({
}) {
const t = useTheme()
const {_} = useLingui()
const listRef = useRef<ScrollView>(null)
const listRef = useRef<React.ComponentRef<typeof ScrollView>>(null)
const [totalWidth, setTotalWidth] = useState(0)
const [scrollX, setScrollX] = useState(0)
const [contentWidth, setContentWidth] = useState(0)
+1 -1
View File
@@ -42,7 +42,7 @@ export function Outer({
}: {
children: React.ReactNode
noBottomBorder?: boolean
headerRef?: React.RefObject<View | null>
headerRef?: React.RefObject<React.ComponentRef<typeof View> | null>
sticky?: boolean
}) {
const t = useTheme()
-3
View File
@@ -446,9 +446,7 @@ function LightboxGalleryItem({
const styles = StyleSheet.create({
avi: {
// @ts-ignore web-only
maxWidth: `calc(min(400px, 100vw))`,
// @ts-ignore web-only
maxHeight: `calc(min(400px, 100vh))`,
padding: 16,
boxSizing: 'border-box',
@@ -458,7 +456,6 @@ const styles = StyleSheet.create({
// column via ScrollView's default flexGrow.
flexGrow: 0,
flexShrink: 0,
// @ts-ignore web-only -sfn
maxHeight: '50vh',
},
menuBtn: {
+1 -1
View File
@@ -35,7 +35,7 @@ const TIMING_OUT = {duration: 150}
export function ImageMenu({onPressShare, onPressSave}: Props) {
const {t: l} = useLingui()
const triggerRef = useRef<View>(null)
const triggerRef = useRef<React.ComponentRef<typeof View>>(null)
const [isMounted, setIsMounted] = useState(false)
const [anchor, setAnchor] = useState<Anchor | null>(null)
const progress = useSharedValue(0)
@@ -82,7 +82,7 @@ const ImageItem = ({
const scrollHandler = useAnimatedScrollHandler({
onScroll(e) {
'worklet'
const nextIsScaled = e.zoomScale > 1
const nextIsScaled = (e.zoomScale ?? 1) > 1
if (scaled !== nextIsScaled) {
scheduleOnRN(handleZoom, nextIsScaled)
}
@@ -109,7 +109,6 @@ const ImageItem = ({
height: number
}) {
const scrollResponderRef = scrollViewRef?.current?.getScrollResponder()
// @ts-ignore
scrollResponderRef?.scrollResponderZoomTo({
...nextZoomRect, // This rect is in screen coordinates
animated: true,
@@ -218,7 +217,6 @@ const ImageItem = ({
return (
<GestureDetector gesture={composedGesture}>
<Animated.ScrollView
// @ts-ignore Something's up with the types here
ref={scrollViewRef}
pinchGestureEnabled
showsHorizontalScrollIndicator={false}
+6 -3
View File
@@ -11,7 +11,10 @@ import {useCallback, useEffect, useMemo, useState} from 'react'
import {PixelRatio, StyleSheet, useWindowDimensions, View} from 'react-native'
import {SystemBars} from 'react-native-edge-to-edge'
import {Gesture} from 'react-native-gesture-handler'
import PagerView from 'react-native-pager-view'
import PagerView, {
type PagerViewOnPageSelectedEvent,
type PageScrollStateChangedNativeEvent,
} from 'react-native-pager-view'
import Animated, {
type AnimatableValue,
type AnimatedRef,
@@ -383,7 +386,7 @@ function ImageView({
<PagerView
scrollEnabled={!isScaled}
initialPage={initialImageIndex}
onPageSelected={e => {
onPageSelected={(e: PagerViewOnPageSelectedEvent) => {
const next = e.nativeEvent.position
setImageIndex(prev => {
if (metricsContext && prev !== next) {
@@ -401,7 +404,7 @@ function ImageView({
})
setIsScaled(false)
}}
onPageScrollStateChanged={e => {
onPageScrollStateChanged={(e: PageScrollStateChangedNativeEvent) => {
setIsDragging(e.nativeEvent.pageScrollState !== 'idle')
}}
overdrag={true}
+2 -2
View File
@@ -478,7 +478,7 @@ export function InlineLinkText({
onIn: onInteract,
onOut: onInteractOut,
} = useInteractionState()
const flattenedStyle = flatten(style) || {}
const flattenedStyle = flatten(style)
return (
<Text
@@ -570,7 +570,7 @@ export function SimpleInlineLinkText({
onIn: onInteract,
onOut: onInteractOut,
} = useInteractionState()
const flattenedStyle = flatten(style) || {}
const flattenedStyle = flatten(style)
const isExternal = isExternalUrl(to)
let href = to
+2 -2
View File
@@ -11,7 +11,7 @@ import {useLingui} from '@lingui/react'
import {DropdownMenu} from 'radix-ui'
import {useA11y} from '#/state/a11y'
import {atoms as a, flatten, useTheme, web} from '#/alf'
import {atoms as a, flatten, flattenToCSS, useTheme, web} from '#/alf'
import type * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {
@@ -413,7 +413,7 @@ export function Divider() {
const t = useTheme()
return (
<DropdownMenu.Separator
style={flatten([
style={flattenToCSS([
a.my_xs,
t.atoms.bg_contrast_100,
a.flex_shrink_0,
+1 -1
View File
@@ -28,7 +28,7 @@ export type RadixPassThroughTriggerProps = {
['aria-controls']?: string
['aria-haspopup']?: boolean
['aria-expanded']?: AccessibilityProps['aria-expanded']
onKeyDown: (e: React.KeyboardEvent) => void
onKeyDown: PressableProps['onKeyDown']
/**
* Radix provides this, but we override on web to use `onPress` instead,
* which is less sensitive while scrolling.
@@ -149,7 +149,7 @@ function canPlayBskyVideoCodecs(): boolean {
type CachedPromise<T> = Promise<T> & {value: undefined | T}
const promiseForHls = import(
// @ts-ignore
// @ts-expect-error
'hls.js/dist/hls.min'
// oxlint-disable-next-line typescript/no-unsafe-member-access
).then(mod => mod.default) as CachedPromise<typeof HlsTypes.default>
+1 -1
View File
@@ -267,7 +267,7 @@ function TranslationResult({
? codeToLanguageName(resultSourceLanguage, i18n.locale)
: undefined
const flattenedStyle = flatten(postTextStyle) ?? {}
const flattenedStyle = flatten(postTextStyle)
const fontSize = flattenedStyle.fontSize
return (
@@ -27,7 +27,7 @@ export function PostControlButton({
activeColor,
...props
}: Omit<ButtonProps, 'hitSlop'> & {
ref?: React.Ref<View>
ref?: React.Ref<React.ComponentRef<typeof View>>
active?: boolean
big?: boolean
color?: string
+37
View File
@@ -0,0 +1,37 @@
import {
Pressable as NativePressable,
type PressableStateCallbackType as NativePressableStateCallbackType,
type StyleProp,
type ViewStyle,
} from 'react-native'
export interface PressableStateCallbackType extends NativePressableStateCallbackType {
/** Provided by react-native-web. */
readonly focused?: boolean
/** Provided by react-native-web. */
readonly hovered?: boolean
}
export type PressableProps = Omit<
React.ComponentProps<typeof NativePressable>,
'children' | 'style'
> & {
children?:
React.ReactNode | ((state: PressableStateCallbackType) => React.ReactNode)
style?:
| StyleProp<ViewStyle>
| ((state: PressableStateCallbackType) => StyleProp<ViewStyle>)
}
/**
* React Native Pressable with react-native-web's callback state represented in
* its types. The web-only fields are optional because native does not provide
* them at runtime.
*/
export function Pressable({children, style, ...props}: PressableProps) {
return (
<NativePressable {...props} style={style}>
{children}
</NativePressable>
)
}
@@ -318,11 +318,10 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
return (
<View
// @ts-ignore View is being used as div
ref={refs.setReference}
onPointerMove={onPointerMoveTarget}
onPointerLeave={onPointerLeaveTarget}
// @ts-ignore web only prop
// @ts-expect-error web only prop
onMouseUp={onPress}
style={[a.flex_shrink, props.inline && a.inline]}>
{props.children}
@@ -1,5 +1,5 @@
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {TextInput, View, type ViewToken} from 'react-native'
import {type ListViewToken as ViewToken, TextInput, View} from 'react-native'
import {type ModerationOpts} from '@bsky/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro'
@@ -139,7 +139,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
const [searchText, setSearchText] = useState(lastSearchText)
const moderationOpts = useModerationOpts()
const listRef = useRef<ListMethods>(null)
const inputRef = useRef<TextInput>(null)
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const [headerHeight, setHeaderHeight] = useState(0)
const {currentAccount} = useSession()
@@ -374,7 +374,7 @@ let Header = ({
interestsDisplayNames,
}: {
guide?: Follow10ProgressGuide
inputRef: React.RefObject<TextInput | null>
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
listRef: React.RefObject<ListMethods | null>
onSelectTab: (v: string) => void
searchText: string
@@ -679,7 +679,7 @@ function SearchInput({
}: {
onChangeText: (text: string) => void
onEscape: () => void
inputRef: React.RefObject<TextInput | null>
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
defaultValue: string
}) {
const t = useTheme()
+3 -6
View File
@@ -93,7 +93,7 @@ export function RichText({
if (!facets?.length) {
if (isOnlyEmoji(text)) {
const flattenedStyle = flatten(style) ?? {}
const flattenedStyle = flatten(style)
const fontSize =
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
return (
@@ -104,7 +104,6 @@ export function RichText({
style={[plainStyles, {fontSize}, suffixStyles]}
onLayout={onLayout}
onTextLayout={onTextLayout}
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
{text}
{suffix ? ' ' : null}
@@ -121,7 +120,6 @@ export function RichText({
numberOfLines={numberOfLines}
onLayout={onLayout}
onTextLayout={onTextLayout}
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
{text}
{suffix ? ' ' : null}
@@ -150,7 +148,7 @@ export function RichText({
selectable={selectable}
to={`/profile/${mention.did}`}
style={interactiveStyles}
// @ts-ignore TODO
// @ts-expect-error TODO
dataSet={WORD_WRAP}
shouldProxy={shouldProxyLinks}
onPress={onLinkPress}>
@@ -169,7 +167,7 @@ export function RichText({
key={key}
to={link.uri}
style={interactiveStyles}
// @ts-ignore TODO
// @ts-expect-error TODO
dataSet={WORD_WRAP}
shareOnLongPress
shouldProxy={shouldProxyLinks}
@@ -209,7 +207,6 @@ export function RichText({
numberOfLines={numberOfLines}
onLayout={onLayout}
onTextLayout={onTextLayout}
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
{els}
{suffix ? ' ' : null}
+10 -1
View File
@@ -100,7 +100,16 @@ export function Trigger({children, hitSlop, label}: TriggerProps) {
} else {
return (
<Button
hitSlop={hitSlop}
hitSlop={
typeof hitSlop === 'number'
? {
top: hitSlop,
right: hitSlop,
bottom: hitSlop,
left: hitSlop,
}
: (hitSlop ?? undefined)
}
label={label}
onPress={control.open}
style={[a.flex_1, a.justify_between, a.pl_lg, a.pr_md]}
+9 -9
View File
@@ -3,7 +3,7 @@ import {View} from 'react-native'
import {Select as RadixSelect} from 'radix-ui'
import {useA11y} from '#/state/a11y'
import {atoms as a, flatten, useTheme, web} from '#/alf'
import {atoms as a, flatten, flattenToCSS, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {
@@ -94,7 +94,7 @@ export function Trigger({children, label}: TriggerProps) {
onBlur={onBlur}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={flatten([
style={flattenToCSS([
a.flex,
a.relative,
t.atoms.bg_contrast_50,
@@ -196,7 +196,7 @@ export function Content<T>({
return (
<RadixSelect.Portal>
<RadixSelect.Content
style={flatten([t.atoms.bg, a.rounded_sm, a.overflow_hidden])}
style={flattenToCSS([t.atoms.bg, a.rounded_sm, a.overflow_hidden])}
position="popper"
align="center"
sideOffset={5}
@@ -212,17 +212,17 @@ export function Content<T>({
a.overflow_hidden,
!reduceMotionEnabled && a.zoom_fade_in,
]}>
<RadixSelect.ScrollUpButton style={flatten(up)}>
<RadixSelect.ScrollUpButton style={flattenToCSS(up)}>
<ChevronUpIcon style={[t.atoms.text]} size="xs" />
</RadixSelect.ScrollUpButton>
<RadixSelect.Viewport style={flatten([a.p_xs])}>
<RadixSelect.Viewport style={flattenToCSS([a.p_xs])}>
{items.map((item, index) => (
<Fragment key={valueExtractor(item)}>
{renderItem(item, index, selectedValue)}
</Fragment>
))}
</RadixSelect.Viewport>
<RadixSelect.ScrollDownButton style={flatten(down)}>
<RadixSelect.ScrollDownButton style={flattenToCSS(down)}>
<ChevronDownIcon style={[t.atoms.text]} size="xs" />
</RadixSelect.ScrollDownButton>
</View>
@@ -273,7 +273,7 @@ export function Item({ref, value, style, children}: ItemProps) {
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onBlur}
style={flatten([
style={flattenToCSS([
t.atoms.text,
a.relative,
a.flex,
@@ -307,7 +307,7 @@ export const ItemText = function ItemText({children, style}: ItemTextProps) {
export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
return (
<RadixSelect.ItemIndicator
style={flatten([
style={flattenToCSS([
a.absolute,
{left: 0, width: 30},
a.flex,
@@ -324,7 +324,7 @@ export function Separator() {
return (
<RadixSelect.Separator
style={flatten([
style={flattenToCSS([
{
height: 1,
backgroundColor: t.atoms.border_contrast_low.borderColor,
@@ -2,10 +2,6 @@ import {forwardRef, useCallback, useImperativeHandle, useState} from 'react'
import {type ListRenderItemInfo, View} from 'react-native'
import {AtUri} from '@atproto/syntax'
import {type ModerationOpts} from '@bsky/sdk/moderation'
import {
type InfiniteData,
type UseInfiniteQueryResult,
} from '@tanstack/react-query'
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
@@ -26,9 +22,6 @@ function keyExtractor(item: app.bsky.actor.defs.ProfileView, index: number) {
interface ProfilesListProps {
listUri: string
listMembersQuery: UseInfiniteQueryResult<
InfiniteData<app.bsky.graph.getList.$OutputBody>
>
moderationOpts: ModerationOpts
headerHeight: number
scrollElRef: ListRef
+5 -4
View File
@@ -27,8 +27,9 @@ import {
import {Text} from '#/components/Typography'
const TooltipPortal = createPortalGroup()
const TooltipProviderContext =
createContext<React.RefObject<View | null> | null>(null)
const TooltipProviderContext = createContext<React.RefObject<React.ComponentRef<
typeof View
> | null> | null>(null)
/**
* Provider for Tooltip component. Only needed when you need to position the tooltip relative to a container,
@@ -37,7 +38,7 @@ const TooltipProviderContext =
* Only really necessary on iOS but can work on Android.
*/
export function SheetCompatProvider({children}: {children: React.ReactNode}) {
const ref = useRef<View | null>(null)
const ref = useRef<React.ComponentRef<typeof View> | null>(null)
return (
<GlobalGestureEventsProvider style={[a.flex_1]}>
<TooltipPortal.Provider>
@@ -154,7 +155,7 @@ export function Outer({
export function Target({children}: {children: React.ReactNode}) {
const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext)
const [hasLaidOut, setHasLaidOut] = useState(false)
const targetRef = useRef<View>(null)
const targetRef = useRef<React.ComponentRef<typeof View>>(null)
const containerRef = useContext(TooltipProviderContext)
const keyboardIsOpen = useIsKeyboardVisible()
+2 -2
View File
@@ -3,7 +3,7 @@ import {View} from 'react-native'
import {utils} from '@bsky.app/alf'
import {Popover} from 'radix-ui'
import {atoms as a, flatten, useTheme} from '#/alf'
import {atoms as a, flattenToCSS, useTheme} from '#/alf'
import {
ARROW_SIZE,
BUBBLE_MAX_WIDTH,
@@ -85,7 +85,7 @@ export function Content({
evt.preventDefault()
}
}}
style={flatten([
style={flattenToCSS([
a.rounded_sm,
{
backgroundColor: style.surface,
@@ -25,13 +25,13 @@ export function OTPInput({
label: string
value: string
onChange: (text: string) => void
ref?: React.Ref<TextInput>
ref?: React.Ref<React.ComponentRef<typeof TextInput>>
numberOfDigits?: number
onComplete?: (code: string) => void
}) {
const t = useTheme()
const {_} = useLingui()
const innerRef = useRef<TextInput>(null)
const innerRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const [selection, setSelection] = useState({start: 0, end: 0})
@@ -383,7 +383,7 @@ export function ViewMatches({
ref={listRef}
data={items}
renderItem={renderItem}
ListFooterComponent={!isEmpty ? <ListFooter height={20} /> : null}
ListFooterComponent={!isEmpty ? <ListFooter height={20} /> : undefined}
keyExtractor={keyExtractor}
keyboardDismissMode="interactive"
automaticallyAdjustKeyboardInsets
@@ -97,7 +97,7 @@ export function SearchablePeopleList({
const [headerHeight, setHeaderHeight] = useState(0)
const listRef = useRef<ListMethods>(null)
const {currentAccount} = useSession()
const inputRef = useRef<TextInput>(null)
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const [searchText, setSearchText] = useState('')
@@ -634,7 +634,7 @@ function SearchInput({
value: string
onChangeText: (text: string) => void
onEscape: () => void
inputRef: React.RefObject<TextInput | null>
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
}) {
const t = useTheme()
const {t: l} = useLingui()
@@ -182,9 +182,9 @@ function ListsContent({
<View style={[a.align_center, a.py_lg]}>
<Loader size="lg" />
</View>
) : null
) : undefined
}
ListEmptyComponent={!isLoading && data ? <Empty /> : null}
ListEmptyComponent={!isLoading && data ? <Empty /> : undefined}
webInnerContentContainerStyle={[a.py_0]}
style={platform({
web: [a.px_2xl, a.pb_md],
+1 -1
View File
@@ -117,7 +117,7 @@ function Inner({
}, [activeNux, setActiveNux])
if (__DEV__ && typeof window !== 'undefined') {
// @ts-ignore
// @ts-expect-error
window.clearNuxDialog = (id: Nux) => {
if (!__DEV__ || !id) return
resetNuxs([id])
+2 -2
View File
@@ -134,7 +134,7 @@ export function AddMembersFlow({
const [footerHeight, setFooterHeight] = useState(0)
const listRef = useRef<ListMethods>(null)
const inputRef = useRef<TextInput>(null)
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const [{groupChatDids, groupChatProfiles, searchText}, dispatch] = useReducer(
reducer,
@@ -502,7 +502,7 @@ export function AddMembersFlow({
<View style={[a.flex_1, a.align_center, a.justify_center]}>
<Loader size="xl" />
</View>
) : null
) : undefined
}
keyExtractor={(item: Item) => item.key}
style={[
+1 -1
View File
@@ -249,7 +249,7 @@ export function InitiateChatFlow({
const listRef = useRef<ListMethods>(null)
const {currentAccount} = useSession()
const aa = useAgeAssurance()
const inputRef = useRef<TextInput>(null)
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const accountTooNewPromptControl = Dialog.useDialogControl()
const {data: convos} = useListConvosQuery({
+1 -1
View File
@@ -255,7 +255,7 @@ function ReactionTabs({
const t = useTheme()
const {t: l} = useLingui()
const scrollViewRef = useRef<ScrollView>(null)
const scrollViewRef = useRef<React.ComponentRef<typeof ScrollView>>(null)
const scrollState = useRef({x: 0, width: 0})
const tabLayouts = useRef<Map<string, {x: number; width: number}>>(new Map())
@@ -14,7 +14,7 @@ export function UserSearchInput({
value: string
onChangeText: (text: string) => void
onEscape: () => void
inputRef: React.RefObject<TextInput | null>
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
}) {
const t = useTheme()
const {t: l} = useLingui()
@@ -38,7 +38,6 @@ export function UserSearchInput({
fill={interacted ? t.palette.primary_500 : t.palette.contrast_300}
/>
<TextInput
// @ts-ignore bottom sheet input types issue - esb
ref={inputRef}
placeholder={l`Search for people`}
value={value}
+3 -3
View File
@@ -11,7 +11,7 @@ import {normalizeTextStyles} from '#/alf/typography'
import {IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
export type AutosizedTextareaProps = Omit<TextInputProps, 'multiline'> & {
ref?: React.Ref<TextInput>
ref?: React.Ref<React.ComponentRef<typeof TextInput>>
label: string
minRows?: number
maxRows?: number
@@ -44,7 +44,7 @@ export function AutosizedTextarea({
...rest
}: AutosizedTextareaProps) {
const {theme: t, fonts} = useAlf()
const internalRef = useRef<TextInput>(null)
const internalRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const {style, minInputHeight, maxInputHeight, verticalContentPadding} =
useMemo(() => {
const normalizedStyles = normalizeTextStyles(
@@ -181,7 +181,7 @@ export function AutosizedTextarea({
]}
{...rest}
ref={mergeRefs([
(node: TextInput | null) => {
(node: React.ComponentRef<typeof TextInput> | null) => {
internalRef.current = node
// bop resize on first render
if (IS_WEB && node) handleResizeWeb()
@@ -93,7 +93,6 @@ export function DateField({
open
timeZoneOffsetInMinutes={0}
theme={t.scheme}
// @ts-ignore TODO
buttonColor={t.name === 'light' ? '#000000' : '#ffffff'}
date={initialDate}
onConfirm={onChangeInternal}
+1 -1
View File
@@ -62,7 +62,7 @@ export function DateField({
<TextField.Icon icon={CalendarDays} />
<Input
value={value === '' ? '' : toSimpleDateString(value)}
inputRef={inputRef as React.Ref<TextInput>}
inputRef={inputRef as React.Ref<React.ComponentRef<typeof TextInput>>}
label={label}
onChange={handleOnChange}
testID={testID}
+2 -2
View File
@@ -19,7 +19,7 @@ type Props = Omit<TextField.InputProps, 'label'> & {
*/
onClearText?: () => void
hotkey?: boolean
ref?: React.Ref<TextInput>
ref?: React.Ref<React.ComponentRef<typeof TextInput>>
}
export function SearchInput({
@@ -33,7 +33,7 @@ export function SearchInput({
const t = useTheme()
const {t: l} = useLingui()
const showClear = value && value.length > 0
const internalRef = useRef<TextInput>(null)
const internalRef = useRef<React.ComponentRef<typeof TextInput>>(null)
useEffect(() => {
if (!hotkey) return
+37 -35
View File
@@ -14,6 +14,8 @@ import {mergeRefs} from '#/lib/merge-refs'
import {
applyFonts,
atoms as a,
flatten,
type MutableTextStyle,
platform,
type TextStyleProp,
tokens,
@@ -26,7 +28,7 @@ import {type Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography'
const Context = createContext<{
inputRef: React.RefObject<TextInput | null> | null
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null> | null
isInvalid: boolean
hovered: boolean
onHoverIn: () => void
@@ -55,7 +57,7 @@ export type RootProps = React.PropsWithChildren<
>
export function Root({children, isInvalid = false, style}: RootProps) {
const inputRef = useRef<TextInput>(null)
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const {
state: hovered,
onIn: onHoverIn,
@@ -161,7 +163,9 @@ export type InputProps = Omit<
value?: string
onChangeText?: (value: string) => void
isInvalid?: boolean
inputRef?: React.RefObject<TextInput | null> | React.ForwardedRef<TextInput>
inputRef?:
| React.RefObject<React.ComponentRef<typeof TextInput> | null>
| React.ForwardedRef<React.ComponentRef<typeof TextInput>>
/**
* Note: this currently falls back to the label if not specified. However,
* most new designs have no placeholder. We should eventually remove this fallback
@@ -208,43 +212,41 @@ export function createInput(Component: typeof TextInput) {
const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))
const flattened = StyleSheet.flatten<TextStyle>([
a.relative,
a.z_20,
a.flex_1,
a.text_md,
t.atoms.text,
a.px_xs,
{
// paddingVertical doesn't work w/multiline - esb
lineHeight: a.text_md.fontSize * 1.2,
textAlignVertical: rest.multiline ? 'top' : undefined,
minHeight: rest.multiline ? 80 : undefined,
minWidth: 0,
paddingTop: 13,
paddingBottom: 13,
},
/*
* Margins are needed here to avoid autofill background overlapping the
* top and bottom borders - esb
*/
web({
paddingTop: 11,
paddingBottom: 11,
marginTop: 2,
marginBottom: 2,
}),
style,
])
const flattened: MutableTextStyle = {
...flatten([
a.relative,
a.z_20,
a.flex_1,
a.text_md,
t.atoms.text,
a.px_xs,
{
// paddingVertical doesn't work w/multiline - esb
lineHeight: a.text_md.fontSize * 1.2,
textAlignVertical: rest.multiline ? 'top' : undefined,
minHeight: rest.multiline ? 80 : undefined,
minWidth: 0,
paddingTop: 13,
paddingBottom: 13,
},
/*
* Margins are needed here to avoid autofill background overlapping the
* top and bottom borders - esb
*/
web({
paddingTop: 11,
paddingBottom: 11,
marginTop: 2,
marginBottom: 2,
}),
style,
]),
}
applyFonts(flattened, fonts.family)
// should always be defined on `typography`
// @ts-ignore
if (flattened.fontSize) {
// @ts-ignore
flattened.fontSize = Math.round(
// @ts-ignore
flattened.fontSize * fonts.scaleMultiplier,
)
}
+2 -2
View File
@@ -1,13 +1,13 @@
import {useEffect} from 'react'
import {
Keyboard,
type KeyboardEventListener,
type KeyboardEvent,
type KeyboardEventName,
} from 'react-native'
export function useOnKeyboard(
eventName: KeyboardEventName,
cb: KeyboardEventListener,
cb: (event: KeyboardEvent) => unknown,
) {
useEffect(() => {
const subscription = Keyboard.addListener(eventName, cb)
+1 -1
View File
@@ -23,7 +23,7 @@ export const IconTemplate_Stroke2_Corner0_Rounded = forwardRef(
<Svg
fill="none"
{...rest}
// @ts-ignore it's fiiiiine
// @ts-expect-error it's fiiiiine
ref={ref}
viewBox="0 0 24 24"
width={size}
+7 -5
View File
@@ -65,7 +65,7 @@ interface GalleryProps {
}
const Context = createContext<{
bleedRef: React.RefObject<View | null>
bleedRef: React.RefObject<React.ComponentRef<typeof View> | null>
bleedWidth: number
}>({
bleedRef: {current: null},
@@ -73,7 +73,7 @@ const Context = createContext<{
})
export function GalleryBleed({children}: {children: React.ReactNode}) {
const ref = useRef<View>(null)
const ref = useRef<React.ComponentRef<typeof View>>(null)
const [bleedWidth, setBleedWidth] = useState(0)
if (!isValidElement(children)) {
@@ -145,7 +145,7 @@ export function Gallery({
* scroll position, so it works correctly for off-screen FlatList items.
*/
const {bleedRef, bleedWidth} = useGalleryBleed()
const contentRef = useRef<View>(null)
const contentRef = useRef<React.ComponentRef<typeof View>>(null)
const [contentDims, setContentDims] = useState<{x: number; width: number}>()
const measure = () => {
if (contentRef.current && bleedRef.current) {
@@ -168,7 +168,9 @@ export function Gallery({
const flatListRef = useRef<FlatList>(null)
const itemWidthsRef = useRef<Map<number, number>>(new Map())
const itemRefsRef = useRef<Map<number, View>>(new Map())
const itemRefsRef = useRef<Map<number, React.ComponentRef<typeof View>>>(
new Map(),
)
const containerRefsRef = useRef<Map<number, AnimatedRef<any>>>(new Map())
const thumbDimsRef = useRef<Map<number, Dimensions>>(new Map())
const currentIndexRef = useRef(0)
@@ -408,7 +410,7 @@ function GalleryImage({
index: number
imageCount: number
onWidthChange: (index: number, width: number) => void
itemRef: (node: View | null) => void
itemRef: (node: React.ComponentRef<typeof View> | null) => void
largeAltBadge?: boolean
onContainerRef: (index: number, ref: AnimatedRef<any>) => void
onThumbDims: (index: number, dims: Dimensions) => void
+1 -1
View File
@@ -226,7 +226,7 @@ function BlockDialogInner({
<View style={[a.py_lg, a.align_center, a.justify_center]}>
<Loader size="lg" />
</View>
) : null
) : undefined
}
footer={
<Dialog.FlatListFooter
@@ -159,7 +159,7 @@ function Inner(
const logger = ax.logger.useChild(ax.logger.Context.ReportDialog)
const t = useTheme()
const {t: l} = useLingui()
const ref = useRef<ScrollView>(null)
const ref = useRef<React.ComponentRef<typeof ScrollView>>(null)
const {
data: allLabelers,
isLoading: isLabelerLoading,