Skip to content

Commit 8ad0c43

Browse files
authored
Separate menutrigger state (#5597)
* Separate types for submenu trigger and root menu trigger states
1 parent 3900ead commit 8ad0c43

File tree

10 files changed

+52
-30
lines changed

10 files changed

+52
-30
lines changed

packages/@react-aria/menu/src/useMenuTrigger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface MenuTriggerAria<T> {
4343
* Provides the behavior and accessibility implementation for a menu trigger.
4444
* @param props - Props for the menu trigger.
4545
* @param state - State for the menu trigger.
46+
* @param ref - Ref to the HTML element trigger for the menu.
4647
*/
4748
export function useMenuTrigger<T>(props: AriaMenuTriggerProps, state: MenuTriggerState, ref: RefObject<Element>): MenuTriggerAria<T> {
4849
let {

packages/@react-spectrum/menu/src/context.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
*/
1212

1313
import {DOMProps, FocusStrategy, HoverEvents, KeyboardEvents, PressEvents} from '@react-types/shared';
14-
import {MenuTriggerState} from '@react-stately/menu';
1514
import React, {HTMLAttributes, MutableRefObject, RefObject, useContext} from 'react';
15+
import {RootMenuTriggerState} from '@react-stately/menu';
1616
import {TreeState} from '@react-stately/tree';
1717

1818
export interface MenuContextValue extends Omit<HTMLAttributes<HTMLElement>, 'autoFocus' | 'onKeyDown'>, Pick<KeyboardEvents, 'onKeyDown'> {
@@ -21,7 +21,7 @@ export interface MenuContextValue extends Omit<HTMLAttributes<HTMLElement>, 'aut
2121
shouldFocusWrap?: boolean,
2222
autoFocus?: boolean | FocusStrategy,
2323
ref?: MutableRefObject<HTMLDivElement>,
24-
state?: MenuTriggerState,
24+
state?: RootMenuTriggerState,
2525
onBackButtonPress?: () => void,
2626
submenuLevel?: number
2727
}
@@ -53,7 +53,7 @@ export interface MenuStateContextValue<T> {
5353
trayContainerRef?: RefObject<HTMLElement>,
5454
menu?: RefObject<HTMLDivElement>,
5555
submenu?: RefObject<HTMLDivElement>,
56-
rootMenuTriggerState?: MenuTriggerState
56+
rootMenuTriggerState?: RootMenuTriggerState
5757
}
5858

5959
export const MenuStateContext = React.createContext<MenuStateContextValue<any>>(undefined);

packages/@react-stately/combobox/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"@react-stately/collections": "^3.10.3",
2626
"@react-stately/form": "^3.0.0",
2727
"@react-stately/list": "^3.10.1",
28-
"@react-stately/menu": "^3.5.7",
28+
"@react-stately/overlays": "^3.6.4",
2929
"@react-stately/select": "^3.6.0",
3030
"@react-stately/utils": "^3.9.0",
3131
"@react-types/combobox": "^3.9.0",

packages/@react-stately/combobox/src/useComboBoxState.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {ListCollection, useSingleSelectListState} from '@react-stately/list';
1818
import {SelectState} from '@react-stately/select';
1919
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
2020
import {useControlledState} from '@react-stately/utils';
21-
import {useMenuTriggerState} from '@react-stately/menu';
21+
import {useOverlayTriggerState} from '@react-stately/overlays';
2222

2323
export interface ComboBoxState<T> extends SelectState<T>, FormValidationState{
2424
/** The current value of the combo box input. */
@@ -27,6 +27,8 @@ export interface ComboBoxState<T> extends SelectState<T>, FormValidationState{
2727
setInputValue(value: string): void,
2828
/** Selects the currently focused item and updates the input value. */
2929
commit(): void,
30+
/** Controls which item will be auto focused when the menu opens. */
31+
readonly focusStrategy: FocusStrategy,
3032
/** Opens the menu. */
3133
open(focusStrategy?: FocusStrategy | null, trigger?: MenuTriggerAction): void,
3234
/** Toggles the menu. */
@@ -62,6 +64,7 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
6264

6365
let [showAllItems, setShowAllItems] = useState(false);
6466
let [isFocused, setFocusedState] = useState(false);
67+
let [focusStrategy, setFocusStrategy] = useState<FocusStrategy>(null);
6568

6669
let onSelectionChange = (key) => {
6770
if (props.onSelectionChange) {
@@ -111,8 +114,8 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
111114
}
112115
};
113116

114-
let triggerState = useMenuTriggerState({...props, onOpenChange, isOpen: undefined, defaultOpen: undefined});
115-
let open = (focusStrategy?: FocusStrategy, trigger?: MenuTriggerAction) => {
117+
let triggerState = useOverlayTriggerState({...props, onOpenChange, isOpen: undefined, defaultOpen: undefined});
118+
let open = (focusStrategy: FocusStrategy = null, trigger?: MenuTriggerAction) => {
116119
let displayAllItems = (trigger === 'manual' || (trigger === 'focus' && menuTrigger === 'focus'));
117120
// Prevent open operations from triggering if there is nothing to display
118121
// Also prevent open operations from triggering if items are uncontrolled but defaultItems is empty, even if displayAllItems is true.
@@ -124,11 +127,12 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
124127
}
125128

126129
menuOpenTrigger.current = trigger;
127-
triggerState.open(focusStrategy);
130+
setFocusStrategy(focusStrategy);
131+
triggerState.open();
128132
}
129133
};
130134

131-
let toggle = (focusStrategy?: FocusStrategy, trigger?: MenuTriggerAction) => {
135+
let toggle = (focusStrategy: FocusStrategy = null, trigger?: MenuTriggerAction) => {
132136
let displayAllItems = (trigger === 'manual' || (trigger === 'focus' && menuTrigger === 'focus'));
133137
// If the menu is closed and there is nothing to display, early return so toggle isn't called to prevent extraneous onOpenChange
134138
if (!(allowsEmptyCollection || filteredCollection.size > 0 || (displayAllItems && originalCollection.size > 0) || props.items) && !triggerState.isOpen) {
@@ -154,12 +158,13 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
154158

155159
// If menu is going to close, save the current collection so we can freeze the displayed collection when the
156160
// user clicks outside the popover to close the menu. Prevents the menu contents from updating as the menu closes.
157-
let toggleMenu = useCallback((focusStrategy) => {
161+
let toggleMenu = useCallback((focusStrategy: FocusStrategy = null) => {
158162
if (triggerState.isOpen) {
159163
updateLastCollection();
160164
}
161165

162-
triggerState.toggle(focusStrategy);
166+
setFocusStrategy(focusStrategy);
167+
triggerState.toggle();
163168
}, [triggerState, updateLastCollection]);
164169

165170
let closeMenu = useCallback(() => {
@@ -347,6 +352,7 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
347352
return {
348353
...validation,
349354
...triggerState,
355+
focusStrategy,
350356
toggle,
351357
open,
352358
close: commitValue,

packages/@react-stately/menu/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ export {useMenuTriggerState} from './useMenuTriggerState';
1414
export {UNSTABLE_useSubmenuTriggerState} from './useSubmenuTriggerState';
1515

1616
export type {MenuTriggerProps} from '@react-types/menu';
17-
export type {MenuTriggerState} from './useMenuTriggerState';
17+
export type {MenuTriggerState, RootMenuTriggerState} from './useMenuTriggerState';
1818
export type {SubmenuTriggerProps, SubmenuTriggerState} from './useSubmenuTriggerState';

packages/@react-stately/menu/src/useMenuTriggerState.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,10 @@ export interface MenuTriggerState extends OverlayTriggerState {
2323
open(focusStrategy?: FocusStrategy | null): void,
2424

2525
/** Toggles the menu. */
26-
toggle(focusStrategy?: FocusStrategy | null): void,
27-
28-
/** Closes the menu and all submenus in the menu tree. */
29-
close: () => void,
26+
toggle(focusStrategy?: FocusStrategy | null): void
27+
}
3028

29+
export interface RootMenuTriggerState extends MenuTriggerState {
3130
/** Opens a specific submenu tied to a specific menu item at a specific level. */
3231
UNSTABLE_openSubmenu: (triggerKey: Key, level: number) => void,
3332

@@ -37,15 +36,18 @@ export interface MenuTriggerState extends OverlayTriggerState {
3736
/** An array of open submenu trigger keys within the menu tree.
3837
* The index of key within array matches the submenu level in the tree.
3938
*/
40-
UNSTABLE_expandedKeysStack: Key[]
39+
UNSTABLE_expandedKeysStack: Key[],
40+
41+
/** Closes the menu and all submenus in the menu tree. */
42+
close: () => void
4143
}
4244

4345
/**
4446
* Manages state for a menu trigger. Tracks whether the menu is currently open,
4547
* and controls which item will receive focus when it opens. Also tracks the open submenus within
4648
* the menu tree via their trigger keys.
4749
*/
48-
export function useMenuTriggerState(props: MenuTriggerProps): MenuTriggerState {
50+
export function useMenuTriggerState(props: MenuTriggerProps): RootMenuTriggerState {
4951
let overlayTriggerState = useOverlayTriggerState(props);
5052
let [focusStrategy, setFocusStrategy] = useState<FocusStrategy>(null);
5153
let [expandedKeysStack, setExpandedKeysStack] = useState<Key[]>([]);

packages/@react-stately/menu/src/useSubmenuTriggerState.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
*/
1212

1313
import {FocusStrategy, Key} from '@react-types/shared';
14-
import type {MenuTriggerState} from './useMenuTriggerState';
1514
import type {OverlayTriggerState} from '@react-stately/overlays';
15+
import {RootMenuTriggerState} from './useMenuTriggerState';
1616
import {useCallback, useMemo, useState} from 'react';
1717

1818
export interface SubmenuTriggerProps {
@@ -43,7 +43,7 @@ export interface SubmenuTriggerState extends OverlayTriggerState {
4343
* Manages state for a submenu trigger. Tracks whether the submenu is currently open, the level of the submenu, and
4444
* controls which item will receive focus when it opens.
4545
*/
46-
export function UNSTABLE_useSubmenuTriggerState(props: SubmenuTriggerProps, state: MenuTriggerState): SubmenuTriggerState {
46+
export function UNSTABLE_useSubmenuTriggerState(props: SubmenuTriggerProps, state: RootMenuTriggerState): SubmenuTriggerState {
4747
let {triggerKey} = props;
4848
let {UNSTABLE_expandedKeysStack, UNSTABLE_openSubmenu, UNSTABLE_closeSubmenu, close: closeAll} = state;
4949
let [submenuLevel] = useState(UNSTABLE_expandedKeysStack?.length);

packages/@react-stately/select/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"dependencies": {
2525
"@react-stately/form": "^3.0.0",
2626
"@react-stately/list": "^3.10.1",
27-
"@react-stately/menu": "^3.5.7",
27+
"@react-stately/overlays": "^3.6.4",
2828
"@react-types/select": "^3.9.0",
2929
"@react-types/shared": "^3.22.0",
3030
"@swc/helpers": "^0.5.0"

packages/@react-stately/select/src/useSelectState.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,30 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {CollectionStateBase} from '@react-types/shared';
13+
import {CollectionStateBase, FocusStrategy} from '@react-types/shared';
1414
import {FormValidationState, useFormValidationState} from '@react-stately/form';
15-
import {MenuTriggerState, useMenuTriggerState} from '@react-stately/menu';
15+
import {OverlayTriggerState, useOverlayTriggerState} from '@react-stately/overlays';
1616
import {SelectProps} from '@react-types/select';
1717
import {SingleSelectListState, useSingleSelectListState} from '@react-stately/list';
1818
import {useState} from 'react';
1919

2020
export interface SelectStateOptions<T> extends Omit<SelectProps<T>, 'children'>, CollectionStateBase<T> {}
2121

22-
export interface SelectState<T> extends SingleSelectListState<T>, MenuTriggerState, FormValidationState {
22+
export interface SelectState<T> extends SingleSelectListState<T>, OverlayTriggerState, FormValidationState {
2323
/** Whether the select is currently focused. */
2424
readonly isFocused: boolean,
2525

2626
/** Sets whether the select is focused. */
27-
setFocused(isFocused: boolean): void
27+
setFocused(isFocused: boolean): void,
28+
29+
/** Controls which item will be auto focused when the menu opens. */
30+
readonly focusStrategy: FocusStrategy,
31+
32+
/** Opens the menu. */
33+
open(focusStrategy?: FocusStrategy | null): void,
34+
35+
/** Toggles the menu. */
36+
toggle(focusStrategy?: FocusStrategy | null): void
2837
}
2938

3039
/**
@@ -33,7 +42,8 @@ export interface SelectState<T> extends SingleSelectListState<T>, MenuTriggerSta
3342
* multiple selection state.
3443
*/
3544
export function useSelectState<T extends object>(props: SelectStateOptions<T>): SelectState<T> {
36-
let triggerState = useMenuTriggerState(props);
45+
let triggerState = useOverlayTriggerState(props);
46+
let [focusStrategy, setFocusStrategy] = useState<FocusStrategy>(null);
3747
let listState = useSingleSelectListState({
3848
...props,
3949
onSelectionChange: (key) => {
@@ -57,15 +67,18 @@ export function useSelectState<T extends object>(props: SelectStateOptions<T>):
5767
...validationState,
5868
...listState,
5969
...triggerState,
60-
open() {
70+
focusStrategy,
71+
open(focusStrategy: FocusStrategy = null) {
6172
// Don't open if the collection is empty.
6273
if (listState.collection.size !== 0) {
74+
setFocusStrategy(focusStrategy);
6375
triggerState.open();
6476
}
6577
},
66-
toggle(focusStrategy) {
78+
toggle(focusStrategy: FocusStrategy = null) {
6779
if (listState.collection.size !== 0) {
68-
triggerState.toggle(focusStrategy);
80+
setFocusStrategy(focusStrategy);
81+
triggerState.toggle();
6982
}
7083
},
7184
isFocused,

packages/react-stately/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type {DateFieldState, DateFieldStateOptions, DatePickerState, DatePickerS
1717
export type {DraggableCollectionStateOptions, DraggableCollectionState, DroppableCollectionStateOptions, DroppableCollectionState} from '@react-stately/dnd';
1818
export type {AsyncListData, AsyncListOptions, ListData, ListOptions, TreeData, TreeOptions} from '@react-stately/data';
1919
export type {ListProps, ListState, SingleSelectListProps, SingleSelectListState} from '@react-stately/list';
20-
export type {MenuTriggerProps, MenuTriggerState} from '@react-stately/menu';
20+
export type {MenuTriggerProps, MenuTriggerState, RootMenuTriggerState} from '@react-stately/menu';
2121
export type {OverlayTriggerProps, OverlayTriggerState} from '@react-stately/overlays';
2222
export type {RadioGroupProps, RadioGroupState} from '@react-stately/radio';
2323
export type {SearchFieldProps, SearchFieldState} from '@react-stately/searchfield';

0 commit comments

Comments
 (0)