Skip to content

Commit 1256081

Browse files
authored
Fix inner styling of Pressable (#2982)
### References closes: #2979 mentioned in: #2980 ## Description All styles given to Pressable are applied exclusively to the outer View component. ## Test plan Pull commit [734c429](734c429) to see the issue. Pull commit [36f1a24](36f1a24) to see the same example with this issue resolved. Both commits contain examples reproducing this issue under: `Example app > Release tests > Pressable Gesturization > Flex styling`. In the commit [without a fix applied](734c429), you can see the square red shape used for presentation is misaligned with it's parent, which is caused by inner box-model styles being assigned to the outer `View` container. This behaviour does on occur on the [fixed commit](36f1a24).
1 parent f0dfef2 commit 1256081

File tree

2 files changed

+110
-50
lines changed

2 files changed

+110
-50
lines changed

src/components/Pressable/Pressable.tsx

Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
isTouchWithinInset,
1919
adaptTouchEvent,
2020
addInsets,
21+
splitStyles,
2122
} from './utils';
2223
import { PressabilityDebugView } from '../../handlers/PressabilityDebugView';
2324
import { GestureTouchEvent } from '../../handlers/gestureHandlerCommon';
@@ -35,15 +36,21 @@ export default function Pressable(props: PressableProps) {
3536
const isPressCallbackEnabled = useRef<boolean>(true);
3637
const isPressedDown = useRef<boolean>(false);
3738

38-
const normalizedHitSlop: Insets =
39-
typeof props.hitSlop === 'number'
40-
? numberAsInset(props.hitSlop)
41-
: props.hitSlop ?? {};
39+
const normalizedHitSlop: Insets = useMemo(
40+
() =>
41+
typeof props.hitSlop === 'number'
42+
? numberAsInset(props.hitSlop)
43+
: props.hitSlop ?? {},
44+
[props.hitSlop]
45+
);
4246

43-
const normalizedPressRetentionOffset: Insets =
44-
typeof props.pressRetentionOffset === 'number'
45-
? numberAsInset(props.pressRetentionOffset)
46-
: props.pressRetentionOffset ?? {};
47+
const normalizedPressRetentionOffset: Insets = useMemo(
48+
() =>
49+
typeof props.pressRetentionOffset === 'number'
50+
? numberAsInset(props.pressRetentionOffset)
51+
: props.pressRetentionOffset ?? {},
52+
[props.pressRetentionOffset]
53+
);
4754

4855
const pressGesture = useMemo(
4956
() =>
@@ -53,7 +60,7 @@ export default function Pressable(props: PressableProps) {
5360
isPressCallbackEnabled.current = false;
5461
}
5562
}),
56-
[isPressCallbackEnabled, props.onLongPress, isPressedDown]
63+
[props]
5764
);
5865

5966
const hoverInTimeout = useRef<number | null>(null);
@@ -88,41 +95,48 @@ export default function Pressable(props: PressableProps) {
8895
}
8996
props.onHoverOut?.(adaptStateChangeEvent(event));
9097
}),
91-
[props.onHoverIn, props.onHoverOut, props.delayHoverIn, props.delayHoverOut]
98+
[props]
9299
);
93100

94101
const pressDelayTimeoutRef = useRef<number | null>(null);
95-
const pressInHandler = useCallback((event: GestureTouchEvent) => {
96-
props.onPressIn?.(adaptTouchEvent(event));
97-
isPressCallbackEnabled.current = true;
98-
pressDelayTimeoutRef.current = null;
99-
setPressedState(true);
100-
}, []);
101-
const pressOutHandler = useCallback((event: GestureTouchEvent) => {
102-
if (
103-
!isPressedDown.current ||
104-
event.allTouches.length > event.changedTouches.length
105-
) {
106-
return;
107-
}
108-
109-
if (props.unstable_pressDelay && pressDelayTimeoutRef.current !== null) {
110-
// When delay is preemptively finished by lifting touches,
111-
// we want to immediately activate it's effects - pressInHandler,
112-
// even though we are located at the pressOutHandler
113-
clearTimeout(pressDelayTimeoutRef.current);
114-
pressInHandler(event);
115-
}
116-
117-
props.onPressOut?.(adaptTouchEvent(event));
118-
119-
if (isPressCallbackEnabled.current) {
120-
props.onPress?.(adaptTouchEvent(event));
121-
}
102+
const pressInHandler = useCallback(
103+
(event: GestureTouchEvent) => {
104+
props.onPressIn?.(adaptTouchEvent(event));
105+
isPressCallbackEnabled.current = true;
106+
pressDelayTimeoutRef.current = null;
107+
setPressedState(true);
108+
},
109+
[props]
110+
);
122111

123-
isPressedDown.current = false;
124-
setPressedState(false);
125-
}, []);
112+
const pressOutHandler = useCallback(
113+
(event: GestureTouchEvent) => {
114+
if (
115+
!isPressedDown.current ||
116+
event.allTouches.length > event.changedTouches.length
117+
) {
118+
return;
119+
}
120+
121+
if (props.unstable_pressDelay && pressDelayTimeoutRef.current !== null) {
122+
// When delay is preemptively finished by lifting touches,
123+
// we want to immediately activate it's effects - pressInHandler,
124+
// even though we are located at the pressOutHandler
125+
clearTimeout(pressDelayTimeoutRef.current);
126+
pressInHandler(event);
127+
}
128+
129+
props.onPressOut?.(adaptTouchEvent(event));
130+
131+
if (isPressCallbackEnabled.current) {
132+
props.onPress?.(adaptTouchEvent(event));
133+
}
134+
135+
isPressedDown.current = false;
136+
setPressedState(false);
137+
},
138+
[pressInHandler, props]
139+
);
126140

