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',
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',
}

View File

@ -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<CalendarContextType>(

View File

@ -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;
}
}
`

View File

@ -70,6 +70,7 @@ export const Calendar: React.FC<CalendarProps> & {
const [endDate, setEndDate] = useState<Date | null>(
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<CalendarProps> & {
onDateFocus,
goToPreviousMonths,
goToNextMonths,
goToNextYear,
goToPreviousYear,
goToDate,
} = useDatepicker({
startDate,
endDate,
@ -183,8 +183,9 @@ export const Calendar: React.FC<CalendarProps> & {
onDateHover,
goToPreviousMonths,
goToNextMonths,
goToNextYear,
goToPreviousYear,
goToDate,
changeYearMode,
setChangeYearMode,
}}
>
<TooltipBase

View File

@ -17,8 +17,9 @@ export const Month = ({
month,
firstDayOfWeek,
}: MonthProps) => {
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 (
<>
<MonthHeader monthLabel={monthLabel} size={size} />
<MonthHeader monthLabel={monthLabel} monthNumber={month} size={size} />
<table className={calendarClasses.monthTable}>
<thead>
<WeekdayHeader weekdayLabels={weekdayLabels} />

View File

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

View File

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

View File

@ -77,9 +77,6 @@ export const DateRangePicker: React.FC<DateRangePickerProps> & {
const onStartInputChange = (e: ChangeEvent<HTMLInputElement>) => {
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<DateRangePickerProps> & {
}
}
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<DateRangePickerProps> & {
icon={withCalendar && <CalendarIcon color="primary" />}
// 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<DateRangePickerProps> & {
icon={withCalendar && <CalendarIcon color="primary" />}
// 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<DateRangePickerProps> & {
calendarType,
size,
)}
size={size}
/>
</Portal>
)}