From e90758939d9590405ddaac19a08a5e4ad799bffe Mon Sep 17 00:00:00 2001 From: Paul Negrutiu Date: Mon, 10 Jun 2024 09:42:28 +0300 Subject: [PATCH 1/3] feat: added password input component --- src/components/Input/index.tsx | 2 +- src/components/Password/index.tsx | 73 ++++++++++++++ src/components/index.ts | 1 + stories/Password/Docs.mdx | 137 ++++++++++++++++++++++++++ stories/Password/Password.example.tsx | 26 +++++ stories/Password/Password.stories.ts | 53 ++++++++++ 6 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 src/components/Password/index.tsx create mode 100644 stories/Password/Docs.mdx create mode 100644 stories/Password/Password.example.tsx create mode 100644 stories/Password/Password.stories.ts diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index c7f4a8a5..7bfe9aa1 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -126,4 +126,4 @@ const Input = React.forwardRef( } ) -export { Input, InputProps } +export { Input, InputProps, formVariants } diff --git a/src/components/Password/index.tsx b/src/components/Password/index.tsx new file mode 100644 index 00000000..78d2776e --- /dev/null +++ b/src/components/Password/index.tsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react' +import { cva } from 'class-variance-authority' + +import { cn } from '@/lib/utils' +import { BsEye, BsEyeSlash } from '@/assets' + +import { formVariants } from '../Input' + +const toggleMaskVariants = cva(['flex', 'self-center text-inherit']) + +const passwordVariants = cva( + ['flex', 'flex-grow', 'items-center', 'outline-none', 'bg-transparent'], + { + variants: { + variant: { + primary: ['text-foreground', 'dark:text-foreground-dark'], + error: ['text-feedback-red'], + success: ['text-green-700'] + } + }, + defaultVariants: { + variant: 'primary' + } + } +) + +interface PasswordProps extends React.HTMLProps { + formClassName?: string + variant?: 'primary' | 'error' | 'success' + toggleMask?: boolean +} + +const Password = React.forwardRef( + ( + { + className, + formClassName, + variant = 'primary', + toggleMask = true, + ...props + }, + ref + ) => { + const [isMaskOn, setIsMaskOn] = useState(true) + + const onToggleMask = () => { + setIsMaskOn(!isMaskOn) + } + + return ( +
+ +
+ {toggleMask && ( + + )} +
+ ) + } +) + +export { Password, PasswordProps } diff --git a/src/components/index.ts b/src/components/index.ts index 7551714d..b161efcb 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -10,6 +10,7 @@ export * from './Label' export * from './Link' export * from './Modal' export * from './Pagination' +export * from './Password' export * from './Popover' export * from './Radio' export * from './Select' diff --git a/stories/Password/Docs.mdx b/stories/Password/Docs.mdx new file mode 100644 index 00000000..90536010 --- /dev/null +++ b/stories/Password/Docs.mdx @@ -0,0 +1,137 @@ +import { Canvas, Meta } from '@storybook/blocks' + +import * as PasswordStories from './Password.stories' + + + +# Password + +Password is an input field for entering passwords. The input is masked by default. The masking can be toggled using an optional reveal button. + +## Default password + + + +## Without mask toggle + + + +## Disabled password + + + +## Error password + + + +## Attributes + +This component accepts all attributes for the input component of type text, with the addition of `toggleMask` boolena to toggle masking. + +## Input + +```tsx +import React, { useState } from 'react' +import { cva } from 'class-variance-authority' + +import { cn } from '@/lib/utils' +import { BsEye, BsEyeSlash } from '@/assets' + +import { formVariants } from '../Input' + +const toggleMaskVariants = cva(['flex', 'self-center text-inherit']) + +const passwordVariants = cva( + ['flex', 'flex-grow', 'items-center', 'outline-none', 'bg-transparent'], + { + variants: { + variant: { + primary: ['text-foreground', 'dark:text-foreground-dark'], + error: ['text-feedback-red'], + success: ['text-green-700'] + } + }, + defaultVariants: { + variant: 'primary' + } + } +) + +interface PasswordProps extends React.HTMLProps { + formClassName?: string + variant?: 'primary' | 'error' | 'success' + toggleMask?: boolean +} + +const Password = React.forwardRef( + ( + { + className, + formClassName, + variant = 'primary', + toggleMask = true, + ...props + }, + ref + ) => { + const [isMaskOn, setIsMaskOn] = useState(true) + + const onToggleMask = () => { + setIsMaskOn(!isMaskOn) + } + + return ( +
+ +
+ {toggleMask && ( + + )} +
+ ) + } +) + +export { Password, PasswordProps } +``` + +## Usage + +```tsx +import { useState } from 'react' +import { Password, PasswordProps } from '@/index' + +const PasswordDemo = ({ + variant, + toggleMask, + disabled +}: PasswordProps) => { + const [trackedValue, setTrackedValue] = useState('') + + const handleOnChange = (e: React.FormEvent) => { + setTrackedValue(e.currentTarget.value) + } + + return ( + + ) +} +export { PasswordDemo } +``` diff --git a/stories/Password/Password.example.tsx b/stories/Password/Password.example.tsx new file mode 100644 index 00000000..b6a19d7d --- /dev/null +++ b/stories/Password/Password.example.tsx @@ -0,0 +1,26 @@ +import { useState } from 'react' +import { Password, PasswordProps } from '@/index' + +const PasswordDemo = ({ + variant, + toggleMask, + disabled, + value +}: PasswordProps) => { + const [trackedValue, setTrackedValue] = useState(value) + + const handleOnChange = (e: React.FormEvent) => { + setTrackedValue(e.currentTarget.value) + } + + return ( + + ) +} +export { PasswordDemo } diff --git a/stories/Password/Password.stories.ts b/stories/Password/Password.stories.ts new file mode 100644 index 00000000..201e5555 --- /dev/null +++ b/stories/Password/Password.stories.ts @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { PasswordDemo as Password } from './Password.example' + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta = { + title: 'Form/Password', + component: Password, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout + layout: 'centered' + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + argTypes: { + disabled: { + options: [true, false, 'indeterminate'], + control: { type: 'radio' } + }, + className: { + controle: 'text', + description: 'Alter the className to change the style' + } + } + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes +} satisfies Meta + +export default meta +type Story = StoryObj + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const PasswordDefault: Story = { + args: {} +} + +export const WithoutToggleMask: Story = { + args: { + value: 'secret', + toggleMask: false + } +} + +export const Disabled: Story = { + args: { + value: 'secret', + disabled: true + } +} + +export const Error: Story = { + args: { + variant: 'error' + } +} From d73aacad3b6608084c2c656e091fffddcfaa42c5 Mon Sep 17 00:00:00 2001 From: Paul Negrutiu Date: Mon, 10 Jun 2024 11:17:08 +0300 Subject: [PATCH 2/3] chore: fixed race condition in state update --- src/components/Password/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Password/index.tsx b/src/components/Password/index.tsx index 78d2776e..a4564059 100644 --- a/src/components/Password/index.tsx +++ b/src/components/Password/index.tsx @@ -44,7 +44,7 @@ const Password = React.forwardRef( const [isMaskOn, setIsMaskOn] = useState(true) const onToggleMask = () => { - setIsMaskOn(!isMaskOn) + setIsMaskOn(isMaskOn => !isMaskOn) } return ( From 7c3dea7c627d6fa565a8886969501cdc2271249e Mon Sep 17 00:00:00 2001 From: Paul Negrutiu Date: Mon, 10 Jun 2024 11:38:30 +0300 Subject: [PATCH 3/3] chore: update docs with race condition fix --- stories/Password/Docs.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stories/Password/Docs.mdx b/stories/Password/Docs.mdx index 90536010..e4ec3d35 100644 --- a/stories/Password/Docs.mdx +++ b/stories/Password/Docs.mdx @@ -77,7 +77,7 @@ const Password = React.forwardRef( const [isMaskOn, setIsMaskOn] = useState(true) const onToggleMask = () => { - setIsMaskOn(!isMaskOn) + setIsMaskOn(isMaskOn => !isMaskOn) } return (