Skip to content

Commit 164477f

Browse files
authored
Merge pull request #68 from pooulad/main
feat: Add password masking support to Terminal (Fixes #7)
2 parents 1448237 + 9950ed1 commit 164477f

File tree

3 files changed

+89
-32
lines changed

3 files changed

+89
-32
lines changed

demo/index.tsx

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,46 @@ import Terminal, { ColorMode, TerminalInput, TerminalOutput } from '../src/index
55
import './style.css';
66

77
const TerminalController = (props = {}) => {
8+
const [isPasswordMode, setIsPasswordMode] = useState<boolean>(false);
89
const [colorMode, setColorMode] = useState(ColorMode.Dark);
910
const [lineData, setLineData] = useState([
1011
<TerminalOutput>Welcome to the React Terminal UI Demo!&#128075;</TerminalOutput>,
1112
<TerminalOutput></TerminalOutput>,
1213
<TerminalOutput>The following example commands are provided:</TerminalOutput>,
1314
<TerminalOutput>'view-source' will navigate to the React Terminal UI github source.</TerminalOutput>,
1415
<TerminalOutput>'view-react-docs' will navigate to the react docs.</TerminalOutput>,
16+
<TerminalOutput>'login' will show input password with "*" instead of real string</TerminalOutput>,
1517
<TerminalOutput>'clear' will clear the terminal.</TerminalOutput>,
1618
]);
1719

18-
function toggleColorMode (e: MouseEvent) {
20+
function toggleColorMode(e: MouseEvent) {
1921
e.preventDefault();
2022
setColorMode(colorMode === ColorMode.Light ? ColorMode.Dark : ColorMode.Light);
2123
}
2224

23-
function onInput (input: string) {
24-
let ld = [...lineData];
25+
function onInput(input: string) {
26+
let ld = [...lineData];
27+
if (isPasswordMode) {
28+
ld.push(<TerminalInput>{'*'.repeat(input.length)}</TerminalInput>);
29+
ld.push(<TerminalOutput>Your password received successfully</TerminalOutput>);
30+
setIsPasswordMode(false);
31+
setLineData(ld);
32+
} else {
2533
ld.push(<TerminalInput>{input}</TerminalInput>);
26-
if (input.toLocaleLowerCase().trim() === 'view-source') {
27-
window.open('https://github.com/jonmbake/react-terminal-ui', '_blank');
28-
} else if (input.toLocaleLowerCase().trim() === 'view-react-docs') {
29-
window.open('https://reactjs.org/docs/getting-started.html', '_blank');
30-
} else if (input.toLocaleLowerCase().trim() === 'clear') {
31-
ld = [];
32-
} else if (input) {
33-
ld.push(<TerminalOutput>Unrecognized command</TerminalOutput>);
34+
if (input.toLocaleLowerCase().trim() === 'view-source') {
35+
window.open('https://github.com/jonmbake/react-terminal-ui', '_blank');
36+
} else if (input.toLocaleLowerCase().trim() === 'view-react-docs') {
37+
window.open('https://reactjs.org/docs/getting-started.html', '_blank');
38+
} else if (input.toLocaleLowerCase().trim() === 'clear') {
39+
ld = [];
40+
} else if (input.toLocaleLowerCase().trim() === 'login') {
41+
ld.push(<TerminalOutput>Please enter your password:</TerminalOutput>);
42+
setIsPasswordMode(true);
43+
} else if (input) {
44+
ld.push(<TerminalOutput>Unrecognized command</TerminalOutput>);
45+
}
46+
setLineData(ld);
3447
}
35-
setLineData(ld);
3648
}
3749

3850
const redBtnClick = () => {
@@ -56,15 +68,16 @@ const TerminalController = (props = {}) => {
5668
return (
5769
<div className="container" >
5870
<div className="d-flex flex-row-reverse p-2">
59-
<button className={ btnClasses.join(' ') } onClick={ toggleColorMode } >Enable { colorMode === ColorMode.Light ? 'Dark' : 'Light' } Mode</button>
71+
<button className={btnClasses.join(' ')} onClick={toggleColorMode} >Enable {colorMode === ColorMode.Light ? 'Dark' : 'Light'} Mode</button>
6072
</div>
61-
<Terminal
62-
name='React Terminal UI'
63-
colorMode={ colorMode }
64-
onInput={ onInput }
65-
redBtnCallback={ redBtnClick }
66-
yellowBtnCallback={ yellowBtnClick }
67-
greenBtnCallback={ greenBtnClick }
73+
<Terminal
74+
name='React Terminal UI'
75+
colorMode={colorMode}
76+
onInput={onInput}
77+
redBtnCallback={redBtnClick}
78+
yellowBtnCallback={yellowBtnClick}
79+
greenBtnCallback={greenBtnClick}
80+
passwordField={isPasswordMode}
6881
>
6982
{lineData}
7083
</Terminal>

src/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface Props {
2828
children?: ReactNode;
2929
onInput?: ((input: string) => void) | null | undefined;
3030
startingInputValue?: string;
31+
passwordField?: boolean;
3132
redBtnCallback?: () => void;
3233
yellowBtnCallback?: () => void;
3334
greenBtnCallback?: () => void;
@@ -42,6 +43,7 @@ const Terminal = ({
4243
onInput,
4344
children,
4445
startingInputValue = "",
46+
passwordField = false,
4547
redBtnCallback,
4648
yellowBtnCallback,
4749
greenBtnCallback,
@@ -256,7 +258,7 @@ const Terminal = ({
256258
data-terminal-prompt={prompt || "$"}
257259
key="terminal-line-prompt"
258260
>
259-
{currentLineInput}
261+
{passwordField ? "*".repeat(currentLineInput.length) : currentLineInput}
260262
<span
261263
className="cursor"
262264
style={{ left: `${cursorPos + 1}px` }}
@@ -269,6 +271,7 @@ const Terminal = ({
269271
className="terminal-hidden-input"
270272
placeholder="Terminal Hidden Input"
271273
value={currentLineInput}
274+
type={passwordField ? "password" : "text"}
272275
autoFocus={onInput != null}
273276
onChange={updateCurrentLineInput}
274277
onKeyDown={handleInputKeyDown}

tests/index.spec.tsx

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import Terminal, { ColorMode, TerminalInput, TerminalOutput } from '../src/index';
33
import { render, fireEvent, screen } from '@testing-library/react';
44
import '@testing-library/jest-dom';
@@ -14,14 +14,14 @@ describe('Terminal component', () => {
1414
})
1515

1616
test('Should render prompt', () => {
17-
const { container } = render(<Terminal onInput={ (input: string) => '' } />);
17+
const { container } = render(<Terminal onInput={(input: string) => ''} />);
1818
expect(container.querySelectorAll('.react-terminal-line')).toHaveLength(1);
1919
expect(container.querySelector('.react-terminal-line.react-terminal-active-input[data-terminal-prompt="$"]')).not.toBeNull();
2020
expect(screen.getByPlaceholderText('Terminal Hidden Input')).toBeInTheDocument();
2121
});
2222

2323
test('Should not render prompt if onInput prop is null or not defined', () => {
24-
const { container } = render(<Terminal onInput={ null }><TerminalOutput>Some terminal output</TerminalOutput></Terminal>);
24+
const { container } = render(<Terminal onInput={null}><TerminalOutput>Some terminal output</TerminalOutput></Terminal>);
2525
// Still renders output line...
2626
expect(container.querySelectorAll('.react-terminal-line')).toHaveLength(1);
2727
// ... but not the prompt
@@ -30,7 +30,7 @@ describe('Terminal component', () => {
3030

3131
test('Should render terminal lines', () => {
3232
const { container } = render(
33-
<Terminal onInput={ (input: string) => '' }>
33+
<Terminal onInput={(input: string) => ''}>
3434
<TerminalInput>Some terminal input</TerminalInput>,
3535
<TerminalOutput>Some terminal output</TerminalOutput>
3636
</Terminal>
@@ -44,7 +44,7 @@ describe('Terminal component', () => {
4444

4545
test('Input prompt should not scroll into view when component first loads', () => {
4646
render(
47-
<Terminal onInput={ (input: string) => '' }>
47+
<Terminal onInput={(input: string) => ''}>
4848
<TerminalInput>Some terminal input</TerminalInput>,
4949
<TerminalOutput>Some terminal output</TerminalOutput>
5050
</Terminal>
@@ -55,26 +55,26 @@ describe('Terminal component', () => {
5555

5656
test('Should accept input and scroll into view', () => {
5757
const onInput = jest.fn();
58-
const { rerender } = render(<Terminal onInput={ onInput }/>);
58+
const { rerender } = render(<Terminal onInput={onInput} />);
5959
const hiddenInput = screen.getByPlaceholderText('Terminal Hidden Input');
6060
fireEvent.change(hiddenInput, { target: { value: 'a' } });
6161
expect(screen.getByText('a').className).toEqual('react-terminal-line react-terminal-input react-terminal-active-input');
6262
screen.getByDisplayValue('a');
6363
expect(onInput.mock.calls.length).toEqual(0);
6464
fireEvent.keyDown(hiddenInput, { key: 'Enter', code: 'Enter' });
6565
expect(onInput).toHaveBeenCalledWith('a');
66-
rerender(<Terminal onInput={ onInput }><TerminalInput>a</TerminalInput></Terminal>)
66+
rerender(<Terminal onInput={onInput}><TerminalInput>a</TerminalInput></Terminal>)
6767
jest.runAllTimers();
6868
expect(scrollIntoViewFn).toHaveBeenCalledTimes(1);
6969
});
7070

7171
test('Should support changing color mode', () => {
72-
const { container } = render(<Terminal colorMode={ ColorMode.Light } onInput={ (input: string) => '' }/>);
72+
const { container } = render(<Terminal colorMode={ColorMode.Light} onInput={(input: string) => ''} />);
7373
expect(container.querySelector('.react-terminal-wrapper.react-terminal-light')).not.toBeNull();
7474
});
7575

7676
test('Should focus if onInput is defined', () => {
77-
const { container } = render(<Terminal onInput={ (input: string) => '' }/>)
77+
const { container } = render(<Terminal onInput={(input: string) => ''} />)
7878
expect(container.ownerDocument.activeElement?.nodeName).toEqual('INPUT');
7979
expect(container.ownerDocument.activeElement?.className).toEqual('terminal-hidden-input');
8080
});
@@ -85,7 +85,7 @@ describe('Terminal component', () => {
8585
});
8686

8787
test('Should take starting input value', () => {
88-
render(<Terminal onInput={ (input: string) => '' } startingInputValue="cat file.txt " />)
88+
render(<Terminal onInput={(input: string) => ''} startingInputValue="cat file.txt " />)
8989
const renderedLine = screen.getByText('cat file.txt');
9090
expect(renderedLine.className).toContain('react-terminal-line');
9191
});
@@ -96,7 +96,48 @@ describe('Terminal component', () => {
9696
});
9797

9898
test('Should not render top button panel if null props passed', () => {
99-
const { container } = render(<Terminal TopButtonsPanel={()=> null} />);
99+
const { container } = render(<Terminal TopButtonsPanel={() => null} />);
100100
expect(container.querySelector('.react-terminal-window-buttons')).toBeNull();
101101
});
102+
103+
test('renders input as password and handles masked input', () => {
104+
const TerminalWrapper = () => {
105+
const [lines, setLines] = useState<React.ReactNode[]>([
106+
<TerminalOutput key="welcome">Welcome!</TerminalOutput>
107+
]);
108+
const [isPasswordMode, setIsPasswordMode] = useState(true);
109+
110+
return (
111+
<Terminal
112+
passwordField={isPasswordMode}
113+
onInput={(input: string) => {
114+
const newLines = [...lines];
115+
if (isPasswordMode) {
116+
newLines.push(<TerminalInput key="masked">{'*'.repeat(input.length)}</TerminalInput>);
117+
newLines.push(<TerminalOutput key="success">Password received</TerminalOutput>);
118+
setIsPasswordMode(false);
119+
} else {
120+
newLines.push(<TerminalInput key={input}>{input}</TerminalInput>);
121+
}
122+
setLines(newLines);
123+
}}
124+
>
125+
{lines}
126+
</Terminal>
127+
);
128+
};
129+
130+
render(<TerminalWrapper />);
131+
132+
const hiddenInput = screen.getByPlaceholderText('Terminal Hidden Input') as HTMLInputElement;
133+
134+
expect(hiddenInput.type).toBe('password');
135+
136+
fireEvent.change(hiddenInput, { target: { value: 'mypassword' } });
137+
expect(hiddenInput.value).toBe('mypassword');
138+
139+
fireEvent.keyDown(hiddenInput, { key: 'Enter', code: 'Enter' });
140+
141+
expect(screen.getByText('Password received')).toBeInTheDocument();
142+
});
102143
});

0 commit comments

Comments
 (0)