Using icon in component prop object in Storybook crash browser - reactjs

Didn't find anything about this question, so I will appreciate any help/ideas from you.
I have simple input component in stories.tsx like this one:
export const Icon: Story<InputProps> = (args) => {
...
return (
<Input
...
rightIcon={{ icon: <OpenEyeIcon size="medium" /> }}
/>
);
};
Using construction in rightIcon prop is what I need and it works good in my app and when I watching stories. But when I try to use this component in docs.mdx like this:
<Canvas>
<Story id="components-input--icon" />
</Canvas>
it crash the browser and timeout error occur.
This is not the problem about webpack loader that has many issues in github, I've tried to specify loader for storybook, but it wont help.
Fun things happens when I declare component outside of the story like this:
const IconComponent = () => {
...
return (
<Input
...
rightIcon={{ icon: <OpenEyeIcon size="medium" /> }}
/>
);
}
export const Icon: Story<InputProps> = (args) => {
return <IconComponent />
}
And this works as intended.
Any other components work from story without problem, problem occurs only when I use icon inside of object. I tried this on storybook versions 6.4.x, 6.5.x (latest by now).
Any ideas why this happens, and how to fix it?

Related

Limiting React children prop to one component of each given type with Typescript

Let's say I have Input component, I'd like to optionally receive one Button component, one Label and/or one Icon component. Order is not important.
// Correct use case
<Input>
<Button />
<Label />
<Icon />
</Input>
// Correct use case
<Input>
<Button />
</Input>
// Incorrect use case
<Input>
<Button />
<Button />
</Input>
Does anyone know what typescript interface would solve this problem?
Real use case:
<Input
placeholder="Username"
name="username"
type={InputType.TEXT}
>
<Input.Label htmlFor={"username"}>Label</Input.Label>
<Input.Max onClick={() => console.log("Max clicked!")} />
<Input.Icon type={IconType.AVATAR} />
<Input.Button
onClick={onClickHandler}
type={ButtonType.GRADIENT}
loading={isLoading}
>
<Icon type={IconType.ARROW_RIGHT} />
</Input.Button>
<Input.Status>Status Message</Input.Status>
<Input.Currency>BTC</Input.Currency>
</Input>
I have a dynamic check that would throw an error if anything but listed child components is being used (all of possible options are presented), but I'd like to somehow include the Typescript as well and show type errors whenever somebody uses child component of different type or duplicates a component.
Looking at your code and question, the key factors can be regarded as these:
allow only one occurrence for each Button/Icon/Label component.
use JSX bracket hierarchy with given specific elements; use React.Children API.
use typescript.
These requests are contradicting at the moment. Because with typescript, it's really difficult to implement permutable array type. Here's two way of solving it, each highlights some of the key favors.
Solution 1. If the tool is not restricted to typescript, React's top level Children API can easily solve this problem.
const Button = () => {
return <button>button</button>;
}
const Label = () => {
return <label>label</label>
}
const Icon = () => {
return <div>icon</div>
}
const WithSoleChild = (props) => {
// used top-level react child API
// https://reactjs.org/docs/react-api.html#reactchildren
const childNames = React.Children.map(props.children, c => c.type.name);
// count the duplication
const childDupes = childNames.reduce((ac, cv) => ({ ...ac, [cv]: (ac[cv] || 0) + 1 }),{})
// same child should not be duplicated
const childValid = Object.values(childDupes).some(amount => amount < 2);
if (!childValid) {
// added stack trace for better dev experience.
console.error('WithSoleChild: only one Button/Icon/Label is allowed', new Error().stack);
// disable render when it does not fit to the condition.
return null;
}
return props.children;
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<div>
{/* this does not render, and throws an error to console */}
<WithSoleChild>
<Button />
<Button />
</WithSoleChild>
{/* this does render */}
<WithSoleChild>
<Button />
<Icon />
<Label />
</WithSoleChild>
</div>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
<div id="root"></div>
Solution 2. However, there is a more graceful solution, which works perfectly in typescript too; it is called slot component approach, also known as containment component in the official React document.
// App.tsx
import React from "react";
// extend is necessary to prevent these types getting regarded as same type.
interface TButton extends React.FC {};
interface TIcon extends React.FC {};
interface TLabel extends React.FC {};
const Button: TButton = () => {
return <button className="button">button</button>;
};
const Icon: TIcon = () => {
return <div>icon</div>;
};
const Label: TLabel = () => {
return <label>label</label>;
};
interface IWithSoleChild {
button: React.ReactElement<TButton>;
icon: React.ReactElement<TIcon>;
label: React.ReactElement<TLabel>;
}
const WithSoleChild: React.FC<IWithSoleChild> = ({ button, Icon, label }) => {
return null;
};
function App() {
return (
<div className="App">
<WithSoleChild button={<Button />} icon={<Icon />} label={<Label />} />
</div>
);
}

Can't use onChange property on input file by using useRef

I want customize the input file button so I decided to use useRef, but in this situation I cant read file properties. How can I do this?
const inputFile = useRef(null);
const onButtonClick = () => {
inputFile.current.click();
};
return (
<>
<ToggleButtonGroup size="small" className={classes.marginEnd}>
<input
style={{ display: "none" }}
// accept=".xls,.xlsx"
ref={inputFile}
onChange={(e) => handleInput(e)}
type="file"
// onClick={handleInput}
/>
<Tooltip title="upload">
<ToggleButton size={"small"}>
<CloudUpload onClick={onButtonClick}></CloudUpload>
</ToggleButton>
</Tooltip>
</ToggleButtonGroup>
</>
);
I tried to create a codepen to test the same, it worked well for me. Can you try commenting out the onChange handler if it works for you? Rest of my code is quite similar to yours. The other issue might be that the onButtonClick function is not triggered, if you can put a console log or a debugger to test it out once.
Link to Codepen - Click on File Upload button, the popup comes up.
Also, some notes from React JS Documentation about the same - React docs for File Upload Input

Redux store updates everything even if I just update a nested attribute?

In my application, the user should have the possibility to edit his name. I thought about having his initial name as the default value of useState(). This works perfectly - the name is also updated as well.
function EditName(props) {
const { navigation, dispatch, initFirstName } = props;
const [text, setText] = useState(initFirstName);
return (
<>
<Button title="Save" onPress={() => dispatch(updateFirstName(text))} />
<TextInput
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
onChangeText={(inp) => setText(inp)}
value={text}
/>
</>
)
}
function mapStateToProps(state) {
return (
{
initFirstName: state.user.data.firstName,
}
);
}
export default connect(mapStateToProps)(EditName);
But now I came across a problem if the user clicks on the save button and dispatch(updateFirstName(text)) is handled. text is overwriten by initFirstName again.
My Redux (toolkit) store is structured as follows:
{
data: {
firstName: "Test"
},
updateFirstName: {
pending: null,
error: null,
},
}
The updateFirstName(text) method dispatches an action that sets state.updateFirstName.pending = true;. If I leave this out - everything works.
So I guess that a new reference to the store (state) is created and React thinks: "Oh - I got a new initFirstName" (beside it's still the same because the update hasn't happened yet).
What would be a better way to do this? What's a solution for this problem?
Maybe try to create a function that will be called instead of assigning onPress an arrow function. Basically, the function will be called with the initial value of text.
Make sure you use the current value of text, try this:
const saveHandler = () => {
dispatch(updateFirstName(text))
}
...
<Button title="Save" onPress={saveHandler} />
#Taghi Khavari had a good guess: it had nothing to do with the code that I posted on StackOverflow - it was a React Navigation issue.
I use nested navigators in my app. I declared the nested navigator inside my App function which resulted in the EditName component remounted multiple times after the state changed (I actually don't know why though).
When I declared the nested navigators OUTSIDE my App function (like in the following example), it worked and the component only got mounted once. Can anyone explain why?
function Home() {
return (
<NestedStack.Navigator>
<NestedStack.Screen name="Profile" component={Profile} />
<NestedStack.Screen name="Settings" component={Settings} />
</NestedStack.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<RootStack.Navigator mode="modal">
<RootStack.Screen
name="Home"
component={Home}
options={{ headerShown: false }}
/>
<RootStack.Screen name="EditPost" component={EditPost} />
</RootStack.Navigator>
</NavigationContainer>
);
}

how to click a button programmatically in react native

i have tried couple of ways to figure out how to perform click event programmatically, i found that useRef can solve the problem. but i tried it doesn't seems to work for me. below is the code,
//declaration
const reff=useReff(null);
//button design
<Button
onPress={()=>{console.log('Pressed')}}
title="Press me"
ref={reff}
/>
//Triggering the event
reff.current.onPress();
its giving me an error "onPress is undefined". i have tried with onClick(),click(),press() nothing worked for me unfortunately.
can someone help me on this please..
import React, { useRef } from 'react';
import { Button, View } from 'react-native';
const App = () => {
const btnRef = useRef();
return (
<View>
<Button
title="Ref"
onPress={() => {
console.info('Ref props', btnRef.current.props);
btnRef.current.props.onPress();
btnRef.current.props.onHandlePress();
}}
/>
<Button
title="Click Me"
ref={btnRef}
onPress={() => console.log('onPress props called')}
onHandlePress={() => console.log('Custom onHandlePress called')}
/>
</View>
);
};
export default App;
I'm using react native cli
"react": "16.13.1",
"react-native": "0.63.4"
I guess, whatever props that passed in Button component can be accessed by calling ref.current.props.anyprops, Good luck!
Try this once, you just need to set ref :
<Button id={buttonId}
onClick={e => console.log(e.target)}
ref={(ref) => { this.myButton = ref; }}
/>
and then you can add this in your function :
yourFunction = () => {
this.myButton.click()
}
Hope it helps. feel free for doubts
This works for me:
<Button
onClick={() => console.log('pressed')}
title="Press me"
/>
Just use the curly braces when you write the callback functions on multiple lines.

Material-ui's Switch component onChange handler is not firing

I've put some Switches in an app and they work fine. Then I put the Switches in another app, but they don't work when clicked.
Both apps are using the same component. Here it is working in one app:
And here's the other app, not working:
In the second app, the onChange handler doesn't seem to ever fire.
The code in both apps looks like the following:
<Switch
checked={(console.log('checked:', status === 'visible'), status === 'visible')}
onChange={(e, c) => console.log('event:', e, c)}
/>
In the first app I see the output of those console.logs, while in the second app I only see the initial console.log of the checked prop, but I never see any of the onChange prop.
I checked if any ancestor elements have click handlers, and I didn't find any that are returning false, calling stopPropagation, or calling preventDefault.
Notice in the gif that when I click, the ripple effect still works, so click handling is obviously still working.
Any ideas why onChange may not be firing?
UPDATE! I replaced the switches with regular <input type="checkbox"> elements, and it works great! See:
Looks to me like something is wrong with material-ui's <Switch> component. I have a hunch that I will investigate when I get a chance: there might be more than one React singleton in the application. I'll be back to post an update.
I think, this is a weird fix and it is working smoothly for me. So, instead of handleChange I am using handleClick. I am not using event here, instead I am passing a string which is obviously the name of the state or id in case of arrays.
<Switch
checked={this.state.active}
onClick={() => this.handleToggle('active')}
value="active"
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>
handleToggle = (name: string) => {
this.setState({ active: !this.state.active });
};
I tried handleChange, but the problem still persists. I hope this will get fixed soon.
I've had the same issue with Checkbox and Switch in the WordPress admin area.
Turns out, there was global CSS rule like:
input[type="checkbox"] {
height: 1rem;
width: 1rem;
}
Clicking the upper left corner of the element works, though.
As a solution, I reset some styles in my app root.
EDIT: Nevermind, I just put my whole app into shadow DOM. There are a few gotchas, I'll list them here:
You have to provide a custom insertion point for Material-UI style elements inside the shadow DOM. In general, you have to make nothing gets put outside of you shadow DOM.
You have to load/link the font inside the shadow DOM and outside the shadow DOM (the light DOM).
Use ScopedCssBaseline instead of the global reset.
Dialogs have to have their container prop specified.
This is how I've set things up with Material-UI:
// configure-shadow-dom.js
import { create } from 'jss';
import { jssPreset } from '#material-ui/core/styles';
const shadowHostId = 'my-app-root-id'
export const appRoot = document.createElement('div')
appRoot.setAttribute('id', 'app-root')
const styleInsertionPoint = document.createComment('jss-insertion-point')
export const jss = create({
...jssPreset(),
insertionPoint: styleInsertionPoint,
})
const robotoFontLink = document.createElement('link')
robotoFontLink.setAttribute('rel', 'stylesheet')
robotoFontLink.setAttribute('href', 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap')
const shadowHost = document.getElementById(shadowHostId)
shadowHost.attachShadow({ mode: 'open' })
const shadowRoot = shadowHost.shadowRoot
shadowRoot.appendChild(robotoFontLink)
shadowRoot.appendChild(styleInsertionPoint)
shadowRoot.appendChild(appRoot)
// index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import ScopedCssBaseline from '#material-ui/core/ScopedCssBaseline';
import { StylesProvider } from '#material-ui/core/styles';
import App from './App';
import { jss, appRoot } from './configure-shadow-dom';
ReactDOM.render(
<React.StrictMode>
<StylesProvider jss={jss}>
<ScopedCssBaseline>
<App />
</ScopedCssBaseline>
</StylesProvider>
</React.StrictMode>,
appRoot
);
It turns out that in my case, there was a CSS in the page, something like
.some-component { pointer-events: none; }
.some-component * { pointer-events: auto; }
where .some-component was containing my material-ui buttons and switches. I had to manually set pointer-events to all (or some value, I don't remember at at the moment) for the elements inside the switches.
So, that's one thing to look out for: check what pointer-events style is doing.
in Switch component, changing onChange to onClick worked for me:
<Switch
checked={this.state.active}
onClick={() => this.handleToggle('active')}
value="active"
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>
still running into similar issues getting an event from the switch. here's a quick fix using internal state (you should be using hooks now and functional components, if not you are missing out, but you can use setState to do what i'm showing here in a class component.)
const [switchChecked, setSwitchChecked] = useState(false);
.......
<Switch checked={switchChecked} onChange={() => {setSwitchChecked(!switchChecked);}}/>
you'll need to have a value for switchChecked in the components local state.
Try this one checked or unchecked passed as a second argument.
<Switch
checked={this.state.active}
onClick={(e,flag) => this.handleToggle('active', flag)}
value="active"
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>```
The issue is that you have to sniff the checked attribute from the event: event.target.checked
Full solution:
import { Switch } from '#material-ui/core';
import { useState } from 'react';
function App() {
const [checked, setChecked] = useState(false);
const switchHandler = (event) => {
//THIS IS THE SOLUTION - use event.target.checked to get value of switch
setChecked(event.target.checked);
};
return (
<div>
<Switch checked={checked} onChange={switchHandler} />
</div>
);
}
export default App;

Resources