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 @@ -209,6 +209,33 @@ 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. 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 @@ -302,7 +310,7 @@ const MenuItem = ({
};

type MorphingTriggerProps = {
triggerVariant: Variant;
triggerVariant: MenuVariant;
closeVariant: Variant;
triggerContainerColor?: ColorValue;
triggerContentColor?: ColorValue;
Expand Down Expand Up @@ -544,7 +552,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
83 changes: 71 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,38 @@ 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
) {
const previous = previousFocusedElement.current;
if (
previous?.isConnected &&
!previous.closest('[aria-hidden="true"], [inert]')
) {
previous.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 +349,7 @@ const Shell = ({
return (
<Surface
ref={ref}
aria-hidden={visible ? undefined : true}
backgroundColor={containerBg}
borderRadius={borderRadius}
style={[
Expand All @@ -318,16 +358,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
Loading
Loading