Skip to content

Commit 585e656

Browse files
authored
feat: input-pin (#56)
* feat: input-pin * fix: styling as per our design * fix: added small variant * fix: hide pin on select * fix: gap between pins * fix: color of focus state * fix: removed legacy code and also fix some css to tailwind css * fix: css * fix: optional props * feat: added color variants
1 parent 17503e2 commit 585e656

File tree

4 files changed

+215
-1
lines changed

4 files changed

+215
-1
lines changed

infrastructure/eid-wallet/src/app.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@
3939

4040
--color-black: #1F1F1F;
4141

42+
--color-red-900: #FF5255;
43+
--color-red-700: #FF7B77;
44+
--color-red-500: #FF968E;
45+
--color-red-300: #FFB1A7;
46+
--color-red-100: #FFDCDD;
47+
4248
--color-danger-500: #ff5255;
4349
--color-danger-300: #ffdcdd;
4450
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import InputPin from './InputPin.svelte';
2+
3+
export default {
4+
title: 'UI/InputPin',
5+
component: InputPin,
6+
tags: ['autodocs'],
7+
render: (args: any) => ({
8+
Component: InputPin,
9+
props: args
10+
})
11+
};
12+
13+
export const Default = {
14+
args: {
15+
size: 4
16+
}
17+
};
18+
19+
export const Small = {
20+
args: {
21+
size: 4,
22+
variant: "sm"
23+
}
24+
};
25+
26+
export const Error = {
27+
args: {
28+
size: 4,
29+
isError: true,
30+
}
31+
};
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<script lang="ts">
2+
import { cn } from '$lib/utils';
3+
import { onMount } from 'svelte';
4+
import type { HTMLAttributes } from 'svelte/elements';
5+
6+
const KEYBOARD = {
7+
BACKSPACE: 'Backspace',
8+
DELETE: 'Delete',
9+
ANDROID_BACKSPACE: 'Backspace'
10+
};
11+
12+
let inputs = $state([0]);
13+
let pins: { [key: number]: string } = $state({});
14+
15+
interface IInputPinProps extends HTMLAttributes<HTMLDivElement> {
16+
pin: string;
17+
variant?: "lg" | "sm";
18+
size?: number;
19+
focusOnMount?: boolean | undefined;
20+
inFocus?: boolean | undefined;
21+
isError?: boolean;
22+
}
23+
24+
let {
25+
pin = $bindable(''),
26+
variant = "lg",
27+
size = 4,
28+
focusOnMount = true,
29+
inFocus = false,
30+
isError = $bindable(false),
31+
...restProps
32+
}: IInputPinProps = $props();
33+
34+
onMount(async () => {
35+
inputs = createArray(size);
36+
pins = await createValueSlot(inputs);
37+
pin = calcPin(pins);
38+
if (!focusOnMount) return;
39+
document.getElementById('pin0')?.focus();
40+
});
41+
42+
$effect(() => {
43+
pin = calcPin(pins);
44+
});
45+
46+
const calcPin = (pins: { [key: number]: string }) => {
47+
return Object.values(pins).join('') || '';
48+
};
49+
50+
const isKeyDelete = (key: string) => {
51+
return (
52+
key === KEYBOARD.BACKSPACE || key === KEYBOARD.DELETE || key === KEYBOARD.ANDROID_BACKSPACE
53+
);
54+
};
55+
56+
const changeHandler = (e: KeyboardEvent, i: number) => {
57+
const current = document.activeElement ?? document.getElementById('pin0');
58+
const items = Array.from(document.getElementsByClassName('pin-item'));
59+
const currentIndex = items.indexOf(current as HTMLElement);
60+
let newIndex: number;
61+
62+
const regx = /^\d+$/;
63+
64+
// backspace pressed
65+
if (isKeyDelete(e.key)) {
66+
if (pins[i] !== '') {
67+
// If there is a value in the current pin, just clear it and stay on the same input
68+
pins[i] = '';
69+
return;
70+
} else {
71+
// If the current input is already empty, move to the previous input
72+
newIndex = (currentIndex - 1 + items.length) % items.length;
73+
}
74+
} else {
75+
// When a number is typed, replace the current digit with the typed number
76+
if (regx.test(e.key)) {
77+
pins[i] = e.key;
78+
newIndex = (currentIndex + 1) % items.length;
79+
} else {
80+
return;
81+
}
82+
}
83+
84+
// Set focus to the new input if it’s needed
85+
(items[newIndex] as HTMLInputElement)?.focus();
86+
};
87+
88+
const createArray = (size: number) => {
89+
return new Array(size);
90+
};
91+
92+
const createValueSlot = (arr: any[]) => {
93+
return arr.reduce((obj, item) => {
94+
return {
95+
...obj,
96+
[item]: ''
97+
};
98+
}, {});
99+
};
100+
101+
let uniqueId = 'input' + Math.random().toString().split('.')[1];
102+
const cBase = "relative w-full margin-x-[auto] flex justify-start items-center gap-[10px] flex-row flex-nowrap select-none"
103+
</script>
104+
105+
<div {...restProps} class={cn(`${cBase} ${variant === "sm" && "sm" }`, restProps.class)}>
106+
{#if inputs.length}
107+
{#each inputs as item, i}
108+
<div class="singular-input relative w-[68px] h-[81px] flex justify-center items-center select-none">
109+
<input
110+
bind:value={pins[i]}
111+
maxLength="1"
112+
class="pin-item w-[68px] h-[81px] rounded-[64px] border-[1px] border-transparent text-xl text-center bg-gray-900 select-none {pins[i] ? 'has-value' : ''}"
113+
class:error={isError}
114+
id={uniqueId}
115+
type="tel"
116+
pattern="\d{1}"
117+
onfocusin={() => (inFocus = true)}
118+
onfocusout={() => {
119+
if (i === inputs.length - 1) inFocus = false;
120+
}}
121+
maxlength="1"
122+
onkeydown={(event) => {
123+
event.preventDefault();
124+
changeHandler(event, i);
125+
}}
126+
placeholder=""
127+
/>
128+
{#if pins[i] !== ''}
129+
<div class="mask">·</div>
130+
{/if}
131+
</div>
132+
{/each}
133+
{/if}
134+
</div>
135+
136+
<style>
137+
.sm {
138+
scale: 0.8;
139+
transform-origin: 0 0;
140+
}
141+
142+
.singular-input .mask {
143+
position: absolute;
144+
top: 50%;
145+
left: 50%;
146+
transform: translate(-50%, -50%);
147+
font-size: 24px;
148+
visibility: hidden;
149+
}
150+
151+
input.error + .mask {
152+
color: var(--color-danger-500);
153+
}
154+
155+
input {
156+
color: transparent;
157+
box-sizing: border-box;
158+
transition: all 0.4s;
159+
line-height: 81px;
160+
-webkit-text-security: disc;
161+
}
162+
163+
input.error {
164+
border-color: var(--color-danger-500);
165+
}
166+
167+
input:focus {
168+
outline: none;
169+
border-color: var(--color-primary);
170+
}
171+
172+
/* Show the mask when the input has a value */
173+
.singular-input input.has-value + .mask {
174+
visibility: visible;
175+
}
176+
</style>
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export {default as Drawer} from "./Drawer/Drawer.svelte";
1+
export {default as Drawer} from "./Drawer/Drawer.svelte";
2+
export { default as InputPin } from "./InputPin/InputPin.svelte";

0 commit comments

Comments
 (0)