Skip to content
Open
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 @@ -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
<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
23 changes: 20 additions & 3 deletions src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -78,12 +79,20 @@ 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`, 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;

// 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 @@ -243,6 +252,14 @@ const Checkbox = ({
disabled={disabled}
{...accessibilityProps}
testID={testID}
hitSlop={
rest.hitSlop !== undefined
? rest.hitSlop
: disabled
? undefined
: CHECKBOX_HIT_SLOP
}
borderRadius={FOCUS_RING_RADIUS}
style={[
styles.tapTarget,
Platform.OS === 'web' ? webNoOutline : undefined,
Expand Down
82 changes: 78 additions & 4 deletions src/components/Chip/Chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -156,6 +158,37 @@ export type Props = Omit<ViewProps, 'style'> & {
ref?: React.Ref<View>;
};

/**
* 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.
*
* 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;

/**
* 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;

/**
* 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,
});
// 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.
* They can have an icon or avatar on the left, and a close button icon on the right.
Expand Down Expand Up @@ -270,7 +303,7 @@ const Chip = ({
};

const contentSpacings = {
paddingRight: onClose ? 34 : 0,
paddingRight: onClose ? CLOSE_AFFORDANCE_WIDTH : 0,
};

const labelTextStyle = {
Expand All @@ -292,6 +325,7 @@ const Chip = ({
borderless
background={background}
style={[{ borderRadius }, styles.touchable]}
borderRadius={borderRadius}
onPress={onPress}
onLongPress={onLongPress}
onPressIn={hasPassedTouchHandler ? handlePressIn : undefined}
Expand All @@ -304,7 +338,13 @@ const Chip = ({
aria-disabled={disabled}
testID={testID}
theme={theme}
hitSlop={hitSlop}
hitSlop={
hitSlop !== undefined
? hitSlop
: disabled
? undefined
: CHIP_BODY_HIT_SLOP
}
>
<View
style={[
Expand Down Expand Up @@ -391,8 +431,19 @@ const Chip = ({
role="button"
aria-label={closeIconAccessibilityLabel}
testID={closeIconTestID}
style={styles.closeButton}
hitSlop={disabled ? undefined : CHIP_BODY_HIT_SLOP}
>
<View style={[styles.icon, styles.closeIcon, styles.md3CloseIcon]}>
{/* 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 && (
<View aria-hidden style={styles.closeButtonWebTouchTarget} />
)}
<View
testID={testID ? `${testID}-close-icon` : undefined}
style={[styles.icon, styles.closeIcon, styles.md3CloseIcon]}
>
{closeIcon ? (
<Icon source={closeIcon} color={iconColor} size={iconSize} />
) : (
Expand Down Expand Up @@ -428,6 +479,7 @@ const styles = StyleSheet.create({
},
md3Content: {
paddingLeft: 0,
minHeight: CHIP_BODY_HEIGHT,
},
icon: {
padding: 4,
Expand All @@ -443,6 +495,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',
Expand Down Expand Up @@ -473,9 +529,27 @@ 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',
...(Platform.OS === 'web' && { position: 'relative' }),
},
closeButtonWebTouchTarget: {
position: 'absolute',
top: -CLOSE_BUTTON_WEB_TOUCH_TARGET_INSET,
bottom: -CLOSE_BUTTON_WEB_TOUCH_TARGET_INSET,
left: 0,
right: 0,
},
touchable: {
width: '100%',
Expand Down
7 changes: 7 additions & 0 deletions src/components/Chip/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* MD3 Chip spec dimensions.
* @see https://m3.material.io/components/chips/specs
*/
export const ChipTokens = {
containerHeight: 32,
} as const;
1 change: 1 addition & 0 deletions src/components/Drawer/DrawerItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const DrawerItem = ({
{ backgroundColor, borderRadius },
style,
]}
borderRadius={borderRadius}
role="button"
aria-selected={active}
aria-label={ariaLabel}
Expand Down
1 change: 1 addition & 0 deletions src/components/FAB/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ const MenuItem = ({
{ borderRadius },
Platform.OS === 'web' ? webNoOutline : null,
]}
borderRadius={borderRadius}
testID={testID}
>
<Content
Expand Down
Loading