# useMaskedDateRangeInput

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

Composable for a masked date range input that pairs naturally with Kendo Vue `MaskedTextBox` and `Calendar`. It handles typed input in the mask `MM/DD/YYYY - MM/DD/YYYY`, keyboard steppers (ArrowUp/ArrowDown), wheel steppers, caret persistence, min/max clamping, ordered range validation, span calculation, and optional popup-close coordination when the calendar sets the value.

This composable is designed to preserve the user's raw input: it does not coerce or rewrite what the user has typed in `onChange`. It only emits a parsed `SelectionRange` when the input is complete and valid.

## Prerequisites

- Vue 3 Composition API
- `@progress/kendo-vue-inputs` (for `MaskedTextBox`)
- `@progress/kendo-vue-dateinputs` (for `Calendar`, `SelectionRange`)
- Optional: `@progress/kendo-vue-popup` for calendar popup, and a focus trap (e.g., `usePopupTrap`) if desired.

## Quick Start

```vue
<script setup lang="ts">
import { ref } from "vue";
import { MaskedTextBox } from "@progress/kendo-vue-inputs";
import { SvgIcon } from "@progress/kendo-vue-common";
import { Calendar, type SelectionRange } from "@progress/kendo-vue-dateinputs";
import { Popup } from "@progress/kendo-vue-popup";
import { useMaskedDateRangeInput, usePopupTrap } from "@featherk/composables/range";

const showCal = ref(false);
const dateRange = ref<SelectionRange | null | undefined>();
const maskedTextBoxRef = ref<InstanceType<typeof MaskedTextBox> | null>(null);

// Example bounds (customize for your app)
const ONE_YEAR = 365 * 24 * 60 * 60 * 1000;
const MIN_DATE = new Date(Date.now() - ONE_YEAR);
const MAX_DATE = new Date(Date.now() + ONE_YEAR);

const openCalendar = () => { showCal.value = true; };
const closeCalendar = () => { showCal.value = false; };

const range = useMaskedDateRangeInput({
  id: "date-range-input",
  onChange: ({ value }) => { dateRange.value = value ?? undefined; },
  onShowCalendar: openCalendar,
  externalValue: dateRange,
  // Validation bounds
  min: MIN_DATE,
  max: MAX_DATE,
  // Optional: cap the span length in days (uncomment to enforce)
  // maxSpanDays: 90,
  isOpen: showCal,
  onRequestClose: closeCalendar,
  closeDelay: 280,
  debug: true,
});

// Example icon; provide your own `dateRangeIcon` or CSS mask
const dateRangeIcon = { name: "calendar" } as any; // use @featherk/icons getCustomIcon here

const rawInput = range.raw;
const clickAdornment = () => { showCal.value ? closeCalendar() : openCalendar(); };

// Focus trap for the calendar popup
usePopupTrap({
  isOpen: showCal,
  onRequestClose: closeCalendar,
  triggerEl: maskedTextBoxRef,
  popupSelector: [".k-calendar", ".k-popup"],
});
</script>

<template>
  <MaskedTextBox
    ref="maskedTextBoxRef"
    id="date-range-input"
    v-model="rawInput"
    :mask="'00/00/0000 - 00/00/0000'"
    placeholder="Start Date - End Date"
    :inputSuffix="'calendar-adornment'"
    @change="range.handleChange"
    @keydown="range.handleKeyDown"
    @keyup="range.handleKeyUp"
    @wheel="range.handleWheel"
    @click="range.handleClick"
    @blur="range.handleBlur"
  >
    <template #calendar-adornment>
      <SvgIcon
        :icon="dateRangeIcon"
        class="calendar-adornment-icon"
        @mousedown.prevent.stop="clickAdornment"
      />
    </template>
  </MaskedTextBox>

  <Popup :anchor="'date-range-input'" :show="showCal" :animate="false">
    <Calendar
      v-model="dateRange"
      :mode="'range'"
      :min="MIN_DATE"
      :max="MAX_DATE"
      @change="range.onCalendarChange"
    />
  </Popup>

  <!-- Example debug line showing validation info -->
  <div style="margin-top: 0.5rem; font-size: 0.875rem;">
    Span (days): {{ range.spanDays ?? '-' }}
    • Valid: {{ range.valid ?? '-' }}
    • Reason: {{ range.reason ?? '-' }}
  </div>
</template>

<style scoped>
.calendar-adornment-icon { cursor: pointer; }
</style>
```

