# useMaskedTimeInput

[← Back to Composables README](https://github.com/NantHealth/featherk/blob/integration/packages/composables/README.md)

Composable for a masked single-time input that pairs naturally with Kendo Vue `MaskedTextBox`, and optionally coordinates with a `TimePicker`. It handles typed input in the mask `hh:mm AM`, keyboard steppers (ArrowUp/ArrowDown), wheel steppers, caret persistence, minute step granularity, min/max time validation, and clean emission only when the input is complete and valid.

This composable preserves the user's raw input: it does not coerce or rewrite what the user has typed in `onChange` beyond lightweight conveniences (e.g., completing `AM/PM`). It emits a parsed `Date` (today's date with the chosen time) only when the input is complete and valid.

## Prerequisites

- Vue 3 Composition API
- `@progress/kendo-vue-inputs` (for `MaskedTextBox`)
- Optional: `@progress/kendo-vue-dateinputs` (for `TimePicker`) and `@progress/kendo-vue-popup` for a popup

## Quick Start

```vue
<script setup lang="ts">
import { ref } from "vue";
import { TimePicker } from "@progress/kendo-vue-dateinputs";
import { MaskedTextBox } from "@progress/kendo-vue-inputs";
import { Error } from "@progress/kendo-vue-labels";
import { useMaskedTimeInput } from "@featherk/composables/time";
import { usePopupTrap } from "@featherk/composables/trap";

// Shared model for TimePicker and composable
const selectedTime = ref<Date | null | undefined>();
const showPicker = ref(false);
const timePickerRef = ref<HTMLElement | null>(null);

// Example bounds (customize for your app)
const minTime = new Date(new Date().setHours(8, 0, 0, 0)); // 8:00 AM
const maxTime = new Date(new Date().setHours(17, 0, 0, 0)); // 5:00 PM

const masked = useMaskedTimeInput({
  id: "startTime",
  onChange: ({ value }) => { selectedTime.value = value ?? undefined; },
  onShowPicker: () => (showPicker.value = true), // Space opens the time menu
  externalValue: selectedTime,                    // Mirror TimePicker selection into the mask
  minuteStep: 5,
  min: minTime,
  max: maxTime,
  required: true,
});

// Destructure for simpler template bindings
const {
  raw,
  rules,
  placeholder,
  isValid,
  validationMessage,
  hour24,          // 24-hour format (0-23)
  time24,          // HH:MM:SS format for database
  parsedRawTime,   // Parsed Date object
  debugEnabled,
  debugLines,
} = masked;

// Integrate focus trap for the TimePicker popup
usePopupTrap({
  isOpen: showPicker,
  onRequestClose: () => (showPicker.value = false),
  triggerEl: timePickerRef,
  popupSelector: [".k-timeselector", ".k-popup"],
});
</script>

<template>
  <div ref="timePickerRef">
    <TimePicker
      v-model="selectedTime"
      dateInput="masked"
      :format="'hh:mm a'"
      :show="showPicker"
      @open="showPicker = true"
      @close="showPicker = false"
      :style="{ width: 'fit-content' }"
    >
      <template #masked="{ props }">
        <MaskedTextBox
          id="startTime"
          :mask="'Hh:Mm Aa'"
          :rules="rules"
          :value="raw"
          :placeholder="placeholder"
          :showClearButton="false"
          @change="masked.handleChange"
          @keydown="masked.handleKeyDown"
          @keyup="masked.handleKeyUp"
          @click="masked.handleClick"
          @wheel="masked.handleWheel"
          @blur="masked.handleBlur"
        />
      </template>
    </TimePicker>
  </div>
  <Error for="startTime">{{ validationMessage }}</Error>

  <!-- Optional: show internal debug lines during integration -->
  <div v-if="debugEnabled" style="margin-top: 8px; color: #475467;">
    <div v-for="(row, idx) in debugLines" :key="idx">
      <strong>{{ row[0] }}:</strong> {{ row[1] }}
    </div>
  </div>
  </template>
```

### Full Example

For a complete integration including a focus trap, store sync, and UI tweaks, see the demo view referenced in this repository.

## API

### `useMaskedTimeInput(options)`

Creates a controller for a masked single-time input.

#### Options

- `id: string`: DOM id of the `MaskedTextBox` input element.
  - Required for composable-managed focus restoration after TimePicker popup actions (`Now`/`Set`) close the menu.
- `onChange: (p: ChangePayload) => void`: Called with `{ value, event }` when the composable has a new parsed value to emit.
  - Emits `value = null` if input is empty, incomplete, invalid, or outside min/max.
  - Does not mutate the user's raw string beyond minor conveniences.
