-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(PasswordInput): add component (#1745)
Co-authored-by: Evgeny Alaev <[email protected]>
- Loading branch information
1 parent
bc87468
commit 2e7f2c7
Showing
17 changed files
with
319 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
@use '../../variables'; | ||
|
||
$block: '.#{variables.$ns}password-input'; | ||
|
||
#{$block} { | ||
&__input-control { | ||
&::-ms-reveal, | ||
&::-ms-clear { | ||
display: none; | ||
} | ||
} | ||
|
||
&__copy-button { | ||
margin-inline-end: 4px; | ||
} | ||
} |
115 changes: 115 additions & 0 deletions
115
src/components/controls/PasswordInput/PasswordInput.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
'use client'; | ||
|
||
import React from 'react'; | ||
|
||
import {Eye, EyeSlash} from '@gravity-ui/icons'; | ||
|
||
import {useControlledState} from '../../../hooks'; | ||
import {ActionTooltip} from '../../ActionTooltip'; | ||
import {Button} from '../../Button'; | ||
import {ClipboardButton} from '../../ClipboardButton'; | ||
import {Icon} from '../../Icon'; | ||
import {block} from '../../utils/cn'; | ||
import {TextInput} from '../TextInput'; | ||
import type {TextInputProps} from '../TextInput'; | ||
|
||
import {i18n} from './i18n'; | ||
import {getActionButtonSizeAndIconSize} from './utils'; | ||
|
||
import './PasswordInput.scss'; | ||
|
||
const b = block('password-input'); | ||
|
||
export type PasswordInputProps = Omit<TextInputProps, 'type'> & { | ||
/** Hide copy button */ | ||
hideCopyButton?: boolean; | ||
/** Hide reveal button */ | ||
hideRevealButton?: boolean; | ||
/** Determines whether to display the tooltip for the copy button */ | ||
showCopyTooltip?: boolean; | ||
/** Determines whether to display the tooltip for the reveal button */ | ||
showRevealTooltip?: boolean; | ||
/** Determines the visibility state of the password input field */ | ||
revealValue?: boolean; | ||
/** A callback function that is invoked whenever the revealValue state changes */ | ||
onRevealValueUpdate?: (value: boolean) => void; | ||
}; | ||
|
||
export const PasswordInput = (props: PasswordInputProps) => { | ||
const { | ||
autoComplete, | ||
controlProps, | ||
endContent, | ||
rightContent, | ||
hideCopyButton = false, | ||
hideRevealButton = false, | ||
showCopyTooltip = false, | ||
showRevealTooltip = false, | ||
size = 'm', | ||
} = props; | ||
|
||
const [inputValue, setInputValue] = useControlledState( | ||
props.value, | ||
props.defaultValue ?? '', | ||
props.onUpdate, | ||
); | ||
|
||
const [revealValue, setRevealValue] = useControlledState( | ||
props.revealValue, | ||
false, | ||
props.onRevealValueUpdate, | ||
); | ||
|
||
const {actionButtonSize, iconSize} = getActionButtonSizeAndIconSize(size); | ||
|
||
const additionalEndContent = ( | ||
<React.Fragment> | ||
{endContent || rightContent} | ||
{inputValue && !hideCopyButton && !props.disabled ? ( | ||
<ClipboardButton | ||
view="flat-secondary" | ||
text={inputValue} | ||
hasTooltip={showCopyTooltip} | ||
size={actionButtonSize} | ||
className={b('copy-button')} | ||
/> | ||
) : null} | ||
{hideRevealButton ? null : ( | ||
<ActionTooltip | ||
disabled={!showRevealTooltip} | ||
title={revealValue ? i18n('label_hide-password') : i18n('label_show-password')} | ||
> | ||
<Button | ||
view="flat-secondary" | ||
disabled={props.disabled} | ||
onClick={() => setRevealValue(!revealValue)} | ||
size={actionButtonSize} | ||
extraProps={{ | ||
'aria-label': revealValue | ||
? i18n('label_hide-password') | ||
: i18n('label_show-password'), | ||
onMouseDown: (event: React.SyntheticEvent) => event.preventDefault(), | ||
}} | ||
> | ||
<Icon data={revealValue ? EyeSlash : Eye} size={iconSize} /> | ||
</Button> | ||
</ActionTooltip> | ||
)} | ||
</React.Fragment> | ||
); | ||
|
||
return ( | ||
<TextInput | ||
{...props} | ||
type={revealValue ? 'text' : 'password'} | ||
unstable_endContent={additionalEndContent} | ||
autoComplete={autoComplete ? autoComplete : 'new-password'} | ||
controlProps={{ | ||
...controlProps, | ||
className: b('input-control', controlProps?.className), | ||
}} | ||
value={inputValue} | ||
onUpdate={setInputValue} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
<!--GITHUB_BLOCK--> | ||
|
||
## Password Input | ||
|
||
<!--/GITHUB_BLOCK--> | ||
|
||
```tsx | ||
import {PasswordInput} from '@gravity-ui/uikit'; | ||
``` | ||
|
||
`TextInput` for typing passwords and other sensitive information. It can be rendered with copy and reveal buttons for more convinient usage. | ||
|
||
### Copy button | ||
|
||
This button allows users to easily copy the input value to their clipboard. You can hide this button with `hideCopyButton` boolean prop. | ||
|
||
<!--LANDING_BLOCK | ||
<ExampleBlock | ||
code={` <PasswordInput hideCopyButton={true} /> `} | ||
> | ||
<UIKit.PasswordInput hideCopyButton={true} /> | ||
</ExampleBlock> | ||
LANDING_BLOCK--> | ||
|
||
### Reveal button | ||
|
||
The `hideRevealButton` prop allows users to toggle the visibility of the password. | ||
|
||
<!--LANDING_BLOCK | ||
<ExampleBlock | ||
code={` <PasswordInput hideRevealButton={true} /> `} | ||
> | ||
<UIKit.PasswordInput hideRevealButton={true} /> | ||
</ExampleBlock> | ||
LANDING_BLOCK--> | ||
|
||
### Properties | ||
|
||
`TextInput` [properties](https://github.com/gravity-ui/uikit/blob/main/src/components/controls/TextInput/README.md#properties), with some exceptions and additions: | ||
|
||
- `type` is omitted; | ||
|
||
| Name | Description | Type | Default | | ||
| :------------------ | :------------------------------------------------------------------------- | :--------: | :-----: | | ||
| hideCopyButton | Show copy button | `boolean` | `false` | | ||
| hideRevealButton | Show reveal button | `boolean` | `false` | | ||
| showCopyTooltip | Determines whether to display the tooltip for the copy button | `boolean` | `false` | | ||
| showRevealTooltip | Determines whether to display the tooltip for the reveal button | `boolean` | `false` | | ||
| revealValue | Determines the visibility state of the password input field | `boolean` | `false` | | ||
| onRevealValueUpdate | A callback function that is invoked whenever the revealValue state changes | `function` | | | ||
|
||
<!--GITHUB_BLOCK--> | ||
|
||
#### Usage example | ||
|
||
```tsx | ||
function MyComponent() { | ||
const [value, setValue] = React.useState(''); | ||
|
||
return <PasswordInput onUpdate={setValue} value={value} />; | ||
} | ||
``` | ||
|
||
<!--GITHUB_BLOCK--> |
Binary file added
BIN
+2.93 KB
...l.test.tsx-snapshots/PasswordInput-render-story-Default-dark-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.69 KB
...ual.test.tsx-snapshots/PasswordInput-render-story-Default-dark-webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.71 KB
....test.tsx-snapshots/PasswordInput-render-story-Default-light-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.48 KB
...al.test.tsx-snapshots/PasswordInput-render-story-Default-light-webkit-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import {Meta, Markdown} from '@storybook/addon-docs'; | ||
import * as Stories from './PasswordInput.stories'; | ||
import Readme from '../README.md?raw'; | ||
|
||
<Meta of={Stories} /> | ||
|
||
<Markdown>{Readme}</Markdown> |
55 changes: 55 additions & 0 deletions
55
src/components/controls/PasswordInput/__stories__/PasswordInput.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import React from 'react'; | ||
|
||
import type {Meta, StoryFn} from '@storybook/react'; | ||
|
||
import {Button} from '../../../Button'; | ||
import {Flex, spacing} from '../../../layout'; | ||
import type {PasswordInputProps} from '../PasswordInput'; | ||
import {PasswordInput} from '../PasswordInput'; | ||
|
||
export default { | ||
title: 'Components/Inputs/PasswordInput', | ||
component: PasswordInput, | ||
args: { | ||
controlProps: { | ||
'aria-label': 'Password', | ||
}, | ||
}, | ||
} as Meta<typeof PasswordInput>; | ||
|
||
const DefaultTemplate: StoryFn<PasswordInputProps> = (args) => { | ||
const [value, setValue] = React.useState(''); | ||
|
||
return <PasswordInput {...args} onUpdate={setValue} value={value} />; | ||
}; | ||
|
||
export const Default = DefaultTemplate.bind({}); | ||
|
||
const WithGenerateRandomValueTemplate: StoryFn<PasswordInputProps> = (args) => { | ||
const [value, setValue] = React.useState(''); | ||
|
||
const generateRandomValue = React.useCallback(() => { | ||
let randomValue = ''; | ||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | ||
const charactersLength = characters.length; | ||
let counter = 0; | ||
|
||
while (counter < charactersLength) { | ||
randomValue += characters.charAt(Math.floor(Math.random() * charactersLength)); | ||
counter += 1; | ||
} | ||
|
||
setValue(randomValue); | ||
}, []); | ||
|
||
return ( | ||
<Flex> | ||
<PasswordInput {...args} onUpdate={setValue} value={value} /> | ||
<Flex className={spacing({ml: 2})}> | ||
<Button onClick={generateRandomValue}>Generate random value</Button> | ||
</Flex> | ||
</Flex> | ||
); | ||
}; | ||
|
||
export const WithGenerateRandomValue = WithGenerateRandomValueTemplate.bind({}); |
13 changes: 13 additions & 0 deletions
13
src/components/controls/PasswordInput/__tests__/PasswordInput.visual.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import React from 'react'; | ||
|
||
import {test} from '~playwright/core'; | ||
|
||
import {PasswordInputStories} from './helpersPlaywright'; | ||
|
||
test.describe('PasswordInput', () => { | ||
test('render story: <Default>', async ({mount, expectScreenshot}) => { | ||
await mount(<PasswordInputStories.Default />); | ||
|
||
await expectScreenshot(); | ||
}); | ||
}); |
5 changes: 5 additions & 0 deletions
5
src/components/controls/PasswordInput/__tests__/helpersPlaywright.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import {composeStories} from '@storybook/react'; | ||
|
||
import * as DefaultPasswordInputStories from '../__stories__/PasswordInput.stories'; | ||
|
||
export const PasswordInputStories = composeStories(DefaultPasswordInputStories); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"label_show-password": "Show password", | ||
"label_hide-password": "Hide password" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import {addComponentKeysets} from '../../../../i18n'; | ||
|
||
import en from './en.json'; | ||
import ru from './ru.json'; | ||
|
||
const COMPONENT = 'PasswordInput'; | ||
|
||
export const i18n = addComponentKeysets({en, ru}, COMPONENT); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"label_show-password": "Показать пароль", | ||
"label_hide-password": "Скрыть пароль" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './PasswordInput'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import type {ButtonSize} from '../../Button'; | ||
import type {InputControlSize} from '../types'; | ||
|
||
export const getActionButtonSizeAndIconSize = ( | ||
textInputSize: InputControlSize, | ||
): {actionButtonSize: ButtonSize; iconSize: number} => { | ||
let actionButtonSize: ButtonSize = 's'; | ||
let iconSize = 16; | ||
|
||
switch (textInputSize) { | ||
case 's': { | ||
actionButtonSize = 'xs'; | ||
iconSize = 12; | ||
break; | ||
} | ||
case 'l': { | ||
actionButtonSize = 'm'; | ||
break; | ||
} | ||
case 'xl': { | ||
actionButtonSize = 'l'; | ||
} | ||
} | ||
|
||
return {actionButtonSize, iconSize}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './TextArea'; | ||
export * from './TextInput'; | ||
export * from './PasswordInput'; | ||
export type {InputControlPin, InputControlSize, InputControlState, InputControlView} from './types'; |