Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions .changeset/solid-goats-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clack/prompts": minor
"@clack/core": minor
---

Add input reset to password prompt
3 changes: 3 additions & 0 deletions packages/core/src/prompts/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export default class PasswordPrompt extends Prompt<string> {
const s2 = masked.slice(this.cursor);
return `${s1}${color.inverse(s2[0])}${s2.slice(1)}`;
}
clear() {
this._clearUserInput();
}
constructor({ mask, ...opts }: PasswordOptions) {
super(opts);
this._mask = mask ?? '•';
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/prompts/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ export default class Prompt<TValue> {
}
}

protected _clearUserInput(): void {
this.rl?.write(null, { ctrl: true, name: 'u' });
this._setUserInput('');
}

private onKeypress(char: string | undefined, key: Key) {
if (this._track && key.name !== 'return') {
if (key.name && this._isActionKey(char, key)) {
Expand Down
4 changes: 4 additions & 0 deletions packages/prompts/src/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface PasswordOptions extends CommonOptions {
message: string;
mask?: string;
validate?: (value: string | undefined) => string | Error | undefined;
clearOnError?: boolean;
}
export const password = (opts: PasswordOptions) => {
return new PasswordPrompt({
Expand All @@ -22,6 +23,9 @@ export const password = (opts: PasswordOptions) => {
switch (this.state) {
case 'error': {
const maskedText = masked ? ` ${masked}` : '';
if (opts.clearOnError) {
this.clear();
}
return `${title.trim()}\n${color.yellow(S_BAR)}${maskedText}\n${color.yellow(
S_BAR_END
)} ${color.yellow(this.error)}\n`;
Expand Down
86 changes: 86 additions & 0 deletions packages/prompts/test/__snapshots__/password.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,49 @@ exports[`password (isCI = false) > can be aborted by a signal 1`] = `
]
`;

exports[`password (isCI = false) > clears input on error when clearOnError is true 1`] = `
[
"<cursor.hide>",
"│
◆ foo
│ _
└
",
"<cursor.backward count=999><cursor.up count=4>",
"<cursor.down count=2>",
"<erase.line><cursor.left count=1>",
"│ ▪_",
"<cursor.down count=2>",
"<cursor.backward count=999><cursor.up count=4>",
"<cursor.down count=1>",
"<erase.down>",
"▲ foo
│ ▪
└ Error
",
"<cursor.backward count=999><cursor.up count=4>",
"<cursor.down count=1>",
"<erase.down>",
"◆ foo
│ ▪_
└
",
"<cursor.backward count=999><cursor.up count=4>",
"<cursor.down count=2>",
"<erase.line><cursor.left count=1>",
"│ ▪▪_",
"<cursor.down count=2>",
"<cursor.backward count=999><cursor.up count=4>",
"<cursor.down count=1>",
"<erase.down>",
"◇ foo
│ ▪▪",
"
",
"<cursor.show>",
]
`;

exports[`password (isCI = false) > renders and clears validation errors 1`] = `
[
"<cursor.hide>",
Expand Down Expand Up @@ -168,6 +211,49 @@ exports[`password (isCI = true) > can be aborted by a signal 1`] = `
]
`;

exports[`password (isCI = true) > clears input on error when clearOnError is true 1`] = `
[
"<cursor.hide>",
"│
◆ foo
│ _
└
",
"<cursor.backward count=999><cursor.up count=4>",
"<cursor.down count=2>",
"<erase.line><cursor.left count=1>",
"│ ▪_",
"<cursor.down count=2>",
"<cursor.backward count=999><cursor.up count=4>",
"<cursor.down count=1>",
"<erase.down>",
"▲ foo
│ ▪
└ Error
",
"<cursor.backward count=999><cursor.up count=4>",
"<cursor.down count=1>",
"<erase.down>",
"◆ foo
│ ▪_
└
",
"<cursor.backward count=999><cursor.up count=4>",
"<cursor.down count=2>",
"<erase.line><cursor.left count=1>",
"│ ▪▪_",
"<cursor.down count=2>",
"<cursor.backward count=999><cursor.up count=4>",
"<cursor.down count=1>",
"<erase.down>",
"◇ foo
│ ▪▪",
"
",
"<cursor.show>",
]
`;

exports[`password (isCI = true) > renders and clears validation errors 1`] = `
[
"<cursor.hide>",
Expand Down
24 changes: 23 additions & 1 deletion packages/prompts/test/password.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ describe.each(['true', 'false'])('password (isCI = %s)', (isCI) => {
input.emit('keypress', 'y', { name: 'y' });
input.emit('keypress', '', { name: 'return' });

await result;
const value = await result;

expect(value).toBe('xy');
expect(output.buffer).toMatchSnapshot();
});

Expand Down Expand Up @@ -127,4 +128,25 @@ describe.each(['true', 'false'])('password (isCI = %s)', (isCI) => {
expect(prompts.isCancel(value)).toBe(true);
expect(output.buffer).toMatchSnapshot();
});

test('clears input on error when clearOnError is true', async () => {
const result = prompts.password({
message: 'foo',
input,
output,
validate: (v) => (v === 'yz' ? undefined : 'Error'),
clearOnError: true,
});

input.emit('keypress', 'x', { name: 'x' });
input.emit('keypress', '', { name: 'return' });
input.emit('keypress', 'y', { name: 'y' });
input.emit('keypress', 'z', { name: 'z' });
input.emit('keypress', '', { name: 'return' });

const value = await result;

expect(value).toBe('yz');
expect(output.buffer).toMatchSnapshot();
});
});