When a user sets focus on the edit box of a single-selection react-select component, the currently-selected option will continue to be shown until the user types a key. I don't like this behavior. Instead I'd like do to what Google does which is to clear the edit box on focus.
How to do this?
I noticed there's a long-open issue in the react-select GitHub repo, but the solutions on that issue seem either complex or have UX tradeoffs that I'd like to avoid.
It took some experimentation, but the easiest solution was creating a custom SingleValue component that renders no content when the menu is open. Posting the answer here to save the time of others who may run into the same problem.
Note that the text shown isn't actually in the edit box. It's a separate HTML element of static text that's hidden by the library when the user types. I'm just accelerating that process. :-)
Live example
import { Fragment, useState } from "react";
import Select, { components } from "react-select";
const options = [
{ label: "one", value: 1 },
{ label: "two", value: 2 },
{ label: "three", value: 3 }
];
export default function App() {
const [selectedOption, setSelectedOption] = useState(options[1]);
return (
<Select
options={options}
components={{ SingleValue }}
value={selectedOption}
onChange={setSelectedOption}
placeholder="Pick a number"
/>
);
}
/** Hide the selected value when showing the dropdown list */
function SingleValue(props) {
const { children, ...rest } = props;
const { selectProps } = props;
if (selectProps.menuIsOpen) return <Fragment></Fragment>;
return <components.SingleValue {...rest}>{children}</components.SingleValue>;
}
// For TypeScript, declare like this:
// function SingleValue<T>(props: SingleValueProps<T, false>) {
If you want to show a placeholder instead of a blank edit box, you can just return your placeholder in place of children. Like this: (live example)
/** Show a placeholder instead of the selected value when showing the dropdown list */
function SingleValue(props) {
const { children, ...rest } = props;
const { selectProps } = props;
let contents = children;
if (selectProps.menuIsOpen) {
contents = selectProps.placeholder ? (
<div style={{ color: "#999" }}>{selectProps.placeholder}</div>
) : (
<Fragment></Fragment>
);
}
return <components.SingleValue {...rest}>{contents}</components.SingleValue>;
}
It's a shame that the docs for each react-select prop are so limited and they lack a table of contents to easily navigate. It's really full-featured once you figure out how the advanced stuff works like replacing custom components. If you're considering contributing feature PRs to that library, consider enhancing the docs too!
Related
I have created a component which adds an additional selection box dropdown whenever a key inside an object is another object.
For example, consider the following object:
{
a1: {
x1: 1,
x2: 2,
x3: 3,
x4: {
z1: "z1",
z2: "z2"
},
x5: [
{
x5a: {
z5a1: 1,
z5a2: 2
}
},
{
x5b: {
z5b1: 1,
z5b2: 2
}
}
]
},
a2: {
x1: 1,
x2: 2,
x3: 3
},
a3: "some values"
};
What I want to achieve is (when I select a value from the dropdown menu):
if subTree[value] is an object ({}) or an array ([]), display its keys or indices in
a new selection box drop down, directly bellow the current
else stop
Initial display
Selecting a value in the dropdown
After I select a value, the next selection will show empty, and so on and so forth...
The problem
When I update a value in a selection box, my code doesn't update/clear the selections bellow it properly.
The source code of my project is available at: https://codesandbox.io/s/frosty-grass-9jdue
When changing a value that is not the last in the path, you need to clear all subsequent selections because they were based on a different path. I'm not quite sure how we do that in your setup because I haven't quite wrapped my head around it.
What makes sense to me is to store the pieces of the path as an array. That way we can use slice to remove the tail. I am going to use lodash's get method as a helper to access the value at a path. I am expecting the prop data to be the object itself rather than Object.entries like you were doing before.
import React, { useState, useEffect } from "react";
import { MenuItem, TextField } from "#material-ui/core";
import _get from "lodash/get";
const InfiniteSelection = ({ data, onCaseCadeSelection }) => {
// an array of segments like ['a1', 'x4', 'z1']
const [path, setPath] = useState([]);
// joins to a string like `a1.x4.z1`
const cascade = path.join(".");
// call callback whenever the cascade changes
useEffect(() => {
if (onCaseCadeSelection) {
onCaseCadeSelection(cascade);
}
}, [cascade]);
// need to know the index in the paths array where the change occurred
const handleChange = (index) => (event) => {
// set this value and delete everything after it
setPath([...path.slice(0, index), event.target.value]);
};
// options for the NEXT value from a given path
const optionsForPath = (path) => {
// lodash get handles this except when path is empty array []
const value = path.length > 0 ? _get(data, path) : data;
// get the options from this path, or null if it is terminal
return typeof value === "object" ? Object.keys(value) : null;
};
// either the current path is to a terminal value, or there should be one more level of selects
const currentOptions = optionsForPath(path);
// helper function can be used as a callback to path.map
// will also be called one extra time for the next values if not on a terminal value
const renderSelect = (value, index) => {
return (
<SelectControlled
className="text form_text"
variant="outlined"
list={optionsForPath(path.slice(0, index)) ?? []}
onChange={handleChange(index)}
value={value ?? ""}
/>
);
};
// render selects for each element in the path and maybe a next select
return (
<div className="vertically_spaced">
{path.map(renderSelect)}
{currentOptions === null || renderSelect("", path.length)}
</div>
);
};
Code Sandbox Link
From #LindaPaiste's answer:
When changing a value that is not the last in the path, you need to clear all subsequent selections because they were based on a different path.
That's the key to solving your problem! You have to somehow blow away and forget everything bellow the selection box whose value you are currently changing.
React was designed around the "blow away and forget" principle. Note also that The Data Flows Down. With that in mind, your task should be fairly easy to complete and while Linda's solution seems to work, it is perhaps not as simple as it could be.
What if we could have a special component that (1) accepts a sub-tree of your data, (2) renders its 1st level children as a selection box dropdown and then (3) repeats the process recursively? Something like this:
<RecursiveComponent subTree={DATA_SAMPLE} {/*maybe some other props*/}/>
When we think of recursion, we have to think of terminal conditions. In our case, this happens when the sub-tree is a primitive type (i.e. not an object ({}) or an array ([])).
Every RecursiveComponent has to:
render the selection menu dropdown, containing all the 1st level children of the sub-tree
render the nested RecursiveComponent, based on props.subTree[selection]
handle user interaction
Something like this:
import { MenuItem, Select } from "#material-ui/core";
import { useState } from "react";
function RecursiveComponent(props) {
const [selection, setSelection] = useState(props.currentSelection);
const handleChange = (event) => {
setSelection(event.target.value);
};
return (
<>
<Select variant="outlined" value={selection} onChange={handleChange}>
{Object.keys(props.subTree).map((key) => (
<MenuItem value={key}>{key}</MenuItem>
))}
</Select>
<div /> {/* forces a line break between selection boxes */}
{props.subTree[selection] !== Object(props.subTree[selection]) ? (
<></>
) : (
<RecursiveComponent
subTree={props.subTree[selection]}
currentSelection=""
/>
)}
</>
);
}
export default RecursiveComponent;
This is how you can use RecursiveComponent in your project by editing index.js:
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { DATA_SAMPLE } from "./DataSample";
import RecursiveComponent from "./RecursiveComponent";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<RecursiveComponent subTree={DATA_SAMPLE} currentSelection="" />
</StrictMode>,
rootElement
);
I have a React functional component:
interface ISpecialButton {
text: string;
}
const SpecialButton: FC<ISpecialButton > = ({
/* Prop description */
text
} = {
//code
});
And would like a developer who uses it in a JSX template to see "Prop description" when he hovers over text prop when adding the SpecialButton to a template. I have tried various ways to do it with JSDoc, but none of these work. What is the correct way to use JSDoc for a React functional component so that VSCode shows tooltips?
The interface is the one that should be documented so every other place it's going to show the docs when hovering over it. Something like this:
interface ISpecialButton {
/**
* Prop description
*/
text: string;
}
const SpecialButton: React.FC<ISpecialButton> = ({
text
}) => {
return <>{ text }</>;
};
const Template: React.FC = () => {
return <>
<SpecialButton text='' />
</>
}
Given that I can't test internals directly with react-testing-library, how would I go about testing a component that uses react-select? For instance, if I have a conditional render based on the value of the react-select, which doesn't render a traditional <select/>, can I still trigger the change?
import React, { useState } from "react";
import Select from "react-select";
const options = [
{ value: "First", label: "First" },
{ value: "Second", label: "Second" },
{ value: "Third", label: "Third" },
];
function TestApp() {
const [option, setOption] = useState(null);
return (
<div>
<label htmlFor="option-select">Select Option</label>
<Select
value={option}
options={options}
onChange={option => setOption(option)}
/>
{option && <div>{option.label}</div>}
</div>
);
}
export default TestApp;
I'm not even sure what I should query for. Is it the hidden input?
My team has a test utility in our project that lets us select an item easily after spending too much time trying to figure out how to do this properly. Sharing it here to hopefully help others.
This doesn't rely on any React Select internals or mocking but does require you to have set up a <label> which has a for linking to the React Select input. It uses the label to select a given choice value just like a user would on the real page.
const KEY_DOWN = 40
// Select an item from a React Select dropdown given a label and
// choice label you wish to pick.
export async function selectItem(
container: HTMLElement,
label: string,
choice: string
): Promise<void> {
// Focus and enable the dropdown of options.
fireEvent.focus(getByLabelText(container, label))
fireEvent.keyDown(getByLabelText(container, label), {
keyCode: KEY_DOWN,
})
// Wait for the dropdown of options to be drawn.
await findByText(container, choice)
// Select the item we care about.
fireEvent.click(getByText(container, choice))
// Wait for your choice to be set as the input value.
await findByDisplayValue(container, choice)
}
It can be used like this:
it('selects an item', async () => {
const { container } = render(<MyComponent/>)
await selectItem(container, 'My label', 'value')
})
You can try the following to get it working:
Fire focus event on the ReactSelect component .react-select input element.
Fire a mouseDown event on the .react-select__control element
Fire a click on the option element that you want to select
You can add a className and classNamePrefix props with the value of "react-select" in order to specifically select the component you are trying to test.
PS: In case you are still stuck I'd encourage you to take a look at this conversation from where the above answer is borrowed - https://spectrum.chat/react-testing-library/general/testing-react-select~5857bb70-b3b9-41a7-9991-83f782377581
Am new to ReactJS. I need to make the "placeholder" which is set to "State" initially to Empty/Null when onClicked or onFocus and then when it's not focused on, it goes back to "State" again. Can someone help me with this, am very new to react so any help will be appreciated.
import React from "react";
import { render } from "react-dom";
import { Container, Button, Modal, Dropdown } from "semantic-ui-react";
const stateOptions = [
{ key: "AL", value: "AL", text: "Alabama" },
{ key: "NY", value: "NY", text: "New York" }
];
const App = () => (
<Dropdown
placeholder="State"
fluid
multiple
search
selection
options={stateOptions}
/>
);
render(<App />, document.getElementById("root"));
From React's perspective, placeholder is a state that needs to be changed according to user's actions (onClick, onBlur)
So create a state to hold placeholder value that need to change.
There are two ways (since v16.8.0 with the introduction of React Hooks).
Using Class Component
class DropDown extends React.Component {
defaultPlaceholderState = "State";
state = { placeholder: this.defaultPlaceholderState };
clearPlaceholder = () => this.setState({ placeholder: "" });
resetPlaceholder = () =>
this.setState({ placeholder: this.defaultPlaceholderState });
render() {
return (
<Dropdown
onClick={this.clearPlaceholder}
onFocus={this.clearPlaceholder}
onBlur={this.resetPlaceholder}
placeholder={this.state.placeholder}
fluid
multiple
search
selection
options={stateOptions}
/>
);
}
}
In the code above, placeholder declared as a state with default value set to this.defaultPlaceholderState.
When a user clicks on the dropdown, onClick clears the placeholder value by setting it to an empty string. Same for onFocus when the Dropdown is on focus.
When a user clicks outside (onBlur), resetPlaceHolder sets the placeholder value to the default this.defaultPlaceholderState.
Using Function Component with useState hook
React v16.8.0 introduces Hooks, which enables Function Components (not a Functional Component, as it refers to Functional Programming) to hold states.
You can use React.useState hook to hold placeholder value.
const DropDownUsingHook = () => {
const defaultPlaceholderState = "State";
const [placeholder, setPlaceholder] = React.useState(defaultPlaceholderState);
const clearPlaceholder = () => setPlaceholder("");
const resetPlaceholder = () => setPlaceholder(defaultPlaceholderState);
return (
<Dropdown
onClick={clearPlaceholder}
onFocus={clearPlaceholder}
onBlur={resetPlaceholder}
placeholder={placeholder}
fluid
multiple
search
selection
options={stateOptions}
/>
);
};
⚠ Note: Unlike the Class version, clearPlaceholder, resetPlaceholder methods and placeholder state don't use this. prefix.
The implementation is similar but you use useState hook to declare the state and the setter (setPlaceholder).
Refer to the Hooks documentation, Using State Hook for more info.
You can play around with the working code on CodeSandbox.
I'd like part of the record to be included in the label for a BooleanField (and BooleanInput). I'm trying to use WithProps to accomplish this.
If I use
<BooleanField source="FileSystem" label="FileSystem" />
This seems to work just fine. If, instead I try to wrap it
const makeLabel = (props)=>{
let label = `Filesystem for ${props.record.id}`;
return {label};
}
const withLabel = withProps(makeLabel);
const BooleanFieldWithLabel = compose(withLabel)((props)=>{
console.log("props after compose",props);
return <BooleanField {...props}/>
});
And then use <BooleanFieldWithLabel source="FileSystem" /> It doesn't render any label. I've tried a few different ways and nothing seems to work even though I can see in the console.log that the correct label is in props. What am I doing wrong here?
I have the same question, I cannot display the label base on field's value on "Show" page.
From react-admin source code, it seems only I set "addLabel" prop on the direct child of "SimpleShowLayout" or "TabbedShowLayout", then I can see label on my custom field.
But it is not configurable, I want to show/hide label base on field's value. Do I need to implement my own custom "SimpleShowLayout" or "TabbedShowLayout"? Or is there any better approaches?
Update my post.
I just figure out the solution by implementing an HOC like below. I am wondering is there any better approaches to implement the same feature?
import React from "react";
import get from "lodash/get";
import { TextField, DateField, Labeled } from "react-admin";
const NullableField = WrappedComponent => props => {
const { record, source } = props;
const value = get(record, source);
return value ? (
<Labeled {...props}>
<WrappedComponent {...props} />
</Labeled>
) : null;
};
const NullableTextField = NullableField(TextField);
const NullableDateField = NullableField(DateField);
export { NullableTextField, NullableDateField };