React Material Tabs indicator not showing on first load - reactjs

I have a simple Tabs setup with React Material UI (https://material-ui.com/components/tabs/) where the path value is set dynamically
export const Subnav: React.FC<Props> = ({ routes = [] }) => {
const { pathname } = useLocation();
const { push } = useHistory();
const handleChange = (e: ChangeEvent<{}>, path: string) => push(path);
return (
<Tabs
indicatorColor="primary"
onChange={handleChange}
scrollButtons="auto"
textColor="primary"
value={pathname}
variant="scrollable"
>
{routes.map(r => (
<Tab label={r.name} value={r.path} />
))}
</Tabs>
);
};
When I first load a page / navigate to one of the tab routes, the correct tab is selected, but the indicator is not shown. In order for the indicator to be shown I have to click the same tab again or select another.

For this, there is one more way to handle this.
In React material UI there is component <Grow/>
you can warp <Tabs/> within <Grow/> and <Grow/> will correct the indicator position.
Following is the example:
<Grow in={true}>
<Tabs
action={ref => ref.updateIndicator()}
>
<Tab label="Item One" />
<Tab label="Item Two" />
<Tab label="Item Three" />
<Tabs>
</Grow>

This was resolved via https://github.com/mui-org/material-ui/issues/20527
You need to manually trigger the updateIndicator method. Easiest way to do this, is to call a resize event (which triggers the method)
useEffect(() => {
window.dispatchEvent(new CustomEvent("resize"));
}, []);
Alternatively add a ref to the actions prop and call the method directly. Still seems like a non-ideal solution, but it is what the maintainer provided.

My solution based on above ones but a bit different. The difference is that I update the indicator via a deferred call (setTimeout) with 400ms delay. Don't know real reason.
Below my definition of Mui-Tabs wrapper
import * as React from 'react';
import MaterialTabs, { TabsActions } from '#material-ui/core/Tabs';
import { withStyles } from '#material-ui/core';
import { IProps } from './types';
import styles from './styles';
class Tabs extends React.PureComponent<IProps> {
tabsActions: React.RefObject<TabsActions>;
constructor(props) {
super(props);
this.tabsActions = React.createRef();
}
/**
* Read more about this here
* - https://github.com/mui-org/material-ui/issues/9337#issuecomment-413789329
*/
componentDidMount(): void {
setTimeout(() => {
if (this.tabsActions.current) {
this.tabsActions.current.updateIndicator();
}
}, 400); // started working only with this timing
}
render(): JSX.Element {
return (
<MaterialTabs
action={this.tabsActions}
indicatorColor="primary"
variant="scrollable"
scrollButtons="auto"
{...this.props}
/>
);
}
}
export default withStyles(styles)(Tabs);

export const Subnav: React.FC<Props> = ({ routes = [], render }) => {
const { pathname } = useLocation();
const { push } = useHistory();
const handleChange = (e: ChangeEvent<{}>, path: string) => push(path);
const ref = useRef();
useEffect(() => {ref.current.updateIndicator()}, [pathname, render])
return (
<Tabs
action={ref}
indicatorColor="primary"
onChange={handleChange}
scrollButtons="auto"
textColor="primary"
value={pathname}
variant="scrollable"
>
{routes.map(r => (
<Tab label={r.name} value={r.path} />
))}
</Tabs>
);
};
Put into props.render your anything dynamically value

Related

TypeError: Cannot read properties of undefined (reading '_focus') while using ChakraUI Select

I have written page that uses Tabs and Tab Panel. I'm writing UI tests using react-testing library. The theme has been modified into custom theme that is imported at the root of the Next.js app. I'm using page that uses a component with form for the Tab. In the listening it's <MyComponent />. The component inside has Select component used from ChakraUI. Other inputs don't affect on the error that appears.
Libraries:
ChakraUI 2.8
react-hook-form
Next.js 12
The error that appears is
Cannot read properties of undefined (reading '_focus')
TypeError: Cannot read properties of undefined (reading '_focus')
at /path/to/project/node_modules/#chakra-ui/select/dist/index.cjs.js:102:22
My Page in a nutchell looks like
const MyPage () => {
const instrumentInfoTab = React.useRef() as React.MutableRefObject<HTMLInputElement>;
return (
<Tabs>
<TabList><Tab>...</Tab</TabLists>
</Tabs>
<TabPanels>
<TabPanel>
<MyComponent updateTransactionState={/*some function to handle state*/} nextTabRef={instrumentInfoTab} />
</TabPanel>
</TabPanels>
<Tab
)
}
MyComponent
interface IInstrumentInfoPanelProps {
nextTabRef: React.MutableRefObject<HTMLInputElement>;
updateTransactionState: (data: InstrumentInfoInput) => void;
}
const MyComponent = (props: IInstrumentInfoPanelProps) => {
const { nextTabRef, updateTransactionState } = props;
const textColor = useColorModeValue('secondaryGray.900', 'white');
const methods = useForm<InstrumentInfoInput>({
resolver: zodResolver(instrumentInfoSchema)
});
const { handleSubmit, register } = methods;
const onSubmit = (data: InstrumentInfoInput) => {
updateTransactionState(data);
nextTabRef.current.click();
};
return (
<TabPanel>
<CustomCard>
<Text>
Instrument Info
</Text>
<Flex>
<form onSubmit={handleSubmit(onSubmit)}>
<FormProvider {...methods}>
<SimpleGrid>
<Stack>
<Flex direction="column" mb="34px">
<FormLabel
ms="10px"
htmlFor="transactionType"
fontSize="sm"
color={textColor}
fontWeight="bold"
_hover={{ cursor: 'pointer' }}>
Transaction type*
</FormLabel>
<Select
{...register('transactionType')}
id="transactionType"
variant="main"
defaultValue="buy">
<option value="buy">BUY</option>
<option value="sell">SELL</option>
</Select>
</Flex>
</Stack>
<Stack>
<InputField
id="accountName"
name="accountName"
placeholder="eg. Coinbase"
label="Account Name*"
data-testid="instrumentInfoPanel-accountName"
/>
</Stack>
</SimpleGrid>
<Flex justify="space-between" mt="24px">
<Button
type="submit">
Next
</Button>
</Flex>
</FormProvider>
</form>
</Flex>
</CustomCard>
</TabPanel>
);
};
export default MyComponent;
My Test looks like. The error appears in render function
jest.mock('next/link', () => {
return ({ children }) => {
return children;
};
});
interface IWrapedComponent {
children?: JSX.Element;
}
const test = (ref: MutableRefObject<HTMLElement | null>): void => {
if (ref.current) ref.current.focus();
};
const WrappedComponent = (props: IWrapedComponent) => {
const { children } = props;
const ref = React.useRef() as React.MutableRefObject<HTMLInputElement>;
useEffect(() => {
test(ref);
}, []);
return (
<Tabs>
<TabList>
<Tab ref={ref}></Tab>
</TabList>
<TabPanels>
<MyComponent />
</TabPanels>
</Tabs>
);
};
describe('Instrument Info Panel', () => {
it('should render inputs for instrument info', () => {
render(
<QueryClientProvider client={new QueryClient()}>
<WrappedComponent />
</QueryClientProvider>
);
});
});
In order to debug an issue I have tried to remove other inputs from form and it worked when there were different input types than Select from ChakraUI.

URL changes set by react-router-dom 'useSearchParams' are not maintained when set by mui 'Tabs' component's onChange callback

I would like to set a tab navigation component based on the search parameter named tab.
If my url is example.com?tab=test2, I would like my navigation bar to display the test2 item as selected.
I'm using mui Tabs component for this.
I'm able to successfully set my Tab state by getting the search parameter with searchParams.get('tab'), however, when I set the tab property, it's only reflected for a moment, before being overwritten with a null state (and so the URL is changed to have no search parameters).
I've tried to add a null checker to set a default search parameter, but it isn't "keeping."
What's very strange is that if I simply set search params using plain buttons, the changes "keep."
I've created a codesandbox minimal implementation to demonstrate, however, in short, my change function, that gets invoked both within the mui Tabs component's onChange callback as well as my simple button onClick callbacks, looks like this:
const handleChange = (event: SyntheticEvent, newValue: string) => {
searchParams.set("tab", newValue);
setSearchParams(searchParams);
};
And the value is initially set upon component instantiation (I believe based on console log experiments) with:
const PartsTabNav = () => {
let [searchParams, setSearchParams] = useSearchParams();
let queryTab = searchParams.get("tab");
Why do my buttons change the URL, but not the MUI Tabs component's onChange, even though both invoke the same function?
EDIT: It seems it might actually have something to do with the <Link> component based on my debugging.
The full example code is below:
import { Grid, Tab, Tabs } from "#mui/material";
import { useTheme } from "#mui/material/styles";
import { SyntheticEvent, useEffect } from "react";
import { Link, useSearchParams, Routes, Route } from "react-router-dom";
const tabOptions = [
{
label: "Test1",
route: "test1"
},
{
label: "Test2",
route: "test2"
}
];
const PartsTabNav = () => {
let [searchParams, setSearchParams] = useSearchParams();
let queryTab = searchParams.get("tab");
queryTab = queryTab ? queryTab : "test1";
useEffect(() => {
if (!queryTab) {
searchParams.set("tab", "test1");
setSearchParams(searchParams);
}
}, [searchParams, setSearchParams, queryTab]);
const handleChange = (event: SyntheticEvent, newValue: string) => {
searchParams.set("tab", newValue);
setSearchParams(searchParams);
};
return (
<div>
<Grid container spacing={1}>
<Grid item xs={12}>
<ul>
<li>
<button
onClick={(e) => {
handleChange(e, "test1");
}}
>
one
</button>
</li>
<li>
<button
onClick={(e) => {
handleChange(e, "test2");
}}
>
two
</button>
</li>
</ul>
<Tabs
value={queryTab}
indicatorColor="primary"
textColor="primary"
aria-label="Tabs"
onChange={handleChange}
>
{tabOptions.map((tab, index) => (
<Tab
key={index}
component={Link}
to="#"
label={tab.label}
value={tab.route}
/>
))}
</Tabs>
</Grid>
</Grid>
</div>
);
};
export default function App() {
return (
<div>
<Routes>
<Route path="/" element={<PartsTabNav />} />
</Routes>
</div>
);
}
The issue was coming from using Link (from react-router) as a component prop for Tab. There was some default click handling that was happening that was causing a page refresh. Changing to the below, fixed the issue.
{tabOptions.map((tab, index) => (
<Tab
key={index}
icon={tab.icon}
label={tab.label}
value={tab.route}
/>
))}

Passing array of object into functional component and mapping

I'm trying to use Material UI to create a reusable navigation tab, however, I am having trouble passing the object over to my functional component and mapping it out. Nothing displays when mapping.
I am fairly new to react hooks. Thanks in advance.
Class Component (passing state over to Navigation)
class MyWorkspace extends Component {
state = {
menuItem: [
{
name: "menu 01",
urlPath: "/home/menu01"
},
{
name: "menu 02",
urlPath: "/home/menu02"
},
{
name: "Reports",
urlPath: "/home/menu03"
},
],
}
}
render () {
return (
<div>
<Navigation menuItem />
</div>
)
}
Functional Component
export default function Navigation({ menuItem }) {
const [value, setValue] = React.useState(2);
const handleChange = (event, newValue) => {
setValue(newValue);
};
const MenuList = () => {
return (
<>
{menuItem.map(item => {
return <Tab label={item.name} className="Nav-Tab" />;
})}
</>
)
}
return (
<div className="Nav-Title row">
<Tabs
className="Nav-Tab-List"
value={value}
indicatorColor="primary"
textColor="primary"
onChange={handleChange}
>
<MenuList />
</Tabs>
</div>
);
}
In the class component, you should assign a value to the prop being passed:
render () {
return (
<div>
<Navigation menuItem={this.state.menuItem} />
</div>
)
}
In function component, you should call MenuList() inside the render :
export default function Navigation({ menuItem }) {
const [value, setValue] = React.useState(2);
const handleChange = (event, newValue) => {
setValue(newValue);
};
const MenuList = () => {
return (
<>
{menuItem.map(item => {
return <Tab label={item.name} className="Nav-Tab" />;
})}
</>
)
}
return (
<div className="Nav-Title row">
<Tabs
className="Nav-Tab-List"
value={value}
indicatorColor="primary"
textColor="primary"
onChange={handleChange}
>
{MenuList()} // call this or put the map here
</Tabs>
</div>
);
}
First you need to define state in constructor
Second destruct menuitem from state
const {menuItem} = this.state
Third pass props like this
<Navigation menuItem={menuItem} />
If you pass like this Navigation menuItem /> you will get boolean value true inside child component.
In MyWorkspace/render function, you don't actually pass the menuItem state.
<Navigation menuItem /> will pass menuItem as true value. Replace it with: <Navigation menuItem={this.state.menuItem} />
Navigation component code looks correct

How to call API inside switch case

I have created a tab component where I have three tabs each tab will contain data from different end points. I have planned to hit API's using switch case since I am having different end points. I have created a onChange function where I used to call three different end points, unfortunately I couldn't able to achieve the result, I am getting error stating that useEffect cannot be used inside onChange. Could anyone guide me to achieve the result. Thanks in advance.
API'S I am using are :https://reqres.in/api/users?page=1, https://reqres.in/api/users?page=2, https://reqres.in/api/users?page=3
import React, { useState, useEffect } from "react";
import Paper from "#material-ui/core/Paper";
import Tab from "#material-ui/core/Tab";
import Tabs from "#material-ui/core/Tabs";
const Tabb = () => {
const [profileData, setProfileData] = useState(0);
useEffect(() => {
fetch("https://reqres.in/api/users?page=1")
.then((results) => results.json())
.then((data) => {
console.log("data", data);
});
});
const handleChange = (e, value) => {
switch (value) {
case 0:
break;
case 1:
break;
case 2:
break;
default:
break;
}
setProfileData(value);
};
return (
<div style={{ marginLeft: "40%" }}>
<h2>Tbas in React JS</h2>
<Paper square>
<Tabs
value={profileData}
textColor="primary"
indicatorColor="primary"
onChange={handleChange}
>
<Tab label="Tab One" />
<Tab label="Tab Two" />
<Tab label="Tab Three" />
</Tabs>
{/* <h3>Tab NO: {value} clicked!</h3> */}
</Paper>
</div>
);
};
You have a single API with a changing page parameter. Whenever you set the page (tab changed), update the state with the number of the page.
Set the page as the useEffect() dependency, so an API call would be issued whenever the page changes. Add the current page to the base query url to the current url.
Demo - choose a tab in the select menu:
const { useState, useEffect } = React;
const Paper = 'div';
const Tabs = 'select';
const Tab = ({ label, ...rest }) => (<option {...rest}>{label}</option>);
const Demo = () => {
const [page, setPage] = useState(0);
useEffect(() => {
const url = `https://reqres.in/api/users?page=${page}`;
fetch(url)
.then((results) => results.json())
.then((data) => {
console.log("data", data);
});
}, [page]);
const handleChange = e => {
setPage(+e.target.value);
};
return (
<Paper>
<Tabs onChange={handleChange}>
<Tab label="Tab One" value={1} />
<Tab label="Tab Two" value={2} />
<Tab label="Tab Three" value={3} />
</Tabs>
{/* <h3>Tab NO: {value} clicked!</h3> */}
</Paper>
);
};
ReactDOM.render(
<Demo />,
root
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
If I understand your question/issue you want to conditionally fetch some data and update state via a switch statement.
Issues
The useEffect without a dependency will be called every render cycle. This is likely not what you want.
You can't conditionally call hooks in loops and callbacks, they must only be called from react functions and other custom hooks.
Solution
I don't think you need the useEffect hook at all, you can define a function to handle the fetch, or better, just use a single fetch in the onClick handler.
const handleChange = (e, value) => {
let url;
switch (value) {
case 0:
default:
url = "https://reqres.in/api/users?page=1";
break;
case 1:
url = "https://reqres.in/api/users?page=2";
break;
case 2:
url = "https://reqres.in/api/users?page=3";
break;
}
fetch(url)
.then((results) => results.json())
.then((data) => {
console.log("data", data);
});
setProfileData(value);
};
You can simplify this a bit more, the switch cases are basically just adding 1 to the value and forming the url. Just use a string template to form the request URL.
const handleChange = (e, value) => {
fetch(`https://reqres.in/api/users?page=${value + 1}`)
.then((results) => results.json())
.then((data) => {
console.log("data", data);
});
setProfileData(value);
};

React-admin tab name translate

I am using React-admin and following the demo that they give out. So far everything is working except for the Tab name/title translation. I have done the translation correctly because other components that have label attribute works fine with the translation.
Translations are getting from en.js file and added to app.js according to the react-admin documentation.
Here is my code :
class TabbedDatagrid extends React.Component {
tabs = [
{ id: 'countries', name: 'root.countries.title' },
{ id: 'languages', name: 'root.languages.title' },
];
state = { countries: [], languages: [] };
static getDerivedStateFromProps(props, state) {
if (props.ids !== state[props.filterValues.status]) {
return { ...state, [props.filterValues.status]: props.ids };
}
return null;
}
handleChange = (event, value) => {
const { filterValues, setFilters } = this.props;
setFilters({ ...filterValues, status: value });
};
render() {
const { classes, filterValues, ...props } = this.props;
return (
<Fragment>
<Tabs
fullWidth
centered
value={filterValues.status}
indicatorColor="primary"
onChange={this.handleChange}
>
{this.tabs.map(choice => (
<Tab
key={choice.id}
label={choice.name}
value={choice.id}
/>
))}
</Tabs>
<Divider />
<Responsive
small={<SimpleList primaryText={record => record.title} />}
medium={
<div>
{filterValues.status === 'countries' && (
<Datagrid hover={false}
{...props}
ids={this.state['countries']}
>
<TextField source="id" />
<TextField source="name" label="root.countries.fields.name"/>
</Datagrid>
)}
{filterValues.status === 'languages' && (
<Datagrid hover={false}
{...props}
ids={this.state['languages']}
>
<TextField source="id" />
<TextField source="name" label="root.languages.fields.name"/>
</Datagrid>
)}
</div>
}
/>
</Fragment>
);
}
}
The translations seems to work everywhere else but the Tab label, What I get instead of the Title is uppercase string of this root.countries.title.
Is there a workaround or how to fix this issue?
You probably used <Tab/> 'directly' from material-ui.
You need to use (create) 'enhanced version' (using translate prop) of this component.
Take inspiration from menu or other translatable components.
You need to pass your translations to your App.js as follows :
import React from 'react';
import { Admin, Resource } from 'react-admin';
import frenchMessages from 'ra-language-french';
import englishMessages from 'ra-language-english';
const messages = {
fr: { component:{label:'test'},...frenchMessages },
en: { component:{label:'test'},...englishMessages },,
}
const i18nProvider = locale => messages[locale];
const App = () => (
<Admin locale="en" i18nProvider={i18nProvider}>
...
</Admin>
);
export default App;
than when you want to use translations inside a component, you need to connect it to the react-admin's translate function as follows :
import { TextInput, translate } from 'react-admin';
const translatedComponent = ({translate, ...props}) => {
return <TextInput label={translate('component.label')} />
}
export default translate(translatedComponent);
it is important to connect the component with translate and to get the translate function from props to get the translation work.

Resources