Skip to content
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export type ButtonSlots = {
// @public (undocumented)
export type ButtonState = ComponentState<ButtonSlots> & Required<Pick<ButtonProps, 'appearance' | 'disabledFocusable' | 'disabled' | 'iconPosition' | 'shape' | 'size'>> & {
iconOnly: boolean;
isHovered: boolean;
};

// @public
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,24 @@ export type ButtonState = ComponentState<ButtonSlots> &
* @default false
*/
iconOnly: boolean;

/**
* Whether the button is currently hovered (via pointer-aware hover detection).
* This is used when `UNSTABLE_usePointerHover` is enabled on FluentProvider to provide
* touch-safe hover behavior that doesn't trigger on touch interactions.
*
* @default false
*/
isHovered: boolean;
};

export type ButtonBaseState = DistributiveOmit<ButtonState, ButtonDesignPropNames>;
export type ButtonBaseState = DistributiveOmit<ButtonState, ButtonDesignPropNames> & {
/**
* Whether the button is currently hovered (via pointer-aware hover detection).
* This is used when `UNSTABLE_usePointerHover` is enabled on FluentProvider to provide
* touch-safe hover behavior that doesn't trigger on touch interactions.
*
* @default false
*/
isHovered: boolean;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import * as React from 'react';
import { ARIAButtonSlotProps, useARIAButtonProps } from '@fluentui/react-aria';
import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities';
import { getIntrinsicElementProps, slot, useHover_unstable } from '@fluentui/react-utilities';
import { useOverrides_unstable as useOverrides } from '@fluentui/react-shared-contexts';
import type { ButtonBaseProps, ButtonBaseState } from './Button.types';

/**
Expand All @@ -16,15 +17,53 @@ export const useButtonBase_unstable = (
): ButtonBaseState => {
const { as = 'button', disabled = false, disabledFocusable = false, icon, iconPosition = 'before' } = props;
const iconShorthand = slot.optional(icon, { elementType: 'span' });

// Check if smart hover behavior is enabled via FluentProvider
const overrides = useOverrides();
const smartHover = overrides.smartHover ?? false;
console.log('Smart Hover Enabled:', smartHover);

// Use smart hover hook - disabled when feature flag is off or button is disabled
const { hoverProps, isHovered } = useHover_unstable({
isDisabled: !smartHover,
});

console.log('hovered', isHovered);

console.log(hoverProps);

// Get base ARIA button props
const ariaButtonProps = useARIAButtonProps(props.as, props);

// Merge hover props with ARIA button props when smart hover is enabled
const mergedProps = smartHover
? {
...ariaButtonProps,
...hoverProps,
// Merge event handlers - ensure both hover and aria handlers are called
onPointerEnter: (e: React.PointerEvent<HTMLButtonElement & HTMLAnchorElement>) => {
hoverProps.onPointerEnter?.(e as React.PointerEvent<HTMLElement>);
ariaButtonProps.onPointerEnter?.(e);
},
onPointerLeave: (e: React.PointerEvent<HTMLButtonElement & HTMLAnchorElement>) => {
hoverProps.onPointerLeave?.(e as React.PointerEvent<HTMLElement>);
ariaButtonProps.onPointerLeave?.(e);
},
// Add data-hovered attribute for CSS styling when hovered
'data-hovered': isHovered ? '' : undefined,
}
: ariaButtonProps;

return {
// Props passed at the top-level
disabled,
disabledFocusable,
iconPosition,
iconOnly: Boolean(iconShorthand?.children && !props.children),
isHovered,
// Slots definition
components: { root: 'button', icon: 'span' },
root: slot.always<ARIAButtonSlotProps<'a'>>(getIntrinsicElementProps(as, useARIAButtonProps(props.as, props)), {
root: slot.always<ARIAButtonSlotProps<'a'>>(getIntrinsicElementProps(as, mergedProps), {
elementType: 'button',
defaultProps: {
ref: ref as React.Ref<HTMLButtonElement & HTMLAnchorElement>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ const useRootBaseClassName = makeResetStyles({
cursor: 'pointer',
},

// Pointer-aware hover styles (used when UNSTABLE_usePointerHover is enabled)
'&[data-hovered]': {
backgroundColor: tokens.colorNeutralBackground1Hover,
borderColor: tokens.colorNeutralStroke1Hover,
color: tokens.colorNeutralForeground1Hover,

cursor: 'pointer',
},

':hover:active,:active:focus-visible': {
backgroundColor: tokens.colorNeutralBackground1Pressed,
borderColor: tokens.colorNeutralStroke1Pressed,
Expand Down Expand Up @@ -92,6 +101,13 @@ const useRootBaseClassName = makeResetStyles({
forcedColorAdjust: 'none',
},

'&[data-hovered]': {
backgroundColor: 'HighlightText',
borderColor: 'Highlight',
color: 'Highlight',
forcedColorAdjust: 'none',
},

':hover:active,:active:focus-visible': {
backgroundColor: 'HighlightText',
borderColor: 'Highlight',
Expand Down Expand Up @@ -144,6 +160,10 @@ const useRootStyles = makeStyles({
backgroundColor: tokens.colorTransparentBackgroundHover,
},

'&[data-hovered]': {
backgroundColor: tokens.colorTransparentBackgroundHover,
},

':hover:active,:active:focus-visible': {
backgroundColor: tokens.colorTransparentBackgroundPressed,
},
Expand All @@ -159,6 +179,12 @@ const useRootStyles = makeStyles({
color: tokens.colorNeutralForegroundOnBrand,
},

'&[data-hovered]': {
backgroundColor: tokens.colorBrandBackgroundHover,
...shorthands.borderColor('transparent'),
color: tokens.colorNeutralForegroundOnBrand,
},

':hover:active,:active:focus-visible': {
backgroundColor: tokens.colorBrandBackgroundPressed,
...shorthands.borderColor('transparent'),
Expand All @@ -177,6 +203,12 @@ const useRootStyles = makeStyles({
color: 'Highlight',
},

'&[data-hovered]': {
backgroundColor: 'HighlightText',
...shorthands.borderColor('Highlight'),
color: 'Highlight',
},

':hover:active,:active:focus-visible': {
backgroundColor: 'HighlightText',
...shorthands.borderColor('Highlight'),
Expand Down Expand Up @@ -207,6 +239,21 @@ const useRootStyles = makeStyles({
},
},

'&[data-hovered]': {
backgroundColor: tokens.colorSubtleBackgroundHover,
...shorthands.borderColor('transparent'),
color: tokens.colorNeutralForeground2Hover,
[`& .${iconFilledClassName}`]: {
display: 'inline',
},
[`& .${iconRegularClassName}`]: {
display: 'none',
},
[`& .${buttonClassNames.icon}`]: {
color: tokens.colorNeutralForeground2BrandHover,
},
},

':hover:active,:active:focus-visible': {
backgroundColor: tokens.colorSubtleBackgroundPressed,
...shorthands.borderColor('transparent'),
Expand All @@ -230,6 +277,13 @@ const useRootStyles = makeStyles({
color: 'Highlight',
},
},
'&[data-hovered]': {
color: 'Highlight',

[`& .${buttonClassNames.icon}`]: {
color: 'Highlight',
},
},
':hover:active,:active:focus-visible': {
color: 'Highlight',

Expand All @@ -256,6 +310,18 @@ const useRootStyles = makeStyles({
},
},

'&[data-hovered]': {
backgroundColor: tokens.colorTransparentBackgroundHover,
...shorthands.borderColor('transparent'),
color: tokens.colorNeutralForeground2BrandHover,
[`& .${iconFilledClassName}`]: {
display: 'inline',
},
[`& .${iconRegularClassName}`]: {
display: 'none',
},
},

':hover:active,:active:focus-visible': {
backgroundColor: tokens.colorTransparentBackgroundPressed,
...shorthands.borderColor('transparent'),
Expand All @@ -273,6 +339,10 @@ const useRootStyles = makeStyles({
backgroundColor: tokens.colorTransparentBackground,
color: 'Highlight',
},
'&[data-hovered]': {
backgroundColor: tokens.colorTransparentBackground,
color: 'Highlight',
},
':hover:active,:active:focus-visible': {
backgroundColor: tokens.colorTransparentBackground,
color: 'Highlight',
Expand Down Expand Up @@ -349,6 +419,25 @@ const useRootDisabledStyles = makeStyles({
},
},

// Disabled buttons should not show hover styles even when data-hovered is present
'&[data-hovered]': {
backgroundColor: tokens.colorNeutralBackgroundDisabled,
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled),
color: tokens.colorNeutralForegroundDisabled,

cursor: 'not-allowed',

[`& .${iconFilledClassName}`]: {
display: 'none',
},
[`& .${iconRegularClassName}`]: {
display: 'inline',
},
[`& .${buttonClassNames.icon}`]: {
color: tokens.colorNeutralForegroundDisabled,
},
},

':hover:active,:active:focus-visible': {
backgroundColor: tokens.colorNeutralBackgroundDisabled,
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled),
Expand Down Expand Up @@ -393,6 +482,16 @@ const useRootDisabledStyles = makeStyles({
},
},

'&[data-hovered]': {
backgroundColor: 'ButtonFace',
...shorthands.borderColor('GrayText'),
color: 'GrayText',

[`& .${buttonClassNames.icon}`]: {
color: 'GrayText',
},
},

':hover:active,:active:focus-visible': {
backgroundColor: 'ButtonFace',
...shorthands.borderColor('GrayText'),
Expand All @@ -413,6 +512,10 @@ const useRootDisabledStyles = makeStyles({
backgroundColor: tokens.colorTransparentBackground,
},

'&[data-hovered]': {
backgroundColor: tokens.colorTransparentBackground,
},

':hover:active,:active:focus-visible': {
backgroundColor: tokens.colorTransparentBackground,
},
Expand All @@ -424,6 +527,10 @@ const useRootDisabledStyles = makeStyles({
...shorthands.borderColor('transparent'),
},

'&[data-hovered]': {
...shorthands.borderColor('transparent'),
},

':hover:active,:active:focus-visible': {
...shorthands.borderColor('transparent'),
},
Expand All @@ -440,6 +547,11 @@ const useRootDisabledStyles = makeStyles({
...shorthands.borderColor('transparent'),
},

'&[data-hovered]': {
backgroundColor: tokens.colorTransparentBackground,
...shorthands.borderColor('transparent'),
},

':hover:active,:active:focus-visible': {
backgroundColor: tokens.colorTransparentBackground,
...shorthands.borderColor('transparent'),
Expand All @@ -454,6 +566,11 @@ const useRootDisabledStyles = makeStyles({
...shorthands.borderColor('transparent'),
},

'&[data-hovered]': {
backgroundColor: tokens.colorTransparentBackground,
...shorthands.borderColor('transparent'),
},

':hover:active,:active:focus-visible': {
backgroundColor: tokens.colorTransparentBackground,
...shorthands.borderColor('transparent'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export const useSplitButtonBase_unstable = (
disabled,
disabledFocusable,
iconPosition,
// SplitButton container doesn't track hover state - child buttons handle their own hover
isHovered: false,
components: { root: 'div', menuButton: MenuButton, primaryActionButton: Button },
root: slot.always(getIntrinsicElementProps('div', { ref, ...props }), { elementType: 'div' }),
menuButton: menuButtonShorthand,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import * as React from 'react';
import type { JSXElement } from '@fluentui/react-components';
import { Button } from '@fluentui/react-components';
import { Button, FluentProvider } from '@fluentui/react-components';
import type { ButtonProps } from '@fluentui/react-components';

export const Default = (props: ButtonProps): JSXElement => <Button {...props}>Example</Button>;
export const Default = (props: ButtonProps): JSXElement => (
<FluentProvider
overrides_unstable={{
smartHover: true,
}}
>
<Button {...props}>Example</Button>
</FluentProvider>
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as React from 'react';
export type OverridesContextValue = {
// No 'underline' as it is not supported by TextArea
inputDefaultAppearance?: 'outline' | 'filled-darker' | 'filled-lighter';
smartHover?: boolean;
};

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/react-components/react-utilities/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export { useControllableState } from './useControllableState';
export { useEventCallback } from './useEventCallback';
export { useFirstMount } from './useFirstMount';
export { useForceUpdate } from './useForceUpdate';
export type { HoverEvent, HoverProps, HoverResult, PointerType } from './useHover';
export { useHover_unstable } from './useHover';
export { IdPrefixProvider, resetIdsForTests, useId } from './useId';
export { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
export type { RefObjectFunction } from './useMergedRefs';
Expand Down
Loading