React DropdownButton - reactjs

I would like to use DropdownButton to change the language on my website. I managed with Select and changing the language works. Unfortunately there is a problem with img placement in Select so I wanted to do it with DropdownButton. And here I have a problem with getting the value from Dropdown.Item.
I get the message: Uncaught TypeError: evt is null. What should I do in such a situation
const DropdownLanguage = () => {
const { i18n } = useTranslation();
const [language, setLanguage] = useState("pl");
const handleLangChange = (evt) => {
const lang = evt.target.value;
console.log(lang);
setLanguage(lang);
i18n.changeLanguage(lang);
};
return (
<>
<DropdownButton
id="dropdown-basic-button"
title="Dropdown button"
value={language}
onSelect={handleLangChange}
>
<Dropdown.Item value="pl">
<img className="flag" src={PL} />
PL
</Dropdown.Item>
<Dropdown.Item value="en">
<img className="flag" src={UK} />
EN
</Dropdown.Item>
</DropdownButton>
</>
);
}
I checked the documentation in React Bootstrap

I was reading the react bootstrap documentation and I see that maybe it's the problem.
The select callback sends you 2 parameters, the first is the eventKey and the second is the event.
OnSelect function description:
A callback fired when a menu item is selected.
(eventKey: any, event: Object) => any
Try changing your handleChange funciton:
const handleLangChange = (evtKey, evt) => {
const lang = evt.target.value;
console.log(lang);
setLanguage(lang);
i18n.changeLanguage(lang);
};
Good luck!!

You are missing the selectKey attribute on your Dropdown.Item components. From the documentation for Dropdown.Item:
Name
Type
Default
Description
...
...
...
...
eventKey
string | number
Value passed to the onSelect handler, useful for identifying the selected menu item.
and note the documentation on Dropdown.onEvent, where the first argument name is eventKey:
Name
Type
Default
Description
...
...
...
...
onSelect
function
A callback fired when a menu item is selected. (eventKey: any, event: Object) => any
Note that DropdownButton is a convenience component, it wraps a Dropdown component with nested Dropdown.Toggle and Dropdown.Menu components, with any Dropdown.Item components used to populate the Dropdown.Menu children.
You may have mistakenly used a value attribute instead of eventKey here. Replacing value with eventKey works, the value of this property is passed directly to your handler as the first argument:
const DropdownLanguage = () => {
const { i18n } = useTranslation();
const [language, setLanguage] = useState("pl");
const handleLangChange = (lang) => {
console.log(lang);
setLanguage(lang);
i18n.changeLanguage(lang);
};
return (
<>
<DropdownButton
id="dropdown-basic-button"
title="Dropdown button"
onSelect={handleLangChange}
>
<Dropdown.Item eventKey="pl" active={language === 'pl'}>
<img className="flag" src={PL} />
PL
</Dropdown.Item>
<Dropdown.Item eventKey="en" active={language === 'en'}>
<img className="flag" src={UK} />
EN
</Dropdown.Item>
</DropdownButton>
</>
);
}
Note that DropdownButton has no value property, either. Instead, to pick a specific menu option as the selected item, set the active property of the selected Dropdown.Item to true.
Here is an online sandbox to demonstrate:
You could otherwise access the event object itself (and event.target) if you give your handleLangChange() handler a second argument to receive it, or you can use arguments[1]. You'll have to use a DOM method in that case, like event.target.getAttribute("value"), to access any attributes. But that's really overkill here.

According to the docs, DropdownButton doesn't have prop onSelect. onSelect is only existed in Dropdown, so you can change from DropdownButton to Dropdown.

Related

How to write test for a button inside a list tag? Unable to get the button element inside a ui tag?

