@featherk/composables 0.11.2 → 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,2 @@
1
+ export { useCompositeId } from "./useCompositeId";
2
+ export type { CompositeIdPart, UseCompositeIdReturn } from "./useCompositeId";
@@ -0,0 +1,17 @@
1
+ /** A stable segment used to compose a component-scoped DOM identifier. */
2
+ export type CompositeIdPart = string | number | null | undefined;
3
+ /** Values returned by {@link useCompositeId}. */
4
+ export type UseCompositeIdReturn = {
5
+ /** Unique Vue component-instance identifier, prefixed for FeatherK markup. */
6
+ instanceId: string;
7
+ /** Combines the component instance ID with stable logical identifier segments. */
8
+ compose: (...parts: CompositeIdPart[]) => string;
9
+ };
10
+ /**
11
+ * Creates component-scoped DOM identifiers from Vue's SSR-safe `useId()`.
12
+ *
13
+ * Use the resulting `instanceId` for a stable local base and `compose` for
14
+ * related trigger, menu, popup, and ARIA relationship IDs. This does not
15
+ * replace consumer-owned domain IDs used for shared state or registries.
16
+ */
17
+ export declare const useCompositeId: (prefix?: string) => UseCompositeIdReturn;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export * from "./grid";
2
2
  export * from "./address";
3
3
  export * from "./form";
4
+ export * from "./id";
4
5
  export * from "./menu";
6
+ export * from "./observer";
5
7
  export * from "./registry";
6
8
  export { useMaskedDateInput } from "./date";
7
9
  export type { ChangePayload as DateChangePayload } from "./date";
@@ -1,9 +1,36 @@
1
- import { type ComponentPublicInstance, type Ref } from "vue";
1
+ import { type ComponentPublicInstance, type ComputedRef, type MaybeRefOrGetter, type Ref } from "vue";
2
2
  /** A template ref that may point to an element or a Vue/Kendo component. */
3
3
  export type PopupMenuElementRef = Ref<HTMLElement | ComponentPublicInstance | null>;
4
4
  export type PopupMenuTriggerMode = "button" | "row";
5
+ export type PopupMenuOffset = {
6
+ left: number;
7
+ top: number;
8
+ };
9
+ /** Offset-positioning options for a Popup with a dynamically changing trigger. */
10
+ export type PopupMenuAnchorOptions = {
11
+ /** Scrollable/clipping container used for out-of-view dismissal. */
12
+ clipRoot?: MaybeRefOrGetter<Element | null>;
13
+ /** Closes the menu after its trigger leaves the clipping container. */
14
+ hideWhenAnchorClipped?: boolean;
15
+ /** Overrides the trigger rect for pointer-positioned menus. */
16
+ getRect?: () => DOMRect | null;
17
+ };
5
18
  /** Why the popup was closed, including the focus policy for mouse paths. */
6
- export type PopupMenuCloseReason = "keyboard" | "mouse-selection" | "outside-click" | "trigger-click" | null;
19
+ export type PopupMenuCloseReason =
20
+ /** Keyboard-driven menu selection (Enter/Space on a menu item); restores the configured focus target. */
21
+ "keyboard"
22
+ /** Escape key; restores the configured focus target, then `escapeFocusTarget`, then the trigger. */
23
+ | "escape"
24
+ /** Pointer menu selection; restores focus to the trigger. */
25
+ | "mouse-selection"
26
+ /** Pointer interaction outside the menu; leaves browser focus unchanged. */
27
+ | "outside-click"
28
+ /** Interaction with the open trigger; leaves browser focus unchanged. */
29
+ | "trigger-click"
30
+ /** Trigger left its clipping area; closes without scrolling it back into view. */
31
+ | "anchor-hidden"
32
+ /** No close has been requested yet. */
33
+ | null;
7
34
  export type PopupMenuCloseTrigger = Exclude<PopupMenuCloseReason, null>;
8
35
  /** Minimal event shape emitted by Kendo Menu `@select`. */
