Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate menutrigger state #5597

Merged
merged 6 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@react-aria/menu/src/useMenuTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface MenuTriggerAria<T> {
* Provides the behavior and accessibility implementation for a menu trigger.
* @param props - Props for the menu trigger.
* @param state - State for the menu trigger.
* @param ref - Ref to the HTML element trigger for the menu.
*/
export function useMenuTrigger<T>(props: AriaMenuTriggerProps, state: MenuTriggerState, ref: RefObject<Element>): MenuTriggerAria<T> {
let {
Expand Down
6 changes: 3 additions & 3 deletions packages/@react-spectrum/menu/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
*/

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

export interface MenuContextValue extends Omit<HTMLAttributes<HTMLElement>, 'autoFocus' | 'onKeyDown'>, Pick<KeyboardEvents, 'onKeyDown'> {
Expand All @@ -21,7 +21,7 @@ export interface MenuContextValue extends Omit<HTMLAttributes<HTMLElement>, 'aut
shouldFocusWrap?: boolean,
autoFocus?: boolean | FocusStrategy,
ref?: MutableRefObject<HTMLDivElement>,
state?: MenuTriggerState,
state?: RootMenuTriggerState,
onBackButtonPress?: () => void,
submenuLevel?: number
}
Expand Down Expand Up @@ -53,7 +53,7 @@ export interface MenuStateContextValue<T> {
trayContainerRef?: RefObject<HTMLElement>,
menu?: RefObject<HTMLDivElement>,
submenu?: RefObject<HTMLDivElement>,
rootMenuTriggerState?: MenuTriggerState
rootMenuTriggerState?: RootMenuTriggerState
}

export const MenuStateContext = React.createContext<MenuStateContextValue<any>>(undefined);
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-stately/combobox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@react-stately/collections": "^3.10.3",
"@react-stately/form": "^3.0.0",
"@react-stately/list": "^3.10.1",
"@react-stately/menu": "^3.5.7",
"@react-stately/overlays": "^3.6.4",
"@react-stately/select": "^3.6.0",
"@react-stately/utils": "^3.9.0",
"@react-types/combobox": "^3.9.0",
Expand Down
20 changes: 13 additions & 7 deletions packages/@react-stately/combobox/src/useComboBoxState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {ListCollection, useSingleSelectListState} from '@react-stately/list';
import {SelectState} from '@react-stately/select';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useControlledState} from '@react-stately/utils';
import {useMenuTriggerState} from '@react-stately/menu';
import {useOverlayTriggerState} from '@react-stately/overlays';

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

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

let onSelectionChange = (key) => {
if (props.onSelectionChange) {
Expand Down Expand Up @@ -111,8 +114,8 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
}
};

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

menuOpenTrigger.current = trigger;
triggerState.open(focusStrategy);
setFocusStrategy(focusStrategy);
triggerState.open();
}
};

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

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

triggerState.toggle(focusStrategy);
setFocusStrategy(focusStrategy);
triggerState.toggle();
}, [triggerState, updateLastCollection]);

