Skip to content

Commit

Permalink
feat(core-components-with-suffix): add hoc
Browse files Browse the repository at this point in the history
  • Loading branch information
reme3d2y committed Oct 7, 2020
1 parent d995190 commit 7055c98
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 0 deletions.
23 changes: 23 additions & 0 deletions packages/with-suffix/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@alfalab/core-components-with-suffix",
"version": "1.0.0",
"description": "",
"keywords": [],
"license": "ISC",
"main": "dist/index.js",
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"classnames": "^2.2.6",
"react": "^16.9.0"
},
"dependencies": {
"@alfalab/core-components-input": "^1.9.0",
"@alfalab/core-components-portal": "^1.3.0",
"react-merge-refs": "^1.1.0"
}
}
61 changes: 61 additions & 0 deletions packages/with-suffix/src/Component.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Meta, Story, Props, Preview } from '@storybook/addon-docs/blocks';
import { text, select, boolean } from '@storybook/addon-knobs';
import { ComponentHeader } from 'storybook/blocks/component-header';
import Icon from '@alfalab/icons-glyph/StarMIcon';

import { Input } from '@alfalab/core-components-input';
import { withSuffix as WithSuffix } from './Component';
import { name, version } from '../package.json';

export const SuffixInput = WithSuffix(Input);


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


<!-- Canvas -->

<Story name='withSuffix'>
<SuffixInput
suffix={text('suffix', '')}
block={boolean('block', false)}
clear={boolean('clear', false)}
size={select('size', ['s', 'm', 'l'], 's')}
disabled={boolean('disabled', false)}
label={text('label', '')}
placeholder={text('placeholder', '0 ₽')}
rightAddons={boolean('rightAddons', false) && !text('error') && <Icon />}
leftAddons={boolean('leftAddons', false) && <Icon />}
bottomAddons={boolean('bottomAddons', false) && <span>bottom text</span>}
/>
</Story>


<!-- Docs -->

<ComponentHeader
name='withSuffix'
version={version}
package='@alfalab/core-components-with-suffix'
stage={1}
/>

```tsx
import { withSuffix } from '@alfalab/core-components-with-suffix';
```

HOC, позволяющий закрепить определенный текст после значения инпута.

Добавляет в переданный инпут доп. пропсу `suffix?: ReactNode`

<Preview>
{React.createElement(() => {
const SuffixInput = WithSuffix(Input);
return <SuffixInput suffix={<b> мес</b>} />;
})}
</Preview>

60 changes: 60 additions & 0 deletions packages/with-suffix/src/Component.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import { render } from '@testing-library/react';
import { Input } from '@alfalab/core-components-input';

import userEvent from '@testing-library/user-event';
import { withSuffix } from './index';

const SuffixInput = withSuffix(Input);

describe('withSuffix', () => {
describe('Snapshots tests', () => {
it('should match snapshot', () => {
expect(render(<SuffixInput value='10' suffix=' лет' />).container).toMatchSnapshot();
});
});

it('should forward ref to html input', () => {
const inputRef = jest.fn();
const dataTestId = 'test-id';
render(<SuffixInput ref={inputRef} dataTestId={dataTestId} />);

expect(inputRef.mock.calls[0][0].tagName).toBe('INPUT');
});

it('should render suffix when controlled', () => {
const suffix = 'suffix';
const { container } = render(<SuffixInput value='10' suffix={suffix} />);

expect(container.querySelector('.suffixVisible')).toBeInTheDocument();
});

it('should render suffix when uncontrolled', async () => {
const dataTestId = 'test-id';
const suffix = 'suffix';
const { container, getByTestId } = render(
<SuffixInput suffix={suffix} dataTestId={dataTestId} />,
);

const input = getByTestId(dataTestId) as HTMLInputElement;

expect(container.querySelector('.suffixVisible')).not.toBeInTheDocument();

await userEvent.type(input, 'some value');

expect(container.querySelector('.suffixVisible')).toBeInTheDocument();
});

it('should set `suffixContainerClassName` class to root', () => {
const className = 'test-class';
const { container } = render(<SuffixInput suffixContainerClassName={className} />);

expect(container.getElementsByClassName(className).length).toBe(1);
});

it('should unmount without errors', () => {
const { unmount } = render(<SuffixInput value='10' suffix=' лет' />);

expect(unmount).not.toThrowError();
});
});
117 changes: 117 additions & 0 deletions packages/with-suffix/src/Component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, {
useState,
useCallback,
useRef,
FC,
RefAttributes,
Fragment,
forwardRef,
ReactNode,
} from 'react';