127141
const handlingOnTouchesDown = useRef<boolean>(false);
128142
const onEndHandlingTouchesDown = useRef<(() => void) | null>(null);
@@ -192,14 +206,10 @@ export default function Pressable(props: PressableProps) {
192206
pressOutHandler(event);
193207
}),
194208
[
195-
props.onPress,
196-
props.onPressIn,
197-
props.onPressOut,
198-
setPressedState,
199-
isPressedDown,
200-
isPressCallbackEnabled,
201209
normalizedHitSlop,
202-
pressDelayTimeoutRef,
210+
props.unstable_pressDelay,
211+
pressInHandler,
212+
pressOutHandler,
203213
]
204214
);
205215

@@ -256,8 +266,12 @@ export default function Pressable(props: PressableProps) {
256266
? props.children({ pressed: pressedState })
257267
: props.children;
258268

269+
const flattenedStyles = StyleSheet.flatten(styleProp ?? {});
270+
271+
const [innerStyles, outerStyles] = splitStyles(flattenedStyles);
272+
259273
return (
260-
<View style={styleProp}>
274+
<View style={outerStyles}>
261275
<GestureDetector gesture={gesture}>
262276
<NativeButton
263277
ref={pressableRef}
@@ -269,7 +283,7 @@ export default function Pressable(props: PressableProps) {
269283
props.android_ripple?.color ?? defaultRippleColor
270284
)}
271285
rippleRadius={props.android_ripple?.radius ?? undefined}
272-
style={[StyleSheet.absoluteFill, pointerStyle]}>
286+
style={[StyleSheet.absoluteFill, pointerStyle, innerStyles]}>
273287
{childrenProp}
274288
{__DEV__ ? (
275289
<PressabilityDebugView color="red" hitSlop={normalizedHitSlop} />

src/components/Pressable/utils.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Insets } from 'react-native';
1+
import { Insets, ViewStyle } from 'react-native';
22
import { LongPressGestureHandlerEventPayload } from '../../handlers/GestureHandlerEventPayload';
33
import {
44
TouchData,
@@ -119,6 +119,51 @@ const adaptTouchEvent = (event: GestureTouchEvent): PressableEvent => {
119119
};
120120
};
121121

122+
type StylePropKeys = (keyof ViewStyle)[];
123+
124+
// Source:
125+
// - From ViewStyle extracted FlexStyle sub-interface which contains all of the box-model manipulating props.
126+
// - From FlexStyle handpicked those styles, which act on the inner part of the box-model.
127+
const innerStyleKeys = new Set([
128+
'alignContent',
129+
'alignItems',
130+
'flexBasis',
131+
'flexDirection',
132+
'flexWrap',
133+
'rowGap',
134+
'gap',
135+
'columnGap',
136+
'justifyContent',
137+
'overflow',
138+
'padding',
139+
'paddingBottom',
140+
'paddingEnd',
141+
'paddingHorizontal',
142+
'paddingLeft',
143+
'paddingRight',
144+
'paddingStart',
145+
'paddingTop',
146+
'paddingVertical',
147+
'start',
148+
'end',
149+
'direction', // iOS only
150+
] as StylePropKeys);
151+
152+
const splitStyles = (from: ViewStyle): [ViewStyle, ViewStyle] => {
153+
const outerStyles: Record<string, unknown> = {};
154+
const innerStyles: Record<string, unknown> = {};
155+
156+
for (const key in from) {
157+
if (innerStyleKeys.has(key as keyof ViewStyle)) {
158+
innerStyles[key] = from[key as keyof ViewStyle];
159+
} else {
160+
outerStyles[key] = from[key as keyof ViewStyle];
161+
}
162+
}
163+
164+
return [innerStyles, outerStyles];
165+
};
166+
122167
export {
123168
numberAsInset,
124169
addInsets,
@@ -127,4 +172,5 @@ export {
127172
isTouchWithinInset,
128173
adaptStateChangeEvent,
129174
adaptTouchEvent,
175+
splitStyles,
130176
};

0 commit comments

Comments
 (0)