let closeMenu = useCallback(() => {
Expand Down Expand Up @@ -347,6 +352,7 @@ export function useComboBoxState<T extends object>(props: ComboBoxStateOptions<T
return {
...validation,
...triggerState,
focusStrategy,
toggle,
open,
close: commitValue,
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-stately/menu/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ export {useMenuTriggerState} from './useMenuTriggerState';
export {UNSTABLE_useSubmenuTriggerState} from './useSubmenuTriggerState';

export type {MenuTriggerProps} from '@react-types/menu';
export type {MenuTriggerState} from './useMenuTriggerState';
export type {MenuTriggerState, RootMenuTriggerState} from './useMenuTriggerState';
export type {SubmenuTriggerProps, SubmenuTriggerState} from './useSubmenuTriggerState';
14 changes: 8 additions & 6 deletions packages/@react-stately/menu/src/useMenuTriggerState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ export interface MenuTriggerState extends OverlayTriggerState {
open(focusStrategy?: FocusStrategy | null): void,

/** Toggles the menu. */
toggle(focusStrategy?: FocusStrategy | null): void,

/** Closes the menu and all submenus in the menu tree. */
close: () => void,
toggle(focusStrategy?: FocusStrategy | null): void
}

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

Expand All @@ -37,15 +36,18 @@ export interface MenuTriggerState extends OverlayTriggerState {
/** An array of open submenu trigger keys within the menu tree.
* The index of key within array matches the submenu level in the tree.
*/
UNSTABLE_expandedKeysStack: Key[]
UNSTABLE_expandedKeysStack: Key[],

/** Closes the menu and all submenus in the menu tree. */
close: () => void
}

/**
* Manages state for a menu trigger. Tracks whether the menu is currently open,
* and controls which item will receive focus when it opens. Also tracks the open submenus within
* the menu tree via their trigger keys.
*/
export function useMenuTriggerState(props: MenuTriggerProps): MenuTriggerState {
export function useMenuTriggerState(props: MenuTriggerProps): RootMenuTriggerState {
let overlayTriggerState = useOverlayTriggerState(props);
let [focusStrategy, setFocusStrategy] = useState<FocusStrategy>(null);
let [expandedKeysStack, setExpandedKeysStack] = useState<Key[]>([]);
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-stately/menu/src/useSubmenuTriggerState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
*/

import {FocusStrategy, Key} from '@react-types/shared';
import type {MenuTriggerState} from './useMenuTriggerState';
import type {OverlayTriggerState} from '@react-stately/overlays';
import {RootMenuTriggerState} from './useMenuTriggerState';
import {useCallback, useMemo, useState} from 'react';

export interface SubmenuTriggerProps {
Expand Down Expand Up @@ -43,7 +43,7 @@ export interface SubmenuTriggerState extends OverlayTriggerState {
* Manages state for a submenu trigger. Tracks whether the submenu is currently open, the level of the submenu, and
* controls which item will receive focus when it opens.
*/
export function UNSTABLE_useSubmenuTriggerState(props: SubmenuTriggerProps, state: MenuTriggerState): SubmenuTriggerState {
export function UNSTABLE_useSubmenuTriggerState(props: SubmenuTriggerProps, state: RootMenuTriggerState): SubmenuTriggerState {
let {triggerKey} = props;
let {UNSTABLE_expandedKeysStack, UNSTABLE_openSubmenu, UNSTABLE_closeSubmenu, close: closeAll} = state;
let [submenuLevel] = useState(UNSTABLE_expandedKeysStack?.length);
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-stately/select/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"dependencies": {
"@react-stately/form": "^3.0.0",
"@react-stately/list": "^3.10.1",
"@react-stately/menu": "^3.5.7",
"@react-stately/overlays": "^3.6.4",
"@react-types/select": "^3.9.0",
"@react-types/shared": "^3.22.0",
"@swc/helpers": "^0.5.0"
Expand Down
29 changes: 21 additions & 8 deletions packages/@react-stately/select/src/useSelectState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,30 @@
* governing permissions and limitations under the License.
*/

import {CollectionStateBase} from '@react-types/shared';
import {CollectionStateBase, FocusStrategy} from '@react-types/shared';
import {FormValidationState, useFormValidationState} from '@react-stately/form';
import {MenuTriggerState, useMenuTriggerState} from '@react-stately/menu';
import {OverlayTriggerState, useOverlayTriggerState} from '@react-stately/overlays';
import {SelectProps} from '@react-types/select';
import {SingleSelectListState, useSingleSelectListState} from '@react-stately/list';
import {useState} from 'react';

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

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

/** Sets whether the select is focused. */
setFocused(isFocused: boolean): void
setFocused(isFocused: boolean): void,

/** Controls which item will be auto focused when the menu opens. */
readonly focusStrategy: FocusStrategy,

/** Opens the menu. */
open(focusStrategy?: FocusStrategy | null): void,

/** Toggles the menu. */
toggle(focusStrategy?: FocusStrategy | null): void
}

/**
Expand All @@ -33,7 +42,8 @@ export interface SelectState<T> extends SingleSelectListState<T>, MenuTriggerSta
* multiple selection state.
*/
export function useSelectState<T extends object>(props: SelectStateOptions<T>): SelectState<T> {
let triggerState = useMenuTriggerState(props);
let triggerState = useOverlayTriggerState(props);
let [focusStrategy, setFocusStrategy] = useState<FocusStrategy>(null);
let listState = useSingleSelectListState({
...props,
onSelectionChange: (key) => {
Expand All @@ -57,15 +67,18 @@ export function useSelectState<T extends object>(props: SelectStateOptions<T>):
...validationState,
...listState,
...triggerState,
open() {
focusStrategy,
open(focusStrategy: FocusStrategy = null) {
// Don't open if the collection is empty.
if (listState.collection.size !== 0) {
setFocusStrategy(focusStrategy);
triggerState.open();
}
},
toggle(focusStrategy) {
toggle(focusStrategy: FocusStrategy = null) {
if (listState.collection.size !== 0) {
triggerState.toggle(focusStrategy);
setFocusStrategy(focusStrategy);
snowystinger marked this conversation as resolved.
Show resolved Hide resolved
triggerState.toggle();
}
},
isFocused,
Expand Down
2 changes: 1 addition & 1 deletion packages/react-stately/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type {DateFieldState, DateFieldStateOptions, DatePickerState, DatePickerS
export type {DraggableCollectionStateOptions, DraggableCollectionState, DroppableCollectionStateOptions, DroppableCollectionState} from '@react-stately/dnd';
export type {AsyncListData, AsyncListOptions, ListData, ListOptions, TreeData, TreeOptions} from '@react-stately/data';
export type {ListProps, ListState, SingleSelectListProps, SingleSelectListState} from '@react-stately/list';
export type {MenuTriggerProps, MenuTriggerState} from '@react-stately/menu';
export type {MenuTriggerProps, MenuTriggerState, RootMenuTriggerState} from '@react-stately/menu';
export type {OverlayTriggerProps, OverlayTriggerState} from '@react-stately/overlays';
export type {RadioGroupProps, RadioGroupState} from '@react-stately/radio';
export type {SearchFieldProps, SearchFieldState} from '@react-stately/searchfield';
Expand Down