Skip to content

Commit

Permalink
feat: added password input component (#485)
Browse files Browse the repository at this point in the history
* feat: added password input component

* chore: fixed race condition in state update

* chore: update docs with race condition fix
  • Loading branch information
floridemai authored Jun 10, 2024
1 parent e706e03 commit 1ffec0b
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/components/Input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,4 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
}
)

export { Input, InputProps }
export { Input, InputProps, formVariants }
73 changes: 73 additions & 0 deletions src/components/Password/index.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement> {
formClassName?: string
variant?: 'primary' | 'error' | 'success'
toggleMask?: boolean
}

const Password = React.forwardRef<HTMLInputElement, PasswordProps>(
(
{
className,
formClassName,
variant = 'primary',
toggleMask = true,
...props
},
ref
) => {
const [isMaskOn, setIsMaskOn] = useState(true)

const onToggleMask = () => {
setIsMaskOn(isMaskOn => !isMaskOn)
}

return (
<div className={cn(formVariants({ variant }), formClassName)}>
<input
type={isMaskOn ? 'password' : 'text'}
className={cn(passwordVariants({ variant }), className)}
ref={ref}
{...props}
/>
<div className="input-right-side"></div>
{toggleMask && (
<button
type="button"
onClick={onToggleMask}
className={toggleMaskVariants()}
>
{isMaskOn ? <BsEye /> : <BsEyeSlash />}
</button>
)}
</div>
)
}
)

export { Password, PasswordProps }
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
137 changes: 137 additions & 0 deletions stories/Password/Docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Canvas, Meta } from '@storybook/blocks'

import * as PasswordStories from './Password.stories'

<Meta of={PasswordStories} />

# 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

<Canvas of={PasswordStories.PasswordDefault} />

## Without mask toggle

<Canvas of={PasswordStories.WithoutToggleMask} />

## Disabled password

<Canvas of={PasswordStories.Disabled} />

## Error password

<Canvas of={PasswordStories.Error} />

## 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<HTMLInputElement> {
formClassName?: string
variant?: 'primary' | 'error' | 'success'
toggleMask?: boolean
}

const Password = React.forwardRef<HTMLInputElement, PasswordProps>(
(
{
className,
formClassName,
variant = 'primary',
toggleMask = true,
...props
},
ref
) => {
const [isMaskOn, setIsMaskOn] = useState(true)

const onToggleMask = () => {
setIsMaskOn(isMaskOn => !isMaskOn)
}

return (
<div className={cn(formVariants({ variant }), formClassName)}>
<input
type={isMaskOn ? 'password' : 'text'}
className={cn(passwordVariants({ variant }), className)}
ref={ref}
{...props}
/>
<div className="input-right-side"></div>
{toggleMask && (
<button
type="button"
onClick={onToggleMask}
className={toggleMaskVariants()}
>
{isMaskOn ? <BsEye /> : <BsEyeSlash />}
</button>
)}
</div>
)
}
)

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<HTMLInputElement>) => {
setTrackedValue(e.currentTarget.value)
}

return (
<Password
variant={variant}
value={trackedValue}
onChange={handleOnChange}
disabled={disabled}
toggleMask={toggleMask}
/>
)
}
export { PasswordDemo }
```
26 changes: 26 additions & 0 deletions stories/Password/Password.example.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => {
setTrackedValue(e.currentTarget.value)
}

return (
<Password
variant={variant}
value={trackedValue}
onChange={handleOnChange}
disabled={disabled}
toggleMask={toggleMask}
/>
)
}
export { PasswordDemo }
53 changes: 53 additions & 0 deletions stories/Password/Password.stories.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Password>

export default meta
type Story = StoryObj<typeof meta>

// 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'
}
}

0 comments on commit 1ffec0b

Please sign in to comment.