diff --git a/apps/component-generator/component-templates/ComponentTemplate/src/ComponentName.tsx b/apps/component-generator/component-templates/ComponentTemplate/src/ComponentName.tsx index c1688b3fd8..e9adee1bd2 100644 --- a/apps/component-generator/component-templates/ComponentTemplate/src/ComponentName.tsx +++ b/apps/component-generator/component-templates/ComponentTemplate/src/ComponentName.tsx @@ -7,7 +7,7 @@ import { stylingSettings } from './ComponentName.styling'; import { compose, mergeProps, withSlots, UseSlots } from '@fluentui-react-native/framework'; import { useComponentName } from './useComponentName'; /** - * A function which determines if a set of styles should be applied to the compoent given the current state and props of the component-name. + * A function which determines if a set of styles should be applied to the component given the current state and props of the component-name. * * @param layer The name of the state that is being checked for * @param userProps The props that were passed into the component-name diff --git a/change/@fluentui-react-native-avatar-3662454b-777e-4f5a-a9fb-925506c75279.json b/change/@fluentui-react-native-avatar-3662454b-777e-4f5a-a9fb-925506c75279.json new file mode 100644 index 0000000000..232c73f3cd --- /dev/null +++ b/change/@fluentui-react-native-avatar-3662454b-777e-4f5a-a9fb-925506c75279.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Typo Fix", + "packageName": "@fluentui-react-native/avatar", + "email": "ayushsinghs@yahoo.in", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-button-6a251b28-f044-4844-b8ba-d6c1847178d9.json b/change/@fluentui-react-native-button-6a251b28-f044-4844-b8ba-d6c1847178d9.json new file mode 100644 index 0000000000..543f255f9c --- /dev/null +++ b/change/@fluentui-react-native-button-6a251b28-f044-4844-b8ba-d6c1847178d9.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Ripple/Fix on FAB", + "packageName": "@fluentui-react-native/button", + "email": "ruaraki@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-experimental-menu-button-e367c22f-dc11-4398-9c6f-f2bd3641e4e1.json b/change/@fluentui-react-native-experimental-menu-button-e367c22f-dc11-4398-9c6f-f2bd3641e4e1.json new file mode 100644 index 0000000000..e3a6f4b276 --- /dev/null +++ b/change/@fluentui-react-native-experimental-menu-button-e367c22f-dc11-4398-9c6f-f2bd3641e4e1.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Ripple changes for RN Pressable", + "packageName": "@fluentui-react-native/experimental-menu-button", + "email": "ayushsinghs@yahoo.in", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-experimental-radio-group-f88773ec-dbe0-49d2-a35e-e72f79f5046c.json b/change/@fluentui-react-native-experimental-radio-group-f88773ec-dbe0-49d2-a35e-e72f79f5046c.json new file mode 100644 index 0000000000..e748938bd1 --- /dev/null +++ b/change/@fluentui-react-native-experimental-radio-group-f88773ec-dbe0-49d2-a35e-e72f79f5046c.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Typo Fix", + "packageName": "@fluentui-react-native/experimental-radio-group", + "email": "ayushsinghs@yahoo.in", + "dependentChangeType": "none" +} diff --git a/packages/components/Avatar/src/Avatar.tsx b/packages/components/Avatar/src/Avatar.tsx index 66605d3fcd..f2559c17c6 100644 --- a/packages/components/Avatar/src/Avatar.tsx +++ b/packages/components/Avatar/src/Avatar.tsx @@ -10,7 +10,7 @@ import { Icon } from '@fluentui-react-native/icon'; import { Svg, Path } from 'react-native-svg'; /** - * A function which determines if a set of styles should be applied to the compoent given the current state and props of the avatar. + * A function which determines if a set of styles should be applied to the component given the current state and props of the avatar. * * @param layer The name of the state that is being checked for * @param state The current state of the avatar diff --git a/packages/components/Button/src/Button.styling.ts b/packages/components/Button/src/Button.styling.ts index df7fcfcde6..40a3436c05 100644 --- a/packages/components/Button/src/Button.styling.ts +++ b/packages/components/Button/src/Button.styling.ts @@ -32,6 +32,21 @@ export const stylingSettings: UseStylingOptions { + return { + style: { + flexDirection: 'row', + alignSelf: 'baseline', + borderRadius: tokens.borderRadius, + overflow: 'hidden', + }, + }; + }, + ['borderRadius'], + ), + }), root: buildProps( (tokens: ButtonTokens, theme: Theme) => ({ style: { @@ -45,8 +60,11 @@ export const stylingSettings: UseStylingOptions { diff --git a/packages/components/Button/src/Button.tsx b/packages/components/Button/src/Button.tsx index b02cb18c37..90002d1f90 100644 --- a/packages/components/Button/src/Button.tsx +++ b/packages/components/Button/src/Button.tsx @@ -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 { buttonName, ButtonType, ButtonProps } from './Button.types'; import { TextV1 as Text } from '@fluentui-react-native/text'; @@ -9,9 +9,10 @@ import { compose, mergeProps, withSlots, UseSlots } from '@fluentui-react-native import { useButton } from './useButton'; import { Icon } from '@fluentui-react-native/icon'; import { createIconProps, IPressableState } from '@fluentui-react-native/interactive-hooks'; +import { extractOuterStylePropsAndroid } from './ExtractStyle.android'; /** - * A function which determines if a set of styles should be applied to the compoent given the current state and props of the button. + * 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 @@ -39,11 +40,13 @@ export const Button = compose({ ...stylingSettings, slots: { root: Pressable, + rippleContainer: View, icon: Icon, content: Text, }, useRender: (userProps: ButtonProps, useSlots: UseSlots) => { const button = useButton(userProps); + const iconProps = createIconProps(userProps.icon); // grab the styled slots const Slots = useSlots(userProps, (layer) => buttonLookup(layer, button.state, userProps)); @@ -69,18 +72,37 @@ export const Button = compose({ } }); } - const label = accessibilityLabel ?? childText; - return ( - + const label = accessibilityLabel ?? childText; + const buttonContent = ( + {loading && } {shouldShowIcon && iconPosition === 'before' && } {React.Children.map(children, (child) => typeof child === 'string' ? {child} : child, )} {shouldShowIcon && iconPosition === 'after' && } - + ); + + const hasRipple = Platform.OS === 'android'; + if (hasRipple) { + const [outerStyleProps, innerStyleProps] = extractOuterStylePropsAndroid(mergedProps.style); + return ( + + {/* RN Pressable needs to be wrapped with a root view to support curved edges */} + + {buttonContent} + + + ); + } else { + return ( + + {buttonContent} + + ); + } }; }, }); diff --git a/packages/components/Button/src/Button.types.ts b/packages/components/Button/src/Button.types.ts index 9e0b77d66d..14fa129bd4 100644 --- a/packages/components/Button/src/Button.types.ts +++ b/packages/components/Button/src/Button.types.ts @@ -5,6 +5,7 @@ import { FontTokens, IBorderTokens, IColorTokens, IShadowTokens, LayoutTokens } import { IFocusable, InteractionEvent, PressablePropsExtended, PressableState } from '@fluentui-react-native/interactive-hooks'; import { IconProps, IconSourcesType } from '@fluentui-react-native/icon'; import { ShadowToken } from '@fluentui-react-native/theme-types'; +import { IViewProps } from '@fluentui-react-native/adapters'; export const buttonName = 'Button'; export type ButtonSize = 'small' | 'medium' | 'large'; @@ -17,6 +18,11 @@ export interface ButtonCoreTokens extends LayoutTokens, FontTokens, IBorderToken */ iconColor?: ColorValue; + /** + * Ripple color for Android. + */ + rippleColor?: ColorValue; + /** * The size of the icon. */ @@ -154,6 +160,7 @@ export interface ButtonInfo { export interface ButtonSlotProps { root: React.PropsWithRef; + rippleContainer?: IViewProps; // Android only icon: IconProps; content: TextProps; } diff --git a/packages/components/Button/src/ButtonColorTokens.ts b/packages/components/Button/src/ButtonColorTokens.ts index 5ddfda2c6d..ce22ee0f5b 100644 --- a/packages/components/Button/src/ButtonColorTokens.ts +++ b/packages/components/Button/src/ButtonColorTokens.ts @@ -8,6 +8,7 @@ export const defaultButtonColorTokens: TokenSettings = (t: color: t.colors.buttonText, borderColor: t.colors.buttonBorder, iconColor: t.colors.buttonIcon, + rippleColor: t.colors.defaultPressedBackground, // Android only disabled: { backgroundColor: t.colors.defaultDisabledBackground, color: t.colors.defaultDisabledContent, diff --git a/packages/components/Button/src/ExtractStyle.android.ts b/packages/components/Button/src/ExtractStyle.android.ts new file mode 100644 index 0000000000..18288d0853 --- /dev/null +++ b/packages/components/Button/src/ExtractStyle.android.ts @@ -0,0 +1,57 @@ +import { memoize } from '@fluentui-react-native/framework'; +import { ViewStyle } from 'react-native'; + +/** + * React Native's Pressable does not support curved edges. + * It needs to be wrapped inside another view and have border set there. + * This function extracts styles that should be applied on the outer view. + * + * @param style Styling that is to be applied on the component + * @returns Array containing split styles that are to be applied on the inner and outer views + */ + +export const extractOuterStylePropsAndroid = memoize((style: ViewStyle = {}): [outerStyleProps: ViewStyle, innerStyleProps: ViewStyle] => { + const { + margin, + marginTop, + marginBottom, + marginLeft, + marginRight, + marginStart, + marginEnd, + marginVertical, + marginHorizontal, + start, + end, + left, + right, + top, + bottom, + display, + opacity, + ...restOfProps + } = style; + + return [ + { + margin, + marginTop, + marginBottom, + marginLeft, + marginRight, + marginStart, + marginEnd, + marginVertical, + marginHorizontal, + start, + end, + left, + right, + top, + bottom, + display, + opacity, + }, + { borderWidth: 0, ...restOfProps }, + ]; +}); diff --git a/packages/components/Button/src/FAB/FAB.styling.ts b/packages/components/Button/src/FAB/FAB.styling.ts index 97e23d40f1..5d18eccc6b 100644 --- a/packages/components/Button/src/FAB/FAB.styling.ts +++ b/packages/components/Button/src/FAB/FAB.styling.ts @@ -1,16 +1,32 @@ -import { ButtonCoreTokens, ButtonCoreProps } from '../Button.types'; -import { FABSlotProps } from './FAB.types'; +import { ButtonCoreTokens } from '../Button.types'; +import { FABProps, FABSlotProps } from './FAB.types'; import { Theme, UseStylingOptions, buildProps } from '@fluentui-react-native/framework'; import { borderStyles, layoutStyles, fontStyles, shadowStyles } from '@fluentui-react-native/tokens'; import { buttonCoreStates } from '../Button.styling'; import { getTextMarginAdjustment } from '@fluentui-react-native/styling-utils'; +import { Platform } from 'react-native'; import { defaultFABTokens } from './FABTokens'; import { defaultFABColorTokens } from './FABColorTokens'; -export const stylingSettings: UseStylingOptions = { +export const stylingSettings: UseStylingOptions = { tokens: [defaultFABTokens, defaultFABColorTokens], states: [...buttonCoreStates], slotProps: { + ...(Platform.OS == 'android' && { + rippleContainer: buildProps( + (tokens: ButtonCoreTokens) => { + return { + style: { + flexDirection: 'row', + alignSelf: 'baseline', + borderRadius: tokens.borderRadius, + overflow: 'hidden', + }, + }; + }, + ['borderRadius'], + ), + }), root: buildProps( (tokens: ButtonCoreTokens, theme: Theme) => ({ style: { @@ -25,9 +41,12 @@ export const stylingSettings: UseStylingOptions ({ diff --git a/packages/components/Button/src/FAB/FABColorTokens.ts b/packages/components/Button/src/FAB/FABColorTokens.ts index de3c619494..765f928774 100644 --- a/packages/components/Button/src/FAB/FABColorTokens.ts +++ b/packages/components/Button/src/FAB/FABColorTokens.ts @@ -8,6 +8,7 @@ export const defaultFABColorTokens: TokenSettings = (t: color: t.colors.brandedContent, borderColor: t.colors.brandedBorder, iconColor: t.colors.brandedIcon, + rippleColor: t.colors.defaultPressedBackground, // Android only disabled: { backgroundColor: t.colors.brandedDisabledBackground, color: t.colors.brandedDisabledContent, diff --git a/packages/components/Button/src/FAB/FABCore.tsx b/packages/components/Button/src/FAB/FABCore.tsx index dc3fc58d1c..0b774ae1f0 100644 --- a/packages/components/Button/src/FAB/FABCore.tsx +++ b/packages/components/Button/src/FAB/FABCore.tsx @@ -1,6 +1,6 @@ /** @jsx withSlots */ import * as React from 'react'; -import { Platform, View } from 'react-native'; +import { Platform, Pressable, View } from 'react-native'; import { fabName, FABProps, FABType } from './FAB.types'; import { TextV1 as Text } from '@fluentui-react-native/text'; import { stylingSettings } from './FAB.styling'; @@ -9,9 +9,10 @@ import { useButton } from '../useButton'; import { Icon } from '@fluentui-react-native/icon'; import { createIconProps, IPressableState } from '@fluentui-react-native/interactive-hooks'; import { Shadow } from '@fluentui-react-native/experimental-shadow'; +import { extractOuterStylePropsAndroid } from '../ExtractStyle.android'; /** - * A function which determines if a set of styles should be applied to the compoent given the current state and props of the button. + * 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 @@ -28,9 +29,10 @@ export const FAB = compose({ displayName: fabName, ...stylingSettings, slots: { - root: View, + root: Pressable, icon: Icon, content: Text, + rippleContainer: View, shadow: Shadow, }, useRender: (userProps: FABProps, useSlots: UseSlots) => { @@ -64,21 +66,37 @@ export const FAB = compose({ } const label = accessibilityLabel ?? childText; - const fabWithoutShadow = ( - + const buttonContent = ( + {icon && } {showContent && React.Children.map(children, (child) => typeof child === 'string' ? {child} : child, )} + + ); + const buttonContentWithRoot = ( + + {buttonContent} ); const hasShadow = Platform.OS === 'ios'; + const hasRipple = Platform.OS === 'android'; + if (hasShadow) { - return {fabWithoutShadow}; + return {buttonContentWithRoot}; + } else if (hasRipple) { + const [outerStyleProps, innerStyleProps] = extractOuterStylePropsAndroid(mergedProps.style); + return ( + + + {buttonContent} + + + ); } else { - return fabWithoutShadow; + return buttonContentWithRoot; } }; }, diff --git a/packages/components/Button/src/FAB/__snapshots__/FAB.test.tsx.snap b/packages/components/Button/src/FAB/__snapshots__/FAB.test.tsx.snap index 96d926e776..b6749aea2c 100644 --- a/packages/components/Button/src/FAB/__snapshots__/FAB.test.tsx.snap +++ b/packages/components/Button/src/FAB/__snapshots__/FAB.test.tsx.snap @@ -5,16 +5,19 @@ exports[`Custom FAB with no shadow(iOS) 1`] = ` accessibilityLabel="Custom FAB with no shadow(iOS)" accessibilityRole="button" accessible={true} + collapsable={false} enableFocusRing={true} focusable={true} iconPosition="before" onBlur={[Function]} + onClick={[Function]} onFocus={[Function]} - onHoverIn={[Function]} - onHoverOut={[Function]} - onPress={[Function]} - onPressIn={[Function]} - onPressOut={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} style={ Object { "alignItems": "center", @@ -96,16 +99,19 @@ exports[`Default FAB (iOS) 1`] = ` accessibilityLabel="Default FAB (iOS)" accessibilityRole="button" accessible={true} + collapsable={false} enableFocusRing={true} focusable={true} iconPosition="before" onBlur={[Function]} + onClick={[Function]} onFocus={[Function]} - onHoverIn={[Function]} - onHoverOut={[Function]} - onPress={[Function]} - onPressIn={[Function]} - onPressOut={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} style={ Object { "alignItems": "center", diff --git a/packages/experimental/MenuButton/jest.config.js b/packages/experimental/MenuButton/jest.config.js index 051098f649..a8bf7c4f87 100644 --- a/packages/experimental/MenuButton/jest.config.js +++ b/packages/experimental/MenuButton/jest.config.js @@ -1,2 +1,2 @@ const { configureReactNativeJest } = require('@fluentui-react-native/scripts'); -module.exports = configureReactNativeJest('android'); +module.exports = configureReactNativeJest('ios'); diff --git a/packages/experimental/MenuButton/src/__tests__/__snapshots__/MenuButton.test.tsx.snap b/packages/experimental/MenuButton/src/__tests__/__snapshots__/MenuButton.test.tsx.snap index dff192a8a9..0a5e8167b0 100644 --- a/packages/experimental/MenuButton/src/__tests__/__snapshots__/MenuButton.test.tsx.snap +++ b/packages/experimental/MenuButton/src/__tests__/__snapshots__/MenuButton.test.tsx.snap @@ -42,8 +42,8 @@ exports[`ContextualMenu default 1`] = ` style={ Object { "color": "#323130", - "fontFamily": "Segoe UI", - "fontSize": 14, + "fontFamily": "System", + "fontSize": 15, "fontWeight": "600", "margin": 0, "marginBottom": 0, @@ -59,7 +59,7 @@ exports[`ContextualMenu default 1`] = ` align="xMidYMid" bbHeight={16} bbWidth={12} - color={-10395295} + color={4284572001} focusable={false} height={16} meetOrSlice={0} @@ -78,7 +78,7 @@ exports[`ContextualMenu default 1`] = ` }, ] } - tintColor={-10395295} + tintColor={4284572001} vbHeight={6} vbWidth={11} width={12} diff --git a/packages/experimental/RadioGroup/src/Radio/Radio.tsx b/packages/experimental/RadioGroup/src/Radio/Radio.tsx index 2ee1f71807..cc29bbe535 100644 --- a/packages/experimental/RadioGroup/src/Radio/Radio.tsx +++ b/packages/experimental/RadioGroup/src/Radio/Radio.tsx @@ -9,7 +9,7 @@ import { filterViewProps } from '@fluentui-react-native/adapters'; import { PressableState } from '@fluentui-react-native/interactive-hooks'; /** - * A function which determines if a set of styles should be applied to the compoent given the current state and props of the Radio. + * A function which determines if a set of styles should be applied to the component given the current state and props of the Radio. * * @param layer The name of the state that is being checked for * @param state The current state of the Radio diff --git a/packages/experimental/RadioGroup/src/RadioGroup/RadioGroup.tsx b/packages/experimental/RadioGroup/src/RadioGroup/RadioGroup.tsx index a4e62b1533..552fca6ef3 100644 --- a/packages/experimental/RadioGroup/src/RadioGroup/RadioGroup.tsx +++ b/packages/experimental/RadioGroup/src/RadioGroup/RadioGroup.tsx @@ -11,7 +11,7 @@ import { RadioGroupProvider } from './radioGroupContext'; import { useRadioGroupContextValue } from './useRadioGroupContextValue'; /** - * A function which determines if a set of styles should be applied to the compoent given the current state and props of the radiogroup. + * A function which determines if a set of styles should be applied to the component given the current state and props of the radiogroup. * * @param layer The name of the state that is being checked for * @param state The current state of the radiogroup