Material UI Autocomplete - Disable listbox after the item selection - reactjs

I'm creating an Autocomplete component in React.js with the help of Material-UI headless useAutoComplete hook. The component is working properly. When the user tries to type any character, the Listbox will automatically open.
But the problem is that when the user selects anything and pays attention to the input element, the ListBox reopens. How can I prevent this?
Codesandbox:
Code:
import React from 'react';
import useAutocomplete from '#material-ui/lab/useAutocomplete';
function AutocompleteComponent(props) {
const { data } = props;
const [value, setValue] = React.useState('');
const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = function () {
if (value.length > 0) {
setIsOpen(true);
}
};
const handleInputChange = function (event, newInputValue) {
setValue(newInputValue);
if (newInputValue.length > 0) {
setIsOpen(true);
} else {
setIsOpen(false);
}
};
const {
getRootProps,
getInputProps,
getListboxProps,
getOptionProps,
groupedOptions
} = useAutocomplete({
id: 'form-control',
options: data,
autoComplete: true,
open: isOpen, // Manually control
onOpen: handleOpen, // Manually control
onClose: () => setIsOpen(false), // Manually control
inputValue: value, // Manually control
onInputChange: handleInputChange, // Manually control
getOptionLabel: (option) => option.name
});
const listItem = {
className: 'form-control__item'
};
return (
<div className="app">
<div className="form">
<div {...getRootProps()}>
<input
type="text"
className="form-control"
placeholder="Location"
{...getInputProps()}
/>
</div>
{groupedOptions.length > 0 && (
<ul
className="form-control__box"
aria-labelledby="autocompleteMenu"
{...getListboxProps()}
>
{groupedOptions.map((option, index) => {
return (
<li {...getOptionProps({ option, index })} {...listItem}>
<div>{option.name}</div>
</li>
);
})}
</ul>
)}
</div>
</div>
);
}
export default AutocompleteComponent;

You can store the selectedItem in a state and use it in handleOpen to decide whether the list has to be displayed.
For setting selectedItem you can modify the default click provided by getOptionProps
import React from 'react';
import useAutocomplete from '#material-ui/lab/useAutocomplete';
function AutocompleteComponent(props) {
const { data } = props;
const [value, setValue] = React.useState('');
const [isOpen, setIsOpen] = React.useState(false);
const [selectedItem, setSelectedItem] = React.useState('');
const handleOpen = function () {
if (value.length > 0 && selectedItem !== value) {
setIsOpen(true);
}
};
const handleInputChange = function (event, newInputValue) {
setValue(newInputValue);
if (newInputValue.length > 0) {
setIsOpen(true);
} else {
setIsOpen(false);
}
};
const {
getRootProps,
getInputProps,
getListboxProps,
getOptionProps,
groupedOptions
} = useAutocomplete({
id: 'form-control',
options: data,
autoComplete: true,
open: isOpen, // Manually control
onOpen: handleOpen, // Manually control
onClose: () => setIsOpen(false), // Manually control
inputValue: value, // Manually control
onInputChange: handleInputChange, // Manually control
getOptionLabel: (option) => option.name
});
const listItem = {
className: 'form-control__item'
};
return (
<div className="app">
<div className="form">
<div {...getRootProps()}>
<input
type="text"
className="form-control"
placeholder="Location"
{...getInputProps()}
/>
</div>
{groupedOptions.length > 0 && (
<ul
className="form-control__box"
aria-labelledby="autocompleteMenu"
{...getListboxProps()}
>
{groupedOptions.map((option, index) => {
return (
<li
{...getOptionProps({ option, index })}
{...listItem}
onClick={(ev) => {
setSelectedItem(option.name);
getOptionProps({ option, index }).onClick(ev);
}}
>
<div>{option.name}</div>
</li>
);
})}
</ul>
)}
</div>
</div>
);
}
export default AutocompleteComponent;

