@yamada-ui/react
Version:
React UI components of the Yamada, by the Yamada, for the Yamada built with React and Emotion
571 lines (567 loc) • 18.7 kB
JavaScript
"use client";
import { isComposing, runKeyAction, visuallyHiddenAttributes } from "../../utils/dom.js";
import { useUpdateEffect } from "../../utils/effect.js";
import { mergeRefs } from "../../utils/ref.js";
import { utils_exports } from "../../utils/index.js";
import { useControllableState } from "../../hooks/use-controllable-state/index.js";
import { useI18n } from "../../providers/i18n-provider/i18n-provider.js";
import { useFieldProps } from "../field/use-field-props.js";
import { useCombobox } from "../../hooks/use-combobox/index.js";
import { useDateTimeFormat } from "../format/use-format-date-time.js";
import { DEFAULT_MAX_DATE, DEFAULT_MIN_DATE, getAdjustedMonth, isAfterDate, isBeforeDate, updateMaybeDateValue, useCalendarProps } from "../calendar/use-calendar.js";
import { cloneElement, isValidElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { jsxs } from "react/jsx-runtime";
import dayjs from "dayjs";
//#region src/components/date-picker/use-date-picker.tsx
const defaultRender = ({ count, focused, index, max, separator, value }) => {
const last = count - 1 === index;
return /* @__PURE__ */ jsxs("span", {
style: { marginInlineEnd: "var(--gap)" },
children: [value, (!(0, utils_exports.isNumber)(max) || count < max) && focused || !last ? separator : null]
});
};
const useDatePicker = ({ maxDate = DEFAULT_MAX_DATE, minDate = DEFAULT_MIN_DATE,...props } = {}) => {
if (dayjs(minDate).isAfter(dayjs(maxDate))) maxDate = minDate;
const { locale: defaultLocale, t } = useI18n("datePicker");
const { props: { id, ref, name, allowInput = true, allowInputBeyond = false, closeOnChange = false, multiple = false, closeOnSelect = !multiple, defaultInputValue, defaultMonth = /* @__PURE__ */ new Date(), range = false, defaultValue = range ? {
end: void 0,
start: void 0
} : multiple ? [] : void 0, disabled, focusOnClear = true, format, inputValue: inputValueProp, locale = defaultLocale, max, month: monthProp, openOnChange = true, openOnClick = true, openOnFocus = true, parseDate, pattern, placeholder: placeholderProp, readOnly, render = defaultRender, required, separator = range ? "-" : ",", value: valueProp, onChange: onChangeProp, onChangeMonth: onChangeMonthProp, onInputChange: onInputChangeProp,...computedProps }, ariaProps, dataProps, eventProps } = useFieldProps(props);
const dateTimeFormat = useDateTimeFormat({ locale });
const { calendarFormat, inputFormat } = useMemo(() => {
const { input: inputFormat$1,...calendarFormat$1 } = format ?? {};
return {
calendarFormat: calendarFormat$1,
inputFormat: inputFormat$1 !== null ? inputFormat$1 ?? {
day: "numeric",
month: "long",
year: "numeric"
} : void 0
};
}, [format]);
const [calendarProps, rest] = useCalendarProps(computedProps);
const { excludeDate } = calendarProps;
const fieldRef = useRef(null);
const contentRef = useRef(null);
const startInputRef = useRef(null);
const endInputRef = useRef(null);
const focusByClickRef = useRef(false);
const [focused, setFocused] = useState(false);
const [value, setValue] = useControllableState({
defaultValue,
value: valueProp,
onChange: onChangeProp
});
const [month, setMonth] = useControllableState({
defaultValue: () => {
let value$1 = defaultMonth;
if (dayjs(minDate).isAfter(dayjs(defaultMonth))) value$1 = dayjs(minDate).set("date", 1).toDate();
if (valueProp) value$1 = getAdjustedMonth(valueProp, value$1);
else if (defaultValue) value$1 = getAdjustedMonth(defaultValue, value$1);
return value$1;
},
value: monthProp,
onChange: onChangeMonthProp
});
const stringToDate = useCallback((value$1, start = minDate, end = maxDate) => {
let date = parseDate?.(value$1);
if (!date && dayjs(value$1).isValid()) date = dayjs(value$1, locale).toDate();
if (date == null) return void 0;
if (excludeDate?.(date)) return void 0;
if (!allowInputBeyond) {
if (isBeforeDate(date, start)) date = start;
if (isAfterDate(date, end)) date = end;
}
return date;
}, [
allowInputBeyond,
excludeDate,
locale,
maxDate,
minDate,
parseDate
]);
const dateToString = useCallback((date) => {
if (date == null) return "";
return dateTimeFormat(date, inputFormat);
}, [dateTimeFormat, inputFormat]);
const [inputValue, setInputValue] = useControllableState({
defaultValue: () => {
if (defaultInputValue) if ((0, utils_exports.isObject)(defaultInputValue)) {
if (dayjs(defaultInputValue.start).isValid()) defaultInputValue.start = dateTimeFormat(dayjs(defaultInputValue.start).toDate(), inputFormat);
if (dayjs(defaultInputValue.end).isValid()) defaultInputValue.end = dateTimeFormat(dayjs(defaultInputValue.end).toDate(), inputFormat);
return defaultInputValue;
} else if (dayjs(defaultInputValue).isValid()) return dateTimeFormat(dayjs(defaultInputValue).toDate(), inputFormat);
else return defaultInputValue;
else if ((0, utils_exports.isDate)(value)) return dateToString(value);
else if ((0, utils_exports.isObject)(value) && !(0, utils_exports.isArray)(value)) return {
end: dateToString(value.end),
start: dateToString(value.start)
};
else return "";
},
value: inputValueProp,
onChange: onInputChangeProp
});
const { interactive, open, getContentProps: getComboboxContentProps, getTriggerProps, popoverProps, onClose, onOpen } = useCombobox({
disabled,
matchWidth: false,
openOnClick: false,
openOnEnter: !allowInput,
openOnSpace: !allowInput,
placement: "end-start",
readOnly,
transferFocus: false,
...ariaProps,
...dataProps,
...eventProps,
...rest
});
const children = useMemo(() => {
if (!(0, utils_exports.isArray)(value)) return null;
const count = value.length;
return value.map((date, index) => {
const value$1 = dateToString(date);
const onClear$1 = (ev) => {
ev?.preventDefault();
ev?.stopPropagation();
setValue((prev) => updateMaybeDateValue(date, max)(prev));
};
const component = render({
count,
date,
focused,
index,
max,
separator,
value: value$1,
onClear: onClear$1
});
if (isValidElement(component)) return cloneElement(component, {
...component.props,
key: index
});
else return component;
});
}, [
dateToString,
focused,
max,
render,
separator,
setValue,
value
]);
const { endPlaceholder, startPlaceholder } = useMemo(() => {
if (range) {
const startPlaceholder$1 = placeholderProp ?? dateTimeFormat(/* @__PURE__ */ new Date(), inputFormat);
return {
endPlaceholder: placeholderProp ?? dateTimeFormat(dayjs().add(1, "day").toDate(), inputFormat),
startPlaceholder: startPlaceholder$1
};
} else {
const placeholder = placeholderProp ?? dateTimeFormat(/* @__PURE__ */ new Date(), inputFormat);
return {
endPlaceholder: placeholder,
startPlaceholder: placeholder
};
}
}, [
dateTimeFormat,
inputFormat,
placeholderProp,
range
]);
const onChange = useCallback((value$1) => {
setValue(value$1);
if ((0, utils_exports.isArray)(value$1)) setInputValue("");
else if ((0, utils_exports.isObject)(value$1) && !(0, utils_exports.isDate)(value$1)) setInputValue({
end: dateToString(value$1.end),
start: dateToString(value$1.start)
});
else setInputValue(dateToString(value$1));
if (!closeOnSelect) return;
if ((0, utils_exports.isObject)(value$1) && !(0, utils_exports.isArray)(value$1) && !(0, utils_exports.isDate)(value$1)) {
if (value$1.end && value$1.start) onClose();
} else onClose();
}, [
setValue,
closeOnSelect,
setInputValue,
dateToString,
onClose
]);
const onInputChange = useCallback((ev) => {
if (!allowInput) return;
if ((0, utils_exports.isArray)(value) && value.length === max) return;
if ((0, utils_exports.runIfFn)(closeOnChange, ev)) onClose();
else if ((0, utils_exports.runIfFn)(openOnChange, ev)) onOpen();
let inputValue$1 = ev.target.value;
if (pattern) inputValue$1 = inputValue$1.replace(pattern, "");
if ((0, utils_exports.isArray)(value)) {
const date = stringToDate(inputValue$1);
setInputValue(inputValue$1);
if (date) setMonth(dayjs(date).set("date", 1).toDate());
} else if ((0, utils_exports.isObject)(value) && !(0, utils_exports.isDate)(value)) {
const align = (0, utils_exports.contains)(endInputRef.current, ev.target) ? "end" : "start";
const minDate$1 = value.start && align === "end" ? dayjs(value.start).add(1, "day").toDate() : void 0;
const maxDate$1 = value.end && align === "start" ? dayjs(value.end).subtract(1, "day").toDate() : void 0;
const date = stringToDate(inputValue$1, minDate$1, maxDate$1);
setInputValue((prev) => (0, utils_exports.isObject)(prev) ? {
...prev,
[align]: inputValue$1
} : prev);
setValue((prev) => (0, utils_exports.isObject)(prev) && !(0, utils_exports.isDate)(prev) ? {
...prev,
[align]: date
} : prev);
} else {
const date = stringToDate(inputValue$1);
setInputValue(inputValue$1);
setValue(date);
}
}, [
allowInput,
closeOnChange,
max,
onClose,
onOpen,
openOnChange,
pattern,
setInputValue,
setMonth,
setValue,
stringToDate,
value
]);
const onKeyDown = useCallback((ev) => {
if (disabled || isComposing(ev)) return;
const inputValue$1 = (0, utils_exports.cast)(ev.target).value;
runKeyAction(ev, {
Backspace: (ev$1) => {
if (!value || (0, utils_exports.isDate)(value)) return;
if (!!inputValue$1.length) return;
if ((0, utils_exports.isArray)(value)) {
ev$1.preventDefault();
setValue((prev) => (0, utils_exports.isArray)(prev) ? prev.slice(0, -1) : prev);
} else if ((0, utils_exports.contains)(endInputRef.current, ev$1.target)) {
ev$1.preventDefault();
setInputValue({
end: "",
start: ""
});
setValue({
end: void 0,
start: void 0
});
startInputRef.current?.focus();
}
},
Enter: (ev$1) => {
if (!open || !inputValue$1.length) return;
if ((0, utils_exports.isDate)(value) || (0, utils_exports.isArray)(value)) {
const date = stringToDate(inputValue$1);
if (!date) return;
ev$1.preventDefault();
if ((0, utils_exports.isArray)(value)) setInputValue("");
else setInputValue(dateToString(date));
setValue((prev) => updateMaybeDateValue(date, max)(prev));
} else if ((0, utils_exports.isObject)(value)) {
const align = (0, utils_exports.contains)(endInputRef.current, ev$1.target) ? "end" : "start";
const date = stringToDate(inputValue$1, value.start && align === "end" ? dayjs(value.start).add(1, "day").toDate() : void 0, value.end && align === "start" ? dayjs(value.end).subtract(1, "day").toDate() : void 0);
if (!date) return;
ev$1.preventDefault();
setInputValue((prev) => (0, utils_exports.isObject)(prev) ? {
...prev,
[align]: dateToString(date)
} : prev);
setValue((prev) => (0, utils_exports.isObject)(prev) && !(0, utils_exports.isDate)(prev) ? {
...prev,
[align]: date
} : prev);
if (align === "start") endInputRef.current?.focus();
}
}
}, { preventDefault: false });
}, [
dateToString,
disabled,
max,
open,
setInputValue,
setValue,
stringToDate,
value
]);
const onClick = useCallback((ev) => {
if (!interactive) return;
focusByClickRef.current = true;
if (allowInput) if ((0, utils_exports.isObject)(value) && !(0, utils_exports.isArray)(value) && !(0, utils_exports.isDate)(value)) {
if ((0, utils_exports.contains)(startInputRef.current, ev.target)) return;
if ((0, utils_exports.contains)(endInputRef.current, ev.target)) return;
const { end, start } = value;
if (!start && !end || !!end) startInputRef.current?.focus();
else endInputRef.current?.focus();
} else startInputRef.current?.focus();
if (openOnClick) onOpen();
}, [
allowInput,
interactive,
onOpen,
openOnClick,
value
]);
const onMouseDown = useCallback((ev) => {
if (!openOnFocus) return;
ev.preventDefault();
ev.stopPropagation();
}, [openOnFocus]);
const onFieldFocus = useCallback(() => {
if (allowInput) return;
if (openOnFocus) onOpen();
focusByClickRef.current = false;
}, [
allowInput,
onOpen,
openOnFocus
]);
const onInputFocus = useCallback((ev) => {
ev.preventDefault();
ev.stopPropagation();
setFocused(true);
if (openOnFocus && !focusByClickRef.current) onOpen();
focusByClickRef.current = false;
}, [onOpen, openOnFocus]);
const onBlur = useCallback((ev) => {
setFocused(false);
if ((0, utils_exports.contains)(fieldRef.current, ev.relatedTarget) || (0, utils_exports.contains)(contentRef.current, ev.relatedTarget)) ev.preventDefault();
else if ((0, utils_exports.isArray)(value)) setInputValue("");
else if ((0, utils_exports.isObject)(value) && !(0, utils_exports.isDate)(value)) setInputValue((prev) => (0, utils_exports.isObject)(prev) ? {
end: dateToString(value.end),
start: dateToString(value.start)
} : prev);
else setInputValue(dateToString(value));
}, [
dateToString,
setInputValue,
value
]);
const onClear = useCallback(() => {
if (!interactive) return;
setValue((prev) => {
if ((0, utils_exports.isDate)(prev)) return;
else if ((0, utils_exports.isArray)(prev)) return [];
else if ((0, utils_exports.isObject)(prev)) return {
end: void 0,
start: void 0
};
else return prev;
});
setInputValue(range ? {
end: "",
start: ""
} : "");
if (focusOnClear) if (allowInput) startInputRef.current?.focus();
else fieldRef.current?.focus();
}, [
allowInput,
focusOnClear,
interactive,
range,
setInputValue,
setValue
]);
useEffect(() => {
if (!open) return;
return (0, utils_exports.focusTransfer)(contentRef.current, allowInput ? startInputRef.current : fieldRef.current);
}, [allowInput, open]);
useUpdateEffect(() => {
setMonth((prev) => getAdjustedMonth(value, prev));
}, [value, range]);
useUpdateEffect(() => {
if ((0, utils_exports.isArray)(valueProp)) return;
if ((0, utils_exports.isObject)(valueProp) && !(0, utils_exports.isDate)(valueProp)) setInputValue({
end: dateToString(valueProp.end),
start: dateToString(valueProp.start)
});
else setInputValue(dateToString(valueProp));
}, [valueProp]);
const getRootProps = useCallback((props$1) => ({
"data-range": (0, utils_exports.dataAttr)(range),
...dataProps,
...props$1
}), [dataProps, range]);
const getFieldProps = useCallback(({ ref: ref$1,...props$1 } = {}) => getTriggerProps({
ref: mergeRefs(ref$1, fieldRef),
"aria-haspopup": "dialog",
tabIndex: !allowInput ? 0 : -1,
...props$1,
onClick: (0, utils_exports.handlerAll)(props$1.onClick, onClick),
onFocus: (0, utils_exports.handlerAll)(props$1.onFocus, onFieldFocus),
onMouseDown: (0, utils_exports.handlerAll)(props$1.onMouseDown, onMouseDown)
}), [
allowInput,
getTriggerProps,
onClick,
onFieldFocus,
onMouseDown
]);
const getInputProps = useCallback(({ align = "start",...props$1 } = {}) => {
const inputProps = {
style: {
...!allowInput ? { pointerEvents: "none" } : {},
...props$1.style
},
autoComplete: "off",
disabled,
readOnly,
required,
tabIndex: allowInput ? 0 : -1,
...dataProps,
...props$1,
onBlur: (0, utils_exports.handlerAll)(props$1.onBlur, onBlur),
onChange: (0, utils_exports.handlerAll)(props$1.onChange, onInputChange),
onFocus: (0, utils_exports.handlerAll)(props$1.onFocus, onInputFocus),
onKeyDown: (0, utils_exports.handlerAll)(props$1.onKeyDown, onKeyDown),
onMouseDown: (0, utils_exports.handlerAll)(props$1.onMouseDown, onMouseDown)
};
if ((0, utils_exports.isObject)(inputValue)) {
if (align === "start") {
inputProps.ref = mergeRefs(props$1.ref, ref, startInputRef);
inputProps.value = inputValue.start;
inputProps.placeholder = startPlaceholder;
} else {
inputProps.ref = mergeRefs(props$1.ref, ref, endInputRef);
inputProps.value = inputValue.end;
inputProps.placeholder = endPlaceholder;
}
if (!inputValue.start && align === "start") {
inputProps.id = id;
inputProps.name = name;
}
if (!!inputValue.start && align === "end") {
inputProps.id = id;
inputProps.name = name;
}
} else {
inputProps.ref = mergeRefs(props$1.ref, ref, startInputRef);
inputProps.value = inputValue;
inputProps.id = id;
inputProps.name = name;
if ((0, utils_exports.isArray)(value)) {
inputProps.style = {
...!focused && !!value.length ? visuallyHiddenAttributes.style : {},
...inputProps.style
};
inputProps.placeholder = !value.length ? startPlaceholder : void 0;
inputProps["data-max"] = (0, utils_exports.dataAttr)((0, utils_exports.isNumber)(max) && value.length >= max);
} else inputProps.placeholder = startPlaceholder;
}
return inputProps;
}, [
allowInput,
dataProps,
disabled,
endPlaceholder,
focused,
id,
inputValue,
max,
name,
onBlur,
onInputFocus,
onInputChange,
onKeyDown,
onMouseDown,
readOnly,
ref,
required,
startPlaceholder,
value
]);
const getContentProps = useCallback(({ ref: ref$1,...props$1 } = {}) => getComboboxContentProps({
ref: mergeRefs(ref$1, contentRef),
role: "dialog",
...props$1
}), [getComboboxContentProps]);
const getCalendarProps = useCallback((props$1) => ({
disabled: !interactive,
format: calendarFormat,
locale,
max,
maxDate,
minDate,
month,
multiple,
range,
value,
onChange,
onChangeMonth: setMonth,
...calendarProps,
...props$1
}), [
interactive,
calendarFormat,
locale,
max,
maxDate,
minDate,
month,
multiple,
range,
value,
onChange,
setMonth,
calendarProps
]);
const getIconProps = useCallback((props$1) => ({
...dataProps,
...props$1
}), [dataProps]);
return {
children,
dateToString,
inputValue,
interactive,
month,
open,
range,
separator,
setInputValue,
setMonth,
setValue,
stringToDate,
value,
getCalendarProps,
getClearIconProps: useCallback((props$1 = {}) => getIconProps({
"aria-disabled": (0, utils_exports.ariaAttr)(!interactive),
"aria-label": t("Clear value"),
role: "button",
tabIndex: interactive ? 0 : -1,
...props$1,
onClick: (0, utils_exports.handlerAll)(props$1.onClick, onClear),
onKeyDown: (0, utils_exports.handlerAll)(props$1.onKeyDown, (ev) => runKeyAction(ev, {
Enter: onClear,
Space: onClear
}))
}), [
getIconProps,
interactive,
onClear,
t
]),
getContentProps,
getFieldProps,
getIconProps,
getInputProps,
getRootProps,
popoverProps,
onChange,
onClose,
onInputChange,
onOpen
};
};
//#endregion
export { useDatePicker };
//# sourceMappingURL=use-date-picker.js.map