refactor(app): fix type-aware lint errors in UI components (#710)

This commit is contained in:
Mohamed Boudra
2026-05-05 19:58:55 +07:00
parent 8d3946c36c
commit fd30eafafc
5 changed files with 190 additions and 155 deletions
+29 -27
View File
@@ -159,7 +159,9 @@ interface MarkdownWithStableRendererProps {
const MarkdownWithStableRenderer = Markdown as ComponentType<MarkdownWithStableRendererProps>; const MarkdownWithStableRenderer = Markdown as ComponentType<MarkdownWithStableRendererProps>;
const ThemedMarkdown = withUnistyles(MarkdownWithStableRenderer); const ThemedMarkdown = withUnistyles(MarkdownWithStableRenderer);
const markdownStyleMapping = (theme: Theme) => ({ style: createMarkdownStyles(theme) }) as never; const markdownStyleMapping = (theme: Theme): Partial<MarkdownWithStableRendererProps> => ({
style: createMarkdownStyles(theme),
});
const ThemedMicVocal = withUnistyles(MicVocal); const ThemedMicVocal = withUnistyles(MicVocal);
const ThemedTodoCheckIcon = withUnistyles(Check); const ThemedTodoCheckIcon = withUnistyles(Check);
@@ -428,6 +430,8 @@ function getUserMessageAttachmentLabel(attachment: AgentAttachment): string {
return `Issue #${attachment.number}`; return `Issue #${attachment.number}`;
case "text": case "text":
return attachment.title ?? "Text attachment"; return attachment.title ?? "Text attachment";
default:
return "";
} }
} }
@@ -634,7 +638,7 @@ const AssistantMarkdownResolvedImage = memo(function AssistantMarkdownResolvedIm
useEffect(() => { useEffect(() => {
if (cachedMetadata) { if (cachedMetadata) {
setLoadState(getAssistantImageLoadStateFromMetadata(cachedMetadata)); setLoadState(getAssistantImageLoadStateFromMetadata(cachedMetadata));
return; return () => {};
} }
setLoadState({ status: "loading" }); setLoadState({ status: "loading" });
@@ -924,11 +928,11 @@ function getInlineCodeAutoLinkUrl(
return null; return null;
} }
const matches = markdownParser.linkify.match(trimmed) as Array<{ const matches: Array<{
index: number; index: number;
lastIndex: number; lastIndex: number;
url: string; url: string;
}> | null; }> | null = markdownParser.linkify.match(trimmed);
if (!matches || matches.length !== 1) { if (!matches || matches.length !== 1) {
return null; return null;
} }
@@ -950,7 +954,7 @@ function nodeHasParentType(parent: unknown, type: string): boolean {
typeof parent === "object" && typeof parent === "object" &&
parent !== null && parent !== null &&
"type" in parent && "type" in parent &&
(parent as { type?: string }).type === type (parent as Record<"type", unknown>)["type"] === type
); );
} }
@@ -1638,16 +1642,13 @@ export const AssistantMessage = memo(function AssistantMessage({
style={styles.link} style={styles.link}
onPress={handleLinkPress} onPress={handleLinkPress}
> >
{Children.map(children, (child) => {Children.map(children, (child) => {
isValidElement(child) if (!isValidElement(child)) return child;
? cloneElement(child, { const childProps = child.props as { style?: StyleProp<TextStyle> };
style: [ return cloneElement(child, {
(child.props as { style?: StyleProp<TextStyle> }).style, style: [childProps.style, { color: styles.link.color }],
{ color: styles.link.color as string | undefined }, } as Partial<{ style: StyleProp<TextStyle> }>);
], })}
} as Partial<{ style: StyleProp<TextStyle> }>)
: child,
)}
</MarkdownLink> </MarkdownLink>
), ),
image: ( image: (
@@ -2421,12 +2422,13 @@ function useDetailWheelPropagationBlocker(input: {
const { detailWrapperRef, enabled } = input; const { detailWrapperRef, enabled } = input;
useEffect(() => { useEffect(() => {
if (!enabled) { if (!enabled) {
return; return () => {};
} }
const node = detailWrapperRef.current as unknown as HTMLElement | null; const rawRef: unknown = detailWrapperRef.current;
if (!node || typeof node.addEventListener !== "function") { if (!(rawRef instanceof HTMLElement)) {
return; return () => {};
} }
const node = rawRef;
const stopWheelPropagation = (event: WheelEvent) => { const stopWheelPropagation = (event: WheelEvent) => {
if (shouldStopDetailWheelPropagation(node, event)) { if (shouldStopDetailWheelPropagation(node, event)) {
event.stopPropagation(); event.stopPropagation();
@@ -2596,7 +2598,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
enabled: !isNative && isExpanded && hasDetailContent, enabled: !isNative && isExpanded && hasDetailContent,
}); });
const shimmerLabelStyle = useMemo( const shimmerLabelStyle = useMemo<StyleProp<TextStyle>>(
() => () =>
buildShimmerTextStyle({ buildShimmerTextStyle({
isWebShimmer, isWebShimmer,
@@ -2605,7 +2607,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
webShimmerTrackStart, webShimmerTrackStart,
webShimmerTrackEnd, webShimmerTrackEnd,
offsetX: labelOffsetX, offsetX: labelOffsetX,
}) as never, }),
[ [
isWebShimmer, isWebShimmer,
webShimmerPeakWidth, webShimmerPeakWidth,
@@ -2616,7 +2618,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
], ],
); );
const shimmerSecondaryStyle = useMemo( const shimmerSecondaryStyle = useMemo<StyleProp<TextStyle>>(
() => () =>
buildShimmerTextStyle({ buildShimmerTextStyle({
isWebShimmer, isWebShimmer,
@@ -2625,7 +2627,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
webShimmerTrackStart, webShimmerTrackStart,
webShimmerTrackEnd, webShimmerTrackEnd,
offsetX: secondaryOffsetX, offsetX: secondaryOffsetX,
}) as never, }),
[ [
isWebShimmer, isWebShimmer,
webShimmerPeakWidth, webShimmerPeakWidth,
@@ -2807,9 +2809,9 @@ function areExpandableBadgePropsEqual(previous: ExpandableBadgeProps, next: Expa
interface ToolCallProps { interface ToolCallProps {
toolName: string; toolName: string;
args?: unknown | null; args?: unknown;
result?: unknown | null; result?: unknown;
error?: unknown | null; error?: unknown;
status: "executing" | "running" | "completed" | "failed" | "canceled"; status: "executing" | "running" | "completed" | "failed" | "canceled";
detail?: ToolCallDetail; detail?: ToolCallDetail;
cwd?: string; cwd?: string;
@@ -2948,7 +2950,7 @@ export const ToolCall = memo(function ToolCall({
useEffect(() => { useEffect(() => {
if (!onInlineDetailsExpandedChange) { if (!onInlineDetailsExpandedChange) {
return; return () => {};
} }
return () => { return () => {
onInlineDetailsExpandedChange(false); onInlineDetailsExpandedChange(false);
+35 -24
View File
@@ -125,6 +125,26 @@ interface SplitPaneDropData {
paneId: string; paneId: string;
} }
function isWorkspaceTabDragData(data: unknown): data is WorkspaceTabDragData {
return typeof data === "object" && data !== null && Reflect.get(data, "kind") === "workspace-tab";
}
function isSplitPaneDropData(data: unknown): data is SplitPaneDropData {
return (
typeof data === "object" && data !== null && Reflect.get(data, "kind") === "split-pane-drop"
);
}
function asWorkspaceTabDragData(data: unknown): WorkspaceTabDragData | undefined {
return isWorkspaceTabDragData(data) ? data : undefined;
}
function asDragOverData(data: unknown): WorkspaceTabDragData | SplitPaneDropData | undefined {
if (isWorkspaceTabDragData(data)) return data;
if (isSplitPaneDropData(data)) return data;
return undefined;
}
interface SplitNodeViewProps extends Omit<SplitContainerProps, "layout" | "onMoveTabToPane"> { interface SplitNodeViewProps extends Omit<SplitContainerProps, "layout" | "onMoveTabToPane"> {
node: SplitNode; node: SplitNode;
uiTabs: WorkspaceTab[]; uiTabs: WorkspaceTab[];
@@ -185,10 +205,10 @@ const MountedTabSlot = memo(function MountedTabSlot({
[buildPaneContentModel, paneId, tabDescriptor], [buildPaneContentModel, paneId, tabDescriptor],
); );
const wrapperStyle = useMemo( const wrapperStyle = useMemo(() => {
() => ({ display: (isVisible ? "flex" : "none") as "flex" | "none", flex: 1 }), const display: "flex" | "none" = isVisible ? "flex" : "none";
[isVisible], return { display, flex: 1 };
); }, [isVisible]);
const handleFocusPane = useCallback(() => { const handleFocusPane = useCallback(() => {
onFocusPane(paneId); onFocusPane(paneId);
}, [onFocusPane, paneId]); }, [onFocusPane, paneId]);
@@ -388,8 +408,8 @@ export function SplitContainer({
}, [focusModeEnabled, layout.root, layout.focusedPaneId, panesById]); }, [focusModeEnabled, layout.root, layout.focusedPaneId, panesById]);
const handleDragStart = useCallback((event: DragStartEvent) => { const handleDragStart = useCallback((event: DragStartEvent) => {
const data = event.active.data.current as WorkspaceTabDragData | undefined; const data = asWorkspaceTabDragData(event.active.data.current);
if (data?.kind !== "workspace-tab") { if (!data) {
setActiveDragTabId(null); setActiveDragTabId(null);
setDropPreview(null); setDropPreview(null);
setTabDropPreview(null); setTabDropPreview(null);
@@ -406,11 +426,8 @@ export function SplitContainer({
const updateDropPreview = useCallback( const updateDropPreview = useCallback(
(event: Pick<DragMoveEvent, "active" | "over"> | Pick<DragOverEvent, "active" | "over">) => { (event: Pick<DragMoveEvent, "active" | "over"> | Pick<DragOverEvent, "active" | "over">) => {
const activeData = event.active.data.current as WorkspaceTabDragData | undefined; const activeData = asWorkspaceTabDragData(event.active.data.current);
const overData = event.over?.data.current as const overData = asDragOverData(event.over?.data.current);
| WorkspaceTabDragData
| SplitPaneDropData
| undefined;
if (activeData?.kind !== "workspace-tab") { if (activeData?.kind !== "workspace-tab") {
setDropPreview(null); setDropPreview(null);
@@ -513,11 +530,8 @@ export function SplitContainer({
const handleDragEnd = useCallback( const handleDragEnd = useCallback(
(event: DragEndEvent) => { (event: DragEndEvent) => {
const activeData = event.active.data.current as WorkspaceTabDragData | undefined; const activeData = asWorkspaceTabDragData(event.active.data.current);
const overData = event.over?.data.current as const overData = asDragOverData(event.over?.data.current);
| WorkspaceTabDragData
| SplitPaneDropData
| undefined;
setActiveDragTabId(null); setActiveDragTabId(null);
@@ -905,17 +919,14 @@ function SplitPaneView({
useEffect(() => { useEffect(() => {
if (isNative) { if (isNative) {
return; return () => {};
} }
const paneElement = paneRef.current as unknown as HTMLElement | null; const rawRef: unknown = paneRef.current;
if ( if (!(rawRef instanceof HTMLElement)) {
!paneElement || return () => {};
typeof paneElement.addEventListener !== "function" ||
typeof paneElement.removeEventListener !== "function"
) {
return;
} }
const paneElement = rawRef;
const handlePanePointerDown = (event: PointerEvent) => { const handlePanePointerDown = (event: PointerEvent) => {
if (!shouldFocusPaneFromEventTarget(event.target)) { if (!shouldFocusPaneFromEventTarget(event.target)) {
@@ -3,6 +3,7 @@
import { import {
useCallback, useCallback,
useEffect, useEffect,
useImperativeHandle,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@@ -142,6 +143,16 @@ function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value)); return Math.min(max, Math.max(min, value));
} }
function isTerminalState(value: unknown): value is TerminalState {
return (
typeof value === "object" &&
value !== null &&
"rows" in value &&
"cols" in value &&
"grid" in value
);
}
function ensureTerminalScrollbarStyle(): void { function ensureTerminalScrollbarStyle(): void {
if (typeof document === "undefined") { if (typeof document === "undefined") {
return; return;
@@ -229,20 +240,41 @@ export default function TerminalEmulator({
const [isScrollVisible, setIsScrollVisible] = useState(false); const [isScrollVisible, setIsScrollVisible] = useState(false);
const [isScrollActive, setIsScrollActive] = useState(false); const [isScrollActive, setIsScrollActive] = useState(false);
const domBridgeRef = useRef<DOMImperativeFactory | null>(null);
useDOMImperativeHandle( useDOMImperativeHandle(
ref as Ref<DOMImperativeFactory>, domBridgeRef,
() => (): DOMImperativeFactory => ({
({ writeOutput: (...args) => {
writeOutput: (text: string) => { const text = args[0];
runtimeRef.current?.write({ text }); if (typeof text === "string") runtimeRef.current?.write({ text });
}, },
renderSnapshot: (state: TerminalState | null) => { renderSnapshot: (...args) => {
const state = args[0];
if (state === null) {
runtimeRef.current?.renderSnapshot({ state: null });
} else if (isTerminalState(state)) {
runtimeRef.current?.renderSnapshot({ state }); runtimeRef.current?.renderSnapshot({ state });
}, }
clear: () => { },
runtimeRef.current?.clear(); clear: () => {
}, runtimeRef.current?.clear();
}) as unknown as DOMImperativeFactory, },
}),
[],
);
useImperativeHandle(
ref,
(): TerminalEmulatorHandle => ({
writeOutput: (text: string) => {
runtimeRef.current?.write({ text });
},
renderSnapshot: (state: TerminalState | null) => {
runtimeRef.current?.renderSnapshot({ state });
},
clear: () => {
runtimeRef.current?.clear();
},
}),
[], [],
); );
@@ -259,7 +291,7 @@ export default function TerminalEmulator({
useEffect(() => { useEffect(() => {
const root = rootRef.current; const root = rootRef.current;
if (!root || !swipeGesturesEnabled) { if (!root || !swipeGesturesEnabled) {
return; return () => {};
} }
const SWIPE_MIN_PX = 22; const SWIPE_MIN_PX = 22;
@@ -376,7 +408,7 @@ export default function TerminalEmulator({
const host = hostRef.current; const host = hostRef.current;
const root = rootRef.current; const root = rootRef.current;
if (!host || !root) { if (!host || !root) {
return; return () => {};
} }
const runtime = new TerminalEmulatorRuntime(); const runtime = new TerminalEmulatorRuntime();
@@ -423,7 +455,7 @@ export default function TerminalEmulator({
useEffect(() => { useEffect(() => {
if (focusRequestToken <= 0) { if (focusRequestToken <= 0) {
return; return () => {};
} }
runtimeRef.current?.resize({ force: true }); runtimeRef.current?.resize({ force: true });
return focusWithRetries({ return focusWithRetries({
@@ -451,14 +483,14 @@ export default function TerminalEmulator({
useEffect(() => { useEffect(() => {
const host = hostRef.current; const host = hostRef.current;
if (!host) { if (!host) {
return; return () => {};
} }
const viewportElement = host.querySelector<HTMLElement>(".xterm-viewport"); const viewportElement = host.querySelector<HTMLElement>(".xterm-viewport");
if (!viewportElement) { if (!viewportElement) {
viewportRef.current = null; viewportRef.current = null;
setViewportMetrics({ offset: 0, viewportSize: 0, contentSize: 0 }); setViewportMetrics({ offset: 0, viewportSize: 0, contentSize: 0 });
return; return () => {};
} }
viewportRef.current = viewportElement; viewportRef.current = viewportElement;
@@ -566,7 +598,7 @@ export default function TerminalEmulator({
useEffect(() => { useEffect(() => {
if (!isDraggingScrollbar) { if (!isDraggingScrollbar) {
return; return () => {};
} }
const handlePointerMove = (event: PointerEvent) => { const handlePointerMove = (event: PointerEvent) => {
+30 -41
View File
@@ -11,7 +11,6 @@ import {
type ReactElement, type ReactElement,
type ReactNode, type ReactNode,
type Ref, type Ref,
type MutableRefObject,
} from "react"; } from "react";
import { import {
ActivityIndicator, ActivityIndicator,
@@ -89,7 +88,7 @@ function useControllableOpenState({
}): [boolean, (next: boolean) => void] { }): [boolean, (next: boolean) => void] {
const [internalOpen, setInternalOpen] = useState(Boolean(defaultOpen)); const [internalOpen, setInternalOpen] = useState(Boolean(defaultOpen));
const isControlled = typeof open === "boolean"; const isControlled = typeof open === "boolean";
const value = isControlled ? Boolean(open) : internalOpen; const value = isControlled ? open : internalOpen;
const setValue = useCallback( const setValue = useCallback(
(next: boolean) => { (next: boolean) => {
if (!isControlled) setInternalOpen(next); if (!isControlled) setInternalOpen(next);
@@ -175,25 +174,23 @@ function computePosition({
return { x, y, actualPlacement }; return { x, y, actualPlacement };
} }
function isCallable(fn: unknown): fn is (...args: unknown[]) => void {
return typeof fn === "function";
}
function coerceEventPoint(event: unknown): { pageX: number; pageY: number } | null { function coerceEventPoint(event: unknown): { pageX: number; pageY: number } | null {
const wrapper = event as if (typeof event !== "object" || event === null) {
| { return null;
nativeEvent?: { pageX?: number; pageY?: number; clientX?: number; clientY?: number }; }
pageX?: number; const nativeEvent = Reflect.get(event, "nativeEvent");
pageY?: number; const native = typeof nativeEvent === "object" && nativeEvent !== null ? nativeEvent : event;
clientX?: number; const pageX = Reflect.get(native, "pageX");
clientY?: number; const pageY = Reflect.get(native, "pageY");
}
| null
| undefined;
const nativeEvent = wrapper?.nativeEvent ?? wrapper;
const pageX = nativeEvent?.pageX;
const pageY = nativeEvent?.pageY;
if (typeof pageX === "number" && typeof pageY === "number") { if (typeof pageX === "number" && typeof pageY === "number") {
return { pageX, pageY }; return { pageX, pageY };
} }
const clientX = nativeEvent?.clientX; const clientX = Reflect.get(native, "clientX");
const clientY = nativeEvent?.clientY; const clientY = Reflect.get(native, "clientY");
if (typeof clientX === "number" && typeof clientY === "number") { if (typeof clientX === "number" && typeof clientY === "number") {
return { pageX: clientX, pageY: clientY }; return { pageX: clientX, pageY: clientY };
} }
@@ -206,7 +203,7 @@ function assignRef<T>(ref: Ref<T> | undefined, value: T): void {
return; return;
} }
if (ref && typeof ref === "object") { if (ref && typeof ref === "object") {
(ref as MutableRefObject<T>).current = value; Object.assign(ref, { current: value });
} }
} }
@@ -326,10 +323,13 @@ export function ContextMenuTrigger({
if (isNative) { if (isNative) {
return; return;
} }
const e = event as { preventDefault?: () => void; stopPropagation?: () => void } | undefined; if (typeof event === "object" && event !== null) {
e?.preventDefault?.(); const preventDefault = Reflect.get(event, "preventDefault");
e?.stopPropagation?.(); const stopPropagation = Reflect.get(event, "stopPropagation");
openAtEvent(event as GestureResponderEvent); if (isCallable(preventDefault)) preventDefault.call(event);
if (isCallable(stopPropagation)) stopPropagation.call(event);
}
openAtEvent(event);
}, },
[openAtEvent], [openAtEvent],
); );
@@ -337,7 +337,7 @@ export function ContextMenuTrigger({
const pressableStyle = useCallback( const pressableStyle = useCallback(
({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => { ({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => {
if (typeof style === "function") { if (typeof style === "function") {
return style({ pressed, hovered: Boolean(hovered), open: ctx.open }); return style({ pressed, hovered, open: ctx.open });
} }
return style; return style;
}, },
@@ -434,40 +434,29 @@ export function ContextMenuContent({
// Measure trigger when opening (fallback) and capture point anchors. // Measure trigger when opening (fallback) and capture point anchors.
useEffect(() => { useEffect(() => {
if (useMobileSheet) { if (useMobileSheet || !open) {
setTriggerRect(null); setTriggerRect(null);
setContentSize(null); setContentSize(null);
setPosition(null); setPosition(null);
return; return () => {};
}
if (!open) {
setTriggerRect(null);
setContentSize(null);
setPosition(null);
return;
} }
if (anchorRect) { if (anchorRect) {
setTriggerRect(anchorRect); setTriggerRect(anchorRect);
return; return () => {};
} }
if (!triggerRef.current) { if (!triggerRef.current) {
setTriggerRect(null); setTriggerRect(null);
return; return () => {};
} }
const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0; const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
let cancelled = false; let cancelled = false;
measureElement(triggerRef.current).then((rect) => { void measureElement(triggerRef.current).then((rect) => {
if (cancelled) return; if (!cancelled) setTriggerRect({ ...rect, y: rect.y + statusBarHeight });
setTriggerRect({ return undefined;
...rect,
y: rect.y + statusBarHeight,
});
return;
}); });
return () => { return () => {
+46 -45
View File
@@ -11,7 +11,6 @@ import {
useState, useState,
type PropsWithChildren, type PropsWithChildren,
type ReactElement, type ReactElement,
type Ref,
} from "react"; } from "react";
import { import {
Dimensions, Dimensions,
@@ -82,24 +81,20 @@ function shouldOpenOnFocus(): boolean {
return !isWeb || lastInputWasKeyboard; return !isWeb || lastInputWasKeyboard;
} }
function composeEventHandlers<E>( function isCallable(fn: unknown): fn is (...args: unknown[]) => void {
original?: (event: E) => void, return typeof fn === "function";
injected?: (event: E) => void,
): (event: E) => void {
return (event: E) => {
original?.(event);
injected?.(event);
};
} }
function assignRef<T>(ref: Ref<T> | undefined, value: T): void { function composeEventHandlers(
if (typeof ref === "function") { original: unknown,
ref(value); injected: (event: unknown) => void,
return; ): (event: unknown) => void {
} return (event: unknown) => {
if (ref && typeof ref === "object") { if (isCallable(original)) {
(ref as { current: T }).current = value; original(event);
} }
injected(event);
};
} }
function useControllableOpenState({ function useControllableOpenState({
@@ -320,7 +315,7 @@ export function TooltipTrigger({
const handleHoverIn = useCallback( const handleHoverIn = useCallback(
(e?: unknown) => { (e?: unknown) => {
onHoverIn?.(e as never); if (isCallable(onHoverIn)) onHoverIn(e);
scheduleOpen(); scheduleOpen();
}, },
[onHoverIn, scheduleOpen], [onHoverIn, scheduleOpen],
@@ -328,7 +323,7 @@ export function TooltipTrigger({
const handleHoverOut = useCallback( const handleHoverOut = useCallback(
(e?: unknown) => { (e?: unknown) => {
onHoverOut?.(e as never); if (isCallable(onHoverOut)) onHoverOut(e);
close(); close();
}, },
[onHoverOut, close], [onHoverOut, close],
@@ -336,7 +331,7 @@ export function TooltipTrigger({
const handleFocus = useCallback( const handleFocus = useCallback(
(e: unknown) => { (e: unknown) => {
onFocus?.(e as never); if (isCallable(onFocus)) onFocus(e);
if (!ctx.enabled || disabled) return; if (!ctx.enabled || disabled) return;
if (!shouldOpenOnFocus()) return; if (!shouldOpenOnFocus()) return;
clearOpenTimer(); clearOpenTimer();
@@ -347,7 +342,7 @@ export function TooltipTrigger({
const handleBlur = useCallback( const handleBlur = useCallback(
(e: unknown) => { (e: unknown) => {
onBlur?.(e as never); if (isCallable(onBlur)) onBlur(e);
close(); close();
}, },
[close, onBlur], [close, onBlur],
@@ -355,7 +350,7 @@ export function TooltipTrigger({
const handlePress = useCallback( const handlePress = useCallback(
(e: unknown) => { (e: unknown) => {
onPress?.(e as never); if (isCallable(onPress)) onPress(e);
if (!ctx.enabled || disabled) { if (!ctx.enabled || disabled) {
return; return;
} }
@@ -394,26 +389,33 @@ export function TooltipTrigger({
throw new Error("TooltipTrigger with asChild expects a single React element child"); throw new Error("TooltipTrigger with asChild expects a single React element child");
} }
const childProps = child.props as Record<string, unknown>; const rawProps: unknown = child.props;
const mergedProps = { if (typeof rawProps !== "object" || rawProps === null) {
...childProps, throw new Error("TooltipTrigger asChild child must have props object");
}
const mergedProps: Record<string, unknown> = {
...Object.assign({}, rawProps),
...triggerProps, ...triggerProps,
disabled: childProps.disabled || disabled, disabled: Reflect.get(rawProps, "disabled") || disabled,
onHoverIn: composeEventHandlers(childProps.onHoverIn as never, handleHoverIn), onHoverIn: composeEventHandlers(Reflect.get(rawProps, "onHoverIn"), handleHoverIn),
onHoverOut: composeEventHandlers(childProps.onHoverOut as never, handleHoverOut), onHoverOut: composeEventHandlers(Reflect.get(rawProps, "onHoverOut"), handleHoverOut),
onFocus: composeEventHandlers(childProps.onFocus as never, handleFocus), onFocus: composeEventHandlers(Reflect.get(rawProps, "onFocus"), handleFocus),
onBlur: composeEventHandlers(childProps.onBlur as never, handleBlur), onBlur: composeEventHandlers(Reflect.get(rawProps, "onBlur"), handleBlur),
onPress: composeEventHandlers(childProps.onPress as never, handlePress), onPress: composeEventHandlers(Reflect.get(rawProps, "onPress"), handlePress),
onPointerEnter: composeEventHandlers(childProps.onPointerEnter as never, handleHoverIn), onPointerEnter: composeEventHandlers(Reflect.get(rawProps, "onPointerEnter"), handleHoverIn),
onPointerLeave: composeEventHandlers(childProps.onPointerLeave as never, handleHoverOut), onPointerLeave: composeEventHandlers(Reflect.get(rawProps, "onPointerLeave"), handleHoverOut),
onMouseEnter: composeEventHandlers(childProps.onMouseEnter as never, handleHoverIn), onMouseEnter: composeEventHandlers(Reflect.get(rawProps, "onMouseEnter"), handleHoverIn),
onMouseLeave: composeEventHandlers(childProps.onMouseLeave as never, handleHoverOut), onMouseLeave: composeEventHandlers(Reflect.get(rawProps, "onMouseLeave"), handleHoverOut),
} as Record<string, unknown>; };
const existingRefProp = childProps[triggerRefProp] as Ref<View | null> | undefined; const existingRefProp = Reflect.get(rawProps, triggerRefProp);
mergedProps[triggerRefProp] = (node: View | null) => { mergedProps[triggerRefProp] = (node: View) => {
assignRef(existingRefProp, node); if (isCallable(existingRefProp)) {
assignRef(ctx.triggerRef, node); existingRefProp(node);
} else if (existingRefProp && typeof existingRefProp === "object") {
Object.assign(existingRefProp, { current: node });
}
Object.assign(ctx.triggerRef, { current: node });
}; };
return cloneElement(child, mergedProps); return cloneElement(child, mergedProps);
@@ -453,16 +455,15 @@ export function TooltipContent({
setTriggerRect(null); setTriggerRect(null);
setContentSize(null); setContentSize(null);
setPosition(null); setPosition(null);
return; return () => {};
} }
const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0; const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
let cancelled = false; let cancelled = false;
measureElement(ctx.triggerRef.current).then((rect) => { void measureElement(ctx.triggerRef.current).then((rect) => {
if (cancelled) return; if (!cancelled) setTriggerRect({ ...rect, y: rect.y + statusBarHeight });
setTriggerRect({ ...rect, y: rect.y + statusBarHeight }); return undefined;
return;
}); });
return () => { return () => {