[ButtonV1][Win32] Fix focus borders on primary buttons (#2693)

* Some attempts

* More tests

* More tests

* Trying more things

* More trying things

* Logic cleanup

* Fix bug

* Fix bug

* Revert tests

* Add more test cases

* Fix bug

* Fix bugs

* Make changes to compound button

* Make button fixes

* Copy of CompoundButtonTokens for win32

* Undo changes to main compound button file

* Compound button fixes

* Change files

* Revert some changes

* Revert some changes

* Update snapshots

* Code cleanup

* Code cleanup

* Change files

* Fix HC bug

* Rename state variables

* Perf improvement, rename variable

* Address feedback
This commit is contained in:
Ruriko Araki 2023-03-13 15:12:44 -07:00 коммит произвёл GitHub
Родитель 3512da9de4
Коммит b724356507
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
26 изменённых файлов: 448 добавлений и 19 удалений

Просмотреть файл

@ -35,6 +35,15 @@ export const ButtonShapeTest: React.FunctionComponent = () => {
<CompoundButton secondaryContent="circular" shape="circular" style={commonTestStyles.vmargin}>
Compound Button
</CompoundButton>
<CompoundButton appearance="primary" secondaryContent="rounded" shape="rounded" style={commonTestStyles.vmargin}>
Compound Button
</CompoundButton>
<CompoundButton appearance="primary" secondaryContent="square" shape="square" style={commonTestStyles.vmargin}>
Compound Button
</CompoundButton>
<CompoundButton appearance="primary" secondaryContent="circular" shape="circular" style={commonTestStyles.vmargin}>
Compound Button
</CompoundButton>
</View>
);
};

Просмотреть файл

