Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8beab93
fix(fab): align MD3 variants and interaction states
likevy Sep 7, 2026
2b290b0
fix(fab): address review feedback
likevy Sep 11, 2026
968b711
fix(fab): skip hidden focus restoration targets
likevy Sep 23, 2026
3a9c585
fix(fab): remove the branded variant
likevy Oct 30, 2026
2c5d495
docs(fab): show how to keep the surface look after migrating
likevy Oct 30, 2026
ea12e01
refactor(fab): drop the redundant document check
likevy Oct 30, 2026
0bd1016
fix(fab): name the menu trigger in the example
likevy Oct 30, 2026
240d5be
docs(fab): format the migration snippet and table
likevy Oct 30, 2026
e9dace0
fix(fab): restore focus only after keyboard focus
likevy Oct 30, 2026
0aefeb3
test(fab): name the disabled FAB test after its behavior
likevy Oct 30, 2026
86ef160
test(fab): cover the press guard on a hidden FAB
likevy Oct 30, 2026
e881eca
test(fab): cover focus restoration when a FAB hides on web
likevy Oct 30, 2026
b9c67a4
test(fab): compare elevation between interaction states
likevy Oct 30, 2026
79a3c5f
test(fab): check that focus alone keeps the resting elevation
likevy Oct 30, 2026
76b2bfa
test(fab): pin the direction of the hover elevation
likevy Oct 30, 2026
fb7e14a
test(fab): cover the blur fallback when focus can't move back
likevy Oct 30, 2026
5ad44a7
docs(fab): import useTheme from paper in the surface snippet
likevy Oct 30, 2026
de315af
docs(fab): note that explicit v5 variants still type-check
likevy Nov 4, 2026
89a91a7
fix(fab): type the touchable ref for React Native 0.88
likevy Nov 6, 2026
2c6a466
test(fab): type the FAB test refs for React Native 0.88
likevy Nov 6, 2026
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
42 changes: 42 additions & 0 deletions docs/6.x/docs/guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,48 @@ The misspelled `traileringIcon` props have been renamed:
/>
```

### FAB

`variant="primary"`, `"secondary"` and `"tertiary"` still type-check in 6.x, but they now draw the filled role color instead of the 5.x container color, so an app that sets them explicitly changes look without any error. To preserve the v5 FAB color treatment, update the `variant` prop:

| v5 | v6 |
| ----------- | -------------------- |
| `primary` | `primaryContainer` |
| `secondary` | `secondaryContainer` |
| `tertiary` | `tertiaryContainer` |

If you omit `variant`, no change is needed. The `surface` variant was removed. To keep its look, pass the colors directly:

```jsx
import { FAB, useTheme } from 'react-native-paper';

