@featherk/composables 0.11.1 → 0.12.0
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 +1226 -1104
- 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/id/index.d.ts +2 -0
- package/dist/id/useCompositeId.d.ts +17 -0
- package/dist/id/useCompositeId.test.d.ts +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/menu/usePopupMenu.d.ts +68 -11
- package/dist/observer/index.d.ts +1 -0
- package/dist/observer/useIntersectionObserver.d.ts +31 -0
- package/dist/observer/useIntersectionObserver.test.d.ts +1 -0
- 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/date/useMaskedDateInput.md +9 -3
- package/docs/id/useCompositeId.md +29 -0
- package/docs/menu/usePopupMenu.md +129 -26
- package/docs/observer/useIntersectionObserver.md +95 -0
- package/docs/registry/useActiveIdRegistry.md +136 -0
- package/package.json +16 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# useIntersectionObserver
|
|
2
|
+
|
|
3
|
+
[Back to Composables README](https://github.com/NantHealth/featherk/blob/integration/packages/composables/README.md)
|
|
4
|
+
|
|
5
|
+
Observes a reactive DOM target against a lazily resolved scroll container. It is useful for grid cells, virtualized rows, and other elements that must react after they leave their visible viewport.
|
|
6
|
+
|
|
7
|
+
## Grid Containers
|
|
8
|
+
|
|
9
|
+
When observing an element rendered in a Kendo Grid, explicitly provide the grid's scrolling content element through `root`. The composable cannot infer the correct viewport from the Grid wrapper because a grid may be nested in additional scrolling layouts.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
root: () => cellRef.value?.closest(".k-grid-content") ?? null,
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Do not use the outer Grid component element unless it is the element that actually scrolls. For standard Kendo Grid layouts, use `.k-grid-content`.
|
|
16
|
+
|
|
17
|
+
## Native Tables
|
|
18
|
+
|
|
19
|
+
The composable works the same way with a regular HTML table. Place the table in a scrolling wrapper and use that wrapper as `root`; the `<tr>` is the observed target.
|
|
20
|
+
|
|
21
|
+
```vue
|
|
22
|
+
<template>
|
|
23
|
+
<div ref="tableViewportRef" class="table-viewport">
|
|
24
|
+
<table>
|
|
25
|
+
<tbody>
|
|
26
|
+
<tr ref="rowRef">
|
|
27
|
+
<td>Product row</td>
|
|
28
|
+
</tr>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
import { ref } from "vue";
|
|
36
|
+
import { useIntersectionObserver } from "@featherk/composables";
|
|
37
|
+
|
|
38
|
+
// Step 1: bind refs to the native scroll wrapper and target table row.
|
|
39
|
+
const tableViewportRef = ref<HTMLElement | null>(null);
|
|
40
|
+
const rowRef = ref<HTMLTableRowElement | null>(null);
|
|
41
|
+
|
|
42
|
+
// Step 2: use the scrolling wrapper as root and react in consumer state.
|
|
43
|
+
useIntersectionObserver({
|
|
44
|
+
target: rowRef,
|
|
45
|
+
root: () => tableViewportRef.value,
|
|
46
|
+
threshold: 0,
|
|
47
|
+
onChange: (entry) => {
|
|
48
|
+
if (!entry.isIntersecting) closeMenu();
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<style>
|
|
54
|
+
.table-viewport {
|
|
55
|
+
height: 400px;
|
|
56
|
+
overflow: auto;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
1. Create a ref for the element to observe.
|
|
64
|
+
2. Pass a lazy `root` resolver for its clipping container.
|
|
65
|
+
3. Handle intersection changes in the consuming component; the composable does not own menu or application state.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { ref } from "vue";
|
|
69
|
+
import { useIntersectionObserver } from "@featherk/composables";
|
|
70
|
+
|
|
71
|
+
// Step 1: bind this ref to the target element.
|
|
72
|
+
const cellRef = ref<Element | null>(null);
|
|
73
|
+
|
|
74
|
+
// Step 2: resolve the scroll root only after the target is mounted.
|
|
75
|
+
// Step 3: keep application-specific visibility behavior in the consumer.
|
|
76
|
+
useIntersectionObserver({
|
|
77
|
+
target: cellRef,
|
|
78
|
+
root: () => cellRef.value?.closest(".k-grid-content") ?? null,
|
|
79
|
+
threshold: 0,
|
|
80
|
+
onChange: (entry) => {
|
|
81
|
+
if (!entry.isIntersecting) closeMenu();
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Threshold
|
|
87
|
+
|
|
88
|
+
`threshold` accepts one number or an array of numbers in the inclusive interval $[0, 1]$. Values outside that range are invalid according to the browser `IntersectionObserver` API. It defaults to `1`.
|
|
89
|
+
|
|
90
|
+
- `0`: callback runs when the target enters the root and again when it fully leaves. Use this to dismiss a popup only after its trigger has fully scrolled out of view.
|
|
91
|
+
- `0.5`: callback runs as the visible portion crosses $50\%$.
|
|
92
|
+
- `1`: callback runs when the target becomes fully visible or stops being fully visible. This is the default.
|
|
93
|
+
- `[0, 0.5, 1]`: callback runs when the target crosses any listed visibility boundary.
|
|
94
|
+
|
|
95
|
+
`entry.isIntersecting` becomes `false` only after the target leaves the root entirely, regardless of the configured threshold. The composable disconnects automatically on component unmount and re-observes when `target` changes.
|
|
@@ -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.
|
|
3
|
+
"version": "0.12.0",
|
|
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,16 @@
|
|
|
40
40
|
"import": "./dist/featherk-composables.es.js",
|
|
41
41
|
"require": "./dist/featherk-composables.umd.js"
|
|
42
42
|
},
|
|
43
|
+
"./observer": {
|
|
44
|
+
"types": "./dist/observer/index.d.ts",
|
|
45
|
+
"import": "./dist/featherk-composables.es.js",
|
|
46
|
+
"require": "./dist/featherk-composables.umd.js"
|
|
47
|
+
},
|
|
48
|
+
"./registry": {
|
|
49
|
+
"types": "./dist/registry/index.d.ts",
|
|
50
|
+
"import": "./dist/featherk-composables.es.js",
|
|
51
|
+
"require": "./dist/featherk-composables.umd.js"
|
|
52
|
+
},
|
|
43
53
|
"./form": {
|
|
44
54
|
"types": "./dist/form/index.d.ts",
|
|
45
55
|
"import": "./dist/featherk-composables.es.js",
|
|
@@ -49,6 +59,11 @@
|
|
|
49
59
|
"types": "./dist/address/index.d.ts",
|
|
50
60
|
"import": "./dist/featherk-composables.es.js",
|
|
51
61
|
"require": "./dist/featherk-composables.umd.js"
|
|
62
|
+
},
|
|
63
|
+
"./id": {
|
|
64
|
+
"types": "./dist/id/index.d.ts",
|
|
65
|
+
"import": "./dist/featherk-composables.es.js",
|
|
66
|
+
"require": "./dist/featherk-composables.umd.js"
|
|
52
67
|
}
|
|
53
68
|
},
|
|
54
69
|
"files": [
|