diff --git a/src/components/controls/PasswordInput/PasswordInput.scss b/src/components/controls/PasswordInput/PasswordInput.scss new file mode 100644 index 0000000000..9e8f8c231b --- /dev/null +++ b/src/components/controls/PasswordInput/PasswordInput.scss @@ -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; + } +} diff --git a/src/components/controls/PasswordInput/PasswordInput.tsx b/src/components/controls/PasswordInput/PasswordInput.tsx new file mode 100644 index 0000000000..e86adc8bc5 --- /dev/null +++ b/src/components/controls/PasswordInput/PasswordInput.tsx @@ -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 & { + /** 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 = ( + + {endContent || rightContent} + {inputValue && !hideCopyButton && !props.disabled ? ( + + ) : null} + {hideRevealButton ? null : ( + + + + )} + + ); + + return ( + + ); +}; diff --git a/src/components/controls/PasswordInput/README.md b/src/components/controls/PasswordInput/README.md new file mode 100644 index 0000000000..f0964937d3 --- /dev/null +++ b/src/components/controls/PasswordInput/README.md @@ -0,0 +1,64 @@ + + +## Password Input + + + +```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. + + + +### Reveal button + +The `hideRevealButton` prop allows users to toggle the visibility of the password. + + + +### 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` | | + + + +#### Usage example + +```tsx +function MyComponent() { + const [value, setValue] = React.useState(''); + + return ; +} +``` + + diff --git a/src/components/controls/PasswordInput/__snapshots__/PasswordInput.visual.test.tsx-snapshots/PasswordInput-render-story-Default-dark-chromium-linux.png b/src/components/controls/PasswordInput/__snapshots__/PasswordInput.visual.test.tsx-snapshots/PasswordInput-render-story-Default-dark-chromium-linux.png new file mode 100644 index 0000000000..ace4d75b98 Binary files /dev/null and b/src/components/controls/PasswordInput/__snapshots__/PasswordInput.visual.test.tsx-snapshots/PasswordInput-render-story-Default-dark-chromium-linux.png differ diff --git a/src/components/controls/PasswordInput/__snapshots__/PasswordInput.visual.test.tsx-snapshots/PasswordInput-render-story-Default-dark-webkit-linux.png b/src/components/controls/PasswordInput/__snapshots__/PasswordInput.visual.test.tsx-snapshots/PasswordInput-render-story-Default-dark-webkit-linux.png new file mode 100644 index 0000000000..4091f4dc08 Binary files /dev/null and b/src/components/controls/PasswordInput/__snapshots__/PasswordInput.visual.test.tsx-snapshots/PasswordInput-render-story-Default-dark-webkit-linux.png differ diff --git a/src/components/controls/PasswordInput/__snapshots__/PasswordInput.visual.test.tsx-snapshots/PasswordInput-render-story-Default-light-chromium-linux.png b/src/components/controls/PasswordInput/__snapshots__/PasswordInput.visual.test.tsx-snapshots/PasswordInput-render-story-Default-light-chromium-linux.png new file mode 100644 index 0000000000..a2becc8a8c Binary files /dev/null and b/src/components/controls/PasswordInput/__snapshots__/PasswordInput.visual.test.tsx-snapshots/PasswordInput-render-story-Default-light-chromium-linux.png differ diff --git a/src/components/controls/PasswordInput/__snapshots__/PasswordInput.visual.test.tsx-snapshots/PasswordInput-render-story-Default-light-webkit-linux.png b/src/components/controls/PasswordInput/__snapshots__/PasswordInput.visual.test.tsx-snapshots/PasswordInput-render-story-Default-light-webkit-linux.png new file mode 100644 index 0000000000..11a1b5bc2d Binary files /dev/null and b/src/components/controls/PasswordInput/__snapshots__/PasswordInput.visual.test.tsx-snapshots/PasswordInput-render-story-Default-light-webkit-linux.png differ diff --git a/src/components/controls/PasswordInput/__stories__/Docs.mdx b/src/components/controls/PasswordInput/__stories__/Docs.mdx new file mode 100644 index 0000000000..343d63b894 --- /dev/null +++ b/src/components/controls/PasswordInput/__stories__/Docs.mdx @@ -0,0 +1,7 @@ +import {Meta, Markdown} from '@storybook/addon-docs'; +import * as Stories from './PasswordInput.stories'; +import Readme from '../README.md?raw'; + + + +{Readme} diff --git a/src/components/controls/PasswordInput/__stories__/PasswordInput.stories.tsx b/src/components/controls/PasswordInput/__stories__/PasswordInput.stories.tsx new file mode 100644 index 0000000000..575b1d736a --- /dev/null +++ b/src/components/controls/PasswordInput/__stories__/PasswordInput.stories.tsx @@ -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; + +const DefaultTemplate: StoryFn = (args) => { + const [value, setValue] = React.useState(''); + + return ; +}; + +export const Default = DefaultTemplate.bind({}); + +const WithGenerateRandomValueTemplate: StoryFn = (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 ( + + + + + + + ); +}; + +export const WithGenerateRandomValue = WithGenerateRandomValueTemplate.bind({}); diff --git a/src/components/controls/PasswordInput/__tests__/PasswordInput.visual.test.tsx b/src/components/controls/PasswordInput/__tests__/PasswordInput.visual.test.tsx new file mode 100644 index 0000000000..f6ab293e36 --- /dev/null +++ b/src/components/controls/PasswordInput/__tests__/PasswordInput.visual.test.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import {test} from '~playwright/core'; + +import {PasswordInputStories} from './helpersPlaywright'; + +test.describe('PasswordInput', () => { + test('render story: ', async ({mount, expectScreenshot}) => { + await mount(); + + await expectScreenshot(); + }); +}); diff --git a/src/components/controls/PasswordInput/__tests__/helpersPlaywright.ts b/src/components/controls/PasswordInput/__tests__/helpersPlaywright.ts new file mode 100644 index 0000000000..7c6a1d906b --- /dev/null +++ b/src/components/controls/PasswordInput/__tests__/helpersPlaywright.ts @@ -0,0 +1,5 @@ +import {composeStories} from '@storybook/react'; + +import * as DefaultPasswordInputStories from '../__stories__/PasswordInput.stories'; + +export const PasswordInputStories = composeStories(DefaultPasswordInputStories); diff --git a/src/components/controls/PasswordInput/i18n/en.json b/src/components/controls/PasswordInput/i18n/en.json new file mode 100644 index 0000000000..130afcee1b --- /dev/null +++ b/src/components/controls/PasswordInput/i18n/en.json @@ -0,0 +1,4 @@ +{ + "label_show-password": "Show password", + "label_hide-password": "Hide password" +} diff --git a/src/components/controls/PasswordInput/i18n/index.ts b/src/components/controls/PasswordInput/i18n/index.ts new file mode 100644 index 0000000000..5a8bb879a3 --- /dev/null +++ b/src/components/controls/PasswordInput/i18n/index.ts @@ -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); diff --git a/src/components/controls/PasswordInput/i18n/ru.json b/src/components/controls/PasswordInput/i18n/ru.json new file mode 100644 index 0000000000..838beedfba --- /dev/null +++ b/src/components/controls/PasswordInput/i18n/ru.json @@ -0,0 +1,4 @@ +{ + "label_show-password": "Показать пароль", + "label_hide-password": "Скрыть пароль" +} diff --git a/src/components/controls/PasswordInput/index.ts b/src/components/controls/PasswordInput/index.ts new file mode 100644 index 0000000000..1eed3611f8 --- /dev/null +++ b/src/components/controls/PasswordInput/index.ts @@ -0,0 +1 @@ +export * from './PasswordInput'; diff --git a/src/components/controls/PasswordInput/utils.ts b/src/components/controls/PasswordInput/utils.ts new file mode 100644 index 0000000000..3fbea684c2 --- /dev/null +++ b/src/components/controls/PasswordInput/utils.ts @@ -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}; +}; diff --git a/src/components/controls/index.ts b/src/components/controls/index.ts index 211411bace..7319aa37d2 100644 --- a/src/components/controls/index.ts +++ b/src/components/controls/index.ts @@ -1,3 +1,4 @@ export * from './TextArea'; export * from './TextInput'; +export * from './PasswordInput'; export type {InputControlPin, InputControlSize, InputControlState, InputControlView} from './types';