DEV;Add TimePicker component

This commit is contained in:
romanjaros 2021-09-12 21:09:11 +02:00 committed by Gitea
parent 97c5141773
commit cab561c77a
20 changed files with 372 additions and 91 deletions

View file

@ -31,6 +31,10 @@ import {
faFolderOpen,
faUserCircle,
faUserTie,
faClock,
faBackspace,
faMinus,
faPlus,
} from '@fortawesome/free-solid-svg-icons';
library.add(
@ -64,5 +68,10 @@ library.add(
faArrowAltCircleRight,
faUserCircle,
faFolderOpen,
faFile
faFile,
faClock,
faTimes,
faBackspace,
faMinus,
faPlus
);

View file

@ -1,13 +1,8 @@
import React, { useCallback, useMemo } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { format as setFormat } from 'date-fns';
import { STATUS } from '@treejs/types/common';
import Button from '../../Button';
import Calendar from '../../Calendar/components/Calendar';
import { Grid, GridCol } from '../../Grid';
import { ModalId } from '../../Modal/types';
type IProps = {
@ -15,12 +10,11 @@ type IProps = {
format?: ModalId;
name: ModalId;
onChange: (value: string) => void;
onRemove: () => void;
value: Date;
};
const DatePickerModal: React.FC<IProps> = (props) => {
const { onChange, onRemove, closeModal, name, value, format = 'dd.MM.yyyy' } = props;
const { onChange, closeModal, name, value, format = 'dd.MM.yyyy' } = props;
const setActiveOption = useCallback(
(newValue: Date) => {
@ -32,31 +26,11 @@ const DatePickerModal: React.FC<IProps> = (props) => {
[onChange, closeModal, name, format]
);
const removeActiveOption = useCallback((): void => {
if (onRemove) {
onRemove();
}
closeModal(name);
}, [onRemove, closeModal, name]);
const date = useMemo(() => {
return new Date(value);
}, [value]);
return (
<>
<Calendar value={date} onChange={setActiveOption} hover />
<Grid cols={4}>
<GridCol start={4} end={4}>
<Button
label={<FontAwesomeIcon icon="trash" />}
status={STATUS.warning}
onClick={removeActiveOption}
/>
</GridCol>
</Grid>
</>
);
return <Calendar value={date} onChange={setActiveOption} hover />;
};
export default DatePickerModal;

View file

@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useDispatch } from 'react-redux';
import { ModalId } from '@treejs/components/Modal/types';
@ -44,33 +43,19 @@ const DatePicker: React.FC<IDatePickerFieldProps> = (props) => {
}
}, []);
const handleDateRemove = useCallback((): void => {
setValue(null);
if (onChange) {
onChange(null, null);
}
}, []);
const handleOpenModal = useCallback((): void => {
addModalComponent(
name,
<DatePickerModal
name={name}
value={value}
closeModal={handleCloseModal}
onChange={handleDateClick}
onRemove={handleDateRemove}
/>
<DatePickerModal name={name} value={value} closeModal={handleCloseModal} onChange={handleDateClick} />
);
dispatch(openModal({ id: name, title, width: 300 }));
}, [name, value, title]);
return (
<div id={name}>
<div>
<TextField
name={name}
title={title}
rightIcon={<FontAwesomeIcon icon="calendar-alt" />}
onClick={handleOpenModal}
onFocus={handleOpenModal}
onBlur={handleBlur}

View file

@ -5,24 +5,26 @@ import cx from 'classnames';
import { isNilOrEmpty } from '@treejs/utils';
import { GRID_SIZE_ENUM } from './types';
import { GRID_SIZE, VERTICAL_ALIGN } from './types';
type IGridProps = {
children?: React.ReactElement | React.ReactElement[];
cols?: Partial<Record<GRID_SIZE_ENUM, number>> | number;
className?: string;
cols?: Partial<Record<GRID_SIZE, number>> | number;
space?: number;
vertical?: VERTICAL_ALIGN;
};
const isBiggerDefined = (cols: Partial<Record<GRID_SIZE_ENUM, number>> | number): boolean => {
const isBiggerDefined = (cols: Partial<Record<GRID_SIZE, number>> | number): boolean => {
if (!isNilOrEmpty(cols) && !is(Number, cols)) {
const sizes = keys(cols);
let bigger = [];
if (includes(GRID_SIZE_ENUM.SM, sizes)) {
bigger = [GRID_SIZE_ENUM.MD, GRID_SIZE_ENUM.LG, GRID_SIZE_ENUM.XL];
} else if (includes(GRID_SIZE_ENUM.MD, sizes)) {
bigger = [GRID_SIZE_ENUM.LG, GRID_SIZE_ENUM.XL];
} else if (includes(GRID_SIZE_ENUM.LG, sizes)) {
bigger = [GRID_SIZE_ENUM.XL];
if (includes(GRID_SIZE.SM, sizes)) {
bigger = [GRID_SIZE.MD, GRID_SIZE.LG, GRID_SIZE.XL];
} else if (includes(GRID_SIZE.MD, sizes)) {
bigger = [GRID_SIZE.LG, GRID_SIZE.XL];
} else if (includes(GRID_SIZE.LG, sizes)) {
bigger = [GRID_SIZE.XL];
} else {
return false;
}
@ -31,12 +33,23 @@ const isBiggerDefined = (cols: Partial<Record<GRID_SIZE_ENUM, number>> | number)
return true;
};
export const Grid = (props: IGridProps): React.ReactElement => {
const { cols = 1, space = 4, children } = props;
export const Grid: React.FC<IGridProps> = ({
cols = 1,
space = 4,
className,
children,
vertical,
}): React.ReactElement => {
return (
<div
className={cx(
'grid',
className,
{
'grid-items-start': vertical === VERTICAL_ALIGN.TOP,
'grid-items-center': vertical === VERTICAL_ALIGN.CENTER,
'grid-items-end': vertical === VERTICAL_ALIGN.BOTTOM,
},
ifElse(
is(Number),
() => `grid-cols-${cols}`,
@ -64,7 +77,7 @@ Grid.displayName = 'Grid';
type IGridColProps = {
children?: React.ReactElement | React.ReactElement[] | string;
colSpan?: Partial<Record<GRID_SIZE_ENUM, number>> | number;
colSpan?: Partial<Record<GRID_SIZE, number>> | number;
end?: number;
start?: number;
};
@ -87,7 +100,7 @@ export const GridCol = (props: IGridColProps): React.ReactElement => {
)
)(colSpan),
{
'sm-m:col-span-1': !has(GRID_SIZE_ENUM.SM)(colSpan) && !isNil(colSpan),
'sm-m:col-span-1': !has(GRID_SIZE.SM)(colSpan) && !isNil(colSpan),
}
)}
>

View file

@ -1,7 +1,13 @@
export enum GRID_SIZE_ENUM {
export enum GRID_SIZE {
'2XL' = '2xp',
LG = 'lg',
MD = 'md',
SM = 'sm',
XL = 'xl',
}
export enum VERTICAL_ALIGN {
BOTTOM = 'bottom',
CENTER = 'center',
TOP = 'top',
}

View file

@ -2,18 +2,18 @@ import { any, includes, is, keys } from 'ramda';
import { isNilOrEmpty } from '@treejs/utils';
import { GRID_SIZE_ENUM } from './types';
import { GRID_SIZE } from './types';
export const isBiggerDefined = (cols: Partial<Record<GRID_SIZE_ENUM, number>> | number): boolean => {
export const isBiggerDefined = (cols: Partial<Record<GRID_SIZE, number>> | number): boolean => {
if (!isNilOrEmpty(cols) && !is(Number, cols)) {
const sizes = keys(cols);
let bigger = [];
if (includes(GRID_SIZE_ENUM.SM, sizes)) {
bigger = [GRID_SIZE_ENUM.MD, GRID_SIZE_ENUM.LG, GRID_SIZE_ENUM.XL];
} else if (includes(GRID_SIZE_ENUM.MD, sizes)) {
bigger = [GRID_SIZE_ENUM.LG, GRID_SIZE_ENUM.XL];
} else if (includes(GRID_SIZE_ENUM.LG, sizes)) {
bigger = [GRID_SIZE_ENUM.XL];
if (includes(GRID_SIZE.SM, sizes)) {
bigger = [GRID_SIZE.MD, GRID_SIZE.LG, GRID_SIZE.XL];
} else if (includes(GRID_SIZE.MD, sizes)) {
bigger = [GRID_SIZE.LG, GRID_SIZE.XL];
} else if (includes(GRID_SIZE.LG, sizes)) {
bigger = [GRID_SIZE.XL];
} else {
return false;
}

View file

@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { find, propEq, propOr } from 'ramda';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useDispatch } from 'react-redux';
import { ModalId } from '@treejs/components/Modal/types';
@ -87,7 +86,6 @@ const SelectBox: React.FC<ISelectBoxFieldProps> = (props) => {
disabled={disabled}
name={name}
title={title}
rightIcon={<FontAwesomeIcon icon="caret-square-down" />}
onClick={handleOpenModal}
onFocus={handleOpenModal}
onBlur={handleBlur}

View file

@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import cx from 'classnames';
import { toPattern } from 'vanilla-masker';
@ -9,11 +10,11 @@ import { isNilOrEmpty } from '@treejs/utils';
export type ITextFieldProps<V = string> = {
autoFocus?: boolean;
clearable?: boolean;
defaultValue?: string;
disabled?: boolean;
mask?: string;
readonly?: boolean;
rightIcon?: React.ReactElement;
status?: STATUS;
type?: 'text' | 'password';
value?: string;
@ -30,11 +31,11 @@ const TextField: React.FC<ITextFieldProps> = (props) => {
name,
title,
readonly,
rightIcon,
status,
autoFocus,
defaultValue,
mask,
clearable = true,
} = props;
const inputRef = React.createRef<HTMLInputElement>();
@ -61,10 +62,6 @@ const TextField: React.FC<ITextFieldProps> = (props) => {
setValue(applyMask(props.value));
}, [props.value]);
const setFocus = (): void => {
inputRef.current.focus();
};
const handleClick = (e: React.MouseEvent<HTMLInputElement>): void => {
if (onClick) {
onClick(e.currentTarget.value, e);
@ -83,6 +80,13 @@ const TextField: React.FC<ITextFieldProps> = (props) => {
}
};
const handleClearValue = (e: React.MouseEvent<HTMLInputElement>): void => {
setValue(null);
if (onChange) {
onChange(null, e);
}
};
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.currentTarget.value;
@ -122,9 +126,11 @@ const TextField: React.FC<ITextFieldProps> = (props) => {
onChange={handleChange}
autoFocus={autoFocus}
/>
<div className="field-icon" onClick={setFocus}>
{rightIcon}
</div>
{!disabled && clearable && (
<div className="field-icon field-icon--clear" onClick={handleClearValue}>
<FontAwesomeIcon icon="backspace" />
</div>
)}
</div>
);
};

View file

@ -0,0 +1,119 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { format, setHours, setMinutes } from 'date-fns';
import { VERTICAL_ALIGN } from '@treejs/components/Grid/types';
import TextField from '@treejs/components/TextField';
import { isNilOrEmpty } from '@treejs/utils';
import Button from '../../Button';
import { Grid, GridCol } from '../../Grid';
type IProps = {
onChange: (value: Date) => void;
value: Date;
};
const ButtonField: React.FC<{ name: string; onChange: (a: string) => void; title: string; value: string }> = ({
title,
name,
onChange,
value,
}) => {
const handleChange = useCallback(
(newValue): any => {
onChange(newValue);
},
[onChange]
);
const increase = useCallback((): any => {
const newValue = (Number(value) + 1).toString();
handleChange(newValue);
}, [value, handleChange]);
const decrease = useCallback((): any => {
const newValue = (Number(value) - 1).toString();
handleChange(newValue);
}, [value, handleChange]);
return (
<Grid cols={4} vertical={VERTICAL_ALIGN.BOTTOM}>
<GridCol colSpan={1}>
<Button label={<FontAwesomeIcon icon="minus" />} onClick={decrease} />
</GridCol>
<GridCol colSpan={2}>
<TextField name={name} title={title} clearable={false} value={value} onChange={handleChange} />
</GridCol>
<GridCol colSpan={1}>
<Button label={<FontAwesomeIcon icon="plus" />} onClick={increase} />
</GridCol>
</Grid>
);
};
const TimePickerModal: React.FC<IProps> = (props) => {
const { onChange } = props;
const [value, setValue] = useState(props.value);
useEffect(() => {
setValue(props.value);
}, [props.value]);
const dateTime = useMemo(() => {
if (!isNilOrEmpty(value)) {
return new Date(value);
}
return new Date();
}, [value]);
const handleChangeHours = useCallback(
(hour) => {
if (isNilOrEmpty(hour)) {
hour = 0;
} else if (Number(hour) > 24 || Number(hour) < 0) {
hour = 23;
}
const newHour = setHours(value, hour);
setValue(newHour);
onChange(newHour);
},
[value, onChange]
);
const handleChangeMinutes = useCallback(
(minute) => {
if (isNilOrEmpty(minute)) {
minute = 0;
} else if (Number(minute) > 60 || Number(minute) < 0) {
minute = 59;
}
const newMinute = setMinutes(value, minute);
setValue(newMinute);
onChange(newMinute);
},
[value, onChange]
);
return (
<>
<Grid cols={1}>
<GridCol>
<ButtonField name="hour" title="Hour" value={format(dateTime, 'H')} onChange={handleChangeHours} />
</GridCol>
<GridCol>
<ButtonField
name="minute"
title="Minute"
value={format(dateTime, 'm')}
onChange={handleChangeMinutes}
/>
</GridCol>
</Grid>
</>
);
};
export default TimePickerModal;

View file

@ -0,0 +1,63 @@
import React, { useCallback, useEffect, useState } from 'react';
import format from 'date-fns/format';
import { useDispatch } from 'react-redux';
import { STATUS } from '@treejs/types/common';
import { IHTMLElement } from '@treejs/types/form';
import { isNilOrEmpty } from '@treejs/utils';
import { openModal } from '../Modal/actions';
import { addModalComponent } from '../Modal/utils';
import TextField from '../TextField';
import TimePickerModal from './components/TimePickerModal';
export type ITimePickerFieldProps<V = string> = {
status?: STATUS;
value?: string;
} & IHTMLElement<V>;
const TimePicker: React.FC<ITimePickerFieldProps> = (props) => {
const { onBlur, onChange, title, status, name } = props;
const dispatch = useDispatch();
const [value, setValue] = useState(null);
useEffect(() => {
setValue(props.value);
}, [props.value]);
const handleBlur = useCallback((newValue, e): void => {
if (onBlur) {
onBlur(newValue, e);
}
}, []);
const handleTimeClick = useCallback((newValue): void => {
setValue(newValue);
if (onChange) {
onChange(newValue, null);
}
}, []);
const handleOpenModal = useCallback((): void => {
addModalComponent(name, <TimePickerModal value={value} onChange={handleTimeClick} />);
dispatch(openModal({ id: name, title, width: 300 }));
}, [name, value, title]);
return (
<TextField
name={name}
title={title}
onClick={handleOpenModal}
onFocus={handleOpenModal}
onBlur={handleBlur}
value={!isNilOrEmpty(value) ? format(value, 'H:mm') : null}
status={status}
readonly
/>
);
};
export default TimePicker;

View file

@ -8,7 +8,7 @@ import { isNilOrEmpty } from '@treejs/utils';
type IProps = IDatePickerProps<string>;
const DatePicker = (props: IProps): React.ReactElement => {
const DatePickerField = (props: IProps): React.ReactElement => {
const { name, title, value } = props;
const { control, errors } = useFormContext();
@ -38,4 +38,4 @@ const DatePicker = (props: IProps): React.ReactElement => {
);
};
export default DatePicker;
export default DatePickerField;

View file

@ -27,7 +27,7 @@ const button = (theme) => ({
},
'&--error': {
color: theme('colors.black'),
backgroundColor: theme('colors.error'),
backgroundColor: theme('colors.error.DEFAULT'),
},
'&:active': {

View file

@ -16,7 +16,7 @@ const field = (theme) => ({
fontWeight: 300,
borderRadius: '2px',
'&--error': {
borderLeft: `2px solid ${theme('colors.error')}`,
borderLeft: `2px solid ${theme('colors.error.DEFAULT')}`,
},
'&--info': {
borderLeft: `2px solid ${theme('colors.info')}`,
@ -37,17 +37,22 @@ const field = (theme) => ({
border: 'none',
},
},
'&read-only': {
'&:read-only': {
cursor: 'auto',
},
},
'.field-icon': {
position: 'absolute',
top: '1.75rem',
right: '1rem',
top: '1.27rem',
right: '0',
padding: '0.51rem',
color: theme('colors.primary.DEFAULT'),
fontSize: '1.125rem',
lineHeight: '1.75rem',
lineHeight: '1.25rem',
'&--clear': {
borderRadius: '0 2px 2px 0',
color: theme('colors.error.dark'),
},
'input:disabled &': {
color: theme('colors.gray.500'),
'& svg': {
@ -62,7 +67,7 @@ const field = (theme) => ({
color: theme('colors.gray.500'),
},
'.field-validationMessage': {
color: theme('colors.error'),
color: theme('colors.error.DEFAULT'),
textAlign: 'left',
marginBottom: '0.5rem',
fontSize: '0.7rem',

View file

@ -0,0 +1,19 @@
const button = () => ({
'.grid': {
'&-items-start': {
alignItems: 'first baseline',
},
'&-items-center': {
alignItems: 'center',
},
'&-items-end': {
alignItems: 'last baseline',
},
},
});
module.exports = ({ addComponents, theme, config }) => {
addComponents(button(theme, config));
};
module.exports.button = button;

View file

@ -29,7 +29,7 @@ const message = (theme) => ({
},
'&--error': {
color: theme('colors.black'),
backgroundColor: theme('colors.error'),
backgroundColor: theme('colors.error.DEFAULT'),
},
},
});

View file

@ -24,6 +24,7 @@
--color-success: theme('colors.green.300');
--color-warning: theme('colors.yellow.300');
--color-error: theme('colors.red.300');
--color-error--dark: theme('colors.red.400');
.purple {
--color-primary: theme('colors.purple.400');

View file

@ -23,6 +23,7 @@ module.exports = {
plugin(require('./components/message')),
plugin(require('./components/skeleton')),
plugin(require('./components/table')),
plugin(require('./components/grid')),
],
corePlugins: {
float: false,

View file

@ -27,7 +27,10 @@ module.exports = {
info: 'var(--color-info)',
success: 'var(--color-success)',
warning: 'var(--color-warning)',
error: 'var(--color-error)',
error: {
DEFAULT: 'var(--color-error)',
dark: 'var(--color-error--dark)',
},
},
screens: {
'lg-m': { min: '768px', max: '1024px' },

View file

@ -52,7 +52,7 @@ export const Template = (args) => (
<Preview>
<Story
name="Without NTH"
name="Without -nth"
args={{
nthStyle: false,
}}

View file

@ -0,0 +1,79 @@
import { action } from '@storybook/addon-actions';
import { ArgsTable, Meta, Preview, Story } from '@storybook/addon-docs';
import Modals from '@treejs/components/Modal/components/Modals';
import TimePicker from '@treejs/components/TimePicker';
import StoreProvider from './Modal/redux';
<Meta
title={'Components/TimePicker'}
component={TimePicker}
argTypes={{
modalProps: {
control: {
disable: true,
},
table: {
disable: true,
},
},
}}
/>
# TimePicker
`import TimePicker from '@treejs/components/TimePicker';`
### Requirements
1. Wrapped with redux store provider.
```tsx dark
import { ROOT_REDUCER_NAME } from '@treejs/constants/redux';
import { MODAL_REDUCER_NAME } from '@treejs/components/Modal/constants';
import modalReducer from '@treejs/components/Modal/reducer';
export const mainReducer = combineReducers({
[ROOT_REDUCER_NAME]: combineReducers({
[MODAL_REDUCER_NAME]: modalReducer,
}),
});
```
2. Somewhere in redux store provider must be used `Modals` component
from `@treejs/components/Modal/components/Modals`
<ArgsTable />
export const Template = (args) => {
const { modalProps, ...select } = args;
return (
<StoreProvider>
<TimePicker onChange={action('clicked')} {...select} />
<Modals {...modalProps} />
</StoreProvider>
);
};
## Default
<Preview>
<Story
name="Default"
height="400px"
args={{
name: 'datePicker',
title: 'Choose time',
value: new Date(new Date().setHours(20, 0)),
}}
argTypes={{
value: {
control: { type: 'Date' },
},
}}
>
{Template.bind({})}
</Story>
</Preview>