Skip to content
This repository was archived by the owner on Jun 28, 2026. It is now read-only.

Commit 2741478

Browse files
feat: New useKeyFilter hook
1 parent 8c318a7 commit 2741478

4 files changed

Lines changed: 144 additions & 0 deletions

File tree

packages/hooks/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './use-attr-selector';
22
export * from './use-controlled-state';
33
export * from './use-event-listener';
44
export * from './use-id';
5+
export * from './use-key-filter';
56
export * from './use-match-media';
67
export * from './use-mount-effect';
78
export * from './use-previous';

packages/hooks/src/use-key-filter/index.test.ts

Whitespace-only changes.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { isAndroid } from '@primeuix/utils';
2+
3+
/**
4+
* The options for the `useKeyFilter` hook.
5+
*/
6+
export interface UseKeyFilterOptions {
7+
/**
8+
* Sets the pattern for key filtering.
9+
* @default /./
10+
*/
11+
pattern?: 'pint' | 'int' | 'pnum' | 'money' | 'num' | 'hex' | 'email' | 'alpha' | 'alphanum' | RegExp;
12+
/**
13+
* When enabled, instead of blocking keys, input is validated internally to test against the regular expression.
14+
* @default false
15+
*/
16+
validateOnly?: boolean;
17+
}
18+
19+
export interface UseKeyFilterExposes {
20+
/**
21+
* Handles input events for key filter.
22+
* Processes character input and composition events while applying the filter pattern.
23+
* @param event - The form or composition event from the input element
24+
*/
25+
onBeforeInput: (event: React.CompositionEvent<HTMLInputElement>) => void;
26+
/**
27+
* Handles keypress events for character input validation.
28+
* Validates and places characters according to the filter pattern.
29+
* @param event - The keyboard event from the input element
30+
*/
31+
onKeyPress: (event: React.KeyboardEvent<HTMLInputElement>) => void;
32+
/**
33+
* Handles paste events for clipboard content insertion.
34+
* Processes pasted content according to the mask pattern.
35+
* @param event - The clipboard event from the input element
36+
*/
37+
onPaste: (event: React.ClipboardEvent<HTMLInputElement>) => void;
38+
/**
39+
* Validates the current input value against the filter pattern.
40+
* @param event - The form event from the input element
41+
* @returns true if the value matches the pattern, false otherwise
42+
*/
43+
validate: (event: React.FormEvent<HTMLInputElement>) => boolean;
44+
}
45+
46+
export function useKeyFilter(options: UseKeyFilterOptions): UseKeyFilterExposes {
47+
const { pattern = /./, validateOnly = false } = options;
48+
49+
const DEFAULT_MASKS = {
50+
pint: /[\d]/,
51+
int: /[\d-]/,
52+
pnum: /[\d.]/,
53+
money: /[\d.\s,]/,
54+
num: /[\d-.]/,
55+
hex: /[0-9a-f]/i,
56+
email: /[a-z0-9_.-@]/i,
57+
alpha: /[a-z_]/i,
58+
alphanum: /[a-z0-9_]/
59+
};
60+
61+
const getRegex = (): RegExp => {
62+
return typeof pattern === 'string' ? DEFAULT_MASKS[pattern] : pattern;
63+
};
64+
65+
const onBeforeInput = (event: React.CompositionEvent<HTMLInputElement>) => {
66+
// android devices must use beforeinput https://stackoverflow.com/questions/36753548/keycode-on-android-is-always-229
67+
if (validateOnly || !isAndroid()) {
68+
return;
69+
}
70+
71+
validateKey(event, event.data);
72+
};
73+
74+
const onKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
75+
// non android devices use keydown
76+
if (validateOnly || isAndroid()) {
77+
return;
78+
}
79+
80+
if (event.ctrlKey || event.altKey || event.metaKey) {
81+
return;
82+
}
83+
84+
validateKey(event, event.key);
85+
};
86+
87+
const onPaste = (event: React.ClipboardEvent<HTMLInputElement>) => {
88+
if (validateOnly) {
89+
return;
90+
}
91+
92+
const regex = getRegex();
93+
const clipboard = event.clipboardData.getData('text');
94+
95+
// loop over each letter pasted and if any fail prevent the paste
96+
[...clipboard].forEach((c) => {
97+
if (!regex.test(c)) {
98+
event.preventDefault();
99+
100+
return false;
101+
}
102+
});
103+
};
104+
105+
const validateKey = (event: React.CompositionEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>, key: string) => {
106+
if (key === null || key === undefined) {
107+
return;
108+
}
109+
110+
// some AZERTY keys come in with 2 chars like ´ç if Dead key is pressed first
111+
const isPrintableKey = key.length <= 2;
112+
113+
if (!isPrintableKey) {
114+
return;
115+
}
116+
117+
const regex = getRegex();
118+
119+
if (!regex.test(key)) {
120+
event.preventDefault();
121+
}
122+
};
123+
124+
const validate = (event: React.FormEvent<HTMLInputElement>) => {
125+
const value = (event.target as HTMLInputElement).value;
126+
let validatePattern = true;
127+
128+
const regex = getRegex();
129+
130+
if (value && !regex.test(value)) {
131+
validatePattern = false;
132+
}
133+
134+
return validatePattern;
135+
};
136+
137+
return {
138+
onBeforeInput,
139+
onKeyPress,
140+
onPaste,
141+
validate
142+
};
143+
}

packages/hooks/src/use-mask/index.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)