@react-fabric/core 1.0.0-beta.5 → 1.0.0-beta.6

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.
Files changed (66) hide show
  1. package/dist/cjs/components/animations/Animations.js +12 -5
  2. package/dist/cjs/components/avatar/Avatar.js +6 -7
  3. package/dist/cjs/components/button/ConfirmButton.js +4 -2
  4. package/dist/cjs/components/card/Card.js +4 -2
  5. package/dist/cjs/components/collapsable/Collapsable.js +3 -2
  6. package/dist/cjs/components/dropdown/Dropdown.js +2 -1
  7. package/dist/cjs/components/dropdown/DropdownTool.js +3 -2
  8. package/dist/cjs/components/icon/Icon.js +7 -7
  9. package/dist/cjs/components/menu/ContextMenu.js +30 -10
  10. package/dist/cjs/components/menu/Menu.js +1 -1
  11. package/dist/cjs/components/navigator/Navigator.js +2 -0
  12. package/dist/cjs/components/panel/Panel.js +2 -0
  13. package/dist/cjs/components/tabpanel/Tab.js +54 -36
  14. package/dist/cjs/core/aside/Aside.js +3 -0
  15. package/dist/cjs/core/content/Content.js +8 -5
  16. package/dist/cjs/core/page/Page.js +1 -0
  17. package/dist/cjs/typography/Typography.module.css +1 -1
  18. package/dist/esm/components/animations/Animations.js +12 -5
  19. package/dist/esm/components/avatar/Avatar.js +7 -8
  20. package/dist/esm/components/button/ConfirmButton.js +4 -2
  21. package/dist/esm/components/card/Card.js +4 -2
  22. package/dist/esm/components/collapsable/Collapsable.js +3 -2
  23. package/dist/esm/components/dropdown/Dropdown.js +2 -1
  24. package/dist/esm/components/dropdown/DropdownTool.js +3 -2
  25. package/dist/esm/components/icon/Icon.js +7 -7
  26. package/dist/esm/components/menu/ContextMenu.js +32 -12
  27. package/dist/esm/components/menu/Menu.js +1 -1
  28. package/dist/esm/components/navigator/Navigator.js +2 -0
  29. package/dist/esm/components/panel/Panel.js +2 -0
  30. package/dist/esm/components/tabpanel/Tab.js +55 -37
  31. package/dist/esm/core/aside/Aside.js +3 -0
  32. package/dist/esm/core/content/Content.js +8 -5
  33. package/dist/esm/core/page/Page.js +1 -0
  34. package/dist/esm/typography/Typography.module.css +1 -1
  35. package/dist/types/components/actionLabel/ActionLabel.d.ts +2 -2
  36. package/dist/types/components/animations/Animations.d.ts +1 -1
  37. package/dist/types/components/button/ConfirmButton.d.ts +6 -1
  38. package/dist/types/components/card/Card.d.ts +3 -3
  39. package/dist/types/components/collapsable/Collapsable.d.ts +5 -1
  40. package/dist/types/components/dropdown/DropdownTool.d.ts +2 -1
  41. package/dist/types/components/media/Media.d.ts +2 -2
  42. package/dist/types/components/menu/ContextMenu.d.ts +2 -1
  43. package/dist/types/components/tabpanel/Tab.d.ts +4 -4
  44. package/dist/types/core/content/Content.d.ts +3 -2
  45. package/dist/types/types/index.d.ts +4 -0
  46. package/package.json +3 -3
  47. package/src/components/actionLabel/ActionLabel.tsx +12 -3
  48. package/src/components/animations/Animations.tsx +8 -2
  49. package/src/components/avatar/Avatar.tsx +68 -81
  50. package/src/components/button/ConfirmButton.tsx +10 -4
  51. package/src/components/card/Card.tsx +4 -0
  52. package/src/components/collapsable/Collapsable.tsx +6 -0
  53. package/src/components/dropdown/Dropdown.tsx +1 -0
  54. package/src/components/dropdown/DropdownTool.tsx +3 -1
  55. package/src/components/icon/Icon.tsx +45 -57
  56. package/src/components/media/Media.tsx +2 -2
  57. package/src/components/menu/ContextMenu.tsx +36 -11
  58. package/src/components/menu/Menu.tsx +1 -1
  59. package/src/components/navigator/Navigator.tsx +2 -0
  60. package/src/components/panel/Panel.tsx +2 -0
  61. package/src/components/tabpanel/Tab.tsx +73 -59
  62. package/src/core/aside/Aside.tsx +3 -0
  63. package/src/core/content/Content.tsx +16 -10
  64. package/src/core/page/Page.tsx +1 -0
  65. package/src/types/index.ts +5 -0
  66. package/src/typography/Typography.module.css +1 -1
