Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/tanstack-textarea-onchange-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"use-mask-input": patch
---

Widen `TanStackFormInputProps.onChange`/`onBlur` to accept `HTMLTextAreaElement` in addition to `HTMLInputElement`.

`use-mask-input` supports masking `<textarea>` elements, but #184 pinned the TanStack Form `onChange`/`onBlur` event types to `ChangeEvent<HTMLInputElement>`/`FocusEvent<HTMLInputElement>` only. That broke consumers masking a `<textarea>` field who explicitly annotate their handler as `ChangeEvent<HTMLTextAreaElement>`, and mistyped the event for anyone relying on inline (unannotated) handlers rendering a `<textarea>`. The event types are now `ChangeEvent<HTMLInputElement | HTMLTextAreaElement>` / `FocusEvent<HTMLInputElement | HTMLTextAreaElement>`.
45 changes: 45 additions & 0 deletions packages/use-mask-input/src/api/useTanStackFormMask.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {

import useTanStackFormMask from './useTanStackFormMask';

import type { ChangeEvent } from 'react';
import type { TanStackFormInputProps } from '../types';

vi.mock('../core/inputmask', () => ({
Expand Down Expand Up @@ -103,4 +104,48 @@ describe('useTanStackFormMask', () => {

expect(handleChange).toHaveBeenCalledWith('12345');
});

it('infers the onChange event type for a masked textarea field', () => {
const { result } = renderHook(() => useTanStackFormMask());
const handleChange = vi.fn();

// use-mask-input also supports masking <textarea> elements. The inline
// `event` parameter must infer a type whose `.target` covers
// HTMLTextAreaElement, not be pinned to HTMLInputElement only.
const masked = result.current('999-999', {
name: 'notes',
value: '',
onBlur: vi.fn(),
onChange: (event) => handleChange(event.target.value),
});

const textarea = document.createElement('textarea');
textarea.value = 'hello';
masked.onChange?.({ target: textarea } as never);

expect(handleChange).toHaveBeenCalledWith('hello');
});

it('accepts a handler explicitly typed for HTMLInputElement-only members', () => {
const { result } = renderHook(() => useTanStackFormMask());
const handleFiles = vi.fn();

// A handler narrowly annotated as ChangeEvent<HTMLInputElement> (reading an
// input-only member like `.files`) must still be assignable to onChange.
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
handleFiles(event.target.files);
};

const masked = result.current('cpf', {
name: 'cpf',
value: '',
onBlur: vi.fn(),
onChange,
});

const input = document.createElement('input');
masked.onChange?.({ target: input } as never);

expect(handleFiles).toHaveBeenCalledWith(input.files);
});
});
8 changes: 6 additions & 2 deletions packages/use-mask-input/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,12 @@ export interface TanStackFormInputProps {
name?: string;
value?: string | number | readonly string[];
ref?: RefCallback<HTMLElement | null>;
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
// Method shorthand (not an arrow-typed property) so the parameter is checked
// bivariantly: handlers explicitly typed for just HTMLInputElement (e.g. using
// `event.target.files`) or just HTMLTextAreaElement (e.g. `event.target.cols`)
// both remain assignable, alongside the common inline, unannotated handler.
onChange?(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void;
onBlur?(event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>): void;
[key: string]: unknown;
}

Expand Down
Loading