Skip to content

Commit

Permalink
fixup! ✨ TopAppBarMenu - add custom menu
Browse files Browse the repository at this point in the history
  • Loading branch information
clementdejoie committed Feb 29, 2024
1 parent 7d65e40 commit 6b9d2c7
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 246 deletions.
56 changes: 17 additions & 39 deletions Storybook/components/TopAppBar/TopAppBar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
Headline,
TopAppBar,
} from '../../../src/components/topAppBar/TopAppBar';

import TopAppBarMenuItem from '../../../src/components/topAppBar/Menu/TopAppBarMenuItem';

import type { Title } from 'src/components/topAppBar/TopAppBar';
import { TopAppBarMenuProvider } from '../../../src/components/topAppBar/Menu/TopAppMenuBarContext';
import { IconName } from '../../../src/components/icons/IconProps';
import TopAppBarMenuAction from '../../../src/components/topAppBar/Menu/TopAppBarMenuAction';
import { TopAppBar, Title } from '../../../src/components/topAppBar/TopAppBar';
import { Headline } from '../../../src/components/typography/Headline';

const asString = { value: 'menu' };
const asButton = { value: 'menu', onPress: () => {} };
const asButton = {
value: 'menu',
onPress: () => {},
};
const asComponent = { value: <Headline size='h1'>Headline H1</Headline> };

