@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.
@@ -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.11.1",
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": [