const MyFAB = () => {
const { colors } = useTheme();

return (
<FAB
icon="plus"
containerColor={colors.surfaceContainerHigh}
contentColor={colors.primary}
/>
);
};
```

For custom colors, replace `color` with `contentColor` and move
`style.backgroundColor` to `containerColor`:

```diff
<FAB
icon="plus"
- color="#ffffff"
- style={{ backgroundColor: '#6750a4', position: 'absolute', bottom: 16, right: 16 }}
+ contentColor="#ffffff"
+ containerColor="#6750a4"
+ style={{ position: 'absolute', bottom: 16, right: 16 }}
/>
```

### 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
10 changes: 6 additions & 4 deletions example/src/Examples/FABExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ const variants: FabColor[] = [
'primary',
'secondary',
'tertiary',
'tonalPrimary',
'tonalSecondary',
'tonalTertiary',
'primaryContainer',
'secondaryContainer',
'tertiaryContainer',
'custom',
];

Expand Down Expand Up @@ -91,7 +91,7 @@ const FABExample = () => {
const { colors } = useTheme();
const insets = useSafeAreaInsets();

const [variant, setVariant] = React.useState<FabColor>('tonalPrimary');
const [variant, setVariant] = React.useState<FabColor>('primaryContainer');
const activeVariant = variant === 'custom' ? undefined : variant;
const activeContainerColor =
variant === 'custom' ? CUSTOM_CONTAINER_COLOR : undefined;
Expand Down Expand Up @@ -187,6 +187,7 @@ const FABExample = () => {
{type === 'icon' && (
<FAB
icon="pencil"
aria-label="Compose"
variant={activeVariant}
containerColor={activeContainerColor}
size={size}
Expand Down Expand Up @@ -214,6 +215,7 @@ const FABExample = () => {
alignment={position}
trigger={{
icon: 'pencil',
Comment thread
likevy marked this conversation as resolved.
Resolved
'aria-label': 'Compose',
variant: activeVariant,
containerColor: activeContainerColor,
size,
Expand Down
4 changes: 2 additions & 2 deletions src/components/FAB/Extended.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type Props = {
*/
label: string;
/**
* Role-color preset. Defaults to `tonalPrimary`.
* Role-color preset. Defaults to `primaryContainer`.
*/
variant?: Variant;
/**
Expand Down Expand Up @@ -154,7 +154,7 @@ export type Props = {
const Extended = ({
icon,
label,
variant = 'tonalPrimary',
variant = 'primaryContainer',
containerColor,
contentColor,
size = 'default',
Expand Down
4 changes: 2 additions & 2 deletions src/components/FAB/FAB.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type Props = {
*/
icon: IconSource;
/**
* Role-color preset. Defaults to `tonalPrimary`.
* Role-color preset. Defaults to `primaryContainer`.
*/
variant?: Variant;
/**
Expand Down Expand Up @@ -118,7 +118,7 @@ export type Props = {
*/
const FAB = ({
icon,
variant = 'tonalPrimary',
variant = 'primaryContainer',
size = 'default',
visible = true,
onPress,
Expand Down
22 changes: 14 additions & 8 deletions src/components/FAB/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,23 +122,29 @@ export type MenuProps = {
* tonal (container) role color.
*/
const getCloseVariant = (triggerVariant: Variant): Variant => {
if (triggerVariant === 'primary' || triggerVariant === 'tonalPrimary') {
if (triggerVariant === 'primary' || triggerVariant === 'primaryContainer') {
return 'primary';
}
if (triggerVariant === 'secondary' || triggerVariant === 'tonalSecondary') {
if (
triggerVariant === 'secondary' ||
triggerVariant === 'secondaryContainer'
) {
return 'secondary';
}
return 'tertiary';
};

const getItemsVariant = (triggerVariant: Variant): Variant => {
if (triggerVariant === 'primary' || triggerVariant === 'tonalPrimary') {
return 'tonalPrimary';
if (triggerVariant === 'primary' || triggerVariant === 'primaryContainer') {
return 'primaryContainer';
}
if (triggerVariant === 'secondary' || triggerVariant === 'tonalSecondary') {
return 'tonalSecondary';
if (
triggerVariant === 'secondary' ||
triggerVariant === 'secondaryContainer'
) {
return 'secondaryContainer';
}
return 'tonalTertiary';
return 'tertiaryContainer';
};

// Per-item delay used by the stagger. Compose uses a single SlowEffects-driven
Expand Down Expand Up @@ -544,7 +550,7 @@ const Menu = ({
const isRTL = direction === 'rtl';
const insets = useSafeAreaInsets();

const triggerVariant: Variant = trigger.variant ?? 'tonalPrimary';
const triggerVariant: Variant = trigger.variant ?? 'primaryContainer';
const size: Size = trigger.size ?? 'default';
const openIcon: IconSource = trigger.icon;
const openOnPress = trigger.onPress;
Expand Down
87 changes: 75 additions & 12 deletions src/components/FAB/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { getDimensions, resolveColors } from './utils';
import { useInternalTheme } from '../../core/theming';
import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext';
import { toRawSpring } from '../../theme/tokens/sys/motion';
import type { Elevation, ThemeProp } from '../../theme/types';
import type { ThemeProp } from '../../theme/types';
import type { ShapeToken } from '../../theme/utils/shape';
import type { IconSource } from '../Icon';
import Surface from '../Surface';
Expand All @@ -46,7 +46,7 @@ export type ShellProps = {
*/
label?: string;
/**
* Role-color preset. Defaults to `tonalPrimary`.
* Role-color preset. Defaults to `primaryContainer`.
*/
variant?: Variant;
/**
Expand Down Expand Up @@ -77,11 +77,6 @@ export type ShellProps = {
* Trailing-padding override.
*/
trailing?: number;
/**
* Resting elevation level. Defaults to the FAB's enabled-state elevation.
* Pass `0` to disable the shadow entirely.
*/
elevation?: Elevation;
/**
* When `false`, the shell animates out (scale + alpha) and stops accepting
* touches.
Expand Down Expand Up @@ -186,15 +181,14 @@ export type ShellProps = {
const Shell = ({
icon,
label,
variant = 'tonalPrimary',
variant = 'primaryContainer',
size = 'default',
containerColor,
contentColor,
shape,
iconSize,
leading,
trailing,
elevation = Tokens.stateElevation.enabled,
visible = true,
onPress,
'aria-label': ariaLabel = label,
Expand All @@ -217,6 +211,19 @@ const Shell = ({
ref,
}: ShellProps) => {
const theme = useInternalTheme(themeOverrides);
const [hovered, setHovered] = React.useState(false);
const [pressed, setPressed] = React.useState(false);
const touchableRef = React.useRef<React.ComponentRef<typeof View>>(null);
const previousFocusedElement = React.useRef<HTMLElement | null>(null);

const resolvedElevation =
visible && onPress
? pressed
? Tokens.stateElevation.pressed
: hovered
? Tokens.stateElevation.hover
: Tokens.stateElevation.enabled
: Tokens.stateElevation.enabled;

const dimensions = React.useMemo(
() => getDimensions({ theme, size, shape, iconSize, leading, trailing }),
Expand Down Expand Up @@ -299,6 +306,42 @@ const Shell = ({

const { focusedSV, onFocus, onBlur } = useFocusRing();

React.useEffect(() => {
if (!visible) {
if (Platform.OS === 'web') {
const target: unknown = touchableRef.current;
if (
target instanceof HTMLElement &&
target === document.activeElement
) {
const previous = previousFocusedElement.current;
// Only restore keyboard focus. A mouse click or tap also focuses the
// FAB, and moving focus back could jump into an earlier text field.
if (
target.matches(':focus-visible') &&
previous?.isConnected &&
!previous.closest('[aria-hidden="true"], [inert]')
) {
previous.focus({ preventScroll: true });
}
// Blur when focus didn't move back, e.g. after a mouse click or when
// the previous element is gone or can't take focus.
if (target === document.activeElement) target.blur();
}
}
setHovered(false);
setPressed(false);
onBlur();
}
}, [visible, onBlur]);

React.useEffect(() => {
if (!onPress) {
setHovered(false);
setPressed(false);
}
}, [onPress]);

const focusRingStyle = useAnimatedStyle(
() => ({
opacity: focusedSV.value ? 1 : 0,
Expand All @@ -310,6 +353,7 @@ const Shell = ({
return (
<Surface
ref={ref}
aria-hidden={visible ? undefined : true}
backgroundColor={containerBg}
borderRadius={borderRadius}
style={[
Expand All @@ -318,16 +362,35 @@ const Shell = ({
outerStyle,
visible ? styles.pointerEventsAuto : styles.pointerEventsNone,
]}
elevation={elevation}
elevation={resolvedElevation}
theme={theme}
>
<Animated.View style={[styles.clip, clipStyle]}>
{overlay}
<TouchableRipple
ref={Platform.OS === 'web' ? touchableRef : undefined}
borderless
background={background}
onPress={onPress}
onFocus={onFocus}
onPress={visible ? onPress : undefined}
disabled={!onPress}
accessible={visible}
focusable={visible && !!onPress}
tabIndex={visible ? undefined : -1}
onHoverIn={() => setHovered(true)}
onHoverOut={() => setHovered(false)}
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
onFocus={(event) => {
if (Platform.OS === 'web') {
const previous =
'relatedTarget' in event.nativeEvent
? event.nativeEvent.relatedTarget
: null;
previousFocusedElement.current =
previous instanceof HTMLElement ? previous : null;
}
onFocus();
}}
onBlur={onBlur}
aria-label={ariaLabel}
role="button"
Expand Down
12 changes: 6 additions & 6 deletions src/components/FAB/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export type Variant =
| 'primary'
| 'secondary'
| 'tertiary'
| 'tonalPrimary'
| 'tonalSecondary'
| 'tonalTertiary';
| 'primaryContainer'
| 'secondaryContainer'
| 'tertiaryContainer';

export type Size = 'default' | 'medium' | 'large';

Expand Down Expand Up @@ -71,15 +71,15 @@ const variants = {
primary: { container: 'primary', content: 'onPrimary' },
secondary: { container: 'secondary', content: 'onSecondary' },
tertiary: { container: 'tertiary', content: 'onTertiary' },
tonalPrimary: {
primaryContainer: {
container: 'primaryContainer',
content: 'onPrimaryContainer',
},
tonalSecondary: {
secondaryContainer: {
container: 'secondaryContainer',
content: 'onSecondaryContainer',
},
tonalTertiary: {
tertiaryContainer: {
container: 'tertiaryContainer',
content: 'onTertiaryContainer',
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/FAB/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type ResolvedColors = {
*/
export const resolveColors = ({
theme,
variant = 'tonalPrimary',
variant = 'primaryContainer',
containerColor,
contentColor,
}: {
Expand Down
Loading
Loading