mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-06 13:19:31 -05:00
Strict improvement because we avoid the flash on clientside navigation. One practical bug was scroll restoration between tournament teams list and user page. When user pressed back they ended up at the bottom of the page because the placeholder (smaller height than actual content) rendered. With useHydrated this placeholder is no longer rendered for client side navigations.
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
import * as React from "react";
|
|
import { useHydrated } from "~/hooks/useHydrated";
|
|
import { dateToYearMonthDayHourMinuteString, isValidDate } from "~/utils/dates";
|
|
import { logger } from "~/utils/logger";
|
|
|
|
export interface DateInputProps
|
|
extends Omit<
|
|
React.InputHTMLAttributes<HTMLInputElement>,
|
|
"defaultValue" | "min" | "max" | "onChange" | "value"
|
|
> {
|
|
defaultValue?: Date;
|
|
min?: Date;
|
|
max?: Date;
|
|
onChange?: (newDate: Date | null) => void;
|
|
}
|
|
|
|
export function DateInput({
|
|
name,
|
|
defaultValue,
|
|
min,
|
|
max,
|
|
onChange,
|
|
...inputProps
|
|
}: DateInputProps) {
|
|
// Keeping track of the value as a string is a nice fallback for browsers that
|
|
// don't show a date picker but actually expect the user to type in the date
|
|
// as a text. This was Safari Desktop until recently, but nowadays all current
|
|
// versions of the main browsers set the input to either a valid date string
|
|
// or "". (The browser will handle transitional invalid states internally).
|
|
const [[parsedDate, valueString], setDate] = React.useState<
|
|
[Date | null, string]
|
|
>(() => {
|
|
if (defaultValue) {
|
|
if (isValidDate(defaultValue)) {
|
|
return [defaultValue, dateToYearMonthDayHourMinuteString(defaultValue)];
|
|
}
|
|
logger.warn("DateInput got invalid date as defaultValue");
|
|
}
|
|
return [null, ""];
|
|
});
|
|
const isHydrated = useHydrated();
|
|
|
|
return (
|
|
<>
|
|
{parsedDate && isHydrated && (
|
|
<input name={name} type="hidden" value={parsedDate.getTime() ?? ""} />
|
|
)}
|
|
<input
|
|
{...inputProps}
|
|
type="datetime-local"
|
|
disabled={!isHydrated || inputProps.disabled}
|
|
// This is important, because SSR will likely have a date in the wrong
|
|
// timezone. We can only fill in a value once hydration is over.
|
|
value={isHydrated ? valueString : ""}
|
|
min={min ? dateToYearMonthDayHourMinuteString(min) : undefined}
|
|
max={max ? dateToYearMonthDayHourMinuteString(max) : undefined}
|
|
onChange={(e) => {
|
|
const newValueString = e.target.value;
|
|
const parsedValue = new Date(newValueString);
|
|
const newDate = isValidDate(parsedValue) ? parsedValue : null;
|
|
|
|
setDate([newDate, newValueString]);
|
|
onChange?.(newDate);
|
|
}}
|
|
// Firefox fix for hydration error "prop `disabled` did not match" */
|
|
// https://github.com/facebook/react/issues/21459
|
|
autoComplete="off"
|
|
/>
|
|
</>
|
|
);
|
|
}
|