9
36
  export type KendoMenuSelectEvent = {
@@ -22,10 +49,10 @@ export type UsePopupMenuOptions = {
22
49
  menuRef: PopupMenuElementRef;
23
50
  /** Ref to the trigger button element or component. Ignored by outside-click detection. */
24
51
  triggerRef: PopupMenuElementRef;
25
- /** Called to open the popup menu. */
26
- requestShow: () => void;
27
- /** Called to close the popup menu. */
28
- requestHide: () => void;
52
+ /** Called to open the popup menu. Kendo passes its event args as a rest tuple. */
53
+ requestShow: (...args: unknown[]) => void;
54
+ /** Called to close the popup menu. Kendo passes its event args as a rest tuple. */
55
+ requestHide: (...args: unknown[]) => void;
29
56
  /**
30
57
  * Optional target to focus after a keyboard-driven close.
31
58
  * Supports ordinary elements and Vue/Kendo components exposing `$el`.
@@ -36,6 +63,24 @@ export type UsePopupMenuOptions = {
36
63
  * Takes precedence over `focusTargetRef`.
37
64
  */
38
65
  resolveFocusTarget?: () => HTMLElement | null;
66
+ /**
67
+ * Resolves a fallback focus target for Escape closes when no `focusTargetRef`/
68
+ * `resolveFocusTarget` is configured, e.g. the trigger's containing grid row so
69
+ * keyboard users land back in row navigation instead of on the trigger button.
70
+ * Only consulted for `triggerMode: "button"`; row triggers already restore to
71
+ * themselves. Takes effect only for Escape, not keyboard menu selection.
72
+ * Takes precedence over `escapeFocusContainerSelector`.
73
+ */
74
+ escapeFocusTarget?: () => HTMLElement | null;
75
+ /**
76
+ * CSS selector for the closest ancestor of the trigger (or, if the menu was
77
+ * never opened, of `document.activeElement`) to focus on Escape. Sugar over
78
+ * `escapeFocusTarget` for the common "collapse back into the row" case.
79
+ * Defaults to `".k-table-row"` for `triggerMode: "button"`; harmless to leave
80
+ * at its default for non-grid button triggers since a missing ancestor simply
81
+ * falls through to focusing the trigger. Ignored when `escapeFocusTarget` is set.
82
+ */
83
+ escapeFocusContainerSelector?: string;
39
84
  /**
40
85
  * CSS selector used to find the first focusable menu item after open.
41
86
  * Defaults to `".k-menu-item:not(.k-disabled)"` for Kendo Menu.
@@ -53,16 +98,18 @@ export type UsePopupMenuOptions = {
53
98
  * silently managed.
54
99
  */
55
100
  triggerMode: PopupMenuTriggerMode;
101
+ /** Enables offset positioning and lifecycle tracking for a dynamic trigger. */
102
+ anchor?: PopupMenuAnchorOptions | true;
56
103
  };
57
104
  /** Functions returned by {@link usePopupMenu} for the action menu. */
58
105
  export type UsePopupMenuReturn = {
59
106
  /** Opens the menu, or closes it when it is already open. */
60
- handleActionButtonClick: () => void;
107
+ handleActionButtonClick: (...args: unknown[]) => void;
61
108
  /**
62
109
  * Handles keyboard input on the action button. Enter and Space open or close
63
110
  * the menu. Escape closes an open menu or moves focus to the configured target.
64
111
  */
65
- handleActionButtonKeydown: (event: KeyboardEvent) => void;
112
+ handleActionButtonKeydown: (...args: unknown[]) => void;
66
113
  /** Closes the menu when Escape is pressed while using the menu. */
67
114
  handleActionMenuEscape: (event: KeyboardEvent) => void;
68
115
  /**
@@ -75,6 +122,8 @@ export type UsePopupMenuReturn = {
75
122
  * configured focus target; mouse menu selections return focus to the trigger.
76
123
  */
77
124
  handlePopupClose: () => void;
125
+ /** Bind to Kendo Popup's `offset` prop when `anchor` is enabled. */
126
+ offset: ComputedRef<PopupMenuOffset>;
78
127
  };
79
128
  /**
80
129
  * Composable for accessible Kendo Popup + Menu action-menu interactions.
@@ -0,0 +1 @@
1
+ export * from "./useIntersectionObserver";
@@ -0,0 +1,31 @@
1
+ import { type Ref } from "vue";
2
+ export type UseIntersectionObserverOptions = {
3
+ /** Element to observe; the observer reinitializes whenever this ref changes. */
4
+ target: Readonly<Ref<Element | null>>;
5
+ /**
6
+ * Resolves the scroll container that clips the target. Grid consumers must
7
+ * provide the actual scrolling container, such as `.k-grid-content`. The
8
+ * observer reinitializes whenever the resolved root element changes.
9
+ */
10
+ root: () => Element | null;
11
+ /**
12
+ * One or more intersection ratios from `0` through `1`. `0` observes entry
13
+ * and complete departure; `1` observes full visibility. Defaults to `1`.
14
+ */
15
+ threshold?: number | number[];
16
+ /** Called whenever the target's intersection state changes. */
17
+ onChange: (entry: IntersectionObserverEntry) => void;
18
+ };
19
+ export type UseIntersectionObserverReturn = {
20
+ /** Stops observing the current target. Observation resumes if the target ref changes. */
21
+ disconnect: () => void;
22
+ };
23
+ /**
24
+ * Observes a reactive element against a lazily resolved clipping root.
25
+ *
26
+ * By default, the observer uses a threshold of `1`, so consumers are notified
27
+ * when the target is no longer fully visible within its root. The observer
28
+ * reinitializes whenever the target or resolved root changes. If
29
+ * `IntersectionObserver` is unavailable (e.g. SSR), this is a no-op.
30
+ */
31
+ export declare const useIntersectionObserver: (options: UseIntersectionObserverOptions) => UseIntersectionObserverReturn;
@@ -0,0 +1 @@
1
+ export {};
@@ -22,9 +22,10 @@ import { ref } from "vue";
22
22
  import { DatePicker } from "@progress/kendo-vue-dateinputs";
