react-select remove focus when disabled - reactjs

Using react-select 3.1.0
I have a filter panel in which 3 select drop-downs cascade to choose the make/model/series for a car.
The general idea is that you can only select a model after make has been selected, and only select a series after model. If you change the make it will reset the following options.
That all works fine... except (as you'll see in the GIF below) the focus on the "series" select box remains despite it being disabled.
Any ideas on how I can remove that focus? Here's what my code looks like (each box is essentially the same):
<Select
id="series-select"
data-selenium-test="series-select"
isDisabled={state.model === null}
isSearchable={false}
options={series}
value={selection}
placeholder="All Series"
isClearable
// eslint-disable-next-line no-console
onChange={/* istanbul ignore next */ (option) => setSeries(option ? option.value : null)}
// handles edge-case where menu is open but MAKE/MODEL is cleared
menuIsOpen={state.model === null ? false : undefined}
/>

Wrapping by setTimeout() with empty time prop in the body of onChange function solved this issue for me. I've made this for first two inputs.
So for you it should looks like this:
onChange={(option) => setTimeout(() => {setSeries(option ? option.value : null)})}
Also you can use blurInputOnSelect prop, but there would be some cases, where issue would be reproduced

Related

Conditionally enabling a form button with react-hook-form and material ui

I'm creating a React form with Material UI. My goal is to force the user to answer all the questions before they can click the download button. In other words I'm trying to leave the download button in the disabled state until I can determine that values are set on each field. I've tried to get this working with and without react-hook-form.
What I've tried
Without react-hook-form...
I have my example in coding sandbox here:
https://codesandbox.io/s/enable-download-button-xwois4?file=/src/App.js
In this attempt I abandoned react-hook-form and added some logic that executes onChange. It looks through each of the formValues and ensures none of them are empty or set to "no answer". Like this:
const handleInputChange = (e) => {
// do setFormValues stuff first
// check that every question has been answered and enable / disable the download button
let disableDownload = false;
Object.values(formValues).forEach((val) => {
if (
typeof val === "undefined" ||
val === null ||
val === "no answer" ||
val === ""
) {
disableDownload = true;
}
});
setBtnDisabled(disableDownload);
The problem with this approach, as you'll see from playing with the UI in codesandbox, is that it requires an extra toggle of the form field value in order to detect that every field has been set. After the extra "toggle" the button does indeed re-enable. Maybe I could change this to onBlur but overall I feel like this approach isn't the right way to do it.
Using react-hook-form
With this approach...the approach I prefer to get working but really struggled with, I ran into several problems.
First the setup:
I removed the logic for setBtnDisabled() in the handleInputChange function.
I tried following the example on the react-hook-form website for material ui but in that example they're explicitly defining the defaultValues in useForm where-as mine come from useEffect. I want my initial values to come from my questionsObject so I don't think I want to get rid of useEffect.
I couldn't figure out what to do with {...field} as in the linked material ui example. I tried dropping it on RadioGroup
<Controller
name="RadioGroup"
control={control}
rules={{ required: true }}
render={({ field }) => (
<RadioGroup
questiontype={question.type}
name={questionId}
value={formValues[questionId]}
onChange={handleInputChange}
row
{...field}
>
but I get an MUI error of : MUI: A component is changing the uncontrolled value state of RadioGroup to be controlled.
Also, I don't see that useForm's state is changing at all. For example, I was hoping the list of touchedfields would increase as I clicked radio buttons but it isn't. I read that I should pass formState into useEffect() like this:
useEffect(() => {
const outputObj = {};
Object.keys(questionsObject).map(
(question) => (outputObj[question] = questionsObject[question].value)
);
setFormValues(outputObj);
}, [formState]);
but that makes me question what I need to do with formValues. Wondering if formState is supposed to replace useState for this.

React-Hook-Form setValue for Material-UI Autocomplete doesn't work

I'm building a form with React-Hook-Form and Material-UI. Each Material-UI form component is wrapped into a react-hook-form Controller component.
The user has the option to populate the form automatically with different sets of pre-defined values when clicks on a button. I'm trying to use setValue() to achieve that and it seems to work fine for text input and select. However, for autocomplete the new value is not rendered although when submitting the form it is correctly sent. Also, when a text area is rendered the content seems to be mixed with the label.
Here is a complete example:
CodeSandbox Link
You need to cotroll the value of AutoComplete by passing value attribute to AutoComplete component, But since you are using string value you couldn't pass value to AutoComplete directly and you should to find the target option like this:
<Autocomplete
...
value={typeof value === 'string' ? options.find(o => o.name === value) : (value || null)}
/>
Your textarea problem occurs because your value by default is undefined and material considers the textare as uncotrolled input, to solve it you can pass empty string when value is undefined, like this:
<TextField
...
value={value || ''}
...
/>

Testing Material-UI Textfield select component with jest

I am trying to write a test for a simple Material-UI Textfield select component. The test should show that selecting an options triggers the corresponding event.
Here ist the component
<TextField
inputProps ={{"data-testid": "testId"}}
id="TextFieldId"
aria-label={"TextFieldAriaLabel"}
select
label="Files"
value={limit}
onChange={handleLimitChange}
SelectProps={{
native: true,
}}
variant="outlined"
>
{[{value: 5, label: "5"}, {value: 10, label: "10"}, {value: 15, label: "15"}].map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</TextField>
Selecting the value "10" triggers the call of a function with 10 as an input parameter.
I use jest to write the test. I want to click on the select component to open the dropdown. Then I want to click on one of the options. To prove that the event was triggered, I check if the related function is called with the right argument.
It sound very simple, but I ran into many issues. Most of the issues were related to not finding the elements, since material ui nests different html components. My so far best approach looks like this.
testObject.renderResult.getByTestId('testId').click();
testObject.rerender();
jest.runAllTimers();
const dropdown = testObject.renderResult.getByTestId('testId');
within(dropdown).getByText('10').click();
testObject.rerender();
jest.runAllTimers();
expect(mostRecentImports).toHaveBeenCalledWith(10)
Jest finds the elements, but the test fails. The component remains in its default, showing the value 5 (not 10). What am I doing wrong?
I also had the same problem in my app. Finally I solved it using this solution I found here: https://github.com/testing-library/react-testing-library/issues/322#issuecomment-581650108
Write a simple function that opens the select menu and selects the wanted option:
const selectMaterialUiSelectOption = (element, optionText) => {
// The the button that opens the dropdown, which is a sibling of the input
const selectButton = element.parentNode.querySelector('[role=button]');
// Open the select dropdown
UserEvent.click(selectButton);
// Get the dropdown element. We don't use getByRole() because it includes <select>s too.
const listbox = document.body.querySelector('ul[role=listbox]');
// Click the list item
const listItem = within(listbox).getByText(optionText);
UserEvent.click(listItem);
};
[... in your test]
selectMaterialUISelectOption(getByTestId('testId'), "10")
You can utilize the findAByDisplayValue method of #testing-library/react
https://testing-library.com/docs/dom-testing-library/api-queries#bydisplayvalue

material-ui-pickers cancel actually displays value

using //https://material-ui-pickers.dev/demo/datepicker
Configured like below. this.state.start_year_date starts at null. Works great when user brings up the modal and selects a date and clicks OK. However, if they bring up modal, and hit cancel, it displays 2019 (!??!?!) after the modal closes. However since they didn't actually choose 2019, state.start_year_date is not set to 2019, it is still null. Things go sideways from there. How can I prevent cancel from doing that?
<DatePicker
value={this.state.start_year_date}
onChange={this.handleStartDateChange}
views={["year"]}
disablePast={true}
label={"Start year"}
onAccept={this.handleStartDateAccept}
maxDate={ this.state.end_year_date ? this.state.end_year_date : (new Date().setYear(2099) ) }
/>
I believe you can use the onClose method to either reset the value to the original value or set it to whatever you like.
<DatePicker
value={this.state.start_year_date}
onChange={this.handleStartDateChange}
views={["year"]}
disablePast={true}
label={"Start year"}
onAccept={this.handleStartDateAccept}
maxDate={ this.state.end_year_date ? this.state.end_year_date : (new Date().setYear(2099) ) }
onClose={ () => this.setState({start_year_date: 2019 }) }
/>

Disable drag selection on react-big-calendar

I don't see anything in API for disabling this behavior.
I just want users to have to click on a specific date.
use the prop onSelecting. For example: onSelecting = {slot => false}
I had to get selection for clicked date, without drag.
Used the onSelectSlot with required logic.
onSelectSlot={(e) => {
if (e.slots?.length > 1) return; // or a message
alert("onSelectSlot" + JSON.stringify(e));
}}
Note: this wont handle drag selection styling!!

Resources