@featherk/composables 0.10.0 → 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/README.md +7 -0
- package/dist/featherk-composables.es.js +922 -843
- package/dist/featherk-composables.es.js.map +1 -1
- package/dist/featherk-composables.umd.js +1 -1
- package/dist/featherk-composables.umd.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/menu/index.d.ts +1 -0
- package/dist/menu/usePopupMenu.d.ts +81 -0
- package/dist/menu/usePopupMenu.test.d.ts +1 -0
- package/docs/menu/usePopupMenu.md +146 -0
- package/package.json +6 -1
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@featherk/composables",
|
|
3
|
-
"version": "0.10.
|
|
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",
|