23
23
  import { MaskedTextBox } from "@progress/kendo-vue-inputs";
24
24
  import { Error } from "@progress/kendo-vue-labels";
25
- import { useMaskedDateInput, usePopupTrap } from "@featherk/composables";
25
+ import { useMaskedDateInput } from "@featherk/composables/date";
26
+ import { usePopupTrap } from "@featherk/composables/trap";
26
27
 
27
- const selectedDate = ref<Date | null | undefined>();
28
+ const selectedDate = ref<Date | undefined>();
28
29
  const showPicker = ref(false);
29
30
  const pickerRoot = ref<HTMLElement | null>(null);
30
31
 
@@ -35,7 +36,7 @@ const masked = useMaskedDateInput({
35
36
  id: "startDate",
36
37
  onChange: ({ value }) => { selectedDate.value = value ?? undefined; },
37
38
  onShowCalendar: () => (showPicker.value = true),
38
- externalValue: selectedDate as any,
39
+ externalValue: selectedDate,
39
40
  min: MIN_DATE,
40
41
  max: MAX_DATE,
41
42
  required: true,
@@ -105,6 +106,11 @@ Note: Destructure only the values used as component props that would otherwise r
105
106
 
106
107
  `useMaskedDateInput` can also power a custom masked input inside Kendo `DatePicker` via the `dateInput="masked"` slot. See the full reference in [src/components/custom-date-picker/CustomDatePicker.vue](../src/components/custom-date-picker/CustomDatePicker.vue).
107
108
 
109
+ When binding to Kendo `DatePicker`, use `Ref<Date | undefined>` for the calendar
110
+ `v-model`. The composable's `ChangePayload` uses `Date | null` to represent an
111
+ incomplete or invalid mask, so normalize that payload at the callback boundary with
112
+ `value ?? undefined` before assigning it to the DatePicker model.
113
+
108
114
  ## API
109
115
 
110
116
  ### `useMaskedDateInput(options)`
@@ -0,0 +1,29 @@
1
+ # useCompositeId
2
+
3
+ [Back to Composables README](https://github.com/NantHealth/featherk/blob/integration/packages/composables/README.md)
4
+
5
+ Creates SSR-safe, component-scoped DOM IDs using Vue's `useId()`. Use it for related trigger, popup, menu, and ARIA relationship IDs within one rendered component instance.
6
+
7
+ ## Quick Start
8
+
9
+ 1. Call `useCompositeId()` from component setup.
10
+ 2. Use `instanceId` when one component-level DOM ID is needed.
11
+ 3. Use `compose(...)` for related IDs with stable logical segments.
12
+
13
+ ```ts
14
+ import { useCompositeId } from "@featherk/composables";
15
+
16
+ // Step 1: Vue creates a unique ID for this rendered component instance.
17
+ const ids = useCompositeId("action-cell");
18
+
19
+ // Step 2: use the component-level ID where a single DOM ID is needed.
20
+ const cellId = ids.instanceId;
21
+
22
+ // Step 3: compose related DOM and ARIA relationship IDs.
23
+ const triggerId = ids.compose("patient", "trigger");
24
+ const menuId = ids.compose("patient", "menu");
25
+ ```
26
+
27
+ The default prefix is `fkid`, producing IDs like `fkid-v-0-patient-menu`. Pass a custom prefix when a component-specific DOM prefix improves inspection or debugging.
28
+
29
+ `useCompositeId` is for component-scoped DOM identifiers. Keep deterministic row/trigger IDs used with `useActiveIdRegistry` consumer-owned, for example `patient:42`.
@@ -9,7 +9,7 @@ Composable for accessible Kendo UI for Vue `Popup` + `Menu` action menus. It man
9
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
10
  2. Create parent-owned refs for `isOpen`, `triggerRef`, `menuRef`, and an optional keyboard focus target.
11
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.
12
+ 4. Route Kendo Menu `@select` through `handleMenuSelect(event, onAction)` so your business action runs while the active menu context is still available, before the composable closes the popup.
13
13
  5. Bind trigger button `@click` and `@keydown` to the composable handlers.
14
14
  6. Bind Popup `@close` and Menu `@keydown.escape` so keyboard and mouse close paths restore focus correctly.
15
15
 
@@ -63,7 +63,7 @@ const menu = usePopupMenu({
63
63
  requestHide: () => (isOpen.value = false),
64
64
  });
65
65
 
66
- // Step 4: run close+modality handling first, then app-specific action logic
66
+ // Step 4: act while the active context is available; usePopupMenu closes afterward
67
67
  const onSelect = (event: KendoMenuSelectEvent) => {
68
68
  menu.handleMenuSelect(event, (selected) => {
69
69
  // Keep application-specific actions in the consuming component.
@@ -82,7 +82,7 @@ For a virtualized Kendo Grid, use `resolveFocusTarget` to locate the current row
82
82
  3. Create trigger and menu refs as component refs (`ComponentPublicInstance`) for Kendo wrappers.
83
83
  4. Call `usePopupMenu(...)` with an explicit `triggerMode: "button"` and map `requestShow`/`requestHide` to row-specific emits.
84
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.
85
+ 6. Route menu selection through `handleMenuSelect(event, onAction)` so row action logic can use its active state before the popup closes.
86
86
 
87
87
  ```ts
88
88
  <script setup lang="ts">
@@ -122,7 +122,7 @@ const menu = usePopupMenu({
122
122
  },
123
123
  });
124
124
 
125
- // Step 6: let usePopupMenu close first, then run row-specific business action
125
+ // Step 6: run row-specific business action before usePopupMenu closes
126
126
  const onSelect = (event: KendoMenuSelectEvent) => {
127
127
  menu.handleMenuSelect(event, (selected) => {
128
128
  // Row-specific action logic remains here.
@@ -174,7 +174,7 @@ A single `usePopupMenu` instance can manage an entire list of same-mode triggers
174
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
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
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.
177
+ 4. Enable `anchor: true` and bind the shared `Popup` to `:offset="menu.offset"`, 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. Use Kendo's `anchorAlign` and `popupAlign` props when the popup should align to a specific edge of its button. Use `anchor: { getRect: () => ... }` for cursor-positioned menus.
178
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
179
 
180
180
  ```ts
@@ -187,6 +187,7 @@ const rowMenu = usePopupMenu({
187
187
  triggerRef: rowRegistry.activeElement,
188
188
  menuRef: rowMenuRef,
189
189
  triggerMode: "row",
190
+ anchor: true,
190
191
  requestShow: () => {}, // state is set directly by the row click handler, not by the composable
191
192
  requestHide: () => rowRegistry.deactivate(),
192
193
  });
@@ -222,7 +223,7 @@ const toggleButtonMenu = (id: number) => {
222
223
  };
223
224
  ```
224
225
 
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
+ 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 composable-owned offset tracking. `useActiveIdRegistry` is now part of `@featherk/composables`, so shared-instance consumers can import it directly instead of keeping a demo-local copy.
226
227
 
227
228
  ## API
228
229
 
@@ -240,6 +241,7 @@ See [UsePopupMenu.vue](https://github.com/NantHealth/featherk/blob/integration/d
240
241
  - `menuItemSelector?: string`: Selector for the first enabled item. Defaults to `.k-menu-item:not(.k-disabled)`.
241
242
  - `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
243
  - `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.
244
+ - `anchor?: true | PopupMenuAnchorOptions`: Enables document-relative `offset` tracking for dynamically retargeted Popups. `true` uses the resolved trigger's rectangle and nearest scrollable ancestor. `PopupMenuAnchorOptions` accepts `clipRoot`, `hideWhenAnchorClipped`, and `getRect` for pointer-positioned menus. Internally, the anchor-hidden policy uses `useIntersectionObserver` with `threshold: 1`; a clipped trigger closes with `"anchor-hidden"` without restoring focus.
243
245
 
244
246
  `PopupMenuElementRef` accepts a normal `HTMLElement` ref or a Vue/Kendo component ref that exposes `$el`.
245
247
 
@@ -250,6 +252,7 @@ See [UsePopupMenu.vue](https://github.com/NantHealth/featherk/blob/integration/d
250
252
  - `handleActionMenuEscape(event)`: Closes an open menu with keyboard modality.
251
253
  - `handleMenuSelect(event, onAction?)`: Records the selection modality, requests close, then invokes optional business logic.
252
254
  - `handlePopupClose()`: Restores focus after Kendo Popup has closed.
255
+ - `offset`: A computed `{ left, top }` value for Kendo Popup's `:offset`; it is `{ left: 0, top: 0 }` while closed.
253
256
 
254
257
  #### Types
255
258
 
@@ -259,6 +262,7 @@ export type PopupMenuCloseReason =
259
262
  | "mouse-selection"
260
263
  | "outside-click"
261
264
  | "trigger-click"
265
+ | "anchor-hidden"
262
266
  | null;
263
267
  export type PopupMenuCloseTrigger = Exclude<PopupMenuCloseReason, null>;
264
268
  export type KendoMenuSelectEvent = {
@@ -276,9 +280,10 @@ export type KendoMenuSelectEvent = {
276
280
  - 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.
277
281
  - Opening focuses the first enabled Kendo Menu item after Vue renders it.
278
282
  - Outside clicks close the menu, while the trigger is ignored to prevent a close/reopen race.
279
- - Menu selection records keyboard or mouse modality from the selection event type.
283
+ - Menu selection runs the optional business callback while the active state is still available, then records modality and closes the popup.
280
284
  - `Escape` on the trigger or inside the menu uses the keyboard close path.
281
285
  - Keyboard-driven closes focus `resolveFocusTarget()` or `focusTargetRef`, then fall back to the trigger.
282
286
  - Mouse selection closes restore focus to the trigger.
283
287
  - Outside-click and trigger-click closes do not force focus restoration.
288
+ - An anchor-hidden close does not restore focus, preventing an off-screen grid row from being scrolled back into view.
284
289
  - Business-specific actions are never implemented by the composable.
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@featherk/composables",
3
- "version": "0.11.2",
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,11 @@
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
+ },
43
48
  "./registry": {
44
49
  "types": "./dist/registry/index.d.ts",
45
50
  "import": "./dist/featherk-composables.es.js",
@@ -54,6 +59,11 @@
54
59
  "types": "./dist/address/index.d.ts",
55
60
  "import": "./dist/featherk-composables.es.js",
56
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"
57
67
  }
58
68
  },
59
69
  "files": [