You can modify the props passed to the inputbox, before applying them. That way you can delete the event that happens when the input is clicked.
var newProps = getInputProps();
delete newProps.onMouseDown; //delete the extra MouseDown event
return (
...
<input
...
placeholder="Location"
{...newProps} //use newProps rather than calling getInputProps
And it will stop showing that popup when you click it.
You can check the working code here

Related

Focus Trap React and a few component's in 2 popup's

I have 2 popup's(I reuse CloseButton(component) and Modal(component) in 2 popup's) and need to do focus trap at all. I lf answer 4 better way.
1 popup Screen, components: ModalLogin-Modal-CloseButton.
I read about some hooks: useRef() and forwardRef(props, ref)
but i don't undestand why it's not work in my case. I am trying to find a solution. I need help :)
In ModalLogin, I try to do a focus trap. To do this, I mark what should happen with focus when moving to 1 and the last element. I need to pass my ref hook obtained via Modal-CloseButton. I read that you can't just transfer refs to functional components. I try to use the forwardref hook in the necessary components where I transfer it, here's what I do:
All links without focus-trap and hook's!.
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/form-login/modal-login.jsx [Modal-login full]
const ModalLogin = () => {
const topTabTrap* = useRef();
const bottomTabTrap* = useRef();
const firstFocusableElement = useRef();
const lastFocusableElement = useRef();
useEffect(() => {
const trapFocus = (event) => {
if (event.target === topTabTrap.current) {
lastFocusableElement.current.focus()
}
if (event.target === bottomTabTrap.current) {
firstFocusableElement.current.focus()
}
}
document.addEventListener('focusin', trapFocus)
return () => document.removeEventListener('focusin', trapFocus)
}, [firstFocusableElement, lastFocusableElement])
return (
<Modal onCloseModal={() => onCloseForm()} ref={lastFocusableElement}>
<form >
<span ref={topTabTrap} tabIndex="0" />
<Logo />
<Input id="email" ref={firstFocusableElement} />
<Input id="password" />
<Button type="submit" />
<span ref={bottomTabTrap} tabIndex="0"/>
</form>
</Modal>
);
};
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/modal/modal.jsx [Modal full]
const Modal = forwardRef(({ props, ref }) => {
const { children, onCloseModal, ...props } = props;
const overlayRef = useRef();
useEffect(() => {
const preventWheelScroll = (evt) => evt.preventDefault();
document.addEventListener('keydown', onEscClick);
window.addEventListener('wheel', preventWheelScroll, { passive: false });
return () => {
document.removeEventListener('keydown', onEscClick);
window.removeEventListener('wheel', preventWheelScroll);
};
});
const onCloseModalButtonClick = () => {
onCloseModal();
};
return (
<div className="overlay" ref={overlayRef}
onClick={(evt) => onOverlayClick(evt)}>
<div className="modal">
<CloseButton
ref={ref}
onClick={() => onCloseModalButtonClick()}
{...props}
/>
{children}
</div>
</div>
);
});
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/close-button/close-button.jsx [CloseButton full]
const CloseButton = forwardRef(({ props, ref }) => {
const {className, onClick, ...props} = props;
return (
<button className={`${className} close-button`}
onClick={(evt) => onClick(evt)}
tabIndex="0"
ref={ref}
{...props}
>Close</button>
);
});
And now i have a lot of errors just like: 1 - Cannot read properties of undefined (reading 'children') - Modal, 2 - ... className undefined in CloseButton etc.
2 popup Screen, components: Modal(reuse in 1 popup) - InfoSuccess- CloseButton(reuse in 1 popup)
I have only 1 interactive element - button (tabindex) and no more. Now i don't have any idea about 2 popup with focus-trap ((
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/success-modal/success-modal.jsx [SuccessModal full]
const SuccessModal = ({ className, onChangeVisibleSuccess }) => {
return (
<Modal onCloseModal={() => onChangeVisibleSuccess(false)}>
<InfoSuccess className={className} />
</Modal>
);
};
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/info-block/info-block.jsx [Infoblock full]
const InfoBlock = ({ className, title, desc, type }) => {
return (
<section className={`info-block ${className} info-block--${type}`}>
<h3 className="info-block__title">{title}</h3>
<p className="info-block__desc">{desc}</p>
</section>
);
};
const InfoSuccess = ({ className }) => (
<InfoBlock
title="Спасибо за обращение в наш банк."
desc="Наш менеджер скоро свяжется с вами по указанному номеру телефона."
type="center"
className={className}
/>
);
I know about 3 in 1 = 1 component and no problem in popup with Focus-Trap. But i want understand about my case, it's real to life or not and what best practice.

How to manage radio button state with React Typescript?

I am implementing a simple signup page with React Typescript.
I'm trying to set the gender with the radio button, save it in the state, and send it to the server, but the toggle doesn't work.
What should I do?
//RegisterPage.tsx
const [radioState, setradioState] = useState(null);
const [toggle, settoggle] = useState<boolean>(false);
const onRadioChange = (e: any) => {
setradioState(e);
console.log(radioState);
};
const genderOps: ops[] = [
{ view: "man", value: "man" },
{ view: "woman", value: "woman" },
];
<div>
{genderOps.map(({ title, gender }: any) => {
return (
<>
<input
type="radio"
value={gender}
name={gender}
checked={gender === radioState}
onChange={(e) => onRadioChange(gender)}
/>
{title}
</>
);
})}
</div>
You should do some changes on your code, here what you should do:
import React, { EventHandler, useState } from "react";
import "./styles.css";
export default function App() {
const [radioState, setradioState] = useState("");
const [toggle, settoggle] = useState<boolean>(false);
const onRadioChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setradioState(e.currentTarget.value);
};
const genderOps = [
{ view: "man", value: "man" },
{ view: "woman", value: "woman" }
];
return (
<div className="App">
<div>
{genderOps.map(({ view: title, value: gender }: any) => {
return (
<>
<input
type="radio"
value={gender}
name={gender}
checked={gender === radioState}
onChange={(e) => onRadioChange(e)}
/>
{title}
</>
);
})}
</div>{" "}
</div>
);
}