@ -23,6 +23,15 @@ export const ButtonSizeTest: React.FunctionComponent = () => {
style={commonTestStyles.vmargin}
tooltip="button tooltip"
/>
<Button
iconOnly
size="small"
appearance="primary"
icon={iconProps}
accessibilityLabel="Small size button with accessibility icon"
style={commonTestStyles.vmargin}
tooltip="button tooltip"
/>
<Button
iconOnly
size="medium"
@ -30,6 +39,14 @@ export const ButtonSizeTest: React.FunctionComponent = () => {
accessibilityLabel="Medium size button with accessibility icon"
style={commonTestStyles.vmargin}
/>
<Button
iconOnly
size="medium"
appearance="primary"
icon={iconProps}
accessibilityLabel="Medium size button with accessibility icon"
style={commonTestStyles.vmargin}
/>
<Button
iconOnly
size="large"
@ -37,15 +54,32 @@ export const ButtonSizeTest: React.FunctionComponent = () => {
accessibilityLabel="Large size button with accessibility icon"
style={commonTestStyles.vmargin}
/>
<Button
iconOnly
size="large"
appearance="primary"
icon={iconProps}
accessibilityLabel="Large size button with accessibility icon"
style={commonTestStyles.vmargin}
/>
<Button size="small" icon={iconProps} style={commonTestStyles.vmargin}>
Small Button with icon
</Button>
<Button size="small" appearance="primary" icon={iconProps} style={commonTestStyles.vmargin}>
Small Button with icon
</Button>
<Button size="medium" icon={iconProps} style={commonTestStyles.vmargin}>
Medium Button with icon
</Button>
<Button size="medium" appearance="primary" icon={iconProps} style={commonTestStyles.vmargin}>
Medium Button with icon
</Button>
<Button size="large" icon={iconProps} style={commonTestStyles.vmargin}>
Large Button with icon
</Button>
<Button size="large" appearance="primary" icon={iconProps} style={commonTestStyles.vmargin}>
Large Button with icon
</Button>
{Platform.OS == 'android' && (
<FAB size="small" icon={iconProps} style={commonTestStyles.vmargin}>
Small FAB
@ -79,12 +113,21 @@ export const ButtonSizeTest: React.FunctionComponent = () => {
<CompoundButton secondaryContent="Small compound button" size="small" style={commonTestStyles.vmargin}>
Compound Button
</CompoundButton>
<CompoundButton appearance="primary" secondaryContent="Small compound button" size="small" style={commonTestStyles.vmargin}>
Compound Button
</CompoundButton>
<CompoundButton secondaryContent="Medium compound button" size="medium" style={commonTestStyles.vmargin}>
Compound Button
</CompoundButton>
<CompoundButton appearance="primary" secondaryContent="Medium compound button" size="medium" style={commonTestStyles.vmargin}>
Compound Button
</CompoundButton>
<CompoundButton secondaryContent="Large compound button" size="large" style={commonTestStyles.vmargin}>
Compound Button
</CompoundButton>
<CompoundButton appearance="primary" secondaryContent="Large compound button" size="large" style={commonTestStyles.vmargin}>
Compound Button
</CompoundButton>
{svgIconsEnabled && (
<>
<CompoundButton icon={iconProps} secondaryContent="SecondaryContent" size="small" style={commonTestStyles.vmargin}>

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Fix focus border for primary button on win32",
"packageName": "@fluentui-react-native/button",
"email": "ruaraki@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Update snapshots",
"packageName": "@fluentui-react-native/experimental-menu-button",
"email": "ruaraki@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Code cleanup",
"packageName": "@fluentui-react-native/menu-button",
"email": "ruaraki@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Update snapshots",
"packageName": "@fluentui-react-native/menu",
"email": "ruaraki@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Update snapshots",
"packageName": "@fluentui-react-native/notification",
"email": "ruaraki@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Add more test cases",
"packageName": "@fluentui-react-native/tester",
"email": "ruaraki@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -15,19 +15,19 @@ import { defaultButtonTokens } from './ButtonTokens';
export const buttonStates: (keyof ButtonTokens)[] = [
'block',
'primary',
'subtle',
'outline',
'hovered',
'small',
'medium',
'large',
'hasContent',
'hasIconAfter',
'hasIconBefore',
'primary',
'subtle',
'outline',
'rounded',
'circular',
'square',
'hovered',
'focused',
'pressed',
'disabled',
@ -91,6 +91,17 @@ export const stylingSettings: UseStylingOptions<ButtonProps, ButtonSlotProps, Bu
}),
['iconColor', 'iconSize'],
),
focusInnerBorder: buildProps(
(tokens: ButtonTokens) => ({
style: {
position: 'absolute',
borderWidth: tokens.borderInnerWidth,
borderColor: tokens.borderInnerColor,
borderRadius: tokens.borderInnerRadius,
},
}),
['borderInnerWidth', 'borderInnerColor', 'borderInnerRadius'],
),
},
};

Просмотреть файл

@ -4,7 +4,7 @@ import { Platform, Pressable, View } from 'react-native';
import { ActivityIndicator } from '@fluentui-react-native/experimental-activity-indicator';
import type { UseSlots } from '@fluentui-react-native/framework';
import { compose, mergeProps, withSlots } from '@fluentui-react-native/framework';
import { compose, memoize, mergeProps, withSlots } from '@fluentui-react-native/framework';
import { Icon, createIconProps } from '@fluentui-react-native/icon';
import type { IPressableState } from '@fluentui-react-native/interactive-hooks';
import { TextV1 as Text } from '@fluentui-react-native/text';
@ -44,7 +44,8 @@ export const Button = compose<ButtonType>({
...stylingSettings,
slots: {
root: Pressable,
rippleContainer: View,
rippleContainer: Platform.OS === 'android' && View,
focusInnerBorder: Platform.OS === ('win32' as any) && View,
icon: Icon,
content: Text,
},
@ -110,9 +111,26 @@ export const Button = compose<ButtonType>({
return (
<Slots.root {...mergedProps} accessibilityLabel={label}>
{buttonContent}
{button.state.focused && button.state.shouldUseTwoToneFocusBorder && (
<Slots.focusInnerBorder
style={getFocusBorderStyle(button.state.measuredHeight, button.state.measuredWidth)}
accessible={false}
focusable={false}
/>
)}
</Slots.root>
);
}
};
},
});
const getFocusBorderStyleWorker = (height: number, width: number) => {
const adjustment = 2; // width of border * 2
return {
height: height - adjustment,
width: width - adjustment,
};
};
export const getFocusBorderStyle = memoize(getFocusBorderStyleWorker);

Просмотреть файл

@ -59,7 +59,7 @@ export interface ButtonCoreTokens extends LayoutTokens, FontTokens, IBorderToken
shadowToken?: ShadowToken;
/**
* Focused State on Android has inner and outer borders.
* Focused State on Android and win32 primary has inner and outer borders.
* Outer Border is equivalent to the border tokens from IBorders.
*/
borderInnerColor?: ColorValue;
@ -166,14 +166,23 @@ export interface ButtonProps extends ButtonCoreProps {
loading?: boolean;
}
interface ButtonState extends PressableState {
measuredHeight?: number;
measuredWidth?: number;
// win32 only. Whether the component should use a tone-tone focus border instead of single-tone
shouldUseTwoToneFocusBorder?: boolean;
}
export interface ButtonInfo {
props: ButtonProps & React.ComponentPropsWithRef<any>;
state: PressableState;
state: ButtonState;
}
export interface ButtonSlotProps {
root: React.PropsWithRef<PressablePropsExtended>;
rippleContainer?: IViewProps; // Android only
focusInnerBorder?: IViewProps; // Win32 only
icon: IconProps;
content: TextProps;
}

Просмотреть файл

@ -66,7 +66,8 @@ export const defaultButtonColorTokens: TokenSettings<ButtonTokens, Theme> = (t:
focused: {
backgroundColor: t.colors.brandBackgroundHover,
color: t.colors.neutralForegroundOnBrandHover,
borderColor: t.colors.transparentStroke,
borderColor: t.colors.strokeFocus2,
borderInnerColor: t.colors.strokeFocus1,
iconColor: t.colors.neutralForegroundOnBrandHover,
},
},

Просмотреть файл

@ -1,22 +1,36 @@
import type { Theme } from '@fluentui-react-native/framework';
import { globalTokens } from '@fluentui-react-native/theme-tokens';
import { isHighContrast } from '@fluentui-react-native/theming-utils';
import type { TokenSettings } from '@fluentui-react-native/use-styling';
import type { ButtonTokens } from './Button.types';
export const defaultButtonTokens: TokenSettings<ButtonTokens, Theme> = () =>
export const defaultButtonTokens: TokenSettings<ButtonTokens, Theme> = (theme: Theme) =>
({
borderWidth: globalTokens.stroke.width10,
borderInnerWidth: globalTokens.stroke.width10,
block: {
width: '100%',
},
medium: {
padding: globalTokens.size80 - globalTokens.stroke.width10,
borderWidth: globalTokens.stroke.width10,
iconSize: 16,
focused: {
borderWidth: 0,
padding: globalTokens.size80,
},
primary: !isHighContrast(theme) && {
focused: {
borderWidth: globalTokens.stroke.width20,
padding: globalTokens.size80 - globalTokens.stroke.width20,
},
square: {
focused: {
borderWidth: globalTokens.stroke.width10,
padding: globalTokens.size80 - globalTokens.stroke.width10,
},
},
},
hasContent: {
minWidth: 96,
padding: globalTokens.size60 - globalTokens.stroke.width10,
@ -31,16 +45,39 @@ export const defaultButtonTokens: TokenSettings<ButtonTokens, Theme> = () =>
padding: globalTokens.size60,
paddingHorizontal: globalTokens.size120,
},
primary: !isHighContrast(theme) && {
focused: {
padding: globalTokens.size60 - globalTokens.stroke.width20,
paddingHorizontal: globalTokens.size120 - globalTokens.stroke.width20,
},
square: {
focused: {
padding: globalTokens.size60 - globalTokens.stroke.width10,
paddingHorizontal: globalTokens.size120 - globalTokens.stroke.width10,
},
},
},
},
},
small: {
padding: globalTokens.size40 - globalTokens.stroke.width10,
borderWidth: globalTokens.stroke.width10,
iconSize: 16,
focused: {
borderWidth: 0,
padding: globalTokens.size40,
},
primary: !isHighContrast(theme) && {
focused: {
borderWidth: globalTokens.stroke.width20,
padding: globalTokens.size40 - globalTokens.stroke.width20,
},
square: {
focused: {
borderWidth: globalTokens.stroke.width10,
padding: globalTokens.size40 - globalTokens.stroke.width10,
},
},
},
hasContent: {
minWidth: 64,
minHeight: 32,
@ -54,16 +91,37 @@ export const defaultButtonTokens: TokenSettings<ButtonTokens, Theme> = () =>
focused: {
paddingHorizontal: globalTokens.size80,
},
primary: !isHighContrast(theme) && {
focused: {
paddingHorizontal: globalTokens.size80 - globalTokens.stroke.width20,
},
square: {
focused: {
paddingHorizontal: globalTokens.size80 - globalTokens.stroke.width10,
},
},
},
},
},
large: {
padding: globalTokens.size100 - globalTokens.stroke.width10,
borderWidth: globalTokens.stroke.width10,
iconSize: 20,
focused: {
borderWidth: 0,
padding: globalTokens.size100,
},
primary: !isHighContrast(theme) && {
focused: {
borderWidth: globalTokens.stroke.width20,
padding: globalTokens.size100 - globalTokens.stroke.width20,
},
square: {
focused: {
borderWidth: globalTokens.stroke.width10,
padding: globalTokens.size100 - globalTokens.stroke.width10,
},
},
},
hasContent: {
minWidth: 96,
minHeight: 40,
@ -79,15 +137,30 @@ export const defaultButtonTokens: TokenSettings<ButtonTokens, Theme> = () =>
padding: globalTokens.size80,
paddingHorizontal: globalTokens.size160,
},
primary: !isHighContrast(theme) && {
focused: {
padding: globalTokens.size80 - globalTokens.stroke.width20,
paddingHorizontal: globalTokens.size160 - globalTokens.stroke.width20,
},
square: {
focused: {
padding: globalTokens.size80 - globalTokens.stroke.width10,
paddingHorizontal: globalTokens.size160 - globalTokens.stroke.width10,
},
},
},
},
},
rounded: {
borderRadius: globalTokens.corner.radius40,
borderInnerRadius: globalTokens.corner.radius40 - 1, // reduce the rounding so that the curvature matches
},
circular: {
borderRadius: globalTokens.corner.radiusCircular,
borderInnerRadius: globalTokens.corner.radiusCircular - 1, // reduce the rounding so that the curvature matches
},
square: {
borderRadius: globalTokens.corner.radiusNone,
borderInnerRadius: globalTokens.corner.radiusNone,
},
} as ButtonTokens);

Просмотреть файл

@ -74,5 +74,16 @@ export const stylingSettings: UseStylingOptions<CompoundButtonProps, CompoundBut
}),
['iconColor', 'iconSize'],
),
focusInnerBorder: buildProps(
(tokens: CompoundButtonTokens) => ({
style: {
position: 'absolute',
borderWidth: tokens.borderInnerWidth,
borderColor: tokens.borderInnerColor,
borderRadius: tokens.borderInnerRadius,
},
}),
['borderInnerWidth', 'borderInnerColor', 'borderInnerRadius'],
),
},
};

