@seed-design/react-menu 1.0.0 → 2.0.1
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/LICENSE +201 -0
- package/NOTICE +20 -0
- package/lib/{Menu-12s-BZEIsPP0.cjs → Menu-12s-B13-vH6b.cjs} +81 -74
- package/lib/{Menu-12s-Cfv1stLD.js → Menu-12s-DuVl-aKo.js} +82 -75
- package/lib/index.cjs +1 -1
- package/lib/index.d.ts +0 -567
- package/lib/index.js +2 -2
- package/lib/{useMenu-12s-Cv5FQJ3x.cjs → useMenu-12s-B5iYt8f4.cjs} +32 -48
- package/lib/{useMenu-12s-BrOJabEO.js → useMenu-12s-a4Nmkf8N.js} +33 -50
- package/package.json +11 -11
- package/src/Menu.tsx +79 -69
- package/src/{useMenu.vitest.tsx → useMenu.test.tsx} +140 -29
- package/src/useMenu.ts +33 -34
|
@@ -43,9 +43,6 @@ function useMenuState(props) {
|
|
|
43
43
|
const elementsRef = React.useRef([]);
|
|
44
44
|
const labelsRef = React.useRef([]);
|
|
45
45
|
const triggerRef = React.useRef(null);
|
|
46
|
-
const groupIndexCounter = React.useRef(0);
|
|
47
|
-
groupIndexCounter.current = 0;
|
|
48
|
-
const menuId = React.useId();
|
|
49
46
|
return {
|
|
50
47
|
open,
|
|
51
48
|
setOpenState,
|
|
@@ -53,13 +50,11 @@ function useMenuState(props) {
|
|
|
53
50
|
setActiveIndex,
|
|
54
51
|
elementsRef,
|
|
55
52
|
labelsRef,
|
|
56
|
-
triggerRef
|
|
57
|
-
groupIndexCounter,
|
|
58
|
-
menuId
|
|
53
|
+
triggerRef
|
|
59
54
|
};
|
|
60
55
|
}
|
|
61
56
|
function useMenu(props) {
|
|
62
|
-
const { open, setOpenState, activeIndex, setActiveIndex, elementsRef, labelsRef, triggerRef
|
|
57
|
+
const { open, setOpenState, activeIndex, setActiveIndex, elementsRef, labelsRef, triggerRef } = useMenuState(props);
|
|
63
58
|
const { disabled = false, placement = "bottom", gutter = 8, overflowPadding = 8, strategy = "absolute", matchReferenceWidth = false } = props;
|
|
64
59
|
const [safeArea, setSafeArea] = React.useState({
|
|
65
60
|
top: 0,
|
|
@@ -276,49 +271,38 @@ function useMenu(props) {
|
|
|
276
271
|
"aria-disabled": itemProps.disabled ? "true" : undefined
|
|
277
272
|
})
|
|
278
273
|
};
|
|
279
|
-
},
|
|
280
|
-
// implement when submenu is needed
|
|
281
|
-
// getSubmenuTriggerProps: (itemProps: UseMenuSubmenuTriggerProps, index: number) => {
|
|
282
|
-
// const isActive = activeIndex === index;
|
|
283
|
-
//
|
|
284
|
-
// const itemStateProps = elementProps({
|
|
285
|
-
// "data-highlighted": dataAttr(isActive),
|
|
286
|
-
// "data-disabled": dataAttr(itemProps.disabled),
|
|
287
|
-
// });
|
|
288
|
-
//
|
|
289
|
-
// return {
|
|
290
|
-
// isHighlighted: isActive,
|
|
291
|
-
// isDisabled: itemProps.disabled,
|
|
292
|
-
//
|
|
293
|
-
// stateProps: itemStateProps,
|
|
294
|
-
//
|
|
295
|
-
// rootProps: elementProps({
|
|
296
|
-
// ...itemStateProps,
|
|
297
|
-
// ...triggerInteractions.getItemProps(),
|
|
298
|
-
// role: "menuitem",
|
|
299
|
-
// tabIndex: isActive ? 0 : -1,
|
|
300
|
-
// "aria-disabled": itemProps.disabled ? "true" : undefined,
|
|
301
|
-
// }),
|
|
302
|
-
// };
|
|
303
|
-
// },
|
|
304
|
-
getGroupProps: ()=>{
|
|
305
|
-
const groupIndex = groupIndexCounter.current++;
|
|
306
|
-
const labelId = `menu:${menuId}:group-${groupIndex}:label`;
|
|
307
|
-
return {
|
|
308
|
-
labelId,
|
|
309
|
-
rootProps: domUtils.elementProps({
|
|
310
|
-
role: "group",
|
|
311
|
-
"aria-labelledby": labelId
|
|
312
|
-
})
|
|
313
|
-
};
|
|
314
|
-
},
|
|
315
|
-
getGroupLabelProps: (labelId)=>{
|
|
316
|
-
return domUtils.elementProps({
|
|
317
|
-
role: "presentation",
|
|
318
|
-
id: labelId
|
|
319
|
-
});
|
|
320
274
|
}
|
|
321
275
|
};
|
|
322
276
|
}
|
|
277
|
+
// A group advertises aria-labelledby only while its label is actually rendered.
|
|
278
|
+
// Mirrors useFieldset: a callback ref flips a boolean synchronously at commit, so
|
|
279
|
+
// the attribute is present on first paint and never points at a missing id — no
|
|
280
|
+
// menu-global registry or render-order ids needed. Each group owns its own id and
|
|
281
|
+
// presence flag, so conditionally rendering or reordering groups can never make one
|
|
282
|
+
// group inherit another's label reference.
|
|
283
|
+
function useMenuGroup() {
|
|
284
|
+
const id = React.useId();
|
|
285
|
+
const labelId = `menu-group:${id}:label`;
|
|
286
|
+
const [isLabelRendered, setIsLabelRendered] = React.useState(false);
|
|
287
|
+
const labelRef = React.useCallback((node)=>{
|
|
288
|
+
setIsLabelRendered(!!node);
|
|
289
|
+
}, []);
|
|
290
|
+
return {
|
|
291
|
+
refs: {
|
|
292
|
+
label: labelRef
|
|
293
|
+
},
|
|
294
|
+
rootProps: domUtils.elementProps({
|
|
295
|
+
role: "group",
|
|
296
|
+
...isLabelRendered && {
|
|
297
|
+
"aria-labelledby": labelId
|
|
298
|
+
}
|
|
299
|
+
}),
|
|
300
|
+
labelProps: domUtils.elementProps({
|
|
301
|
+
role: "presentation",
|
|
302
|
+
id: labelId
|
|
303
|
+
})
|
|
304
|
+
};
|
|
305
|
+
}
|
|
323
306
|
|
|
324
307
|
exports.useMenu = useMenu;
|
|
308
|
+
exports.useMenuGroup = useMenuGroup;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useFloating, offset, size, flip, shift, useTransitionStatus, autoUpdate, useClick, useRole, useListNavigation, useTypeahead, useInteractions } from '@floating-ui/react';
|
|
3
3
|
import { useControllableState } from '@seed-design/react-use-controllable-state';
|
|
4
4
|
import { elementProps, dataAttr, buttonProps } from '@seed-design/dom-utils';
|
|
5
|
-
import { useState, useCallback, useEffect, useMemo, useRef
|
|
5
|
+
import { useId, useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
6
6
|
|
|
7
7
|
const MIN_HEIGHT = 200;
|
|
8
8
|
// flip/size/shift derive collisions from numeric padding, so the safe-area insets
|
|
@@ -43,9 +43,6 @@ function useMenuState(props) {
|
|
|
43
43
|
const elementsRef = useRef([]);
|
|
44
44
|
const labelsRef = useRef([]);
|
|
45
45
|
const triggerRef = useRef(null);
|
|
46
|
-
const groupIndexCounter = useRef(0);
|
|
47
|
-
groupIndexCounter.current = 0;
|
|
48
|
-
const menuId = useId();
|
|
49
46
|
return {
|
|
50
47
|
open,
|
|
51
48
|
setOpenState,
|
|
@@ -53,13 +50,11 @@ function useMenuState(props) {
|
|
|
53
50
|
setActiveIndex,
|
|
54
51
|
elementsRef,
|
|
55
52
|
labelsRef,
|
|
56
|
-
triggerRef
|
|
57
|
-
groupIndexCounter,
|
|
58
|
-
menuId
|
|
53
|
+
triggerRef
|
|
59
54
|
};
|
|
60
55
|
}
|
|
61
56
|
function useMenu(props) {
|
|
62
|
-
const { open, setOpenState, activeIndex, setActiveIndex, elementsRef, labelsRef, triggerRef
|
|
57
|
+
const { open, setOpenState, activeIndex, setActiveIndex, elementsRef, labelsRef, triggerRef } = useMenuState(props);
|
|
63
58
|
const { disabled = false, placement = "bottom", gutter = 8, overflowPadding = 8, strategy = "absolute", matchReferenceWidth = false } = props;
|
|
64
59
|
const [safeArea, setSafeArea] = useState({
|
|
65
60
|
top: 0,
|
|
@@ -276,49 +271,37 @@ function useMenu(props) {
|
|
|
276
271
|
"aria-disabled": itemProps.disabled ? "true" : undefined
|
|
277
272
|
})
|
|
278
273
|
};
|
|
279
|
-
},
|
|
280
|
-
// implement when submenu is needed
|
|
281
|
-
// getSubmenuTriggerProps: (itemProps: UseMenuSubmenuTriggerProps, index: number) => {
|
|
282
|
-
// const isActive = activeIndex === index;
|
|
283
|
-
//
|
|
284
|
-
// const itemStateProps = elementProps({
|
|
285
|
-
// "data-highlighted": dataAttr(isActive),
|
|
286
|
-
// "data-disabled": dataAttr(itemProps.disabled),
|
|
287
|
-
// });
|
|
288
|
-
//
|
|
289
|
-
// return {
|
|
290
|
-
// isHighlighted: isActive,
|
|
291
|
-
// isDisabled: itemProps.disabled,
|
|
292
|
-
//
|
|
293
|
-
// stateProps: itemStateProps,
|
|
294
|
-
//
|
|
295
|
-
// rootProps: elementProps({
|
|
296
|
-
// ...itemStateProps,
|
|
297
|
-
// ...triggerInteractions.getItemProps(),
|
|
298
|
-
// role: "menuitem",
|
|
299
|
-
// tabIndex: isActive ? 0 : -1,
|
|
300
|
-
// "aria-disabled": itemProps.disabled ? "true" : undefined,
|
|
301
|
-
// }),
|
|
302
|
-
// };
|
|
303
|
-
// },
|
|
304
|
-
getGroupProps: ()=>{
|
|
305
|
-
const groupIndex = groupIndexCounter.current++;
|
|
306
|
-
const labelId = `menu:${menuId}:group-${groupIndex}:label`;
|
|
307
|
-
return {
|
|
308
|
-
labelId,
|
|
309
|
-
rootProps: elementProps({
|
|
310
|
-
role: "group",
|
|
311
|
-
"aria-labelledby": labelId
|
|
312
|
-
})
|
|
313
|
-
};
|
|
314
|
-
},
|
|
315
|
-
getGroupLabelProps: (labelId)=>{
|
|
316
|
-
return elementProps({
|
|
317
|
-
role: "presentation",
|
|
318
|
-
id: labelId
|
|
319
|
-
});
|
|
320
274
|
}
|
|
321
275
|
};
|
|
322
276
|
}
|
|
277
|
+
// A group advertises aria-labelledby only while its label is actually rendered.
|
|
278
|
+
// Mirrors useFieldset: a callback ref flips a boolean synchronously at commit, so
|
|
279
|
+
// the attribute is present on first paint and never points at a missing id — no
|
|
280
|
+
// menu-global registry or render-order ids needed. Each group owns its own id and
|
|
281
|
+
// presence flag, so conditionally rendering or reordering groups can never make one
|
|
282
|
+
// group inherit another's label reference.
|
|
283
|
+
function useMenuGroup() {
|
|
284
|
+
const id = useId();
|
|
285
|
+
const labelId = `menu-group:${id}:label`;
|
|
286
|
+
const [isLabelRendered, setIsLabelRendered] = useState(false);
|
|
287
|
+
const labelRef = useCallback((node)=>{
|
|
288
|
+
setIsLabelRendered(!!node);
|
|
289
|
+
}, []);
|
|
290
|
+
return {
|
|
291
|
+
refs: {
|
|
292
|
+
label: labelRef
|
|
293
|
+
},
|
|
294
|
+
rootProps: elementProps({
|
|
295
|
+
role: "group",
|
|
296
|
+
...isLabelRendered && {
|
|
297
|
+
"aria-labelledby": labelId
|
|
298
|
+
}
|
|
299
|
+
}),
|
|
300
|
+
labelProps: elementProps({
|
|
301
|
+
role: "presentation",
|
|
302
|
+
id: labelId
|
|
303
|
+
})
|
|
304
|
+
};
|
|
305
|
+
}
|
|
323
306
|
|
|
324
|
-
export { useMenu as u };
|
|
307
|
+
export { useMenu as a, useMenuGroup as u };
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seed-design/react-menu",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/daangn/seed-design.git",
|
|
7
7
|
"directory": "packages/react-headless/menu"
|
|
8
8
|
},
|
|
9
|
+
"license": "Apache-2.0",
|
|
9
10
|
"sideEffects": false,
|
|
10
11
|
"type": "module",
|
|
11
12
|
"exports": {
|
|
@@ -19,29 +20,28 @@
|
|
|
19
20
|
"main": "./lib/index.cjs",
|
|
20
21
|
"files": [
|
|
21
22
|
"lib",
|
|
22
|
-
"src"
|
|
23
|
+
"src",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"NOTICE"
|
|
23
26
|
],
|
|
24
27
|
"scripts": {
|
|
25
28
|
"clean": "rm -rf lib",
|
|
26
29
|
"build": "bunchee",
|
|
27
|
-
"lint:publish": "bun publint"
|
|
28
|
-
"test": "vitest run"
|
|
30
|
+
"lint:publish": "bun publint"
|
|
29
31
|
},
|
|
30
32
|
"dependencies": {
|
|
31
33
|
"@floating-ui/react": "^0.27.0",
|
|
32
34
|
"@radix-ui/react-compose-refs": "^1.1.2",
|
|
33
35
|
"@radix-ui/react-focus-scope": "^1.1.8",
|
|
34
|
-
"@seed-design/dom-utils": "^2.0.
|
|
35
|
-
"@seed-design/react-dismissible-layer": "^1.0.
|
|
36
|
-
"@seed-design/react-primitive": "^2.0.
|
|
37
|
-
"@seed-design/react-use-controllable-state": "^2.0.
|
|
36
|
+
"@seed-design/dom-utils": "^2.0.1",
|
|
37
|
+
"@seed-design/react-dismissible-layer": "^1.0.1",
|
|
38
|
+
"@seed-design/react-primitive": "^2.0.1",
|
|
39
|
+
"@seed-design/react-use-controllable-state": "^2.0.1"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@types/react": "^19.1.6",
|
|
41
|
-
"jsdom": "^29.0.2",
|
|
42
43
|
"react": "^19.1.0",
|
|
43
|
-
"react-dom": "^19.1.0"
|
|
44
|
-
"vitest": "^4.1.3"
|
|
44
|
+
"react-dom": "^19.1.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"react": ">=18.0.0",
|
package/src/Menu.tsx
CHANGED
|
@@ -12,11 +12,17 @@ import { DismissibleLayer } from "@seed-design/react-dismissible-layer";
|
|
|
12
12
|
import { mergeProps } from "@seed-design/dom-utils";
|
|
13
13
|
import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
|
|
14
14
|
import React, { forwardRef, createContext } from "react";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
useMenu,
|
|
17
|
+
useMenuGroup,
|
|
18
|
+
type UseMenuGroupReturn,
|
|
19
|
+
type UseMenuItemProps,
|
|
20
|
+
type UseMenuProps,
|
|
21
|
+
} from "./useMenu";
|
|
16
22
|
import { MenuProvider, useMenuContext } from "./useMenuContext";
|
|
17
23
|
import { MenuItemProvider } from "./useMenuItemContext";
|
|
18
24
|
|
|
19
|
-
const
|
|
25
|
+
const MenuGroupContext = createContext<UseMenuGroupReturn | null>(null);
|
|
20
26
|
|
|
21
27
|
export interface MenuRootProps extends UseMenuProps {
|
|
22
28
|
children?: React.ReactNode;
|
|
@@ -105,72 +111,71 @@ export const MenuPositioner = forwardRef<HTMLDivElement, MenuPositionerProps>(
|
|
|
105
111
|
);
|
|
106
112
|
MenuPositioner.displayName = "MenuPositioner";
|
|
107
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Holds a Radix FocusScope registration for as long as the menu is open, so parent
|
|
116
|
+
* FocusScopes (Dialog, Drawer) pause their trap and focus can reach content
|
|
117
|
+
* rendered in a portal.
|
|
118
|
+
*
|
|
119
|
+
* The scope needs no behavior of its own — trapping and the tab loop stay off and
|
|
120
|
+
* both autofocus events are prevented — so all it does is enter Radix's
|
|
121
|
+
* focusScopesStack, which is keyed on mount, not on the element it wraps. Hence
|
|
122
|
+
* this empty hidden element rather than a wrapper around the content: wrapping
|
|
123
|
+
* swaps the element type at the content's position on every open/close, and React
|
|
124
|
+
* responds by remounting the whole menu subtree, handing the exit transition a
|
|
125
|
+
* scroll container freshly reset to the top.
|
|
126
|
+
*
|
|
127
|
+
* Mounting only while open is what lands it on top of the stack — a permanently
|
|
128
|
+
* mounted scope would register at page load, below any Dialog opened later.
|
|
129
|
+
*/
|
|
130
|
+
const FocusScopeRegistration = () => (
|
|
131
|
+
<FocusScope
|
|
132
|
+
hidden
|
|
133
|
+
trapped={false}
|
|
134
|
+
loop={false}
|
|
135
|
+
onMountAutoFocus={(event) => event.preventDefault()}
|
|
136
|
+
onUnmountAutoFocus={(event) => event.preventDefault()}
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
|
|
108
140
|
export interface MenuContentProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
|
|
109
141
|
|
|
110
142
|
export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>((props, ref) => {
|
|
111
143
|
const { floatingContext, contentProps, open, setOpen, elementsRef, labelsRef } = useMenuContext();
|
|
112
144
|
|
|
113
|
-
const content = <Primitive.div ref={ref} {...mergeProps(contentProps, props)} />;
|
|
114
|
-
|
|
115
145
|
// FloatingFocusManager: handles position-aware initial focus, return focus,
|
|
116
146
|
// closeOnFocusOut, tab order guards, and useListNavigation coordination.
|
|
117
|
-
//
|
|
118
|
-
// FocusScope: participates in Radix's focusScopesStack so that parent
|
|
119
|
-
// FocusScopes (e.g. Dialog, Drawer) are automatically paused while this
|
|
120
|
-
// Menu is open. Without this, focus cannot leave a trapped parent scope
|
|
121
|
-
// to reach Menu content rendered in a Portal.
|
|
122
|
-
//
|
|
123
|
-
// FocusScope is conditionally rendered (only when open) because Menu
|
|
124
|
-
// content is always in the DOM (hidden via data-hidden). If FocusScope
|
|
125
|
-
// were always mounted, it would register in the stack at page load —
|
|
126
|
-
// before any Dialog — and could never re-register above a Dialog that
|
|
127
|
-
// mounts later. Mounting only when open ensures it lands at the top of
|
|
128
|
-
// the stack, pausing the parent scope.
|
|
129
147
|
return (
|
|
130
|
-
|
|
131
|
-
<
|
|
132
|
-
{
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
trapped={false}
|
|
162
|
-
loop={false}
|
|
163
|
-
onMountAutoFocus={(e) => e.preventDefault()}
|
|
164
|
-
onUnmountAutoFocus={(e) => e.preventDefault()}
|
|
165
|
-
>
|
|
166
|
-
{content}
|
|
167
|
-
</FocusScope>
|
|
168
|
-
) : (
|
|
169
|
-
content
|
|
170
|
-
)}
|
|
171
|
-
</DismissibleLayer>
|
|
172
|
-
</FloatingList>
|
|
173
|
-
</FloatingFocusManager>
|
|
148
|
+
<>
|
|
149
|
+
<FloatingFocusManager context={floatingContext} disabled={!open} modal={false}>
|
|
150
|
+
<FloatingList elementsRef={elementsRef} labelsRef={labelsRef}>
|
|
151
|
+
<DismissibleLayer
|
|
152
|
+
enabled={open}
|
|
153
|
+
pressBehavior="drag"
|
|
154
|
+
onEscapeKeyDown={(event) => {
|
|
155
|
+
setOpen(false, { reason: "escapeKeyDown", event });
|
|
156
|
+
}}
|
|
157
|
+
onPressOutside={(event) => {
|
|
158
|
+
setOpen(false, { reason: "interactOutside", event });
|
|
159
|
+
}}
|
|
160
|
+
onCascadeDismiss={({ dismissedParent }) => {
|
|
161
|
+
setOpen(false, { reason: "cascadeDismiss", dismissedParent });
|
|
162
|
+
}}
|
|
163
|
+
onFocusOutside={() => {
|
|
164
|
+
// focus trapping is handled by FloatingFocusManager — nothing to do here
|
|
165
|
+
}}
|
|
166
|
+
exclude={(target) => {
|
|
167
|
+
const reference = floatingContext.refs.reference.current;
|
|
168
|
+
if (!(reference instanceof HTMLElement)) return false;
|
|
169
|
+
|
|
170
|
+
return reference.contains(target);
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<Primitive.div ref={ref} {...mergeProps(contentProps, props)} />
|
|
174
|
+
</DismissibleLayer>
|
|
175
|
+
</FloatingList>
|
|
176
|
+
</FloatingFocusManager>
|
|
177
|
+
{open && <FocusScopeRegistration />}
|
|
178
|
+
</>
|
|
174
179
|
);
|
|
175
180
|
});
|
|
176
181
|
MenuContent.displayName = "MenuContent";
|
|
@@ -198,13 +203,12 @@ MenuItem.displayName = "MenuItem";
|
|
|
198
203
|
export interface MenuGroupProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
|
|
199
204
|
|
|
200
205
|
export const MenuGroup = forwardRef<HTMLDivElement, MenuGroupProps>((props, ref) => {
|
|
201
|
-
const
|
|
202
|
-
const { labelId, rootProps } = getGroupProps();
|
|
206
|
+
const group = useMenuGroup();
|
|
203
207
|
|
|
204
208
|
return (
|
|
205
|
-
<
|
|
206
|
-
<Primitive.div ref={ref} {...mergeProps(rootProps, props)} />
|
|
207
|
-
</
|
|
209
|
+
<MenuGroupContext.Provider value={group}>
|
|
210
|
+
<Primitive.div ref={ref} {...mergeProps(group.rootProps, props)} />
|
|
211
|
+
</MenuGroupContext.Provider>
|
|
208
212
|
);
|
|
209
213
|
});
|
|
210
214
|
MenuGroup.displayName = "MenuGroup";
|
|
@@ -212,10 +216,16 @@ MenuGroup.displayName = "MenuGroup";
|
|
|
212
216
|
export interface MenuGroupLabelProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
|
|
213
217
|
|
|
214
218
|
export const MenuGroupLabel = forwardRef<HTMLDivElement, MenuGroupLabelProps>((props, ref) => {
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
if (!labelId) throw new Error("MenuGroupLabel must be used within a MenuGroup");
|
|
219
|
+
const group = React.useContext(MenuGroupContext);
|
|
220
|
+
if (!group) throw new Error("MenuGroupLabel must be used within a MenuGroup");
|
|
218
221
|
|
|
219
|
-
|
|
222
|
+
// Compose the group's label ref so the group advertises aria-labelledby only
|
|
223
|
+
// while this label is actually rendered (see useMenuGroup).
|
|
224
|
+
return (
|
|
225
|
+
<Primitive.div
|
|
226
|
+
ref={composeRefs(group.refs.label, ref)}
|
|
227
|
+
{...mergeProps(group.labelProps, props)}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
220
230
|
});
|
|
221
231
|
MenuGroupLabel.displayName = "MenuGroupLabel";
|