diff --git a/src/components/text-input/text-input.test.tsx b/src/components/text-input/text-input.test.tsx
index b8d02eb033..cc0e35987f 100644
--- a/src/components/text-input/text-input.test.tsx
+++ b/src/components/text-input/text-input.test.tsx
@@ -1,5 +1,6 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
+import { createRef } from 'react';
import { InputType, TextInput } from './text-input';
const testInputType = (type: InputType) => (): void => {
@@ -76,4 +77,125 @@ describe('', () => {
const textInput = screen.getByTestId('textInput');
expect(textInput).toHaveClass('a-text-input--full');
});
+
+ it('attaches inputRef to the underlying input', () => {
+ const inputRef = createRef();
+ render();
+
+ expect(inputRef.current).toBe(screen.getByTestId('textInput'));
+ });
+
+ it('supports callback inputRef', () => {
+ let node: HTMLInputElement | null = null;
+ render(
+ {
+ node = element;
+ }}
+ />,
+ );
+
+ expect(node).toBe(screen.getByTestId('textInput'));
+ });
+
+ it('attaches both inputRef and forwarded ref', () => {
+ const inputRef = createRef();
+ const forwardedRef = createRef();
+ render(
+ ,
+ );
+
+ const textInput = screen.getByTestId('textInput');
+ expect(inputRef.current).toBe(textInput);
+ expect(forwardedRef.current).toBe(textInput);
+ });
+
+ it('keeps a stable inputRef attached across rerenders', () => {
+ const calls: (HTMLInputElement | null)[] = [];
+ const inputRef = (element: HTMLInputElement | null): void => {
+ calls.push(element);
+ };
+ const { rerender } = render(
+ ,
+ );
+
+ rerender(
+ ,
+ );
+
+ expect(calls).toEqual([screen.getByTestId('textInput')]);
+ });
+
+ it('clears object refs on unmount', () => {
+ const inputRef = createRef();
+ const forwardedRef = createRef();
+ const { unmount } = render(
+ ,
+ );
+
+ unmount();
+
+ expect(inputRef.current).toBeNull();
+ expect(forwardedRef.current).toBeNull();
+ });
+
+ it('runs ref cleanups on unmount', () => {
+ const calls: string[] = [];
+ const { unmount } = render(
+ {
+ if (!element) return;
+ calls.push('input-attach');
+ return () => {
+ calls.push('input-cleanup');
+ };
+ }}
+ ref={(element) => {
+ if (!element) return;
+ calls.push('forwarded-attach');
+ return () => {
+ calls.push('forwarded-cleanup');
+ };
+ }}
+ />,
+ );
+
+ expect(calls).toEqual(['input-attach', 'forwarded-attach']);
+ unmount();
+ expect(calls).toEqual([
+ 'input-attach',
+ 'forwarded-attach',
+ 'input-cleanup',
+ 'forwarded-cleanup',
+ ]);
+ });
+
+ it('calls a callback inputRef with null on unmount when it returns no cleanup', () => {
+ const calls: (HTMLInputElement | null)[] = [];
+ const inputRef = (element: HTMLInputElement | null): void => {
+ calls.push(element);
+ };
+ const { unmount } = render(
+ ,
+ );
+
+ unmount();
+
+ expect(calls.at(-1)).toBeNull();
+ expect(calls[0]).toBeInstanceOf(HTMLInputElement);
+ });
});
diff --git a/src/components/text-input/text-input.tsx b/src/components/text-input/text-input.tsx
index 5193d2183a..9ea9a6223b 100644
--- a/src/components/text-input/text-input.tsx
+++ b/src/components/text-input/text-input.tsx
@@ -2,14 +2,37 @@ import classnames from 'classnames';
import {
forwardRef,
JSX,
+ useCallback,
type ComponentPropsWithoutRef,
- type ReactNode,
+ type Ref,
} from 'react';
import './cfpb-text-input.scss';
import type { TextInputStatusType } from './text-input-status';
import { getTextInputStatusClass } from './text-input-status';
-type TextInputReference = ReactNode;
+type TextInputReference = Ref;
+
+const releaseNothing = (): void => undefined;
+
+const bindReference = (
+ target: Ref | undefined,
+ element: HTMLInputElement | null,
+): (() => void) => {
+ if (typeof target === 'function') {
+ const cleanup = target(element);
+ return (): void => {
+ if (typeof cleanup === 'function') cleanup();
+ else target(null);
+ };
+ }
+ if (target) {
+ target.current = element;
+ return (): void => {
+ target.current = null;
+ };
+ }
+ return releaseNothing;
+};
export type InputType =
'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url';
@@ -42,7 +65,7 @@ export const TextInput = forwardRef(
{
className,
id,
- inputRef: _inputReference,
+ inputRef,
isDisabled = false,
name,
status,
@@ -62,6 +85,18 @@ export const TextInput = forwardRef(
classes.push('a-text-input--full');
}
+ const setReferences = useCallback(
+ (element: HTMLInputElement | null): (() => void) => {
+ const releaseInputReference = bindReference(inputRef, element);
+ const releaseForwardedReference = bindReference(reference, element);
+ return (): void => {
+ releaseInputReference();
+ releaseForwardedReference();
+ };
+ },
+ [inputRef, reference],
+ );
+
const input = (
(
id={id}
name={name}
type={type}
- ref={reference}
+ ref={setReferences}
{...otherInputProperties}
/>
);