@featherk/composables 0.13.3 → 0.13.4
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 +2 -2
- package/dist/docs-manifest.js +23 -23
- package/dist/featherk-composables.es.js +1240 -1222
- 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 → computeCollisionOffset.d.ts} +14 -10
- package/dist/geometry/index.d.ts +1 -1
- package/dist/menu/usePopupMenu.d.ts +11 -10
- package/docs/menu/usePopupMenu.md +24 -18
- package/package.json +1 -1
- /package/dist/geometry/{computeFlippedOffset.test.d.ts → computeCollisionOffset.test.d.ts} +0 -0
|
@@ -10,7 +10,12 @@ export type GeometryBounds = {
|
|
|
10
10
|
right: number;
|
|
11
11
|
bottom: number;
|
|
12
12
|
};
|
|
13
|
-
export type
|
|
13
|
+
export type GeometryCollisionType = "fit" | "flip";
|
|
14
|
+
export type GeometryCollision = {
|
|
15
|
+
horizontal: GeometryCollisionType;
|
|
16
|
+
vertical: GeometryCollisionType;
|
|
17
|
+
};
|
|
18
|
+
export type ComputeCollisionOffsetInput = {
|
|
14
19
|
/** The anchor rect (or a zero-size point rect for a click/keyboard activation). */
|
|
15
20
|
anchor: DOMRect;
|
|
16
21
|
/** The popup's own measured size, or `null` before it has been measured. */
|
|
@@ -21,20 +26,19 @@ export type ComputeFlippedOffsetInput = {
|
|
|
21
26
|
popupAlign: GeometryAlign;
|
|
22
27
|
/** Clipping bounds checked for overflow; same coordinate space as `anchor`. */
|
|
23
28
|
bounds: GeometryBounds;
|
|
29
|
+
/** Per-axis collision behavior applied when the preferred corner overflows. */
|
|
30
|
+
collision: GeometryCollision;
|
|
24
31
|
};
|
|
25
32
|
/**
|
|
26
33
|
* Computes a popup's top-left corner from an anchor rect and alignment pair,
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* "flip"` semantics, which only run when Kendo positions via a real `anchor`
|
|
31
|
-
* DOM ref - this reproduces them for offset-based positioning.
|
|
34
|
+
* then applies Kendo-style `fit` or `flip` collision behavior independently
|
|
35
|
+
* on each axis. Fit shifts the popup within bounds without changing sides;
|
|
36
|
+
* flip moves it to the anchor's opposite side only when that reduces overflow.
|
|
32
37
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* or `bounds` cannot be checked - there is nothing to compare against yet.
|
|
38
|
+
* Returns the preferred position when `popup` is `null`, since collision
|
|
39
|
+
* behavior cannot be calculated before the popup has been measured.
|
|
36
40
|
*/
|
|
37
|
-
export declare const
|
|
41
|
+
export declare const computeCollisionOffset: ({ anchor, popup, anchorAlign, popupAlign, bounds, collision, }: ComputeCollisionOffsetInput) => {
|
|
38
42
|
left: number;
|
|
39
43
|
top: number;
|
|
40
44
|
};
|
package/dist/geometry/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./computeCollisionOffset";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ComponentPublicInstance, type ComputedRef, type MaybeRefOrGetter, type Ref } from "vue";
|
|
2
|
-
import { type GeometryAlign } from "../geometry";
|
|
2
|
+
import { type GeometryAlign, type GeometryCollision } from "../geometry";
|
|
3
3
|
/** A template ref that may point to an element or a Vue/Kendo component. */
|
|
4
4
|
export type PopupMenuElementRef = Ref<HTMLElement | ComponentPublicInstance | null>;
|
|
5
5
|
export type PopupMenuTriggerMode = "button" | "row";
|
|
@@ -9,6 +9,8 @@ export type PopupMenuOffset = {
|
|
|
9
9
|
};
|
|
10
10
|
/** Mirrors Kendo Popup's `anchorAlign`/`popupAlign` point shape. */
|
|
11
11
|
export type PopupMenuAlign = GeometryAlign;
|
|
12
|
+
/** Per-axis viewport collision behavior for offset-positioned popups. */
|
|
13
|
+
export type PopupMenuCollision = GeometryCollision;
|
|
12
14
|
export type PopupMenuAnchorActivation = {
|
|
13
15
|
triggerType: "click" | "keyboard";
|
|
14
16
|
coordinates?: {
|
|
@@ -35,6 +37,8 @@ type PopupMenuAnchorBaseOptions = {
|
|
|
35
37
|
hideWhenAnchorClipped?: boolean;
|
|
36
38
|
/** Intersection ratio required to keep the anchor considered visible. */
|
|
37
39
|
intersectionThreshold?: number | number[];
|
|
40
|
+
/** Per-axis collision behavior. Defaults to `flip` on both axes. */
|
|
41
|
+
collision?: PopupMenuCollision;
|
|
38
42
|
/**
|
|
39
43
|
* Point on the anchor rect that `offset` is computed from. Mirrors Kendo
|
|
40
44
|
* Popup's `anchorAlign` semantics. Defaults to `{ vertical: "bottom",
|
|
@@ -49,9 +53,8 @@ type PopupMenuAnchorBaseOptions = {
|
|
|
49
53
|
* Point on the popup itself that is placed at the computed anchor point.
|
|
50
54
|
* Mirrors Kendo Popup's `popupAlign` semantics. Defaults to `{ vertical:
|
|
51
55
|
* "top", horizontal: "left" }`. Requires measuring the rendered popup
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* fall back to "left"/"top".
|
|
56
|
+
* border box; until it has a size (e.g. the first open frame), non-"left"/
|
|
57
|
+
* "top" alignments briefly fall back to "left"/"top".
|
|
55
58
|
*/
|
|
56
59
|
popupAlign?: PopupMenuAlign;
|
|
57
60
|
};
|
|
@@ -101,12 +104,10 @@ export type UsePopupMenuOptions = {
|
|
|
101
104
|
/** Ref to the popup menu element or component. Used for outside-click detection. */
|
|
102
105
|
menuRef: PopupMenuElementRef;
|
|
103
106
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* (e.g. a title), throwing off `popupAlign`'s corner calculation. Not read
|
|
109
|
-
* for outside-click detection; that always uses `menuRef`.
|
|
107
|
+
* Optional measurement source for custom popup markup. The source is
|
|
108
|
+
* promoted to its nearest Kendo `.k-popup` so alignment and collision use
|
|
109
|
+
* the complete border box. Defaults to `menuRef`; outside-click detection
|
|
110
|
+
* always uses `menuRef` directly.
|
|
110
111
|
*/
|
|
111
112
|
popupContentRef?: PopupMenuElementRef;
|
|
112
113
|
/** Ref to the trigger button element or component. Ignored by outside-click detection. */
|
|
@@ -315,7 +315,7 @@ usePopupTrap({
|
|
|
315
315
|
|
|
316
316
|
- `isOpen: Readonly<Ref<boolean>>`: Parent-owned popup visibility state observed, but never mutated, by the composable.
|
|
317
317
|
- `menuRef: PopupMenuElementRef`: Ref to the Kendo `Menu` element or component.
|
|
318
|
-
- `popupContentRef?: PopupMenuElementRef`:
|
|
318
|
+
- `popupContentRef?: PopupMenuElementRef`: Optional measurement source for custom popup markup. The composable measures its nearest Kendo `.k-popup` border box, falling back to the source element itself. Defaults to `menuRef`; see "Aligning the Popup to the Anchor" below.
|
|
319
319
|
- `triggerRef: PopupMenuElementRef`: Ref to the action trigger element or component.
|
|
320
320
|
- `requestShow(): void`: Opens the parent-owned popup state.
|
|
321
321
|
- `requestHide(): void`: Closes the parent-owned popup state.
|
|
@@ -326,7 +326,7 @@ usePopupTrap({
|
|
|
326
326
|
- `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.
|
|
327
327
|
- `menuLabel?: MaybeRefOrGetter<string | null | undefined>`: Optional accessible name applied as `aria-label` to the rendered `ul[role="menubar"]`. Empty labels remove the managed attribute.
|
|
328
328
|
- `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.
|
|
329
|
-
- `anchor?: true | PopupMenuAnchorOptions`: Enables document-relative offset tracking. `true` follows the trigger's bottom-left corner. `activationPoint` captures pointer/keyboard positions relative to the trigger and defaults its clipping threshold to `0`; `getRect(trigger)` remains the mutually exclusive custom-geometry escape hatch. Without `clipRoot`, the composable prefers the nearest overflow-enabled ancestor that can actually scroll, skipping styled wrappers with no overflow. Other anchors default to threshold `0.5`. A fully clipped trigger closes with `"anchor-hidden"` without restoring focus. `anchorAlign`/`popupAlign`
|
|
329
|
+
- `anchor?: true | PopupMenuAnchorOptions`: Enables document-relative offset tracking. `true` follows the trigger's bottom-left corner. `activationPoint` captures pointer/keyboard positions relative to the trigger and defaults its clipping threshold to `0`; `getRect(trigger)` remains the mutually exclusive custom-geometry escape hatch. Without `clipRoot`, the composable prefers the nearest overflow-enabled ancestor that can actually scroll, skipping styled wrappers with no overflow. Other anchors default to threshold `0.5`. A fully clipped trigger closes with `"anchor-hidden"` without restoring focus. `anchorAlign`/`popupAlign` configure the computed corner, while `collision` configures per-axis `fit` or `flip` behavior.
|
|
330
330
|
|
|
331
331
|
`PopupMenuElementRef` accepts a normal `HTMLElement` ref or a Vue/Kendo component ref that exposes `$el`.
|
|
332
332
|
|
|
@@ -355,6 +355,10 @@ export type PopupMenuAlign = {
|
|
|
355
355
|
horizontal: "left" | "center" | "right";
|
|
356
356
|
vertical: "top" | "center" | "bottom";
|
|
357
357
|
};
|
|
358
|
+
export type PopupMenuCollision = {
|
|
359
|
+
horizontal: "fit" | "flip";
|
|
360
|
+
vertical: "fit" | "flip";
|
|
361
|
+
};
|
|
358
362
|
export type KendoMenuSelectEvent = {
|
|
359
363
|
item?: { text?: string };
|
|
360
364
|
event?: { type?: string } | null;
|
|
@@ -366,17 +370,14 @@ export type KendoMenuSelectEvent = {
|
|
|
366
370
|
Kendo's `<Popup>` only runs its own `anchorAlign`/`popupAlign` alignment engine when it is positioned via a real `anchor` DOM ref. `usePopupMenu` always positions through `:offset` instead - because Kendo's `anchor` prop resolves a static template-ref name once at mount and cannot retarget a different element per open (see the shared-instance section above) - so passing `anchor-align`/`popup-align` directly to `<Popup>` is a silent no-op when using `usePopupMenu`. Configure alignment through `anchor.anchorAlign`/`anchor.popupAlign` instead; `usePopupMenu` computes the aligned `offset` itself.
|
|
367
371
|
|
|
368
372
|
- `anchorAlign?: PopupMenuAlign`: The point on the anchor rect that `offset` is computed from. Defaults to `{ horizontal: "left", vertical: "bottom" }`, matching Kendo's own default. Meaningless (a no-op) against an `activationPoint` anchor, since that anchor is already a single point with no width/height.
|
|
369
|
-
- `popupAlign?: PopupMenuAlign`: The point on the popup itself that lands at the computed anchor point. Defaults to `{ horizontal: "left", vertical: "top" }`. A `"center"`/`"right"`/`"bottom"` value requires measuring the rendered popup
|
|
370
|
-
- `popupContentRef?: PopupMenuElementRef`:
|
|
373
|
+
- `popupAlign?: PopupMenuAlign`: The point on the popup itself that lands at the computed anchor point. Defaults to `{ horizontal: "left", vertical: "top" }`. A `"center"`/`"right"`/`"bottom"` value requires measuring the rendered popup, so on the very first open there can be a one-frame snap from the top-left default to the final aligned position while that measurement completes.
|
|
374
|
+
- `popupContentRef?: PopupMenuElementRef`: Optional measurement source for custom popup markup. The composable promotes it (or `menuRef` by default) to the nearest Kendo `.k-popup`, ensuring titles, sibling content, and popup borders are included in alignment and collision calculations.
|
|
371
375
|
|
|
372
376
|
```ts
|
|
373
377
|
const buttonMenu = usePopupMenu({
|
|
374
378
|
isOpen: registry.isActive,
|
|
375
379
|
triggerRef: registry.activeElement,
|
|
376
380
|
menuRef: menuRef,
|
|
377
|
-
// Only needed because the template below renders a title above the Menu -
|
|
378
|
-
// without it, popupAlign would measure just the Menu and mis-align the popup.
|
|
379
|
-
popupContentRef: popupContentRef,
|
|
380
381
|
triggerMode: "button",
|
|
381
382
|
anchor: {
|
|
382
383
|
anchorAlign: { horizontal: "right", vertical: "bottom" },
|
|
@@ -389,24 +390,29 @@ const buttonMenu = usePopupMenu({
|
|
|
389
390
|
|
|
390
391
|
```vue
|
|
391
392
|
<Popup :show="isOpen" :offset="buttonMenu.offset.value">
|
|
392
|
-
|
|
393
|
-
<
|
|
394
|
-
<strong v-if="activeTitle">{{ activeTitle }}</strong>
|
|
395
|
-
<Menu ref="menuRef" :items="activeMenuItems" />
|
|
396
|
-
</div>
|
|
393
|
+
<strong v-if="activeTitle">{{ activeTitle }}</strong>
|
|
394
|
+
<Menu ref="menuRef" :items="activeMenuItems" />
|
|
397
395
|
</Popup>
|
|
398
396
|
```
|
|
399
397
|
|
|
400
|
-
##
|
|
398
|
+
## Handling Viewport Collision
|
|
401
399
|
|
|
402
|
-
Whenever `anchor` is configured, `usePopupMenu`
|
|
400
|
+
Whenever `anchor` is configured, `usePopupMenu` applies collision behavior if the preferred `anchorAlign`/`popupAlign` corner would render outside the viewport or configured `clipRoot`. Configure each axis independently with `anchor.collision`; both axes default to `"flip"`, preserving the original behavior.
|
|
403
401
|
|
|
404
|
-
-
|
|
402
|
+
- `"flip"` moves the popup to the opposite side of the anchor when that reduces overflow.
|
|
403
|
+
- `"fit"` shifts the popup just far enough to remain within bounds without changing anchor sides. If the popup is larger than the available space, that axis is pinned to the bounds' starting edge.
|
|
404
|
+
- Mixed strategies are supported, for example `{ horizontal: "fit", vertical: "flip" }`.
|
|
405
405
|
- A flip is only committed if it actually reduces the overflow on that axis; a popup wider or taller than the available space is left at its preferred position rather than flipped back and forth.
|
|
406
|
-
-
|
|
406
|
+
- Collision detection requires measuring the popup's real size, so `popupRect` is measured via `ResizeObserver` whenever `anchor` is configured.
|
|
407
407
|
- `"center"` alignment on either axis never flips - flipping center to center is a no-op by definition.
|
|
408
|
-
- Bounds default to the viewport (`window.innerWidth`/`innerHeight`); pass `anchor.clipRoot` to constrain
|
|
409
|
-
- The underlying math is a pure, stateless function - `
|
|
408
|
+
- Bounds default to the viewport (`window.innerWidth`/`innerHeight`); pass `anchor.clipRoot` to constrain collision handling to a scrollable ancestor instead (the same container used for `hideWhenAnchorClipped`).
|
|
409
|
+
- The underlying math is a pure, stateless function - `computeCollisionOffset` in `@featherk/composables/geometry` - reusable outside `usePopupMenu` for any anchor/popup positioning problem.
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
anchor: {
|
|
413
|
+
collision: { horizontal: "fit", vertical: "flip" },
|
|
414
|
+
}
|
|
415
|
+
```
|
|
410
416
|
|
|
411
417
|
## Behavior
|
|
412
418
|
|
package/package.json
CHANGED
|
File without changes
|