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
+30 -41
View File
@@ -11,7 +11,6 @@ import {
type ReactElement,
type ReactNode,
type Ref,
type MutableRefObject,
} from "react";
import {
ActivityIndicator,
@@ -89,7 +88,7 @@ function useControllableOpenState({
}): [boolean, (next: boolean) => void] {
const [internalOpen, setInternalOpen] = useState(Boolean(defaultOpen));
const isControlled = typeof open === "boolean";
const value = isControlled ? Boolean(open) : internalOpen;
const value = isControlled ? open : internalOpen;
const setValue = useCallback(
(next: boolean) => {
if (!isControlled) setInternalOpen(next);
@@ -175,25 +174,23 @@ function computePosition({
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 {
const wrapper = event as
| {
nativeEvent?: { pageX?: number; pageY?: number; clientX?: number; clientY?: number };
pageX?: number;
pageY?: number;
clientX?: number;
clientY?: number;
}
| null
| undefined;
const nativeEvent = wrapper?.nativeEvent ?? wrapper;
const pageX = nativeEvent?.pageX;
const pageY = nativeEvent?.pageY;
if (typeof event !== "object" || event === null) {
return null;
}
const nativeEvent = Reflect.get(event, "nativeEvent");
const native = typeof nativeEvent === "object" && nativeEvent !== null ? nativeEvent : event;
const pageX = Reflect.get(native, "pageX");
const pageY = Reflect.get(native, "pageY");
if (typeof pageX === "number" && typeof pageY === "number") {
return { pageX, pageY };
}
const clientX = nativeEvent?.clientX;
const clientY = nativeEvent?.clientY;
const clientX = Reflect.get(native, "clientX");
const clientY = Reflect.get(native, "clientY");
if (typeof clientX === "number" && typeof clientY === "number") {
return { pageX: clientX, pageY: clientY };
}
@@ -206,7 +203,7 @@ function assignRef<T>(ref: Ref<T> | undefined, value: T): void {
return;
}
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) {
return;
}
const e = event as { preventDefault?: () => void; stopPropagation?: () => void } | undefined;
e?.preventDefault?.();
e?.stopPropagation?.();
openAtEvent(event as GestureResponderEvent);
if (typeof event === "object" && event !== null) {
const preventDefault = Reflect.get(event, "preventDefault");
const stopPropagation = Reflect.get(event, "stopPropagation");
if (isCallable(preventDefault)) preventDefault.call(event);
if (isCallable(stopPropagation)) stopPropagation.call(event);
}
openAtEvent(event);
},
[openAtEvent],
);
@@ -337,7 +337,7 @@ export function ContextMenuTrigger({
const pressableStyle = useCallback(
({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => {
if (typeof style === "function") {
return style({ pressed, hovered: Boolean(hovered), open: ctx.open });
return style({ pressed, hovered, open: ctx.open });
}
return style;
},
@@ -434,40 +434,29 @@ export function ContextMenuContent({
// Measure trigger when opening (fallback) and capture point anchors.
useEffect(() => {
if (useMobileSheet) {
if (useMobileSheet || !open) {
setTriggerRect(null);
setContentSize(null);
setPosition(null);
return;
}
if (!open) {
setTriggerRect(null);
setContentSize(null);
setPosition(null);
return;
return () => {};
}
if (anchorRect) {
setTriggerRect(anchorRect);
return;
return () => {};
}
if (!triggerRef.current) {
setTriggerRect(null);
return;
return () => {};
}
const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
let cancelled = false;
measureElement(triggerRef.current).then((rect) => {
if (cancelled) return;
setTriggerRect({
...rect,
y: rect.y + statusBarHeight,
});
return;
void measureElement(triggerRef.current).then((rect) => {
if (!cancelled) setTriggerRect({ ...rect, y: rect.y + statusBarHeight });
return undefined;
});
return () => {
+46 -45
View File
@@ -11,7 +11,6 @@ import {
useState,
type PropsWithChildren,
type ReactElement,
type Ref,
} from "react";
import {
Dimensions,
@@ -82,24 +81,20 @@ function shouldOpenOnFocus(): boolean {
return !isWeb || lastInputWasKeyboard;
}
function composeEventHandlers<E>(
original?: (event: E) => void,
injected?: (event: E) => void,
): (event: E) => void {
return (event: E) => {
original?.(event);
injected?.(event);
};
function isCallable(fn: unknown): fn is (...args: unknown[]) => void {
return typeof fn === "function";
}
function assignRef<T>(ref: Ref<T> | undefined, value: T): void {
if (typeof ref === "function") {
ref(value);
return;
}
if (ref && typeof ref === "object") {
(ref as { current: T }).current = value;
}
function composeEventHandlers(
original: unknown,
injected: (event: unknown) => void,
): (event: unknown) => void {
return (event: unknown) => {
if (isCallable(original)) {
original(event);
}
injected(event);
};
}
function useControllableOpenState({
@@ -320,7 +315,7 @@ export function TooltipTrigger({
const handleHoverIn = useCallback(
(e?: unknown) => {
onHoverIn?.(e as never);
if (isCallable(onHoverIn)) onHoverIn(e);
scheduleOpen();
},
[onHoverIn, scheduleOpen],
@@ -328,7 +323,7 @@ export function TooltipTrigger({
const handleHoverOut = useCallback(
(e?: unknown) => {
onHoverOut?.(e as never);
if (isCallable(onHoverOut)) onHoverOut(e);
close();
},
[onHoverOut, close],
@@ -336,7 +331,7 @@ export function TooltipTrigger({
const handleFocus = useCallback(
(e: unknown) => {
onFocus?.(e as never);
if (isCallable(onFocus)) onFocus(e);
if (!ctx.enabled || disabled) return;
if (!shouldOpenOnFocus()) return;
clearOpenTimer();
@@ -347,7 +342,7 @@ export function TooltipTrigger({
const handleBlur = useCallback(
(e: unknown) => {
onBlur?.(e as never);
if (isCallable(onBlur)) onBlur(e);
close();
},
[close, onBlur],
@@ -355,7 +350,7 @@ export function TooltipTrigger({
const handlePress = useCallback(
(e: unknown) => {
onPress?.(e as never);
if (isCallable(onPress)) onPress(e);
if (!ctx.enabled || disabled) {
return;
}
@@ -394,26 +389,33 @@ export function TooltipTrigger({
throw new Error("TooltipTrigger with asChild expects a single React element child");
}
const childProps = child.props as Record<string, unknown>;
const mergedProps = {
...childProps,
const rawProps: unknown = child.props;
if (typeof rawProps !== "object" || rawProps === null) {
throw new Error("TooltipTrigger asChild child must have props object");
}
const mergedProps: Record<string, unknown> = {
...Object.assign({}, rawProps),
...triggerProps,
disabled: childProps.disabled || disabled,
onHoverIn: composeEventHandlers(childProps.onHoverIn as never, handleHoverIn),
onHoverOut: composeEventHandlers(childProps.onHoverOut as never, handleHoverOut),
onFocus: composeEventHandlers(childProps.onFocus as never, handleFocus),
onBlur: composeEventHandlers(childProps.onBlur as never, handleBlur),
onPress: composeEventHandlers(childProps.onPress as never, handlePress),
onPointerEnter: composeEventHandlers(childProps.onPointerEnter as never, handleHoverIn),
onPointerLeave: composeEventHandlers(childProps.onPointerLeave as never, handleHoverOut),
onMouseEnter: composeEventHandlers(childProps.onMouseEnter as never, handleHoverIn),
onMouseLeave: composeEventHandlers(childProps.onMouseLeave as never, handleHoverOut),
} as Record<string, unknown>;
disabled: Reflect.get(rawProps, "disabled") || disabled,
onHoverIn: composeEventHandlers(Reflect.get(rawProps, "onHoverIn"), handleHoverIn),
onHoverOut: composeEventHandlers(Reflect.get(rawProps, "onHoverOut"), handleHoverOut),
onFocus: composeEventHandlers(Reflect.get(rawProps, "onFocus"), handleFocus),
onBlur: composeEventHandlers(Reflect.get(rawProps, "onBlur"), handleBlur),
onPress: composeEventHandlers(Reflect.get(rawProps, "onPress"), handlePress),
onPointerEnter: composeEventHandlers(Reflect.get(rawProps, "onPointerEnter"), handleHoverIn),
onPointerLeave: composeEventHandlers(Reflect.get(rawProps, "onPointerLeave"), handleHoverOut),
onMouseEnter: composeEventHandlers(Reflect.get(rawProps, "onMouseEnter"), handleHoverIn),
onMouseLeave: composeEventHandlers(Reflect.get(rawProps, "onMouseLeave"), handleHoverOut),
};
const existingRefProp = childProps[triggerRefProp] as Ref<View | null> | undefined;
mergedProps[triggerRefProp] = (node: View | null) => {
assignRef(existingRefProp, node);
assignRef(ctx.triggerRef, node);
const existingRefProp = Reflect.get(rawProps, triggerRefProp);
mergedProps[triggerRefProp] = (node: View) => {
if (isCallable(existingRefProp)) {
existingRefProp(node);
} else if (existingRefProp && typeof existingRefProp === "object") {
Object.assign(existingRefProp, { current: node });
}
Object.assign(ctx.triggerRef, { current: node });
};
return cloneElement(child, mergedProps);
@@ -453,16 +455,15 @@ export function TooltipContent({
setTriggerRect(null);
setContentSize(null);
setPosition(null);
return;
return () => {};
}
const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
let cancelled = false;
measureElement(ctx.triggerRef.current).then((rect) => {
if (cancelled) return;
setTriggerRect({ ...rect, y: rect.y + statusBarHeight });
return;
void measureElement(ctx.triggerRef.current).then((rect) => {
if (!cancelled) setTriggerRect({ ...rect, y: rect.y + statusBarHeight });
return undefined;
});
return () => {