@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.
- package/README.md +5 -0
- package/dist/docs-manifest.d.ts +5 -0
- package/dist/docs-manifest.js +27 -17
- package/dist/featherk-composables.es.js +1539 -1380
- 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/geometry/computeFlippedOffset.d.ts +40 -0
- package/dist/geometry/computeFlippedOffset.test.d.ts +1 -0
- package/dist/geometry/index.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/menu/usePopupMenu.d.ts +77 -11
- package/docs/menu/usePopupMenu.md +137 -25
- package/docs/menu/usePopupMenuActivationAnchor.md +132 -0
- package/docs/menu/usePopupMenuFocusAndExclusivity.md +105 -0
- package/docs/menu/usePopupMenuGridOrchestration.md +520 -0
- package/docs/menu/usePopupMenuSharedInstance.md +106 -0
- package/docs/menu/usePopupMenuUpgrade0130.md +115 -0
- package/docs/trap/usePopupTrap.md +1 -1
- package/package.json +6 -1
|
@@ -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.
|
|
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",
|