Material-UI - Tooltip on Select/MenuItem component stays displayed after selection - reactjs

I'm using Material-UI's Select component with a Tooltip surrounding it, like so:
<Tooltip title="Tooltip Test">
<Select value={this.state.age} onChange={this.handleChange}
inputProps={{ name: "age", id: "age-simple" }}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>...</MenuItem>
</Select>
</Tooltip>
My problem is that when I click the Select component, the Tooltip stays displayed, during the use of the Select, and even after an item was selected.
I want it to disappear as soon as the Select is clicked so that it doesn't stay over the MenuItems (Changing the zIndex is not the solution I want) and also still is not displayed even after selecting an item in the menu.
I made a codesandbox with the simple issue I have: https://codesandbox.io/s/yvloqr5qoj
But I am using typescript and this is the actual code I'm working with:
ControlledTooltip
import * as React from 'react';
import PropTypes from 'prop-types';
import withStyles, { WithStyles } from 'material-ui/styles/withStyles';
import { Tooltip } from 'material-ui';
const styles = {
}
type State = {
open: boolean,
};
type Props = {
id: string,
msg: string,
children: PropTypes.node,
};
class ControlledTooltip extends React.PureComponent<Props & WithStyles<keyof typeof styles>, State> {
constructor(props) {
super(props);
this.state = {
open: false,
};
}
private handleTooltipClose(): void {
this.setState({ open: false });
}
private handleTooltipOpen(): void {
this.setState({ open: true });
}
render() {
const { id, msg, children } = this.props;
const { open } = this.state;
return(
<div>
<Tooltip id={id}
title={msg}
open={open}
onClose={this.handleTooltipClose}
onOpen={this.handleTooltipOpen}
>
{children ? children : null}
</Tooltip>
</div>
);
}
}
export default withStyles(styles)(ControlledTooltip);
Component using the Tooltip
<ControlledTooltip msg={'Filter'}>
<Select value={this.state.type} onChange={this.handleChange.bind(this, Filter.Type)}>
{this.docTypeFilters.map(item => {
return (<MenuItem key={item} value={item}>{item}</MenuItem>);
})}
</Select>
</ControlledTooltip>

