diff --git a/package-lock.json b/package-lock.json index 8f7e61a3d..662add545 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2529,6 +2529,11 @@ "postcss-selector-parser": "^6.0.13" } }, + "node_modules/@date-fns/tz": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", + "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==" + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -20743,6 +20748,34 @@ "react": "^16.3.0 || ^17.0.1 || ^18.0.0" } }, + "node_modules/react-day-picker": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.4.2.tgz", + "integrity": "sha512-qKunVfJ+QWJqdHylZ9TsXSSJzzK9vDLsZ+c80/r+ZwOWqGW8mADwPy1iOBrNcZiAokQ4xrSsPLnWzTRHS4mSsQ==", + "dependencies": { + "@date-fns/tz": "^1.2.0", + "date-fns": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-day-picker/node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/react-docgen": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.0.3.tgz", @@ -26007,7 +26040,7 @@ "lodash.debounce": "^4.0.8", "lodash.escape": "^4.0.1", "polished": "^4.1.3", - "react-day-picker": "^7.4.10", + "react-day-picker": "^9.4.2", "react-transition-group": "^4.4.2", "react-virtuoso": "^4.7.11" }, @@ -26069,17 +26102,6 @@ "peerDependencies": { "@testing-library/dom": ">=7.21.4" } - }, - "packages/react-components/node_modules/react-day-picker": { - "version": "7.4.10", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-7.4.10.tgz", - "integrity": "sha512-/QkK75qLKdyLmv0kcVzhL7HoJPazoZXS8a6HixbVoK6vWey1Od1WRLcxfyEiUsRfccAlIlf6oKHShqY2SM82rA==", - "dependencies": { - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": "~0.13.x || ~0.14.x || ^15.0.0 || ^16.0.0 || ^17.0.0" - } } } } diff --git a/packages/react-components/package.json b/packages/react-components/package.json index ba1d1a72a..13bea4c6f 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -82,7 +82,7 @@ "lodash.debounce": "^4.0.8", "lodash.escape": "^4.0.1", "polished": "^4.1.3", - "react-day-picker": "^7.4.10", + "react-day-picker": "^9.4.2", "react-transition-group": "^4.4.2", "react-virtuoso": "^4.7.11" } diff --git a/packages/react-components/src/components/DatePicker/DatePicker.mdx b/packages/react-components/src/components/DatePicker/DatePicker.mdx index 57139eaa8..c5aaf09e2 100644 --- a/packages/react-components/src/components/DatePicker/DatePicker.mdx +++ b/packages/react-components/src/components/DatePicker/DatePicker.mdx @@ -8,7 +8,7 @@ import * as DatePickerStories from './DatePicker.stories'; [Component API](#ComponentAPI) | [Content Spec](#ContentSpec) - + #### Example implementation @@ -22,7 +22,7 @@ import * as DatePickerStories from './DatePicker.stories'; ## Component API - + ## Content Spec diff --git a/packages/react-components/src/components/DatePicker/DatePicker.module.scss b/packages/react-components/src/components/DatePicker/DatePicker.module.scss index e770e74ea..9859cd5ef 100644 --- a/packages/react-components/src/components/DatePicker/DatePicker.module.scss +++ b/packages/react-components/src/components/DatePicker/DatePicker.module.scss @@ -1,316 +1,111 @@ $base-class: 'date-picker'; .#{$base-class} { - display: inline-block; - color: var(--content-basic-primary); - - &:not(.#{$base-class}--interaction-disabled) - &__day:not(.#{$base-class}__day--disabled):not( - .#{$base-class}__day--selected - ):not(.#{$base-class}__day--outside):hover { - .#{$base-class}__day-content { - border-radius: 2px; - background-color: var(--surface-primary-hover); - } - } - &__wrapper { - position: relative; - flex-direction: row; - transition: 0.2s border-color ease-in-out; - border: 1px solid transparent; - border-radius: var(--radius-3); - padding-bottom: 10px; - user-select: none; - - &:focus { - transition: 0.2s border-color ease-in-out; - outline: none; - border: 1px solid var(--color-action-default); - border-radius: 4px; - } - } - - &__months { - display: flex; - flex-wrap: wrap; + display: inline-flex; + align-items: center; justify-content: center; - } - - &__month { - display: table; - margin: 0; - user-select: none; - } - - &__nav-bar { - display: flex; - position: absolute; - top: 0; - left: 50%; - justify-content: space-between; - transform: translateX(-50%); - width: 100%; - } - - &__nav-button { - display: flex; - align-content: center; - transition: 0.2s border-color ease-in-out; - border: 1px solid transparent; - background-color: transparent; - cursor: pointer; - padding: 0; - color: var(--content-basic-secondary); - - &:hover, - &:focus { - outline: none; - - svg { - color: var(--content-basic-primary); - } - } - - &--interaction-disabled { - display: none; - pointer-events: none; - } - } - - &__caption { - display: table-caption; - margin-bottom: 12px; - padding: 0 50px; - - > div { - text-align: center; - line-height: 20px; - color: var(--content-basic-primary); - font-size: 14px; - font-weight: 600; - } - } - - &__weekdays { - display: table-header-group; - } - - &__weekdays-row { - display: table-row; + margin: var(--spacing-05); + border-radius: var(--radius-3); + width: 28px; + height: 28px; } &__weekday { - display: table-cell; - margin-bottom: 2px; - padding: 6px 0; - text-align: center; - line-height: 16px; - letter-spacing: 0.2px; color: var(--content-basic-secondary); font-size: 12px; - - abbr[title] { - border-bottom: initial; - text-decoration: none; - } - } - - &__body { - display: table-row-group; - } - - &__week { - display: table-row; + font-weight: 400; } &__day { - display: table-cell; - cursor: pointer; - width: 18px; - height: 18px; + transition: all var(--transition-duration-fast-2) ease-in-out; color: var(--content-basic-primary); font-size: 13px; + font-weight: 400; - &:focus { - outline: none; - - .#{$base-class}__day-content { - border-radius: 2px; - box-shadow: 0 0 0 1px var(--border-basic-primary); - } - } - - &-wrapper { - margin: 1px 0; - padding: 0 2px; - } - - &-content { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; + &:hover { + background-color: var(--surface-primary-hover); + cursor: pointer; } + } - &--today { - .#{$base-class}__day-content { - border-radius: 2px; - box-shadow: 0 0 0 1px var(--border-basic-primary); - background-color: var(--surface-primary-default); - } - } + &__day-button { + display: flex; + align-items: center; + justify-content: center; + transition: box-shadow var(--transition-duration-fast-2) ease-in-out; + border: 0; + border-radius: var(--radius-3); + background: transparent; + cursor: pointer; + width: 100%; + height: 100%; + color: inherit; - &--selected { - .#{$base-class}__day-content { - border-radius: 2px; - background-color: var(--action-primary-default); - color: var(--content-invert-primary); - } + &:focus-visible { + outline: 0; + box-shadow: var(--shadow-focus); } + } - &--outside { - cursor: default; - color: var(--content-basic-disabled); - } + &__today { + border: 1px solid var(--border-basic-primary); - &--disabled { - cursor: default; - color: var(--content-basic-disabled); - pointer-events: none; + &:hover { + border-color: var(--border-basic-hover); } } - &--interaction-disabled { - .#{$base-class}__day { - cursor: default; - pointer-events: none; - } + &__selected, + &__selected:hover { + background-color: var(--action-primary-default); + color: var(--content-invert-primary); } - &__footer { - padding-top: 5px; + &__month-caption { + display: flex; + align-items: center; + justify-content: center; } - &__input { - display: inline-block; + &__range-start { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + background-color: var(--action-primary-default); + } - &-overlay-wrapper { - position: relative; - } + &__range-middle { + position: relative; + border-radius: 0; + background-color: var(--surface-accent-emphasis-low-info); + color: var(--content-basic-primary); - &-overlay { - display: block; + &::after { + position: absolute; + right: calc(var(--spacing-05) * -1); + left: calc(var(--spacing-05) * -1); + z-index: -1; + background-color: var(--surface-accent-emphasis-low-info); + height: 100%; + content: ''; } } - &--range { - .#{$base-class}__day { - &--selected { - &:not(.#{$base-class}__day--disabled):not( - .#{$base-class}__day--outside - ):not(.#{$base-class}__day--start):not(.#{$base-class}__day--end):not( - .#{$base-class}__day--single - ):not(.#{$base-class}__day--sunday):not( - .#{$base-class}__day--monday - ) { - .#{$base-class}__day-wrapper { - background-color: var(--surface-accent-emphasis-low-info); - } - } - .#{$base-class}__day-content { - border-radius: 0; - background-color: transparent; - color: var(--content-default); - } - } - - &--start:not(.#{$base-class}__day--end):not( - .#{$base-class}__day--sunday - ):not(.#{$base-class}__day--monday), - &--monday.#{$base-class}__day--selected:not(.#{$base-class}__day--end) { - .#{$base-class}__day-wrapper { - background: linear-gradient( - to left, - var(--surface-accent-emphasis-low-info) 0%, - var(--surface-accent-emphasis-low-info) 50%, - var(--surface-primary-default) 50%, - var(--surface-primary-default) 100% - ); - } - } - - &--end:not(.#{$base-class}__day--start):not( - .#{$base-class}__day--monday - ):not(.#{$base-class}__day--sunday), - &--sunday.#{$base-class}__day--selected:not(.#{$base-class}__day--start) { - .#{$base-class}__day-wrapper { - background: linear-gradient( - to right, - var(--surface-accent-emphasis-low-info) 0%, - var(--surface-accent-emphasis-low-info) 50%, - var(--surface-primary-default) 50%, - var(--surface-primary-default) 100% - ); - } - } + &__range-end { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + background-color: var(--action-primary-default); + } - &--monday.#{$base-class}__day--selected:not( - .#{$base-class}__day--start - ):not(.#{$base-class}__day--end), - &--sunday.#{$base-class}__day--selected:not( - .#{$base-class}__day--start - ):not(.#{$base-class}__day--end) { - .#{$base-class}__day-content { - background-color: var(--surface-accent-emphasis-low-info); - } - } + &__disabled { + opacity: 0.4; - &--start, - &--end { - .#{$base-class}__day-content { - border-radius: 2px; - background-color: var(--action-primary-default); - color: var(--content-invert-primary); - } - } + &:hover { + background-color: transparent; } - &__select-input { - border-color: transparent; - padding: 0 5px; - max-width: 90px; - text-align: left; + button { + cursor: not-allowed; } - - &__calendars-wrapper { - display: flex; - align-items: flex-start; - - .#{$base-class}__months { - flex-wrap: nowrap; - margin: 0 -12px; - - &::before { - position: absolute; - top: 0; - left: 50%; - background-color: var(--border-basic-tertiary); - width: 1px; - height: 100%; - content: ''; - } - } - - .#{$base-class}__month { - padding: 0 12px; - } - } - } - - .#{$base-class}__navbar-buttons-wrapper { - display: flex; - gap: 4px; } } diff --git a/packages/react-components/src/components/DatePicker/DatePicker.spec.tsx b/packages/react-components/src/components/DatePicker/DatePicker.spec.tsx index cd087ce4b..36f7c3154 100644 --- a/packages/react-components/src/components/DatePicker/DatePicker.spec.tsx +++ b/packages/react-components/src/components/DatePicker/DatePicker.spec.tsx @@ -1,4 +1,3 @@ -import { within } from '@testing-library/react'; import { vi } from 'vitest'; import { render, userEvent } from 'test-utils'; @@ -21,10 +20,10 @@ describe(' component', () => { userEvent.click(nextYearButton); // Datepicker has 12 o'clock as default hour - expect(onMonthChange).toHaveBeenCalledWith(new Date(2023, 0, 1, 12)); + expect(onMonthChange).toHaveBeenCalledWith(new Date(2023, 0, 1)); userEvent.click(previousYearButton); // Datepicker has 12 o'clock as default hour - expect(onMonthChange).toHaveBeenCalledWith(new Date(2021, 0, 1, 12)); + expect(onMonthChange).toHaveBeenCalledWith(new Date(2021, 0, 1)); }); it('should call onMonthChange when next and previous month button user click', () => { @@ -38,10 +37,10 @@ describe(' component', () => { userEvent.click(nextMonthButton); // Datepicker has 12 o'clock as default hour - expect(onMonthChange).toHaveBeenCalledWith(new Date(2022, 1, 1, 12)); + expect(onMonthChange).toHaveBeenCalledWith(new Date(2022, 1, 1)); userEvent.click(previousMonthButton); // Datepicker has 12 o'clock as default hour - expect(onMonthChange).toHaveBeenCalledWith(new Date(2021, 12, 1, 12)); + expect(onMonthChange).toHaveBeenCalledWith(new Date(2021, 11, 1)); }); it('should call onDayClick when user click on a day', () => { @@ -57,14 +56,10 @@ describe(' component', () => { }); it('should have Monday as a default first weekday', () => { - const { getAllByRole } = renderComponent({ firstDayOfWeek: 9 }); - - const weekdays = getAllByRole('columnheader'); + const { container } = renderComponent({ weekStartsOn: undefined }); + const weekdays = container.querySelectorAll('th'); expect(weekdays).toHaveLength(7); - - const firstWeekday = within(weekdays[0]).getByTitle('Monday'); - - expect(firstWeekday).toBeDefined(); + expect(weekdays[0]).toHaveAttribute('aria-label', 'Monday'); }); }); diff --git a/packages/react-components/src/components/DatePicker/DatePicker.stories.tsx b/packages/react-components/src/components/DatePicker/DatePicker.stories.tsx index 9376d43fd..84faa4c08 100644 --- a/packages/react-components/src/components/DatePicker/DatePicker.stories.tsx +++ b/packages/react-components/src/components/DatePicker/DatePicker.stories.tsx @@ -2,27 +2,20 @@ import * as React from 'react'; import { Meta, StoryFn } from '@storybook/react'; -import { DatePicker as DatePickerComponent } from './DatePicker'; +import { DatePicker } from './DatePicker'; import { IDatePickerProps } from './types'; +const DISABLED_RANGE_OPTIONS = [ + 'No Disabled Dates', + 'Disable Before Current Date', + 'Disable After Current Date', +]; + export default { title: 'Components/DatePicker', - component: DatePickerComponent, - parameters: { - date: new Date('2023-07-01'), - }, + component: DatePicker, argTypes: { - innerRef: { - table: { - disable: true, - }, - }, - range: { - table: { - disable: true, - }, - }, - firstDayOfWeek: { + weekStartsOn: { description: 'Number representing first day of the week', table: { type: { @@ -35,80 +28,98 @@ export default { type: 'number', }, }, - disabledDays: { - description: - 'You can disable choosen dates. Pass either a date or array of dates', + disabled: { + description: 'Range of disabled dates', + control: { + type: 'select', + }, + options: DISABLED_RANGE_OPTIONS, + defaultValue: DISABLED_RANGE_OPTIONS[0], table: { type: { - summary: 'Date | Array', + summary: 'object', + detail: + '{ before: Date } | { after: Date } | { dayOfWeek: []: array of numbers 0-6 } | undefined', }, }, - control: { - type: 'date', - }, }, - fromMonth: { + month: { + description: 'Providing this prop will make the date picker controlled', control: { type: 'date', }, }, - toMonth: { + startMonth: { + description: 'Start month for the date picker', control: { type: 'date', }, }, - month: { + endMonth: { + description: 'End month for the date picker', control: { type: 'date', }, }, }, -} as Meta; +} as Meta; const StoryTemplate = (args: IDatePickerProps) => { - const argsSelectedDate = args.selectedDays - ? new Date(args.selectedDays as Date) - : void 0; - const argsDisabledDays = args.disabledDays - ? new Date(args.disabledDays as Date) - : void 0; - const [selectedDate, setSelectedDate] = React.useState< - IDatePickerProps['selectedDays'] | undefined - >(argsSelectedDate); + const { month, ...restArgs } = args; + + const [selectedDate, setSelectedDate] = React.useState(); + const [selectedMonth, setSelectedMonth] = React.useState(); + const [initialMonth] = React.useState( + month ? new Date(month) : undefined + ); + + React.useEffect(() => { + const newMonth = month ? new Date(month) : undefined; + + if (newMonth !== initialMonth) { + setSelectedMonth(newMonth); + } + }, [month, initialMonth]); + + const getDisabledDates = () => { + switch (restArgs.disabled as unknown) { + case DISABLED_RANGE_OPTIONS[1]: + return { before: new Date() }; + case DISABLED_RANGE_OPTIONS[2]: + return { after: new Date() }; + default: + return undefined; + } + }; return ( -
- { - setSelectedDate(date); - alert(`Selected date: ${date.toDateString()}`); - }} - selectedDays={selectedDate} - /> -
+ ); }; -export const DatePicker: StoryFn = StoryTemplate.bind({}); -DatePicker.args = {}; +export const Default: StoryFn = StoryTemplate.bind({}); +Default.args = {}; + +export const WithCustomCurrentDate: StoryFn = StoryTemplate.bind({}); +WithCustomCurrentDate.args = { + today: new Date('01/20/2024'), +}; -export const DatePickerWithDatesBetweenTwoMonths: StoryFn = StoryTemplate.bind( - {} -); -DatePickerWithDatesBetweenTwoMonths.args = { - fromMonth: new Date('06/20/2021'), - toMonth: new Date('09/20/2021'), - month: new Date('09/20/2021'), +export const WithDatesBetweenTwoMonths: StoryFn = StoryTemplate.bind({}); +WithDatesBetweenTwoMonths.args = { + startMonth: new Date('11/20/2023'), + endMonth: new Date('01/20/2024'), }; -export const DatePickerWithCustomFirstDayOfWeek: StoryFn = StoryTemplate.bind( - {} -); -DatePickerWithCustomFirstDayOfWeek.args = { - firstDayOfWeek: 0, +export const WithCustomFirstDayOfWeek: StoryFn = StoryTemplate.bind({}); +WithCustomFirstDayOfWeek.args = { + weekStartsOn: 0, }; diff --git a/packages/react-components/src/components/DatePicker/DatePicker.tsx b/packages/react-components/src/components/DatePicker/DatePicker.tsx index d317396f5..16388f3f3 100644 --- a/packages/react-components/src/components/DatePicker/DatePicker.tsx +++ b/packages/react-components/src/components/DatePicker/DatePicker.tsx @@ -1,40 +1,30 @@ import * as React from 'react'; -import ReactDayPicker from 'react-day-picker'; +import cx from 'clsx'; +import { DayPicker } from 'react-day-picker'; -import DatePickerNavbar from './DatePickerNavbar'; -import { getDatePickerClassNames, isDateWithinRange } from './helpers'; +import { Text } from '../Typography'; + +import { DatePickerCustomNavigation } from './components/DatePickerCustomNavigation'; +import { isDateWithinRange } from './helpers'; import { IDatePickerProps } from './types'; +import 'react-day-picker/style.css'; + import styles from './DatePicker.module.scss'; const baseClass = 'date-picker'; -const defaultDayRenderer = (day: Date): React.ReactElement => { - const date = day.getDate(); - - return ( -
-
{date}
-
- ); -}; - -const DatePickerComponent: React.FC = (props) => { - const { - classNames, - range, - toMonth, - month, - fromMonth, - firstDayOfWeek: propsFirstDayOfWeek, - numberOfMonths, - navbarElement, - renderDay, - innerRef, - ...restProps - } = props; - +export const DatePicker: React.FC = ({ + month, + weekStartsOn = 1, + fromMonth, + startMonth, + toMonth, + endMonth, + onMonthChange, + ...props +}) => { const [currentMonth, setCurrentMonth] = React.useState(month || new Date()); React.useEffect(() => { @@ -52,61 +42,56 @@ const DatePickerComponent: React.FC = (props) => { }, [month, currentMonth, toMonth, fromMonth]); const handleMonthChange = React.useCallback( - (month: Date) => { - if (props.onMonthChange && month) { - props.onMonthChange(month); + (newMonth: Date) => { + if (onMonthChange) { + onMonthChange(newMonth); return; } - setCurrentMonth(month); + setCurrentMonth(newMonth); }, - [month, props.onMonthChange] - ); - - let firstDayOfWeek = 1; - - if ( - propsFirstDayOfWeek === 0 || - (propsFirstDayOfWeek && propsFirstDayOfWeek < 7) - ) { - firstDayOfWeek = propsFirstDayOfWeek; - } - - const datePickerClassNames = React.useMemo( - () => getDatePickerClassNames(range, classNames), - [range, classNames] + [month, onMonthChange] ); return ( - - ) - } - ref={innerRef} - classNames={datePickerClassNames} - numberOfMonths={numberOfMonths} - toMonth={toMonth} - fromMonth={fromMonth} - firstDayOfWeek={firstDayOfWeek} + ( + + {children} + + ), + Nav: () => ( + + ), + }} + startMonth={startMonth || fromMonth} + endMonth={endMonth || toMonth} + weekStartsOn={weekStartsOn} + {...props} /> ); }; - -export const DatePicker = React.forwardRef( - (props, ref) => -); - -DatePicker.displayName = 'DatePicker'; diff --git a/packages/react-components/src/components/DatePicker/DatePickerNavbar.tsx b/packages/react-components/src/components/DatePicker/DatePickerNavbar.tsx deleted file mode 100644 index e8bcc2280..000000000 --- a/packages/react-components/src/components/DatePicker/DatePickerNavbar.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import * as React from 'react'; - -import { - ChevronLeft, - ChevronRight, - DoubleArrowLeft, - DoubleArrowRight, -} from '@livechat/design-system-icons'; -import cx from 'clsx'; -import { - subMonths, - addMonths, - differenceInCalendarMonths, - isSameMonth, -} from 'date-fns'; - -import { Icon } from '../Icon'; - -import { IDatePickerNavbarProps } from './types'; - -import styles from './DatePicker.module.scss'; - -const baseClass = 'date-picker'; - -const DatePickerNavbar: React.FC = (props) => { - const { - onPreviousClick, - onMonthChange, - onNextClick, - showNextButton, - showPreviousButton, - className, - classNames, - numberOfMonths, - month, - fromMonth, - toMonth, - } = props; - - // prev and next button handler are passed by react-day-picker, added check to be safe - // see: https://github.com/gpbl/react-day-picker/blob/v7/src/DayPicker.js#L529 - const handlePrevClick = () => { - if (typeof onPreviousClick === 'function') { - onPreviousClick(); - } - }; - - const handleNextClick = () => { - if (typeof onNextClick === 'function') { - onNextClick(); - } - }; - - const handlePrevYearClick = () => { - if (!fromMonth) { - const newMonth = subMonths(month, 12); - - return onMonthChange(newMonth); - } - const diff = Math.abs(differenceInCalendarMonths(month, fromMonth)); - const newMonth = subMonths( - month, - !Number.isNaN(diff) && diff > 12 ? 12 : diff - ); - - return onMonthChange(newMonth); - }; - - const handleNextYearClick = () => { - if (!toMonth) { - const newMonth = addMonths(month, 12); - - return onMonthChange(newMonth); - } - const diff = Math.abs(differenceInCalendarMonths(toMonth, month)); - const newMonth = addMonths( - month, - !Number.isNaN(diff) && diff > 12 ? 12 : diff - ); - - if (numberOfMonths === 2 && isSameMonth(newMonth, toMonth)) { - return onMonthChange(subMonths(newMonth, 1)); - } - - return onMonthChange(newMonth); - }; - - return ( -
-
- - -
-
- - -
-
- ); -}; - -export default DatePickerNavbar; diff --git a/packages/react-components/src/components/DatePicker/RangeDatePicker.spec.tsx b/packages/react-components/src/components/DatePicker/RangeDatePicker.spec.tsx index ff326238b..2d0edb052 100644 --- a/packages/react-components/src/components/DatePicker/RangeDatePicker.spec.tsx +++ b/packages/react-components/src/components/DatePicker/RangeDatePicker.spec.tsx @@ -1,3 +1,4 @@ +import { format } from 'date-fns'; import { vi } from 'vitest'; import { render, userEvent } from 'test-utils'; @@ -10,6 +11,8 @@ import { IRangeDatePickerChildrenPayload, } from './types'; +const formattedDate = (date: Date) => format(date, 'EEEE, MMMM do, yyyy'); + const options = [ { id: 'custom_date', @@ -34,15 +37,14 @@ describe(' component', () => { it('should call onChange callback if user chooses custom date', () => { const onChange = vi.fn(); const { getByLabelText } = renderComponent({ - initialFromDate: new Date(2022, 0, 1), - initialToDate: new Date(2022, 1, 1), + toMonth: new Date(2022, 1, 1), onChange, options, initialSelectedItemKey: 'custom_date', children, }); - const startDate = getByLabelText(new Date(2022, 0, 1).toDateString()); - const endDate = getByLabelText(new Date(2022, 1, 1).toDateString()); + const startDate = getByLabelText(formattedDate(new Date(2022, 0, 1))); + const endDate = getByLabelText(formattedDate(new Date(2022, 1, 1))); userEvent.click(startDate); userEvent.click(endDate); diff --git a/packages/react-components/src/components/DatePicker/RangeDatePicker.tsx b/packages/react-components/src/components/DatePicker/RangeDatePicker.tsx index a68e197a8..7f03ecbac 100644 --- a/packages/react-components/src/components/DatePicker/RangeDatePicker.tsx +++ b/packages/react-components/src/components/DatePicker/RangeDatePicker.tsx @@ -1,15 +1,9 @@ import { ReactElement, useCallback, useEffect, useMemo, useRef } from 'react'; -import { - isAfter, - isSameDay, - subMonths, - differenceInCalendarDays, -} from 'date-fns'; +import { isAfter, isSameDay, differenceInCalendarDays } from 'date-fns'; import { calculateDatePickerMonth, - getRangeDatePickerModifiers, getSelectedOption, isDateWithinRange, isSelectingFirstDay, @@ -29,6 +23,7 @@ export const RangeDatePicker = ({ initialToDate, toMonth, onChange, + onSelect, children, }: IRangeDatePickerProps): ReactElement => { const prevSelectedItem = useRef( @@ -125,6 +120,17 @@ export const RangeDatePicker = ({ onChange(optionsHash[selectedItem]); }, [onChange, state.selectedItem, options]); + const handleOnSelect = useCallback(() => { + const { from, to } = state; + + if (from && to && onSelect) { + onSelect({ + from, + to, + }); + } + }, [onSelect]); + const handleDayMouseEnter = useCallback( (day: Date) => { const isInRange = toMonth @@ -206,16 +212,12 @@ export const RangeDatePicker = ({ const getRangeDatePickerApi = (): IRangeDatePickerChildrenPayload => { const { currentMonth, from, selectedItem, temporaryTo, to } = state; - const modifiers = useMemo( - () => getRangeDatePickerModifiers(from, temporaryTo), - [from, temporaryTo] - ); const selectedOption = useMemo(() => { return getSelectedOption(selectedItem, options); }, [options, selectedItem]); const selectedDays = useMemo(() => { - return [from, { from, to: temporaryTo }]; + return { from, to: temporaryTo }; }, [from, temporaryTo]); const disabledDays = useMemo(() => { @@ -232,17 +234,16 @@ export const RangeDatePicker = ({ toDate: to, }, datepicker: { - range: true, + mode: 'range', month: currentMonth, numberOfMonths: 2, onDayClick: handleDayClick, - selectedDays, - modifiers, - initialMonth: toMonth && subMonths(toMonth, 1), - toMonth: toMonth, - disabledDays, + selected: selectedDays, + endMonth: toMonth, + disabled: disabledDays, onDayMouseEnter: handleDayMouseEnter, onMonthChange: handleMonthChange, + onSelect: handleOnSelect, }, selectedOption, }; diff --git a/packages/react-components/src/components/DatePicker/components/DatePickerCustomNavigation.module.scss b/packages/react-components/src/components/DatePicker/components/DatePickerCustomNavigation.module.scss new file mode 100644 index 000000000..fa05c624b --- /dev/null +++ b/packages/react-components/src/components/DatePicker/components/DatePickerCustomNavigation.module.scss @@ -0,0 +1,11 @@ +$base-class: 'date-picker-custom-navigation'; + +.#{$base-class} { + display: flex; + position: absolute; + top: -1px; + flex: 1; + align-items: center; + justify-content: space-between; + width: 100%; +} diff --git a/packages/react-components/src/components/DatePicker/components/DatePickerCustomNavigation.tsx b/packages/react-components/src/components/DatePicker/components/DatePickerCustomNavigation.tsx new file mode 100644 index 000000000..818b194d9 --- /dev/null +++ b/packages/react-components/src/components/DatePicker/components/DatePickerCustomNavigation.tsx @@ -0,0 +1,120 @@ +import * as React from 'react'; + +import { + ChevronLeft, + ChevronRight, + DoubleArrowLeft, + DoubleArrowRight, +} from '@livechat/design-system-icons'; +import { + subMonths, + addMonths, + differenceInCalendarMonths, + isSameMonth, +} from 'date-fns'; + +import { Button } from '../../Button'; +import { Icon } from '../../Icon'; +import { IDatePickerCustomNavigationProps } from '../types'; + +import styles from './DatePickerCustomNavigation.module.scss'; + +const baseClass = 'date-picker-custom-navigation'; + +export const DatePickerCustomNavigation: React.FC< + IDatePickerCustomNavigationProps +> = ({ currentMonth, setMonth, startMonth, endMonth, numberOfMonths }) => { + const shouldDisablePreviousButton = + !startMonth || !isSameMonth(currentMonth, startMonth); + const shouldDisableNextButton = + !endMonth || !isSameMonth(currentMonth, endMonth); + + const handlePreviousMonth = () => { + const newMonth = new Date(currentMonth); + newMonth.setMonth(currentMonth.getMonth() - 1); + setMonth(newMonth); + }; + + const handleNextMonth = () => { + const newMonth = new Date(currentMonth); + newMonth.setMonth(currentMonth.getMonth() + 1); + setMonth(newMonth); + }; + + const handlePreviousYearClick = () => { + if (!startMonth) { + const newMonth = subMonths(currentMonth, 12); + + return setMonth(newMonth); + } + + const diff = Math.abs(differenceInCalendarMonths(currentMonth, startMonth)); + const newMonth = subMonths( + currentMonth, + !Number.isNaN(diff) && diff > 12 ? 12 : diff + ); + + return setMonth(newMonth); + }; + + const handleNextYearClick = () => { + if (!endMonth) { + const newMonth = addMonths(currentMonth, 12); + + return setMonth(newMonth); + } + + const diff = Math.abs(differenceInCalendarMonths(endMonth, currentMonth)); + const newMonth = addMonths( + currentMonth, + !Number.isNaN(diff) && diff > 12 ? 12 : diff + ); + + if (numberOfMonths === 2 && isSameMonth(newMonth, endMonth)) { + return setMonth(subMonths(newMonth, 1)); + } + + return setMonth(newMonth); + }; + + return ( +
+
+
+
+
+
+ ); +}; diff --git a/packages/react-components/src/components/DatePicker/helpers.ts b/packages/react-components/src/components/DatePicker/helpers.ts index 55a58d21e..0beb815ba 100644 --- a/packages/react-components/src/components/DatePicker/helpers.ts +++ b/packages/react-components/src/components/DatePicker/helpers.ts @@ -1,13 +1,4 @@ -import cx from 'clsx'; -import { - isAfter, - isSameDay, - isSameMonth, - subMonths, - differenceInCalendarDays, -} from 'date-fns'; -import { Modifiers } from 'react-day-picker'; -import { ClassNames } from 'react-day-picker/types/ClassNames'; +import { isAfter, isSameDay, isSameMonth, subMonths } from 'date-fns'; import { IRangeDatePickerProps, @@ -15,10 +6,6 @@ import { IRangeDatePickerOption, } from './types'; -import styles from './DatePicker.module.scss'; - -const baseClass = 'date-picker'; - export const isDateWithinRange = ( date: Date, range: { from?: Date; to?: Date } @@ -54,38 +41,6 @@ export const calculateDatePickerMonth = ( return initialToDate; }; -export const getRangeDatePickerModifiers = ( - from?: Date, - to?: Date -): Partial => { - const base = { - [styles[`${baseClass}__day--monday`]]: { daysOfWeek: [1] }, - [styles[`${baseClass}__day--sunday`]]: { daysOfWeek: [0] }, - [styles[`${baseClass}__day--start`]]: from, - [styles[`${baseClass}__day--end`]]: from, - }; - - if (!to || !from) return base; - - const diff = differenceInCalendarDays(to, from); - - if (diff > 0) { - return { - ...base, - [styles[`${baseClass}__day--end`]]: to, - }; - } - - if (diff < 0) { - return { - ...base, - [styles[`${baseClass}__day--start`]]: to, - }; - } - - return base; -}; - export const getSelectedOption = ( itemId: string | null, options: IRangeDatePickerOption[] @@ -137,45 +92,3 @@ export const getInitialStateFromProps = ( return state; }; - -export const getDatePickerClassNames = ( - range?: boolean, - classNames?: ClassNames -): ClassNames & { start: string; end: string } => ({ - container: cx({ - [styles[`${baseClass}`]]: true, - [styles[`${baseClass}--range`]]: range, - }), - wrapper: styles[`${baseClass}__wrapper`], - interactionDisabled: styles[`${baseClass}--interaction-disabled`], - months: styles[`${baseClass}__months`], - month: styles[`${baseClass}__month`], - navBar: styles[`${baseClass}__nav-bar`], - navButtonPrev: cx( - styles[`${baseClass}__nav-button`], - styles[`${baseClass}__nav-button--prev`] - ), - navButtonNext: cx( - styles[`${baseClass}__nav-button`], - styles[`${baseClass}__nav-button--next`] - ), - navButtonInteractionDisabled: - styles[`${baseClass}__nav-button--interaction-disabled`], - caption: styles[`${baseClass}__caption`], - weekdays: styles[`${baseClass}__weekdays`], - weekdaysRow: styles[`${baseClass}__weekdays-row`], - weekday: styles[`${baseClass}__weekday`], - body: styles[`${baseClass}__body`], - week: styles[`${baseClass}__week`], - weekNumber: styles[`${baseClass}__week-number`], - day: styles[`${baseClass}__day`], - footer: styles[`${baseClass}__footer`], - todayButton: styles[`${baseClass}__today-button`], - today: styles[`${baseClass}__day--today`], - selected: styles[`${baseClass}__day--selected`], - disabled: styles[`${baseClass}__day--disabled`], - outside: styles[`${baseClass}__day--outside`], - start: styles[`${baseClass}__day--start`], - end: styles[`${baseClass}__day--end`], - ...classNames, -}); diff --git a/packages/react-components/src/components/DatePicker/types.ts b/packages/react-components/src/components/DatePicker/types.ts index db932e706..78be210de 100644 --- a/packages/react-components/src/components/DatePicker/types.ts +++ b/packages/react-components/src/components/DatePicker/types.ts @@ -1,26 +1,15 @@ -import { Reducer, Ref, ReactElement } from 'react'; +import { Reducer, ReactElement } from 'react'; -import ReactDayPicker, { DayPickerProps } from 'react-day-picker'; -import { ClassNames } from 'react-day-picker/types/ClassNames'; +import { DayPickerProps, DateRange } from 'react-day-picker'; -export interface IDatePickerProps - extends Omit { - innerRef?: Ref; - range?: boolean; -} +export type IDatePickerProps = DayPickerProps; -export interface IDatePickerNavbarProps { - showPreviousButton?: boolean; - showNextButton?: boolean; - month: Date; - fromMonth?: Date; - toMonth?: Date; +export interface IDatePickerCustomNavigationProps { + currentMonth: Date; + setMonth: (date: Date) => void; + startMonth?: Date; + endMonth?: Date; numberOfMonths?: number; - className?: string; - classNames: ClassNames; - onPreviousClick?: () => void; - onNextClick?: () => void; - onMonthChange: (newMonth: Date) => void; } export enum RangeDatePickerAction { @@ -83,21 +72,6 @@ export interface IRangeDatePickerOption { } | null; } -interface IRangeDatePickerChildrenPayloadDatePicker { - modifiers?: DayPickerProps['modifiers']; - initialMonth?: Date; - month: Date; - range?: boolean; - numberOfMonths: number; - fromMonth?: Date; - toMonth?: Date; - selectedDays?: DayPickerProps['selectedDays']; - disabledDays?: DayPickerProps['disabledDays']; - onDayMouseEnter: DayPickerProps['onDayMouseEnter']; - onDayClick(day: Date): void; - onMonthChange(month: Date): void; -} - export interface IRangeDatePickerChildrenPayload { select: { selected: string | number; @@ -107,7 +81,7 @@ export interface IRangeDatePickerChildrenPayload { fromDate?: Date; toDate?: Date; }; - datepicker: IRangeDatePickerChildrenPayloadDatePicker; + datepicker: DayPickerProps; selectedOption?: IRangeDatePickerOption; } @@ -118,5 +92,6 @@ export interface IRangeDatePickerProps { initialToDate?: Date; toMonth?: Date; onChange: (selected: IRangeDatePickerOption | null) => void; + onSelect?: (selected: DateRange | null) => void; children(payload: IRangeDatePickerChildrenPayload): ReactElement; }