mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Navigation: Proof-of-concept for pinning navbar items (#44775)
This commit is contained in:
parent
7c826cb43f
commit
b6682cdcb9
@ -22,6 +22,7 @@ export interface NavModelItem {
|
|||||||
highlightText?: string;
|
highlightText?: string;
|
||||||
highlightId?: string;
|
highlightId?: string;
|
||||||
tabSuffix?: ComponentType<{ className?: string }>;
|
tabSuffix?: ComponentType<{ className?: string }>;
|
||||||
|
hideFromNavbar?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NavSection {
|
export enum NavSection {
|
||||||
|
@ -5,6 +5,7 @@ export type IconSize = ComponentSize | 'xl' | 'xxl' | 'xxxl';
|
|||||||
|
|
||||||
export const getAvailableIcons = () =>
|
export const getAvailableIcons = () =>
|
||||||
[
|
[
|
||||||
|
'anchor',
|
||||||
'angle-double-down',
|
'angle-double-down',
|
||||||
'angle-double-right',
|
'angle-double-right',
|
||||||
'angle-double-up',
|
'angle-double-up',
|
||||||
|
@ -33,7 +33,9 @@ const NavBarItem = ({
|
|||||||
const { i18n } = useLingui();
|
const { i18n } = useLingui();
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const menuItems = link.children ?? [];
|
const menuItems = link.children ?? [];
|
||||||
const menuItemsSorted = reverseMenuDirection ? menuItems.reverse() : menuItems;
|
|
||||||
|
// Spreading `menuItems` here as otherwise we'd be mutating props
|
||||||
|
const menuItemsSorted = reverseMenuDirection ? [...menuItems].reverse() : menuItems;
|
||||||
const filteredItems = menuItemsSorted
|
const filteredItems = menuItemsSorted
|
||||||
.filter((item) => !item.hideFromMenu)
|
.filter((item) => !item.hideFromMenu)
|
||||||
.map((i) => ({ ...i, menuItemType: NavMenuItemType.Item }));
|
.map((i) => ({ ...i, menuItemType: NavMenuItemType.Item }));
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem } from '@grafana/data';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from 'test/redux-rtl';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { NavBarMenu } from './NavBarMenu';
|
import { NavBarMenu } from './NavBarMenu';
|
||||||
|
|
||||||
|
@ -6,6 +6,9 @@ import { useDialog } from '@react-aria/dialog';
|
|||||||
import { useOverlay } from '@react-aria/overlays';
|
import { useOverlay } from '@react-aria/overlays';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { NavBarMenuItem } from './NavBarMenuItem';
|
import { NavBarMenuItem } from './NavBarMenuItem';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { togglePin } from 'app/core/reducers/navBarTree';
|
||||||
|
import { getConfig } from 'app/core/config';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
activeItem?: NavModelItem;
|
activeItem?: NavModelItem;
|
||||||
@ -14,6 +17,11 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NavBarMenu({ activeItem, navItems, onClose }: Props) {
|
export function NavBarMenu({ activeItem, navItems, onClose }: Props) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const toggleItemPin = (id: string) => {
|
||||||
|
dispatch(togglePin({ id }));
|
||||||
|
};
|
||||||
|
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
@ -27,6 +35,7 @@ export function NavBarMenu({ activeItem, navItems, onClose }: Props) {
|
|||||||
ref
|
ref
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const newNavigationEnabled = getConfig().featureToggles.newNavigation;
|
||||||
return (
|
return (
|
||||||
<FocusScope contain restoreFocus autoFocus>
|
<FocusScope contain restoreFocus autoFocus>
|
||||||
<div data-testid="navbarmenu" className={styles.container} ref={ref} {...overlayProps} {...dialogProps}>
|
<div data-testid="navbarmenu" className={styles.container} ref={ref} {...overlayProps} {...dialogProps}>
|
||||||
@ -37,8 +46,8 @@ export function NavBarMenu({ activeItem, navItems, onClose }: Props) {
|
|||||||
<nav className={styles.content}>
|
<nav className={styles.content}>
|
||||||
<CustomScrollbar>
|
<CustomScrollbar>
|
||||||
<ul>
|
<ul>
|
||||||
{navItems.map((link, index) => (
|
{navItems.map((link) => (
|
||||||
<div className={styles.section} key={index}>
|
<div className={styles.section} key={link.text}>
|
||||||
<NavBarMenuItem
|
<NavBarMenuItem
|
||||||
isActive={activeItem === link}
|
isActive={activeItem === link}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -50,12 +59,15 @@ export function NavBarMenu({ activeItem, navItems, onClose }: Props) {
|
|||||||
text={link.text}
|
text={link.text}
|
||||||
url={link.url}
|
url={link.url}
|
||||||
isMobile={true}
|
isMobile={true}
|
||||||
|
pinned={!link.hideFromNavbar}
|
||||||
|
canPin={newNavigationEnabled && link.id !== 'search'}
|
||||||
|
onTogglePin={() => link.id && toggleItemPin(link.id)}
|
||||||
/>
|
/>
|
||||||
{link.children?.map(
|
{link.children?.map(
|
||||||
(childLink, childIndex) =>
|
(childLink) =>
|
||||||
!childLink.divider && (
|
!childLink.divider && (
|
||||||
<NavBarMenuItem
|
<NavBarMenuItem
|
||||||
key={childIndex}
|
key={childLink.text}
|
||||||
icon={childLink.icon as IconName}
|
icon={childLink.icon as IconName}
|
||||||
isActive={activeItem === childLink}
|
isActive={activeItem === childLink}
|
||||||
isDivider={childLink.divider}
|
isDivider={childLink.divider}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Icon, IconName, Link, useTheme2 } from '@grafana/ui';
|
import { Icon, IconButton, IconName, Link, useTheme2 } from '@grafana/ui';
|
||||||
import { css } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
@ -14,6 +14,9 @@ export interface Props {
|
|||||||
url?: string;
|
url?: string;
|
||||||
adjustHeightForBorder?: boolean;
|
adjustHeightForBorder?: boolean;
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
|
canPin?: boolean;
|
||||||
|
pinned?: boolean;
|
||||||
|
onTogglePin?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NavBarMenuItem({
|
export function NavBarMenuItem({
|
||||||
@ -26,10 +29,20 @@ export function NavBarMenuItem({
|
|||||||
text,
|
text,
|
||||||
url,
|
url,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
|
canPin = false,
|
||||||
|
pinned = false,
|
||||||
|
onTogglePin,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme, isActive, styleOverrides);
|
const styles = getStyles(theme, isActive, styleOverrides);
|
||||||
|
|
||||||
|
const onClickPin = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
onTogglePin?.();
|
||||||
|
};
|
||||||
|
|
||||||
const linkContent = (
|
const linkContent = (
|
||||||
<div className={styles.linkContent}>
|
<div className={styles.linkContent}>
|
||||||
<div>
|
<div>
|
||||||
@ -60,30 +73,77 @@ export function NavBarMenuItem({
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return isDivider ? (
|
return isDivider ? (
|
||||||
<li data-testid="dropdown-child-divider" className={styles.divider} tabIndex={-1} aria-disabled />
|
<li data-testid="dropdown-child-divider" className={styles.divider} tabIndex={-1} aria-disabled />
|
||||||
) : (
|
) : (
|
||||||
<li>{element}</li>
|
<li className={styles.listItem}>
|
||||||
|
{element}
|
||||||
|
{canPin && (
|
||||||
|
<IconButton
|
||||||
|
name="anchor"
|
||||||
|
className={cx('pin-button', styles.pinButton, { [styles.visible]: pinned })}
|
||||||
|
onClick={onClickPin}
|
||||||
|
tooltip={`${pinned ? 'Unpin' : 'Pin'} menu item`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isDivider ? (
|
return isDivider ? (
|
||||||
<div data-testid="dropdown-child-divider" className={styles.divider} tabIndex={-1} aria-disabled />
|
<div data-testid="dropdown-child-divider" className={styles.divider} tabIndex={-1} aria-disabled />
|
||||||
) : (
|
) : (
|
||||||
<>{element}</>
|
<div style={{ position: 'relative' }}>{element}</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
NavBarMenuItem.displayName = 'NavBarMenuItem';
|
NavBarMenuItem.displayName = 'NavBarMenuItem';
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], styleOverrides: Props['styleOverrides']) => ({
|
const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], styleOverrides: Props['styleOverrides']) => ({
|
||||||
|
visible: css`
|
||||||
|
color: ${theme.colors.text.primary} !important;
|
||||||
|
opacity: 100% !important;
|
||||||
|
`,
|
||||||
divider: css`
|
divider: css`
|
||||||
border-bottom: 1px solid ${theme.colors.border.weak};
|
border-bottom: 1px solid ${theme.colors.border.weak};
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin: ${theme.spacing(1)} 0;
|
margin: ${theme.spacing(1)} 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`,
|
`,
|
||||||
|
listItem: css`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
|
color: ${theme.colors.text.primary};
|
||||||
|
|
||||||
|
> *:first-child::after {
|
||||||
|
background-color: ${theme.colors.action.hover};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .pin-button {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover > .pin-button,
|
||||||
|
&:focus-visible > .pin-button {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
pinButton: css`
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 2;
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
element: css`
|
element: css`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: none;
|
background: none;
|
||||||
@ -93,23 +153,24 @@ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], styleOverr
|
|||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 5px 12px 5px 10px;
|
padding: 5px 12px 5px 10px;
|
||||||
position: relative;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:hover,
|
&:focus-visible + .pin-button {
|
||||||
&:focus-visible {
|
opacity: 100%;
|
||||||
background-color: ${theme.colors.action.hover};
|
|
||||||
color: ${theme.colors.text.primary};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&::after {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
outline: 2px solid ${theme.colors.primary.main};
|
outline: 2px solid ${theme.colors.primary.main};
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
display: ${isActive ? 'block' : 'none'};
|
display: ${isActive ? 'block' : 'none'};
|
||||||
@ -123,6 +184,15 @@ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], styleOverr
|
|||||||
background-image: ${theme.colors.gradients.brandVertical};
|
background-image: ${theme.colors.gradients.brandVertical};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
${styleOverrides};
|
${styleOverrides};
|
||||||
`,
|
`,
|
||||||
externalLinkIcon: css`
|
externalLinkIcon: css`
|
||||||
|
@ -14,7 +14,7 @@ import { NavBarMenu } from './NavBarMenu';
|
|||||||
import NavBarItem from './NavBarItem';
|
import NavBarItem from './NavBarItem';
|
||||||
import { NavBarItemWithoutMenu } from './NavBarItemWithoutMenu';
|
import { NavBarItemWithoutMenu } from './NavBarItemWithoutMenu';
|
||||||
import { Branding } from '../Branding/Branding';
|
import { Branding } from '../Branding/Branding';
|
||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
const onOpenSearch = () => {
|
const onOpenSearch = () => {
|
||||||
locationService.partial({ search: 'open' });
|
locationService.partial({ search: 'open' });
|
||||||
@ -27,17 +27,9 @@ const searchItem: NavModelItem = {
|
|||||||
icon: 'search',
|
icon: 'search',
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
export const NavBarNext = React.memo(() => {
|
||||||
navBarTree: state.navBarTree,
|
const navBarTree = useSelector((state: StoreState) => state.navBarTree);
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = {};
|
|
||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
|
||||||
|
|
||||||
export interface Props extends ConnectedProps<typeof connector> {}
|
|
||||||
|
|
||||||
export const NavBarNextUnconnected = React.memo(({ navBarTree }: Props) => {
|
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -48,12 +40,15 @@ export const NavBarNextUnconnected = React.memo(({ navBarTree }: Props) => {
|
|||||||
};
|
};
|
||||||
const navTree = cloneDeep(navBarTree);
|
const navTree = cloneDeep(navBarTree);
|
||||||
const coreItems = navTree.filter((item) => item.section === NavSection.Core);
|
const coreItems = navTree.filter((item) => item.section === NavSection.Core);
|
||||||
|
const pinnedCoreItems = coreItems.filter((item) => !item.hideFromNavbar);
|
||||||
const pluginItems = navTree.filter((item) => item.section === NavSection.Plugin);
|
const pluginItems = navTree.filter((item) => item.section === NavSection.Plugin);
|
||||||
|
const pinnedPluginItems = pluginItems.filter((item) => !item.hideFromNavbar);
|
||||||
const configItems = enrichConfigItems(
|
const configItems = enrichConfigItems(
|
||||||
navTree.filter((item) => item.section === NavSection.Config),
|
navTree.filter((item) => item.section === NavSection.Config),
|
||||||
location,
|
location,
|
||||||
toggleSwitcherModal
|
toggleSwitcherModal
|
||||||
);
|
);
|
||||||
|
const pinnedConfigItems = configItems.filter((item) => !item.hideFromNavbar);
|
||||||
const activeItem = isSearchActive(location) ? searchItem : getActiveItem(navTree, location.pathname);
|
const activeItem = isSearchActive(location) ? searchItem : getActiveItem(navTree, location.pathname);
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
@ -77,7 +72,7 @@ export const NavBarNextUnconnected = React.memo(({ navBarTree }: Props) => {
|
|||||||
</NavBarSection>
|
</NavBarSection>
|
||||||
|
|
||||||
<NavBarSection>
|
<NavBarSection>
|
||||||
{coreItems.map((link, index) => (
|
{pinnedCoreItems.map((link, index) => (
|
||||||
<NavBarItem
|
<NavBarItem
|
||||||
key={`${link.id}-${index}`}
|
key={`${link.id}-${index}`}
|
||||||
isActive={isMatchOrChildMatch(link, activeItem)}
|
isActive={isMatchOrChildMatch(link, activeItem)}
|
||||||
@ -89,9 +84,9 @@ export const NavBarNextUnconnected = React.memo(({ navBarTree }: Props) => {
|
|||||||
))}
|
))}
|
||||||
</NavBarSection>
|
</NavBarSection>
|
||||||
|
|
||||||
{pluginItems.length > 0 && (
|
{pinnedPluginItems.length > 0 && (
|
||||||
<NavBarSection>
|
<NavBarSection>
|
||||||
{pluginItems.map((link, index) => (
|
{pinnedPluginItems.map((link, index) => (
|
||||||
<NavBarItem key={`${link.id}-${index}`} isActive={isMatchOrChildMatch(link, activeItem)} link={link}>
|
<NavBarItem key={`${link.id}-${index}`} isActive={isMatchOrChildMatch(link, activeItem)} link={link}>
|
||||||
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
||||||
{link.img && <img src={link.img} alt={`${link.text} logo`} />}
|
{link.img && <img src={link.img} alt={`${link.text} logo`} />}
|
||||||
@ -103,7 +98,7 @@ export const NavBarNextUnconnected = React.memo(({ navBarTree }: Props) => {
|
|||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
|
|
||||||
<NavBarSection>
|
<NavBarSection>
|
||||||
{configItems.map((link, index) => (
|
{pinnedConfigItems.map((link, index) => (
|
||||||
<NavBarItem
|
<NavBarItem
|
||||||
key={`${link.id}-${index}`}
|
key={`${link.id}-${index}`}
|
||||||
isActive={isMatchOrChildMatch(link, activeItem)}
|
isActive={isMatchOrChildMatch(link, activeItem)}
|
||||||
@ -128,9 +123,7 @@ export const NavBarNextUnconnected = React.memo(({ navBarTree }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
NavBarNextUnconnected.displayName = 'NavBarNext';
|
NavBarNext.displayName = 'NavBarNext';
|
||||||
|
|
||||||
export const NavBarNext = connector(NavBarNextUnconnected);
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
search: css`
|
search: css`
|
||||||
|
@ -1,15 +1,32 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem } from '@grafana/data';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
export const initialState: NavModelItem[] = config.bootData.navTree;
|
const defaultPins = ['home', 'dashboards', 'explore', 'alerting'].join(',');
|
||||||
|
const storedPins = (window.localStorage.getItem('pinnedNavItems') ?? defaultPins).split(',');
|
||||||
|
|
||||||
|
export const initialState: NavModelItem[] = (config.bootData.navTree as NavModelItem[]).map((n: NavModelItem) => ({
|
||||||
|
...n,
|
||||||
|
hideFromNavbar: n.id === undefined || !storedPins.includes(n.id),
|
||||||
|
}));
|
||||||
|
|
||||||
const navTreeSlice = createSlice({
|
const navTreeSlice = createSlice({
|
||||||
name: 'navBarTree',
|
name: 'navBarTree',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {},
|
reducers: {
|
||||||
|
togglePin: (state, action: PayloadAction<{ id: string }>) => {
|
||||||
|
const navItemIndex = state.findIndex((navItem) => navItem.id === action.payload.id);
|
||||||
|
state[navItemIndex].hideFromNavbar = !state[navItemIndex].hideFromNavbar;
|
||||||
|
window.localStorage.setItem(
|
||||||
|
'pinnedNavItems',
|
||||||
|
state
|
||||||
|
.filter((n) => !n.hideFromNavbar)
|
||||||
|
.map((n) => n.id)
|
||||||
|
.join(',')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {} = navTreeSlice.actions;
|
export const { togglePin } = navTreeSlice.actions;
|
||||||
|
|
||||||
export const navTreeReducer = navTreeSlice.reducer;
|
export const navTreeReducer = navTreeSlice.reducer;
|
||||||
|
1423
public/test/mocks/navModel.ts
Normal file
1423
public/test/mocks/navModel.ts
Normal file
File diff suppressed because it is too large
Load Diff
35
public/test/redux-rtl.tsx
Normal file
35
public/test/redux-rtl.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render as rtlRender } from '@testing-library/react';
|
||||||
|
import { AnyAction, configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { ThunkMiddlewareFor } from '@reduxjs/toolkit/dist/getDefaultMiddleware';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { createRootReducer } from 'app/core/reducers/root';
|
||||||
|
import { StoreState } from 'app/types';
|
||||||
|
import { mockNavModel } from './mocks/navModel';
|
||||||
|
|
||||||
|
function render(
|
||||||
|
ui: React.ReactElement,
|
||||||
|
{
|
||||||
|
preloadedState = { navIndex: mockNavModel },
|
||||||
|
store = configureStore<
|
||||||
|
StoreState,
|
||||||
|
AnyAction,
|
||||||
|
ReadonlyArray<ThunkMiddlewareFor<StoreState, { thunk: true; serializableCheck: false; immutableCheck: false }>>
|
||||||
|
>({
|
||||||
|
reducer: createRootReducer(),
|
||||||
|
preloadedState,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware({ thunk: true, serializableCheck: false, immutableCheck: false }),
|
||||||
|
}),
|
||||||
|
...renderOptions
|
||||||
|
}: { preloadedState?: Partial<StoreState>; store?: ReturnType<typeof configureStore> } = {}
|
||||||
|
) {
|
||||||
|
function Wrapper({ children }: { children: React.ReactNode }) {
|
||||||
|
return <Provider store={store}>{children}</Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from '@testing-library/react';
|
||||||
|
export { render };
|
Loading…
Reference in New Issue
Block a user