From 7a276d51ecbfdb313c0e3e1f810698ce92d15148 Mon Sep 17 00:00:00 2001 From: Kamil Emeleev Date: Thu, 2 Jul 2026 16:57:18 +0300 Subject: [PATCH] docs(Input): improve Password story --- .../src/components/Input/Input.stories.tsx | 151 ++++++++++++++++-- 1 file changed, 134 insertions(+), 17 deletions(-) diff --git a/packages/components/src/components/Input/Input.stories.tsx b/packages/components/src/components/Input/Input.stories.tsx index c7fad4939..3aefdc231 100644 --- a/packages/components/src/components/Input/Input.stories.tsx +++ b/packages/components/src/components/Input/Input.stories.tsx @@ -2,10 +2,12 @@ import { useState } from 'react'; import { useBoolean } from '@koobiq/react-core'; import { + IconCheckS16, IconEye16, IconEyeSlash16, IconGlobe16, IconMagnifyingGlass16, + IconXmarkS16, } from '@koobiq/react-icons'; import * as Icons from '@koobiq/react-icons'; import type { Meta, StoryObj } from '@storybook/react'; @@ -15,6 +17,7 @@ import { Button } from '../Button'; import { FlexBox } from '../FlexBox'; import { Form } from '../Form'; import { IconButton } from '../IconButton'; +import { Tooltip } from '../Tooltip'; import { Typography } from '../Typography'; import { Input, inputPropVariant } from './index'; @@ -288,30 +291,144 @@ export const ControlledValue: Story = { export const Password: Story = { render: function Render(args) { - const [hiddenPassword, { toggle }] = useBoolean(true); + const passwordRuleStateColor = { + neutral: 'var(--kbq-foreground-contrast-tertiary)', + success: 'var(--kbq-foreground-success)', + error: 'var(--kbq-foreground-error)', + } as const; + + type PasswordRuleState = keyof typeof passwordRuleStateColor; + type PasswordRule = { + id: string; + label: string; + test: (value: string) => boolean; + }; + + const disallowedPasswordCharacters = + /[^ !"#$%&'()*+,\-./\\:;<=>?@[\]^_`{|}~A-Za-z0-9]/; + + const passwordRules: PasswordRule[] = [ + { + id: 'length', + label: '8 to 15 characters', + test: (value) => value.length >= 8 && value.length <= 15, + }, + { + id: 'uppercase', + label: 'Uppercase Latin letter', + test: (value) => /[A-Z]/.test(value), + }, + { + id: 'lowercase', + label: 'Lowercase Latin letter', + test: (value) => /[a-z]/.test(value), + }, + { + id: 'digit', + label: 'Number', + test: (value) => /\d/.test(value), + }, + { + id: 'special', + label: 'Only Latin letters, numbers, spaces, and special characters', + test: (value) => + value.length > 0 && !disallowedPasswordCharacters.test(value), + }, + { + id: 'uppercase-count', + label: 'At least 5 uppercase letters', + test: (value) => (value.match(/[A-Z]/g)?.length ?? 0) >= 5, + }, + ]; + + const [value, setValue] = useState(''); + const [isInvalid, { on: showError, off: hideError }] = useBoolean(false); + + const [isPasswordHidden, { toggle: togglePasswordVisibility }] = + useBoolean(true); + + const [isEdited, { on: markEdited }] = useBoolean(false); + + const hasValue = value !== ''; + const isPasswordValid = passwordRules.every((rule) => rule.test(value)); + const passwordType = isPasswordHidden ? 'password' : 'text'; + + const passwordVisibilityText = isPasswordHidden + ? 'Show password' + : 'Hide password'; + + const getPasswordRuleState = (isRulePassed: boolean): PasswordRuleState => { + if (!isEdited) return 'neutral'; + + return isRulePassed ? 'success' : 'error'; + }; + + const renderPasswordRequirement = (rule: PasswordRule) => { + const isRulePassed = rule.test(value); + const state = getPasswordRuleState(isRulePassed); + const Icon = isEdited && isRulePassed ? IconCheckS16 : IconXmarkS16; + + return ( + + + {rule.label} + + ); + }; + + const handleChange = (nextValue: string) => { + markEdited(); + hideError(); + setValue(nextValue); + }; + + const handleBlur = () => { + if (hasValue && !isPasswordValid) { + showError(); + + return; + } + + hideError(); + }; return ( ( + + , + , + ]} + /> + + )} > - , - , - ]} - /> - + {passwordVisibilityText} + + } + caption={ + + {passwordRules.map(renderPasswordRequirement)} + } {...args} />