Skip to content

Commit 74e2cb7

Browse files
authored
fix: cancel InlineLoading success timeout on unmount (#18605)
1 parent 36d3e38 commit 74e2cb7

File tree

2 files changed

+46
-8
lines changed

2 files changed

+46
-8
lines changed

packages/react/src/components/InlineLoading/InlineLoading-test.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2023
2+
* Copyright IBM Corp. 2016, 2025
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -87,4 +87,25 @@ describe('InlineLoading', () => {
8787
expect(onSuccess).toHaveBeenCalled();
8888
jest.useRealTimers();
8989
});
90+
91+
it('should cancel the onSuccess timeout if the component unmounts', () => {
92+
jest.useFakeTimers();
93+
const onSuccess = jest.fn();
94+
const { unmount } = render(
95+
<InlineLoading
96+
status="finished"
97+
onSuccess={onSuccess}
98+
successDelay={2500}
99+
/>
100+
);
101+
102+
// Unmount the component before the timeout fires.
103+
unmount();
104+
105+
// Run all timers. If the cleanup works, `onSuccess` should not be called.
106+
jest.runAllTimers();
107+
expect(onSuccess).not.toHaveBeenCalled();
108+
109+
jest.useRealTimers();
110+
});
90111
});

packages/react/src/components/InlineLoading/InlineLoading.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2023
2+
* Copyright IBM Corp. 2016, 2025
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import React from 'react';
8+
import React, { useEffect, useRef } from 'react';
99
import PropTypes from 'prop-types';
1010
import classNames from 'classnames';
1111
import { CheckmarkFilled, ErrorFilled } from '@carbon/icons-react';
@@ -72,6 +72,25 @@ const InlineLoading = ({
7272
}: InlineLoadingProps) => {
7373
const prefix = usePrefix();
7474
const loadingClasses = classNames(`${prefix}--inline-loading`, className);
75+
const timerRef = useRef<NodeJS.Timeout | null>(null);
76+
77+
useEffect(() => {
78+
if (status === 'finished') {
79+
timerRef.current = setTimeout(() => {
80+
if (onSuccess) {
81+
onSuccess();
82+
}
83+
}, successDelay);
84+
}
85+
86+
return () => {
87+
if (timerRef.current) {
88+
clearTimeout(timerRef.current);
89+
timerRef.current = null;
90+
}
91+
};
92+
}, [status, onSuccess, successDelay]);
93+
7594
const getLoading = () => {
7695
let iconLabel = iconDescription ? iconDescription : status;
7796
if (status === 'error') {
@@ -82,11 +101,6 @@ const InlineLoading = ({
82101
);
83102
}
84103
if (status === 'finished') {
85-
setTimeout(() => {
86-
if (onSuccess) {
87-
onSuccess();
88-
}
89-
}, successDelay);
90104
return (
91105
<CheckmarkFilled
92106
className={`${prefix}--inline-loading__checkmark-container`}>
@@ -109,6 +123,9 @@ const InlineLoading = ({
109123
}
110124
return undefined;
111125
};
126+
127+
// TODO: Should this element only be constructed, similar to
128+
// `loadingAnimation`, if `description` is specified?
112129
const loadingText = (
113130
<div className={`${prefix}--inline-loading__text`}>{description}</div>
114131
);

0 commit comments

Comments
 (0)