- `onShowPicker: (e: KeyboardEvent) => void`: Called when Space is pressed on the input; use this to open a time menu/popup.
- `externalValue?: Ref<string | Date | null | undefined>`: Reactive external source for the selected time. When it updates to a valid `Date`, the composable mirrors it into `raw` without clobbering in‑progress typing.
- `minuteStep?: 1 | 5 | 10 | 15 | 20 | 30`: Stepping granularity when ArrowUp/ArrowDown are used in the minutes segment.
- `minuteStepRef?: Ref<1 | 5 | 10 | 15 | 20 | 30 | undefined>`: Reactive alternative to update step granularity at runtime.
- `timeFormat?: string`: Placeholder/format hint (default: `hh:mm AM`).
- `min?: Date | null | undefined` / `max?: Date | null | undefined`: Bounds for validation. These are plain `Date` values (not `Ref<Date>`), since min/max do not require reactivity.
- `required?: boolean`: When `true`, the field is treated as required. While focused, blank/incomplete values are not flagged as invalid; after blur, a blank or incomplete value is considered invalid with reason `"incomplete"` and validation message `"Required"`.
- `debug?: boolean`: Enables internal debug reporting (e.g., `debugLines`).

#### Returns

- `raw: Ref<string>`: The masked string `hh:mm AM`. Use as `v-model` for `MaskedTextBox`.
- `rules: Record<string, RegExp>`: Relaxed mask rules for Kendo `MaskedTextBox` to allow free typing, followed by composable validation.
- `cursorPos: Ref<number | undefined>`: Caret position for restoring after programmatic updates.
- `placeholder: Ref<string>`: Format placeholder (defaults to `hh:mm AM`).
- `digitsOnly: Computed<string>`: Raw string stripped of non-digits.
- `hour: Readonly<Computed<number>>`: 12-hour format hour (1-12).
- `hour24: Readonly<Computed<number>>`: 24-hour format hour (0-23). Useful for calculations and comparisons.
- `minute: Readonly<Computed<number>>`: Minutes (0-59).
- `period: Readonly<Computed<"AM" | "PM">>`: AM or PM period.
- `time24: Readonly<Computed<string | null>>`: Time in 24-hour `HH:MM:SS` format (e.g., `"14:30:00"`) for database storage. Returns `null` if input is incomplete or invalid.
- `parsedRawTime: Readonly<Computed<Date | null>>`: The parsed time as a `Date` object (today's date with the entered time). Returns `null` if input is incomplete or invalid.
- `parsedRawTime: Readonly<Computed<Date | null>>`: The parsed time as a `Date` object (today's date with the entered time). Returns `null` if input is incomplete or invalid.
- `initStyling(): void`: Re-applies the `fk-timepicker` theming class to the parent of the closest `.k-timepicker` element. Exposed so consumers can re-run theming when Kendo updates the DOM; the function is idempotent. Use this to apply the fk-timepicker style hook when the time picker isn't available on initial load
- `isComplete: Computed<boolean>`: `true` when the raw string matches `^(\d{2}):(\d{2})\s([AP]M)$`.
- `isValid: Computed<boolean>`: Validity considering completeness, parsability, min/max bounds, and (when enabled) required state.
- `reason: Computed<string | undefined>`: Machine-readable reason for the current validation state:
  - `"incomplete"` (required + blank/incomplete after blur)
  - `"invalid-time"` (complete but not a real time)
  - `"out-of-bounds"` (outside min/max)
  - `"valid"` (everything OK)
  - `undefined` (no reason yet, e.g., optional + incomplete while typing)
- `validationMessage: Computed<string>`: Human-readable message for invalid states:
  - `"Required"` when `reason === "incomplete"`.
  - `Must be in hh:mm AM format.` (or your custom `timeFormat`) when `reason === "invalid-time"`.
  - Range messages (between/on or after/on or before) when `reason === "out-of-bounds"`.
- Event handlers to wire to the input:
  - `handleChange(event)` — Assigns `raw`, tracks caret, fills `AM/PM` when appropriate, emits parsed time when valid. For Kendo `TimePicker` popup actions, Kendo does not provide an explicit action type (`Now` vs `Set`) in the change payload, so the composable infers it from the clicked DOM target (`.k-time-now` / `.k-time-accept`) and normalizes behavior accordingly.
  - `handleKeyDown(event)` — Space opens the picker via `onShowPicker`; ArrowUp/ArrowDown act as steppers.
  - `handleWheel(event)` — Prevents page scroll; interprets wheel as steppers (up/down) for the focused part.
  - `handleKeyUp(event)` — Maintains caret position after cursor keys/steppers.
  - `handleClick(event)` — Captures caret position on click for restoring.
  - `handleBlur(event)` — Marks the field as touched (used for required logic) and, when `required` is true, converts an `externalValue` of `undefined` with a blank mask into an empty string so downstream validation can treat it as incomplete/required.

- Debug:
  - `debugEnabled: Ref<boolean>`
  - `debugLines: Computed<Array<{ label: string; value: string }>>`

## Behavior Details

- **Mask and parsing**: Expects `00:00 AM`. Parsing requires the input to be complete and form a real time (`01–12` hours, `00–59` minutes), with `AM/PM`. Parsed output is a `Date` for today at the chosen time.
- **Multiple format outputs**: The composable provides the time in multiple formats for different use cases:
  - `raw`: User-facing 12-hour format with AM/PM (e.g., `"02:30 PM"`)
  - `hour24`: 24-hour format hour (0-23) for calculations
  - `time24`: Database-friendly `HH:MM:SS` format (e.g., `"14:30:00"`)
  - `parsedRawTime`: Full `Date` object with today's date and the entered time
- **AM/PM convenience**: When caret is in the AM/PM segment and a user types `A/a` or `P/p`, the composable auto-completes to `AM`/`PM`.
- **Emission policy**: Emits `Date` only when the raw is complete, valid, and within `min`/`max`. Otherwise emits `null`. Empty input emits `null`. When `required` is enabled, a blank or incomplete value is only considered invalid after blur.
- **Kendo popup action inference**: Kendo `TimePicker` change events do not distinguish `Now` and `Set` actions. The composable infers these actions by inspecting the event target in the popup DOM (`.k-time-now` and `.k-time-accept`) so each action can be handled predictably.
- **Focus after popup close**: After `Now` or `Set` is used and the popup closes, the composable restores focus to the masked input using the provided `id`. Focus is deferred until after close/render lifecycle completion to avoid focus loss during popup teardown.
- **Min/Max validation**: Bounds are evaluated by minutes since midnight. Times outside bounds are invalid (`isValid=false`) and produce a range `validationMessage`.
- **Steppers**: ArrowUp/ArrowDown and mouse wheel increment/decrement the focused part (hours, minutes with `minuteStep`, or `AM/PM`). If raw is incomplete/invalid, steppers initialize to a sensible baseline before emitting.
- **Caret persistence**: After programmatic updates (including steppers), the caret is restored to the prior position to preserve typing flow.
- **Space key**: The input consumes Space and calls `onShowPicker` to open a time menu without inserting a space into `raw`.
- **External sync**: When `externalValue` is set to a valid `Date`, `raw` mirrors it (e.g., from a `TimePicker` selection). If external becomes null/invalid, `raw` is preserved to avoid clobbering in-progress text.
- **Styling hook**: On mount, adds a theming class (e.g., `fk-timepicker`) to the parent of the closest Kendo time picker element for styling.
- **Styling hook**: On mount, adds a theming class (e.g., `fk-timepicker`) to the parent of the closest Kendo time picker element for styling. The composable also exposes `initStyling()` so consumers can re-run styling when needed (for example, after Kendo performs DOM updates). `initStyling()` is safe to call repeatedly — it is idempotent.

## Integration Patterns

- **MaskedTextBox only**: Use the Quick Start to accept typed times with validation — no time menu required.
- **Popup + TimePicker**: Bind a `TimePicker` to the same `timeValue` ref passed as `externalValue`. Use Space key (`onShowPicker`) to open the popup and keep bounds (`minTime`/`maxTime`) aligned.
- **Minute step control**: Provide `minuteStep` or a reactive `minuteStepRef` to control ArrowUp/ArrowDown behavior in the minutes segment at runtime.
- **Database storage**: Use the `time24` computed property to get time in `HH:MM:SS` format (e.g., `"14:30:00"`), perfect for storing in SQL TIME columns or string fields. This format is sortable, unambiguous, and follows database standards.

### Important: Kendo TimePicker slot name

When replacing the built‑in input inside Kendo Vue `TimePicker`, the component expects the prop/slot name `dateInput`, not `timeInput`. This is a Kendo API quirk shared with `DatePicker`.

Example:

```vue
<TimePicker v-model="timeValue" dateInput="masked">
  <template #masked="{ props }">
    <!-- your custom MaskedTextBox here -->
  </template>
</TimePicker>
```

If you use `timeInput`, the slot will not render and you may waste time debugging. Our demo uses `dateInput="masked"` on `TimePicker` to align with this behavior.

## Accessibility Notes

- Ensure labels (e.g., Kendo `Label` with `for`) correctly reference the input `id`.
- If using a popup time menu, consider adding a focus trap and Escape/OutsideClick handling (see `usePopupTrap`).
- Keep tab order logical; Space opens the menu, Escape should close it.

## Limitations & Assumptions

- Time format is fixed to `hh:mm AM`; internationalization and 24-hour masks are not included yet.
- Built to pair with Kendo Vue inputs/dateinputs; other inputs may need minor adjustments.

## Tips

- Keep `min`/`max` aligned with your `TimePicker` configuration for consistent validation.
- Use `debug: true` and display `debugLines` during integration to verify caret, parsed values, and masks.
- Avoid mutating `raw` externally except through `v-model`; let the composable manage its value and emit parsed times.
- **Database storage**: Use the `time24` property to get the time in `HH:MM:SS` format (e.g., `"14:30:00"`), which is ideal for storing in SQL TIME columns or as string fields. This format is sortable, unambiguous, and database-standard.
- **24-hour calculations**: Use `hour24` for time comparisons and calculations without needing to worry about AM/PM conversions.

## Types

```ts
export type ChangePayload = { value: Date | null; event: any };
```

## Reference Implementation

See the project’s full reference usage in [src/components/custom-time-picker/CustomTimePicker.vue](../src/components/custom-time-picker/CustomTimePicker.vue), which includes time menu integration, focus-trap, external store sync, and accessibility tweaks.