## API

### `useMaskedDateRangeInput(options)`

Creates a controller for a masked date range input.

#### Options

- **`id: string`**: The DOM id of the `MaskedTextBox` input element.
- **`onChange: (p: { value: SelectionRange | null; event: any }) => void`**: Callback when the composable has a new parsed value to emit.
  - Emits `value=null` if input is incomplete, invalid, outside min/max, or out of order.
  - Does not mutate the user's raw string.
- **`onShowCalendar: (e: KeyboardEvent) => void`**: Called when the user presses Space on the input; the composable prevents default and triggers this to open the calendar.
- **`externalValue?: Ref<SelectionRange | null | undefined>`**: A reactive range source (e.g., the `v-model` bound to the calendar). When it updates, the composable mirrors it into `raw` without clobbering in-progress user typing.
- **`externalValid?: Ref<boolean | undefined>`**: Optional external validity ref to mirror computed validity state into.
- **`manageValid?: boolean`**: Defaults to `true`. When enabled, mirrors `validComputed` into `externalValid`.
- **`min?: Date` / `max?: Date`**: Bounds for clamping and validation.
- **`maxSpanDays?: number`**: Optional maximum allowed span between start and end (in days).
- **`allowReverse?: boolean`**: Whether `end` may be before `start`. Defaults to `false` (requires ordered start ≤ end).
- **`isOpen?: Ref<boolean>`**: Reactive flag for the calendar popup. If provided, the composable can coordinate closing after calendar changes.
- **`onRequestClose?: () => void`**: Called to close the popup when `externalValue` changes due to a calendar selection.
- **`closeDelay?: Ref<number> | number`**: Delay in ms before calling `onRequestClose` after a calendar-originated change.
- **`debug?: boolean`**: Enables internal debug reporting (e.g., `debugLines`).

#### Returns

- **`raw: Ref<string>`**: The masked string `MM/DD/YYYY - MM/DD/YYYY`. Use as `v-model` for `MaskedTextBox`.
- **`cursorPos: Ref<number | undefined>`**: Tracks caret position to restore after programmatic updates.
- **`debugEnabled: Ref<boolean>`** and **`debugLines: Computed<Array<{label:string; value:string}>>`**: Debug info for UI display.
- **`digitsOnly: Computed<string>`**: The raw string with non-digits removed.
- **`valid: Ref<boolean | undefined>`**: Managed validity state (mirrored into `externalValid` when enabled).
- **`validComputed: Readonly<ComputedRef<boolean | undefined>>`**: Derived validity considering mask completeness, min/max clamping, ordering, and span constraints. Not flagged until the first blur/clear; live thereafter.
- **`reason: Readonly<ComputedRef<string | undefined>>`**: Explanation for invalid states; returns `'valid'` when the range passes validation and `undefined` before the first blur/clear.
- **`validationMessage: Readonly<ComputedRef<string>>`**: User-facing message derived from `reason`; always in sync with `validComputed`.
- **`spanDays: Readonly<ComputedRef<number | undefined>>`**: Span in days between clamped start/end; `undefined` when incomplete or invalid.
- Readonly computed parts: **`month1`**, **`day1`**, **`year1`**, **`month2`**, **`day2`**, **`year2`** (strings). Useful for diagnostics.
- **`initStyling(): void`**: Re-applies the `fk-daterangepicker` class to the input's parent container. Exposed so consumers can re-run theming after Kendo manipulates the DOM; the function is idempotent.  Use this to apply the fk-daterangepicker style hook when the date range picker isn't available on initial load
- Event handlers to wire to the input:
  - **`handleChange(event)`**: Assigns `raw` from the input event and emits parsed range when complete and valid.
  - **`handleKeyDown(event)`**:
    - Space: prevents default and calls `onShowCalendar`.
    - ArrowUp/ArrowDown: interprets as steppers for the focused date part (month/day/year) and emits parsed range or `null` accordingly.
  - **`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)`**: Validates the input and updates the validity state when the input loses focus.
  - **`onCalendarChange()`**: Marks subsequent `externalValue` updates as calendar-originated; used to optionally auto-close the popup.

## Behavior Details