TL;DR: You have to make your tooltip controlled.
UPDATE:
Based on your actual code, replace the return of your render() with this:
<Tooltip id={id}
title={msg}
open={open}
>
<div onMouseEnter={this.handleTooltipOpen}
onMouseLeave={this.handleTooltipClose}
onClick={this.handleTooltipClose}
>
{children ? children : null}
</div>
</Tooltip>
The problem was that you were using onClose/onOpen from the tooltip, which works as is uncontrolled. Now the div containing the select(or any children) has the control over the tooltip.
ANSWER FOR THE PASTEBINS
You will need to handle the open prop of the Tooltip:
class SimpleSelect ...
constructor(...
state = {..., open: false}; // the variable to control the tooltip
Alter your change handling:
handleChange = event => {
this.setState({ [event.target.name]: event.target.value, open: false });// keeps the tooltip hidding on the select changes
};
handleOpen = (open) => {
this.setState({ open }); // will show/hide tooltip when called
}
And reflect it in your render():
const { open } = this.state; // obtains the current value for the tooltip prop
...
<Tooltip title="Tooltip Test" open={open}>
...
<Select ... onMouseEnter={() => this.handleOpen(true)}
onMouseLeave={() => this.handleOpen(false)}
onClick={() => this.handleOpen(false)} >
The event handlers(onMouseEnter, onMouseLeave, onClick) in Select now control the tooltip show/hide behavior.

Same as David's answer, but for functional components with React hooks.
In your component before the return:
const [tooltipOpen, setTooltipOpen] = useState(false)
const handleTooltip = bool => {
setTooltipOpen(bool)
}
Then in your return:
<Tooltip ... open={tooltipOpen}>
<Select
onMouseEnter={() => {handleTooltip(true)}}
onMouseLeave={() => {handleTooltip(false)}}
onMouseClick={() => {handleTooltip(false)}}
...
>

Related

Change Component property through onClick

I have a ButtonGroup with a few Buttons in it, and when one of the buttons gets clicked, I want to change its color, I kinda want to make them behave like radio buttons:
<ButtonGroup>
<Button
variant={"info"}
onClick={(e) => {
..otherFunctions..
handleClick(e);
}}
>
<img src={square} alt={".."} />
</Button>
</ButtonGroup>
function handleClick(e) {
console.log(e.variant);
}
But that doesnt work, e.variant is undefined.
If it was just a single button I would have used useState and I would be able to make this work, but how do I make it work when there are multiple buttons, how do I know which button is clicked and change the variant prop of that button? And then revert the other buttons to variant="info"
Another approach that I could think of is to create my own Button that wraps the bootstrap Button and that way I can have access to the inner state and use onClick inside to control each buttons state, but I'm not sure if that will work, as then how would I restore the other buttons that werent clicked..?
To further from my comment above, you could create your own button component to handle its own state and remove the need to have lots of state variables in your main component e.g.
const ColourButton = ({ children }) => {
const [colour, setColour] = React.useState(true)
return (
<button
onClick={ () => setColour(!colour) }
style = {{color: colour ? "red" : "blue"} }
>
{ children }
</button>
)
}
That way you can just wrap your image in your new ColourButton:
<ColourButton><img src={square} alt={".."} /></ColourButton>
Edit:
I actually like to use styled-components and pass a prop to them rather than change the style prop directly. e.g. https://styled-components.com/docs/basics#adapting-based-on-props
EDIT: Kitson response is a good way to handle your buttons state locally :)
I like to handle the generation of multiple elements with a function. It allows me to customize handleClick.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [buttons, setButtons] = useState([
{
id: 1,
variant: "info"
},
{
id: 2,
variant: "alert"
}
]);
const handleClick = id => {
setButtons(previous_buttons => {
return previous_buttons.map(b => {
if (b.id !== id) return b;
return {
id,
variant: "other color"
};
});
});
};
const generateButtons = () => {
return buttons.map(button => {
return (
<button key={button.id} onClick={() => handleClick(button.id)}>
Hey {button.id} - {button.variant}
</button>
);
});
};
return <div>{generateButtons()}</div>;
}
https://jrjvv.csb.app/
You can maintain a state variable for your selected button.
export default class ButtonGroup extends Component {
constructor(props) {
super(props);
this.state = {
selected: null
};
}
handleClick = e => {
this.setState({
selected: e.target.name
});
};
render() {
const selected = this.state.selected;
return (
<>
<button
name="1"
style={{ backgroundColor: selected == 1 ? "red" : "blue" }}
onClick={this.handleClick}
/>
<button
name="2"
style={{ backgroundColor: selected == 2 ? "red" : "blue" }}
onClick={this.handleClick}
/>
<button
name="1"
style={{ backgroundColor: selected == 3 ? "red" : "blue" }}
onClick={this.handleClick}
/>
</>
);
}
}
Here is a working demo:
https://codesandbox.io/live/OXm3G

react-select focus() doesn't show cursor after change