state in custom hook not updating

I am not able to get an updated state from the custom hook.
the example is simple. this is a custom hook allowing to switch from dark to light theme
inside the custom hook, I use useMemo to return a memoized value, but this does not seem to update each time I update the state with switchOn.
I do not wish to use a context in this example
import { useState, useMemo, useCallback } from "react";
const useTheme = (initial) => {
const [isOn, turnOn] = useState(false);
const switchOn = useCallback((e) => turnOn(e.target.checked), [turnOn]);
return useMemo(() => {
return {
theme: isOn ? "light" : "dark",
buttonTheme: isOn ? "dark" : "light",
lightsIsOn: isOn ? "On" : "Off",
isChecked: isOn,
switchOn,
};
}, [switchOn, isOn]);
};
export default useTheme;
import { useState, useRef, useReducer } from "react";
import useTheme from "./useTheme";
import todos from "./reducer";
import "./App.css";
const Item = ({ id, text, done, edit, remove }) => {
const { buttonTheme } = useTheme();
const isDone = done ? "done" : "";
return (
<div style={{ display: "flex" }}>
<li className={isDone} key={id} onClick={() => edit(id)}>
{text}
</li>
<button type="button" onClick={() => remove(id)} className={buttonTheme}>
<small>x</small>
</button>
</div>
);
};
const SwitchInput = () => {
const { switchOn, isChecked, lightsIsOn } = useTheme();
return (
<>
<label class="switch">
<input type="checkbox" onChange={switchOn} checked={isChecked} />
<span class="slider round"></span>
</label>
<span>
{" "}
Lights <b>{lightsIsOn}</b>
</span>
<br /> <br />
</>
);
};
const initialState = {
items: [],
};
const init = (state) => {
return {
...state,
items: [
{ id: 1, text: "Learning the hooks", done: false },
{ id: 2, text: "Study JS", done: false },
{ id: 3, text: "Buy conference ticket", done: false },
],
};
};
function App() {
const inputRef = useRef();
const [state, dispatch] = useReducer(todos, initialState, init);
const [inputValue, setInputValue] = useState(null);
const { theme } = useTheme();
const handleOnChange = (e) => setInputValue(e.target.value);
const handleOnSubmit = (e) => {
e.preventDefault();
dispatch({ type: "add", payload: inputValue });
inputRef.current.value = null;
};
return (
<div className={`App ${theme}`}>
<SwitchInput />
<form onSubmit={handleOnSubmit}>
<input ref={inputRef} type="text" onChange={handleOnChange} />
<ul>
{state.items.map((item) => (
<Item
{...item}
key={item.id}
remove={(id) => dispatch({ type: "remove", payload: { id } })}
edit={(id) => dispatch({ type: "edit", payload: { id } })}
/>
))}
</ul>
</form>
</div>
);
}
export default App;
my custom hook: useTheme.js
below, the component where I want to access logic from custom hook: App.js
I use an App.css to apply theme style : dark and light
below a demo. We can see that the values do not change
How is an effective way to subscribe to state changes share global states between components?
You need to pass the values of useTheme() from <App> to <SwitchInput> to share it as follows.
function App() {
const { theme, switchOn, isChecked, lightsIsOn } = useTheme();
return (
<div className={`App ${theme}`}>
<SwitchInput
switchOn={switchOn}
isChecked={isChecked}
lightsIsOn={lightsIsOn}
/>
</div>
);
}
Then, <SwitchInput> will receive them in props.
const SwitchInput = ({ switchOn, isChecked, lightsIsOn }) => {
return (
<>
<label class="switch">
<input type="checkbox" onChange={switchOn} checked={isChecked} />
<span class="slider round"></span>
</label>
<span>
{" "}
Lights <b>{lightsIsOn}</b>
</span>
<br /> <br />
</>
);
};
In your example, useTheme() is called in both <App> and <SwitchInput>, but theme and other values are initialized separately and are not shared between each component.

How to uncheck selected radio input onChange with react to reset current showing data?

