@featherk/composables 0.4.12 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -122
- package/dist/date/index.d.ts +1 -0
- package/dist/date/useMaskedDateInput.d.ts +33 -0
- package/dist/featherk-composables.es.js +1140 -228
- package/dist/featherk-composables.umd.js +1 -1
- package/dist/grid/index.d.ts +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/range/index.d.ts +1 -0
- package/dist/range/useMaskedDateRangeInput.d.ts +48 -0
- package/dist/time/index.d.ts +1 -0
- package/dist/time/useMaskedTimeInput.d.ts +42 -0
- package/dist/trap/index.d.ts +1 -0
- package/dist/trap/usePopupTrap.d.ts +20 -0
- package/docs/date/useMaskedDateInput.md +173 -0
- package/docs/grid/useGridA11y.md +243 -0
- package/docs/range/useMaskedDateRangeInput.md +213 -0
- package/docs/time/useMaskedTimeInput.md +197 -0
- package/docs/trap/usePopupTrap.md +138 -0
- package/package.json +53 -3
- package/dist/useGridComposableEx.d.ts +0 -11
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# useMaskedDateRangeInput
|
|
2
|
+
|
|
3
|
+
[← Back to Composables README](../../README.md)
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
- Vue 3 Composition API
|
|
12
|
+
- `@progress/kendo-vue-inputs` (for `MaskedTextBox`)
|
|
13
|
+
- `@progress/kendo-vue-dateinputs` (for `Calendar`, `SelectionRange`)
|
|
14
|
+
- Optional: `@progress/kendo-vue-popup` for calendar popup, and a focus trap (e.g., `usePopupTrap`) if desired.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```vue
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { ref } from "vue";
|
|
21
|
+
import { MaskedTextBox } from "@progress/kendo-vue-inputs";
|
|
22
|
+
import { SvgIcon } from "@progress/kendo-vue-common";
|
|
23
|
+
import { Calendar, type SelectionRange } from "@progress/kendo-vue-dateinputs";
|
|
24
|
+
import { Popup } from "@progress/kendo-vue-popup";
|
|
25
|
+
import { useMaskedDateRangeInput } from "@featherk/composables/range";
|
|
26
|
+
|
|
27
|
+
const showCal = ref(false);
|
|
28
|
+
const dateRange = ref<SelectionRange | null | undefined>();
|
|
29
|
+
|
|
30
|
+
// Example bounds (customize for your app)
|
|
31
|
+
const ONE_YEAR = 365 * 24 * 60 * 60 * 1000;
|
|
32
|
+
const MIN_DATE = new Date(Date.now() - ONE_YEAR);
|
|
33
|
+
const MAX_DATE = new Date(Date.now() + ONE_YEAR);
|
|
34
|
+
|
|
35
|
+
const openCalendar = () => { showCal.value = true; };
|
|
36
|
+
const closeCalendar = () => { showCal.value = false; };
|
|
37
|
+
|
|
38
|
+
const range = useMaskedDateRangeInput({
|
|
39
|
+
id: "date-range-input",
|
|
40
|
+
onChange: ({ value }) => { dateRange.value = value ?? undefined; },
|
|
41
|
+
onShowCalendar: openCalendar,
|
|
42
|
+
externalValue: dateRange,
|
|
43
|
+
// Validation bounds
|
|
44
|
+
min: MIN_DATE,
|
|
45
|
+
max: MAX_DATE,
|
|
46
|
+
// Optional: cap the span length in days (uncomment to enforce)
|
|
47
|
+
// maxSpanDays: 90,
|
|
48
|
+
isOpen: showCal,
|
|
49
|
+
onRequestClose: closeCalendar,
|
|
50
|
+
closeDelay: 280,
|
|
51
|
+
debug: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Example icon; provide your own `dateRangeIcon` or CSS mask
|
|
55
|
+
const dateRangeIcon = { name: "calendar" } as any; // use @featherk/icons getCustomIcon here
|
|
56
|
+
|
|
57
|
+
const rawInput = range.raw;
|
|
58
|
+
const clickAdornment = () => { showCal.value ? closeCalendar() : openCalendar(); };
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<MaskedTextBox
|
|
63
|
+
id="date-range-input"
|
|
64
|
+
v-model="rawInput"
|
|
65
|
+
:mask="'00/00/0000 - 00/00/0000'"
|
|
66
|
+
placeholder="Start Date - End Date"
|
|
67
|
+
:inputSuffix="'calendar-adornment'"
|
|
68
|
+
@change="range.handleChange"
|
|
69
|
+
@keydown="range.handleKeyDown"
|
|
70
|
+
@keyup="range.handleKeyUp"
|
|
71
|
+
@wheel.prevent="range.handleWheel"
|
|
72
|
+
@click="range.handleClick"
|
|
73
|
+
>
|
|
74
|
+
<template #calendar-adornment>
|
|
75
|
+
<SvgIcon
|
|
76
|
+
:icon="dateRangeIcon"
|
|
77
|
+
class="calendar-adornment-icon"
|
|
78
|
+
@mousedown.prevent.stop="clickAdornment"
|
|
79
|
+
/>
|
|
80
|
+
</template>
|
|
81
|
+
</MaskedTextBox>
|
|
82
|
+
|
|
83
|
+
<Popup :anchor="'date-range-input'" :show="showCal" :animate="false">
|
|
84
|
+
<Calendar
|
|
85
|
+
v-model="dateRange"
|
|
86
|
+
:mode="'range'"
|
|
87
|
+
:min="MIN_DATE"
|
|
88
|
+
:max="MAX_DATE"
|
|
89
|
+
@change="range.onCalendarChange"
|
|
90
|
+
/>
|
|
91
|
+
</Popup>
|
|
92
|
+
|
|
93
|
+
<!-- Example debug line showing validation info -->
|
|
94
|
+
<div style="margin-top: 0.5rem; font-size: 0.875rem;">
|
|
95
|
+
Span (days): {{ range.spanDays ?? '-' }}
|
|
96
|
+
• Valid: {{ range.valid ?? '-' }}
|
|
97
|
+
• Reason: {{ range.reason ?? '-' }}
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|
|
100
|
+
|
|
101
|
+
<style scoped>
|
|
102
|
+
.calendar-adornment-icon { cursor: pointer; }
|
|
103
|
+
</style>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## API
|
|
107
|
+
|
|
108
|
+
### `useMaskedDateRangeInput(options)`
|
|
109
|
+
|
|
110
|
+
Creates a controller for a masked date range input.
|
|
111
|
+
|
|
112
|
+
#### Options
|
|
113
|
+
|
|
114
|
+
- **`id: string`**: The DOM id of the `MaskedTextBox` input element.
|
|
115
|
+
- **`onChange: (p: { value: SelectionRange | null; event: any }) => void`**: Callback when the composable has a new parsed value to emit.
|
|
116
|
+
- Emits `value=null` if input is incomplete, invalid, outside min/max, or out of order.
|
|
117
|
+
- Does not mutate the user's raw string.
|
|
118
|
+
- **`onShowCalendar: (e: KeyboardEvent) => void`**: Called when the user presses Space on the input; the composable prevents default and triggers this to open the calendar.
|
|
119
|
+
- **`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.
|
|
120
|
+
- **`externalValid?: Ref<boolean | undefined>`**: Optional external validity ref to mirror computed validity state into.
|
|
121
|
+
- **`manageValid?: boolean`**: Defaults to `true`. When enabled, mirrors `validComputed` into `externalValid`.
|
|
122
|
+
- **`min?: Date` / `max?: Date`**: Bounds for clamping and validation.
|
|
123
|
+
- **`maxSpanDays?: number`**: Optional maximum allowed span between start and end (in days).
|
|
124
|
+
- **`allowReverse?: boolean`**: Whether `end` may be before `start`. Defaults to `false` (requires ordered start ≤ end).
|
|
125
|
+
- **`isOpen?: Ref<boolean>`**: Reactive flag for the calendar popup. If provided, the composable can coordinate closing after calendar changes.
|
|
126
|
+
- **`onRequestClose?: () => void`**: Called to close the popup when `externalValue` changes due to a calendar selection.
|
|
127
|
+
- **`closeDelay?: Ref<number> | number`**: Delay in ms before calling `onRequestClose` after a calendar-originated change.
|
|
128
|
+
- **`debug?: boolean`**: Enables internal debug reporting (e.g., `debugLines`).
|
|
129
|
+
|
|
130
|
+
#### Returns
|
|
131
|
+
|
|
132
|
+
- **`raw: Ref<string>`**: The masked string `MM/DD/YYYY - MM/DD/YYYY`. Use as `v-model` for `MaskedTextBox`.
|
|
133
|
+
- **`cursorPos: Ref<number | undefined>`**: Tracks caret position to restore after programmatic updates.
|
|
134
|
+
- **`debugEnabled: Ref<boolean>`** and **`debugLines: Computed<Array<{label:string; value:string}>>`**: Debug info for UI display.
|
|
135
|
+
- **`digitsOnly: Computed<string>`**: The raw string with non-digits removed.
|
|
136
|
+
- **`valid: Ref<boolean | undefined>`**: Managed validity state (mirrored into `externalValid` when enabled).
|
|
137
|
+
- **`validComputed: Readonly<ComputedRef<boolean | undefined>>`**: Derived validity considering mask completeness, min/max clamping, ordering, and span constraints.
|
|
138
|
+
- **`reason: Readonly<ComputedRef<string | undefined>>`**: Explanation for invalid states; returns `'valid'` when the range passes validation.
|
|
139
|
+
- **`spanDays: Readonly<ComputedRef<number | undefined>>`**: Span in days between clamped start/end; `undefined` when incomplete or invalid.
|
|
140
|
+
- Readonly computed parts: **`month1`**, **`day1`**, **`year1`**, **`month2`**, **`day2`**, **`year2`** (strings). Useful for diagnostics.
|
|
141
|
+
- Event handlers to wire to the input:
|
|
142
|
+
- **`handleChange(event)`**: Assigns `raw` from the input event and emits parsed range when complete and valid.
|
|
143
|
+
- **`handleKeyDown(event)`**:
|
|
144
|
+
- Space: prevents default and calls `onShowCalendar`.
|
|
145
|
+
- ArrowUp/ArrowDown: interprets as steppers for the focused date part (month/day/year) and emits parsed range or `null` accordingly.
|
|
146
|
+
- **`handleWheel(event)`**: Prevents page scroll; interprets wheel as steppers (up/down) for the focused part.
|
|
147
|
+
- **`handleKeyUp(event)`**: Maintains caret position after cursor keys/steppers.
|
|
148
|
+
- **`handleClick(event)`**: Captures caret position on click for restoring.
|
|
149
|
+
- **`onCalendarChange()`**: Marks subsequent `externalValue` updates as calendar-originated; used to optionally auto-close the popup.
|
|
150
|
+
|
|
151
|
+
## Behavior Details
|
|
152
|
+
|
|
153
|
+
- **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.
|
|
154
|
+
- **Validation**:
|
|
155
|
+
- Each side must be a valid date (including month/day bounds like Feb 29).
|
|
156
|
+
- Clamped to `min`/`max` if provided; outside results in `value=null`.
|
|
157
|
+
- Ordered range required unless `allowReverse=true`.
|
|
158
|
+
- `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.
|
|
159
|
+
- **Steppers**:
|
|
160
|
+
- 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).
|
|
161
|
+
- If both sides are missing, steppers initialize the range to today on both ends and restore caret.
|
|
162
|
+
- **Caret persistence**: After programmatic updates, caret is restored to the prior position to preserve typing flow.
|
|
163
|
+
- **Space key**: The input consumes Space to open the calendar via `onShowCalendar` without inserting a space into `raw`.
|
|
164
|
+
- **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`.
|
|
165
|
+
- **Raw preservation**: When external range becomes invalid/empty, the composable does not overwrite `raw`, preserving in-progress text.
|
|
166
|
+
- **Styling hook**: On mount, adds `fk-daterangepicker` class to the input's parent container for theming.
|
|
167
|
+
|
|
168
|
+
## Integration Pattern (Kendo + Popup)
|
|
169
|
+
|
|
170
|
+
Wire the returned handlers to the `MaskedTextBox`. Use `externalValue` for syncing with the `Calendar` `v-model`. Optionally coordinate popup closing with `isOpen`/`onRequestClose`.
|
|
171
|
+
|
|
172
|
+
Key event bindings:
|
|
173
|
+
|
|
174
|
+
- `@change="range.handleChange"`
|
|
175
|
+
- `@keydown="range.handleKeyDown"`
|
|
176
|
+
- `@keyup="range.handleKeyUp"`
|
|
177
|
+
- `@wheel.prevent="range.handleWheel"`
|
|
178
|
+
- `@click="range.handleClick"`
|
|
179
|
+
|
|
180
|
+
Calendar coordination:
|
|
181
|
+
|
|
182
|
+
- Bind `v-model` on `Calendar` to the same `SelectionRange` ref passed as `externalValue`.
|
|
183
|
+
- Call `@change="range.onCalendarChange"` so the composable knows the update came from the calendar.
|
|
184
|
+
- Provide `isOpen`, `onRequestClose`, and `closeDelay` for smooth auto-closing after calendar selection.
|
|
185
|
+
|
|
186
|
+
<!-- Example moved above into Quick Start to reduce duplication. Full reference implementation: -->
|
|
187
|
+
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.
|
|
188
|
+
|
|
189
|
+
## Accessibility Notes
|
|
190
|
+
|
|
191
|
+
- Consider applying a focus trap and escape/OutsideClick handling to the calendar popup (e.g., using a `usePopupTrap` composable).
|
|
192
|
+
- 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.
|
|
193
|
+
|
|
194
|
+
## Limitations & Assumptions
|
|
195
|
+
|
|
196
|
+
- Date format is fixed to `MM/DD/YYYY`; internationalization of the mask and parser is not yet included.
|
|
197
|
+
- Built to pair with Kendo Vue inputs/dateinputs; other inputs may need minor adjustments.
|
|
198
|
+
|
|
199
|
+
## Tips
|
|
200
|
+
|
|
201
|
+
- Keep `min`/`max` aligned with your calendar configuration for consistent clamping.
|
|
202
|
+
- Use `debug: true` and display `debugLines` during integration to verify caret, parsed values, and masks.
|
|
203
|
+
- Avoid mutating `raw` externally except through `v-model`; let the composable manage its value and emit parsed ranges.
|
|
204
|
+
|
|
205
|
+
## Types
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
export type RangeChangePayload = { value: SelectionRange | null; event: any };
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Example: CustomDateRangePicker Wiring
|
|
212
|
+
|
|
213
|
+
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`.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# useMaskedTimeInput
|
|
2
|
+
|
|
3
|
+
[← Back to Composables README](../../README.md)
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
- Vue 3 Composition API
|
|
12
|
+
- `@progress/kendo-vue-inputs` (for `MaskedTextBox`)
|
|
13
|
+
- Optional: `@progress/kendo-vue-dateinputs` (for `TimePicker`) and `@progress/kendo-vue-popup` for a popup
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```vue
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
import { ref } from "vue";
|
|
20
|
+
import { TimePicker } from "@progress/kendo-vue-dateinputs";
|
|
21
|
+
import { MaskedTextBox } from "@progress/kendo-vue-inputs";
|
|
22
|
+
import { Error } from "@progress/kendo-vue-labels";
|
|
23
|
+
import { useMaskedTimeInput } from "@featherk/composables/time";
|
|
24
|
+
|
|
25
|
+
// Shared model for TimePicker and composable
|
|
26
|
+
const selectedTime = ref<Date | null | undefined>();
|
|
27
|
+
const showPicker = ref(false);
|
|
28
|
+
|
|
29
|
+
// Example bounds (customize for your app)
|
|
30
|
+
const minTime = ref(new Date(new Date().setHours(8, 0, 0, 0))); // 8:00 AM
|
|
31
|
+
const maxTime = ref(new Date(new Date().setHours(17, 0, 0, 0))); // 5:00 PM
|
|
32
|
+
|
|
33
|
+
const masked = useMaskedTimeInput({
|
|
34
|
+
id: "startTime",
|
|
35
|
+
onChange: ({ value }) => { selectedTime.value = value ?? undefined; },
|
|
36
|
+
onShowPicker: () => (showPicker.value = true), // Space opens the time menu
|
|
37
|
+
externalValue: selectedTime, // Mirror TimePicker selection into the mask
|
|
38
|
+
minuteStep: 5,
|
|
39
|
+
minTime,
|
|
40
|
+
maxTime,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Destructure for simpler template bindings
|
|
44
|
+
const {
|
|
45
|
+
raw,
|
|
46
|
+
rules,
|
|
47
|
+
placeholder,
|
|
48
|
+
isValid,
|
|
49
|
+
validationMessage,
|
|
50
|
+
debugEnabled,
|
|
51
|
+
debugLines,
|
|
52
|
+
} = masked;
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<TimePicker
|
|
57
|
+
v-model="selectedTime"
|
|
58
|
+
dateInput="masked"
|
|
59
|
+
:format="'hh:mm a'"
|
|
60
|
+
:show="showPicker"
|
|
61
|
+
@open="showPicker = true"
|
|
62
|
+
@close="showPicker = false"
|
|
63
|
+
:style="{ width: 'fit-content' }"
|
|
64
|
+
>
|
|
65
|
+
<template #masked="{ props }">
|
|
66
|
+
<MaskedTextBox
|
|
67
|
+
id="startTime"
|
|
68
|
+
:mask="'Hh:Mm Aa'"
|
|
69
|
+
:rules="rules"
|
|
70
|
+
:value="raw"
|
|
71
|
+
:placeholder="placeholder"
|
|
72
|
+
:showClearButton="false"
|
|
73
|
+
@change="masked.handleChange"
|
|
74
|
+
@keydown="masked.handleKeyDown"
|
|
75
|
+
@keyup="masked.handleKeyUp"
|
|
76
|
+
@click="masked.handleClick"
|
|
77
|
+
@wheel="masked.handleWheel"
|
|
78
|
+
/>
|
|
79
|
+
</template>
|
|
80
|
+
</TimePicker>
|
|
81
|
+
<Error for="startTime">{{ validationMessage }}</Error>
|
|
82
|
+
|
|
83
|
+
<!-- Optional: show internal debug lines during integration -->
|
|
84
|
+
<div v-if="debugEnabled" style="margin-top: 8px; color: #475467;">
|
|
85
|
+
<div v-for="(row, idx) in debugLines" :key="idx">
|
|
86
|
+
<strong>{{ row[0] }}:</strong> {{ row[1] }}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Full Example
|
|
93
|
+
|
|
94
|
+
For a complete integration including a focus trap, store sync, and UI tweaks, see the demo view referenced in this repository.
|
|
95
|
+
|
|
96
|
+
## API
|
|
97
|
+
|
|
98
|
+
### `useMaskedTimeInput(options)`
|
|
99
|
+
|
|
100
|
+
Creates a controller for a masked single-time input.
|
|
101
|
+
|
|
102
|
+
#### Options
|
|
103
|
+
|
|
104
|
+
- `id: string`: DOM id of the `MaskedTextBox` input element.
|
|
105
|
+
- `onChange: (p: ChangePayload) => void`: Called with `{ value, event }` when the composable has a new parsed value to emit.
|
|
106
|
+
- Emits `value = null` if input is empty, incomplete, invalid, or outside min/max.
|
|
107
|
+
- Does not mutate the user's raw string beyond minor conveniences.
|
|
108
|
+
- `onShowPicker: (e: KeyboardEvent) => void`: Called when Space is pressed on the input; use this to open a time menu/popup.
|
|
109
|
+
- `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.
|
|
110
|
+
- `minuteStep?: 1 | 5 | 10 | 15 | 20 | 30`: Stepping granularity when ArrowUp/ArrowDown are used in the minutes segment.
|
|
111
|
+
- `minuteStepRef?: Ref<1 | 5 | 10 | 15 | 20 | 30 | undefined>`: Reactive alternative to update step granularity at runtime.
|
|
112
|
+
- `timeFormat?: string`: Placeholder/format hint (default: `hh:mm AM`).
|
|
113
|
+
- `minTime?: Ref<Date | null | undefined>` / `maxTime?: Ref<Date | null | undefined>`: Bounds for validation.
|
|
114
|
+
- `debug?: boolean`: Enables internal debug reporting (e.g., `debugLines`).
|
|
115
|
+
|
|
116
|
+
#### Returns
|
|
117
|
+
|
|
118
|
+
- `raw: Ref<string>`: The masked string `hh:mm AM`. Use as `v-model` for `MaskedTextBox`.
|
|
119
|
+
- `rules: Record<string, RegExp>`: Relaxed mask rules for Kendo `MaskedTextBox` to allow free typing, followed by composable validation.
|
|
120
|
+
- `cursorPos: Ref<number | undefined>`: Caret position for restoring after programmatic updates.
|
|
121
|
+
- `placeholder: Ref<string>`: Format placeholder (defaults to `hh:mm AM`).
|
|
122
|
+
- `digitsOnly: Computed<string>`: Raw string stripped of non-digits.
|
|
123
|
+
- `hour`, `minute`, `period`: Readonly computed parts (numeric hour/minute, `AM`/`PM`).
|
|
124
|
+
- `isComplete: Computed<boolean>`: `true` when the raw string matches `^(\d{2}):(\d{2})\s([AP]M)$`.
|
|
125
|
+
- `isValid: Computed<boolean>`: Validity considering completeness, parsability, and min/max bounds.
|
|
126
|
+
- `validationMessage: Computed<string>`: Human-readable message for invalid states (format or range messages).
|
|
127
|
+
- Event handlers to wire to the input:
|
|
128
|
+
- `handleChange(event)` — Assigns `raw`, tracks caret, fills `AM/PM` when appropriate, emits parsed time when valid.
|
|
129
|
+
- `handleKeyDown(event)` — Space opens the picker via `onShowPicker`; ArrowUp/ArrowDown act as steppers.
|
|
130
|
+
- `handleWheel(event)` — Prevents page scroll; interprets wheel as steppers (up/down) for the focused part.
|
|
131
|
+
- `handleKeyUp(event)` — Maintains caret position after cursor keys/steppers.
|
|
132
|
+
- `handleClick(event)` — Captures caret position on click for restoring.
|
|
133
|
+
|
|
134
|
+
- Debug:
|
|
135
|
+
- `debugEnabled: Ref<boolean>`
|
|
136
|
+
- `debugLines: Computed<Array<{ label: string; value: string }>>`
|
|
137
|
+
|
|
138
|
+
## Behavior Details
|
|
139
|
+
|
|
140
|
+
- **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.
|
|
141
|
+
- **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`.
|
|
142
|
+
- **Emission policy**: Emits `Date` only when the raw is complete, valid, and within `minTime`/`maxTime`. Otherwise emits `null`. Empty input emits `null`.
|
|
143
|
+
- **Min/Max validation**: Bounds are evaluated by minutes since midnight. Times outside bounds are invalid (`isValid=false`) and produce a range `validationMessage`.
|
|
144
|
+
- **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.
|
|
145
|
+
- **Caret persistence**: After programmatic updates (including steppers), the caret is restored to the prior position to preserve typing flow.
|
|
146
|
+
- **Space key**: The input consumes Space and calls `onShowPicker` to open a time menu without inserting a space into `raw`.
|
|
147
|
+
- **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.
|
|
148
|
+
- **Styling hook**: On mount, adds a theming class (e.g., `fk-timepicker`) to the closest Kendo time picker container for styling.
|
|
149
|
+
|
|
150
|
+
## Integration Patterns
|
|
151
|
+
|
|
152
|
+
- **MaskedTextBox only**: Use the Quick Start to accept typed times with validation — no time menu required.
|
|
153
|
+
- **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.
|
|
154
|
+
- **Minute step control**: Provide `minuteStep` or a reactive `minuteStepRef` to control ArrowUp/ArrowDown behavior in the minutes segment at runtime.
|
|
155
|
+
|
|
156
|
+
### Important: Kendo TimePicker slot name
|
|
157
|
+
|
|
158
|
+
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`.
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
|
|
162
|
+
```vue
|
|
163
|
+
<TimePicker v-model="timeValue" dateInput="masked">
|
|
164
|
+
<template #masked="{ props }">
|
|
165
|
+
<!-- your custom MaskedTextBox here -->
|
|
166
|
+
</template>
|
|
167
|
+
</TimePicker>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
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.
|
|
171
|
+
|
|
172
|
+
## Accessibility Notes
|
|
173
|
+
|
|
174
|
+
- Ensure labels (e.g., Kendo `Label` with `for`) correctly reference the input `id`.
|
|
175
|
+
- If using a popup time menu, consider adding a focus trap and Escape/OutsideClick handling (see `usePopupTrap`).
|
|
176
|
+
- Keep tab order logical; Space opens the menu, Escape should close it.
|
|
177
|
+
|
|
178
|
+
## Limitations & Assumptions
|
|
179
|
+
|
|
180
|
+
- Time format is fixed to `hh:mm AM`; internationalization and 24-hour masks are not included yet.
|
|
181
|
+
- Built to pair with Kendo Vue inputs/dateinputs; other inputs may need minor adjustments.
|
|
182
|
+
|
|
183
|
+
## Tips
|
|
184
|
+
|
|
185
|
+
- Keep `minTime`/`maxTime` aligned with your `TimePicker` configuration for consistent validation.
|
|
186
|
+
- Use `debug: true` and display `debugLines` during integration to verify caret, parsed values, and masks.
|
|
187
|
+
- Avoid mutating `raw` externally except through `v-model`; let the composable manage its value and emit parsed times.
|
|
188
|
+
|
|
189
|
+
## Types
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
export type ChangePayload = { value: Date | null; event: any };
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Reference Implementation
|
|
196
|
+
|
|
197
|
+
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.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# usePopupTrap
|
|
2
|
+
|
|
3
|
+
[← Back to Composables README](../../README.md)
|
|
4
|
+
|
|
5
|
+
Composable for managing focus trapping and close behavior for popup UIs (e.g., Kendo `Popup`, `DatePicker`, `TimePicker` dropdowns). It provides focus trapping via `@vueuse/integrations/useFocusTrap`, Escape/OutsideClick to close, automatic popup element discovery, and optional return-to-trigger focus.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Vue 3 Composition API
|
|
10
|
+
- `@vueuse/integrations` (`useFocusTrap`)
|
|
11
|
+
- `@vueuse/core` (`onClickOutside`)
|
|
12
|
+
- Optional: Kendo Vue components (`Popup`, `DatePicker`, `TimePicker`) or any popup-like UI
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { ref, shallowRef } from "vue";
|
|
19
|
+
import { DatePicker } from "@progress/kendo-vue-dateinputs";
|
|
20
|
+
import { usePopupTrap } from "@featherk/composables/trap";
|
|
21
|
+
|
|
22
|
+
const open = ref(false);
|
|
23
|
+
const datePickerRef = shallowRef<HTMLElement | null>(null);
|
|
24
|
+
|
|
25
|
+
usePopupTrap({
|
|
26
|
+
isOpen: open,
|
|
27
|
+
onRequestClose: () => (open.value = false),
|
|
28
|
+
triggerEl: datePickerRef,
|
|
29
|
+
// Use defaults: finds .k-popup near the trigger within .k-animation-container
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div ref="datePickerRef">
|
|
35
|
+
<DatePicker v-model="/* your value */" :show="open" @open="open=true" @close="open=false" />
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### TimePicker example (custom selectors)
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
<script setup lang="ts">
|
|
44
|
+
import { ref, useTemplateRef } from "vue";
|
|
45
|
+
import { TimePicker } from "@progress/kendo-vue-dateinputs";
|
|
46
|
+
import { usePopupTrap } from "@featherk/composables/trap";
|
|
47
|
+
|
|
48
|
+
const showPicker = ref(false);
|
|
49
|
+
const timePickerRef = useTemplateRef("timePickerRef");
|
|
50
|
+
|
|
51
|
+
usePopupTrap({
|
|
52
|
+
isOpen: showPicker,
|
|
53
|
+
onRequestClose: () => (showPicker.value = false),
|
|
54
|
+
triggerEl: timePickerRef,
|
|
55
|
+
popupSelector: [".k-timeselector", ".k-popup"],
|
|
56
|
+
returnFocusToTrigger: false, // parent will refocus the custom input
|
|
57
|
+
});
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<div ref="timePickerRef">
|
|
62
|
+
<TimePicker :show="showPicker" @open="showPicker=true" @close="showPicker=false" />
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
### `usePopupTrap(options)`
|
|
70
|
+
|
|
71
|
+
Sets up focus trap and close behaviors for a discovered popup element.
|
|
72
|
+
|
|
73
|
+
#### Options
|
|
74
|
+
|
|
75
|
+
- `isOpen: Ref<boolean>`: Reactive flag indicating whether the popup is open.
|
|
76
|
+
- `onRequestClose?: (reason: CloseReason, ev?: Event) => void`: Callback to request closing the popup. Reasons: `'escape' | 'outside'`.
|
|
77
|
+
- `popupSelector?: string | string[]`: CSS selector(s) used to locate the popup element. Defaults target common Kendo popup containers.
|
|
78
|
+
- `triggerEl?: Ref<HTMLElement | null>`: The trigger/root element used as a scope to discover the popup.
|
|
79
|
+
- `resolvePopupEl?: () => HTMLElement | null`: Provide your own resolver when direct selection is needed.
|
|
80
|
+
- `initialFocus?: InitialFocus`: The initial focus target inside the popup: a CSS selector string or function `(root) => HTMLElement | null`.
|
|
81
|
+
- `focusTrapOptions?: Parameters<typeof useFocusTrap>[1]`: Options forwarded to `useFocusTrap`.
|
|
82
|
+
- `returnFocusToTrigger?: boolean`: When closing, return focus to `triggerEl`. Defaults to `true`.
|
|
83
|
+
|
|
84
|
+
#### Returns
|
|
85
|
+
|
|
86
|
+
- `popupRef: Ref<HTMLElement | null>`: The active popup element.
|
|
87
|
+
- `activate(): void`: Manually activate the focus trap.
|
|
88
|
+
- `deactivate(): void`: Manually deactivate the focus trap.
|
|
89
|
+
- `setPopupEl(el: HTMLElement | null): void`: Override the discovered popup element.
|
|
90
|
+
|
|
91
|
+
#### Types
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
export type CloseReason = "escape" | "outside";
|
|
95
|
+
export type InitialFocus = string | ((root: HTMLElement) => HTMLElement | null | undefined);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Behavior Details
|
|
99
|
+
|
|
100
|
+
- **Discovery**: Attempts to find the popup near `triggerEl` within its closest `.k-animation-container` or `document.body`. Default selectors:
|
|
101
|
+
- `.k-animation-container .k-popup`
|
|
102
|
+
- `.k-popup`
|
|
103
|
+
- `.k-timepicker-popup`
|
|
104
|
+
- `.k-menu-popup`
|
|
105
|
+
- **Focus trap**: Uses `useFocusTrap(popupRef)` with a fallback/initial focus inside the popup. `escapeDeactivates` and `clickOutsideDeactivates` are disabled; closing is managed explicitly.
|
|
106
|
+
- **Escape to close**: Adds a `keydown` listener on the popup when opened; pressing Escape calls `onRequestClose('escape', event)` and stops propagation.
|
|
107
|
+
- **Outside click to close**: Uses `onClickOutside(popupRef, ...)` to call `onRequestClose('outside', event)` when open.
|
|
108
|
+
- **Open/close lifecycle**: When `isOpen` becomes true, discovers the popup, sets `popupRef`, and activates the focus trap. When it becomes false, deactivates, removes listeners, clears `popupRef`, and optionally returns focus to `triggerEl`.
|
|
109
|
+
|
|
110
|
+
## Integration Patterns
|
|
111
|
+
|
|
112
|
+
- **Kendo DatePicker/Popup**: Use defaults; pass `triggerEl` pointing to the DatePicker wrapper and `isOpen` from the component’s open state.
|
|
113
|
+
- **Kendo TimePicker**: Provide `popupSelector` like `[".k-timeselector", ".k-popup"]` to target the menu container reliably.
|
|
114
|
+
- Note: Kendo Vue `TimePicker` uses the `dateInput` slot/prop to replace its internal input (same as `DatePicker`). Use `dateInput="masked"` and define the `#masked` slot; `timeInput` will not work.
|
|
115
|
+
- **Manual resolution**: When a popup is outside the default scope, set `resolvePopupEl` to return the element directly.
|
|
116
|
+
- **Initial focus**: Use `initialFocus` to direct focus to a primary interactive element inside the popup (e.g., first calendar cell or button).
|
|
117
|
+
|
|
118
|
+
## Accessibility Notes
|
|
119
|
+
|
|
120
|
+
- Trap focus while the popup is open and ensure Escape closes it.
|
|
121
|
+
- Return focus to the trigger after closing (or handle focus yourself by setting `returnFocusToTrigger: false`).
|
|
122
|
+
- Provide an accessible label for the trigger and ensure tab order is logical.
|
|
123
|
+
|
|
124
|
+
## Limitations & Assumptions
|
|
125
|
+
|
|
126
|
+
- Discovery relies on stable CSS selectors; customize `popupSelector` or use `resolvePopupEl` if your UI differs.
|
|
127
|
+
- Focus guard behavior is tuned for Kendo popups; other libraries may require selector adjustments.
|
|
128
|
+
|
|
129
|
+
## Tips
|
|
130
|
+
|
|
131
|
+
- Keep `onRequestClose` idempotent; it can be called by Escape and outside click.
|
|
132
|
+
- When combining with masked inputs, consider focusing the custom input after closing (disable `returnFocusToTrigger` and handle focus yourself).
|
|
133
|
+
- If multiple popups can be open, ensure selectors narrow to the correct one or provide `resolvePopupEl`.
|
|
134
|
+
|
|
135
|
+
## Reference Implementations
|
|
136
|
+
|
|
137
|
+
- Date example: [src/components/custom-date-picker/CustomDatePicker.vue](../src/components/custom-date-picker/CustomDatePicker.vue)
|
|
138
|
+
- Time example: [src/components/custom-time-picker/CustomTimePicker.vue](../src/components/custom-time-picker/CustomTimePicker.vue)
|
package/package.json
CHANGED
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@featherk/composables",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"main": "dist/featherk-composables.umd.js",
|
|
5
5
|
"module": "dist/featherk-composables.es.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/featherk-composables.es.js",
|
|
11
|
+
"require": "./dist/featherk-composables.umd.js"
|
|
12
|
+
},
|
|
13
|
+
"./grid": {
|
|
14
|
+
"types": "./dist/grid/index.d.ts",
|
|
15
|
+
"import": "./dist/featherk-composables.es.js",
|
|
16
|
+
"require": "./dist/featherk-composables.umd.js"
|
|
17
|
+
},
|
|
18
|
+
"./date": {
|
|
19
|
+
"types": "./dist/date/index.d.ts",
|
|
20
|
+
"import": "./dist/featherk-composables.es.js",
|
|
21
|
+
"require": "./dist/featherk-composables.umd.js"
|
|
22
|
+
},
|
|
23
|
+
"./range": {
|
|
24
|
+
"types": "./dist/range/index.d.ts",
|
|
25
|
+
"import": "./dist/featherk-composables.es.js",
|
|
26
|
+
"require": "./dist/featherk-composables.umd.js"
|
|
27
|
+
},
|
|
28
|
+
"./time": {
|
|
29
|
+
"types": "./dist/time/index.d.ts",
|
|
30
|
+
"import": "./dist/featherk-composables.es.js",
|
|
31
|
+
"require": "./dist/featherk-composables.umd.js"
|
|
32
|
+
},
|
|
33
|
+
"./trap": {
|
|
34
|
+
"types": "./dist/trap/index.d.ts",
|
|
35
|
+
"import": "./dist/featherk-composables.es.js",
|
|
36
|
+
"require": "./dist/featherk-composables.umd.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
7
39
|
"files": [
|
|
8
|
-
"dist"
|
|
40
|
+
"dist",
|
|
41
|
+
"docs"
|
|
9
42
|
],
|
|
10
43
|
"scripts": {
|
|
11
44
|
"build": "vite build && npm run build:types",
|
|
@@ -15,11 +48,28 @@
|
|
|
15
48
|
"prepublishOnly": "npm run build"
|
|
16
49
|
},
|
|
17
50
|
"peerDependencies": {
|
|
18
|
-
"vue": ">=3.0.0"
|
|
51
|
+
"vue": ">=3.0.0",
|
|
52
|
+
"@vueuse/core": ">=10.0.0",
|
|
53
|
+
"@vueuse/integrations": ">=10.0.0",
|
|
54
|
+
"focus-trap": ">=7.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"@vueuse/core": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"@vueuse/integrations": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"focus-trap": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
19
66
|
},
|
|
20
67
|
"devDependencies": {
|
|
21
68
|
"@types/node": "^24.5.2",
|
|
22
69
|
"@vitejs/plugin-vue": "^6.0.1",
|
|
70
|
+
"@vueuse/core": "^13.5.0",
|
|
71
|
+
"@vueuse/integrations": "^13.5.0",
|
|
72
|
+
"focus-trap": "^7.6.0",
|
|
23
73
|
"typescript": "~5.8.3",
|
|
24
74
|
"vite": "^7.1.7",
|
|
25
75
|
"vitest": "^3.2.4",
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
interface GridAccessibilityOptions {
|
|
2
|
-
activeByDefault?: boolean;
|
|
3
|
-
autoActivateDelay?: number;
|
|
4
|
-
}
|
|
5
|
-
export declare function useGridComposableEx(options?: GridAccessibilityOptions): {
|
|
6
|
-
isGridActive: import("vue").Ref<boolean, boolean>;
|
|
7
|
-
activateGrid: () => void;
|
|
8
|
-
deactivateGrid: () => void;
|
|
9
|
-
toggleGrid: () => void;
|
|
10
|
-
};
|
|
11
|
-
export {};
|