bits-ui 1.3.13 → 1.3.14

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.
@@ -4,11 +4,13 @@
4
4
  import { useMenubarMenu } from "../menubar.svelte.js";
5
5
  import Menu from "../../menu/components/menu.svelte";
6
6
  import { useId } from "../../../internal/use-id.js";
7
+ import { noop } from "../../../internal/noop.js";
7
8
 
8
- let { value = useId(), ...restProps }: MenubarMenuProps = $props();
9
+ let { value = useId(), onOpenChange = noop, ...restProps }: MenubarMenuProps = $props();
9
10
 
10
11
  const menuState = useMenubarMenu({
11
12
  value: box.with(() => value),
13
+ onOpenChange: box.with(() => onOpenChange),
12
14
  });
13
15
  </script>
14
16
 
@@ -3,7 +3,7 @@ import type { InteractOutsideBehaviorType } from "../utilities/dismissible-layer
3
3
  import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
4
4
  import { type UseRovingFocusReturn } from "../../internal/use-roving-focus.svelte.js";
5
5
  import type { Direction } from "../../shared/index.js";
6
- import type { BitsFocusEvent, BitsKeyboardEvent, BitsPointerEvent, WithRefProps } from "../../internal/types.js";
6
+ import type { BitsFocusEvent, BitsKeyboardEvent, BitsPointerEvent, OnChangeFn, WithRefProps } from "../../internal/types.js";
7
7
  import { type FocusScopeContextValue } from "../utilities/focus-scope/use-focus-scope.svelte.js";
