Skip to content

[Draft] Switch Button, Checkbox, and RadioButton from View to Pressable #1070

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 13 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const Slider: React.FunctionComponent<ISliderProps> = (props: ISliderProp
<Track style={styles.track} />
{trackLength.current > 0 && (
<Pressable
renderStyle={(state) => onThumbRenderStyle(state, thumbLocation)}
style={(state) => onThumbRenderStyle(state, thumbLocation)}
onStartShouldSetResponder={() => trackLength.current > 0}
onResponderStart={(e) => {
startTouchPosition.current = e.nativeEvent.pageX;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ const pressable: React.FunctionComponent = () => {
const [hoverProps, hoverState] = useHoverState({});

return (
<Stack horizontal gap={5}>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This made the Pressable test page look weird, where one square was off center. Removing both props fixed the issue.

<Stack>
<Square color="blue" />
<Pressable renderStyle={renderStyle}>
<Pressable style={renderStyle}>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prop was named as such because of an issue where the types for react native didn't support a function that returns a style. That's no longer the case, so we can just make this match ViewProps/PressableProps

<Square />
</Pressable>
<Square color="green" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Switch Button, Checkbox, and Radiobutton to use Pressable",
"packageName": "@fluentui-react-native/button",
"email": "sanajmi@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Switch Button, Checkbox, and Radiobutton to use Pressable",
"packageName": "@fluentui-react-native/checkbox",
"email": "sanajmi@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Switch Button, Checkbox, and Radiobutton to use Pressable",
"packageName": "@fluentui-react-native/experimental-button",
"email": "sanajmi@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Switch Button, Checkbox, and Radiobutton to use Pressable",
"packageName": "@fluentui-react-native/experimental-checkbox",
"email": "sanajmi@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Switch Button, Checkbox, and Radiobutton to use Pressable",
"packageName": "@fluentui-react-native/interactive-hooks",
"email": "sanajmi@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Switch Button, Checkbox, and Radiobutton to use Pressable",
"packageName": "@fluentui-react-native/pressable",
"email": "sanajmi@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Switch Button, Checkbox, and Radiobutton to use Pressable",
"packageName": "@fluentui-react-native/radio-group",
"email": "sanajmi@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Switch Button, Checkbox, and Radiobutton to use Pressable",
"packageName": "@fluentui-react-native/tester",
"email": "sanajmi@microsoft.com",
"dependentChangeType": "patch"
}
7 changes: 4 additions & 3 deletions packages/components/Button/src/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @jsx withSlots */
import * as React from 'react';
import { View } from 'react-native';
import { Pressable } from '@fluentui-react-native/pressable';
import { IButtonSlotProps, IButtonState, IButtonProps, IButtonRenderData, buttonName, IButtonType } from './Button.types';
import { compose, IUseComposeStyling } from '@uifabricshared/foundation-compose';
import { ISlots, withSlots } from '@uifabricshared/foundation-composable';
Expand All @@ -11,11 +12,11 @@ import { filterViewProps } from '@fluentui-react-native/adapters';
import { mergeSettings } from '@uifabricshared/foundation-settings';

import {
useAsPressable,
useKeyCallback,
useViewCommandFocus,
createIconProps,
useOnPressWithFocus,
usePressableState,
} from '@fluentui-react-native/interactive-hooks';
import { Icon } from '@fluentui-react-native/icon';

Expand All @@ -39,7 +40,7 @@ export const Button = compose<IButtonType>({
// Ensure focus is placed on button after click
const onPressWithFocus = useOnPressWithFocus(componentRef, onClick);
// attach the pressable state handlers
const pressable = useAsPressable({ ...rest, onPress: onPressWithFocus });
const pressable = usePressableState({ ...rest, onPress: onPressWithFocus });
const onKeyUp = useKeyCallback(onClick, ' ', 'Enter');
// set up state
const state: IButtonState = {
Expand Down Expand Up @@ -91,7 +92,7 @@ export const Button = compose<IButtonType>({
);
},
slots: {
root: View,
root: Pressable,
stack: { slotType: View, filter: filterViewProps },
borderWrapper: { slotType: View, filter: filterViewProps },
startIcon: { slotType: Icon as React.ComponentType },
Expand Down
8 changes: 4 additions & 4 deletions packages/components/Checkbox/src/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import { mergeSettings } from '@uifabricshared/foundation-settings';
import { foregroundColorTokens, textTokens, borderTokens, getPaletteFromTheme } from '@fluentui-react-native/tokens';
import {
useAsToggle,
useAsPressable,
usePressableState,
useViewCommandFocus,
useKeyCallback,
useOnPressWithFocus,
} from '@fluentui-react-native/interactive-hooks';
import { backgroundColorTokens } from '@fluentui-react-native/tokens';
import { IPressableProps } from '@fluentui-react-native/pressable';
import { Pressable, IPressableProps } from '@fluentui-react-native/pressable';

export const Checkbox = compose<ICheckboxType>({
displayName: checkboxName,
Expand Down Expand Up @@ -49,7 +49,7 @@ export const Checkbox = compose<ICheckboxType>({
// Ensure focus is placed on checkbox after click
const toggleCheckedWithFocus = useOnPressWithFocus(componentRef, toggleChecked);

const pressable = useAsPressable({ onPress: toggleCheckedWithFocus, ...(rest as IPressableProps) });
const pressable = usePressableState({ onPress: toggleCheckedWithFocus, ...(rest as IPressableProps) });

const buttonRef = useViewCommandFocus(componentRef);

Expand Down Expand Up @@ -115,7 +115,7 @@ export const Checkbox = compose<ICheckboxType>({

settings,
slots: {
root: View,
root: Pressable,
checkbox: { slotType: View, filter: filterViewProps },
checkmark: Text,
content: Text,
Expand Down
3 changes: 2 additions & 1 deletion packages/components/Link/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@
"prettier-fix": "fluentui-scripts prettier --fix true"
},
"dependencies": {
"@uifabricshared/foundation-compose": "^1.10.18",
"@fluentui-react-native/adapters": ">=0.7.4 <1.0.0",
"@fluentui-react-native/interactive-hooks": ">=0.10.49 <1.0.0",
"@fluentui-react-native/pressable": "^0.7.25",
"@fluentui-react-native/text": ">=0.10.20 <1.0.0",
"@fluentui-react-native/tokens": ">=0.10.0 <1.0.0",
"@uifabricshared/foundation-composable": ">=0.9.2 <1.0.0",
"@uifabricshared/foundation-compose": "^1.10.18",
"@uifabricshared/foundation-settings": ">=0.10.2 <1.0.0"
},
"devDependencies": {
Expand Down
17 changes: 9 additions & 8 deletions packages/components/Link/src/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
/** @jsx withSlots */
import * as React from 'react';

import { Linking, View } from 'react-native';
import { Linking } from 'react-native';
import { Text } from '@fluentui-react-native/text';
import { compose, IUseComposeStyling } from '@uifabricshared/foundation-compose';
import { ILinkProps, ILinkSlotProps, ILinkState, ILinkRenderData, IWithLinkOptions, linkName, ILinkType } from './Link.types';
import { ILinkProps, ILinkSlotProps, ILinkState, ILinkRenderData, linkName, ILinkType } from './Link.types';
import { settings } from './Link.settings';
import { foregroundColorTokens, textTokens, borderTokens } from '@fluentui-react-native/tokens';
import { useAsPressable, useKeyCallback, useOnPressWithFocus, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks';
import { useKeyCallback, useOnPressWithFocus, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks';
import { mergeSettings } from '@uifabricshared/foundation-settings';
import { ISlots, withSlots } from '@uifabricshared/foundation-composable';
import { IViewProps } from '@fluentui-react-native/adapters';
import { Pressable } from '@fluentui-react-native/pressable';
import { usePressableState } from '@fluentui-react-native/interactive-hooks';

export type ILinkHooks = [IWithLinkOptions<IViewProps>, ILinkState];
export type ILinkHooks = [ILinkProps, ILinkState];

export function useAsLink(userProps: IWithLinkOptions<IViewProps>, ref: React.RefObject<any>): ILinkHooks {
export function useAsLink(userProps: ILinkProps, ref: React.RefObject<any>): ILinkHooks {
const { url, onPress, ...rest } = userProps;

const [linkState, setLinkState] = React.useState({ visited: false });
Expand All @@ -33,7 +34,7 @@ export function useAsLink(userProps: IWithLinkOptions<IViewProps>, ref: React.Re
// Ensure focus is placed on link after click
const linkOnPressWithFocus = useOnPressWithFocus(ref, linkOnPress);

const pressable = useAsPressable({ onPress: linkOnPressWithFocus, ...rest });
const pressable = usePressableState({ onPress: linkOnPressWithFocus, ...rest });
const onKeyUp = useKeyCallback(linkOnPress, ' ', 'Enter');

const newState = {
Expand Down Expand Up @@ -88,7 +89,7 @@ export const Link = compose<ILinkType>({
);
},
slots: {
root: View,
root: Pressable,
content: Text,
},
styles: {
Expand Down
6 changes: 3 additions & 3 deletions packages/components/Link/src/Link.types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';
import { ViewProps } from 'react-native';
import { IRenderData } from '@uifabricshared/foundation-composable';
import { IForegroundColorTokens, FontTokens, IBorderTokens } from '@fluentui-react-native/tokens';
import { ITextProps } from '@fluentui-react-native/text';
import { IPressableProps } from '@fluentui-react-native/pressable';
import { IFocusable, IPressableState, IWithPressableOptions } from '@fluentui-react-native/interactive-hooks';

export const linkName = 'RNFLink';
Expand Down Expand Up @@ -45,7 +45,7 @@ export interface ILinkOptions {
// eslint-disable-next-line @typescript-eslint/ban-types
export type IWithLinkOptions<T extends object> = ILinkOptions & IWithPressableOptions<T>;

export interface ILinkProps extends IWithLinkOptions<ITextProps> {
export interface ILinkProps extends IWithLinkOptions<IPressableProps> {
/**
* The visible text of the link that the user sees.
* @default undefined
Expand All @@ -58,7 +58,7 @@ export interface ILinkProps extends IWithLinkOptions<ITextProps> {
}

export type ILinkSlotProps = {
root: React.PropsWithRef<ViewProps>;
root: React.PropsWithRef<IPressableProps>;
content: ITextProps;
};

Expand Down
40 changes: 2 additions & 38 deletions packages/components/Pressable/src/Pressable.props.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,8 @@
import * as React from 'react';
import { ViewStyle, StyleProp } from 'react-native';
import { IViewProps } from '@fluentui-react-native/adapters';
import { IWithPressableOptions, IPressableState } from '@fluentui-react-native/interactive-hooks';
import { IWithPressableOptions } from '@fluentui-react-native/interactive-hooks';

// eslint-disable-next-line @typescript-eslint/ban-types
export type IPressableProps<TBase extends object = IViewProps> = IWithPressableOptions<TBase> & {
children?: IRenderChild<IPressableState>;

// Typescript will not allow an extension of the IView* interface
// that allows style to take on a function value. This is not a problem
// with children, presumably because function components are valid as children.
// As such, a renderStyle prop that takes a function value is provided
// instead, in conjunction with the base style prop (StyleProp<ViewStyle>).
// The style prop will only be used if a renderStyle is not provided.
renderStyle?: IRenderStyle<IPressableState, ViewStyle>;
};

/**
* Used by IRenderChild, it simply describes a function that takes
* some generic state type T and returns a ReactNode
*/
export type IChildAsFunction<T> = (state: T) => React.ReactNode;

/**
* An IRenderChild describes children as a function that take the current
* state of the parent component. It is up to the parent to invoke the function
* and make proper use of the more typical ReactNode object that is returned
* This is an especially helpful construct when children of a Touchable require
* knowledge of the interaction state of their parent to properly render themselves
* (e.g. foreground color of a text child)
*/
export type IRenderChild<T> = IChildAsFunction<T> | React.ReactNode;

/**
* An IRenderStyle describes style as a function that takes the current
* state of the parent component. It is up to the parent to invoke the function
* and make proper use of the more typical StyleProp<S> object that is returned
* This is convenient for when styles need to be calculated depending on interaction states.
*/
export type IRenderStyle<T, S> = (state: T) => StyleProp<S>;
export type IPressableProps<TBase extends object = IViewProps> = IWithPressableOptions<TBase>;

// eslint-disable-next-line @typescript-eslint/ban-types
export type IPressableType<TBase extends object = IViewProps> = {
Expand Down
34 changes: 2 additions & 32 deletions packages/components/Pressable/src/Pressable.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,3 @@
/**
* This is primarily a fork of React Native's Touchable Mixin.
* It has been repurposed as it's own standalone control for win32,
* as it needs to support a richer set of functionality on the desktop.
* The touchable variants can be rewritten as wrappers around TouchableWin32
* by passing the correct set of props down and managing state correctly.
*
* React Native's Touchable.js file (https://github.com/facebook/react-native/blob/master/Libraries/Components/Touchable/Touchable.js)
* provides an overview over how touchables work and interact with the gesture responder system.
*/
'use strict';
import { Pressable } from 'react-native';

import { IUseStyling, composable } from '@uifabricshared/foundation-composable';
import { View } from 'react-native';
import { IPressableProps, IPressableType } from './Pressable.props';
import { mergeSettings } from '@uifabricshared/foundation-settings';
import { useAsPressable } from '@fluentui-react-native/interactive-hooks';

export const Pressable = composable<IPressableType>({
slots: { root: View },
usePrepareProps: (userProps: IPressableProps, useStyling: IUseStyling<IPressableType>) => {
const { renderStyle, ...rest } = userProps;
const { props, state } = useAsPressable(rest);
const styleProps = useStyling(props);
renderStyle && (props.style = renderStyle(state));
return {
slotProps: mergeSettings<IPressableType['slotProps']>(styleProps, { root: props }),
state: { state },
};
},
});

export default Pressable;
export { Pressable };
31 changes: 31 additions & 0 deletions packages/components/Pressable/src/Pressable.win32.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* This is primarily a fork of React Native's Touchable Mixin.
* It has been repurposed as it's own standalone control for win32,
* as it needs to support a richer set of functionality on the desktop.
* The touchable variants can be rewritten as wrappers around TouchableWin32
* by passing the correct set of props down and managing state correctly.
*
* React Native's Touchable.js file (https://github.com/facebook/react-native/blob/master/Libraries/Components/Touchable/Touchable.js)
* provides an overview over how touchables work and interact with the gesture responder system.
*/
'use strict';

import { IUseStyling, composable } from '@uifabricshared/foundation-composable';
import { View } from 'react-native';
import { IPressableProps, IPressableType } from './Pressable.props';
import { mergeSettings } from '@uifabricshared/foundation-settings';
import { useAsPressable } from '@fluentui-react-native/interactive-hooks';

export const Pressable = composable<IPressableType>({
slots: { root: View },
usePrepareProps: (userProps: IPressableProps, useStyling: IUseStyling<IPressableType>) => {
const { props, state } = useAsPressable(userProps);
const styleProps = useStyling(props);
return {
slotProps: mergeSettings<IPressableType['slotProps']>(styleProps, { root: props }),
state: { state },
};
},
});

export default Pressable;
7 changes: 4 additions & 3 deletions packages/components/RadioGroup/src/RadioButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
'use strict';
import * as React from 'react';
import { View } from 'react-native';
import { Pressable } from '@fluentui-react-native/pressable';
import { Text } from '@fluentui-react-native/text';
import { radioButtonName, IRadioButtonType, IRadioButtonProps, IRadioButtonSlotProps, IRadioButtonRenderData } from './RadioButton.types';
import { compose, IUseComposeStyling } from '@uifabricshared/foundation-compose';
Expand All @@ -10,7 +11,7 @@ import { ISlots, withSlots } from '@uifabricshared/foundation-composable';
import { settings, radioButtonSelectActionLabel } from './RadioButton.settings';
import { mergeSettings } from '@uifabricshared/foundation-settings';
import { foregroundColorTokens, textTokens, borderTokens, backgroundColorTokens, getPaletteFromTheme } from '@fluentui-react-native/tokens';
import { useAsPressable, useOnPressWithFocus, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks';
import { usePressableState, useOnPressWithFocus, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks';
import { RadioGroupContext } from './RadioGroup';

export const RadioButton = compose<IRadioButtonType>({
Expand Down Expand Up @@ -58,7 +59,7 @@ export const RadioButton = compose<IRadioButtonType>({
const changeSelectionWithFocus = useOnPressWithFocus(componentRef, changeSelection);

/* RadioButton changes selection when focus is moved between each RadioButton and on a click */
const pressable = useAsPressable({
const pressable = usePressableState({
...rest,
onPress: changeSelectionWithFocus,
onFocus: changeSelection,
Expand Down Expand Up @@ -118,7 +119,7 @@ export const RadioButton = compose<IRadioButtonType>({

settings,
slots: {
root: View,
root: Pressable,
button: { slotType: View, filter: filterViewProps },
innerCircle: { slotType: View, filter: filterViewProps },
content: Text,
Expand Down
Loading