diff --git a/packages/lsd-react/src/components/Calendar/Calendar.classes.ts b/packages/lsd-react/src/components/Calendar/Calendar.classes.ts index 86c09dd..ec0bf99 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.classes.ts +++ b/packages/lsd-react/src/components/Calendar/Calendar.classes.ts @@ -8,14 +8,18 @@ export const calendarClasses = { header: 'lsd-calendar-header', weekDay: 'lsd-calendar__week_day', button: 'lsd-calendar__button', - row: 'lsd-calendar__row', changeYear: 'lsd-calendar__change-year', - changeYearButton: 'lsd-calendar__change-year__button', + changeYearActive: 'lsd-calendar__change-year--active', + changeYearIconContainer: 'lsd-calendar__change-year-icon-container', year: 'lsd-calendar-year', month: 'lsd-calendar-month', day: 'lsd-calendar-day', + yearAndIcon: 'lsd-calendar__year-and-icon', + + monthAndYear: 'lsd-calendar__month-and-year', + dayContainer: 'lsd-calendar-day__container', dayRange: 'lsd-calendar-day--range', daySelected: 'lsd-calendar-day--selected', @@ -33,4 +37,6 @@ export const calendarClasses = { nextMonthButton: 'lsd-calendar__next-month-button', previousMonthButton: 'lsd-calendar__previous-month-button', + + yearDropdown: 'lsd-calendar__year-dropdown', } diff --git a/packages/lsd-react/src/components/Calendar/Calendar.context.ts b/packages/lsd-react/src/components/Calendar/Calendar.context.ts index b9c4690..8b62010 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.context.ts +++ b/packages/lsd-react/src/components/Calendar/Calendar.context.ts @@ -16,8 +16,9 @@ export type CalendarContextType = { onDateSelect: (date: Date) => void goToPreviousMonths: () => void goToNextMonths: () => void - goToNextYear: () => void - goToPreviousYear: () => void + changeYearMode: boolean + setChangeYearMode: (value: boolean) => void + goToDate: (date: Date) => void } export const CalendarContext = React.createContext( diff --git a/packages/lsd-react/src/components/Calendar/Calendar.styles.ts b/packages/lsd-react/src/components/Calendar/Calendar.styles.ts index 596e833..933bce6 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.styles.ts +++ b/packages/lsd-react/src/components/Calendar/Calendar.styles.ts @@ -45,36 +45,52 @@ export const CalendarStyles = css` margin-bottom: 4px; } - .${calendarClasses.row} { - display: flex; - justify-content: center; - align-items: center; - } - .${calendarClasses.changeYear} { + position: relative; display: flex; justify-content: center; align-items: center; - border: 1px solid rgb(var(--lsd-border-primary)); - padding: 2px 6px; + padding: 2px 0xp 2px 8px; gap: 6px; + + /* The transparent border prevents slight layout shifts when the hover border shows up. */ + border: 1px solid transparent; } - .${calendarClasses.changeYearButton} { + .${calendarClasses.changeYearActive} { + .${calendarClasses.year} { + padding: 5px 0px 5px 10px; + } + + .${calendarClasses.yearAndIcon} { + border: 1px solid rgb(var(--lsd-border-primary)); + } + + .${calendarClasses.changeYearIconContainer} { + padding-right: 5px; + } + } + + .${calendarClasses.changeYearIconContainer} { display: flex; justify-content: center; align-items: center; cursor: pointer; border: none; - height: 14px; width: 14px; - padding: 0; + padding-left: 5px; } .${calendarClasses.month} { margin-right: 8px; } + .${calendarClasses.monthAndYear} { + display: flex; + align-items: center; + justify-content: center; + } + .${calendarClasses.dayContainer} { cursor: pointer; background: transparent; @@ -180,4 +196,45 @@ export const CalendarStyles = css` .${calendarClasses.monthTable} { border-collapse: collapse; } + + .${calendarClasses.yearDropdown} { + box-sizing: border-box; + + position: absolute; + top: 100%; + left: 0; + + max-height: 200px; + overflow-y: auto; + width: 100%; + + border: 1px solid rgb(var(--lsd-border-primary)); + z-index: 1; + + .${calendarClasses.year} { + border-bottom: 1px solid rgb(var(--lsd-border-primary)); + } + } + + .${calendarClasses.year} { + display: flex; + cursor: pointer; + transition: background-color 0.2s; + align-items: center; + + background: rgb(var(--lsd-surface-primary)); + + :hover { + text-decoration: underline; + padding: 5px 0px 5px 10px; + } + } + + .${calendarClasses.yearAndIcon}:hover { + border: 1px solid rgb(var(--lsd-border-primary)); + + .${calendarClasses.changeYearIconContainer} { + padding-right: 5px; + } + } ` diff --git a/packages/lsd-react/src/components/Calendar/Calendar.tsx b/packages/lsd-react/src/components/Calendar/Calendar.tsx index ff86d14..8502e13 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.tsx +++ b/packages/lsd-react/src/components/Calendar/Calendar.tsx @@ -70,6 +70,7 @@ export const Calendar: React.FC & { const [endDate, setEndDate] = useState( endDateProp ? safeConvertDate(endDateProp, minDate, maxDate).date : null, ) + const [changeYearMode, setChangeYearMode] = useState(false) useClickAway(ref, (event) => { if (!open) return @@ -108,8 +109,7 @@ export const Calendar: React.FC & { onDateFocus, goToPreviousMonths, goToNextMonths, - goToNextYear, - goToPreviousYear, + goToDate, } = useDatepicker({ startDate, endDate, @@ -183,8 +183,9 @@ export const Calendar: React.FC & { onDateHover, goToPreviousMonths, goToNextMonths, - goToNextYear, - goToPreviousYear, + goToDate, + changeYearMode, + setChangeYearMode, }} > { - const sizeContext = useCalendarContext() - const size = sizeContext?.size ?? _size + const calendarContext = useCalendarContext() + const size = calendarContext?.size ?? _size + const { days, weekdayLabels, monthLabel } = useMonth({ year, month, @@ -27,7 +28,7 @@ export const Month = ({ return ( <> - + diff --git a/packages/lsd-react/src/components/Calendar/MonthHelpers.tsx b/packages/lsd-react/src/components/Calendar/MonthHelpers.tsx index 44e5ce9..6fc901c 100644 --- a/packages/lsd-react/src/components/Calendar/MonthHelpers.tsx +++ b/packages/lsd-react/src/components/Calendar/MonthHelpers.tsx @@ -40,50 +40,91 @@ export const CalendarNavigationButton: FC = ({ type YearControlProps = { year: string + monthNumber: number size: 'large' | 'medium' | 'small' } -export const YearControl: FC = ({ year, size }) => { +export const YearControl: FC = ({ + year, + monthNumber, + size, +}) => { const ref = useRef(null) - const { goToNextYear, goToPreviousYear } = useCalendarContext() + const { goToDate, changeYearMode, setChangeYearMode } = useCalendarContext() + + useClickAway(ref, () => { + setChangeYearMode(false) + }) + + const handleYearClick = (selectedYear: number) => { + const selectedDate = new Date(selectedYear, monthNumber, 1) + goToDate(selectedDate) + setChangeYearMode(false) + } + + const yearsList = Array.from({ length: 101 }, (_, i) => 1950 + i) return ( -
- - {year} - -
- { - goToNextYear() - }} - className={calendarClasses.changeYearButton} +
{ + setChangeYearMode(!changeYearMode) + }} + > +
+ - - - { - goToPreviousYear() - }} - className={calendarClasses.changeYearButton} - > - - + {year} + + +
+ {changeYearMode ? ( + + ) : ( + + )} +
+ + {changeYearMode && ( +
+ {yearsList.map((year) => ( +
handleYearClick(year)} + > + + {year} + +
+ ))} +
+ )}
) } type MonthHeaderProps = { monthLabel: string + monthNumber: number size: 'large' | 'medium' | 'small' } -export const MonthHeader: FC = ({ monthLabel, size }) => { +export const MonthHeader: FC = ({ + monthLabel, + monthNumber, + size, +}) => { const { goToPreviousMonths, goToNextMonths } = useCalendarContext() const [month, year] = monthLabel.split(' ') @@ -94,7 +135,7 @@ export const MonthHeader: FC = ({ monthLabel, size }) => { onClick={goToPreviousMonths} className={calendarClasses.previousMonthButton} /> -
+
= ({ monthLabel, size }) => { {month} - +
& { const onStartInputChange = (e: ChangeEvent) => { if (!endInput.value || isValidRange(e.target.value, endInput.value)) { startInput.onChange(e) - - // Switch to endDate calendar when the startDate is set. - setCalendarType('endDate') } } @@ -89,9 +86,13 @@ export const DateRangePicker: React.FC & { } } - const calendarStartDateChange = (date: Date) => + const calendarStartDateChange = (date: Date) => { startInput.setValue(dateToISODateString(removeDateTimezoneOffset(date))) + // Switch to endDate calendar when the startDate is set. + setCalendarType('endDate') + } + const calendarEndDateChange = (date: Date) => endInput.setValue(dateToISODateString(removeDateTimezoneOffset(date))) @@ -143,7 +144,7 @@ export const DateRangePicker: React.FC & { icon={withCalendar && } // The DateField component is only controlled when the value prop is provided OR the calendar is open. value={ - isStartValueControlled || isStartDateCalendar + isStartValueControlled || isCalendarOpen ? startInput.value : undefined } @@ -164,9 +165,7 @@ export const DateRangePicker: React.FC & { icon={withCalendar && } // The DateField component is only controlled when the value prop is provided OR the calendar is open. value={ - isEndValueControlled || isEndDateCalendar - ? endInput.value - : undefined + isEndValueControlled || isCalendarOpen ? endInput.value : undefined } onIconClick={() => setCalendarType((currentCalendarType) => @@ -217,6 +216,7 @@ export const DateRangePicker: React.FC & { calendarType, size, )} + size={size} /> )}