From e55060c6f3c3893ac6d363614d4daacb8d496f84 Mon Sep 17 00:00:00 2001 From: Bernardo Cardoso Date: Wed, 31 Jan 2024 17:49:06 +0000 Subject: [PATCH 01/14] Added new props to ion-input --- core/src/components.d.ts | 6 + core/src/components/input/input.scss | 55 ++++- core/src/components/input/input.tsx | 197 +++++++++++++++++- .../src/components/input/test/type/index.html | 67 ++++++ 4 files changed, 309 insertions(+), 16 deletions(-) create mode 100644 core/src/components/input/test/type/index.html diff --git a/core/src/components.d.ts b/core/src/components.d.ts index bcea90eeb27..3f20cdb4a45 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1281,6 +1281,9 @@ export namespace Components { * The shape of the input. If "round" it will have an increased border radius. */ "shape"?: 'round'; + "showPasswordIcon": boolean; + "showPasswordStrength": boolean; + "showPasswordValidations": boolean; "size"?: number; /** * If `true`, the element will have its spelling and grammar checked. @@ -6012,6 +6015,9 @@ declare namespace LocalJSX { * The shape of the input. If "round" it will have an increased border radius. */ "shape"?: 'round'; + "showPasswordIcon"?: boolean; + "showPasswordStrength"?: boolean; + "showPasswordValidations"?: boolean; "size"?: number; /** * If `true`, the element will have its spelling and grammar checked. diff --git a/core/src/components/input/input.scss b/core/src/components/input/input.scss index e2504b032a5..ecc4f62fe70 100644 --- a/core/src/components/input/input.scss +++ b/core/src/components/input/input.scss @@ -211,7 +211,8 @@ @include margin(0); } -.input-clear-icon { +.input-clear-icon, +.input-show-password { @include margin(auto); @include padding(0); @include background-position(center); @@ -233,7 +234,6 @@ color: $text-color-step-400; - visibility: hidden; appearance: none; } @@ -248,15 +248,15 @@ * However, the clear button always disappears after * being activated, so we never get to that state. */ -.input-clear-icon:focus { +.input-clear-icon:focus, +.input-show-password:focus { opacity: 0.5; } -:host(.has-value) .input-clear-icon { - visibility: visible; +:host(:not(.has-value)) .input-clear-icon { + visibility: hidden; } - // Input Has focus // -------------------------------------------------- @@ -272,7 +272,6 @@ pointer-events: auto; } - // Item Floating: Placeholder // ---------------------------------------------------------------- // When used with a floating item the placeholder should hide @@ -461,7 +460,7 @@ */ max-width: 200px; - transition: color 150ms cubic-bezier(.4, 0, .2, 1), transform 150ms cubic-bezier(.4, 0, .2, 1); + transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1); /** * This ensures that double tapping this text @@ -655,6 +654,46 @@ max-width: calc(100% / #{$form-control-label-stacked-scale}); } +// Password Strength Indicator +// ---------------------------------------------------------------- +.input-bottom-password-info { + $form-control-label-margin: 12px; + + ion-note, + ion-list, + ion-progress-bar { + @include margin($form-control-label-margin, 0, 0, 0); + } + + ion-note { + font-size: dynamic-font(10px); + display: block; + } + + ion-item { + --min-height: initial; + font-size: dynamic-font(12px); + } +} + +.password-strength-indicator { + --buffer-background: #{ion-color(light, base)}; + + &.password-strength-level { + &-weak { + --progress-background: #{ion-color(danger, base)}; + } + + &-moderate { + --progress-background: #{ion-color(warning, base)}; + } + + &-strong { + --progress-background: #{ion-color(success, base)}; + } + } +} + // Start/End Slots // ---------------------------------------------------------------- diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 3607b103221..5c5631cab25 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -1,5 +1,18 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, forceUpdate, h } from '@stencil/core'; +import { + Build, + Component, + Element, + Event, + Fragment, + Host, + Method, + Prop, + State, + Watch, + forceUpdate, + h, +} from '@stencil/core'; import type { LegacyFormController, NotchController } from '@utils/forms'; import { createLegacyFormController, createNotchController } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; @@ -14,7 +27,7 @@ import { printIonWarning } from '@utils/logging'; import { createSlotMutationController } from '@utils/slot-mutation-controller'; import type { SlotMutationController } from '@utils/slot-mutation-controller'; import { createColorClasses, hostContext } from '@utils/theme'; -import { closeCircle, closeSharp } from 'ionicons/icons'; +import { closeCircle, closeSharp, eyeOffOutline, eyeOutline } from 'ionicons/icons'; import { getIonMode } from '../../global/ionic-global'; import type { AutocompleteTypes, Color, StyleEventDetail, TextFieldTypes } from '../../interface'; @@ -270,6 +283,12 @@ export class Input implements ComponentInterface { */ @Prop() shape?: 'round'; + @Prop() showPasswordIcon = false; + + @Prop() showPasswordStrength = false; + + @Prop() showPasswordValidations = false; + /** * If `true`, the element will have its spelling and grammar checked. */ @@ -512,6 +531,7 @@ export class Input implements ComponentInterface { if (input) { this.value = input.value || ''; } + this.emitInputChange(ev); }; @@ -604,6 +624,10 @@ export class Input implements ComponentInterface { this.emitInputChange(ev); }; + private showPassword = () => { + this.type = this.type === 'text' ? 'password' : 'text'; + }; + private hasValue(): boolean { return this.getValue().length > 0; } @@ -626,6 +650,135 @@ export class Input implements ComponentInterface { return
{getCounterText(value, maxlength, counterFormatter)}
; } + private renderPasswordValidations() { + if (this.showPasswordValidations === false) { + return null; + } + const value = this.getValue(); + const validationItems = []; + const patternRegex = this.pattern!.slice(1, -1); // Remove leading and trailing slashes from pattern attribute + + if (/.{(\d+),}/.test(patternRegex)) { + validationItems.push({ condition: /.{(\d+),}/.test(value), text: 'Must have minimum of 8 characters' }); + } + + if (/[A-Z]/.test(patternRegex)) { + validationItems.push({ condition: /[A-Z]/.test(value), text: 'Must have a uppercase character' }); + } + + if (/[a-z]/.test(patternRegex)) { + validationItems.push({ condition: /[a-z]/.test(value), text: 'Must have a lowercase character' }); + } + + if (/\d/.test(patternRegex)) { + validationItems.push({ condition: /\d/.test(value), text: 'Must have a digit' }); + } + + if (/(?=.*[@#$%^&+=])/.test(patternRegex)) { + validationItems.push({ condition: /(?=.*[@#$%^&+=])/.test(value), text: 'Must have a special character' }); + } + + return ( + + {validationItems.map((item, index) => ( + + + {item.text} + + ))} + + ); + } + + private renderPasswordIndicator() { + if (this.showPasswordStrength === false) { + return null; + } + + const { value } = this; + + const passwordStrength = this.checkPasswordStrength(value as string); + + if (passwordStrength.value === 0) { + return null; + } + + return ( + + + + Your Password is {passwordStrength.level} + + + ); + } + + private checkPasswordStrength(password: string) { + const passwordStrength = { value: 0, level: '' }; + + // Check length + if (password.length >= 8) { + passwordStrength.value += 1; + } + + // Check for uppercase letters + if (/[A-Z]/.test(password)) { + passwordStrength.value += 1; + } + + // Check for lowercase letters + if (/[a-z]/.test(password)) { + passwordStrength.value += 1; + } + + // Check for digits + if (/\d/.test(password)) { + passwordStrength.value += 1; + } + + // Check for special characters + if (/[$@$!%*?&#]/.test(password)) { + passwordStrength.value += 1; + } + + // Display the password strength + switch (passwordStrength.value) { + case 0: + passwordStrength.value = 0; + passwordStrength.level = 'weak'; + break; + case 1: + passwordStrength.value = 0.33; + passwordStrength.level = 'weak'; + break; + case 2: + case 3: + passwordStrength.value = 0.66; + passwordStrength.level = 'moderate'; + break; + case 4: + case 5: + passwordStrength.value = 1; + passwordStrength.level = 'strong'; + break; + } + return passwordStrength; + } + /** * Responsible for rendering helper text, * error text, and counter. This element should only @@ -640,15 +793,24 @@ export class Input implements ComponentInterface { */ const hasHintText = !!helperText || !!errorText; const hasCounter = counter === true && maxlength !== undefined; - if (!hasHintText && !hasCounter) { + if (!hasHintText && !hasCounter && !this.showPasswordStrength && !this.showPasswordValidations) { return; } return ( -
- {this.renderHintText()} - {this.renderCounter()} -
+ +
+ {this.renderHintText()} + {this.renderCounter()} +
+ + {(this.showPasswordStrength || this.showPasswordValidations) && ( +
+ {this.renderPasswordIndicator()} + {this.renderPasswordValidations()} +
+ )} +
); } @@ -756,7 +918,6 @@ export class Input implements ComponentInterface { */ const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots)); - return (