8
8
  type MenubarRootStateProps = WithRefProps<ReadableBoxedValues<{
9
9
  dir: Direction;
@@ -16,7 +16,7 @@ declare class MenubarRootState {
16
16
  rovingFocusGroup: UseRovingFocusReturn;
17
17
  wasOpenedByKeyboard: boolean;
18
18
  triggerIds: string[];
19
- valueToContentId: Map<string, ReadableBox<string>>;
19
+ valueToChangeHandler: Map<string, ReadableBox<OnChangeFn<boolean>>>;
20
20
  constructor(opts: MenubarRootStateProps);
21
21
  /**
22
22
  * @param id - the id of the trigger to register
@@ -28,7 +28,8 @@ declare class MenubarRootState {
28
28
  * @param contentId - the content id to associate with the value
29
29
  * @returns - a function to de-register the menu
30
30
  */
31
- registerMenu(value: string, contentId: ReadableBox<string>): () => void;
31
+ registerMenu(value: string, onOpenChange: ReadableBox<OnChangeFn<boolean>>): () => void;
32
+ updateValue(value: string): void;
32
33
  getTriggers(): HTMLButtonElement[];
33
34
  onMenuOpen(id: string, triggerId: string): void;
34
35
  onMenuClose(): void;
@@ -41,6 +42,7 @@ declare class MenubarRootState {
41
42
  }
42
43
  type MenubarMenuStateProps = ReadableBoxedValues<{
43
44
  value: string;
45
+ onOpenChange: OnChangeFn<boolean>;
44
46
  }>;
45
47
  declare class MenubarMenuState {
46
48
  readonly opts: MenubarMenuStateProps;
@@ -13,7 +13,7 @@ class MenubarRootState {
13
13
  rovingFocusGroup;
14
14
  wasOpenedByKeyboard = $state(false);
15
15
  triggerIds = $state([]);
16
- valueToContentId = new Map();
16
+ valueToChangeHandler = new Map();
17
17
  constructor(opts) {
18
18
  this.opts = opts;
19
19
  useRefById(opts);
@@ -43,12 +43,24 @@ class MenubarRootState {
43
43
  * @param contentId - the content id to associate with the value
44
44
  * @returns - a function to de-register the menu
45
45
  */
46
- registerMenu(value, contentId) {
47
- this.valueToContentId.set(value, contentId);
46
+ registerMenu(value, onOpenChange) {
47
+ this.valueToChangeHandler.set(value, onOpenChange);
48
48
  return () => {
49
- this.valueToContentId.delete(value);
49
+ this.valueToChangeHandler.delete(value);
50
50
  };
51
51
  }
52
+ updateValue(value) {
53
+ const currValue = this.opts.value.current;
54
+ const currHandler = this.valueToChangeHandler.get(currValue)?.current;
55
+ const nextHandler = this.valueToChangeHandler.get(value)?.current;
56
+ this.opts.value.current = value;
57
+ if (currHandler && currValue !== value) {
58
+ currHandler(false);
59
+ }
60
+ if (nextHandler) {
61
+ nextHandler(true);
62
+ }
63
+ }
52
64
  getTriggers() {
53
65
  const node = this.opts.ref.current;
54
66
  if (!node)
@@ -56,14 +68,14 @@ class MenubarRootState {
56
68
  return Array.from(node.querySelectorAll(`[${MENUBAR_TRIGGER_ATTR}]`));
57
69
  }
58
70
  onMenuOpen(id, triggerId) {
59
- this.opts.value.current = id;
71
+ this.updateValue(id);
60
72
  this.rovingFocusGroup.setCurrentTabStopId(triggerId);
61
73
  }
62
74
  onMenuClose() {
63
- this.opts.value.current = "";
75
+ this.updateValue("");
64
76
  }
65
77
  onMenuToggle(id) {
66
- this.opts.value.current = this.opts.value.current ? "" : id;
78
+ this.updateValue(this.opts.value.current ? "" : id);
67
79
  }
68
80
  props = $derived.by(() => ({
69
81
  id: this.opts.id.current,
@@ -87,7 +99,7 @@ class MenubarMenuState {
87
99
  }
88
100
  });
89
101
  onMount(() => {
90
- return this.root.registerMenu(this.opts.value.current, box.with(() => this.contentNode?.id ?? ""));
102
+ return this.root.registerMenu(this.opts.value.current, opts.onOpenChange);
91
103
  });
92
104
  }
93
105
  getTriggerNode() {
@@ -29,6 +29,10 @@ export type MenubarMenuPropsWithoutHTML = WithChildren<{
29
29
  * within the menubar.
30
30
  */
31
31
  value?: string;
32
+ /**
33
+ * A callback that is called when the menu is opened or closed.
34
+ */
35
+ onOpenChange?: OnChangeFn<boolean>;
32
36
  }>;
33
37
  export type MenubarMenuProps = MenubarMenuPropsWithoutHTML;
34
38
  export type MenubarTriggerPropsWithoutHTML = WithChild<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "1.3.13",
3
+ "version": "1.3.14",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",
@@ -1,5 +0,0 @@
1
- import type { Polygon } from "../polygon.js";
2
- /**
3
- * Debugging utility to visualize the grace area of a floating layer component.
4
- */
5
- export declare function visualizeGraceArea(graceArea: Polygon): void;
@@ -1,28 +0,0 @@
1
- let graceAreaElement = null;
2
- let svgContainer = null;
3
- /**
4
- * Debugging utility to visualize the grace area of a floating layer component.
5
- */
6
- export function visualizeGraceArea(graceArea) {
7
- if (graceAreaElement) {
8
- graceAreaElement.remove();
9
- }
10
- if (!svgContainer) {
11
- svgContainer = document.createElementNS("http://www.w3.org/2000/svg", "svg");
12
- svgContainer.style.position = "absolute";
13
- svgContainer.style.top = "0";
14
- svgContainer.style.left = "0";
15
- svgContainer.style.width = "100%";
16
- svgContainer.style.height = "100%";
17
- svgContainer.style.pointerEvents = "none";
18
- document.body.appendChild(svgContainer);
19
- }
20
- graceAreaElement = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
21
- const pointsString = graceArea.map((p) => `${p.x},${p.y}`).join(" ");
22
- graceAreaElement.setAttribute("points", pointsString);
23
- graceAreaElement.setAttribute("fill", "rgba(255, 0, 0, 0.3)");
24
- graceAreaElement.setAttribute("stroke", "red");
25
- graceAreaElement.setAttribute("stroke-width", "1");
26
- graceAreaElement.style.pointerEvents = "none";
27
- svgContainer.appendChild(graceAreaElement);
28
- }
@@ -1,10 +0,0 @@
1
- export interface Point {
2
- x: number;
3
- y: number;
4
- }
5
- export type Polygon = Array<Point>;
6
- export declare function makeHullPresorted<P extends Point>(points: Readonly<Array<P>>): Array<P>;
7
- export declare function POINT_COMPARATOR(a: Point, b: Point): number;
8
- export declare function makeHullFromElements(els: Array<HTMLElement>): Array<Point>;
9
- export declare function pointInPolygon(point: Point, polygon: Polygon): boolean;
10
- export declare function isPointerInGraceArea(e: Pick<PointerEvent, "clientX" | "clientY">, area?: Polygon): boolean;
@@ -1,115 +0,0 @@
1
- /*
2
- * Convex hull algorithm - Library (TypeScript)
3
- *
4
- * Copyright (c) 2021 Project Nayuki
5
- * https://www.nayuki.io/page/convex-hull-algorithm
6
- *
7
- * This program is free software: you can redistribute it and/or modify
8
- * it under the terms of the GNU Lesser General Public License as published by
9
- * the Free Software Foundation, either version 3 of the License, or
10
- * (at your option) any later version.
11
- *
12
- * This program is distributed in the hope that it will be useful,
13
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- * GNU Lesser General Public License for more details.
16
- *
17
- * You should have received a copy of the GNU Lesser General Public License
18
- * along with this program (see COPYING.txt and COPYING.LESSER.txt).
19
- * If not, see <http://www.gnu.org/licenses/>.
20
- */
21
- // Returns a new array of points representing the convex hull of
22
- // the given set of points. The convex hull excludes collinear points.
23
- // This algorithm runs in O(n log n) time.
24
- function makeHull(points) {
25
- const newPoints = points.slice();
26
- newPoints.sort(POINT_COMPARATOR);
27
- return makeHullPresorted(newPoints);
28
- }
29
- // Returns the convex hull, assuming that each points[i] <= points[i + 1]. Runs in O(n) time.
30
- export function makeHullPresorted(points) {
31
- if (points.length <= 1)
32
- return points.slice();
33
- // Andrew's monotone chain algorithm. Positive y coordinates correspond to "up"
34
- // as per the mathematical convention, instead of "down" as per the computer
35
- // graphics convention. This doesn't affect the correctness of the result.
36
- const upperHull = [];
37
- for (let i = 0; i < points.length; i++) {
38
- const p = points[i];
39
- while (upperHull.length >= 2) {
40
- const q = upperHull[upperHull.length - 1];
41
- const r = upperHull[upperHull.length - 2];
42
- if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x))
43
- upperHull.pop();
44
- else
45
- break;
46
- }
47
- upperHull.push(p);
48
- }
49
- upperHull.pop();
50
- const lowerHull = [];
51
- for (let i = points.length - 1; i >= 0; i--) {
52
- const p = points[i];
53
- while (lowerHull.length >= 2) {
54
- const q = lowerHull[lowerHull.length - 1];
55
- const r = lowerHull[lowerHull.length - 2];
56
- if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x))
57
- lowerHull.pop();
58
- else
59
- break;
60
- }
61
- lowerHull.push(p);
62
- }
63
- lowerHull.pop();
64
- if (upperHull.length === 1 &&
65
- lowerHull.length === 1 &&
66
- upperHull[0].x === lowerHull[0].x &&
67
- upperHull[0].y === lowerHull[0].y)
68
- return upperHull;
69
- else
70
- return upperHull.concat(lowerHull);
71
- }
72
- export function POINT_COMPARATOR(a, b) {
73
- if (a.x < b.x)
74
- return -1;
75
- else if (a.x > b.x)
76
- return +1;
77
- else if (a.y < b.y)
78
- return -1;
79
- else if (a.y > b.y)
80
- return +1;
81
- else
82
- return 0;
83
- }
84
- function getPointsFromEl(el) {
85
- const rect = el.getBoundingClientRect();
86
- return [
87
- { x: rect.left, y: rect.top },
88
- { x: rect.right, y: rect.top },
89
- { x: rect.right, y: rect.bottom },
90
- { x: rect.left, y: rect.bottom },
91
- ];
92
- }
93
- export function makeHullFromElements(els) {
94
- const points = els.flatMap((el) => getPointsFromEl(el));
95
- return makeHull(points);
96
- }
97
- export function pointInPolygon(point, polygon) {
98
- let inside = false;
99
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
100
- const xi = polygon[i].x;
101
- const yi = polygon[i].y;
102
- const xj = polygon[j].x;
103
- const yj = polygon[j].y;
104
- const intersect = yi > point.y !== yj > point.y &&
105
- point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
106
- if (intersect)
107
- inside = !inside;
108
- }
109
- return inside;
110
- }
111
- export function isPointerInGraceArea(e, area) {
112
- if (!area)
113
- return false;
114
- return pointInPolygon({ x: e.clientX, y: e.clientY }, area);
115
- }