diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b7cffcd8..ced561d71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,16 @@ ### Dependency updates +## [17.0.0] - 2022-11-15 + +### Added + +- `DatePickerInput`: restriction to use `formatDate` if `typeable=true` ([@qubis741](https://github.com/qubis741)) in [#2441](https://github.com/teamleadercrm/ui/pull/2441)) + +### Fixed + +- `DatePickerInput`: inability to click on `MonthPicker` while inside `Dialog` ([@qubis741](https://github.com/qubis741)) in [#2441](https://github.com/teamleadercrm/ui/pull/2441)) + ## [16.5.0] - 2022-11-10 ### Added diff --git a/package.json b/package.json index 6c98ecb20..cc2fbad38 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@teamleader/ui", "description": "Teamleader UI library", - "version": "16.5.0", + "version": "17.0.0", "author": "Teamleader ", "bugs": { "url": "https://github.com/teamleadercrm/ui/issues" diff --git a/src/components/datepicker/DatePickerInput.tsx b/src/components/datepicker/DatePickerInput.tsx index 71eb3b8d9..1ef958bad 100644 --- a/src/components/datepicker/DatePickerInput.tsx +++ b/src/components/datepicker/DatePickerInput.tsx @@ -1,8 +1,7 @@ import { IconCalendarSmallOutline, IconCloseBadgedSmallFilled } from '@teamleader/ui-icons'; import React, { ReactNode, useEffect, useState } from 'react'; -import { DayPickerProps as ReactDayPickerProps } from 'react-day-picker'; +import { DayPickerProps as ReactDayPickerProps, Modifier } from 'react-day-picker'; import DatePicker from '.'; -import { GenericComponent } from '../../@types/types'; import { SIZES } from '../../constants'; import Box, { pickBoxProps } from '../box'; import { BoxProps } from '../box/Box'; @@ -13,11 +12,12 @@ import Popover from '../popover'; import { PopoverProps } from '../popover/Popover'; import { formatDate, parseMultiFormatsDate } from './localeUtils'; import theme from './theme.css'; +import { isAllowedDate } from './utils'; const DEFAULT_FORMAT = 'dd/MM/yyyy'; const ALLOWED_DATE_FORMATS = [DEFAULT_FORMAT, 'd/M/yyyy', 'dd.MM.yyyy', 'd.M.yyyy', 'dd-MM-yyyy', 'd-M-yyyy']; -export interface DatePickerInputProps extends Omit { +export interface DatePickerInputProps extends Omit { /** A class name for the wrapper to give custom styles. */ className?: string; /** Object with props for the DatePicker component. */ @@ -25,7 +25,7 @@ export interface DatePickerInputProps extends Omit string; + formatDate?: IsTypeable extends true ? never : (selectedDate: Date, locale: string) => string; /** Object with props for the Input component. */ inputProps?: InputProps; /** If true, component will be rendered in inverse mode. */ @@ -51,19 +51,21 @@ export interface DatePickerInputProps extends Omit { +export type AllowedDisabledDays = Modifier | Date[]; +interface DayPickerProps extends Omit { numberOfMonths?: number; showOutsideDays?: boolean; showWeekNumbers?: boolean; withMonthPicker?: boolean; + disabledDays?: AllowedDisabledDays; } -const DatePickerInput: GenericComponent = ({ +function DatePickerInput({ className, dayPickerProps, footer, @@ -79,10 +81,10 @@ const DatePickerInput: GenericComponent = ({ clearable = false, onChange, onBlur, - typeable = true, + typeable = true as IsTypeable, errorText, ...others -}) => { +}: DatePickerInputProps) { const getFormattedDateString = (date: Date) => { if (!date) { return ''; @@ -129,7 +131,7 @@ const DatePickerInput: GenericComponent = ({ const handleInputChange = (event: React.ChangeEvent) => { const value = event.target.value; const date = parseMultiFormatsDate(value, ALLOWED_DATE_FORMATS, locale); - if (date) { + if (date && isAllowedDate(date, dayPickerProps?.disabledDays)) { setSelectedDate(date); } handleInputValueChange(value); @@ -139,7 +141,7 @@ const DatePickerInput: GenericComponent = ({ inputProps?.onBlur && inputProps.onBlur(event); if (typeable && !customFormatDate && inputValue) { const date = parseMultiFormatsDate(inputValue, ALLOWED_DATE_FORMATS, locale); - if (date) { + if (date && isAllowedDate(date, dayPickerProps?.disabledDays)) { handleInputValueChange(getFormattedDateString(date)); } else { setDisplayError(true); @@ -254,6 +256,6 @@ const DatePickerInput: GenericComponent = ({ ); -}; +} export default DatePickerInput; diff --git a/src/components/datepicker/__tests__/utils.spec.ts b/src/components/datepicker/__tests__/utils.spec.ts new file mode 100644 index 000000000..84ac8a5bc --- /dev/null +++ b/src/components/datepicker/__tests__/utils.spec.ts @@ -0,0 +1,30 @@ +// @todo uncomment when jest is added +// const testDate = new Date(2022, 10, 15); +// describe('DatePicker utils', () => { +// describe('isAllowedDate', () => { +// test.each([ +// { disabledDays: new Date(2022, 10, 16), output: true }, +// { disabledDays: new Date(2022, 10, 15), output: false }, +// { disabledDays: [new Date(2022, 10, 16), new Date(2022, 10, 14)], output: true }, +// { disabledDays: [new Date(2022, 10, 16), new Date(2022, 10, 15)], output: false }, +// { disabledDays: [{incorrect: 'value'}], output: true }, +// { disabledDays: [new Date(2022, 10, 16), {incorrect: 'value'}], output: true }, +// { disabledDays: { from: new Date(2022, 10, 15), to: null }, output: true }, +// { disabledDays: { from: null, to: new Date(2022, 10, 15) }, output: true }, +// { disabledDays: { from: new Date(2022, 10, 16), to: new Date(2022, 10, 20) }, output: true }, +// { disabledDays: { from: new Date(2022, 10, 14), to: new Date(2022, 10, 15) }, output: false }, +// { disabledDays: { before: new Date(2022, 10, 14) }, output: true }, +// { disabledDays: { before: new Date(2022, 10, 16) }, output: false }, +// { disabledDays: { after: new Date(2022, 10, 16) }, output: true }, +// { disabledDays: { after: new Date(2022, 10, 14) }, output: false }, +// { disabledDays: { before: new Date(2022, 10, 14), after: new Date(2022, 10, 16) }, output: true }, +// { disabledDays: { before: new Date(2022, 10, 16), after: new Date(2022, 10, 17) }, output: false }, +// { disabledDays: { daysOfWeek: [1, 3, 4] }, output: true }, +// { disabledDays: { daysOfWeek: [1, 2, 3, 4] }, output: false }, +// { disabledDays: (_date: Date) => false, output: true }, +// { disabledDays: (_date: Date) => true, output: false }, +// ])('works with disabled days: `$disabledDays`', ({ disabledDays, output }) => { +// expect(isAllowedDate(testDate, disabledDays)).toEqual(output); +// }); +// }); +// }); diff --git a/src/components/datepicker/datePickerInput.stories.tsx b/src/components/datepicker/datePickerInput.stories.tsx index 619e5673e..4743c9dc3 100644 --- a/src/components/datepicker/datePickerInput.stories.tsx +++ b/src/components/datepicker/datePickerInput.stories.tsx @@ -38,7 +38,7 @@ export const clearableInputSingleDate: ComponentStory = numberOfMonths: 1, showOutsideDays: true, showWeekNumbers: true, - withMonthPicker: false, + withMonthPicker: true, }} inputProps={{ helpText: 'Pick a date', @@ -75,9 +75,9 @@ export const inputSingleDateWithCustomFormat: ComponentStory ); }; @@ -103,7 +103,6 @@ export const inputSingleDateWithoutTyping: ComponentStory ); }; diff --git a/src/components/datepicker/utils.ts b/src/components/datepicker/utils.ts index 9adef0707..b6979ed6f 100644 --- a/src/components/datepicker/utils.ts +++ b/src/components/datepicker/utils.ts @@ -1,4 +1,5 @@ -import { DateUtils, DayModifiers } from 'react-day-picker'; +import { BeforeAfterModifier, DateUtils, DayModifiers } from 'react-day-picker'; +import { AllowedDisabledDays } from './DatePickerInput'; export const convertModifiersToClassnames = (modifiers: DayModifiers, theme: Record) => { if (!modifiers) { @@ -18,3 +19,53 @@ export const isSelectingFirstDay = (from: Date, to: Date, day: Date) => { const isRangeSelected = from && to; return !from || isBeforeFirstDay || isRangeSelected; }; + +const getDateTimeAtStartOfDay = (date: Date) => { + date.setHours(0, 0, 0, 0); + return date.getTime(); +}; +export const isAllowedDate = (date: Date, disabledDays?: AllowedDisabledDays) => { + if (!disabledDays) { + return true; + } + const dateTime = getDateTimeAtStartOfDay(date); + if (disabledDays instanceof Date) { + return dateTime !== getDateTimeAtStartOfDay(disabledDays); + } + if (Array.isArray(disabledDays) && disabledDays.length > 0 && disabledDays[0] instanceof Date) { + return !disabledDays.some((disabledDay) => { + return disabledDay instanceof Date ? dateTime === getDateTimeAtStartOfDay(disabledDay) : false; + }); + } + if ('from' in disabledDays && disabledDays.from && 'to' in disabledDays && disabledDays.to) { + return !( + getDateTimeAtStartOfDay(disabledDays.from) <= dateTime && dateTime <= getDateTimeAtStartOfDay(disabledDays.to) + ); + } + if ('before' in disabledDays && disabledDays.before && !('after' in disabledDays)) { + return getDateTimeAtStartOfDay(disabledDays.before) <= dateTime; + } + if ('after' in disabledDays && disabledDays.after && !('before' in disabledDays)) { + return getDateTimeAtStartOfDay(disabledDays.after) >= dateTime; + } + const disabledDaysBeforeAfter = disabledDays as BeforeAfterModifier; + if ( + 'before' in disabledDaysBeforeAfter && + 'after' in disabledDaysBeforeAfter && + disabledDaysBeforeAfter.before && + disabledDaysBeforeAfter.after + ) { + return ( + getDateTimeAtStartOfDay(disabledDaysBeforeAfter.before) <= dateTime && + dateTime <= getDateTimeAtStartOfDay(disabledDaysBeforeAfter.after) + ); + } + if ('daysOfWeek' in disabledDays && Array.isArray(disabledDays.daysOfWeek)) { + return !disabledDays.daysOfWeek.includes(date.getDay()); + } + if (typeof disabledDays === 'function') { + return !disabledDays(date); + } + + return true; +}; diff --git a/src/components/select/Select.tsx b/src/components/select/Select.tsx index 517be86b2..a88598d3d 100644 --- a/src/components/select/Select.tsx +++ b/src/components/select/Select.tsx @@ -15,6 +15,8 @@ import ReactSelect, { Props, StylesConfig, ValueContainerProps, + components as ReactSelectComponents, + InputProps, } from 'react-select'; import ReactCreatableSelect from 'react-select/creatable'; import SelectType from 'react-select/dist/declarations/src/Select'; @@ -125,7 +127,10 @@ const ClearIndicator =