Просмотреть файл

@ -1,6 +1,6 @@
/** @jsx withSlots */
import * as React from 'react';
import { Pressable, View } from 'react-native';
import { Platform, Pressable, View } from 'react-native';
import { ActivityIndicator } from '@fluentui-react-native/experimental-activity-indicator';
import type { UseSlots } from '@fluentui-react-native/framework';
@ -11,7 +11,7 @@ import { TextV1 as Text } from '@fluentui-react-native/text';
import { stylingSettings } from './CompoundButton.styling';
import type { CompoundButtonProps, CompoundButtonType } from './CompoundButton.types';
import { compoundButtonName } from './CompoundButton.types';
import { buttonLookup } from '../Button';
import { buttonLookup, getFocusBorderStyle } from '../Button';
import { useButton } from '../useButton';
export const CompoundButton = compose<CompoundButtonType>({
@ -23,6 +23,7 @@ export const CompoundButton = compose<CompoundButtonType>({
content: Text,
secondaryContent: Text,
contentContainer: View,
focusInnerBorder: Platform.OS === ('win32' as any) && View,
},
useRender: (userProps: CompoundButtonProps, useSlots: UseSlots<CompoundButtonType>) => {
const button = useButton(userProps);
@ -82,6 +83,13 @@ export const CompoundButton = compose<CompoundButtonType>({
)}
</Slots.contentContainer>
{shouldShowIcon && iconPosition === 'after' && <Slots.icon {...iconProps} accessible={false} />}
{button.state.focused && button.state.shouldUseTwoToneFocusBorder && (
<Slots.focusInnerBorder
style={getFocusBorderStyle(button.state.measuredHeight, button.state.measuredWidth)}
accessible={false}
focusable={false}
/>
)}
</Slots.root>
);
};

Просмотреть файл

@ -0,0 +1,133 @@
import type { Theme } from '@fluentui-react-native/framework';
import { globalTokens } from '@fluentui-react-native/theme-tokens';
import { isHighContrast } from '@fluentui-react-native/theming-utils';
import type { TokenSettings } from '@fluentui-react-native/use-styling';
import type { CompoundButtonTokens } from './CompoundButton.types';
export const defaultCompoundButtonTokens: TokenSettings<CompoundButtonTokens, Theme> = (theme: Theme): CompoundButtonTokens => ({
medium: {
padding: globalTokens.size120 - globalTokens.stroke.width10,
focused: {
padding: globalTokens.size120,
},
primary: !isHighContrast(theme) && {
focused: {
borderWidth: globalTokens.stroke.width20,
padding: globalTokens.size120 - globalTokens.stroke.width20,
},
square: {
focused: {
borderWidth: globalTokens.stroke.width10,
padding: globalTokens.size120 - globalTokens.stroke.width10,
},
},
},
hasContent: {
paddingHorizontal: globalTokens.size120 - globalTokens.stroke.width10,
minWidth: 96,
focused: {
paddingHorizontal: globalTokens.size120,
},
primary: !isHighContrast(theme) && {
focused: {
paddingHorizontal: globalTokens.size120 - globalTokens.stroke.width20,
},
square: {
focused: {
paddingHorizontal: globalTokens.size120 - globalTokens.stroke.width10,
},
},
},
hasIconAfter: {
spacingIconContentAfter: globalTokens.size120,
},
hasIconBefore: {
spacingIconContentBefore: globalTokens.size120,
},
},
},
small: {
padding: globalTokens.size80 - globalTokens.stroke.width10,
focused: {
borderWidth: 0,
padding: globalTokens.size80,
},
primary: !isHighContrast(theme) && {
focused: {
borderWidth: globalTokens.stroke.width20,
padding: globalTokens.size80 - globalTokens.stroke.width20,
},
square: {
focused: {
borderWidth: globalTokens.stroke.width10,
padding: globalTokens.size80 - globalTokens.stroke.width10,
},
},
},
hasContent: {
paddingHorizontal: globalTokens.size80 - globalTokens.stroke.width10,
minWidth: 64,
focused: {
paddingHorizontal: globalTokens.size80,
},
primary: !isHighContrast(theme) && {
focused: {
paddingHorizontal: globalTokens.size80 - globalTokens.stroke.width20,
},
square: {
focused: {
paddingHorizontal: globalTokens.size80 - globalTokens.stroke.width10,
},
},
},
hasIconAfter: {
spacingIconContentAfter: globalTokens.size80,
},
hasIconBefore: {
spacingIconContentBefore: globalTokens.size80,
},
},
},
large: {
padding: globalTokens.size160 - globalTokens.stroke.width10,
focused: {
padding: globalTokens.size160,
},
primary: !isHighContrast(theme) && {
focused: {
borderWidth: globalTokens.stroke.width20,
padding: globalTokens.size160 - globalTokens.stroke.width20,
},
square: {
focused: {
borderWidth: globalTokens.stroke.width10,
padding: globalTokens.size160 - globalTokens.stroke.width10,
},
},
},
hasContent: {
paddingHorizontal: globalTokens.size160 - globalTokens.stroke.width10,
minWidth: 96,
focused: {
paddingHorizontal: globalTokens.size160,
},
primary: !isHighContrast(theme) && {
focused: {
paddingHorizontal: globalTokens.size160 - globalTokens.stroke.width20,
},
square: {
focused: {
paddingHorizontal: globalTokens.size160 - globalTokens.stroke.width10,
},
},
},
hasIconAfter: {
spacingIconContentAfter: globalTokens.size160,
},
hasIconBefore: {
spacingIconContentBefore: globalTokens.size160,
},
},
},
});

Просмотреть файл

@ -12,6 +12,7 @@ exports[`Custom FAB with no shadow(iOS) 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -105,6 +106,7 @@ exports[`Default FAB (iOS) 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}

Просмотреть файл

@ -48,5 +48,16 @@ export const stylingSettings: UseStylingOptions<ToggleButtonProps, ToggleButtonS
}),
['iconColor', 'iconSize'],
),
focusInnerBorder: buildProps(
(tokens: ToggleButtonTokens) => ({
style: {
position: 'absolute',
borderWidth: tokens.borderInnerWidth,
borderColor: tokens.borderInnerColor,
borderRadius: tokens.borderInnerRadius,
},
}),
['borderInnerWidth', 'borderInnerColor', 'borderInnerRadius'],
),
},
};

