Since the addition of tertiary-text variant, the original tertiary variant has seen its background color changing from transparent to greyscale-100, the change has impacted lots of components making them look odd. So that's why I updated most of old tertiary to tertiary-text. Fixes #223
339 lines
11 KiB
TypeScript
339 lines
11 KiB
TypeScript
import React, { forwardRef, Ref, useMemo, useRef } from "react";
|
|
import {
|
|
CalendarDate,
|
|
createCalendar,
|
|
DateValue,
|
|
GregorianCalendar,
|
|
toCalendar,
|
|
} from "@internationalized/date";
|
|
import {
|
|
DateFormatterOptions,
|
|
useDateFormatter,
|
|
useLocale,
|
|
} from "@react-aria/i18n";
|
|
import {
|
|
CalendarState,
|
|
RangeCalendarState,
|
|
useCalendarState,
|
|
useRangeCalendarState,
|
|
} from "@react-stately/calendar";
|
|
import {
|
|
CalendarAria,
|
|
useCalendar,
|
|
useRangeCalendar,
|
|
} from "@react-aria/calendar";
|
|
import {
|
|
useSelect,
|
|
UseSelectReturnValue,
|
|
UseSelectStateChange,
|
|
} from "downshift";
|
|
import classNames from "classnames";
|
|
import { CalendarProps, RangeCalendarProps } from "react-aria";
|
|
import { range } from ":/utils";
|
|
import { Button } from ":/components/Button";
|
|
import { CalendarGrid } from ":/components/Forms/DatePicker/CalendarGrid";
|
|
import { useCunningham } from ":/components/Provider";
|
|
|
|
// todo to be factorized with the select component
|
|
interface Option {
|
|
value: number;
|
|
label: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
// todo to be factorized with the select component
|
|
const optionToString = (option: Option | null) => {
|
|
return option ? option.label : "";
|
|
};
|
|
|
|
type DropdownValuesProps = {
|
|
options: Array<Option>;
|
|
downShift: UseSelectReturnValue<Option>;
|
|
};
|
|
|
|
// todo to be factorized with the select component
|
|
const DropdownValues = ({ options, downShift }: DropdownValuesProps) => (
|
|
<div
|
|
className={classNames("c__calendar__menu", {
|
|
"c__calendar__menu--opened": downShift.isOpen,
|
|
})}
|
|
{...downShift.getMenuProps()}
|
|
>
|
|
<ul>
|
|
{downShift.isOpen &&
|
|
options.map((item, index) => (
|
|
<li
|
|
key={`${item.value}${index}`}
|
|
{...downShift.getItemProps({
|
|
item,
|
|
index,
|
|
disabled: item.disabled,
|
|
})}
|
|
className={classNames("c__calendar__menu__item", {
|
|
"c__calendar__menu__item--highlight":
|
|
downShift.highlightedIndex === index,
|
|
"c__calendar__menu__item--selected":
|
|
downShift.selectedItem?.label === item.label,
|
|
"c__calendar__menu__item--disabled": item.disabled,
|
|
})}
|
|
>
|
|
<span>{item.label}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
|
|
interface CalendarAuxProps extends CalendarAria {
|
|
minYear?: number;
|
|
maxYear?: number;
|
|
state: RangeCalendarState | CalendarState;
|
|
}
|
|
|
|
const CalendarAux = forwardRef(
|
|
(
|
|
{
|
|
state,
|
|
minYear = 1900, // in gregorian calendar.
|
|
maxYear = 2050, // in gregorian calendar.
|
|
prevButtonProps,
|
|
nextButtonProps,
|
|
calendarProps,
|
|
}: CalendarAuxProps,
|
|
ref: Ref<HTMLDivElement>,
|
|
) => {
|
|
const { t } = useCunningham();
|
|
|
|
const useTimeZoneFormatter = (formatOptions: DateFormatterOptions) => {
|
|
return useDateFormatter({
|
|
...formatOptions,
|
|
timeZone: state.timeZone,
|
|
});
|
|
};
|
|
|
|
const monthItemsFormatter = useTimeZoneFormatter({ month: "long" });
|
|
const selectedMonthItemFormatter = useTimeZoneFormatter({ month: "short" });
|
|
const yearItemsFormatter = useTimeZoneFormatter({ year: "numeric" });
|
|
|
|
const monthItems: Array<Option> = useMemo(() => {
|
|
// Note that in some calendar systems, such as the Hebrew, the number of months may differ between years.
|
|
const numberOfMonths = state.focusedDate.calendar.getMonthsInYear(
|
|
state.focusedDate,
|
|
);
|
|
return range(1, numberOfMonths).map((monthNumber) => {
|
|
const date = state.focusedDate.set({ month: monthNumber });
|
|
return {
|
|
value: monthNumber,
|
|
label: monthItemsFormatter.format(date.toDate(state.timeZone)),
|
|
disabled:
|
|
(!!state.minValue && state.minValue.month > monthNumber) ||
|
|
(!!state.maxValue && state.maxValue.month < monthNumber),
|
|
};
|
|
});
|
|
}, [state.maxValue, state.minValue, state.focusedDate.year]);
|
|
|
|
const yearItems: Array<Option> = useMemo(() => {
|
|
const calendarCurrentUser = createCalendar(
|
|
new Intl.DateTimeFormat().resolvedOptions().calendar,
|
|
);
|
|
const minDate = toCalendar(
|
|
new CalendarDate(new GregorianCalendar(), minYear, 1, 1),
|
|
calendarCurrentUser,
|
|
);
|
|
const maxDate = toCalendar(
|
|
new CalendarDate(new GregorianCalendar(), maxYear, 12, 31),
|
|
calendarCurrentUser,
|
|
);
|
|
return range(minDate.year, maxDate.year).map((yearNumber) => {
|
|
const date = state.focusedDate.set({ year: yearNumber });
|
|
return {
|
|
value: yearNumber,
|
|
label: yearItemsFormatter.format(date.toDate(state.timeZone)),
|
|
disabled:
|
|
(!!state.minValue && state.minValue.year > yearNumber) ||
|
|
(!!state.maxValue && state.maxValue.year < yearNumber),
|
|
};
|
|
});
|
|
}, [state.focusedDate, state.timeZone, state.maxValue, state.minValue]);
|
|
|
|
const useDownshiftSelect = (
|
|
key: string,
|
|
items: Array<Option>,
|
|
): UseSelectReturnValue<Option> => {
|
|
return useSelect({
|
|
items,
|
|
itemToString: optionToString,
|
|
onSelectedItemChange: (e: UseSelectStateChange<Option>) => {
|
|
const updatedFocusedDate = state.focusedDate.set({
|
|
[key]: e?.selectedItem?.value,
|
|
});
|
|
state.setFocusedDate(updatedFocusedDate);
|
|
},
|
|
});
|
|
};
|
|
const downshiftMonth = useDownshiftSelect("month", monthItems);
|
|
const downshiftYear = useDownshiftSelect("year", yearItems);
|
|
|
|
// isDisabled, onPress and onFocusChange props don't exist on the <Button /> component.
|
|
// remove them to avoid any warning.
|
|
const {
|
|
isDisabled: isPrevButtonDisabled,
|
|
onPress: onPressPrev,
|
|
onFocusChange: onFocusChangePrev,
|
|
...prevButtonOtherProps
|
|
} = prevButtonProps;
|
|
const {
|
|
isDisabled: isNextButtonDisabled,
|
|
onPress: onPressNext,
|
|
onFocusChange: onFocusChangeNext,
|
|
...nextButtonOtherProps
|
|
} = nextButtonProps;
|
|
|
|
const getToggleButtonProps = (
|
|
key: string,
|
|
items: Array<Option>,
|
|
downshift: UseSelectReturnValue<Option>,
|
|
) => ({
|
|
...downshift.getToggleButtonProps(),
|
|
onClick: () => {
|
|
const selectedItem = items.find(
|
|
(item) => item.value === state.focusedDate[key as keyof CalendarDate],
|
|
);
|
|
if (selectedItem) {
|
|
downshift.selectItem(selectedItem);
|
|
}
|
|
downshift.toggleMenu();
|
|
},
|
|
"aria-label": t(
|
|
`components.forms.date_picker.${key}_select_button_aria_label`,
|
|
),
|
|
});
|
|
|
|
return (
|
|
<div className="c__calendar">
|
|
<div
|
|
ref={ref}
|
|
{...calendarProps}
|
|
className={classNames("c__calendar__wrapper", {
|
|
"c__calendar__wrapper--opened":
|
|
!downshiftMonth.isOpen && !downshiftYear.isOpen,
|
|
})}
|
|
>
|
|
<div className="c__calendar__wrapper__header">
|
|
<div className="c__calendar__wrapper__header__actions">
|
|
<Button
|
|
color="tertiary-text"
|
|
size="small"
|
|
icon={<span className="material-icons">navigate_before</span>}
|
|
{...{
|
|
...prevButtonOtherProps,
|
|
"aria-label": t(
|
|
"components.forms.date_picker.previous_month_button_aria_label",
|
|
),
|
|
}}
|
|
disabled={isPrevButtonDisabled}
|
|
onClick={() => state.focusPreviousSection()}
|
|
/>
|
|
<Button
|
|
className="c__calendar__wrapper__header__actions__dropdown"
|
|
color="tertiary-text"
|
|
size="small"
|
|
iconPosition="right"
|
|
icon={<span className="material-icons">arrow_drop_down</span>}
|
|
{...getToggleButtonProps("month", monthItems, downshiftMonth)}
|
|
>
|
|
{selectedMonthItemFormatter.format(
|
|
state.focusedDate.toDate(state.timeZone),
|
|
)}
|
|
</Button>
|
|
<Button
|
|
color="tertiary-text"
|
|
size="small"
|
|
icon={<span className="material-icons">navigate_next</span>}
|
|
{...{
|
|
...nextButtonOtherProps,
|
|
"aria-label": t(
|
|
"components.forms.date_picker.next_month_button_aria_label",
|
|
),
|
|
}}
|
|
disabled={isNextButtonDisabled}
|
|
onClick={() => state.focusNextSection()}
|
|
/>
|
|
</div>
|
|
<div className="c__calendar__wrapper__header__actions">
|
|
<Button
|
|
color="tertiary-text"
|
|
size="small"
|
|
icon={<span className="material-icons">navigate_before</span>}
|
|
onClick={() => state.focusPreviousSection(true)}
|
|
disabled={
|
|
!!state.minValue &&
|
|
state.minValue.year >
|
|
state.focusedDate.add({ years: -1 }).year
|
|
}
|
|
aria-label={t(
|
|
"components.forms.date_picker.previous_year_button_aria_label",
|
|
)}
|
|
/>
|
|
<Button
|
|
className="c__calendar__wrapper__header__actions__dropdown"
|
|
color="tertiary-text"
|
|
size="small"
|
|
iconPosition="right"
|
|
icon={<span className="material-icons">arrow_drop_down</span>}
|
|
{...getToggleButtonProps("year", yearItems, downshiftYear)}
|
|
>
|
|
{yearItemsFormatter.format(
|
|
state.focusedDate.toDate(state.timeZone),
|
|
)}
|
|
</Button>
|
|
<Button
|
|
color="tertiary-text"
|
|
size="small"
|
|
icon={<span className="material-icons">navigate_next</span>}
|
|
onClick={() => state.focusNextSection(true)}
|
|
disabled={
|
|
!!state.maxValue &&
|
|
state.maxValue.year < state.focusedDate.add({ years: 1 }).year
|
|
}
|
|
aria-label={t(
|
|
"components.forms.date_picker.next_year_button_aria_label",
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{!downshiftMonth.isOpen && !downshiftYear.isOpen && (
|
|
<CalendarGrid state={state} />
|
|
)}
|
|
</div>
|
|
<DropdownValues options={monthItems} downShift={downshiftMonth} />
|
|
<DropdownValues options={yearItems} downShift={downshiftYear} />
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
|
|
export const Calendar = (props: CalendarProps<DateValue>) => {
|
|
const { locale } = useLocale();
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const state = useCalendarState({
|
|
...props,
|
|
locale,
|
|
createCalendar,
|
|
});
|
|
const calendarProps = useCalendar(props, state);
|
|
return <CalendarAux {...calendarProps} state={state} ref={ref} />;
|
|
};
|
|
|
|
export const CalendarRange = (props: RangeCalendarProps<DateValue>) => {
|
|
const { locale } = useLocale();
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const state = useRangeCalendarState({
|
|
...props,
|
|
locale,
|
|
createCalendar,
|
|
});
|
|
const calendarProps = useRangeCalendar(props, state, ref);
|
|
return <CalendarAux {...calendarProps} state={state} ref={ref} />;
|
|
};
|