Skip to content

Commit

Permalink
feat(date-input): add component (#751)
Browse files Browse the repository at this point in the history
  • Loading branch information
reme3d2y committed Jul 23, 2021
1 parent 5d312bf commit 4b94bee
Show file tree
Hide file tree
Showing 15 changed files with 514 additions and 4 deletions.
Empty file.
25 changes: 25 additions & 0 deletions packages/date-input/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@alfalab/core-components-date-input",
"version": "1.0.0",
"description": "",
"keywords": [],
"license": "MIT",
"main": "dist/index.js",
"module": "./dist/esm/index.js",
"files": [
"dist"
],
"scripts": {
"postinstall": "node ./dist/send-stats.js > /dev/null 2>&1 || exit 0"
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.1"
},
"dependencies": {
"@alfalab/core-components-masked-input": "^4.0.1",
"date-fns": "^2.16.1"
}
}
91 changes: 91 additions & 0 deletions packages/date-input/src/Component.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, waitFor } from '@testing-library/react';

import { DateInput } from './index';

describe('DateInput', () => {
describe('Display tests', () => {
it('should match snapshot', () => {
expect(render(<DateInput value='01.01.2021' />).container).toMatchSnapshot();
});
});

it('should set `data-test-id` attribute', () => {
const testId = 'test-id';
const { getByTestId } = render(<DateInput dataTestId={testId} />);

expect(getByTestId(testId)).toBeInTheDocument();
});

it('should set custom class', () => {
const className = 'custom-class';
const { container } = render(<DateInput className={className} />);

expect(container.firstElementChild).toHaveClass(className);
});

it('should render input[type=date] if mobileMode=native', async () => {
const { container } = render(<DateInput mobileMode='native' />);

expect(container.querySelector('input[type="date"]')).toBeInTheDocument();
});

describe('Controlled-way', () => {
it('should set value to input', () => {
const value = '01.01.2020';
const value2 = '02.01.2020';
const { queryByRole, rerender } = render(<DateInput value={value} />);

expect(queryByRole('textbox')).toHaveValue(value);

rerender(<DateInput value={value2} />);

expect(queryByRole('textbox')).toHaveValue(value2);
});
});

describe('Uncontrolled-way', () => {
it('should set default value to input', () => {
const value = '01.01.2020';
const { queryByRole } = render(<DateInput defaultValue={value} />);

expect(queryByRole('textbox')).toHaveValue(value);
});

it('should set value to input', async () => {
const value = '01.01.2020';
const { queryByRole } = render(<DateInput />);

const input = queryByRole('textbox') as HTMLInputElement;

userEvent.type(input, value);

await waitFor(() => {
expect(input).toHaveValue(value);
});
});
});

describe('Callback tests', () => {
it('should call onChange callback', () => {
const cb = jest.fn();
const value = '01.01.2020';
const { queryByRole } = render(<DateInput onChange={cb} />);

const input = queryByRole('textbox') as HTMLInputElement;

userEvent.type(input, value);

expect(cb).toBeCalledTimes(value.length);
});
});

describe('Render tests', () => {
test('should unmount without errors', () => {
const { unmount } = render(<DateInput />);

expect(unmount).not.toThrowError();
});
});
});
131 changes: 131 additions & 0 deletions packages/date-input/src/Component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React, { ChangeEvent, useCallback, useMemo, useState } from 'react';
import { MaskedInput, MaskedInputProps } from '@alfalab/core-components-masked-input';

import {
SUPPORTS_INPUT_TYPE_DATE,
NATIVE_DATE_FORMAT,
createAutoCorrectedDatePipe,
parseDateString,
formatDate,
mask,
} from './utils';

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

export type DateInputProps = Omit<MaskedInputProps, 'onBeforeDisplay' | 'mask' | 'onChange'> & {
/**
* Минимальный год, доступный для ввода
*/
minYear?: number;

/**
* Максимальный год, доступный для ввода
*/
maxYear?: number;

/**
* Управление нативным режимом на мобильных устройствах
*/
mobileMode?: 'input' | 'native';

/**
* Обработчик изменения значения
*/
onChange?: (
event: ChangeEvent<HTMLInputElement>,
payload: { date: Date; value: string },
) => void;
};

