dla tych, którzy nie czytali pierwszego artykułu - warto obczaić
Dzisiaj dowiesz się, w jaki sposób zbudować TAKI DATE PICKER: 😍
Czyli w skrócie:
dobra, koniec pierdolenia - jazda
zdecydowałem się na wybranie react-spring
ze względu na wielkość - ok. 2x mniejsza od framer-motion
plus nie potrzebujemy takiej kobyły do animacji
ponieważ to było moje starcie z animacjami w CSS to zapytałem się ChatGPT o to, w jaki sposób mogę użyć tej libki. później już sam zacząłem grzebać w dokumentacji libki i zacząłem robić swoje.
najpierw - animacja pojawiania się date pickera:
import { animated, useTransition } from "@react-spring/web";
// useTransition pozwala na to, aby zanimować "zamontowanie" jak i "odmontowanie" komponentu.
const menuTransitions = useTransition(menuMode === "expanded", {
from: { opacity: 1, transform: "translate(-30%, -200%)" },
enter: { opacity: 1, transform: "translate(-30%, -20%)" },
leave: { opacity: 0, transform: "translate(-30%, -200%)" },
});
// i podmieniamy <menu> na:
{menuTransitions(
(styles, isExpanded) =>
isExpanded && (
<animated.menu
style={styles}
>
// ...
</animated.menu>
))
}
tak to się prezentuje:
no i najs - teraz zajmiemy się przejściami pomiędzy dniami, miesiącami i latami.
const pickerTransitions = useTransition(pickerMode, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
config: {
duration: 100,
},
});
// w JSX:
{pickerTransitions(
(styles, mode) =>
mode === "days" && (
<animated.div style={styles}>
<animated.ul>
// renderowanie dni
</animated.ul>
</animated.div>
)
)}
{pickerTransitions(
(styles, mode) =>
mode === "months" && (
<animated.div style={styles}>
<animated.ul>
// renderowanie miesięcy
</animated.ul>
</animated.div>
)
)}
{pickerTransitions(
(styles, mode) =>
mode === "years" && (
<animated.div style={styles}>
<animated.ul>
// renderowanie lat
</animated.ul>
</animated.div>
)
)}
w pickerTransition
dodatkowo ustawiam długość animacji, aby nie była zbyt długa.
no i tak to wygląda:
elegancko! teraz zajmiemy się najważniejszą animacją:
na początku skorzystamy z useSpring
i zdefiniujemy animację startową:
const [daysListProps, daysListApi] = useSpring(
() => ({
from: { transform: "translateY(100%)" },
}),
[],
);
const [virtualizedDaysListProps, virtualizedDaysListApi] = useSpring(
() => ({
from: { transform: "translateY(0%)" },
}),
[],
);
no i teraz możesz się zastanawiać, co tu robi virtualizedDaysListProps
skoro mamy jeden ul
?
otóż aby zrobić taką animację, to potrzebujemy tak naprawdę dwóch ul
- jeden, który będzie renderował dni, a drugi, który też będzie renderował dni, ale wirtualnie.
co oznacza “wirtualnie”? może w tym przypadku nie jest to najlepsze słowo, ale chodzi o to, żeby podczas animowania użytkownik miał takie wrażenie, że jeden “ul` z poprzednią datą będzie się przesuwał w górę (albo w dół, w zależności czy kliknie “Previous month” czy “Next month), a drugi “ul” już z nowym miesiącem zajmie jego miejsce.
dlatego potrzebujemy dwóch elementów.
teraz musimy nadpisać dotychczasowe działanie handleNextMonth
i handlePreviousMonth
:
const TRANSITION_DURATION = 500;
const handlePreviousMonth = () => {
virtualizedDaysListApi.start({
to: async (next) => {
virtualizedDaysListApi.set({ transform: "translateY(-100%)" });
await next({ transform: "translateY(0%)" });
},
config: {
easing: easings.easeOutExpo,
duration: TRANSITION_DURATION,
},
});
daysListApi.start({
to: async (next) => {
daysListApi.set({ transform: "translateY(0%)" });
setDatePickerDate(
new Date(datePickerDate.setMonth(datePickerDate.getMonth() - 1))
);
await next({ transform: "translateY(100%)" });
},
config: {
easing: easings.easeOutExpo,
duration: TRANSITION_DURATION,
},
});
};
const handleNextMonth = () => {
virtualizedDaysListApi.start({
to: async (next) => {
virtualizedDaysListApi.set({ transform: "translateY(0%)" });
await next({ transform: "translateY(-100%)" });
},
config: {
easing: easings.easeOutExpo,
duration: 500,
},
});
daysListApi.start({
to: async (next) => {
daysListApi.set({ transform: "translateY(100%)" });
setDatePickerDate(
new Date(datePickerDate.setMonth(datePickerDate.getMonth() + 1))
);
await next({ transform: "translateY(0%)" });
},
config: {
easing: easings.easeOutExpo,
duration: 500,
},
});
};
hola hola hola, co tu się odpierdala?
już wyjaśniam dla widza:
virtualizedDaysListApi
- przesuwamy go w górę (lub w dół) o 100% (czyli o wysokość całego ul
) i czekamy na zakończenie animacji,daysListApi
i w trakcie animacji zmieniamy datę w datePickerDate
efekt?
no panie, to wygląda już całkiem całkiem 😉
może ten kod nie jest zły (bo nie jest), ale możemy sprawić, aby był łatwiejszy do utrzymania i czytania.
co można zrobić?
dayjs
,dobra, zatem do roboty:
aktualnie mamy kilka useState’ów, które są ze sobą powiązane:
const [mode, setMode] = useState<"simple" | "expanded">("simple");
const [pickerMode, setPickerMode] = useState<"days" | "months" | "years">(
"days"
);
const [datePickerDate, setDatePickerDate] = useState(new Date());
const [shortDate, setShortDate] = useState(new Date());
i w zależności od tego, co się dzieje, to zmieniamy jeden lub kilka useState’ów.
czy coś jest z tym nie tak? no nie, ale już samo zarządzanie stanem może być problematyczne, bo już w tym momencie:
tak więc tak to będzie wyglądać po refactorze:
type DatePickerState = {
menuMode: "simple" | "expanded";
pickerMode: "days" | "months" | "years";
datePickerDate: Date;
shortDate: Date;
};
type DatePickerAction =
| { type: "UPDATE_SHORT_DATE" }
| { type: "PREVIOUS_YEAR" }
| { type: "NEXT_YEAR" }
| { type: "PREVIOUS_MONTH" }
| { type: "NEXT_MONTH" }
| { type: "TOGGLE_DATE_PICKER" }
| { type: "SHOW_MONTHS_VIEW" }
| { type: "SHOW_YEARS_VIEW" }
| {
type: "SHOW_DAYS_VIEW";
payload:
| {
value: number;
dateType: "year";
}
| {
value: number;
dateType: "month";
};
};
const datePickerReducer = (
state: DatePickerState,
action: DatePickerAction,
): DatePickerState => {
switch (action.type) {
case "UPDATE_SHORT_DATE":
return {
...state,
shortDate: new Date(),
};
case "TOGGLE_DATE_PICKER":
return {
...state,
datePickerDate:
state.menuMode === "expanded" ? new Date() : state.datePickerDate,
menuMode: state.menuMode === "simple" ? "expanded" : "simple",
};
case "PREVIOUS_YEAR":
return {
...state,
datePickerDate: dayjs(state.datePickerDate)
.subtract(1, "year")
.toDate(),
};
case "NEXT_YEAR":
return {
...state,
datePickerDate: dayjs(state.datePickerDate).add(1, "year").toDate(),
};
case "PREVIOUS_MONTH":
return {
...state,
datePickerDate: dayjs(state.datePickerDate)
.subtract(1, "month")
.toDate(),
};
case "NEXT_MONTH":
return {
...state,
datePickerDate: dayjs(state.datePickerDate).add(1, "month").toDate(),
};
case "SHOW_MONTHS_VIEW":
return {
...state,
pickerMode: "months",
};
case "SHOW_YEARS_VIEW":
return {
...state,
pickerMode: "years",
};
case "SHOW_DAYS_VIEW":
return {
...state,
datePickerDate: dayjs(state.datePickerDate)
.set(action.payload.dateType, action.payload.value)
.toDate(),
pickerMode: "days",
};
default:
return state;
}
};
export function DatePicker() {
const [{ menuMode, pickerMode, datePickerDate, shortDate }, dispatch] =
useReducer(datePickerReducer, {
menuMode: "simple",
pickerMode: "days",
datePickerDate: new Date(),
shortDate: new Date(),
});
// zamiast setState'ów, teraz używamy dispatcha do wykonania odpowiedniej akcji / "rozkazu"
useEffect(() => {
const ONE_SECOND = 1000;
const interval = setInterval(() => {
dispatch({ type: "UPDATE_SHORT_DATE" });
}, ONE_SECOND);
return () => clearInterval(interval);
}, []);
// reszta logiki...
}
może na początek to wydawać się bardziej przetłaczające od wariantu z useState, ale w dłuższej perspektywie czasu jest to o wiele lepsze rozwiązanie.
czy w przypadku naszego date pickera nie jest to może przesadą? być może, ale warto wiedzieć, że takie podejście istnieje i jest bardzo często stosowane w większych projektach.
po co to robić? a dlatego, żeby nie “uzależniać” DatePickera bezpośrednio od react-springa i łatwiej będzie wprowadzić zmiany w przyszłości.
najpiew tworzymy custom hook:
const useAnimatedDatePicker = ({
menuMode,
pickerMode,
}: Pick<DatePickerState, "menuMode" | "pickerMode">) => {
const menuTransitions = useTransition(menuMode === "expanded", {
from: { opacity: 1, transform: "translate(-30%, -200%)" },
enter: { opacity: 1, transform: "translate(-30%, -20%)" },
leave: { opacity: 0, transform: "translate(-30%, -200%)" },
});
const pickerTransitions = useTransition(pickerMode, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
config: {
duration: 100,
},
});
const [daysListProps, daysListApi] = useSpring(
() => ({
from: { transform: "translateY(100%)" },
}),
[],
);
const [virtualizedDaysListProps, virtualizedDaysListApi] = useSpring(
() => ({
from: { transform: "translateY(0%)" },
}),
[],
);
const TRANSITION_DURATION = 500;
const animatePreviousMonthTransition = (onTransiton: () => void) => {
virtualizedDaysListApi.start({
to: async (next) => {
virtualizedDaysListApi.set({ transform: "translateY(-100%)" });
await next({ transform: "translateY(0%)" });
},
config: {
easing: easings.easeOutExpo,
duration: TRANSITION_DURATION,
},
});
daysListApi.start({
to: async (next) => {
daysListApi.set({ transform: "translateY(0%)" });
onTransiton();
await next({ transform: "translateY(100%)" });
},
config: {
easing: easings.easeOutExpo,
duration: TRANSITION_DURATION,
},
});
};
const animateNextMonthTransition = (onTransiton: () => void) => {
virtualizedDaysListApi.start({
to: async (next) => {
virtualizedDaysListApi.set({ transform: "translateY(0%)" });
await next({ transform: "translateY(-100%)" });
},
config: {
easing: easings.easeOutExpo,
duration: 500,
},
});
daysListApi.start({
to: async (next) => {
daysListApi.set({ transform: "translateY(100%)" });
onTransiton();
await next({ transform: "translateY(0%)" });
},
config: {
easing: easings.easeOutExpo,
duration: 500,
},
});
};
return {
menuTransitions,
pickerTransitions,
daysListProps,
virtualizedDaysListProps,
animatePreviousMonthTransition,
animateNextMonthTransition,
};
};
w komponencie:
export function DatePicker() {
// reducer...
const {
menuTransitions,
pickerTransitions,
daysListProps,
virtualizedDaysListProps,
animatePreviousMonthTransition,
animateNextMonthTransition,
} = useAnimatedDatePicker({ menuMode, pickerMode });
// ...
// podmianka w handlerach:
const handlePreviousMonth = () => {
animatePreviousMonthTransition(() => dispatch({ type: "PREVIOUS_MONTH" }));
};
const handleNextMonth = () => {
animateNextMonthTransition(() => dispatch({ type: "NEXT_MONTH" }));
};
// ...
}
jest gituwa 😎
a dlatego, że wbudowane Date API to ściek i warto użyć czegoś bardziej “cywilizowanego” do operowania na datach.
import dayjs from "dayjs";
const generateMonths = () => {
return Array.from({ length: 12 }, (_, i) => {
const date = dayjs().month(i).startOf("month");
return {
month: date.toDate().toLocaleString(navigator.language, {
month: "short",
}),
num: date.get("month"),
};
});
};
const months = generateMonths();
// generate the last 100 years and the next 100 years
const generateYears = () => {
const currentYear = dayjs().get("year") - 100;
return Array.from({ length: 200 }, (_, year) => currentYear + year);
};
const years = generateYears();
const daysInCurrentMonth = (now: Date) => {
return Array.from({ length: dayjs(now).daysInMonth() }, (_, i) => i + 1);
};
const daysInPreviousMonth = (now: Date) => {
const firstDayInMonth = dayjs(now).startOf("month");
const firstDayOfWeek = firstDayInMonth.startOf("week");
const generateDays = () => {
return Array.from(
{ length: firstDayInMonth.diff(firstDayOfWeek, "day") },
(_, i) => firstDayOfWeek.add(i, "day").get("D"),
);
};
return generateDays();
};
const daysInNextMonth = (daysToGenerate: number) => {
const now = dayjs();
const lastDayInCurrentMonth = now.add(1, "month").startOf("month");
return Array.from({ length: daysToGenerate }, (_, i) =>
lastDayInCurrentMonth.add(i, "day").get("D"),
);
};
const generateDays = (date: Date = new Date()) => {
const DATES_TO_GENERATE = 42; // 6 rows of 7 days, like in Windows calendar
const prevMonth = daysInPreviousMonth(date);
const currentMonth = daysInCurrentMonth(date);
const generatedDays = prevMonth.length + currentMonth.length;
return [
...prevMonth,
...currentMonth,
...daysInNextMonth(DATES_TO_GENERATE - generatedDays),
];
};
do tej pory gdy zmieniało się miesiące to one nie były do końca konsystetne z tym, jak rzeczywiście działa date picker:
przed:
po:
cały błąd leżał w funkcji daysInPreviousMonth
, która nie do końca poprawnie działała.
dzięki dayjs
mogliśmy to łatwo ogarnąć:
const daysInPreviousMonth = (now: Date) => {
const firstDayInMonth = dayjs(now).startOf("month");
const firstDayOfWeek = firstDayInMonth.startOf("week");
const generateDays = () => {
return Array.from(
{ length: firstDayInMonth.diff(firstDayOfWeek, "day") },
(_, i) => firstDayOfWeek.add(i, "day").get("D"),
);
};
return generateDays();
najważniejsza jest ta linijka: firstDayInMonth.diff(firstDayOfWeek, "day")
, ponieważ ona sprawia, że bierze różnicę dni pomiędzy pierwszym dniem w miesiącu, a pierwszym dniem w tygodniu, dzięki czemu ten problem został rozwiązany.
póki co komponent wygląda bardzo “surowo”, tak więc ogarnijmy ładne kolory, dark mode oraz responsywność, ale nie chcę tu męczyć kodem, tak więc od razu przejdźmy do finału:
import { useEffect, useRef, ElementRef, useReducer } from "react";
import { animated, useSpring, useTransition, easings } from "@react-spring/web";
import dayjs from "dayjs";
const generateMonths = () => {
return Array.from({ length: 12 }, (_, i) => {
const date = dayjs().month(i).startOf("month");
return {
month: date.toDate().toLocaleString(navigator.language, {
month: "short",
}),
num: date.get("month"),
};
});
};
const months = generateMonths();
// generate the last 100 years and the next 100 years
const generateYears = () => {
const currentYear = dayjs().get("year") - 100;
return Array.from({ length: 200 }, (_, year) => currentYear + year);
};
const years = generateYears();
const daysInCurrentMonth = (now: Date) => {
return Array.from({ length: dayjs(now).daysInMonth() }, (_, i) => i + 1);
};
const daysInPreviousMonth = (now: Date) => {
const firstDayInMonth = dayjs(now).startOf("month");
const firstDayOfWeek = firstDayInMonth.startOf("week");
const generateDays = () => {
return Array.from(
{ length: firstDayInMonth.diff(firstDayOfWeek, "day") },
(_, i) => firstDayOfWeek.add(i, "day").get("D"),
);
};
return generateDays();
};
const daysInNextMonth = (daysToGenerate: number) => {
const now = dayjs();
const lastDayInCurrentMonth = now.add(1, "month").startOf("month");
return Array.from({ length: daysToGenerate }, (_, i) =>
lastDayInCurrentMonth.add(i, "day").get("D"),
);
};
const generateDays = (date: Date = new Date()) => {
const DATES_TO_GENERATE = 42; // 6 rows of 7 days, like in Windows calendar
const prevMonth = daysInPreviousMonth(date);
const currentMonth = daysInCurrentMonth(date);
const generatedDays = prevMonth.length + currentMonth.length;
return [
...prevMonth,
...currentMonth,
...daysInNextMonth(DATES_TO_GENERATE - generatedDays),
];
};
type DatePickerState = {
menuMode: "simple" | "expanded";
pickerMode: "days" | "months" | "years";
datePickerDate: Date;
shortDate: Date;
};
type DatePickerAction =
| { type: "UPDATE_SHORT_DATE" }
| { type: "PREVIOUS_YEAR" }
| { type: "NEXT_YEAR" }
| { type: "PREVIOUS_MONTH" }
| { type: "NEXT_MONTH" }
| { type: "TOGGLE_DATE_PICKER" }
| { type: "SHOW_MONTHS_VIEW" }
| { type: "SHOW_YEARS_VIEW" }
| {
type: "SHOW_DAYS_VIEW";
payload:
| {
value: number;
dateType: "year";
}
| {
value: number;
dateType: "month";
};
};
const datePickerReducer = (
state: DatePickerState,
action: DatePickerAction,
): DatePickerState => {
switch (action.type) {
case "UPDATE_SHORT_DATE":
return {
...state,
shortDate: new Date(),
};
case "TOGGLE_DATE_PICKER":
return {
...state,
datePickerDate:
state.menuMode === "expanded" ? new Date() : state.datePickerDate,
menuMode: state.menuMode === "simple" ? "expanded" : "simple",
};
case "PREVIOUS_YEAR":
return {
...state,
datePickerDate: dayjs(state.datePickerDate)
.subtract(1, "year")
.toDate(),
};
case "NEXT_YEAR":
return {
...state,
datePickerDate: dayjs(state.datePickerDate).add(1, "year").toDate(),
};
case "PREVIOUS_MONTH":
return {
...state,
datePickerDate: dayjs(state.datePickerDate)
.subtract(1, "month")
.toDate(),
};
case "NEXT_MONTH":
return {
...state,
datePickerDate: dayjs(state.datePickerDate).add(1, "month").toDate(),
};
case "SHOW_MONTHS_VIEW":
return {
...state,
pickerMode: "months",
};
case "SHOW_YEARS_VIEW":
return {
...state,
pickerMode: "years",
};
case "SHOW_DAYS_VIEW":
return {
...state,
datePickerDate: dayjs(state.datePickerDate)
.set(action.payload.dateType, action.payload.value)
.toDate(),
pickerMode: "days",
};
default:
return state;
}
};
const useAnimatedDatePicker = ({
menuMode,
pickerMode,
}: Pick<DatePickerState, "menuMode" | "pickerMode">) => {
const menuTransitions = useTransition(menuMode === "expanded", {
from: { opacity: 1, transform: "translate(-30%, -200%)" },
enter: { opacity: 1, transform: "translate(-30%, -20%)" },
leave: { opacity: 0, transform: "translate(-30%, -200%)" },
});
const pickerTransitions = useTransition(pickerMode, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
config: {
duration: 100,
},
});
const [daysListProps, daysListApi] = useSpring(
() => ({
from: { transform: "translateY(100%)" },
}),
[],
);
const [virtualizedDaysListProps, virtualizedDaysListApi] = useSpring(
() => ({
from: { transform: "translateY(0%)" },
}),
[],
);
const TRANSITION_DURATION = 500;
const animatePreviousMonthTransition = (onTransiton: () => void) => {
virtualizedDaysListApi.start({
to: async (next) => {
virtualizedDaysListApi.set({ transform: "translateY(-100%)" });
await next({ transform: "translateY(0%)" });
},
config: {
easing: easings.easeOutExpo,
duration: TRANSITION_DURATION,
},
});
daysListApi.start({
to: async (next) => {
daysListApi.set({ transform: "translateY(0%)" });
onTransiton();
await next({ transform: "translateY(100%)" });
},
config: {
easing: easings.easeOutExpo,
duration: TRANSITION_DURATION,
},
});
};
const animateNextMonthTransition = (onTransiton: () => void) => {
virtualizedDaysListApi.start({
to: async (next) => {
virtualizedDaysListApi.set({ transform: "translateY(0%)" });
await next({ transform: "translateY(-100%)" });
},
config: {
easing: easings.easeOutExpo,
duration: 500,
},
});
daysListApi.start({
to: async (next) => {
daysListApi.set({ transform: "translateY(100%)" });
onTransiton();
await next({ transform: "translateY(0%)" });
},
config: {
easing: easings.easeOutExpo,
duration: 500,
},
});
};
return {
menuTransitions,
pickerTransitions,
daysListProps,
virtualizedDaysListProps,
animatePreviousMonthTransition,
animateNextMonthTransition,
};
};
export function DatePicker() {
const [{ menuMode, pickerMode, datePickerDate, shortDate }, dispatch] =
useReducer(datePickerReducer, {
menuMode: "simple",
pickerMode: "days",
datePickerDate: new Date(),
shortDate: new Date(),
});
const {
menuTransitions,
pickerTransitions,
daysListProps,
virtualizedDaysListProps,
animatePreviousMonthTransition,
animateNextMonthTransition,
} = useAnimatedDatePicker({ menuMode, pickerMode });
useEffect(() => {
const ONE_SECOND = 1000;
const interval = setInterval(() => {
dispatch({ type: "UPDATE_SHORT_DATE" });
}, ONE_SECOND);
return () => clearInterval(interval);
}, []);
const toggleDatePicker = () => {
dispatch({ type: "TOGGLE_DATE_PICKER" });
};
const handlePreviousYear = () => {
dispatch({ type: "PREVIOUS_YEAR" });
};
const handleNextYears = () => {
dispatch({ type: "NEXT_YEAR" });
};
const days = generateDays(datePickerDate);
const yearRef = useRef<ElementRef<"li">>(null);
useEffect(() => {
if (pickerMode !== "years") return;
yearRef.current?.scrollIntoView({
block: "center",
});
}, [menuMode, pickerMode]);
const handlePreviousMonth = () => {
animatePreviousMonthTransition(() => dispatch({ type: "PREVIOUS_MONTH" }));
};
const handleNextMonth = () => {
animateNextMonthTransition(() => dispatch({ type: "NEXT_MONTH" }));
};
return (
<div className="relative">
{menuTransitions(
(styles, isExpanded) =>
isExpanded && (
<animated.menu
className="absolute bottom-24 h-[200px] w-[250px] bg-slate-200 pt-4 shadow-date-picker-menu dark:bg-black dark:text-white sm:h-[300px] sm:w-[350px]"
style={styles}
>
<div className="flex w-full justify-around pb-4 ">
<button
className="w-[10%] border border-slate-400 transition-colors duration-200 hover:bg-slate-300 dark:border-neutral-400 dark:bg-black dark:text-white dark:hover:bg-neutral-800 sm:h-[35px]"
onClick={
pickerMode === "years" || pickerMode === "months"
? handlePreviousYear
: handlePreviousMonth
}
aria-label="Previous month"
>
←
</button>
{pickerMode === "days" && (
<button
className="w-[50%] text-center font-medium"
onClick={() => dispatch({ type: "SHOW_MONTHS_VIEW" })}
>
<span className="sr-only">Change to months view</span>
{datePickerDate.toLocaleString(navigator.language, {
month: "long",
year: "numeric",
})}
</button>
)}
{pickerMode === "months" || pickerMode === "years" ? (
<button
disabled={pickerMode === "years"}
className="w-[50%] text-center font-medium"
onClick={() => dispatch({ type: "SHOW_YEARS_VIEW" })}
>
<span className="sr-only">Change to years view</span>
{datePickerDate.toLocaleString(navigator.language, {
year: "numeric",
})}
</button>
) : null}
<button
className="w-[10%] border border-slate-400 transition-colors duration-200 hover:bg-slate-300 dark:border-neutral-400 dark:bg-black dark:text-white dark:hover:bg-neutral-800"
onClick={
pickerMode === "years" || pickerMode === "months"
? handleNextYears
: handleNextMonth
}
aria-label="Next month"
>
→
</button>
</div>
<div className="relative h-full overflow-hidden bg-slate-200 dark:bg-black dark:text-white">
{pickerTransitions(
(styles, mode) =>
mode === "days" && (
<animated.div style={styles}>
<animated.ul
className="grid-rows-7 absolute grid h-full w-full grid-cols-7 items-center text-center"
style={daysListProps}
>
{days.map((day, i) => (
<li key={i}>
<button className="h-full w-full font-medium transition-colors duration-200 hover:bg-slate-300 dark:hover:bg-neutral-800 sm:p-2">
{day}
</button>
</li>
))}
</animated.ul>
<animated.ul
className="grid-rows-7 absolute grid h-full w-full grid-cols-7 items-center text-center"
style={virtualizedDaysListProps}
>
{days.map((day, i) => (
<li key={i}>
<button className="h-full w-full font-medium transition-colors duration-200 hover:bg-slate-300 dark:hover:bg-neutral-800 sm:p-2">
{day}
</button>
</li>
))}
</animated.ul>
</animated.div>
),
)}
{pickerTransitions(
(styles, mode) =>
mode === "months" && (
<animated.ul
className="absolute grid h-full w-full list-none grid-cols-4 grid-rows-3 items-center justify-items-center"
style={styles}
>
{months.map(({ month, num }) => (
<li className="h-full w-full" key={month}>
<button
className="h-full w-full font-medium transition-colors duration-200 hover:bg-slate-300 dark:hover:bg-neutral-800"
onClick={() => {
dispatch({
type: "SHOW_DAYS_VIEW",
payload: {
value: num,
dateType: "month",
},
});
}}
>
{month}
</button>
</li>
))}
</animated.ul>
),
)}
{pickerTransitions(
(styles, mode) =>
mode === "years" && (
<animated.ul
className="grid h-full grid-cols-4 grid-rows-4 items-center justify-items-center overflow-y-scroll"
style={styles}
>
{years.map((year, i) => (
<li
key={i}
ref={year === dayjs().get("year") ? yearRef : null}
className="mt-2 h-[50px] w-full"
>
<button
className="h-full w-full font-medium transition-colors duration-200 hover:bg-slate-300 dark:hover:bg-neutral-800"
onClick={() => {
dispatch({
type: "SHOW_DAYS_VIEW",
payload: {
value: year,
dateType: "year",
},
});
}}
>
{year}
</button>
</li>
))}
</animated.ul>
),
)}
</div>
</animated.menu>
),
)}
<button
onClick={toggleDatePicker}
className="rounded-md bg-slate-200 p-2 transition-colors duration-200 hover:bg-slate-300 dark:bg-black dark:text-white dark:hover:bg-neutral-800"
>
<span className="sr-only">
{menuMode === "simple"
? "Open the date picker"
: "Close the date picker"}
</span>
<menu className="p-2">
<p className="text-sm">
{shortDate.toLocaleTimeString(navigator.language, {
hour: "numeric",
minute: "numeric",
})}
</p>
<p className="text-xs">{shortDate.toLocaleDateString()}</p>
</menu>
</button>
</div>
);
}
Tutaj możesz sprawdzić ten komponent w akcji
Czy jest to najlepszy komponent jaki kiedykolwiek powstał? no nie XD
ale co jak co to było ciekawe doświadczenie, żeby odtworzyć jakiś element UI - nie macie co się martwić, bo takich artykułów czy nawet filmów na YT będzie tylko więcej ;p
no i to tyle