@featherk/composables 0.13.3 → 0.13.5
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 +8 -2
- package/dist/docs-manifest.d.ts +1 -0
- package/dist/docs-manifest.js +25 -23
- package/dist/featherk-composables.es.js +1457 -1322
- 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/index.d.ts +1 -0
- package/dist/menu/usePopupMenu.d.ts +11 -10
- package/dist/window/index.d.ts +1 -0
- package/dist/window/useWindow.d.ts +100 -0
- package/dist/window/useWindow.test.d.ts +1 -0
- package/docs/menu/usePopupMenu.md +24 -18
- package/docs/window/useWindow.md +107 -0
- package/package.json +6 -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";
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./id";
|
|
|
6
6
|
export * from "./menu";
|
|
7
7
|
export * from "./observer";
|
|
8
8
|
export * from "./registry";
|
|
9
|
+
export * from "./window";
|
|
9
10
|
export { useMaskedDateInput } from "./date";
|
|
10
11
|
export type { ChangePayload as DateChangePayload } from "./date";
|
|
11
12
|
export { useMaskedDateRangeInput } from "./range";
|
|
@@ -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. */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./useWindow";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type ComponentPublicInstance, type ComputedRef, type Ref } from "vue";
|
|
2
|
+
/** Kendo Window's stage values. Distinct from browser fullscreen APIs. */
|
|
3
|
+
export type WindowStage = "DEFAULT" | "MINIMIZED" | "FULLSCREEN";
|
|
4
|
+
export interface WindowMoveEventPayload {
|
|
5
|
+
drag: boolean;
|
|
6
|
+
end: boolean;
|
|
7
|
+
left: number;
|
|
8
|
+
top: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}
|
|
12
|
+
export type WindowResizeEventPayload = WindowMoveEventPayload;
|
|
13
|
+
export interface WindowSettings {
|
|
14
|
+
title: string;
|
|
15
|
+
draggable: boolean;
|
|
16
|
+
resizable: boolean;
|
|
17
|
+
/** Hidden via `windowClass`, not a real Kendo prop - see docs "Inert Props". */
|
|
18
|
+
closable: boolean;
|
|
19
|
+
fixed: boolean;
|
|
20
|
+
/** Hidden via `windowClass`, not a real Kendo prop - see docs "Inert Props". */
|
|
21
|
+
minimizeButton: boolean;
|
|
22
|
+
/** Hidden via `windowClass`, not a real Kendo prop - see docs "Inert Props". */
|
|
23
|
+
maximizeButton: boolean;
|
|
24
|
+
initialWidth: number;
|
|
25
|
+
initialHeight: number;
|
|
26
|
+
width?: number;
|
|
27
|
+
height?: number;
|
|
28
|
+
left?: number;
|
|
29
|
+
top?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare const WINDOW_SETTINGS_DEFAULTS: WindowSettings;
|
|
32
|
+
export interface UseWindowOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Overrides for one or more settings; any field you omit falls back to
|
|
35
|
+
* `WINDOW_SETTINGS_DEFAULTS`, merged internally - you never need to spread
|
|
36
|
+
* the defaults yourself. Pass a `Ref` (e.g. one already wired to your own
|
|
37
|
+
* persistence layer) to have `useWindow` read and mutate it directly
|
|
38
|
+
* instead of creating its own internal ref; missing fields on that ref are
|
|
39
|
+
* filled in from the defaults once, in place, at setup time.
|
|
40
|
+
*/
|
|
41
|
+
settings?: Partial<WindowSettings> | Ref<Partial<WindowSettings>>;
|
|
42
|
+
initialStage?: WindowStage;
|
|
43
|
+
onMove?: (payload: WindowMoveEventPayload) => void;
|
|
44
|
+
onResize?: (payload: WindowResizeEventPayload) => void;
|
|
45
|
+
onStageChange?: (stage: WindowStage) => void;
|
|
46
|
+
onOpen?: () => void;
|
|
47
|
+
onClose?: () => void;
|
|
48
|
+
}
|
|
49
|
+
/** The subset of Kendo `Window` props useWindow keeps functional - see docs "Inert Props". */
|
|
50
|
+
export interface WindowBindableProps {
|
|
51
|
+
title: string;
|
|
52
|
+
draggable: boolean;
|
|
53
|
+
resizable: boolean;
|
|
54
|
+
left?: number;
|
|
55
|
+
top?: number;
|
|
56
|
+
initialLeft: number;
|
|
57
|
+
initialTop: number;
|
|
58
|
+
initialWidth: number;
|
|
59
|
+
initialHeight: number;
|
|
60
|
+
width?: number;
|
|
61
|
+
height?: number;
|
|
62
|
+
stage: WindowStage;
|
|
63
|
+
windowClass: string;
|
|
64
|
+
}
|
|
65
|
+
export interface WindowEvents {
|
|
66
|
+
move: (...args: unknown[]) => void;
|
|
67
|
+
resize: (...args: unknown[]) => void;
|
|
68
|
+
stagechange: (...args: unknown[]) => void;
|
|
69
|
+
close: (...args: unknown[]) => void;
|
|
70
|
+
}
|
|
71
|
+
export interface UseWindowOpenOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Discards any existing left/top/width/height (including one restored from
|
|
74
|
+
* an externally-persisted `settings` ref) and recomputes the initial inset
|
|
75
|
+
* position and `initialWidth`/`initialHeight` size. Defaults to `false`, so
|
|
76
|
+
* a previously dragged/resized/persisted position survives close + reopen.
|
|
77
|
+
*/
|
|
78
|
+
resetPosition?: boolean;
|
|
79
|
+
}
|
|
80
|
+
export interface UseWindowReturn {
|
|
81
|
+
windowRef: Ref<ComponentPublicInstance | null>;
|
|
82
|
+
settings: Ref<WindowSettings>;
|
|
83
|
+
visible: Ref<boolean>;
|
|
84
|
+
stage: Ref<WindowStage>;
|
|
85
|
+
isDragging: Ref<boolean>;
|
|
86
|
+
isResizing: Ref<boolean>;
|
|
87
|
+
/** Spread via `v-bind` onto `<Window>`. */
|
|
88
|
+
props: ComputedRef<WindowBindableProps>;
|
|
89
|
+
/** Spread via `v-on` onto `<Window>`. */
|
|
90
|
+
events: WindowEvents;
|
|
91
|
+
open: (openOptions?: UseWindowOpenOptions) => Promise<void>;
|
|
92
|
+
close: () => void;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Wraps Kendo UI for Vue's `Window` component (not the browser `window`
|
|
96
|
+
* object) with zero-config defaults, viewport-clamped dragging, and the
|
|
97
|
+
* pointerdown-capture resize/text-selection workaround from
|
|
98
|
+
* docs/design/composable-native-event-listener-tactic.md.
|
|
99
|
+
*/
|
|
100
|
+
export declare function useWindow(options?: UseWindowOptions): UseWindowReturn;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# useWindow
|
|
2
|
+
|
|
3
|
+
[Back to Composables README](https://github.com/NantHealth/featherk/blob/integration/packages/composables/README.md)
|
|
4
|
+
|
|
5
|
+
> Wraps Kendo UI for Vue's `Window` component - not the browser `window` object. If you were expecting something like VueUse's `useWindowSize`/`useWindowScroll`, this is unrelated to those.
|
|
6
|
+
|
|
7
|
+
Composable for a zero/low-config Kendo `Window`: viewport-clamped dragging, resize tracking, stage transitions, and the pointerdown-capture resize/text-selection workaround are all handled internally. Every setting has a sensible default, so `useWindow()` with no arguments is a complete, working Window.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
1. Import `useWindow` and Kendo's `Window` component.
|
|
12
|
+
2. Call `useWindow()` - accept every default setting.
|
|
13
|
+
3. Wire a trigger (e.g. a Button) to `open()`.
|
|
14
|
+
4. Bind `<Window>` via `v-if`, `ref`, `v-bind`, and `v-on` - never wire individual Window props by hand.
|
|
15
|
+
|
|
16
|
+
```vue
|
|
17
|
+
<template>
|
|
18
|
+
<!-- Step 3: trigger opens the Window using its defaults -->
|
|
19
|
+
<Button @click="open">Open Window</Button>
|
|
20
|
+
|
|
21
|
+
<!-- Step 4: v-if/ref/v-bind/v-on is the entire binding surface -->
|
|
22
|
+
<Window v-if="visible" ref="windowRef" v-bind="props" v-on="events">
|
|
23
|
+
<p>This is the content of the window.</p>
|
|
24
|
+
</Window>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
// Step 1: import the composable and the Kendo component it augments
|
|
29
|
+
import { useWindow } from "@featherk/composables/window";
|
|
30
|
+
import { Window } from "@progress/kendo-vue-dialogs";
|
|
31
|
+
import { Button } from "@progress/kendo-vue-buttons";
|
|
32
|
+
|
|
33
|
+
// Step 2: no options - every setting uses useWindow's built-in defaults
|
|
34
|
+
const { windowRef, visible, props, events, open } = useWindow();
|
|
35
|
+
</script>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Overriding Defaults
|
|
39
|
+
|
|
40
|
+
Pass `settings` with only the fields you want to change; `useWindow` merges it over `WINDOW_SETTINGS_DEFAULTS` internally, so you never spread the defaults yourself.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const { windowRef, visible, props, events, open } = useWindow({
|
|
44
|
+
settings: {
|
|
45
|
+
title: "Preferences",
|
|
46
|
+
initialWidth: 600,
|
|
47
|
+
initialHeight: 400,
|
|
48
|
+
resizable: false,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Persisting Settings Externally
|
|
54
|
+
|
|
55
|
+
Pass an existing `Ref` (for example, one already wired to your own persistence layer) as `settings` instead of a plain object. `useWindow` fills in any missing fields from `WINDOW_SETTINGS_DEFAULTS` once, in place, then reads and mutates that same ref directly (position/size updates from dragging and resizing land on it) - persistence stays entirely your concern, `useWindow` has no knowledge of how or whether you persist it.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const persistedSettings = ref<Partial<WindowSettings>>({ ...myLoadedSettings });
|
|
59
|
+
const win = useWindow({ settings: persistedSettings });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A partially-populated ref is fine - any field `myLoadedSettings` doesn't have falls back to the built-in default, exactly like the plain-object form above.
|
|
63
|
+
|
|
64
|
+
`open()` respects this: by default it reopens at whatever `left`/`top`/`width`/`height` are already on `settings` (a prior drag/resize, or a value you restored from persistence), rather than recomputing the initial inset position. Pass `open({ resetPosition: true })` to force the Window back to its computed initial position/size instead - see `UseWindowReturn.open` in the API table below.
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
68
|
+
### `UseWindowOptions`
|
|
69
|
+
|
|
70
|
+
| Option | Type | Description |
|
|
71
|
+
| --------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
72
|
+
| `settings` | `Partial<WindowSettings> \| Ref<Partial<WindowSettings>>` | Overrides merged over `WINDOW_SETTINGS_DEFAULTS` internally. Pass a `Ref` for externally-owned/persisted settings. |
|
|
73
|
+
| `initialStage` | `WindowStage` | Initial `stage` value. Defaults to `"DEFAULT"`. |
|
|
74
|
+
| `onMove` | `(payload) => void` | Called after internal move handling. |
|
|
75
|
+
| `onResize` | `(payload) => void` | Called after internal resize handling. |
|
|
76
|
+
| `onStageChange` | `(stage) => void` | Called after a valid stage transition. |
|
|
77
|
+
| `onOpen` | `() => void` | Called after `open()` finishes sizing the Window. |
|
|
78
|
+
| `onClose` | `() => void` | Called after `close()` hides the Window. |
|
|
79
|
+
|
|
80
|
+
### `UseWindowReturn`
|
|
81
|
+
|
|
82
|
+
| Member | Type | Description |
|
|
83
|
+
| --------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `windowRef` | `Ref<ComponentPublicInstance \| null>` | Bind via `ref="windowRef"`. |
|
|
85
|
+
| `settings` | `Ref<WindowSettings>` | Reactive settings - bind checkboxes/inputs directly for a settings panel. |
|
|
86
|
+
| `visible` | `Ref<boolean>` | Bind via `v-if="visible"`. |
|
|
87
|
+
| `stage` | `Ref<WindowStage>` | Current `"DEFAULT" \| "MINIMIZED" \| "FULLSCREEN"` stage. |
|
|
88
|
+
| `isDragging` / `isResizing` | `Ref<boolean>` | Live drag/resize state, already folded into `windowClass`. |
|
|
89
|
+
| `props` | `ComputedRef<WindowBindableProps>` | Spread via `v-bind="props"` onto `<Window>`. |
|
|
90
|
+
| `events` | `WindowEvents` | Spread via `v-on="events"` onto `<Window>`. |
|
|
91
|
+
| `open` | `(openOptions?: { resetPosition?: boolean }) => Promise<void>` | Shows the Window. By default, reopens at the current `settings` position/size; pass `{ resetPosition: true }` to recompute the initial inset position/size instead. |
|
|
92
|
+
| `close` | `() => void` | Resets `stage` to `"DEFAULT"` and hides the Window. |
|
|
93
|
+
|
|
94
|
+
## Inert Props
|
|
95
|
+
|
|
96
|
+
`props` only contains Kendo `Window` props that actually work: `title`, `draggable`, `resizable`, `left`, `top`, `initialLeft`, `initialTop`, `initialWidth`, `initialHeight`, `width`, `height`, `stage`, and `windowClass`. `settings.closable`, `settings.minimizeButton`, and `settings.maximizeButton` are deliberately **not** included in `props`.
|
|
97
|
+
|
|
98
|
+
If you hand-wire raw Kendo props like `closeButton`/`minimizeButton`/`maximizeButton` directly on `<Window>` alongside `v-bind="props"`, they are **inert** - they will not hide the corresponding titlebar button. `useWindow` only ever suppresses those buttons via CSS (`fk-window-hide-minimize`/`fk-window-hide-maximize`/`fk-window-hide-close` classes folded into `windowClass`), never through Kendo's own button props. Toggle `settings.minimizeButton`/`settings.maximizeButton`/`settings.closable` instead.
|
|
99
|
+
|
|
100
|
+
## Kendo Defects Worked Around
|
|
101
|
+
|
|
102
|
+
- **Titlebar button props are non-functional.** Kendo's `Window` types `minimizeButton`/`maximizeButton`/`closeButton`/`restoreButton` as `[String, Function]`, but the child `WindowTitlebar` that actually renders them types the same props `[String, Function, Object, Boolean]` and renders each button unconditionally regardless of value. There is no supported way to hide these buttons through Kendo's own props - see "Inert Props" above for the CSS-class workaround `useWindow` uses instead.
|
|
103
|
+
- **Resize-handle drag vs. text-selection race.** Kendo Window's resize handles use Kendo's own draggable implementation, which does not prevent the native browser default until after pointer movement begins. If text inside the Window is already selected, native selection-dragging can compete with a resize started from an inward-overlapping handle. `useWindow` prevents this internally with a capture-phase `pointerdown` listener scoped to `.k-resize-handle` elements - see [docs/design/composable-native-event-listener-tactic.md](https://github.com/NantHealth/featherk/blob/integration/docs/design/composable-native-event-listener-tactic.md) for the general pattern. This is fully internal; no `@pointerdown.capture` binding is needed (or exposed) in consumer templates.
|
|
104
|
+
|
|
105
|
+
## Styling
|
|
106
|
+
|
|
107
|
+
All `windowClass` output (`fk-window`, `fk-window-can-drag`, `fk-window-is-dragging`, `fk-window-is-resizing`, `fk-window-fixed`, `fk-window-hide-minimize`, `fk-window-hide-maximize`, `fk-window-hide-close`) is styled in `@featherk/styles` - consumers do not need to write any Window CSS themselves. The titlebar button-hiding workaround, the Kendo defect fixes, and drag/resize motion are all shipped as part of the external stylesheet; header background, text color, icon color, and padding are configured through ThemeBuilder's Window component panel. Just make sure the app includes `@featherk/styles`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@featherk/composables",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.5",
|
|
4
4
|
"main": "dist/featherk-composables.umd.js",
|
|
5
5
|
"module": "dist/featherk-composables.es.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -70,6 +70,11 @@
|
|
|
70
70
|
"import": "./dist/featherk-composables.es.js",
|
|
71
71
|
"require": "./dist/featherk-composables.umd.js"
|
|
72
72
|
},
|
|
73
|
+
"./window": {
|
|
74
|
+
"types": "./dist/window/index.d.ts",
|
|
75
|
+
"import": "./dist/featherk-composables.es.js",
|
|
76
|
+
"require": "./dist/featherk-composables.umd.js"
|
|
77
|
+
},
|
|
73
78
|
"./docs-manifest": {
|
|
74
79
|
"types": "./dist/docs-manifest.d.ts",
|
|
75
80
|
"import": "./dist/docs-manifest.js",
|
|
File without changes
|