diff --git a/packages/ui/src/components/DateInputV2/CalendarDaily.tsx b/packages/ui/src/components/DateInputV2/CalendarDaily.tsx new file mode 100644 index 0000000000..75dff6984a --- /dev/null +++ b/packages/ui/src/components/DateInputV2/CalendarDaily.tsx @@ -0,0 +1,227 @@ +import styled from '@emotion/styled' +import type { MouseEvent as MouseEventReact } from 'react' +import { useContext, useState } from 'react' +import { Button } from '../Button' +import { Row } from '../Row' +import { Text } from '../Text' +import { DateInputContext } from './Context' +import { + CALENDAR_WEEKS, + getMonthDays, + getMonthFirstDay, + getNextMonth, + getPreviousMonth, + isSameDay, +} from './helpers' + +const Day = styled(Button)` +height: 26px; +width: 100%; +padding: 0; + +&.rangeButton { + background-color:${({ theme }) => theme.colors.primary.background}; +} +` +const DayName = styled(Text)` +height: 26px; +width: 100%; +` + +export const Daily = ({ disabled }: { disabled: boolean }) => { + const { + value, + yearToShow, + monthToShow, + setValue, + setMonthToShow, + onChange, + setYearToShow, + excludeDates, + minDate, + maxDate, + DAYS, + selectsRange, + range, + setRange, + } = useContext(DateInputContext) + + const [rangeState, setRangeState] = useState<'start' | 'none' | 'done'>( + range?.start ? 'start' : 'none', + ) // Used when selectsRange is True. Kow the current state of the range: none when start date not selected, start when start date is selected, done when start & end date selected + + const [hoveredDate, setHoveredDate] = useState(null) + + const monthDays = getMonthDays(monthToShow, yearToShow) // Number of days in the month + + const daysFromPreviousMonth = + (getMonthFirstDay(monthToShow, yearToShow) - 1 + 6) % 7 // Number of days from the previous month to show. Shift to align Monday start + const daysFromNextMonth = + CALENDAR_WEEKS * 7 - (daysFromPreviousMonth + monthDays) // We want to display 6 CALENDAR_WEEKS lines, so we show days from the next month + + const [previousMonth, prevMonthYear] = getPreviousMonth( + monthToShow, + yearToShow, + ) + + const [nextMonth, nextMonthYear] = getNextMonth(monthToShow, yearToShow) + const previousMonthDays = getMonthDays(previousMonth, prevMonthYear) + + // Get the dates to be displayed from the previous month + const prevMonthDates = Array(daysFromPreviousMonth) + .keys() + .map((_, index) => ({ + day: index + 1 + (previousMonthDays - daysFromPreviousMonth), + month: -1, + })) + + // Get the dates to be displayed from the current month + const currentMonthDates = Array(monthDays) + .keys() + .map((_, index) => ({ day: index + 1, month: 0 })) + + // Get the dates to be displayed from the next month + const nextMonthDates = Array(daysFromNextMonth) + .keys() + .map((_, index) => ({ day: index + 1, month: 1 })) + + const allDaysToShow = [ + ...prevMonthDates, + ...currentMonthDates, + ...nextMonthDates, + ] // Array of the days to display { day : day n°, isCurrentMonth: if it is the current day} + + return ( + + {Object.entries(DAYS).map(day => ( + + {day[1]} + + ))} + {allDaysToShow.map(data => { + const constructedDate = new Date( + yearToShow, + monthToShow - 1 + data.month, + data.day, + ) + const isExcluded = excludeDates + ? excludeDates + .map(date => isSameDay(constructedDate, date)) + .includes(true) + : false + + // Whether the date < minDate or date > maxDate + const isOutsideRange = + !!(minDate && constructedDate < minDate) || + !!(maxDate && constructedDate > maxDate) + + // Whether the date is selected + const isSelected = + (value && isSameDay(constructedDate, new Date(value))) || + (range?.end && isSameDay(constructedDate, range.end)) || + (range?.start && isSameDay(constructedDate, range.start)) + + // Whether the date is after the start date - useful when selectsRange is set to true + const isAfterStartDate = + selectsRange && range?.start && constructedDate > range.start + + const isInHoveredRange = + (selectsRange && + range?.start && + constructedDate > range.start && + hoveredDate && + constructedDate < hoveredDate) || + (range?.start && + range.end && + constructedDate < range.end && + constructedDate > range.start) + + const getNewDate = () => { + // Clicked on a day from the previous month + if (data.month !== 0 && data.day > 15) { + setMonthToShow(previousMonth) + setYearToShow(prevMonthYear) + + return new Date(prevMonthYear, previousMonth - 1, data.day) + } + + // Clicked on a day from the next month + if (data.month !== 0 && data.day < 15) { + setMonthToShow(nextMonth) + setYearToShow(nextMonthYear) + + return new Date(nextMonthYear, nextMonth - 1, data.day) + } + + return new Date(yearToShow, monthToShow - 1, data.day) + } + + const onClickRange = (event: MouseEventReact, newDate: Date) => { + if (selectsRange) { + // Selecting start date + if (rangeState === 'none') { + setRange?.({ start: newDate, end: null }) + onChange?.([newDate, null], event) + setRangeState('start') + } + + // Selecting end date + else if (isAfterStartDate) { + setRange?.({ start: range.start, end: newDate }) + onChange?.([range.start, newDate], event) + setRangeState('done') + } else { + // End date before start + setRange?.({ start: newDate, end: null }) + onChange?.([newDate, null], event) + } + } + } + const createTestId = () => { + if (isInHoveredRange) return 'rangeButton' + if (data.month === -1) return 'dayLastMonth' + if (data.month === 1) return 'dayNextMonth' + + return undefined + } + + return ( + { + if (!isExcluded && !isOutsideRange) { + const newDate = getNewDate() + + if (selectsRange) { + onClickRange(event, newDate) + } else { + setValue(newDate) + onChange?.(newDate, event) + } + } + }} + onMouseEnter={() => setHoveredDate(constructedDate)} + onMouseLeave={() => setHoveredDate(null)} + > + + {data.day} + + + ) + })} + + ) +} diff --git a/packages/ui/src/components/DateInputV2/CalendarMonthly.tsx b/packages/ui/src/components/DateInputV2/CalendarMonthly.tsx new file mode 100644 index 0000000000..53d768f322 --- /dev/null +++ b/packages/ui/src/components/DateInputV2/CalendarMonthly.tsx @@ -0,0 +1,132 @@ +import styled from '@emotion/styled' +import type { MouseEvent as MouseEventReact } from 'react' +import { useContext, useState } from 'react' +import { Button } from '../Button' +import { Row } from '../Row' +import { Text } from '../Text' +import { DateInputContext } from './Context' +import { isSameMonth } from './helpers' + +const Month = styled(Button)` +height: 26px; +width: 100%; +padding: 0; + +&.rangeButton { + background-color:${({ theme }) => theme.colors.primary.background}; +} +` + +export const Monthly = ({ disabled }: { disabled: boolean }) => { + const { + yearToShow, + setValue, + setMonthToShow, + MONTHS, + onChange, + selectsRange, + setRange, + range, + minDate, + maxDate, + excludeDates, + value, + } = useContext(DateInputContext) + const [rangeState, setRangeState] = useState<'start' | 'none' | 'done'>( + range?.start ? 'start' : 'none', + ) // Used when selectsRange is True. It is used to know the current state of the range: none when start date not selected, start when start date is selected, done when start & end date selected + const [hoveredDate, setHoveredDate] = useState(null) + + return ( + + {Object.entries(MONTHS).map((month, index) => { + const constructedDate = new Date(yearToShow, index, 1) + + const isExcluded = excludeDates + ? excludeDates + .map(date => isSameMonth(constructedDate, date)) + .includes(true) + : false + + const isOutsideRange = + !!(minDate && constructedDate < minDate) || + !!(maxDate && constructedDate > maxDate) + + const isAfterStartDate = + selectsRange && range?.start && constructedDate > range.start + + const isInHoveredRange = + (selectsRange && + range?.start && + constructedDate > range.start && + hoveredDate && + constructedDate < hoveredDate) || + (range?.start && + range.end && + constructedDate < range.end && + constructedDate > range.start) + + const isSelected = + (value && isSameMonth(constructedDate, value)) || + (range?.start && isSameMonth(constructedDate, range.start)) || + (range?.end && isSameMonth(constructedDate, range.end)) + + const onClickRange = (event: MouseEventReact, newDate: Date) => { + // Must wrap inside this if otherwise TypeScript doesn't get the right type for onChange + if (selectsRange) { + // Selecting start date + if (rangeState === 'none') { + setRange?.({ start: newDate, end: null }) + onChange?.([newDate, null], event) + setRangeState('start') + } + + // Selecting end date + else if (isAfterStartDate) { + setRange?.({ start: range.start, end: newDate }) + onChange?.([range.start, newDate], event) + setRangeState('done') + } else { + // End date before start + setRange?.({ start: newDate, end: null }) + onChange?.([newDate, null], event) + } + } + } + + return ( + { + if (!isExcluded && !isOutsideRange) { + if (selectsRange) { + onClickRange(event, constructedDate) + } else { + setMonthToShow(index + 1) + setValue(constructedDate) + onChange?.(constructedDate, event) + } + } + }} + onMouseEnter={() => setHoveredDate(constructedDate)} + onMouseLeave={() => setHoveredDate(null)} + > + + {month[1]} + + + ) + })} + + ) +} diff --git a/packages/ui/src/components/DateInputV2/Popup.tsx b/packages/ui/src/components/DateInputV2/Popup.tsx index c90b51b3f1..e24892af09 100644 --- a/packages/ui/src/components/DateInputV2/Popup.tsx +++ b/packages/ui/src/components/DateInputV2/Popup.tsx @@ -1,28 +1,14 @@ import styled from '@emotion/styled' -import type { - Dispatch, - MouseEvent as MouseEventReact, - ReactNode, - RefObject, - SetStateAction, -} from 'react' -import { useContext, useEffect, useRef, useState } from 'react' +import type { Dispatch, ReactNode, RefObject, SetStateAction } from 'react' +import { useContext, useEffect, useRef } from 'react' import { Button } from '../Button' import { Popup } from '../Popup' -import { Row } from '../Row' import { Stack } from '../Stack' import { Text } from '../Text' +import { Daily } from './CalendarDaily' +import { Monthly } from './CalendarMonthly' import { DateInputContext } from './Context' -import { - CALENDAR_WEEKS, - POPUP_WIDTH, - getMonthDays, - getMonthFirstDay, - getNextMonth, - getPreviousMonth, - isSameDay, - isSameMonth, -} from './helpers' +import { POPUP_WIDTH, getNextMonth, getPreviousMonth } from './helpers' type PopupProps = { children: ReactNode @@ -31,20 +17,6 @@ type PopupProps = { refInput: RefObject } -const ButtonSelectDayMonth = styled(Button)` -height: 26px; -width: 100%; -padding: 0; - -&.rangeButton { - background-color:${({ theme }) => theme.colors.primary.background}; -} -` -const DayName = styled(Text)` -height: 26px; -width: 100%; -` - const StyledPopup = styled(Popup)` width: 100%; background-color: ${({ theme }) => theme.colors.other.elevation.background.raised}; @@ -69,318 +41,6 @@ const handleClickOutside = ( } } -const Monthly = ({ disabled }: { disabled: boolean }) => { - const { - yearToShow, - setValue, - setMonthToShow, - MONTHS, - onChange, - selectsRange, - setRange, - range, - minDate, - maxDate, - excludeDates, - value, - } = useContext(DateInputContext) - const [rangeState, setRangeState] = useState<'start' | 'none' | 'done'>( - range?.start ? 'start' : 'none', - ) // Used when selectsRange is True. It is used to know the current state of the range: none when start date not selected, start when start date is selected, done when start & end date selected - const [hoveredDate, setHoveredDate] = useState(null) - - return ( - - {Object.entries(MONTHS).map((month, index) => { - const constructedDate = new Date(yearToShow, index, 1) - - const isExcluded = excludeDates - ? excludeDates - .map(date => isSameMonth(constructedDate, date)) - .includes(true) - : false - - const isOutsideRange = - !!(minDate && constructedDate < minDate) || - !!(maxDate && constructedDate > maxDate) - - const isAfterStartDate = - selectsRange && range?.start && constructedDate > range.start - - const isInHoveredRange = - (selectsRange && - range?.start && - constructedDate > range.start && - hoveredDate && - constructedDate < hoveredDate) || - (range?.start && - range.end && - constructedDate < range.end && - constructedDate > range.start) - - const isSelected = - (value && isSameMonth(constructedDate, value)) || - (range?.start && isSameMonth(constructedDate, range.start)) || - (range?.end && isSameMonth(constructedDate, range.end)) - - const onClickRange = (event: MouseEventReact, newDate: Date) => { - // Must wrap inside this if otherwise TypeScript doesn't get the right type for onChange - if (selectsRange) { - // Selecting start date - if (rangeState === 'none') { - setRange?.({ start: newDate, end: null }) - onChange?.([newDate, null], event) - setRangeState('start') - } - - // Selecting end date - else if (isAfterStartDate) { - setRange?.({ start: range.start, end: newDate }) - onChange?.([range.start, newDate], event) - setRangeState('done') - } else { - // End date before start - setRange?.({ start: newDate, end: null }) - onChange?.([newDate, null], event) - } - } - } - - return ( - { - if (!isExcluded && !isOutsideRange) { - if (selectsRange) { - onClickRange(event, constructedDate) - } else { - setMonthToShow(index + 1) - setValue(constructedDate) - onChange?.(constructedDate, event) - } - } - }} - onMouseEnter={() => setHoveredDate(constructedDate)} - onMouseLeave={() => setHoveredDate(null)} - > - - {month[1]} - - - ) - })} - - ) -} - -const Daily = ({ disabled }: { disabled: boolean }) => { - const { - value, - yearToShow, - monthToShow, - setValue, - setMonthToShow, - onChange, - setYearToShow, - excludeDates, - minDate, - maxDate, - DAYS, - selectsRange, - range, - setRange, - } = useContext(DateInputContext) - - const [rangeState, setRangeState] = useState<'start' | 'none' | 'done'>( - range?.start ? 'start' : 'none', - ) // Used when selectsRange is True. It is used to know the current state of the range: none when start date not selected, start when start date is selected, done when start & end date selected - - const [hoveredDate, setHoveredDate] = useState(null) - - const monthDays = getMonthDays(monthToShow, yearToShow) // Number of days in the month - - const daysFromPreviousMonth = - (getMonthFirstDay(monthToShow, yearToShow) - 1 + 6) % 7 // Number of days from the previous month to show. Shift to align Monday start - const daysFromNextMonth = - CALENDAR_WEEKS * 7 - (daysFromPreviousMonth + monthDays) // We want to display 6 CALENDAR_WEEKS lines, so we show days from the next month - - const [previousMonth, prevMonthYear] = getPreviousMonth( - monthToShow, - yearToShow, - ) - - const [nextMonth, nextMonthYear] = getNextMonth(monthToShow, yearToShow) - const previousMonthDays = getMonthDays(previousMonth, prevMonthYear) - - // Get the dates to be displayed from the previous month - const prevMonthDates = Array(daysFromPreviousMonth) - .keys() - .map((_, index) => ({ - day: index + 1 + (previousMonthDays - daysFromPreviousMonth), - month: -1, - })) - - // Get the dates to be displayed from the current month - const currentMonthDates = Array(monthDays) - .keys() - .map((_, index) => ({ day: index + 1, month: 0 })) - - // Get the dates to be displayed from the next month - const nextMonthDates = Array(daysFromNextMonth) - .keys() - .map((_, index) => ({ day: index + 1, month: 1 })) - - const allDaysToShow = [ - ...prevMonthDates, - ...currentMonthDates, - ...nextMonthDates, - ] // Array of the days to display { day : day n°, isCurrentMonth: if it is the current day} - - return ( - - {Object.entries(DAYS).map(day => ( - - {day[1]} - - ))} - {allDaysToShow.map(data => { - const constructedDate = new Date( - yearToShow, - monthToShow - 1 + data.month, - data.day, - ) - const isExcluded = excludeDates - ? excludeDates - .map(date => isSameDay(constructedDate, date)) - .includes(true) - : false - - // Whether the date < minDate or date > maxDate - const isOutsideRange = - !!(minDate && constructedDate < minDate) || - !!(maxDate && constructedDate > maxDate) - - // Whether the date is selected - const isSelected = - (value && isSameDay(constructedDate, new Date(value))) || - (range?.end && isSameDay(constructedDate, range.end)) || - (range?.start && isSameDay(constructedDate, range.start)) - - // Whether the date is after the start date - useful when selectsRange is set to true - const isAfterStartDate = - selectsRange && range?.start && constructedDate > range.start - - const isInHoveredRange = - (selectsRange && - range?.start && - constructedDate > range.start && - hoveredDate && - constructedDate < hoveredDate) || - (range?.start && - range.end && - constructedDate < range.end && - constructedDate > range.start) - - const getNewDate = () => { - // Clicked on a day from the previous month - if (data.month !== 0 && data.day > 15) { - setMonthToShow(previousMonth) - setYearToShow(prevMonthYear) - - return new Date(prevMonthYear, previousMonth - 1, data.day) - } - - // Clicked on a day from the next month - if (data.month !== 0 && data.day < 15) { - setMonthToShow(nextMonth) - setYearToShow(nextMonthYear) - - return new Date(nextMonthYear, nextMonth - 1, data.day) - } - - return new Date(yearToShow, monthToShow - 1, data.day) - } - - const onClickRange = (event: MouseEventReact, newDate: Date) => { - if (selectsRange) { - // Selecting start date - if (rangeState === 'none') { - setRange?.({ start: newDate, end: null }) - onChange?.([newDate, null], event) - setRangeState('start') - } - - // Selecting end date - else if (isAfterStartDate) { - setRange?.({ start: range.start, end: newDate }) - onChange?.([range.start, newDate], event) - setRangeState('done') - } else { - // End date before start - setRange?.({ start: newDate, end: null }) - onChange?.([newDate, null], event) - } - } - } - const createTestId = () => { - if (isInHoveredRange) return 'rangeButton' - if (data.month === -1) return 'dayLastMonth' - if (data.month === 1) return 'dayNextMonth' - - return undefined - } - - return ( - { - if (!isExcluded && !isOutsideRange) { - const newDate = getNewDate() - - if (selectsRange) { - onClickRange(event, newDate) - } else { - setValue(newDate) - onChange?.(newDate, event) - } - } - }} - onMouseEnter={() => setHoveredDate(constructedDate)} - onMouseLeave={() => setHoveredDate(null)} - > - - {data.day} - - - ) - })} - - ) -} - const PopupContent = () => { const { showMonthYearPicker, @@ -493,6 +153,7 @@ export const CalendarPopup = ({ debounceDelay={0} maxWidth={POPUP_WIDTH} disableAnimation + align="start" > {children} diff --git a/packages/ui/src/components/DateInputV2/__stories__/Range.stories.tsx b/packages/ui/src/components/DateInputV2/__stories__/Range.stories.tsx index ddcfa4a3d4..f927b2673c 100644 --- a/packages/ui/src/components/DateInputV2/__stories__/Range.stories.tsx +++ b/packages/ui/src/components/DateInputV2/__stories__/Range.stories.tsx @@ -1,4 +1,5 @@ import type { StoryFn } from '@storybook/react' +import type { ComponentProps } from 'react' import { useState } from 'react' import { DateInputV2 } from '..' import { Stack } from '../../Stack' @@ -17,7 +18,7 @@ const months = [ 'November', 'December', ] -export const Range: StoryFn = args => { +export const Range: StoryFn> = args => { const [startDate, setStartDate] = useState(null) const [endDate, setEndDate] = useState(null)