export const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
(
{
maxYear,
minYear,
mobileMode = 'input',
value,
defaultValue,
rightAddons,
onChange,
...restProps
},
ref,
) => {
const uncontrolled = value === undefined;
const shouldRenderNative = SUPPORTS_INPUT_TYPE_DATE && mobileMode === 'native';

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

const inputValue = uncontrolled ? stateValue : value;

const pipe = useMemo(
() =>
createAutoCorrectedDatePipe({
maxYear,
minYear,
}),
[maxYear, minYear],
);

const changeHandler = useCallback(
(event: ChangeEvent<HTMLInputElement>, newValue: string, newDate: Date) => {
if (uncontrolled) {
setStateValue(newValue);
}

if (onChange) {
onChange(event, { date: newDate, value: newValue });
}
},
[onChange, uncontrolled],
);

const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
const newDate = parseDateString(newValue);

changeHandler(event, newValue, newDate);
},
[changeHandler],
);

const handleNativeInputChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const newDate = parseDateString(event.target.value, NATIVE_DATE_FORMAT);
const newValue = event.target.value === '' ? '' : formatDate(newDate);

changeHandler(event, newValue, newDate);
},
[changeHandler],
);

return (
<MaskedInput
{...restProps}
ref={ref}
mask={mask}
keepCharPositions={true}
defaultValue={defaultValue}
value={inputValue}
onBeforeDisplay={pipe}
onChange={handleChange}
rightAddons={
<React.Fragment>
<span className={styles.icon} />
{rightAddons}
{shouldRenderNative && (
<input
type='date'
ref={ref}
defaultValue={defaultValue}
onChange={handleNativeInputChange}
className={styles.nativeInput}
/>
)}
</React.Fragment>
}
/>
);
},
);
35 changes: 35 additions & 0 deletions packages/date-input/src/__snapshots__/Component.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`DateInput Display tests should match snapshot 1`] = `
<div>
<div
class="component s hasRightAddons"
>
<div
class="inner filled"
>
<div
class="inputWrapper"
>
<div
class="input"
>
<input
class="input"
type="text"
value="01.01.2021"
/>
</div>
</div>
<div
class="addons rightAddons"
>
<span
class="icon"
/>
</div>
</div>
</div>
</div>
`;
52 changes: 52 additions & 0 deletions packages/date-input/src/docs/Component.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Meta, Props, Story } from '@storybook/addon-docs/blocks';
import { text, select, boolean } from '@storybook/addon-knobs';
import { ComponentHeader, Tabs, CssVars } from 'storybook/blocks';

import { DateInput } from '../Component';
import { name, version } from '../../package.json';
import Description from './description.mdx';
import Changelog from '../../CHANGELOG.md';
import styles from '!!raw-loader!../index.module.css';


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


<!-- Canvas -->

<Story name='DateInput'>
<DateInput
block={boolean('block', false)}
size={select('size', ['s', 'm', 'l', 'xl'], 's')}
error={text('error', '')}
hint={text('hint', '')}
label={text('label', '')}
/>
</Story>


<!-- Docs -->

<ComponentHeader
name='DateInput'
version={version}
package='@alfalab/core-components/date-input'
stage={1}
/>

```tsx
import { DateInput } from '@alfalab/core-components/date-input';
```


<Tabs
description={<Description />}
props={<Props of={DateInput} />}
changelog={<Changelog />}
cssVars={<CssVars css={styles} />}
/>

3 changes: 3 additions & 0 deletions packages/date-input/src/docs/description.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```tsx live
<DateInput />
```
32 changes: 32 additions & 0 deletions packages/date-input/src/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@import '../../themes/src/default.css';

:root {
--date-input-icon: url('https://alfabank.st/icons/glyph_calendar_m.svg');
}

.icon {
width: 24px;
height: 24px;
display: block;
background: var(--date-input-icon);
background-size: cover;
background-position: center;
}

.nativeInput {
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
appearance: none;
z-index: 1;

&::-webkit-calendar-picker-indicator {
display: none;
}
&::-webkit-inner-spin-button {
display: none;
}
}
1 change: 1 addition & 0 deletions packages/date-input/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Component';

0 comments on commit 4b94bee

Please sign in to comment.