@basic-ui/core 0.0.60 → 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 (214) 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/Tree/Tree.d.ts +3 -0
  100. package/build/esm/Tree/Tree.d.ts.map +1 -0
  101. package/build/esm/Tree/Tree.js +730 -0
  102. package/build/esm/Tree/Tree.js.map +1 -0
  103. package/build/esm/Tree/TreeHeader.d.ts +3 -0
  104. package/build/esm/Tree/TreeHeader.d.ts.map +1 -0
  105. package/build/esm/Tree/TreeHeader.js +5 -0
  106. package/build/esm/Tree/TreeHeader.js.map +1 -0
  107. package/build/esm/Tree/TreeItem.d.ts +3 -0
  108. package/build/esm/Tree/TreeItem.d.ts.map +1 -0
  109. package/build/esm/Tree/TreeItem.js +5 -0
  110. package/build/esm/Tree/TreeItem.js.map +1 -0
  111. package/build/esm/Tree/TreeItemContent.d.ts +3 -0
  112. package/build/esm/Tree/TreeItemContent.d.ts.map +1 -0
  113. package/build/esm/Tree/TreeItemContent.js +69 -0
  114. package/build/esm/Tree/TreeItemContent.js.map +1 -0
  115. package/build/esm/Tree/TreeSection.d.ts +3 -0
  116. package/build/esm/Tree/TreeSection.d.ts.map +1 -0
  117. package/build/esm/Tree/TreeSection.js +5 -0
  118. package/build/esm/Tree/TreeSection.js.map +1 -0
  119. package/build/esm/Tree/collection.d.ts +18 -0
  120. package/build/esm/Tree/collection.d.ts.map +1 -0
  121. package/build/esm/Tree/collection.js +252 -0
  122. package/build/esm/Tree/collection.js.map +1 -0
  123. package/build/esm/Tree/context.d.ts +3 -0
  124. package/build/esm/Tree/context.d.ts.map +1 -0
  125. package/build/esm/Tree/context.js +3 -0
  126. package/build/esm/Tree/context.js.map +1 -0
  127. package/build/esm/Tree/index.d.ts +8 -0
  128. package/build/esm/Tree/index.d.ts.map +1 -0
  129. package/build/esm/Tree/index.js +7 -0
  130. package/build/esm/Tree/index.js.map +1 -0
  131. package/build/esm/Tree/types.d.ts +128 -0
  132. package/build/esm/Tree/types.d.ts.map +1 -0
  133. package/build/esm/Tree/types.js +2 -0
  134. package/build/esm/Tree/types.js.map +1 -0
  135. package/build/esm/hooks/index.d.ts +1 -0
  136. package/build/esm/hooks/index.d.ts.map +1 -1
  137. package/build/esm/hooks/index.js +1 -0
  138. package/build/esm/hooks/index.js.map +1 -1
  139. package/build/esm/hooks/useTransitionStatus.d.ts +7 -0
  140. package/build/esm/hooks/useTransitionStatus.d.ts.map +1 -0
  141. package/build/esm/hooks/useTransitionStatus.js +48 -0
  142. package/build/esm/hooks/useTransitionStatus.js.map +1 -0
  143. package/build/esm/index.d.ts +5 -0
  144. package/build/esm/index.d.ts.map +1 -1
  145. package/build/esm/index.js +5 -0
  146. package/build/esm/index.js.map +1 -1
  147. package/build/esm/toggle/Toggle.d.ts +28 -0
  148. package/build/esm/toggle/Toggle.d.ts.map +1 -0
  149. package/build/esm/toggle/Toggle.js +55 -0
  150. package/build/esm/toggle/Toggle.js.map +1 -0
  151. package/build/esm/toggle/index.d.ts +2 -0
  152. package/build/esm/toggle/index.d.ts.map +1 -0
  153. package/build/esm/toggle/index.js +2 -0
  154. package/build/esm/toggle/index.js.map +1 -0
  155. package/build/esm/utils/assign-ref.d.ts +3 -3
  156. package/build/esm/utils/assign-ref.d.ts.map +1 -1
  157. package/build/esm/utils/assign-ref.js +1 -1
  158. package/build/esm/utils/assign-ref.js.map +1 -1
  159. package/build/tsconfig-build.tsbuildinfo +1 -1
  160. package/build/tsconfig.tsbuildinfo +1 -1
  161. package/package.json +7 -4
  162. package/src/Accordion/AccordionBody.tsx +6 -35
  163. package/src/Accordion/AccordionHeader.tsx +29 -103
  164. package/src/Accordion/AccordionItem.tsx +40 -29
  165. package/src/Accordion/context.ts +0 -18
  166. package/src/Accordion/scopeQuery.ts +4 -0
  167. package/src/Collapsible/Collapsible.story.tsx +153 -0
  168. package/src/Collapsible/Collapsible.tsx +79 -0
  169. package/src/Collapsible/CollapsiblePanel.tsx +103 -0
  170. package/src/Collapsible/CollapsibleTrigger.tsx +60 -0
  171. package/src/Collapsible/context.ts +28 -0
  172. package/src/Collapsible/index.ts +3 -0
  173. package/src/Menu/Menu.story.tsx +70 -1
  174. package/src/Menu/Menu.tsx +141 -65
  175. package/src/Menu/MenuButton.tsx +115 -9
  176. package/src/Menu/MenuItem.tsx +20 -3
  177. package/src/Menu/MenuList.tsx +50 -13
  178. package/src/Menu/MenuPopover.tsx +12 -2
  179. package/src/Menu/MenuSubmenuTrigger.tsx +167 -0
  180. package/src/Menu/context.ts +20 -10
  181. package/src/Menu/index.ts +3 -0
  182. package/src/Menu/scope.ts +4 -1
  183. package/src/Menu/styles.css +57 -22
  184. package/src/MenuBar/MenuBar.story.tsx +92 -0
  185. package/src/MenuBar/MenuBar.tsx +236 -0
  186. package/src/MenuBar/context.ts +46 -0
  187. package/src/MenuBar/index.ts +1 -0
  188. package/src/MenuBar/styles.css +78 -0
  189. package/src/Slider/Slider.story.tsx +1 -1
  190. package/src/Slider/Slider.tsx +145 -8
  191. package/src/Toggle/Toggle.story.tsx +42 -0
  192. package/src/Toggle/Toggle.tsx +95 -0
  193. package/src/Toggle/index.ts +1 -0
  194. package/src/Toggle/styles.css +39 -0
  195. package/src/ToggleGroup/ToggleGroup.story.tsx +86 -0
  196. package/src/ToggleGroup/ToggleGroup.tsx +185 -0
  197. package/src/ToggleGroup/ToggleGroupContext.ts +17 -0
  198. package/src/ToggleGroup/index.ts +2 -0
  199. package/src/ToggleGroup/styles.css +66 -0
  200. package/src/Tree/Tree.story.tsx +221 -0
  201. package/src/Tree/Tree.tsx +1081 -0
  202. package/src/Tree/TreeHeader.tsx +9 -0
  203. package/src/Tree/TreeItem.tsx +9 -0
  204. package/src/Tree/TreeItemContent.tsx +91 -0
  205. package/src/Tree/TreeSection.tsx +9 -0
  206. package/src/Tree/collection.tsx +371 -0
  207. package/src/Tree/context.ts +6 -0
  208. package/src/Tree/index.ts +7 -0
  209. package/src/Tree/styles.css +135 -0
  210. package/src/Tree/types.ts +161 -0
  211. package/src/hooks/index.ts +1 -0
  212. package/src/hooks/useTransitionStatus.ts +65 -0
  213. package/src/index.ts +5 -0
  214. package/src/utils/assign-ref.ts +4 -4
