Skip to content

Commit 9393143

Browse files
committed
feat(PinCode): add pincode component
1 parent 6a561d7 commit 9393143

File tree

7 files changed

+297
-0
lines changed

7 files changed

+297
-0
lines changed

api-docs/functions/PinCode.html

+9
Large diffs are not rendered by default.

api-docs/types/PinCodeProps.html

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<!DOCTYPE html><html class="default" lang="en"><head><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>PinCodeProps | Typedoc project reference documentation</title><meta name="description" content="Documentation for Typedoc project reference documentation"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><div class="table-cell" id="tsd-search" data-base=".."><div class="field"><label for="tsd-search-field" class="tsd-widget tsd-toolbar-icon search no-caption"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><use href="../assets/icons.svg#icon-search"></use></svg></label><input type="text" id="tsd-search-field" aria-label="Search"/></div><div class="field"><div id="tsd-toolbar-links"></div></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">Typedoc project reference documentation</a></div><div class="table-cell" id="tsd-widgets"><a href="#" class="tsd-widget tsd-toolbar-icon menu no-caption" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb"><li><a href="../index.html">Typedoc project reference documentation</a></li><li><a href="PinCodeProps.html">PinCodeProps</a></li></ul><h1>Type alias PinCodeProps</h1></div><div class="tsd-signature"><span class="tsd-kind-type-alias">Pin<wbr/>Code<wbr/>Props</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">Props</span><span class="tsd-signature-symbol"> &amp; </span><span class="tsd-signature-type">NativeAttrs</span></div><aside class="tsd-sources"><ul><li>Defined in src/components/pin-code/pin-code.tsx:23</li></ul></aside></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-index-accordion"><summary class="tsd-accordion-summary"><h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none"><use href="../assets/icons.svg#icon-chevronDown"></use></svg>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><h4 class="uppercase">Member Visibility</h4><form><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-private" name="private"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Private</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></form></div><div class="tsd-theme-toggle"><h4 class="uppercase">Theme</h4><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../index.html"><svg class="tsd-kind-icon" viewBox="0 0 24 24"><use href="../assets/icons.svg#icon-1"></use></svg><span>Typedoc project reference documentation</span></a><ul class="tsd-small-nested-navigation" id="tsd-nav-container" data-base=".."><li>Loading...</li></ul></nav></div></div></div><div class="tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div></body></html>

src/app/components/pin-code/page.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use client';
2+
import Documentation from './pin-code.mdx';
3+
4+
export default function Page() {
5+
return <Documentation />;
6+
}
+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Playground, Attributes } from 'lib/components';
2+
import { PinCode, Spacer, Button } from 'components';
3+
import NextLink from 'next/link';
4+
5+
export const meta = {
6+
title: 'PinCode',
7+
group: 'Data Entry',
8+
};
9+
10+
# PinCode
11+
12+
A component to input a pin code, commonly used for entering security or verification codes.
13+
14+
<Playground
15+
scope={{ PinCode }}
16+
desc="Basic usage example."
17+
code={`
18+
<>
19+
<PinCode />
20+
</>
21+
`}
22+
/>
23+
24+
<Playground
25+
title="Numbers Only"
26+
desc="Restrict the pin code input to numeric values only."
27+
scope={{ PinCode }}
28+
code={`
29+
<>
30+
<PinCode validChars="0-9" />
31+
</>
32+
`}
33+
/>
34+
35+
<Playground
36+
title="Custom Placeholder"
37+
desc="Use a custom placeholder for unentered characters."
38+
scope={{ PinCode }}
39+
code={`
40+
<>
41+
<PinCode placeholder="*" />
42+
</>
43+
`}
44+
/>
45+
46+
<Playground
47+
title="Password Mode"
48+
desc="Obfuscate the pin code input for sensitive data."
49+
scope={{ PinCode }}
50+
code={`
51+
<>
52+
<PinCode passwordMode={true} placeholder="*" onChange={value => console.log(value)} />
53+
</>
54+
`}
55+
/>
56+
57+
58+
<Playground
59+
title="Custom Length"
60+
desc="Customize the length."
61+
scope={{ PinCode, Spacer }}
62+
code={`
63+
<>
64+
<PinCode length={4} onChange={value => console.log(value)} />
65+
<Spacer h={0.5} />
66+
</>
67+
`}
68+
/>
69+
70+
<Playground
71+
title="Disabled"
72+
desc="Disable the pin code input."
73+
scope={{ PinCode }}
74+
code={`
75+
<>
76+
<PinCode disabled />
77+
</>
78+
`}
79+
/>
80+
81+
82+
<Attributes component="PinCode" edit="/app/components/pincode.mdx"></Attributes>

