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
10 changes: 9 additions & 1 deletion apps/mobile/src/components/ScrollComposeButton.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { Box, ExtendedFloatingActionButton, Icon, Text } from "@expo/ui/jetpack-
import {
defaultMinSize,
fillMaxWidth,
graphicsLayer,
height,
onSizeChanged,
Shapes,
size,
} from "@expo/ui/jetpack-compose/modifiers";
import { useState } from "react";
Expand All @@ -16,6 +18,8 @@ import { resolveScaledTextRole } from "../lib/appearancePreferences";
/** Keep the animated width and icon positioning entirely inside Compose, not Yoga. */
export function ScrollComposeButton(props: {
readonly expanded: boolean;
/** A circle beside the floating tab pill, matching its round ends. */
readonly round?: boolean;
readonly onPress: () => void;
readonly size?: number;
readonly className?: string;
Expand Down Expand Up @@ -59,6 +63,10 @@ export function ScrollComposeButton(props: {
defaultMinSize({ minWidth: nativeSize }),
height(nativeSize),
onSizeChanged(rememberWidth),
// The FAB has no shape slot; the clipped layer draws the circle and its shadow.
...(props.round
? [graphicsLayer({ shape: Shapes.Circle, clip: true, shadowElevation: 6 })]
: []),
]}
>
<ExtendedFloatingActionButton.Icon>
Expand Down Expand Up @@ -98,7 +106,7 @@ export function ScrollComposeButton(props: {
top: 0,
bottom: 0,
width: props.expanded ? buttonWidth * scale : fabSize,
borderRadius: 16 * scale,
borderRadius: props.round ? fabSize / 2 : 16 * scale,
overflow: "hidden",
}}
/>
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/home/AndroidHomeFab.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export function AndroidHomeFabLayout(props: ComponentProps<typeof SharedAndroidH
key={`${appearance.baseFontSize}:${fontScale}:${layoutWidth}`}
size={size}
expanded={floatingTabsEnabled ? false : expanded}
round={floatingTabsEnabled}
onPress={props.onStartNewTask}
className="absolute"
style={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import {
type MobileThemeVariables,
} from "../../../lib/mobileTheme";

// A dark page swallows the native shadow, so the fill alone has to lift the pill off the page
// and off the cards it floats over.
// A light pill is a card lifted by its shadow. A dark page swallows the native shadow, so the
// fill alone has to lift the pill off the page and off the cards it floats over.
function floatingContainerColor(colors: MobileThemeVariables) {
const screen = colors["--color-screen"];
if (!isDarkThemeColor(screen)) return flattenThemeColor(colors["--color-card"], screen);
const fill = flattenThemeColor(colors["--color-secondary"], screen);
if (!isDarkThemeColor(screen)) return fill;
return themeColorWithContrast(
fill,
[screen, flattenThemeColor(colors["--color-grouped-card"], screen)],
Expand All @@ -31,11 +31,9 @@ export function androidWorkspaceNavigationColors(
: floating
? floatingContainerColor(colors)
: flattenThemeColor(themeColorWithAlpha(colors["--color-header-foreground"], 0.05), header);
// The lifted dark pill already took the secondary fill's place, so its indicator lifts further.
if (floating) return floatingSelectionColors(colors, containerColor);
const indicatorColor = themeColorWithContrast(
floating && isDarkThemeColor(screen)
? containerColor
: flattenThemeColor(colors["--color-secondary"], containerColor),
flattenThemeColor(colors["--color-secondary"], containerColor),
containerColor,
1.5,
);
Expand All @@ -46,11 +44,31 @@ export function androidWorkspaceNavigationColors(
flattenThemeColor(colors["--color-secondary-foreground"], indicatorColor),
[indicatorColor, containerColor],
),
unselectedColor: floating
? themeColorWithContrast(
flattenThemeColor(colors["--color-secondary-foreground"], containerColor),
containerColor,
)
: flattenThemeColor(colors["--color-header-foreground"], containerColor),
unselectedColor: flattenThemeColor(colors["--color-header-foreground"], containerColor),
};
}

// The floating pill marks the open tab with the accent, as the iOS tab bar does.
function floatingSelectionColors(colors: MobileThemeVariables, containerColor: string) {
const dark = isDarkThemeColor(containerColor);
const indicatorColor = themeColorWithContrast(
flattenThemeColor(
themeColorWithAlpha(colors["--color-primary"], dark ? 0.28 : 0.16),
containerColor,
),
containerColor,
1.5,
);
return {
containerColor,
indicatorColor,
selectedColor: themeColorWithContrast(
flattenThemeColor(colors["--color-primary-text"], indicatorColor),
[indicatorColor, containerColor],
),
unselectedColor: themeColorWithContrast(
flattenThemeColor(colors["--color-foreground-muted"], containerColor),
containerColor,
),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,12 @@ import { floatingTabsMetrics } from "./floating-workspace-tabs";
import { resolveAndroidNavigationStyle } from "../../../lib/androidNavigationStyle";

it.each([
[undefined, "Samsung", false, "one-ui"],
[undefined, "samsung", false, "one-ui"],
[undefined, "Google", false, "material"],
[undefined, null, false, "material"],
[undefined, "Google", true, "one-ui"],
["material", "Samsung", false, "material"],
["one-ui", "Google", false, "one-ui"],
["material", "Google", true, "material"],
] as const)(
"resolves navigation preference %s on %s with preview %s",
(preference, manufacturer, preview, expected) => {
expect(resolveAndroidNavigationStyle(preference, manufacturer, preview)).toBe(expected);
},
);
[undefined, "one-ui"],
["material", "material"],
["one-ui", "one-ui"],
] as const)("resolves navigation preference %s to %s", (preference, expected) => {
expect(resolveAndroidNavigationStyle(preference)).toBe(expected);
});

it.each([
[475, 1, 56, 4, 56],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createContext, startTransition, use, useMemo, type ReactNode } from "react";
import { AppState, Appearance, Platform, useColorScheme } from "react-native";

import { manufacturer } from "expo-device";
import {
resolveAndroidNavigationStyle,
type AndroidNavigationStyle,
Expand Down Expand Up @@ -198,8 +197,6 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
);
const androidNavigationStyle = resolveAndroidNavigationStyle(
storedPreferences?.androidNavigationStyle,
manufacturer,
process.env.EXPO_PUBLIC_T3_FLOATING_TABS === "1",
);
const themeMode = normalizeMobileThemeMode(storedPreferences?.themeMode);
const themeAppearance = themeMode === "system" ? systemColorScheme : themeMode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export function AndroidNavigationAppearanceSection() {
<SettingsSection title="Navigation">
<View accessibilityRole="radiogroup">
<SettingsChoiceRow
label="One UI"
description="Floating navigation with a separate compose button."
label="Floating"
description="A floating tab bar with a separate compose button."
disabled={!isReady}
selected={androidNavigationStyle === "one-ui"}
onPress={() => setAndroidNavigationStyle("one-ui")}
Expand Down
7 changes: 1 addition & 6 deletions apps/mobile/src/lib/androidNavigationStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ export type AndroidNavigationStyle = "one-ui" | "material";

export function resolveAndroidNavigationStyle(
preference: AndroidNavigationStyle | undefined,
manufacturer: string | null,
previewEnabled: boolean,
): AndroidNavigationStyle {
return (
preference ??
(manufacturer?.toLowerCase() === "samsung" || previewEnabled ? "one-ui" : "material")
);
return preference ?? "one-ui";
}
Loading