As described in the official documentation for react-select, I'm trying to use ref and focus() to manually set the focus into the control input field. In most instances it works, but not immediately after selecting an Option from the dropdown.
After selecting an option from the dropdown, the control gets the focus but the cursor doesn't appear. It only appears if you start typing (including hitting the Esc key). On subsequent openings of the menu, the cursor appears along with the focus of the entire control field. Any ideas how to get this working?
I've created a sample code in codesandbox.io here
This is the code:
import React, { Component } from "react";
import ReactDOM from "react-dom";
import Select from "react-select";
import styled from "styled-components";
import { stateOptions } from "./data.js";
class PopoutExample extends Component {
selectRef = React.createRef();
state = {
isOpen: false,
option: undefined,
};
toggleOpen = () => {
const isOpening = !this.state.isOpen;
this.setState(
{
isOpen: isOpening,
},
() => isOpening && setTimeout(() => this.selectRef.focus(), 400),
);
};
onSelectChange = option => {
this.toggleOpen();
this.setState({ option });
};
render() {
const { isOpen, option } = this.state;
return (
<Dropdown
target={
<MainButton onClick={this.toggleOpen}>
{option ? option.label : "Select a State"}
</MainButton>
}
>
<Select
menuIsOpen
ref={ref => {
this.selectRef = ref;
}}
styles={{
container: provided => ({
...provided,
display: isOpen ? "block" : "none",
}),
}}
onChange={this.onSelectChange}
options={stateOptions}
value={option}
controlShouldRenderValue={false}
/>
</Dropdown>
);
}
}
const MainButton = styled.button`
padding: 10px;
background-color: aqua;
width: 100%;
`;
const Dropdown = ({ children, target }) => (
<div>
{target}
{children}
</div>
);
ReactDOM.render(<PopoutExample />, document.getElementById("root"));
You can notice that the bug also exists in the official react-select examples. Even clicking on the blur button after the selection is not solving the problem.
There's probably a small different in the code when user closes the menu and saves + automatically closes action.
I saw you've opened an issue on github. Let's keep an eye on it.
If I can offer an alternative to the behaviour you're trying to achieve, instead of hiding the Select with css why don't just mount / unmount it ?
class PopoutExample extends Component {
state = {
isOpen: false,
option: undefined
};
toggleOpen = () => {
this.setState({
isOpen: !this.state.isOpen
});
};
onSelectChange = option => {
this.setState({ option, isOpen: !this.state.isOpen });
};
render() {
const { isOpen, option } = this.state;
return (
<Dropdown
target={
<MainButton onClick={this.toggleOpen}>
{option ? option.label : "Select a State"}
</MainButton>
}
>
{isOpen && (
<Select
autoFocus
menuIsOpen
onChange={this.onSelectChange}
options={stateOptions}
value={option}
controlShouldRenderValue={false}
/>
)}
</Dropdown>
);
}
}
Here a live example of my solution.

How to use my react component PlaceInput to achieve place autocomplete in the menu input box?