@@ -1,6 +1,6 @@
1
1
  import { type ReactElement } from "react";
2
- import { type AriaProps, type BadgeType, type CallbackReturn, type ChildrenProp, type ColorType, type CssProp, type IconProps, type PolymorphicProps, type RefProp, type TestProps } from "../../types";
3
- export interface TabProps extends Partial<ChildrenProp>, CssProp, RefProp, AriaProps, TestProps, IconProps {
2
+ import { type AriaProps, type BadgeType, type CallbackReturn, type ChildrenProp, type ColorType, type CssProp, type IconProps, type PolymorphicProps, type RefProp, type TestProps, type TooltipProp } from "../../types";
3
+ export interface TabProps extends Partial<ChildrenProp>, CssProp, RefProp, AriaProps, TestProps, IconProps, TooltipProp {
4
4
  /**
5
5
  * tab id
6
6
  */
@@ -16,7 +16,7 @@ export interface TabProps extends Partial<ChildrenProp>, CssProp, RefProp, AriaP
16
16
  /**
17
17
  * tab badge
18
18
  */
19
- badge?: BadgeType;
19
+ badge?: string | number | BadgeType;
20
20
  /**
21
21
  * disabled
22
22
  */
@@ -42,4 +42,4 @@ export interface TabProps extends Partial<ChildrenProp>, CssProp, RefProp, AriaP
42
42
  */
43
43
  onClick?: (id: string) => CallbackReturn;
44
44
  }
45
- export declare const Tab: <Tag extends import("react").ElementType<any, keyof import("react").JSX.IntrinsicElements> = "button">({ ref, as, id, className, label, icon, iconBg, iconColor, rtlFlip, badge, disabled, onClose, active, minimal, onClick, tabFlex, color, actions, activeClassName, ...aria }: TabProps & PolymorphicProps<Tag>) => import("react/jsx-runtime").JSX.Element;
45
+ export declare const Tab: <Tag extends import("react").ElementType<any, keyof import("react").JSX.IntrinsicElements> = "button">({ ref, as, id, className, label, icon, iconBg, iconColor, rtlFlip, badge, disabled, onClose, active, minimal, onClick, tabFlex, color, actions, tooltip, activeClassName, ...aria }: TabProps & PolymorphicProps<Tag>) => import("react/jsx-runtime").JSX.Element;
@@ -1,9 +1,10 @@
1
- import { type ChildrenProp, type CssProp, type PositionObject, type RefProp, type SizeObject, type TestProps } from "../../types";
1
+ import { type MouseEvent } from "react";
2
+ import { type ChildrenProp, type CssProp, type RefProp, type ScrollObject, type SizeObject, type TestProps } from "../../types";
2
3
  export interface ContentProps extends ChildrenProp, CssProp, RefProp, TestProps {
3
4
  /**
4
5
  * scroll vevent handler
5
6
  */
6
- onScroll?: (event: PositionObject) => void;
7
+ onScroll?: (scroll: ScrollObject, event: MouseEvent<HTMLDivElement>) => void;
7
8
  /**
8
9
  * scroll vevent handler
9
10
  */
@@ -138,3 +138,7 @@ export interface SizeObject {
138
138
  width: number;
139
139
  height: number;
140
140
  }
141
+ export interface ScrollObject extends PositionObject, SizeObject {
142
+ scrollWidth?: number;
143
+ scrollHeight: number;
144
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-fabric/core",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.6",
4
4
  "description": "RUF: Core package",
5
5
  "author": "Adarsh Pastakia <adarshpastakia@outlook.com>",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@floating-ui/react": "^0.26.24",
39
- "@react-fabric/utilities": "^1.0.0-beta.5",
39
+ "@react-fabric/utilities": "^1.0.0-beta.6",
40
40
  "@tailwindcss/container-queries": "^0.1.1",
41
41
  "@tailwindcss/forms": "^0.5.7",
42
42
  "@tailwindcss/typography": "^0.5.13",
@@ -56,5 +56,5 @@
56
56
  "node": ">=18.12.0",
57
57
  "npm": ">=8.19.0"
58
58
  },
59
- "gitHead": "7d4a7ed1ea9890652826bef58d01c31280d761c8"
59
+ "gitHead": "21167df13cedeae8b365e87b9bab6d7401298cb6"
60
60
  }
@@ -22,12 +22,21 @@
22
22
  */
23
23
 
24
24
  import classNames from "classnames";
25
- import { type ReactElement, useCallback } from "react";
26
- import { type AriaProps, type ChildProp, type CssProp } from "../../types";
25
+ import { useCallback, type ReactElement } from "react";
26
+ import {
27
+ type AriaProps,
28
+ type ChildProp,
29
+ type CssProp,
30
+ type TestProps,
31
+ } from "../../types";
27
32
  import { ButtonGroup } from "../button/ButtonGroup";
28
33
  import classes from "./ActionLabel.module.css";
29
34
 
30
- export interface ActionLabelProps extends AriaProps, CssProp, ChildProp {
35
+ export interface ActionLabelProps
36
+ extends AriaProps,
37
+ CssProp,
38
+ ChildProp,
39
+ TestProps {
31
40
  /**
32
41
  * action buttons aligment
33
42
  */
@@ -142,6 +142,7 @@ export const AnimationSpinner = ({
142
142
  className={classNames(
143
143
  "absolute inset-0 z-10 flex justify-center items-center cursor-wait",
144
144
  )}
145
+ data-testid="spinner"
145
146
  >
146
147
  <div
147
148
  className={classNames(
@@ -164,15 +165,20 @@ export const Loading = () => {
164
165
  classes.loader,
165
166
  "area-[loader] relative overflow-x-clip",
166
167
  )}
168
+ data-testid="loading"
167
169
  >
168
170
  <div className="absolute z-10 py-px inset-x-0 top-0 after:h-0.5 after:block after:bg-primary-500" />
169
171
  </div>
170
172
  );
171
173
  };
172
174
 
173
- export const Skeleton = () => {
175
+ export const Skeleton = ({ className, ...rest }: CssProp) => {
174
176
  return (
175
- <div className={classNames("p-2 animate-pulse flex gap-2 max-w-lg")}>
177
+ <div
178
+ className={classNames(className, "p-2 animate-pulse flex gap-2 max-w-lg")}
179
+ data-testid="skeleton"
180
+ {...rest}
181
+ >
176
182
  <div className="bg-tint-100 rounded aspect-square h-12" />
177
183
  <div className="flex-1 flex flex-col gap-2">
178
184
  <div className="bg-tint-100 h-3 rounded-full" />
@@ -24,8 +24,7 @@
24
24
  import { isSvgPath } from "@react-fabric/utilities";
25
25
  import BoringAvatar from "boring-avatars";
26
26
  import classNames from "classnames";
27
- import { useState } from "react";
28
- import { useMemoDebugger } from "../../hooks/useEffectDebugger";
27
+ import { useMemo, useState } from "react";
29
28
  import {
30
29
  type AriaProps,
31
30
  type ColorType,
@@ -109,89 +108,77 @@ export const Avatar = ({
109
108
  const [fallback, setFallback] = useState(true);
110
109
 
111
110
  /** ***************** style map *******************/
112
- const styles = useMemoDebugger(
113
- () => {
114
- const s: KeyValue = {};
115
- if (bg) {
116
- s.backgroundColor = getColor(bg);
117
- }
118
- if (color) {
119
- s.color = getColor(color);
120
- }
121
- if (size && !(size in SizeMap)) {
122
- s.fontSize = size;
123
- }
124
- if (size && size in SizeMap) {
125
- s.fontSize = SizeMap[size];
126
- }
127
- return s;
128
- },
129
- [bg, color, size],
130
- "Avatar styles",
131
- );
111
+ const styles = useMemo(() => {
112
+ const s: KeyValue = {};
113
+ if (bg) {
114
+ s.backgroundColor = getColor(bg);
115
+ }
116
+ if (color) {
117
+ s.color = getColor(color);
118
+ }
119
+ if (size && !(size in SizeMap)) {
120
+ s.fontSize = size;
121
+ }
122
+ if (size && size in SizeMap) {
123
+ s.fontSize = SizeMap[size];
124
+ }
125
+ return s;
126
+ }, [bg, color, size]);
132
127
 
133
128
  /** ***************** render icon *******************/
134
- const avatarImage = useMemoDebugger(
135
- () => {
136
- setFallback(!avatar);
129
+ const avatarImage = useMemo(() => {
130
+ setFallback(!avatar);
131
+ return (
132
+ avatar && (
133
+ <div className="contents">
134
+ <div
135
+ className={classNames(
136
+ classes.imgPlaceholder,
137
+ "absolute inset-0 bg-gray animate-pulse",
138
+ )}
139
+ />
140
+ <img
141
+ src={avatar}
142
+ alt={avatar}
143
+ className="align-baseline"
144
+ onError={() => setFallback(true)}
145
+ onLoad={(e) => e.currentTarget.previousElementSibling?.remove()}
146
+ />
147
+ </div>
148
+ )
149
+ );
150
+ }, [avatar]);
151
+
152
+ const fallbackAvatar = useMemo(() => {
153
+ if (fallbackIcon) {
154
+ return isSvgPath(fallbackIcon) ? (
155
+ <svg viewBox={viewBox}>
156
+ <path fill="currentColor" d={fallbackIcon.toString()} />
157
+ </svg>
158
+ ) : (
159
+ <i className={fallbackIcon} />
160
+ );
161
+ }
162
+ if (variant === "text") {
163
+ const [, first, second, group] =
164
+ name.match(/(?=(\w).* (\w).*)|(?=(\w\w).*)/) ?? [];
137
165
  return (
138
- avatar && (
139
- <div className="contents">
140
- <div
141
- className={classNames(
142
- classes.imgPlaceholder,
143
- "absolute inset-0 bg-gray animate-pulse",
144
- )}
145
- />
146
- <img
147
- src={avatar}
148
- alt={avatar}
149
- className="align-baseline"
150
- onError={() => setFallback(true)}
151
- onLoad={(e) => e.currentTarget.previousElementSibling?.remove()}
152
- />
153
- </div>
154
- )
166
+ <svg role="img">
167
+ <text
168
+ x="50%"
169
+ y="50%"
170
+ dy=".1em"
171
+ dominantBaseline="middle"
172
+ textAnchor="middle"
173
+ style={{ fontSize: ".5em", fontWeight: 500 }}
174
+ >
175
+ {group ?? `${first}${second}`}
176
+ </text>
177
+ </svg>
155
178
  );
156
- },
157
- [avatar],
158
- "Avatar icon",
159
- );
160
-
161
- const fallbackAvatar = useMemoDebugger(
162
- () => {
163
- if (fallbackIcon) {
164
- return isSvgPath(fallbackIcon) ? (
165
- <svg viewBox={viewBox}>
166
- <path fill="currentColor" d={fallbackIcon.toString()} />
167
- </svg>
168
- ) : (
169
- <i className={fallbackIcon} />
170
- );
171
- }
172
- if (variant === "text") {
173
- const [, first, second, group] =
174
- name.match(/(?=(\w).* (\w).*)|(?=(\w\w).*)/) ?? [];
175
- return (
176
- <svg role="img">
177
- <text
178
- x="50%"
179
- y="50%"
180
- dy=".1em"
181
- dominantBaseline="middle"
182
- textAnchor="middle"
183
- style={{ fontSize: ".5em", fontWeight: 500 }}
184
- >
185
- {group ?? `${first}${second}`}
186
- </text>
187
- </svg>
188
- );
189
- }
190
- return <BoringAvatar square size={64} variant={variant} name={name} />;
191
- },
192
- [name, variant, fallbackIcon],
193
- "Avatar fallback",
194
- );
179
+ }
180
+ return <BoringAvatar square size={64} variant={variant} name={name} />;
181
+ }, [name, variant, fallbackIcon]);
195
182
 
196
183
  return (
197
184
  <dfn
@@ -22,18 +22,19 @@
22
22
  */
23
23
 
24
24
  import {
25
- FloatingArrow,
26
- FloatingFocusManager,
27
- FloatingPortal,
28
25
  arrow,
29
26
  autoUpdate,
30
27
  flip,
28
+ FloatingArrow,
29
+ FloatingFocusManager,
30
+ FloatingPortal,
31
31
  offset,
32
32
  shift,
33
33
  useClick,
34
34
  useDismiss,
35
35
  useFloating,
36
36
  useInteractions,
37
+ type Placement,
37
38
  } from "@floating-ui/react";
38
39
  import { mergeRefs } from "@react-fabric/utilities";
39
40
  import classNames from "classnames";
@@ -66,6 +67,10 @@ interface ExtendedProps extends Omit<BaseProps, "onClick">, RefProp {
66
67
  * ok label
67
68
  */
68
69
  cancelLabel?: string;
70
+ /**
71
+ * dropdown placement
72
+ */
73
+ placement?: Placement;
69
74
  /**
70
75
  * click handler, return Promise to autoset loading state
71
76
  */
@@ -85,6 +90,7 @@ export const ConfirmButton = <Tag extends React.ElementType = "button">({
85
90
  cancelLabel,
86
91
  onClick,
87
92
  loading,
93
+ placement = "bottom",
88
94
  actionMessage,
89
95
  showActionDone,
90
96
  showActionDoneEvent = "click",
@@ -127,7 +133,7 @@ export const ConfirmButton = <Tag extends React.ElementType = "button">({
127
133
  }
128
134
  },
129
135
  strategy: "fixed",
130
- placement: "bottom",
136
+ placement,
131
137
  whileElementsMounted: autoUpdate,
132
138
  middleware: [
133
139
  shift({ padding: 8 }),
@@ -39,6 +39,7 @@ import {
39
39
  type AriaProps,
40
40
  type ChildrenProp,
41
41
  type CssProp,
42
+ type RefProp,
42
43
  type TestProps,
43
44
  } from "../../types";
44
45
  import { nodeCheck } from "../../utils";
@@ -48,6 +49,7 @@ export interface CardProps
48
49
  extends CssProp,
49
50
  AriaProps,
50
51
  TestProps,
52
+ RefProp<HTMLDivElement>,
51
53
  ChildrenProp<React.ReactElement<HeadFootProps | ContentProps>> {
52
54
  /**
53
55
  * apply flex fill styling
@@ -94,6 +96,7 @@ export const Card = ({
94
96
  draggable,
95
97
  dragKey,
96
98
  dragData,
99
+ ref,
97
100
  ...aria
98
101
  }: CardProps) => {
99
102
  const [header, body, footer] = useMemo(() => {
@@ -136,6 +139,7 @@ export const Card = ({
136
139
  dragKey &&
137
140
  e.dataTransfer?.setData(dragKey, JSON.stringify(dragData))
138
141
  }
142
+ ref={ref}
139
143
  {...aria}
140
144
  >
141
145
  {header}
@@ -66,6 +66,10 @@ export interface CollapsableProps
66
66
  * header button className
67
67
  */
68
68
  headerClassName?: string;
69
+ /**
70
+ * Header position sticky
71
+ */
72
+ stickyHeader?: boolean;
69
73
  /**
70
74
  * open state icon
71
75
  */
@@ -100,6 +104,7 @@ export const Collapsable = ({
100
104
  headerClassName,
101
105
  children,
102
106
  iconAlign,
107
+ stickyHeader,
103
108
  iconClosed = CoreIcons.chevronRight,
104
109
  iconOpen = CoreIcons.chevronDown,
105
110
  open,
@@ -138,6 +143,7 @@ export const Collapsable = ({
138
143
  classes.collapsableHead,
139
144
  "flex gap-1 flex-nowrap py-2 font-medium cursor-pointer items-center select-none hover:opacity-70",
140
145
  headerClassName,
146
+ stickyHeader && "bg-base sticky top-0 px-2 py-1 z-10",
141
147
  iconAlign === "end" ? "flex-row-reverse" : "flex-row",
142
148
  )}
143
149
  onClick={() => !disabled && setOpenState(!openState)}
@@ -248,6 +248,7 @@ const DropdownElement = ({
248
248
  zIndex: "var(--z-popover)",
249
249
  ...floatingStyles,
250
250
  }}
251
+ data-testid="dropdown-body"
251
252
  {...getFloatingProps({
252
253
  onClick: (e) => e.stopPropagation(),
253
254
  })}
@@ -31,10 +31,12 @@ export const DropdownTool = ({
31
31
  children,
32
32
  className,
33
33
  groupHover,
34
+ icon,
34
35
  ...props
35
36
  }: ChildProp &
36
37
  CssProp &
37
38
  Omit<DropdownProps, "children"> & {
39
+ icon?: string;
38
40
  /**
39
41
  * hide until group hovered
40
42
  */
@@ -43,7 +45,7 @@ export const DropdownTool = ({
43
45
  return (
44
46
  <Dropdown {...props}>
45
47
  <Icon
46
- icon={CoreIcons.caretDown}
48
+ icon={icon ?? CoreIcons.caretDown}
47
49
  className={classNames(
48
50
  className,
49
51
  "outline bg-base flex-content pointer-events-auto cursor-pointer",
@@ -23,7 +23,7 @@
23
23
 
24
24
  import { isString, isSvgPath } from "@react-fabric/utilities";
25
25
  import classNames from "classnames";
26
- import { useMemoDebugger } from "../../hooks/useEffectDebugger";
26
+ import { useMemo } from "react";
27
27
  import {
28
28
  type AriaProps,
29
29
  type ColorType,
@@ -101,66 +101,54 @@ export const Icon = ({
101
101
  ...rest
102
102
  }: IconProps) => {
103
103
  /** ***************** style map *******************/
104
- const styles = useMemoDebugger(
105
- () => {
106
- const s: KeyValue = {};
107
- if (bg) {
108
- s.backgroundColor = getColor(bg);
109
- }
110
- if (color) {
111
- s.color = getColor(color);
112
- }
113
- if (size && !(size in SizeMap)) {
114
- s.fontSize = size;
115
- }
116
- if (size && size in SizeMap) {
117
- s.fontSize = SizeMap[size];
118
- }
119
- return s;
120
- },
121
- [bg, color, size],
122
- "Icon styles",
123
- );
104
+ const styles = useMemo(() => {
105
+ const s: KeyValue = {};
106
+ if (bg) {
107
+ s.backgroundColor = getColor(bg);
108
+ }
109
+ if (color) {
110
+ s.color = getColor(color);
111
+ }
112
+ if (size && !(size in SizeMap)) {
113
+ s.fontSize = size;
114
+ }
115
+ if (size && size in SizeMap) {
116
+ s.fontSize = SizeMap[size];
117
+ }
118
+ return s;
119
+ }, [bg, color, size]);
124
120
 
125
121
  /** ***************** check if icon is svg path *******************/
126
- const isSvg = useMemoDebugger(
127
- () => {
128
- return isSvgPath(icon);
129
- },
130
- [icon],
131
- "Icon isSvg",
132
- );
122
+ const isSvg = useMemo(() => {
123
+ return isSvgPath(icon);
124
+ }, [icon]);
133
125
 
134
126
  /** ***************** render icon *******************/
135
- const iconEl = useMemoDebugger(
136
- () => {
137
- if (!isString(icon)) {
138
- throw Error("Invalid icon expected string");
139
- }
140
- return isSvg ? (
141
- <svg viewBox={viewBox}>
142
- <path fill="currentColor" d={icon.toString()} />
143
- </svg>
144
- ) : icon?.toString().length <= 4 ? (
145
- <svg role="img">
146
- <text
147
- x="50%"
148
- y="50%"
149
- dy=".1em"
150
- dominantBaseline="middle"
151
- textAnchor="middle"
152
- style={{ fontSize: SvgTextSize[icon.length] ?? "1em" }}
153
- >
154
- {icon}
155
- </text>
156
- </svg>
157
- ) : (
158
- <i className={`${icon}`} />
159
- );
160
- },
161
- [icon, isSvg, viewBox],
162
- "Icon el",
163
- );
127
+ const iconEl = useMemo(() => {
128
+ if (!isString(icon)) {
129
+ throw Error("Invalid icon expected string");
130
+ }
131
+ return isSvg ? (
132
+ <svg viewBox={viewBox}>
133
+ <path fill="currentColor" d={icon.toString()} />
134
+ </svg>
135
+ ) : icon?.toString().length <= 4 ? (
136
+ <svg role="img">
137
+ <text
138
+ x="50%"
139
+ y="50%"
140
+ dy=".1em"
141
+ dominantBaseline="middle"
142
+ textAnchor="middle"
143
+ style={{ fontSize: SvgTextSize[icon.length] ?? "1em" }}
144
+ >
145
+ {icon}
146
+ </text>
147
+ </svg>
148
+ ) : (
149
+ <i className={`${icon}`} />
150
+ );
151
+ }, [icon, isSvg, viewBox]);
164
152
 
165
153
  return (
166
154
  <dfn
@@ -24,12 +24,12 @@
24
24
  import { getImageColorset } from "@react-fabric/utilities";
25
25
  import classNames from "classnames";
26
26
  import { useMemo, useRef, useState } from "react";
27
- import { type AriaProps, type CssProp } from "../../types";
27
+ import { type AriaProps, type CssProp, type TestProps } from "../../types";
28
28
  import { CoreIcons } from "../../types/icons";
29
29
  import { Button } from "../button/Button";
30
30
  import classes from "./Media.module.css";
31
31
 
32
- export interface BaseProps extends CssProp, AriaProps {
32
+ export interface BaseProps extends CssProp, TestProps, AriaProps {
33
33
  /**
34
34
  * media source
35
35
  */
@@ -29,30 +29,41 @@ import {
29
29
  useClick,
30
30
  useDismiss,
31
31
  useFloating,
32
+ useFloatingNodeId,
32
33
  useInteractions,
33
34
  } from "@floating-ui/react";
34
- import { cloneElement, useRef, useState } from "react";
35
+ import { cloneElement, useEffect, useRef, useState } from "react";
35
36
  import { type ChildrenProp } from "../../types";
36
37
  import { type Menu } from "./Menu";
37
38
 
38
39
  export interface ContextMenuProps extends ChildrenProp {
39
40
  menu: React.ReactElement<typeof Menu>;
41
+
42
+ disabled?: boolean;
40
43
  }
41
44
 
42
- export const ContextMenu = ({ children, menu }: ContextMenuProps) => {
45
+ export const ContextMenu = ({ children, menu, disabled }: ContextMenuProps) => {
43
46
  const [isOpen, setIsOpen] = useState(false);
47
+ const nodeId = useFloatingNodeId();
44
48
 
45
49
  const { refs, floatingStyles, context } = useFloating({
46
50
  open: isOpen,
47
- onOpenChange: setIsOpen,
51
+ nodeId,
52
+ onOpenChange: (open, evt, reason) => {
53
+ const target = evt?.target as HTMLElement;
54
+ if (!target?.closest(".menu-list")) setIsOpen(open);
55
+ },
48
56
  strategy: "fixed",
49
57
  placement: "bottom-start",
50
58
  whileElementsMounted: autoUpdate,
51
59
  middleware: [offset(8), flip()],
52
60
  });
53
61
 
54
- const click = useClick(context, {});
55
- const dismiss = useDismiss(context, {});
62
+ const click = useClick(context, {
63
+ toggle: true,
64
+ ignoreMouse: true,
65
+ });
66
+ const dismiss = useDismiss(context, { bubbles: true });
56
67
 
57
68
  const { getFloatingProps } = useInteractions([dismiss, click]);
58
69
 
@@ -78,10 +89,19 @@ export const ContextMenu = ({ children, menu }: ContextMenuProps) => {
78
89
  return false;
79
90
  });
80
91
 
81
- const wrapperRef = useRef((el: HTMLElement) => {
82
- el?.parentElement?.removeEventListener("contextmenu", handleClick.current);
83
- el?.parentElement?.addEventListener("contextmenu", handleClick.current);
84
- });
92
+ const wrapperRef = useRef<HTMLDivElement>(null);
93
+ useEffect(() => {
94
+ const el = wrapperRef.current;
95
+ if (el && !disabled) {
96
+ el?.parentElement?.addEventListener("contextmenu", handleClick.current);
97
+ return () => {
98
+ el?.parentElement?.removeEventListener(
99
+ "contextmenu",
100
+ handleClick.current,
101
+ );
102
+ };
103
+ }
104
+ }, [disabled]);
85
105
 
86
106
  const tryClosing = useRef((e: React.MouseEvent) => {
87
107
  if ((e.target as HTMLElement)?.closest("[data-dropdown-dismiss='true']"))
@@ -91,12 +111,17 @@ export const ContextMenu = ({ children, menu }: ContextMenuProps) => {
91
111
  return (
92
112
  <div
93
113
  className="contents"
94
- ref={wrapperRef.current as AnyObject}
114
+ ref={wrapperRef}
95
115
  onMouseUpCapture={tryClosing.current}
96
116
  >
97
117
  {children}
98
118
  {isOpen && (
99
- <FloatingPortal>
119
+ <FloatingPortal
120
+ root={
121
+ refs.domReference.current?.closest<HTMLElement>(".theme-base") ??
122
+ undefined
123
+ }
124
+ >
100
125
  {cloneElement(menu as AnyObject, {
101
126
  ...getFloatingProps(),
102
127
  style: {
@@ -224,7 +224,7 @@ const MenuComponent = ({
224
224
  "outline shadow-lg rounded-capped max-h-96 overflow-auto scroll-thin z-[var(--z-popover)]",
225
225
  )}
226
226
  autoFocus
227
- ref={mergeRefs(refs.setFloating, isNested ? undefined : ref)}
227
+ ref={mergeRefs(isNested ? refs.setFloating : ref)}
228
228
  {...getFloatingProps({
229
229
  onClick: isNested ? undefined : handleClick,
230
230
  })}