Просмотреть файл

@ -1,6 +1,6 @@
/** @jsx withSlots */
import * as React from 'react';
import { Pressable } from 'react-native';
import { Platform, Pressable, View } from 'react-native';
import { ActivityIndicator } from '@fluentui-react-native/experimental-activity-indicator';
import type { UseSlots } from '@fluentui-react-native/framework';
@ -12,7 +12,7 @@ import { stylingSettings } from './ToggleButton.styling';
import type { ToggleButtonProps, ToggleButtonType } from './ToggleButton.types';
import { toggleButtonName } from './ToggleButton.types';
import { useToggleButton } from './useToggleButton';
import { buttonLookup } from '../Button';
import { buttonLookup, getFocusBorderStyle } from '../Button';
export const ToggleButton = compose<ToggleButtonType>({
displayName: toggleButtonName,
@ -21,6 +21,7 @@ export const ToggleButton = compose<ToggleButtonType>({
root: Pressable,
icon: Icon,
content: Text,
focusInnerBorder: Platform.OS === ('win32' as any) && View,
},
useRender: (userProps: ToggleButtonProps, useSlots: UseSlots<ToggleButtonType>) => {
const iconProps = createIconProps(userProps.icon);
@ -66,6 +67,13 @@ export const ToggleButton = compose<ToggleButtonType>({
),
)}
{shouldShowIcon && iconPosition === 'after' && <Slots.icon {...iconProps} accessible={false} />}
{toggleButton.state.focused && toggleButton.state.shouldUseTwoToneFocusBorder && (
<Slots.focusInnerBorder
style={getFocusBorderStyle(toggleButton.state.measuredHeight, toggleButton.state.measuredWidth)}
accessible={false}
focusable={false}
/>
)}
</Slots.root>
);
};

Просмотреть файл

@ -24,6 +24,7 @@ exports[`ToggleButton default 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}

Просмотреть файл

@ -11,6 +11,7 @@ exports[`Button component tests Button circular 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -70,6 +71,7 @@ exports[`Button component tests Button composed 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -131,6 +133,7 @@ exports[`Button component tests Button customized 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -189,6 +192,7 @@ exports[`Button component tests Button default 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -252,6 +256,7 @@ exports[`Button component tests Button disabled 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -310,6 +315,7 @@ exports[`Button component tests Button large 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -370,6 +376,7 @@ exports[`Button component tests Button primary 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -428,6 +435,7 @@ exports[`Button component tests Button small 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -487,6 +495,7 @@ exports[`Button component tests Button square 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
@ -547,6 +556,7 @@ exports[`Button component tests Button subtle 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}

Просмотреть файл

@ -1,12 +1,17 @@
import * as React from 'react';
import { Platform } from 'react-native';
import { useFluentTheme } from '@fluentui-react-native/framework';
import type { LayoutEvent } from '@fluentui-react-native/interactive-hooks';
import { usePressableState, useKeyProps, useOnPressWithFocus, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks';
import { isHighContrast } from '@fluentui-react-native/theming-utils';
import type { ButtonProps, ButtonInfo } from './Button.types';
export const useButton = (props: ButtonProps): ButtonInfo => {
const defaultComponentRef = React.useRef(null);
const { onClick, accessibilityRole, componentRef = defaultComponentRef, disabled, loading, enableFocusRing, focusable, ...rest } = props;
const isDisabled = !!disabled || !!loading;
// GH #1336: Set focusRef to null if button is disabled to prevent getting keyboard focus.
const focusRef = isDisabled ? null : componentRef;
@ -15,6 +20,23 @@ export const useButton = (props: ButtonProps): ButtonInfo => {
const onKeyUpProps = useKeyProps(onClick, ' ', 'Enter');
const hasTogglePattern = props.accessibilityActions && !!props.accessibilityActions.find((action) => action.name === 'Toggle');
const theme = useFluentTheme();
const shouldUseTwoToneFocusBorder = Platform.OS === ('win32' as any) && props.appearance === 'primary' && !isHighContrast(theme);
const [baseHeight, setBaseHeight] = React.useState<number | undefined>(undefined);
const [baseWidth, setBaseWidth] = React.useState<number | undefined>(undefined);
const onLayout = React.useCallback(
(e: LayoutEvent) => {
// Only run when shouldUseTwoToneFocusBorder so that state update doesn't
// affect platforms that don't need it.
if (shouldUseTwoToneFocusBorder) {
setBaseHeight(e.nativeEvent.layout.height);
setBaseWidth(e.nativeEvent.layout.width);
}
},
[setBaseHeight, setBaseWidth, shouldUseTwoToneFocusBorder],
);
return {
props: {
...onKeyUpProps,
@ -29,12 +51,18 @@ export const useButton = (props: ButtonProps): ButtonInfo => {
accessibilityRole: accessibilityRole || 'button',
onAccessibilityTap: props.onAccessibilityTap || (!hasTogglePattern ? props.onClick : undefined),
accessibilityLabel: props.accessibilityLabel,
enableFocusRing: enableFocusRing ?? true,
enableFocusRing: enableFocusRing ?? !shouldUseTwoToneFocusBorder,
focusable: focusable ?? !isDisabled,
ref: useViewCommandFocus(componentRef),
iconPosition: props.iconPosition || 'before',
loading,
onLayout,
},
state: {
...pressable.state,
measuredWidth: baseWidth,
measuredHeight: baseHeight,
shouldUseTwoToneFocusBorder: shouldUseTwoToneFocusBorder,
},
state: pressable.state,
};
};

Просмотреть файл

@ -39,6 +39,7 @@ exports[`Menu component tests Menu default 1`] = `
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onLayout={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onResponderGrant={[Function]}
@ -129,6 +130,7 @@ exports[`Menu component tests Menu defaultOpen 1`] = `
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onLayout={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onResponderGrant={[Function]}
@ -219,6 +221,7 @@ exports[`Menu component tests Menu open 1`] = `
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onLayout={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onResponderGrant={[Function]}
@ -309,6 +312,7 @@ exports[`Menu component tests Menu open checkbox and divider 1`] = `
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onLayout={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onResponderGrant={[Function]}
@ -399,6 +403,7 @@ exports[`Menu component tests Menu open checkbox checked 1`] = `
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onLayout={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onResponderGrant={[Function]}
@ -489,6 +494,7 @@ exports[`Menu component tests Menu open checkbox defaultChecked 1`] = `
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onLayout={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onResponderGrant={[Function]}
@ -579,6 +585,7 @@ exports[`Menu component tests Menu open radio 1`] = `
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onLayout={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onResponderGrant={[Function]}
@ -669,6 +676,7 @@ exports[`Menu component tests Menu submenu 1`] = `
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onLayout={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onResponderGrant={[Function]}

Просмотреть файл

@ -23,6 +23,7 @@ exports[`ContextualMenu default 1`] = `
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onLayout={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onResponderGrant={[Function]}

Просмотреть файл

@ -100,6 +100,7 @@ exports[`Notification component tests Notification default 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}

Просмотреть файл

@ -23,6 +23,7 @@ exports[`ContextualMenu default 1`] = `
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onLayout={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onResponderGrant={[Function]}