@seed-design/react-menu 0.0.0-alpha-20260414104312

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/src/Menu.tsx ADDED
@@ -0,0 +1,221 @@
1
+ "use client";
2
+
3
+ import {
4
+ FloatingFocusManager,
5
+ FloatingList,
6
+ FloatingPortal,
7
+ useListItem,
8
+ } from "@floating-ui/react";
9
+ import { composeRefs } from "@radix-ui/react-compose-refs";
10
+ import { FocusScope } from "@radix-ui/react-focus-scope";
11
+ import { DismissibleLayer } from "@seed-design/react-dismissible-layer";
12
+ import { mergeProps } from "@seed-design/dom-utils";
13
+ import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
14
+ import React, { forwardRef, createContext } from "react";
15
+ import { useMenu, type UseMenuItemProps, type UseMenuProps } from "./useMenu";
16
+ import { MenuProvider, useMenuContext } from "./useMenuContext";
17
+ import { MenuItemProvider } from "./useMenuItemContext";
18
+
19
+ const MenuGroupLabelIdContext = createContext<string | null>(null);
20
+
21
+ export interface MenuRootProps extends UseMenuProps {
22
+ children?: React.ReactNode;
23
+ }
24
+
25
+ export const MenuRoot = ({
26
+ open,
27
+ defaultOpen,
28
+ onOpenChange,
29
+ disabled,
30
+ placement,
31
+ gutter,
32
+ overflowPadding,
33
+ strategy,
34
+ matchReferenceWidth,
35
+ children,
36
+ }: MenuRootProps) => {
37
+ const api = useMenu({
38
+ open,
39
+ defaultOpen,
40
+ onOpenChange,
41
+ disabled,
42
+ placement,
43
+ gutter,
44
+ overflowPadding,
45
+ strategy,
46
+ matchReferenceWidth,
47
+ });
48
+
49
+ return <MenuProvider value={api}>{children}</MenuProvider>;
50
+ };
51
+
52
+ export interface MenuAnchorProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
53
+
54
+ export const MenuAnchor = forwardRef<HTMLDivElement, MenuAnchorProps>((props, ref) => {
55
+ const api = useMenuContext();
56
+
57
+ return (
58
+ <Primitive.div
59
+ ref={composeRefs(api.refs.anchor, ref)}
60
+ {...mergeProps(api.anchorProps, props)}
61
+ />
62
+ );
63
+ });
64
+ MenuAnchor.displayName = "MenuAnchor";
65
+
66
+ export interface MenuTriggerProps
67
+ extends PrimitiveProps,
68
+ React.ButtonHTMLAttributes<HTMLButtonElement> {}
69
+
70
+ export const MenuTrigger = forwardRef<HTMLButtonElement, MenuTriggerProps>((props, ref) => {
71
+ const api = useMenuContext();
72
+
73
+ return (
74
+ <Primitive.button
75
+ ref={composeRefs(api.refs.trigger, ref)}
76
+ {...mergeProps(api.triggerProps, props)}
77
+ />
78
+ );
79
+ });
80
+ MenuTrigger.displayName = "MenuTrigger";
81
+
82
+ export interface MenuPositionerProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {
83
+ /**
84
+ * The container element to render the portal into.
85
+ * @default document.body
86
+ */
87
+ container?: React.RefObject<HTMLElement | null>;
88
+ }
89
+
90
+ export const MenuPositioner = forwardRef<HTMLDivElement, MenuPositionerProps>(
91
+ ({ container, ...props }, ref) => {
92
+ const api = useMenuContext();
93
+
94
+ // FloatingPortal (not a generic portal) so that FloatingFocusManager
95
+ // detects the portal context and renders focus-guard sentinels.
96
+ return (
97
+ <FloatingPortal root={container ?? undefined}>
98
+ <Primitive.div
99
+ ref={composeRefs(api.refs.positioner, ref)}
100
+ {...mergeProps(api.positionerProps, props)}
101
+ />
102
+ </FloatingPortal>
103
+ );
104
+ },
105
+ );
106
+ MenuPositioner.displayName = "MenuPositioner";
107
+
108
+ export interface MenuContentProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
109
+
110
+ export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>((props, ref) => {
111
+ const { floatingContext, contentProps, open, setOpen, elementsRef, labelsRef } = useMenuContext();
112
+
113
+ const content = <Primitive.div ref={ref} {...mergeProps(contentProps, props)} />;
114
+
115
+ // FloatingFocusManager: handles position-aware initial focus, return focus,
116
+ // 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
+ return (
130
+ <FloatingFocusManager context={floatingContext} disabled={!open} modal={false}>
131
+ <FloatingList elementsRef={elementsRef} labelsRef={labelsRef}>
132
+ {/* DismissibleLayer must wrap FocusScope, not the other way around.
133
+ FocusScope asChild uses Slot to forward tabIndex/onKeyDown/ref to the
134
+ DOM element; if DismissibleLayer sits between them, those props are
135
+ swallowed by DismissibleLayer's own destructuring and never reach the DOM. */}
136
+ <DismissibleLayer
137
+ enabled={open}
138
+ pressBehavior="drag"
139
+ onEscapeKeyDown={(event) => {
140
+ setOpen(false, { reason: "escapeKeyDown", event });
141
+ }}
142
+ onPressOutside={(event) => {
143
+ setOpen(false, { reason: "interactOutside", event });
144
+ }}
145
+ onCascadeDismiss={({ dismissedParent }) => {
146
+ setOpen(false, { reason: "cascadeDismiss", dismissedParent });
147
+ }}
148
+ onFocusOutside={() => {
149
+ // focus trapping is handled by FloatingFocusManager — nothing to do here
150
+ }}
151
+ exclude={(target) => {
152
+ const reference = floatingContext.refs.reference.current;
153
+ if (!(reference instanceof HTMLElement)) return false;
154
+
155
+ return reference.contains(target);
156
+ }}
157
+ >
158
+ {open ? (
159
+ <FocusScope
160
+ asChild
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>
174
+ );
175
+ });
176
+ MenuContent.displayName = "MenuContent";
177
+
178
+ export interface MenuItemProps
179
+ extends UseMenuItemProps,
180
+ PrimitiveProps,
181
+ Omit<React.HTMLAttributes<HTMLDivElement>, "onClick"> {}
182
+
183
+ export const MenuItem = forwardRef<HTMLDivElement, MenuItemProps>(
184
+ ({ disabled, typeaheadLabel, onClick, ...restProps }, ref) => {
185
+ const { getItemProps } = useMenuContext();
186
+ const { ref: listRef, index } = useListItem({ label: typeaheadLabel });
187
+ const api = getItemProps({ disabled, onClick }, index);
188
+
189
+ return (
190
+ <MenuItemProvider value={api}>
191
+ <Primitive.div ref={composeRefs(listRef, ref)} {...mergeProps(api.rootProps, restProps)} />
192
+ </MenuItemProvider>
193
+ );
194
+ },
195
+ );
196
+ MenuItem.displayName = "MenuItem";
197
+
198
+ export interface MenuGroupProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
199
+
200
+ export const MenuGroup = forwardRef<HTMLDivElement, MenuGroupProps>((props, ref) => {
201
+ const { getGroupProps } = useMenuContext();
202
+ const { labelId, rootProps } = getGroupProps();
203
+
204
+ return (
205
+ <MenuGroupLabelIdContext.Provider value={labelId}>
206
+ <Primitive.div ref={ref} {...mergeProps(rootProps, props)} />
207
+ </MenuGroupLabelIdContext.Provider>
208
+ );
209
+ });
210
+ MenuGroup.displayName = "MenuGroup";
211
+
212
+ export interface MenuGroupLabelProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
213
+
214
+ export const MenuGroupLabel = forwardRef<HTMLDivElement, MenuGroupLabelProps>((props, ref) => {
215
+ const { getGroupLabelProps } = useMenuContext();
216
+ const labelId = React.useContext(MenuGroupLabelIdContext);
217
+ if (!labelId) throw new Error("MenuGroupLabel must be used within a MenuGroup");
218
+
219
+ return <Primitive.div ref={ref} {...mergeProps(getGroupLabelProps(labelId), props)} />;
220
+ });
221
+ MenuGroupLabel.displayName = "MenuGroupLabel";
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ export {
2
+ MenuRoot,
3
+ MenuAnchor,
4
+ MenuTrigger,
5
+ MenuPositioner,
6
+ MenuContent,
7
+ MenuItem,
8
+ MenuGroup,
9
+ MenuGroupLabel,
10
+ type MenuRootProps,
11
+ type MenuAnchorProps,
12
+ type MenuTriggerProps,
13
+ type MenuPositionerProps,
14
+ type MenuContentProps,
15
+ type MenuItemProps,
16
+ type MenuGroupProps,
17
+ type MenuGroupLabelProps,
18
+ } from "./Menu";
19
+
20
+ export { useMenuContext, type UseMenuContext } from "./useMenuContext";
21
+ export { useMenuItemContext, type UseMenuItemContext } from "./useMenuItemContext";
22
+
23
+ export * as Menu from "./Menu.namespace";
24
+
25
+ export type {
26
+ UseMenuProps,
27
+ UseMenuItemProps,
28
+ // implement when submenu is needed
29
+ // UseMenuSubmenuTriggerProps,
30
+ UseMenuReturn,
31
+ } from "./useMenu";
package/src/useMenu.ts ADDED
@@ -0,0 +1,411 @@
1
+ "use client";
2
+
3
+ import {
4
+ useFloating,
5
+ autoUpdate,
6
+ offset,
7
+ flip,
8
+ shift,
9
+ size,
10
+ useClick,
11
+ useRole,
12
+ useListNavigation,
13
+ useTypeahead,
14
+ useInteractions,
15
+ useTransitionStatus,
16
+ type Placement,
17
+ } from "@floating-ui/react";
18
+ import { useControllableState } from "@seed-design/react-use-controllable-state";
19
+ import { buttonProps, dataAttr, elementProps } from "@seed-design/dom-utils";
20
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
21
+
22
+ const MIN_HEIGHT = 200;
23
+
24
+ function getTransformOrigin(placement: string) {
25
+ const [side, align] = placement.split("-");
26
+ const y = { top: "bottom", bottom: "top", left: "center", right: "center" }[side] ?? "top";
27
+ const x = { start: "left", end: "right" }[align] ?? "center";
28
+ return `${x} ${y}`;
29
+ }
30
+
31
+ interface MenuReasonToDetailMap {
32
+ trigger: { event: MouseEvent | KeyboardEvent };
33
+ escapeKeyDown: { event: KeyboardEvent };
34
+ interactOutside: { event: PointerEvent | TouchEvent };
35
+ cascadeDismiss: { dismissedParent: HTMLElement };
36
+ itemClick: { event: MouseEvent };
37
+ }
38
+
39
+ type MenuChangeDetails = {
40
+ [R in keyof MenuReasonToDetailMap]: {
41
+ reason?: R;
42
+ } & MenuReasonToDetailMap[R];
43
+ }[keyof MenuReasonToDetailMap];
44
+
45
+ interface UseMenuStateProps {
46
+ open?: boolean;
47
+ defaultOpen?: boolean;
48
+ onOpenChange?: (open: boolean, details?: MenuChangeDetails) => void;
49
+ }
50
+
51
+ export interface UseMenuProps extends UseMenuStateProps {
52
+ disabled?: boolean;
53
+
54
+ /**
55
+ * Floating UI placement. @default "bottom"
56
+ */
57
+ placement?: Placement;
58
+
59
+ /**
60
+ * Distance between trigger and floating element.
61
+ * @default 0
62
+ */
63
+ gutter?: number;
64
+
65
+ /**
66
+ * Virtual padding around viewport edges.
67
+ * @default 8
68
+ */
69
+ overflowPadding?: number;
70
+
71
+ /**
72
+ * Positioning strategy.
73
+ * @default "absolute"
74
+ */
75
+ strategy?: "absolute" | "fixed";
76
+
77
+ /**
78
+ * Whether the floating element's width should match the reference(trigger/anchor)'s width.
79
+ * @default false
80
+ */
81
+ matchReferenceWidth?: boolean;
82
+ }
83
+
84
+ export interface UseMenuItemProps {
85
+ disabled?: boolean;
86
+ /**
87
+ * Overrides the text label to use when the item is matched during keyboard text navigation.
88
+ * Falls back to the element's text content if not provided.
89
+ */
90
+ typeaheadLabel?: string;
91
+
92
+ // intentionally defined here; onClick is omitted in MenuItemProps since it shouldn't be merged as restProps
93
+ /**
94
+ * Called when the item is activated (click or keyboard).
95
+ * Not called when the item is disabled.
96
+ */
97
+ onClick?: React.MouseEventHandler;
98
+ }
99
+
100
+ // implement when submenu is needed
101
+ // export interface UseMenuSubmenuTriggerProps {
102
+ // disabled?: boolean;
103
+ // /**
104
+ // * Overrides the text label to use when the item is matched during keyboard text navigation.
105
+ // * Falls back to the element's text content if not provided.
106
+ // */
107
+ // typeaheadLabel?: string;
108
+ // }
109
+
110
+ export type UseMenuReturn = ReturnType<typeof useMenu>;
111
+
112
+ export type GetItemPropsReturn = ReturnType<UseMenuReturn["getItemProps"]>;
113
+
114
+ function useMenuState(props: UseMenuStateProps) {
115
+ const [open = false, setOpenState] = useControllableState<
116
+ boolean,
117
+ Parameters<NonNullable<UseMenuStateProps["onOpenChange"]>>[1]
118
+ >({
119
+ prop: props.open,
120
+ defaultProp: props.defaultOpen ?? false,
121
+ onChange: props.onOpenChange,
122
+ });
123
+
124
+ const [activeIndex, setActiveIndex] = useState<number | null>(null);
125
+
126
+ const elementsRef = useRef<(HTMLElement | null)[]>([]);
127
+ const labelsRef = useRef<(string | null)[]>([]);
128
+ const triggerRef = useRef<Element | null>(null);
129
+
130
+ const groupIndexCounter = useRef(0);
131
+ groupIndexCounter.current = 0;
132
+
133
+ const menuId = useId();
134
+
135
+ return {
136
+ open,
137
+ setOpenState,
138
+ activeIndex,
139
+ setActiveIndex,
140
+ elementsRef,
141
+ labelsRef,
142
+ triggerRef,
143
+ groupIndexCounter,
144
+ menuId,
145
+ };
146
+ }
147
+
148
+ export function useMenu(props: UseMenuProps) {
149
+ const {
150
+ open,
151
+ setOpenState,
152
+ activeIndex,
153
+ setActiveIndex,
154
+ elementsRef,
155
+ labelsRef,
156
+ triggerRef,
157
+ groupIndexCounter,
158
+ menuId,
159
+ } = useMenuState(props);
160
+
161
+ const {
162
+ disabled = false,
163
+ placement = "bottom",
164
+ gutter = 8,
165
+ overflowPadding = 8,
166
+ strategy = "absolute",
167
+ matchReferenceWidth = false,
168
+ } = props;
169
+
170
+ const setOpen = useCallback(
171
+ (nextOpen: boolean, details?: MenuChangeDetails) => {
172
+ if (disabled && nextOpen) return;
173
+ setOpenState(nextOpen, details);
174
+ },
175
+ [disabled, setOpenState],
176
+ );
177
+
178
+ const handleFloatingOpenChange = useCallback(
179
+ (nextOpen: boolean, event?: Event, reason?: string) => {
180
+ if (reason === "click" && event) {
181
+ // NOTE: floating-ui passes click/mousedown/keydown on "click" reason
182
+ setOpen(nextOpen, { reason: "trigger", event: event as MouseEvent | KeyboardEvent });
183
+
184
+ return;
185
+ }
186
+
187
+ setOpen(nextOpen);
188
+ },
189
+ [setOpen],
190
+ );
191
+
192
+ const {
193
+ refs: floatingRefs,
194
+ context,
195
+ floatingStyles,
196
+ } = useFloating({
197
+ open,
198
+ onOpenChange: handleFloatingOpenChange,
199
+ strategy,
200
+ placement,
201
+ middleware: [
202
+ offset(gutter),
203
+ size({
204
+ padding: overflowPadding,
205
+ apply({ availableHeight, rects, elements }) {
206
+ elements.floating.style.setProperty(
207
+ "--seed-menu-available-height",
208
+ `${Math.max(MIN_HEIGHT, availableHeight)}px`,
209
+ );
210
+ if (matchReferenceWidth) {
211
+ elements.floating.style.setProperty(
212
+ "--seed-menu-reference-width",
213
+ `${rects.reference.width}px`,
214
+ );
215
+ }
216
+ },
217
+ }),
218
+ flip({ padding: overflowPadding, fallbackStrategy: "initialPlacement" }),
219
+ shift({ padding: overflowPadding }),
220
+ ],
221
+ });
222
+
223
+ const { status } = useTransitionStatus(context);
224
+
225
+ // Keep autoUpdate alive during exit animation so the positioner
226
+ // follows the trigger even while the menu is animating out.
227
+ const mounted = status !== "unmounted";
228
+
229
+ useEffect(() => {
230
+ if (!mounted) return;
231
+ if (!floatingRefs.reference.current || !floatingRefs.floating.current) return;
232
+
233
+ return autoUpdate(
234
+ floatingRefs.reference.current,
235
+ floatingRefs.floating.current,
236
+ context.update,
237
+ );
238
+ }, [mounted, floatingRefs.reference, floatingRefs.floating, context]);
239
+
240
+ const click = useClick(context, {
241
+ enabled: !disabled,
242
+ });
243
+
244
+ const role = useRole(context, {
245
+ role: "menu",
246
+ });
247
+
248
+ const listNavigation = useListNavigation(context, {
249
+ listRef: elementsRef,
250
+ activeIndex,
251
+ onNavigate: setActiveIndex,
252
+ loop: true,
253
+ // Hover and focus states should be differentiated; disable focus-on-hover
254
+ // so that hover styling can be handled separately via CSS :hover.
255
+ focusItemOnHover: false,
256
+ // focusItemOnOpen: false,
257
+ });
258
+
259
+ const typeahead = useTypeahead(context, {
260
+ listRef: labelsRef,
261
+ activeIndex,
262
+ onMatch: setActiveIndex,
263
+ });
264
+
265
+ const triggerInteractions = useInteractions([click, role, listNavigation, typeahead]);
266
+ const anchorInteractions = useInteractions([role, listNavigation, typeahead]);
267
+
268
+ const stateProps = useMemo(
269
+ () =>
270
+ elementProps({
271
+ "data-hidden": dataAttr(status === "unmounted"),
272
+ "data-open": dataAttr(status === "open" || status === "initial"),
273
+ }),
274
+ [status],
275
+ );
276
+
277
+ return {
278
+ open,
279
+ setOpen,
280
+ activeIndex,
281
+ stateProps,
282
+
283
+ floatingContext: context,
284
+ elementsRef,
285
+ labelsRef,
286
+
287
+ refs: {
288
+ anchor: (node: HTMLElement | null) => {
289
+ floatingRefs.setReference(node);
290
+ },
291
+ trigger: (node: HTMLElement | null) => {
292
+ floatingRefs.setReference(node);
293
+ triggerRef.current = node;
294
+ },
295
+ positioner: floatingRefs.setFloating,
296
+ },
297
+
298
+ anchorProps: elementProps({
299
+ "data-open": dataAttr(open),
300
+ ...anchorInteractions.getReferenceProps(),
301
+ }),
302
+
303
+ triggerProps: buttonProps({
304
+ "aria-haspopup": "menu",
305
+ "aria-expanded": open,
306
+ "data-open": dataAttr(open),
307
+ ...triggerInteractions.getReferenceProps(),
308
+ }),
309
+
310
+ positionerProps: elementProps({
311
+ ...stateProps,
312
+ style: floatingStyles,
313
+ }),
314
+
315
+ contentProps: elementProps({
316
+ ...stateProps,
317
+ role: "menu",
318
+ style: {
319
+ "--seed-menu-transform-origin": getTransformOrigin(context.placement),
320
+ } as React.CSSProperties,
321
+ ...triggerInteractions.getFloatingProps(),
322
+ }),
323
+
324
+ getItemProps: (itemProps: UseMenuItemProps, index: number) => {
325
+ const isActive = activeIndex === index;
326
+
327
+ const itemStateProps = elementProps({
328
+ // implement when submenu is needed
329
+ // "data-highlighted": dataAttr(isActive),
330
+ "data-disabled": dataAttr(itemProps.disabled),
331
+ });
332
+
333
+ return {
334
+ // implement when submenu is needed
335
+ // isHighlighted: isActive,
336
+ isDisabled: itemProps.disabled,
337
+
338
+ stateProps: itemStateProps,
339
+
340
+ rootProps: elementProps({
341
+ ...itemStateProps,
342
+ ...triggerInteractions.getItemProps({
343
+ onClick(event) {
344
+ if (itemProps.disabled) return;
345
+ if (event.defaultPrevented) return;
346
+ itemProps.onClick?.(event);
347
+ setOpen(false, { reason: "itemClick", event: event.nativeEvent });
348
+ },
349
+ onKeyDown(event) {
350
+ if (itemProps.disabled) return;
351
+ if (event.key === "Enter" || event.key === " ") {
352
+ event.preventDefault();
353
+
354
+ if (!(event.currentTarget instanceof HTMLElement)) return;
355
+
356
+ event.currentTarget.click();
357
+ }
358
+ },
359
+ }),
360
+ role: "menuitem",
361
+ tabIndex: isActive ? 0 : -1,
362
+ "aria-disabled": itemProps.disabled ? "true" : undefined,
363
+ }),
364
+ };
365
+ },
366
+
367
+ // implement when submenu is needed
368
+ // getSubmenuTriggerProps: (itemProps: UseMenuSubmenuTriggerProps, index: number) => {
369
+ // const isActive = activeIndex === index;
370
+ //
371
+ // const itemStateProps = elementProps({
372
+ // "data-highlighted": dataAttr(isActive),
373
+ // "data-disabled": dataAttr(itemProps.disabled),
374
+ // });
375
+ //
376
+ // return {
377
+ // isHighlighted: isActive,
378
+ // isDisabled: itemProps.disabled,
379
+ //
380
+ // stateProps: itemStateProps,
381
+ //
382
+ // rootProps: elementProps({
383
+ // ...itemStateProps,
384
+ // ...triggerInteractions.getItemProps(),
385
+ // role: "menuitem",
386
+ // tabIndex: isActive ? 0 : -1,
387
+ // "aria-disabled": itemProps.disabled ? "true" : undefined,
388
+ // }),
389
+ // };
390
+ // },
391
+
392
+ getGroupProps: () => {
393
+ const groupIndex = groupIndexCounter.current++;
394
+ const labelId = `menu:${menuId}:group-${groupIndex}:label`;
395
+ return {
396
+ labelId,
397
+ rootProps: elementProps({
398
+ role: "group",
399
+ "aria-labelledby": labelId,
400
+ }),
401
+ };
402
+ },
403
+
404
+ getGroupLabelProps: (labelId: string) => {
405
+ return elementProps({
406
+ role: "presentation",
407
+ id: labelId,
408
+ });
409
+ },
410
+ };
411
+ }