Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
77 changes: 77 additions & 0 deletions src/components/form/Input.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import RadioButton from "./radio/RadioButton";
import CardSection from "../card/CardSection";
import Flex from "../flex/Flex";
import SwitchInput from "./SwitchInput";
import PinInput from "./PinInput";

export default {
title: "Form"
Expand Down Expand Up @@ -352,3 +353,79 @@ export const Switch = () => {
</form>

}

export const Pin = () => {

const length = 6
const [inputs, validate] = useForm({
initialValues: {
number: null
},
validate: {
number: (value) => {
if (!value) return "Error"
if (value.length != length) return "Error"
return null
}
},
onSubmit: (values) => {
console.log(values)
}
})

return (
<Card maw={300}>
<CardSection>
<PinInput
label={"Pin"}
description={"Please input your pin for confirming your action"}
inputLength={length}
splitFields={false}
{...inputs.getInputProps("number")}
/>
</CardSection>
<Button color={"info"} onClick={validate}>
<IconKey size={13}/>
Confirm
</Button>
</Card>
)
}

export const SplitPin = () => {

const length = 6
const [inputs, validate] = useForm({
initialValues: {
number: null
},
validate: {
number: (value) => {
if (!value) return "Error"
if (value.length != length) return "Error"
return null
}
},
onSubmit: (values) => {
console.log(values)
}
})

return (
<Card maw={300}>
<CardSection>
<PinInput
label={"Pin"}
description={"Please input your pin for confirming your action"}
inputLength={length}
splitFields={true}
{...inputs.getInputProps("number")}
/>
</CardSection>
<Button color={"info"} onClick={validate}>
<IconKey size={13}/>
Confirm
</Button>
</Card>
)
}
10 changes: 10 additions & 0 deletions src/components/form/Input.style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,14 @@

}

}

.pin-input-group {
display: flex;
justify-content: space-between;
}

.pin-input {
padding-left: variables.$lg;
width: variables.$md;
}
158 changes: 158 additions & 0 deletions src/components/form/PinInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import Input, {InputProps} from "./Input";
import React, {RefObject, useEffect, useRef, useState} from "react";
import InputLabel from "./InputLabel";
import InputDescription from "./InputDescription";
import InputMessage from "./InputMessage";

const allowedCharacters = /^[0-9a-zA-Z]+$/

interface PinInputProps extends Omit<InputProps<string | null>, "wrapperComponent" | "type" | "left" | "right" | "leftType" | "rightType"> {
inputLength: number
splitFields: boolean
}

//TODO initial Value for pin
//TODO go befor a character press backspace will skip back without removing the char

const PinInput: React.ForwardRefExoticComponent<PinInputProps> = React.forwardRef((props, ref: RefObject<HTMLInputElement>) => {
ref = ref || React.useRef(null)
return <>{(props.splitFields) ? <SplitPinInput {...props} ref={ref}/> : <SinglePinInput {...props} ref={ref}/>}</>
})

const SplitPinInput: React.ForwardRefExoticComponent<PinInputProps> = React.forwardRef((props, ref: RefObject<HTMLInputElement>) => {
const initialPin = props.initialValue
? props.initialValue.slice(0, props.inputLength).split("")
: Array(props.inputLength).fill("");

const [pin, setPin] = useState<string[]>(initialPin);
const indexes = Array.from({ length: props.inputLength }, (_, i) => i);
const inputsRef = useRef<HTMLInputElement[]>([]);

const updateValue = (valArr: string[]) => {
setPin(valArr);
props.formValidation?.setValue(valArr.join(""));
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, index: number) => {
if (e.key !== "Backspace") return;

const inputEl = e.currentTarget;
const cursorAtStart = inputEl.selectionStart === 0;
const hasValue = inputEl.value.length > 0;

const newPin = [...pin];

if (cursorAtStart) {
if (index > 0) {
e.preventDefault();
newPin[index - 1] = "";
updateValue(newPin);
inputsRef.current[index - 1]?.focus();
} else {
newPin[index] = "";
updateValue(newPin);
}
return;
}

if (hasValue) {
newPin[index] = "";
updateValue(newPin);
e.preventDefault();
}
};

const onChange = (e: React.FormEvent<HTMLInputElement>, index: number) => {
const input = e.target as HTMLInputElement;
const val = input.value;

if (!val.match(allowedCharacters)) {
input.value = "";
return;
}

const newPin = [...pin];
newPin[index] = val;
updateValue(newPin);

if (val && index < props.inputLength - 1) {
inputsRef.current[index + 1]?.focus();
}
};

const onPaste = (event: React.ClipboardEvent<HTMLInputElement>) => {
const text = event.clipboardData.getData("text");
const cutString = text.slice(0, props.inputLength);
const newPin = cutString.split("").map((char) =>
char.match(allowedCharacters) ? char : ""
);

updateValue(newPin);
inputsRef.current[newPin.length - 1]?.focus();
};

useEffect(() => {
props.formValidation?.setValue(pin.join(""));
}, [pin]);

return (
<>
{props.label && <InputLabel>{props.label}</InputLabel>}
{props.description && <InputDescription>{props.description}</InputDescription>}
<div className={"pin-input-group"}>
{indexes.map((index) => (
<Input
key={index}
className={"pin-input"}
leftType={"action"}
type={"text"}
inputMode={"numeric"}
maxLength={1}
value={pin[index]}
onChange={(event) => onChange(event, index)}
ref={(el) => {
if (el) inputsRef.current[index] = el;
}}
onKeyDown={(e) => handleKeyDown(e, index)}
onPaste={(event) => onPaste(event)}
/>
))}
</div>
{!props.formValidation?.valid && props.formValidation?.notValidMessage && (
<InputMessage>{props.formValidation.notValidMessage}</InputMessage>
)}
</>
);
});



const SinglePinInput: React.ForwardRefExoticComponent<PinInputProps> = React.forwardRef((props, ref: RefObject<HTMLInputElement>) => {
const [value, setValue] = useState<string>(props.initialValue?.slice(0, props.inputLength) || "");

const onChange = (e: React.FormEvent<HTMLInputElement>) => {
const input = e.target as HTMLInputElement;
const raw = input.value;
const filtered = raw
.split("")
.filter((char) => char.match(allowedCharacters))
.join("")
.slice(0, props.inputLength);

setValue(filtered);
props.formValidation?.setValue(filtered);
};

return <Input
leftType={"action"}
type={"text"}
inputMode={"numeric"}
maxLength={props.inputLength}
ref={ref}
value={value}
onChange={onChange}
{...props}
/>
});

export default PinInput