diff --git a/Storybook/components/TopAppBar/TopAppBar.stories.tsx b/Storybook/components/TopAppBar/TopAppBar.stories.tsx
index c279da69..1772e822 100644
--- a/Storybook/components/TopAppBar/TopAppBar.stories.tsx
+++ b/Storybook/components/TopAppBar/TopAppBar.stories.tsx
@@ -2,12 +2,15 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import React from 'react';
import { StyleSheet, View } from 'react-native';
-import { Headline, TopAppBar } from 'smartway-react-native-ui';
-import type { Title } from 'src/components/topAppBar/TopAppBar';
+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 asComponent = { value: Headline H1 };
+const asButton = {
+ value: 'menu',
+ onPress: () => {},
+};
+const asComponent = { value: Headline H1 };
type ComponentProps = React.ComponentProps & {
withBackButton?: boolean;
@@ -28,12 +31,15 @@ export default {
control: { type: 'radio' },
options: ['small', 'medium', 'large', 'center-aligned'],
},
- withTitleAs: { control: { type: 'radio' }, options: ['string', 'button', 'component'] },
+ withTitleAs: {
+ control: { type: 'radio' },
+ options: ['string', 'button', 'component'],
+ },
withBackButton: { type: 'boolean' },
onBack: { action: 'onBack' },
onPressIcon: { action: 'onPressIcon' },
+ onMenuItemPress: { action: 'onMenuItemPress' },
},
-
decorators: [
(Story) => {
const styles = StyleSheet.create({
@@ -60,9 +66,74 @@ export const Default: Story = {
size={args.size}
onBack={args.withBackButton ? args.onBack : undefined}
title={titleComponent}
- icon={{ name: 'dots-vertical', onPress: args.onPressIcon }}
/>
);
},
};
+export const WithMenuAction = (args) => {
+ let titleComponent: Title = asString;
+ if (args.withTitleAs === 'button') titleComponent = asButton;
+ if (args.withTitleAs === 'component') titleComponent = asComponent;
+
+ return (
+
+
+ args.onMenuItemPress('Ne plus surveiller')
+ }
+ />
+
+ }
+ />
+ );
+};
+export const WithCloseAction = (args) => {
+ let titleComponent: Title = asString;
+ if (args.withTitleAs === 'button') titleComponent = asButton;
+ if (args.withTitleAs === 'component') titleComponent = asComponent;
+
+ return (
+
+ }
+ />
+ );
+};
+
+export const WithPrinterSettingsAction = (args) => {
+ let titleComponent: Title = asString;
+ if (args.withTitleAs === 'button') titleComponent = asButton;
+ if (args.withTitleAs === 'component') titleComponent = asComponent;
+
+ return (
+
+ }
+ />
+ );
+};
+
Default.parameters = { noSafeArea: false };
diff --git a/Storybook/package-lock.json b/Storybook/package-lock.json
index 9e4eb3f4..71381112 100644
--- a/Storybook/package-lock.json
+++ b/Storybook/package-lock.json
@@ -56,7 +56,7 @@
"@storybook/testing-library": "^0.0.13",
"@tsconfig/react-native": "^2.0.2",
"@types/jest": "^29.2.1",
- "@types/react": "^18.0.24",
+ "@types/react": "^18.2.0",
"@types/react-native": "^0.70.6",
"@types/react-test-renderer": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.37.0",
@@ -17874,9 +17874,9 @@
"dev": true
},
"node_modules/@types/react": {
- "version": "18.2.21",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz",
- "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==",
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==",
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
diff --git a/Storybook/package.json b/Storybook/package.json
index d2c9c432..5b26bb82 100644
--- a/Storybook/package.json
+++ b/Storybook/package.json
@@ -64,7 +64,7 @@
"@storybook/testing-library": "^0.0.13",
"@tsconfig/react-native": "^2.0.2",
"@types/jest": "^29.2.1",
- "@types/react": "^18.0.24",
+ "@types/react": "^18.2.0",
"@types/react-native": "^0.70.6",
"@types/react-test-renderer": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.37.0",
diff --git a/jest.config.ts b/jest.config.ts
index 69d5daec..6303f3a8 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -16,7 +16,7 @@ const jestConfig: JestConfigWithTsJest = {
testMatch: ['**/?(*.)test.(ts|tsx)'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
transformIgnorePatterns: [
- 'node_modules/(?!(@react-native|react-native|react-native-drop-shadow|@gorhom/bottom-sheet|react-native-reanimated)/)',
+ 'node_modules/(?!(@react-native|react-native|react-native-drop-shadow|@gorhom/bottom-sheet|react-native-reanimated|react-native-paper)/)',
],
moduleDirectories: ['node_modules', 'src'],
setupFilesAfterEnv: ['./jest.setup.ts'],
diff --git a/package-lock.json b/package-lock.json
index 21a40e95..c840a4f1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"devDependencies": {
"@testing-library/react-native": "^12.4.2",
"@types/jest": "^29.5.5",
- "@types/react": "^18.2.45",
+ "@types/react": "^18.2.0",
"@types/react-native": "^0.73.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"eslint": "^8.56.0",
@@ -4403,9 +4403,9 @@
"devOptional": true
},
"node_modules/@types/react": {
- "version": "18.2.45",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.45.tgz",
- "integrity": "sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==",
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==",
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
@@ -18127,9 +18127,9 @@
"devOptional": true
},
"@types/react": {
- "version": "18.2.45",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.45.tgz",
- "integrity": "sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==",
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==",
"devOptional": true,
"requires": {
"@types/prop-types": "*",
diff --git a/package.json b/package.json
index 37cf415f..38e3cab4 100644
--- a/package.json
+++ b/package.json
@@ -47,11 +47,11 @@
"devDependencies": {
"@testing-library/react-native": "^12.4.2",
"@types/jest": "^29.5.5",
- "@types/react": "^18.2.45",
+ "@types/react": "^18.2.0",
"@types/react-native": "^0.73.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
- "eslint-config-prettier": "^9.1.0",
"eslint": "^8.56.0",
+ "eslint-config-prettier": "^9.1.0",
"eslint-config-standard-with-typescript": "19.0.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.32.2",
diff --git a/src/__tests__/components/TopAppBar.test.tsx b/src/__tests__/components/TopAppBar.test.tsx
new file mode 100644
index 00000000..98966eee
--- /dev/null
+++ b/src/__tests__/components/TopAppBar.test.tsx
@@ -0,0 +1,133 @@
+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 { IconName } from '../../components/icons/IconProps';
+import TopAppBarMenuItem from '../../components/topAppBar/Menu/TopAppBarMenuItem';
+
+const topBarTitle = 'Menu';
+
+describe('TopAppBar mounting with a simple title', () => {
+ it('displays a title', () => {
+ const title = {
+ value: topBarTitle,
+ };
+
+ render();
+
+ expect(screen.getByText(topBarTitle)).toBeOnTheScreen();
+ });
+
+ it.todo("testing menu title 'as string' on press");
+});
+
+describe('TopAppBar mounting with a title by passing a custom component', () => {
+ it('displays a title', () => {
+ const title = {
+ value: {topBarTitle},
+ };
+
+ render();
+
+ expect(screen.getByText(topBarTitle)).toBeOnTheScreen();
+ });
+
+ it.todo("testing menu title 'as component' on press");
+});
+
+describe('TopAppBar mounting with a go back button', () => {
+ beforeEach(() => setupFakeTimer());
+ afterEach(() => cleanUpFakeTimer());
+ it('triggers `goBack` event when user press the go back button', async () => {
+ const user = userEvent.setup();
+
+ const mockOnGoBack = jest.fn();
+
+ const title = {
+ value: topBarTitle,
+ };
+
+ render();
+
+ expect(mockOnGoBack).not.toHaveBeenCalled();
+
+ await user.press(screen.getByLabelText(/back/i));
+
+ expect(mockOnGoBack).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe('TopAppBar mounting with a menu', () => {
+ 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();
+
+ render(
+
+
+
+ }
+ />,
+ );
+ });
+ 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 () => {
+ const user = userEvent.setup();
+
+ expect(screen.queryByText(menuTitle)).not.toBeOnTheScreen();
+
+ await user.press(screen.getByLabelText(/menu/i));
+
+ expect(screen.getByText(menuTitle)).toBeOnTheScreen();
+ });
+
+ it('retrieves menu item data when user press a menu item', async () => {
+ const user = userEvent.setup();
+
+ await user.press(screen.getByLabelText(/menu/i));
+
+ expect(mockOnMenuItemPress).not.toHaveBeenCalled();
+
+ await user.press(screen.getByText(menuTitle));
+
+ expect(mockOnMenuItemPress).toHaveBeenCalledTimes(1);
+ });
+
+ it('hides menu when user press a menu item', async () => {
+ const user = userEvent.setup();
+
+ await user.press(screen.getByLabelText(/menu/i));
+
+ expect(screen.getByText(menuTitle)).toBeOnTheScreen();
+
+ await user.press(screen.getByText(menuTitle));
+
+ await waitForElementToBeRemoved(() => screen.queryByText(menuTitle));
+ });
+});
diff --git a/src/components/topAppBar/Menu/TopAppBarMenu.tsx b/src/components/topAppBar/Menu/TopAppBarMenu.tsx
new file mode 100644
index 00000000..59a44774
--- /dev/null
+++ b/src/components/topAppBar/Menu/TopAppBarMenu.tsx
@@ -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(undefined);
+
+type TopAppBarMenuProps = {
+ children: ReactNode;
+};
+
+const TopAppBarMenu = ({ children }: TopAppBarMenuProps) => {
+ const styles = useStyles();
+
+ const [isOpen, setIsOpen] = React.useState(false);
+
+ const value = { isOpen, setIsOpen };
+
+ return (
+ <>
+
+ setIsOpen(false)}
+ style={styles.modal}
+ contentContainerStyle={styles.modalContent}
+ >
+
+ {children}
+
+
+
+ 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 };
diff --git a/src/components/topAppBar/Menu/TopAppBarMenuItem.tsx b/src/components/topAppBar/Menu/TopAppBarMenuItem.tsx
new file mode 100644
index 00000000..5f87c9b7
--- /dev/null
+++ b/src/components/topAppBar/Menu/TopAppBarMenuItem.tsx
@@ -0,0 +1,74 @@
+import React, { ComponentPropsWithoutRef } from 'react';
+import { Menu } from 'react-native-paper';
+import { Theme, useTheme } from '../../../styles/themes';
+import { GestureResponderEvent, StyleSheet } from 'react-native';
+import { Icon } from '../../icons/Icon';
+import { IconName } from '../../icons/IconProps';
+import { WithTestID } from 'src/shared/type';
+import { useTopAppBarMenu } from './TopAppBarMenu';
+
+type TopAppBarMenuItemProps = WithTestID<
+ Required<
+ Pick, 'title' | 'onPress'>
+ > & {
+ color?: string;
+ iconName: IconName;
+ }
+>;
+const TopAppBarMenuItem = ({
+ onPress,
+ title,
+ testID,
+ color,
+ iconName,
+}: TopAppBarMenuItemProps) => {
+ const theme = useTheme();
+ const styles = useStyles(theme);
+ const { setIsOpen } = useTopAppBarMenu();
+
+ const handleOnPress = (e: GestureResponderEvent) => {
+ setIsOpen(false);
+ onPress(e);
+ };
+
+ const colorTheme = color || theme.sw.colors.primary.main;
+
+ const menuItemTheme = {
+ colors: {
+ onSurface: colorTheme,
+ },
+ };
+
+ const leadingIcon = () => (
+
+ );
+
+ return (
+
+ );
+};
+
+function useStyles(theme: Theme) {
+ return StyleSheet.create({
+ menuItem: {
+ borderRadius: 18,
+ paddingHorizontal: theme.sw.spacing.l,
+ paddingVertical: theme.sw.spacing.m,
+ height: 'auto',
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ });
+}
+
+export default TopAppBarMenuItem;
diff --git a/src/components/topAppBar/TopAppBar.tsx b/src/components/topAppBar/TopAppBar.tsx
index bc721019..99d78b02 100644
--- a/src/components/topAppBar/TopAppBar.tsx
+++ b/src/components/topAppBar/TopAppBar.tsx
@@ -3,59 +3,38 @@ import { Appbar } from 'react-native-paper';
import { useTheme } from '../../styles/themes';
import { StyleSheet, type ViewStyle } from 'react-native';
import { Headline } from '../typography/Headline';
-import DeviceInfo from "react-native-device-info";
-import type { IconSource } from 'react-native-paper/lib/typescript/components/Icon';
+import DeviceInfo from 'react-native-device-info';
import type { WithTestID } from 'src/shared/type';
-
-interface Icon {
- name: IconSource;
- onPress?: () => void;
-}
+import TopAppBarAction from './TopAppBarAction';
+import {TopAppBarMenu} from "./Menu/TopAppBarMenu";
+import TopAppBarMenuItem from "./Menu/TopAppBarMenuItem";
export interface Title {
value: ReactNode;
onPress?: () => void;
}
-export type Props = WithTestID<{
+export type TopAppBarProps = WithTestID<{
size?: 'small' | 'medium' | 'large' | 'center-aligned';
title: Title;
- icon?: Icon;
onBack?: () => void;
style?: ViewStyle;
+ action?: ReactNode;
}>;
-export const TopAppBar = ({
+// eslint-disable-next-line react/function-component-definition
+export function TopAppBar({
size = 'small',
title,
- icon,
onBack,
style,
testID,
-}: Props) => {
+ action,
+}: TopAppBarProps) {
const theme = useTheme();
- const isTablet = DeviceInfo.isTablet();
- const styles = StyleSheet.create({
- button: {
- backgroundColor: 'rgba(145, 158, 171, 0.24)',
- borderRadius: 18,
- marginLeft: isTablet ? 12 : theme.sw.spacing.xs,
- },
- title: {
- paddingTop: size === 'medium' ? 9 : 0,
- paddingBottom: 0,
- justifyContent: 'flex-start',
- },
- header: {
- paddingHorizontal: 12,
- paddingBottom: 0,
- ...style,
- },
- });
- const getIconColor = () => {
- return theme.sw.colors.neutral[600];
- };
+ const styles = useStyles(size, style);
+
return (
- {icon !== undefined && (
-
- )}
+ {action}
);
-};
+}
+TopAppBar.Action = TopAppBarAction;
+TopAppBar.Menu = TopAppBarMenu;
+TopAppBar.MenuItem = TopAppBarMenuItem;
+
+function useStyles(
+ size: TopAppBarProps['size'],
+ style: TopAppBarProps['style'],
+) {
+ const theme = useTheme();
+ const isTablet = DeviceInfo.isTablet();
+ return StyleSheet.create({
+ button: {
+ backgroundColor: 'rgba(145, 158, 171, 0.24)',
+ borderRadius: 18,
+ marginLeft: isTablet ? 12 : theme.sw.spacing.xs,
+ },
+ title: {
+ paddingTop: size === 'medium' ? 9 : 0,
+ paddingBottom: 0,
+ justifyContent: 'flex-start',
+ },
+ header: {
+ paddingHorizontal: 12,
+ paddingBottom: 0,
+ ...style,
+ },
+ });
+}
diff --git a/src/components/topAppBar/TopAppBarAction.tsx b/src/components/topAppBar/TopAppBarAction.tsx
new file mode 100644
index 00000000..19fa1985
--- /dev/null
+++ b/src/components/topAppBar/TopAppBarAction.tsx
@@ -0,0 +1,34 @@
+import React, { ComponentProps } from 'react';
+import { Appbar } from 'react-native-paper';
+import { Theme, useTheme } from '../../styles/themes';
+import DeviceInfo from 'react-native-device-info';
+import { StyleSheet } from 'react-native';
+
+type TopAppBarActionProps = ComponentProps;
+const TopAppBarAction = (props: TopAppBarActionProps) => {
+ const theme = useTheme();
+ const styles = useStyles(theme);
+
+ return (
+
+ );
+};
+
+function useStyles(theme: Theme) {
+ const isTablet = DeviceInfo.isTablet();
+
+ return StyleSheet.create({
+ button: {
+ backgroundColor: 'rgba(145, 158, 171, 0.24)',
+ borderRadius: 18,
+ marginLeft: isTablet ? 12 : theme.sw.spacing.xs,
+ },
+ });
+}
+
+export default TopAppBarAction;
diff --git a/src/index.tsx b/src/index.tsx
index ae53e863..8bb5afdc 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,4 +1,4 @@
-import {
+export {
Body,
Button,
IconButton,
@@ -33,40 +33,4 @@ import {
DateSelector,
} from './components';
-import { ThemeProvider, useTheme } from './styles/themes';
-export {
- Body,
- Button,
- IconButton,
- ActionCard,
- Dialog,
- Headline,
- Icon,
- Logo,
- Screen,
- TextField,
- NumberField,
- Keyboard,
- DropDown,
- SnackBar,
- Toggle,
- Menu,
- TopAppBar,
- ThemeProvider,
- useTheme,
- BottomSheet,
- PrintState,
- EANInput,
- ModifyQuantity,
- NumberSelector,
- Card,
- Tab,
- Label,
- Product,
- Divider,
- LineChart,
- Badge,
- DateField,
- DateSelector,
- NumberValidator,
-};
+export { ThemeProvider, useTheme } from './styles/themes';
diff --git a/src/shared/testUtils.tsx b/src/shared/testUtils.tsx
index 92e79d4a..124d21e4 100644
--- a/src/shared/testUtils.tsx
+++ b/src/shared/testUtils.tsx
@@ -1,6 +1,7 @@
import React, { ReactElement } from 'react';
import {
RenderOptions,
+ act,
render as rtlRender,
} from '@testing-library/react-native';
import { ThemeProvider } from '../styles/themes'; // Replace with the actual path to your ThemeProvider
@@ -13,5 +14,18 @@ const uiRender = (ui: ReactElement, options?: RenderOptions) => {
return rtlRender(ui, { wrapper: Wrapper, ...options });
};
+const setupFakeTimer = () => {
+ jest.useFakeTimers();
+
+ act(() => {
+ jest.runAllTimers();
+ });
+};
+
+const cleanUpFakeTimer = () => {
+ jest.clearAllTimers();
+ jest.useRealTimers();
+};
+
export * from '@testing-library/react-native';
-export { uiRender as render };
+export { uiRender as render, setupFakeTimer, cleanUpFakeTimer };