diff --git a/packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js b/packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js
index fff7d0a1bb8c..421ab9565709 100644
--- a/packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js
+++ b/packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js
@@ -258,6 +258,13 @@ function getTargetInstForInputOrChangeEvent(
if (domEventName === 'input' || domEventName === 'change') {
return getInstIfValueChanged(targetInst);
}
+ // Detect browser autofill: browsers may set input values without firing
+ // input or change events (e.g., Chrome on iOS, password managers).
+ // When the user focuses an autofilled field, check if the value changed.
+ // See https://github.com/facebook/react/issues/1159
+ if (domEventName === 'focusin') {
+ return getInstIfValueChanged(targetInst);
+ }
}
function handleControlledInputBlur(node: HTMLInputElement, props: any) {
diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js
index 04bd96fe2e83..91af87e7c0b2 100644
--- a/packages/react-dom/src/__tests__/ReactDOMInput-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMInput-test.js
@@ -3109,4 +3109,78 @@ describe('ReactDOMInput', () => {
expect(log).toEqual(['']);
expect(node.value).toBe('a');
});
+
+ // @see https://github.com/facebook/react/issues/1159
+ it('should detect autofilled value on focus for controlled inputs', async () => {
+ const log = [];
+ function onChange(e) {
+ log.push(e.target.value);
+ }
+ await act(() => {
+ root.render();
+ });
+
+ const node = container.firstChild;
+
+ // Simulate browser autofill: set value bypassing React's tracked setter.
+ // This mimics what happens when the browser fills a field without firing
+ // input or change events (e.g., Chrome on iOS, password managers).
+ setUntrackedValue.call(node, 'autofilled@example.com');
+
+ // No input or change event was fired, so React hasn't noticed yet.
+ expect(log).toEqual([]);
+
+ // When the user focuses the field, React should detect the changed value.
+ await act(() => {
+ dispatchEventOnNode(node, 'focusin');
+ });
+
+ expect(log).toEqual(['autofilled@example.com']);
+ });
+
+ it('should not fire extra onChange on focus when value has not changed', async () => {
+ const log = [];
+ function onChange(e) {
+ log.push(e.target.value);
+ }
+ await act(() => {
+ root.render();
+ });
+
+ const node = container.firstChild;
+
+ // Focus without changing value — should not fire onChange.
+ await act(() => {
+ dispatchEventOnNode(node, 'focusin');
+ });
+
+ expect(log).toEqual([]);
+ });
+
+ it('should not fire duplicate onChange on focus after input event', async () => {
+ const log = [];
+ function onChange(e) {
+ log.push(e.target.value);
+ }
+ await act(() => {
+ root.render();
+ });
+
+ const node = container.firstChild;
+
+ // Simulate autofill that fires an input event (normal browser behavior).
+ setUntrackedValue.call(node, 'test@test.com');
+ await act(() => {
+ dispatchEventOnNode(node, 'input');
+ });
+
+ expect(log).toEqual(['test@test.com']);
+
+ // Focus should not fire a second onChange since the tracker is in sync.
+ await act(() => {
+ dispatchEventOnNode(node, 'focusin');
+ });
+
+ expect(log).toEqual(['test@test.com']);
+ });
});