@@ -0,0 +1,60 @@
1
+ import type { ElementType, ReactNode, MouseEvent, KeyboardEvent, HTMLAttributes } from 'react';
2
+ import { forwardRef, Fragment } from 'react';
3
+ import { useCollapsibleContext } from './context';
4
+ import { wrapEvent } from '../utils';
5
+
6
+ export interface CollapsibleTriggerProps extends HTMLAttributes<HTMLElement> {
7
+ as?: ElementType<any>;
8
+ innerAs?: ElementType<any>;
9
+ children?: ReactNode;
10
+ onClick?: (e: MouseEvent<any>) => void;
11
+ onKeyDown?: (e: KeyboardEvent<any>) => void;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ export const CollapsibleTrigger = forwardRef<HTMLElement, CollapsibleTriggerProps>(
16
+ function CollapsibleTrigger(props, forwardedRef) {
17
+ const {
18
+ as: Comp = 'button',
19
+ innerAs,
20
+ onClick,
21
+ onKeyDown,
22
+ disabled,
23
+ ...otherProps
24
+ } = props;
25
+
26
+ const { open, panelId, disabled: contextDisabled, onChange } = useCollapsibleContext();
27
+ const isDisabled = disabled ?? contextDisabled;
28
+
29
+ const handleClick = (e: MouseEvent<any>) => {
30
+ if (isDisabled) {
31
+ e.preventDefault();
32
+ return;
33
+ }
34
+ onChange(e, !open);
35
+ };
36
+
37
+ const handleKeyDown = (e: KeyboardEvent<any>) => {
38
+ if (Comp !== 'button' && (e.key === 'Enter' || e.key === ' ')) {
39
+ e.preventDefault();
40
+ handleClick(e as unknown as MouseEvent<any>);
41
+ }
42
+ };
43
+
44
+ return (
45
+ <Comp
46
+ {...(Comp !== Fragment ? { as: innerAs, ref: forwardedRef } : {})}
47
+ type={Comp === 'button' ? 'button' : undefined}
48
+ aria-controls={open ? panelId : undefined}
49
+ aria-expanded={open}
50
+ disabled={isDisabled}
51
+ data-open={open ? '' : undefined}
52
+ data-disabled={isDisabled ? '' : undefined}
53
+ data-state={open ? 'open' : 'closed'}
54
+ onClick={wrapEvent(onClick, handleClick)}
55
+ onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
56
+ {...otherProps}
57
+ />
58
+ );
59
+ }
60
+ );
@@ -0,0 +1,28 @@
1
+ import type { Dispatch, SetStateAction } from 'react';
2
+ import { createContext, useContext } from 'react';
3
+ import type { CollapsibleTriggerEvent } from './Collapsible';
4
+
5
+ import type { TransitionStatus } from '../hooks';
6
+
7
+ export interface CollapsibleContextProps {
8
+ open: boolean;
9
+ mounted: boolean;
10
+ transitionStatus: TransitionStatus;
11
+ setMounted: Dispatch<SetStateAction<boolean>>;
12
+ panelId: string | undefined;
13
+ disabled: boolean;
14
+ onChange: (e: CollapsibleTriggerEvent, isOpen: boolean) => void;
15
+ setPanelId: Dispatch<SetStateAction<string | undefined>>;
16
+ }
17
+
18
+ const CollapsibleContext = createContext<CollapsibleContextProps | null>(null);
19
+
20
+ export const CollapsibleProvider = CollapsibleContext.Provider;
21
+
22
+ export const useCollapsibleContext = () => {
23
+ const context = useContext(CollapsibleContext);
24
+ if (!context) {
25
+ throw new Error('Collapsible components must be used within a <Collapsible>');
26
+ }
27
+ return context;
28
+ };
@@ -0,0 +1,3 @@
1
+ export * from './Collapsible';
2
+ export * from './CollapsibleTrigger';
3
+ export * from './CollapsiblePanel';
@@ -2,7 +2,15 @@ import type { MouseEvent, ReactNode } from 'react';
2
2
  import { useState } from 'react';
3
3
  import { animated, useSpring } from 'react-spring';
4
4
 
5
- import { Menu, MenuButton, MenuItem, MenuList, MenuPopover } from './';
5
+ import {
6
+ Menu,
7
+ MenuButton,
8
+ MenuItem,
9
+ MenuList,
10
+ MenuPopover,
11
+ MenuSubmenu,
12
+ MenuSubmenuTrigger,
13
+ } from './';
6
14
  import './styles.css';
7
15
 
8
16
  export default {
@@ -141,6 +149,61 @@ const MenuWithMultipleButtons = () => {
141
149
  );
142
150
  };
143
151
 
152
+ const MenuWithSubmenu = () => {
153
+ return (
154
+ <Menu>
155
+ <MenuButton>Click me</MenuButton>
156
+ <MenuPopover>
157
+ <MenuList>
158
+ <MenuItem>New file</MenuItem>
159
+ <MenuSubmenu>
160
+ <MenuSubmenuTrigger
161
+ style={{
162
+ display: 'flex',
163
+ alignItems: 'center',
164
+ justifyContent: 'space-between',
165
+ gap: 12,
166
+ }}
167
+ >
168
+ <span>Share</span>
169
+ <span aria-hidden="true">›</span>
170
+ <MenuPopover>
171
+ <MenuList>
172
+ <MenuItem>Email</MenuItem>
173
+ <MenuItem>Copy link</MenuItem>
174
+ <MenuItem>Duplicate</MenuItem>
175
+ <MenuSubmenu>
176
+ <MenuSubmenuTrigger
177
+ style={{
178
+ display: 'flex',
179
+ alignItems: 'center',
180
+ justifyContent: 'space-between',
181
+ gap: 12,
182
+ }}
183
+ >
184
+ <span>More options</span>
185
+ <span aria-hidden="true">›</span>
186
+ <MenuPopover>
187
+ <MenuList>
188
+ <MenuItem>Airdrop</MenuItem>
189
+ <MenuItem>Telegram</MenuItem>
190
+ <MenuItem>Signal</MenuItem>
191
+ <MenuItem>Discord</MenuItem>
192
+ </MenuList>
193
+ </MenuPopover>
194
+ </MenuSubmenuTrigger>
195
+ </MenuSubmenu>
196
+ </MenuList>
197
+ </MenuPopover>
198
+ </MenuSubmenuTrigger>
199
+ </MenuSubmenu>
200
+ <MenuItem>Delete</MenuItem>
201
+ </MenuList>
202
+ </MenuPopover>
203
+ </Menu>
204
+ );
205
+ };
206
+
144
207
  export const Controlled = () => (
145
208
  <Wrapper>
146
209
  <MenuControlled />
@@ -158,3 +221,9 @@ export const WithMultipleButtons = () => (
158
221
  <MenuWithMultipleButtons />
159
222
  </Wrapper>
160
223
  );
224
+
225
+ export const WithSubmenu = () => (
226
+ <Wrapper>
227
+ <MenuWithSubmenu />
228
+ </Wrapper>
229
+ );
package/src/Menu/Menu.tsx CHANGED
@@ -1,82 +1,158 @@
1
- import type {
2
- KeyboardEvent,
3
- MouseEvent,
4
- PointerEvent,
5
- ReactNode,
6
- ElementType,
1
+ import type { ReactNode, ElementType } from 'react';
2
+ import {
3
+ useState,
4
+ forwardRef,
5
+ useRef,
6
+ Fragment,
7
+ useId,
8
+ useCallback,
9
+ useEffect,
7
10
  } from 'react';
8
- import { useState, forwardRef, useRef, Fragment, useId } from 'react';
9
11
 
10
- import type { MenuContextProps } from './context';
11
- import { MenuProvider } from './context';
12
+ import type { MenuContextProps, MenuTriggerEvent } from './context';
13
+ import { MenuProvider, useOptionalMenuContext } from './context';
12
14
  import { useControlledState } from '../hooks';
13
15
  import type { OffsetsFunction } from '../Popper';
16
+ import { useOptionalMenuBarContext } from '../MenuBar/context';
14
17
 
15
18
  export interface MenuProps {
16
19
  as?: ElementType<any>;
17
20
  innerAs?: ElementType<any>;
18
21
  children?: ReactNode;
19
- onChange?: (
20
- e:
21
- | KeyboardEvent<HTMLElement>
22
- | MouseEvent<HTMLElement>
23
- | PointerEvent<HTMLElement>,
24
- isOpen: boolean
25
- ) => void;
22
+ onChange?: (e: MenuTriggerEvent, isOpen: boolean) => void;
26
23
  open?: boolean;
27
24
  defaultOpen?: boolean;
28
25
  }
29
26
 
30
- export const Menu = forwardRef<HTMLDivElement, MenuProps>(function Menu(
31
- props,
32
- forwardedRef
33
- ) {
34
- const {
35
- as: Comp = Fragment,
36
- innerAs,
37
- open: openProp,
38
- defaultOpen = false,
39
- onChange: onChangeProp,
40
- ...otherProps
41
- } = props;
42
- const menuListIdRef = useRef<string | undefined>(undefined);
43
- const openWithArrowKeyRef = useRef<'ArrowUp' | 'ArrowDown' | null>(null);
44
- const buttonRef = useRef<HTMLButtonElement>(null);
45
- const [open, onChange] = useControlledState(
46
- openProp,
47
- onChangeProp,
48
- defaultOpen,
49
- (setState) => (e, isOpen) => {
50
- setState(isOpen);
27
+ export const Menu = forwardRef<HTMLDivElement, MenuProps>(
28
+ function Menu(props, forwardedRef) {
29
+ const {
30
+ as: Comp = Fragment,
31
+ innerAs,
32
+ open: openProp,
33
+ defaultOpen = false,
34
+ onChange: onChangeProp,
35
+ ...otherProps
36
+ } = props;
37
+ const menuListIdRef = useRef<string | undefined>(undefined);
38
+ const openWithArrowKeyRef = useRef<'ArrowUp' | 'ArrowDown' | null>(null);
39
+ const buttonRef = useRef<HTMLElement>(null);
40
+ const didNotifyDefaultOpenRef = useRef(false);
41
+ const menuBar = useOptionalMenuBarContext();
42
+ const menuBarMenuId = useId();
43
+ const parentMenu = useOptionalMenuContext();
44
+ const isMenuBarRootMenu = Boolean(menuBar && !parentMenu);
45
+ const menuBarOpenMenuId = menuBar?.openMenuId;
46
+ const notifyMenuBarOpenChange = menuBar?.notifyMenuOpenChange;
47
+ const registerMenuBarMenu = menuBar?.registerMenu;
48
+ const [openState, setOpenState] = useControlledState(
49
+ openProp,
50
+ onChangeProp,
51
+ defaultOpen,
52
+ (setState) => (e, isOpen) => {
53
+ setState(isOpen);
54
+ }
55
+ );
56
+ const [offsetFn, setOffsetFn] = useState<OffsetsFunction | undefined>(
57
+ undefined
58
+ );
59
+ menuListIdRef.current = useId();
60
+ const isContextMenu = useRef(false);
61
+ const open =
62
+ isMenuBarRootMenu && openProp === undefined
63
+ ? menuBarOpenMenuId === menuBarMenuId
64
+ : openState;
65
+
66
+ const onChange = useCallback(
67
+ (e: MenuTriggerEvent, isOpen: boolean) => {
68
+ setOpenState(e, isOpen);
69
+ if (isMenuBarRootMenu && notifyMenuBarOpenChange) {
70
+ notifyMenuBarOpenChange(menuBarMenuId, isOpen);
71
+ }
72
+ },
73
+ [isMenuBarRootMenu, menuBarMenuId, notifyMenuBarOpenChange, setOpenState]
74
+ );
75
+
76
+ useEffect(() => {
77
+ if (!isMenuBarRootMenu || !registerMenuBarMenu) {
78
+ return;
79
+ }
80
+
81
+ return registerMenuBarMenu({
82
+ id: menuBarMenuId,
83
+ openWithArrowKeyRef,
84
+ onOpenChange: onChange,
85
+ });
86
+ }, [isMenuBarRootMenu, menuBarMenuId, onChange, registerMenuBarMenu]);
87
+
88
+ useEffect(() => {
89
+ if (!isMenuBarRootMenu || !notifyMenuBarOpenChange) {
90
+ return;
91
+ }
92
+
93
+ if (openProp !== undefined) {
94
+ notifyMenuBarOpenChange(menuBarMenuId, open);
95
+ return;
96
+ }
97
+
98
+ if (defaultOpen && !didNotifyDefaultOpenRef.current) {
99
+ didNotifyDefaultOpenRef.current = true;
100
+ notifyMenuBarOpenChange(menuBarMenuId, true);
101
+ }
102
+ }, [
103
+ defaultOpen,
104
+ isMenuBarRootMenu,
105
+ menuBarMenuId,
106
+ notifyMenuBarOpenChange,
107
+ open,
108
+ openProp,
109
+ ]);
110
+
111
+ if (!open && offsetFn) {
112
+ setOffsetFn(undefined);
51
113
  }
52
- );
53
- const [offsetFn, setOffsetFn] = useState<OffsetsFunction | undefined>(
54
- undefined
55
- );
56
- menuListIdRef.current = useId();
57
- const isContextMenu = useRef(false);
58
114
 
59
- if (!open && offsetFn) {
60
- setOffsetFn(undefined);
61
- }
115
+ const closeMenu: MenuContextProps['closeMenu'] = (e, options) => {
116
+ onChange(e, false);
117
+ if (options?.focusTrigger && !isContextMenu.current) {
118
+ buttonRef.current?.focus();
119
+ }
120
+ };
62
121
 
63
- const value: MenuContextProps = {
64
- menuListIdRef,
65
- openWithArrowKeyRef,
66
- open,
67
- onChange,
68
- buttonRef,
69
- offsetFn,
70
- setOffsetFn,
71
- isContextMenu,
72
- };
122
+ const closeAllMenus: MenuContextProps['closeAllMenus'] = (e) => {
123
+ onChange(e, false);
124
+ if (parentMenu) {
125
+ parentMenu.closeAllMenus(e);
126
+ return;
127
+ }
128
+ if (!isContextMenu.current) {
129
+ buttonRef.current?.focus();
130
+ }
131
+ };
73
132
 
74
- return (
75
- <MenuProvider value={value}>
76
- <Comp
77
- {...(Comp !== Fragment ? { as: innerAs, ref: forwardedRef } : {})}
78
- {...otherProps}
79
- />
80
- </MenuProvider>
81
- );
82
- });
133
+ const value: MenuContextProps = {
134
+ menuListIdRef,
135
+ openWithArrowKeyRef,
136
+ open,
137
+ onChange,
138
+ buttonRef,
139
+ offsetFn,
140
+ setOffsetFn,
141
+ isContextMenu,
142
+ parentMenu,
143
+ menuBar: isMenuBarRootMenu ? menuBar! : null,
144
+ menuBarMenuId: isMenuBarRootMenu ? menuBarMenuId : null,
145
+ closeMenu,
146
+ closeAllMenus,
147
+ };
148
+
149
+ return (
150
+ <MenuProvider value={value}>
151
+ <Comp
152
+ {...(Comp !== Fragment ? { as: innerAs, ref: forwardedRef } : {})}
153
+ {...otherProps}
154
+ />
155
+ </MenuProvider>
156
+ );
157
+ }
158
+ );
@@ -2,12 +2,14 @@ import type {
2
2
  ButtonHTMLAttributes,
3
3
  ElementType,
4
4
  KeyboardEvent,
5
+ FocusEvent,
5
6
  MouseEvent,
6
7
  } from 'react';
7
- import { forwardRef, useId } from 'react';
8
+ import { forwardRef, useCallback, useId } from 'react';
8
9
 
9
10
  import { useMenuContext } from './context';
10
11
  import { wrapEvent } from '../utils/wrap-event';
12
+ import { assignRef } from '../utils/assign-ref';
11
13
 
12
14
  export type MenuButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
13
15
  as?: ElementType<any>;
@@ -24,22 +26,89 @@ export const MenuButton = forwardRef<HTMLButtonElement, MenuButtonProps>(
24
26
  innerAs,
25
27
  id: preferredId,
26
28
  onClick,
29
+ onFocus,
27
30
  onKeyDown,
31
+ onMouseMove,
28
32
  disabled,
29
33
  ...otherProps
30
34
  } = props;
31
- const { menuListIdRef, openWithArrowKeyRef, open, buttonRef, onChange } =
32
- useMenuContext();
35
+ const {
36
+ menuListIdRef,
37
+ openWithArrowKeyRef,
38
+ open,
39
+ buttonRef,
40
+ onChange,
41
+ menuBar,
42
+ menuBarMenuId,
43
+ } = useMenuContext();
33
44
 
34
45
  const buttonIdGenerated = useId();
35
46
  const buttonId = preferredId || buttonIdGenerated;
47
+ const isMenuBarButton = Boolean(menuBar && menuBarMenuId);
48
+ const isDisabled = Boolean(disabled || menuBar?.disabled);
49
+ const registerButton = menuBar?.registerButton;
50
+
51
+ const setButtonRef = useCallback(
52
+ (node: HTMLButtonElement | null) => {
53
+ assignRef(forwardedRef, node);
54
+ assignRef(buttonRef, node);
55
+
56
+ if (isMenuBarButton && registerButton) {
57
+ registerButton(menuBarMenuId!, node, isDisabled);
58
+ }
59
+ },
60
+ [
61
+ buttonRef,
62
+ forwardedRef,
63
+ isDisabled,
64
+ isMenuBarButton,
65
+ menuBarMenuId,
66
+ registerButton,
67
+ ]
68
+ );
36
69
 
37
70
  const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
38
- if (disabled) {
71
+ if (isDisabled) {
39
72
  return;
40
73
  }
41
74
  buttonRef.current = e.currentTarget;
42
75
 
76
+ if (isMenuBarButton) {
77
+ const isHorizontal = menuBar!.orientation === 'horizontal';
78
+ const nextKey = isHorizontal ? 'ArrowRight' : 'ArrowDown';
79
+ const previousKey = isHorizontal ? 'ArrowLeft' : 'ArrowUp';
80
+ const firstMenuKey = 'Home';
81
+ const lastMenuKey = 'End';
82
+ const openFirstItemKey = isHorizontal ? 'ArrowDown' : 'ArrowRight';
83
+ const openLastItemKey = isHorizontal ? 'ArrowUp' : undefined;
84
+
85
+ switch (e.key) {
86
+ case nextKey:
87
+ case previousKey:
88
+ menuBar!.moveFocus(menuBarMenuId!, e.key === nextKey ? 1 : -1, e, {
89
+ open: menuBar!.openMenuId !== null,
90
+ focusKey: 'ArrowDown',
91
+ });
92
+ e.preventDefault();
93
+ return;
94
+ case firstMenuKey:
95
+ menuBar!.focusFirstMenu();
96
+ e.preventDefault();
97
+ return;
98
+ case lastMenuKey:
99
+ menuBar!.focusLastMenu();
100
+ e.preventDefault();
101
+ return;
102
+ case openFirstItemKey:
103
+ case openLastItemKey:
104
+ const focusKey = e.key === 'ArrowUp' ? 'ArrowUp' : 'ArrowDown';
105
+ openWithArrowKeyRef.current = focusKey;
106
+ menuBar!.openMenu(menuBarMenuId!, e, focusKey);
107
+ e.preventDefault();
108
+ return;
109
+ }
110
+ }
111
+
43
112
  const isArrowKey = () => ['ArrowUp', 'ArrowDown'].includes(e.key);
44
113
  const isEnterKey = () => [' ', 'Enter'].includes(e.key);
45
114
 
@@ -55,27 +124,64 @@ export const MenuButton = forwardRef<HTMLButtonElement, MenuButtonProps>(
55
124
  };
56
125
 
57
126
  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
58
- if (disabled) {
127
+ if (isDisabled) {
59
128
  return;
60
129
  }
61
130
  buttonRef.current = e.currentTarget;
131
+ if (isMenuBarButton) {
132
+ menuBar!.setActiveMenuId(menuBarMenuId!);
133
+ if (open) {
134
+ menuBar!.closeMenu(menuBarMenuId!, e);
135
+ } else {
136
+ menuBar!.openMenu(menuBarMenuId!, e);
137
+ }
138
+ return;
139
+ }
62
140
  onChange(e, !open);
63
141
  };
64
142
 
143
+ const handleFocus = (e: FocusEvent<HTMLButtonElement>) => {
144
+ if (!isDisabled && isMenuBarButton) {
145
+ menuBar!.setActiveMenuId(menuBarMenuId!);
146
+ buttonRef.current = e.currentTarget;
147
+ }
148
+ };
149
+
150
+ const handleMouseMove = (e: MouseEvent<HTMLButtonElement>) => {
151
+ if (!isDisabled && isMenuBarButton) {
152
+ menuBar!.setActiveMenuId(menuBarMenuId!);
153
+ buttonRef.current = e.currentTarget;
154
+
155
+ if (menuBar!.openMenuId && menuBar!.openMenuId !== menuBarMenuId) {
156
+ menuBar!.openMenu(menuBarMenuId!, e);
157
+ }
158
+ }
159
+ };
160
+
65
161
  return (
66
162
  <Comp
67
- ref={forwardedRef}
163
+ ref={setButtonRef}
68
164
  as={innerAs}
69
165
  id={buttonId}
70
- role="button"
166
+ role={isMenuBarButton ? 'menuitem' : 'button'}
71
167
  type="button"
72
- aria-haspopup={true}
168
+ aria-haspopup={isMenuBarButton ? 'menu' : true}
73
169
  aria-controls={menuListIdRef.current}
74
170
  aria-expanded={open ? true : undefined}
75
171
  data-menu-button=""
172
+ data-menubar-menu-button={isMenuBarButton ? '' : undefined}
173
+ tabIndex={
174
+ isMenuBarButton
175
+ ? menuBar!.activeMenuId === menuBarMenuId
176
+ ? 0
177
+ : -1
178
+ : undefined
179
+ }
76
180
  onClick={wrapEvent(onClick, handleClick)}
181
+ onFocus={wrapEvent(onFocus, handleFocus)}
77
182
  onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
78
- disabled={disabled}
183
+ onMouseMove={wrapEvent(onMouseMove, handleMouseMove)}
184
+ disabled={isDisabled}
79
185
  {...otherProps}
80
186
  />
81
187
  );
@@ -1,9 +1,11 @@
1
1
  import type {
2
+ FocusEventHandler,
2
3
  LiHTMLAttributes,
3
4
  ElementType,
4
5
  MouseEvent,
5
6
  KeyboardEvent,
6
7
  KeyboardEventHandler,
8
+ MouseEventHandler,
7
9
  } from 'react';
8
10
  import { forwardRef, useRef, useId } from 'react';
9
11
 
@@ -31,9 +33,11 @@ export const MenuItem = forwardRef<any, MenuItemProps>(function MenuItem(
31
33
  onSelect,
32
34
  onClick,
33
35
  onKeyDown,
36
+ onFocus,
37
+ onMouseMove,
34
38
  ...otherProps
35
39
  } = props;
36
- const { onChange, buttonRef } = useMenuContext();
40
+ const { closeAllMenus } = useMenuContext();
37
41
  const { navigationItem, onNavigate } = useMenuListContext();
38
42
  const ref = useRef<HTMLLIElement | null>(null);
39
43
  const itemId = useId();
@@ -43,8 +47,7 @@ export const MenuItem = forwardRef<any, MenuItemProps>(function MenuItem(
43
47
  const handleSelect = wrapEvent(
44
48
  onSelect,
45
49
  (e: KeyboardEvent<HTMLLIElement> | MouseEvent<HTMLLIElement>) => {
46
- onChange(e, false);
47
- buttonRef.current?.focus();
50
+ closeAllMenus(e);
48
51
  e.preventDefault();
49
52
  }
50
53
  );
@@ -67,6 +70,18 @@ export const MenuItem = forwardRef<any, MenuItemProps>(function MenuItem(
67
70
  }
68
71
  };
69
72
 
73
+ const handleFocus: FocusEventHandler<HTMLLIElement> = () => {
74
+ if (!disabled && ref.current && ref.current !== navigationItem) {
75
+ onNavigate && onNavigate(ref.current);
76
+ }
77
+ };
78
+
79
+ const handleMouseMove: MouseEventHandler<HTMLLIElement> = () => {
80
+ if (!disabled && ref.current && ref.current !== navigationItem) {
81
+ onNavigate && onNavigate(ref.current);
82
+ }
83
+ };
84
+
70
85
  return (
71
86
  <Comp
72
87
  ref={assignMultipleRefs(ref, forwardedRef)}
@@ -78,6 +93,8 @@ export const MenuItem = forwardRef<any, MenuItemProps>(function MenuItem(
78
93
  onClick={wrapEvent(onClick, handleClick)}
79
94
  tabIndex={disabled ? -1 : 0}
80
95
  onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
96
+ onFocus={wrapEvent(onFocus, handleFocus)}
97
+ onMouseMove={wrapEvent(onMouseMove, handleMouseMove)}
81
98
  data-disabled={disabled ? '' : undefined}
82
99
  aria-disabled={disabled ? '' : undefined}
83
100
  disabled={disabled}