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

single state #33727

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as React from 'react';
Copy link
Collaborator

@fabricteam fabricteam Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵🏾‍♀️ visual regressions to review in the fluentuiv9 Visual Regression Report

Avatar Converged 1 screenshots
Image Name Diff(in Pixels) Image Type
Avatar Converged.Badge Mask RTL.chromium.png 5 Changed
Combobox Converged 2 screenshots
Image Name Diff(in Pixels) Image Type
Combobox Converged.Disabled option.chromium.png 56 Changed
Combobox Converged.With multiselect selection.chromium.png 12 Changed
Drawer 2 screenshots
Image Name Diff(in Pixels) Image Type
Drawer.Full Overlay High Contrast.chromium.png 6 Changed
Drawer.Full Overlay Dark Mode.chromium.png 975 Changed
Dropdown Converged 2 screenshots
Image Name Diff(in Pixels) Image Type
Dropdown Converged.Disabled option.chromium.png 56 Changed
Dropdown Converged.With multiselect selection.chromium.png 12 Changed
Menu 3 screenshots
Image Name Diff(in Pixels) Image Type
Menu.Nested Submenus Small Viewport Flipped.chromium.png 14 Changed
Menu.Nested Submenus Small Viewport Stacked.chromium.png 14 Changed
Menu.Scrollable Menu Small Viewport.chromium.png 10 Changed
Menu Converged - MenuItemLinks 1 screenshots
Image Name Diff(in Pixels) Image Type
Menu Converged - MenuItemLinks.default.chromium.png 14 Changed
Menu Converged - basic 2 screenshots
Image Name Diff(in Pixels) Image Type
Menu Converged - basic.Default RTL.chromium.png 13 Changed
Menu Converged - basic.default.chromium.png 14 Changed
Menu Converged - groups 4 screenshots
Image Name Diff(in Pixels) Image Type
Menu Converged - groups.Default Dark Mode.chromium.png 329 Changed
Menu Converged - groups.Default High Contrast.chromium.png 2754 Changed
Menu Converged - groups.Default RTL.chromium.png 13 Changed
Menu Converged - groups.default.chromium.png 14 Changed
Menu Converged - icon slotted content 2 screenshots
Image Name Diff(in Pixels) Image Type
Menu Converged - icon slotted content.Default RTL.chromium.png 14 Changed
Menu Converged - icon slotted content.default.chromium.png 14 Changed
Menu Converged - long content 2 screenshots
Image Name Diff(in Pixels) Image Type
Menu Converged - long content.Default RTL.chromium.png 13 Changed
Menu Converged - long content.default.chromium.png 14 Changed
Menu Converged - nested submenus 2 screenshots
Image Name Diff(in Pixels) Image Type
Menu Converged - nested submenus.default.chromium.png 14 Changed
Menu Converged - nested submenus.Default RTL.chromium.png 14 Changed
Menu Converged - secondary content 2 screenshots
Image Name Diff(in Pixels) Image Type
Menu Converged - secondary content.default.chromium.png 14 Changed
Menu Converged - secondary content.Default RTL.chromium.png 13 Changed
Menu Converged - selection 9 screenshots
Image Name Diff(in Pixels) Image Type
Menu Converged - selection.Checkbox RTL.chromium.png 13 Changed
Menu Converged - selection.Switch Dark Mode.chromium.png 396 Changed
Menu Converged - selection.Checkbox High Contrast.chromium.png 2754 Changed
Menu Converged - selection.Checkbox Dark Mode.chromium.png 329 Changed
Menu Converged - selection.selectable with long text.chromium.png 61 Changed
Menu Converged - selection.Switch High Contrast.chromium.png 3341 Changed
Menu Converged - selection.Switch RTL.chromium.png 37 Changed
Menu Converged - selection.checkbox.chromium.png 14 Changed
Menu Converged - selection.switch.chromium.png 37 Changed
Menu Converged - selection groups 2 screenshots
Image Name Diff(in Pixels) Image Type
Menu Converged - selection groups.default.chromium.png 14 Changed
Menu Converged - selection groups.Default RTL.chromium.png 13 Changed
Menu Converged - split item 2 screenshots
Image Name Diff(in Pixels) Image Type
Menu Converged - split item.Default RTL.chromium.png 14 Changed
Menu Converged - split item.default.chromium.png 14 Changed
Menu Converged - submenuIndicator slotted content 2 screenshots
Image Name Diff(in Pixels) Image Type
Menu Converged - submenuIndicator slotted content.Default RTL.chromium.png 24 Changed
Menu Converged - submenuIndicator slotted content.default.chromium.png 24 Changed
Menu Multiline items 2 screenshots
Image Name Diff(in Pixels) Image Type
Menu Multiline items.Default RTL.chromium.png 15 Changed
Menu Multiline items.default.chromium.png 16 Changed
Menu nested within a MenuTrigger 1 screenshots
Image Name Diff(in Pixels) Image Type
Menu nested within a MenuTrigger.default.chromium.png 14 Changed
TagPicker 4 screenshots
Image Name Diff(in Pixels) Image Type
TagPicker.Default Open Dark Mode.chromium.png 1371 Changed
TagPicker.Default Open High Contrast.chromium.png 12194 Changed
TagPicker.Grouped High Contrast.chromium.png 12194 Changed
TagPicker.Grouped Dark Mode.chromium.png 1371 Changed

