[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:
Родитель
3512da9de4
Коммит
b724356507
|
@ -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]}
|
||||
|
|
Загрузка…
Ссылка в новой задаче