@basic-ui/core 0.0.59 → 0.0.61

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 (218) hide show
  1. package/build/cjs/index.js.map +1 -1
  2. package/build/esm/Accordion/AccordionBody.d.ts.map +1 -1
  3. package/build/esm/Accordion/AccordionBody.js +6 -26
  4. package/build/esm/Accordion/AccordionBody.js.map +1 -1
  5. package/build/esm/Accordion/AccordionHeader.d.ts.map +1 -1
  6. package/build/esm/Accordion/AccordionHeader.js +21 -69
  7. package/build/esm/Accordion/AccordionHeader.js.map +1 -1
  8. package/build/esm/Accordion/AccordionItem.d.ts.map +1 -1
  9. package/build/esm/Accordion/AccordionItem.js +31 -18
  10. package/build/esm/Accordion/AccordionItem.js.map +1 -1
  11. package/build/esm/Accordion/context.d.ts +0 -8
  12. package/build/esm/Accordion/context.d.ts.map +1 -1
  13. package/build/esm/Accordion/context.js +0 -11
  14. package/build/esm/Accordion/context.js.map +1 -1
  15. package/build/esm/Accordion/scopeQuery.d.ts +1 -0
  16. package/build/esm/Accordion/scopeQuery.d.ts.map +1 -1
  17. package/build/esm/Accordion/scopeQuery.js +3 -0
  18. package/build/esm/Accordion/scopeQuery.js.map +1 -1
  19. package/build/esm/Collapsible/Collapsible.d.ts +13 -0
  20. package/build/esm/Collapsible/Collapsible.d.ts.map +1 -0
  21. package/build/esm/Collapsible/Collapsible.js +53 -0
  22. package/build/esm/Collapsible/Collapsible.js.map +1 -0
  23. package/build/esm/Collapsible/CollapsiblePanel.d.ts +10 -0
  24. package/build/esm/Collapsible/CollapsiblePanel.d.ts.map +1 -0
  25. package/build/esm/Collapsible/CollapsiblePanel.js +85 -0
  26. package/build/esm/Collapsible/CollapsiblePanel.js.map +1 -0
  27. package/build/esm/Collapsible/CollapsibleTrigger.d.ts +11 -0
  28. package/build/esm/Collapsible/CollapsibleTrigger.d.ts.map +1 -0
  29. package/build/esm/Collapsible/CollapsibleTrigger.js +51 -0
  30. package/build/esm/Collapsible/CollapsibleTrigger.js.map +1 -0
  31. package/build/esm/Collapsible/context.d.ts +16 -0
  32. package/build/esm/Collapsible/context.d.ts.map +1 -0
  33. package/build/esm/Collapsible/context.js +11 -0
  34. package/build/esm/Collapsible/context.js.map +1 -0
  35. package/build/esm/Collapsible/index.d.ts +4 -0
  36. package/build/esm/Collapsible/index.d.ts.map +1 -0
  37. package/build/esm/Collapsible/index.js +4 -0
  38. package/build/esm/Collapsible/index.js.map +1 -0
  39. package/build/esm/Menu/Menu.d.ts +3 -2
  40. package/build/esm/Menu/Menu.d.ts.map +1 -1
  41. package/build/esm/Menu/Menu.js +64 -4
  42. package/build/esm/Menu/Menu.js.map +1 -1
  43. package/build/esm/Menu/MenuButton.d.ts.map +1 -1
  44. package/build/esm/Menu/MenuButton.js +85 -8
  45. package/build/esm/Menu/MenuButton.js.map +1 -1
  46. package/build/esm/Menu/MenuItem.d.ts.map +1 -1
  47. package/build/esm/Menu/MenuItem.js +16 -4
  48. package/build/esm/Menu/MenuItem.js.map +1 -1
  49. package/build/esm/Menu/MenuList.d.ts.map +1 -1
  50. package/build/esm/Menu/MenuList.js +47 -12
  51. package/build/esm/Menu/MenuList.js.map +1 -1
  52. package/build/esm/Menu/MenuPopover.d.ts.map +1 -1
  53. package/build/esm/Menu/MenuPopover.js +12 -1
  54. package/build/esm/Menu/MenuPopover.js.map +1 -1
  55. package/build/esm/Menu/MenuSubmenuTrigger.d.ts +8 -0
  56. package/build/esm/Menu/MenuSubmenuTrigger.d.ts.map +1 -0
  57. package/build/esm/Menu/MenuSubmenuTrigger.js +131 -0
  58. package/build/esm/Menu/MenuSubmenuTrigger.js.map +1 -0
  59. package/build/esm/Menu/context.d.ts +13 -3
  60. package/build/esm/Menu/context.d.ts.map +1 -1
  61. package/build/esm/Menu/context.js +1 -0
  62. package/build/esm/Menu/context.js.map +1 -1
  63. package/build/esm/Menu/index.d.ts +3 -0
  64. package/build/esm/Menu/index.d.ts.map +1 -1
  65. package/build/esm/Menu/index.js +2 -0
  66. package/build/esm/Menu/index.js.map +1 -1
  67. package/build/esm/Menu/scope.d.ts +1 -0
  68. package/build/esm/Menu/scope.d.ts.map +1 -1
  69. package/build/esm/Menu/scope.js +2 -1
  70. package/build/esm/Menu/scope.js.map +1 -1
  71. package/build/esm/MenuBar/MenuBar.d.ts +11 -0
  72. package/build/esm/MenuBar/MenuBar.d.ts.map +1 -0
  73. package/build/esm/MenuBar/MenuBar.js +153 -0
  74. package/build/esm/MenuBar/MenuBar.js.map +1 -0
  75. package/build/esm/MenuBar/context.d.ts +29 -0
  76. package/build/esm/MenuBar/context.d.ts.map +1 -0
  77. package/build/esm/MenuBar/context.js +7 -0
  78. package/build/esm/MenuBar/context.js.map +1 -0
  79. package/build/esm/MenuBar/index.d.ts +2 -0
  80. package/build/esm/MenuBar/index.d.ts.map +1 -0
  81. package/build/esm/MenuBar/index.js +2 -0
  82. package/build/esm/MenuBar/index.js.map +1 -0
  83. package/build/esm/Slider/Slider.d.ts +47 -1
  84. package/build/esm/Slider/Slider.d.ts.map +1 -1
  85. package/build/esm/Slider/Slider.js +91 -5
  86. package/build/esm/Slider/Slider.js.map +1 -1
  87. package/build/esm/ToggleGroup/ToggleGroup.d.ts +40 -0
  88. package/build/esm/ToggleGroup/ToggleGroup.d.ts.map +1 -0
  89. package/build/esm/ToggleGroup/ToggleGroup.js +113 -0
  90. package/build/esm/ToggleGroup/ToggleGroup.js.map +1 -0
  91. package/build/esm/ToggleGroup/ToggleGroupContext.d.ts +10 -0
  92. package/build/esm/ToggleGroup/ToggleGroupContext.d.ts.map +1 -0
  93. package/build/esm/ToggleGroup/ToggleGroupContext.js +6 -0
  94. package/build/esm/ToggleGroup/ToggleGroupContext.js.map +1 -0
  95. package/build/esm/ToggleGroup/index.d.ts +3 -0
  96. package/build/esm/ToggleGroup/index.d.ts.map +1 -0
  97. package/build/esm/ToggleGroup/index.js +3 -0
  98. package/build/esm/ToggleGroup/index.js.map +1 -0
  99. package/build/esm/Tooltip/Tooltip.d.ts.map +1 -1
  100. package/build/esm/Tooltip/Tooltip.js +2 -2
  101. package/build/esm/Tooltip/Tooltip.js.map +1 -1
  102. package/build/esm/Tree/Tree.d.ts +3 -0
  103. package/build/esm/Tree/Tree.d.ts.map +1 -0
  104. package/build/esm/Tree/Tree.js +730 -0
  105. package/build/esm/Tree/Tree.js.map +1 -0
  106. package/build/esm/Tree/TreeHeader.d.ts +3 -0
  107. package/build/esm/Tree/TreeHeader.d.ts.map +1 -0
  108. package/build/esm/Tree/TreeHeader.js +5 -0
  109. package/build/esm/Tree/TreeHeader.js.map +1 -0
  110. package/build/esm/Tree/TreeItem.d.ts +3 -0
  111. package/build/esm/Tree/TreeItem.d.ts.map +1 -0
  112. package/build/esm/Tree/TreeItem.js +5 -0
  113. package/build/esm/Tree/TreeItem.js.map +1 -0
  114. package/build/esm/Tree/TreeItemContent.d.ts +3 -0
  115. package/build/esm/Tree/TreeItemContent.d.ts.map +1 -0
  116. package/build/esm/Tree/TreeItemContent.js +69 -0
  117. package/build/esm/Tree/TreeItemContent.js.map +1 -0
  118. package/build/esm/Tree/TreeSection.d.ts +3 -0
  119. package/build/esm/Tree/TreeSection.d.ts.map +1 -0
  120. package/build/esm/Tree/TreeSection.js +5 -0
  121. package/build/esm/Tree/TreeSection.js.map +1 -0
  122. package/build/esm/Tree/collection.d.ts +18 -0
  123. package/build/esm/Tree/collection.d.ts.map +1 -0
  124. package/build/esm/Tree/collection.js +252 -0
  125. package/build/esm/Tree/collection.js.map +1 -0
  126. package/build/esm/Tree/context.d.ts +3 -0
  127. package/build/esm/Tree/context.d.ts.map +1 -0
  128. package/build/esm/Tree/context.js +3 -0
  129. package/build/esm/Tree/context.js.map +1 -0
  130. package/build/esm/Tree/index.d.ts +8 -0
  131. package/build/esm/Tree/index.d.ts.map +1 -0
  132. package/build/esm/Tree/index.js +7 -0
  133. package/build/esm/Tree/index.js.map +1 -0
  134. package/build/esm/Tree/types.d.ts +128 -0
  135. package/build/esm/Tree/types.d.ts.map +1 -0
  136. package/build/esm/Tree/types.js +2 -0
  137. package/build/esm/Tree/types.js.map +1 -0
  138. package/build/esm/hooks/index.d.ts +1 -0
  139. package/build/esm/hooks/index.d.ts.map +1 -1
  140. package/build/esm/hooks/index.js +1 -0
  141. package/build/esm/hooks/index.js.map +1 -1
  142. package/build/esm/hooks/useTransitionStatus.d.ts +7 -0
  143. package/build/esm/hooks/useTransitionStatus.d.ts.map +1 -0
  144. package/build/esm/hooks/useTransitionStatus.js +48 -0
  145. package/build/esm/hooks/useTransitionStatus.js.map +1 -0
  146. package/build/esm/index.d.ts +5 -0
  147. package/build/esm/index.d.ts.map +1 -1
  148. package/build/esm/index.js +5 -0
  149. package/build/esm/index.js.map +1 -1
  150. package/build/esm/toggle/Toggle.d.ts +28 -0
  151. package/build/esm/toggle/Toggle.d.ts.map +1 -0
  152. package/build/esm/toggle/Toggle.js +55 -0
  153. package/build/esm/toggle/Toggle.js.map +1 -0
  154. package/build/esm/toggle/index.d.ts +2 -0
  155. package/build/esm/toggle/index.d.ts.map +1 -0
  156. package/build/esm/toggle/index.js +2 -0
  157. package/build/esm/toggle/index.js.map +1 -0
  158. package/build/esm/utils/assign-ref.d.ts +3 -3
  159. package/build/esm/utils/assign-ref.d.ts.map +1 -1
  160. package/build/esm/utils/assign-ref.js +1 -1
  161. package/build/esm/utils/assign-ref.js.map +1 -1
  162. package/build/tsconfig-build.tsbuildinfo +1 -1
  163. package/build/tsconfig.tsbuildinfo +1 -1
  164. package/package.json +7 -4
  165. package/src/Accordion/AccordionBody.tsx +6 -35
  166. package/src/Accordion/AccordionHeader.tsx +29 -103
  167. package/src/Accordion/AccordionItem.tsx +40 -29
  168. package/src/Accordion/context.ts +0 -18
  169. package/src/Accordion/scopeQuery.ts +4 -0
  170. package/src/Collapsible/Collapsible.story.tsx +153 -0
  171. package/src/Collapsible/Collapsible.tsx +79 -0
  172. package/src/Collapsible/CollapsiblePanel.tsx +103 -0
  173. package/src/Collapsible/CollapsibleTrigger.tsx +60 -0
  174. package/src/Collapsible/context.ts +28 -0
  175. package/src/Collapsible/index.ts +3 -0
  176. package/src/Menu/Menu.story.tsx +70 -1
  177. package/src/Menu/Menu.tsx +141 -65
  178. package/src/Menu/MenuButton.tsx +115 -9
  179. package/src/Menu/MenuItem.tsx +20 -3
  180. package/src/Menu/MenuList.tsx +50 -13
  181. package/src/Menu/MenuPopover.tsx +12 -2
  182. package/src/Menu/MenuSubmenuTrigger.tsx +167 -0
  183. package/src/Menu/context.ts +20 -10
  184. package/src/Menu/index.ts +3 -0
  185. package/src/Menu/scope.ts +4 -1
  186. package/src/Menu/styles.css +57 -22
  187. package/src/MenuBar/MenuBar.story.tsx +92 -0
  188. package/src/MenuBar/MenuBar.tsx +236 -0
  189. package/src/MenuBar/context.ts +46 -0
  190. package/src/MenuBar/index.ts +1 -0
  191. package/src/MenuBar/styles.css +78 -0
  192. package/src/Slider/Slider.story.tsx +1 -1
  193. package/src/Slider/Slider.tsx +145 -8
  194. package/src/Toggle/Toggle.story.tsx +42 -0
  195. package/src/Toggle/Toggle.tsx +95 -0
  196. package/src/Toggle/index.ts +1 -0
  197. package/src/Toggle/styles.css +39 -0
  198. package/src/ToggleGroup/ToggleGroup.story.tsx +86 -0
  199. package/src/ToggleGroup/ToggleGroup.tsx +185 -0
  200. package/src/ToggleGroup/ToggleGroupContext.ts +17 -0
  201. package/src/ToggleGroup/index.ts +2 -0
  202. package/src/ToggleGroup/styles.css +66 -0
  203. package/src/Tooltip/Tooltip.tsx +4 -4
  204. package/src/Tree/Tree.story.tsx +221 -0
  205. package/src/Tree/Tree.tsx +1081 -0
  206. package/src/Tree/TreeHeader.tsx +9 -0
  207. package/src/Tree/TreeItem.tsx +9 -0
  208. package/src/Tree/TreeItemContent.tsx +91 -0
  209. package/src/Tree/TreeSection.tsx +9 -0
  210. package/src/Tree/collection.tsx +371 -0
  211. package/src/Tree/context.ts +6 -0
  212. package/src/Tree/index.ts +7 -0
  213. package/src/Tree/styles.css +135 -0
  214. package/src/Tree/types.ts +161 -0
  215. package/src/hooks/index.ts +1 -0
  216. package/src/hooks/useTransitionStatus.ts +65 -0
  217. package/src/index.ts +5 -0
  218. package/src/utils/assign-ref.ts +4 -4