- **Mask and parsing**: Expects `00/00/0000 - 00/00/0000`. Parsing requires both dates to be complete (10 chars each) and valid per calendar rules.
- **Validation**:
  - No validation is reported until the input first loses focus (or is cleared), matching `useMaskedDateInput`'s required-field behavior.
  - After that first blur, `validComputed`, `reason`, and `validationMessage` all update live from the same source, so the invalid styling and the error text always appear and clear together.
  - `valid` (the managed ref mirrored into `externalValid`) is still committed on blur/clear. Bind `:valid="validComputed"` for the live-after-blur styling.
  - Each side must be a valid date (including month/day bounds like Feb 29).
  - Clamped to `min`/`max` if provided; outside results in `value=null`.
  - Ordered range required unless `allowReverse=true`.
  - `spanDays` computes the day difference (UTC midnight aligned) between clamped `start` and `end`. If `maxSpanDays` is provided and `spanDays` exceeds it, validity fails and `reason` reflects the constraint.
- **Steppers**:
  - ArrowUp/ArrowDown and mouse wheel increments/decrements the focused part (`mm`, `dd`, `yyyy`) with wrapping where appropriate and day overflow correction (e.g., moving from Jan 31 to Feb adjusts day within range).
  - If both sides are missing, steppers initialize the range to today on both ends and restore caret.
- **Caret persistence**: After programmatic updates, caret is restored to the prior position to preserve typing flow.
- **Space key**: The input consumes Space to open the calendar via `onShowCalendar` without inserting a space into `raw`.
- **External sync**: When `externalValue` is set (typically by the calendar), `raw` is updated to reflect the selected range. If a popup is open and the change is marked calendar-originated, the composable calls `onRequestClose` after `closeDelay`.
- **Raw preservation**: When external range becomes invalid/empty, the composable does not overwrite `raw`, preserving in-progress text.
- **Styling hook**: On mount, adds `fk-daterangepicker` class to the input's parent container for theming.
- **Styling hook**: On mount, adds `fk-daterangepicker` class to the input's parent container for theming. 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 Pattern (Kendo + Popup)

Wire the returned handlers to the `MaskedTextBox`. Use `externalValue` for syncing with the `Calendar` `v-model`. Optionally coordinate popup closing with `isOpen`/`onRequestClose`.

Key event bindings:

- `@change="range.handleChange"`
- `@keydown="range.handleKeyDown"`
- `@keyup="range.handleKeyUp"`
- `@wheel="range.handleWheel"`
- `@click="range.handleClick"`
- `@blur="range.handleBlur"`

Calendar coordination:

- Bind `v-model` on `Calendar` to the same `SelectionRange` ref passed as `externalValue`.
- Call `@change="range.onCalendarChange"` so the composable knows the update came from the calendar.
- Provide `isOpen`, `onRequestClose`, and `closeDelay` for smooth auto-closing after calendar selection.

<!-- Example moved above into Quick Start to reduce duplication. Full reference implementation: -->
The project’s full reference implementation is in [src/components/custom-date-range-picker/CustomDateRangePicker.vue](../src/components/custom-date-range-picker/CustomDateRangePicker.vue), which includes advanced styling, focus-trap, and accessibility tweaks.

## Accessibility Notes

- Consider applying a focus trap and escape/OutsideClick handling to the calendar popup (e.g., using a `usePopupTrap` composable).
- For multi-view calendars, you may wish to ensure only one `.k-calendar-table` is tabbable (`tabIndex=0`) and set others to `-1` to improve keyboard navigation.

## Limitations & Assumptions

- Date format is fixed to `MM/DD/YYYY`; internationalization of the mask and parser is not yet included.
- Built to pair with Kendo Vue inputs/dateinputs; other inputs may need minor adjustments.

## Tips

- Keep `min`/`max` aligned with your calendar configuration for consistent clamping.
- 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 ranges.

## Types

```ts
export type RangeChangePayload = { value: SelectionRange | null; event: any };
```

## Example: CustomDateRangePicker Wiring

The project’s `CustomDateRangePicker.vue` demonstrates full integration with calendar adornment, popup, focus trap, and Pinia store updates. Mirror that pattern and wire the composable’s handlers to the `MaskedTextBox`, pass your range ref as `externalValue`, and coordinate popup open/close via `onShowCalendar`, `isOpen`, `onRequestClose`, and `closeDelay`.
