From ef14fca91bbc2300750ae039635a6cf5f3009c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Wed, 26 Aug 2026 15:47:40 +0200 Subject: [PATCH 1/9] fix: expand interactive targets to the 48dp minimum --- src/components/Checkbox/Checkbox.tsx | 7 +- src/components/Chip/Chip.tsx | 43 +++- src/components/IconButton/IconButton.tsx | 26 ++- .../TouchableRipple.native.tsx | 119 ++++++++++ .../TouchableRipple/TouchableRipple.tsx | 106 ++++++++- .../__snapshots__/Checkbox.test.tsx.snap | 7 + .../__snapshots__/CheckboxItem.test.tsx.snap | 4 + src/components/__tests__/Chip.test.tsx | 38 ++++ .../__snapshots__/RadioButton.test.tsx.snap | 4 + .../RadioButtonGroup.test.tsx.snap | 1 + .../RadioButtonItem.test.tsx.snap | 8 + .../__tests__/TouchableRipple.test.tsx | 212 +++++++++++++++++- .../__tests__/TouchableRippleWeb.test.tsx | 134 +++++++++++ .../__snapshots__/DrawerItem.test.tsx.snap | 3 + .../__tests__/__snapshots__/FAB.test.tsx.snap | 13 ++ .../__snapshots__/FABExtended.test.tsx.snap | 6 + .../__snapshots__/FABMenu.test.tsx.snap | 25 +++ .../__snapshots__/ListAccordion.test.tsx.snap | 6 + .../__snapshots__/ListSection.test.tsx.snap | 6 + .../__snapshots__/MenuItem.test.tsx.snap | 5 + .../SegmentedButton.test.tsx.snap | 2 + src/theme/tokens/sys/state.ts | 7 + 22 files changed, 756 insertions(+), 26 deletions(-) create mode 100644 src/components/__tests__/TouchableRippleWeb.test.tsx diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index 3bccccf33b..fe73752eb7 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -78,9 +78,10 @@ const { const FOCUS_THICKNESS = tokens.md.sys.state.focusIndicator.thickness; // Focus indicator is a circular ring at the 40dp state-layer boundary. -// We don't apply `focusIndicator.outerOffset` here because the surrounding -// `TouchableRipple borderless` clips overflow to the tap-target shape, -// so a ring drawn outside the 40dp circle would be cropped. +// We don't apply `focusIndicator.outerOffset`, so the ring stays inside the 40dp +// circle. `TouchableRipple borderless` used to crop anything outside it; on web +// it no longer does, since the touchable cannot clip without clipping the touch +// target. Native still clips. Check both when revisiting the offset. const FOCUS_RING_SIZE = STATE_LAYER_SIZE; const FOCUS_RING_RADIUS = STATE_LAYER_SIZE / 2; diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx index 5a015fcf8d..f7f12aa33d 100644 --- a/src/components/Chip/Chip.tsx +++ b/src/components/Chip/Chip.tsx @@ -179,6 +179,25 @@ export type Props = Omit & { * export default MyComponent; * ``` */ +/** + * Room the chip reserves on its right for the close button, which fills all of + * it, so the body stops here and the two divide the chip. + * + * MD3 splits the same way and does not give a chip's trailing action 48dp; in + * material-web it is 24x24 with no expansion. This column is wider than that and + * gets no vertical expansion, so the strips above and below belong to the body + * and a near miss activates the chip rather than deleting it. + * @see https://github.com/material-components/material-web/blob/main/chips/internal/_trailing-icon.scss + */ +const CLOSE_AFFORDANCE_WIDTH = 34; + +/** + * Floor for the clamp below. The glyph is 18dp and sits 8dp from the right, so + * under this it hangs over the chip body, and part of the visible icon would + * activate the chip instead of removing it. + */ +const CLOSE_AFFORDANCE_MIN_WIDTH = 26; + const Chip = ({ mode = 'flat', children, @@ -270,7 +289,7 @@ const Chip = ({ }; const contentSpacings = { - paddingRight: onClose ? 34 : 0, + paddingRight: onClose ? CLOSE_AFFORDANCE_WIDTH : 0, }; const labelTextStyle = { @@ -391,8 +410,12 @@ const Chip = ({ role="button" aria-label={closeIconAccessibilityLabel} testID={closeIconTestID} + style={styles.closeButton} > - + {closeIcon ? ( ) : ( @@ -443,6 +466,10 @@ const styles = StyleSheet.create({ md3CloseIcon: { marginRight: 8, padding: 0, + // `styles.icon` sets `alignSelf: 'center'`, which beats `alignItems` on the + // parent. Without this the glyph centres in the wider column and moves 4dp + // left. + alignSelf: 'flex-end', }, md3LabelText: { textAlignVertical: 'center', @@ -473,9 +500,19 @@ const styles = StyleSheet.create({ closeButtonStyle: { position: 'absolute', right: 0, + width: CLOSE_AFFORDANCE_WIDTH, + // A chip narrower than this column would hand the whole thing to the close + // button. Never more than half, never less than the glyph needs; minWidth + // wins over maxWidth. + minWidth: CLOSE_AFFORDANCE_MIN_WIDTH, + maxWidth: '50%', + height: '100%', + }, + closeButton: { + width: '100%', height: '100%', + // Vertical only. The glyph pins itself horizontally with `alignSelf`. justifyContent: 'center', - alignItems: 'center', }, touchable: { width: '100%', diff --git a/src/components/IconButton/IconButton.tsx b/src/components/IconButton/IconButton.tsx index 1d673a3d27..0e3b0bb3c2 100644 --- a/src/components/IconButton/IconButton.tsx +++ b/src/components/IconButton/IconButton.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { StyleSheet, View } from 'react-native'; +import { Platform, StyleSheet, View } from 'react-native'; import type { ColorValue, GestureResponderEvent, @@ -177,7 +177,7 @@ const IconButton = ({ pointerEvents="none" style={[ StyleSheet.absoluteFill, - { backgroundColor, opacity: backgroundOpacity }, + { backgroundColor, opacity: backgroundOpacity, borderRadius }, ]} /> )} @@ -186,15 +186,18 @@ const IconButton = ({ centered onPress={onPress} aria-label={ariaLabel} - style={[styles.touchable, contentStyle]} + style={[ + styles.touchable, + { borderRadius }, + // The Surface used to clip the ripple, so the touchable does it now. + // Native only: its own overflow does not clip its hitSlop, but on web + // it would clip the touch target, where the container already clips. + Platform.OS !== 'web' && styles.clipToShape, + contentStyle, + ]} role="button" aria-disabled={disabled} disabled={disabled} - hitSlop={ - TouchableRipple.supported - ? { top: 10, left: 10, bottom: 10, right: 10 } - : { top: 6, left: 6, bottom: 6, right: 6 } - } testID={testID} {...rest} > @@ -212,14 +215,19 @@ const IconButton = ({ const styles = StyleSheet.create({ container: { + // No `overflow: 'hidden'`. An ancestor that clips also clips the touch + // target, which is why the hitSlop this component used to pass never + // applied. The overlay and the touchable clip themselves instead. margin: 6, - overflow: 'hidden', }, touchable: { flexGrow: 1, justifyContent: 'center', alignItems: 'center', }, + clipToShape: { + overflow: 'hidden', + }, }); export default IconButton; diff --git a/src/components/TouchableRipple/TouchableRipple.native.tsx b/src/components/TouchableRipple/TouchableRipple.native.tsx index ec7b13dd91..c1a214e201 100644 --- a/src/components/TouchableRipple/TouchableRipple.native.tsx +++ b/src/components/TouchableRipple/TouchableRipple.native.tsx @@ -6,6 +6,8 @@ import type { ViewStyle, GestureResponderEvent, ColorValue, + Insets, + LayoutChangeEvent, } from 'react-native'; import type { PressableProps } from './Pressable'; @@ -14,12 +16,81 @@ import { getTouchableRippleColors } from './utils'; import { SettingsContext } from '../../core/settings'; import type { Settings } from '../../core/settings'; import { useInternalTheme } from '../../core/theming'; +import { tokens } from '../../theme/tokens'; import type { ThemeProp } from '../../theme/types'; import hasTouchHandler from '../../utils/hasTouchHandler'; const ANDROID_VERSION_LOLLIPOP = 21; const ANDROID_VERSION_PIE = 28; +const { minInteractiveSize } = tokens.md.sys.state; + +/** + * The underlay fills the touchable absolutely and has no radius of its own, so + * it paints square corners over a rounded one. A clipping ancestor used to hide + * that, and those ancestors have to stop clipping for the expansion to work. + */ +const getUnderlayShape = (style: StyleProp): ViewStyle => { + const flat = StyleSheet.flatten(style); + + if (!flat) { + return {}; + } + + const { + borderRadius, + borderTopLeftRadius, + borderTopRightRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + borderTopStartRadius, + borderTopEndRadius, + borderBottomStartRadius, + borderBottomEndRadius, + } = flat; + + return { + borderRadius, + borderTopLeftRadius, + borderTopRightRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + borderTopStartRadius, + borderTopEndRadius, + borderBottomStartRadius, + borderBottomEndRadius, + }; +}; + +/** + * Slop needed to bring a rendered size up to `minInteractiveSize`. Expands + * outside the bounds rather than resizing, so a 40dp state layer keeps its 40dp + * and gains 4dp per side. Returns undefined when the size is already enough, so + * that case does not re-render. + * @see https://developer.android.com/develop/ui/compose/accessibility/api-defaults + */ +const getExpansion = (width: number, height: number): Insets | undefined => { + // A collapsed touchable would otherwise claim 24dp of slop around a point + // where nothing is drawn. + if (width === 0 || height === 0) { + return undefined; + } + + const horizontal = Math.max(0, (minInteractiveSize - width) / 2); + const vertical = Math.max(0, (minInteractiveSize - height) / 2); + + if (horizontal === 0 && vertical === 0) { + return undefined; + } + + return { + top: vertical, + bottom: vertical, + left: horizontal, + right: horizontal, + }; +}; + export type Props = PressableProps & { borderless?: boolean; background?: PressableAndroidRippleConfig; @@ -46,6 +117,8 @@ const TouchableRipple = ({ underlayColor, children, theme: themeOverrides, + hitSlop, + onLayout, ref, ...rest }: Props) => { @@ -63,6 +136,47 @@ const TouchableRipple = ({ const disabled = disabledProp || !hasPassedTouchHandler; + const [expansion, setExpansion] = React.useState( + undefined + ); + + // A caller hitSlop wins, so there is nothing to measure for. `null` counts as + // supplied, it means "no slop". + const shouldMeasure = hitSlop === undefined; + + // Gates whether the measurement is applied, not whether it happens. RN emits + // onLayout on mount and on layout change, so a touchable that mounts disabled + // gets no event once it is enabled and would stay small. + const shouldExpand = shouldMeasure && !disabled; + + const handleLayout = React.useCallback( + (event: LayoutChangeEvent) => { + onLayout?.(event); + + const { width, height } = event.nativeEvent.layout; + const next = getExpansion(width, height); + + setExpansion((current) => { + // Nothing changed, so a big enough touchable does not re-render. + if (current === next) { + return current; + } + if ( + current && + next && + current.top === next.top && + current.bottom === next.bottom && + current.left === next.left && + current.right === next.right + ) { + return current; + } + return next; + }); + }, + [onLayout] + ); + const { calculatedRippleColor, calculatedUnderlayColor } = getTouchableRippleColors({ theme, @@ -92,6 +206,8 @@ const TouchableRipple = ({ {...rest} ref={ref} disabled={disabled} + hitSlop={shouldExpand ? expansion : hitSlop} + onLayout={shouldMeasure ? handleLayout : onLayout} style={[useForeground && styles.overflowHidden, style]} android_ripple={androidRipple} > @@ -105,6 +221,8 @@ const TouchableRipple = ({ {...rest} ref={ref} disabled={disabled} + hitSlop={shouldExpand ? expansion : hitSlop} + onLayout={shouldMeasure ? handleLayout : onLayout} style={[borderless && styles.overflowHidden, style]} > {({ pressed }) => ( @@ -113,6 +231,7 @@ const TouchableRipple = ({ diff --git a/src/components/TouchableRipple/TouchableRipple.tsx b/src/components/TouchableRipple/TouchableRipple.tsx index 128e2c017e..ada76bd255 100644 --- a/src/components/TouchableRipple/TouchableRipple.tsx +++ b/src/components/TouchableRipple/TouchableRipple.tsx @@ -15,12 +15,58 @@ import { getTouchableRippleColors } from './utils'; import { SettingsContext } from '../../core/settings'; import type { Settings } from '../../core/settings'; import { useInternalTheme } from '../../core/theming'; +import { tokens } from '../../theme/tokens'; import type { ThemeProp } from '../../theme/types'; import hasTouchHandler from '../../utils/hasTouchHandler'; +const { minInteractiveSize } = tokens.md.sys.state; + +/** + * react-native-web removed `hitSlop` in 0.13.0, so web needs a real element the + * browser can hit-test instead. An absolutely positioned box at least the + * minimum target size, which is what material-web does, and it costs no layout. + * @see https://github.com/necolas/react-native-web/releases/tag/0.13.0 + * @see https://github.com/material-components/material-web/blob/main/iconbutton/internal/_shared.scss + */ +const getTouchTargetStyle = (hitSlop: PressableProps['hitSlop']): ViewStyle => { + // `undefined` means the caller said nothing, so the minimum applies. `null` + // means "no slop", same as native. + if (hitSlop === undefined) { + return styles.touchTarget; + } + if (hitSlop === null) { + return styles.noTouchTarget; + } + + // A caller hitSlop wins here too, so web matches native instead of ignoring + // the prop. + const inset = (value: number | undefined) => -(value ?? 0); + + return typeof hitSlop === 'number' + ? { + position: 'absolute', + top: inset(hitSlop), + bottom: inset(hitSlop), + left: inset(hitSlop), + right: inset(hitSlop), + } + : { + position: 'absolute', + top: inset(hitSlop.top), + bottom: inset(hitSlop.bottom), + left: inset(hitSlop.left), + right: inset(hitSlop.right), + }; +}; + export type Props = PressableProps & { /** * Whether to render the ripple outside the view bounds. + * + * On web the ripple is bounded by its own container, so this no longer clips + * the touchable's content. The touchable cannot clip without clipping the + * touch target, so children needing a rounded shape carry the radius + * themselves. */ borderless?: boolean; /** @@ -105,12 +151,14 @@ export type Props = PressableProps & { const TouchableRipple = ({ style, background: _background, - borderless = false, + // consumed so it does not reach the DOM; the ripple container clips regardless + borderless: _borderless = false, disabled: disabledProp, rippleColor, underlayColor: _underlayColor, children, theme: themeOverrides, + hitSlop, ref, ...rest }: Props) => { @@ -178,7 +226,16 @@ const TouchableRipple = ({ borderTopRightRadius: style.borderTopRightRadius, borderBottomRightRadius: style.borderBottomRightRadius, borderBottomLeftRadius: style.borderBottomLeftRadius, - overflow: centered ? 'visible' : 'hidden', + // The touchable cannot clip, it would clip the touch target too, so + // the ripple is contained here. This container is inset to the + // touchable and copies its radii, so it clips to the same shape. + // + // Always, not `centered ? 'visible' : 'hidden'` as before. A ripple + // that escaped used to be caught by whichever ancestor clipped, and + // those ancestors have to stop. ToggleButton hit this: it passes + // `borderless={false}` to IconButton, which spreads it over its own, + // so the Surface was holding the ripple in. + overflow: 'hidden', }); // Create span to show the ripple effect @@ -282,7 +339,6 @@ const TouchableRipple = ({ disabled={disabled} style={(state) => [ styles.touchable, - borderless && styles.borderless, // focused state is not ready yet: https://github.com/necolas/react-native-web/issues/1849 // state.focused && { backgroundColor: ___ }, state.hovered && { backgroundColor: hoverColor }, @@ -290,11 +346,26 @@ const TouchableRipple = ({ typeof style === 'function' ? style(state) : style, ]} > - {(state) => - React.Children.only( - typeof children === 'function' ? children(state) : children - ) - } + {(state) => ( + <> + {/* Before the children, not after. It hit-tests, so as the last + sibling it covers anything interactive inside the touchable and + takes its presses, e.g. a pressable List.Item with a control in + `right`. Ahead of them it still covers the area outside the + touchable, where there is nothing else to hit. + Nothing that cannot be pressed gets a target, same as native. */} + {!disabled && ( + + )} + {React.Children.only( + typeof children === 'function' ? children(state) : children + )} + + )} ); }; @@ -317,8 +388,23 @@ const styles = StyleSheet.create({ cursor: 'auto', }), }, - borderless: { - overflow: 'hidden', + noTouchTarget: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + touchTarget: { + position: 'absolute', + top: '50%', + left: '50%', + // max(minInteractiveSize, 100%), same as MD3 web's .touch + width: '100%', + height: '100%', + minWidth: minInteractiveSize, + minHeight: minInteractiveSize, + transform: [{ translateX: '-50%' }, { translateY: '-50%' }], }, }); diff --git a/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap b/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap index 202a95467a..0cd9bcc43f 100644 --- a/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap +++ b/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap @@ -27,6 +27,7 @@ exports[`renders Checkbox with custom testID 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -212,6 +213,7 @@ exports[`renders checked Checkbox with color 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -396,6 +398,7 @@ exports[`renders checked Checkbox with onPress 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -580,6 +583,7 @@ exports[`renders indeterminate Checkbox 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -751,6 +755,7 @@ exports[`renders indeterminate Checkbox with color 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -922,6 +927,7 @@ exports[`renders unchecked Checkbox with color 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1106,6 +1112,7 @@ exports[`renders unchecked Checkbox with onPress 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap b/src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap index e2d338f791..769d9df19a 100644 --- a/src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap +++ b/src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap @@ -26,6 +26,7 @@ exports[`can render leading checkbox control 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -81,6 +82,7 @@ exports[`can render leading checkbox control 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -302,6 +304,7 @@ exports[`renders unchecked 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -394,6 +397,7 @@ exports[`renders unchecked 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx index a7d522e4f0..0774ad6109 100644 --- a/src/components/__tests__/Chip.test.tsx +++ b/src/components/__tests__/Chip.test.tsx @@ -309,3 +309,41 @@ describe('getChipColor - border color', () => { }); }); }); + +describe('close affordance', () => { + // The chip already reserved room on its right, but only the icon was tappable, + // so the body owned the rest of that column. MD3 has the primary action stop + // where the trailing one starts. + it('fills the column the chip reserves for it', async () => { + await render( + {}} onClose={() => {}}> + Example + + ); + + expect(screen.getByLabelText('Close')).toHaveStyle({ + width: '100%', + height: '100%', + }); + }); + + it('keeps the close glyph pinned right so it does not drift', async () => { + await render( + {}} onClose={() => {}}> + Example + + ); + + // `styles.icon` sets alignSelf center, which would otherwise win and move + // the glyph 4dp left + expect(screen.getByTestId('chip-close-icon')).toHaveStyle({ + alignSelf: 'flex-end', + }); + }); + + it('is not rendered without onClose', async () => { + await render( {}}>Example); + + expect(screen.queryByLabelText('Close')).not.toBeOnTheScreen(); + }); +}); diff --git a/src/components/__tests__/RadioButton/__snapshots__/RadioButton.test.tsx.snap b/src/components/__tests__/RadioButton/__snapshots__/RadioButton.test.tsx.snap index c20910f20e..31dffb6970 100644 --- a/src/components/__tests__/RadioButton/__snapshots__/RadioButton.test.tsx.snap +++ b/src/components/__tests__/RadioButton/__snapshots__/RadioButton.test.tsx.snap @@ -26,6 +26,7 @@ exports[`RadioButton RadioButton with custom testID renders properly 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -113,6 +114,7 @@ exports[`RadioButton on default platform renders properly 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -199,6 +201,7 @@ exports[`RadioButton on ios platform renders properly 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -285,6 +288,7 @@ exports[`RadioButton when RadioButton is wrapped by RadioButtonContext.Provider onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/RadioButton/__snapshots__/RadioButtonGroup.test.tsx.snap b/src/components/__tests__/RadioButton/__snapshots__/RadioButtonGroup.test.tsx.snap index 1ae7f560be..54a237c099 100644 --- a/src/components/__tests__/RadioButton/__snapshots__/RadioButtonGroup.test.tsx.snap +++ b/src/components/__tests__/RadioButton/__snapshots__/RadioButtonGroup.test.tsx.snap @@ -29,6 +29,7 @@ exports[`RadioButtonGroup renders properly 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/RadioButton/__snapshots__/RadioButtonItem.test.tsx.snap b/src/components/__tests__/RadioButton/__snapshots__/RadioButtonItem.test.tsx.snap index 5867b1408c..ef32fb657b 100644 --- a/src/components/__tests__/RadioButton/__snapshots__/RadioButtonItem.test.tsx.snap +++ b/src/components/__tests__/RadioButton/__snapshots__/RadioButtonItem.test.tsx.snap @@ -26,6 +26,7 @@ exports[`can render leading radio button control 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -80,6 +81,7 @@ exports[`can render leading radio button control 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -204,6 +206,7 @@ exports[`can render the Android radio button on different platforms 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -294,6 +297,7 @@ exports[`can render the Android radio button on different platforms 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -356,6 +360,7 @@ exports[`can render the iOS radio button on different platforms 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -446,6 +451,7 @@ exports[`can render the iOS radio button on different platforms 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -534,6 +540,7 @@ exports[`renders unchecked 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -624,6 +631,7 @@ exports[`renders unchecked 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/TouchableRipple.test.tsx b/src/components/__tests__/TouchableRipple.test.tsx index 663d540bec..d70418a5f8 100644 --- a/src/components/__tests__/TouchableRipple.test.tsx +++ b/src/components/__tests__/TouchableRipple.test.tsx @@ -1,8 +1,9 @@ +import * as React from 'react'; import { Platform, Text } from 'react-native'; import type { GestureResponderEvent } from 'react-native'; import { describe, expect, it, jest } from '@jest/globals'; -import { userEvent } from '@testing-library/react-native'; +import { act, fireEvent, userEvent } from '@testing-library/react-native'; import { render, screen } from '../../test-utils'; import TouchableRipple from '../TouchableRipple/TouchableRipple.native'; @@ -66,5 +67,214 @@ describe('TouchableRipple', () => { expect(toJSON()).toMatchSnapshot(); }); + + it('takes the shape of the touchable so it does not square off the corners', async () => { + await render( + + Press me! + + ); + + expect(screen.getByTestId('touchable-ripple-underlay')).toHaveStyle({ + borderRadius: 4, + }); + }); + + it('takes per-corner radii too', async () => { + await render( + + Press me! + + ); + + expect(screen.getByTestId('touchable-ripple-underlay')).toHaveStyle({ + borderTopLeftRadius: 8, + borderBottomRightRadius: 2, + }); + }); + }); + + describe('minimum interactive size', () => { + const layout = (width: number, height: number) => ({ + nativeEvent: { layout: { width, height, x: 0, y: 0 } }, + }); + + // hitSlop has no user-visible effect here, the renderer does not lay views + // out or hit-test them. Real behaviour is checked on device; this only stops + // the props being dropped. + /* eslint-disable no-restricted-syntax */ + const hitSlopOf = () => screen.getByTestId('touchable').props.hitSlop; + const onLayoutOf = () => screen.getByTestId('touchable').props.onLayout; + /* eslint-enable no-restricted-syntax */ + + const renderTouchable = async (props = {}) => { + await render( + {}} {...props}> + Button + + ); + return screen.getByTestId('touchable'); + }; + + const fireLayout = async (width: number, height: number) => { + await act(async () => { + await fireEvent( + screen.getByTestId('touchable'), + 'layout', + layout(width, height) + ); + }); + }; + + it('expands a small target out to the minimum interactive size', async () => { + await renderTouchable(); + expect(hitSlopOf()).toBeUndefined(); + + await fireLayout(32, 32); + + // (48 - 32) / 2 on every side + expect(hitSlopOf()).toEqual({ top: 8, bottom: 8, left: 8, right: 8 }); + }); + + it('expands each axis independently', async () => { + await renderTouchable(); + + await fireLayout(40, 100); + + expect(hitSlopOf()).toEqual({ top: 0, bottom: 0, left: 4, right: 4 }); + }); + + it('leaves a target that is already big enough alone', async () => { + await renderTouchable(); + + await fireLayout(48, 48); + + expect(hitSlopOf()).toBeUndefined(); + }); + + it('lets a caller-supplied hitSlop win', async () => { + await renderTouchable({ hitSlop: 2 }); + + await fireLayout(32, 32); + + expect(hitSlopOf()).toBe(2); + }); + + it('does not expand a touchable with no touch handlers', async () => { + await render( + + Not a control + + ); + + expect(hitSlopOf()).toBeUndefined(); + }); + + // Measuring and applying are separate. RN emits onLayout on mount and on + // layout change, so measuring only once interactive would mean no event ever + // arrives and the target stays small. + it('measures even while it cannot be pressed', async () => { + await renderTouchable({ disabled: true }); + + expect(onLayoutOf()).toEqual(expect.any(Function)); + expect(hitSlopOf()).toBeUndefined(); + }); + + it('does not expand a disabled touchable', async () => { + await renderTouchable({ disabled: true }); + + await fireLayout(32, 32); + + expect(hitSlopOf()).toBeUndefined(); + }); + + it('keeps the measurement across losing and regaining interactivity', async () => { + const Harness = ({ disabled }: { disabled: boolean }) => ( + {}} + > + Button + + ); + const view = await render(); + const expanded = { top: 8, bottom: 8, left: 8, right: 8 }; + + await fireLayout(32, 32); + expect(hitSlopOf()).toEqual(expanded); + + await act(async () => { + await view.rerender(); + }); + expect(hitSlopOf()).toBeUndefined(); + + // back again, with no second layout event to rely on + await act(async () => { + await view.rerender(); + }); + expect(hitSlopOf()).toEqual(expanded); + }); + + it('still calls a caller-supplied onLayout', async () => { + const onLayout = jest.fn(); + await renderTouchable({ onLayout }); + + await fireLayout(32, 32); + + expect(onLayout).toHaveBeenCalledTimes(1); + }); + + describe('render cost', () => { + // TouchableRipple renders everywhere, so the cost of measuring is worth + // pinning down. + const withProfiler = async () => { + const commits: string[] = []; + await render( + commits.push(phase)} + > + {}}> + Button + + + ); + return commits; + }; + + it('costs no extra render when the target is already big enough', async () => { + const commits = await withProfiler(); + expect(commits).toEqual(['mount']); + + await fireLayout(56, 56); + + // the updater returned the identical value, so React bails out + expect(commits).toEqual(['mount']); + }); + + it('costs one extra render when the target is too small', async () => { + const commits = await withProfiler(); + + await fireLayout(32, 32); + + expect(commits).toEqual(['mount', 'update']); + }); + + it('settles after a repeated layout at the same size', async () => { + const commits = await withProfiler(); + + await fireLayout(32, 32); + await fireLayout(32, 32); + await fireLayout(32, 32); + + // React renders once more before it can bail out on an unchanged value, + // then stops. Three more layout events, one more render. + expect(commits).toEqual(['mount', 'update', 'update']); + }); + }); }); }); diff --git a/src/components/__tests__/TouchableRippleWeb.test.tsx b/src/components/__tests__/TouchableRippleWeb.test.tsx new file mode 100644 index 0000000000..ccb07f18eb --- /dev/null +++ b/src/components/__tests__/TouchableRippleWeb.test.tsx @@ -0,0 +1,134 @@ +import { Text } from 'react-native'; + +import { describe, expect, it } from '@jest/globals'; + +import { render, screen } from '../../test-utils'; +import type TouchableRippleType from '../TouchableRipple/TouchableRipple'; + +// The web variant, required with its extension on purpose. A bare specifier +// resolves to `TouchableRipple.native.tsx` under the jest preset, so importing +// it the normal way silently tests the native file and none of this runs. +// +// The preset sets `Platform.OS` to 'ios' and there is no DOM, so this renders the +// web source on the native renderer. It pins props and element order, nothing +// more. Hit testing, stacking order, computed styles and clipping ancestors have +// to be checked in a browser. Pressing here would throw, `handlePressIn` reaches +// for `window`. +const TouchableRipple: typeof TouchableRippleType = + require('../TouchableRipple/TouchableRipple.tsx').default; + +const TARGET = 'touchable-ripple-touch-target'; + +// The target is `aria-hidden`, the button already carries the semantics. Testing +// library skips hidden elements, so queries have to opt in or they find nothing +// and the negative cases pass for free. +const HIDDEN = { includeHiddenElements: true } as const; + +describe('TouchableRipple (web)', () => { + // The target is invisible by design, so there is no user-visible assertion to + // make about it. Its style is the behaviour. + const styleOf = (testID: string) => { + // eslint-disable-next-line no-restricted-syntax + const { style } = screen.getByTestId(testID, HIDDEN).props; + return Array.isArray(style) ? Object.assign({}, ...style.flat()) : style; + }; + + it('renders a minimum sized touch target for an interactive touchable', async () => { + await render( + {}}> + Button + + ); + + expect(screen.getByTestId(TARGET, HIDDEN)).toBeOnTheScreen(); + expect(styleOf(TARGET)).toMatchObject({ + position: 'absolute', + minWidth: 48, + minHeight: 48, + width: '100%', + height: '100%', + }); + }); + + it('renders the touch target before the children so it cannot cover them', async () => { + // It hit-tests, so as the last sibling it covers anything interactive inside + // the touchable, e.g. a pressable List.Item with a control in `right`. + await render( + {}}> + child-marker + + ); + + const tree = JSON.stringify(screen.toJSON()); + + expect(tree.indexOf(TARGET)).toBeGreaterThan(-1); + expect(tree.indexOf(TARGET)).toBeLessThan(tree.indexOf('child-marker')); + }); + + it('does not render a touch target when there are no touch handlers', async () => { + await render( + + Not a control + + ); + + expect(screen.queryByTestId(TARGET, HIDDEN)).not.toBeOnTheScreen(); + }); + + it('does not render a touch target when disabled', async () => { + await render( + {}}> + Button + + ); + + expect(screen.queryByTestId(TARGET, HIDDEN)).not.toBeOnTheScreen(); + }); + + it('lets a caller-supplied hitSlop size the target instead', async () => { + await render( + {}}> + Button + + ); + + expect(styleOf(TARGET)).toEqual({ + position: 'absolute', + top: -6, + bottom: -6, + left: -6, + right: -6, + }); + }); + + it('accepts a per-edge hitSlop', async () => { + await render( + {}}> + Button + + ); + + expect(styleOf(TARGET)).toEqual({ + position: 'absolute', + top: -4, + bottom: -0, + left: -8, + right: -0, + }); + }); + + it('no longer clips the touchable itself, which would clip the target', async () => { + await render( + {}} testID="touchable"> + Button + + ); + + const style = styleOf('touchable'); + + // check we have the touchable's own style first, or the absence below passes + // against any empty object + expect(style).toMatchObject({ position: 'relative' }); + expect(style.overflow).toBeUndefined(); + }); +}); diff --git a/src/components/__tests__/__snapshots__/DrawerItem.test.tsx.snap b/src/components/__tests__/__snapshots__/DrawerItem.test.tsx.snap index 4823f954c1..980dd7897b 100644 --- a/src/components/__tests__/__snapshots__/DrawerItem.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DrawerItem.test.tsx.snap @@ -26,6 +26,7 @@ exports[`renders DrawerItem with icon 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -185,6 +186,7 @@ exports[`renders active DrawerItem 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -344,6 +346,7 @@ exports[`renders basic DrawerItem 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/FAB.test.tsx.snap b/src/components/__tests__/__snapshots__/FAB.test.tsx.snap index 4b06302358..a3cee84a46 100644 --- a/src/components/__tests__/__snapshots__/FAB.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/FAB.test.tsx.snap @@ -142,6 +142,7 @@ exports[`renders FAB large size 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -381,6 +382,7 @@ exports[`renders FAB medium size 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -620,6 +622,7 @@ exports[`renders FAB transitioning to not visible 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -859,6 +862,7 @@ exports[`renders FAB transitioning to visible 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1099,6 +1103,7 @@ exports[`renders FAB with aria-label 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1338,6 +1343,7 @@ exports[`renders FAB with containerColor and contentColor overrides 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1577,6 +1583,7 @@ exports[`renders FAB with containerColor override 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1816,6 +1823,7 @@ exports[`renders FAB with default props 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2055,6 +2063,7 @@ exports[`renders FAB with primary variant 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2294,6 +2303,7 @@ exports[`renders FAB with secondary variant 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2533,6 +2543,7 @@ exports[`renders FAB with tertiary variant 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2772,6 +2783,7 @@ exports[`renders FAB with tonalSecondary variant 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3011,6 +3023,7 @@ exports[`renders FAB with tonalTertiary variant 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/FABExtended.test.tsx.snap b/src/components/__tests__/__snapshots__/FABExtended.test.tsx.snap index 644a640c5a..f871c16538 100644 --- a/src/components/__tests__/__snapshots__/FABExtended.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/FABExtended.test.tsx.snap @@ -144,6 +144,7 @@ exports[`renders extended FAB collapsed 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -470,6 +471,7 @@ exports[`renders extended FAB expanded 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -796,6 +798,7 @@ exports[`renders extended FAB large size 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1122,6 +1125,7 @@ exports[`renders extended FAB medium size 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1448,6 +1452,7 @@ exports[`renders extended FAB not visible 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1774,6 +1779,7 @@ exports[`renders extended FAB transitioning to collapsed 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/FABMenu.test.tsx.snap b/src/components/__tests__/__snapshots__/FABMenu.test.tsx.snap index 8b66657112..e9d87ba84b 100644 --- a/src/components/__tests__/__snapshots__/FABMenu.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/FABMenu.test.tsx.snap @@ -123,6 +123,7 @@ exports[`renders FAB.Menu closed 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -299,6 +300,7 @@ exports[`renders FAB.Menu closed 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -606,6 +608,7 @@ exports[`renders FAB.Menu closed 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -900,6 +903,7 @@ exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1076,6 +1080,7 @@ exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1383,6 +1388,7 @@ exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1677,6 +1683,7 @@ exports[`renders FAB.Menu open 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1853,6 +1860,7 @@ exports[`renders FAB.Menu open 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2160,6 +2168,7 @@ exports[`renders FAB.Menu open 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2454,6 +2463,7 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2630,6 +2640,7 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2806,6 +2817,7 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2982,6 +2994,7 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3158,6 +3171,7 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3334,6 +3348,7 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3641,6 +3656,7 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3936,6 +3952,7 @@ exports[`renders FAB.Menu with center alignment 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4112,6 +4129,7 @@ exports[`renders FAB.Menu with center alignment 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4419,6 +4437,7 @@ exports[`renders FAB.Menu with center alignment 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4713,6 +4732,7 @@ exports[`renders FAB.Menu with items having icons 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4919,6 +4939,7 @@ exports[`renders FAB.Menu with items having icons 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -5256,6 +5277,7 @@ exports[`renders FAB.Menu with items having icons 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -5550,6 +5572,7 @@ exports[`renders FAB.Menu with start alignment 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -5726,6 +5749,7 @@ exports[`renders FAB.Menu with start alignment 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -6033,6 +6057,7 @@ exports[`renders FAB.Menu with start alignment 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/ListAccordion.test.tsx.snap b/src/components/__tests__/__snapshots__/ListAccordion.test.tsx.snap index c01fcf856a..9a67eff123 100644 --- a/src/components/__tests__/__snapshots__/ListAccordion.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/ListAccordion.test.tsx.snap @@ -33,6 +33,7 @@ exports[`renders expanded accordion 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -181,6 +182,7 @@ exports[`renders expanded accordion 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -297,6 +299,7 @@ exports[`renders list accordion with children 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -504,6 +507,7 @@ exports[`renders list accordion with custom title and description styles 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -704,6 +708,7 @@ exports[`renders list accordion with left items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -911,6 +916,7 @@ exports[`renders multiline list accordion 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/ListSection.test.tsx.snap b/src/components/__tests__/__snapshots__/ListSection.test.tsx.snap index a64f2e3662..94e51ef9f9 100644 --- a/src/components/__tests__/__snapshots__/ListSection.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/ListSection.test.tsx.snap @@ -477,6 +477,7 @@ exports[`renders list section with custom title style 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -629,6 +630,7 @@ exports[`renders list section with custom title style 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1235,6 +1237,7 @@ exports[`renders list section with subheader 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1387,6 +1390,7 @@ exports[`renders list section with subheader 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1953,6 +1957,7 @@ exports[`renders list section without subheader 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2105,6 +2110,7 @@ exports[`renders list section without subheader 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/MenuItem.test.tsx.snap b/src/components/__tests__/__snapshots__/MenuItem.test.tsx.snap index c316a38599..b3083b43d2 100644 --- a/src/components/__tests__/__snapshots__/MenuItem.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/MenuItem.test.tsx.snap @@ -26,6 +26,7 @@ exports[`Menu Item renders menu item 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -188,6 +189,7 @@ exports[`Menu Item renders menu item 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -350,6 +352,7 @@ exports[`Menu Item renders menu item 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -512,6 +515,7 @@ exports[`Menu Item renders menu item 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -674,6 +678,7 @@ exports[`Menu Item renders menu item 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/SegmentedButton.test.tsx.snap b/src/components/__tests__/__snapshots__/SegmentedButton.test.tsx.snap index 76988d9bc2..5fd65d1b31 100644 --- a/src/components/__tests__/__snapshots__/SegmentedButton.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/SegmentedButton.test.tsx.snap @@ -59,6 +59,7 @@ exports[`renders segmented button 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -185,6 +186,7 @@ exports[`renders segmented button 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/theme/tokens/sys/state.ts b/src/theme/tokens/sys/state.ts index d0742351bf..ab3aa94f68 100644 --- a/src/theme/tokens/sys/state.ts +++ b/src/theme/tokens/sys/state.ts @@ -15,4 +15,11 @@ export const state = { thickness: 3, outerOffset: 2, }, + /** + * Minimum size of an interactive target. Applied by expanding outside the + * component's bounds, so it is separate from the 40dp state layer that + * Checkbox and Switch render. + * @see https://m3.material.io/foundations/designing/structure + */ + minInteractiveSize: 48, } as const; From 199c7c9037cfbf3821b94cbf349c42214be06f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Thu, 27 Aug 2026 16:04:28 +0200 Subject: [PATCH 2/9] fix: hitslop and styling --- src/components/IconButton/IconButton.tsx | 26 ++++++++++++++++--- .../TouchableRipple.native.tsx | 16 +++++------- src/components/__tests__/IconButton.test.tsx | 21 +++++++++++++++ .../__tests__/TouchableRipple.test.tsx | 22 ++++++++++++++++ 4 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/components/IconButton/IconButton.tsx b/src/components/IconButton/IconButton.tsx index 0e3b0bb3c2..fa80fcc15f 100644 --- a/src/components/IconButton/IconButton.tsx +++ b/src/components/IconButton/IconButton.tsx @@ -12,6 +12,7 @@ import Animated, { type AnimatedStyle } from 'react-native-reanimated'; import { getIconButtonColor } from './utils'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../theme/types'; +import { splitStyles } from '../../utils/splitStyles'; import ActivityIndicator from '../ActivityIndicator'; import CrossFadeIcon from '../CrossFadeIcon'; import Icon from '../Icon'; @@ -152,10 +153,26 @@ const IconButton = ({ const buttonSize = size + 2 * PADDING; - const borderStyles = { - borderWidth: mode === 'outlined' && !selected ? 1 : 0, + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const flattenedStyle = (StyleSheet.flatten(style) || {}) as ViewStyle; + + const { borderWidth = mode === 'outlined' && !selected ? 1 : 0 } = + flattenedStyle; + + const [, borderRadiusStyles] = splitStyles( + flattenedStyle, + (style) => style.startsWith('border') && style.endsWith('Radius') + ); + + const shapeStyles = { borderRadius: buttonSize / 2, + ...borderRadiusStyles, + }; + + const borderStyles = { + borderWidth, borderColor, + ...shapeStyles, }; return ( @@ -177,7 +194,8 @@ const IconButton = ({ pointerEvents="none" style={[ StyleSheet.absoluteFill, - { backgroundColor, opacity: backgroundOpacity, borderRadius }, + { backgroundColor, opacity: backgroundOpacity }, + shapeStyles, ]} /> )} @@ -188,7 +206,7 @@ const IconButton = ({ aria-label={ariaLabel} style={[ styles.touchable, - { borderRadius }, + shapeStyles, // The Surface used to clip the ripple, so the touchable does it now. // Native only: its own overflow does not clip its hitSlop, but on web // it would clip the touch target, where the container already clips. diff --git a/src/components/TouchableRipple/TouchableRipple.native.tsx b/src/components/TouchableRipple/TouchableRipple.native.tsx index c1a214e201..a30cc9d35f 100644 --- a/src/components/TouchableRipple/TouchableRipple.native.tsx +++ b/src/components/TouchableRipple/TouchableRipple.native.tsx @@ -140,14 +140,12 @@ const TouchableRipple = ({ undefined ); - // A caller hitSlop wins, so there is nothing to measure for. `null` counts as - // supplied, it means "no slop". - const shouldMeasure = hitSlop === undefined; - // Gates whether the measurement is applied, not whether it happens. RN emits - // onLayout on mount and on layout change, so a touchable that mounts disabled - // gets no event once it is enabled and would stay small. - const shouldExpand = shouldMeasure && !disabled; + // onLayout on mount and on layout change, so a touchable that mounts disabled, + // or with a caller hitSlop, gets no event once that goes away and would stay + // small. A caller hitSlop wins while it is set; `null` counts as set, it means + // "no slop". + const shouldExpand = hitSlop === undefined && !disabled; const handleLayout = React.useCallback( (event: LayoutChangeEvent) => { @@ -207,7 +205,7 @@ const TouchableRipple = ({ ref={ref} disabled={disabled} hitSlop={shouldExpand ? expansion : hitSlop} - onLayout={shouldMeasure ? handleLayout : onLayout} + onLayout={handleLayout} style={[useForeground && styles.overflowHidden, style]} android_ripple={androidRipple} > @@ -222,7 +220,7 @@ const TouchableRipple = ({ ref={ref} disabled={disabled} hitSlop={shouldExpand ? expansion : hitSlop} - onLayout={shouldMeasure ? handleLayout : onLayout} + onLayout={handleLayout} style={[borderless && styles.overflowHidden, style]} > {({ pressed }) => ( diff --git a/src/components/__tests__/IconButton.test.tsx b/src/components/__tests__/IconButton.test.tsx index 914f1e2c55..2e373ed3dd 100644 --- a/src/components/__tests__/IconButton.test.tsx +++ b/src/components/__tests__/IconButton.test.tsx @@ -18,6 +18,9 @@ const styles = StyleSheet.create({ slightlyRounded: { borderRadius: 4, }, + cutCorner: { + borderTopLeftRadius: 0, + }, }); it('renders icon button by default', async () => { @@ -80,6 +83,24 @@ it('renders icon button with small border radius', async () => { expect(toJSON()).toMatchSnapshot(); }); +it('clips to a custom corner radius', async () => { + await render( + {}} + style={styles.cutCorner} + /> + ); + + // The container stopped clipping so the touch target can escape it, so the + // touchable has to take the shape itself, corners included. + expect(screen.getByTestId('icon-button')).toHaveStyle({ + borderTopLeftRadius: 0, + }); +}); + describe('getIconButtonColor - icon color', () => { it('should return custom icon color', () => { expect( diff --git a/src/components/__tests__/TouchableRipple.test.tsx b/src/components/__tests__/TouchableRipple.test.tsx index d70418a5f8..fea4c58136 100644 --- a/src/components/__tests__/TouchableRipple.test.tsx +++ b/src/components/__tests__/TouchableRipple.test.tsx @@ -219,6 +219,28 @@ describe('TouchableRipple', () => { expect(hitSlopOf()).toEqual(expanded); }); + it('expands once a caller-supplied hitSlop is taken away', async () => { + const Harness = ({ hitSlop }: { hitSlop?: number }) => ( + {}} + > + Button + + ); + const view = await render(); + + await fireLayout(32, 32); + expect(hitSlopOf()).toBe(2); + + // back to the default, with no second layout event to rely on + await act(async () => { + await view.rerender(); + }); + expect(hitSlopOf()).toEqual({ top: 8, bottom: 8, left: 8, right: 8 }); + }); + it('still calls a caller-supplied onLayout', async () => { const onLayout = jest.fn(); await renderTouchable({ onLayout }); From 7ee28649afed0286f92ca8e071fae10cb5a725fb Mon Sep 17 00:00:00 2001 From: Konrad Rydygier Date: Mon, 7 Sep 2026 14:55:36 +0200 Subject: [PATCH 3/9] test: update snapshots after rebasing onto main --- .../Appbar/__snapshots__/Appbar.test.tsx.snap | 64 +++--- .../__snapshots__/Banner.test.tsx.snap | 4 + .../__snapshots__/Button.test.tsx.snap | 13 ++ .../__snapshots__/Chip.test.tsx.snap | 34 ++- .../__snapshots__/DataTable.test.tsx.snap | 195 ++++++++---------- .../__snapshots__/IconButton.test.tsx.snap | 80 ++++--- .../__snapshots__/ListItem.test.tsx.snap | 8 + .../__snapshots__/Menu.test.tsx.snap | 4 + .../__snapshots__/Searchbar.test.tsx.snap | 80 ++++--- .../__snapshots__/Snackbar.test.tsx.snap | 1 + .../__snapshots__/TextInput.test.tsx.snap | 128 +++++------- .../__snapshots__/ToggleButton.test.tsx.snap | 54 +++-- 12 files changed, 325 insertions(+), 340 deletions(-) diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap index faeb89916b..89f341ba1c 100644 --- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap +++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap @@ -194,7 +194,6 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` [ { "margin": 6, - "overflow": "hidden", }, { "backgroundColor": undefined, @@ -233,17 +232,10 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` centered={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 6, - "left": 6, - "right": 6, - "top": 6, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -262,6 +254,12 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` "flexGrow": 1, "justifyContent": "center", }, + { + "borderRadius": 20, + }, + { + "overflow": "hidden", + }, undefined, ], ] @@ -360,7 +358,6 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` [ { "margin": 6, - "overflow": "hidden", }, { "backgroundColor": undefined, @@ -399,17 +396,10 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` centered={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 6, - "left": 6, - "right": 6, - "top": 6, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -428,6 +418,12 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` "flexGrow": 1, "justifyContent": "center", }, + { + "borderRadius": 20, + }, + { + "overflow": "hidden", + }, undefined, ], ] @@ -582,7 +578,6 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A [ { "margin": 6, - "overflow": "hidden", }, { "backgroundColor": undefined, @@ -621,17 +616,10 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A centered={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 6, - "left": 6, - "right": 6, - "top": 6, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -650,6 +638,12 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A "flexGrow": 1, "justifyContent": "center", }, + { + "borderRadius": 20, + }, + { + "overflow": "hidden", + }, undefined, ], ] @@ -805,7 +799,6 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A [ { "margin": 6, - "overflow": "hidden", }, { "backgroundColor": undefined, @@ -843,17 +836,10 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A centered={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 6, - "left": 6, - "right": 6, - "top": 6, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -872,6 +858,12 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A "flexGrow": 1, "justifyContent": "center", }, + { + "borderRadius": 20, + }, + { + "overflow": "hidden", + }, undefined, ], ] diff --git a/src/components/__tests__/__snapshots__/Banner.test.tsx.snap b/src/components/__tests__/__snapshots__/Banner.test.tsx.snap index 71e77e2936..b9908258e6 100644 --- a/src/components/__tests__/__snapshots__/Banner.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Banner.test.tsx.snap @@ -300,6 +300,7 @@ exports[`render visible banner, with custom theme 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -909,6 +910,7 @@ exports[`renders visible banner, with action buttons and with image 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1300,6 +1302,7 @@ exports[`renders visible banner, with action buttons and without image 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1507,6 +1510,7 @@ exports[`renders visible banner, with action buttons and without image 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/Button.test.tsx.snap b/src/components/__tests__/__snapshots__/Button.test.tsx.snap index 628087c74b..12b3f5262e 100644 --- a/src/components/__tests__/__snapshots__/Button.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Button.test.tsx.snap @@ -2666,6 +2666,7 @@ exports[`renders button with an accessibility hint 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2871,6 +2872,7 @@ exports[`renders button with an accessibility label 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3075,6 +3077,7 @@ exports[`renders button with button color 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3279,6 +3282,7 @@ exports[`renders button with color 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3483,6 +3487,7 @@ exports[`renders button with custom testID 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3688,6 +3693,7 @@ exports[`renders button with icon 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3940,6 +3946,7 @@ exports[`renders button with icon in reverse order 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4194,6 +4201,7 @@ exports[`renders contained contained with mode 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4399,6 +4407,7 @@ exports[`renders disabled button 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4603,6 +4612,7 @@ exports[`renders loading button 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -5011,6 +5021,7 @@ exports[`renders outlined button with mode 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -5216,6 +5227,7 @@ exports[`renders text button by default 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -5420,6 +5432,7 @@ exports[`renders text button with mode 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap index 04687e301f..79ee8c5459 100644 --- a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap @@ -121,6 +121,7 @@ exports[`renders chip with close button 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -260,11 +261,12 @@ exports[`renders chip with close button 1`] = ` @@ -300,6 +302,13 @@ exports[`renders chip with close button 1`] = ` onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} role="button" + style={ + { + "height": "100%", + "justifyContent": "center", + "width": "100%", + } + } > @@ -653,6 +666,13 @@ exports[`renders chip with custom close button 1`] = ` onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} role="button" + style={ + { + "height": "100%", + "justifyContent": "center", + "width": "100%", + } + } > Date: Tue, 8 Sep 2026 09:55:04 +0200 Subject: [PATCH 4/9] fix: compute per-component hit slops --- src/components/Checkbox/Checkbox.tsx | 9 + src/components/Chip/Chip.tsx | 15 +- src/components/Chip/tokens.ts | 7 + src/components/IconButton/IconButton.tsx | 58 +++- .../RadioButton/RadioButtonAndroid.tsx | 19 +- src/components/RadioButton/RadioButtonIOS.tsx | 22 +- src/components/RadioButton/tokens.ts | 7 + .../SegmentedButtons/SegmentedButtonItem.tsx | 11 +- src/components/Switch/Switch.tsx | 27 ++ .../TouchableRipple.native.tsx | 82 +---- .../TouchableRipple/TouchableRipple.tsx | 29 +- .../Appbar/__snapshots__/Appbar.test.tsx.snap | 100 +++++- .../__tests__/Checkbox/Checkbox.test.tsx | 22 +- .../__snapshots__/Checkbox.test.tsx.snap | 63 +++- .../__snapshots__/CheckboxItem.test.tsx.snap | 20 +- src/components/__tests__/Chip.test.tsx | 29 ++ src/components/__tests__/IconButton.test.tsx | 32 +- .../RadioButton/RadioButton.test.tsx | 24 +- .../__snapshots__/RadioButton.test.tsx.snap | 52 ++- .../RadioButtonGroup.test.tsx.snap | 13 +- .../RadioButtonItem.test.tsx.snap | 56 +++- .../__tests__/SegmentedButton.test.tsx | 42 +++ src/components/__tests__/Switch.test.tsx | 20 ++ .../__tests__/TouchableRipple.test.tsx | 179 +---------- .../__tests__/TouchableRippleWeb.test.tsx | 14 +- .../__snapshots__/Banner.test.tsx.snap | 4 - .../__snapshots__/Button.test.tsx.snap | 13 - .../__snapshots__/Chip.test.tsx.snap | 52 ++- .../__snapshots__/DataTable.test.tsx.snap | 303 +++++++++++++++++- .../__snapshots__/DrawerItem.test.tsx.snap | 3 - .../__tests__/__snapshots__/FAB.test.tsx.snap | 13 - .../__snapshots__/FABExtended.test.tsx.snap | 6 - .../__snapshots__/FABMenu.test.tsx.snap | 25 -- .../__snapshots__/IconButton.test.tsx.snap | 117 ++++++- .../__snapshots__/ListAccordion.test.tsx.snap | 6 - .../__snapshots__/ListItem.test.tsx.snap | 17 +- .../__snapshots__/ListSection.test.tsx.snap | 6 - .../__snapshots__/Menu.test.tsx.snap | 4 - .../__snapshots__/MenuItem.test.tsx.snap | 5 - .../__snapshots__/Searchbar.test.tsx.snap | 125 +++++++- .../SegmentedButton.test.tsx.snap | 18 +- .../__snapshots__/Snackbar.test.tsx.snap | 1 - .../__snapshots__/Switch.test.tsx.snap | 32 ++ .../__snapshots__/TextInput.test.tsx.snap | 200 +++++++++++- .../__snapshots__/ToggleButton.test.tsx.snap | 71 +++- src/utils/getMinInteractiveSizeHitSlop.ts | 40 +++ 46 files changed, 1521 insertions(+), 492 deletions(-) create mode 100644 src/components/Chip/tokens.ts create mode 100644 src/components/RadioButton/tokens.ts create mode 100644 src/utils/getMinInteractiveSizeHitSlop.ts diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index fe73752eb7..7f49338344 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -18,6 +18,7 @@ import { useInternalTheme } from '../../core/theming'; import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext'; import { tokens } from '../../theme/tokens'; import type { ThemeProp } from '../../theme/types'; +import getMinInteractiveSizeHitSlop from '../../utils/getMinInteractiveSizeHitSlop'; import { isKeyboardFocusEvent } from '../../utils/isKeyboardFocusEvent'; import TouchableRipple from '../TouchableRipple/TouchableRipple'; import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple'; @@ -85,6 +86,13 @@ const FOCUS_THICKNESS = tokens.md.sys.state.focusIndicator.thickness; const FOCUS_RING_SIZE = STATE_LAYER_SIZE; const FOCUS_RING_RADIUS = STATE_LAYER_SIZE / 2; +// The state layer is fixed, so the slop to reach the 48dp minimum +// interactive target is a constant rather than something to measure. +const CHECKBOX_HIT_SLOP = getMinInteractiveSizeHitSlop({ + width: STATE_LAYER_SIZE, + height: STATE_LAYER_SIZE, +}); + /** * Checkboxes allow the selection of multiple options from a set. * @@ -244,6 +252,7 @@ const Checkbox = ({ disabled={disabled} {...accessibilityProps} testID={testID} + hitSlop={rest.hitSlop ?? (disabled ? undefined : CHECKBOX_HIT_SLOP)} style={[ styles.tapTarget, Platform.OS === 'web' ? webNoOutline : undefined, diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx index f7f12aa33d..7d0a697a7c 100644 --- a/src/components/Chip/Chip.tsx +++ b/src/components/Chip/Chip.tsx @@ -14,9 +14,11 @@ import useLatestCallback from 'use-latest-callback'; import { getChipColors } from './helpers'; import type { ChipAvatarProps } from './helpers'; +import { ChipTokens } from './tokens'; import { useInternalTheme } from '../../core/theming'; import { white } from '../../theme/colors'; import type { ThemeProp } from '../../theme/types'; +import getMinInteractiveSizeHitSlop from '../../utils/getMinInteractiveSizeHitSlop'; import hasTouchHandler from '../../utils/hasTouchHandler'; import type { IconSource } from '../Icon'; import Icon from '../Icon'; @@ -198,6 +200,16 @@ const CLOSE_AFFORDANCE_WIDTH = 34; */ const CLOSE_AFFORDANCE_MIN_WIDTH = 26; +/** + * The container height is fixed by spec, so the slop to reach the 48dp minimum + * is a constant rather than something to measure. Width grows with the label + * and the whole pill is already the target, so only the vertical axis needs it. + */ +const { containerHeight: CHIP_BODY_HEIGHT } = ChipTokens; +const CHIP_BODY_HIT_SLOP = getMinInteractiveSizeHitSlop({ + height: CHIP_BODY_HEIGHT, +}); + const Chip = ({ mode = 'flat', children, @@ -323,7 +335,7 @@ const Chip = ({ aria-disabled={disabled} testID={testID} theme={theme} - hitSlop={hitSlop} + hitSlop={hitSlop ?? (disabled ? undefined : CHIP_BODY_HIT_SLOP)} > void; + /** + * Radius of every corner of the button. Defaults to a circle (half of the + * button's size). Read as a plain prop rather than out of `style`, since + * `style` may be an animated value on the UI thread that a synchronous + * `StyleSheet.flatten` cannot see. + */ + borderRadius?: number; + borderTopLeftRadius?: number; + borderTopRightRadius?: number; + borderBottomLeftRadius?: number; + borderBottomRightRadius?: number; + borderTopStartRadius?: number; + borderTopEndRadius?: number; + borderBottomStartRadius?: number; + borderBottomEndRadius?: number; style?: StyleProp>; ref?: React.Ref; /** @@ -129,6 +144,15 @@ const IconButton = ({ testID, loading = false, contentStyle, + borderRadius, + borderTopLeftRadius, + borderTopRightRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + borderTopStartRadius, + borderTopEndRadius, + borderBottomStartRadius, + borderBottomEndRadius, ref, ...rest }: Props) => { @@ -152,21 +176,18 @@ const IconButton = ({ }); const buttonSize = size + 2 * PADDING; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const flattenedStyle = (StyleSheet.flatten(style) || {}) as ViewStyle; - - const { borderWidth = mode === 'outlined' && !selected ? 1 : 0 } = - flattenedStyle; - - const [, borderRadiusStyles] = splitStyles( - flattenedStyle, - (style) => style.startsWith('border') && style.endsWith('Radius') - ); + const borderWidth = mode === 'outlined' && !selected ? 1 : 0; const shapeStyles = { - borderRadius: buttonSize / 2, - ...borderRadiusStyles, + borderRadius: borderRadius ?? buttonSize / 2, + borderTopLeftRadius, + borderTopRightRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + borderTopStartRadius, + borderTopEndRadius, + borderBottomStartRadius, + borderBottomEndRadius, }; const borderStyles = { @@ -175,6 +196,14 @@ const IconButton = ({ ...shapeStyles, }; + // Computed straight from `size`, a plain prop known at render time, rather + // than measured. `buttonSize` never changes after mount without `size` also + // changing, so there is nothing to react to. A disabled button gets no + // slop of its own, only what a caller's own `hitSlop` in `rest` supplies. + const hitSlop = disabled + ? undefined + : getMinInteractiveSizeHitSlop({ width: buttonSize, height: buttonSize }); + return ( diff --git a/src/components/RadioButton/RadioButtonAndroid.tsx b/src/components/RadioButton/RadioButtonAndroid.tsx index 91520db69c..fb1bf8d9a2 100644 --- a/src/components/RadioButton/RadioButtonAndroid.tsx +++ b/src/components/RadioButton/RadioButtonAndroid.tsx @@ -4,12 +4,23 @@ import { Animated, StyleSheet, View } from 'react-native'; import { RadioButtonContext } from './RadioButtonGroup'; import type { RadioButtonContextType } from './RadioButtonGroup'; +import { RadioButtonTokens } from './tokens'; import { getSelectionControlColor, handlePress, isChecked } from './utils'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../theme/types'; +import getMinInteractiveSizeHitSlop from '../../utils/getMinInteractiveSizeHitSlop'; import TouchableRipple from '../TouchableRipple/TouchableRipple'; import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple'; +const { stateLayerSize: STATE_LAYER_SIZE } = RadioButtonTokens; + +// The state layer is fixed, so the slop to reach the 48dp minimum +// interactive target is a constant rather than something to measure. +const RADIO_BUTTON_HIT_SLOP = getMinInteractiveSizeHitSlop({ + width: STATE_LAYER_SIZE, + height: STATE_LAYER_SIZE, +}); + export type Props = Omit< React.PropsWithoutRef, 'children' @@ -146,6 +157,9 @@ const RadioButtonAndroid = ({ style={styles.container} testID={testID} theme={theme} + hitSlop={ + rest.hitSlop ?? (disabled ? undefined : RADIO_BUTTON_HIT_SLOP) + } > , 'children' @@ -104,12 +116,15 @@ const RadioButtonIOS = ({ style={styles.container} testID={testID} theme={theme} + hitSlop={ + rest.hitSlop ?? (disabled ? undefined : RADIO_BUTTON_HIT_SLOP) + } > @@ -125,8 +140,9 @@ RadioButtonIOS.displayName = 'RadioButton.IOS'; const styles = StyleSheet.create({ container: { - borderRadius: 18, - padding: 6, + borderRadius: STATE_LAYER_SIZE / 2, + // Centres the 24dp checkmark within the 40dp state layer. + padding: (STATE_LAYER_SIZE - CHECKMARK_SIZE) / 2, }, }); diff --git a/src/components/RadioButton/tokens.ts b/src/components/RadioButton/tokens.ts new file mode 100644 index 0000000000..a3abdfe3e0 --- /dev/null +++ b/src/components/RadioButton/tokens.ts @@ -0,0 +1,7 @@ +/** + * MD3 Radio button spec dimensions. + * @see https://m3.material.io/components/radio-button/specs + */ +export const RadioButtonTokens = { + stateLayerSize: 40, +} as const; diff --git a/src/components/SegmentedButtons/SegmentedButtonItem.tsx b/src/components/SegmentedButtons/SegmentedButtonItem.tsx index 33886d7ec1..d6124f2256 100644 --- a/src/components/SegmentedButtons/SegmentedButtonItem.tsx +++ b/src/components/SegmentedButtons/SegmentedButtonItem.tsx @@ -23,6 +23,7 @@ import { import { useInternalTheme } from '../../core/theming'; import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext'; import type { ThemeProp } from '../../theme/types'; +import getMinInteractiveSizeHitSlop from '../../utils/getMinInteractiveSizeHitSlop'; import type { IconSource } from '../Icon'; import Icon from '../Icon'; import TouchableRipple from '../TouchableRipple/TouchableRipple'; @@ -193,6 +194,14 @@ const SegmentedButtonItem = ({ const paddingVertical = getSegmentedButtonDensityPadding({ density }); + // Height is `2 * paddingVertical + content`, and content is never shorter + // than the 18dp icon (the label's own line height is taller), so that is + // the safe floor to compute slop from without needing to measure. + const contentHeight = 2 * paddingVertical + iconSize; + const defaultHitSlop = disabled + ? undefined + : getMinInteractiveSizeHitSlop({ height: contentHeight }); + const rippleStyle: ViewStyle = { borderRadius, ...segmentBorderRadius, @@ -217,7 +226,7 @@ const SegmentedButtonItem = ({ style={rippleStyle} background={background} theme={theme} - hitSlop={hitSlop} + hitSlop={hitSlop ?? defaultHitSlop} > + {/* react-native-web removed `hitSlop` in 0.13.0 (same as + TouchableRipple), so web needs a real element the browser can + hit-test instead of a native responder inset. */} + {Platform.OS === 'web' && !isDisabled && ( + + )} ): ViewStyle => { const flat = StyleSheet.flatten(style); @@ -62,35 +57,6 @@ const getUnderlayShape = (style: StyleProp): ViewStyle => { }; }; -/** - * Slop needed to bring a rendered size up to `minInteractiveSize`. Expands - * outside the bounds rather than resizing, so a 40dp state layer keeps its 40dp - * and gains 4dp per side. Returns undefined when the size is already enough, so - * that case does not re-render. - * @see https://developer.android.com/develop/ui/compose/accessibility/api-defaults - */ -const getExpansion = (width: number, height: number): Insets | undefined => { - // A collapsed touchable would otherwise claim 24dp of slop around a point - // where nothing is drawn. - if (width === 0 || height === 0) { - return undefined; - } - - const horizontal = Math.max(0, (minInteractiveSize - width) / 2); - const vertical = Math.max(0, (minInteractiveSize - height) / 2); - - if (horizontal === 0 && vertical === 0) { - return undefined; - } - - return { - top: vertical, - bottom: vertical, - left: horizontal, - right: horizontal, - }; -}; - export type Props = PressableProps & { borderless?: boolean; background?: PressableAndroidRippleConfig; @@ -118,7 +84,6 @@ const TouchableRipple = ({ children, theme: themeOverrides, hitSlop, - onLayout, ref, ...rest }: Props) => { @@ -136,45 +101,6 @@ const TouchableRipple = ({ const disabled = disabledProp || !hasPassedTouchHandler; - const [expansion, setExpansion] = React.useState( - undefined - ); - - // Gates whether the measurement is applied, not whether it happens. RN emits - // onLayout on mount and on layout change, so a touchable that mounts disabled, - // or with a caller hitSlop, gets no event once that goes away and would stay - // small. A caller hitSlop wins while it is set; `null` counts as set, it means - // "no slop". - const shouldExpand = hitSlop === undefined && !disabled; - - const handleLayout = React.useCallback( - (event: LayoutChangeEvent) => { - onLayout?.(event); - - const { width, height } = event.nativeEvent.layout; - const next = getExpansion(width, height); - - setExpansion((current) => { - // Nothing changed, so a big enough touchable does not re-render. - if (current === next) { - return current; - } - if ( - current && - next && - current.top === next.top && - current.bottom === next.bottom && - current.left === next.left && - current.right === next.right - ) { - return current; - } - return next; - }); - }, - [onLayout] - ); - const { calculatedRippleColor, calculatedUnderlayColor } = getTouchableRippleColors({ theme, @@ -204,8 +130,7 @@ const TouchableRipple = ({ {...rest} ref={ref} disabled={disabled} - hitSlop={shouldExpand ? expansion : hitSlop} - onLayout={handleLayout} + hitSlop={hitSlop} style={[useForeground && styles.overflowHidden, style]} android_ripple={androidRipple} > @@ -219,8 +144,7 @@ const TouchableRipple = ({ {...rest} ref={ref} disabled={disabled} - hitSlop={shouldExpand ? expansion : hitSlop} - onLayout={handleLayout} + hitSlop={hitSlop} style={[borderless && styles.overflowHidden, style]} > {({ pressed }) => ( diff --git a/src/components/TouchableRipple/TouchableRipple.tsx b/src/components/TouchableRipple/TouchableRipple.tsx index ada76bd255..89a5c73370 100644 --- a/src/components/TouchableRipple/TouchableRipple.tsx +++ b/src/components/TouchableRipple/TouchableRipple.tsx @@ -15,31 +15,21 @@ import { getTouchableRippleColors } from './utils'; import { SettingsContext } from '../../core/settings'; import type { Settings } from '../../core/settings'; import { useInternalTheme } from '../../core/theming'; -import { tokens } from '../../theme/tokens'; import type { ThemeProp } from '../../theme/types'; import hasTouchHandler from '../../utils/hasTouchHandler'; -const { minInteractiveSize } = tokens.md.sys.state; - /** * react-native-web removed `hitSlop` in 0.13.0, so web needs a real element the - * browser can hit-test instead. An absolutely positioned box at least the - * minimum target size, which is what material-web does, and it costs no layout. + * browser can hit-test instead of a native responder inset. * @see https://github.com/necolas/react-native-web/releases/tag/0.13.0 - * @see https://github.com/material-components/material-web/blob/main/iconbutton/internal/_shared.scss */ const getTouchTargetStyle = (hitSlop: PressableProps['hitSlop']): ViewStyle => { - // `undefined` means the caller said nothing, so the minimum applies. `null` - // means "no slop", same as native. - if (hitSlop === undefined) { - return styles.touchTarget; - } - if (hitSlop === null) { + // `undefined` or `null` both mean no slop: nothing for the caller to opt + // into, so the target matches the touchable's own bounds. + if (hitSlop === undefined || hitSlop === null) { return styles.noTouchTarget; } - // A caller hitSlop wins here too, so web matches native instead of ignoring - // the prop. const inset = (value: number | undefined) => -(value ?? 0); return typeof hitSlop === 'number' @@ -395,17 +385,6 @@ const styles = StyleSheet.create({ left: 0, right: 0, }, - touchTarget: { - position: 'absolute', - top: '50%', - left: '50%', - // max(minInteractiveSize, 100%), same as MD3 web's .touch - width: '100%', - height: '100%', - minWidth: minInteractiveSize, - minHeight: minInteractiveSize, - transform: [{ translateX: '-50%' }, { translateY: '-50%' }], - }, }); export default TouchableRipple; diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap index 89f341ba1c..3f30b0ce3e 100644 --- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap +++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap @@ -201,8 +201,16 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -232,10 +240,17 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -255,7 +270,15 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -365,8 +388,16 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -396,10 +427,17 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -419,7 +457,15 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -585,8 +631,16 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -616,10 +670,17 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -639,7 +700,15 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -806,8 +875,16 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -836,10 +913,17 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -859,7 +943,15 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", diff --git a/src/components/__tests__/Checkbox/Checkbox.test.tsx b/src/components/__tests__/Checkbox/Checkbox.test.tsx index a72200bb4e..fa9c520ebb 100644 --- a/src/components/__tests__/Checkbox/Checkbox.test.tsx +++ b/src/components/__tests__/Checkbox/Checkbox.test.tsx @@ -1,6 +1,6 @@ import { expect, it } from '@jest/globals'; -import { render } from '../../../test-utils'; +import { render, screen } from '../../../test-utils'; import Checkbox from '../../Checkbox'; it('renders checked Checkbox with onPress', async () => { @@ -58,3 +58,23 @@ it('renders Checkbox with custom testID', async () => { expect(tree).toMatchSnapshot(); }); + +it('expands hitSlop up to the 48dp minimum when enabled', async () => { + await render(); + + // (48 - 40) / 2 on every side + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('checkbox').props.hitSlop).toEqual({ + top: 4, + bottom: 4, + left: 4, + right: 4, + }); +}); + +it('gives a disabled Checkbox no hitSlop of its own', async () => { + await render(); + + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('checkbox').props.hitSlop).toBeUndefined(); +}); diff --git a/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap b/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap index 0cd9bcc43f..fe7f4ec3ba 100644 --- a/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap +++ b/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap @@ -24,10 +24,17 @@ exports[`renders Checkbox with custom testID 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -210,10 +217,17 @@ exports[`renders checked Checkbox with color 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -395,10 +409,17 @@ exports[`renders checked Checkbox with onPress 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -580,10 +601,17 @@ exports[`renders indeterminate Checkbox 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -752,10 +780,17 @@ exports[`renders indeterminate Checkbox with color 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -924,10 +959,17 @@ exports[`renders unchecked Checkbox with color 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1109,10 +1151,17 @@ exports[`renders unchecked Checkbox with onPress 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap b/src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap index 769d9df19a..b5dd171c87 100644 --- a/src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap +++ b/src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap @@ -26,7 +26,6 @@ exports[`can render leading checkbox control 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -79,10 +78,17 @@ exports[`can render leading checkbox control 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -304,7 +310,6 @@ exports[`renders unchecked 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -394,10 +399,17 @@ exports[`renders unchecked 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx index 0774ad6109..5075db441e 100644 --- a/src/components/__tests__/Chip.test.tsx +++ b/src/components/__tests__/Chip.test.tsx @@ -95,6 +95,35 @@ it('renders chip with zero border radius', async () => { }); }); +it('expands hitSlop up to the 48dp minimum when enabled', async () => { + await render( + {}}> + Active chip + + ); + + // (48 - 32) / 2 top/bottom, none horizontally: the pill's width already + // covers it + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('active-chip').props.hitSlop).toEqual({ + top: 8, + bottom: 8, + left: 0, + right: 0, + }); +}); + +it('gives a disabled Chip no hitSlop of its own', async () => { + await render( + {}}> + Disabled chip + + ); + + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('disabled-chip').props.hitSlop).toBeUndefined(); +}); + describe('getChipColors - text color', () => { it('should return correct disabled color, for theme version 3', () => { expect( diff --git a/src/components/__tests__/IconButton.test.tsx b/src/components/__tests__/IconButton.test.tsx index 2e373ed3dd..5e2e181ba7 100644 --- a/src/components/__tests__/IconButton.test.tsx +++ b/src/components/__tests__/IconButton.test.tsx @@ -18,9 +18,6 @@ const styles = StyleSheet.create({ slightlyRounded: { borderRadius: 4, }, - cutCorner: { - borderTopLeftRadius: 0, - }, }); it('renders icon button by default', async () => { @@ -49,6 +46,33 @@ it('renders disabled icon button', async () => { expect(tree).toMatchSnapshot(); }); +it('expands hitSlop up to the 48dp minimum for a button smaller than that', async () => { + await render(); + + // (48 - 40) / 2 on every side, for the default 24dp icon plus 8dp padding + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('icon-button').props.hitSlop).toEqual({ + top: 4, + bottom: 4, + left: 4, + right: 4, + }); +}); + +it('gives a disabled button no hitSlop of its own', async () => { + await render(); + + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('icon-button').props.hitSlop).toBeUndefined(); +}); + +it('lets a caller-supplied hitSlop win even while disabled', async () => { + await render(); + + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('icon-button').props.hitSlop).toBe(2); +}); + it('renders icon change animated', async () => { const tree = (await render()).toJSON(); @@ -90,7 +114,7 @@ it('clips to a custom corner radius', async () => { testID="icon-button" size={36} onPress={() => {}} - style={styles.cutCorner} + borderTopLeftRadius={0} /> ); diff --git a/src/components/__tests__/RadioButton/RadioButton.test.tsx b/src/components/__tests__/RadioButton/RadioButton.test.tsx index da8a04375d..90e76d68f6 100644 --- a/src/components/__tests__/RadioButton/RadioButton.test.tsx +++ b/src/components/__tests__/RadioButton/RadioButton.test.tsx @@ -6,7 +6,7 @@ import { jest as mockJest, } from '@jest/globals'; -import { render } from '../../../test-utils'; +import { render, screen } from '../../../test-utils'; import RadioButton from '../../RadioButton'; import { RadioButtonContext } from '../../RadioButton/RadioButtonGroup'; @@ -82,4 +82,26 @@ describe('RadioButton', () => { expect(tree).toMatchSnapshot(); }); }); + + describe('hitSlop', () => { + it('expands up to the 48dp minimum when enabled', async () => { + await render(); + + // (48 - 40) / 2 on every side + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('radio').props.hitSlop).toEqual({ + top: 4, + bottom: 4, + left: 4, + right: 4, + }); + }); + + it('gives a disabled RadioButton no hitSlop of its own', async () => { + await render(); + + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('radio').props.hitSlop).toBeUndefined(); + }); + }); }); diff --git a/src/components/__tests__/RadioButton/__snapshots__/RadioButton.test.tsx.snap b/src/components/__tests__/RadioButton/__snapshots__/RadioButton.test.tsx.snap index 31dffb6970..a6e37f97d4 100644 --- a/src/components/__tests__/RadioButton/__snapshots__/RadioButton.test.tsx.snap +++ b/src/components/__tests__/RadioButton/__snapshots__/RadioButton.test.tsx.snap @@ -23,10 +23,17 @@ exports[`RadioButton RadioButton with custom testID renders properly 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -40,8 +47,8 @@ exports[`RadioButton RadioButton with custom testID renders properly 1`] = ` "overflow": "hidden", }, { - "borderRadius": 18, - "padding": 6, + "borderRadius": 20, + "padding": 8, }, ] } @@ -111,10 +118,17 @@ exports[`RadioButton on default platform renders properly 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -128,8 +142,8 @@ exports[`RadioButton on default platform renders properly 1`] = ` "overflow": "hidden", }, { - "borderRadius": 18, - "padding": 6, + "borderRadius": 20, + "padding": 8, }, ] } @@ -198,10 +212,17 @@ exports[`RadioButton on ios platform renders properly 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -215,8 +236,8 @@ exports[`RadioButton on ios platform renders properly 1`] = ` "overflow": "hidden", }, { - "borderRadius": 18, - "padding": 6, + "borderRadius": 20, + "padding": 8, }, ] } @@ -285,10 +306,17 @@ exports[`RadioButton when RadioButton is wrapped by RadioButtonContext.Provider accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -302,8 +330,8 @@ exports[`RadioButton when RadioButton is wrapped by RadioButtonContext.Provider "overflow": "hidden", }, { - "borderRadius": 18, - "padding": 6, + "borderRadius": 20, + "padding": 8, }, ] } diff --git a/src/components/__tests__/RadioButton/__snapshots__/RadioButtonGroup.test.tsx.snap b/src/components/__tests__/RadioButton/__snapshots__/RadioButtonGroup.test.tsx.snap index 54a237c099..82f06ebf6c 100644 --- a/src/components/__tests__/RadioButton/__snapshots__/RadioButtonGroup.test.tsx.snap +++ b/src/components/__tests__/RadioButton/__snapshots__/RadioButtonGroup.test.tsx.snap @@ -26,10 +26,17 @@ exports[`RadioButtonGroup renders properly 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -43,8 +50,8 @@ exports[`RadioButtonGroup renders properly 1`] = ` "overflow": "hidden", }, { - "borderRadius": 18, - "padding": 6, + "borderRadius": 20, + "padding": 8, }, ] } diff --git a/src/components/__tests__/RadioButton/__snapshots__/RadioButtonItem.test.tsx.snap b/src/components/__tests__/RadioButton/__snapshots__/RadioButtonItem.test.tsx.snap index ef32fb657b..1d8934aa6f 100644 --- a/src/components/__tests__/RadioButton/__snapshots__/RadioButtonItem.test.tsx.snap +++ b/src/components/__tests__/RadioButton/__snapshots__/RadioButtonItem.test.tsx.snap @@ -26,7 +26,6 @@ exports[`can render leading radio button control 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -78,10 +77,17 @@ exports[`can render leading radio button control 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -95,8 +101,8 @@ exports[`can render leading radio button control 1`] = ` "overflow": "hidden", }, { - "borderRadius": 18, - "padding": 6, + "borderRadius": 20, + "padding": 8, }, ] } @@ -206,7 +212,6 @@ exports[`can render the Android radio button on different platforms 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -294,10 +299,17 @@ exports[`can render the Android radio button on different platforms 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -311,7 +323,7 @@ exports[`can render the Android radio button on different platforms 1`] = ` "overflow": "hidden", }, { - "borderRadius": 18, + "borderRadius": 20, }, ] } @@ -324,7 +336,7 @@ exports[`can render the Android radio button on different platforms 1`] = ` "borderRadius": 10, "borderWidth": 2, "height": 20, - "margin": 8, + "margin": 10, "width": 20, } } @@ -360,7 +372,6 @@ exports[`can render the iOS radio button on different platforms 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -448,10 +459,17 @@ exports[`can render the iOS radio button on different platforms 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -465,8 +483,8 @@ exports[`can render the iOS radio button on different platforms 1`] = ` "overflow": "hidden", }, { - "borderRadius": 18, - "padding": 6, + "borderRadius": 20, + "padding": 8, }, ] } @@ -540,7 +558,6 @@ exports[`renders unchecked 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -628,10 +645,17 @@ exports[`renders unchecked 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -645,8 +669,8 @@ exports[`renders unchecked 1`] = ` "overflow": "hidden", }, { - "borderRadius": 18, - "padding": 6, + "borderRadius": 20, + "padding": 8, }, ] } diff --git a/src/components/__tests__/SegmentedButton.test.tsx b/src/components/__tests__/SegmentedButton.test.tsx index 47134613ba..3d90abfd69 100644 --- a/src/components/__tests__/SegmentedButton.test.tsx +++ b/src/components/__tests__/SegmentedButton.test.tsx @@ -515,3 +515,45 @@ describe('labelStyle is handled', () => { }); }); }); + +describe('hitSlop', () => { + it('expands up to the 48dp minimum when enabled', async () => { + await render( + {}} + /> + ); + + // (48 - (2 * 9dp default padding + 18dp icon)) / 2 top/bottom, none + // horizontally + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('walking-button').props.hitSlop).toEqual({ + top: 6, + bottom: 6, + left: 0, + right: 0, + }); + }); + + it('gives a disabled button no hitSlop of its own', async () => { + await render( + {}} + /> + ); + + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('walking-button').props.hitSlop).toBeUndefined(); + }); +}); diff --git a/src/components/__tests__/Switch.test.tsx b/src/components/__tests__/Switch.test.tsx index ceff77a0e6..170ce15dbe 100644 --- a/src/components/__tests__/Switch.test.tsx +++ b/src/components/__tests__/Switch.test.tsx @@ -24,6 +24,26 @@ describe('Switch render', () => { ).toMatchSnapshot(); }); + it('expands hitSlop up to the 48dp minimum when enabled', async () => { + await render(); + + // (48 - 40) / 2 top/bottom, none horizontally: the track is already wider + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('switch').props.hitSlop).toEqual({ + top: 4, + bottom: 4, + left: 0, + right: 0, + }); + }); + + it('gives a disabled switch no hitSlop of its own', async () => { + await render(); + + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('switch').props.hitSlop).toBeUndefined(); + }); + it('renders with checked icon', async () => { expect( (await render()).toJSON() diff --git a/src/components/__tests__/TouchableRipple.test.tsx b/src/components/__tests__/TouchableRipple.test.tsx index fea4c58136..173906eb04 100644 --- a/src/components/__tests__/TouchableRipple.test.tsx +++ b/src/components/__tests__/TouchableRipple.test.tsx @@ -1,4 +1,3 @@ -import * as React from 'react'; import { Platform, Text } from 'react-native'; import type { GestureResponderEvent } from 'react-native'; @@ -97,206 +96,56 @@ describe('TouchableRipple', () => { }); }); - describe('minimum interactive size', () => { - const layout = (width: number, height: number) => ({ - nativeEvent: { layout: { width, height, x: 0, y: 0 } }, - }); - + describe('hitSlop', () => { // hitSlop has no user-visible effect here, the renderer does not lay views - // out or hit-test them. Real behaviour is checked on device; this only stops - // the props being dropped. + // out or hit-test them. This only stops the prop being dropped. /* eslint-disable no-restricted-syntax */ const hitSlopOf = () => screen.getByTestId('touchable').props.hitSlop; - const onLayoutOf = () => screen.getByTestId('touchable').props.onLayout; /* eslint-enable no-restricted-syntax */ - const renderTouchable = async (props = {}) => { + it('is not enforced or defaulted: the primitive does not measure', async () => { await render( - {}} {...props}> + {}}> Button ); - return screen.getByTestId('touchable'); - }; - - const fireLayout = async (width: number, height: number) => { - await act(async () => { - await fireEvent( - screen.getByTestId('touchable'), - 'layout', - layout(width, height) - ); - }); - }; - - it('expands a small target out to the minimum interactive size', async () => { - await renderTouchable(); - expect(hitSlopOf()).toBeUndefined(); - - await fireLayout(32, 32); - - // (48 - 32) / 2 on every side - expect(hitSlopOf()).toEqual({ top: 8, bottom: 8, left: 8, right: 8 }); - }); - - it('expands each axis independently', async () => { - await renderTouchable(); - - await fireLayout(40, 100); - - expect(hitSlopOf()).toEqual({ top: 0, bottom: 0, left: 4, right: 4 }); - }); - - it('leaves a target that is already big enough alone', async () => { - await renderTouchable(); - - await fireLayout(48, 48); expect(hitSlopOf()).toBeUndefined(); }); - it('lets a caller-supplied hitSlop win', async () => { - await renderTouchable({ hitSlop: 2 }); - - await fireLayout(32, 32); - - expect(hitSlopOf()).toBe(2); - }); - - it('does not expand a touchable with no touch handlers', async () => { + it('passes a caller-supplied hitSlop straight through', async () => { await render( - - Not a control - - ); - - expect(hitSlopOf()).toBeUndefined(); - }); - - // Measuring and applying are separate. RN emits onLayout on mount and on - // layout change, so measuring only once interactive would mean no event ever - // arrives and the target stays small. - it('measures even while it cannot be pressed', async () => { - await renderTouchable({ disabled: true }); - - expect(onLayoutOf()).toEqual(expect.any(Function)); - expect(hitSlopOf()).toBeUndefined(); - }); - - it('does not expand a disabled touchable', async () => { - await renderTouchable({ disabled: true }); - - await fireLayout(32, 32); - - expect(hitSlopOf()).toBeUndefined(); - }); - - it('keeps the measurement across losing and regaining interactivity', async () => { - const Harness = ({ disabled }: { disabled: boolean }) => ( {}} + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} > Button ); - const view = await render(); - const expanded = { top: 8, bottom: 8, left: 8, right: 8 }; - - await fireLayout(32, 32); - expect(hitSlopOf()).toEqual(expanded); - await act(async () => { - await view.rerender(); - }); - expect(hitSlopOf()).toBeUndefined(); - - // back again, with no second layout event to rely on - await act(async () => { - await view.rerender(); - }); - expect(hitSlopOf()).toEqual(expanded); + expect(hitSlopOf()).toEqual({ top: 8, bottom: 8, left: 8, right: 8 }); }); - it('expands once a caller-supplied hitSlop is taken away', async () => { - const Harness = ({ hitSlop }: { hitSlop?: number }) => ( + it('still calls a caller-supplied onLayout', async () => { + const onLayout = jest.fn(); + await render( {}} + onLayout={onLayout} > Button ); - const view = await render(); - await fireLayout(32, 32); - expect(hitSlopOf()).toBe(2); - - // back to the default, with no second layout event to rely on await act(async () => { - await view.rerender(); + await fireEvent(screen.getByTestId('touchable'), 'layout', { + nativeEvent: { layout: { width: 32, height: 32, x: 0, y: 0 } }, + }); }); - expect(hitSlopOf()).toEqual({ top: 8, bottom: 8, left: 8, right: 8 }); - }); - - it('still calls a caller-supplied onLayout', async () => { - const onLayout = jest.fn(); - await renderTouchable({ onLayout }); - - await fireLayout(32, 32); expect(onLayout).toHaveBeenCalledTimes(1); }); - - describe('render cost', () => { - // TouchableRipple renders everywhere, so the cost of measuring is worth - // pinning down. - const withProfiler = async () => { - const commits: string[] = []; - await render( - commits.push(phase)} - > - {}}> - Button - - - ); - return commits; - }; - - it('costs no extra render when the target is already big enough', async () => { - const commits = await withProfiler(); - expect(commits).toEqual(['mount']); - - await fireLayout(56, 56); - - // the updater returned the identical value, so React bails out - expect(commits).toEqual(['mount']); - }); - - it('costs one extra render when the target is too small', async () => { - const commits = await withProfiler(); - - await fireLayout(32, 32); - - expect(commits).toEqual(['mount', 'update']); - }); - - it('settles after a repeated layout at the same size', async () => { - const commits = await withProfiler(); - - await fireLayout(32, 32); - await fireLayout(32, 32); - await fireLayout(32, 32); - - // React renders once more before it can bail out on an unchanged value, - // then stops. Three more layout events, one more render. - expect(commits).toEqual(['mount', 'update', 'update']); - }); - }); }); }); diff --git a/src/components/__tests__/TouchableRippleWeb.test.tsx b/src/components/__tests__/TouchableRippleWeb.test.tsx index ccb07f18eb..a3e811a390 100644 --- a/src/components/__tests__/TouchableRippleWeb.test.tsx +++ b/src/components/__tests__/TouchableRippleWeb.test.tsx @@ -33,7 +33,9 @@ describe('TouchableRipple (web)', () => { return Array.isArray(style) ? Object.assign({}, ...style.flat()) : style; }; - it('renders a minimum sized touch target for an interactive touchable', async () => { + it("renders a touch target matching the touchable's own bounds by default", async () => { + // No minimum is enforced here: the primitive does not measure, so with no + // caller-supplied `hitSlop` the target is exactly the touchable's bounds. await render( {}}> Button @@ -41,12 +43,12 @@ describe('TouchableRipple (web)', () => { ); expect(screen.getByTestId(TARGET, HIDDEN)).toBeOnTheScreen(); - expect(styleOf(TARGET)).toMatchObject({ + expect(styleOf(TARGET)).toEqual({ position: 'absolute', - minWidth: 48, - minHeight: 48, - width: '100%', - height: '100%', + top: 0, + bottom: 0, + left: 0, + right: 0, }); }); diff --git a/src/components/__tests__/__snapshots__/Banner.test.tsx.snap b/src/components/__tests__/__snapshots__/Banner.test.tsx.snap index b9908258e6..71e77e2936 100644 --- a/src/components/__tests__/__snapshots__/Banner.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Banner.test.tsx.snap @@ -300,7 +300,6 @@ exports[`render visible banner, with custom theme 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -910,7 +909,6 @@ exports[`renders visible banner, with action buttons and with image 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1302,7 +1300,6 @@ exports[`renders visible banner, with action buttons and without image 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1510,7 +1507,6 @@ exports[`renders visible banner, with action buttons and without image 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/Button.test.tsx.snap b/src/components/__tests__/__snapshots__/Button.test.tsx.snap index 12b3f5262e..628087c74b 100644 --- a/src/components/__tests__/__snapshots__/Button.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Button.test.tsx.snap @@ -2666,7 +2666,6 @@ exports[`renders button with an accessibility hint 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2872,7 +2871,6 @@ exports[`renders button with an accessibility label 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3077,7 +3075,6 @@ exports[`renders button with button color 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3282,7 +3279,6 @@ exports[`renders button with color 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3487,7 +3483,6 @@ exports[`renders button with custom testID 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3693,7 +3688,6 @@ exports[`renders button with icon 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3946,7 +3940,6 @@ exports[`renders button with icon in reverse order 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4201,7 +4194,6 @@ exports[`renders contained contained with mode 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4407,7 +4399,6 @@ exports[`renders disabled button 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4612,7 +4603,6 @@ exports[`renders loading button 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -5021,7 +5011,6 @@ exports[`renders outlined button with mode 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -5227,7 +5216,6 @@ exports[`renders text button by default 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -5432,7 +5420,6 @@ exports[`renders text button with mode 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap index 79ee8c5459..9bdf665a2b 100644 --- a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap @@ -118,10 +118,17 @@ exports[`renders chip with close button 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "left": 0, + "right": 0, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -155,6 +162,7 @@ exports[`renders chip with close button 1`] = ` "position": "relative", }, { + "minHeight": 32, "paddingLeft": 0, }, { @@ -482,10 +490,17 @@ exports[`renders chip with custom close button 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "left": 0, + "right": 0, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -519,6 +534,7 @@ exports[`renders chip with custom close button 1`] = ` "position": "relative", }, { + "minHeight": 32, "paddingLeft": 0, }, { @@ -846,10 +862,17 @@ exports[`renders chip with icon 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "left": 0, + "right": 0, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -883,6 +906,7 @@ exports[`renders chip with icon 1`] = ` "position": "relative", }, { + "minHeight": 32, "paddingLeft": 0, }, { @@ -1107,10 +1131,17 @@ exports[`renders chip with onPress 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "left": 0, + "right": 0, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1144,6 +1175,7 @@ exports[`renders chip with onPress 1`] = ` "position": "relative", }, { + "minHeight": 32, "paddingLeft": 0, }, { @@ -1326,7 +1358,6 @@ exports[`renders outlined disabled chip 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1360,6 +1391,7 @@ exports[`renders outlined disabled chip 1`] = ` "position": "relative", }, { + "minHeight": 32, "paddingLeft": 0, }, { @@ -1539,10 +1571,17 @@ exports[`renders selected chip 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "left": 0, + "right": 0, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1576,6 +1615,7 @@ exports[`renders selected chip 1`] = ` "position": "relative", }, { + "minHeight": 32, "paddingLeft": 0, }, { diff --git a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap index 9fc184df45..359971ce61 100644 --- a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap @@ -25,7 +25,6 @@ exports[`DataTable.Cell renders data table cell 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -401,7 +400,6 @@ exports[`DataTable.Cell renders right aligned data table cell 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -696,8 +694,16 @@ exports[`DataTable.Pagination renders data table pagination 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -727,10 +733,17 @@ exports[`DataTable.Pagination renders data table pagination 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -750,7 +763,15 @@ exports[`DataTable.Pagination renders data table pagination 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -812,8 +833,16 @@ exports[`DataTable.Pagination renders data table pagination 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -843,10 +872,17 @@ exports[`DataTable.Pagination renders data table pagination 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -866,7 +902,15 @@ exports[`DataTable.Pagination renders data table pagination 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -986,8 +1030,16 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1017,10 +1069,17 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1040,7 +1099,15 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1102,8 +1169,16 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1133,10 +1208,17 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1156,7 +1238,15 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1218,8 +1308,16 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1249,10 +1347,17 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1272,7 +1377,15 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1334,8 +1447,16 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1365,10 +1486,17 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1388,7 +1516,15 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1508,8 +1644,16 @@ exports[`DataTable.Pagination renders data table pagination with label 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1539,10 +1683,17 @@ exports[`DataTable.Pagination renders data table pagination with label 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1562,7 +1713,15 @@ exports[`DataTable.Pagination renders data table pagination with label 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1624,8 +1783,16 @@ exports[`DataTable.Pagination renders data table pagination with label 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1655,10 +1822,17 @@ exports[`DataTable.Pagination renders data table pagination with label 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1678,7 +1852,15 @@ exports[`DataTable.Pagination renders data table pagination with label 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1912,7 +2094,6 @@ exports[`DataTable.Pagination renders data table pagination with options select onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2097,8 +2278,16 @@ exports[`DataTable.Pagination renders data table pagination with options select "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -2128,10 +2317,17 @@ exports[`DataTable.Pagination renders data table pagination with options select centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2151,7 +2347,15 @@ exports[`DataTable.Pagination renders data table pagination with options select "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -2213,8 +2417,16 @@ exports[`DataTable.Pagination renders data table pagination with options select "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -2244,10 +2456,17 @@ exports[`DataTable.Pagination renders data table pagination with options select centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2267,7 +2486,15 @@ exports[`DataTable.Pagination renders data table pagination with options select "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -2329,8 +2556,16 @@ exports[`DataTable.Pagination renders data table pagination with options select "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -2360,10 +2595,17 @@ exports[`DataTable.Pagination renders data table pagination with options select centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2383,7 +2625,15 @@ exports[`DataTable.Pagination renders data table pagination with options select "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -2445,8 +2695,16 @@ exports[`DataTable.Pagination renders data table pagination with options select "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -2476,10 +2734,17 @@ exports[`DataTable.Pagination renders data table pagination with options select centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2499,7 +2764,15 @@ exports[`DataTable.Pagination renders data table pagination with options select "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", diff --git a/src/components/__tests__/__snapshots__/DrawerItem.test.tsx.snap b/src/components/__tests__/__snapshots__/DrawerItem.test.tsx.snap index 980dd7897b..4823f954c1 100644 --- a/src/components/__tests__/__snapshots__/DrawerItem.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DrawerItem.test.tsx.snap @@ -26,7 +26,6 @@ exports[`renders DrawerItem with icon 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -186,7 +185,6 @@ exports[`renders active DrawerItem 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -346,7 +344,6 @@ exports[`renders basic DrawerItem 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/FAB.test.tsx.snap b/src/components/__tests__/__snapshots__/FAB.test.tsx.snap index a3cee84a46..4b06302358 100644 --- a/src/components/__tests__/__snapshots__/FAB.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/FAB.test.tsx.snap @@ -142,7 +142,6 @@ exports[`renders FAB large size 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -382,7 +381,6 @@ exports[`renders FAB medium size 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -622,7 +620,6 @@ exports[`renders FAB transitioning to not visible 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -862,7 +859,6 @@ exports[`renders FAB transitioning to visible 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1103,7 +1099,6 @@ exports[`renders FAB with aria-label 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1343,7 +1338,6 @@ exports[`renders FAB with containerColor and contentColor overrides 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1583,7 +1577,6 @@ exports[`renders FAB with containerColor override 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1823,7 +1816,6 @@ exports[`renders FAB with default props 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2063,7 +2055,6 @@ exports[`renders FAB with primary variant 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2303,7 +2294,6 @@ exports[`renders FAB with secondary variant 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2543,7 +2533,6 @@ exports[`renders FAB with tertiary variant 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2783,7 +2772,6 @@ exports[`renders FAB with tonalSecondary variant 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3023,7 +3011,6 @@ exports[`renders FAB with tonalTertiary variant 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/FABExtended.test.tsx.snap b/src/components/__tests__/__snapshots__/FABExtended.test.tsx.snap index f871c16538..644a640c5a 100644 --- a/src/components/__tests__/__snapshots__/FABExtended.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/FABExtended.test.tsx.snap @@ -144,7 +144,6 @@ exports[`renders extended FAB collapsed 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -471,7 +470,6 @@ exports[`renders extended FAB expanded 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -798,7 +796,6 @@ exports[`renders extended FAB large size 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1125,7 +1122,6 @@ exports[`renders extended FAB medium size 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1452,7 +1448,6 @@ exports[`renders extended FAB not visible 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1779,7 +1774,6 @@ exports[`renders extended FAB transitioning to collapsed 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/FABMenu.test.tsx.snap b/src/components/__tests__/__snapshots__/FABMenu.test.tsx.snap index e9d87ba84b..8b66657112 100644 --- a/src/components/__tests__/__snapshots__/FABMenu.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/FABMenu.test.tsx.snap @@ -123,7 +123,6 @@ exports[`renders FAB.Menu closed 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -300,7 +299,6 @@ exports[`renders FAB.Menu closed 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -608,7 +606,6 @@ exports[`renders FAB.Menu closed 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -903,7 +900,6 @@ exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1080,7 +1076,6 @@ exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1388,7 +1383,6 @@ exports[`renders FAB.Menu not expanded when trigger is not visible 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1683,7 +1677,6 @@ exports[`renders FAB.Menu open 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1860,7 +1853,6 @@ exports[`renders FAB.Menu open 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2168,7 +2160,6 @@ exports[`renders FAB.Menu open 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2463,7 +2454,6 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2640,7 +2630,6 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2817,7 +2806,6 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2994,7 +2982,6 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3171,7 +3158,6 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3348,7 +3334,6 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3656,7 +3641,6 @@ exports[`renders FAB.Menu with 6 items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -3952,7 +3936,6 @@ exports[`renders FAB.Menu with center alignment 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4129,7 +4112,6 @@ exports[`renders FAB.Menu with center alignment 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4437,7 +4419,6 @@ exports[`renders FAB.Menu with center alignment 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4732,7 +4713,6 @@ exports[`renders FAB.Menu with items having icons 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -4939,7 +4919,6 @@ exports[`renders FAB.Menu with items having icons 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -5277,7 +5256,6 @@ exports[`renders FAB.Menu with items having icons 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -5572,7 +5550,6 @@ exports[`renders FAB.Menu with start alignment 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -5749,7 +5726,6 @@ exports[`renders FAB.Menu with start alignment 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -6057,7 +6033,6 @@ exports[`renders FAB.Menu with start alignment 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/IconButton.test.tsx.snap b/src/components/__tests__/__snapshots__/IconButton.test.tsx.snap index fcc08e35c4..4afccf1c0e 100644 --- a/src/components/__tests__/__snapshots__/IconButton.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/IconButton.test.tsx.snap @@ -14,8 +14,16 @@ exports[`renders disabled icon button 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -47,7 +55,6 @@ exports[`renders disabled icon button 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -67,7 +74,15 @@ exports[`renders disabled icon button 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -132,8 +147,16 @@ exports[`renders icon button by default 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -162,10 +185,17 @@ exports[`renders icon button by default 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -185,7 +215,15 @@ exports[`renders icon button by default 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -250,8 +288,16 @@ exports[`renders icon button with color 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -280,10 +326,17 @@ exports[`renders icon button with color 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -303,7 +356,15 @@ exports[`renders icon button with color 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -491,8 +552,16 @@ exports[`renders icon button with size 1`] = ` "width": 46, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 23, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -521,10 +590,17 @@ exports[`renders icon button with size 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 1, + "left": 1, + "right": 1, + "top": 1, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -544,7 +620,15 @@ exports[`renders icon button with size 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 23, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -732,8 +816,16 @@ exports[`renders icon change animated 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -762,10 +854,17 @@ exports[`renders icon change animated 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -785,7 +884,15 @@ exports[`renders icon change animated 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", diff --git a/src/components/__tests__/__snapshots__/ListAccordion.test.tsx.snap b/src/components/__tests__/__snapshots__/ListAccordion.test.tsx.snap index 9a67eff123..c01fcf856a 100644 --- a/src/components/__tests__/__snapshots__/ListAccordion.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/ListAccordion.test.tsx.snap @@ -33,7 +33,6 @@ exports[`renders expanded accordion 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -182,7 +181,6 @@ exports[`renders expanded accordion 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -299,7 +297,6 @@ exports[`renders list accordion with children 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -507,7 +504,6 @@ exports[`renders list accordion with custom title and description styles 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -708,7 +704,6 @@ exports[`renders list accordion with left items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -916,7 +911,6 @@ exports[`renders multiline list accordion 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap b/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap index 3486b7bed2..6e949d55da 100644 --- a/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap @@ -169,7 +169,6 @@ exports[`renders list item with custom description 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -378,10 +377,17 @@ exports[`renders list item with custom description 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "left": 0, + "right": 0, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -415,6 +421,7 @@ exports[`renders list item with custom description 1`] = ` "position": "relative", }, { + "minHeight": 32, "paddingLeft": 0, }, { @@ -551,7 +558,6 @@ exports[`renders list item with custom title and description styles 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -696,7 +702,6 @@ exports[`renders list item with left and right items 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -886,7 +891,6 @@ exports[`renders list item with left item 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1042,7 +1046,6 @@ exports[`renders list item with right item 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1154,7 +1157,6 @@ exports[`renders list item with title and description 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1295,7 +1297,6 @@ exports[`renders with a description with typeof number 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/ListSection.test.tsx.snap b/src/components/__tests__/__snapshots__/ListSection.test.tsx.snap index 94e51ef9f9..a64f2e3662 100644 --- a/src/components/__tests__/__snapshots__/ListSection.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/ListSection.test.tsx.snap @@ -477,7 +477,6 @@ exports[`renders list section with custom title style 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -630,7 +629,6 @@ exports[`renders list section with custom title style 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1237,7 +1235,6 @@ exports[`renders list section with subheader 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1390,7 +1387,6 @@ exports[`renders list section with subheader 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1957,7 +1953,6 @@ exports[`renders list section without subheader 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -2110,7 +2105,6 @@ exports[`renders list section without subheader 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/Menu.test.tsx.snap b/src/components/__tests__/__snapshots__/Menu.test.tsx.snap index 8dbf3f2d26..3e059001aa 100644 --- a/src/components/__tests__/__snapshots__/Menu.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Menu.test.tsx.snap @@ -131,7 +131,6 @@ exports[`renders not visible menu 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -352,7 +351,6 @@ exports[`renders visible menu 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -670,7 +668,6 @@ exports[`renders visible menu 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -793,7 +790,6 @@ exports[`renders visible menu 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/MenuItem.test.tsx.snap b/src/components/__tests__/__snapshots__/MenuItem.test.tsx.snap index b3083b43d2..c316a38599 100644 --- a/src/components/__tests__/__snapshots__/MenuItem.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/MenuItem.test.tsx.snap @@ -26,7 +26,6 @@ exports[`Menu Item renders menu item 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -189,7 +188,6 @@ exports[`Menu Item renders menu item 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -352,7 +350,6 @@ exports[`Menu Item renders menu item 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -515,7 +512,6 @@ exports[`Menu Item renders menu item 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -678,7 +674,6 @@ exports[`Menu Item renders menu item 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap b/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap index d989dea1be..82d905d737 100644 --- a/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap @@ -103,8 +103,16 @@ exports[`activity indicator snapshot test 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -134,10 +142,17 @@ exports[`activity indicator snapshot test 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -157,7 +172,15 @@ exports[`activity indicator snapshot test 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -943,8 +966,16 @@ exports[`renders with placeholder 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -974,10 +1005,17 @@ exports[`renders with placeholder 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -997,7 +1035,15 @@ exports[`renders with placeholder 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1107,8 +1153,16 @@ exports[`renders with placeholder 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1138,10 +1192,17 @@ exports[`renders with placeholder 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1161,7 +1222,15 @@ exports[`renders with placeholder 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1317,8 +1386,16 @@ exports[`renders with text 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1348,10 +1425,17 @@ exports[`renders with text 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1371,7 +1455,15 @@ exports[`renders with text 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1477,8 +1569,16 @@ exports[`renders with text 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1508,10 +1608,17 @@ exports[`renders with text 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1531,7 +1638,15 @@ exports[`renders with text 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", diff --git a/src/components/__tests__/__snapshots__/SegmentedButton.test.tsx.snap b/src/components/__tests__/__snapshots__/SegmentedButton.test.tsx.snap index 5fd65d1b31..2c017e23a4 100644 --- a/src/components/__tests__/__snapshots__/SegmentedButton.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/SegmentedButton.test.tsx.snap @@ -56,10 +56,17 @@ exports[`renders segmented button 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -183,10 +190,17 @@ exports[`renders segmented button 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/Snackbar.test.tsx.snap b/src/components/__tests__/__snapshots__/Snackbar.test.tsx.snap index 42f4bf648d..353efa0fed 100644 --- a/src/components/__tests__/__snapshots__/Snackbar.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Snackbar.test.tsx.snap @@ -600,7 +600,6 @@ exports[`renders snackbar with action button 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/src/components/__tests__/__snapshots__/Switch.test.tsx.snap b/src/components/__tests__/__snapshots__/Switch.test.tsx.snap index e38a5145e1..877ce2a4c4 100644 --- a/src/components/__tests__/__snapshots__/Switch.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Switch.test.tsx.snap @@ -446,6 +446,14 @@ exports[`Switch render renders off 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 0, + "right": 0, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -644,6 +652,14 @@ exports[`Switch render renders on 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 0, + "right": 0, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -822,6 +838,14 @@ exports[`Switch render renders with checked icon 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 0, + "right": 0, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -1071,6 +1095,14 @@ exports[`Switch render renders with per-state icons 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 0, + "right": 0, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} diff --git a/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap b/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap index bca474a072..25bbdc381c 100644 --- a/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap @@ -157,8 +157,16 @@ exports[`renders filled TextInput with TextInput.Icon accessories 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -198,11 +206,18 @@ exports[`renders filled TextInput with TextInput.Icon accessories 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } multiline={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -222,7 +237,15 @@ exports[`renders filled TextInput with TextInput.Icon accessories 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -344,8 +367,16 @@ exports[`renders filled TextInput with TextInput.Icon accessories 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -385,11 +416,18 @@ exports[`renders filled TextInput with TextInput.Icon accessories 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } multiline={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -409,7 +447,15 @@ exports[`renders filled TextInput with TextInput.Icon accessories 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -627,8 +673,16 @@ exports[`renders filled TextInput with TextInput.Icon accessories when error is "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -668,11 +722,18 @@ exports[`renders filled TextInput with TextInput.Icon accessories when error is centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } multiline={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -692,7 +753,15 @@ exports[`renders filled TextInput with TextInput.Icon accessories when error is "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -814,8 +883,16 @@ exports[`renders filled TextInput with TextInput.Icon accessories when error is "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -855,11 +932,18 @@ exports[`renders filled TextInput with TextInput.Icon accessories when error is centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } multiline={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -879,7 +963,15 @@ exports[`renders filled TextInput with TextInput.Icon accessories when error is "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1275,8 +1367,16 @@ exports[`renders outlined TextInput with TextInput.Icon accessories 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -1316,11 +1416,18 @@ exports[`renders outlined TextInput with TextInput.Icon accessories 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } multiline={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1340,7 +1447,15 @@ exports[`renders outlined TextInput with TextInput.Icon accessories 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1462,8 +1577,16 @@ exports[`renders outlined TextInput with TextInput.Icon accessories 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -1503,11 +1626,18 @@ exports[`renders outlined TextInput with TextInput.Icon accessories 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } multiline={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1527,7 +1657,15 @@ exports[`renders outlined TextInput with TextInput.Icon accessories 1`] = ` "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1725,8 +1863,16 @@ exports[`renders outlined TextInput with TextInput.Icon accessories when error i "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -1766,11 +1912,18 @@ exports[`renders outlined TextInput with TextInput.Icon accessories when error i centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } multiline={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1790,7 +1943,15 @@ exports[`renders outlined TextInput with TextInput.Icon accessories when error i "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -1912,8 +2073,16 @@ exports[`renders outlined TextInput with TextInput.Icon accessories when error i "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -1953,11 +2122,18 @@ exports[`renders outlined TextInput with TextInput.Icon accessories when error i centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } multiline={false} onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1977,7 +2153,15 @@ exports[`renders outlined TextInput with TextInput.Icon accessories when error i "justifyContent": "center", }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", diff --git a/src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap b/src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap index 7936342952..018760b28d 100644 --- a/src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap @@ -14,8 +14,16 @@ exports[`renders disabled toggle button 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", - "borderRadius": 4, + "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -57,7 +65,6 @@ exports[`renders disabled toggle button 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -75,7 +82,15 @@ exports[`renders disabled toggle button 1`] = ` "justifyContent": "center", }, { - "borderRadius": 4, + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -140,8 +155,16 @@ exports[`renders toggle button 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", - "borderRadius": 4, + "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -180,10 +203,17 @@ exports[`renders toggle button 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -201,7 +231,15 @@ exports[`renders toggle button 1`] = ` "justifyContent": "center", }, { - "borderRadius": 4, + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", @@ -266,8 +304,16 @@ exports[`renders unchecked toggle button 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", - "borderRadius": 4, + "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -309,7 +355,6 @@ exports[`renders unchecked toggle button 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} - onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -327,7 +372,15 @@ exports[`renders unchecked toggle button 1`] = ` "justifyContent": "center", }, { - "borderRadius": 4, + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, }, { "overflow": "hidden", diff --git a/src/utils/getMinInteractiveSizeHitSlop.ts b/src/utils/getMinInteractiveSizeHitSlop.ts new file mode 100644 index 0000000000..3317a79070 --- /dev/null +++ b/src/utils/getMinInteractiveSizeHitSlop.ts @@ -0,0 +1,40 @@ +import type { Insets } from 'react-native'; + +import { tokens } from '../theme/tokens'; + +const { minInteractiveSize } = tokens.md.sys.state; + +/** + * Slop needed to bring a fixed-size element up to the 48dp minimum + * interactive target, expanding outward rather than resizing. Pass the + * element's own rendered width and/or height; omit an axis that is already + * big enough on its own (e.g. a pill that grows with its label) to opt it out + * of slop entirely. Returns `undefined` when there is nothing to add, so that + * case does not create a new object on every call. + * @see https://developer.android.com/develop/ui/compose/accessibility/api-defaults + */ +const getMinInteractiveSizeHitSlop = ({ + width, + height, +}: { + width?: number; + height?: number; +}): Insets | undefined => { + const horizontal = + width === undefined ? 0 : Math.max(0, (minInteractiveSize - width) / 2); + const vertical = + height === undefined ? 0 : Math.max(0, (minInteractiveSize - height) / 2); + + if (horizontal === 0 && vertical === 0) { + return undefined; + } + + return { + top: vertical, + bottom: vertical, + left: horizontal, + right: horizontal, + }; +}; + +export default getMinInteractiveSizeHitSlop; From 18bcb20ea86396cc690faaee516436f65c331ca1 Mon Sep 17 00:00:00 2001 From: Konrad Rydygier Date: Tue, 8 Sep 2026 10:19:27 +0200 Subject: [PATCH 5/9] fix: keep Chip JSDoc attached to component declaration The component doc comment ended up separated from `const Chip =` by the hitSlop helper constants, so the docs generator could no longer find it. --- src/components/Chip/Chip.tsx | 46 ++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx index 7d0a697a7c..13c4ec95c0 100644 --- a/src/components/Chip/Chip.tsx +++ b/src/components/Chip/Chip.tsx @@ -158,29 +158,6 @@ export type Props = Omit & { ref?: React.Ref; }; -/** - * Chips are compact elements that can represent inputs, attributes, or actions. - * They can have an icon or avatar on the left, and a close button icon on the right. - * They are typically used to: - *
    - *
  • Present multiple options
  • - *
  • Represent attributes active or chosen
  • - *
  • Present filter options
  • - *
  • Trigger actions related to primary content
  • - *
- * - * ## Usage - * ```js - * import * as React from 'react'; - * import { Chip } from 'react-native-paper'; - * - * const MyComponent = () => ( - * console.log('Pressed')}>Example Chip - * ); - * - * export default MyComponent; - * ``` - */ /** * Room the chip reserves on its right for the close button, which fills all of * it, so the body stops here and the two divide the chip. @@ -210,6 +187,29 @@ const CHIP_BODY_HIT_SLOP = getMinInteractiveSizeHitSlop({ height: CHIP_BODY_HEIGHT, }); +/** + * Chips are compact elements that can represent inputs, attributes, or actions. + * They can have an icon or avatar on the left, and a close button icon on the right. + * They are typically used to: + *
    + *
  • Present multiple options
  • + *
  • Represent attributes active or chosen
  • + *
  • Present filter options
  • + *
  • Trigger actions related to primary content
  • + *
+ * + * ## Usage + * ```js + * import * as React from 'react'; + * import { Chip } from 'react-native-paper'; + * + * const MyComponent = () => ( + * console.log('Pressed')}>Example Chip + * ); + * + * export default MyComponent; + * ``` + */ const Chip = ({ mode = 'flat', children, From 1f6aa1ec6b910d0fedc828847ae39bfdfb630d88 Mon Sep 17 00:00:00 2001 From: Konrad Rydygier Date: Tue, 8 Sep 2026 10:23:09 +0200 Subject: [PATCH 6/9] test: pass explicit testID after removal of hardcoded defaults main removed the hardcoded default testIDs from Chip and IconButton (#5088). Guard Chip's close-icon testID the same way its container already is, and pass explicit testID props in the hitSlop/close-icon tests that relied on the old defaults. --- src/components/Chip/Chip.tsx | 2 +- src/components/__tests__/Chip.test.tsx | 2 +- src/components/__tests__/IconButton.test.tsx | 8 +++++--- src/components/__tests__/__snapshots__/Chip.test.tsx.snap | 2 -- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx index 13c4ec95c0..9c46dd4928 100644 --- a/src/components/Chip/Chip.tsx +++ b/src/components/Chip/Chip.tsx @@ -425,7 +425,7 @@ const Chip = ({ style={styles.closeButton} > {closeIcon ? ( diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx index 5075db441e..dd7dc85d5a 100644 --- a/src/components/__tests__/Chip.test.tsx +++ b/src/components/__tests__/Chip.test.tsx @@ -358,7 +358,7 @@ describe('close affordance', () => { it('keeps the close glyph pinned right so it does not drift', async () => { await render( - {}} onClose={() => {}}> + {}} onClose={() => {}}> Example ); diff --git a/src/components/__tests__/IconButton.test.tsx b/src/components/__tests__/IconButton.test.tsx index 5e2e181ba7..872a68fe00 100644 --- a/src/components/__tests__/IconButton.test.tsx +++ b/src/components/__tests__/IconButton.test.tsx @@ -47,7 +47,7 @@ it('renders disabled icon button', async () => { }); it('expands hitSlop up to the 48dp minimum for a button smaller than that', async () => { - await render(); + await render(); // (48 - 40) / 2 on every side, for the default 24dp icon plus 8dp padding // eslint-disable-next-line no-restricted-syntax @@ -60,14 +60,16 @@ it('expands hitSlop up to the 48dp minimum for a button smaller than that', asyn }); it('gives a disabled button no hitSlop of its own', async () => { - await render(); + await render(); // eslint-disable-next-line no-restricted-syntax expect(screen.getByTestId('icon-button').props.hitSlop).toBeUndefined(); }); it('lets a caller-supplied hitSlop win even while disabled', async () => { - await render(); + await render( + + ); // eslint-disable-next-line no-restricted-syntax expect(screen.getByTestId('icon-button').props.hitSlop).toBe(2); diff --git a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap index 9bdf665a2b..cb49e687a2 100644 --- a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap @@ -335,7 +335,6 @@ exports[`renders chip with close button 1`] = ` }, ] } - testID="chip-close-icon" > Date: Thu, 10 Sep 2026 12:27:40 +0200 Subject: [PATCH 7/9] fix: address review feedback on 48dp touch targets --- docs/6.x/docs/guides/migration.md | 32 ++++ eslint.config.mjs | 1 + src/components/Button/Button.tsx | 7 +- src/components/Button/utils.tsx | 16 +- src/components/Checkbox/Checkbox.tsx | 17 ++- src/components/Chip/Chip.tsx | 36 ++++- src/components/Drawer/DrawerItem.tsx | 1 + src/components/FAB/Menu.tsx | 1 + src/components/IconButton/IconButton.tsx | 27 +++- .../RadioButton/RadioButtonAndroid.tsx | 7 +- src/components/RadioButton/RadioButtonIOS.tsx | 7 +- .../SegmentedButtons/SegmentedButtonItem.tsx | 10 +- src/components/SegmentedButtons/utils.ts | 18 ++- src/components/ToggleButton/ToggleButton.tsx | 31 +++- .../ToggleButton/ToggleButtonRow.tsx | 69 ++++++--- .../TouchableRipple.native.tsx | 77 +++++----- .../TouchableRipple/TouchableRipple.tsx | 126 +++++++++------- src/components/TouchableRipple/utils.ts | 12 ++ .../Appbar/__snapshots__/Appbar.test.tsx.snap | 16 ++ src/components/__tests__/IconButton.test.tsx | 24 +++ .../__tests__/ToggleButton.test.tsx | 104 ++++++++++++- .../__tests__/TouchableRipple.test.tsx | 5 +- .../__tests__/TouchableRippleWeb.test.tsx | 141 +++++++++++++----- .../__snapshots__/Chip.test.tsx.snap | 16 ++ .../__snapshots__/DataTable.test.tsx.snap | 48 ++++++ .../__snapshots__/IconButton.test.tsx.snap | 20 +++ .../__snapshots__/Searchbar.test.tsx.snap | 20 +++ .../__snapshots__/TextInput.test.tsx.snap | 32 ++++ .../__snapshots__/ToggleButton.test.tsx.snap | 27 ++-- src/theme/tokens/sys/state.ts | 7 - src/utils/getMinInteractiveSizeHitSlop.ts | 12 +- 31 files changed, 743 insertions(+), 224 deletions(-) diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md index 35a8f74830..8680ed79f3 100644 --- a/docs/6.x/docs/guides/migration.md +++ b/docs/6.x/docs/guides/migration.md @@ -188,6 +188,38 @@ e.g.: - The default elevation changed from level `1` to level `3`. - The `style` prop no longer configures the background color or border radius. You can override `theme.colors.surfaceContainerHigh` and `theme.shapes.corner.extraLarge` using the `theme` prop instead. +### Chip + +The close button (`onClose`) now fills the entire trailing 34dp column reserved for it, matching Material Design 3's touch target guidance, instead of only its 24x18 icon. Taps near the top or bottom of that column, which used to fall through to the chip's own `onPress`, now activate `onClose` instead. + +### TouchableRipple + +- `borderless` no longer clips the touchable's own content on web; it only clips the ripple itself, in its own container. A child that needs a clipped or rounded shape should carry that shape itself. +- Corner radius and border width set through `style` no longer shape the highlight underlay (native) or the ripple's self-clipping container (web). Pass them as dedicated props instead: + - `borderRadius` + - `borderTopLeftRadius` + - `borderTopRightRadius` + - `borderBottomLeftRadius` + - `borderBottomRightRadius` + - `borderTopStartRadius` + - `borderTopEndRadius` + - `borderBottomStartRadius` + - `borderBottomEndRadius` + - `borderWidth` (web only) + +e.g.: + +```diff + {}} +> + Content + +``` + ### TextInput The Paper 6.x `TextInput` is a complete rewrite with a new API. Import the component the same way, but note that the props and behavior have changed significantly. diff --git a/eslint.config.mjs b/eslint.config.mjs index ac6e65951f..776d90544b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -221,6 +221,7 @@ export default defineConfig( 'src/components/__tests__/Appbar/Appbar.test.tsx', 'src/components/__tests__/Dialog.test.tsx', 'src/components/__tests__/Searchbar.test.tsx', + 'src/components/__tests__/TouchableRippleWeb.test.tsx', ], rules: { 'testing-library/no-node-access': 'off', diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 82b327dc74..36ec075c39 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -262,6 +262,10 @@ const Button = ({ }); const touchableStyle = { borderRadius }; + const touchableRippleStyle = getButtonTouchableRippleStyle( + touchableStyle, + borderWidth + ); const { color: customLabelColor, fontSize: customLabelSize } = StyleSheet.flatten(labelStyle) || {}; @@ -334,7 +338,8 @@ const Button = ({ accessible={accessible} hitSlop={hitSlop} disabled={disabled} - style={getButtonTouchableRippleStyle(touchableStyle, borderWidth)} + style={touchableRippleStyle} + {...touchableRippleStyle} testID={testID} theme={theme} ref={touchableRef} diff --git a/src/components/Button/utils.tsx b/src/components/Button/utils.tsx index ec3503c82b..bbebfd7791 100644 --- a/src/components/Button/utils.tsx +++ b/src/components/Button/utils.tsx @@ -4,6 +4,7 @@ import { black, white } from '../../theme/colors'; import { tokens } from '../../theme/tokens'; import type { InternalTheme } from '../../theme/types'; import { splitStyles } from '../../utils/splitStyles'; +import type { BorderRadiusStyle } from '../TouchableRipple/utils'; const stateOpacity = tokens.md.sys.state.opacity; @@ -191,20 +192,7 @@ export const getButtonColors = ({ }; }; -type ViewStyleBorderRadiusStyles = Partial< - Pick< - ViewStyle, - | 'borderBottomEndRadius' - | 'borderBottomLeftRadius' - | 'borderBottomRightRadius' - | 'borderBottomStartRadius' - | 'borderTopEndRadius' - | 'borderTopLeftRadius' - | 'borderTopRightRadius' - | 'borderTopStartRadius' - | 'borderRadius' - > ->; +type ViewStyleBorderRadiusStyles = Partial; export const getButtonTouchableRippleStyle = ( style?: ViewStyle, borderWidth: number = 0 diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index 7f49338344..bdb3c9dbf2 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -79,10 +79,10 @@ const { const FOCUS_THICKNESS = tokens.md.sys.state.focusIndicator.thickness; // Focus indicator is a circular ring at the 40dp state-layer boundary. -// We don't apply `focusIndicator.outerOffset`, so the ring stays inside the 40dp -// circle. `TouchableRipple borderless` used to crop anything outside it; on web -// it no longer does, since the touchable cannot clip without clipping the touch -// target. Native still clips. Check both when revisiting the offset. +// We don't apply `focusIndicator.outerOffset`, keeping the ring inside the +// 40dp circle: whether TouchableRipple clips content past that boundary +// depends on platform and ripple mode, so staying inside it avoids relying +// on any of that. const FOCUS_RING_SIZE = STATE_LAYER_SIZE; const FOCUS_RING_RADIUS = STATE_LAYER_SIZE / 2; @@ -252,7 +252,14 @@ const Checkbox = ({ disabled={disabled} {...accessibilityProps} testID={testID} - hitSlop={rest.hitSlop ?? (disabled ? undefined : CHECKBOX_HIT_SLOP)} + hitSlop={ + rest.hitSlop !== undefined + ? rest.hitSlop + : disabled + ? undefined + : CHECKBOX_HIT_SLOP + } + borderRadius={FOCUS_RING_RADIUS} style={[ styles.tapTarget, Platform.OS === 'web' ? webNoOutline : undefined, diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx index 9c46dd4928..df6e2ebd0f 100644 --- a/src/components/Chip/Chip.tsx +++ b/src/components/Chip/Chip.tsx @@ -162,11 +162,10 @@ export type Props = Omit & { * Room the chip reserves on its right for the close button, which fills all of * it, so the body stops here and the two divide the chip. * - * MD3 splits the same way and does not give a chip's trailing action 48dp; in - * material-web it is 24x24 with no expansion. This column is wider than that and - * gets no vertical expansion, so the strips above and below belong to the body - * and a near miss activates the chip rather than deleting it. - * @see https://github.com/material-components/material-web/blob/main/chips/internal/_trailing-icon.scss + * Matches material-web's own remove button, which expands to a 48px touch + * target the same way; the 24x24 dimensions in its `_trailing-icon.scss` are + * for the ripple and focus ring, not the touch target. + * @see https://github.com/material-components/material-web/blob/main/chips/internal/_shared.scss */ const CLOSE_AFFORDANCE_WIDTH = 34; @@ -186,6 +185,9 @@ const { containerHeight: CHIP_BODY_HEIGHT } = ChipTokens; const CHIP_BODY_HIT_SLOP = getMinInteractiveSizeHitSlop({ height: CHIP_BODY_HEIGHT, }); +// The close button's own box is the same fixed height as the body, so it +// needs the same vertical slop to reach 48dp. +const CLOSE_BUTTON_WEB_TOUCH_TARGET_INSET = CHIP_BODY_HIT_SLOP?.top ?? 0; /** * Chips are compact elements that can represent inputs, attributes, or actions. @@ -323,6 +325,7 @@ const Chip = ({ borderless background={background} style={[{ borderRadius }, styles.touchable]} + borderRadius={borderRadius} onPress={onPress} onLongPress={onLongPress} onPressIn={hasPassedTouchHandler ? handlePressIn : undefined} @@ -335,7 +338,13 @@ const Chip = ({ aria-disabled={disabled} testID={testID} theme={theme} - hitSlop={hitSlop ?? (disabled ? undefined : CHIP_BODY_HIT_SLOP)} + hitSlop={ + hitSlop !== undefined + ? hitSlop + : disabled + ? undefined + : CHIP_BODY_HIT_SLOP + } > + {/* react-native-web removed `hitSlop` in 0.13.0, + so web needs a real element the browser can + hit-test instead of a native responder inset. */} + {Platform.OS === 'web' && !disabled && ( + + )} ; /** * Function to execute on press. */ onPress?: (e: GestureResponderEvent) => void; + /** + * Width of the button's inner content. Defaults to the button's size. + */ + width?: number; + /** + * Height of the button's inner content. Defaults to the button's size. + */ + height?: number; /** * Radius of every corner of the button. Defaults to a circle (half of the * button's size). Read as a plain prop rather than out of `style`, since @@ -144,6 +153,8 @@ const IconButton = ({ testID, loading = false, contentStyle, + width, + height, borderRadius, borderTopLeftRadius, borderTopRightRadius, @@ -196,13 +207,15 @@ const IconButton = ({ ...shapeStyles, }; - // Computed straight from `size`, a plain prop known at render time, rather - // than measured. `buttonSize` never changes after mount without `size` also - // changing, so there is nothing to react to. A disabled button gets no - // slop of its own, only what a caller's own `hitSlop` in `rest` supplies. + const touchableWidth = width ?? buttonSize - 2 * borderWidth; + const touchableHeight = height ?? buttonSize - 2 * borderWidth; + const hitSlop = disabled ? undefined - : getMinInteractiveSizeHitSlop({ width: buttonSize, height: buttonSize }); + : getMinInteractiveSizeHitSlop({ + width: touchableWidth, + height: touchableHeight, + }); return ( & { + borderEndWidth?: number; +}; + export const getSegmentedButtonBorderRadius = ({ segment, + borderRadius, }: { theme: InternalTheme; segment?: 'first' | 'last'; -}): ViewStyle => { + borderRadius: number; +}): SegmentBorderRadiusStyle => { if (segment === 'first') { return { + borderRadius, borderTopRightRadius: 0, borderBottomRightRadius: 0, borderEndWidth: 0, }; } else if (segment === 'last') { return { + borderRadius, borderTopLeftRadius: 0, borderBottomLeftRadius: 0, }; diff --git a/src/components/ToggleButton/ToggleButton.tsx b/src/components/ToggleButton/ToggleButton.tsx index 72f6dc3161..7c43cf0ffe 100644 --- a/src/components/ToggleButton/ToggleButton.tsx +++ b/src/components/ToggleButton/ToggleButton.tsx @@ -1,6 +1,11 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; -import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native'; +import type { + GestureResponderEvent, + Insets, + StyleProp, + ViewStyle, +} from 'react-native'; import type { AnimatedStyle } from 'react-native-reanimated'; @@ -54,8 +59,26 @@ export type Props = { * testID to be used on tests. */ testID?: string; + /** + * @optional + * Set by `ToggleButton.Row` to divide the touch targets of adjoining + * buttons; not meant to be passed directly. + */ + hitSlop?: Insets; + /** + * @optional + * Set by `ToggleButton.Row` to square off the corners shared with a + * neighbouring button; not meant to be passed directly. + */ + borderRadius?: number; + borderTopLeftRadius?: number; + borderTopRightRadius?: number; + borderBottomLeftRadius?: number; + borderBottomRightRadius?: number; }; +export const TOGGLE_BUTTON_SIZE = 42; + /** * Toggle buttons can be used to group related options. To emphasize groups of related toggle buttons, * a group should share a common container. @@ -133,11 +156,11 @@ const ToggleButton = ({ styles.content, { backgroundColor, - borderRadius, borderColor, }, style, ]} + borderRadius={borderRadius} ref={ref} theme={theme} {...rest} @@ -150,8 +173,8 @@ const ToggleButton = ({ const styles = StyleSheet.create({ content: { - width: 42, - height: 42, + width: TOGGLE_BUTTON_SIZE, + height: TOGGLE_BUTTON_SIZE, margin: 0, }, }); diff --git a/src/components/ToggleButton/ToggleButtonRow.tsx b/src/components/ToggleButton/ToggleButtonRow.tsx index 46e6aee48f..d09ed989df 100644 --- a/src/components/ToggleButton/ToggleButtonRow.tsx +++ b/src/components/ToggleButton/ToggleButtonRow.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; -import type { StyleProp, ViewStyle } from 'react-native'; +import type { Insets, StyleProp, ViewStyle } from 'react-native'; -import ToggleButton from './ToggleButton'; +import ToggleButton, { TOGGLE_BUTTON_SIZE } from './ToggleButton'; import ToggleButtonGroup from './ToggleButtonGroup'; +import getMinInteractiveSizeHitSlop from '../../utils/getMinInteractiveSizeHitSlop'; export type Props = { /** @@ -21,6 +22,34 @@ export type Props = { style?: StyleProp; }; +type Position = 'first' | 'middle' | 'last'; + +// Buttons in a row sit flush against each other, so an unrestricted hitSlop +// would have each button's expanded target reach into its neighbour's own +// visible bounds. On web, whichever button is later in the row wins that +// overlap, so a tap meant for one button's own edge could activate the other +// instead. Zero the slop on every edge shared with a neighbour; the outer +// edges (and, for a single button, every edge) keep the usual slop. +const DEFAULT_HIT_SLOP = getMinInteractiveSizeHitSlop({ + width: TOGGLE_BUTTON_SIZE, + height: TOGGLE_BUTTON_SIZE, +}); + +const HIT_SLOP_BY_POSITION: Record = + DEFAULT_HIT_SLOP + ? { + first: { ...DEFAULT_HIT_SLOP, right: 0 }, + middle: { ...DEFAULT_HIT_SLOP, left: 0, right: 0 }, + last: { ...DEFAULT_HIT_SLOP, left: 0 }, + } + : { first: undefined, middle: undefined, last: undefined }; + +const RADIUS_OVERRIDES_BY_POSITION: Record = { + first: { borderTopRightRadius: 0, borderBottomRightRadius: 0 }, + middle: { borderRadius: 0 }, + last: { borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }, +}; + /** * Toggle button row renders a group of toggle buttons in a row. * @@ -53,18 +82,29 @@ const ToggleButtonRow = ({ value, onValueChange, children, style }: Props) => { {React.Children.map(children, (child, i) => { // @ts-expect-error: TypeScript complains about child.type but it doesn't matter if (child && child.type === ToggleButton) { + const position: Position = + i === 0 ? 'first' : i === count - 1 ? 'last' : 'middle'; + // @ts-expect-error: We're sure that child is a React Element return React.cloneElement(child, { style: [ styles.button, - i === 0 - ? styles.first - : i === count - 1 - ? styles.last - : styles.middle, + position !== 'first' && styles.noLeftBorder, // @ts-expect-error: We're sure that child is a React Element child.props.style, ], + ...RADIUS_OVERRIDES_BY_POSITION[position], + hitSlop: + // @ts-expect-error: We're sure that child is a React Element + child.props.hitSlop !== undefined + ? // @ts-expect-error: We're sure that child is a React Element + child.props.hitSlop + : // @ts-expect-error: We're sure that child is a React Element + child.props.disabled + ? undefined + : count > 1 + ? HIT_SLOP_BY_POSITION[position] + : DEFAULT_HIT_SLOP, }); } @@ -84,21 +124,8 @@ const styles = StyleSheet.create({ button: { borderWidth: StyleSheet.hairlineWidth, }, - - first: { - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - }, - - middle: { - borderRadius: 0, - borderLeftWidth: 0, - }, - - last: { + noLeftBorder: { borderLeftWidth: 0, - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, }, }); diff --git a/src/components/TouchableRipple/TouchableRipple.native.tsx b/src/components/TouchableRipple/TouchableRipple.native.tsx index 5fc858b164..caeb449982 100644 --- a/src/components/TouchableRipple/TouchableRipple.native.tsx +++ b/src/components/TouchableRipple/TouchableRipple.native.tsx @@ -20,43 +20,6 @@ import hasTouchHandler from '../../utils/hasTouchHandler'; const ANDROID_VERSION_LOLLIPOP = 21; const ANDROID_VERSION_PIE = 28; -/** - * The underlay fills the touchable absolutely and has no radius of its own, so - * it paints square corners over a rounded one. A clipping ancestor used to hide - * that, and those ancestors have to stop clipping to reach into the `hitSlop`. - */ -const getUnderlayShape = (style: StyleProp): ViewStyle => { - const flat = StyleSheet.flatten(style); - - if (!flat) { - return {}; - } - - const { - borderRadius, - borderTopLeftRadius, - borderTopRightRadius, - borderBottomLeftRadius, - borderBottomRightRadius, - borderTopStartRadius, - borderTopEndRadius, - borderBottomStartRadius, - borderBottomEndRadius, - } = flat; - - return { - borderRadius, - borderTopLeftRadius, - borderTopRightRadius, - borderBottomLeftRadius, - borderBottomRightRadius, - borderTopStartRadius, - borderTopEndRadius, - borderBottomStartRadius, - borderBottomEndRadius, - }; -}; - export type Props = PressableProps & { borderless?: boolean; background?: PressableAndroidRippleConfig; @@ -72,6 +35,21 @@ export type Props = PressableProps & { style?: StyleProp; ref?: React.Ref; theme?: ThemeProp; + borderRadius?: number; + borderTopLeftRadius?: number; + borderTopRightRadius?: number; + borderBottomLeftRadius?: number; + borderBottomRightRadius?: number; + borderTopStartRadius?: number; + borderTopEndRadius?: number; + borderBottomStartRadius?: number; + borderBottomEndRadius?: number; + /** + * Web-only: widens the touch target back out past the touchable's own + * border. Accepted here too so both platforms share one `Props` type; has + * no effect on native, where `hitSlop` isn't offset from inside the border. + */ + borderWidth?: number; }; const TouchableRipple = ({ @@ -84,9 +62,32 @@ const TouchableRipple = ({ children, theme: themeOverrides, hitSlop, + borderRadius, + borderTopLeftRadius, + borderTopRightRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + borderTopStartRadius, + borderTopEndRadius, + borderBottomStartRadius, + borderBottomEndRadius, + // consumed so it does not reach the underlying Pressable; web-only, no + // native effect + borderWidth: _borderWidth, ref, ...rest }: Props) => { + const underlayShape: ViewStyle = { + borderRadius, + borderTopLeftRadius, + borderTopRightRadius, + borderBottomLeftRadius, + borderBottomRightRadius, + borderTopStartRadius, + borderTopEndRadius, + borderBottomStartRadius, + borderBottomEndRadius, + }; const theme = useInternalTheme(themeOverrides); const { rippleEffectEnabled } = React.useContext(SettingsContext); @@ -153,7 +154,7 @@ const TouchableRipple = ({ diff --git a/src/components/TouchableRipple/TouchableRipple.tsx b/src/components/TouchableRipple/TouchableRipple.tsx index 89a5c73370..edc0722ccc 100644 --- a/src/components/TouchableRipple/TouchableRipple.tsx +++ b/src/components/TouchableRipple/TouchableRipple.tsx @@ -18,37 +18,6 @@ import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../theme/types'; import hasTouchHandler from '../../utils/hasTouchHandler'; -/** - * react-native-web removed `hitSlop` in 0.13.0, so web needs a real element the - * browser can hit-test instead of a native responder inset. - * @see https://github.com/necolas/react-native-web/releases/tag/0.13.0 - */ -const getTouchTargetStyle = (hitSlop: PressableProps['hitSlop']): ViewStyle => { - // `undefined` or `null` both mean no slop: nothing for the caller to opt - // into, so the target matches the touchable's own bounds. - if (hitSlop === undefined || hitSlop === null) { - return styles.noTouchTarget; - } - - const inset = (value: number | undefined) => -(value ?? 0); - - return typeof hitSlop === 'number' - ? { - position: 'absolute', - top: inset(hitSlop), - bottom: inset(hitSlop), - left: inset(hitSlop), - right: inset(hitSlop), - } - : { - position: 'absolute', - top: inset(hitSlop.top), - bottom: inset(hitSlop.bottom), - left: inset(hitSlop.left), - right: inset(hitSlop.right), - }; -}; - export type Props = PressableProps & { /** * Whether to render the ripple outside the view bounds. @@ -111,6 +80,28 @@ export type Props = PressableProps & { * @optional */ theme?: ThemeProp; + /** + * Radius of every corner of the touchable. Native-only: it shapes the + * highlight underlay there. On web the ripple container clips itself + * regardless, so this has no effect. + */ + borderRadius?: number; + borderTopLeftRadius?: number; + borderTopRightRadius?: number; + borderBottomLeftRadius?: number; + borderBottomRightRadius?: number; + borderTopStartRadius?: number; + borderTopEndRadius?: number; + borderBottomStartRadius?: number; + borderBottomEndRadius?: number; + /** + * Width of the touchable's own border, if it draws one. Web-only: the touch + * target's absolute offsets start inside the border, not the visible outer + * edge, so this widens the target back out to it. Read as a plain prop + * rather than out of `style`, since `style` can be a function of press + * state instead of a plain object to read a value off. + */ + borderWidth?: number; }; /** @@ -149,6 +140,18 @@ const TouchableRipple = ({ children, theme: themeOverrides, hitSlop, + // consumed so they do not reach the DOM; native-only, the ripple container + // clips itself regardless of shape on web + borderRadius: _borderRadius, + borderTopLeftRadius: _borderTopLeftRadius, + borderTopRightRadius: _borderTopRightRadius, + borderBottomLeftRadius: _borderBottomLeftRadius, + borderBottomRightRadius: _borderBottomRightRadius, + borderTopStartRadius: _borderTopStartRadius, + borderTopEndRadius: _borderTopEndRadius, + borderBottomStartRadius: _borderBottomStartRadius, + borderBottomEndRadius: _borderBottomEndRadius, + borderWidth, ref, ...rest }: Props) => { @@ -336,26 +339,40 @@ const TouchableRipple = ({ typeof style === 'function' ? style(state) : style, ]} > - {(state) => ( - <> - {/* Before the children, not after. It hit-tests, so as the last - sibling it covers anything interactive inside the touchable and - takes its presses, e.g. a pressable List.Item with a control in - `right`. Ahead of them it still covers the area outside the - touchable, where there is nothing else to hit. - Nothing that cannot be pressed gets a target, same as native. */} - {!disabled && ( - - )} - {React.Children.only( - typeof children === 'function' ? children(state) : children - )} - - )} + {(state) => { + const border = borderWidth ?? 0; + const inset = (value: number | undefined) => -((value ?? 0) + border); + + const touchTargetStyle: ViewStyle | undefined = + hitSlop == null + ? undefined + : typeof hitSlop === 'number' + ? { + position: 'absolute', + top: inset(hitSlop), + bottom: inset(hitSlop), + left: inset(hitSlop), + right: inset(hitSlop), + } + : { + position: 'absolute', + top: inset(hitSlop.top), + bottom: inset(hitSlop.bottom), + left: inset(hitSlop.left), + right: inset(hitSlop.right), + }; + + return ( + <> + {!disabled && touchTargetStyle && ( + + )} + {React.Children.only( + typeof children === 'function' ? children(state) : children + )} + + ); + }} ); }; @@ -378,13 +395,6 @@ const styles = StyleSheet.create({ cursor: 'auto', }), }, - noTouchTarget: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - }, }); export default TouchableRipple; diff --git a/src/components/TouchableRipple/utils.ts b/src/components/TouchableRipple/utils.ts index 2874eafc86..e81a5036f5 100644 --- a/src/components/TouchableRipple/utils.ts +++ b/src/components/TouchableRipple/utils.ts @@ -2,6 +2,18 @@ import type { ColorValue } from 'react-native'; import type { InternalTheme } from '../../theme/types'; +export type BorderRadiusStyle = { + borderRadius?: number; + borderTopLeftRadius?: number; + borderTopRightRadius?: number; + borderBottomLeftRadius?: number; + borderBottomRightRadius?: number; + borderTopStartRadius?: number; + borderTopEndRadius?: number; + borderBottomStartRadius?: number; + borderBottomEndRadius?: number; +}; + const getUnderlayColor = ({ calculatedRippleColor, underlayColor, diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap index 3f30b0ce3e..80c909be8a 100644 --- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap +++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap @@ -283,6 +283,10 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -470,6 +474,10 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -713,6 +721,10 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -956,6 +968,10 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] diff --git a/src/components/__tests__/IconButton.test.tsx b/src/components/__tests__/IconButton.test.tsx index 872a68fe00..36c28686a1 100644 --- a/src/components/__tests__/IconButton.test.tsx +++ b/src/components/__tests__/IconButton.test.tsx @@ -75,6 +75,30 @@ it('lets a caller-supplied hitSlop win even while disabled', async () => { expect(screen.getByTestId('icon-button').props.hitSlop).toBe(2); }); +it('computes hitSlop from explicit width/height rather than the button size', async () => { + await render( + + ); + + // (48 - 20) / 2 on every side + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('icon-button').props.hitSlop).toEqual({ + top: 14, + bottom: 14, + left: 14, + right: 14, + }); +}); + +it('drops hitSlop when explicit width/height already meet the 48dp minimum', async () => { + await render( + + ); + + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('icon-button').props.hitSlop).toBeUndefined(); +}); + it('renders icon change animated', async () => { const tree = (await render()).toJSON(); diff --git a/src/components/__tests__/ToggleButton.test.tsx b/src/components/__tests__/ToggleButton.test.tsx index 006ae179c5..a575ecada6 100644 --- a/src/components/__tests__/ToggleButton.test.tsx +++ b/src/components/__tests__/ToggleButton.test.tsx @@ -1,6 +1,6 @@ import { describe, expect, it } from '@jest/globals'; -import { render } from '../../test-utils'; +import { render, screen } from '../../test-utils'; import { DarkTheme, LightTheme } from '../../theme/schemes'; import ToggleButton from '../ToggleButton'; import { getToggleButtonColor } from '../ToggleButton/utils'; @@ -33,6 +33,108 @@ it('renders unchecked toggle button', async () => { expect(tree).toMatchSnapshot(); }); +describe('ToggleButton.Row', () => { + it('divides hitSlop between adjoining buttons, zeroing the shared edge', async () => { + await render( + {}}> + + + + + ); + + // (48 - 42) / 2 on every edge with no neighbour + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('first').props.hitSlop).toEqual({ + top: 3, + bottom: 3, + left: 3, + right: 0, + }); + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('middle').props.hitSlop).toEqual({ + top: 3, + bottom: 3, + left: 0, + right: 0, + }); + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('last').props.hitSlop).toEqual({ + top: 3, + bottom: 3, + left: 0, + right: 3, + }); + }); + + it('keeps the usual hitSlop for a lone button in a row', async () => { + await render( + {}}> + + + ); + + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('only').props.hitSlop).toEqual({ + top: 3, + bottom: 3, + left: 3, + right: 3, + }); + }); + + it('gives a disabled button in a row no hitSlop of its own', async () => { + await render( + {}}> + + + + ); + + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('first').props.hitSlop).toBeUndefined(); + }); + + it('lets a caller-supplied hitSlop win over the row default', async () => { + const customHitSlop = { top: 2, bottom: 2, left: 2, right: 2 }; + + await render( + {}}> + + + + ); + + // eslint-disable-next-line no-restricted-syntax + expect(screen.getByTestId('first').props.hitSlop).toBe(customHitSlop); + }); + + it('squares off the corners shared with a neighbour', async () => { + await render( + {}}> + + + + + ); + + expect(screen.getByTestId('first')).toHaveStyle({ + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + }); + expect(screen.getByTestId('middle')).toHaveStyle({ borderRadius: 0 }); + expect(screen.getByTestId('last')).toHaveStyle({ + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + }); + }); +}); + describe('getToggleButtonColor', () => { it('should return correct color when checked and theme version 3', () => { expect(getToggleButtonColor({ theme: LightTheme, checked: true })).toBe( diff --git a/src/components/__tests__/TouchableRipple.test.tsx b/src/components/__tests__/TouchableRipple.test.tsx index 173906eb04..6c8441ffb8 100644 --- a/src/components/__tests__/TouchableRipple.test.tsx +++ b/src/components/__tests__/TouchableRipple.test.tsx @@ -69,7 +69,7 @@ describe('TouchableRipple', () => { it('takes the shape of the touchable so it does not square off the corners', async () => { await render( - + Press me! ); @@ -83,7 +83,8 @@ describe('TouchableRipple', () => { await render( Press me! diff --git a/src/components/__tests__/TouchableRippleWeb.test.tsx b/src/components/__tests__/TouchableRippleWeb.test.tsx index a3e811a390..c02e2cae3a 100644 --- a/src/components/__tests__/TouchableRippleWeb.test.tsx +++ b/src/components/__tests__/TouchableRippleWeb.test.tsx @@ -1,6 +1,7 @@ import { Text } from 'react-native'; import { describe, expect, it } from '@jest/globals'; +import type { TestInstance } from 'test-renderer'; import { render, screen } from '../../test-utils'; import type TouchableRippleType from '../TouchableRipple/TouchableRipple'; @@ -17,84 +18,110 @@ import type TouchableRippleType from '../TouchableRipple/TouchableRipple'; const TouchableRipple: typeof TouchableRippleType = require('../TouchableRipple/TouchableRipple.tsx').default; -const TARGET = 'touchable-ripple-touch-target'; - -// The target is `aria-hidden`, the button already carries the semantics. Testing -// library skips hidden elements, so queries have to opt in or they find nothing -// and the negative cases pass for free. -const HIDDEN = { includeHiddenElements: true } as const; +const TOUCHABLE = 'touchable'; + +// `children` admits raw text nodes since a host element can render one, but +// the touchable's own children never do; this makes that assumption explicit +// to the type checker instead of leaving `children[0]` typed as `TestNode`. +const asElement = (node: TestInstance | string): TestInstance => { + if (typeof node === 'string') { + throw new Error('Expected an element, not a text node'); + } + return node; +}; + +// The target has no testID of its own (it would be identical, and thus +// ambiguous, on every instance) and no role, so instead of querying for it +// directly, it's addressed by position: it is always the touchable's first +// host child when rendered, and is simply absent (not a hidden sibling) +// otherwise. +const getTarget = () => { + const children = screen.getByTestId(TOUCHABLE).children; + return children.length > 1 ? asElement(children[0]) : null; +}; + +// Throws instead of returning null, so tests that expect a target don't need +// a non-null assertion to use it. +const requireTarget = () => { + const target = getTarget(); + if (target === null) { + throw new Error('Expected a touch target to be rendered'); + } + return target; +}; + +const styleOf = (node: TestInstance) => { + // eslint-disable-next-line no-restricted-syntax + const { style } = node.props; + return Array.isArray(style) ? Object.assign({}, ...style.flat()) : style; +}; describe('TouchableRipple (web)', () => { - // The target is invisible by design, so there is no user-visible assertion to - // make about it. Its style is the behaviour. - const styleOf = (testID: string) => { - // eslint-disable-next-line no-restricted-syntax - const { style } = screen.getByTestId(testID, HIDDEN).props; - return Array.isArray(style) ? Object.assign({}, ...style.flat()) : style; - }; - - it("renders a touch target matching the touchable's own bounds by default", async () => { + it('does not render a touch target when there is no hitSlop', async () => { // No minimum is enforced here: the primitive does not measure, so with no - // caller-supplied `hitSlop` the target is exactly the touchable's bounds. + // caller-supplied `hitSlop` there is nothing to expand into and no target + // to render. await render( - {}}> + {}} testID={TOUCHABLE}> Button ); - expect(screen.getByTestId(TARGET, HIDDEN)).toBeOnTheScreen(); - expect(styleOf(TARGET)).toEqual({ - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - }); + expect(getTarget()).toBeNull(); }); it('renders the touch target before the children so it cannot cover them', async () => { // It hit-tests, so as the last sibling it covers anything interactive inside // the touchable, e.g. a pressable List.Item with a control in `right`. await render( - {}}> + {}} testID={TOUCHABLE}> child-marker ); - const tree = JSON.stringify(screen.toJSON()); + const [first, second] = screen + .getByTestId(TOUCHABLE) + .children.map(asElement); - expect(tree.indexOf(TARGET)).toBeGreaterThan(-1); - expect(tree.indexOf(TARGET)).toBeLessThan(tree.indexOf('child-marker')); + // eslint-disable-next-line no-restricted-syntax + expect(first.props['aria-hidden']).toBe(true); + // eslint-disable-next-line no-restricted-syntax + expect(second.props.children).toBe('child-marker'); }); it('does not render a touch target when there are no touch handlers', async () => { await render( - + Not a control ); - expect(screen.queryByTestId(TARGET, HIDDEN)).not.toBeOnTheScreen(); + expect(getTarget()).toBeNull(); }); it('does not render a touch target when disabled', async () => { await render( - {}}> + {}} + testID={TOUCHABLE} + > Button ); - expect(screen.queryByTestId(TARGET, HIDDEN)).not.toBeOnTheScreen(); + expect(getTarget()).toBeNull(); }); - it('lets a caller-supplied hitSlop size the target instead', async () => { + it('lets a caller-supplied hitSlop size the target', async () => { await render( - {}}> + {}} testID={TOUCHABLE}> Button ); - expect(styleOf(TARGET)).toEqual({ + expect(styleOf(requireTarget())).toEqual({ position: 'absolute', top: -6, bottom: -6, @@ -105,12 +132,16 @@ describe('TouchableRipple (web)', () => { it('accepts a per-edge hitSlop', async () => { await render( - {}}> + {}} + testID={TOUCHABLE} + > Button ); - expect(styleOf(TARGET)).toEqual({ + expect(styleOf(requireTarget())).toEqual({ position: 'absolute', top: -4, bottom: -0, @@ -119,14 +150,44 @@ describe('TouchableRipple (web)', () => { }); }); + it('extends the target past a border, since absolute offsets start inside it', async () => { + // Offsets on a position: absolute child are relative to the parent's + // padding edge, inside its border, not its visible outer edge. Without + // adding the border back in, the target would fall short of `hitSlop` + // past what's actually visible. + await render( + {}} + testID={TOUCHABLE} + > + Button + + ); + + expect(styleOf(requireTarget())).toEqual({ + position: 'absolute', + top: -6, + bottom: -6, + left: -6, + right: -6, + }); + }); + it('no longer clips the touchable itself, which would clip the target', async () => { await render( - {}} testID="touchable"> + {}} testID={TOUCHABLE}> Button ); - const style = styleOf('touchable'); + // eslint-disable-next-line no-restricted-syntax + const { style: rawStyle } = screen.getByTestId(TOUCHABLE).props; + const style = Array.isArray(rawStyle) + ? Object.assign({}, ...rawStyle.flat()) + : rawStyle; // check we have the touchable's own style first, or the absence below passes // against any empty object diff --git a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap index cb49e687a2..c5477e7e73 100644 --- a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap @@ -300,6 +300,14 @@ exports[`renders chip with close button 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "left": 0, + "right": 0, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -671,6 +679,14 @@ exports[`renders chip with custom close button 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "left": 0, + "right": 0, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} diff --git a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap index 359971ce61..a6e2dc6d3f 100644 --- a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap @@ -776,6 +776,10 @@ exports[`DataTable.Pagination renders data table pagination 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -915,6 +919,10 @@ exports[`DataTable.Pagination renders data table pagination 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1112,6 +1120,10 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1251,6 +1263,10 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1390,6 +1406,10 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1529,6 +1549,10 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1726,6 +1750,10 @@ exports[`DataTable.Pagination renders data table pagination with label 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1865,6 +1893,10 @@ exports[`DataTable.Pagination renders data table pagination with label 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -2360,6 +2392,10 @@ exports[`DataTable.Pagination renders data table pagination with options select { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -2499,6 +2535,10 @@ exports[`DataTable.Pagination renders data table pagination with options select { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -2638,6 +2678,10 @@ exports[`DataTable.Pagination renders data table pagination with options select { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -2777,6 +2821,10 @@ exports[`DataTable.Pagination renders data table pagination with options select { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] diff --git a/src/components/__tests__/__snapshots__/IconButton.test.tsx.snap b/src/components/__tests__/__snapshots__/IconButton.test.tsx.snap index 4afccf1c0e..71d404ed58 100644 --- a/src/components/__tests__/__snapshots__/IconButton.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/IconButton.test.tsx.snap @@ -87,6 +87,10 @@ exports[`renders disabled icon button 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -228,6 +232,10 @@ exports[`renders icon button by default 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -369,6 +377,10 @@ exports[`renders icon button with color 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -633,6 +645,10 @@ exports[`renders icon button with size 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -897,6 +913,10 @@ exports[`renders icon change animated 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] diff --git a/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap b/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap index 82d905d737..9c01dfd366 100644 --- a/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap @@ -185,6 +185,10 @@ exports[`activity indicator snapshot test 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1048,6 +1052,10 @@ exports[`renders with placeholder 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1235,6 +1243,10 @@ exports[`renders with placeholder 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1468,6 +1480,10 @@ exports[`renders with text 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1651,6 +1667,10 @@ exports[`renders with text 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] diff --git a/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap b/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap index 25bbdc381c..ba5d86731d 100644 --- a/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap @@ -250,6 +250,10 @@ exports[`renders filled TextInput with TextInput.Icon accessories 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -460,6 +464,10 @@ exports[`renders filled TextInput with TextInput.Icon accessories 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -766,6 +774,10 @@ exports[`renders filled TextInput with TextInput.Icon accessories when error is { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -976,6 +988,10 @@ exports[`renders filled TextInput with TextInput.Icon accessories when error is { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1460,6 +1476,10 @@ exports[`renders outlined TextInput with TextInput.Icon accessories 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1670,6 +1690,10 @@ exports[`renders outlined TextInput with TextInput.Icon accessories 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1956,6 +1980,10 @@ exports[`renders outlined TextInput with TextInput.Icon accessories when error i { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -2166,6 +2194,10 @@ exports[`renders outlined TextInput with TextInput.Icon accessories when error i { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] diff --git a/src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap b/src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap index 018760b28d..ee61910403 100644 --- a/src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap @@ -19,7 +19,7 @@ exports[`renders disabled toggle button 1`] = ` "borderBottomRightRadius": undefined, "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", - "borderRadius": 20, + "borderRadius": 4, "borderTopEndRadius": undefined, "borderTopLeftRadius": undefined, "borderTopRightRadius": undefined, @@ -34,7 +34,6 @@ exports[`renders disabled toggle button 1`] = ` { "backgroundColor": "rgba(230, 224, 233, 1)", "borderColor": "rgba(121, 116, 126, 1)", - "borderRadius": 4, }, undefined, ] @@ -86,7 +85,7 @@ exports[`renders disabled toggle button 1`] = ` "borderBottomLeftRadius": undefined, "borderBottomRightRadius": undefined, "borderBottomStartRadius": undefined, - "borderRadius": 20, + "borderRadius": 4, "borderTopEndRadius": undefined, "borderTopLeftRadius": undefined, "borderTopRightRadius": undefined, @@ -95,6 +94,10 @@ exports[`renders disabled toggle button 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -160,7 +163,7 @@ exports[`renders toggle button 1`] = ` "borderBottomRightRadius": undefined, "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", - "borderRadius": 20, + "borderRadius": 4, "borderTopEndRadius": undefined, "borderTopLeftRadius": undefined, "borderTopRightRadius": undefined, @@ -175,7 +178,6 @@ exports[`renders toggle button 1`] = ` { "backgroundColor": "rgba(230, 224, 233, 1)", "borderColor": "rgba(121, 116, 126, 1)", - "borderRadius": 4, }, undefined, ] @@ -235,7 +237,7 @@ exports[`renders toggle button 1`] = ` "borderBottomLeftRadius": undefined, "borderBottomRightRadius": undefined, "borderBottomStartRadius": undefined, - "borderRadius": 20, + "borderRadius": 4, "borderTopEndRadius": undefined, "borderTopLeftRadius": undefined, "borderTopRightRadius": undefined, @@ -244,6 +246,10 @@ exports[`renders toggle button 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -309,7 +315,7 @@ exports[`renders unchecked toggle button 1`] = ` "borderBottomRightRadius": undefined, "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", - "borderRadius": 20, + "borderRadius": 4, "borderTopEndRadius": undefined, "borderTopLeftRadius": undefined, "borderTopRightRadius": undefined, @@ -324,7 +330,6 @@ exports[`renders unchecked toggle button 1`] = ` { "backgroundColor": "rgba(243, 237, 247, 1)", "borderColor": "rgba(121, 116, 126, 1)", - "borderRadius": 4, }, undefined, ] @@ -376,7 +381,7 @@ exports[`renders unchecked toggle button 1`] = ` "borderBottomLeftRadius": undefined, "borderBottomRightRadius": undefined, "borderBottomStartRadius": undefined, - "borderRadius": 20, + "borderRadius": 4, "borderTopEndRadius": undefined, "borderTopLeftRadius": undefined, "borderTopRightRadius": undefined, @@ -385,6 +390,10 @@ exports[`renders unchecked toggle button 1`] = ` { "overflow": "hidden", }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] diff --git a/src/theme/tokens/sys/state.ts b/src/theme/tokens/sys/state.ts index ab3aa94f68..d0742351bf 100644 --- a/src/theme/tokens/sys/state.ts +++ b/src/theme/tokens/sys/state.ts @@ -15,11 +15,4 @@ export const state = { thickness: 3, outerOffset: 2, }, - /** - * Minimum size of an interactive target. Applied by expanding outside the - * component's bounds, so it is separate from the 40dp state layer that - * Checkbox and Switch render. - * @see https://m3.material.io/foundations/designing/structure - */ - minInteractiveSize: 48, } as const; diff --git a/src/utils/getMinInteractiveSizeHitSlop.ts b/src/utils/getMinInteractiveSizeHitSlop.ts index 3317a79070..d379dbe5aa 100644 --- a/src/utils/getMinInteractiveSizeHitSlop.ts +++ b/src/utils/getMinInteractiveSizeHitSlop.ts @@ -1,8 +1,10 @@ import type { Insets } from 'react-native'; -import { tokens } from '../theme/tokens'; - -const { minInteractiveSize } = tokens.md.sys.state; +/** + * Minimum size of an interactive target. + * @see https://m3.material.io/foundations/designing/structure + */ +const MIN_INTERACTIVE_SIZE = 48; /** * Slop needed to bring a fixed-size element up to the 48dp minimum @@ -21,9 +23,9 @@ const getMinInteractiveSizeHitSlop = ({ height?: number; }): Insets | undefined => { const horizontal = - width === undefined ? 0 : Math.max(0, (minInteractiveSize - width) / 2); + width === undefined ? 0 : Math.max(0, (MIN_INTERACTIVE_SIZE - width) / 2); const vertical = - height === undefined ? 0 : Math.max(0, (minInteractiveSize - height) / 2); + height === undefined ? 0 : Math.max(0, (MIN_INTERACTIVE_SIZE - height) / 2); if (horizontal === 0 && vertical === 0) { return undefined; From 89107236370284883e0f53ddc97f479ba1d1ab40 Mon Sep 17 00:00:00 2001 From: Konrad Rydygier Date: Thu, 10 Sep 2026 12:40:21 +0200 Subject: [PATCH 8/9] test: fix tests broken by rebase onto upstream main --- .../Appbar/__snapshots__/Appbar.test.tsx.snap | 140 ++++++++++++--- .../__snapshots__/CheckboxItem.test.tsx.snap | 8 + src/components/__tests__/IconButton.test.tsx | 2 +- .../__tests__/TouchableRipple.test.tsx | 13 +- .../__snapshots__/DataTable.test.tsx.snap | 8 + .../__snapshots__/IconButton.test.tsx.snap | 70 ++++++-- .../__snapshots__/Searchbar.test.tsx.snap | 70 ++++++-- .../SegmentedButton.test.tsx.snap | 104 +++++++++++ .../TouchableRipple.test.tsx.snap | 166 ++++++++++++++++++ 9 files changed, 523 insertions(+), 58 deletions(-) diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap index 80c909be8a..532ee99c82 100644 --- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap +++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap @@ -1162,7 +1162,6 @@ exports[`AppbarAction should be rendered with custom color 1`] = ` [ { "margin": 6, - "overflow": "hidden", }, { "backgroundColor": undefined, @@ -1170,8 +1169,16 @@ exports[`AppbarAction should be rendered with custom color 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1202,10 +1209,10 @@ exports[`AppbarAction should be rendered with custom color 1`] = ` focusable={true} hitSlop={ { - "bottom": 6, - "left": 6, - "right": 6, - "top": 6, + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, } } onBlur={[Function]} @@ -1229,6 +1236,24 @@ exports[`AppbarAction should be rendered with custom color 1`] = ` "flexGrow": 1, "justifyContent": "center", }, + { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, + }, + { + "overflow": "hidden", + }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1420,7 +1445,6 @@ exports[`AppbarAction should be rendered with default theme color 1`] = ` [ { "margin": 6, - "overflow": "hidden", }, { "backgroundColor": undefined, @@ -1428,8 +1452,16 @@ exports[`AppbarAction should be rendered with default theme color 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1460,10 +1492,10 @@ exports[`AppbarAction should be rendered with default theme color 1`] = ` focusable={true} hitSlop={ { - "bottom": 6, - "left": 6, - "right": 6, - "top": 6, + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, } } onBlur={[Function]} @@ -1487,6 +1519,24 @@ exports[`AppbarAction should be rendered with default theme color 1`] = ` "flexGrow": 1, "justifyContent": "center", }, + { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, + }, + { + "overflow": "hidden", + }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1678,7 +1728,6 @@ exports[`AppbarAction should be rendered with specific theme color if is leading [ { "margin": 6, - "overflow": "hidden", }, { "backgroundColor": undefined, @@ -1686,8 +1735,16 @@ exports[`AppbarAction should be rendered with specific theme color if is leading "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1718,10 +1775,10 @@ exports[`AppbarAction should be rendered with specific theme color if is leading focusable={true} hitSlop={ { - "bottom": 6, - "left": 6, - "right": 6, - "top": 6, + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, } } onBlur={[Function]} @@ -1745,6 +1802,24 @@ exports[`AppbarAction should be rendered with specific theme color if is leading "flexGrow": 1, "justifyContent": "center", }, + { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, + }, + { + "overflow": "hidden", + }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -1936,7 +2011,6 @@ exports[`AppbarAction should render AppbarBackAction with custom color 1`] = ` [ { "margin": 6, - "overflow": "hidden", }, { "backgroundColor": undefined, @@ -1944,8 +2018,16 @@ exports[`AppbarAction should render AppbarBackAction with custom color 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -1977,10 +2059,10 @@ exports[`AppbarAction should render AppbarBackAction with custom color 1`] = ` focusable={true} hitSlop={ { - "bottom": 6, - "left": 6, - "right": 6, - "top": 6, + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, } } onBlur={[Function]} @@ -2004,6 +2086,24 @@ exports[`AppbarAction should render AppbarBackAction with custom color 1`] = ` "flexGrow": 1, "justifyContent": "center", }, + { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, + }, + { + "overflow": "hidden", + }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] diff --git a/src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap b/src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap index b5dd171c87..98d6c4b51d 100644 --- a/src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap +++ b/src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap @@ -682,6 +682,14 @@ exports[`should have maxFontSizeMultiplier set to 1.5 by default 1`] = ` centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} diff --git a/src/components/__tests__/IconButton.test.tsx b/src/components/__tests__/IconButton.test.tsx index 36c28686a1..02161487ed 100644 --- a/src/components/__tests__/IconButton.test.tsx +++ b/src/components/__tests__/IconButton.test.tsx @@ -2,7 +2,7 @@ import { StyleSheet } from 'react-native'; import { describe, expect, it } from '@jest/globals'; -import { render } from '../../test-utils'; +import { render, screen } from '../../test-utils'; import { pink500 } from '../../theme/colors'; import { LightTheme } from '../../theme/schemes'; import { tokens } from '../../theme/tokens'; diff --git a/src/components/__tests__/TouchableRipple.test.tsx b/src/components/__tests__/TouchableRipple.test.tsx index 6c8441ffb8..57ecb6bc43 100644 --- a/src/components/__tests__/TouchableRipple.test.tsx +++ b/src/components/__tests__/TouchableRipple.test.tsx @@ -68,19 +68,17 @@ describe('TouchableRipple', () => { }); it('takes the shape of the touchable so it does not square off the corners', async () => { - await render( + const { toJSON } = await render( Press me! ); - expect(screen.getByTestId('touchable-ripple-underlay')).toHaveStyle({ - borderRadius: 4, - }); + expect(toJSON()).toMatchSnapshot(); }); it('takes per-corner radii too', async () => { - await render( + const { toJSON } = await render( { ); - expect(screen.getByTestId('touchable-ripple-underlay')).toHaveStyle({ - borderTopLeftRadius: 8, - borderBottomRightRadius: 2, - }); + expect(toJSON()).toMatchSnapshot(); }); }); diff --git a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap index a6e2dc6d3f..878acb80f7 100644 --- a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap @@ -141,6 +141,14 @@ exports[`DataTable.Cell renders data table cell children without wrapping text c centered={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} diff --git a/src/components/__tests__/__snapshots__/IconButton.test.tsx.snap b/src/components/__tests__/__snapshots__/IconButton.test.tsx.snap index 71d404ed58..76a64b972f 100644 --- a/src/components/__tests__/__snapshots__/IconButton.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/IconButton.test.tsx.snap @@ -434,7 +434,6 @@ exports[`renders icon button with custom border radius 1`] = ` [ { "margin": 6, - "overflow": "hidden", }, { "backgroundColor": undefined, @@ -442,8 +441,16 @@ exports[`renders icon button with custom border radius 1`] = ` "width": 52, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 26, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -474,14 +481,6 @@ exports[`renders icon button with custom border radius 1`] = ` centered={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 6, - "left": 6, - "right": 6, - "top": 6, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -503,6 +502,24 @@ exports[`renders icon button with custom border radius 1`] = ` "flexGrow": 1, "justifyContent": "center", }, + { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": 26, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, + }, + { + "overflow": "hidden", + }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -702,7 +719,6 @@ exports[`renders icon button with small border radius 1`] = ` [ { "margin": 6, - "overflow": "hidden", }, { "backgroundColor": undefined, @@ -710,8 +726,16 @@ exports[`renders icon button with small border radius 1`] = ` "width": 52, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 26, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, { @@ -742,14 +766,6 @@ exports[`renders icon button with small border radius 1`] = ` centered={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 6, - "left": 6, - "right": 6, - "top": 6, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -771,6 +787,24 @@ exports[`renders icon button with small border radius 1`] = ` "flexGrow": 1, "justifyContent": "center", }, + { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": 26, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, + }, + { + "overflow": "hidden", + }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] diff --git a/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap b/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap index 9c01dfd366..ee645748bb 100644 --- a/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap @@ -560,7 +560,6 @@ exports[`renders searchbar in "view" mode 1`] = ` [ { "margin": 6, - "overflow": "hidden", }, { "backgroundColor": undefined, @@ -568,8 +567,16 @@ exports[`renders searchbar in "view" mode 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -601,10 +608,10 @@ exports[`renders searchbar in "view" mode 1`] = ` focusable={true} hitSlop={ { - "bottom": 6, - "left": 6, - "right": 6, - "top": 6, + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, } } onBlur={[Function]} @@ -628,6 +635,24 @@ exports[`renders searchbar in "view" mode 1`] = ` "flexGrow": 1, "justifyContent": "center", }, + { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, + }, + { + "overflow": "hidden", + }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] @@ -726,7 +751,6 @@ exports[`renders searchbar in "view" mode 1`] = ` [ { "margin": 6, - "overflow": "hidden", }, { "backgroundColor": undefined, @@ -734,8 +758,16 @@ exports[`renders searchbar in "view" mode 1`] = ` "width": 40, }, { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, "borderWidth": 0, }, undefined, @@ -767,10 +799,10 @@ exports[`renders searchbar in "view" mode 1`] = ` focusable={true} hitSlop={ { - "bottom": 6, - "left": 6, - "right": 6, - "top": 6, + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, } } onBlur={[Function]} @@ -794,6 +826,24 @@ exports[`renders searchbar in "view" mode 1`] = ` "flexGrow": 1, "justifyContent": "center", }, + { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": 20, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, + }, + { + "overflow": "hidden", + }, + { + "height": undefined, + "width": undefined, + }, undefined, ], ] diff --git a/src/components/__tests__/__snapshots__/SegmentedButton.test.tsx.snap b/src/components/__tests__/__snapshots__/SegmentedButton.test.tsx.snap index 2c017e23a4..5fce9077ed 100644 --- a/src/components/__tests__/__snapshots__/SegmentedButton.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/SegmentedButton.test.tsx.snap @@ -338,6 +338,14 @@ exports[`should have \`accessibilityState={ checked: true }\` when selected show accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -512,6 +520,14 @@ exports[`should have \`accessibilityState={ checked: true }\` when selected show accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -638,6 +654,14 @@ exports[`should have \`accessibilityState={ checked: true }\` when selected show accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -780,6 +804,14 @@ exports[`should not render icon when icon prop is not passed 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -907,6 +939,14 @@ exports[`should not render icon when icon prop is not passed 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -1048,6 +1088,14 @@ exports[`should not render icon when icon prop is passed along with label, butto accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -1224,6 +1272,14 @@ exports[`should not render icon when icon prop is passed along with label, butto accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -1414,6 +1470,14 @@ exports[`should render icon when icon prop is passed 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -1588,6 +1652,14 @@ exports[`should render icon when icon prop is passed 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -1776,6 +1848,14 @@ exports[`should render icon when icon prop is passed along with label, button is accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -1952,6 +2032,14 @@ exports[`should render icon when icon prop is passed along with label, button is accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -2142,6 +2230,14 @@ exports[`should render icon when icon prop is passed along with label, no matter accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -2318,6 +2414,14 @@ exports[`should render icon when icon prop is passed along with label, no matter accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 6, + "left": 0, + "right": 0, + "top": 6, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} diff --git a/src/components/__tests__/__snapshots__/TouchableRipple.test.tsx.snap b/src/components/__tests__/__snapshots__/TouchableRipple.test.tsx.snap index 00ac206260..4683e4bd10 100644 --- a/src/components/__tests__/__snapshots__/TouchableRipple.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/TouchableRipple.test.tsx.snap @@ -49,6 +49,17 @@ exports[`TouchableRipple on iOS displays the underlay when pressed 1`] = ` "top": 0, "zIndex": 2, }, + { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": undefined, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, + }, { "backgroundColor": "rgba(29, 27, 32, 0.1)", }, @@ -110,6 +121,17 @@ exports[`TouchableRipple on iOS renders custom underlay color 1`] = ` "top": 0, "zIndex": 2, }, + { + "borderBottomEndRadius": undefined, + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderBottomStartRadius": undefined, + "borderRadius": undefined, + "borderTopEndRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "borderTopStartRadius": undefined, + }, { "backgroundColor": "purple", }, @@ -121,3 +143,147 @@ exports[`TouchableRipple on iOS renders custom underlay color 1`] = ` `; + +exports[`TouchableRipple on iOS takes per-corner radii too 1`] = ` + + + + Press me! + + +`; + +exports[`TouchableRipple on iOS takes the shape of the touchable so it does not square off the corners 1`] = ` + + + + Press me! + + +`; From 205cd3e3287a60c2137bc5fdfa13f94a54b12f7c Mon Sep 17 00:00:00 2001 From: Konrad Rydygier Date: Thu, 10 Sep 2026 13:09:54 +0200 Subject: [PATCH 9/9] refactor: trim redundant comments and simplify TouchableRippleWeb tests --- src/components/IconButton/IconButton.tsx | 4 +- .../SegmentedButtons/SegmentedButtonItem.tsx | 4 - .../TouchableRipple/TouchableRipple.tsx | 19 +-- .../__tests__/Checkbox/Checkbox.test.tsx | 1 - src/components/__tests__/Chip.test.tsx | 2 - src/components/__tests__/IconButton.test.tsx | 2 - .../RadioButton/RadioButton.test.tsx | 1 - .../__tests__/SegmentedButton.test.tsx | 2 - src/components/__tests__/Switch.test.tsx | 1 - .../__tests__/ToggleButton.test.tsx | 1 - .../__tests__/TouchableRippleWeb.test.tsx | 111 ++++++------------ 11 files changed, 44 insertions(+), 104 deletions(-) diff --git a/src/components/IconButton/IconButton.tsx b/src/components/IconButton/IconButton.tsx index 16ca0d3d1e..a1b0203748 100644 --- a/src/components/IconButton/IconButton.tsx +++ b/src/components/IconButton/IconButton.tsx @@ -86,9 +86,7 @@ export type Props = Omit< height?: number; /** * Radius of every corner of the button. Defaults to a circle (half of the - * button's size). Read as a plain prop rather than out of `style`, since - * `style` may be an animated value on the UI thread that a synchronous - * `StyleSheet.flatten` cannot see. + * button's size). */ borderRadius?: number; borderTopLeftRadius?: number; diff --git a/src/components/SegmentedButtons/SegmentedButtonItem.tsx b/src/components/SegmentedButtons/SegmentedButtonItem.tsx index f37ceb672a..afbf1166c4 100644 --- a/src/components/SegmentedButtons/SegmentedButtonItem.tsx +++ b/src/components/SegmentedButtons/SegmentedButtonItem.tsx @@ -194,10 +194,6 @@ const SegmentedButtonItem = ({ }; const paddingVertical = getSegmentedButtonDensityPadding({ density }); - - // Height is `2 * paddingVertical + content`, and content is never shorter - // than the 18dp icon (the label's own line height is taller), so that is - // the safe floor to compute slop from without needing to measure. const contentHeight = 2 * paddingVertical + iconSize; const defaultHitSlop = disabled ? undefined diff --git a/src/components/TouchableRipple/TouchableRipple.tsx b/src/components/TouchableRipple/TouchableRipple.tsx index edc0722ccc..50357830ea 100644 --- a/src/components/TouchableRipple/TouchableRipple.tsx +++ b/src/components/TouchableRipple/TouchableRipple.tsx @@ -22,9 +22,9 @@ export type Props = PressableProps & { /** * Whether to render the ripple outside the view bounds. * - * On web the ripple is bounded by its own container, so this no longer clips - * the touchable's content. The touchable cannot clip without clipping the - * touch target, so children needing a rounded shape carry the radius + * On web the ripple is bounded by its own container regardless of this prop. + * The touchable never clips its content, since clipping would also clip the + * touch target, so children needing a rounded shape must carry the radius * themselves. */ borderless?: boolean; @@ -97,9 +97,7 @@ export type Props = PressableProps & { /** * Width of the touchable's own border, if it draws one. Web-only: the touch * target's absolute offsets start inside the border, not the visible outer - * edge, so this widens the target back out to it. Read as a plain prop - * rather than out of `style`, since `style` can be a function of press - * state instead of a plain object to read a value off. + * edge, so this widens the target back out to it. */ borderWidth?: number; }; @@ -219,15 +217,6 @@ const TouchableRipple = ({ borderTopRightRadius: style.borderTopRightRadius, borderBottomRightRadius: style.borderBottomRightRadius, borderBottomLeftRadius: style.borderBottomLeftRadius, - // The touchable cannot clip, it would clip the touch target too, so - // the ripple is contained here. This container is inset to the - // touchable and copies its radii, so it clips to the same shape. - // - // Always, not `centered ? 'visible' : 'hidden'` as before. A ripple - // that escaped used to be caught by whichever ancestor clipped, and - // those ancestors have to stop. ToggleButton hit this: it passes - // `borderless={false}` to IconButton, which spreads it over its own, - // so the Surface was holding the ripple in. overflow: 'hidden', }); diff --git a/src/components/__tests__/Checkbox/Checkbox.test.tsx b/src/components/__tests__/Checkbox/Checkbox.test.tsx index fa9c520ebb..393b32ad1d 100644 --- a/src/components/__tests__/Checkbox/Checkbox.test.tsx +++ b/src/components/__tests__/Checkbox/Checkbox.test.tsx @@ -62,7 +62,6 @@ it('renders Checkbox with custom testID', async () => { it('expands hitSlop up to the 48dp minimum when enabled', async () => { await render(); - // (48 - 40) / 2 on every side // eslint-disable-next-line no-restricted-syntax expect(screen.getByTestId('checkbox').props.hitSlop).toEqual({ top: 4, diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx index dd7dc85d5a..97d48846b4 100644 --- a/src/components/__tests__/Chip.test.tsx +++ b/src/components/__tests__/Chip.test.tsx @@ -102,8 +102,6 @@ it('expands hitSlop up to the 48dp minimum when enabled', async () => { ); - // (48 - 32) / 2 top/bottom, none horizontally: the pill's width already - // covers it // eslint-disable-next-line no-restricted-syntax expect(screen.getByTestId('active-chip').props.hitSlop).toEqual({ top: 8, diff --git a/src/components/__tests__/IconButton.test.tsx b/src/components/__tests__/IconButton.test.tsx index 02161487ed..953dcb3c19 100644 --- a/src/components/__tests__/IconButton.test.tsx +++ b/src/components/__tests__/IconButton.test.tsx @@ -49,7 +49,6 @@ it('renders disabled icon button', async () => { it('expands hitSlop up to the 48dp minimum for a button smaller than that', async () => { await render(); - // (48 - 40) / 2 on every side, for the default 24dp icon plus 8dp padding // eslint-disable-next-line no-restricted-syntax expect(screen.getByTestId('icon-button').props.hitSlop).toEqual({ top: 4, @@ -80,7 +79,6 @@ it('computes hitSlop from explicit width/height rather than the button size', as ); - // (48 - 20) / 2 on every side // eslint-disable-next-line no-restricted-syntax expect(screen.getByTestId('icon-button').props.hitSlop).toEqual({ top: 14, diff --git a/src/components/__tests__/RadioButton/RadioButton.test.tsx b/src/components/__tests__/RadioButton/RadioButton.test.tsx index 90e76d68f6..e31d4efd65 100644 --- a/src/components/__tests__/RadioButton/RadioButton.test.tsx +++ b/src/components/__tests__/RadioButton/RadioButton.test.tsx @@ -87,7 +87,6 @@ describe('RadioButton', () => { it('expands up to the 48dp minimum when enabled', async () => { await render(); - // (48 - 40) / 2 on every side // eslint-disable-next-line no-restricted-syntax expect(screen.getByTestId('radio').props.hitSlop).toEqual({ top: 4, diff --git a/src/components/__tests__/SegmentedButton.test.tsx b/src/components/__tests__/SegmentedButton.test.tsx index 3d90abfd69..23c868e063 100644 --- a/src/components/__tests__/SegmentedButton.test.tsx +++ b/src/components/__tests__/SegmentedButton.test.tsx @@ -526,8 +526,6 @@ describe('hitSlop', () => { /> ); - // (48 - (2 * 9dp default padding + 18dp icon)) / 2 top/bottom, none - // horizontally // eslint-disable-next-line no-restricted-syntax expect(screen.getByTestId('walking-button').props.hitSlop).toEqual({ top: 6, diff --git a/src/components/__tests__/Switch.test.tsx b/src/components/__tests__/Switch.test.tsx index 170ce15dbe..deec243fe6 100644 --- a/src/components/__tests__/Switch.test.tsx +++ b/src/components/__tests__/Switch.test.tsx @@ -27,7 +27,6 @@ describe('Switch render', () => { it('expands hitSlop up to the 48dp minimum when enabled', async () => { await render(); - // (48 - 40) / 2 top/bottom, none horizontally: the track is already wider // eslint-disable-next-line no-restricted-syntax expect(screen.getByTestId('switch').props.hitSlop).toEqual({ top: 4, diff --git a/src/components/__tests__/ToggleButton.test.tsx b/src/components/__tests__/ToggleButton.test.tsx index a575ecada6..8876157043 100644 --- a/src/components/__tests__/ToggleButton.test.tsx +++ b/src/components/__tests__/ToggleButton.test.tsx @@ -43,7 +43,6 @@ describe('ToggleButton.Row', () => { ); - // (48 - 42) / 2 on every edge with no neighbour // eslint-disable-next-line no-restricted-syntax expect(screen.getByTestId('first').props.hitSlop).toEqual({ top: 3, diff --git a/src/components/__tests__/TouchableRippleWeb.test.tsx b/src/components/__tests__/TouchableRippleWeb.test.tsx index c02e2cae3a..bd2c92174f 100644 --- a/src/components/__tests__/TouchableRippleWeb.test.tsx +++ b/src/components/__tests__/TouchableRippleWeb.test.tsx @@ -20,9 +20,9 @@ const TouchableRipple: typeof TouchableRippleType = const TOUCHABLE = 'touchable'; -// `children` admits raw text nodes since a host element can render one, but -// the touchable's own children never do; this makes that assumption explicit -// to the type checker instead of leaving `children[0]` typed as `TestNode`. +// Host elements can render raw text children; the touchable's own children +// never do. This narrows the type instead of leaving `children[0]` typed as +// `TestNode` at every call site. const asElement = (node: TestInstance | string): TestInstance => { if (typeof node === 'string') { throw new Error('Expected an element, not a text node'); @@ -30,18 +30,22 @@ const asElement = (node: TestInstance | string): TestInstance => { return node; }; -// The target has no testID of its own (it would be identical, and thus -// ambiguous, on every instance) and no role, so instead of querying for it -// directly, it's addressed by position: it is always the touchable's first -// host child when rendered, and is simply absent (not a hidden sibling) -// otherwise. +// The target has no testID (a hardcoded one would collide across instances) +// and no accessible role (it's `aria-hidden`), so it's matched by that marker +// instead of a user-visible query. const getTarget = () => { const children = screen.getByTestId(TOUCHABLE).children; - return children.length > 1 ? asElement(children[0]) : null; + return ( + children.find( + (child): child is TestInstance => + typeof child !== 'string' && + // eslint-disable-next-line no-restricted-syntax + !!child.props['aria-hidden'] + ) ?? null + ); }; -// Throws instead of returning null, so tests that expect a target don't need -// a non-null assertion to use it. +// Throws so tests that expect a target don't need a non-null assertion. const requireTarget = () => { const target = getTarget(); if (target === null) { @@ -57,12 +61,16 @@ const styleOf = (node: TestInstance) => { }; describe('TouchableRipple (web)', () => { - it('does not render a touch target when there is no hitSlop', async () => { - // No minimum is enforced here: the primitive does not measure, so with no - // caller-supplied `hitSlop` there is nothing to expand into and no target - // to render. + it.each([ + { when: 'there is no hitSlop', props: { onPress: () => {} } }, + { when: 'there are no touch handlers', props: { hitSlop: 4 } }, + { + when: 'it is disabled', + props: { hitSlop: 4, onPress: () => {}, disabled: true }, + }, + ])('does not render a touch target when $when', async ({ props }) => { await render( - {}} testID={TOUCHABLE}> + Button ); @@ -89,64 +97,27 @@ describe('TouchableRipple (web)', () => { expect(second.props.children).toBe('child-marker'); }); - it('does not render a touch target when there are no touch handlers', async () => { + it.each([ + { + name: 'sizes the target from a numeric hitSlop', + hitSlop: 6 as const, + expected: { top: -6, bottom: -6, left: -6, right: -6 }, + }, + { + name: 'sizes the target from a per-edge hitSlop, defaulting unset edges to zero', + hitSlop: { top: 4, left: 8 }, + expected: { top: -4, bottom: -0, left: -8, right: -0 }, + }, + ])('$name', async ({ hitSlop, expected }) => { await render( - - Not a control - - ); - - expect(getTarget()).toBeNull(); - }); - - it('does not render a touch target when disabled', async () => { - await render( - {}} - testID={TOUCHABLE} - > - Button - - ); - - expect(getTarget()).toBeNull(); - }); - - it('lets a caller-supplied hitSlop size the target', async () => { - await render( - {}} testID={TOUCHABLE}> + {}} testID={TOUCHABLE}> Button ); expect(styleOf(requireTarget())).toEqual({ position: 'absolute', - top: -6, - bottom: -6, - left: -6, - right: -6, - }); - }); - - it('accepts a per-edge hitSlop', async () => { - await render( - {}} - testID={TOUCHABLE} - > - Button - - ); - - expect(styleOf(requireTarget())).toEqual({ - position: 'absolute', - top: -4, - bottom: -0, - left: -8, - right: -0, + ...expected, }); }); @@ -183,11 +154,7 @@ describe('TouchableRipple (web)', () => { ); - // eslint-disable-next-line no-restricted-syntax - const { style: rawStyle } = screen.getByTestId(TOUCHABLE).props; - const style = Array.isArray(rawStyle) - ? Object.assign({}, ...rawStyle.flat()) - : rawStyle; + const style = styleOf(screen.getByTestId(TOUCHABLE)); // check we have the touchable's own style first, or the absence below passes // against any empty object