DEV;Add TimePicker component
This commit is contained in:
parent
97c5141773
commit
cab561c77a
20 changed files with 372 additions and 91 deletions
|
@ -31,6 +31,10 @@ import {
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
faUserCircle,
|
faUserCircle,
|
||||||
faUserTie,
|
faUserTie,
|
||||||
|
faClock,
|
||||||
|
faBackspace,
|
||||||
|
faMinus,
|
||||||
|
faPlus,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -64,5 +68,10 @@ library.add(
|
||||||
faArrowAltCircleRight,
|
faArrowAltCircleRight,
|
||||||
faUserCircle,
|
faUserCircle,
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
faFile
|
faFile,
|
||||||
|
faClock,
|
||||||
|
faTimes,
|
||||||
|
faBackspace,
|
||||||
|
faMinus,
|
||||||
|
faPlus
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { format as setFormat } from 'date-fns';
|
import { format as setFormat } from 'date-fns';
|
||||||
|
|
||||||
import { STATUS } from '@treejs/types/common';
|
|
||||||
|
|
||||||
import Button from '../../Button';
|
|
||||||
import Calendar from '../../Calendar/components/Calendar';
|
import Calendar from '../../Calendar/components/Calendar';
|
||||||
import { Grid, GridCol } from '../../Grid';
|
|
||||||
import { ModalId } from '../../Modal/types';
|
import { ModalId } from '../../Modal/types';
|
||||||
|
|
||||||
type IProps = {
|
type IProps = {
|
||||||
|
@ -15,12 +10,11 @@ type IProps = {
|
||||||
format?: ModalId;
|
format?: ModalId;
|
||||||
name: ModalId;
|
name: ModalId;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onRemove: () => void;
|
|
||||||
value: Date;
|
value: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DatePickerModal: React.FC<IProps> = (props) => {
|
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(
|
const setActiveOption = useCallback(
|
||||||
(newValue: Date) => {
|
(newValue: Date) => {
|
||||||
|
@ -32,31 +26,11 @@ const DatePickerModal: React.FC<IProps> = (props) => {
|
||||||
[onChange, closeModal, name, format]
|
[onChange, closeModal, name, format]
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeActiveOption = useCallback((): void => {
|
|
||||||
if (onRemove) {
|
|
||||||
onRemove();
|
|
||||||
}
|
|
||||||
closeModal(name);
|
|
||||||
}, [onRemove, closeModal, name]);
|
|
||||||
|
|
||||||
const date = useMemo(() => {
|
const date = useMemo(() => {
|
||||||
return new Date(value);
|
return new Date(value);
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return <Calendar value={date} onChange={setActiveOption} hover />;
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DatePickerModal;
|
export default DatePickerModal;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import { ModalId } from '@treejs/components/Modal/types';
|
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 => {
|
const handleOpenModal = useCallback((): void => {
|
||||||
addModalComponent(
|
addModalComponent(
|
||||||
name,
|
name,
|
||||||
<DatePickerModal
|
<DatePickerModal name={name} value={value} closeModal={handleCloseModal} onChange={handleDateClick} />
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
closeModal={handleCloseModal}
|
|
||||||
onChange={handleDateClick}
|
|
||||||
onRemove={handleDateRemove}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
dispatch(openModal({ id: name, title, width: 300 }));
|
dispatch(openModal({ id: name, title, width: 300 }));
|
||||||
}, [name, value, title]);
|
}, [name, value, title]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={name}>
|
<div>
|
||||||
<TextField
|
<TextField
|
||||||
name={name}
|
name={name}
|
||||||
title={title}
|
title={title}
|
||||||
rightIcon={<FontAwesomeIcon icon="calendar-alt" />}
|
|
||||||
onClick={handleOpenModal}
|
onClick={handleOpenModal}
|
||||||
onFocus={handleOpenModal}
|
onFocus={handleOpenModal}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
|
|
|
@ -5,24 +5,26 @@ import cx from 'classnames';
|
||||||
|
|
||||||
import { isNilOrEmpty } from '@treejs/utils';
|
import { isNilOrEmpty } from '@treejs/utils';
|
||||||
|
|
||||||
import { GRID_SIZE_ENUM } from './types';
|
import { GRID_SIZE, VERTICAL_ALIGN } from './types';
|
||||||
|
|
||||||
type IGridProps = {
|
type IGridProps = {
|
||||||
children?: React.ReactElement | React.ReactElement[];
|
children?: React.ReactElement | React.ReactElement[];
|
||||||
cols?: Partial<Record<GRID_SIZE_ENUM, number>> | number;
|
className?: string;
|
||||||
|
cols?: Partial<Record<GRID_SIZE, number>> | number;
|
||||||
space?: 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)) {
|
if (!isNilOrEmpty(cols) && !is(Number, cols)) {
|
||||||
const sizes = keys(cols);
|
const sizes = keys(cols);
|
||||||
let bigger = [];
|
let bigger = [];
|
||||||
if (includes(GRID_SIZE_ENUM.SM, sizes)) {
|
if (includes(GRID_SIZE.SM, sizes)) {
|
||||||
bigger = [GRID_SIZE_ENUM.MD, GRID_SIZE_ENUM.LG, GRID_SIZE_ENUM.XL];
|
bigger = [GRID_SIZE.MD, GRID_SIZE.LG, GRID_SIZE.XL];
|
||||||
} else if (includes(GRID_SIZE_ENUM.MD, sizes)) {
|
} else if (includes(GRID_SIZE.MD, sizes)) {
|
||||||
bigger = [GRID_SIZE_ENUM.LG, GRID_SIZE_ENUM.XL];
|
bigger = [GRID_SIZE.LG, GRID_SIZE.XL];
|
||||||
} else if (includes(GRID_SIZE_ENUM.LG, sizes)) {
|
} else if (includes(GRID_SIZE.LG, sizes)) {
|
||||||
bigger = [GRID_SIZE_ENUM.XL];
|
bigger = [GRID_SIZE.XL];
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -31,12 +33,23 @@ const isBiggerDefined = (cols: Partial<Record<GRID_SIZE_ENUM, number>> | number)
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Grid = (props: IGridProps): React.ReactElement => {
|
export const Grid: React.FC<IGridProps> = ({
|
||||||
const { cols = 1, space = 4, children } = props;
|
cols = 1,
|
||||||
|
space = 4,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
vertical,
|
||||||
|
}): React.ReactElement => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
'grid',
|
'grid',
|
||||||
|
className,
|
||||||
|
{
|
||||||
|
'grid-items-start': vertical === VERTICAL_ALIGN.TOP,
|
||||||
|
'grid-items-center': vertical === VERTICAL_ALIGN.CENTER,
|
||||||
|
'grid-items-end': vertical === VERTICAL_ALIGN.BOTTOM,
|
||||||
|
},
|
||||||
ifElse(
|
ifElse(
|
||||||
is(Number),
|
is(Number),
|
||||||
() => `grid-cols-${cols}`,
|
() => `grid-cols-${cols}`,
|
||||||
|
@ -64,7 +77,7 @@ Grid.displayName = 'Grid';
|
||||||
|
|
||||||
type IGridColProps = {
|
type IGridColProps = {
|
||||||
children?: React.ReactElement | React.ReactElement[] | string;
|
children?: React.ReactElement | React.ReactElement[] | string;
|
||||||
colSpan?: Partial<Record<GRID_SIZE_ENUM, number>> | number;
|
colSpan?: Partial<Record<GRID_SIZE, number>> | number;
|
||||||
end?: number;
|
end?: number;
|
||||||
start?: number;
|
start?: number;
|
||||||
};
|
};
|
||||||
|
@ -87,7 +100,7 @@ export const GridCol = (props: IGridColProps): React.ReactElement => {
|
||||||
)
|
)
|
||||||
)(colSpan),
|
)(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),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
export enum GRID_SIZE_ENUM {
|
export enum GRID_SIZE {
|
||||||
'2XL' = '2xp',
|
'2XL' = '2xp',
|
||||||
LG = 'lg',
|
LG = 'lg',
|
||||||
MD = 'md',
|
MD = 'md',
|
||||||
SM = 'sm',
|
SM = 'sm',
|
||||||
XL = 'xl',
|
XL = 'xl',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum VERTICAL_ALIGN {
|
||||||
|
BOTTOM = 'bottom',
|
||||||
|
CENTER = 'center',
|
||||||
|
TOP = 'top',
|
||||||
|
}
|
||||||
|
|
|
@ -2,18 +2,18 @@ import { any, includes, is, keys } from 'ramda';
|
||||||
|
|
||||||
import { isNilOrEmpty } from '@treejs/utils';
|
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)) {
|
if (!isNilOrEmpty(cols) && !is(Number, cols)) {
|
||||||
const sizes = keys(cols);
|
const sizes = keys(cols);
|
||||||
let bigger = [];
|
let bigger = [];
|
||||||
if (includes(GRID_SIZE_ENUM.SM, sizes)) {
|
if (includes(GRID_SIZE.SM, sizes)) {
|
||||||
bigger = [GRID_SIZE_ENUM.MD, GRID_SIZE_ENUM.LG, GRID_SIZE_ENUM.XL];
|
bigger = [GRID_SIZE.MD, GRID_SIZE.LG, GRID_SIZE.XL];
|
||||||
} else if (includes(GRID_SIZE_ENUM.MD, sizes)) {
|
} else if (includes(GRID_SIZE.MD, sizes)) {
|
||||||
bigger = [GRID_SIZE_ENUM.LG, GRID_SIZE_ENUM.XL];
|
bigger = [GRID_SIZE.LG, GRID_SIZE.XL];
|
||||||
} else if (includes(GRID_SIZE_ENUM.LG, sizes)) {
|
} else if (includes(GRID_SIZE.LG, sizes)) {
|
||||||
bigger = [GRID_SIZE_ENUM.XL];
|
bigger = [GRID_SIZE.XL];
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { find, propEq, propOr } from 'ramda';
|
import { find, propEq, propOr } from 'ramda';
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import { ModalId } from '@treejs/components/Modal/types';
|
import { ModalId } from '@treejs/components/Modal/types';
|
||||||
|
@ -87,7 +86,6 @@ const SelectBox: React.FC<ISelectBoxFieldProps> = (props) => {
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
name={name}
|
name={name}
|
||||||
title={title}
|
title={title}
|
||||||
rightIcon={<FontAwesomeIcon icon="caret-square-down" />}
|
|
||||||
onClick={handleOpenModal}
|
onClick={handleOpenModal}
|
||||||
onFocus={handleOpenModal}
|
onFocus={handleOpenModal}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { toPattern } from 'vanilla-masker';
|
import { toPattern } from 'vanilla-masker';
|
||||||
|
|
||||||
|
@ -9,11 +10,11 @@ import { isNilOrEmpty } from '@treejs/utils';
|
||||||
|
|
||||||
export type ITextFieldProps<V = string> = {
|
export type ITextFieldProps<V = string> = {
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
|
clearable?: boolean;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
mask?: string;
|
mask?: string;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
rightIcon?: React.ReactElement;
|
|
||||||
status?: STATUS;
|
status?: STATUS;
|
||||||
type?: 'text' | 'password';
|
type?: 'text' | 'password';
|
||||||
value?: string;
|
value?: string;
|
||||||
|
@ -30,11 +31,11 @@ const TextField: React.FC<ITextFieldProps> = (props) => {
|
||||||
name,
|
name,
|
||||||
title,
|
title,
|
||||||
readonly,
|
readonly,
|
||||||
rightIcon,
|
|
||||||
status,
|
status,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
mask,
|
mask,
|
||||||
|
clearable = true,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const inputRef = React.createRef<HTMLInputElement>();
|
const inputRef = React.createRef<HTMLInputElement>();
|
||||||
|
@ -61,10 +62,6 @@ const TextField: React.FC<ITextFieldProps> = (props) => {
|
||||||
setValue(applyMask(props.value));
|
setValue(applyMask(props.value));
|
||||||
}, [props.value]);
|
}, [props.value]);
|
||||||
|
|
||||||
const setFocus = (): void => {
|
|
||||||
inputRef.current.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLInputElement>): void => {
|
const handleClick = (e: React.MouseEvent<HTMLInputElement>): void => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick(e.currentTarget.value, e);
|
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(
|
const handleChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
const value = e.currentTarget.value;
|
const value = e.currentTarget.value;
|
||||||
|
@ -122,9 +126,11 @@ const TextField: React.FC<ITextFieldProps> = (props) => {
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
/>
|
/>
|
||||||
<div className="field-icon" onClick={setFocus}>
|
{!disabled && clearable && (
|
||||||
{rightIcon}
|
<div className="field-icon field-icon--clear" onClick={handleClearValue}>
|
||||||
|
<FontAwesomeIcon icon="backspace" />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
63
packages/components/src/TimePicker/index.tsx
Normal file
63
packages/components/src/TimePicker/index.tsx
Normal 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;
|
|
@ -8,7 +8,7 @@ import { isNilOrEmpty } from '@treejs/utils';
|
||||||
|
|
||||||
type IProps = IDatePickerProps<string>;
|
type IProps = IDatePickerProps<string>;
|
||||||
|
|
||||||
const DatePicker = (props: IProps): React.ReactElement => {
|
const DatePickerField = (props: IProps): React.ReactElement => {
|
||||||
const { name, title, value } = props;
|
const { name, title, value } = props;
|
||||||
|
|
||||||
const { control, errors } = useFormContext();
|
const { control, errors } = useFormContext();
|
||||||
|
@ -38,4 +38,4 @@ const DatePicker = (props: IProps): React.ReactElement => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DatePicker;
|
export default DatePickerField;
|
||||||
|
|
|
@ -27,7 +27,7 @@ const button = (theme) => ({
|
||||||
},
|
},
|
||||||
'&--error': {
|
'&--error': {
|
||||||
color: theme('colors.black'),
|
color: theme('colors.black'),
|
||||||
backgroundColor: theme('colors.error'),
|
backgroundColor: theme('colors.error.DEFAULT'),
|
||||||
},
|
},
|
||||||
|
|
||||||
'&:active': {
|
'&:active': {
|
||||||
|
|
|
@ -16,7 +16,7 @@ const field = (theme) => ({
|
||||||
fontWeight: 300,
|
fontWeight: 300,
|
||||||
borderRadius: '2px',
|
borderRadius: '2px',
|
||||||
'&--error': {
|
'&--error': {
|
||||||
borderLeft: `2px solid ${theme('colors.error')}`,
|
borderLeft: `2px solid ${theme('colors.error.DEFAULT')}`,
|
||||||
},
|
},
|
||||||
'&--info': {
|
'&--info': {
|
||||||
borderLeft: `2px solid ${theme('colors.info')}`,
|
borderLeft: `2px solid ${theme('colors.info')}`,
|
||||||
|
@ -37,17 +37,22 @@ const field = (theme) => ({
|
||||||
border: 'none',
|
border: 'none',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'&read-only': {
|
'&:read-only': {
|
||||||
cursor: 'auto',
|
cursor: 'auto',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'.field-icon': {
|
'.field-icon': {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '1.75rem',
|
top: '1.27rem',
|
||||||
right: '1rem',
|
right: '0',
|
||||||
|
padding: '0.51rem',
|
||||||
color: theme('colors.primary.DEFAULT'),
|
color: theme('colors.primary.DEFAULT'),
|
||||||
fontSize: '1.125rem',
|
fontSize: '1.125rem',
|
||||||
lineHeight: '1.75rem',
|
lineHeight: '1.25rem',
|
||||||
|
'&--clear': {
|
||||||
|
borderRadius: '0 2px 2px 0',
|
||||||
|
color: theme('colors.error.dark'),
|
||||||
|
},
|
||||||
'input:disabled &': {
|
'input:disabled &': {
|
||||||
color: theme('colors.gray.500'),
|
color: theme('colors.gray.500'),
|
||||||
'& svg': {
|
'& svg': {
|
||||||
|
@ -62,7 +67,7 @@ const field = (theme) => ({
|
||||||
color: theme('colors.gray.500'),
|
color: theme('colors.gray.500'),
|
||||||
},
|
},
|
||||||
'.field-validationMessage': {
|
'.field-validationMessage': {
|
||||||
color: theme('colors.error'),
|
color: theme('colors.error.DEFAULT'),
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
marginBottom: '0.5rem',
|
marginBottom: '0.5rem',
|
||||||
fontSize: '0.7rem',
|
fontSize: '0.7rem',
|
||||||
|
|
19
packages/styles/src/components/grid.js
Normal file
19
packages/styles/src/components/grid.js
Normal 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;
|
|
@ -29,7 +29,7 @@ const message = (theme) => ({
|
||||||
},
|
},
|
||||||
'&--error': {
|
'&--error': {
|
||||||
color: theme('colors.black'),
|
color: theme('colors.black'),
|
||||||
backgroundColor: theme('colors.error'),
|
backgroundColor: theme('colors.error.DEFAULT'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
--color-success: theme('colors.green.300');
|
--color-success: theme('colors.green.300');
|
||||||
--color-warning: theme('colors.yellow.300');
|
--color-warning: theme('colors.yellow.300');
|
||||||
--color-error: theme('colors.red.300');
|
--color-error: theme('colors.red.300');
|
||||||
|
--color-error--dark: theme('colors.red.400');
|
||||||
|
|
||||||
.purple {
|
.purple {
|
||||||
--color-primary: theme('colors.purple.400');
|
--color-primary: theme('colors.purple.400');
|
||||||
|
|
|
@ -23,6 +23,7 @@ module.exports = {
|
||||||
plugin(require('./components/message')),
|
plugin(require('./components/message')),
|
||||||
plugin(require('./components/skeleton')),
|
plugin(require('./components/skeleton')),
|
||||||
plugin(require('./components/table')),
|
plugin(require('./components/table')),
|
||||||
|
plugin(require('./components/grid')),
|
||||||
],
|
],
|
||||||
corePlugins: {
|
corePlugins: {
|
||||||
float: false,
|
float: false,
|
||||||
|
|
|
@ -27,7 +27,10 @@ module.exports = {
|
||||||
info: 'var(--color-info)',
|
info: 'var(--color-info)',
|
||||||
success: 'var(--color-success)',
|
success: 'var(--color-success)',
|
||||||
warning: 'var(--color-warning)',
|
warning: 'var(--color-warning)',
|
||||||
error: 'var(--color-error)',
|
error: {
|
||||||
|
DEFAULT: 'var(--color-error)',
|
||||||
|
dark: 'var(--color-error--dark)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
'lg-m': { min: '768px', max: '1024px' },
|
'lg-m': { min: '768px', max: '1024px' },
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const Template = (args) => (
|
||||||
|
|
||||||
<Preview>
|
<Preview>
|
||||||
<Story
|
<Story
|
||||||
name="Without NTH"
|
name="Without -nth"
|
||||||
args={{
|
args={{
|
||||||
nthStyle: false,
|
nthStyle: false,
|
||||||
}}
|
}}
|
||||||
|
|
79
stories/components/TimePicker.stories.mdx
Normal file
79
stories/components/TimePicker.stories.mdx
Normal 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>
|
Loading…
Add table
Reference in a new issue