feat: adds year selection dropdown and other minor improvements

This commit is contained in:
jongomez 2023-10-17 14:10:46 +01:00 committed by Jon
parent 69a46e613b
commit e36753b3f4
8 changed files with 166 additions and 61 deletions

View File

@ -8,14 +8,18 @@ export const calendarClasses = {
header: 'lsd-calendar-header', header: 'lsd-calendar-header',
weekDay: 'lsd-calendar__week_day', weekDay: 'lsd-calendar__week_day',
button: 'lsd-calendar__button', button: 'lsd-calendar__button',
row: 'lsd-calendar__row',
changeYear: 'lsd-calendar__change-year', 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', year: 'lsd-calendar-year',
month: 'lsd-calendar-month', month: 'lsd-calendar-month',
day: 'lsd-calendar-day', day: 'lsd-calendar-day',
yearAndIcon: 'lsd-calendar__year-and-icon',
monthAndYear: 'lsd-calendar__month-and-year',
dayContainer: 'lsd-calendar-day__container', dayContainer: 'lsd-calendar-day__container',
dayRange: 'lsd-calendar-day--range', dayRange: 'lsd-calendar-day--range',
daySelected: 'lsd-calendar-day--selected', daySelected: 'lsd-calendar-day--selected',
@ -33,4 +37,6 @@ export const calendarClasses = {
nextMonthButton: 'lsd-calendar__next-month-button', nextMonthButton: 'lsd-calendar__next-month-button',
previousMonthButton: 'lsd-calendar__previous-month-button', previousMonthButton: 'lsd-calendar__previous-month-button',
yearDropdown: 'lsd-calendar__year-dropdown',
} }

View File

