@featherk/composables 0.12.5 → 0.13.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.
@@ -0,0 +1,106 @@
1
+ # usePopupMenu Shared Instances
2
+
3
+ [Back to usePopupMenu](https://github.com/NantHealth/featherk/blob/integration/packages/composables/docs/menu/usePopupMenu.md)
4
+
5
+ Use one `usePopupMenu` instance for many equivalent triggers when only one item may own the menu at a time.
6
+
7
+ ## Quick Start
8
+
9
+ 1. Register every trigger with `useActiveIdRegistry`.
10
+ 2. Pass `registry.isActive` and `registry.activeElement` to `usePopupMenu`.
11
+ 3. Activate or deactivate ids in a consumer-owned toggle function.
12
+ 4. Render one Popup and bind its `show`, `offset`, and close handler.
13
+
14
+ ```vue
15
+ <template>
16
+ <ul>
17
+ <li v-for="record in records" :key="record.id">
18
+ <button
19
+ :ref="(element) => registerTrigger(record.id, element)"
20
+ type="button"
21
+ aria-haspopup="menu"
22
+ aria-expanded="false"
23
+ @click="toggleMenu(record.id)"
24
+ >
25
+ Actions for {{ record.name }}
26
+ </button>
27
+ </li>
28
+ </ul>
29
+
30
+ <Popup
31
+ :show="registry.isActive.value"
32
+ :offset="menu.offset.value"
33
+ @close="menu.handlePopupClose"
34
+ >
35
+ <Menu
36
+ ref="menuRef"
37
+ :items="items"
38
+ :vertical="true"
39
+ @keydown.escape="menu.handleMenuEscape"
40
+ @select="menu.handleMenuSelect"
41
+ />
42
+ </Popup>
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ import { ref, type ComponentPublicInstance } from "vue";
47
+ import { Popup } from "@progress/kendo-vue-popup";
48
+ import { Menu } from "@progress/kendo-vue-layout";
49
+ import { usePopupMenu } from "@featherk/composables/menu";
50
+ import { useActiveIdRegistry } from "@featherk/composables/registry";
51
+
52
+ const records = [
53
+ { id: 1, name: "Alpha" },
54
+ { id: 2, name: "Beta" },
55
+ ];
56
+ const items = [{ text: "Open" }, { text: "Archive" }];
57
+ const registry = useActiveIdRegistry<number>();
58
+ const menuRef = ref<ComponentPublicInstance | null>(null);
59
+
60
+ // Step 2: one reactive pair follows whichever trigger id is active.
61
+ const menu = usePopupMenu({
62
+ isOpen: registry.isActive,
63
+ triggerRef: registry.activeElement,
64
+ menuRef,
65
+ triggerMode: "button",
66
+ anchor: true,
67
+ requestShow: () => {},
68
+ requestHide: () => registry.deactivate(),
69
+ });
70
+
71
+ // Step 1: registration remains stable across keyed rerenders.
72
+ const registerTrigger = (id: number, element: unknown) => {
73
+ registry.register(
74
+ id,
75
+ element as HTMLElement | ComponentPublicInstance | null,
76
+ );
77
+ };
78
+
79
+ // Step 3: shared state must distinguish closing the active id from switching ids.
80
+ const toggleMenu = (id: number) => {
81
+ if (registry.isIdActive(id)) {
82
+ menu.handleTriggerClick();
83
+ return;
84
+ }
85
+
86
+ if (registry.isActive.value) {
87
+ menu.handleTriggerClick();
88
+ }
89
+ registry.activate(id);
90
+ };
91
+ </script>
92
+ ```
93
+
94
+ ## Why Offset Positioning
95
+
96
+ Bind the returned `offset` instead of Kendo Popup's `anchor` prop for a shared instance. Kendo resolves an anchor ref name when the Popup mounts; it does not dynamically retarget a different trigger element. `usePopupMenu` remeasures the active registered element on scroll, resize, and trigger replacement.
97
+
98
+ ## Trigger Switching
99
+
100
+ When an active id changes, `usePopupMenu` demotes the previous element to `aria-expanded="false"` and promotes the new trigger. Each trigger must provide the static baseline:
101
+
102
+ ```html
103
+ aria-haspopup="menu" aria-expanded="false"
104
+ ```
105
+
106
+ The composable does not add `aria-haspopup`, roles, or keyboard focusability.
@@ -0,0 +1,115 @@
1
+ # Upgrading usePopupMenu to 0.13.0
2
+
3
+ [Back to usePopupMenu](https://github.com/NantHealth/featherk/blob/integration/packages/composables/docs/menu/usePopupMenu.md)
4
+
5
+ This guide covers migration from `@featherk/composables` 0.12.x to 0.13.0.
6
+
7
+ ## Required Changes
8
+
9
+ Version 0.13.0 renames three returned handlers so their names apply equally to button and row triggers.
10
+
11
+ | 0.12.x | 0.13.0 |
12
+ | --------------------------- | ---------------------- |
13
+ | `handleActionButtonClick` | `handleTriggerClick` |
14
+ | `handleActionButtonKeydown` | `handleTriggerKeydown` |
15
+ | `handleActionMenuEscape` | `handleMenuEscape` |
16
+
17
+ Before:
18
+
19
+ ```vue
20
+ <button
21
+ @click="menu.handleActionButtonClick"
22
+ @keydown="menu.handleActionButtonKeydown"
23
+ />
24
+ <Menu @keydown.escape="menu.handleActionMenuEscape" />
25
+ ```
26
+
27
+ After:
28
+
29
+ ```vue
30
+ <button @click="menu.handleTriggerClick" @keydown="menu.handleTriggerKeydown" />
31
+ <Menu @keydown.escape="menu.handleMenuEscape" />
32
+ ```
33
+
34
+ Search all consumer source for:
35
+
36
+ ```text
37
+ handleActionButtonClick
38
+ handleActionButtonKeydown
39
+ handleActionMenuEscape
40
+ ```
41
+
42
+ The old properties are removed rather than retained as aliases.
43
+
44
+ ## Optional: Replace Manual Activation Geometry
45
+
46
+ Custom `getRect(trigger)` remains supported. Consumers that manually store pointer coordinates relative to a trigger can remove that state and use activation-point anchoring.
47
+
48
+ Before:
49
+
50
+ ```ts
51
+ const point = ref<{ x: number; y: number } | null>(null);
52
+
53
+ const menu = usePopupMenu({
54
+ // Other required options omitted.
55
+ anchor: {
56
+ intersectionThreshold: 0,
57
+ getRect: (trigger) => {
58
+ const rect = trigger.getBoundingClientRect();
59
+ return point.value
60
+ ? new DOMRect(rect.left + point.value.x, rect.top + point.value.y, 0, 0)
61
+ : null;
62
+ },
63
+ },
64
+ });
65
+ ```
66
+
67
+ After:
68
+
69
+ ```ts
70
+ const menu = usePopupMenu({
71
+ // Other required options omitted.
72
+ anchor: {
73
+ activationPoint: { pointerOffset: { x: -12, y: -12 } },
74
+ },
75
+ });
76
+
77
+ const trigger = registry.resolve(id);
78
+ if (trigger && menu.setAnchorActivation(trigger, context)) {
79
+ registry.activate(id);
80
+ }
81
+ ```
82
+
83
+ Capture before activating a shared registry id. Pointer coordinates become trigger-relative automatically; keyboard activation defaults to 8px from the left and vertically centered. Internal activation state clears when the menu closes.
84
+
85
+ ## Optional: Simplify Focus Restoration
86
+
87
+ Before:
88
+
89
+ ```ts
90
+ resolveFocusTarget: () =>
91
+ registry.activeElement.value?.closest(".k-table-row") ?? null,
92
+ ```
93
+
94
+ After:
95
+
96
+ ```ts
97
+ focusTargetContainerSelector: ".k-table-row",
98
+ ```
99
+
100
+ Keep `resolveFocusTarget` when the target cannot be expressed as the trigger's closest matching ancestor. Its precedence remains higher than `focusTargetRef` and `focusTargetContainerSelector`.
101
+
102
+ ## Unchanged Contracts
103
+
104
+ - The consumer owns `isOpen`, `requestShow`, and `requestHide`.
105
+ - The consumer provides static `aria-haspopup="menu"` and baseline `aria-expanded="false"`.
106
+ - `usePopupMenu` updates only `aria-expanded` unless ARIA management is disabled.
107
+ - `anchor: true` continues to follow the trigger's bottom-left corner.
108
+ - Custom `getRect(trigger)` remains supported and is source-compatible with zero-argument callbacks.
109
+ - Intersection dismissal and the `"anchor-hidden"` close reason are unchanged.
110
+ - `useActiveIdRegistry`, `useExclusiveGroup`, and `useGridRowAction` remain independent composables.
111
+ - Package root and `@featherk/composables/menu` imports remain supported.
112
+
113
+ ## Custom Geometry Fallback
114
+
115
+ Activation-point anchoring is optional. Retain `getRect(trigger)` when the popup must follow geometry that is not derived from a pointer or keyboard activation, such as a selection rectangle or a domain-specific virtual anchor.
@@ -101,7 +101,7 @@ export type InitialFocus = string | ((root: HTMLElement) => HTMLElement | null |
101
101
  - `.k-popup`
102
102
  - `.k-timepicker-popup`
103
103
  - `.k-menu-popup`
104
- - **Focus trap**: Uses `useFocusTrap(popupRef)` with a fallback/initial focus inside the popup. `escapeDeactivates` and `clickOutsideDeactivates` are disabled; closing is managed explicitly.
104
+ - **Focus trap**: Uses `useFocusTrap(popupRef)` with a fallback/initial focus inside the popup. `escapeDeactivates` and `clickOutsideDeactivates` are disabled; closing is managed explicitly. `allowOutsideClick` defaults to `true` - without it, `focus-trap`'s own default (`false`) `preventDefault()`s and swallows *every* outside pointerdown/click while the trap is active, not just ones that would close it, blocking interaction with anything else on the page (e.g. a second trigger in the same grid row) until the trap deactivates. Override via `focusTrapOptions` only if a specific popup genuinely needs that stricter modal-style behavior.
105
105
  - **Escape to close**: Adds a `keydown` listener on the popup when opened; pressing Escape calls `onRequestClose('escape', event)` and stops propagation.
106
106
  - **Outside click to close**: Uses `onClickOutside(popupRef, ...)` to call `onRequestClose('outside', event)` when open.
107
107
  - **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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@featherk/composables",
3
- "version": "0.12.5",
3
+ "version": "0.13.1",
4
4
  "main": "dist/featherk-composables.umd.js",
5
5
  "module": "dist/featherk-composables.es.js",
6
6
  "types": "dist/index.d.ts",
@@ -45,6 +45,11 @@
45
45
  "import": "./dist/featherk-composables.es.js",
46
46
  "require": "./dist/featherk-composables.umd.js"
47
47
  },
48
+ "./geometry": {
49
+ "types": "./dist/geometry/index.d.ts",
50
+ "import": "./dist/featherk-composables.es.js",
51
+ "require": "./dist/featherk-composables.umd.js"
52
+ },
48
53
  "./registry": {
49
54
  "types": "./dist/registry/index.d.ts",
50
55
  "import": "./dist/featherk-composables.es.js",