reka-ui
Version:
Vue port for Radix UI Primitives.
700 lines (698 loc) • 23.4 kB
JavaScript
import { getDaysInMonth, toDate } from "./comparators.js";
import { useKbd } from "../shared/useKbd.js";
import { isAcceptableSegmentKey, isNumberString, isSegmentNavigationKey } from "./segment.js";
import { computed } from "vue";
//#region src/shared/date/useDateField.ts
function commonSegmentAttrs(props) {
return {
role: "spinbutton",
contenteditable: true,
tabindex: props.disabled ? void 0 : 0,
spellcheck: false,
inputmode: "numeric",
autocorrect: "off",
enterkeyhint: "next",
style: "caret-color: transparent;"
};
}
function daySegmentAttrs(props) {
const { segmentValues, placeholder } = props;
const isEmpty = segmentValues.day === null;
const date = segmentValues.day ? placeholder.set({ day: segmentValues.day }) : placeholder;
const valueNow = date.day;
const valueMin = 1;
const valueMax = getDaysInMonth(date);
const valueText = isEmpty ? "Empty" : `${valueNow}`;
return {
...commonSegmentAttrs(props),
"aria-label": "day,",
"aria-valuemin": valueMin,
"aria-valuemax": valueMax,
"aria-valuenow": valueNow,
"aria-valuetext": valueText,
"data-placeholder": isEmpty ? "" : void 0
};
}
function monthSegmentAttrs(props) {
const { segmentValues, placeholder, formatter } = props;
const isEmpty = segmentValues.month === null;
const date = segmentValues.month ? placeholder.set({ month: segmentValues.month }) : placeholder;
const valueNow = date.month;
const valueMin = 1;
const valueMax = 12;
const valueText = isEmpty ? "Empty" : `${valueNow} - ${formatter.fullMonth(toDate(date))}`;
return {
...commonSegmentAttrs(props),
"aria-label": "month, ",
"contenteditable": true,
"aria-valuemin": valueMin,
"aria-valuemax": valueMax,
"aria-valuenow": valueNow,
"aria-valuetext": valueText,
"data-placeholder": isEmpty ? "" : void 0
};
}
function yearSegmentAttrs(props) {
const { segmentValues, placeholder } = props;
const isEmpty = segmentValues.year === null;
const date = segmentValues.year ? placeholder.set({ year: segmentValues.year }) : placeholder;
const valueMin = 1;
const valueMax = 9999;
const valueNow = date.year;
const valueText = isEmpty ? "Empty" : `${valueNow}`;
return {
...commonSegmentAttrs(props),
"aria-label": "year, ",
"aria-valuemin": valueMin,
"aria-valuemax": valueMax,
"aria-valuenow": valueNow,
"aria-valuetext": valueText,
"data-placeholder": isEmpty ? "" : void 0
};
}
function hourSegmentAttrs(props) {
const { segmentValues, hourCycle, placeholder } = props;
if (!("hour" in segmentValues) || !("hour" in placeholder)) return {};
const isEmpty = segmentValues.hour === null;
const date = segmentValues.hour ? placeholder.set({ hour: segmentValues.hour }) : placeholder;
const valueMin = hourCycle === 12 ? 1 : 0;
const valueMax = hourCycle === 12 ? 12 : 23;
const valueNow = date.hour;
const valueText = isEmpty ? "Empty" : `${valueNow} ${segmentValues.dayPeriod ?? ""}`;
return {
...commonSegmentAttrs(props),
"aria-label": "hour, ",
"aria-valuemin": valueMin,
"aria-valuemax": valueMax,
"aria-valuenow": valueNow,
"aria-valuetext": valueText,
"data-placeholder": isEmpty ? "" : void 0
};
}
function minuteSegmentAttrs(props) {
const { segmentValues, placeholder } = props;
if (!("minute" in segmentValues) || !("minute" in placeholder)) return {};
const isEmpty = segmentValues.minute === null;
const date = segmentValues.minute ? placeholder.set({ minute: segmentValues.minute }) : placeholder;
const valueNow = date.minute;
const valueMin = 0;
const valueMax = 59;
const valueText = isEmpty ? "Empty" : `${valueNow}`;
return {
...commonSegmentAttrs(props),
"aria-label": "minute, ",
"aria-valuemin": valueMin,
"aria-valuemax": valueMax,
"aria-valuenow": valueNow,
"aria-valuetext": valueText,
"data-placeholder": isEmpty ? "" : void 0
};
}
function secondSegmentAttrs(props) {
const { segmentValues, placeholder } = props;
if (!("second" in segmentValues) || !("second" in placeholder)) return {};
const isEmpty = segmentValues.second === null;
const date = segmentValues.second ? placeholder.set({ second: segmentValues.second }) : placeholder;
const valueNow = date.second;
const valueMin = 0;
const valueMax = 59;
const valueText = isEmpty ? "Empty" : `${valueNow}`;
return {
...commonSegmentAttrs(props),
"aria-label": "second, ",
"aria-valuemin": valueMin,
"aria-valuemax": valueMax,
"aria-valuenow": valueNow,
"aria-valuetext": valueText,
"data-placeholder": isEmpty ? "" : void 0
};
}
function dayPeriodSegmentAttrs(props) {
const { segmentValues } = props;
if (!("dayPeriod" in segmentValues)) return {};
const valueMin = 0;
const valueMax = 12;
const valueNow = segmentValues.hour ? segmentValues.hour > 12 ? segmentValues.hour - 12 : segmentValues.hour : 0;
const valueText = segmentValues.dayPeriod ?? "AM";
return {
...commonSegmentAttrs(props),
"inputmode": "text",
"aria-label": "AM/PM",
"aria-valuemin": valueMin,
"aria-valuemax": valueMax,
"aria-valuenow": valueNow,
"aria-valuetext": valueText
};
}
function literalSegmentAttrs(_props) {
return {
"aria-hidden": true,
"data-segment": "literal"
};
}
function timeZoneSegmentAttrs(props) {
return {
"role": "textbox",
"aria-label": "timezone, ",
"data-readonly": true,
"data-segment": "timeZoneName",
"tabindex": props.disabled ? void 0 : 0,
"style": "caret-color: transparent;"
};
}
function eraSegmentAttrs(props) {
const { segmentValues, placeholder } = props;
const valueMin = 0;
const valueMax = 0;
const valueNow = 0;
const valueText = "era" in segmentValues ? segmentValues.era : placeholder.era;
return {
...commonSegmentAttrs(props),
"aria-label": "era",
"aria-valuemin": valueMin,
"aria-valuemax": valueMax,
"aria-valuenow": valueNow,
"aria-valuetext": valueText
};
}
const segmentBuilders = {
day: { attrs: daySegmentAttrs },
month: { attrs: monthSegmentAttrs },
year: { attrs: yearSegmentAttrs },
hour: { attrs: hourSegmentAttrs },
minute: { attrs: minuteSegmentAttrs },
second: { attrs: secondSegmentAttrs },
dayPeriod: { attrs: dayPeriodSegmentAttrs },
literal: { attrs: literalSegmentAttrs },
timeZoneName: { attrs: timeZoneSegmentAttrs },
era: { attrs: eraSegmentAttrs }
};
function useDateField(props) {
const kbd = useKbd();
function minuteSecondIncrementation({ e, part, dateRef, prevValue }) {
const step = props.step.value[part] ?? 1;
const sign = e.key === kbd.ARROW_UP ? step : -step;
const min = 0;
const max = 59;
if (prevValue === null) return sign > 0 ? min : max;
const cycleArgs = [part, sign];
return dateRef.set({ [part]: prevValue }).cycle(...cycleArgs)[part];
}
function deleteValue(prevValue) {
props.hasLeftFocus.value = false;
if (prevValue === null) return prevValue;
const str = prevValue.toString();
if (str.length === 1) {
props.modelValue.value = void 0;
return null;
}
return Number.parseInt(str.slice(0, -1));
}
function dateTimeValueIncrementation({ e, part, dateRef, prevValue, hourCycle }) {
const step = props.step.value[part] ?? 1;
const sign = e.key === kbd.ARROW_UP ? step : -step;
if (prevValue === null) return dateRef[part];
if (part === "hour" && "hour" in dateRef) {
const cycleArgs$1 = [
part,
sign,
{ hourCycle }
];
return dateRef.set({ [part]: prevValue }).cycle(...cycleArgs$1)[part];
}
const cycleArgs = [part, sign];
if (part === "day") return dateRef.set({
[part]: prevValue,
month: props.segmentValues.value.month ?? 1
}).cycle(...cycleArgs)[part];
return dateRef.set({ [part]: prevValue }).cycle(...cycleArgs)[part];
}
function updateDayOrMonth(max, num, prev) {
let moveToNext = false;
const maxStart = Math.floor(max / 10);
/**
* If the user has left the segment, we want to reset the
* `prev` value so that we can start the segment over again
* when the user types a number.
*/
if (props.hasLeftFocus.value) {
props.hasLeftFocus.value = false;
prev = null;
}
if (prev === null) {
/**
* If the user types a 0 as the first number, we want
* to keep track of that so that when they type the next
* number, we can move to the next segment.
*/
if (num === 0) {
props.lastKeyZero.value = true;
return {
value: null,
moveToNext
};
}
/**
* If the last key was a 0, or if the first number is
* greater than the max start digit (0-3 in most cases), then
* we want to move to the next segment, since it's not possible
* to continue typing a valid number in this segment.
*/
if (props.lastKeyZero.value || num > maxStart) moveToNext = true;
props.lastKeyZero.value = false;
/**
* If none of the above conditions are met, then we can just
* return the number as the segment value and continue typing
* in this segment.
*/
return {
value: num,
moveToNext
};
}
/**
* If the number of digits is 2, or if the total with the existing digit
* and the pressed digit is greater than the maximum value for this
* month, then we will reset the segment as if the user had pressed the
* backspace key and then typed the number.
*/
const digits = prev.toString().length;
const total = Number.parseInt(prev.toString() + num.toString());
/**
* If the number of digits is 2, or if the total with the existing digit
* and the pressed digit is greater than the maximum value for this
* month, then we will reset the segment as if the user had pressed the
* backspace key and then typed the number.
*/
if (digits === 2 || total > max) {
/**
* As we're doing elsewhere, we're checking if the number is greater
* than the max start digit (0-3 in most months), and if so, we're
* going to move to the next segment.
*/
if (num > maxStart || total > max) moveToNext = true;
return {
value: num,
moveToNext
};
}
moveToNext = true;
return {
value: total,
moveToNext
};
}
function updateMinuteOrSecond(num, prev) {
const max = 59;
let moveToNext = false;
const maxStart = Math.floor(max / 10);
/**
* If the user has left the segment, we want to reset the
* `prev` value so that we can start the segment over again
* when the user types a number.
*/
if (props.hasLeftFocus.value) {
props.hasLeftFocus.value = false;
prev = null;
}
if (prev === null) {
/**
* If the user types a 0 as the first number, we want
* to keep track of that so that when they type the next
* number, we can move to the next segment.
*/
if (num === 0) {
props.lastKeyZero.value = true;
return {
value: 0,
moveToNext
};
}
/**
* If the last key was a 0, or if the first number is
* greater than the max start digit (0-3 in most cases), then
* we want to move to the next segment, since it's not possible
* to continue typing a valid number in this segment.
*/
if (props.lastKeyZero.value || num > maxStart) moveToNext = true;
props.lastKeyZero.value = false;
/**
* If none of the above conditions are met, then we can just
* return the number as the segment value and continue typing
* in this segment.
*/
return {
value: num,
moveToNext
};
}
/**
* If the number of digits is 2, or if the total with the existing digit
* and the pressed digit is greater than the maximum value for this
* month, then we will reset the segment as if the user had pressed the
* backspace key and then typed the number.
*/
const digits = prev.toString().length;
const total = Number.parseInt(prev.toString() + num.toString());
/**
* If the number of digits is 2, or if the total with the existing digit
* and the pressed digit is greater than the maximum value for this
* month, then we will reset the segment as if the user had pressed the
* backspace key and then typed the number.
*/
if (digits === 2 || total > max) {
/**
* As we're doing elsewhere, we're checking if the number is greater
* than the max start digit (0-3 in most months), and if so, we're
* going to move to the next segment.
*/
if (num > maxStart) moveToNext = true;
return {
value: num,
moveToNext
};
}
moveToNext = true;
return {
value: total,
moveToNext
};
}
function updateHour(num, prev) {
const max = 24;
let moveToNext = false;
const maxStart = Math.floor(max / 10);
/**
* If the user has left the segment, we want to reset the
* `prev` value so that we can start the segment over again
* when the user types a number.
*/
if (props.hasLeftFocus.value) {
props.hasLeftFocus.value = false;
prev = null;
}
if (prev === null) {
/**
* If the user types a 0 as the first number, we want
* to keep track of that so that when they type the next
* number, we can move to the next segment.
*/
if (num === 0) {
props.lastKeyZero.value = true;
return {
value: 0,
moveToNext
};
}
/**
* If the last key was a 0, or if the first number is
* greater than the max start digit (0-3 in most cases), then
* we want to move to the next segment, since it's not possible
* to continue typing a valid number in this segment.
*/
if (props.lastKeyZero.value || num > maxStart) moveToNext = true;
props.lastKeyZero.value = false;
/**
* If none of the above conditions are met, then we can just
* return the number as the segment value and continue typing
* in this segment.
*/
return {
value: num,
moveToNext
};
}
/**
* If the number of digits is 2, or if the total with the existing digit
* and the pressed digit is greater than the maximum value for this
* month, then we will reset the segment as if the user had pressed the
* backspace key and then typed the number.
*/
const digits = prev.toString().length;
const total = Number.parseInt(prev.toString() + num.toString());
/**
* If the number of digits is 2, or if the total with the existing digit
* and the pressed digit is greater than the maximum value for this
* month, then we will reset the segment as if the user had pressed the
* backspace key and then typed the number.
*/
if (digits === 2 || total > max) {
/**
* As we're doing elsewhere, we're checking if the number is greater
* than the max start digit (0-3 in most months), and if so, we're
* going to move to the next segment.
*/
if (num > maxStart) moveToNext = true;
return {
value: num,
moveToNext
};
}
moveToNext = true;
return {
value: total,
moveToNext
};
}
function updateYear(num, prev) {
let moveToNext = false;
/**
* If the user has left the segment, we want to reset the
* `prev` value so that we can start the segment over again
* when the user types a number.
*/
if (props.hasLeftFocus.value) {
props.hasLeftFocus.value = false;
prev = null;
}
if (prev === null) return {
value: num === 0 ? 1 : num,
moveToNext
};
const str = prev.toString() + num.toString();
if (str.length > 4) return {
value: num === 0 ? 1 : num,
moveToNext
};
if (str.length === 4) moveToNext = true;
const int = Number.parseInt(str);
return {
value: int,
moveToNext
};
}
const attributes = computed(() => segmentBuilders[props.part]?.attrs({
disabled: props.disabled.value,
placeholder: props.placeholder.value,
hourCycle: props.hourCycle,
segmentValues: props.segmentValues.value,
formatter: props.formatter
}) ?? {});
function handleDaySegmentKeydown(e) {
if (!isAcceptableSegmentKey(e.key) || isSegmentNavigationKey(e.key)) return;
const prevValue = props.segmentValues.value.day;
if (e.key === kbd.ARROW_DOWN || e.key === kbd.ARROW_UP) {
props.segmentValues.value.day = dateTimeValueIncrementation({
e,
part: "day",
dateRef: props.placeholder.value,
prevValue
});
return;
}
if (isNumberString(e.key)) {
const num = Number.parseInt(e.key);
const segmentMonthValue = props.segmentValues.value.month;
const daysInMonth = segmentMonthValue ? getDaysInMonth(props.placeholder.value.set({ month: segmentMonthValue })) : 31;
const { value, moveToNext } = updateDayOrMonth(daysInMonth, num, prevValue);
props.segmentValues.value.day = value;
if (moveToNext) props.focusNext();
}
if (e.key === kbd.BACKSPACE) {
props.hasLeftFocus.value = false;
props.segmentValues.value.day = deleteValue(prevValue);
}
}
function handleMonthSegmentKeydown(e) {
if (!isAcceptableSegmentKey(e.key) || isSegmentNavigationKey(e.key)) return;
const prevValue = props.segmentValues.value.month;
if (e.key === kbd.ARROW_DOWN || e.key === kbd.ARROW_UP) {
props.segmentValues.value.month = dateTimeValueIncrementation({
e,
part: "month",
dateRef: props.placeholder.value,
prevValue
});
return;
}
if (isNumberString(e.key)) {
const num = Number.parseInt(e.key);
const { value, moveToNext } = updateDayOrMonth(12, num, prevValue);
props.segmentValues.value.month = value;
if (moveToNext) props.focusNext();
}
if (e.key === kbd.BACKSPACE) {
props.hasLeftFocus.value = false;
props.segmentValues.value.month = deleteValue(prevValue);
}
}
function handleYearSegmentKeydown(e) {
if (!isAcceptableSegmentKey(e.key) || isSegmentNavigationKey(e.key)) return;
const prevValue = props.segmentValues.value.year;
if (e.key === kbd.ARROW_DOWN || e.key === kbd.ARROW_UP) {
props.segmentValues.value.year = dateTimeValueIncrementation({
e,
part: "year",
dateRef: props.placeholder.value,
prevValue
});
return;
}
if (isNumberString(e.key)) {
const num = Number.parseInt(e.key);
const { value, moveToNext } = updateYear(num, prevValue);
props.segmentValues.value.year = value;
if (moveToNext) props.focusNext();
}
if (e.key === kbd.BACKSPACE) {
props.hasLeftFocus.value = false;
props.segmentValues.value.year = deleteValue(prevValue);
}
}
function handleHourSegmentKeydown(e) {
const dateRef = props.placeholder.value;
if (!isAcceptableSegmentKey(e.key) || isSegmentNavigationKey(e.key) || !("hour" in dateRef) || !("hour" in props.segmentValues.value)) return;
const prevValue = props.segmentValues.value.hour;
const hourCycle = props.hourCycle;
if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_DOWN) {
props.segmentValues.value.hour = dateTimeValueIncrementation({
e,
part: "hour",
dateRef: props.placeholder.value,
prevValue,
hourCycle
});
if ("dayPeriod" in props.segmentValues.value) {
if (props.segmentValues.value.hour < 12) props.segmentValues.value.dayPeriod = "AM";
else if (props.segmentValues.value.hour) props.segmentValues.value.dayPeriod = "PM";
}
return;
}
if (isNumberString(e.key)) {
const num = Number.parseInt(e.key);
const { value, moveToNext } = updateHour(num, prevValue);
if ("dayPeriod" in props.segmentValues.value && value && value > 12) props.segmentValues.value.dayPeriod = "PM";
else if ("dayPeriod" in props.segmentValues.value && value) props.segmentValues.value.dayPeriod = "AM";
props.segmentValues.value.hour = value;
if (moveToNext) props.focusNext();
}
if (e.key === kbd.BACKSPACE) {
props.hasLeftFocus.value = false;
props.segmentValues.value.hour = deleteValue(prevValue);
}
}
function handleMinuteSegmentKeydown(e) {
const dateRef = props.placeholder.value;
if (!isAcceptableSegmentKey(e.key) || isSegmentNavigationKey(e.key) || !("minute" in dateRef) || !("minute" in props.segmentValues.value)) return;
const prevValue = props.segmentValues.value.minute;
if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_DOWN) props.segmentValues.value.minute = minuteSecondIncrementation({
e,
part: "minute",
dateRef: props.placeholder.value,
prevValue
});
if (isNumberString(e.key)) {
const num = Number.parseInt(e.key);
const { value, moveToNext } = updateMinuteOrSecond(num, prevValue);
props.segmentValues.value.minute = value;
if (moveToNext) props.focusNext();
}
if (e.key === kbd.BACKSPACE) {
props.hasLeftFocus.value = false;
props.segmentValues.value.minute = deleteValue(prevValue);
}
}
function handleSecondSegmentKeydown(e) {
const dateRef = props.placeholder.value;
if (!isAcceptableSegmentKey(e.key) || isSegmentNavigationKey(e.key) || !("second" in dateRef) || !("second" in props.segmentValues.value)) return;
const prevValue = props.segmentValues.value.second;
if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_DOWN) props.segmentValues.value.second = minuteSecondIncrementation({
e,
part: "second",
dateRef: props.placeholder.value,
prevValue
});
if (isNumberString(e.key)) {
const num = Number.parseInt(e.key);
const { value, moveToNext } = updateMinuteOrSecond(num, prevValue);
props.segmentValues.value.second = value;
if (moveToNext) props.focusNext();
}
if (e.key === kbd.BACKSPACE) {
props.hasLeftFocus.value = false;
props.segmentValues.value.second = deleteValue(prevValue);
}
}
function handleDayPeriodSegmentKeydown(e) {
if ((!isAcceptableSegmentKey(e.key) || isSegmentNavigationKey(e.key)) && e.key !== "a" && e.key !== "p" || !("hour" in props.placeholder.value) || !("dayPeriod" in props.segmentValues.value)) return;
if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_DOWN) {
if (props.segmentValues.value.dayPeriod === "AM") {
props.segmentValues.value.dayPeriod = "PM";
props.segmentValues.value.hour = props.segmentValues.value.hour + 12;
return;
}
props.segmentValues.value.dayPeriod = "AM";
props.segmentValues.value.hour = props.segmentValues.value.hour - 12;
return;
}
if (["a", "A"].includes(e.key) && props.segmentValues.value.dayPeriod !== "AM") {
props.segmentValues.value.dayPeriod = "AM";
props.segmentValues.value.hour = props.segmentValues.value.hour - 12;
return;
}
if (["p", "P"].includes(e.key) && props.segmentValues.value.dayPeriod !== "PM") {
props.segmentValues.value.dayPeriod = "PM";
props.segmentValues.value.hour = props.segmentValues.value.hour + 12;
}
}
function handleSegmentClick(e) {
const disabled = props.disabled.value;
if (disabled) e.preventDefault();
}
function handleSegmentKeydown(e) {
const disabled = props.disabled.value;
const readonly = props.readonly.value;
if (e.key !== kbd.TAB) e.preventDefault();
if (disabled || readonly) return;
const segmentKeydownHandlers = {
day: handleDaySegmentKeydown,
month: handleMonthSegmentKeydown,
year: handleYearSegmentKeydown,
hour: handleHourSegmentKeydown,
minute: handleMinuteSegmentKeydown,
second: handleSecondSegmentKeydown,
dayPeriod: handleDayPeriodSegmentKeydown,
timeZoneName: () => {}
};
segmentKeydownHandlers[props.part](e);
if (![kbd.ARROW_LEFT, kbd.ARROW_RIGHT].includes(e.key) && e.key !== kbd.TAB && e.key !== kbd.SHIFT && isAcceptableSegmentKey(e.key)) {
if (Object.values(props.segmentValues.value).every((item) => item !== null)) {
const updateObject = { ...props.segmentValues.value };
let dateRef = props.placeholder.value.copy();
Object.keys(updateObject).forEach((part) => {
const value = updateObject[part];
dateRef = dateRef.set({ [part]: value });
});
props.modelValue.value = dateRef.copy();
}
}
}
return {
handleSegmentClick,
handleSegmentKeydown,
attributes
};
}
//#endregion
export { useDateField };
//# sourceMappingURL=useDateField.js.map