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
17 changes: 13 additions & 4 deletions src/components/Appbar/AppbarBackIcon.tsx

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

could we also respect configured icon renderer on iOS while preserving bundled chevron as the default?

Currently, Platform.OS === 'ios' always returns back-chevron.png so PaperProvider settings={{ icon: CustomIcon }} remains ignored on iOS

so maybe we can do smth like that:

const { direction } = useLocale();
const { icon } = React.useContext(SettingsContext);

const shouldUseIOSAsset =
  Platform.OS === 'ios' && (!icon || icon === MaterialCommunityIcon);

return shouldUseIOSAsset ? (
  // existing bundled iOS chevron
) : (
  <Icon
    source={{ source: 'arrow-left', direction }}
    color={color}
    size={size}
  />
);

this should retain existing iOS appearance with default settings while satisfying reported expectation that components use the icon renderer configured through PaperProvider

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. Applied close to your sketch, with two adjustments after looking at the surrounding code.

I read settings via React.useContext<Settings>(SettingsContext) to match how TouchableRipple does it (src/components/TouchableRipple/TouchableRipple.tsx:128).

I kept the !icon || half of the guard, it is not dead. PaperProvider builds the value as { icon: MaterialCommunityIcon, rippleEffectEnabled: true, ...settings } (src/core/PaperProvider.tsx:51-58), so a caller passing settings={{ icon: undefined }} spreads icon back to undefined and the identity check alone would miss it.

The identity comparison does hold, which was the part of the sketch I was least sure about: SettingsContext's own default (src/core/settings.tsx:17-20) and PaperProvider's default both point at the same MaterialCommunityIcon module export, so an app that configures nothing still gets the bundled chevron on iOS.

Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as React from 'react';
import { Image, Platform, StyleSheet, View } from 'react-native';
import type { ColorValue } from 'react-native';

import { useLocale } from '../../core/locale';
import { SettingsContext } from '../../core/settings';
import type { Settings } from '../../core/settings';
import Icon from '../Icon';
import MaterialCommunityIcon from '../MaterialCommunityIcon';

const AppbarBackIcon = ({
Expand All @@ -12,10 +16,16 @@ const AppbarBackIcon = ({
color: ColorValue;
}) => {
const { direction } = useLocale();
const { icon } = React.useContext<Settings>(SettingsContext);
const isRTL = direction === 'rtl';
const iosIconSize = size - 3;

return Platform.OS === 'ios' ? (
// The bundled chevron is only kept while the default icon renderer is in
// place, so a renderer configured through `PaperProvider` wins on iOS too.
const shouldUseIOSAsset =
Platform.OS === 'ios' && (!icon || icon === MaterialCommunityIcon);

return shouldUseIOSAsset ? (
<View
style={[
styles.wrapper,
Expand All @@ -34,11 +44,10 @@ const AppbarBackIcon = ({
/>
</View>
) : (
<MaterialCommunityIcon
name="arrow-left"
<Icon
source={{ source: 'arrow-left', direction }}
color={color}
size={size}
direction={direction}
/>
);
};
Expand Down
75 changes: 72 additions & 3 deletions src/components/__tests__/Appbar/Appbar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Animated } from 'react-native';
import { Animated, Platform, Text as RNText } from 'react-native';

import { describe, expect, it, jest } from '@jest/globals';
import { act } from '@testing-library/react-native';
import { afterEach, describe, expect, it, jest } from '@jest/globals';
import { act, render as rtlRender } from '@testing-library/react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';

import PaperProvider from '../../../core/PaperProvider';
import { getTheme } from '../../../core/theming';
import { render, screen } from '../../../test-utils';
import { tokens } from '../../../theme/tokens';
Expand All @@ -14,6 +15,7 @@ import {
modeTextVariant,
renderAppbarContent as utilRenderAppbarContent,
} from '../../Appbar/utils';
import type { IconProps } from '../../MaterialCommunityIcon';
import Menu from '../../Menu/Menu';
import Searchbar from '../../Searchbar';
import Text from '../../Typography/Text';
Expand Down Expand Up @@ -453,3 +455,70 @@ describe('animated value changes correctly', () => {
});
});
});

describe('Appbar.BackAction icon', () => {
const originalPlatform = Platform.OS;

afterEach(() => {
Platform.OS = originalPlatform;
});

const CustomIcon = ({ name, size, direction, testID }: IconProps) => (
<RNText
testID={testID}
style={{
fontSize: size,
transform: [{ scaleX: direction === 'rtl' ? -1 : 1 }],
}}
>
{`custom-${name}`}
</RNText>
);

const renderBackAction = (direction?: 'ltr' | 'rtl') =>
rtlRender(
<PaperProvider settings={{ icon: CustomIcon }} direction={direction}>
<Appbar.BackAction onPress={() => {}} testID="back-action" />
</PaperProvider>
);

it('renders the icon provided through PaperProvider settings', async () => {
Platform.OS = 'android';

await renderBackAction();

expect(
screen.getByText('custom-arrow-left', { includeHiddenElements: true })
).toBeOnTheScreen();
});

it('renders the icon provided through PaperProvider settings on iOS', async () => {
Platform.OS = 'ios';

await renderBackAction();

expect(
screen.getByText('custom-arrow-left', { includeHiddenElements: true })
).toBeOnTheScreen();
});

it('keeps the icon mirrored in RTL', async () => {
Platform.OS = 'android';

await renderBackAction('rtl');

expect(
screen.getByText('custom-arrow-left', { includeHiddenElements: true })
).toHaveStyle({ transform: [{ scaleX: -1 }] });
});

it('keeps the icon unmirrored in LTR', async () => {
Platform.OS = 'android';

await renderBackAction('ltr');

expect(
screen.getByText('custom-arrow-left', { includeHiddenElements: true })
).toHaveStyle({ transform: [{ scaleX: 1 }] });
});
});
Comment on lines +485 to +524

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

could we also add an iOS case alongside the existing tests?

smth like that:

it('renders the icon provided through PaperProvider settings on iOS', async () => {
  Platform.OS = 'ios';

  await renderBackAction();

  expect(
    screen.getByText('custom-arrow-left', { includeHiddenElements: true })
  ).toBeOnTheScreen();
});

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added, as written. Appbar.test.tsx goes from 33 to 34 tests.

Counterfactual: keeping the test and reverting AppbarBackIcon.tsx to the previous Platform.OS === 'ios' ? branch, the new case fails with Unable to find an element with text: custom-arrow-left, and the rendered tree it dumps shows the bundled <Image /> where the custom icon should be. The three Android cases stay green, so the failure is specific to the iOS path. Restoring the fix puts it back to 34/34.