diff --git a/src/components/PasswordInput/PasswordInput.scss b/src/components/PasswordInput/PasswordInput.scss new file mode 100644 index 00000000..388aea09 --- /dev/null +++ b/src/components/PasswordInput/PasswordInput.scss @@ -0,0 +1,21 @@ +@use '../variables'; + +$block: '.#{variables.$ns}password-input'; + +#{$block} { + &__input-control { + &::-ms-reveal, + &::-ms-clear { + display: none; + } + } + + &__additional-right-content { + display: flex; + align-items: center; + } + + &__copy-button { + margin-right: 4px; + } +} diff --git a/src/components/PasswordInput/PasswordInput.tsx b/src/components/PasswordInput/PasswordInput.tsx new file mode 100644 index 00000000..48930f73 --- /dev/null +++ b/src/components/PasswordInput/PasswordInput.tsx @@ -0,0 +1,110 @@ +import React from 'react'; + +import {Eye, EyeSlash} from '@gravity-ui/icons'; +import {Button, ClipboardButton, Icon, TextInput, TextInputProps, Tooltip} from '@gravity-ui/uikit'; + +import {block} from '../utils/cn'; + +import i18n from './i18n'; +import {getCopyButtonSizeAndIconSize} from './utils'; + +import './PasswordInput.scss'; + +const b = block('password-input'); + +export type PasswordInputProps = Required> & + Omit & { + /** Show copy button */ + showCopyButton?: boolean; + /** Show reveal button */ + showRevealButton?: boolean; + /** Disable the tooltip for the copy button. The tooltip will not be displayed */ + hasCopyTooltip?: boolean; + /** Disable the tooltip for the reveal button. The tooltip will not be displayed */ + hasRevealTooltip?: boolean; + }; + +export const PasswordInput: React.FC = (props) => { + const { + autoComplete, + value, + showCopyButton, + rightContent, + showRevealButton, + size = 'm', + hasCopyTooltip = true, + hasRevealTooltip = true, + controlProps, + } = props; + + const [hideValue, setHideValue] = React.useState(true); + + const additionalRightContent = React.useMemo(() => { + if (!showRevealButton && !showCopyButton) { + return {rightContent}; + } + + const onClick = () => { + setHideValue((hideValue) => !hideValue); + }; + + const {copyButtonSize, iconSize} = getCopyButtonSizeAndIconSize(size); + + return ( +
+ {rightContent} + {value && showCopyButton ? ( + + ) : null} + {showRevealButton ? ( + + + + ) : null} +
+ ); + }, [ + showRevealButton, + showCopyButton, + rightContent, + value, + hasRevealTooltip, + hasCopyTooltip, + hideValue, + size, + ]); + + return ( + + ); +}; diff --git a/src/components/PasswordInput/README.md b/src/components/PasswordInput/README.md new file mode 100644 index 00000000..cdb226e7 --- /dev/null +++ b/src/components/PasswordInput/README.md @@ -0,0 +1,35 @@ +## PasswordInput + +Password Input display component + +### PropTypes + +Same as [TextInput component](https://github.com/gravity-ui/uikit/blob/main/src/components/controls/TextInput/README.md), with some exceptions: + +- `value` is required property; +- `onUpdate` is required property; +- `type` is omitted; + +| Property | Type | Required | Default | Description | +| :--------------- | :-------- | :------- | :------ | :--------------------------------------------------------------------------- | +| showCopyButton | `boolean` | | | Show copy button | +| showRevealButton | `boolean` | | | Show reveal button | +| hasCopyTooltip | `boolean` | | `true` | Disable the tooltip for the copy button. The tooltip will not be displayed | +| hasRevealTooltip | `boolean` | | `true` | Disable the tooltip for the reveal button. The tooltip will not be displayed | + +#### Usage example + +```jsx harmony +function MyComponent() { + const [value, setValue] = React.useState(''); + + return ( + + ); +} +``` diff --git a/src/components/PasswordInput/__stories__/PasswordInput.stories.tsx b/src/components/PasswordInput/__stories__/PasswordInput.stories.tsx new file mode 100644 index 00000000..5c90bb31 --- /dev/null +++ b/src/components/PasswordInput/__stories__/PasswordInput.stories.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import {Button} from '@gravity-ui/uikit'; +import type {Meta, StoryFn} from '@storybook/react'; + +import {cn} from '../../utils/cn'; +import {PasswordInput, PasswordInputProps} from '../PasswordInput'; + +import './PasswordInputStories.scss'; + +const b = cn('password-input-stories'); + +export default { + title: 'Components/PasswordInput', + component: PasswordInput, + args: { + showCopyButton: true, + showRevealButton: true, + }, +} 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/PasswordInput/__stories__/PasswordInputStories.scss b/src/components/PasswordInput/__stories__/PasswordInputStories.scss new file mode 100644 index 00000000..ebc92b17 --- /dev/null +++ b/src/components/PasswordInput/__stories__/PasswordInputStories.scss @@ -0,0 +1,7 @@ +.password-input-stories { + display: flex; + + &__button-generate-random-value { + margin-left: 8px; + } +} diff --git a/src/components/PasswordInput/i18n/en.json b/src/components/PasswordInput/i18n/en.json new file mode 100644 index 00000000..c4b20079 --- /dev/null +++ b/src/components/PasswordInput/i18n/en.json @@ -0,0 +1,4 @@ +{ + "label_show-password": "Show password", + "label_hide-password": "Hide password" +} \ No newline at end of file diff --git a/src/components/PasswordInput/i18n/index.ts b/src/components/PasswordInput/i18n/index.ts new file mode 100644 index 00000000..a79a9a36 --- /dev/null +++ b/src/components/PasswordInput/i18n/index.ts @@ -0,0 +1,8 @@ +import {registerKeyset} from '../../utils/registerKeyset'; + +import en from './en.json'; +import ru from './ru.json'; + +const COMPONENT = 'PasswordInput'; + +export default registerKeyset({en, ru}, COMPONENT); diff --git a/src/components/PasswordInput/i18n/ru.json b/src/components/PasswordInput/i18n/ru.json new file mode 100644 index 00000000..0b0ef115 --- /dev/null +++ b/src/components/PasswordInput/i18n/ru.json @@ -0,0 +1,4 @@ +{ + "label_show-password": "Показать пароль", + "label_hide-password": "Скрыть пароль" +} \ No newline at end of file diff --git a/src/components/PasswordInput/index.ts b/src/components/PasswordInput/index.ts new file mode 100644 index 00000000..1eed3611 --- /dev/null +++ b/src/components/PasswordInput/index.ts @@ -0,0 +1 @@ +export * from './PasswordInput'; diff --git a/src/components/PasswordInput/utils.ts b/src/components/PasswordInput/utils.ts new file mode 100644 index 00000000..8418edba --- /dev/null +++ b/src/components/PasswordInput/utils.ts @@ -0,0 +1,26 @@ +import type {ButtonSize, InputControlSize} from '@gravity-ui/uikit'; + +export const getCopyButtonSizeAndIconSize = ( + textInputSize: InputControlSize, +): {copyButtonSize: ButtonSize; iconSize: number} => { + let copyButtonSize: ButtonSize = 's'; + let iconSize = 16; + + switch (textInputSize) { + case 's': { + copyButtonSize = 'xs'; + iconSize = 12; + break; + } + case 'l': { + copyButtonSize = 'm'; + break; + } + case 'xl': { + copyButtonSize = 'l'; + iconSize = 20; + } + } + + return {copyButtonSize, iconSize}; +}; diff --git a/src/components/index.ts b/src/components/index.ts index 63264242..9640a46b 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -7,6 +7,7 @@ export * from './InfiniteScroll'; export * from './ItemSelector'; export * from './Notification'; export * from './Notifications'; +export * from './PasswordInput'; export * from './PlaceholderContainer'; export * from './PromoSheet'; export * from './ActionsPanel';