-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
simplify and improve otp input component, and responsiveness on layou…
…t header
- Loading branch information
Showing
6 changed files
with
91 additions
and
149 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,4 @@ | ||
<div class="otp-input-container flex space-x-2"> | ||
{{#each this.otpValues as |value index|}} | ||
<input type="text" id={{concat "otp-input-" index}} value={{value}} maxlength="1" {{on "input" (fn this.handleInput index)}} {{on "focus" (fn this.handleFocus index)}} {{on "keydown" (fn this.handleKeyDown index)}} {{on "paste" (fn this.handlePaste index)}} {{did-insert (fn this.handleDidInsert index)}} /> | ||
{{/each}} | ||
<div class="otp-input-container"> | ||
<Input @value={{this.value}} @type="tel" class="form-input form-input-lg otp-input" autocomplete="off" {{did-insert this.setup}} placeholder={{this.placeholder}} {{on "input" this.validate}} ...attributes /> | ||
{{!-- <span class="otp-input-placeholder" contenteditable="true">{{this.value}}</span> --}} | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,173 +1,81 @@ | ||
// app/components/otp-input.js | ||
import Component from '@glimmer/component'; | ||
import { tracked } from '@glimmer/tracking'; | ||
import { isBlank } from '@ember/utils'; | ||
import { notifyPropertyChange } from '@ember/object'; | ||
import { action } from '@ember/object'; | ||
import IMask from 'imask'; | ||
|
||
/** | ||
* Glimmer component for handling OTP (One-Time Password) input. | ||
* This component is responsible for rendering an OTP input field and managing its state. | ||
* | ||
* @class OtpInputComponent | ||
* @extends Component | ||
*/ | ||
export default class OtpInputComponent extends Component { | ||
numberOfDigits = 6; | ||
|
||
/** | ||
* Array to track individual digit values of the OTP. | ||
* | ||
* @property {Array} otpValues | ||
* @default ['', '', '', '', '', ''] | ||
* @tracked | ||
* Tracks the size of the OTP, typically the number of characters the OTP should have. | ||
* @property size | ||
* @type {Number} | ||
* @default 6 | ||
*/ | ||
@tracked otpValues; | ||
@tracked size = 6; | ||
|
||
/** | ||
* Tracked property for handling the OTP value passed from the parent. | ||
* | ||
* @property {String} value | ||
* @tracked | ||
* Tracks the current value entered by the user in the OTP input. | ||
* @property value | ||
* @type {String} | ||
*/ | ||
@tracked value; | ||
@tracked placeholder; | ||
|
||
/** | ||
* Constructor for the OTP input component. | ||
* Component constructor that initializes the component with specified properties. | ||
* Allows setting the initial `size` and `value` of the OTP input upon component instantiation. | ||
* | ||
* @constructor | ||
* @param owner The owner object of this component instance. | ||
* @param {Object} args Component arguments. | ||
*/ | ||
constructor() { | ||
constructor(owner, { size, value }) { | ||
super(...arguments); | ||
this.otpValues = Array.from({ length: this.numberOfDigits }, () => ''); | ||
this.handleInput = this.handleInput.bind(this); | ||
this.handleFocus = this.handleFocus.bind(this); | ||
this.handleKeyDown = this.handleKeyDown.bind(this); | ||
} | ||
|
||
/** | ||
* Getter for the complete OTP value obtained by joining individual digits. | ||
* | ||
* @property {String} otpValue | ||
*/ | ||
get otpValue() { | ||
return this.otpValues.join(''); | ||
this.value = value; | ||
this.size = size; | ||
this.placeholder = '0'.repeat(size); | ||
} | ||
|
||
/** | ||
* Setter for updating the OTP value based on user input. | ||
* Focus action that sets the focus on the given HTML element. | ||
* Typically used to focus the input element when the component is rendered. | ||
* | ||
* @property {String} otpValue | ||
* @method focus | ||
* @param {HTMLElement} el The element to be focused. | ||
*/ | ||
set otpValue(newValue) { | ||
if (typeof newValue === 'string') { | ||
this.otpValues = newValue.split('').slice(0, this.numberOfDigits); | ||
} | ||
@action setup(inputEl) { | ||
inputEl.focus(); | ||
} | ||
|
||
/** | ||
* Handles focus on the input field at a specified index. | ||
* Validates the input as the user types into the OTP field. | ||
* Checks the length of the entered value and triggers appropriate callbacks on certain conditions. | ||
* | ||
* @method handleFocus | ||
* @param {Number} index - The index of the input field to focus on. | ||
* @method validate | ||
* @param {Event} event The input event that triggered this action. | ||
*/ | ||
handleFocus(index) { | ||
const inputId = `otp-input-${index}`; | ||
const inputElement = document.getElementById(inputId); | ||
|
||
if (inputElement) { | ||
inputElement.focus(); | ||
} | ||
} | ||
|
||
/** | ||
* Handles input events on the input field at a specified index. | ||
* | ||
* @method handleInput | ||
* @param {Number} index - The index of the input field being edited. | ||
* @param {Event} event - The input event object. | ||
*/ | ||
handleInput(index, event) { | ||
if (!event || !event.target) { | ||
console.error('Invalid event object in handleInput'); | ||
return; | ||
} | ||
|
||
const inputValue = event.target.value; | ||
|
||
this.otpValues[index] = inputValue; | ||
@action validate({ target }) { | ||
const value = target.value; | ||
|
||
if (inputValue === '' && index > 0) { | ||
this.handleFocus(index - 1); | ||
} else if (index < this.numberOfDigits - 1) { | ||
this.handleFocus(index + 1); | ||
} | ||
// Update value | ||
this.value = value; | ||
|
||
// on every input | ||
// Call the onInput function if provided in the component's arguments. | ||
if (typeof this.args.onInput === 'function') { | ||
this.args.onInput(inputValue); | ||
this.args.onInput(value); | ||
} | ||
|
||
if (this.otpValues.every((value) => !isBlank(value))) { | ||
const completeOtpValue = this.otpValues.join(''); | ||
|
||
// Check if the entered value meets the required size and if so, trigger the onInputCompleted callback. | ||
if (typeof value === 'string' && value.length === this.size) { | ||
if (typeof this.args.onInputCompleted === 'function') { | ||
this.args.onInputCompleted(completeOtpValue); | ||
this.args.onInputCompleted(value); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Handles keydown events on the input field at a specified index. | ||
* | ||
* @method handleKeyDown | ||
* @param {Number} index - The index of the input field. | ||
* @param {Event} event - The keydown event object. | ||
*/ | ||
handleKeyDown(index, event) { | ||
switch (event.keyCode) { | ||
case 37: | ||
if (index > 0) { | ||
this.handleFocus(index - 1); | ||
} | ||
break; | ||
case 39: | ||
if (index < this.numberOfDigits - 1) { | ||
this.handleFocus(index + 1); | ||
} | ||
break; | ||
case 8: | ||
if (this.otpValues[index] !== '') { | ||
this.otpValues[index] = ''; | ||
} else if (index > 0) { | ||
this.handleFocus(index - 1); | ||
} | ||
break; | ||
default: | ||
break; | ||
} | ||
} | ||
|
||
handlePaste = (index, event) => { | ||
event.preventDefault(); | ||
const pastedData = event.clipboardData.getData('text/plain'); | ||
|
||
if (/^\d{6}$/.test(pastedData)) { | ||
const pastedValues = pastedData.split(''); | ||
|
||
for (let i = 0; i < this.numberOfDigits; i++) { | ||
this.otpValues[index + i] = pastedValues[i] || ''; | ||
} | ||
const completeOtpValue = this.otpValues.join(''); | ||
|
||
if (typeof this.args.onInputCompleted === 'function') { | ||
this.args.onInputCompleted(completeOtpValue); | ||
} | ||
} | ||
notifyPropertyChange(this, 'otpValues'); | ||
}; | ||
|
||
handleDidInsert(index, element) { | ||
if (index === 0) { | ||
element.focus(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,64 @@ | ||
.otp-input-container { | ||
position: relative; | ||
width: 100%; | ||
padding: 0 2rem; | ||
display: flex; | ||
justify-content: space-between; | ||
width: 400px; | ||
align-items: center; | ||
justify-content: center; | ||
font-size: 1.5rem; | ||
line-height: 1.5rem; | ||
letter-spacing: 1rem; | ||
text-align: center; | ||
font-family: monospace; | ||
} | ||
|
||
.otp-input-container input { | ||
width: 68px; | ||
height: 68px; | ||
font-size: 24px; | ||
.otp-input-container > .otp-input-placeholder { | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
font-size: 1.72rem; | ||
line-height: 1.5rem; | ||
letter-spacing: 1.1rem; | ||
color: #6b7280; | ||
opacity: 0.5; | ||
text-align: center; | ||
font-family: monospace; | ||
pointer-events: none; | ||
background-color: transparent; | ||
position: absolute; | ||
left: 0; | ||
right: 0; | ||
bottom: 0; | ||
top: 0; | ||
width: 100%; | ||
overflow: hidden; | ||
} | ||
|
||
body[data-theme="dark"] > input.otp-input, | ||
.otp-input-container > input.otp-input, | ||
.otp-input-container > input { | ||
text-align: center; | ||
font-family: monospace; | ||
font-size: 1.5rem; | ||
line-height: 1.5rem; | ||
letter-spacing: 1rem; | ||
border: 3px solid #3498db; | ||
border-radius: 8px; | ||
background-color: #2c3e50; | ||
color: #ecf0f1; | ||
margin: 0; | ||
transition: all 0.3s ease; | ||
} | ||
|
||
body[data-theme="dark"] > input.otp-input::placeholder, | ||
.otp-input-container > input.otp-input::placeholder, | ||
.otp-input-container > input::placeholder { | ||
color: rgba(107, 114, 128, 0.5); | ||
letter-spacing: 1rem; | ||
} | ||
|
||
.otp-input-container input:focus { | ||
outline: none; | ||
border-color: #2980b9; | ||
transform: scale(1.1); | ||
} | ||
|
||
.otp-input-container input:hover { | ||
transform: scale(1.1); | ||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters