@featherk/composables 0.9.8 → 0.10.1

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/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./grid";
2
2
  export * from "./address";
3
3
  export * from "./form";
4
+ export * from "./menu";
4
5
  export { useMaskedDateInput } from "./date";
5
6
  export type { ChangePayload as DateChangePayload } from "./date";
6
7
  export { useMaskedDateRangeInput } from "./range";
@@ -0,0 +1 @@
1
+ export * from "./usePopupMenu";
@@ -0,0 +1,81 @@
1
+ import { type ComponentPublicInstance, type Ref } from "vue";
2
+ /** A template ref that may point to an element or a Vue/Kendo component. */
3
+ export type PopupMenuElementRef = Ref<HTMLElement | ComponentPublicInstance | null>;
4
+ /** Why the popup was closed, including the focus policy for mouse paths. */
5
+ export type PopupMenuCloseReason = "keyboard" | "mouse-selection" | "outside-click" | "trigger-click" | null;
6
+ export type PopupMenuCloseTrigger = Exclude<PopupMenuCloseReason, null>;
7
+ /** Minimal event shape emitted by Kendo Menu `@select`. */
8
+ export type KendoMenuSelectEvent = {
9
+ item?: {
10
+ text?: string;
11
+ };
12
+ event?: {
13
+ type?: string;
14
+ } | null;
15
+ };
16
+ /** Options for configuring {@link usePopupMenu}. */
17
+ export type UsePopupMenuOptions = {
18
+ /** Reactive flag reflecting whether the popup menu is currently open. */
19
+ isOpen: Ref<boolean>;
20
+ /** Ref to the popup menu element or component. Used for outside-click detection. */
21
+ menuRef: PopupMenuElementRef;
22
+ /** Ref to the trigger button element or component. Ignored by outside-click detection. */
23
+ triggerRef: PopupMenuElementRef;
24
+ /** Called to open the popup menu. */
25
+ requestShow: () => void;
26
+ /** Called to close the popup menu. */
27
+ requestHide: () => void;
28
+ /**
29
+ * Optional target to focus after a keyboard-driven close.
30
+ * Supports ordinary elements and Vue/Kendo components exposing `$el`.
31
+ */
32
+ focusTargetRef?: PopupMenuElementRef;
33
+ /**
34
+ * Resolves the focus target when it is virtualized or dynamically rendered.
35
+ * Takes precedence over `focusTargetRef`.
36
+ */
37
+ resolveFocusTarget?: () => HTMLElement | null;
38
+ /**
39
+ * CSS selector used to find the first focusable menu item after open.
40
+ * Defaults to `".k-menu-item:not(.k-disabled)"` for Kendo Menu.
41
+ */
42
+ menuItemSelector?: string;
43
+ /**
44
+ * Keeps `aria-expanded` on the trigger in sync with `isOpen`.
45
+ * Applies only to button-like triggers. Defaults to `true`.
46
+ */
47
+ manageAriaExpanded?: boolean;
48
+ };
49
+ /** Functions returned by {@link usePopupMenu} for the action menu. */
50
+ export type UsePopupMenuReturn = {
51
+ /** Opens the menu, or closes it when it is already open. */
52
+ handleActionButtonClick: () => void;
53
+ /**
54
+ * Handles keyboard input on the action button. Enter and Space open or close
55
+ * the menu. Escape closes an open menu or moves focus to the configured target.
56
+ */
57
+ handleActionButtonKeydown: (event: KeyboardEvent) => void;
58
+ /** Closes the menu when Escape is pressed while using the menu. */
59
+ handleActionMenuEscape: (event: KeyboardEvent) => void;
60
+ /**
61
+ * Closes the menu after an item is chosen, then runs the optional action
62
+ * provided by the consuming component.
63
+ */
64
+ handleMenuSelect: (event: KendoMenuSelectEvent, onAction?: (event: KendoMenuSelectEvent) => void) => void;
65
+ /**
66
+ * Restores focus after the popup closes. Keyboard actions prefer the
67
+ * configured focus target; mouse menu selections return focus to the trigger.
68
+ */
69
+ handlePopupClose: () => void;
70
+ };
71
+ /**
72
+ * Composable for accessible Kendo Popup + Menu action-menu interactions.
73
+ *
74
+ * It manages toggling, menu-item focus, selection modality, and contextual
75
+ * focus restoration. The consuming component keeps ownership of menu state
76
+ * and business-specific actions.
77
+ *
78
+ * @param options - Parent-owned popup state, refs, and focus configuration.
79
+ * @returns Template event handlers for the action trigger, Kendo Menu, and Popup.
80
+ */
81
+ export declare const usePopupMenu: (options: UsePopupMenuOptions) => UsePopupMenuReturn;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,146 @@
1
+ # usePopupMenu
2
+
3
+ [Back to Composables README](https://github.com/NantHealth/featherk/blob/integration/packages/composables/README.md)
4
+
5
+ Composable for accessible Kendo UI for Vue `Popup` + `Menu` action menus. It manages trigger toggling, first-item focus, outside clicks, input-modality tracking, and focus restoration. It does not trap focus or own popup state; keep `usePopupTrap` separate when focus trapping or generic popup lifecycle behavior is needed.
6
+
7
+ ## Quick Start
8
+
9
+ ```ts
10
+ <script setup lang="ts">
11
+ import { computed, ref } from "vue";
12
+ import { usePopupMenu } from "@featherk/composables/menu";
13
+
14
+ const isOpen = ref(false);
15
+ const triggerRef = ref<HTMLElement | null>(null);
16
+ const menuRef = ref<HTMLElement | null>(null);
17
+ const panelRef = ref<HTMLElement | null>(null);
18
+
19
+ const menu = usePopupMenu({
20
+ isOpen,
21
+ triggerRef,
22
+ menuRef,
23
+ focusTargetRef: panelRef,
24
+ requestShow: () => (isOpen.value = true),
25
+ requestHide: () => (isOpen.value = false),
26
+ });
27
+
28
+ const onSelect = (event: Parameters<typeof menu.handleMenuSelect>[0]) => {
29
+ menu.handleMenuSelect(event, (selected) => {
30
+ // Keep application-specific actions in the consuming component.
31
+ if (selected.item?.text === "Archive") archiveRecord();
32
+ });
33
+ };
34
+ </script>
35
+
36
+ <template>
37
+ <section ref="panelRef">
38
+ <button
39
+ ref="triggerRef"
40
+ type="button"
41
+ aria-haspopup="true"
42
+ @click="menu.handleActionButtonClick"
43
+ @keydown="menu.handleActionButtonKeydown"
44
+ >
45
+ Actions
46
+ </button>
47
+
48
+ <Popup :show="isOpen" @close="menu.handlePopupClose">
49
+ <Menu
50
+ ref="menuRef"
51
+ @keydown.escape="menu.handleActionMenuEscape"
52
+ @select="onSelect"
53
+ />
54
+ </Popup>
55
+ </section>
56
+ </template>
57
+ ```
58
+
59
+ ## Grid Action Cell
60
+
61
+ For a virtualized Kendo Grid, use `resolveFocusTarget` to locate the current row when the popup closes by keyboard. The resolver is evaluated at focus time, after the grid and popup have settled.
62
+
63
+ ```ts
64
+ <script setup lang="ts">
65
+ import { computed, ref, type ComponentPublicInstance } from "vue";
66
+ import { usePopupMenu } from "@featherk/composables/menu";
67
+
68
+ const props = defineProps<{ dataItem: { id: string; showMenu: boolean } }>();
69
+ const emit = defineEmits<{
70
+ "update:showMenu": [id: string];
71
+ "update:hideMenu": [id: string];
72
+ }>();
73
+
74
+ const triggerRef = ref<ComponentPublicInstance | null>(null);
75
+ const menuRef = ref<ComponentPublicInstance | null>(null);
76
+
77
+ const menu = usePopupMenu({
78
+ isOpen: computed(() => props.dataItem.showMenu),
79
+ triggerRef,
80
+ menuRef,
81
+ requestShow: () => emit("update:showMenu", props.dataItem.id),
82
+ requestHide: () => emit("update:hideMenu", props.dataItem.id),
83
+ resolveFocusTarget: () => {
84
+ const trigger = triggerRef.value?.$el as HTMLElement | undefined;
85
+ return (
86
+ trigger?.closest(".k-table-row[data-grid-row-index]") ?? null
87
+ ) as HTMLElement | null;
88
+ },
89
+ });
90
+
91
+ const onSelect = (event: Parameters<typeof menu.handleMenuSelect>[0]) => {
92
+ menu.handleMenuSelect(event, (selected) => {
93
+ // Row-specific action logic remains here.
94
+ runGridAction(props.dataItem.id, selected.item?.text);
95
+ });
96
+ };
97
+ </script>
98
+ ```
99
+
100
+ ## API
101
+
102
+ ### `usePopupMenu(options)`
103
+
104
+ #### Options
105
+
106
+ - `isOpen: Ref<boolean>`: Parent-owned popup visibility state.
107
+ - `menuRef: PopupMenuElementRef`: Ref to the Kendo `Menu` element or component.
108
+ - `triggerRef: PopupMenuElementRef`: Ref to the action trigger element or component.
109
+ - `requestShow(): void`: Opens the parent-owned popup state.
110
+ - `requestHide(): void`: Closes the parent-owned popup state.
111
+ - `focusTargetRef?: PopupMenuElementRef`: Optional element/component to focus after a keyboard-driven close.
112
+ - `resolveFocusTarget?: () => HTMLElement | null`: Dynamic focus-target resolver. It takes precedence over `focusTargetRef` and is useful for virtualized grid rows.
113
+ - `menuItemSelector?: string`: Selector for the first enabled item. Defaults to `.k-menu-item:not(.k-disabled)`.
114
+ - `manageAriaExpanded?: boolean`: Keeps `aria-expanded` on a button-like trigger in sync with `isOpen`. Defaults to `true`; set to `false` to bind the attribute yourself.
115
+
116
+ `PopupMenuElementRef` accepts a normal `HTMLElement` ref or a Vue/Kendo component ref that exposes `$el`.
117
+
118
+ #### Returns
119
+
120
+ - `handleActionButtonClick()`: Toggles the menu and focuses the first enabled item after opening.
121
+ - `handleActionButtonKeydown(event)`: `Enter` and `Space` toggle; `Escape` closes, or focuses the configured target when already closed.
122
+ - `handleActionMenuEscape(event)`: Closes an open menu with keyboard modality.
123
+ - `handleMenuSelect(event, onAction?)`: Records the selection modality, requests close, then invokes optional business logic.
124
+ - `handlePopupClose()`: Restores focus after Kendo Popup has closed.
125
+
126
+ #### Types
127
+
128
+ ```ts
129
+ export type PopupMenuCloseReason = "keyboard" | "mouse" | null;
130
+ export type PopupMenuCloseTrigger = "keyboard" | "mouse";
131
+ export type KendoMenuSelectEvent = {
132
+ item?: { text?: string };
133
+ event?: { type?: string } | null;
134
+ };
135
+ ```
136
+
137
+ ## Behavior
138
+
139
+ - Pointer click, `Enter`, and `Space` toggle the menu.
140
+ - `aria-expanded` is written to the trigger whenever it is a `<button>`, has `role="button"`, or declares `aria-haspopup`.
141
+ - Opening focuses the first enabled Kendo Menu item after Vue renders it.
142
+ - Outside clicks close the menu, while the trigger is ignored to prevent a close/reopen race.
143
+ - Selection and Escape preserve keyboard versus mouse modality.
144
+ - Mouse-driven closes restore focus to the trigger.
145
+ - Keyboard-driven closes focus `resolveFocusTarget()` or `focusTargetRef`, then fall back to the trigger.
146
+ - Business-specific actions are never implemented by the composable.
@@ -145,8 +145,9 @@ Creates a controller for a masked date range input.
145
145
  - **`debugEnabled: Ref<boolean>`** and **`debugLines: Computed<Array<{label:string; value:string}>>`**: Debug info for UI display.
146
146
  - **`digitsOnly: Computed<string>`**: The raw string with non-digits removed.
147
147
  - **`valid: Ref<boolean | undefined>`**: Managed validity state (mirrored into `externalValid` when enabled).
148
- - **`validComputed: Readonly<ComputedRef<boolean | undefined>>`**: Derived validity considering mask completeness, min/max clamping, ordering, and span constraints.
149
- - **`reason: Readonly<ComputedRef<string | undefined>>`**: Explanation for invalid states; returns `'valid'` when the range passes validation.
148
+ - **`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.
149
+ - **`reason: Readonly<ComputedRef<string | undefined>>`**: Explanation for invalid states; returns `'valid'` when the range passes validation and `undefined` before the first blur/clear.
150
+ - **`validationMessage: Readonly<ComputedRef<string>>`**: User-facing message derived from `reason`; always in sync with `validComputed`.
150
151
  - **`spanDays: Readonly<ComputedRef<number | undefined>>`**: Span in days between clamped start/end; `undefined` when incomplete or invalid.
151
152
  - Readonly computed parts: **`month1`**, **`day1`**, **`year1`**, **`month2`**, **`day2`**, **`year2`** (strings). Useful for diagnostics.
152
153
  - **`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
@@ -165,7 +166,9 @@ Creates a controller for a masked date range input.
165
166
 
166
167
  - **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.
167
168
  - **Validation**:
168
- - Validation is performed only when the input loses focus (on blur).
169
+ - No validation is reported until the input first loses focus (or is cleared), matching `useMaskedDateInput`'s required-field behavior.
170
+ - 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.
171
+ - `valid` (the managed ref mirrored into `externalValid`) is still committed on blur/clear. Bind `:valid="validComputed"` for the live-after-blur styling.
169
172
  - Each side must be a valid date (including month/day bounds like Feb 29).
170
173
  - Clamped to `min`/`max` if provided; outside results in `value=null`.
171
174
  - Ordered range required unless `allowReverse=true`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@featherk/composables",
3
- "version": "0.9.8",
3
+ "version": "0.10.1",
4
4
  "main": "dist/featherk-composables.umd.js",
5
5
  "module": "dist/featherk-composables.es.js",
6
6
  "types": "dist/index.d.ts",
@@ -35,6 +35,11 @@
35
35
  "import": "./dist/featherk-composables.es.js",
36
36
  "require": "./dist/featherk-composables.umd.js"
37
37
  },
38
+ "./menu": {
39
+ "types": "./dist/menu/index.d.ts",
40
+ "import": "./dist/featherk-composables.es.js",
41
+ "require": "./dist/featherk-composables.umd.js"
42
+ },
38
43
  "./form": {
39
44
  "types": "./dist/form/index.d.ts",
40
45
  "import": "./dist/featherk-composables.es.js",