I have a PlaceInput component which support google place autocomplete.
class PlaceInput extends Component {
state = {
scriptLoaded: false
};
handleScriptLoaded = () => this.setState({ scriptLoaded: true });
render() {
const {
input,
width,
onSelect,
placeholder,
options,
meta: { touched, error }
} = this.props;
return (
<Form.Field error={touched && !!error} width={width}>
<Script
url="https://maps.googleapis.com/maps/api/js?key={my google api key}&libraries=places"
onLoad={this.handleScriptLoaded}
/>
{this.state.scriptLoaded &&
<PlacesAutocomplete
inputProps={{...input, placeholder}}
options={options}
onSelect={onSelect}
styles={styles}
/>}
{touched && error && <Label basic color='red'>{error}</Label>}
</Form.Field>
);
}
}
export default PlaceInput;
I also have a menu item which is an<Input> from semantic-ui-react. The frontend is like below:
The menu code is like below:
<Menu.Item>
<Input
icon='marker'
iconPosition='left'
action={{
icon: "search",
onClick: () => this.handleClick()}}
placeholder='My City'
/>
</Menu.Item>
How can I leverage the PlaceInput component to make menu <Input> box to achieve the place autocomplete? Thanks!
If you could share a working sample of your app (in e.g. codesandbox) I should be able to help you make your PlaceInput class work with the Menu.Input from semantic-ui-react.
Otherwise, you can test a fully working example of such integration with the code below, which is based off of the Getting Started code from react-places-autocomplete.
import React from "react";
import ReactDOM from "react-dom";
import PlacesAutocomplete, {
geocodeByAddress,
getLatLng
} from "react-places-autocomplete";
import { Input, Menu } from "semantic-ui-react";
const apiScript = document.createElement("script");
apiScript.src =
"https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places";
document.head.appendChild(apiScript);
const styleLink = document.createElement("link");
styleLink.rel = "stylesheet";
styleLink.href =
"https://cdn.jsdelivr.net/npm/semantic-ui/dist/semantic.min.css";
document.head.appendChild(styleLink);
class LocationSearchInput extends React.Component {
constructor(props) {
super(props);
this.state = { address: "" };
}
handleChange = address => {
this.setState({ address });
};
handleSelect = address => {
geocodeByAddress(address)
.then(results => getLatLng(results[0]))
.then(latLng => console.log("Success", latLng))
.catch(error => console.error("Error", error));
};
render() {
return (
<PlacesAutocomplete
value={this.state.address}
onChange={this.handleChange}
onSelect={this.handleSelect}
>
{({ getInputProps, suggestions, getSuggestionItemProps, loading }) => (
<div>
<Menu>
<Menu.Item>
<Input
icon="marker"
iconPosition="left"
placeholder="My City"
{...getInputProps({
placeholder: "Search Places ...",
className: "location-search-input"
})}
/>
</Menu.Item>
</Menu>
<div className="autocomplete-dropdown-container">
{loading && <div>Loading...</div>}
{suggestions.map(suggestion => {
const className = suggestion.active
? "suggestion-item--active"
: "suggestion-item";
// inline style for demonstration purpose
const style = suggestion.active
? { backgroundColor: "#fafafa", cursor: "pointer" }
: { backgroundColor: "#ffffff", cursor: "pointer" };
return (
<div
{...getSuggestionItemProps(suggestion, {
className,
style
})}
>
<span>{suggestion.description}</span>
</div>
);
})}
</div>
</div>
)}
</PlacesAutocomplete>
);
}
}
ReactDOM.render(<LocationSearchInput />, document.getElementById("root"));
Hope this helps!

Implement dynamic propTypes in ReactJs component

I have an object with icons available to some button.
const icons = {
check: 'icon-CheckSmall',
chat: 'icon-ChatMedium',
investors: 'icon-InvestorsMedium',
download: 'icon-DownloadMedium',
};
const Button = (props) => {
const {
buttonType,
buttonText,
onClick,
disabled,
} = props;
return (
<button
style={ icons }
type={ buttonType }
onClick={ onClick }
disabled={ disabled && 'disabled' }
>
{ <FormattedMessage id={ buttonText } /> }
</button>
);
};
I want to create prop types for component with keys array
Button.propTypes = {
icon: PropTypes.oneOf(Object.keys(icons)),
};
In this case, Object.keys does not work.
Is there anyone who manage to implement dynamic prop types in the component?

Hide/Show React-slick arrows