checkResult is a helper function which is imported in my component.jsx
component.jsx
return(
<ul>
{options.map((option) => {
return (
<li key={option.value}>
<button
data-testid="unlock-btn"
onClick={() => {
checkResult()
? lunch(option.value)
: showError();
}}
>
{option.label}
</button>
</li>
);
})}
</ul>;
)
my test
import * as helper from "../helpers/checkResult";
it("fires action when lunch is clicked", async () => {
const spy = jest.spyOn(helper, 'checkResult');
let component;
await act(async()=>{
component = <component /> ;
})
await expect(screen.queryByTestId("unlock-btn"));
fireEvent.click(screen.queryByTestId("unlock-btn"));
expect(spy).toHaveBeenCalled();
});
this is the error i'm getting
Unable to fire a "click" event - please provide a DOM element.
i have also provided my getComponent Method above
You're not providing options to the component so it has nothing to render. You're also using a map to render a list of items all of which have the same id. You should do something like
map((option, index) => {
return (
<li key={option.value}>
<button
data-testid=`unlock-btn-${index}`
This way you can target each individual option by ID in your test.
Edit: Your fireEvent is not defined in your example either.
The right way would be using the aria-label and attributes to be able to select those buttons without the need of data-testid.
<button
onClick={() => { checkResult() ? lunch(option.value): showError();}}
name={option.label} // assuming the labels are unique
>
{option.label}
</button>
then:
import React from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
it('Should do some test', ()=>{
render(<MyComponent/>)
const button = screen.getByRole('button', {name: "some-label"})
fireEvent.click(button)
expect(....).toBe(...)
}

`Popover` (as React component) with `OverlayTrigger`

I'm trying to create rich React component as popover content.
If I use example with simple const popover (https://react-bootstrap.netlify.app/components/overlays/#examples-1) everything works fine.
Problem
But custom react component fails to position itself. It appears on top left of the screen
const MyPopover = React.forwardRef((props, ref) => {
return <Popover ref={ref} placement={"bottom"}>
<Popover.Header as="h3">
<Form.Control></Form.Control>
</Popover.Header>
<Popover.Body>
<strong>Holy guacamole!</strong> Check this info.
</Popover.Body>
</Popover>
})
const PopoverChooser = ({children, container}) => {
const _refTarget = useRef(null)
return <>
<OverlayTrigger
trigger="click"
placement="bottom"
overlay={<MyPopover ref={_refTarget}/>}
target={_refTarget.current}
>
{children}
</OverlayTrigger>
</>
}
export default PopoverChooser;
As you can see, I'v tried to use ref's, but it's doesn't help.
Question
How can it link popover to target button (in image as dropdown button and in code as {children}).
Or should I position MyPopover manually (by checking button ID and assigning position.. etc.?)
Completely different approach to dig in..?
Your approach to forward the ref was right. What you actually forgot is to also inject props. According to the documentation:
The and components do not position themselves.
Instead the (or ) components, inject ref and
style props.
https://react-bootstrap.netlify.app/components/overlays/#overview
So what you need to do is to spread the props like this:
const MyPopover = React.forwardRef((props, ref) => {
return (
<Popover ref={ref} {...props}>
https://codesandbox.io/s/trusting-sid-0050g9

Two instances of the same element seem to share state

In my React application I have a Collapsible component that I use more than once, like so:
const [openFAQ, setOpenFAQ] = React.useState("");
const handleFAQClick = (question: string) => {
if (question === openFAQ) {
setOpenFAQ("");
} else {
setOpenFAQ(question);
}
};
return ({
FAQS.map(({ question, answer }, index) => (
<Collapsible
key={index}
title={question}
open={openFAQ === question}
onClick={() => handleFAQClick(question)}
>
{answer}
</Collapsible>
))
})
And the Collapsible element accepts open as a prop and does not have own state:
export const Collapsible = ({
title,
open,
children,
onClick,
...props
}: Props) => {
return (
<Container {...props}>
<Toggle open={open} />
<Title onClick={onClick}>{title}</Title>
<Content>
<InnerContent>{children}</InnerContent>
</Content>
</Container>
);
};
However, when I click on the second Collapsible, the first one opens... I can't figure out why.
A working example is available in a sandbox here.
You will need to ensure that the label and id for each checkbox is the same. Here's a working example
But if you're trying to implement an accordion style, you may need another approach.
on Collapsible.tsx line 36, the input id is set the same for both the Collapsibles. you need to make them different from each other and the problem would be solved.
One thing is that you have the same id which is wrong BUT it can still work. Just change the checkbox input from 'defaultChecked={...}' to 'checked={...}'.
The reason is that, if you use defaultSomething - it tells react that even if this value will be changed - do not change this value in the DOM - https://reactjs.org/docs/uncontrolled-components.html#default-values
Changing the value of defaultValue attribute after a component has mounted will not cause any update of the value in the DOM

Is there any pitfall of using ref as conditional statement?

Context: I am trying to scroll view to props.toBeExpandItem item which keeps changing depending on click event in parent component. Every time a user clicks on some button in parent component I want to show them this list and scroll in the clicked item to view port. Also I am trying to avoid adding ref to all the list items.
I am using react ref and want to add it conditionally only once in my component. My code goes as below. In all cases the option.id === props.toBeExpandItem would be truthy only once in loop at any given point of time. I want to understand will it add any overhead if I am adding ref=null for rest of the loop elements?
export const MyComponent = (
props,
) => {
const rootRef = useRef(null);
useEffect(() => {
if (props.toBeExpandItem && rootRef.current) {
setTimeout(() => {
rootRef.current?.scrollIntoView({ behavior: 'smooth' });
});
}
}, [props.toBeExpandItem]);
return (
<>
{props.values.map((option) => (
<div
key={option.id}
ref={option.id === props.toBeExpandItem ? rootRef : null}
>
{option.id}
</div>
))}
</>
);
};
Depending upon your recent comment, you can get the target from your click handler event. Will this work according to your ui?
const handleClick = (e) => {
e.target.scrollIntoView()
}
return (
<ul>
<li onClick={handleClick}>Milk</li>
<li onclick={handleClick}>Cheese </li>
</ul>
)

How to pass state from parent to child in react?

How do I pass a state attribute from parent to child? In the following implementation, the Dropdown component has a state "isActive" and I want to access it in the Button component to attach propper styling to it. The Dropdown has to generic as it is supposed to take different sorts of buttons.
<Dropdown items="...">
<Button active ="false" />
</Dropdown>
Dropdwon.js
...
constructor(props){
super(props)
this.state = {
isActive: true,
}
}
render (){
return (
<div className={styles.toggle} onClick={(event) => this.showMenu(event)}>
{this.props.children} /* want to set active prop for the child button here */
</div>
);
}
...
You have two possibilities:
Lift your Dropdown state and keep it in its parent component;
Use useContext hook;
The first approach would be better, but it may not be good for your application (I cannot know that). Let me make an example for both cases.
This is an example where I've lifted the isActive state to the parent component.
const ParentComponent = () => {
const [isActive, setIsActive] = useState(false);
handleIsActiveChange = (newValue) => {
setIsActive(newValue);
}
<Dropdown isActive={isActive} setIsActive={handleIsActiveChange}>
<Button isActive={isActive} />
</Dropdown>
}
const Dropdown = props => {
// You can use `props.isActive` to know whether the dropdown is active or not.
// You can use `props.handleIsActiveChange` to update the `isActive` state.
}
const Button = props => {
// You can use `props.isActive` to know whether the dropdown is active or not.
}
Instead, this exploits the useContext API:
const dropdownContext = React.createContext(null);
const Dropdown = props => {
const [isActive, setIsActive] = useState(false);
return (
<dropdownContext.Provider value={{ isActive }}>
{props.children}
</dropdownContext.Provider>
);
}
const Button = props => {
const dropdownCtx = React.useContext(dropdownContext);
// You can use `dropdownCtx.isActive` to know whether the dropdown is active or not.
}
Aside from the answer I linked, there might be another way of achieving this that I didn't see mentioned.
You can send a function as a children element of your dropdown which will take isActive as a variable :
<Dropdown items="...">
{isActive => <Button active={isActive} />}
</Dropdown>
Then, is the render function, simply call the function and send your state value as a parameter :
render(){
return (
<div className={styles.toggle} onClick={(event) => this.showMenu(event)}>
{this.props.children(this.state.isActive)}
</div>
);
}
<Dropdown >
<Button isActive={this.state.isActive} />
</Dropdown>
In your button get it with this.props.isActive

Resources