src/components/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export type { ImageBrowserProps, ImageProps } from './image';
6666
export { default as Input } from './input';
6767
export type { InputInternalProps, InputPasswordProps, InputProps, InputType } from './input';
6868

69+
export { default as PinCode } from './pin-code';
70+
export type { PinCodeProps } from './pin-code';
71+
6972
export { default as Keyboard } from './keyboard';
7073
export type { KeyboardProps } from './keyboard';
7174

src/components/pin-code/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import PinCode from './pin-code';
2+
3+
export type { PinCodeProps } from './pin-code';
4+
export default PinCode;

src/components/pin-code/pin-code.tsx

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// PinCode.tsx
2+
'use client';
3+
4+
import React, { useRef, useState, useEffect, forwardRef } from 'react';
5+
import useClasses from '../use-classes';
6+
import useScale, { withScale } from '../use-scale';
7+
8+
interface Props {
9+
value?: string;
10+
length?: number;
11+
validChars?: string;
12+
placeholder?: string;
13+
autoFocus?: boolean;
14+
passwordMode?: boolean;
15+
className?: string;
16+
onChange?: (value: string) => void;
17+
onFocus?: () => void;
18+
onBlur?: () => void;
19+
onComplete?: (value: string) => void;
20+
}
21+
22+
type NativeAttrs = Omit<React.HTMLAttributes<HTMLDivElement>, keyof Props>;
23+
export type PinCodeProps = Props & NativeAttrs;
24+
25+
const PinCodeComponent = forwardRef<HTMLInputElement, PinCodeProps>(
26+
(
27+
{
28+
value,
29+
length = 5,
30+
validChars = 'A-Za-z0-9',
31+
placeholder = '·',
32+
autoFocus = false,
33+
passwordMode = false,
34+
className,
35+
onChange,
36+
onFocus,
37+
onBlur,
38+
onComplete,
39+
...props
40+
},
41+
ref,
42+
) => {
43+
const { SCALE, UNIT, CLASS_NAMES } = useScale();
44+
45+
const [localValue, setLocalValue] = useState('');
46+
const [isActive, setActive] = useState(false);
47+
const inputRef = useRef<HTMLInputElement>(null);
48+
49+
useEffect(() => {
50+
if (autoFocus && inputRef.current) {
51+
inputRef.current.focus();
52+
}
53+
}, [autoFocus]);
54+
55+
useEffect(() => {
56+
if (ref) {
57+
if (typeof ref === 'function') {
58+
ref(inputRef.current);
59+
} else {
60+
ref.current = inputRef.current;
61+
}
62+
}
63+
}, [ref]);
64+
65+
const handleClick = () => {
66+
inputRef.current?.focus();
67+
};
68+
69+
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
70+
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
71+
event.preventDefault();
72+
}
73+
};
74+
75+
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
76+
const newInputVal = event.target.value.replace(/\s/g, '');
77+
if (RegExp(`^[${validChars}]{0,${length}}$`).test(newInputVal)) {
78+
onChange?.(newInputVal);
79+
setLocalValue(newInputVal);
80+
if (newInputVal.length === length) {
81+
onComplete?.(newInputVal);
82+
}
83+
}
84+
};
85+
86+
const getValue = () => value ?? localValue;
87+
88+
return (
89+
<div className={useClasses('pincode-container', CLASS_NAMES)} onClick={() => inputRef.current?.focus()}>
90+
<input
91+
ref={inputRef}
92+
aria-label="pincode input"
93+
spellCheck={false}
94+
value={getValue()}
95+
onChange={handleInputChange}
96+
className={useClasses('pincode-input', className)}
97+
onKeyDown={handleKeyDown}
98+
onFocus={() => {
99+
setActive(true);
100+
onFocus?.();
101+
}}
102+
onBlur={() => {
103+
setActive(false);
104+
onBlur?.();
105+
}}
106+
type={passwordMode ? 'password' : 'text'}
107+
style={{ opacity: 0, position: 'absolute', zIndex: -1 }}
108+
{...props}
109+
/>
110+
{[...Array(length)].map((_, i) => (
111+
<div
112+
className={useClasses('pincode-character', {
113+
selected: isActive && getValue().length === i,
114+
filled: getValue().length > i,
115+
inactive: getValue()[i] === placeholder,
116+
})}
117+
onClick={handleClick}
118+
key={i}
119+
>
120+
{passwordMode && getValue()[i] ? '*' : getValue()[i] || placeholder}
121+
</div>
122+
))}
123+
124+
<style jsx>{`
125+
.pincode-input {
126+
top: 0;
127+
right: 0;
128+
bottom: 0;
129+
left: 0;
130+
box-sizing: border-box;
131+
position: absolute;
132+
color: transparent;
133+
background: transparent;
134+
caret-color: transparent;
135+
outline: none;
136+
border: 0 none transparent;
137+
}
138+
139+
.pincode-input::-ms-reveal,
140+
.pincode-input::-ms-clear {
141+
display: none;
142+
}
143+
144+
.pincode-input::selection {
145+
background: transparent;
146+
}
147+
148+
:where(.pincode-container) {
149+
position: relative;
150+
display: flex;
151+
gap: 8px;
152+
}
153+
154+
:where(.pincode-character) {
155+
height: 100%;
156+
flex-grow: 1;
157+
flex-basis: 0;
158+
text-align: center;
159+
color: var(--color-foreground-1000);
160+
background-color: var(--color-background-1000);
161+
border: 1px solid var(--color-background-800);
162+
border-radius: var(--layout-radius);
163+
cursor: default;
164+
user-select: none;
165+
box-sizing: border-box;
166+
}
167+
168+
:where(.pincode-character.inactive) {
169+
color: var(--color-foreground-1000);
170+
background-color: var(--color-background-600);
171+
}
172+
173+
:where(.pincode-character.selected) {
174+
outline: 2px solid var(--color-foreground-600);
175+
color: var(--color-foreground-1000);
176+
}
177+
178+
${SCALE.padding(0, value => `padding: ${value.top} ${value.right} ${value.bottom} ${value.left};`, undefined, 'pincode-container')}
179+
${SCALE.margin(0, value => `margin: ${value.top} ${value.right} ${value.bottom} ${value.left};`, undefined, 'pincode-container')}
180+
${SCALE.w(16, value => `width: ${value};`, undefined, 'pincode-container')}
181+
${SCALE.h(2.4, value => `height: ${value};`, undefined, 'pincode-container')}
182+
${SCALE.font(1, value => `font-size: ${value};`, 'inherit', 'pincode-character')}
183+
${SCALE.lineHeight(2.4, value => `line-height: ${value};`, undefined, 'pincode-character')}
184+
${UNIT('pincode-container')}
185+
`}</style>
186+
</div>
187+
);
188+
},
189+
);
190+
PinCodeComponent.displayName = 'HimalayaPinCode';
191+
const PinCode = withScale(PinCodeComponent);
192+
export default PinCode;

0 commit comments

Comments
 (0)