From 3cce32f376a42590b2bfa3e050baf166356a917a Mon Sep 17 00:00:00 2001 From: Erik Rasmussen Date: Wed, 4 Feb 2026 17:30:40 +0100 Subject: [PATCH 1/6] Update react-dom peerDependencies to match react versions Closes #28 Closes #30 react-dom now supports React 17, 18, and 19 (matching react peerDeps). --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7538a1e..a7bd3e5 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "peerDependencies": { "final-form": ">=5.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^18.2.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-final-form": ">=7.0.0" }, "jest": { From 280ce650531b158b61d783c1676ded88f8ea2106 Mon Sep 17 00:00:00 2001 From: "Dinesh (AI Agent)" Date: Fri, 6 Feb 2026 18:57:21 +0100 Subject: [PATCH 2/6] Fix: Remove ReactDOM.findDOMNode for React 19 compatibility Replace findDOMNode with React refs to support React 19 where findDOMNode has been removed. Changes: - Removed ReactDOM import (no longer needed) - Added rootRef using React.createRef() - Replaced ReactDOM.findDOMNode(this) with this.rootRef.current - Merged innerRef and rootRef in render to maintain both internal and external ref functionality Per CodeRabbit feedback on PR #37. --- src/Html5ValidationField.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Html5ValidationField.tsx b/src/Html5ValidationField.tsx index c8dd60d..a5ad77c 100644 --- a/src/Html5ValidationField.tsx +++ b/src/Html5ValidationField.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import ReactDOM from 'react-dom' import { Field } from 'react-final-form' import { Html5ValidationFieldProps } from './types' import warning from './warning' @@ -24,6 +23,7 @@ interface WithValidity { class Html5ValidationField extends React.Component { private input: WithValidity | null = null + private rootRef = React.createRef() static defaultProps = { badInput: 'Incorrect input', @@ -42,7 +42,7 @@ class Html5ValidationField extends React.Component { } componentDidMount(): void { - const root = ReactDOM.findDOMNode(this) + const root = this.rootRef.current if (root) { let input: WithValidity | null = null if (/input|textarea|select/.test(root.nodeName.toLowerCase())) { @@ -137,10 +137,22 @@ class Html5ValidationField extends React.Component { ...fieldProps } = rest + // Merge innerRef with rootRef for internal use + const mergedRef = (node: HTMLElement | null) => { + // Set internal ref + (this.rootRef as React.MutableRefObject).current = node + // Call innerRef if provided + if (typeof innerRef === 'function') { + innerRef(node) + } else if (innerRef) { + (innerRef as React.MutableRefObject).current = node + } + } + return React.createElement(Field, { ...fieldProps, validate: this.validate, - ref: innerRef as React.Ref, + ref: mergedRef, component: 'input' }) } From f8ed216e28e6b69896f9ce2275c1726c7d7ace8f Mon Sep 17 00:00:00 2001 From: "Dinesh (AI Agent)" Date: Fri, 6 Feb 2026 19:01:40 +0100 Subject: [PATCH 3/6] Fix: Properly attach ref to Field's rendered input element Previous approach tried to ref the Field component, which doesn't work. Now wrapping the render/children function to inject the ref into the actual input element that Field renders. Changes: - Renamed rootRef to fieldRef for clarity - Added findInput() helper called in componentDidMount/Update - Wrap render/children function to clone element with merged ref - Merge innerRef with fieldRef to support both internal and external refs This ensures we get access to the actual DOM input element for HTML5 validation while maintaining React 19 compatibility (no findDOMNode). --- src/Html5ValidationField.tsx | 46 +++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/src/Html5ValidationField.tsx b/src/Html5ValidationField.tsx index a5ad77c..41de2e2 100644 --- a/src/Html5ValidationField.tsx +++ b/src/Html5ValidationField.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Field } from 'react-final-form' +import { Field, FieldRenderProps } from 'react-final-form' import { Html5ValidationFieldProps } from './types' import warning from './warning' @@ -23,7 +23,7 @@ interface WithValidity { class Html5ValidationField extends React.Component { private input: WithValidity | null = null - private rootRef = React.createRef() + private fieldRef = React.createRef() static defaultProps = { badInput: 'Incorrect input', @@ -42,7 +42,17 @@ class Html5ValidationField extends React.Component { } componentDidMount(): void { - const root = this.rootRef.current + this.findInput() + } + + componentDidUpdate(): void { + if (!this.input) { + this.findInput() + } + } + + private findInput = (): void => { + const root = this.fieldRef.current if (root) { let input: WithValidity | null = null if (/input|textarea|select/.test(root.nodeName.toLowerCase())) { @@ -120,6 +130,9 @@ class Html5ValidationField extends React.Component { typeMismatch, valueMissing, innerRef, + component, + render, + children, ...rest } = this.props @@ -137,11 +150,9 @@ class Html5ValidationField extends React.Component { ...fieldProps } = rest - // Merge innerRef with rootRef for internal use + // Merge innerRef with fieldRef const mergedRef = (node: HTMLElement | null) => { - // Set internal ref - (this.rootRef as React.MutableRefObject).current = node - // Call innerRef if provided + (this.fieldRef as React.MutableRefObject).current = node if (typeof innerRef === 'function') { innerRef(node) } else if (innerRef) { @@ -149,11 +160,28 @@ class Html5ValidationField extends React.Component { } } + // Wrap render function to inject ref + const wrappedRender = (fieldProps: FieldRenderProps) => { + // Call user's render/children function if provided + const userRender = render || children + if (userRender && typeof userRender === 'function') { + const element = userRender(fieldProps) + // Clone and inject ref + return React.isValidElement(element) + ? React.cloneElement(element, { ref: mergedRef } as any) + : element + } + // Default: render input with ref + return React.createElement(component || 'input', { + ...fieldProps.input, + ref: mergedRef + }) + } + return React.createElement(Field, { ...fieldProps, validate: this.validate, - ref: mergedRef, - component: 'input' + children: wrappedRender }) } } From b3f06f7c1a286f23d6b92c13124c9790b9de4d7e Mon Sep 17 00:00:00 2001 From: "Dinesh (AI Agent)" Date: Fri, 6 Feb 2026 19:02:18 +0100 Subject: [PATCH 4/6] WIP: Update tests to remove ReactDOM.findDOMNode mocks Tests need further work to properly test ref-based approach without mocking findDOMNode. --- src/Html5ValidationField.test.tsx | 32 +++++++------------------------ 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/src/Html5ValidationField.test.tsx b/src/Html5ValidationField.test.tsx index bddae60..77893ec 100644 --- a/src/Html5ValidationField.test.tsx +++ b/src/Html5ValidationField.test.tsx @@ -1,5 +1,4 @@ import React from 'react' -import ReactDOM from 'react-dom' import { render, cleanup } from '@testing-library/react' import { Form, FieldRenderProps, FieldInputProps } from 'react-final-form' import Html5ValidationField, { @@ -160,30 +159,10 @@ describe('Html5ValidationField', () => { }) describe('Html5ValidationField.validity', () => { - let findDOMNodeSpy: jest.SpyInstance - afterEach(() => { - if (findDOMNodeSpy) { - findDOMNodeSpy.mockRestore() - } - }) - const mockFindNode = (querySelector: jest.Mock) => { - const div = document.createElement('div') - div.querySelector = querySelector - findDOMNodeSpy = jest.spyOn(ReactDOM, 'findDOMNode').mockReturnValue(div) - return div - } - it('should use the root node if it is an input element', () => { - const input = document.createElement('input') - input.name = 'foo' - input.setCustomValidity = jest.fn() - Object.defineProperty(input, 'validity', { - value: { valid: true } as ValidityState, - configurable: true - }) - findDOMNodeSpy = jest - .spyOn(ReactDOM, 'findDOMNode') - .mockReturnValue(input) + const setCustomValiditySpy = jest.fn() + HTMLInputElement.prototype.setCustomValidity = setCustomValiditySpy + render(
{() => ( @@ -193,7 +172,10 @@ describe('Html5ValidationField', () => { )}
) - expect(input.setCustomValidity).toHaveBeenCalled() + // Wait for componentDidMount to find the input + setTimeout(() => { + expect(setCustomValiditySpy).toHaveBeenCalled() + }, 0) }) it('should search DOM for input if the root is not the input', () => { From d1bbf16f93ab260d0e2ced3215a01d19e85922d2 Mon Sep 17 00:00:00 2001 From: Erik Rasmussen Date: Fri, 13 Feb 2026 04:30:19 +0100 Subject: [PATCH 5/6] Fix: Address CodeRabbit issues - Fix noChildrenProp lint rule by passing wrappedRender as third arg - Update ref types from any/Html5ValidationField to HTMLElement - Fix async test to use jest.spyOn and waitFor instead of setTimeout - Add waitFor import from @testing-library/react Note: mockFindNode tests still need refactoring to work with ref-based approach --- src/Html5ValidationField.test.tsx | 15 +++++++++------ src/Html5ValidationField.tsx | 19 +++++++++++-------- src/types.ts | 2 +- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/Html5ValidationField.test.tsx b/src/Html5ValidationField.test.tsx index 77893ec..005614c 100644 --- a/src/Html5ValidationField.test.tsx +++ b/src/Html5ValidationField.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { render, cleanup } from '@testing-library/react' +import { render, cleanup, waitFor } from '@testing-library/react' import { Form, FieldRenderProps, FieldInputProps } from 'react-final-form' import Html5ValidationField, { Html5ValidationField as Html5ValidationFieldClass @@ -159,9 +159,11 @@ describe('Html5ValidationField', () => { }) describe('Html5ValidationField.validity', () => { - it('should use the root node if it is an input element', () => { - const setCustomValiditySpy = jest.fn() - HTMLInputElement.prototype.setCustomValidity = setCustomValiditySpy + it('should use the root node if it is an input element', async () => { + const setCustomValiditySpy = jest.spyOn( + HTMLInputElement.prototype, + 'setCustomValidity' + ) render(
@@ -173,9 +175,10 @@ describe('Html5ValidationField', () => {
) // Wait for componentDidMount to find the input - setTimeout(() => { + await waitFor(() => { expect(setCustomValiditySpy).toHaveBeenCalled() - }, 0) + }) + setCustomValiditySpy.mockRestore() }) it('should search DOM for input if the root is not the input', () => { diff --git a/src/Html5ValidationField.tsx b/src/Html5ValidationField.tsx index 41de2e2..49141c3 100644 --- a/src/Html5ValidationField.tsx +++ b/src/Html5ValidationField.tsx @@ -23,7 +23,7 @@ interface WithValidity { class Html5ValidationField extends React.Component { private input: WithValidity | null = null - private fieldRef = React.createRef() + private fieldRef = React.createRef() static defaultProps = { badInput: 'Incorrect input', @@ -178,24 +178,27 @@ class Html5ValidationField extends React.Component { }) } - return React.createElement(Field, { - ...fieldProps, - validate: this.validate, - children: wrappedRender - }) + return React.createElement( + Field, + { + ...fieldProps, + validate: this.validate + }, + wrappedRender + ) } } function Html5ValidationFieldWithRef( props: Omit, - ref: React.Ref + ref: React.Ref ): React.ReactElement { const { name, ...rest } = props return } const ForwardedHtml5ValidationField = React.forwardRef< - Html5ValidationField, + HTMLElement, Omit >(Html5ValidationFieldWithRef) diff --git a/src/types.ts b/src/types.ts index 50ce5fa..717a60a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,5 +17,5 @@ export interface Messages { export interface Html5ValidationFieldProps extends FieldProps, Messages { validate?: FieldValidator - innerRef?: React.Ref + innerRef?: React.Ref } \ No newline at end of file From 8858035b15149c81cc0d39f971579a30405af03a Mon Sep 17 00:00:00 2001 From: Erik Rasmussen Date: Wed, 6 May 2026 14:08:33 +0200 Subject: [PATCH 6/6] Fix ref-based validation tests --- src/Html5ValidationField.test.tsx | 483 ++++++++---------------------- src/Html5ValidationField.tsx | 41 +-- src/index.ts | 2 +- src/types.ts | 38 ++- src/warning.ts | 4 +- 5 files changed, 183 insertions(+), 385 deletions(-) diff --git a/src/Html5ValidationField.test.tsx b/src/Html5ValidationField.test.tsx index 005614c..701d934 100644 --- a/src/Html5ValidationField.test.tsx +++ b/src/Html5ValidationField.test.tsx @@ -159,12 +159,69 @@ describe('Html5ValidationField', () => { }) describe('Html5ValidationField.validity', () => { + const renderWithNestedInput = ({ + name = 'foo', + validity = { valid: true } as ValidityState, + validationMessage, + validate, + fieldProps = {}, + initialValues = {} + }: { + name?: string + validity?: ValidityState + validationMessage?: string + validate?: (value: unknown, allValues: object) => unknown + fieldProps?: Record + initialValues?: Record + } = {}) => { + const setCustomValidity = jest.fn() + const result = render( +
+ {() => ( + + {({ input }: TestFieldRenderProps) => ( +
+ { + if (node) { + node.setCustomValidity = setCustomValidity + Object.defineProperty(node, 'validity', { + value: validity, + configurable: true + }) + if (validationMessage) { + Object.defineProperty(node, 'validationMessage', { + value: validationMessage, + configurable: true + }) + } + } + }} + /> +
+ )} +
+ )} +
+ ) + return { ...result, setCustomValidity } + } + it('should use the root node if it is an input element', async () => { const setCustomValiditySpy = jest.spyOn( HTMLInputElement.prototype, 'setCustomValidity' ) - + render(
{() => ( @@ -174,104 +231,64 @@ describe('Html5ValidationField', () => { )}
) - // Wait for componentDidMount to find the input await waitFor(() => { expect(setCustomValiditySpy).toHaveBeenCalled() }) setCustomValiditySpy.mockRestore() }) - it('should search DOM for input if the root is not the input', () => { - const input = document.createElement('input') - input.name = 'foo' - const querySelector = jest.fn().mockReturnValue(input) - const div = mockFindNode(querySelector) - render( -
{}}> - {() => ( - - {({ input }: TestFieldRenderProps) => ( -
div}>{input.value}
- )} -
- )} -
+ it('should search DOM for input if the root is not the input', async () => { + const querySelectorSpy = jest.spyOn( + HTMLElement.prototype, + 'querySelector' ) - expect(querySelector).toHaveBeenCalled() - expect(querySelector).toHaveBeenCalledTimes(1) - const calls = querySelector.mock.calls - if (calls.length > 0) { - expect(calls[0][0]).toBe( - 'input[name="foo"],textarea[name="foo"],select[name="foo"]' - ) - } - }) - - it('should search DOM for input if the root is not the input, even for deep fields', () => { - const input = document.createElement('input') - input.name = 'foo.bar' - const querySelector = jest.fn().mockReturnValue(input) - const div = mockFindNode(querySelector) - render( -
{}}> - {() => ( - - {({ input }: TestFieldRenderProps) => ( -
div}>{input.value}
- )} -
- )} -
+ const { setCustomValidity } = renderWithNestedInput() + await waitFor(() => expect(setCustomValidity).toHaveBeenCalled()) + expect(querySelectorSpy).toHaveBeenCalledWith( + 'input[name="foo"],textarea[name="foo"],select[name="foo"]' ) - expect(querySelector).toHaveBeenCalled() - expect(querySelector).toHaveBeenCalledTimes(1) - const calls = querySelector.mock.calls - if (calls.length > 0) { - expect(calls[0][0]).toBe( - 'input[name="foo.bar"],textarea[name="foo.bar"],select[name="foo.bar"]' - ) - } + querySelectorSpy.mockRestore() }) - it('should fail silently if no DOM node could be found (probably SSR)', () => { - const consoleSpy = jest - .spyOn(global.console, 'error') - .mockImplementation(() => {}) - findDOMNodeSpy = jest.spyOn(ReactDOM, 'findDOMNode').mockReturnValue(null) - render( -
- {() => ( - - {({ input }: TestFieldRenderProps) => } - - )} -
+ it('should search DOM for input if the root is not the input, even for deep fields', async () => { + const querySelectorSpy = jest.spyOn( + HTMLElement.prototype, + 'querySelector' ) - expect(consoleSpy).not.toHaveBeenCalled() - consoleSpy.mockRestore() + const { setCustomValidity } = renderWithNestedInput({ name: 'foo.bar' }) + await waitFor(() => expect(setCustomValidity).toHaveBeenCalled()) + expect(querySelectorSpy).toHaveBeenCalledWith( + 'input[name="foo.bar"],textarea[name="foo.bar"],select[name="foo.bar"]' + ) + querySelectorSpy.mockRestore() }) it('should warn if no input could be found because DOM node has no querySelector API', () => { const consoleSpy = jest .spyOn(global.console, 'error') .mockImplementation(() => {}) - const div = document.createElement('div') - Object.defineProperty(div, 'querySelector', { - value: undefined, - configurable: true - }) render(
{() => ( {({ input }: TestFieldRenderProps) => ( -
div}>{input.value}
+
{ + if (node) { + Object.defineProperty(node, 'querySelector', { + value: undefined, + configurable: true + }) + } + }} + > + {input.value} +
)}
)}
) - expect(consoleSpy).toHaveBeenCalled() expect(consoleSpy).toHaveBeenCalledTimes(1) expect(consoleSpy).toHaveBeenCalledWith( 'Warning: Could not find DOM input with HTML validity API' @@ -283,349 +300,110 @@ describe('Html5ValidationField', () => { const consoleSpy = jest .spyOn(global.console, 'error') .mockImplementation(() => {}) - const querySelector = jest.fn(() => null) - const div = mockFindNode(querySelector) render(
{() => ( - {({ input }: TestFieldRenderProps) => ( -
div}>{input.value}
- )} + {({ input }: TestFieldRenderProps) =>
{input.value}
}
)}
) - expect(querySelector).toHaveBeenCalled() - expect(querySelector).toHaveBeenCalledTimes(1) - expect(querySelector).toHaveBeenCalledWith( - 'input[name="foo"],textarea[name="foo"],select[name="foo"]' - ) - expect(consoleSpy).toHaveBeenCalled() - expect(consoleSpy).toHaveBeenCalledTimes(1) expect(consoleSpy).toHaveBeenCalledWith( 'Warning: Could not find DOM input with HTML validity API' ) consoleSpy.mockRestore() }) - it('should read/write validity from/to the input', () => { - const setCustomValidity = jest.fn() - const input = document.createElement('input') - input.name = 'foo' - input.setCustomValidity = setCustomValidity - Object.defineProperty(input, 'validity', { - value: { + it('should read/write validity from/to the input', async () => { + const { setCustomValidity } = renderWithNestedInput({ + validity: { valueMissing: true, valid: false - } as ValidityState, - configurable: true + } as ValidityState }) - const querySelector = jest.fn().mockReturnValue(input) - const div = mockFindNode(querySelector) - render( -
- {() => ( - - {({ input }: TestFieldRenderProps) => ( -
div}>{input.value}
- )} -
- )} -
- ) - expect(setCustomValidity).toHaveBeenCalled() - expect(setCustomValidity).toHaveBeenCalledTimes(2) + await waitFor(() => expect(setCustomValidity).toHaveBeenCalledTimes(2)) expect(setCustomValidity.mock.calls[0][0]).toBe('') expect(setCustomValidity.mock.calls[1][0]).toBe('Required') }) - it('should use field-level validation function', () => { + it('should use field-level validation function', async () => { const validate = jest.fn().mockReturnValue('bar') - const setCustomValidity = jest.fn() - const input = document.createElement('input') - input.name = 'foo' - input.setCustomValidity = setCustomValidity - Object.defineProperty(input, 'validity', { - value: { valid: true } as ValidityState, - configurable: true + const { setCustomValidity } = renderWithNestedInput({ + validate, + initialValues: { foo: 'test' } }) - const querySelector = jest.fn().mockReturnValue(input) - const div = document.createElement('div') - div.querySelector = querySelector - findDOMNodeSpy = jest.spyOn(ReactDOM, 'findDOMNode').mockReturnValue(div) - render( -
- {() => ( - - {({ input }: TestFieldRenderProps) => ( -
div}>{input.value}
- )} -
- )} -
- ) - expect(validate).toHaveBeenCalled() - expect(validate).toHaveBeenCalledTimes(1) + await waitFor(() => expect(validate).toHaveBeenCalled()) expect(validate.mock.calls[0][0]).toBe('test') + expect(setCustomValidity).toHaveBeenCalledWith('bar') }) - it('should not call setCustomValidity if no validity API is found', () => { + it('should not call setCustomValidity if validation error is not a string', async () => { const consoleSpy = jest .spyOn(global.console, 'error') .mockImplementation(() => {}) - const querySelector = jest.fn(() => null) - const div = mockFindNode(querySelector) - render( -
- {() => ( - - {({ input }: TestFieldRenderProps) => ( -
div}>{input.value}
- )} -
- )} -
- ) - expect(consoleSpy).toHaveBeenCalled() - expect(consoleSpy).toHaveBeenCalledTimes(1) - expect(consoleSpy).toHaveBeenCalledWith( - 'Warning: Could not find DOM input with HTML validity API' - ) - consoleSpy.mockRestore() - }) - - it('should not call setCustomValidity if validation error is not a string', () => { - const consoleSpy = jest - .spyOn(global.console, 'error') - .mockImplementation(() => {}) - const setCustomValidity = jest.fn() - const input = document.createElement('input') - input.name = 'foo' - input.setCustomValidity = setCustomValidity - Object.defineProperty(input, 'validity', { - value: { + const validate = jest.fn(() => ({ notAString: true })) + const { setCustomValidity } = renderWithNestedInput({ + validate, + initialValues: { foo: 'test' }, + validity: { valid: false, valueMissing: false - } as ValidityState, - configurable: true - }) - const querySelector = jest.fn().mockReturnValue(input) - const div = document.createElement('div') - div.querySelector = querySelector - findDOMNodeSpy = jest.spyOn(ReactDOM, 'findDOMNode').mockReturnValue(div) - const validate = () => ({ notAString: true }) - render( -
- {() => ( - - {({ input }: TestFieldRenderProps) => ( -
div}>{input.value}
- )} -
- )} -
- ) - expect(consoleSpy).not.toHaveBeenCalled() - setCustomValidity.mock.calls.forEach((call) => { - expect(call[0]).toBe('') + } as ValidityState }) - consoleSpy.mockRestore() - }) - - it('should not call setCustomValidity if no validation error', () => { - const consoleSpy = jest - .spyOn(global.console, 'error') - .mockImplementation(() => {}) - const setCustomValidity = jest.fn() - const input = document.createElement('input') - input.name = 'foo' - input.setCustomValidity = setCustomValidity - Object.defineProperty(input, 'validity', { - value: { - valid: true - } as ValidityState, - configurable: true - }) - const querySelector = jest.fn().mockReturnValue(input) - const div = mockFindNode(querySelector) - render( -
- {() => ( - - {({ input }: TestFieldRenderProps) => ( -
div}>{input.value}
- )} -
- )} -
- ) + await waitFor(() => expect(validate).toHaveBeenCalled()) expect(consoleSpy).not.toHaveBeenCalled() - expect(setCustomValidity).toHaveBeenCalled() - expect(setCustomValidity).toHaveBeenCalledTimes(1) - expect(setCustomValidity.mock.calls[0][0]).toBe('') + expect(setCustomValidity).not.toHaveBeenCalled() consoleSpy.mockRestore() }) - it('should not call setCustomValidity if valid === true', () => { + it('should not call setCustomValidity with an error if no validation error', async () => { const consoleSpy = jest .spyOn(global.console, 'error') .mockImplementation(() => {}) - const setCustomValidity = jest.fn() - const input = document.createElement('input') - input.name = 'foo' - input.setCustomValidity = setCustomValidity - Object.defineProperty(input, 'validity', { - value: { - valid: true - } as ValidityState, - configurable: true - }) - const querySelector = jest.fn().mockReturnValue(input) - const div = mockFindNode(querySelector) - render( -
- {() => ( - - {({ input }: TestFieldRenderProps) => ( -
div}>{input.value}
- )} -
- )} -
- ) + const { setCustomValidity } = renderWithNestedInput() + await waitFor(() => expect(setCustomValidity).toHaveBeenCalledTimes(1)) expect(consoleSpy).not.toHaveBeenCalled() - expect(setCustomValidity).toHaveBeenCalled() - expect(setCustomValidity).toHaveBeenCalledTimes(1) expect(setCustomValidity.mock.calls[0][0]).toBe('') consoleSpy.mockRestore() }) - it('should report back validity custom error to Final Form', () => { - const consoleSpy = jest - .spyOn(global.console, 'error') - .mockImplementation(() => {}) - const setCustomValidity = jest.fn() - const input = document.createElement('input') - input.name = 'foo' - input.setCustomValidity = setCustomValidity - Object.defineProperty(input, 'validity', { - value: { + it('should report back validity custom error to Final Form', async () => { + const { setCustomValidity } = renderWithNestedInput({ + validity: { valid: false, customError: true } as ValidityState, - configurable: true - }) - Object.defineProperty(input, 'validationMessage', { - value: 'Ooh, how custom!', - configurable: true + validationMessage: 'Ooh, how custom!' }) - const querySelector = jest.fn().mockReturnValue(input) - const div = mockFindNode(querySelector) - render( -
- {() => ( - - {({ input }: TestFieldRenderProps) => ( -
div}>{input.value}
- )} -
- )} -
- ) - expect(consoleSpy).not.toHaveBeenCalled() - expect(setCustomValidity).toHaveBeenCalled() - expect(setCustomValidity).toHaveBeenCalledTimes(1) + await waitFor(() => expect(setCustomValidity).toHaveBeenCalledTimes(1)) expect(setCustomValidity.mock.calls[0][0]).toBe('') - consoleSpy.mockRestore() }) - it('should support functions as default error keys', () => { - const setCustomValidity = jest.fn() - const input = document.createElement('input') - input.name = 'foo' - input.setCustomValidity = setCustomValidity - Object.defineProperty(input, 'validity', { - value: { + it('should support functions as default error keys', async () => { + const { setCustomValidity } = renderWithNestedInput({ + validity: { tooShort: true, valid: false } as ValidityState, - configurable: true + initialValues: { foo: 'bar' }, + fieldProps: { + minLength: 8, + tooShort: (value?: unknown, props?: Record) => + `Value ${value} should have at least ${props?.minLength} characters.` + } }) - const querySelector = jest.fn().mockReturnValue(input) - const div = document.createElement('div') - div.querySelector = querySelector - findDOMNodeSpy = jest.spyOn(ReactDOM, 'findDOMNode').mockReturnValue(div) - render( -
- {() => ( - ) => - `Value ${value} should have at least ${props?.minLength} characters.` - } - minLength={8} - name="foo" - > - {({ input }: TestFieldRenderProps) => ( -
div}>{input.value}
- )} -
- )} -
- ) - expect(setCustomValidity).toHaveBeenCalledTimes(2) - expect(setCustomValidity.mock.calls[0][0]).toBe('') + await waitFor(() => expect(setCustomValidity).toHaveBeenCalledTimes(2)) expect(setCustomValidity.mock.calls[1][0]).toBe( 'Value bar should have at least 8 characters.' ) }) - it('should warn if the root node is not an input and has no querySelector API', () => { - const consoleSpy = jest - .spyOn(global.console, 'error') - .mockImplementation(() => {}) - const root = document.createElement('div') - Object.defineProperty(root, 'querySelector', { - value: undefined, - configurable: true - }) - const findDOMNodeSpy = jest - .spyOn(ReactDOM, 'findDOMNode') - .mockReturnValue(root) - render( -
- {() => ( - - {({ input }: TestFieldRenderProps) =>
{input.value}
} -
- )} -
- ) - expect(consoleSpy).toHaveBeenCalledWith( - 'Warning: Could not find DOM input with HTML validity API' - ) - consoleSpy.mockRestore() - findDOMNodeSpy.mockRestore() - }) - it('should use validate prop when no input element is found', async () => { const validate = jest.fn().mockReturnValue('Validation error') const consoleSpy = jest .spyOn(global.console, 'error') .mockImplementation(() => {}) - findDOMNodeSpy = jest.spyOn(ReactDOM, 'findDOMNode').mockReturnValue(null) render(
{() => ( @@ -635,14 +413,11 @@ describe('Html5ValidationField', () => { )}
) - // Wait for component to mount and validate - await new Promise((resolve) => setTimeout(resolve, 0)) + await waitFor(() => expect(validate).toHaveBeenCalled()) expect(consoleSpy).toHaveBeenCalledWith( 'Warning: Could not find DOM input with HTML validity API' ) - expect(validate).toHaveBeenCalled() consoleSpy.mockRestore() - findDOMNodeSpy.mockRestore() }) }) }) diff --git a/src/Html5ValidationField.tsx b/src/Html5ValidationField.tsx index 49141c3..2e98886 100644 --- a/src/Html5ValidationField.tsx +++ b/src/Html5ValidationField.tsx @@ -45,12 +45,6 @@ class Html5ValidationField extends React.Component { this.findInput() } - componentDidUpdate(): void { - if (!this.input) { - this.findInput() - } - } - private findInput = (): void => { const root = this.fieldRef.current if (root) { @@ -152,39 +146,50 @@ class Html5ValidationField extends React.Component { // Merge innerRef with fieldRef const mergedRef = (node: HTMLElement | null) => { - (this.fieldRef as React.MutableRefObject).current = node + ;(this.fieldRef as React.MutableRefObject).current = + node if (typeof innerRef === 'function') { innerRef(node) } else if (innerRef) { - (innerRef as React.MutableRefObject).current = node + ;(innerRef as React.MutableRefObject).current = node } } // Wrap render function to inject ref - const wrappedRender = (fieldProps: FieldRenderProps) => { + const wrappedRender = ( + fieldProps: FieldRenderProps + ) => { // Call user's render/children function if provided const userRender = render || children if (userRender && typeof userRender === 'function') { const element = userRender(fieldProps) // Clone and inject ref return React.isValidElement(element) - ? React.cloneElement(element, { ref: mergedRef } as any) + ? React.cloneElement(element, { + ref: mergedRef + } as React.RefAttributes) : element } - // Default: render input with ref + // Default: render input with ref and pass through HTML field props return React.createElement(component || 'input', { + ...fieldProps, ...fieldProps.input, ref: mergedRef }) } - return React.createElement( - Field, - { - ...fieldProps, - validate: this.validate - }, - wrappedRender + const validateField = this.validate + const FieldComponent = Field as React.ComponentType< + typeof fieldProps & { + children: typeof wrappedRender + validate: typeof validateField + } + > + + return ( + + {wrappedRender} + ) } } diff --git a/src/index.ts b/src/index.ts index 0344df5..b768d9e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ import Html5ValidationField from './Html5ValidationField' export default Html5ValidationField -export { default as Field } from './Html5ValidationField' \ No newline at end of file +export { default as Field } from './Html5ValidationField' diff --git a/src/types.ts b/src/types.ts index 717a60a..c0ce7e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,18 +4,36 @@ import type { Html5ValidationField } from './Html5ValidationField' import * as React from 'react' export interface Messages { - badInput?: string | ((value?: unknown, props?: Record) => string) - patternMismatch?: string | ((value?: unknown, props?: Record) => string) - rangeOverflow?: string | ((value?: unknown, props?: Record) => string) - rangeUnderflow?: string | ((value?: unknown, props?: Record) => string) - stepMismatch?: string | ((value?: unknown, props?: Record) => string) - tooLong?: string | ((value?: unknown, props?: Record) => string) - tooShort?: string | ((value?: unknown, props?: Record) => string) - typeMismatch?: string | ((value?: unknown, props?: Record) => string) - valueMissing?: string | ((value?: unknown, props?: Record) => string) + badInput?: + | string + | ((value?: unknown, props?: Record) => string) + patternMismatch?: + | string + | ((value?: unknown, props?: Record) => string) + rangeOverflow?: + | string + | ((value?: unknown, props?: Record) => string) + rangeUnderflow?: + | string + | ((value?: unknown, props?: Record) => string) + stepMismatch?: + | string + | ((value?: unknown, props?: Record) => string) + tooLong?: + | string + | ((value?: unknown, props?: Record) => string) + tooShort?: + | string + | ((value?: unknown, props?: Record) => string) + typeMismatch?: + | string + | ((value?: unknown, props?: Record) => string) + valueMissing?: + | string + | ((value?: unknown, props?: Record) => string) } export interface Html5ValidationFieldProps extends FieldProps, Messages { validate?: FieldValidator innerRef?: React.Ref -} \ No newline at end of file +} diff --git a/src/warning.ts b/src/warning.ts index 9db7574..dbe56f3 100644 --- a/src/warning.ts +++ b/src/warning.ts @@ -5,6 +5,6 @@ export default function warning(condition: boolean, message: string): void { } try { throw new Error(`Warning: ${message}`) - } catch (e) { } + } catch (e) {} } -} \ No newline at end of file +}