I am using react-slick and I have my own customised arrows. This Slider is NOT an infinite loop and I would like to hide the arrows at the start and end of the loop. So basically at start PrevArrow should be hidden and at the end the NextArrow should be hidden. Which I am trying to set hidden class name depending on state changes. However the class name is not changing although the state is changing correctly. Do you know what's the problem with this code? and how to get it work?
Below is my setting for Slider and the component which renders Slider.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Slider from 'react-slick';
import Arrow from '../slider/Arrow';
export default class CityCarousel extends Component {
constructor(props) {
super(props);
this.state = {
displayLeftArrow: true,
displayRightArrow: true
};
this.slidesToShow = 5;
this.sliderSetting = {
dots: false,
arrows: true,
infinite: false,
initialSlide: 0,
slidesToShow: this.slidesToShow,
slidesToScroll: 1,
speed: 500,
rows: 0,
nextArrow: <Arrow
styleClassName={`city-carousel__right ${
this.state.displayRightArrow ? '' : 'city-carousel__right--hide'
}`}
direction="right"
clickHandler={this.clickHandler}
/>,
prevArrow: <Arrow
styleClassName={`city-carousel__left ${
this.state.displayLeftArrow ? '' : 'city-carousel__left--hide'
}`}
direction="left"
clickHandler={this.clickHandler}
/>,
afterChange: currentSlide => this.setArrowDisplay(currentSlide)
};
}
clickHandler = direction => {
if (direction === 'left') {
this.slider.slickPrev();
} else if (direction === 'right') {
this.slider.slickNext();
}
};
setArrowDisplay = currentSlide => {
const { cityList } = this.props;
const displayLeftArrow = currentSlide !== 0;
const displayRightArrow = currentSlide !== cityList.length - this.slidesToShow;
this.setState({ displayRightArrow, displayLeftArrow });
};
render() {
const { cityList, tours } = this.props;
return (
<div>
<div className="city-selection">
<Slider
ref={c => this.slider = c }
{...this.sliderSetting}
>
{cityList.length > 0 ? this.renderCityList() : null}
</Slider>
</div>
</div>
);
}
}
Here is also code for Arrow component
import React from 'react';
import PropTypes from 'prop-types';
const Arrow = ({ styleClassName, direction, clickHandler }) => {
return(
<div
className={`slick-arrow--${direction} slider-arrow--${direction} ${styleClassName}`}
onClick={e => clickHandler(direction)}
/>
)};
Arrow.propTypes = {
styleClassName: PropTypes.string,
direction: PropTypes.string,
clickHandler: PropTypes.func
};
export default Arrow;
It seems react-slick is not re-rendering <Arrow /> with new props, and it's always with first initialized props setting.
I think the solution is to get <Arrow /> out of slider like this:
<div className="city-selection">
<Arrow
styleClassName={`city-carousel__right ${
this.state.displayRightArrow ? '' : 'city-carousel__right--hide'
}`}
direction="right"
clickHandler={this.clickHandler}
/>
<Arrow
styleClassName={`city-carousel__left ${
this.state.displayLeftArrow ? '' : 'city-carousel__left--hide'
}`}
direction="left"
clickHandler={this.clickHandler}
/>
<Slider
{/* list of items*/}
</Slider>
</div>
you can see how it's working in here
Pass the current slide prop to the component and do a check for the slide you want to trigger hiding the arrow:
function SamplePrevArrow(props) {
const { currentSlide, className, onClick } = props;
if (currentSlide === 0) {
return false;
} else {
return <div className={className} onClick={onClick} />;
}
}
In your this.sliderSettings set nextArrow & prevArrow for your Arrow component do something like this
<Arrow
styleClassName={'YOUR_CLASSES_HERE'}
direction="right"
clickHandler={this.clickHandler}
isHidden={this.state.isHidden}
/>
Where this.state.isHidden is the state variable where you are trying toggle the arrows.
Then in your Arrow component do something like this.
const Arrow = ({ styleClassName, direction, clickHandler, isHidden }) => {
return (
<div
className={`slick-arrow--${direction} slider-arrow--${direction}
${styleClassName}`}
style={{ display: isHidden: 'none': 'block' }}
onClick={e => clickHandler(direction)}
/>
)
};
If you want the best manual controls over the arrow then use a custom arrow.
Create it by using a simple Arrow component.
Set the onClick handler of that component by using a ref in the main Slider component.
const sliderRef = useRef<Slider>(null);
<Slider {...settings} ref={sliderRef}>
Use states to conditionally show or hide arrows.
I wrote a blog post which covers the workflow to achieve similar kind of functionalities you want. You can have a look there.
https://medium.com/#imasharaful/image-slider-with-react-slick-d54a049f043
In 2022, we have boolean arrows, just declare it false in settings.
const settings = {
...,
arrows: false,
};

Resources