import { getIntrinsicElementProps, useId, useMergedRefs, slot } from '@fluentui/react-utilities';
import { getIntrinsicElementProps, useId, useMergedRefs, slot, useEventCallback } from '@fluentui/react-utilities';
import { useActiveDescendantContext } from '@fluentui/react-aria';
import { CheckmarkFilled, Checkmark12Filled } from '@fluentui/react-icons';
import { useListboxContext_unstable } from '../../contexts/ListboxContext';
import type { OptionValue } from '../../utils/OptionCollection.types';
import type { OptionProps, OptionState } from './Option.types';
import { useSetKeyboardNavigation } from '@fluentui/react-tabster';

function getTextString(text: string | undefined, children: React.ReactNode) {
if (text !== undefined) {
Expand Down Expand Up @@ -87,6 +88,15 @@ export const useOption_unstable = (props: OptionProps, ref: React.Ref<HTMLElemen
props.onClick?.(event);
};

const setKeyboardNavigationState = useSetKeyboardNavigation();
const onMouseMove = useEventCallback((e: React.MouseEvent<HTMLDivElement>) => {
if (activeDescendantController.active() !== id) {
activeDescendantController.focus(id);
}

setKeyboardNavigationState(false);
});

// register option data with context
React.useEffect(() => {
if (id && optionRef.current) {
Expand Down Expand Up @@ -114,6 +124,7 @@ export const useOption_unstable = (props: OptionProps, ref: React.Ref<HTMLElemen
...semanticProps,
...props,
onClick,
onMouseMove,
}),
{ elementType: 'div' },
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ const useStyles = makeStyles({
padding: `${tokens.spacingVerticalSNudge} ${tokens.spacingHorizontalS}`,
position: 'relative',

':hover': {
backgroundColor: tokens.colorNeutralBackground1Hover,
color: tokens.colorNeutralForeground1Hover,
[`& .${optionClassNames.checkIcon}`]: shorthands.borderColor(tokens.colorNeutralForeground1Hover),
},
// ':hover': {
// backgroundColor: tokens.colorNeutralBackground1Hover,
// color: tokens.colorNeutralForeground1Hover,
// [`& .${optionClassNames.checkIcon}`]: shorthands.borderColor(tokens.colorNeutralForeground1Hover),
// },

':active': {
backgroundColor: tokens.colorNeutralBackground1Pressed,
Expand All @@ -40,6 +40,11 @@ const useStyles = makeStyles({
},

active: {
[`&[data-activedescendant]`]: {
backgroundColor: tokens.colorNeutralBackground1Hover,
color: tokens.colorNeutralForeground1Hover,
[`& .${optionClassNames.checkIcon}`]: shorthands.borderColor(tokens.colorNeutralForeground1Hover),
},
[`[${ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE}]::after`]: {
content: '""',
position: 'absolute',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export type MenuListContextValue = Pick<MenuListProps, 'checkedValues' | 'hasIco
toggleCheckbox?: SelectableHandler;
selectRadio?: SelectableHandler;
onCheckedValueChange?: (e: MenuCheckedValueChangeEvent, data: MenuCheckedValueChangeData) => void;
mouseInputState?: MouseInputState;
};

// @public (undocumented)
Expand Down Expand Up @@ -260,6 +261,7 @@ export type MenuListState = ComponentState<MenuListSlots> & Required<Pick<MenuLi
setFocusByFirstCharacter: NonNullable<MenuListContextValue['setFocusByFirstCharacter']>;
toggleCheckbox: SelectableHandler;
hasMenuContext?: boolean;
mouseInputState?: MenuListContextValue['mouseInputState'];
};

// @public
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useMenuContext_unstable } from '../../contexts/menuContext';
import { MENU_ENTER_EVENT, useOnMenuMouseEnter } from '../../utils/index';
import { useIsSubmenu } from '../../utils/useIsSubmenu';
import type { MenuOpenChangeData, MenuOpenEvent, MenuProps, MenuState } from './Menu.types';
import { useMenuListContext_unstable } from '../../contexts/menuListContext';

// If it's not possible to position the submenu in smaller viewports, try
// and fallback to this order of positions
Expand Down Expand Up @@ -269,9 +270,16 @@ const useMenuOpenState = (
}, [findFirstFocusable, state.menuPopoverRef]);

const firstMount = useFirstMount();
const mouseInputState = useMenuListContext_unstable(context => context.mouseInputState);
React.useEffect(() => {
if (open) {
focusFirst();
// TODO this should not be called when user is moving the mouse around
// This only applies to submenus
// Submenu should only read this state if it's wrapped by a menulist
// MenuList should set this state
if (!mouseInputState?.isMouseInput()) {
focusFirst();
}
} else {
if (!firstMount) {
if (targetDocument?.activeElement === targetDocument?.body) {
Expand All @@ -287,7 +295,7 @@ const useMenuOpenState = (
}
// firstMount change should not re-run this effect
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.triggerRef, state.isSubmenu, open, focusFirst, targetDocument, state.menuPopoverRef]);
}, [state.triggerRef, state.isSubmenu, open, focusFirst, targetDocument, state.menuPopoverRef, mouseInputState]);

return [open, setOpen] as const;
};
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const ChevronLeftIcon = bundleIcon(ChevronLeftFilled, ChevronLeftRegular);
export const useMenuItem_unstable = (props: MenuItemProps, ref: React.Ref<ARIAButtonElement<'div'>>): MenuItemState => {
const isSubmenuTrigger = useMenuTriggerContext_unstable();
const persistOnClickContext = useMenuContext_unstable(context => context.persistOnItemClick);
const mouseInputState = useMenuListContext_unstable(ctx => ctx.mouseInputState);
const { as = 'div', disabled = false, hasSubmenu = isSubmenuTrigger, persistOnClick = persistOnClickContext } = props;
const { hasIcons, hasCheckmarks } = useIconAndCheckmarkAlignment({ hasSubmenu });
const setOpen = useMenuContext_unstable(context => context.setOpen);
Expand Down Expand Up @@ -75,6 +76,10 @@ export const useMenuItem_unstable = (props: MenuItemProps, ref: React.Ref<ARIABu
}
}),
onMouseMove: useEventCallback(event => {
if (!mouseInputState?.isMouseInput()) {
mouseInputState?.setMouseInput(true);
}

if (event.currentTarget.ownerDocument.activeElement !== event.currentTarget) {
innerRef.current?.focus();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ const useRootBaseStyles = makeResetStyles({
cursor: 'pointer',
gap: '4px',

':hover': {
...createFocusOutlineStyle(),
':focus': {
outline: 'none',
backgroundColor: tokens.colorNeutralBackground1Hover,
color: tokens.colorNeutralForeground2Hover,

Expand All @@ -62,6 +64,7 @@ const useRootBaseStyles = makeResetStyles({
[`& .${menuItemClassNames.subText}`]: {
color: tokens.colorNeutralForeground3Pressed,
},
...createFocusOutlineStyle({ style: { outlineColor: 'Highlight' } }),
},

// High contrast styles
Expand All @@ -71,11 +74,9 @@ const useRootBaseStyles = makeResetStyles({
borderColor: 'Highlight',
color: 'Highlight',
},
...createFocusOutlineStyle({ style: { outlineColor: 'Highlight' } }),
},

userSelect: 'none',
...createFocusOutlineStyle(),
});

const useContentBaseStyles = makeResetStyles({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export type MenuListState = ComponentState<MenuListSlots> &
* States if the MenuList is inside MenuContext
*/
hasMenuContext?: boolean;

mouseInputState?: MenuListContextValue['mouseInputState'];
};

export type MenuListContextValues = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import {
useFocusFinders,
TabsterMoveFocusEventName,
type TabsterMoveFocusEvent,
useOnKeyboardNavigationChange,
useSetKeyboardNavigation,
} from '@fluentui/react-tabster';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { useHasParentContext } from '@fluentui/react-context-selector';
import { useMenuContext_unstable } from '../../contexts/menuContext';
import { MenuContext } from '../../contexts/menuContext';
import type { MenuListProps, MenuListState } from './MenuList.types';
import { useMenuListContext_unstable } from '../../contexts/menuListContext';

/**
* Returns the props and state required to render the component
Expand Down Expand Up @@ -133,6 +136,8 @@ export const useMenuList_unstable = (props: MenuListProps, ref: React.Ref<HTMLEl
handleCheckedValueChange?.(e, { name, checkedItems: newCheckedItems });
});

const mouseInputState = useMouseInputState();

return {
components: {
root: 'div',
Expand All @@ -157,6 +162,7 @@ export const useMenuList_unstable = (props: MenuListProps, ref: React.Ref<HTMLEl
setFocusByFirstCharacter,
selectRadio,
toggleCheckbox,
mouseInputState,
};
};

Expand Down Expand Up @@ -196,3 +202,23 @@ const usingPropsAndMenuContext = (

return hasMenuContext && isUsingPropsAndContext;
};

const useMouseInputState = () => {
const parentContext = useMenuListContext_unstable(ctx => ctx.mouseInputState);
const setKeyboardNavigationState = useSetKeyboardNavigation();
const isMouseInputRef = React.useRef(!!parentContext?.isMouseInput());
const setMouseInput = React.useCallback(
(isMouseInput: boolean) => {
setKeyboardNavigationState(!isMouseInput);
parentContext?.setMouseInput(isMouseInput);
isMouseInputRef.current = isMouseInput;
},
[parentContext, setKeyboardNavigationState],
);

useOnKeyboardNavigationChange(isNavigatingWithKeyboard => {
setMouseInput(!isNavigatingWithKeyboard);
});

return { isMouseInput: () => isMouseInputRef.current, setMouseInput };
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import type { MenuListContextValues, MenuListState } from './MenuList.types';

export function useMenuListContextValues_unstable(state: MenuListState): MenuListContextValues {
const { checkedValues, hasCheckmarks, hasIcons, selectRadio, setFocusByFirstCharacter, toggleCheckbox } = state;
const {
checkedValues,
hasCheckmarks,
hasIcons,
selectRadio,
setFocusByFirstCharacter,
toggleCheckbox,
mouseInputState,
} = state;

// This context is created with "@fluentui/react-context-selector", these is no sense to memoize it
const menuList = {
Expand All @@ -11,6 +19,7 @@ export function useMenuListContextValues_unstable(state: MenuListState): MenuLis
selectRadio,
setFocusByFirstCharacter,
toggleCheckbox,
mouseInputState,
};

return { menuList };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import type { ContextSelector, Context } from '@fluentui/react-context-selector'
import type { SelectableHandler } from '../selectable/index';
import type { MenuCheckedValueChangeData, MenuCheckedValueChangeEvent, MenuListProps } from '../components/index';

export interface MouseInputState {
isMouseInput: () => boolean;
setMouseInput(isMouseInput: boolean): void;
}

export const MenuListContext: Context<MenuListContextValue> = createContext<MenuListContextValue | undefined>(
undefined,
) as Context<MenuListContextValue>;
Expand All @@ -15,6 +20,7 @@ const menuListContextDefaultValue: MenuListContextValue = {
selectRadio: () => null,
hasIcons: false,
hasCheckmarks: false,
mouseInputState: { isMouseInput: () => false, setMouseInput: () => null },
};

/**
Expand All @@ -34,6 +40,7 @@ export type MenuListContextValue = Pick<MenuListProps, 'checkedValues' | 'hasIco
* the signature remains just to avoid breaking changes
*/
onCheckedValueChange?: (e: MenuCheckedValueChangeEvent, data: MenuCheckedValueChangeData) => void;
mouseInputState?: MouseInputState;
};

export const MenuListProvider = MenuListContext.Provider;
Expand Down
Loading