Skip to content

Commit 7209bfb

Browse files
Merge pull request #8232 from primefaces/v11-keyfilter
New Hook: useKeyFilter
2 parents 8c318a7 + 38c373c commit 7209bfb

8 files changed

Lines changed: 302 additions & 0 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { useKeyFilter } from '@primereact/hooks';
2+
import { InputText } from 'primereact/inputtext';
3+
import { Label } from 'primereact/label';
4+
import * as React from 'react';
5+
6+
export default function PatternDemo() {
7+
const { onKeyPress: onIntegerKeyPress } = useKeyFilter({ pattern: 'int' });
8+
const { onKeyPress: onNumberKeyPress } = useKeyFilter({ pattern: 'num' });
9+
const { onKeyPress: onMoneyKeyPress } = useKeyFilter({ pattern: 'money' });
10+
const { onKeyPress: onHexKeyPress } = useKeyFilter({ pattern: 'hex' });
11+
const { onKeyPress: onAlphaKeyPress } = useKeyFilter({ pattern: 'alpha' });
12+
const { onKeyPress: onAlphanumKeyPress } = useKeyFilter({ pattern: 'alphanum' });
13+
14+
const [integer, setInteger] = React.useState('');
15+
const [number, setNumber] = React.useState('');
16+
const [money, setMoney] = React.useState('');
17+
const [hex, setHex] = React.useState('');
18+
const [alphabetic, setAlphabetic] = React.useState('');
19+
const [alphanumeric, setAlphanumeric] = React.useState('');
20+
21+
return (
22+
<div className="card">
23+
<div className="flex flex-wrap gap-3 mb-4">
24+
<div className="flex-auto">
25+
<Label htmlFor="integer" className="font-bold block mb-2">
26+
Integer
27+
</Label>
28+
<InputText id="integer" value={integer} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInteger(e.target.value)} onKeyPress={onIntegerKeyPress} className="w-full" />
29+
</div>
30+
<div className="flex-auto">
31+
<Label htmlFor="number" className="font-bold block mb-2">
32+
Number
33+
</Label>
34+
<InputText id="number" value={number} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNumber(e.target.value)} onKeyPress={onNumberKeyPress} className="w-full" />
35+
</div>
36+
<div className="flex-auto">
37+
<Label htmlFor="money" className="font-bold block mb-2">
38+
Money
39+
</Label>
40+
<InputText id="money" value={money} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMoney(e.target.value)} onKeyPress={onMoneyKeyPress} className="w-full" />
41+
</div>
42+
</div>
43+
<div className="flex flex-wrap gap-3">
44+
<div className="flex-auto">
45+
<Label htmlFor="hex" className="font-bold block mb-2">
46+
Hex
47+
</Label>
48+
<InputText id="hex" value={hex} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setHex(e.target.value)} onKeyPress={onHexKeyPress} className="w-full" />
49+
</div>
50+
<div className="flex-auto">
51+
<Label htmlFor="alphabetic" className="font-bold block mb-2">
52+
Alphabetic
53+
</Label>
54+
<InputText id="alphabetic" value={alphabetic} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setAlphabetic(e.target.value)} onKeyPress={onAlphaKeyPress} className="w-full" />
55+
</div>
56+
<div className="flex-auto">
57+
<Label htmlFor="alphanumeric" className="font-bold block mb-2">
58+
Alphanumeric
59+
</Label>
60+
<InputText id="alphanumeric" value={alphanumeric} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setAlphanumeric(e.target.value)} onKeyPress={onAlphanumKeyPress} className="w-full" />
61+
</div>
62+
</div>
63+
</div>
64+
);
65+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useKeyFilter } from '@primereact/hooks';
2+
import { InputText } from 'primereact/inputtext';
3+
import { Label } from 'primereact/label';
4+
import * as React from 'react';
5+
6+
export default function RegexDemo() {
7+
const { onKeyPress: onSpaceKeyPress } = useKeyFilter({ pattern: /[^\s]/ });
8+
const { onKeyPress: onCharsKeyPress } = useKeyFilter({ pattern: /^[^<>*!]+$/ });
9+
10+
const [spacekey, setSpacekey] = React.useState('');
11+
const [chars, setChars] = React.useState('');
12+
13+
return (
14+
<div className="card">
15+
<div className="flex flex-wrap gap-3">
16+
<div className="flex-auto">
17+
<Label for="spacekey" className="font-bold block mb-2">
18+
Block Space
19+
</Label>
20+
<InputText id="spacekey" value={spacekey} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSpacekey(e.target.value)} onKeyPress={onSpaceKeyPress} fluid />
21+
</div>
22+
<div className="flex-auto">
23+
<Label htmlFor="chars" className="font-bold block mb-2">
24+
Block &lt; &gt; * !
25+
</Label>
26+
<InputText id="chars" value={chars} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setChars(e.target.value)} onKeyPress={onCharsKeyPress} fluid />
27+
</div>
28+
</div>
29+
</div>
30+
);
31+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useKeyFilter } from '@primereact/hooks';
2+
import { InputText } from 'primereact/inputtext';
3+
import { Label } from 'primereact/label';
4+
import * as React from 'react';
5+
6+
export default function RegexWordDemo() {
7+
const { onKeyPress, validate } = useKeyFilter({ pattern: /^[+]?(\d{1,12})?$/, validateOnly: true });
8+
9+
const [text, setText] = React.useState('');
10+
11+
const validateInput = (e: React.ChangeEvent<HTMLInputElement>) => {
12+
if (validate(e)) {
13+
setText(e.target.value);
14+
}
15+
};
16+
17+
return (
18+
<div className="card flex justify-center">
19+
<div>
20+
<Label htmlFor="numkeys" className="font-bold block mb-2">
21+
Block Numeric (allow &quot;+&quot; only once at start)
22+
</Label>
23+
<InputText id="numkeys" value={text} onChange={(e: React.ChangeEvent<HTMLInputElement>) => validateInput(e)} onKeyPress={onKeyPress} fluid />
24+
</div>
25+
</div>
26+
);
27+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
title: useKeyFilter
3+
description: useKeyFilter used to block individual keystrokes based on a pattern.
4+
component: usekeyfilter
5+
---
6+
7+
## Usage
8+
9+
```tsx
10+
import { useKeyFilter } from '@primereact/hooks';
11+
```
12+
13+
```tsx
14+
const keyfilter = useKeyFilter();
15+
```
16+
17+
## Examples
18+
19+
### Pattern
20+
21+
useKeyFilter provides various presets configured with the _pattern_ option.
22+
23+
<DocDemoViewer name="usekeyfilter:pattern-demo" />
24+
25+
### Regex
26+
27+
In addition to the presets, a regular expression can be configured for customization of blocking a single key press.
28+
29+
<DocDemoViewer name="usekeyfilter:regex-demo" />
30+
31+
### Regex Word
32+
33+
In addition to the presets, a regular expression can be used to validate the entire word using _validateOnly_ option.
34+
35+
<DocDemoViewer name="usekeyfilter:regex-word-demo" />

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)