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
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 @@ -307,6 +307,48 @@ The misspelled `traileringIcon` props have been renamed:
/>
```

### FAB

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',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The menu trigger is also an icon-only FAB, but it has no aria-label. With Type set to menu, the trigger shows up in the iOS accessibility tree as a button with no label, and on web its <button> has no accessible name. The icon FAB got aria-label="Compose" in this PR, so the trigger can get one too:

Suggested change
icon: 'pencil',
icon: 'pencil',
'aria-label': 'Compose',

(via Claude, verify if it makes sense)

'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<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