mirror of https://github.com/acid-info/lsd.git
feat: adds year selection dropdown and other minor improvements
This commit is contained in:
parent
69a46e613b
commit
e36753b3f4
|
@ -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',
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>(
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in New Issue