@featherk/composables 0.11.2 → 0.12.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 +61 -29
- package/dist/featherk-composables.es.js +1193 -1098
- 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 +2 -0
- package/dist/menu/usePopupMenu.d.ts +57 -8
- 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/docs/date/useMaskedDateInput.md +9 -3
- package/docs/id/useCompositeId.md +29 -0
- package/docs/menu/usePopupMenu.md +12 -7
- package/docs/observer/useIntersectionObserver.md +95 -0
- package/package.json +11 -1
|
@@ -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 =
|
|
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: (
|
|
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
|
|
25
|
+
import { useMaskedDateInput } from "@featherk/composables/date";
|
|
26
|
+
import { usePopupTrap } from "@featherk/composables/trap";
|
|
26
27
|
|
|
27
|
-
const selectedDate = ref<Date |
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.12.1",
|
|
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": [
|