Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/6.x/docs/guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,38 @@ The misspelled `traileringIcon` props have been renamed:
/>
```

### 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
<TouchableRipple
borderless
- style={{ borderRadius: 8 }}
+ borderRadius={8}
onPress={() => {}}
>
<Text>Content</Text>
</TouchableRipple>
```

### 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.
Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 6 additions & 1 deletion src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ const Button = ({
});

const touchableStyle = { borderRadius };
const touchableRippleStyle = getButtonTouchableRippleStyle(
touchableStyle,
borderWidth
);

const { color: customLabelColor, fontSize: customLabelSize } =
StyleSheet.flatten(labelStyle) || {};
Expand Down Expand Up @@ -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}
Expand Down
16 changes: 2 additions & 14 deletions src/components/Button/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -191,20 +192,7 @@ export const getButtonColors = ({
};
};

type ViewStyleBorderRadiusStyles = Partial<
Pick<
ViewStyle,
| 'borderBottomEndRadius'
| 'borderBottomLeftRadius'
| 'borderBottomRightRadius'
| 'borderBottomStartRadius'
| 'borderTopEndRadius'
| 'borderTopLeftRadius'
| 'borderTopRightRadius'
| 'borderTopStartRadius'
| 'borderRadius'
>
>;
type ViewStyleBorderRadiusStyles = Partial<BorderRadiusStyle>;
export const getButtonTouchableRippleStyle = (
style?: ViewStyle,
borderWidth: number = 0
Expand Down
10 changes: 9 additions & 1 deletion src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { getCardColors } from './utils';
import { useInternalTheme } from '../../core/theming';
import type { Elevation, ThemeProp } from '../../theme/types';
import hasTouchHandler from '../../utils/hasTouchHandler';
import { useFocusRing } from '../../utils/useFocusRing';
import Surface from '../Surface';
import type { SurfaceStyle } from '../Surface';

Expand Down Expand Up @@ -148,7 +149,10 @@ const Card = ({
...rest
}: (OutlinedCardProps | ElevatedCardProps | ContainedCardProps) & Props) => {
const theme = useInternalTheme(themeOverrides);

const { target: focusTarget, ring: focusRing } = useFocusRing(
disabled,
theme.colors.secondary
);
const isMode = React.useCallback(
(modeToCompare: Mode) => {
return cardMode === modeToCompare;
Expand Down Expand Up @@ -252,6 +256,10 @@ const Card = ({
onPressIn={handlePressIn}
onPressOut={handlePressOut}
testID={testID}
onFocus={focusTarget.onFocus}
onBlur={focusTarget.onBlur}
{...focusRing.dataSetProps}
style={[{ borderRadius }, ...focusRing.style]}
>
{content}
</Pressable>
Expand Down
65 changes: 16 additions & 49 deletions src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { Platform, StyleSheet, View } from 'react-native';
import type {
ColorValue,
GestureResponderEvent,
NativeSyntheticEvent,
StyleProp,
TargetedEvent,
ViewStyle,
} from 'react-native';

Expand All @@ -16,9 +14,8 @@ import { getSelectionVisualState } from './utils';
import { useLocale } from '../../core/locale';
import { useInternalTheme } from '../../core/theming';
import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext';
import { tokens } from '../../theme/tokens';
import type { ThemeProp } from '../../theme/types';
import { isKeyboardFocusEvent } from '../../utils/isKeyboardFocusEvent';
import getMinInteractiveSizeHitSlop from '../../utils/getMinInteractiveSizeHitSlop';
import TouchableRipple from '../TouchableRipple/TouchableRipple';
import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple';

Expand Down Expand Up @@ -76,13 +73,12 @@ const {
stateLayerSize: STATE_LAYER_SIZE,
} = CheckboxTokens;

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.
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.
Expand Down Expand Up @@ -128,7 +124,6 @@ const Checkbox = ({
// Web (react-native-web) doesn't auto-mirror layout, so flip the mask
// anchor manually for RTL. Native handles it via `I18nManager`.
const flipMaskForWebRTL = Platform.OS === 'web' && direction === 'rtl';
const [focused, setFocused] = React.useState(false);

const selected = status === 'checked' || status === 'indeterminate';

Expand Down Expand Up @@ -202,19 +197,6 @@ const Checkbox = ({
}
const showIndeterminate = nextGlyph === 'indeterminate';

const handleFocus = React.useCallback(
(e: NativeSyntheticEvent<TargetedEvent>) => {
if (disabled) return;
if (!isKeyboardFocusEvent(e)) return;
setFocused(true);
},
[disabled]
);

const handleBlur = React.useCallback(() => {
setFocused(false);
}, []);

const checked: boolean | 'mixed' =
status === 'indeterminate' ? 'mixed' : status === 'checked';

Expand All @@ -238,24 +220,20 @@ const Checkbox = ({
borderless
centered
onPress={onPress}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
{...accessibilityProps}
testID={testID}
style={[
styles.tapTarget,
Platform.OS === 'web' ? webNoOutline : undefined,
style,
]}
hitSlop={
rest.hitSlop !== undefined
? rest.hitSlop
: disabled
? undefined
: CHECKBOX_HIT_SLOP
}
borderRadius={STATE_LAYER_SIZE / 2}
style={[styles.tapTarget, style]}
>
<View pointerEvents="none" style={styles.tapTargetInner}>
{focused && !disabled ? (
<View
pointerEvents="none"
style={[styles.focusRing, { borderColor: theme.colors.secondary }]}
/>
) : null}
<View style={[styles.container, { opacity: visual.containerOpacity }]}>
<Animated.View
pointerEvents="none"
Expand Down Expand Up @@ -301,10 +279,6 @@ const Checkbox = ({
);
};

// Web-only style; not in StyleSheet because `outline` is outside ViewStyle.
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const webNoOutline = { outline: 'none' } as unknown as ViewStyle;

const styles = StyleSheet.create({
tapTarget: {
width: STATE_LAYER_SIZE,
Expand All @@ -319,13 +293,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
focusRing: {
position: 'absolute',
width: FOCUS_RING_SIZE,
height: FOCUS_RING_SIZE,
borderRadius: FOCUS_RING_RADIUS,
borderWidth: FOCUS_THICKNESS,
},
container: {
width: CONTAINER_SIZE,
height: CONTAINER_SIZE,
Expand Down
Loading
Loading