I have created multiple Radio buttons options. For each radio button based on the value date, I would display different users based on availability.
Now, when I select one or the second radio button it will display different users.
But for some reason, the "CHECKED" value will stay whenever I switch to another radio input option. It will display different data but I would like to reset these mapped (currentConsult) values to display none.
Code:
import React, { useEffect, useState } from 'react';
// import ConsultSelect from './ConsultSelect';
const Appointment = (props) => {
const { loading, data, consultSelect } = props;
const [consult, setConsult] = useState([]);
const [filteredConsult, setFilteredConsult] = useState([]);
const [currentConsult, setCurrentConsult] = useState([]);
const [checked, setChecked] = useState({
selected: '',
});
useEffect(() => {
setConsult(data);
if (consultSelect === 'consult_business') {
setFilteredConsult(consult.consult_business);
}
if (consultSelect === 'consult_strategy') {
setFilteredConsult(consult.consult_strategy);
}
if (consultSelect === '') {
setFilteredConsult(...consult);
}
});
const ShowUsers = (selectedDate) => {
const result = filteredConsult.filter(
(v) =>
v.consult__availlability &&
v.consult__users &&
v.consult__date === selectedDate
);
setCurrentConsult(result[0].consult__users);
};
const onSelectChange = (event) => {
setChecked({ selected: event.target.value });
const selectedDate = event.target.value;
consult && checked && ShowUsers(selectedDate);
};
return (
<div className="consult__meetinplanner form-group">
{loading !== true && <p>loading...</p>}
{loading && consultSelect === '' && (
<h3 style={{ marginTop: '2rem', marginBottom: '2rem' }}>
Maak een keuze aub.
</h3>
)}
{filteredConsult &&
filteredConsult.map((value, i) => {
const {
consult__date: date,
consult__time: time,
consult__availlability: is_available,
} = value;
return (
<div key={i} className="consult__results">
<label htmlFor={date}>
{is_available && (
<div>
<input
type="radio"
id={date}
name={date}
value={date}
checked=""
onChange={onSelectChange}
disabled={props.disabled}
required
/>
<span>{date}</span>
—
<span>{time}</span>
</div>
)}
</label>
</div>
);
})}
{consultSelect !== '' &&
currentConsult.map((user, i) => {
return (
<article key={user.consultation_user_item.nickname}>
<h4>{user.consultation_user_item.nickname}</h4>
</article>
);
})}
</div>
);
};
export default Appointment;
Goednmorgen Nino.
You are not binding checked prop to anything, here is an example how you can do this:
function App() {
const [checked, setChecked] = React.useState({
selected: 'one',
});
const onSelectChange = (event) => {
setChecked({ selected: event.target.value });
// const selectedDate = event.target.value;
// consult && checked && ShowUsers(selectedDate);
};
return (
<div>
{['one', 'two', 'three'].map((value, index) => (
<label key={index}>
{value}
<input
type="checkbox"
checked={checked.selected === value} //bind checked prop
value={value}
onChange={onSelectChange}
/>
</label>
))}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Function is running one time in hook

I have a hook like this :
export default function AddSections ({ onCheckItem }) {
const checkItem = (item) => {
console.log('check')
}
return (
<div>
<Checkbox
className="section_item"
key={index}
id={section.name}
name="add-sections"
type="radio"
label={'section.label'}
value={'section.name'}
onChange={val => checkItem(item)}
/>
</div>
)
}
for the first time when i check the checkbox the function return the console.log, when i try to undo the check it never works
Problem is you have defined the Checkbox type to be radio and you are using the uncontrolled input so it doesn't allow your to toggle.
You have two solutions
Change the type to checkbox
sample code:
export default function AddSections ({ onCheckItem }) {
const checkItem = (item) => {
console.log('check')
}
return (
<div>
<Checkbox
className="section_item"
key={index}
id={section.name}
name="add-sections"
type="checkbox"
label={'section.label'}
value={'section.name'}
onChange={val => checkItem(item)}
/>
</div>
)
}
Use Controlled input
sample code
export default function AddSections ({ onCheckItem }) {
const [checked, setChecked] = useState('');
const checkItem = (item) => {
setChecked(checked => (checked == item? '': item));
}
return (
<div>
<Checkbox
className="section_item"
key={index}
id={section.name}
name="add-sections"
checked={checked === item}
type="radio"
label={'section.label'}
value={'section.name'}
onChange={val => checkItem(item)}
/>
</div>
)
}
I suppose that Checkbox component is gonna map the props to native checkbox element. If that is the case, you should use checked prop as well. I don't see any hooks in your code though.
import React, {useState, useEffect} from 'react'
export default function AddSections ({ onCheckItem }) {
const [checked, setChecked] = useState(false)
const handleCheckItem = (item) => {
setChecked(!checked)
}
useEffect(() => {
console.log('clicked')
}, [checked])
return (
<div>
<Checkbox
className="section_item"
key={index}
id={section.name}
name="add-sections"
type="radio"
label={'section.label'}
value={'section.name'}
checked={checked}
onChange={handleCheckItem}
/>
</div>
)
}

Resources