From c2e56d6f2bf86c2207401bead41767d2bdc77711 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Wed, 22 May 2024 16:41:36 +0300 Subject: [PATCH 01/10] DateInput component --- src/assets/icons/calendar.svg | 5 ++ src/form/Calendar/Calendar.tsx | 39 ++++++--- src/form/DateInput/DateInput.story.tsx | 32 +++++++ src/form/DateInput/DateInput.tsx | 117 +++++++++++++++++++++++++ src/form/Input/Input.tsx | 3 +- src/form/Input/InputTheme.ts | 2 +- 6 files changed, 182 insertions(+), 16 deletions(-) create mode 100644 src/assets/icons/calendar.svg create mode 100644 src/form/DateInput/DateInput.story.tsx create mode 100644 src/form/DateInput/DateInput.tsx diff --git a/src/assets/icons/calendar.svg b/src/assets/icons/calendar.svg new file mode 100644 index 00000000..0d784657 --- /dev/null +++ b/src/assets/icons/calendar.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/form/Calendar/Calendar.tsx b/src/form/Calendar/Calendar.tsx index af0f5236..1f929a6d 100644 --- a/src/form/Calendar/Calendar.tsx +++ b/src/form/Calendar/Calendar.tsx @@ -27,7 +27,7 @@ import { Divider } from '@/layout/Divider'; export type CalendarViewType = 'days' | 'months' | 'years'; -export interface CalendarProps { +export type CalendarProps = { /** * The selected date(s) for the calendar. */ @@ -54,11 +54,6 @@ export interface CalendarProps { */ disabled?: boolean; - /** - * Whether the calendar is a range picker. - */ - isRange?: boolean; - /** * The text or icon to use for next. */ @@ -79,11 +74,6 @@ export interface CalendarProps { */ animated?: boolean; - /** - * A callback function that is called when the selected date(s) change. - */ - onChange?: (value: Date | [Date, Date]) => void; - /** * A callback function that is called when the calendar view changes. */ @@ -93,7 +83,30 @@ export interface CalendarProps { * Theme for the Calendar. */ theme?: CalendarTheme; -} +} & ( + | { + /** + * Whether the calendar is a range picker. + */ + isRange?: true; + + /** + * A callback function that is called when the selected date(s) change. + */ + onChange?: (value: [Date, Date]) => void; + } + | { + /** + * Whether the calendar is a range picker. + */ + isRange?: false; + + /** + * A callback function that is called when the selected date(s) change. + */ + onChange?: (value: Date) => void; + } +); export const Calendar: FC = ({ min, @@ -167,7 +180,7 @@ export const Calendar: FC = ({ const dateChangeHandler = useCallback( (date: Date) => { - if (!isRange) { + if (isRange === false || isRange === undefined) { onChange?.(date); setMonthValue(getMonth(date)); setYearValue(getYear(date)); diff --git a/src/form/DateInput/DateInput.story.tsx b/src/form/DateInput/DateInput.story.tsx new file mode 100644 index 00000000..5ff51f7d --- /dev/null +++ b/src/form/DateInput/DateInput.story.tsx @@ -0,0 +1,32 @@ +import { DateFormat } from '@/data'; +import { Stack } from '@/layout'; +import { useState } from 'react'; +import { DateInput } from './DateInput'; + +export default { + title: 'Components/Form/Date Input', + component: DateInput +}; + +export const Simple = () => { + const [date, setDate] = useState(new Date()); + + return ( + + + + + ); +}; + +export const Error = () => { + const [date, setDate] = useState(new Date()); + + return ; +}; + +export const Disabled = () => { + const [date, setDate] = useState(new Date()); + + return ; +}; diff --git a/src/form/DateInput/DateInput.tsx b/src/form/DateInput/DateInput.tsx new file mode 100644 index 00000000..64d5e186 --- /dev/null +++ b/src/form/DateInput/DateInput.tsx @@ -0,0 +1,117 @@ +import React, { + ChangeEvent, + FC, + useCallback, + useEffect, + useRef, + useState +} from 'react'; +import { format as formatDate, isValid, parse } from 'date-fns'; + +import { IconButton } from '@/elements'; +import { Menu } from '@/layers'; +import { Card } from '@/layout'; +import { Calendar, Input, InputProps, InputRef } from '@/form'; +import { Placement } from '@/utils'; + +import CalendarIcon from '@/assets/icons/calendar.svg?react'; + +export interface DateInputProps extends Omit { + /** + * The current date value. + * @type {Date} + */ + value: Date; + + /** + * The format in which the date should be displayed. + * @type {string} + */ + format?: string; + + /** + * Calendar placement type. + */ + placement?: Placement; + + /** + * Callback function to handle date changes. + * @param {Date} value - The new date value. + */ + onChange: (value: Date) => void; +} +export const DateInput: FC = ({ + disabled, + value, + format = 'MM/dd/yyyy', + placement = 'bottom-start', + onChange, + ...rest +}) => { + const [open, setOpen] = useState(false); + const ref = useRef(null); + const [inputValue, setInputValue] = useState(''); + + const changeHandler = useCallback( + (value: Date) => { + setOpen(false); + onChange(value); + }, + [onChange] + ); + + const inputChangeHandler = useCallback( + (event: ChangeEvent) => { + const dateStr = event.target.value; + + setInputValue(dateStr); + + const date = parse(dateStr, format, new Date()); + + if (isValid(date) && formatDate(date, format) === dateStr) { + onChange(date); + } + }, + [format, onChange] + ); + + useEffect(() => { + if (value) { + setInputValue(formatDate(value, format)); + } + }, [format, value]); + + return ( + <> + setOpen(true)} + > + + + } + placeholder={format.toUpperCase()} + {...rest} + value={inputValue} + onChange={inputChangeHandler} + /> + setOpen(false)} + reference={ref?.current?.containerRef} + placement={placement} + > + {() => ( + + + + )} + + + ); +}; diff --git a/src/form/Input/Input.tsx b/src/form/Input/Input.tsx index 830fce19..917f34a1 100644 --- a/src/form/Input/Input.tsx +++ b/src/form/Input/Input.tsx @@ -1,5 +1,4 @@ import React, { - FC, forwardRef, RefObject, useImperativeHandle, @@ -100,7 +99,7 @@ export interface InputRef { select?: () => void; } -export const Input: FC = forwardRef( +export const Input = forwardRef( ( { className, diff --git a/src/form/Input/InputTheme.ts b/src/form/Input/InputTheme.ts index c3c6880c..2032e599 100644 --- a/src/form/Input/InputTheme.ts +++ b/src/form/Input/InputTheme.ts @@ -24,7 +24,7 @@ const baseTheme: InputTheme = { input: 'flex-1 font-normal font-sans bg-transparent border-0 p-0 m-0 disabled:pointer-events-none outline-none px-0.5 disabled:cursor-not-allowed disabled:text-disabled', inline: 'bg-transparent border-0 outline-none', - disabled: '', + disabled: 'text-waterloo', fullWidth: 'w-full', error: 'border-error', sizes: { From 76504e56928e992d972247f1c83b225c50393967 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 23 May 2024 12:45:59 +0300 Subject: [PATCH 02/10] Fix type --- src/form/Calendar/Calendar.tsx | 3 ++- src/form/Calendar/CalendarRange.tsx | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/form/Calendar/Calendar.tsx b/src/form/Calendar/Calendar.tsx index 1f929a6d..26721734 100644 --- a/src/form/Calendar/Calendar.tsx +++ b/src/form/Calendar/Calendar.tsx @@ -180,7 +180,8 @@ export const Calendar: FC = ({ const dateChangeHandler = useCallback( (date: Date) => { - if (isRange === false || isRange === undefined) { + if (!isRange) { + // @ts-ignore onChange?.(date); setMonthValue(getMonth(date)); setYearValue(getYear(date)); diff --git a/src/form/Calendar/CalendarRange.tsx b/src/form/Calendar/CalendarRange.tsx index e6f1a7c5..f4a0d71d 100644 --- a/src/form/Calendar/CalendarRange.tsx +++ b/src/form/Calendar/CalendarRange.tsx @@ -52,6 +52,11 @@ export interface CalendarRangeProps * Theme for the CalendarRange. */ theme?: CalendarRangeTheme; + + /** + * A callback function that is called when the selected date(s) change. + */ + onChange?: (value: [Date, Date]) => void; } export const CalendarRange: FC = ({ From a1a997968e79108263c8a249e4685226fbf4bf6b Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Fri, 24 May 2024 14:36:18 +0300 Subject: [PATCH 03/10] Extend DateInput with isRange --- package-lock.json | 4 +- src/form/Calendar/Calendar.tsx | 34 +++------ src/form/DateInput/DateInput.story.tsx | 21 +++++- src/form/DateInput/DateInput.tsx | 95 ++++++++++++++++++-------- 4 files changed, 100 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1a7f31f..5673ccac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "reablocks", - "version": "7.7.3", + "version": "7.7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "reablocks", - "version": "7.7.3", + "version": "7.7.4", "license": "Apache-2.0", "dependencies": { "@marko19907/string-to-color": "^1.0.0", diff --git a/src/form/Calendar/Calendar.tsx b/src/form/Calendar/Calendar.tsx index 26721734..282b292b 100644 --- a/src/form/Calendar/Calendar.tsx +++ b/src/form/Calendar/Calendar.tsx @@ -54,6 +54,11 @@ export type CalendarProps = { */ disabled?: boolean; + /** + * Whether the calendar is a range picker. + */ + isRange?: boolean; + /** * The text or icon to use for next. */ @@ -83,30 +88,12 @@ export type CalendarProps = { * Theme for the Calendar. */ theme?: CalendarTheme; -} & ( - | { - /** - * Whether the calendar is a range picker. - */ - isRange?: true; - - /** - * A callback function that is called when the selected date(s) change. - */ - onChange?: (value: [Date, Date]) => void; - } - | { - /** - * Whether the calendar is a range picker. - */ - isRange?: false; - /** - * A callback function that is called when the selected date(s) change. - */ - onChange?: (value: Date) => void; - } -); + /** + * A callback function that is called when the selected date(s) change. + */ + onChange?: (value: Date | [Date, Date]) => void; +}; export const Calendar: FC = ({ min, @@ -181,7 +168,6 @@ export const Calendar: FC = ({ const dateChangeHandler = useCallback( (date: Date) => { if (!isRange) { - // @ts-ignore onChange?.(date); setMonthValue(getMonth(date)); setYearValue(getYear(date)); diff --git a/src/form/DateInput/DateInput.story.tsx b/src/form/DateInput/DateInput.story.tsx index 5ff51f7d..c4f3f618 100644 --- a/src/form/DateInput/DateInput.story.tsx +++ b/src/form/DateInput/DateInput.story.tsx @@ -13,7 +13,7 @@ export const Simple = () => { return ( - + ); @@ -30,3 +30,22 @@ export const Disabled = () => { return ; }; + +export const Range = () => { + const [date, setDate] = useState<[Date, Date]>([new Date(), new Date()]); + + return ( + + + -{' '} + + + setDate(value)} + /> + + ); +}; diff --git a/src/form/DateInput/DateInput.tsx b/src/form/DateInput/DateInput.tsx index 64d5e186..fa2fa49d 100644 --- a/src/form/DateInput/DateInput.tsx +++ b/src/form/DateInput/DateInput.tsx @@ -11,18 +11,12 @@ import { format as formatDate, isValid, parse } from 'date-fns'; import { IconButton } from '@/elements'; import { Menu } from '@/layers'; import { Card } from '@/layout'; -import { Calendar, Input, InputProps, InputRef } from '@/form'; import { Placement } from '@/utils'; +import { Calendar, Input, InputProps, InputRef } from '@/form'; import CalendarIcon from '@/assets/icons/calendar.svg?react'; -export interface DateInputProps extends Omit { - /** - * The current date value. - * @type {Date} - */ - value: Date; - +export type DateInputProps = Omit & { /** * The format in which the date should be displayed. * @type {string} @@ -33,18 +27,24 @@ export interface DateInputProps extends Omit { * Calendar placement type. */ placement?: Placement; - - /** - * Callback function to handle date changes. - * @param {Date} value - The new date value. - */ - onChange: (value: Date) => void; -} +} & ( + | { + isRange?: true; + value: [Date, Date]; + onChange: (value: [Date, Date]) => void; + } + | { + isRange?: false; + value: Date; + onChange: (value: Date) => void; + } + ); export const DateInput: FC = ({ disabled, value, format = 'MM/dd/yyyy', placement = 'bottom-start', + isRange, onChange, ...rest }) => { @@ -53,11 +53,20 @@ export const DateInput: FC = ({ const [inputValue, setInputValue] = useState(''); const changeHandler = useCallback( - (value: Date) => { - setOpen(false); - onChange(value); + (value: Date | [Date, Date]) => { + if (isRange) { + onChange(value as [Date, Date]); + + if (value[0] && value[1]) { + setOpen(false); + } + } else { + setOpen(false); + // @ts-expect-error because isRange optional + onChange(value); + } }, - [onChange] + [isRange, onChange] ); const inputChangeHandler = useCallback( @@ -66,20 +75,43 @@ export const DateInput: FC = ({ setInputValue(dateStr); - const date = parse(dateStr, format, new Date()); + if (isRange) { + const [startStr, endStr] = dateStr.split('-'); + const startDate = parse(startStr, format, new Date()); + const endDate = parse(endStr, format, new Date()); - if (isValid(date) && formatDate(date, format) === dateStr) { - onChange(date); + if ( + isValid(startDate) && + isValid(endDate) && + formatDate(startDate, format) === startStr && + formatDate(endDate, format) === endStr + ) { + onChange?.([startDate, endDate]); + } + } else { + const date = parse(dateStr, format, new Date()); + + if (isValid(date) && formatDate(date, format) === dateStr) { + // @ts-expect-error because isRange optional + onChange?.(date); + } } }, - [format, onChange] + [format, isRange, onChange] ); useEffect(() => { if (value) { - setInputValue(formatDate(value, format)); + if (isRange) { + const [start, end] = value; + setInputValue( + `${start ? formatDate(start, format) : ''}-${end ? formatDate(end, format) : ''}` + ); + } else if (!isRange) { + setInputValue(formatDate(value as Date, format)); + } } - }, [format, value]); + }, [format, isRange, value]); return ( <> @@ -95,7 +127,11 @@ export const DateInput: FC = ({ } - placeholder={format.toUpperCase()} + placeholder={ + isRange + ? `${format.toUpperCase()} - ${format.toUpperCase()}` + : format.toUpperCase() + } {...rest} value={inputValue} onChange={inputChangeHandler} @@ -108,7 +144,12 @@ export const DateInput: FC = ({ > {() => ( - + )} From 42da095a6580b409cabdaaf64f7043643e43a3a1 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Fri, 24 May 2024 14:52:46 +0300 Subject: [PATCH 04/10] Rollback some changes --- src/form/Calendar/Calendar.tsx | 14 +++++++------- src/form/Calendar/CalendarRange.tsx | 5 ----- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/form/Calendar/Calendar.tsx b/src/form/Calendar/Calendar.tsx index 282b292b..af0f5236 100644 --- a/src/form/Calendar/Calendar.tsx +++ b/src/form/Calendar/Calendar.tsx @@ -27,7 +27,7 @@ import { Divider } from '@/layout/Divider'; export type CalendarViewType = 'days' | 'months' | 'years'; -export type CalendarProps = { +export interface CalendarProps { /** * The selected date(s) for the calendar. */ @@ -79,6 +79,11 @@ export type CalendarProps = { */ animated?: boolean; + /** + * A callback function that is called when the selected date(s) change. + */ + onChange?: (value: Date | [Date, Date]) => void; + /** * A callback function that is called when the calendar view changes. */ @@ -88,12 +93,7 @@ export type CalendarProps = { * Theme for the Calendar. */ theme?: CalendarTheme; - - /** - * A callback function that is called when the selected date(s) change. - */ - onChange?: (value: Date | [Date, Date]) => void; -}; +} export const Calendar: FC = ({ min, diff --git a/src/form/Calendar/CalendarRange.tsx b/src/form/Calendar/CalendarRange.tsx index f4a0d71d..e6f1a7c5 100644 --- a/src/form/Calendar/CalendarRange.tsx +++ b/src/form/Calendar/CalendarRange.tsx @@ -52,11 +52,6 @@ export interface CalendarRangeProps * Theme for the CalendarRange. */ theme?: CalendarRangeTheme; - - /** - * A callback function that is called when the selected date(s) change. - */ - onChange?: (value: [Date, Date]) => void; } export const CalendarRange: FC = ({ From cd21462e0f3391828a516d03279e4b0e2285774d Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Fri, 24 May 2024 14:56:27 +0300 Subject: [PATCH 05/10] Update package-lock.json --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa3e074f..3ea24805 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "reablocks", - "version": "7.8.0", + "version": "7.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "reablocks", - "version": "7.8.0", + "version": "7.8.1", "license": "Apache-2.0", "dependencies": { "@marko19907/string-to-color": "^1.0.0", From 4ad796b7cfa63433dde1433f8757f5c572d26163 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Fri, 24 May 2024 15:02:24 +0300 Subject: [PATCH 06/10] Reduce padding --- src/form/DateInput/DateInput.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/form/DateInput/DateInput.tsx b/src/form/DateInput/DateInput.tsx index fa2fa49d..f73052a0 100644 --- a/src/form/DateInput/DateInput.tsx +++ b/src/form/DateInput/DateInput.tsx @@ -120,6 +120,7 @@ export const DateInput: FC = ({ disabled={disabled} endAdornment={ setOpen(true)} From fa45e387c76f01cac334db0ae06acc8457ef17d3 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Fri, 24 May 2024 15:27:28 +0300 Subject: [PATCH 07/10] Improve disable state --- src/form/DateInput/DateInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/form/DateInput/DateInput.tsx b/src/form/DateInput/DateInput.tsx index f73052a0..cc1758c0 100644 --- a/src/form/DateInput/DateInput.tsx +++ b/src/form/DateInput/DateInput.tsx @@ -121,7 +121,6 @@ export const DateInput: FC = ({ endAdornment={ setOpen(true)} > @@ -146,6 +145,7 @@ export const DateInput: FC = ({ {() => ( Date: Fri, 24 May 2024 15:35:50 +0300 Subject: [PATCH 08/10] Add ability to pass custom icon --- src/form/DateInput/DateInput.story.tsx | 25 +++++++++++++++++++++++++ src/form/DateInput/DateInput.tsx | 9 ++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/form/DateInput/DateInput.story.tsx b/src/form/DateInput/DateInput.story.tsx index c4f3f618..e257df29 100644 --- a/src/form/DateInput/DateInput.story.tsx +++ b/src/form/DateInput/DateInput.story.tsx @@ -31,6 +31,31 @@ export const Disabled = () => { return ; }; +export const CustomIcon = () => { + const [date, setDate] = useState(new Date()); + + return ( + + + + } + /> + ); +}; + export const Range = () => { const [date, setDate] = useState<[Date, Date]>([new Date(), new Date()]); diff --git a/src/form/DateInput/DateInput.tsx b/src/form/DateInput/DateInput.tsx index cc1758c0..9b47bfa5 100644 --- a/src/form/DateInput/DateInput.tsx +++ b/src/form/DateInput/DateInput.tsx @@ -1,6 +1,7 @@ import React, { ChangeEvent, FC, + ReactElement, useCallback, useEffect, useRef, @@ -27,6 +28,11 @@ export type DateInputProps = Omit & { * Calendar placement type. */ placement?: Placement; + + /** + * Icon to show in open calendar button. + */ + icon?: ReactElement; } & ( | { isRange?: true; @@ -45,6 +51,7 @@ export const DateInput: FC = ({ format = 'MM/dd/yyyy', placement = 'bottom-start', isRange, + icon = , onChange, ...rest }) => { @@ -124,7 +131,7 @@ export const DateInput: FC = ({ variant="text" onClick={() => setOpen(true)} > - + {icon} } placeholder={ From a671d99ed78fbcbc64cf84b6732ab915f4b6bc46 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Fri, 24 May 2024 15:39:24 +0300 Subject: [PATCH 09/10] Update imports --- src/form/DateInput/DateInput.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/form/DateInput/DateInput.tsx b/src/form/DateInput/DateInput.tsx index 9b47bfa5..beb8c593 100644 --- a/src/form/DateInput/DateInput.tsx +++ b/src/form/DateInput/DateInput.tsx @@ -9,10 +9,10 @@ import React, { } from 'react'; import { format as formatDate, isValid, parse } from 'date-fns'; -import { IconButton } from '@/elements'; -import { Menu } from '@/layers'; -import { Card } from '@/layout'; -import { Placement } from '@/utils'; +import { IconButton } from '@/elements/IconButton'; +import { Menu } from '@/layers/Menu'; +import { Card } from '@/layout/Card'; +import { Placement } from '@/utils/Position'; import { Calendar, Input, InputProps, InputRef } from '@/form'; import CalendarIcon from '@/assets/icons/calendar.svg?react'; From 8a0437a1dc1145de02a71cd2b87d7e9797560693 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Fri, 24 May 2024 15:40:51 +0300 Subject: [PATCH 10/10] Update imports --- src/form/DateInput/DateInput.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/form/DateInput/DateInput.tsx b/src/form/DateInput/DateInput.tsx index beb8c593..aeaac1c2 100644 --- a/src/form/DateInput/DateInput.tsx +++ b/src/form/DateInput/DateInput.tsx @@ -13,7 +13,8 @@ import { IconButton } from '@/elements/IconButton'; import { Menu } from '@/layers/Menu'; import { Card } from '@/layout/Card'; import { Placement } from '@/utils/Position'; -import { Calendar, Input, InputProps, InputRef } from '@/form'; +import { Calendar } from '@/form/Calendar'; +import { Input, InputProps, InputRef } from '@/form/Input'; import CalendarIcon from '@/assets/icons/calendar.svg?react';