Skip to content

Commit 1604f97

Browse files
authored
feat: add param to clear input after password prompt error (#364)
1 parent 4c89dd7 commit 1604f97

File tree

6 files changed

+127
-1
lines changed

6 files changed

+127
-1
lines changed

.changeset/solid-goats-shop.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@clack/prompts": minor
3+
"@clack/core": minor
4+
---
5+
6+
Add `clearOnError` option to password prompt to automatically clear input when validation fails

packages/core/src/prompts/password.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export default class PasswordPrompt extends Prompt<string> {
2525
const s2 = masked.slice(this.cursor);
2626
return `${s1}${color.inverse(s2[0])}${s2.slice(1)}`;
2727
}
28+
clear() {
29+
this._clearUserInput();
30+
}
2831
constructor({ mask, ...opts }: PasswordOptions) {
2932
super(opts);
3033
this._mask = mask ?? '•';

packages/core/src/prompts/prompt.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ export default class Prompt<TValue> {
186186
}
187187
}
188188

189+
protected _clearUserInput(): void {
190+
this.rl?.write(null, { ctrl: true, name: 'u' });
191+
this._setUserInput('');
192+
}
193+
189194
private onKeypress(char: string | undefined, key: Key) {
190195
if (this._track && key.name !== 'return') {
191196
if (key.name && this._isActionKey(char, key)) {

packages/prompts/src/password.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface PasswordOptions extends CommonOptions {
66
message: string;
77
mask?: string;
88
validate?: (value: string | undefined) => string | Error | undefined;
9+
clearOnError?: boolean;
910
}
1011
export const password = (opts: PasswordOptions) => {
1112
return new PasswordPrompt({
@@ -22,6 +23,9 @@ export const password = (opts: PasswordOptions) => {
2223
switch (this.state) {
2324
case 'error': {
2425
const maskedText = masked ? ` ${masked}` : '';
26+
if (opts.clearOnError) {
27+
this.clear();
28+
}
2529
return `${title.trim()}\n${color.yellow(S_BAR)}${maskedText}\n${color.yellow(
2630
S_BAR_END
2731
)} ${color.yellow(this.error)}\n`;

packages/prompts/test/__snapshots__/password.test.ts.snap

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,49 @@ exports[`password (isCI = false) > can be aborted by a signal 1`] = `
1414
]
1515
`;
1616
17+
exports[`password (isCI = false) > clears input on error when clearOnError is true 1`] = `
18+
[
19+
"<cursor.hide>",
20+
"│
21+
◆ foo
22+
│ _
23+
└
24+
",
25+
"<cursor.backward count=999><cursor.up count=4>",
26+
"<cursor.down count=2>",
27+
"<erase.line><cursor.left count=1>",
28+
"│ ▪_",
29+
"<cursor.down count=2>",
30+
"<cursor.backward count=999><cursor.up count=4>",
31+
"<cursor.down count=1>",
32+
"<erase.down>",
33+
"▲ foo
34+
│ ▪
35+
└ Error
36+
",
37+
"<cursor.backward count=999><cursor.up count=4>",
38+
"<cursor.down count=1>",
39+
"<erase.down>",
40+
"◆ foo
41+
│ ▪_
42+
└
43+
",
44+
"<cursor.backward count=999><cursor.up count=4>",
45+
"<cursor.down count=2>",
46+
"<erase.line><cursor.left count=1>",
47+
"│ ▪▪_",
48+
"<cursor.down count=2>",
49+
"<cursor.backward count=999><cursor.up count=4>",
50+
"<cursor.down count=1>",
51+
"<erase.down>",
52+
"◇ foo
53+
│ ▪▪",
54+
"
55+
",
56+
"<cursor.show>",
57+
]
58+
`;
59+
1760
exports[`password (isCI = false) > renders and clears validation errors 1`] = `
1861
[
1962
"<cursor.hide>",
@@ -168,6 +211,49 @@ exports[`password (isCI = true) > can be aborted by a signal 1`] = `
168211
]
169212
`;
170213
214+
exports[`password (isCI = true) > clears input on error when clearOnError is true 1`] = `
215+
[
216+
"<cursor.hide>",
217+
"│
218+
◆ foo
219+
│ _
220+
└
221+
",
222+
"<cursor.backward count=999><cursor.up count=4>",
223+
"<cursor.down count=2>",
224+
"<erase.line><cursor.left count=1>",
225+
"│ ▪_",
226+
"<cursor.down count=2>",
227+
"<cursor.backward count=999><cursor.up count=4>",
228+
"<cursor.down count=1>",
229+
"<erase.down>",
230+
"▲ foo
231+
│ ▪
232+
└ Error
233+
",
234+
"<cursor.backward count=999><cursor.up count=4>",
235+
"<cursor.down count=1>",
236+
"<erase.down>",
237+
"◆ foo
238+
│ ▪_
239+
└
240+
",
241+
"<cursor.backward count=999><cursor.up count=4>",
242+
"<cursor.down count=2>",
243+
"<erase.line><cursor.left count=1>",
244+
"│ ▪▪_",
245+
"<cursor.down count=2>",
246+
"<cursor.backward count=999><cursor.up count=4>",
247+
"<cursor.down count=1>",
248+
"<erase.down>",
249+
"◇ foo
250+
│ ▪▪",
251+
"
252+
",
253+
"<cursor.show>",
254+
]
255+
`;
256+
171257
exports[`password (isCI = true) > renders and clears validation errors 1`] = `
172258
[
173259
"<cursor.hide>",

packages/prompts/test/password.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,9 @@ describe.each(['true', 'false'])('password (isCI = %s)', (isCI) => {
9292
input.emit('keypress', 'y', { name: 'y' });
9393
input.emit('keypress', '', { name: 'return' });
9494

95-
await result;
95+
const value = await result;
9696

97+
expect(value).toBe('xy');
9798
expect(output.buffer).toMatchSnapshot();
9899
});
99100

@@ -127,4 +128,25 @@ describe.each(['true', 'false'])('password (isCI = %s)', (isCI) => {
127128
expect(prompts.isCancel(value)).toBe(true);
128129
expect(output.buffer).toMatchSnapshot();
129130
});
131+
132+
test('clears input on error when clearOnError is true', async () => {
133+
const result = prompts.password({
134+
message: 'foo',
135+
input,
136+
output,
137+
validate: (v) => (v === 'yz' ? undefined : 'Error'),
138+
clearOnError: true,
139+
});
140+
141+
input.emit('keypress', 'x', { name: 'x' });
142+
input.emit('keypress', '', { name: 'return' });
143+
input.emit('keypress', 'y', { name: 'y' });
144+
input.emit('keypress', 'z', { name: 'z' });
145+
input.emit('keypress', '', { name: 'return' });
146+
147+
const value = await result;
148+
149+
expect(value).toBe('yz');
150+
expect(output.buffer).toMatchSnapshot();
151+
});
130152
});

0 commit comments

Comments
 (0)