@noya-app/noya-designsystem 0.1.52 → 0.1.53

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -89,7 +89,7 @@ export const Button = forwardRef(function Button(
89
89
  disabled ? "opacity-25" : "",
90
90
  !className?.includes("flex") ? "flex-none" : "",
91
91
  active
92
- ? "bg-primary text-white hover:bg-primary-light active:bg-primary"
92
+ ? "bg-primary-pastel text-primary hover:bg-primary-pastel-light min-w-[31px]"
93
93
  : variantStyles[variant],
94
94
  sizeStyles[
95
95
  variant === "thin"
@@ -63,13 +63,12 @@ export function ColorSwatchControl(props: ColorSwatchControlProps) {
63
63
  checked={value === option}
64
64
  />
65
65
  );
66
- return readOnly ? (
67
- item
68
- ) : (
66
+ return (
69
67
  <ToggleGroupPrimitive.Item
70
68
  asChild
71
69
  value={option as string}
72
70
  key={option as string}
71
+ disabled={readOnly}
73
72
  >
74
73
  {item}
75
74
  </ToggleGroupPrimitive.Item>
@@ -77,14 +76,13 @@ export function ColorSwatchControl(props: ColorSwatchControlProps) {
77
76
  })}
78
77
  </div>
79
78
  );
80
- return readOnly ? (
81
- content
82
- ) : (
79
+ return (
83
80
  <ToggleGroupPrimitive.Root
84
81
  type="single"
85
82
  value={value as string}
86
83
  onValueChange={onValueChange}
87
84
  className="w-full"
85
+ disabled={readOnly}
88
86
  >
89
87
  {content}
90
88
  </ToggleGroupPrimitive.Root>
@@ -2,13 +2,16 @@ import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
2
2
  import { memoGeneric } from "@noya-app/react-utils";
3
3
  import * as RadixContextMenu from "@radix-ui/react-context-menu";
4
4
  import React, { ReactNode, useCallback, useMemo } from "react";
5
+ import {
6
+ portalScopeProps,
7
+ usePortalScopeId,
8
+ } from "../contexts/PortalScopeContext";
5
9
  import {
6
10
  MenuItem,
7
11
  getKeyboardShortcutsForMenuItems,
8
12
  styles,
9
13
  } from "./internal/Menu";
10
14
  import { MenuViewport } from "./internal/MenuViewport";
11
-
12
15
  export interface MenuItemProps<T extends string> {
13
16
  value?: T;
14
17
  children: ReactNode;
@@ -60,6 +63,8 @@ function ContextMenuRoot<T extends string>({
60
63
  [items, onSelect, shouldBindKeyboardShortcuts]
61
64
  );
62
65
 
66
+ const portalScopeId = usePortalScopeId();
67
+
63
68
  useKeyboardShortcuts(keymap);
64
69
 
65
70
  // We call preventDefault both to:
@@ -85,7 +90,10 @@ function ContextMenuRoot<T extends string>({
85
90
  {children}
86
91
  </RadixContextMenu.Trigger>
87
92
  <RadixContextMenu.Portal>
88
- <RadixContextMenu.Content className={styles.contentStyle}>
93
+ <RadixContextMenu.Content
94
+ {...portalScopeProps(portalScopeId)}
95
+ className={styles.contentStyle}
96
+ >
89
97
  <MenuViewport
90
98
  items={items}
91
99
  Components={Components}
@@ -1,36 +1,11 @@
1
1
  import { DragHandleDots2Icon } from "@noya-app/noya-icons";
2
2
  import { memoGeneric } from "@noya-app/react-utils";
3
- import React, { forwardRef, useCallback, useState } from "react";
3
+ import React, { useCallback, useState } from "react";
4
4
  import { cssVars } from "../theme";
5
5
  import { cx } from "../utils/classNames";
6
6
  import { DropdownMenu } from "./DropdownMenu";
7
7
  import { MenuItem } from "./internal/Menu";
8
8
 
9
- const DotButton = forwardRef<
10
- HTMLDivElement,
11
- React.HTMLAttributes<HTMLDivElement>
12
- >(({ className, ...props }, ref) => (
13
- <div
14
- ref={ref}
15
- className={cx(
16
- `cursor-pointer rounded flex items-center justify-center py-[2px] hover:bg-input-background-light active:bg-active-background `,
17
- className
18
- )}
19
- {...props}
20
- />
21
- ));
22
-
23
- const Trigger = forwardRef<
24
- HTMLDivElement,
25
- React.HTMLAttributes<HTMLDivElement>
26
- >(({ className, ...props }, ref) => (
27
- <div
28
- ref={ref}
29
- className={cx(`pointer-events-none h-[15px] `, className)}
30
- {...props}
31
- />
32
- ));
33
-
34
9
  /**
35
10
  * A button that opens a menu when clicked, but also allows dragging the
36
11
  */
@@ -39,11 +14,17 @@ export const DraggableMenuButton = memoGeneric(function DraggableMenuButton<
39
14
  >({
40
15
  items,
41
16
  onSelect,
17
+ onClick,
42
18
  isVisible = true,
19
+ className,
20
+ style,
43
21
  }: {
44
22
  items?: MenuItem<T>[];
45
23
  onSelect?: (value: T) => void;
24
+ onClick?: () => void;
46
25
  isVisible?: boolean;
26
+ className?: string;
27
+ style?: React.CSSProperties;
47
28
  }) {
48
29
  const [open, setOpen] = useState(false);
49
30
  const [downPosition, setDownPosition] = useState<{
@@ -94,12 +75,18 @@ export const DraggableMenuButton = memoGeneric(function DraggableMenuButton<
94
75
  );
95
76
 
96
77
  return (
97
- <DotButton
78
+ <div
79
+ className={cx(
80
+ "cursor-pointer rounded flex items-center justify-center py-[2px] hover:bg-input-background-light active:bg-active-background ",
81
+ className
82
+ )}
83
+ style={style}
98
84
  onPointerDownCapture={handlePointerDownCapture}
99
85
  onPointerUp={handlePointerUp}
100
86
  onClick={(event) => {
101
87
  event.stopPropagation();
102
88
  event.preventDefault();
89
+ onClick?.();
103
90
  }}
104
91
  >
105
92
  {items && onSelect ? (
@@ -110,17 +97,17 @@ export const DraggableMenuButton = memoGeneric(function DraggableMenuButton<
110
97
  onSelect={onSelect}
111
98
  shouldBindKeyboardShortcuts={false}
112
99
  >
113
- <Trigger>
100
+ <div className="pointer-events-none h-[15px]">
114
101
  <DragHandleDots2Icon
115
102
  color={isVisible || open ? cssVars.colors.icon : "transparent"}
116
103
  />
117
- </Trigger>
104
+ </div>
118
105
  </DropdownMenu>
119
106
  ) : (
120
107
  <DragHandleDots2Icon
121
108
  color={isVisible || open ? cssVars.colors.icon : "transparent"}
122
109
  />
123
110
  )}
124
- </DotButton>
111
+ </div>
125
112
  );
126
113
  });
@@ -7,6 +7,10 @@ import React, {
7
7
  SyntheticEvent,
8
8
  useMemo,
9
9
  } from "react";
10
+ import {
11
+ portalScopeProps,
12
+ usePortalScopeId,
13
+ } from "../contexts/PortalScopeContext";
10
14
  import { MenuProps } from "./ContextMenu";
11
15
  import { getKeyboardShortcutsForMenuItems, styles } from "./internal/Menu";
12
16
  import { MenuViewport } from "./internal/MenuViewport";
@@ -57,6 +61,8 @@ const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
57
61
  contentStyle,
58
62
  } = props;
59
63
 
64
+ const portalScopeId = usePortalScopeId();
65
+
60
66
  const keymap = useMemo(
61
67
  () =>
62
68
  shouldBindKeyboardShortcuts === false
@@ -86,6 +92,7 @@ const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
86
92
  </RadixDropdownMenu.Trigger>
87
93
  <RadixDropdownMenu.Portal>
88
94
  <RadixDropdownMenu.Content
95
+ {...portalScopeProps(portalScopeId)}
89
96
  className={styles.contentStyle}
90
97
  side={side}
91
98
  sideOffset={sideOffset}
@@ -1,5 +1,9 @@
1
1
  import * as PopoverPrimitive from "@radix-ui/react-popover";
2
2
  import React, { ComponentProps } from "react";
3
+ import {
4
+ portalScopeProps,
5
+ usePortalScopeId,
6
+ } from "../contexts/PortalScopeContext";
3
7
  import { cx } from "../utils/classNames";
4
8
  import { IconButton } from "./IconButton";
5
9
 
@@ -56,11 +60,14 @@ export function Popover({
56
60
  onClickClose,
57
61
  className,
58
62
  }: Props) {
63
+ const portalScopeId = usePortalScopeId();
64
+
59
65
  return (
60
66
  <PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
61
67
  <PopoverPrimitive.Trigger asChild>{trigger}</PopoverPrimitive.Trigger>
62
68
  <PopoverPrimitive.Portal>
63
69
  <PopoverPrimitive.Content
70
+ {...portalScopeProps(portalScopeId)}
64
71
  className={cx(
65
72
  popoverStyle.base,
66
73
  variant === "large" && popoverStyle.large,
@@ -15,6 +15,10 @@ import React, {
15
15
  import { useLabel, useLabelPosition } from "../hooks/useLabel";
16
16
  import { cx } from "../utils/classNames";
17
17
 
18
+ import {
19
+ portalScopeProps,
20
+ usePortalScopeId,
21
+ } from "../contexts/PortalScopeContext";
18
22
  import { getInsetEndStyles } from "../utils/inputs";
19
23
  import { Button } from "./Button";
20
24
  import { renderIcon } from "./Icons";
@@ -148,6 +152,8 @@ export const SelectMenu = memoGeneric(function SelectMenu<
148
152
  contentStyle: contentStyleProp,
149
153
  ...props
150
154
  }: Props<T>) {
155
+ const portalScopeId = usePortalScopeId();
156
+
151
157
  const selectedItem = items.find(
152
158
  (item): item is SelectableMenuItem<T> =>
153
159
  isSelectableMenuItem(item) && item.value === value
@@ -213,6 +219,7 @@ export const SelectMenu = memoGeneric(function SelectMenu<
213
219
  />
214
220
  <Select.Portal>
215
221
  <Select.Content
222
+ {...portalScopeProps(portalScopeId)}
216
223
  className={styles.contentStyle}
217
224
  position="popper"
218
225
  sideOffset={6}
@@ -1,9 +1,12 @@
1
- import { MenuItem, ToolbarMenu } from "@noya-app/noya-designsystem";
1
+ import {
2
+ portalScopeProps,
3
+ usePortalScopeId,
4
+ } from "@noya-app/noya-designsystem";
2
5
  import { Rect } from "@noya-app/noya-geometry";
3
- import { memoGeneric, useSize } from "@noya-app/react-utils";
6
+ import { useSize } from "@noya-app/react-utils";
4
7
  import * as PopperPrimitive from "@radix-ui/react-popper";
5
8
  import type { MeasurableElement } from "@radix-ui/utils";
6
- import React, { CSSProperties, useMemo } from "react";
9
+ import React, { CSSProperties, memo, useMemo } from "react";
7
10
 
8
11
  // Create a Measurable from a Rect
9
12
  const createVirtualElement = (rect: Rect): MeasurableElement => ({
@@ -20,51 +23,54 @@ const createVirtualElement = (rect: Rect): MeasurableElement => ({
20
23
  }),
21
24
  });
22
25
 
23
- type SelectionToolbarProps<T extends string> = {
26
+ type SelectionToolbarContainerProps = {
24
27
  rect: Rect;
25
- menuItems: MenuItem<T>[];
26
- onSelectMenuItem: (item: T) => void;
28
+ children: React.ReactNode;
27
29
  };
28
30
 
29
- export const SelectionToolbar = memoGeneric(function SelectionToolbar<
30
- T extends string,
31
- >({ rect, menuItems, onSelectMenuItem }: SelectionToolbarProps<T>) {
32
- const containerRef = React.useRef<HTMLDivElement>(null);
33
- const size = useSize(containerRef, "width");
31
+ export const SelectionToolbarContainer = memo(
32
+ function SelectionToolbarContainer({
33
+ rect,
34
+ children,
35
+ }: SelectionToolbarContainerProps) {
36
+ const portalScopeId = usePortalScopeId();
37
+ const containerRef = React.useRef<HTMLDivElement>(null);
38
+ const size = useSize(containerRef, "width");
34
39
 
35
- // Create a virtual reference element from the rect
36
- const virtualRef = useMemo(
37
- () => ({
38
- current: createVirtualElement(rect),
39
- }),
40
- [rect]
41
- );
40
+ // Create a virtual reference element from the rect
41
+ const virtualRef = useMemo(
42
+ () => ({
43
+ current: createVirtualElement(rect),
44
+ }),
45
+ [rect]
46
+ );
42
47
 
43
- return (
44
- <PopperPrimitive.Root>
45
- <PopperPrimitive.Anchor virtualRef={virtualRef} />
46
- <PopperPrimitive.Content
47
- side="top"
48
- sideOffset={10}
49
- align="center"
50
- avoidCollisions
51
- collisionPadding={10}
52
- data-editor-overlay
53
- contentEditable={false}
54
- style={{
55
- zIndex: 1000,
56
- opacity: size ? 1 : 0,
57
- transition: "opacity 0.2s ease-in-out",
58
- ["--n-input-background" as keyof CSSProperties]: "transparent",
59
- }}
60
- >
61
- <div
62
- ref={containerRef}
63
- className="flex gap-1 bg-popover-background rounded shadow-popover p-1"
48
+ return (
49
+ <PopperPrimitive.Root>
50
+ <PopperPrimitive.Anchor virtualRef={virtualRef} />
51
+ <PopperPrimitive.Content
52
+ {...portalScopeProps(portalScopeId)}
53
+ side="top"
54
+ sideOffset={10}
55
+ align="center"
56
+ avoidCollisions
57
+ collisionPadding={10}
58
+ contentEditable={false}
59
+ style={{
60
+ zIndex: 1000,
61
+ opacity: size ? 1 : 0,
62
+ transition: "opacity 0.2s ease-in-out",
63
+ ["--n-input-background" as keyof CSSProperties]: "transparent",
64
+ }}
64
65
  >
65
- <ToolbarMenu items={menuItems} onSelectMenuItem={onSelectMenuItem} />
66
- </div>
67
- </PopperPrimitive.Content>
68
- </PopperPrimitive.Root>
69
- );
70
- });
66
+ <div
67
+ ref={containerRef}
68
+ className="flex gap-1 bg-popover-background rounded shadow-popover p-1"
69
+ >
70
+ {children}
71
+ </div>
72
+ </PopperPrimitive.Content>
73
+ </PopperPrimitive.Root>
74
+ );
75
+ }
76
+ );
@@ -3,7 +3,6 @@ import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
3
3
  import React from "react";
4
4
 
5
5
  import { memoGeneric } from "@noya-app/react-utils";
6
- import { cssVars } from "../theme";
7
6
  import { BaseToolbar } from "./BaseToolbar";
8
7
  import { Button } from "./Button";
9
8
  import { DividerVertical } from "./Divider";
@@ -13,10 +12,79 @@ import {
13
12
  getKeyboardShortcutsForMenuItems,
14
13
  isSelectableMenuItem,
15
14
  MenuItem,
15
+ SelectableMenuItem,
16
+ SubMenuItem,
16
17
  } from "./internal/Menu";
17
18
  import { Spacer } from "./Spacer";
18
19
  import { Tooltip } from "./Tooltip";
19
20
 
21
+ export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
22
+ T extends string,
23
+ >({
24
+ item,
25
+ onSelectMenuItem,
26
+ }: {
27
+ item: SubMenuItem<T>;
28
+ onSelectMenuItem?: (value: T) => void;
29
+ }) {
30
+ const [open, setOpen] = React.useState(false);
31
+
32
+ return (
33
+ <DropdownMenu
34
+ open={open}
35
+ onOpenChange={setOpen}
36
+ items={item.items}
37
+ onSelect={(value) => {
38
+ if (onSelectMenuItem && value) {
39
+ onSelectMenuItem(value);
40
+ }
41
+ }}
42
+ >
43
+ <Button disabled={item.disabled} active={item.checked || open}>
44
+ {item.title}
45
+ {item.title && item.icon && <Spacer.Horizontal inline size={6} />}
46
+ {item.icon && renderIcon(item.icon)}
47
+ <Spacer.Horizontal size={6} />
48
+ <DropdownChevronIcon />
49
+ </Button>
50
+ </DropdownMenu>
51
+ );
52
+ });
53
+
54
+ export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
55
+ T extends string,
56
+ >({
57
+ item,
58
+ onSelectMenuItem,
59
+ }: {
60
+ item: SelectableMenuItem<T>;
61
+ onSelectMenuItem?: (value: T) => void;
62
+ }) {
63
+ const content = (
64
+ <Button
65
+ disabled={item.disabled}
66
+ active={item.checked}
67
+ onClick={() => {
68
+ if (onSelectMenuItem && item.value) {
69
+ onSelectMenuItem(item.value);
70
+ }
71
+ }}
72
+ >
73
+ {item.title}
74
+ {item.title && item.icon && <Spacer.Horizontal inline size={6} />}
75
+ {item.icon && renderIcon(item.icon)}
76
+ </Button>
77
+ );
78
+
79
+ return item.tooltip ? (
80
+ <Tooltip sideOffset={10} content={item.tooltip}>
81
+ {content}
82
+ </Tooltip>
83
+ ) : (
84
+ content
85
+ );
86
+ });
87
+
20
88
  export interface ToolbarProps<T extends string = string> {
21
89
  children?: React.ReactNode;
22
90
  logo?: React.ReactNode;
@@ -80,11 +148,6 @@ export function Toolbar<T extends string = string>({
80
148
  );
81
149
  }
82
150
 
83
- const activeButtonStyle = {
84
- backgroundColor: cssVars.colors.primaryPastel,
85
- color: cssVars.colors.primary,
86
- };
87
-
88
151
  export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
89
152
  items,
90
153
  onSelectMenuItem,
@@ -102,53 +165,21 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
102
165
  }
103
166
 
104
167
  if (isSelectableMenuItem(item)) {
105
- const content = (
106
- <Button
168
+ return (
169
+ <ToolbarMenuButton
107
170
  key={i}
108
- disabled={item.disabled}
109
- style={item.checked ? activeButtonStyle : undefined}
110
- onClick={() => {
111
- if (onSelectMenuItem && item.value) {
112
- onSelectMenuItem(item.value);
113
- }
114
- }}
115
- >
116
- {item.title}
117
- {item.title && item.icon && <Spacer.Horizontal inline size={6} />}
118
- {item.icon && renderIcon(item.icon)}
119
- </Button>
120
- );
121
-
122
- return item.tooltip ? (
123
- <Tooltip sideOffset={10} content={item.tooltip} key={i}>
124
- {content}
125
- </Tooltip>
126
- ) : (
127
- content
171
+ item={item}
172
+ onSelectMenuItem={onSelectMenuItem}
173
+ />
128
174
  );
129
175
  }
130
176
 
131
177
  return (
132
- <DropdownMenu
178
+ <ToolbarMenuDropdown
133
179
  key={i}
134
- items={item.items}
135
- onSelect={(value) => {
136
- if (onSelectMenuItem && value) {
137
- onSelectMenuItem(value);
138
- }
139
- }}
140
- >
141
- <Button
142
- disabled={item.disabled}
143
- style={item.checked ? activeButtonStyle : undefined}
144
- >
145
- {item.title}
146
- {item.title && item.icon && <Spacer.Horizontal inline size={6} />}
147
- {item.icon && renderIcon(item.icon)}
148
- <Spacer.Horizontal size={6} />
149
- <DropdownChevronIcon />
150
- </Button>
151
- </DropdownMenu>
180
+ item={item}
181
+ onSelectMenuItem={onSelectMenuItem}
182
+ />
152
183
  );
153
184
  })}
154
185
  </>
@@ -1,5 +1,9 @@
1
1
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2
2
  import * as React from "react";
3
+ import {
4
+ portalScopeProps,
5
+ usePortalScopeId,
6
+ } from "../contexts/PortalScopeContext";
3
7
  import { textStyles } from "./Text";
4
8
 
5
9
  // const Arrow = styled(TooltipPrimitive.Arrow)(({ theme }) => ({
@@ -17,12 +21,15 @@ export const Tooltip = React.memo(function Tooltip({
17
21
  content,
18
22
  sideOffset = 2,
19
23
  }: Props) {
24
+ const portalScopeId = usePortalScopeId();
25
+
20
26
  return (
21
27
  <TooltipPrimitive.Provider>
22
28
  <TooltipPrimitive.Root>
23
29
  <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
24
30
  <TooltipPrimitive.Portal>
25
31
  <TooltipPrimitive.Content
32
+ {...portalScopeProps(portalScopeId)}
26
33
  side="bottom"
27
34
  align="center"
28
35
  sideOffset={sideOffset}
@@ -95,7 +95,7 @@ export type SelectableMenuItem<T extends string> = BaseMenuItem & {
95
95
  tooltip?: ReactNode;
96
96
  };
97
97
 
98
- type SubMenuItem<T extends string> = BaseMenuItem & {
98
+ export type SubMenuItem<T extends string> = BaseMenuItem & {
99
99
  type: "submenu";
100
100
  items: MenuItem<T>[];
101
101
  id: string;
@@ -2,7 +2,11 @@ import { ChevronRightIcon } from "@noya-app/noya-icons";
2
2
  import * as RadixContextMenu from "@radix-ui/react-context-menu";
3
3
  import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
4
4
  import React from "react";
5
- import { cx } from "../..";
5
+ import {
6
+ portalScopeProps,
7
+ usePortalScopeId,
8
+ } from "../../contexts/PortalScopeContext";
9
+ import { cx } from "../../utils/classNames";
6
10
  import { MenuItem, SectionHeader, styles } from "./Menu";
7
11
  import { SelectItem } from "./SelectItem";
8
12
 
@@ -59,6 +63,8 @@ export const MenuViewport = <T extends string>({
59
63
  Components,
60
64
  onSelect,
61
65
  }: MenuViewportProps<T>) => {
66
+ const portalScopeId = usePortalScopeId();
67
+
62
68
  const hasCheckedItem = items.some(
63
69
  (item) =>
64
70
  item.type !== "separator" && item.type !== "sectionHeader" && item.checked
@@ -105,6 +111,7 @@ export const MenuViewport = <T extends string>({
105
111
  e.stopPropagation();
106
112
  e.preventDefault();
107
113
  }}
114
+ disabled={item.disabled}
108
115
  >
109
116
  <SelectItem
110
117
  value={item.id}
@@ -123,6 +130,7 @@ export const MenuViewport = <T extends string>({
123
130
  </Components.SubTrigger>
124
131
  <Components.Portal>
125
132
  <Components.SubContent
133
+ {...portalScopeProps(portalScopeId)}
126
134
  alignOffset={-5}
127
135
  className={styles.contentStyle}
128
136
  >
@@ -0,0 +1,48 @@
1
+ import * as React from "react";
2
+
3
+ export type PortalScopeContextValue = string;
4
+
5
+ const PortalScopeContext = React.createContext<
6
+ PortalScopeContextValue | undefined
7
+ >(undefined);
8
+
9
+ export const PortalScopeProvider = React.memo(function PortalScopeProvider({
10
+ children,
11
+ portalScopeId,
12
+ }: {
13
+ children: React.ReactNode;
14
+ portalScopeId: string;
15
+ }) {
16
+ return (
17
+ <PortalScopeContext.Provider value={portalScopeId}>
18
+ {children}
19
+ </PortalScopeContext.Provider>
20
+ );
21
+ });
22
+
23
+ export function usePortalScopeId(): string {
24
+ return React.useContext(PortalScopeContext) ?? "";
25
+ }
26
+
27
+ export const portalScopeDataSetName = "noyaPortalScope";
28
+ export const portalScopePropName = `data-noya-portal-scope`;
29
+
30
+ export function portalScopeProps(id?: string) {
31
+ return {
32
+ [portalScopePropName]: id,
33
+ };
34
+ }
35
+
36
+ export function getClosestPortalScope(element: EventTarget) {
37
+ if (!(element instanceof HTMLElement)) {
38
+ return;
39
+ }
40
+
41
+ const closest = element.closest(`[${portalScopePropName}]`);
42
+
43
+ if (!closest || !(closest instanceof HTMLElement)) {
44
+ return;
45
+ }
46
+
47
+ return closest.dataset[portalScopeDataSetName];
48
+ }
package/src/index.tsx CHANGED
@@ -78,6 +78,7 @@ export * from "./contexts/DialogContext";
78
78
  export * from "./contexts/FloatingWindowContext";
79
79
  export * from "./contexts/GlobalInputBlurContext";
80
80
  export * from "./contexts/ImageDataContext";
81
+ export * from "./contexts/PortalScopeContext";
81
82
  // Hooks
82
83
  export * from "./hooks/useHover";
83
84
  export * from "./hooks/useIndent";