Skip to content

Commit

Permalink
feat(password-input): add password input (PDS-233) (#691)
Browse files Browse the repository at this point in the history
* feat(password-input): add password input

* feat(password-input): fixes

* test(password-input): add screenshot tests

* feat(password-input): hide reveal icon in ms-edge browser

* feat(password-input): change icon-button view to secondary
  • Loading branch information
dmitrsavk committed Jun 29, 2021
1 parent 579c8e1 commit d872e4d
Show file tree
Hide file tree
Showing 17 changed files with 372 additions and 9 deletions.
8 changes: 4 additions & 4 deletions .storybook/blocks/tabs/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ const TabTitle = {

type Props = {
description: ReactNode;
prpos: ReactNode;
props: ReactNode;
cssVars?: ReactNode;
defaultSelected?: TabName;
};

export const Tabs: FC<Props> = ({
description,
prpos,
props,
cssVars,
defaultSelected = TabName.DESCRIPTION,
}) => {
Expand All @@ -41,7 +41,7 @@ export const Tabs: FC<Props> = ({
<div style={{ marginTop: '40px' }}>{description}</div>
</Tab>,
<Tab title={TabTitle[TabName.PROPS]} id={TabName.PROPS}>
{prpos}
{props}
</Tab>,
];

Expand All @@ -56,7 +56,7 @@ export const Tabs: FC<Props> = ({
return result;
};

const tabs = useMemo(() => renderTabs(), [description, prpos, cssVars]);
const tabs = useMemo(() => renderTabs(), [description, props, cssVars]);

return (
<TabsResponsive selectedId={selected} onChange={handleChange}>
Expand Down
2 changes: 1 addition & 1 deletion packages/button/src/docs/component.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,6 @@ import { Button } from '@alfalab/core-components/button';

<Tabs
description={<Description />}
prpos={<Props of={Button} /> }
props={<Props of={Button} /> }
cssVars={<CssVars css={`${styles}${colorsStyles}`} />}
/>
Empty file.
25 changes: 25 additions & 0 deletions packages/password-input/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@alfalab/core-components-password-input",
"version": "1.0.0",
"description": "Input password",
"keywords": [],
"license": "MIT",
"main": "dist/index.js",
"module": "./dist/esm/index.js",
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.1",
"react-dom": "^16.9.0 || ^17.0.1"
},
"dependencies": {
"@alfalab/core-components-icon-button": "^2.0.2",
"@alfalab/core-components-input": "^6.1.3",
"@alfalab/icons-glyph": "^1.141.0",
"classnames": "^2.2.6"
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions packages/password-input/src/__snapshots__/component.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`PasswordInput snapshots tests should match snapshot 1`] = `
<body>
<div>
<div
class="component s hasRightAddons"
>
<div
class="inner"
>
<div
class="inputWrapper"
>
<div
class="input"
>
<input
class="input input"
type="password"
value=""
/>
</div>
</div>
<div
class="addons rightAddons addons"
>
<button
class="component ghost s component ghost iconOnly secondary"
type="button"
>
<span
class="addons"
>
<span
class="iconWrapper s"
>
<svg
class="icon"
fill="currentColor"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
>
<path
d="M20.944 14.702a.197.197 0 00.293-.014C21.886 13.89 22.564 12.93 23 12c-1.914-4.089-6.19-7.01-11-7.01-.164 0-.326.003-.489.01-.17.007-.246.022-.125.143l9.558 9.559z"
/>
<path
clip-rule="evenodd"
d="M5.5 2L22 18.5 20.5 20l-2.44-2.527C16.28 18.561 14.21 19 12 19c-4.81 0-9.086-2.911-11-7 1.155-2.468 3.252-4.633 5.69-5.896L4 3.5 5.5 2zm8.177 11.09l1.571 1.573a4.2 4.2 0 11-5.91-5.911l1.571 1.571a2 2 0 102.767 2.767z"
fill-rule="evenodd"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</body>
`;
30 changes: 30 additions & 0 deletions packages/password-input/src/component.screenshots.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { setupScreenshotTesting, createSpriteStorybookUrl } from '../../screenshot-utils';

const screenshotTesting = setupScreenshotTesting({
it,
beforeAll,
afterAll,
expect,
});

describe(
'PasswordInput | screenshots',
screenshotTesting({
cases: [
[
'sprite',
createSpriteStorybookUrl({
componentName: 'PasswordInput',
knobs: {
passwordVisible: [true, false],
value: 'my password',
},
size: { width: 300, height: 70 },
}),
],
],
screenshotOpts: {
clip: { x: 0, y: 0, width: 600, height: 150 },
},
}),
);
71 changes: 71 additions & 0 deletions packages/password-input/src/component.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { useState, FC } from 'react';
import { render, fireEvent } from '@testing-library/react';

import { PasswordInput, PasswordInputProps } from '.';

const ControlledPasswordInput: FC<PasswordInputProps> = props => {
const [passwordVisible, setPasswordVisible] = useState(false);

const handlePasswordVisibleChange = (visible: boolean) => {
setPasswordVisible(visible);
};

return (
<PasswordInput
{...props}
passwordVisible={passwordVisible}
onPasswordVisibleChange={handlePasswordVisibleChange}
/>
);
};

const isPasswordHidden = (input: HTMLInputElement) => input.getAttribute('type') === 'password';
const isPasswordVisible = (input: HTMLInputElement) => input.getAttribute('type') === 'text';

describe('PasswordInput', () => {
const dataTestId = 'test-id';

describe('snapshots tests', () => {
it('should match snapshot', () => {
expect(render(<PasswordInput />).baseElement).toMatchSnapshot();
});
});

describe('interactions tests', () => {
it('should switch password visibility (uncontrolled)', () => {
const { baseElement, getByTestId } = render(<PasswordInput dataTestId={dataTestId} />);

const button = baseElement.querySelector('button') as HTMLButtonElement;
const input = getByTestId(dataTestId) as HTMLInputElement;

expect(isPasswordHidden(input)).toBe(true);

fireEvent.click(button);

expect(isPasswordVisible(input)).toBe(true);

fireEvent.click(button);

expect(isPasswordHidden(input)).toBe(true);
});

it('should switch password visibility (controlled)', () => {
const { baseElement, getByTestId } = render(
<ControlledPasswordInput dataTestId={dataTestId} />,
);

const button = baseElement.querySelector('button') as HTMLButtonElement;
const input = getByTestId(dataTestId) as HTMLInputElement;

expect(isPasswordHidden(input)).toBe(true);

fireEvent.click(button);

expect(isPasswordVisible(input)).toBe(true);

fireEvent.click(button);

expect(isPasswordHidden(input)).toBe(true);
});
});
});
61 changes: 61 additions & 0 deletions packages/password-input/src/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { forwardRef, useCallback, useState } from 'react';
import { Input, InputProps } from '@alfalab/core-components-input';
import { IconButton } from '@alfalab/core-components-icon-button';
import EyeMIcon from '@alfalab/icons-glyph/EyeMIcon';
import EyeOffMIcon from '@alfalab/icons-glyph/EyeOffMIcon';

import styles from './index.module.css';

export type PasswordInputProps = InputProps & {
/**
* Управление видимостью пароля (controlled)
*/
passwordVisible?: boolean;

/**
* Коллбэк при изменении видимости пароля
*/
onPasswordVisibleChange?: (visible: boolean) => void;
};

export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
({ onPasswordVisibleChange, passwordVisible, disabled, ...restProps }, ref) => {
const uncontrolled = passwordVisible === undefined;

const [statePasswordVisible, setStatePasswordVisible] = useState(
uncontrolled ? false : passwordVisible,
);

const handleButtonClick = useCallback(() => {
if (onPasswordVisibleChange) {
onPasswordVisibleChange(!passwordVisible);
}

if (uncontrolled) {
setStatePasswordVisible(visible => !visible);
}
}, [passwordVisible, uncontrolled, onPasswordVisibleChange]);

const isPasswordVisible = uncontrolled ? statePasswordVisible : passwordVisible;

return (
<Input
{...restProps}
disabled={disabled}
type={isPasswordVisible ? 'text' : 'password'}
ref={ref}
rightAddons={
<IconButton
view='secondary'
size='s'
icon={isPasswordVisible ? EyeMIcon : EyeOffMIcon}
onClick={handleButtonClick}
disabled={disabled}
/>
}
addonsClassName={styles.addons}
inputClassName={styles.input}
/>
);
},
);
51 changes: 51 additions & 0 deletions packages/password-input/src/docs/component.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
import { ComponentHeader } from 'storybook/blocks/component-header';
import { text, select, boolean } from '@storybook/addon-knobs';
import { Tabs } from 'storybook/blocks';

import { PasswordInput } from '..';
import { name, version } from '../../package.json';
import Description from './description.mdx';

<Meta
title='Компоненты'
component={PasswordInput}
parameters={{ 'theme-switcher': { themes: ['click'] } }}
/>

<!-- Canvas -->

<Story name='PasswordInput'>
<PasswordInput
block={boolean('block', false)}
size={select('size', ['s', 'm', 'l', 'xl'], 's')}
disabled={boolean('disabled', false)}
placeholder={text('placeholder', '')}
label={text('label', '')}
hint={text('hint', '')}
error={text('error', '')}
success={boolean('success', false)}
leftAddons={boolean('leftAddons', false) && <Icon />}
bottomAddons={boolean('bottomAddons', false) && <span>bottom text</span>}
readOnly={boolean('readOnly', false)}
/>
</Story>

<!-- Docs -->

<ComponentHeader
name='PasswordInput'
version={version}
package='@alfalab/core-components-password-input'
stage={2}
design='https://www.figma.com/file/uGndMZufgvqxaNQTaNTEjG/B2B-Web-Components?node-id=17757%3A0'
/>

```tsx
import { PasswordInput } from '@alfalab/core-components/password-input';
```

<Tabs
description={<Description />}
props={<Props of={PasswordInput} /> }
/>
36 changes: 36 additions & 0 deletions packages/password-input/src/docs/description.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Container } from 'storybook/blocks';

import { PasswordInput } from '../';

Компонент ввода пароля.

Uncontrolled-way:

```tsx live
<Container>
<PasswordInput />
</Container>
```

Controlled-way:

```tsx live
const ControlledPasswordInput = () => {
const [passwordVisible, setPasswordVisible] = React.useState(false);

return (
<PasswordInput
passwordVisible={passwordVisible}
onPasswordVisibleChange={visible => {
setPasswordVisible(visible);
}}
/>
);
};

render(
<Container>
<ControlledPasswordInput />
</Container>,
);
```

0 comments on commit d872e4d

Please sign in to comment.