@featherk/composables 0.11.0 → 0.11.2
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 +6 -0
- package/dist/featherk-composables.es.js +740 -706
- 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/usePopupMenu.d.ts +11 -3
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/useActiveIdRegistry.d.ts +35 -0
- package/dist/registry/useActiveIdRegistry.test.d.ts +1 -0
- package/docs/menu/usePopupMenu.md +123 -25
- package/docs/registry/useActiveIdRegistry.md +136 -0
- package/package.json +6 -1
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from "./grid";
|
|
|
2
2
|
export * from "./address";
|
|
3
3
|
export * from "./form";
|
|
4
4
|
export * from "./menu";
|
|
5
|
+
export * from "./registry";
|
|
5
6
|
export { useMaskedDateInput } from "./date";
|
|
6
7
|
export type { ChangePayload as DateChangePayload } from "./date";
|
|
7
8
|
export { useMaskedDateRangeInput } from "./range";
|
|
@@ -41,10 +41,18 @@ export type UsePopupMenuOptions = {
|
|
|
41
41
|
* Defaults to `".k-menu-item:not(.k-disabled)"` for Kendo Menu.
|
|
42
42
|
*/
|
|
43
43
|
menuItemSelector?: string;
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Keeps `aria-expanded` in sync with `isOpen` on the resolved `triggerRef`.
|
|
46
|
+
* Defaults to `true`. The consumer is always responsible for the static
|
|
47
|
+
* `aria-haspopup="menu"` and baseline `aria-expanded="false"` attributes.
|
|
48
|
+
*/
|
|
45
49
|
manageMenuTriggerAria?: boolean;
|
|
46
|
-
/**
|
|
47
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Declares whether `triggerRef` is a button or a `tr.k-table-row`. Required so
|
|
52
|
+
* misrouted refs (e.g. a button-mode ref that resolves to a row) are never
|
|
53
|
+
* silently managed.
|
|
54
|
+
*/
|
|
55
|
+
triggerMode: PopupMenuTriggerMode;
|
|
48
56
|
};
|
|
49
57
|
/** Functions returned by {@link usePopupMenu} for the action menu. */
|
|
50
58
|
export type UsePopupMenuReturn = {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ComputedRef, type DeepReadonly, type Ref } from "vue";
|
|
2
|
+
/** An element ref value that may be a plain element or a Vue/Kendo ref-like object exposing `$el`. */
|
|
3
|
+
export type RegisterableElement = HTMLElement | {
|
|
4
|
+
$el?: HTMLElement | null;
|
|
5
|
+
} | null;
|
|
6
|
+
/** Functions and state returned by {@link useActiveIdRegistry}. */
|
|
7
|
+
export type UseActiveIdRegistryReturn<Id> = {
|
|
8
|
+
/** The currently active id, or `null` when nothing is active. Read-only; use `activate`/`deactivate` to change it. */
|
|
9
|
+
activeId: DeepReadonly<Ref<Id | null>>;
|
|
10
|
+
/** `true` while any id is active. */
|
|
11
|
+
isActive: ComputedRef<boolean>;
|
|
12
|
+
/** The registered element for the active id, or `null`. */
|
|
13
|
+
activeElement: ComputedRef<HTMLElement | null>;
|
|
14
|
+
/** Registers (or unregisters, when `el` is `null`) the element for `id`. */
|
|
15
|
+
register: (id: Id, el: RegisterableElement) => void;
|
|
16
|
+
/** Returns the currently registered element for `id`, if any. */
|
|
17
|
+
resolve: (id: Id) => HTMLElement | null;
|
|
18
|
+
/** Marks `id` as active. */
|
|
19
|
+
activate: (id: Id) => void;
|
|
20
|
+
/** Clears the active id. */
|
|
21
|
+
deactivate: () => void;
|
|
22
|
+
/** Returns whether `id` is the currently active id. */
|
|
23
|
+
isIdActive: (id: Id) => boolean;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Tracks which single id, out of a rendered list of items (grid rows, tabs,
|
|
27
|
+
* action buttons, etc.), is currently "active," alongside a registry
|
|
28
|
+
* resolving each id to its mounted DOM element.
|
|
29
|
+
*
|
|
30
|
+
* This helper is useful for shared-instance patterns where one popup/menu instance
|
|
31
|
+
* is reused across a list while a single item remains active at a time.
|
|
32
|
+
*
|
|
33
|
+
* @returns Active-id state, an element registry, and activation helpers.
|
|
34
|
+
*/
|
|
35
|
+
export declare const useActiveIdRegistry: <Id = string | number>() => UseActiveIdRegistryReturn<Id>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -6,27 +6,29 @@ Composable for accessible Kendo UI for Vue `Popup` + `Menu` action menus. It man
|
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
|
-
1.
|
|
10
|
-
2.
|
|
11
|
-
3.
|
|
12
|
-
4.
|
|
13
|
-
5. Bind
|
|
9
|
+
1. Add static `aria-haspopup="menu"` and baseline `aria-expanded="false"` to the trigger element in your template. The composable never sets `aria-haspopup` and never removes `aria-expanded`; the consumer owns both as the closed-state baseline.
|
|
10
|
+
2. Create parent-owned refs for `isOpen`, `triggerRef`, `menuRef`, and an optional keyboard focus target.
|
|
11
|
+
3. Call `usePopupMenu(...)` with `requestShow`, `requestHide`, and an explicit `triggerMode: "button"` (or `"row"`).
|
|
12
|
+
4. Route Kendo Menu `@select` through `handleMenuSelect(event, onAction)` so the composable closes first and your business action runs second.
|
|
13
|
+
5. Bind trigger button `@click` and `@keydown` to the composable handlers.
|
|
14
|
+
6. Bind Popup `@close` and Menu `@keydown.escape` so keyboard and mouse close paths restore focus correctly.
|
|
14
15
|
|
|
15
16
|
```vue
|
|
16
17
|
<template>
|
|
17
18
|
<section ref="panelRef">
|
|
18
|
-
<!-- Step
|
|
19
|
+
<!-- Step 1: static aria-haspopup/aria-expanded baseline; Step 5: bind trigger handlers -->
|
|
19
20
|
<button
|
|
20
21
|
ref="triggerRef"
|
|
21
22
|
type="button"
|
|
22
23
|
aria-haspopup="menu"
|
|
24
|
+
aria-expanded="false"
|
|
23
25
|
@click="menu.handleActionButtonClick"
|
|
24
26
|
@keydown="menu.handleActionButtonKeydown"
|
|
25
27
|
>
|
|
26
28
|
Actions
|
|
27
29
|
</button>
|
|
28
30
|
|
|
29
|
-
<!-- Step
|
|
31
|
+
<!-- Step 6: bind popup/menu close hooks for modality-aware focus restoration -->
|
|
30
32
|
<Popup :show="isOpen" @close="menu.handlePopupClose">
|
|
31
33
|
<Menu
|
|
32
34
|
ref="menuRef"
|
|
@@ -44,23 +46,24 @@ import {
|
|
|
44
46
|
type KendoMenuSelectEvent,
|
|
45
47
|
} from "@featherk/composables/menu";
|
|
46
48
|
|
|
47
|
-
// Step
|
|
49
|
+
// Step 2: parent-owned menu state and element refs
|
|
48
50
|
const isOpen = ref(false);
|
|
49
51
|
const triggerRef = ref<HTMLElement | null>(null);
|
|
50
52
|
const menuRef = ref<HTMLElement | null>(null);
|
|
51
53
|
const panelRef = ref<HTMLElement | null>(null);
|
|
52
54
|
|
|
53
|
-
// Step
|
|
55
|
+
// Step 3: wire composable show/hide ownership to parent state
|
|
54
56
|
const menu = usePopupMenu({
|
|
55
57
|
isOpen,
|
|
56
58
|
triggerRef,
|
|
57
59
|
menuRef,
|
|
60
|
+
triggerMode: "button",
|
|
58
61
|
focusTargetRef: panelRef,
|
|
59
62
|
requestShow: () => (isOpen.value = true),
|
|
60
63
|
requestHide: () => (isOpen.value = false),
|
|
61
64
|
});
|
|
62
65
|
|
|
63
|
-
// Step
|
|
66
|
+
// Step 4: run close+modality handling first, then app-specific action logic
|
|
64
67
|
const onSelect = (event: KendoMenuSelectEvent) => {
|
|
65
68
|
menu.handleMenuSelect(event, (selected) => {
|
|
66
69
|
// Keep application-specific actions in the consuming component.
|
|
@@ -74,11 +77,12 @@ const onSelect = (event: KendoMenuSelectEvent) => {
|
|
|
74
77
|
|
|
75
78
|
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.
|
|
76
79
|
|
|
77
|
-
1.
|
|
78
|
-
2.
|
|
79
|
-
3.
|
|
80
|
-
4.
|
|
81
|
-
5.
|
|
80
|
+
1. Add static `aria-haspopup="menu"` and `aria-expanded="false"` to the row's trigger button template.
|
|
81
|
+
2. Read `showMenu` from the row item and keep open/close state parent-owned.
|
|
82
|
+
3. Create trigger and menu refs as component refs (`ComponentPublicInstance`) for Kendo wrappers.
|
|
83
|
+
4. Call `usePopupMenu(...)` with an explicit `triggerMode: "button"` and map `requestShow`/`requestHide` to row-specific emits.
|
|
84
|
+
5. Provide `resolveFocusTarget` so keyboard close restores focus to the current virtualized row.
|
|
85
|
+
6. Route menu selection through `handleMenuSelect(event, onAction)` so close/focus behavior runs before row action logic.
|
|
82
86
|
|
|
83
87
|
```ts
|
|
84
88
|
<script setup lang="ts">
|
|
@@ -94,21 +98,22 @@ const emit = defineEmits<{
|
|
|
94
98
|
"update:hideMenu": [id: string];
|
|
95
99
|
}>();
|
|
96
100
|
|
|
97
|
-
// Step
|
|
101
|
+
// Step 2: read row-owned open state from the data item
|
|
98
102
|
const isOpen = computed(() => props.dataItem.showMenu);
|
|
99
103
|
|
|
100
|
-
// Step
|
|
104
|
+
// Step 3: Kendo refs are component instances; usePopupMenu resolves $el internally
|
|
101
105
|
const triggerRef = ref<ComponentPublicInstance | null>(null);
|
|
102
106
|
const menuRef = ref<ComponentPublicInstance | null>(null);
|
|
103
107
|
|
|
104
|
-
// Step
|
|
108
|
+
// Step 4: keep popup ownership in parent row state via emits
|
|
105
109
|
const menu = usePopupMenu({
|
|
106
110
|
isOpen,
|
|
107
111
|
triggerRef,
|
|
108
112
|
menuRef,
|
|
113
|
+
triggerMode: "button",
|
|
109
114
|
requestShow: () => emit("update:showMenu", props.dataItem.id),
|
|
110
115
|
requestHide: () => emit("update:hideMenu", props.dataItem.id),
|
|
111
|
-
// Step
|
|
116
|
+
// Step 5: keyboard close restores focus to current virtualized grid row
|
|
112
117
|
resolveFocusTarget: () => {
|
|
113
118
|
const trigger = triggerRef.value?.$el as HTMLElement | undefined;
|
|
114
119
|
return (
|
|
@@ -117,7 +122,7 @@ const menu = usePopupMenu({
|
|
|
117
122
|
},
|
|
118
123
|
});
|
|
119
124
|
|
|
120
|
-
// Step
|
|
125
|
+
// Step 6: let usePopupMenu close first, then run row-specific business action
|
|
121
126
|
const onSelect = (event: KendoMenuSelectEvent) => {
|
|
122
127
|
menu.handleMenuSelect(event, (selected) => {
|
|
123
128
|
// Row-specific action logic remains here.
|
|
@@ -127,6 +132,98 @@ const onSelect = (event: KendoMenuSelectEvent) => {
|
|
|
127
132
|
</script>
|
|
128
133
|
```
|
|
129
134
|
|
|
135
|
+
## Row + Button Dual Trigger
|
|
136
|
+
|
|
137
|
+
A grid row can have both a row-level context menu (`triggerMode: "row"`) and a nested button-level quick-actions menu (`triggerMode: "button"`). Each trigger role needs its own composable instance and its own resolved DOM node; sharing a `triggerRef` between them lets one instance silently overwrite the other's `aria-expanded` state.
|
|
138
|
+
|
|
139
|
+
1. Add static `aria-haspopup="menu"` and `aria-expanded="false"` to every menu-capable trigger (the row's rendered `tr` and the nested button) up front, in the template, not through the composable.
|
|
140
|
+
2. Create one `usePopupMenu` instance per trigger role, each with its own `triggerRef`, `isOpen`, `requestShow`, and `requestHide`.
|
|
141
|
+
3. Resolve the row instance's `triggerRef` from a stable cell inside the row (e.g. `cellRef.value?.closest(".k-table-row")`); resolve the button instance's `triggerRef` from the button component ref directly. Never derive one from the other.
|
|
142
|
+
4. Set `triggerMode` explicitly for each instance (`"row"` for the row trigger, `"button"` for the nested button).
|
|
143
|
+
5. Verify in devtools that each instance's `triggerRef.value` resolves to a distinct DOM node before wiring `requestShow`/`requestHide`.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// Step 2-4: independent instances, independent triggerRef, explicit triggerMode
|
|
147
|
+
const rowTriggerRef = computed(() => cellRef.value?.closest(".k-table-row") as HTMLElement | null);
|
|
148
|
+
|
|
149
|
+
const buttonMenu = usePopupMenu({
|
|
150
|
+
isOpen: computed(() => props.dataItem.showButtonMenu),
|
|
151
|
+
triggerRef: buttonRef,
|
|
152
|
+
menuRef: buttonMenuRef,
|
|
153
|
+
triggerMode: "button",
|
|
154
|
+
requestShow: () => emit("update:toggleButtonMenu", props.dataItem.id),
|
|
155
|
+
requestHide: () => emit("update:hideButtonMenu", props.dataItem.id),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const rowMenu = usePopupMenu({
|
|
159
|
+
isOpen: computed(() => props.dataItem.showRowMenu),
|
|
160
|
+
triggerRef: rowTriggerRef,
|
|
161
|
+
menuRef: rowMenuRef,
|
|
162
|
+
triggerMode: "row",
|
|
163
|
+
requestShow: () => emit("update:toggleRowMenu", props.dataItem.id),
|
|
164
|
+
requestHide: () => emit("update:hideRowMenu", props.dataItem.id),
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
See [UsePopupMenu.vue](https://github.com/NantHealth/featherk/blob/integration/demos/src/views/composables/UsePopupMenu.vue) for the full reference implementation: exactly two shared instances (one row-mode, one button-mode) for the whole grid, with per-row/button trigger registries and offset-positioned Popups instead of per-row `usePopupMenu` calls.
|
|
169
|
+
|
|
170
|
+
## Sharing One Instance Across Many Triggers (Grid-Wide)
|
|
171
|
+
|
|
172
|
+
A single `usePopupMenu` instance can manage an entire list of same-mode triggers (every row, or every row's action button) instead of instantiating one composable per row. This avoids one popup lifecycle, one `onClickOutside` listener, and one set of watchers per row in large grids. It requires a small amount of consumer-owned bookkeeping the composable itself does not provide; the public helper `useActiveIdRegistry` from `@featherk/composables` handles that bookkeeping so each view does not need to reimplement it.
|
|
173
|
+
|
|
174
|
+
1. Track exactly one "active id" for the whole list, instead of a boolean per row item, via `useActiveIdRegistry<Id>()`. Only one trigger in the list can be open at a time. It exposes a read-only `activeId`, a reactive `isActive`/`activeElement` pair, and `register`/`resolve`/`activate`/`deactivate`/`isIdActive` helpers.
|
|
175
|
+
2. Register each row/button element into the registry as it mounts (e.g. a `row-render` ref callback, or a component `:ref` callback) via `registry.register(id, el)`. The registry's internal element map is reactive, so `activeElement` stays correct even if a keyed/virtualized re-render swaps out the active id's underlying DOM node.
|
|
176
|
+
3. Feed `isOpen: registry.isActive` and `triggerRef: registry.activeElement` straight into `usePopupMenu`, rather than deriving them from a per-row prop.
|
|
177
|
+
4. Position the shared `Popup` using `:offset` computed from the clicked element's `getBoundingClientRect()` (or click coordinates), not `:anchor`. Kendo's `Popup.anchor` only resolves a static template-ref name once at mount; it cannot dynamically retarget a different element after the popup is already mounted.
|
|
178
|
+
5. Drive show/hide through `registry.activate(id)` / `registry.deactivate()` rather than binding `handleActionButtonClick` directly to every trigger's `@click`. A single instance's `isOpen` is one shared boolean, so `handleActionButtonClick()` cannot tell "close this trigger" apart from "open a different trigger" — calling it while another id is active will close the wrong thing.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// Step 1: one registry tracks the active id and resolves it to its DOM element
|
|
182
|
+
const rowRegistry = useActiveIdRegistry<number>();
|
|
183
|
+
|
|
184
|
+
// Step 3: isOpen/triggerRef derive from the registry, not a per-row prop
|
|
185
|
+
const rowMenu = usePopupMenu({
|
|
186
|
+
isOpen: rowRegistry.isActive,
|
|
187
|
+
triggerRef: rowRegistry.activeElement,
|
|
188
|
+
menuRef: rowMenuRef,
|
|
189
|
+
triggerMode: "row",
|
|
190
|
+
requestShow: () => {}, // state is set directly by the row click handler, not by the composable
|
|
191
|
+
requestHide: () => rowRegistry.deactivate(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Step 2: register each row's element as it renders (e.g. inside a Grid row-render function)
|
|
195
|
+
const registerRowTrigger = (id: number, el: HTMLElement | null) => {
|
|
196
|
+
rowRegistry.register(id, el);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Step 5: toggle logic owns the open/close decision; the composable only reacts
|
|
200
|
+
const toggleRowMenu = (id: number) => {
|
|
201
|
+
if (rowRegistry.isIdActive(id)) {
|
|
202
|
+
rowRegistry.deactivate(); // closes: the reactive watcher demotes aria-expanded
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
rowRegistry.activate(id); // switches directly: previous trigger is demoted, new one promoted
|
|
206
|
+
};
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
For a second shared instance driven by a real `@click` handler on each trigger (e.g. a per-row action button, where `handleActionButtonClick` toggle semantics would otherwise misfire on a different trigger), close the currently active trigger explicitly through the composable before switching the active id, so its `triggerRef` still resolves to the correct element at close time:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const toggleButtonMenu = (id: number) => {
|
|
213
|
+
if (buttonRegistry.isIdActive(id)) {
|
|
214
|
+
buttonMenu.handleActionButtonClick(); // closes: triggerRef still matches this id
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (buttonRegistry.isActive.value) {
|
|
218
|
+
buttonMenu.handleActionButtonClick(); // closes the OTHER trigger correctly, before switching
|
|
219
|
+
}
|
|
220
|
+
buttonRegistry.activate(id); // the reactive watcher promotes the new trigger's aria-expanded
|
|
221
|
+
nextTick(focusFirstMenuItem); // reproduce first-item focus manually; the "open" branch never ran
|
|
222
|
+
};
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
See [UsePopupMenu.vue](https://github.com/NantHealth/featherk/blob/integration/demos/src/views/composables/UsePopupMenu.vue) for the complete working version of both patterns, including the registry-building `row-render` function and the offset-computation logic. `useActiveIdRegistry` is now part of `@featherk/composables`, so shared-instance consumers can import it directly instead of keeping a demo-local copy.
|
|
226
|
+
|
|
130
227
|
## API
|
|
131
228
|
|
|
132
229
|
### `usePopupMenu(options)`
|
|
@@ -141,8 +238,8 @@ const onSelect = (event: KendoMenuSelectEvent) => {
|
|
|
141
238
|
- `focusTargetRef?: PopupMenuElementRef`: Optional element/component to focus after a keyboard-driven close.
|
|
142
239
|
- `resolveFocusTarget?: () => HTMLElement | null`: Dynamic focus-target resolver. It takes precedence over `focusTargetRef` and is useful for virtualized grid rows.
|
|
143
240
|
- `menuItemSelector?: string`: Selector for the first enabled item. Defaults to `.k-menu-item:not(.k-disabled)`.
|
|
144
|
-
- `manageMenuTriggerAria?: boolean`: Keeps `aria-
|
|
145
|
-
- `triggerMode
|
|
241
|
+
- `manageMenuTriggerAria?: boolean`: Keeps `aria-expanded` in sync with `isOpen` on the resolved `triggerRef` element. Defaults to `true`. The consumer always owns the static `aria-haspopup="menu"` attribute and the baseline `aria-expanded="false"`; the composable only ever writes `aria-expanded` and never removes it. Set to `false` to own `aria-expanded` yourself too.
|
|
242
|
+
- `triggerMode: "button" | "row"`: Required. Declares the trigger semantics so a misrouted `triggerRef` (e.g. one that unexpectedly resolves to a `tr.k-table-row` while `triggerMode: "button"` is set) is never silently managed; a mismatch logs a `console.warn` and skips ARIA management for that resolution. Use `"row"` only when a `tr.k-table-row` owns the menu trigger. Neither mode adds `role`, `tabindex`, or keyboard behavior.
|
|
146
243
|
|
|
147
244
|
`PopupMenuElementRef` accepts a normal `HTMLElement` ref or a Vue/Kendo component ref that exposes `$el`.
|
|
148
245
|
|
|
@@ -173,9 +270,10 @@ export type KendoMenuSelectEvent = {
|
|
|
173
270
|
## Behavior
|
|
174
271
|
|
|
175
272
|
- Pointer click, `Enter`, and `Space` toggle the menu.
|
|
176
|
-
- `aria-haspopup="menu"`
|
|
177
|
-
-
|
|
178
|
-
-
|
|
273
|
+
- `aria-haspopup="menu"` is a static, consumer-owned template attribute; the composable never sets or removes it.
|
|
274
|
+
- `aria-expanded` is written to every resolved `triggerRef` element, including native buttons and Vue/Kendo component refs resolved through `$el`. The consumer provides the baseline `aria-expanded="false"` in the template; the composable only ever updates the value afterward.
|
|
275
|
+
- A `tr.k-table-row` is managed only with `triggerMode: "row"`; rows are never automatically made keyboard buttons. A mismatched `triggerMode`/resolved-element combination logs a `console.warn` and skips management for that element.
|
|
276
|
+
- Trigger replacement and virtualization are handled by re-resolving the ref. The previous trigger is demoted to `aria-expanded="false"`, not stripped of attributes, so a shared instance can move between many rows (e.g. a grid) without losing each row's `aria-haspopup` baseline. Set `manageMenuTriggerAria` to `false` when the consumer owns `aria-expanded` too.
|
|
179
277
|
- Opening focuses the first enabled Kendo Menu item after Vue renders it.
|
|
180
278
|
- Outside clicks close the menu, while the trigger is ignored to prevent a close/reopen race.
|
|
181
279
|
- Menu selection records keyboard or mouse modality from the selection event type.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# useActiveIdRegistry
|
|
2
|
+
|
|
3
|
+
[Back to Composables README](https://github.com/NantHealth/featherk/blob/integration/packages/composables/README.md)
|
|
4
|
+
|
|
5
|
+
Tracks a single active id across a list of rendered elements (grid rows, tabs, action buttons, etc.) while also resolving that id back to its mounted DOM element.
|
|
6
|
+
|
|
7
|
+
This helper is for shared-instance patterns where one popup/menu instance is reused across many triggers, but only a single item should be active/open at a time.
|
|
8
|
+
|
|
9
|
+
## Why use it
|
|
10
|
+
|
|
11
|
+
When a popup or menu instance is shared across a grid or list, a simple boolean per item is not enough to coordinate the active target. `useActiveIdRegistry` keeps two things in sync:
|
|
12
|
+
|
|
13
|
+
- the active id for the list
|
|
14
|
+
- the mounted element for that active id
|
|
15
|
+
|
|
16
|
+
This lets consumers derive both `isOpen` and `triggerRef` from the same registry, rather than re-implementing one-off bookkeeping in each view.
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
1. Create a registry instance at the shared list boundary so one active id drives the whole set of triggers.
|
|
21
|
+
2. Register each mounted element by id as it appears or re-renders, including Kendo/Vue component refs via `$el`.
|
|
22
|
+
3. Bind the registry's `isActive`/`activeElement` to the shared popup/menu instance and activate the item that should open.
|
|
23
|
+
4. Deactivate when the item closes so the previously active trigger is demoted and the next item can become active.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// Step 1: one registry manages the list-wide active id
|
|
27
|
+
const registry = useActiveIdRegistry<number>();
|
|
28
|
+
|
|
29
|
+
// Step 2: register each element as it mounts or re-renders
|
|
30
|
+
registry.register(1, rowElement); // Step 2
|
|
31
|
+
registry.register(2, buttonElement); // Step 2
|
|
32
|
+
|
|
33
|
+
// Step 3: connect the registry to a shared popup/menu instance
|
|
34
|
+
const isOpen = registry.isActive;
|
|
35
|
+
const triggerRef = registry.activeElement;
|
|
36
|
+
|
|
37
|
+
// Step 4: activate/deactivate when the current item opens or closes
|
|
38
|
+
registry.activate(2);
|
|
39
|
+
|
|
40
|
+
if (registry.isIdActive(2)) {
|
|
41
|
+
// active row/button is selected
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
registry.deactivate();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Basic Usage
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const rowRegistry = useActiveIdRegistry<number>();
|
|
51
|
+
|
|
52
|
+
const rowMenu = usePopupMenu({
|
|
53
|
+
isOpen: rowRegistry.isActive,
|
|
54
|
+
triggerRef: rowRegistry.activeElement,
|
|
55
|
+
menuRef: rowMenuRef,
|
|
56
|
+
triggerMode: "row",
|
|
57
|
+
requestShow: () => {
|
|
58
|
+
// parent-owned open state is handled by registry activation
|
|
59
|
+
},
|
|
60
|
+
requestHide: () => rowRegistry.deactivate(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const toggleRowMenu = (id: number) => {
|
|
64
|
+
if (rowRegistry.isIdActive(id)) {
|
|
65
|
+
rowRegistry.deactivate();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
rowRegistry.activate(id);
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This pattern is designed for one shared popup/menu instance across many rendered items. The active id stays in sync with the mounted DOM element, so the popup opens against the correct trigger without a per-item boolean for every row.
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const registry = useActiveIdRegistry<number>();
|
|
79
|
+
|
|
80
|
+
registry.register(1, rowElement);
|
|
81
|
+
registry.register(2, buttonElement);
|
|
82
|
+
|
|
83
|
+
registry.activate(2);
|
|
84
|
+
|
|
85
|
+
const isOpen = registry.isActive;
|
|
86
|
+
const triggerRef = registry.activeElement;
|
|
87
|
+
|
|
88
|
+
if (registry.isIdActive(2)) {
|
|
89
|
+
// active row/button is selected
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
registry.deactivate();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Return shape
|
|
96
|
+
|
|
97
|
+
- `activeId`: readonly current id or `null`
|
|
98
|
+
- `isActive`: computed `true` when an id is active
|
|
99
|
+
- `activeElement`: computed DOM element for the active id
|
|
100
|
+
- `register(id, el)`: stores or removes the element for a given id
|
|
101
|
+
- `resolve(id)`: returns the registered DOM element for `id`
|
|
102
|
+
- `activate(id)`: sets the active id
|
|
103
|
+
- `deactivate()`: clears the active id
|
|
104
|
+
- `isIdActive(id)`: checks whether a given id is the current active id
|
|
105
|
+
|
|
106
|
+
## Shared-instance popup usage
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const rowRegistry = useActiveIdRegistry<number>();
|
|
110
|
+
|
|
111
|
+
const rowMenu = usePopupMenu({
|
|
112
|
+
isOpen: rowRegistry.isActive,
|
|
113
|
+
triggerRef: rowRegistry.activeElement,
|
|
114
|
+
menuRef: rowMenuRef,
|
|
115
|
+
triggerMode: "row",
|
|
116
|
+
requestShow: () => {
|
|
117
|
+
// parent-owned open state is handled by activation logic
|
|
118
|
+
},
|
|
119
|
+
requestHide: () => rowRegistry.deactivate(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const toggleRowMenu = (id: number) => {
|
|
123
|
+
if (rowRegistry.isIdActive(id)) {
|
|
124
|
+
rowRegistry.deactivate();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
rowRegistry.activate(id);
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Notes
|
|
133
|
+
|
|
134
|
+
- Accepts both raw DOM elements and Vue/Kendo component refs exposing `$el`.
|
|
135
|
+
- Works well with virtualized or keyed re-renders because the registry re-resolves the current active element when the underlying DOM node changes.
|
|
136
|
+
- This helper is intentionally generic; it does not own popup behavior or ARIA attributes itself.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@featherk/composables",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"main": "dist/featherk-composables.umd.js",
|
|
5
5
|
"module": "dist/featherk-composables.es.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -40,6 +40,11 @@
|
|
|
40
40
|
"import": "./dist/featherk-composables.es.js",
|
|
41
41
|
"require": "./dist/featherk-composables.umd.js"
|
|
42
42
|
},
|
|
43
|
+
"./registry": {
|
|
44
|
+
"types": "./dist/registry/index.d.ts",
|
|
45
|
+
"import": "./dist/featherk-composables.es.js",
|
|
46
|
+
"require": "./dist/featherk-composables.umd.js"
|
|
47
|
+
},
|
|
43
48
|
"./form": {
|
|
44
49
|
"types": "./dist/form/index.d.ts",
|
|
45
50
|
"import": "./dist/featherk-composables.es.js",
|