import cn from 'classnames';
import mergeRefs from 'react-merge-refs';
import { Portal } from '@alfalab/core-components-portal';
import { InputProps } from '@alfalab/core-components-input';

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

export type withSuffixProps = InputProps & {
/**
* Дополнительный закрепленный текст справа от основного значения.
* Например: value='85' suffix=' мес' -> 85 мес
*/
suffix?: ReactNode;

/**
* Дополнительный класс для контейнера с суффиксом
*/
suffixContainerClassName?: string;
};

export const withSuffix = (Input: FC<InputProps & RefAttributes<HTMLInputElement>>) =>
forwardRef<HTMLInputElement, withSuffixProps>(
(
{
value,
defaultValue,
onChange,
onClear,
suffix = '',
placeholder,
className,
disabled,
suffixContainerClassName,
...restProps
},
ref,
) => {
const uncontrolled = value === undefined;

const inputRef = useRef<HTMLInputElement>(null);

const [stateValue, setStateValue] = useState(defaultValue || '');

const handleInputChange = useCallback<Required<InputProps>['onChange']>(
(event, payload) => {
if (onChange) {
onChange(event, payload);
}

if (uncontrolled) {
setStateValue(payload.value);
}
},
[onChange, uncontrolled],
);

const handleClear = useCallback<Required<InputProps>['onClear']>(
event => {
if (uncontrolled) {
setStateValue('');
}

if (onClear) {
onClear(event);
}
},
[onClear, uncontrolled],
);

const getPortalContainer = useCallback(
// TODO: Изменить сигнатуру getPortalContainer в Portal
() => (inputRef.current as HTMLElement).parentElement as HTMLElement,
[],
);

const visibleValue = uncontrolled ? stateValue : value;

return (
<Fragment>
<Input
ref={mergeRefs([ref, inputRef])}
value={visibleValue}
disabled={disabled}
onChange={handleInputChange}
onClear={handleClear}
placeholder={placeholder}
className={cn(className, {
[styles.suffixVisible]: Boolean(visibleValue),
[styles.hasSuffix]: suffix,
})}
{...restProps}
/>
<Portal getPortalContainer={getPortalContainer}>
<div className={cn(styles.suffixContainer, suffixContainerClassName)}>
<span className={styles.spacer}>{visibleValue}</span>
{suffix && (
<div className={cn(styles.suffix, { [styles.disabled]: disabled })}>
{suffix}
</div>
)}
</div>
</Portal>
</Fragment>
);
},
);
42 changes: 42 additions & 0 deletions packages/with-suffix/src/__snapshots__/Component.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`withSuffix Snapshots tests should match snapshot 1`] = `
<div>
<div
class="component s filled formControl suffixVisible hasSuffix"
>
<div
class="inner"
>
<div
class="inputWrapper"
>
<div
class="input"
>
<input
class="input"
type="text"
value="10"
/>
<div
class="suffixContainer"
>
<span
class="spacer"
>
10
</span>
<div
class="suffix"
>
лет
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
34 changes: 34 additions & 0 deletions packages/with-suffix/src/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.suffixContainer {
position: absolute;
left: 0;
top: 0;
bottom: 0;
align-items: center;
display: none;
pointer-events: none;
}

input:focus + .suffixContainer,
.suffixVisible .suffixContainer {
display: inline-flex;
}

.hasSuffix input:focus::placeholder {
color: transparent;
transition: none;
}

.spacer {
visibility: hidden;
flex-shrink: 0;
white-space: pre;
}

.suffix {
white-space: pre;
}

.disabled {
/* TODO: заменить на --input-disabled-color */
color: var(--color-dark-indigo-60-flat);
}
1 change: 1 addition & 0 deletions packages/with-suffix/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Component';
13 changes: 13 additions & 0 deletions packages/with-suffix/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"include": ["src", "../../typings"],
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDirs": ["src"],
"baseUrl": ".",
"paths": {
"@alfalab/core-components-*": ["../*/src"]
}
},
"references": [{ "path": "../input" }, { "path": "../portal" }]
}

0 comments on commit 7055c98

Please sign in to comment.