@@ -12,7 +12,7 @@ import { useMenuContext, MenuListProvider } from './context';
12
12
  import { assignMultipleRefs } from '../utils/assign-ref';
13
13
  import { useOnClickOutside } from '../hooks/useOnClickOutside';
14
14
  import { useScope } from '../hooks';
15
- import { queryScope } from './scope';
15
+ import { MENU_SUBMENU_TRIGGER_ATTR, queryScope } from './scope';
16
16
  import { getCircularIndex, wrapEvent } from '../utils';
17
17
 
18
18
  const useEnhancedEffect =
@@ -44,10 +44,12 @@ export const MenuList = forwardRef<HTMLUListElement, MenuListProps>(
44
44
  const {
45
45
  menuListIdRef,
46
46
  buttonRef,
47
- onChange,
47
+ closeMenu,
48
48
  openWithArrowKeyRef,
49
49
  open,
50
50
  isContextMenu,
51
+ menuBar,
52
+ menuBarMenuId,
51
53
  } = useMenuContext();
52
54
 
53
55
  const [navigationItem, setNavigationItem] = useState<
@@ -59,6 +61,15 @@ export const MenuList = forwardRef<HTMLUListElement, MenuListProps>(
59
61
  const menuListRef = useRef<HTMLUListElement | null>(null);
60
62
 
61
63
  const scope = useScope<HTMLLIElement, HTMLUListElement>(menuListRef);
64
+ const getAllItems = useCallback(
65
+ () =>
66
+ scope.current.queryAllNodes(
67
+ (type, props, instance) =>
68
+ queryScope(type, props) &&
69
+ instance.closest('[data-menu-list]') === menuListRef.current
70
+ ),
71
+ [scope]
72
+ );
62
73
 
63
74
  const onNavigate = (el: HTMLElement) => {
64
75
  el.focus({ preventScroll: true });
@@ -69,7 +80,7 @@ export const MenuList = forwardRef<HTMLUListElement, MenuListProps>(
69
80
 
70
81
  useEnhancedEffect(() => {
71
82
  if (!mounted) {
72
- const allItems = scope.current.queryAllNodes(queryScope);
83
+ const allItems = getAllItems();
73
84
  let index = getCircularIndex(
74
85
  openWithArrowKeyRef.current === 'ArrowUp' ? -1 : 0,
75
86
  allItems.length
@@ -107,7 +118,7 @@ export const MenuList = forwardRef<HTMLUListElement, MenuListProps>(
107
118
  navigationItem,
108
119
  onNavigate,
109
120
  openWithArrowKeyRef,
110
- scope,
121
+ getAllItems,
111
122
  defaultActiveItemValue,
112
123
  ]);
113
124
 
@@ -122,7 +133,7 @@ export const MenuList = forwardRef<HTMLUListElement, MenuListProps>(
122
133
  }
123
134
 
124
135
  if (e.button === 0) {
125
- onChange(e as any, false);
136
+ closeMenu(e);
126
137
  }
127
138
  } else {
128
139
  if (
@@ -130,24 +141,52 @@ export const MenuList = forwardRef<HTMLUListElement, MenuListProps>(
130
141
  e.target !== buttonRef.current &&
131
142
  !buttonRef.current?.contains(e.target)
132
143
  ) {
133
- onChange(e as any, false);
144
+ closeMenu(e);
134
145
  }
135
146
  }
136
147
  e.preventDefault();
137
148
  },
138
- [buttonRef, isContextMenu, onChange]
149
+ [buttonRef, isContextMenu, closeMenu]
139
150
  );
140
151
 
141
152
  useOnClickOutside(menuListRef, handleClickOutside, open);
142
153
 
143
154
  function handleKeyDown(e: KeyboardEvent<HTMLUListElement>) {
155
+ const isHorizontalMenuBarMenu =
156
+ menuBar && menuBarMenuId && menuBar.orientation === 'horizontal';
157
+
144
158
  switch (e.key) {
145
159
  case 'Escape':
146
160
  case 'Tab': {
147
- onChange(e, false);
161
+ closeMenu(e, { focusTrigger: true });
148
162
  e.preventDefault(); // prevents focusing on next element, because we will be handling it
149
163
  itemSearchStr.current = '';
150
- buttonRef.current?.focus();
164
+ break;
165
+ }
166
+ case 'ArrowRight': {
167
+ if (isHorizontalMenuBarMenu) {
168
+ menuBar!.moveFocus(menuBarMenuId!, 1, e, {
169
+ open: true,
170
+ focusKey: 'ArrowDown',
171
+ });
172
+ e.preventDefault();
173
+ itemSearchStr.current = '';
174
+ }
175
+ break;
176
+ }
177
+ case 'ArrowLeft': {
178
+ if (buttonRef.current?.hasAttribute(MENU_SUBMENU_TRIGGER_ATTR)) {
179
+ closeMenu(e, { focusTrigger: true });
180
+ e.preventDefault();
181
+ itemSearchStr.current = '';
182
+ } else if (isHorizontalMenuBarMenu) {
183
+ menuBar!.moveFocus(menuBarMenuId!, -1, e, {
184
+ open: true,
185
+ focusKey: 'ArrowDown',
186
+ });
187
+ e.preventDefault();
188
+ itemSearchStr.current = '';
189
+ }
151
190
  break;
152
191
  }
153
192
  case 'Home':
@@ -156,7 +195,7 @@ export const MenuList = forwardRef<HTMLUListElement, MenuListProps>(
156
195
  case 'ArrowUp':
157
196
  e.preventDefault();
158
197
  itemSearchStr.current = '';
159
- const allItems = scope ? scope.current.queryAllNodes(queryScope) : [];
198
+ const allItems = getAllItems();
160
199
  const currentIndex = allItems.findIndex((e) => e === navigationItem);
161
200
  if (allItems.length === 0) {
162
201
  return;
@@ -196,9 +235,7 @@ export const MenuList = forwardRef<HTMLUListElement, MenuListProps>(
196
235
  itemSearchStr.current = '';
197
236
  }, 500);
198
237
 
199
- const allItems = scope
200
- ? scope.current.queryAllNodes(queryScope)
201
- : [];
238
+ const allItems = getAllItems();
202
239
  const currentIndex = allItems.findIndex(
203
240
  (e) => e === navigationItem
204
241
  );
@@ -4,6 +4,7 @@ import { forwardRef } from 'react';
4
4
  import type { PopperProps } from '../Popper';
5
5
  import { Popper } from '../Popper';
6
6
  import { useMenuContext } from './context';
7
+ import { MENU_SUBMENU_TRIGGER_ATTR } from './scope';
7
8
 
8
9
  export interface MenuPopoverProps extends Omit<PopperProps, 'anchorEl'> {
9
10
  as?: ElementType<any>;
@@ -13,8 +14,17 @@ export interface MenuPopoverProps extends Omit<PopperProps, 'anchorEl'> {
13
14
 
14
15
  export const MenuPopover = forwardRef<HTMLDivElement, MenuPopoverProps>(
15
16
  function MenuPopover(props, forwardedRef) {
16
- const { as = 'div', innerAs, ...otherProps } = props;
17
+ const { as = 'div', innerAs, placement, ...otherProps } = props;
17
18
  const { buttonRef, open, offsetFn, isContextMenu } = useMenuContext();
19
+ const getDefaultPlacement = () => {
20
+ if (buttonRef.current?.hasAttribute(MENU_SUBMENU_TRIGGER_ATTR)) {
21
+ return 'right-start';
22
+ }
23
+ if (isContextMenu.current) {
24
+ return 'bottom-start';
25
+ }
26
+ return undefined;
27
+ };
18
28
 
19
29
  if (!open) {
20
30
  return null;
@@ -27,7 +37,7 @@ export const MenuPopover = forwardRef<HTMLDivElement, MenuPopoverProps>(
27
37
  ref={forwardedRef}
28
38
  anchorEl={buttonRef}
29
39
  offsetFn={offsetFn}
30
- placement={isContextMenu.current ? 'bottom-start' : undefined}
40
+ placement={placement ?? getDefaultPlacement()}
31
41
  {...otherProps}
32
42
  />
33
43
  );
@@ -0,0 +1,167 @@
1
+ import type {
2
+ FocusEventHandler,
3
+ KeyboardEvent,
4
+ KeyboardEventHandler,
5
+ LiHTMLAttributes,
6
+ ElementType,
7
+ MouseEvent,
8
+ MouseEventHandler,
9
+ PointerEvent,
10
+ PointerEventHandler,
11
+ } from 'react';
12
+ import { forwardRef, useEffect, useId, useRef } from 'react';
13
+
14
+ import { useMenuContext, useMenuListContext } from './context';
15
+ import { MENU_SUBMENU_TRIGGER_ATTR } from './scope';
16
+ import { assignMultipleRefs } from '../utils/assign-ref';
17
+ import { wrapEvent } from '../utils';
18
+
19
+ export interface MenuSubmenuTriggerProps
20
+ extends LiHTMLAttributes<HTMLLIElement> {
21
+ as?: ElementType<any>;
22
+ innerAs?: ElementType<any>;
23
+ disabled?: boolean;
24
+ }
25
+
26
+ export const MenuSubmenuTrigger = forwardRef<any, MenuSubmenuTriggerProps>(
27
+ function MenuSubmenuTrigger(props, forwardedRef) {
28
+ const {
29
+ as: Comp = 'li',
30
+ innerAs,
31
+ disabled,
32
+ onClick,
33
+ onFocus,
34
+ onKeyDown,
35
+ onMouseMove,
36
+ onPointerUp,
37
+ ...otherProps
38
+ } = props;
39
+ const {
40
+ buttonRef,
41
+ menuListIdRef,
42
+ open,
43
+ onChange,
44
+ openWithArrowKeyRef,
45
+ } = useMenuContext();
46
+ const { navigationItem, onNavigate } = useMenuListContext();
47
+ const ref = useRef<HTMLLIElement | null>(null);
48
+ const itemId = useId();
49
+
50
+ const isActive = ref.current && ref.current === navigationItem;
51
+
52
+ const setTrigger = () => {
53
+ if (ref.current) {
54
+ buttonRef.current = ref.current;
55
+ }
56
+ };
57
+
58
+ const createMenuTriggerEvent = () =>
59
+ ({
60
+ // This close path is only used when roving focus leaves the trigger.
61
+ // Internal state transitions only need currentTarget/target plus
62
+ // preventDefault/defaultPrevented for wrapEvent compatibility.
63
+ currentTarget: ref.current,
64
+ target: ref.current,
65
+ preventDefault() {},
66
+ defaultPrevented: false,
67
+ }) as unknown as Parameters<typeof onChange>[0];
68
+
69
+ const openSubmenu = (
70
+ e:
71
+ | MouseEvent<HTMLLIElement>
72
+ | KeyboardEvent<HTMLLIElement>
73
+ | PointerEvent<HTMLLIElement>,
74
+ focusFirstItem = false
75
+ ) => {
76
+ if (disabled || !ref.current) {
77
+ return;
78
+ }
79
+ setTrigger();
80
+ onNavigate && onNavigate(ref.current);
81
+ if (focusFirstItem) {
82
+ openWithArrowKeyRef.current = 'ArrowDown';
83
+ }
84
+ onChange(e, true);
85
+ };
86
+
87
+ useEffect(() => {
88
+ if (!open || !ref.current) {
89
+ return;
90
+ }
91
+ if (navigationItem && navigationItem !== ref.current) {
92
+ onChange(createMenuTriggerEvent(), false);
93
+ }
94
+ }, [navigationItem, onChange, open]);
95
+
96
+ const handleClick = (e: MouseEvent<HTMLLIElement>) => {
97
+ openSubmenu(e);
98
+ e.preventDefault();
99
+ };
100
+
101
+ const handleFocus: FocusEventHandler<HTMLLIElement> = () => {
102
+ if (!disabled && ref.current && ref.current !== navigationItem) {
103
+ onNavigate && onNavigate(ref.current);
104
+ }
105
+ setTrigger();
106
+ };
107
+
108
+ const handleKeyDown: KeyboardEventHandler<HTMLLIElement> = (e) => {
109
+ switch (e.key) {
110
+ case ' ':
111
+ case 'Enter':
112
+ openSubmenu(e, true);
113
+ e.preventDefault();
114
+ break;
115
+ case 'ArrowRight':
116
+ if (!open) {
117
+ openSubmenu(e, true);
118
+ e.preventDefault();
119
+ }
120
+ break;
121
+ }
122
+ };
123
+
124
+ const handleMouseMove: MouseEventHandler<HTMLLIElement> = (e) => {
125
+ if (!disabled && ref.current && ref.current !== navigationItem) {
126
+ onNavigate && onNavigate(ref.current);
127
+ }
128
+ if (!disabled && !open) {
129
+ openSubmenu(e);
130
+ } else {
131
+ setTrigger();
132
+ }
133
+ };
134
+
135
+ const handlePointerUp: PointerEventHandler<HTMLLIElement> = (e) => {
136
+ if (e.pointerType === 'touch') {
137
+ openSubmenu(e);
138
+ e.preventDefault();
139
+ }
140
+ };
141
+
142
+ return (
143
+ <Comp
144
+ ref={assignMultipleRefs(ref, forwardedRef)}
145
+ as={innerAs}
146
+ id={disabled ? undefined : itemId}
147
+ {...{ [MENU_SUBMENU_TRIGGER_ATTR]: '' }}
148
+ data-menu-submenu-trigger=""
149
+ data-highlighted={isActive ? '' : undefined}
150
+ role="menuitem"
151
+ aria-haspopup="menu"
152
+ aria-controls={menuListIdRef.current}
153
+ aria-expanded={open ? true : undefined}
154
+ onClick={wrapEvent(onClick, handleClick)}
155
+ tabIndex={disabled ? -1 : 0}
156
+ onFocus={wrapEvent(onFocus, handleFocus)}
157
+ onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
158
+ onMouseMove={wrapEvent(onMouseMove, handleMouseMove)}
159
+ onPointerUp={wrapEvent(onPointerUp, handlePointerUp)}
160
+ data-disabled={disabled ? '' : undefined}
161
+ aria-disabled={disabled ? '' : undefined}
162
+ disabled={disabled}
163
+ {...otherProps}
164
+ />
165
+ );
166
+ }
167
+ );
@@ -4,34 +4,44 @@ import type {
4
4
  KeyboardEvent,
5
5
  MouseEvent,
6
6
  MutableRefObject,
7
- PointerEvent,
7
+ PointerEvent as ReactPointerEvent,
8
8
  SetStateAction,
9
9
  } from 'react';
10
10
  import { createContext, useContext } from 'react';
11
11
 
12
+ import type { MenuBarContextProps } from '../MenuBar/context';
13
+
12
14
  export type ItemObject = { text: string; value: any; id: string | undefined };
15
+ export type MenuTriggerEvent =
16
+ | KeyboardEvent<any>
17
+ | MouseEvent<any>
18
+ | ReactPointerEvent<any>
19
+ | PointerEvent;
13
20
 
14
21
  // MenuRoot
15
22
  export interface MenuContextProps {
16
23
  buttonRef: MutableRefObject<HTMLElement | null>;
17
24
  menuListIdRef: MutableRefObject<undefined | string>;
18
25
  openWithArrowKeyRef: MutableRefObject<string | null>;
19
- onChange: (
20
- e:
21
- | KeyboardEvent<HTMLElement>
22
- | MouseEvent<HTMLElement>
23
- | PointerEvent<HTMLElement>,
24
- isOpen: boolean
25
- ) => void;
26
+ onChange: (e: MenuTriggerEvent, isOpen: boolean) => void;
26
27
  open: boolean;
27
28
  offsetFn: OffsetsFunction | undefined;
28
29
  setOffsetFn: Dispatch<SetStateAction<OffsetsFunction | undefined>>;
29
30
  isContextMenu: MutableRefObject<boolean>;
31
+ parentMenu: MenuContextProps | null;
32
+ menuBar: MenuBarContextProps | null;
33
+ menuBarMenuId: string | null;
34
+ closeMenu: (
35
+ e: MenuTriggerEvent,
36
+ options?: { focusTrigger?: boolean }
37
+ ) => void;
38
+ closeAllMenus: (e: MenuTriggerEvent) => void;
30
39
  }
31
40
 
32
- const menuContext = createContext<MenuContextProps>(null as any);
41
+ const menuContext = createContext<MenuContextProps | null>(null);
33
42
  export const { Provider: MenuProvider } = menuContext;
34
- export const useMenuContext = () => useContext(menuContext);
43
+ export const useMenuContext = () => useContext(menuContext) as MenuContextProps;
44
+ export const useOptionalMenuContext = () => useContext(menuContext);
35
45
 
36
46
  // MenuList
37
47
  export interface MenuListContextProps {
package/src/Menu/index.ts CHANGED
@@ -4,3 +4,6 @@ export * from './ContextMenuTrigger';
4
4
  export * from './MenuItem';
5
5
  export * from './MenuList';
6
6
  export * from './MenuPopover';
7
+ export { Menu as MenuSubmenu } from './Menu';
8
+ export type { MenuProps as MenuSubmenuProps } from './Menu';
9
+ export * from './MenuSubmenuTrigger';
package/src/Menu/scope.ts CHANGED
@@ -1,6 +1,9 @@
1
+ export const MENU_SUBMENU_TRIGGER_ATTR = 'data-menu-submenu-trigger';
2
+
1
3
  export function queryScope(type: string, props: Record<string, unknown>) {
2
4
  return (
3
- props['data-menu-item'] === '' &&
5
+ (props['data-menu-item'] === '' ||
6
+ props[MENU_SUBMENU_TRIGGER_ATTR] === '') &&
4
7
  props['data-disabled'] !== '' &&
5
8
  props['data-disabled'] !== true
6
9
  );
@@ -1,32 +1,71 @@
1
- [data-menu-item] {
2
- padding: 8px;
1
+ [data-menu-button] {
2
+ box-sizing: border-box;
3
+ height: 2rem;
4
+ margin: 0;
5
+ padding: 0 0.75rem;
6
+ border: 0;
7
+ background: transparent;
8
+ color: #1f1f1f;
9
+ font: sans-serif;
10
+ cursor: default;
11
+ user-select: none;
12
+ }
13
+
14
+ [data-menu-button]:hover:not([disabled]),
15
+ [data-menu-button]:focus-visible,
16
+ [data-menu-button][aria-expanded='true'] {
17
+ background: #f0f0f0;
18
+ }
19
+
20
+ [data-menu-list] {
21
+ font-family: sans-serif;
22
+ min-width: 180px;
23
+ margin: 0;
24
+ padding: 4px 0;
25
+ border: 1px solid #d8d8d8;
26
+ background: white;
27
+ color: #1f1f1f;
28
+ box-shadow: 4px 4px 0 rgb(0 0 0 / 10%);
3
29
  list-style: none;
4
- cursor: pointer;
5
30
  }
6
31
 
7
- [data-menu-item][data-highlighted] {
8
- background-color: #e7e7e7;
32
+ [data-menu-list]:focus {
33
+ outline: 0;
9
34
  }
10
35
 
11
- [data-menu-item][data-highlighted]:hover {
12
- background-color: #d7d7d7;
36
+ [data-menu-list] > hr {
37
+ height: 1px;
38
+ margin: 4px;
39
+ border: 0;
40
+ background: #d8d8d8;
13
41
  }
14
42
 
15
- [data-menu-item]:hover {
16
- background-color: #f3f3f3;
43
+ [data-menu-item],
44
+ [data-menu-submenu-trigger] {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 1rem;
48
+ padding: 0.5rem 1rem;
49
+ outline: 0;
50
+ list-style: none;
51
+ cursor: default;
52
+ user-select: none;
17
53
  }
18
54
 
19
- [data-menu-item][data-disabled] {
20
- background-color: #ffffff;
21
- color: #777;
22
- cursor: not-allowed;
55
+ [data-menu-submenu-trigger] {
56
+ padding-right: 0.5rem;
23
57
  }
24
58
 
25
- [data-menu-list] {
26
- margin: 0;
27
- padding: 0;
28
- box-shadow: 0px 2px 6px hsla(0, 0%, 0%, 0.15);
29
- border-radius: 3px;
59
+ [data-menu-item][data-highlighted],
60
+ [data-menu-submenu-trigger][data-highlighted] {
61
+ background: #1f1f1f;
62
+ color: white;
63
+ }
64
+
65
+ [data-menu-item][data-disabled],
66
+ [data-menu-submenu-trigger][data-disabled] {
67
+ color: #777;
68
+ cursor: not-allowed;
30
69
  }
31
70
 
32
71
  [data-popper-placement='top'] [data-menu-list] {
@@ -36,7 +75,3 @@
36
75
  [data-popper-placement='bottom'] [data-menu-list] {
37
76
  transform-origin: top center;
38
77
  }
39
-
40
- [data-menu-list]:focus {
41
- outline: none;
42
- }
@@ -0,0 +1,92 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import {
4
+ Menu,
5
+ MenuButton,
6
+ MenuItem,
7
+ MenuList,
8
+ MenuPopover,
9
+ MenuSubmenu,
10
+ MenuSubmenuTrigger,
11
+ } from '../Menu';
12
+ import { MenuBar } from './MenuBar';
13
+ import './styles.css';
14
+
15
+ export default {
16
+ title: 'components/MenuBar',
17
+ };
18
+
19
+ const Divider = () => <li role="separator" />;
20
+
21
+ const MenuEntry = ({ children }: { children: ReactNode }) => (
22
+ <MenuItem>{children}</MenuItem>
23
+ );
24
+
25
+ export const Basic = () => (
26
+ <div className="MenuBarStory-frame">
27
+ <MenuBar>
28
+ <Menu>
29
+ <MenuButton>File</MenuButton>
30
+ <MenuPopover>
31
+ <MenuList>
32
+ <MenuEntry>New</MenuEntry>
33
+ <MenuEntry>Open</MenuEntry>
34
+ <MenuEntry>Save</MenuEntry>
35
+ <MenuSubmenu>
36
+ <MenuSubmenuTrigger>
37
+ <span>Export</span>
38
+ <span aria-hidden="true">&rsaquo;</span>
39
+ <MenuPopover>
40
+ <MenuList>
41
+ <MenuEntry>Email</MenuEntry>
42
+ <MenuEntry>Copy link</MenuEntry>
43
+ <MenuEntry>Duplicate</MenuEntry>
44
+ </MenuList>
45
+ </MenuPopover>
46
+ </MenuSubmenuTrigger>
47
+ </MenuSubmenu>
48
+ <Divider />
49
+ <MenuEntry>Archive</MenuEntry>
50
+ </MenuList>
51
+ </MenuPopover>
52
+ </Menu>
53
+
54
+ <Menu>
55
+ <MenuButton>Edit</MenuButton>
56
+ <MenuPopover>
57
+ <MenuList>
58
+ <MenuEntry>Undo</MenuEntry>
59
+ <MenuEntry>Redo</MenuEntry>
60
+ <MenuEntry>Cut</MenuEntry>
61
+ <MenuEntry>Copy</MenuEntry>
62
+ <MenuEntry>Paste</MenuEntry>
63
+ </MenuList>
64
+ </MenuPopover>
65
+ </Menu>
66
+
67
+ <Menu>
68
+ <MenuButton>View</MenuButton>
69
+ <MenuPopover>
70
+ <MenuList>
71
+ <MenuEntry>Zoom In</MenuEntry>
72
+ <MenuEntry>Zoom Out</MenuEntry>
73
+ <MenuEntry>Reset Zoom</MenuEntry>
74
+ <MenuEntry>Full Screen</MenuEntry>
75
+ <MenuSubmenu>
76
+ <MenuSubmenuTrigger>
77
+ <span>Layout</span>
78
+ <span aria-hidden="true">&rsaquo;</span>
79
+ <MenuPopover>
80
+ <MenuList>
81
+ <MenuEntry>Single page</MenuEntry>
82
+ <MenuEntry>Two pages</MenuEntry>
83
+ </MenuList>
84
+ </MenuPopover>
85
+ </MenuSubmenuTrigger>
86
+ </MenuSubmenu>
87
+ </MenuList>
88
+ </MenuPopover>
89
+ </Menu>
90
+ </MenuBar>
91
+ </div>
92
+ );