diff --git a/Storybook/components/TopAppBar/TopAppBar.stories.tsx b/Storybook/components/TopAppBar/TopAppBar.stories.tsx index c279da69..fb83110c 100644 --- a/Storybook/components/TopAppBar/TopAppBar.stories.tsx +++ b/Storybook/components/TopAppBar/TopAppBar.stories.tsx @@ -1,13 +1,19 @@ /* eslint-disable @typescript-eslint/no-empty-function */ 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 { Pressable, StyleSheet, Text, View } from 'react-native'; +import { + Headline, + TopAppBar, +} from '../../../src/components/topAppBar/TopAppBar'; + +import TopAppBarMenuItem from '../../../src/components/topAppBar/TopAppBarMenuItem'; + import type { Title } from 'src/components/topAppBar/TopAppBar'; const asString = { value: 'menu' }; const asButton = { value: 'menu', onPress: () => {} }; -const asComponent = { value: Headline H1 }; +const asComponent = { value: Headline H1 }; type ComponentProps = React.ComponentProps & { withBackButton?: boolean; @@ -28,10 +34,14 @@ 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: [ @@ -60,9 +70,46 @@ export const Default: Story = { size={args.size} onBack={args.withBackButton ? args.onBack : undefined} title={titleComponent} - icon={{ name: 'dots-vertical', onPress: args.onPressIcon }} /> ); }, }; +export const WithMenu = (args) => { + let titleComponent: Title = asString; + if (args.withTitleAs === 'button') titleComponent = asButton; + if (args.withTitleAs === 'component') titleComponent = asComponent; + + const handlePress = (menuItem, hideMenu) => () => { + hideMenu(); + args.onMenuItemPress(menuItem); + }; + + const menu = { + items: [ + { + title: 'Ne plus surveiller', + id: 'noLongerMonitor', + }, + ], + renderItem: (menuItem, { hideMenu }) => { + return ( + + ); + }, + }; + + 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..8ba6e91a --- /dev/null +++ b/src/__tests__/components/TopAppBar.test.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import { TopAppBar } from '../../components/topAppBar/TopAppBar'; +import { + render, + screen, + setupFakeTimer, + userEvent, + waitForElementToBeRemoved, +} from '../../shared/testUtils'; +import { Text } from 'react-native'; +import TopAppBarMenuItem from '../../components/topAppBar/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', () => { + it('triggers `goBack` event when user press the go back button', async () => { + setupFakeTimer(); + + 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).toHaveBeenCalled(); + }); +}); + +describe('TopAppBar mounting with a menu', () => { + const noLongerMonitorMenuItem = { + id: 'noLongerMonitor' as const, + title: 'Ne plus surveiller', + }; + const title = { + value: topBarTitle, + }; + let mockOnMenuItemPress: jest.Mock; + + beforeEach(() => { + mockOnMenuItemPress = jest.fn(); + + const handlePress = + (menuItem: typeof noLongerMonitorMenuItem, hideMenu: () => void) => + () => { + hideMenu(); + mockOnMenuItemPress(menuItem); + }; + + render( + { + return ( + + ); + }, + }} + />, + ); + }); + + 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(noLongerMonitorMenuItem.title), + ).not.toBeOnTheScreen(); + + await user.press(screen.getByLabelText(/menu/i)); + + expect( + screen.getByText(noLongerMonitorMenuItem.title), + ).toBeOnTheScreen(); + }); + + it('retreives 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(noLongerMonitorMenuItem.title)); + + expect(mockOnMenuItemPress).toHaveBeenCalledWith( + noLongerMonitorMenuItem, + ); + }); + + 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(noLongerMonitorMenuItem.title), + ).toBeOnTheScreen(); + + await user.press(screen.getByText(noLongerMonitorMenuItem.title)); + + await waitForElementToBeRemoved(() => + screen.queryByText(noLongerMonitorMenuItem.title), + ); + }); +}); diff --git a/src/__tests__/components/TopAppBarMenuItem.test.tsx b/src/__tests__/components/TopAppBarMenuItem.test.tsx new file mode 100644 index 00000000..50ded9cf --- /dev/null +++ b/src/__tests__/components/TopAppBarMenuItem.test.tsx @@ -0,0 +1,53 @@ +import React, { ComponentProps } from 'react'; +import { + setupFakeTimer, + render, + screen, + userEvent, +} from '../../shared/testUtils'; + +import TopAppBarMenuItem from '../../components/topAppBar/TopAppBarMenuItem'; + +describe('TopAppBarMenuItem mounting', () => { + const title = 'Ne plus surveiller'; + const id = 'noLongerMonitor'; + const onPress = jest.fn(); + + it('displays a menu item', () => { + render(); + + expect(screen.getByText(title)).toBeOnTheScreen(); + }); + + it('triggers onPress event when user press the menu item', async () => { + setupFakeTimer(); + + const user = userEvent.setup(); + + render(); + + await user.press(screen.getByText(title)); + + expect(onPress).toHaveBeenCalledTimes(1); + }); +}); + +describe('TopAppBarMenuItem failing to mount', () => { + const title = 'Ne plus surveiller'; + const notExpectedId = '__not_expected_id__' as ComponentProps< + typeof TopAppBarMenuItem + >['id']; + const onPress = jest.fn(); + + it('throw an error if menu item id is not handled', () => { + expect(() => + render( + , + ), + ).toThrowError(); + }); +}); diff --git a/src/components/topAppBar/TopAppBar.tsx b/src/components/topAppBar/TopAppBar.tsx index bc721019..4cb48a1b 100644 --- a/src/components/topAppBar/TopAppBar.tsx +++ b/src/components/topAppBar/TopAppBar.tsx @@ -1,61 +1,45 @@ -import React, { ReactNode } from 'react'; +import React, { ComponentPropsWithoutRef, ReactNode } from 'react'; 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 TopAppBarMenuAction from './TopAppBarMenuAction'; +import { TopAppBarMenuProvider } from './TopAppMenuBarContext'; 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; + menu?: { + items: ComponentPropsWithoutRef< + typeof TopAppBarMenuProvider + >['menuItems']; + renderItem: ComponentPropsWithoutRef< + typeof TopAppBarMenuProvider + >['renderMenuItem']; + }; }>; export const TopAppBar = ({ size = 'small', title, - icon, onBack, style, testID, -}: Props) => { + menu, +}: 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 && ( - + {menu && ( + + + )} ); }; + +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/TopAppBarMenuAction.tsx b/src/components/topAppBar/TopAppBarMenuAction.tsx new file mode 100644 index 00000000..09aad074 --- /dev/null +++ b/src/components/topAppBar/TopAppBarMenuAction.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Appbar } from 'react-native-paper'; +import { useTopBarMenu } from './TopAppMenuBarContext'; +import { Theme, useTheme } from '../../styles/themes'; +import DeviceInfo from 'react-native-device-info'; +import { StyleSheet } from 'react-native'; + +const TopAppBarMenuAction = () => { + const { showMenu } = useTopBarMenu(); + + 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 TopAppBarMenuAction; diff --git a/src/components/topAppBar/TopAppBarMenuItem.tsx b/src/components/topAppBar/TopAppBarMenuItem.tsx new file mode 100644 index 00000000..37c5cedd --- /dev/null +++ b/src/components/topAppBar/TopAppBarMenuItem.tsx @@ -0,0 +1,92 @@ +import React, { ComponentPropsWithoutRef } from 'react'; +import { Menu } from 'react-native-paper'; +import { Theme, useTheme } from '../../styles/themes'; +import { StyleSheet } from 'react-native'; +import { Icon } from '../icons/Icon'; +import { IconName } from '../icons/IconProps'; + +const topAppBarMenuItemIds = ['noLongerMonitor'] as const; +type TopAppBarMenuItemId = (typeof topAppBarMenuItemIds)[number]; + +type TopAppBarMenuItemProps = Required< + Pick, 'title' | 'onPress'> & { + id: TopAppBarMenuItemId; + } +>; +const TopAppBarMenuItem = ({ onPress, title, id }: TopAppBarMenuItemProps) => { + const swtheme = useTheme(); + const styles = useStyles(swtheme); + const menuItemConfig = getMenuItemConfig(swtheme, id); + + if (!menuItemConfig) { + throw new Error( + `Can't get MenuItem with the given id : ${id}. \n It must be one of those ids : ${topAppBarMenuItemIds.join( + ', ', + )}`, + ); + } + + const theme = { + colors: { + onSurface: menuItemConfig.color, + }, + }; + + const leadingIcon = () => ( + + ); + + return ( + + ); +}; + +function getMenuItemConfig(theme: Theme, id: TopAppBarMenuItemId) { + type MenuItemConfig = Record< + TopAppBarMenuItemId, + { + testID: string; + iconName: IconName; + color: string; + } + >; + + const menuItemConfig: MenuItemConfig = { + noLongerMonitor: { + testID: 'topAppBarMenuItem/noLongerMonitor', + iconName: 'notifications-off', + color: theme.sw.colors.primary.main, + }, + }; + + return menuItemConfig[id]; +} + +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/TopAppMenuBarContext.tsx b/src/components/topAppBar/TopAppMenuBarContext.tsx new file mode 100644 index 00000000..dfd85f85 --- /dev/null +++ b/src/components/topAppBar/TopAppMenuBarContext.tsx @@ -0,0 +1,99 @@ +import React, { ComponentProps, ReactNode } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { Menu, Modal, Portal } from 'react-native-paper'; +import { useTheme } from '../../styles/themes'; +import TopAppBarMenuItem from './TopAppBarMenuItem'; + +type TopAppBarMenuContextValue = + | { + showMenu: () => void; + hideMenu: () => void; + } + | undefined; + +type MenuItemId = ComponentProps['id']; + +type MenuItem = Record & { + title: string; + id: MenuItemId; +}; + +const TopAppBarMenuContext = + React.createContext(undefined); + +type TopAppBarMenuProviderProps = { + children: ReactNode; + menuItems: MenuItem[]; + renderMenuItem: ( + menuItem: MenuItem, + { hideMenu }: { hideMenu: () => void }, + ) => React.ReactElement; +}; +const TopAppBarMenuProvider: React.FC = ({ + children, + renderMenuItem, + menuItems, +}) => { + const [visible, setVisible] = React.useState(false); + const showMenu = () => setVisible(true); + const hideMenu = () => setVisible(false); + + const styles = useStyles(); + + const value = { showMenu, hideMenu }; + + return ( + + + + + {menuItems.map((item) => { + return ( + + {renderMenuItem(item, { hideMenu })} + + ); + })} + + + + {children} + + ); +}; + +function useTopBarMenu() { + const context = React.useContext(TopAppBarMenuContext); + if (context === undefined) { + throw new Error( + 'useTopBarMenu must be used within a TopAppBarMenuProvider', + ); + } + 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 { TopAppBarMenuProvider, useTopBarMenu }; diff --git a/src/shared/testUtils.tsx b/src/shared/testUtils.tsx index 92e79d4a..23434fab 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,13 @@ const uiRender = (ui: ReactElement, options?: RenderOptions) => { return rtlRender(ui, { wrapper: Wrapper, ...options }); }; +const setupFakeTimer = () => { + jest.useFakeTimers(); + + act(() => { + jest.runAllTimers(); + }); +}; + export * from '@testing-library/react-native'; -export { uiRender as render }; +export { uiRender as render, setupFakeTimer }; diff --git a/tsconfig.json b/tsconfig.json index 69d3cbc8..e4e8b67b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,31 +1,31 @@ { - "compilerOptions": { - "baseUrl": "./", - "paths": { - "smartway-react-native-ui": ["./src/index"] + "compilerOptions": { + "baseUrl": "./", + "paths": { + "smartway-react-native-ui": ["./src/index"] + }, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "lib", + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "esnext" }, - "allowUnreachableCode": false, - "allowUnusedLabels": false, - "declaration": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "jsx": "react", - "lib": ["esnext"], - "module": "esnext", - "moduleResolution": "node", - "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, - "noImplicitUseStrict": false, - "noStrictGenericChecks": false, - "noUncheckedIndexedAccess": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "outDir": "lib", - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true, - "target": "esnext" - }, - "include": ["src", "./.eslintrc.json", "./modules.d.ts", "./jest.setup.ts"], - "exclude": ["src/components/charts/*"] + "include": ["src", "./.eslintrc.json", "./modules.d.ts", "./jest.setup.ts"], + "exclude": ["src/components/charts/*"] }