UNPKG

@kobalte/core

Version:

Unstyled components and primitives for building accessible web apps and design systems with SolidJS.

818 lines (811 loc) • 26.7 kB
import { SpinButtonRoot } from '../chunk/LUHKRAPV.js'; import { useLocale, createFilter } from '../chunk/7TPOQ36Z.js'; import { FormControlLabel } from '../chunk/7ZHN3PYD.js'; import { createFormResetListener } from '../chunk/ANN3A2QM.js'; import { FormControlErrorMessage } from '../chunk/ICNSTULC.js'; export { FormControlErrorMessage as ErrorMessage } from '../chunk/ICNSTULC.js'; import { FormControlDescription, useFormControlContext, FORM_CONTROL_PROP_NAMES, createFormControl, FormControlContext } from '../chunk/YKGT7A57.js'; export { FormControlDescription as Description } from '../chunk/YKGT7A57.js'; import { createRegisterId } from '../chunk/E4R2EMM4.js'; import { createControllableSignal } from '../chunk/BLN63FDC.js'; import { Polymorphic } from '../chunk/6Y7B2NEO.js'; import { spread, mergeProps, effect, style, createComponent, memo, template, setAttribute, insert } from 'solid-js/web'; import { visuallyHiddenStyles, mergeDefaultProps, mergeRefs, createFocusManager, createGenerateId, access, isIOS, getActiveElement, callHandler, isMac, scrollIntoViewport, getScrollParent, getWindow } from '@kobalte/utils'; import { createContext, splitProps, createEffect, Index, createSignal, createUniqueId, createMemo, mergeProps as mergeProps$1, children, on, onCleanup, Show, useContext } from 'solid-js'; import { NumberParser } from '@internationalized/number'; var TimeFieldContext = createContext(); function useTimeFieldContext() { const context = useContext(TimeFieldContext); if (context === void 0) { throw new Error("[kobalte]: `useTimeFieldContext` must be used within a `TimeField` component"); } return context; } // src/time-field/time-field-hidden-input.tsx var _tmpl$ = /* @__PURE__ */ template(`<input type="text" tabindex="-1" aria-hidden="true">`); function TimeFieldHiddenInput(props) { const formControlContext = useFormControlContext(); const context = useTimeFieldContext(); return ( // biome-ignore lint/a11y/noAriaHiddenOnFocusable: it is not focusable. (() => { const _el$ = _tmpl$(); _el$.addEventListener("change", (e) => context.setValue(parseTime(e.currentTarget.value))); spread(_el$, mergeProps({ get name() { return formControlContext.name(); }, get value() { return context.formattedValue() || ""; }, get required() { return formControlContext.isRequired(); }, get disabled() { return formControlContext.isDisabled(); }, get readOnly() { return formControlContext.isReadOnly(); } }, props), false, false); effect((_$p) => style(_el$, visuallyHiddenStyles, _$p)); return _el$; })() ); } var parseTime = (value) => { const [time, period] = value.split(" "); const [hours, minutes, seconds] = time.split(":"); const parsedHours = period === "PM" ? Number.parseInt(hours) + 12 : Number.parseInt(hours); const parsedMinutes = Number.parseInt(minutes); const parsedSeconds = Number.parseInt(seconds); return { hour: Number.isNaN(parsedHours) ? void 0 : parsedHours, minute: Number.isNaN(parsedMinutes) ? void 0 : parsedMinutes, second: Number.isNaN(parsedSeconds) ? void 0 : parsedSeconds }; }; function TimeFieldInput(props) { const formControlContext = useFormControlContext(); const timeFieldContext = useTimeFieldContext(); const mergedProps = mergeDefaultProps({ id: timeFieldContext.generateId("input") }, props); const [local, others] = splitProps(mergedProps, ["ref", "children", "onKeyDown", "onFocusOut", "aria-labelledby", "aria-describedby"]); createEffect(() => timeFieldContext.setFieldAriaLabel(others["aria-label"])); createEffect(() => { timeFieldContext.setFieldAriaLabelledBy(formControlContext.getAriaLabelledBy(others.id, others["aria-label"], local["aria-labelledby"])); }); createEffect(() => { timeFieldContext.setFieldAriaDescribedBy([local["aria-describedby"], timeFieldContext.ariaDescribedBy()].filter(Boolean).join(" ")); }); const { direction } = useLocale(); const onKeyDown = (e) => { callHandler(e, local.onKeyDown); switch (e.key) { case "ArrowLeft": e.preventDefault(); e.stopPropagation(); if (direction() === "rtl") { timeFieldContext.focusManager().focusNext(); } else { timeFieldContext.focusManager().focusPrevious(); } break; case "ArrowRight": e.preventDefault(); e.stopPropagation(); if (direction() === "rtl") { timeFieldContext.focusManager().focusPrevious(); } else { timeFieldContext.focusManager().focusNext(); } break; } }; const onFocusOut = (e) => { callHandler(e, local.onFocusOut); if (formControlContext.isDisabled() || formControlContext.isReadOnly()) { return; } }; return createComponent(Polymorphic, mergeProps({ as: "div", role: "presentation", ref(r$) { const _ref$ = mergeRefs(timeFieldContext.setInputRef, local.ref); typeof _ref$ === "function" && _ref$(r$); }, get ["aria-labelledby"]() { return timeFieldContext.fieldAriaLabelledBy(); }, get ["aria-describedby"]() { return timeFieldContext.fieldAriaDescribedBy(); }, onKeyDown, onFocusOut }, () => formControlContext.dataset(), others, { get children() { return createComponent(Index, { get each() { return timeFieldContext.segments(); }, children: (segment) => local.children?.(segment) }); } })); } function TimeFieldLabel(props) { return createComponent(FormControlLabel, mergeProps({ as: "span" }, props)); } var _tmpl$2 = /* @__PURE__ */ template(`<div>`); function TimeFieldValueDescription() { const context = useTimeFieldContext(); const defaultId = context.generateId("value-description"); const isValid = () => context.value()?.toString() !== void 0; createEffect(() => { if (!isValid()) { return; } onCleanup(context.registerValueDescriptionId(defaultId)); }); return createComponent(Show, { get when() { return isValid(); }, get children() { const _el$ = _tmpl$2(); setAttribute(_el$, "id", defaultId); _el$.style.setProperty("display", "none"); insert(_el$, () => context.translations().selectedTimeDescription(context.formattedValue())); return _el$; } }); } // src/time-field/time-field.intl.ts var TIME_FIELD_INTL_MESSAGES = { hour: "hour", minute: "minute", second: "second", am: "AM", pm: "PM", dayPeriod: "AM/PM", timeZoneName: "time zone", selectedTimeDescription: (time) => `Selected Time: ${time}` }; // src/time-field/time-field-root.tsx function TimeFieldRoot(props) { const [ref, setRef] = createSignal(); const defaultId = `time-field-${createUniqueId()}`; const mergedProps = mergeDefaultProps({ id: defaultId, granularity: "minute", translations: TIME_FIELD_INTL_MESSAGES }, props); const [local, formControlProps, others] = splitProps(mergedProps, ["ref", "translations", "min", "max", "placeholder", "hourCycle", "granularity", "forceLeadingZeros", "validationState", "value", "defaultValue", "onChange", "aria-labelledby", "aria-describedby", "children"], FORM_CONTROL_PROP_NAMES); const [inputRef, setInputRef] = createSignal(); const [valueDescriptionId, setValueDescriptionId] = createSignal(); const [fieldAriaLabel, setFieldAriaLabel] = createSignal(); const [fieldAriaLabelledBy, setFieldAriaLabelledBy] = createSignal(); const [fieldAriaDescribedBy, setFieldAriaDescribedBy] = createSignal(); const focusManager = createFocusManager(inputRef); const [value, _setValue] = createControllableSignal({ value: () => local.value, defaultValue: () => local.defaultValue, // @ts-ignore onChange: (value2) => local.onChange?.(value2) }); const setValue = (v) => { if (!v) { _setValue(void 0); return; } const newValue = { ...value() }; if ("hour" in v) newValue.hour = v.hour; if ("minute" in v) newValue.minute = v.minute; if ("second" in v) newValue.second = v.second; _setValue(newValue); }; createFormResetListener(ref, () => { setValue(local.defaultValue); }); const validationState = createMemo(() => { if (local.validationState) { return local.validationState; } const minTime = Number.parseInt(`${(local.min?.hour ?? "00").toString().padStart(2, "0")}${(local.min?.minute ?? "00").toString().padStart(2, "0")}${(local.min?.second ?? "00").toString().padStart(2, "0")}`); const maxTime = Number.parseInt(`${(local.max?.hour ?? "23").toString().padStart(2, "0")}${(local.max?.minute ?? "59").toString().padStart(2, "0")}${(local.max?.second ?? "59").toString().padStart(2, "0")}`); const val = Number.parseInt(`${(value()?.hour ?? "00").toString().padStart(2, "0")}${(value()?.minute ?? "00").toString().padStart(2, "0")}${(value()?.second ?? "00").toString().padStart(2, "0")}`); if (val > maxTime || val < minTime) return "invalid"; return void 0; }); const { formControlContext } = createFormControl(mergeProps$1(formControlProps, { get validationState() { return validationState(); } })); const resolvedGranularity = createMemo(() => { const granularity = props.granularity ?? "minute"; if (typeof granularity === "object") return granularity; return { hour: true, minute: granularity === "minute" || granularity === "second", second: granularity === "second" }; }); const formattedValue = createMemo(() => { let hour = value()?.hour ?? 0; const pm = hour > 12; if (local.hourCycle === 12 && pm) { hour -= 12; } const padding = local.forceLeadingZeros ? 2 : 1; const segments2 = []; if (resolvedGranularity().hour) { segments2.push(hour.toString().padStart(padding, "0")); } if (resolvedGranularity().minute) { segments2.push((value()?.minute ?? 0).toString().padStart(padding, "0")); } if (resolvedGranularity().second) { segments2.push((value()?.second ?? 0).toString().padStart(padding, "0")); } let val = segments2.join(":"); if (local.hourCycle === 12) { val += ` ${pm ? local.translations?.pm : local.translations?.am}`; } return val; }); const ariaLabelledBy = () => { return formControlContext.getAriaLabelledBy(access(formControlProps.id), others["aria-label"], local["aria-labelledby"]); }; const ariaDescribedBy = () => { return [valueDescriptionId(), formControlContext.getAriaDescribedBy(local["aria-describedby"])].filter(Boolean).join(" ") || void 0; }; const segments = createMemo(() => { const seg = Object.keys(resolvedGranularity()).filter((k) => resolvedGranularity()[k]); if (seg.includes("hour") && local.hourCycle === 12) seg.push("dayPeriod"); return seg; }); const context = { translations: () => local.translations, value, setValue, hourCycle: () => local.hourCycle, resolvedGranularity, forceLeadingZeros: () => local.forceLeadingZeros ?? false, placeholder: () => local.placeholder, formattedValue, focusManager: () => focusManager, isDisabled: () => formControlContext.isDisabled() ?? false, ariaDescribedBy, inputRef, setInputRef, valueDescriptionId, registerValueDescriptionId: createRegisterId(setValueDescriptionId), generateId: createGenerateId(() => access(formControlProps.id)), segments, fieldAriaLabel, fieldAriaLabelledBy, fieldAriaDescribedBy, setFieldAriaLabel, setFieldAriaLabelledBy, setFieldAriaDescribedBy }; return createComponent(FormControlContext.Provider, { value: formControlContext, get children() { return createComponent(TimeFieldContext.Provider, { value: context, get children() { return createComponent(Polymorphic, mergeProps({ as: "div", ref(r$) { const _ref$ = mergeRefs(setRef, local.ref); typeof _ref$ === "function" && _ref$(r$); }, role: "group", get id() { return access(formControlProps.id); }, get ["aria-invalid"]() { return formControlContext.validationState() === "invalid" || void 0; }, get ["aria-required"]() { return formControlContext.isRequired() || void 0; }, get ["aria-disabled"]() { return formControlContext.isDisabled() || void 0; }, get ["aria-readonly"]() { return formControlContext.isReadOnly() || void 0; }, get ["aria-labelledby"]() { return ariaLabelledBy(); }, get ["aria-describedby"]() { return ariaDescribedBy(); } }, () => formControlContext.dataset(), others, { get children() { return [memo(() => local.children), createComponent(TimeFieldValueDescription, {})]; } })); } }); } }); } var _tmpl$3 = /* @__PURE__ */ template(`<span>:`); var PAGE_STEP = { hour: 2, minute: 15, second: 15 }; function TimeFieldSegment(props) { let ref; const formControlContext = useFormControlContext(); const context = useTimeFieldContext(); const mergedProps = mergeDefaultProps({ id: `${context.generateId("segment")}-${createUniqueId()}` }, props); const [local, others] = splitProps(mergedProps, ["ref", "segment", "onKeyDown", "onBeforeInput", "onInput", "onFocus", "children"]); const { locale } = useLocale(); const resolvedChildren = children(() => local.children); let enteredKeys = ""; let composition = ""; const touchPropOverrides = createMemo(() => { return isIOS() ? { role: "textbox", "aria-valuemax": void 0, "aria-valuemin": void 0, "aria-valuetext": void 0, "aria-valuenow": void 0 } : {}; }); const firstSegment = createMemo(() => context.segments()[0]); const name = createMemo(() => { return context.translations()[local.segment]; }); const ariaLabel = createMemo(() => { return [name(), context.fieldAriaLabel()].filter(Boolean).join(", "); }); const ariaDescribedBy = createMemo(() => { if (local.segment !== firstSegment() && formControlContext.validationState() !== "invalid") { return void 0; } return context.fieldAriaDescribedBy(); }); const ariaLabelledBy = createMemo(() => { return [mergedProps.id, context.fieldAriaLabelledBy()].filter(Boolean).join(" ") || void 0; }); const inputMode = createMemo(() => { return formControlContext.isDisabled() || local.segment === "dayPeriod" ? void 0 : "numeric"; }); const filter = createFilter({ sensitivity: "base" }); const numberParser = createMemo(() => { return new NumberParser(locale(), { maximumFractionDigits: 0 }); }); const maxValue = () => local.segment === "hour" ? 23 : 59; const onBackspaceKeyDown = () => { if (local.segment !== "dayPeriod" && context.value()?.[local.segment] === void 0) { context.focusManager().focusPrevious(); return; } if (local.segment === "dayPeriod") { if ((context.value()?.hour ?? 0) >= 12) { if (!formControlContext.isReadOnly()) context.setValue({ hour: context.value().hour - 12 }); } else context.focusManager().focusPrevious(); return; } if (formControlContext.isReadOnly()) return; let newValue = (context.value()?.[local.segment] ?? 0).toString().slice(0, -1); const parsed = numberParser().parse(newValue); newValue = parsed === 0 ? "" : newValue; if (newValue.length === 0 || parsed === 0) { context.setValue({ [local.segment]: void 0 }); } else { context.setValue({ [local.segment]: parsed }); } enteredKeys = newValue; }; const onKeyDown = (e) => { callHandler(e, local.onKeyDown); if (e.key === "a" && (isMac() ? e.metaKey : e.ctrlKey)) { e.preventDefault(); } if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) { return; } switch (e.key) { case "Backspace": case "Delete": { e.preventDefault(); e.stopPropagation(); onBackspaceKeyDown(); break; } } }; const onInputBase = (key) => { if (formControlContext.isDisabled() || formControlContext.isReadOnly()) { return; } const newValue = enteredKeys + key; switch (local.segment) { case "dayPeriod": if (filter.startsWith(context.translations().am, key)) { if ((context.value()?.hour ?? 0) >= 12) context.setValue({ hour: context.value().hour - 12 }); } else if (filter.startsWith(context.translations().pm, key)) { if ((context.value()?.hour ?? 0) < 12) context.setValue({ hour: context.value().hour + 12 }); } else { break; } context.focusManager().focusNext(); break; case "hour": case "minute": case "second": { if (!numberParser().isValidPartialNumber(newValue)) { return; } let numberValue = numberParser().parse(newValue); let allowsZero = true; if (local.segment === "hour" && context.hourCycle() === 12) { allowsZero = false; if (numberValue >= 12) { numberValue = numberParser().parse(key); } if ((context.value()?.hour ?? 0) >= 12) { numberValue += 12; } } if (numberValue > maxValue()) { numberValue = numberParser().parse(key); } if (Number.isNaN(numberValue)) return; const shouldSetValue = numberValue !== 0 || allowsZero; if (shouldSetValue) { context.setValue({ [local.segment]: numberValue }); } if (Number(`${numberValue}0`) > maxValue() || newValue.length >= String(maxValue).length) { enteredKeys = ""; if (shouldSetValue) { context.focusManager().focusNext(); } } else { enteredKeys = newValue; } break; } } }; const onBeforeInput = (e) => { callHandler(e, local.onBeforeInput); e.preventDefault(); switch (e.inputType) { case "deleteContentBackward": case "deleteContentForward": if (local.segment !== "dayPeriod" && context.value()?.[local.segment] !== void 0 && !formControlContext.isReadOnly()) { onBackspaceKeyDown(); } break; case "insertCompositionText": if (ref) { composition = ref.textContent; ref.textContent = ref.textContent; } break; default: if (e.data != null) { onInputBase(e.data); } break; } }; const onInput = (e) => { callHandler(e, local.onInput); const { inputType, data } = e; if (ref && data != null) { switch (inputType) { case "insertCompositionText": ref.textContent = composition; if (filter.startsWith(context.translations().am, data) || filter.startsWith(context.translations().pm, data)) { onInputBase(data); } break; } } }; const onFocus = (e) => { callHandler(e, local.onFocus); if (ref) { enteredKeys = ""; scrollIntoViewport(ref, { containingElement: getScrollParent(ref) }); const selection = getWindow(ref).getSelection(); selection?.collapse(ref); } }; const cycleDayPeriod = () => { if ((context.value()?.hour ?? 0) >= 12) context.setValue({ hour: context.value().hour - 12 }); else context.setValue({ hour: context.value().hour + 12 }); }; const adjust = (delta) => { const hour = context.value()?.hour ?? context.placeholder()?.hour ?? 0; if (local.segment === "hour" && context.hourCycle() === 12) { const isPM = hour >= 12; const h12 = hour % 12; const next = (h12 + delta + 12) % 12; return next + (isPM ? 12 : 0); } const max = maxValue(); return ((context.value()?.[local.segment] ?? context.placeholder()?.[local.segment] ?? 0) + delta + (max + 1)) % (max + 1); }; const onIncrement = () => { enteredKeys = ""; if (local.segment === "dayPeriod") { cycleDayPeriod(); return; } context.setValue({ [local.segment]: adjust(1) }); }; const onDecrement = () => { enteredKeys = ""; if (local.segment === "dayPeriod") { cycleDayPeriod(); return; } context.setValue({ [local.segment]: adjust(-1) }); }; const onIncrementPage = () => { enteredKeys = ""; if (local.segment === "dayPeriod") { cycleDayPeriod(); return; } context.setValue({ [local.segment]: adjust(PAGE_STEP[local.segment]) }); }; const onDecrementPage = () => { enteredKeys = ""; if (local.segment === "dayPeriod") { cycleDayPeriod(); return; } context.setValue({ [local.segment]: adjust(-PAGE_STEP[local.segment]) }); }; const onDecrementToMin = () => { enteredKeys = ""; if (local.segment === "dayPeriod") { cycleDayPeriod(); return; } if (local.segment === "hour" && context.hourCycle() === 12) { if ((context.value()?.hour ?? 0) >= 12) context.setValue({ hour: 12 }); else context.setValue({ hour: 0 }); } else context.setValue({ [local.segment]: 0 }); }; const onIncrementToMax = () => { enteredKeys = ""; if (local.segment === "dayPeriod") { cycleDayPeriod(); return; } if (local.segment === "hour" && context.hourCycle() === 12) { if ((context.value()?.hour ?? 0) >= 12) context.setValue({ hour: 24 }); else context.setValue({ hour: 12 }); } else context.setValue({ [local.segment]: maxValue() }); }; createEffect(on([() => ref, () => context.focusManager()], ([ref2, focusManager]) => { const element = ref2; onCleanup(() => { if (getActiveElement(element) === element) { const prev = focusManager.focusPrevious(); if (!prev) { focusManager.focusNext(); } } }); })); const getValue = () => { if (local.segment === "dayPeriod") return context.translations()[(context.value()?.hour ?? context.placeholder()?.hour ?? 0) >= 12 ? "pm" : "am"]; if (local.segment === "hour") { const val = context.value()?.hour ?? context.placeholder()?.hour; if (val === void 0) return void 0; if (context.hourCycle() === 12) { if (val > 12) return val - 12; if (val === 0) return 12; } return val; } return context.value()?.[local.segment] ?? context.placeholder()?.[local.segment]; }; const padding = () => local.segment !== "hour" ? 2 : context.forceLeadingZeros() ? 2 : 1; const textValue = () => (getValue()?.toString() ?? "-".padStart(padding(), "-")).padStart(padding(), "0"); return [createComponent(SpinButtonRoot, mergeProps({ ref(r$) { const _ref$ = mergeRefs((el) => ref = el, local.ref); typeof _ref$ === "function" && _ref$(r$); }, get tabIndex() { return formControlContext.isDisabled() ? void 0 : 0; }, get value() { return getValue(); }, get textValue() { return textValue(); }, minValue: 0, get maxValue() { return maxValue(); }, get validationState() { return formControlContext.validationState(); }, get required() { return formControlContext.isRequired(); }, get disabled() { return formControlContext.isDisabled(); }, get readOnly() { return formControlContext.isReadOnly(); }, get contentEditable() { return !formControlContext.isReadOnly(); }, get inputMode() { return inputMode(); }, get autocorrect() { return !formControlContext.isReadOnly() ? "off" : void 0; }, get autoCapitalize() { return !formControlContext.isReadOnly() ? "off" : void 0; }, get spellcheck() { return !formControlContext.isReadOnly() ? false : void 0; }, get enterkeyhint() { return !formControlContext.isReadOnly() ? "next" : void 0; }, style: { "caret-color": "transparent" }, get ["aria-label"]() { return ariaLabel(); }, get ["aria-labelledby"]() { return ariaLabelledBy(); }, get ["aria-describedby"]() { return ariaDescribedBy(); }, get ["data-placeholder"]() { return context.value()?.[local.segment === "dayPeriod" ? "hour" : local.segment] === void 0 ? "" : void 0; }, get ["data-type"]() { return local.segment; }, onKeyDown, onBeforeInput, onInput, onFocus, onIncrement, onDecrement, onIncrementPage, onDecrementPage, onDecrementToMin, onIncrementToMax }, () => formControlContext.dataset(), others, touchPropOverrides, { get children() { return createComponent(Show, { get when() { return resolvedChildren(); }, get fallback() { return textValue().replaceAll("-", "\u2013"); }, get children() { return resolvedChildren(); } }); } })), createComponent(Show, { get when() { return memo(() => local.segment === "hour")() && (context.resolvedGranularity().minute || context.resolvedGranularity().second) || local.segment === "minute" && context.resolvedGranularity().second; }, get children() { return _tmpl$3(); } })]; } // src/time-field/index.tsx var TimeField = Object.assign(TimeFieldRoot, { Label: TimeFieldLabel, Input: TimeFieldInput, Segment: TimeFieldSegment, Description: FormControlDescription, ErrorMessage: FormControlErrorMessage, HiddenInput: TimeFieldHiddenInput }); export { TimeFieldHiddenInput as HiddenInput, TimeFieldInput as Input, TimeFieldLabel as Label, TimeFieldRoot as Root, TimeFieldSegment as Segment, TimeField };