How to create fully controlled dropdown in semantic-ui-react - reactjs

I want to create a fully controlled dropdown in order to use react-window to show really long list of items in it.
I've checked docs, and there is no any example of controlled dropdown with Dropdown.Item specified.
This is how my component looks like:
<Dropdown
placeholder="Filter Posts"
clearable={true}
search={true}
onChange={this.handleChange}
text={tagOptions[1].value}
value={tagOptions[1].value}
onSearchChange={this.handleChange}
>
<Dropdown.Menu>
{tagOptions.map(option => (
<Dropdown.Item key={option.value} {...option} onClick={this.handleItemClick} />
))}
</Dropdown.Menu>
</Dropdown>;
I've encounter with 2 issues:
Initial value is not appears, I dig into the code, and saw that if i don't pass options property it won't find the given value, therefore, it will not be shown. I can use the text property, but it seems like a hack.
I need to implement handleItemClick by myself, and I see that there is logic in the original handleItemClick.
Any suggestions? did I missed something here?

I've able to hack it around with using ref on the dropdown and passing the original handleItemClick method.
The only downside for now is that keyboard nav is not works :\
Seem like it was not designed to be fully controlled.
https://codesandbox.io/s/ql3q086l5q

The dropdown module simply doesn't have support for controlling it's inner components, that being said this is the closest I've gotten to a controlled dropdown with react-window support. I'm posting it here for anyone in the future that wants a select dropdown with virtualisation without a headache.
VirtualisedDropdown.js
import React, { forwardRef, useCallback, useRef, useState } from "react"
import { Dropdown, Ref } from "semantic-ui-react"
import { FixedSizeList } from "react-window"
import "./VirtualisedDropdown.scss"
const SUI_DROPDOWN_MENU_HEIGHT = 300
const SUI_DROPDOWN_MENU_ITEM_HEIGHT = 37
const VirtualisedDropdown = ({
options, value,
...restProps
}) => {
const dropdownRef = useRef()
const listRef = useRef()
const [open, setOpen] = useState(false)
const OuterDiv = useCallback(({ style, ...props }, ref) => {
const { position, overflow, ...restStyle } = style
return (
<Ref innerRef={ref}>
<Dropdown.Menu open={open} {...props} style={restStyle}>
{props.children}
</Dropdown.Menu>
</Ref>
)
}, [open])
const InnerDiv = useCallback(props => {
return (
<Dropdown.Menu className="inner" open={open} style={{ ...props.style, maxHeight: props.style.height }}>
{props.children}
</Dropdown.Menu>
)
}, [open])
return (
<Dropdown
className="virtualised selection"
onClose={() => setOpen(false)}
onOpen={() => {
setOpen(true)
listRef.current.scrollToItem(options.findIndex(i => i.value === value))
}}
// This causes "Warning: Failed prop type: Prop `children` in `Dropdown` conflicts with props: `options`. They cannot be defined together, choose one or the other."
// but is necessary for some logic to work e.g. the selected item text.
options={options}
ref={dropdownRef}
selectOnNavigation={false}
value={value}
{...restProps}
>
<FixedSizeList
height={options.length * SUI_DROPDOWN_MENU_ITEM_HEIGHT < SUI_DROPDOWN_MENU_HEIGHT ? options.length * SUI_DROPDOWN_MENU_ITEM_HEIGHT + 1 : SUI_DROPDOWN_MENU_HEIGHT}
innerElementType={InnerDiv}
itemCount={options.length}
itemData={{
options,
handleClick: (_e, x) => dropdownRef.current.handleItemClick(_e, x),
selectedIndex: options.findIndex(i => i.value === value),
}}
itemSize={SUI_DROPDOWN_MENU_ITEM_HEIGHT}
outerElementType={forwardRef(OuterDiv)}
ref={listRef}
>
{Row}
</FixedSizeList>
</Dropdown>
)
}
const Row = ({ index, style, data }) => {
const { options, handleClick, selectedIndex } = data
const item = options[index]
return (
<Dropdown.Item
active={index === selectedIndex}
className="ellipsis"
key={item.value}
onClick={handleClick}
selected={index === selectedIndex}
style={style}
title={item.text}
{...item}
/>
)
}
export default VirtualisedDropdown
VirtualisedDropdown.scss
.ui.dropdown.virtualised .menu {
&.inner {
margin: 0 -1px !important;
left: 0;
overflow: initial;
border-radius: 0 !important;
border: 0;
}
> .item {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}

To solve first problem remove clearable={true} and text={tagOptions[1].value}
What handleItemClick function should do?

Related

Autocomplete not rendering as expected Material UI

My autocomplete component is pulling a list of books from an API. I am rendering them as options in the Autocomplete component, and also outputting them as a list at the bottom of the page for debugging purposes. Also outputting the JSON from the API.
Two issues seem to be intertwined. First, the Autocomplete options don't seem to be all rendering. There are up to 10 results (limited to 10 by the API call) and they're all rending in the list below the autocomplete, but not in the list of options in the Autocomplete. Second, when the API is being called (like the time between changing the text from "abc" to "abcd") it shows "No options" rather than displaying the options from just "abc".
In the sandbox code here try typing slowly - 1 2 3 4 5 6 - you'll see that there are results in the <ul> but not in the <Autocomplete>.
Any ideas on why this (or maybe both separately) are happening?
Thanks!
Code from sandbox:
import React, { useState, useEffect } from "react";
import Autocomplete from "#material-ui/lab/Autocomplete";
import {
makeStyles,
Typography,
Popper,
InputAdornment,
TextField,
Card,
CardContent,
CircularProgress,
Grid,
Container
} from "#material-ui/core";
import MenuBookIcon from "#material-ui/icons/MenuBook";
import moment from "moment";
// sample ISBN: 9781603090254
function isbnMatch(isbn) {
const str = String(isbn).replace(/[^0-9a-zA-Z]/, ""); // strip out everything except alphanumeric
const r = /^[0-9]{13}$|^[0-9]{10}$|^[0-9]{9}[Xx]$/; // set the regex for 10, 13, or 9+X characters
return str.match(r);
// return str.match(/^[0-9]{3}$|^[0-9]{3}$|^[0-9]{2}[Xx]$/);
}
const useStyles = makeStyles((theme) => ({
adornedEnd: {
backgroundColor: "inherit",
height: "2.4rem",
maxHeight: "3rem"
},
popper: {
maxWidth: "fit-content"
}
}));
export default function ISBNAutocomplete() {
console.log(`Starting ISBNAutocomplete`);
const classes = useStyles();
const [options, setOptions] = useState([]);
const [inputText, setInputText] = useState("");
const [open, setOpen] = useState(false);
const loading = open && options.length === 0 && inputText.length > 0;
useEffect(() => {
async function fetchData(searchText) {
const isbn = isbnMatch(searchText);
//console.log(`searchText = ${searchText}`);
//console.log(`isbnMatch(searchText) = ${isbn}`);
const fetchString = `https://www.googleapis.com/books/v1/volumes?maxResults=10&q=${
isbn ? "isbn:" + isbn : searchText
}&projection=full`;
//console.log(fetchString);
const res = await fetch(fetchString);
const json = await res.json();
//console.log(JSON.stringify(json, null, 4));
json && json.items ? setOptions(json.items) : setOptions([]);
}
if (inputText?.length > 0) {
// only search the API if there is something in the text box
fetchData(inputText);
} else {
setOptions([]);
setOpen(false);
}
}, [inputText, setOptions]);
const styles = (theme) => ({
popper: {
maxWidth: "fit-content",
overflow: "hidden"
}
});
const OptionsPopper = function (props) {
return <Popper {...props} style={styles.popper} placement="bottom-start" />;
};
console.log(`Rendering ISBNAutocomplete`);
return (
<>
<Container>
<h1>Autocomplete</h1>
<Autocomplete
id="isbnSearch"
options={options}
open={open}
//noOptionsText=""
style={{ width: 400 }}
PopperComponent={OptionsPopper}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
onChange={(event, value) => {
console.log("ONCHANGE!");
console.log(`value: ${JSON.stringify(value, null, 4)}`);
}}
onMouseDownCapture={(event) => {
event.stopPropagation();
console.log("STOPPED PROPAGATION");
}}
onInputChange={(event, newValue) => {
// text box value changed
//console.log("onInputChange start");
setInputText(newValue);
// if ((newValue).length > 3) { setInputText(newValue); }
// else { setOptions([]); }
//console.log("onInputChange end");
}}
getOptionLabel={(option) =>
option.volumeInfo && option.volumeInfo.title
? option.volumeInfo.title
: "Unknown Title"
}
getOptionSelected={(option, value) => option.id === value.id}
renderOption={(option) => {
console.log(`OPTIONS LENGTH: ${options.length}`);
return (
<Card>
<CardContent>
<Grid container>
<Grid item xs={4}>
{option.volumeInfo &&
option.volumeInfo.imageLinks &&
option.volumeInfo.imageLinks.smallThumbnail ? (
<img
src={option.volumeInfo.imageLinks.smallThumbnail}
width="50"
height="50"
/>
) : (
<MenuBookIcon size="50" />
)}
</Grid>
<Grid item xs={8}>
<Typography variant="h5">
{option.volumeInfo.title}
</Typography>
<Typography variant="h6">
(
{new moment(option.volumeInfo.publishedDate).isValid()
? new moment(option.volumeInfo.publishedDate).format(
"yyyy"
)
: option.volumeInfo.publishedDate}
)
</Typography>
</Grid>
</Grid>
</CardContent>
</Card>
);
}}
renderInput={(params) => (
<>
<TextField
{...params}
label="ISBN - 10 or 13 digit"
//"Search for a book"
variant="outlined"
value={inputText}
InputProps={{
...params.InputProps, // make sure the "InputProps" is same case - not "inputProps"
autoComplete: "new-password", // forces no auto-complete history
endAdornment: (
<InputAdornment
position="end"
color="inherit"
className={classes.adornedEnd}
>
<>
{loading ? (
<CircularProgress color="secondary" size={"2rem"} />
) : null}
</>
{/* <>{<CircularProgress color="secondary" size={"2rem"} />}</> */}
</InputAdornment>
),
style: {
paddingRight: "5px"
}
}}
/>
</>
)}
/>
<ul>
{options &&
options.map((item) => (
<li key={item.id}>{item.volumeInfo.title}</li>
))}
</ul>
<span>
inputText: <pre>{inputText && inputText}</pre>
</span>
<span>
<pre>
{options && JSON.stringify(options, null, 3).substr(0, 500)}
</pre>
</span>
<span>Sample ISBN: 9781603090254</span>
</Container>
</>
);
}
By default, Autocomplete filters the current options array by the current input value. In use cases where the options are static, this doesn't cause any issue. Even when the options are asynchronously loaded, this only causes an issue if the number of query matches is limited. In your case, the fetch is executed with maxResults=10 so only 10 matches are returned at most. So if you are typing "123" slowly, typing "1" brings back 10 matches for "1" and none of those matches contain "12" so once you type the "2", none of those 10 options match the new input value, so it gets filtered to an empty array and the "No options" text is displayed until the fetch for "12" completes. If you now delete the "2", you won't see the problem repeat because all of the options for "12" also contain "1", so after filtering by the input value there are still options displayed. You also wouldn't see this problem if all of the matches for "1" had been returned, because then some of those options would also contain "12" so when you type the "2" the options would just be filtered down to that subset.
Fortunately, it is easy to address this. If you want Autocomplete to always show the options you have provided it (on the assumption that you will modify the options prop asynchronously based on changes to the input value), you can override its filterOptions function so that it doesn't do any filtering:
<Autocomplete
id="isbnSearch"
options={options}
filterOptions={(options) => options}
open={open}
...
Autocomplete custom filter documentation: https://material-ui.com/components/autocomplete/#custom-filter

Select matching words in the editable area

There is array with words. const = ['cat','dog']. Also textarea or input, or editable div. I need to write words in this field and highlight words if they match.
Now I have a bad variant because I use the highlight npm package and contain it over the input field and hide input text. Caret runs ahead and also I have a lot of problems besides all this.
The default value from this form goes to useState.
Also, I should be able to call the onKeyPress function or something similar, because I add a new tag if e.keyCode === 32 and contain #.
I need to do it use react function component.
Are you looking for something like this(code mentioned below)?
Also, I didn't understand the part of using a key, what do you exactly want to achieve with it.
And keyCode is deprecated, I'll suggest you use key key==='#'
Plus, if you can add a code snippet or elaborate more would help in helping you better
import { useState, useEffect, useMemo } from "react";
const Input = () => {
const [inputValue, setInputValue] = useState("");
const tagsArr = useMemo(() => ({ cats: false, dogs: false }), []);
useEffect(() => {
Object.keys(tagsArr).forEach((el, i) => {
if (inputValue.includes(el)) {
tagsArr[el] = true;
} else {
tagsArr[el] = false;
}
});
}, [inputValue, tagsArr]);
const handleChange = ({ target }) => {
setInputValue(target.value);
};
return (
<div>
<input type="text" value={inputValue} onChange={handleChange} />
<div>
<ul>
{Object.keys(tagsArr).map((el, i) =>
tagsArr[el] ? (
<li
key={el}
style={{
listStyle: "none",
backgroundColor: "springgreen",
width: "max-content",
display: "inline",
padding: "10px",
marginRight: "10px",
}}
>
{"#" + el}
<button>X</button>
</li>
) : null
)}
</ul>
</div>
</div>
);
};
export default Input;

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.

Change selected value component in react-select

Is there any way to change the selected value component design, At my option menu, I show CscId and CscDesc but when I select the option, I only want to show CscId only. Is there any way to change the selected value component? I google for this one and it already took 1 day. Please Help me.
Here is my react-select
import React from "react";
import Select from 'react-select';
const costcenterselect = ({ value, onChange, id, datasource }) => {
const formatOptionLabel = ({ CscID, CscDesc }) => (
<div style={{ display: "flex"}}>
<div style={{width:'40%'}}>{CscID}</div>
<div>{CscDesc}</div>
</div>
);
return (
<div>
<Select
id={id}
menuIsOpen={true}
formatOptionLabel={formatOptionLabel}
getOptionValue={option => `${option.CscID}`}
options={datasource}
onChange={onChange}
defaultValue={value}
/>
</div>
)
}
export default costcenterselect;
You can do it using formatOptionLabel itself. It has a second argument which provides you with meta information like context which you can use to conditionally render. Here is a working demo.
You can see that context === value allows you to render for selected value while context === menu renders for the options.
const formatOptionLabel = ({ CscID, CscDesc }, { context }) => {
if (context === "value") {
return <div>{CscID}</div>;
} else if (context === "menu") {
return (
<div style={{ display: "flex" }}>
<div style={{ width: "40%" }}>{CscID}</div>
<div>{CscDesc}</div>
</div>
);
}
};

Deleting individual items from react-beautiful-dnd horizontal list

I have react-beautiful-dnd horizontal multiple list(6 rows with the same items in each row) with the same items in each list.
I want to delete individual selected items from each list, but just having a button component with onClick fires the onClick while rendering the lists itself. How do i configure the list so that an individual item is deleted from that list when i click on the close/delete (x) button?
Below, is my code,
import React, { Component } from 'react';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import {Button, Icon} from 'semantic-ui-react'
// a little function to help us with reordering the result
const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const grid = 12;
const getItemStyle = (isDragging, draggableStyle) => ({
// some basic styles to make the items look a bit nicer
userSelect: 'none',
padding: grid / 2,
margin: `0 ${grid}px 0 0`,
// change background colour if dragging
background: isDragging ? 'lightgreen' : 'lightgray',
// styles we need to apply on draggables
...draggableStyle,
});
const getListStyle = isDraggingOver => ({
background: isDraggingOver ? 'lightblue' : 'white',
display: 'flex',
padding: grid,
overflow: 'auto',
});
class DragAndDrop extends Component {
constructor(props) {
super(props);
this.state = {
items: this.props.uniqueEntries
};
this.onDragEnd = this.onDragEnd.bind(this)
this.removeSubject = this.removeSubject.bind(this)
}
onDragEnd(result) {
// dropped outside the list
if (!result.destination) {
return;
}
const items = reorder(
this.state.items,
result.source.index,
result.destination.index
);
this.setState({
items,
});
}
componentWillReceiveProps(newProps){
this.setState({
items : newProps.uniqueEntries
})
}
removeItem = (index) => {
this.state.items.splice(index, 1)
}
render() {
return (
<DragDropContext onDragEnd={this.onDragEnd}>
<Droppable droppableId="droppable" direction="horizontal">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}
{...provided.droppableProps}
>
{this.state.items.map((item, index) => (
<Draggable key={item.Id} draggableId={item.Id}
index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}
>
<Button icon size='mini'
style={{backgroundColor : 'lightgray' ,padding:
'0', float: 'right'}}
onClick = {this.removeItem(index)}>
<Icon name='close' />
</Button>
{item.name}
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
}
}
export default DragAndDrop
A Note for this bug that might hit you later and future viewers. 😋
You are not supposed to mutate the state directly. It will have some side effects or none at all.
I also recommend you generate unique IDs because if you create more list, it will also have some unexpected result even react-beautiful-dnd won't notice.
If you want to update or remove state data, use setState(). First modify existing data with a copy. Then assign the new value.
removeItem(index) {
// do magic here to filter out the unwanted element
// Update via 'setState'
this.setState({
items : newModifiedItems
})
}
For an example, my trial method below:
removeItem(e) {
e.preventDefault();
// give your single list a className so you can select them all
const sourceList = document.querySelectorAll(".list-name");
const arrList= Array.from(sourceList);
// make shallow copy of your state data
const newItems = Array.from(this.state.items);
// Find index element from whole list Arr by traversing the DOM
const removeItemIndex = arrList.indexOf(e.target.parentElement);
// Remove it
newItems.splice(removeItemIndex, 1);
this.setState({
items: newItems
})
}
I found out why it was firing at render, instead of passing the function i was initiating it so through the loop it was getting called. Then i did this and it worked. May be this will help someone who might face a similar issue.
<Button icon size='mini'
style={{backgroundColor : 'lightgray' ,padding: '0',
float: 'right', marginLeft:'15px'}}
onClick = {() => this.removeItem(index)}>
<Icon name='close' />
</Button>

Resources