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
27 changes: 27 additions & 0 deletions docs/6.x/docs/guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,33 @@ 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.

### 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. Replace `variant="surface"` with
one of the supported color variants, such as `primaryContainer`.

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

Expand Down Expand Up @@ -91,7 +92,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 @@ -138,12 +139,24 @@ const FABExample = () => {
<View style={styles.controls}>
<ChipRow
label="Color"
options={variants}
options={
type === 'menu' ? variants.filter((v) => v !== 'branded') : variants
}
value={variant}
onChange={setVariant}
/>
<ChipRow label="Size" options={sizes} value={size} onChange={setSize} />
<ChipRow label="Type" options={types} value={type} onChange={setType} />
<ChipRow
label="Type"
options={types}
value={type}
onChange={(nextType) => {
if (nextType === 'menu' && variant === 'branded') {
setVariant('primaryContainer');
}
setType(nextType);
}}
/>
<ChipRow
label="Position"
options={positions}
Expand Down Expand Up @@ -187,6 +200,7 @@ const FABExample = () => {
{type === 'icon' && (
<FAB
icon="pencil"
aria-label="Compose"
variant={activeVariant}
containerColor={activeContainerColor}
size={size}
Expand Down Expand Up @@ -214,7 +228,7 @@ const FABExample = () => {
alignment={position}
trigger={{
icon: 'pencil',
variant: activeVariant,
variant: activeVariant === 'branded' ? undefined : activeVariant,
containerColor: activeContainerColor,
size,
visible: showFab,
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
32 changes: 20 additions & 12 deletions src/components/FAB/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ export type MenuItemProps = {
testID?: string;
};

type MenuVariant = Exclude<Variant, 'branded'>;

export type MenuTriggerProps = {
/**
* Icon displayed in the trigger FAB (and cross-faded to `closeIcon` when
* the menu is open).
*/
icon: IconSource;
variant?: Variant;
variant?: MenuVariant;
size?: Size;
containerColor?: ColorValue;
contentColor?: ColorValue;
Expand Down Expand Up @@ -121,24 +123,30 @@ export type MenuProps = {
* The close button is always the saturated role color; items are always the
* tonal (container) role color.
*/
const getCloseVariant = (triggerVariant: Variant): Variant => {
if (triggerVariant === 'primary' || triggerVariant === 'tonalPrimary') {
const getCloseVariant = (triggerVariant: MenuVariant): Variant => {
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';
const getItemsVariant = (triggerVariant: MenuVariant): Variant => {
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 @@ -303,7 +311,7 @@ const MenuItem = ({
};

type MorphingTriggerProps = {
triggerVariant: Variant;
triggerVariant: MenuVariant;
closeVariant: Variant;
triggerContainerColor?: ColorValue;
triggerContentColor?: ColorValue;
Expand Down Expand Up @@ -545,7 +553,7 @@ const Menu = ({
const isRTL = direction === 'rtl';
const insets = useSafeAreaInsets();

const triggerVariant: Variant = trigger.variant ?? 'tonalPrimary';
const triggerVariant: MenuVariant = trigger.variant ?? 'primaryContainer';
const size: Size = trigger.size ?? 'default';
const openIcon: IconSource = trigger.icon;
const openOnPress = trigger.onPress;
Expand Down
79 changes: 67 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,34 @@ const Shell = ({

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

React.useEffect(() => {
if (!visible) {
if (Platform.OS === 'web' && typeof document !== 'undefined') {
const target: unknown = touchableRef.current;
if (
target instanceof HTMLElement &&
target === document.activeElement
) {
if (previousFocusedElement.current?.isConnected) {
previousFocusedElement.current.focus({ preventScroll: true });
}
// The previous element may have been removed or become unfocusable.
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 +345,7 @@ const Shell = ({
return (
<Surface
ref={ref}
aria-hidden={visible ? undefined : true}
backgroundColor={containerBg}
borderRadius={borderRadius}
style={[
Expand All @@ -318,17 +354,36 @@ const Shell = ({
outerStyle,
visible ? styles.pointerEventsAuto : styles.pointerEventsNone,
]}
elevation={elevation}
elevation={resolvedElevation}
testID={`${testID}-container`}
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
15 changes: 9 additions & 6 deletions src/components/FAB/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ export type Variant =
| 'primary'
| 'secondary'
| 'tertiary'
| 'tonalPrimary'
| 'tonalSecondary'
| 'tonalTertiary';
| 'primaryContainer'
| 'secondaryContainer'
| 'tertiaryContainer'
| 'branded';

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

Expand Down Expand Up @@ -68,18 +69,20 @@ const stateElevation = {
} as const satisfies Record<string, Elevation>;

const variants = {
// Branded artwork has no prescribed icon color; onSurface is a fallback.
branded: { container: 'surfaceContainerHigh', content: 'onSurface' },
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
Loading