@zvk/ui 0.1.6 → 0.1.7
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/CHANGELOG.md +7 -0
- package/dist/components/context-menu/context-menu.d.ts +3 -1
- package/dist/components/context-menu/context-menu.js +57 -3
- package/dist/components/context-menu/index.d.ts +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/menubar/index.d.ts +1 -1
- package/dist/components/menubar/menubar.d.ts +1 -1
- package/dist/components/menubar/menubar.js +31 -3
- package/dist/internal/floating/auto-update.js +21 -9
- package/dist/styles.css +0 -1
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.7](https://github.com/brandon-schabel/zvk/compare/v0.1.6...v0.1.7) (2026-06-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* harden popup positioning ([efb8c8a](https://github.com/brandon-schabel/zvk/commit/efb8c8a98c2b759f4e84a60f5ae292b1e8900907))
|
|
9
|
+
|
|
3
10
|
## [0.1.6](https://github.com/brandon-schabel/zvk/compare/v0.1.5...v0.1.6) (2026-06-12)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -14,6 +14,8 @@ export interface ContextMenuContentProps extends React.HTMLAttributes<HTMLDivEle
|
|
|
14
14
|
side?: FloatingSide;
|
|
15
15
|
align?: FloatingAlign;
|
|
16
16
|
alignOffset?: number;
|
|
17
|
+
sideOffset?: number;
|
|
18
|
+
collisionPadding?: number;
|
|
17
19
|
ref?: React.Ref<HTMLDivElement>;
|
|
18
20
|
}
|
|
19
21
|
export interface ContextMenuItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
@@ -38,7 +40,7 @@ export interface ContextMenuRadioItemProps extends ContextMenuItemProps {
|
|
|
38
40
|
}
|
|
39
41
|
declare function ContextMenuRoot({ children, className, defaultOpen, onOpenChange, open: openProp, ref, ...props }: ContextMenuProps): React.JSX.Element;
|
|
40
42
|
declare function ContextMenuTrigger({ asChild, children, className, onContextMenu, onKeyDown, ref, ...props }: ContextMenuTriggerProps): React.JSX.Element;
|
|
41
|
-
declare function ContextMenuContent({ align, alignOffset
|
|
43
|
+
declare function ContextMenuContent({ align, alignOffset, children, className, collisionPadding, onKeyDown, ref, side, sideOffset, style, ...props }: ContextMenuContentProps): React.JSX.Element | null;
|
|
42
44
|
declare function ContextMenuItem({ asChild, children, className, disabled, onClick, onKeyDown, onSelect, ref, tone, type, ...props }: ContextMenuItemProps): React.JSX.Element;
|
|
43
45
|
declare function ContextMenuLabel({ className, ref, ...props }: ContextMenuLabelProps): React.JSX.Element;
|
|
44
46
|
declare function ContextMenuCheckboxItem({ asChild, checked, children, className, defaultChecked, disabled, onCheckedChange, onClick, onKeyDown, onSelect, ref, tone, type, ...props }: ContextMenuCheckboxItemProps): React.JSX.Element;
|
|
@@ -6,6 +6,7 @@ import { cn } from "../../utils/cn.js";
|
|
|
6
6
|
import { useControllableState } from "../../hooks/use-controllable-state.js";
|
|
7
7
|
import { createCollection } from "../../internal/collection/index.js";
|
|
8
8
|
import { DismissableLayer } from "../../internal/dismissable-layer/index.js";
|
|
9
|
+
import { clamp, computeFloatingPosition } from "../../internal/floating/index.js";
|
|
9
10
|
import { placementFromSideAlign, placementParts } from "../../internal/floating/placement-aliases.js";
|
|
10
11
|
import { Portal } from "../../internal/portal/index.js";
|
|
11
12
|
import { Slot } from "../../internal/slot/index.js";
|
|
@@ -114,20 +115,73 @@ function currentIndex(items) {
|
|
|
114
115
|
const enabled = items.filter((item) => item.data.disabled !== true && item.data.ref !== null);
|
|
115
116
|
return enabled.findIndex((item) => item.data.ref === document.activeElement);
|
|
116
117
|
}
|
|
117
|
-
function ContextMenuContent({ align, alignOffset
|
|
118
|
+
function ContextMenuContent({ align, alignOffset = 0, children, className, collisionPadding = 4, onKeyDown, ref, side, sideOffset = 0, style, ...props }) {
|
|
118
119
|
const { close, contentId, getItems, open, point } = useContextMenuContext("ContextMenu.Content");
|
|
119
120
|
const contentPlacement = placementFromSideAlign(side, align, "bottom-start");
|
|
120
|
-
const
|
|
121
|
+
const [contentStyle, setContentStyle] = React.useState({});
|
|
122
|
+
const [resolvedPlacement, setResolvedPlacement] = React.useState(contentPlacement);
|
|
123
|
+
const contentRef = React.useRef(null);
|
|
124
|
+
const updatePosition = React.useCallback(() => {
|
|
125
|
+
const node = contentRef.current;
|
|
126
|
+
if (node === null || typeof window === "undefined") {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const rect = node.getBoundingClientRect();
|
|
130
|
+
const computed = computeFloatingPosition({
|
|
131
|
+
reference: { x: point.x, y: point.y, width: 0, height: 0 },
|
|
132
|
+
floating: { x: 0, y: 0, width: rect.width, height: rect.height },
|
|
133
|
+
boundary: { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight },
|
|
134
|
+
placement: contentPlacement,
|
|
135
|
+
strategy: "fixed",
|
|
136
|
+
offset: sideOffset,
|
|
137
|
+
alignmentOffset: alignOffset,
|
|
138
|
+
collisionPadding,
|
|
139
|
+
flip: true,
|
|
140
|
+
shift: true
|
|
141
|
+
});
|
|
142
|
+
const minX = collisionPadding;
|
|
143
|
+
const minY = collisionPadding;
|
|
144
|
+
const maxX = Math.max(minX, window.innerWidth - collisionPadding - rect.width);
|
|
145
|
+
const maxY = Math.max(minY, window.innerHeight - collisionPadding - rect.height);
|
|
146
|
+
setContentStyle({
|
|
147
|
+
position: computed.strategy,
|
|
148
|
+
left: `${clamp(computed.x, minX, maxX)}px`,
|
|
149
|
+
top: `${clamp(computed.y, minY, maxY)}px`
|
|
150
|
+
});
|
|
151
|
+
setResolvedPlacement(computed.placement);
|
|
152
|
+
}, [alignOffset, collisionPadding, contentPlacement, point.x, point.y, sideOffset]);
|
|
153
|
+
const schedulePositionUpdate = React.useCallback(() => {
|
|
154
|
+
if (typeof queueMicrotask === "function") {
|
|
155
|
+
queueMicrotask(updatePosition);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
window.setTimeout(updatePosition, 0);
|
|
159
|
+
}, [updatePosition]);
|
|
121
160
|
React.useLayoutEffect(() => {
|
|
122
161
|
if (!open) {
|
|
123
162
|
return;
|
|
124
163
|
}
|
|
125
164
|
queueMicrotask(() => focusItem(getItems(), 0));
|
|
126
165
|
}, [getItems, open]);
|
|
166
|
+
React.useLayoutEffect(() => {
|
|
167
|
+
if (!open) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
updatePosition();
|
|
171
|
+
window.addEventListener("resize", updatePosition);
|
|
172
|
+
return () => {
|
|
173
|
+
window.removeEventListener("resize", updatePosition);
|
|
174
|
+
};
|
|
175
|
+
}, [open, updatePosition]);
|
|
127
176
|
if (!open) {
|
|
128
177
|
return null;
|
|
129
178
|
}
|
|
130
|
-
return (_jsx(Portal, { children: _jsx(DismissableLayer, { open: open, onDismiss: close, children: _jsx("div", { ...props, ref:
|
|
179
|
+
return (_jsx(Portal, { children: _jsx(DismissableLayer, { open: open, onDismiss: close, children: _jsx("div", { ...props, ref: (node) => {
|
|
180
|
+
setComposedRef(contentRef, ref, node);
|
|
181
|
+
if (node !== null) {
|
|
182
|
+
schedulePositionUpdate();
|
|
183
|
+
}
|
|
184
|
+
}, id: contentId, role: "menu", className: cn("zvk-ui-context-menu__content", className), "data-align": placementParts(resolvedPlacement).align, "data-side": placementParts(resolvedPlacement).side, style: { ...style, ...contentStyle }, onKeyDown: composeEventHandlers(onKeyDown, (event) => {
|
|
131
185
|
const items = getItems();
|
|
132
186
|
const index = currentIndex(items);
|
|
133
187
|
if (event.key === "ArrowDown") {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { ContextMenu } from "./context-menu.js";
|
|
2
|
-
export type { ContextMenuCheckboxItemProps, ContextMenuContentProps, ContextMenuItemProps, ContextMenuLabelProps, ContextMenuProps, ContextMenuSeparatorProps, ContextMenuTriggerProps } from "./context-menu.js";
|
|
2
|
+
export type { ContextMenuCheckboxItemProps, ContextMenuContentProps, ContextMenuItemProps, ContextMenuLabelProps, ContextMenuProps, ContextMenuRadioItemProps, ContextMenuSeparatorProps, ContextMenuTriggerProps } from "./context-menu.js";
|
|
@@ -41,9 +41,9 @@ export type { CommandDialogProps, CommandEmptyProps, CommandGroupProps, CommandI
|
|
|
41
41
|
export { Combobox } from "./combobox/index.js";
|
|
42
42
|
export type { ComboboxOption, ComboboxProps } from "./combobox/index.js";
|
|
43
43
|
export { ContextMenu } from "./context-menu/index.js";
|
|
44
|
-
export type { ContextMenuCheckboxItemProps, ContextMenuContentProps, ContextMenuItemProps, ContextMenuLabelProps, ContextMenuProps, ContextMenuSeparatorProps, ContextMenuTriggerProps } from "./context-menu/index.js";
|
|
44
|
+
export type { ContextMenuCheckboxItemProps, ContextMenuContentProps, ContextMenuItemProps, ContextMenuLabelProps, ContextMenuProps, ContextMenuRadioItemProps, ContextMenuSeparatorProps, ContextMenuTriggerProps } from "./context-menu/index.js";
|
|
45
45
|
export { Menubar } from "./menubar/index.js";
|
|
46
|
-
export type { MenubarContentProps, MenubarItemProps, MenubarLabelProps, MenubarMenuProps, MenubarProps, MenubarSeparatorProps, MenubarShortcutProps, MenubarTriggerProps } from "./menubar/index.js";
|
|
46
|
+
export type { MenubarCheckboxItemProps, MenubarContentProps, MenubarItemProps, MenubarLabelProps, MenubarMenuProps, MenubarProps, MenubarRadioItemProps, MenubarSeparatorProps, MenubarShortcutProps, MenubarTriggerProps } from "./menubar/index.js";
|
|
47
47
|
export { EmptyState } from "./empty-state/index.js";
|
|
48
48
|
export type { EmptyStateAlign, EmptyStateProps, EmptyStateSize } from "./empty-state/index.js";
|
|
49
49
|
export { ErrorBoundary, ErrorFallback } from "./error-boundary/index.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Menubar } from "./menubar.js";
|
|
2
|
-
export type { MenubarContentProps, MenubarItemProps, MenubarLabelProps, MenubarMenuProps, MenubarProps, MenubarSeparatorProps, MenubarShortcutProps, MenubarTriggerProps } from "./menubar.js";
|
|
2
|
+
export type { MenubarCheckboxItemProps, MenubarContentProps, MenubarItemProps, MenubarLabelProps, MenubarMenuProps, MenubarProps, MenubarRadioItemProps, MenubarSeparatorProps, MenubarShortcutProps, MenubarTriggerProps } from "./menubar.js";
|
|
@@ -46,7 +46,7 @@ export interface MenubarShortcutProps extends React.HTMLAttributes<HTMLSpanEleme
|
|
|
46
46
|
declare function MenubarRoot({ children, className, defaultValue, onKeyDown, onValueChange, ref, value, ...props }: MenubarProps): React.JSX.Element;
|
|
47
47
|
declare function MenubarMenu({ children, value }: MenubarMenuProps): React.JSX.Element;
|
|
48
48
|
declare function MenubarTrigger({ asChild, children, className, disabled, onClick, onKeyDown, ref, type, ...props }: MenubarTriggerProps): React.JSX.Element;
|
|
49
|
-
declare function MenubarContent({ align, alignOffset
|
|
49
|
+
declare function MenubarContent({ align, alignOffset, children, className, onKeyDown, ref, side, sideOffset, style, ...props }: MenubarContentProps): React.JSX.Element | null;
|
|
50
50
|
declare function MenubarItem({ asChild, children, className, disabled, onClick, onKeyDown, onSelect, ref, type, ...props }: MenubarItemProps): React.JSX.Element;
|
|
51
51
|
declare function MenubarCheckboxItem({ asChild, checked, children, className, defaultChecked, disabled, onCheckedChange, onClick, onKeyDown, onSelect, ref, type, ...props }: MenubarCheckboxItemProps): React.JSX.Element;
|
|
52
52
|
declare function MenubarRadioItem({ asChild, checked, children, className, disabled, onClick, onKeyDown, onSelect, ref, type, ...props }: MenubarRadioItemProps): React.JSX.Element;
|
|
@@ -6,6 +6,7 @@ import { cn } from "../../utils/cn.js";
|
|
|
6
6
|
import { useControllableState } from "../../hooks/use-controllable-state.js";
|
|
7
7
|
import { createCollection } from "../../internal/collection/index.js";
|
|
8
8
|
import { DismissableLayer } from "../../internal/dismissable-layer/index.js";
|
|
9
|
+
import { useFloatingPosition } from "../../internal/floating/index.js";
|
|
9
10
|
import { placementFromSideAlign, placementParts } from "../../internal/floating/placement-aliases.js";
|
|
10
11
|
import { Portal } from "../../internal/portal/index.js";
|
|
11
12
|
import { Slot } from "../../internal/slot/index.js";
|
|
@@ -55,6 +56,14 @@ function setComposedRef(internalRef, externalRef, node) {
|
|
|
55
56
|
externalRef.current = node;
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
function setForwardedRef(externalRef, node) {
|
|
60
|
+
if (typeof externalRef === "function") {
|
|
61
|
+
externalRef(node);
|
|
62
|
+
}
|
|
63
|
+
else if (externalRef !== undefined && externalRef !== null) {
|
|
64
|
+
externalRef.current = node;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
58
67
|
function isActivationKey(key) {
|
|
59
68
|
return key === "Enter" || key === " " || key === "Space" || key === "Spacebar";
|
|
60
69
|
}
|
|
@@ -172,15 +181,34 @@ function MenubarTrigger({ asChild = false, children, className, disabled, onClic
|
|
|
172
181
|
setComposedRef(triggerRef, ref, node);
|
|
173
182
|
}, id: menu.triggerId, type: type, role: "menuitem", disabled: disabled, "aria-controls": menu.contentId, "aria-expanded": menu.open ? "true" : "false", "aria-haspopup": "menu", className: cn("zvk-ui-menubar__trigger", className), "data-disabled": disabled ? "true" : undefined, "data-state": menu.open ? "open" : "closed", onClick: triggerProps.onClick, onKeyDown: triggerProps.onKeyDown, children: children }));
|
|
174
183
|
}
|
|
175
|
-
function MenubarContent({ align, alignOffset
|
|
184
|
+
function MenubarContent({ align, alignOffset = 0, children, className, onKeyDown, ref, side, sideOffset = 4, style, ...props }) {
|
|
176
185
|
const menubar = useMenubarContext("Menubar.Content");
|
|
177
186
|
const menu = useMenuContext("Menubar.Content");
|
|
178
187
|
const contentPlacement = placementFromSideAlign(side, align, "bottom-start");
|
|
179
|
-
const
|
|
188
|
+
const { floatingRef, floatingStyle, placement: resolvedPlacement, referenceRef } = useFloatingPosition({
|
|
189
|
+
open: menu.open,
|
|
190
|
+
placement: contentPlacement,
|
|
191
|
+
strategy: "fixed",
|
|
192
|
+
offset: sideOffset,
|
|
193
|
+
alignmentOffset: alignOffset,
|
|
194
|
+
collisionPadding: 4
|
|
195
|
+
});
|
|
196
|
+
React.useLayoutEffect(() => {
|
|
197
|
+
if (!menu.open) {
|
|
198
|
+
referenceRef(null);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const activeTrigger = menubar.getTriggers().find((trigger) => trigger.data.value === menu.value);
|
|
202
|
+
referenceRef(activeTrigger?.data.ref ?? null);
|
|
203
|
+
return () => referenceRef(null);
|
|
204
|
+
}, [menubar, menu.open, menu.value, referenceRef]);
|
|
180
205
|
if (!menu.open) {
|
|
181
206
|
return null;
|
|
182
207
|
}
|
|
183
|
-
return (_jsx(Portal, { children: _jsx(DismissableLayer, { open: menu.open, onDismiss: () => menubar.setOpenValue(undefined), children: _jsx("div", { ...props, ref:
|
|
208
|
+
return (_jsx(Portal, { children: _jsx(DismissableLayer, { open: menu.open, onDismiss: () => menubar.setOpenValue(undefined), children: _jsx("div", { ...props, ref: (node) => {
|
|
209
|
+
floatingRef(node);
|
|
210
|
+
setForwardedRef(ref, node);
|
|
211
|
+
}, id: menu.contentId, role: "menu", "aria-label": menu.label, "aria-labelledby": menu.triggerId, className: cn("zvk-ui-menubar__content", className), style: { ...style, ...floatingStyle }, "data-align": placementParts(resolvedPlacement).align, "data-side": placementParts(resolvedPlacement).side, onKeyDown: composeEventHandlers(onKeyDown, (event) => {
|
|
184
212
|
const items = menu.getItems();
|
|
185
213
|
const index = activeIndex(items);
|
|
186
214
|
if (event.key === "ArrowDown") {
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
function hasBrowserApis() {
|
|
2
2
|
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
3
3
|
}
|
|
4
|
+
function scheduleFrame(callback) {
|
|
5
|
+
if (typeof window.requestAnimationFrame === "function") {
|
|
6
|
+
const frame = window.requestAnimationFrame(callback);
|
|
7
|
+
return () => {
|
|
8
|
+
if (typeof window.cancelAnimationFrame === "function") {
|
|
9
|
+
window.cancelAnimationFrame(frame);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const timeout = window.setTimeout(callback, 0);
|
|
14
|
+
return () => {
|
|
15
|
+
window.clearTimeout(timeout);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
4
18
|
export function autoUpdateFloating(params) {
|
|
5
19
|
const { enabled, reference, floating, update } = params;
|
|
6
20
|
if (!hasBrowserApis() || !enabled || reference === null || floating === null) {
|
|
7
21
|
return () => { };
|
|
8
22
|
}
|
|
9
|
-
let
|
|
23
|
+
let cancelScheduledUpdate = null;
|
|
10
24
|
const scheduleUpdate = () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
frameRequest = window.requestAnimationFrame(() => {
|
|
15
|
-
frameRequest = null;
|
|
25
|
+
cancelScheduledUpdate?.();
|
|
26
|
+
cancelScheduledUpdate = scheduleFrame(() => {
|
|
27
|
+
cancelScheduledUpdate = null;
|
|
16
28
|
update();
|
|
17
29
|
});
|
|
18
30
|
};
|
|
@@ -40,9 +52,9 @@ export function autoUpdateFloating(params) {
|
|
|
40
52
|
for (const cleanup of cleanupFns) {
|
|
41
53
|
cleanup();
|
|
42
54
|
}
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
if (cancelScheduledUpdate !== null) {
|
|
56
|
+
cancelScheduledUpdate();
|
|
57
|
+
cancelScheduledUpdate = null;
|
|
46
58
|
}
|
|
47
59
|
};
|
|
48
60
|
}
|
package/dist/styles.css
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvk/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "A polished, zero-runtime-dependency React component library for ZvkUi applications.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -270,13 +270,14 @@
|
|
|
270
270
|
"test:ssr": "vitest run tests/ssr --environment node",
|
|
271
271
|
"test:exports": "vitest run tests/exports --environment node",
|
|
272
272
|
"test:accessibility": "vitest run tests/accessibility",
|
|
273
|
+
"test:browser": "playwright test --config playwright.config.ts",
|
|
273
274
|
"test:types": "tsd",
|
|
274
275
|
"docs:lint": "bun run scripts/lint-docs.mjs",
|
|
275
276
|
"verify:style-contract": "bun run scripts/verify-style-contract.mjs",
|
|
276
277
|
"validate:exports": "bun run scripts/validate-exports.mjs",
|
|
277
278
|
"tarball:inspect": "bun run scripts/check-tarball.mjs",
|
|
278
279
|
"pack:dry": "npm pack --dry-run",
|
|
279
|
-
"preflight": "bun run typecheck && bun run build && bun run test:unit && bun run test:ssr && bun run test:types && bun run test:exports && bun run test:accessibility && bun run docs:lint && bun run verify:style-contract && bun run validate:exports && bun run tarball:inspect && bun run pack:dry"
|
|
280
|
+
"preflight": "bun run typecheck && bun run build && bun run test:unit && bun run test:ssr && bun run test:types && bun run test:exports && bun run test:accessibility && bun run test:browser && bun run docs:lint && bun run verify:style-contract && bun run validate:exports && bun run tarball:inspect && bun run pack:dry"
|
|
280
281
|
},
|
|
281
282
|
"peerDependencies": {
|
|
282
283
|
"react": "^19.0.0",
|
|
@@ -287,6 +288,7 @@
|
|
|
287
288
|
"directory": "tests/types"
|
|
288
289
|
},
|
|
289
290
|
"devDependencies": {
|
|
291
|
+
"@playwright/test": "^1.60.0",
|
|
290
292
|
"@testing-library/jest-dom": "^6.9.1",
|
|
291
293
|
"@testing-library/react": "^16.3.2",
|
|
292
294
|
"@testing-library/user-event": "^14.6.1",
|