-
Notifications
You must be signed in to change notification settings - Fork 163
/
Copy pathButton.tsx
141 lines (129 loc) · 5.6 KB
/
Button.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/** @jsxRuntime classic */
/** @jsx withSlots */
import * as React from 'react';
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, 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';
import { stylingSettings } from './Button.styling';
import { buttonName } from './Button.types';
import type { ButtonType, ButtonProps } from './Button.types';
import { getDefaultSize, getPlatformSpecificAppearance } from './ButtonPlatform';
import { extractOuterStylePropsAndroid } from './ExtractStyle.android';
import { useButton } from './useButton';
/**
* A function which determines if a set of styles should be applied to the component given the current state and props of the button.
*
* @param layer The name of the state that is being checked for
* @param state The current state of the button
* @param userProps The props that were passed into the button
* @returns Whether the styles that are assigned to the layer should be applied to the button
*/
export const buttonLookup = (layer: string, state: IPressableState, userProps: ButtonProps): boolean => {
return (
state[layer] ||
userProps[layer] ||
layer === getPlatformSpecificAppearance(userProps['appearance']) ||
layer === userProps['size'] ||
(!userProps['size'] && layer === getDefaultSize()) ||
layer === userProps['shape'] ||
(!userProps['shape'] && layer === 'rounded') ||
(layer === 'hovered' && state[layer] && !userProps.loading) ||
(layer === 'hasContent' && !userProps.iconOnly) ||
(layer === 'hasIconAfter' && (userProps.icon || userProps.loading) && userProps.iconPosition === 'after') ||
(layer === 'hasIconBefore' && (userProps.icon || userProps.loading) && (!userProps.iconPosition || userProps.iconPosition === 'before'))
);
};
export const Button = compose<ButtonType>({
displayName: buttonName,
...stylingSettings,
slots: {
root: Pressable,
rippleContainer: Platform.OS === 'android' && View,
focusInnerBorder: (Platform.OS === ('win32' as any) || Platform.OS === 'windows') && View,
icon: Icon,
content: Text,
},
useRender: (userProps: ButtonProps, useSlots: UseSlots<ButtonType>) => {
const button = useButton(userProps);
const iconProps = createIconProps(userProps.icon);
// grab the styled slots
const Slots = useSlots(userProps, (layer) => buttonLookup(layer, button.state, userProps));
// now return the handler for finishing render
return (final: ButtonProps, ...children: React.ReactNode[]) => {
const { icon, iconOnly, iconPosition, loading, accessibilityLabel, ...mergedProps } = mergeProps(button.props, final);
const shouldShowIcon = !loading && icon;
if (__DEV__ && iconOnly) {
React.Children.forEach(children, (child) => {
if (typeof child === 'string') {
console.warn('iconOnly should not be set when Button has content.');
}
});
}
let childText = '';
if (accessibilityLabel === undefined) {
React.Children.forEach(children, (child) => {
if (typeof child === 'string') {
childText = child; // We only automatically support the one child string.
}
});
}
const label = accessibilityLabel ?? childText;
const buttonContent = (
<React.Fragment>
{loading && <ActivityIndicator />}
{shouldShowIcon && iconPosition === 'before' && <Slots.icon {...iconProps} accessible={false} />}
{React.Children.map(children, (child) =>
typeof child === 'string' ? (
<Slots.content accessible={false} key="content">
{child}
</Slots.content>
) : (
child
),
)}
{shouldShowIcon && iconPosition === 'after' && <Slots.icon {...iconProps} accessible={false} />}
</React.Fragment>
);
const hasRipple = Platform.OS === 'android';
if (hasRipple) {
const [outerStyleProps, innerStyleProps] = extractOuterStylePropsAndroid(mergedProps.style);
return (
<Slots.rippleContainer style={outerStyleProps}>
{/* RN Pressable needs to be wrapped with a root view to support curved edges */}
<Slots.root accessibilityLabel={label} {...mergedProps} style={innerStyleProps}>
{buttonContent}
</Slots.root>
</Slots.rippleContainer>
);
} else {
return (
<Slots.root {...mergedProps} accessibilityLabel={label}>
{buttonContent}
{button.state.focused &&
!!button.state.measuredHeight &&
!!button.state.measuredWidth &&
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);