@ -16,8 +16,9 @@ export type CalendarContextType = {
onDateSelect: (date: Date) => void onDateSelect: (date: Date) => void
goToPreviousMonths: () => void goToPreviousMonths: () => void
goToNextMonths: () => void goToNextMonths: () => void
goToNextYear: () => void changeYearMode: boolean
goToPreviousYear: () => void setChangeYearMode: (value: boolean) => void
goToDate: (date: Date) => void
} }
export const CalendarContext = React.createContext<CalendarContextType>( export const CalendarContext = React.createContext<CalendarContextType>(

View File

@ -45,36 +45,52 @@ export const CalendarStyles = css`
margin-bottom: 4px; margin-bottom: 4px;
} }
.${calendarClasses.row} {
display: flex;
justify-content: center;
align-items: center;
}
.${calendarClasses.changeYear} { .${calendarClasses.changeYear} {
position: relative;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border: 1px solid rgb(var(--lsd-border-primary)); padding: 2px 0xp 2px 8px;
padding: 2px 6px;
gap: 6px; 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; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
border: none; border: none;
height: 14px;
width: 14px; width: 14px;
padding: 0; padding-left: 5px;
} }
.${calendarClasses.month} { .${calendarClasses.month} {
margin-right: 8px; margin-right: 8px;
} }
.${calendarClasses.monthAndYear} {
display: flex;
align-items: center;
justify-content: center;
}
.${calendarClasses.dayContainer} { .${calendarClasses.dayContainer} {
cursor: pointer; cursor: pointer;
background: transparent; background: transparent;
@ -180,4 +196,45 @@ export const CalendarStyles = css`
.${calendarClasses.monthTable} { .${calendarClasses.monthTable} {
border-collapse: collapse; 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;
}
}
` `

View File

@ -70,6 +70,7 @@ export const Calendar: React.FC<CalendarProps> & {
const [endDate, setEndDate] = useState<Date | null>( const [endDate, setEndDate] = useState<Date | null>(
endDateProp ? safeConvertDate(endDateProp, minDate, maxDate).date : null, endDateProp ? safeConvertDate(endDateProp, minDate, maxDate).date : null,
) )
const [changeYearMode, setChangeYearMode] = useState(false)
useClickAway(ref, (event) => { useClickAway(ref, (event) => {
if (!open) return if (!open) return
@ -108,8 +109,7 @@ export const Calendar: React.FC<CalendarProps> & {
onDateFocus, onDateFocus,
goToPreviousMonths, goToPreviousMonths,
goToNextMonths, goToNextMonths,
goToNextYear, goToDate,
goToPreviousYear,
} = useDatepicker({ } = useDatepicker({
startDate, startDate,
endDate, endDate,
@ -183,8 +183,9 @@ export const Calendar: React.FC<CalendarProps> & {
onDateHover, onDateHover,
goToPreviousMonths, goToPreviousMonths,
goToNextMonths, goToNextMonths,
goToNextYear, goToDate,
goToPreviousYear, changeYearMode,
setChangeYearMode,
}} }}
> >
<TooltipBase <TooltipBase

View File

@ -17,8 +17,9 @@ export const Month = ({
month, month,
firstDayOfWeek, firstDayOfWeek,
}: MonthProps) => { }: MonthProps) => {
const sizeContext = useCalendarContext() const calendarContext = useCalendarContext()
const size = sizeContext?.size ?? _size const size = calendarContext?.size ?? _size
const { days, weekdayLabels, monthLabel } = useMonth({ const { days, weekdayLabels, monthLabel } = useMonth({
year, year,
month, month,
@ -27,7 +28,7 @@ export const Month = ({
return ( return (
<> <>
<MonthHeader monthLabel={monthLabel} size={size} /> <MonthHeader monthLabel={monthLabel} monthNumber={month} size={size} />
<table className={calendarClasses.monthTable}> <table className={calendarClasses.monthTable}>
<thead> <thead>
<WeekdayHeader weekdayLabels={weekdayLabels} /> <WeekdayHeader weekdayLabels={weekdayLabels} />

View File

@ -40,50 +40,91 @@ export const CalendarNavigationButton: FC<CalendarNavigationButtonProps> = ({
type YearControlProps = { type YearControlProps = {
year: string year: string
monthNumber: number
size: 'large' | 'medium' | 'small' size: 'large' | 'medium' | 'small'
} }
export const YearControl: FC<YearControlProps> = ({ year, size }) => { export const YearControl: FC<YearControlProps> = ({
year,
monthNumber,
size,
}) => {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(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 ( return (
<div ref={ref} className={calendarClasses.changeYear}> <div
ref={ref}
className={clsx(
calendarClasses.changeYear,
changeYearMode && calendarClasses.changeYearActive,
)}
onClick={() => {
setChangeYearMode(!changeYearMode)
}}
>
<div className={clsx(calendarClasses.year, calendarClasses.yearAndIcon)}>
<Typography <Typography
component="span" component="span"
className={calendarClasses.year}
variant={size === 'large' ? 'label1' : 'label2'} variant={size === 'large' ? 'label1' : 'label2'}
> >
{year} {year}
</Typography> </Typography>
<div className={calendarClasses.row}>
<IconButton <div className={calendarClasses.changeYearIconContainer}>
onClick={() => { {changeYearMode ? (
goToNextYear()
}}
className={calendarClasses.changeYearButton}
>
<ArrowUpIcon color="primary" /> <ArrowUpIcon color="primary" />
</IconButton> ) : (
<IconButton
onClick={() => {
goToPreviousYear()
}}
className={calendarClasses.changeYearButton}
>
<ArrowDownIcon color="primary" /> <ArrowDownIcon color="primary" />
</IconButton> )}
</div> </div>
</div> </div>
{changeYearMode && (
<div className={calendarClasses.yearDropdown}>
{yearsList.map((year) => (
<div
key={year}
className={calendarClasses.year}
onClick={() => handleYearClick(year)}
>
<Typography
component="span"
variant={size === 'large' ? 'label1' : 'label2'}
>
{year}
</Typography>
</div>
))}
</div>
)}
</div>
) )
} }
type MonthHeaderProps = { type MonthHeaderProps = {
monthLabel: string monthLabel: string
monthNumber: number
size: 'large' | 'medium' | 'small' size: 'large' | 'medium' | 'small'
} }
export const MonthHeader: FC<MonthHeaderProps> = ({ monthLabel, size }) => { export const MonthHeader: FC<MonthHeaderProps> = ({
monthLabel,
monthNumber,
size,
}) => {
const { goToPreviousMonths, goToNextMonths } = useCalendarContext() const { goToPreviousMonths, goToNextMonths } = useCalendarContext()
const [month, year] = monthLabel.split(' ') const [month, year] = monthLabel.split(' ')
@ -94,7 +135,7 @@ export const MonthHeader: FC<MonthHeaderProps> = ({ monthLabel, size }) => {
onClick={goToPreviousMonths} onClick={goToPreviousMonths}
className={calendarClasses.previousMonthButton} className={calendarClasses.previousMonthButton}
/> />
<div className={calendarClasses.row}> <div className={calendarClasses.monthAndYear}>
<Typography <Typography
className={calendarClasses.month} className={calendarClasses.month}
component="span" component="span"
@ -103,7 +144,7 @@ export const MonthHeader: FC<MonthHeaderProps> = ({ monthLabel, size }) => {
{month} {month}
</Typography> </Typography>
<YearControl year={year} size={size} /> <YearControl year={year} monthNumber={monthNumber} size={size} />
</div> </div>
<CalendarNavigationButton <CalendarNavigationButton
direction="next" direction="next"

View File

@ -10,14 +10,12 @@ export default {
name: 'enum', name: 'enum',
value: ['small', 'medium', 'large'], value: ['small', 'medium', 'large'],
}, },
defaultValue: 'large',
}, },
variant: { variant: {
type: { type: {
name: 'enum', name: 'enum',
value: ['outlined', 'outlined-bottom'], value: ['outlined', 'outlined-bottom'],
}, },
defaultValue: 'large',
}, },
}, },
} as Meta } as Meta

View File

@ -77,9 +77,6 @@ export const DateRangePicker: React.FC<DateRangePickerProps> & {
const onStartInputChange = (e: ChangeEvent<HTMLInputElement>) => { const onStartInputChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!endInput.value || isValidRange(e.target.value, endInput.value)) { if (!endInput.value || isValidRange(e.target.value, endInput.value)) {
startInput.onChange(e) startInput.onChange(e)
// Switch to endDate calendar when the startDate is set.
setCalendarType('endDate')
} }
} }
@ -89,9 +86,13 @@ export const DateRangePicker: React.FC<DateRangePickerProps> & {
} }
} }
const calendarStartDateChange = (date: Date) => const calendarStartDateChange = (date: Date) => {
startInput.setValue(dateToISODateString(removeDateTimezoneOffset(date))) startInput.setValue(dateToISODateString(removeDateTimezoneOffset(date)))
// Switch to endDate calendar when the startDate is set.
setCalendarType('endDate')
}
const calendarEndDateChange = (date: Date) => const calendarEndDateChange = (date: Date) =>
endInput.setValue(dateToISODateString(removeDateTimezoneOffset(date))) endInput.setValue(dateToISODateString(removeDateTimezoneOffset(date)))
@ -143,7 +144,7 @@ export const DateRangePicker: React.FC<DateRangePickerProps> & {
icon={withCalendar && <CalendarIcon color="primary" />} icon={withCalendar && <CalendarIcon color="primary" />}
// The DateField component is only controlled when the value prop is provided OR the calendar is open. // The DateField component is only controlled when the value prop is provided OR the calendar is open.
value={ value={
isStartValueControlled || isStartDateCalendar isStartValueControlled || isCalendarOpen
? startInput.value ? startInput.value
: undefined : undefined
} }
@ -164,9 +165,7 @@ export const DateRangePicker: React.FC<DateRangePickerProps> & {
icon={withCalendar && <CalendarIcon color="primary" />} icon={withCalendar && <CalendarIcon color="primary" />}
// The DateField component is only controlled when the value prop is provided OR the calendar is open. // The DateField component is only controlled when the value prop is provided OR the calendar is open.
value={ value={
isEndValueControlled || isEndDateCalendar isEndValueControlled || isCalendarOpen ? endInput.value : undefined
? endInput.value
: undefined
} }
onIconClick={() => onIconClick={() =>
setCalendarType((currentCalendarType) => setCalendarType((currentCalendarType) =>
@ -217,6 +216,7 @@ export const DateRangePicker: React.FC<DateRangePickerProps> & {
calendarType, calendarType,
size, size,
)} )}
size={size}
/> />
</Portal> </Portal>
)} )}