type ComponentProps = React.ComponentProps<typeof TopAppBar> & {
Expand Down Expand Up @@ -81,37 +75,21 @@ export const WithMenuAction = (args) => {
if (args.withTitleAs === 'button') titleComponent = asButton;
if (args.withTitleAs === 'component') titleComponent = asComponent;

const handlePress = (menuItem, hideMenu) => () => {
hideMenu();
args.onMenuItemPress(menuItem);
};

const menuItem = {
id: 'noLongerMonitor' as const,
iconName: 'notifications-off' as const satisfies IconName,
title: 'Ne plus surveiller',
};

return (
<TopAppBar
size={args.size}
onBack={args.withBackButton ? args.onBack : undefined}
title={titleComponent}
action={
<TopAppBarMenuProvider
menuItems={[menuItem]}
renderMenuItem={(menuItem, { hideMenu }) => {
return (
<TopAppBarMenuItem
iconName={menuItem.iconName}
title={menuItem.title}
onPress={handlePress(menuItem, hideMenu)}
/>
);
}}
>
<TopAppBarMenuAction />
</TopAppBarMenuProvider>
<TopAppBar.Menu>
<TopAppBar.MenuItem
iconName='notifications-off'
title='Ne plus surveiller'
onPress={() =>
args.onMenuItemPress('Ne plus surveiller')
}
/>
</TopAppBar.Menu>
}
/>
);
Expand All @@ -130,7 +108,7 @@ export const WithCloseAction = (args) => {
<TopAppBar.Action
icon='close'
accessibilityLabel='Close'
onPress={args.onMenuItemPress}
onPress={args.onPressIcon}
/>
}
/>
Expand All @@ -151,7 +129,7 @@ export const WithPrinterSettingsAction = (args) => {
<TopAppBar.Action
icon='cog'
accessibilityLabel='Settings'
onPress={args.onMenuItemPress}
onPress={args.onPressIcon}
/>
}
/>
Expand Down
67 changes: 22 additions & 45 deletions src/__tests__/components/TopAppBar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import React from 'react';
import { TopAppBar } from '../../components/topAppBar/TopAppBar';
import {
cleanUpFakeTimer,
render,
screen,
setupFakeTimer,
userEvent,
waitForElementToBeRemoved,
} from '../../shared/testUtils';
import { Text } from 'react-native';
import TopAppBarMenuItem from '../../components/topAppBar/Menu/TopAppBarMenuItem';
import { TopAppBarMenuProvider } from '../../components/topAppBar/Menu/TopAppMenuBarContext';
import { IconName } from '../../components/icons/IconProps';
import TopAppBarMenuAction from '../../components/topAppBar/Menu/TopAppBarMenuAction';
import TopAppBarMenuItem from '../../components/topAppBar/Menu/TopAppBarMenuItem';

const topBarTitle = 'Menu';

Expand Down Expand Up @@ -44,9 +43,9 @@ describe('TopAppBar mounting with a title by passing a custom component', () =>
});

describe('TopAppBar mounting with a go back button', () => {
beforeEach(() => setupFakeTimer());
afterEach(() => cleanUpFakeTimer());
it('triggers `goBack` event when user press the go back button', async () => {
setupFakeTimer();

const user = userEvent.setup();

const mockOnGoBack = jest.fn();
Expand All @@ -66,91 +65,69 @@ describe('TopAppBar mounting with a go back button', () => {
});

describe('TopAppBar mounting with a menu', () => {
const menuItem = {
id: 'noLongerMonitor' as const,
iconName: 'notifications-off' as const satisfies IconName,
title: 'Ne plus surveiller',
};
const menuIconName = 'notifications-off' as const satisfies IconName;
const menuTitle = 'Ne plus surveiller';
const title = {
value: topBarTitle,
};
let mockOnMenuItemPress: jest.Mock;

beforeEach(() => {
setupFakeTimer();
mockOnMenuItemPress = jest.fn();

const handlePress =
(aMenuItem: typeof menuItem, hideMenu: () => void) => () => {
hideMenu();
mockOnMenuItemPress(aMenuItem);
};

render(
<TopAppBar
title={title}
action={
<TopAppBarMenuProvider<typeof menuItem>
menuItems={[menuItem]}
renderMenuItem={(menuItem, { hideMenu }) => {
return (
<TopAppBarMenuItem
iconName={menuItem.iconName}
title={menuItem.title}
onPress={handlePress(menuItem, hideMenu)}
/>
);
}}
>
<TopAppBarMenuAction />
</TopAppBarMenuProvider>
<TopAppBar.Menu>
<TopAppBarMenuItem
iconName={menuIconName}
title={menuTitle}
onPress={mockOnMenuItemPress}
/>
</TopAppBar.Menu>
}
/>,
);
});
afterEach(() => cleanUpFakeTimer());

it('displays action menu button', () => {
expect(screen.getByLabelText(/menu/i)).toBeOnTheScreen();
});

it('displays menu items when user press the action menu button', async () => {
setupFakeTimer();

const user = userEvent.setup();

expect(screen.queryByText(menuItem.title)).not.toBeOnTheScreen();
expect(screen.queryByText(menuTitle)).not.toBeOnTheScreen();

await user.press(screen.getByLabelText(/menu/i));

expect(screen.getByText(menuItem.title)).toBeOnTheScreen();
expect(screen.getByText(menuTitle)).toBeOnTheScreen();
});

it('retrieves menu item data when user press a menu item', async () => {
setupFakeTimer();

const user = userEvent.setup();

await user.press(screen.getByLabelText(/menu/i));

expect(mockOnMenuItemPress).not.toHaveBeenCalled();

await user.press(screen.getByText(menuItem.title));
await user.press(screen.getByText(menuTitle));

expect(mockOnMenuItemPress).toHaveBeenCalledWith(menuItem);
expect(mockOnMenuItemPress).toHaveBeenCalledTimes(1);
});

it('hides menu when user press a menu item', async () => {
setupFakeTimer();

const user = userEvent.setup();

await user.press(screen.getByLabelText(/menu/i));

expect(screen.getByText(menuItem.title)).toBeOnTheScreen();
expect(screen.getByText(menuTitle)).toBeOnTheScreen();

await user.press(screen.getByText(menuItem.title));
await user.press(screen.getByText(menuTitle));

await waitForElementToBeRemoved(() =>
screen.queryByText(menuItem.title),
);
await waitForElementToBeRemoved(() => screen.queryByText(menuTitle));
});
});
45 changes: 0 additions & 45 deletions src/__tests__/components/TopAppBarMenuItem.test.tsx

This file was deleted.

75 changes: 75 additions & 0 deletions src/components/topAppBar/Menu/TopAppBarMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { ReactNode } from 'react';
import { StyleSheet, View } from 'react-native';
import { Modal, Portal } from 'react-native-paper';
import { useTheme } from '../../../styles/themes';
import TopAppBarAction from '../TopAppBarAction';

type TopAppBarMenuContextValue =
| { isOpen: boolean; setIsOpen: (isOpen: boolean) => void }
| undefined;

const TopAppBarMenuContext =
React.createContext<TopAppBarMenuContextValue>(undefined);

type TopAppBarMenuProps = {
children: ReactNode;
};

const TopAppBarMenu = ({ children }: TopAppBarMenuProps) => {
const styles = useStyles();

const [isOpen, setIsOpen] = React.useState(false);

const value = { isOpen, setIsOpen };

return (
<>
<Portal>
<Modal
visible={isOpen}
onDismiss={() => setIsOpen(false)}
style={styles.modal}
contentContainerStyle={styles.modalContent}
>
<TopAppBarMenuContext.Provider value={value}>
<View style={styles.menu}>{children}</View>
</TopAppBarMenuContext.Provider>
</Modal>
</Portal>
<TopAppBarAction
accessibilityLabel='Menu'
icon='dots-vertical'
onPress={() => setIsOpen(true)}
/>
</>
);
};

function useTopAppBarMenu() {
const context = React.useContext(TopAppBarMenuContext);
if (context === undefined) {
throw new Error('useTopAppBarMenu must be used within a TopAppBarMenu');
}
return context;
}

function useStyles() {
const theme = useTheme();
return StyleSheet.create({
modal: {
alignItems: 'flex-end',
justifyContent: 'flex-start',
},
modalContent: {
marginTop: 84,
marginRight: theme.sw.spacing.xs,
},
menu: {
backgroundColor: theme.sw.colors.neutral['50'],
borderRadius: 18,
width: 248,
},
});
}

export { TopAppBarMenu, useTopAppBarMenu };
17 changes: 0 additions & 17 deletions src/components/topAppBar/Menu/TopAppBarMenuAction.tsx

This file was deleted.

Loading

0 comments on commit 6b9d2c7

Please sign in to comment.