fix(core): add null checks for timeout refs and event listeners for React 19 compatibility (#9116)

## Description
- Add null checks before clearTimeout calls in colorful-fallback.tsx, edgeless.dialog.tsx, and local.dialog.tsx
- Fix event listener cleanup in unfolding.tsx
- Update tsconfig.jsx to use react-jsx transform

## Testing
- [x] Verified type safety improvements for React 19 compatibility
- [x] Ensured proper cleanup of event listeners and timeouts
- [x] Confirmed no unintended side effects from the changes

Link to Devin run: https://app.devin.ai/sessions/2e790f3ea0d84402837ec6c3c6f83e4c
This commit is contained in:
devin-ai-integration
2024-12-12 09:43:42 +00:00
parent dd39d049fe
commit e100d252b2
39 changed files with 496 additions and 368 deletions

View File

@@ -2,7 +2,7 @@ import type { AnimationItem } from 'lottie-web';
import lottie from 'lottie-web';
import { useEffect, useRef } from 'react';
interface CustomLottieProps {
export interface CustomLottieProps {
options: {
loop?: boolean | number | undefined;
autoReverse?: boolean | undefined;
@@ -26,7 +26,7 @@ export const InternalLottie = ({
height,
}: CustomLottieProps) => {
const element = useRef<HTMLDivElement>(null);
const lottieInstance = useRef<AnimationItem>();
const lottieInstance = useRef<AnimationItem | null>(null);
const directionRef = useRef<1 | -1>(1);
useEffect(() => {

View File

@@ -1,5 +1,6 @@
import { useI18n } from '@affine/i18n';
import { SignOutIcon } from '@blocksuite/icons/rc';
import type { JSX } from 'react';
import { Avatar } from '../../ui/avatar';
import { Button, IconButton } from '../../ui/button';

View File

@@ -1,5 +1,6 @@
import { atom } from 'jotai';
import { nanoid } from 'nanoid';
import type { JSX, ReactNode } from 'react';
/**
* @deprecated use `import type { Notification } from '@affine/component'` instead
@@ -12,7 +13,7 @@ export type Notification = {
theme?: 'light' | 'dark' | 'default';
timeout?: number;
progressingBar?: boolean;
multimedia?: React.ReactNode | JSX.Element;
multimedia?: ReactNode | JSX.Element;
// actions
action?: () => Promise<void>;
actionLabel?: string;

View File

@@ -85,7 +85,7 @@ function NotificationCard(props: NotificationCardProps): ReactNode {
const [animationKey, setAnimationKey] = useState(0);
const animationRef = useRef<SVGAnimateElement>(null);
const notificationRef = useRef<HTMLLIElement>(null);
const timerIdRef = useRef<number>();
const timerIdRef = useRef<number | null>(null);
const isFront = index === 0;
const isVisible = index + 1 <= 3;
const progressDuration = notification.timeout || 3000;

View File

@@ -1,5 +1,5 @@
import clsx from 'clsx';
import { useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import {
DefaultAvatarBottomItemStyle,
@@ -27,11 +27,15 @@ export const ColorfulFallback = ({ char }: { char: string }) => {
return colorsSchema[index % colorsSchema.length];
}, [char]);
const timer = useRef<ReturnType<typeof setTimeout>>();
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
const [topColor, middleColor, bottomColor] = colors;
const [isHover, setIsHover] = useState(false);
useEffect(() => {
return () => void (timer.current && clearTimeout(timer.current));
}, []);
return (
<div
className={DefaultAvatarContainerStyle}
@@ -41,7 +45,9 @@ export const ColorfulFallback = ({ char }: { char: string }) => {
}, 300);
}}
onMouseLeave={() => {
clearTimeout(timer.current);
if (timer.current) {
clearTimeout(timer.current);
}
setIsHover(false);
}}
>

View File

@@ -4,6 +4,7 @@ import type {
HTMLAttributes,
MouseEvent,
ReactElement,
SVGAttributes,
} from 'react';
import { cloneElement, forwardRef, useCallback } from 'react';
@@ -53,7 +54,7 @@ export interface ButtonProps
*
* If `loading` is true, will be replaced by a spinner.(`prefixClassName` and `prefixStyle` still work)
* */
prefix?: ReactElement;
prefix?: ReactElement<SVGAttributes<SVGElement>>;
prefixClassName?: string;
prefixStyle?: CSSProperties;
contentClassName?: string;
@@ -63,7 +64,7 @@ export interface ButtonProps
* By default, it is considered as an icon with preset size and color,
* can be overridden by `suffixClassName` and `suffixStyle`.
* */
suffix?: ReactElement;
suffix?: ReactElement<SVGAttributes<SVGElement>>;
suffixClassName?: string;
suffixStyle?: CSSProperties;
@@ -79,7 +80,7 @@ const IconSlot = ({
className,
...attrs
}: {
icon?: ReactElement;
icon?: ReactElement<SVGAttributes<SVGElement>>;
loading?: boolean;
} & HTMLAttributes<HTMLElement>) => {
const showLoadingHere = loading !== undefined;
@@ -91,7 +92,7 @@ const IconSlot = ({
? cloneElement(icon, {
width: '100%',
height: '100%',
...icon.props,
...(icon.props as Record<string, unknown>),
})
: null}
</div>

View File

@@ -1,6 +1,11 @@
import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx';
import { type CSSProperties, forwardRef, type ReactElement } from 'react';
import {
type CSSProperties,
forwardRef,
type ReactElement,
type SVGAttributes,
} from 'react';
import { Button, type ButtonProps } from './button';
import { iconButton, iconSizeVar } from './button.css';
@@ -20,9 +25,9 @@ export interface IconButtonProps
| 'suffixStyle'
> {
/** Icon element */
children?: ReactElement;
children?: ReactElement<SVGAttributes<SVGElement>>;
/** Same as `children`, compatibility of the old API */
icon?: ReactElement;
icon?: ReactElement<SVGAttributes<SVGElement>>;
variant?: 'plain' | 'solid' | 'danger' | 'custom';
/**
* Use preset size,