Skip to content

Commit

Permalink
simplify and improve otp input component, and responsiveness on layou…
Browse files Browse the repository at this point in the history
…t header
  • Loading branch information
roncodes committed May 20, 2024
1 parent 94bd41f commit 74460fb
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 149 deletions.
2 changes: 1 addition & 1 deletion addon/components/layout/header.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<div class="org-badge">
{{first-char @user.company_name}}
</div>
<div class="text-sm w-10/12 truncate flex-shrink-0 text-gray-800 dark:text-white">{{@user.company_name}}</div>
<div class="text-sm w-10 md:w-14 lg:w-10/12 truncate flex-shrink-0 text-gray-800 dark:text-white">{{@user.company_name}}</div>
</div>
</Layout::Header::Dropdown>
</div>
Expand Down
7 changes: 3 additions & 4 deletions addon/components/otp-input.hbs
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>
170 changes: 39 additions & 131 deletions addon/components/otp-input.js
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();
}
}
}
2 changes: 1 addition & 1 deletion addon/styles/components/input.css
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ body[data-theme='dark'] .iti__dropdown-content {
line-height: 1.25rem;
}

body[data-theme="dark"] .iti__selected-dial-code {
body[data-theme='dark'] .iti__selected-dial-code {
color: #fff;
}

Expand Down
57 changes: 46 additions & 11 deletions addon/styles/components/otp-input.css
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);
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fleetbase/ember-ui",
"version": "0.2.15",
"version": "0.2.16",
"description": "Fleetbase UI provides all the interface components, helpers, services and utilities for building a Fleetbase extension into the Console.",
"keywords": [
"fleetbase-ui",
Expand Down

0 comments on commit 74460fb

Please sign in to comment.