@octanejs/aria 0.0.4 → 0.0.5
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/package.json +7 -5
- package/src/autocomplete/useAutocomplete.ts +656 -0
- package/src/collections/BaseCollection.ts +374 -0
- package/src/collections/CollectionBuilder.ts +343 -0
- package/src/collections/Document.ts +451 -0
- package/src/collections/Hidden.ts +84 -0
- package/src/collections/useCachedChildren.ts +103 -0
- package/src/components/Autocomplete.ts +124 -0
- package/src/components/Breadcrumbs.ts +170 -0
- package/src/components/Button.ts +209 -0
- package/src/components/Checkbox.ts +593 -0
- package/src/components/Collection.ts +301 -0
- package/src/components/ComboBox.ts +477 -0
- package/src/components/Dialog.ts +195 -0
- package/src/components/Disclosure.ts +322 -0
- package/src/components/DragAndDrop.ts +177 -0
- package/src/components/FieldError.ts +79 -0
- package/src/components/Form.ts +65 -0
- package/src/components/GridList.ts +1099 -0
- package/src/components/Group.ts +146 -0
- package/src/components/Header.ts +32 -0
- package/src/components/Heading.ts +45 -0
- package/src/components/Input.ts +139 -0
- package/src/components/Keyboard.ts +23 -0
- package/src/components/Label.ts +30 -0
- package/src/components/Link.ts +137 -0
- package/src/components/ListBox.ts +886 -0
- package/src/components/Menu.ts +685 -0
- package/src/components/Meter.ts +100 -0
- package/src/components/Modal.ts +352 -0
- package/src/components/NumberField.ts +221 -0
- package/src/components/OverlayArrow.ts +117 -0
- package/src/components/Popover.ts +385 -0
- package/src/components/ProgressBar.ts +112 -0
- package/src/components/RadioGroup.ts +542 -0
- package/src/components/SearchField.ts +208 -0
- package/src/components/Select.ts +505 -0
- package/src/components/SelectionIndicator.ts +52 -0
- package/src/components/Separator.ts +97 -0
- package/src/components/SharedElementTransition.ts +209 -0
- package/src/components/Slider.ts +472 -0
- package/src/components/Switch.ts +449 -0
- package/src/components/Tabs.ts +675 -0
- package/src/components/TagGroup.ts +443 -0
- package/src/components/Text.ts +27 -0
- package/src/components/TextArea.ts +92 -0
- package/src/components/TextField.ts +200 -0
- package/src/components/ToggleButton.ts +147 -0
- package/src/components/ToggleButtonGroup.ts +106 -0
- package/src/components/Toolbar.ts +74 -0
- package/src/components/Tooltip.ts +249 -0
- package/src/components/index.ts +302 -0
- package/src/components/useDragAndDrop.ts +164 -0
- package/src/components/utils.ts +521 -0
- package/src/intl/autocomplete/ar-AE.json +3 -0
- package/src/intl/autocomplete/bg-BG.json +3 -0
- package/src/intl/autocomplete/cs-CZ.json +3 -0
- package/src/intl/autocomplete/da-DK.json +3 -0
- package/src/intl/autocomplete/de-DE.json +3 -0
- package/src/intl/autocomplete/el-GR.json +3 -0
- package/src/intl/autocomplete/en-US.json +3 -0
- package/src/intl/autocomplete/es-ES.json +3 -0
- package/src/intl/autocomplete/et-EE.json +3 -0
- package/src/intl/autocomplete/fi-FI.json +3 -0
- package/src/intl/autocomplete/fr-FR.json +3 -0
- package/src/intl/autocomplete/he-IL.json +3 -0
- package/src/intl/autocomplete/hr-HR.json +3 -0
- package/src/intl/autocomplete/hu-HU.json +3 -0
- package/src/intl/autocomplete/index.ts +75 -0
- package/src/intl/autocomplete/it-IT.json +3 -0
- package/src/intl/autocomplete/ja-JP.json +3 -0
- package/src/intl/autocomplete/ko-KR.json +3 -0
- package/src/intl/autocomplete/lt-LT.json +3 -0
- package/src/intl/autocomplete/lv-LV.json +3 -0
- package/src/intl/autocomplete/nb-NO.json +3 -0
- package/src/intl/autocomplete/nl-NL.json +3 -0
- package/src/intl/autocomplete/pl-PL.json +3 -0
- package/src/intl/autocomplete/pt-BR.json +3 -0
- package/src/intl/autocomplete/pt-PT.json +3 -0
- package/src/intl/autocomplete/ro-RO.json +3 -0
- package/src/intl/autocomplete/ru-RU.json +3 -0
- package/src/intl/autocomplete/sk-SK.json +3 -0
- package/src/intl/autocomplete/sl-SI.json +3 -0
- package/src/intl/autocomplete/sr-SP.json +3 -0
- package/src/intl/autocomplete/sv-SE.json +3 -0
- package/src/intl/autocomplete/tr-TR.json +3 -0
- package/src/intl/autocomplete/uk-UA.json +3 -0
- package/src/intl/autocomplete/zh-CN.json +3 -0
- package/src/intl/autocomplete/zh-TW.json +3 -0
- package/src/stately/autocomplete/useAutocompleteState.ts +85 -0
- package/src/utils/animation.ts +149 -0
- package/src/utils/useLoadMoreSentinel.ts +78 -0
- package/src/utils/useViewportSize.ts +123 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
// Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/Menu.tsx).
|
|
2
|
+
// octane adaptations: `.tsx` → `.ts`, JSX → `createElement` (multi-child elements use the
|
|
3
|
+
// positional-children form); NO forwardRef — the forwarded ref is `props.ref`, passed into
|
|
4
|
+
// `useContextProps` explicitly; the plain-`.ts` components use the S()/subSlot component-slot
|
|
5
|
+
// convention. The collection composes the Phase-4 engine: `CollectionBuilder`/
|
|
6
|
+
// `createLeafComponent`/`createBranchComponent` from `../collections/CollectionBuilder`,
|
|
7
|
+
// `CollectionRoot`/`CollectionBranch` via `CollectionRendererContext`. Upstream's RAC-local
|
|
8
|
+
// `CollectionProps` import is our `ItemCollectionProps` (see ./Collection.ts). react-aria/
|
|
9
|
+
// react-stately private imports come from the binding's ported modules; React's element/ref
|
|
10
|
+
// types → structural aliases. Upstream's inline `useMenuTriggerState({})` fallback inside the
|
|
11
|
+
// Provider values array is hoisted to a local (octane hooks are slot-keyed, so the conditional
|
|
12
|
+
// call needs no rules-of-hooks escape comment; the defined/undefined stability assumption is
|
|
13
|
+
// upstream's).
|
|
14
|
+
import type {
|
|
15
|
+
FocusEvents,
|
|
16
|
+
FocusStrategy,
|
|
17
|
+
HoverEvents,
|
|
18
|
+
Key,
|
|
19
|
+
LinkDOMProps,
|
|
20
|
+
MultipleSelection,
|
|
21
|
+
Node,
|
|
22
|
+
PressEvents,
|
|
23
|
+
} from '@react-types/shared';
|
|
24
|
+
import { createContext, createElement, useContext, useMemo, useRef } from 'octane';
|
|
25
|
+
|
|
26
|
+
import { type AriaMenuProps, useMenu } from '../menu/useMenu';
|
|
27
|
+
import { useMenuItem } from '../menu/useMenuItem';
|
|
28
|
+
import { useMenuSection } from '../menu/useMenuSection';
|
|
29
|
+
import { useMenuTrigger } from '../menu/useMenuTrigger';
|
|
30
|
+
import { useSubmenuTrigger } from '../menu/useSubmenuTrigger';
|
|
31
|
+
import {
|
|
32
|
+
BaseCollection,
|
|
33
|
+
CollectionNode,
|
|
34
|
+
ItemNode,
|
|
35
|
+
SectionNode,
|
|
36
|
+
} from '../collections/BaseCollection';
|
|
37
|
+
import {
|
|
38
|
+
CollectionBuilder,
|
|
39
|
+
createBranchComponent,
|
|
40
|
+
createLeafComponent,
|
|
41
|
+
} from '../collections/CollectionBuilder';
|
|
42
|
+
import { useIsHidden } from '../collections/Hidden';
|
|
43
|
+
import { FocusScope } from '../focus/FocusScope';
|
|
44
|
+
import { PressResponder } from '../interactions/PressResponder';
|
|
45
|
+
import { useHover } from '../interactions/useHover';
|
|
46
|
+
import { S, subSlot } from '../internal';
|
|
47
|
+
import {
|
|
48
|
+
type MenuTriggerProps as BaseMenuTriggerProps,
|
|
49
|
+
type RootMenuTriggerState,
|
|
50
|
+
useMenuTriggerState,
|
|
51
|
+
} from '../stately/menu/useMenuTriggerState';
|
|
52
|
+
import { useSubmenuTriggerState } from '../stately/menu/useSubmenuTriggerState';
|
|
53
|
+
import { SelectionManager } from '../stately/selection/SelectionManager';
|
|
54
|
+
import type { MultipleSelectionState } from '../stately/selection/types';
|
|
55
|
+
import { useMultipleSelectionState } from '../stately/selection/useMultipleSelectionState';
|
|
56
|
+
import { type TreeState, useTreeState } from '../stately/tree/useTreeState';
|
|
57
|
+
import { filterDOMProps } from '../utils/filterDOMProps';
|
|
58
|
+
import { mergeProps } from '../utils/mergeProps';
|
|
59
|
+
import { useObjectRef } from '../utils/useObjectRef';
|
|
60
|
+
import {
|
|
61
|
+
FieldInputContext,
|
|
62
|
+
SelectableCollectionContext,
|
|
63
|
+
type SelectableCollectionContextValue,
|
|
64
|
+
} from './Autocomplete';
|
|
65
|
+
import {
|
|
66
|
+
Collection,
|
|
67
|
+
CollectionRendererContext,
|
|
68
|
+
type ItemCollectionProps,
|
|
69
|
+
type ItemRenderProps,
|
|
70
|
+
SectionContext,
|
|
71
|
+
type SectionProps,
|
|
72
|
+
usePersistedKeys,
|
|
73
|
+
} from './Collection';
|
|
74
|
+
import { OverlayTriggerStateContext } from './Dialog';
|
|
75
|
+
import { HeaderContext } from './Header';
|
|
76
|
+
import { KeyboardContext } from './Keyboard';
|
|
77
|
+
import { PopoverContext } from './Popover';
|
|
78
|
+
import { SelectionIndicatorContext } from './SelectionIndicator';
|
|
79
|
+
import { SeparatorContext } from './Separator';
|
|
80
|
+
import { SharedElementTransition } from './SharedElementTransition';
|
|
81
|
+
import { TextContext } from './Text';
|
|
82
|
+
import {
|
|
83
|
+
type ClassNameOrFunction,
|
|
84
|
+
type ContextValue,
|
|
85
|
+
DEFAULT_SLOT,
|
|
86
|
+
dom,
|
|
87
|
+
type DOMRenderProps,
|
|
88
|
+
type PossibleLinkDOMRenderProps,
|
|
89
|
+
Provider,
|
|
90
|
+
type RenderProps,
|
|
91
|
+
type SlotProps,
|
|
92
|
+
type StyleRenderProps,
|
|
93
|
+
useContextProps,
|
|
94
|
+
useRenderProps,
|
|
95
|
+
useSlot,
|
|
96
|
+
useSlottedContext,
|
|
97
|
+
} from './utils';
|
|
98
|
+
|
|
99
|
+
// octane adaptations: structural aliases for the React types upstream drags along.
|
|
100
|
+
type ReactNode = any;
|
|
101
|
+
type ReactElement = any;
|
|
102
|
+
type GlobalDOMAttributes = Record<string, any>;
|
|
103
|
+
type RefObject<T> = { current: T };
|
|
104
|
+
|
|
105
|
+
export const MenuContext = createContext<ContextValue<MenuProps<any>, HTMLDivElement>>(null);
|
|
106
|
+
export const MenuStateContext = createContext<TreeState<any> | null>(null);
|
|
107
|
+
export const RootMenuTriggerStateContext = createContext<RootMenuTriggerState | null>(null);
|
|
108
|
+
const SelectionManagerContext = createContext<SelectionManager | null>(null);
|
|
109
|
+
|
|
110
|
+
export interface MenuTriggerProps extends BaseMenuTriggerProps {
|
|
111
|
+
children: ReactNode;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function MenuTrigger(props: MenuTriggerProps): any {
|
|
115
|
+
const slot = S('MenuTrigger');
|
|
116
|
+
let state = useMenuTriggerState(props, subSlot(slot, 'state'));
|
|
117
|
+
let ref = useRef<HTMLButtonElement | null>(null, subSlot(slot, 'triggerRef'));
|
|
118
|
+
let { menuTriggerProps, menuProps } = useMenuTrigger(
|
|
119
|
+
{
|
|
120
|
+
...props,
|
|
121
|
+
type: 'menu',
|
|
122
|
+
},
|
|
123
|
+
state,
|
|
124
|
+
ref,
|
|
125
|
+
subSlot(slot, 'trigger'),
|
|
126
|
+
);
|
|
127
|
+
let scrollRef = useRef<HTMLElement | null>(null, subSlot(slot, 'scrollRef'));
|
|
128
|
+
|
|
129
|
+
// If within a collection (e.g. Tabs), render nothing.
|
|
130
|
+
// Not using createHideableComponent for this because that also creates a forwardRef.
|
|
131
|
+
let isHidden = useIsHidden();
|
|
132
|
+
if (isHidden) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return createElement(Provider, {
|
|
137
|
+
values: [
|
|
138
|
+
[MenuContext, { ...menuProps, ref: scrollRef }],
|
|
139
|
+
[OverlayTriggerStateContext, state],
|
|
140
|
+
[RootMenuTriggerStateContext, state],
|
|
141
|
+
[
|
|
142
|
+
PopoverContext,
|
|
143
|
+
{
|
|
144
|
+
trigger: 'MenuTrigger',
|
|
145
|
+
triggerRef: ref,
|
|
146
|
+
scrollRef,
|
|
147
|
+
placement: 'bottom start',
|
|
148
|
+
'aria-labelledby': (menuProps as any)['aria-labelledby'],
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
] as any,
|
|
152
|
+
children: createElement(PressResponder, {
|
|
153
|
+
...menuTriggerProps,
|
|
154
|
+
ref,
|
|
155
|
+
isPressed: state.isOpen,
|
|
156
|
+
children: props.children,
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface SubmenuTriggerProps {
|
|
162
|
+
/**
|
|
163
|
+
* The contents of the SubmenuTrigger. The first child should be an Item (the trigger) and the
|
|
164
|
+
* second child should be the Popover (for the submenu).
|
|
165
|
+
*/
|
|
166
|
+
children: ReactElement[];
|
|
167
|
+
/**
|
|
168
|
+
* The delay time in milliseconds for the submenu to appear after hovering over the trigger.
|
|
169
|
+
*
|
|
170
|
+
* @default 200
|
|
171
|
+
*/
|
|
172
|
+
delay?: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const SubmenuTriggerContext = createContext<{
|
|
176
|
+
parentMenuRef: RefObject<HTMLElement | null>;
|
|
177
|
+
shouldUseVirtualFocus?: boolean;
|
|
178
|
+
} | null>(null);
|
|
179
|
+
|
|
180
|
+
class SubmenuTriggerNode<T> extends CollectionNode<T> {
|
|
181
|
+
static readonly type = 'submenutrigger';
|
|
182
|
+
|
|
183
|
+
filter(
|
|
184
|
+
collection: BaseCollection<T>,
|
|
185
|
+
newCollection: BaseCollection<T>,
|
|
186
|
+
filterFn: (textValue: string, node: Node<T>) => boolean,
|
|
187
|
+
): CollectionNode<T> | null {
|
|
188
|
+
let triggerNode = collection.getItem(this.firstChildKey!);
|
|
189
|
+
if (triggerNode && filterFn(triggerNode.textValue, this)) {
|
|
190
|
+
let clone = this.clone();
|
|
191
|
+
newCollection.addDescendants(clone, collection);
|
|
192
|
+
return clone;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A submenu trigger is used to wrap a submenu's trigger item and the submenu itself.
|
|
201
|
+
*
|
|
202
|
+
* @version alpha
|
|
203
|
+
*/
|
|
204
|
+
export const SubmenuTrigger: (props: SubmenuTriggerProps & { ref?: any }) => any =
|
|
205
|
+
/*#__PURE__*/ createBranchComponent(
|
|
206
|
+
SubmenuTriggerNode,
|
|
207
|
+
(props: SubmenuTriggerProps, ref: any, item: Node<any>): any => {
|
|
208
|
+
const slot = S('SubmenuTrigger');
|
|
209
|
+
let { CollectionBranch } = useContext(CollectionRendererContext);
|
|
210
|
+
let state = useContext(MenuStateContext)!;
|
|
211
|
+
let rootMenuTriggerState = useContext(RootMenuTriggerStateContext)!;
|
|
212
|
+
let submenuTriggerState = useSubmenuTriggerState(
|
|
213
|
+
{ triggerKey: item.key },
|
|
214
|
+
rootMenuTriggerState,
|
|
215
|
+
subSlot(slot, 'state'),
|
|
216
|
+
);
|
|
217
|
+
let submenuRef = useRef<HTMLDivElement | null>(null, subSlot(slot, 'submenuRef'));
|
|
218
|
+
let itemRef = useObjectRef<any>(ref, subSlot(slot, 'itemRef'));
|
|
219
|
+
let { parentMenuRef, shouldUseVirtualFocus } = useContext(SubmenuTriggerContext)!;
|
|
220
|
+
let { submenuTriggerProps, submenuProps, popoverProps } = useSubmenuTrigger(
|
|
221
|
+
{
|
|
222
|
+
parentMenuRef,
|
|
223
|
+
submenuRef,
|
|
224
|
+
delay: props.delay,
|
|
225
|
+
shouldUseVirtualFocus,
|
|
226
|
+
},
|
|
227
|
+
submenuTriggerState,
|
|
228
|
+
itemRef,
|
|
229
|
+
subSlot(slot, 'trigger'),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
return createElement(
|
|
233
|
+
Provider,
|
|
234
|
+
// octane adaptation: the two children arrive positionally below.
|
|
235
|
+
{
|
|
236
|
+
values: [
|
|
237
|
+
[MenuItemContext, { ...submenuTriggerProps, onAction: undefined, ref: itemRef }],
|
|
238
|
+
[
|
|
239
|
+
MenuContext,
|
|
240
|
+
{
|
|
241
|
+
ref: submenuRef,
|
|
242
|
+
...submenuProps,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
[OverlayTriggerStateContext, submenuTriggerState],
|
|
246
|
+
[
|
|
247
|
+
PopoverContext,
|
|
248
|
+
{
|
|
249
|
+
trigger: 'SubmenuTrigger',
|
|
250
|
+
triggerRef: itemRef,
|
|
251
|
+
placement: 'end top',
|
|
252
|
+
'aria-labelledby': (submenuProps as any)['aria-labelledby'],
|
|
253
|
+
...popoverProps,
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
] as any,
|
|
257
|
+
} as any,
|
|
258
|
+
createElement(CollectionBranch, { collection: state.collection, parent: item }),
|
|
259
|
+
props.children[1],
|
|
260
|
+
);
|
|
261
|
+
},
|
|
262
|
+
(props) => props.children[0],
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
export interface MenuRenderProps {
|
|
266
|
+
/**
|
|
267
|
+
* Whether the menu has no items and should display its empty state.
|
|
268
|
+
*
|
|
269
|
+
* @selector [data-empty]
|
|
270
|
+
*/
|
|
271
|
+
isEmpty: boolean;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface MenuProps<T>
|
|
275
|
+
extends
|
|
276
|
+
Omit<AriaMenuProps<T>, 'children'>,
|
|
277
|
+
ItemCollectionProps<T>,
|
|
278
|
+
StyleRenderProps<MenuRenderProps>,
|
|
279
|
+
SlotProps,
|
|
280
|
+
GlobalDOMAttributes {
|
|
281
|
+
/**
|
|
282
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
283
|
+
* element. A function may be provided to compute the class based on component state.
|
|
284
|
+
*
|
|
285
|
+
* @default 'react-aria-Menu'
|
|
286
|
+
*/
|
|
287
|
+
className?: ClassNameOrFunction<MenuRenderProps>;
|
|
288
|
+
/** Provides content to display when there are no items in the list. */
|
|
289
|
+
renderEmptyState?: () => ReactNode;
|
|
290
|
+
/** Whether the menu should close when the menu item is selected. */
|
|
291
|
+
shouldCloseOnSelect?: boolean;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* A menu displays a list of actions or options that a user can choose.
|
|
296
|
+
*/
|
|
297
|
+
export function Menu<T>(props: MenuProps<T>): any {
|
|
298
|
+
const slot = S('Menu');
|
|
299
|
+
let ref: any;
|
|
300
|
+
[props, ref] = useContextProps(props, (props as any).ref, MenuContext, subSlot(slot, 'ctx'));
|
|
301
|
+
|
|
302
|
+
// Delay rendering the actual menu until we have the collection so that auto focus works properly.
|
|
303
|
+
return createElement(CollectionBuilder, {
|
|
304
|
+
content: createElement(Collection, props as any),
|
|
305
|
+
children: (collection: BaseCollection<any>) =>
|
|
306
|
+
createElement(MenuInner, { props, collection, menuRef: ref } as any),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
interface MenuInnerProps<T> {
|
|
311
|
+
// For now we append filter and other autocomplete context props here for typescript, but eventually we can consider exposing these
|
|
312
|
+
// as top level props for users to use with standalone Menus
|
|
313
|
+
props: MenuProps<T> & {
|
|
314
|
+
filter?: SelectableCollectionContextValue<object>['filter'];
|
|
315
|
+
shouldUseVirtualFocus?: boolean;
|
|
316
|
+
};
|
|
317
|
+
collection: BaseCollection<any>;
|
|
318
|
+
menuRef: RefObject<HTMLElement | null>;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function MenuInner<T>({ props, collection, menuRef }: MenuInnerProps<T>): any {
|
|
322
|
+
const slot = S('MenuInner');
|
|
323
|
+
let ref: any;
|
|
324
|
+
[props, ref] = useContextProps(
|
|
325
|
+
props as any,
|
|
326
|
+
menuRef as any,
|
|
327
|
+
SelectableCollectionContext as any,
|
|
328
|
+
subSlot(slot, 'ctx'),
|
|
329
|
+
);
|
|
330
|
+
let { filter, ...autocompleteMenuProps } = props;
|
|
331
|
+
let filteredCollection = useMemo(
|
|
332
|
+
() => (filter ? collection.filter(filter) : collection),
|
|
333
|
+
[collection, filter],
|
|
334
|
+
subSlot(slot, 'filtered'),
|
|
335
|
+
);
|
|
336
|
+
let state = useTreeState(
|
|
337
|
+
{
|
|
338
|
+
...props,
|
|
339
|
+
collection: filteredCollection,
|
|
340
|
+
children: undefined,
|
|
341
|
+
} as any,
|
|
342
|
+
subSlot(slot, 'state'),
|
|
343
|
+
);
|
|
344
|
+
let triggerState = useContext(RootMenuTriggerStateContext);
|
|
345
|
+
let { isVirtualized, CollectionRoot } = useContext(CollectionRendererContext);
|
|
346
|
+
let { menuProps } = useMenu(
|
|
347
|
+
{ ...props, isVirtualized, onClose: props.onClose || triggerState?.close } as any,
|
|
348
|
+
state,
|
|
349
|
+
ref,
|
|
350
|
+
subSlot(slot, 'menu'),
|
|
351
|
+
);
|
|
352
|
+
let renderProps = useRenderProps(
|
|
353
|
+
{
|
|
354
|
+
...props,
|
|
355
|
+
children: undefined,
|
|
356
|
+
defaultClassName: 'react-aria-Menu',
|
|
357
|
+
values: {
|
|
358
|
+
isEmpty: state.collection.size === 0,
|
|
359
|
+
},
|
|
360
|
+
} as any,
|
|
361
|
+
subSlot(slot, 'render'),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
let emptyState: any = null;
|
|
365
|
+
if (state.collection.size === 0 && props.renderEmptyState) {
|
|
366
|
+
emptyState = createElement('div', {
|
|
367
|
+
role: 'menuitem',
|
|
368
|
+
style: { display: 'contents' },
|
|
369
|
+
children: props.renderEmptyState(),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let DOMProps = filterDOMProps(props, { global: true });
|
|
374
|
+
|
|
375
|
+
// octane adaptation: hooks hoisted out of the createElement expressions below.
|
|
376
|
+
let persistedKeys = usePersistedKeys(
|
|
377
|
+
state.selectionManager.focusedKey,
|
|
378
|
+
subSlot(slot, 'persisted'),
|
|
379
|
+
);
|
|
380
|
+
/* Ensure root MenuTriggerState is defined, in case Menu is rendered outside a MenuTrigger. */
|
|
381
|
+
/* We assume the context can never change between defined and undefined. */
|
|
382
|
+
let rootTriggerState = triggerState ?? useMenuTriggerState({}, subSlot(slot, 'rootTriggerState'));
|
|
383
|
+
|
|
384
|
+
return createElement(FocusScope, {
|
|
385
|
+
children: createElement(
|
|
386
|
+
dom.div,
|
|
387
|
+
{
|
|
388
|
+
...mergeProps(DOMProps, renderProps, menuProps),
|
|
389
|
+
ref,
|
|
390
|
+
slot: props.slot || undefined,
|
|
391
|
+
'data-empty': state.collection.size === 0 || undefined,
|
|
392
|
+
onScroll: (props as any).onScroll,
|
|
393
|
+
},
|
|
394
|
+
createElement(Provider, {
|
|
395
|
+
values: [
|
|
396
|
+
[MenuStateContext, state],
|
|
397
|
+
[SeparatorContext, { elementType: 'div' }],
|
|
398
|
+
[SectionContext, { name: 'MenuSection', render: MenuSectionInner }],
|
|
399
|
+
[
|
|
400
|
+
SubmenuTriggerContext,
|
|
401
|
+
{
|
|
402
|
+
parentMenuRef: ref,
|
|
403
|
+
shouldUseVirtualFocus: autocompleteMenuProps?.shouldUseVirtualFocus,
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
[MenuItemContext, { shouldCloseOnSelect: props.shouldCloseOnSelect }],
|
|
407
|
+
[SelectableCollectionContext, null],
|
|
408
|
+
[FieldInputContext, null],
|
|
409
|
+
[SelectionManagerContext, state.selectionManager],
|
|
410
|
+
[RootMenuTriggerStateContext, rootTriggerState],
|
|
411
|
+
] as any,
|
|
412
|
+
children: createElement(SharedElementTransition, {
|
|
413
|
+
children: createElement(CollectionRoot, {
|
|
414
|
+
collection: state.collection,
|
|
415
|
+
persistedKeys,
|
|
416
|
+
scrollRef: ref,
|
|
417
|
+
}),
|
|
418
|
+
}),
|
|
419
|
+
}),
|
|
420
|
+
emptyState,
|
|
421
|
+
),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export interface MenuSectionProps<T>
|
|
426
|
+
extends
|
|
427
|
+
SectionProps<T>,
|
|
428
|
+
Omit<MultipleSelection, 'disabledKeys'>,
|
|
429
|
+
DOMRenderProps<'section', undefined> {
|
|
430
|
+
/**
|
|
431
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
432
|
+
* element.
|
|
433
|
+
*
|
|
434
|
+
* @default 'react-aria-MenuSection'
|
|
435
|
+
*/
|
|
436
|
+
className?: string;
|
|
437
|
+
/** Whether the menu should close when the menu item is selected. */
|
|
438
|
+
shouldCloseOnSelect?: boolean;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// A subclass of SelectionManager that forwards focus-related properties to the parent,
|
|
442
|
+
// but has its own local selection state.
|
|
443
|
+
class GroupSelectionManager extends SelectionManager {
|
|
444
|
+
private parent: SelectionManager;
|
|
445
|
+
|
|
446
|
+
constructor(parent: SelectionManager, state: MultipleSelectionState) {
|
|
447
|
+
super(parent.collection, state);
|
|
448
|
+
this.parent = parent;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
get focusedKey() {
|
|
452
|
+
return this.parent.focusedKey;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
get isFocused() {
|
|
456
|
+
return this.parent.isFocused;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
setFocusedKey(key: Key | null, childFocusStrategy?: FocusStrategy): void {
|
|
460
|
+
return this.parent.setFocusedKey(key, childFocusStrategy);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
setFocused(isFocused: boolean): void {
|
|
464
|
+
this.parent.setFocused(isFocused);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
get childFocusStrategy() {
|
|
468
|
+
return this.parent.childFocusStrategy;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function MenuSectionInner<T>(
|
|
473
|
+
props: MenuSectionProps<T>,
|
|
474
|
+
ref: any,
|
|
475
|
+
section: Node<T>,
|
|
476
|
+
className = 'react-aria-MenuSection',
|
|
477
|
+
): any {
|
|
478
|
+
const slot = S('MenuSectionInner');
|
|
479
|
+
let state = useContext(MenuStateContext)!;
|
|
480
|
+
let { CollectionBranch } = useContext(CollectionRendererContext);
|
|
481
|
+
let [headingRef, heading] = useSlot(undefined, subSlot(slot, 'headingSlot'));
|
|
482
|
+
let { headingProps, groupProps } = useMenuSection(
|
|
483
|
+
{
|
|
484
|
+
heading,
|
|
485
|
+
'aria-label': section.props['aria-label'] ?? undefined,
|
|
486
|
+
},
|
|
487
|
+
subSlot(slot, 'section'),
|
|
488
|
+
);
|
|
489
|
+
let renderProps = useRenderProps(
|
|
490
|
+
{
|
|
491
|
+
...props,
|
|
492
|
+
id: undefined,
|
|
493
|
+
children: undefined,
|
|
494
|
+
defaultClassName: className,
|
|
495
|
+
className: section.props?.className,
|
|
496
|
+
style: section.props?.style,
|
|
497
|
+
values: undefined,
|
|
498
|
+
} as any,
|
|
499
|
+
subSlot(slot, 'render'),
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
let parent = useContext(SelectionManagerContext)!;
|
|
503
|
+
let selectionState = useMultipleSelectionState(props, subSlot(slot, 'selectionState'));
|
|
504
|
+
let manager =
|
|
505
|
+
props.selectionMode != null ? new GroupSelectionManager(parent, selectionState) : parent;
|
|
506
|
+
|
|
507
|
+
let closeOnSelect = useSlottedContext(MenuItemContext)?.shouldCloseOnSelect;
|
|
508
|
+
|
|
509
|
+
let DOMProps = filterDOMProps(props as any, { global: true });
|
|
510
|
+
delete DOMProps.id;
|
|
511
|
+
|
|
512
|
+
return createElement(dom.section, {
|
|
513
|
+
...mergeProps(DOMProps, renderProps, groupProps),
|
|
514
|
+
ref,
|
|
515
|
+
children: createElement(Provider, {
|
|
516
|
+
values: [
|
|
517
|
+
[HeaderContext, { ...headingProps, ref: headingRef }],
|
|
518
|
+
[SelectionManagerContext, manager],
|
|
519
|
+
[MenuItemContext, { shouldCloseOnSelect: props.shouldCloseOnSelect ?? closeOnSelect }],
|
|
520
|
+
] as any,
|
|
521
|
+
children: createElement(CollectionBranch, {
|
|
522
|
+
collection: state.collection,
|
|
523
|
+
parent: section,
|
|
524
|
+
}),
|
|
525
|
+
}),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* A MenuSection represents a section within a Menu.
|
|
531
|
+
*/
|
|
532
|
+
export const MenuSection: <T extends object>(props: MenuSectionProps<T> & { ref?: any }) => any =
|
|
533
|
+
/*#__PURE__*/ createBranchComponent(SectionNode, MenuSectionInner) as any;
|
|
534
|
+
|
|
535
|
+
export interface MenuItemRenderProps extends ItemRenderProps {
|
|
536
|
+
/**
|
|
537
|
+
* Whether the item has a submenu.
|
|
538
|
+
*
|
|
539
|
+
* @selector [data-has-submenu]
|
|
540
|
+
*/
|
|
541
|
+
hasSubmenu: boolean;
|
|
542
|
+
/**
|
|
543
|
+
* Whether the item's submenu is open.
|
|
544
|
+
*
|
|
545
|
+
* @selector [data-open]
|
|
546
|
+
*/
|
|
547
|
+
isOpen: boolean;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export interface MenuItemProps<T = object>
|
|
551
|
+
extends
|
|
552
|
+
Omit<RenderProps<MenuItemRenderProps>, 'render'>,
|
|
553
|
+
PossibleLinkDOMRenderProps<'div', MenuItemRenderProps>,
|
|
554
|
+
LinkDOMProps,
|
|
555
|
+
HoverEvents,
|
|
556
|
+
FocusEvents,
|
|
557
|
+
PressEvents,
|
|
558
|
+
Omit<GlobalDOMAttributes, 'onClick'> {
|
|
559
|
+
/**
|
|
560
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
561
|
+
* element. A function may be provided to compute the class based on component state.
|
|
562
|
+
*
|
|
563
|
+
* @default 'react-aria-MenuItem'
|
|
564
|
+
*/
|
|
565
|
+
className?: ClassNameOrFunction<MenuItemRenderProps>;
|
|
566
|
+
/** The unique id of the item. */
|
|
567
|
+
id?: Key;
|
|
568
|
+
/**
|
|
569
|
+
* The object value that this item represents. When using dynamic collections, this is set
|
|
570
|
+
* automatically.
|
|
571
|
+
*/
|
|
572
|
+
value?: T;
|
|
573
|
+
/** A string representation of the item's contents, used for features like typeahead. */
|
|
574
|
+
textValue?: string;
|
|
575
|
+
/** An accessibility label for this item. */
|
|
576
|
+
'aria-label'?: string;
|
|
577
|
+
/** Whether the item is disabled. */
|
|
578
|
+
isDisabled?: boolean;
|
|
579
|
+
/** Handler that is called when the item is selected. */
|
|
580
|
+
onAction?: () => void;
|
|
581
|
+
/** Whether the menu should close when the menu item is selected. */
|
|
582
|
+
shouldCloseOnSelect?: boolean;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const MenuItemContext = createContext<ContextValue<MenuItemProps, HTMLDivElement>>(null);
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* A MenuItem represents an individual action in a Menu.
|
|
589
|
+
*/
|
|
590
|
+
export const MenuItem: <T extends object = object>(props: MenuItemProps<T> & { ref?: any }) => any =
|
|
591
|
+
/*#__PURE__*/ createLeafComponent(
|
|
592
|
+
ItemNode,
|
|
593
|
+
// The third (item) parameter is declared so `render.length === 3` keeps the
|
|
594
|
+
// engine's "cannot be rendered outside a collection" guard; it is always
|
|
595
|
+
// provided when rendered from a collection node.
|
|
596
|
+
function MenuItem<T>(props: MenuItemProps<T>, forwardedRef: any, item?: Node<T>): any {
|
|
597
|
+
const slot = S('MenuItem');
|
|
598
|
+
[props, forwardedRef] = useContextProps(
|
|
599
|
+
props,
|
|
600
|
+
forwardedRef,
|
|
601
|
+
MenuItemContext as any,
|
|
602
|
+
subSlot(slot, 'ctx'),
|
|
603
|
+
);
|
|
604
|
+
let id = useSlottedContext(MenuItemContext)?.id as string;
|
|
605
|
+
let state = useContext(MenuStateContext)!;
|
|
606
|
+
let ref = useObjectRef<any>(forwardedRef, subSlot(slot, 'objectRef'));
|
|
607
|
+
let selectionManager = useContext(SelectionManagerContext)!;
|
|
608
|
+
let { isVirtualized } = useContext(CollectionRendererContext);
|
|
609
|
+
let { menuItemProps, labelProps, descriptionProps, keyboardShortcutProps, ...states } =
|
|
610
|
+
useMenuItem(
|
|
611
|
+
{
|
|
612
|
+
...props,
|
|
613
|
+
id,
|
|
614
|
+
key: item!.key,
|
|
615
|
+
selectionManager,
|
|
616
|
+
isVirtualized: isVirtualized,
|
|
617
|
+
} as any,
|
|
618
|
+
state,
|
|
619
|
+
ref,
|
|
620
|
+
subSlot(slot, 'item'),
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
let { hoverProps, isHovered } = useHover(
|
|
624
|
+
{
|
|
625
|
+
isDisabled: states.isDisabled,
|
|
626
|
+
},
|
|
627
|
+
subSlot(slot, 'hover'),
|
|
628
|
+
);
|
|
629
|
+
let renderProps = useRenderProps<MenuItemRenderProps, any>(
|
|
630
|
+
{
|
|
631
|
+
...props,
|
|
632
|
+
id: undefined,
|
|
633
|
+
children: item!.rendered,
|
|
634
|
+
defaultClassName: 'react-aria-MenuItem',
|
|
635
|
+
values: {
|
|
636
|
+
...states,
|
|
637
|
+
isHovered,
|
|
638
|
+
isFocusVisible: states.isFocusVisible,
|
|
639
|
+
selectionMode: selectionManager.selectionMode,
|
|
640
|
+
selectionBehavior: selectionManager.selectionBehavior,
|
|
641
|
+
hasSubmenu: !!(props as any)['aria-haspopup'],
|
|
642
|
+
isOpen: (props as any)['aria-expanded'] === 'true',
|
|
643
|
+
},
|
|
644
|
+
} as any,
|
|
645
|
+
subSlot(slot, 'render'),
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
let ElementType = (props as any).href ? dom.a : dom.div;
|
|
649
|
+
let DOMProps = filterDOMProps(props as any, { global: true });
|
|
650
|
+
delete DOMProps.id;
|
|
651
|
+
delete DOMProps.onClick;
|
|
652
|
+
|
|
653
|
+
return createElement(ElementType, {
|
|
654
|
+
...mergeProps(DOMProps, renderProps, menuItemProps, hoverProps),
|
|
655
|
+
ref,
|
|
656
|
+
'data-disabled': states.isDisabled || undefined,
|
|
657
|
+
'data-hovered': isHovered || undefined,
|
|
658
|
+
'data-focused': states.isFocused || undefined,
|
|
659
|
+
'data-focus-visible': states.isFocusVisible || undefined,
|
|
660
|
+
'data-pressed': states.isPressed || undefined,
|
|
661
|
+
'data-selected': states.isSelected || undefined,
|
|
662
|
+
'data-selection-mode':
|
|
663
|
+
selectionManager.selectionMode === 'none' ? undefined : selectionManager.selectionMode,
|
|
664
|
+
'data-has-submenu': !!(props as any)['aria-haspopup'] || undefined,
|
|
665
|
+
'data-open': (props as any)['aria-expanded'] === 'true' || undefined,
|
|
666
|
+
children: createElement(Provider, {
|
|
667
|
+
values: [
|
|
668
|
+
[
|
|
669
|
+
TextContext,
|
|
670
|
+
{
|
|
671
|
+
slots: {
|
|
672
|
+
[DEFAULT_SLOT]: labelProps,
|
|
673
|
+
label: labelProps,
|
|
674
|
+
description: descriptionProps,
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
[KeyboardContext, keyboardShortcutProps],
|
|
679
|
+
[SelectionIndicatorContext, { isSelected: states.isSelected }],
|
|
680
|
+
] as any,
|
|
681
|
+
children: renderProps.children,
|
|
682
|
+
}),
|
|
683
|
+
});
|
|
684
|
+
},
|
|
685
|
+
) as any;
|