I am working with ReactJS and using Semantic UI.
I have a breadcrumb style navigation menu on my page built using the 'Breadcrumb' and 'Dropdown' components from Semantic UI.
I am trying to make the behaviour of the menu accessible by allowing the user to use the keyboard (tab, arrows and enter) to navigate the menu, the dropdown options and to make a selection from the dropdown options.
The issue I am trying to solve is when using the keyboard, the user should only be able to select an option when it is focused on (using the arrow keys) followed by pressing the 'enter' key. At the moment when the user focuses on an option, it gets selected when they 'tab' to the next element.
Is it possible to change this behaviour so an option is only selected when the enter key is pressed?
I tried implementing "selectOnNavigation={false}" on the Dropdown component however this still results in the option being selected when 'tabbing' away from the element.
I also tried manipulating the event handler onBlur() but it still selected the option when tabbing.
Code for my component:
import React from 'react';
import { connect } from 'react-redux';
import { Breadcrumb, Dropdown } from 'semantic-ui-react';
import PropTypes from 'prop-types';
import {
saveBreadcrumbOptions,
changeBreadcrumbMarket,
changeBreadcrumbParentGroup,
changeBreadcrumbAgency
} from '../../actions/breadcrumbActions';
import MenuFiltersAPI from '../../api/MenuFiltersAPI';
import './OMGBreadcrumb.css';
export class OMGBreadcrumb extends React.Component {
constructor(props) {
super(props);
this.state = {
markets: [],
parentGroups: [],
agencies: []
};
this.getBreadcrumbOptions = this.getBreadcrumbOptions.bind(this);
this.handleMarketChange = this.handleMarketChange.bind(this);
this.handleParentGroupChange = this.handleParentGroupChange.bind(this);
this.handleAgencyChange = this.handleAgencyChange.bind(this);
this.handleParentGroupBlur = this.handleParentGroupBlur.bind(this);
}
componentDidMount() {
this.getBreadcrumbOptions();
}
async getBreadcrumbOptions() {
// get all options
const breadcrumb = this.props.breadcrumb.options.length
? this.props.breadcrumb.options
: await MenuFiltersAPI.getBreadcrumb();
// if a market is selected, use it
// otherwise use the first one
let selectedMarket = breadcrumb.find(m => (
m.id === this.props.breadcrumb.selectedMarket
));
selectedMarket = selectedMarket
? selectedMarket.id
: breadcrumb[0].id;
this.props.saveBreadcrumbOptions(breadcrumb);
this.setState({ markets: breadcrumb }, () => this.changeMarket(selectedMarket));
}
changeMarket(id) {
// get parent group options for given market
const parentGroups = this.state.markets.find(market => market.id === id).parent_groups;
// if a parent group is selected, use it
// otherwise use the first one
let selectedParentGroup = parentGroups.find(pg => (
pg.id === this.props.breadcrumb.selectedParentGroup
));
selectedParentGroup = selectedParentGroup
? selectedParentGroup.id
: parentGroups[0].id;
this.props.changeBreadcrumbMarket(id);
this.setState({ parentGroups }, () => this.changeParentGroup(selectedParentGroup));
}
changeParentGroup(id) {
// get agency options for dropdown menu
const agencies = this.state.parentGroups.find(parentGroup => parentGroup.id === id).agencies;
let selectedAgency = agencies.find(a => (
a.id === this.props.breadcrumb.selectedAgency
));
selectedAgency = selectedAgency
? selectedAgency.id
: agencies[0].id;
this.props.changeBreadcrumbParentGroup(id);
this.setState({ agencies }, () => this.changeAgency(selectedAgency));
}
changeAgency(id) {
// const selectedAgency = agencyOptions[0].value
this.props.changeBreadcrumbAgency(id);
}
handleMarketChange(e, { value }) {
console.log(value)
this.changeMarket(value);
}
handleParentGroupChange(e, { value }) {
console.log(value)
// if(!!value){
// return;
// }
this.changeParentGroup(value);
}
handleAgencyChange(e, { value }) {
console.log(value)
this.changeAgency(value);
}
handleParentGroupBlur(e, {value}) {
e.preventDefault();
console.log(e.key)
if(e.key !== 'Enter'){
console.log('key was not enter')
return;
}
}
render() {
return (
<div id="OMGBreadcrumb">
<b>Show information by: </b>
<Breadcrumb>
<Breadcrumb.Section>
<Dropdown
selectOnNavigation={false}
options={this.state.markets.reduce((acc, cur) => {
acc.push({ text: cur.name, value: cur.id });
return acc;
}, [])}
value={this.props.breadcrumb.selectedMarket}
onChange={this.handleMarketChange}
openOnFocus={false}
/>
</Breadcrumb.Section>
<Breadcrumb.Divider icon='right chevron' />
<Breadcrumb.Section>
<Dropdown
selectOnNavigation={false}
options={this.state.parentGroups.reduce((acc, cur) => {
acc.push({ text: cur.name, value: cur.id });
return acc;
}, [])}
value={this.props.breadcrumb.selectedParentGroup}
onChange={this.handleParentGroupChange}
openOnFocus={false}
onBlur={this.handleParentGroupBlur}
/>
</Breadcrumb.Section>
<Breadcrumb.Divider icon='right chevron' />
<Breadcrumb.Section>
<Dropdown
// selectOnNavigation={false}
options={this.state.agencies.reduce((acc, cur) => {
acc.push({ text: cur.name, value: cur.id });
return acc;
}, [])}
value={this.props.breadcrumb.selectedAgency}
onChange={this.handleAgencyChange}
openOnFocus={false}
/>
</Breadcrumb.Section>
</Breadcrumb>
</div>
);
}
}
OMGBreadcrumb.propTypes = {
saveBreadcrumbOptions: PropTypes.func.isRequired,
changeBreadcrumbMarket: PropTypes.func.isRequired,
changeBreadcrumbParentGroup: PropTypes.func.isRequired,
changeBreadcrumbAgency: PropTypes.func.isRequired,
breadcrumb: PropTypes.objectOf(
PropTypes.oneOfType([
PropTypes.number,
PropTypes.array
])
).isRequired
};
export default connect(
store => ({
breadcrumb: store.breadcrumb
}),
{
saveBreadcrumbOptions,
changeBreadcrumbMarket,
changeBreadcrumbParentGroup,
changeBreadcrumbAgency
}
)(OMGBreadcrumb);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Appreciate any guidance.
You can add selectOnBlur={false} to your dropdown component and it will no longer select when the dropdown is blurred.
<Dropdown
...
selectOnBlur={false}
/>
https://codesandbox.io/s/semantic-ui-example-7qgcu?module=%2Fexample.js
Related
I have used a drop-down in my React project. When I select a value I want to close it. But it doesn't automatically close. How can I do it ?
Dropdown.js
import React from 'react';
import { PropTypes } from 'prop-types';
import { DropdownToggle, DropdownMenu, UncontrolledDropdown } from 'reactstrap';
import * as Icon from 'react-bootstrap-icons';
import './DropDown.scss';
/**
* This is a reusable dropdown
* onClick function and dropdown items come as props
*/
class DropDown extends React.Component {
render() {
const { dropDownItemArray, text, onClick } = this.props;
const dropdownItems = dropDownItemArray.map((item,key) => {
return (
<div className="dropdown-items" onClick={onClick} >
{item}
</div>
);
});
return (
<div>
<UncontrolledDropdown className="multi-select-wrapper text p4">
<DropdownToggle className="select-dropdown">
<div className="select-text text p4">{text}</div>
<Icon.CaretDownFill />
</DropdownToggle>
<DropdownMenu name='test'>{dropdownItems}</DropdownMenu>
</UncontrolledDropdown>
</div>
);
}
}
DropDown.propTypes = {
text: PropTypes.string,
onClick: PropTypes.func,
menuItemArray: PropTypes.array
};
export default DropDown;
This handles all the input values from the input fields and selected values from dropdowns.
handleChangeInputs = (event) => {
if (event[0] === '<') {
this.setState({
editorHtml: event
});
} else {
if (event.type === 'change') {
this.setState({
[event.target.name]: event.target.value
});
} else {
if (event.target.parentNode.innerText[0] === 'C') {
console.log(event);
this.setState({
ticketType: event.currentTarget.textContent
});
} else {
console.log("test");
this.setState({
ticketPriority: event.currentTarget.textContent
});
}
}
}
};
This part is related to drop-down handling
if (event.target.parentNode.innerText[0] === 'C') {
console.log(event);
this.setState({
ticketType: event.currentTarget.textContent
});
} else {
console.log("test");
this.setState({
ticketPriority: event.currentTarget.textContent
});
}
You could add a toggleDropdown function along with a property in your state dropdownOpen which will cause the dropdown to be open or closed:
toggleDropdown = () => {
const dropdownOpen = this.state.dropdownOpen ? false : true;
this.setState({ dropdownOpen: dropdownOpen})
};
Pass toggleDropdown and dropdownOpen in the props and reference them in your code:
const { toggleDropdown, dropdownOpen } = this.props;
[...]
<UncontrolledDropdown
isOpen={dropdownOpen || false}
toggle={toggleDropdown}
>
Your code references an onClick function but you've named your function handleChangeInputs. Here's an onClick function that would work.
onClick = (event) => {
this.setState({ ticketType: event.currentTarget.textContent }, () => this.toggleDropdown()}
Calling toggleDropdown inside of onClick only seems to work if it's in the callback of this.setState.
import { Highlight, connectAutoComplete } from 'react-instantsearch-dom';
import AutoSuggest from 'react-autosuggest';
import Autocomplete, { createFilterOptions } from '#material-ui/lab/Autocomplete';
class AutoCompleteuix extends Component {
static propTypes = {
hits: PropTypes.arrayOf(PropTypes.object).isRequired,
currentRefinement: PropTypes.string.isRequired, };
state = {
value: this.props.currentRefinement,
};
onChange = (_, { newValue }) => {
if (!newValue) {
this.props.onSuggestionCleared();
}
this.setState({
value: newValue,
});
};
onSuggestionsFetchRequested = ({ value }) => {
this.props.refine(value);
};
onSuggestionsClearRequested = () => {
this.props.refine();
};
render() {
const { hits, onSuggestionSelected } = this.props;
const { value } = this.state;
const inputProps = {
onChange: this.onChange,
};
return (
<Autocomplete
options={hits}
getOptionLabel={(hits) => hits.ROMAN}
onInputChange = {(ev,obj,val) => this.props.refine(obj) }
renderInput={(params) => <TextField {...params} label="search for a Word" variant="outlined"/> }
value
/>
);
}
}
export default connectAutoComplete(AutoCompleteuix);
hits has the options with are being passed into options but my problem is the dropdown is not updated as per the latest value but the hits have all the updated values.
im tryign to use the algolia search which give the values of hit coming from another component
I want to click on an option in the menu. This options should then display all the items associated with that option in the child component. I know I am going wrong in two places. Firstly, I am going wrong in the onClick function. Secondly, I am not sure how to display all the items of ONLY the option (Eg.Brand, Holiday destination, Film) that is clicked in the child component. Any help would be greatly appreciated.
horizantalscroll.js
import React from 'react';
import ScrollMenu from 'react-horizontal-scrolling-menu';
import './hrizontalscroll.css';
import Items from './items';
// list of items
// One item component
// selected prop will be passed
const MenuItem = ({ text, selected }) => {
return (
<div
className="menu-item"
>
{text}
</div>
);
};
onclick(){
this.onClick(name)}
}
// All items component
// Important! add unique key
export const Menu = (list) => list.map(el => {
const { name } = el;
return (
<MenuItem
text={name}
key={name}
onclick={this.onclick.name}
/>
);
});
const Arrow = ({ text, className }) => {
return (
<div
className={className}
>{text}</div>
);
};
const ArrowLeft = Arrow({ text: '<', className: 'arrow-prev' });
const ArrowRight = Arrow({ text: '>', className: 'arrow-next' });
class HorizantScroller extends React.Component {
state = {
selected: 0,
statelist: [
{name: "Brands",
items: ["1", "2", "3"]
},
{name: "Films",
items: ["f1", "f2", "f3"]
},
{name: "Holiday Destination",
items: ["f1", "f2", "f3"]
}
]
};
onSelect = key => {
this.setState({ selected: key });
}
render() {
const { selected } = this.state;
// Create menu from items
const menu = Menu(this.state.statelist, selected);
const {statelist} = this.state;
return (
<div className="HorizantScroller">
<ScrollMenu
data={menu}
arrowLeft={ArrowLeft}
arrowRight={ArrowRight}
selected={selected}
onSelect={this.onSelect}
/>
<items items={items}/>
</div>
);
}
}
export default HorizantScroller;
items.js
import React, { Component } from "react";
import HorizontalScroller from "horizontalscroll.js";
class Items extends React.Component{
render() {
return (
<div>
{this.statelist.items.map({items, name}) =>
name === this.statelist.name && <div>{items}</div>
}
</div>
);
}
}
export default Items;
The answer is in passing to your Items component the correct array it needs to show. You are already creating a state variable for what's selected. So it would be along the lines of:
<Items items={items[selected]}/>
I'm working under the to-do list project and have got a problem with adding a comment to my list item. This is what I have right now:
App flow
After adding a list item you should be able to click in this item and a new window with comments will appear. In the comments section, comments should be added with Ctrl+Enter combination.
I've got a problem with adding comments to the list item (they should be added to the "comments" array of a particular list item).
Could you please explain what I'm doing wrong and why my comments aren't adding.
UPDATE: I've updated my code but the following mistake appears when I press Ctr+Enter to add a comment: [Error] (http://joxi.ru/Q2KR1G3U4lZJYm)
I've tried to bind the addItem method but no result. What's wrong with the addItem method?
Here is my main component:
App.js
import React, { Component } from 'react';
import './App.css';
import ListInput from './components/listInput'
import ListItem from './components/listItem'
import SideBar from './components/sideBar'
import CommentsSection from './components/commentsSection'
class App extends Component {
constructor(props){
super(props);
this.state = {
items: [
{
id: 0,
text: 'First item',
commentsCount: 0,
comments: [],
displayComment: false
},
{
id: 1,
text: 'Second item',
commentsCount: 0,
comments: [],
displayComment: false
},
{
id: 2,
text: 'Third item',
commentsCount: 0,
comments: [
'Very first comment',
'Second comment',
],
displayComment: false
},
],
nextId: 3,
activeComment: [],
}
}
// Add new item to the list
addItem = inputText => {
let itemsCopy = this.state.items.slice();
itemsCopy.push({id: this.state.nextId, text: inputText});
this.setState({
items: itemsCopy,
nextId: this.state.nextId + 1
})
}
// Remove the item from the list: check if the clicked button id is match
removeItem = id =>
this.setState({
items: this.state.items.filter((item, index) => item.id !== id)
})
setActiveComment = (id) => this.setState({ activeComment: this.state.items[id] });
addComment = (inputComment, activeCommentId ) => {
// find item with id passed and select its comments array
let commentCopy = this.state.items.find(item => item.id === activeCommentId)['comments']
commentCopy.push({comments: inputComment})
this.setState({
comments: commentCopy
})
}
render() {
return (
<div className='App'>
<SideBar />
<div className='flex-container'>
<div className='list-wrapper'>
<h1>Items</h1>
<ListInput inputText='' addItem={this.addItem}/>
<ul>
{
this.state.items.map((item) => {
return <ListItem item={item} key={item.id} id={item.id} removeItem={this.removeItem} setActiveComment={() => this.setActiveComment(item.id)}/>
})
}
</ul>
</div>
<CommentsSection inputComment='' items={this.state.activeComment}/>
</div>
</div>
);
}
}
export default App;
and my Comments Section component:
commentsSection.js
import React from 'react';
import './commentsSection.css';
import CommentInput from './commentInput'
import CommentsItem from './commentsItem'
export default class CommentsSection extends React.Component {
constructor(props){
super(props);
this.state = {value: this.props.inputComment};
this.handleChange = this.handleChange.bind(this);
this.handleEnter = this.handleEnter.bind(this);
this.addComment = this.addComment.bind(this)
}
handleChange = event => this.setState({value: event.target.value})
handleEnter(event) {
if (event.charCode === 13 && event.ctrlKey) {
this.addComment(this.state.value)
}
}
addComment(comment) {
// Ensure the todo text isn't empty
if (comment.length > 0) {
this.props.addComment(comment, this.props.activeComment);
this.setState({value: ''});
}
}
render() {
return (
<div className='component-section'>
<h1>{this.props.items.text}</h1>
<ul>
{ this.props.items.comments &&
this.props.items.comments.map((comment, index) => <p key={index}>{comment}</p>)
}
</ul>
<CommentsItem />
{/*<CommentInput />*/}
<div className='comment-input'>
<input type='text' value={this.state.value} onChange={this.handleChange} onKeyPress={this.handleEnter}/>
</div>
</div>
)
}
}
Change your CommentSection component addComment method and handleEnter method
addComment(comment) {
// Ensure the todo text isn't empty
if (comment.length > 0) {
// pass another argument to this.props.addComment
// looking at your code pass "this.props.items"
this.props.addComment(comment, this.props.items.id);
this.setState({value: ''});
}
}
handleEnter(event) {
if (event.charCode === 13 && event.ctrlKey) {
// passing component state value property as new comment
this.addComment(this.state.value)
}
}
Change your App Component addComment method
addComment = (inputComment, activeCommentId )=> {
// find item with id passed and select its comments array
let commentCopy = this.state.items.find(item => item.id === activeCommentId)['comments']
// if you want to push comments as object
// but looking at your code this is not you want
// commentCopy.push({comments: inputComment})
// if you want to push comments as string
commentCopy.push( inputComment)
this.setState({
comments: commentCopy
})
}
EDIT -
I am using this example here of the react-table select table (HOC)
Simply adding this code in the example code at the link mentioned above provides the results I am trying to achieve.
logSelection = () => {
console.log('selection:', this.state.selection);
this.setState({ selection: [] });
}
Background
I am using react-table in my React.js application. Specifically I am using the select table which provides a check box next to each row. In my case this allows the user to select multiple rows. Once the user submits their selections the data from the rows they selected is sent off for other use in my application.
After submission I am clearing the data that is being held in state. This is important in case they need to make another submission directly afterwords. After I clear the data from the state array holding each row that had previously been selected, the table still shows the previous rows selected even though the data is no longer being held in the state array.
Example Code
This is how I am clearing the array holding the selected arrays,
exportExcel() {
this.setState({ selection: [], selectAll: false });
}
This is all the relevant code in the class,
import React, {Component} from 'react';
import _ from 'lodash';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input } from 'reactstrap';
import ReactTable from 'react-table';
import checkboxHOC from 'react-table/lib/hoc/selectTable';
import 'react-table/react-table.css';
import Chance from 'chance';
import { connect } from 'react-redux';
import { fetchOdx } from '../../store/actions/Odx';
const CheckboxTable = checkboxHOC(ReactTable);
const chance = new Chance();
class TypeAHead extends Component {
constructor(props) {
super(props);
this.state = {
showRow: false,
showExcelForm: false,
modal: false,
selection: [],
selectAll: false,
state: {},
row: {},
column: {},
instance: {},
data: [],
};
this.showRow = this.showRow.bind(this);
this.showExcelForm = this.showExcelForm.bind(this);
this.toggleSelection = this.toggleSelection.bind(this);
this.toggleAll = this.toggleAll.bind(this);
this.isSelected = this.isSelected.bind(this);
this.exportExcel = this.exportExcel.bind(this);
this.setClientEmail = this.setClientEmail.bind(this);
this.props.fetchOdx();
}
componentWillReceiveProps(nextProps) {
if (nextProps.odxData) {
this.setState({
data: nextProps.odxData
});
}
}
showRow() {
this.setState({
showRow: !this.state.showRow
});
}
showExcelForm() {
this.setState({
clientEmail: '',
showExcelForm: !this.state.showExcelForm
});
}
toggleSelection(key, shift, row) {
let selection = [
...this.state.selection
];
const keyIndex = selection.indexOf(key);
if (keyIndex >= 0) {
selection = [
...selection.slice(0, keyIndex),
...selection.slice(keyIndex + 1)
];
} else {
selection.push(row);
}
this.setState({ selection });
}
toggleAll() {
const selectAll = this.state.selectAll ? false : true;
const selection = [];
if (selectAll) {
const wrappedInstance = this.checkboxTable.getWrappedInstance();
const currentRecords = wrappedInstance.getResolvedState().sortedData;
currentRecords.forEach((item) => {
selection.push(item._original._id);
});
}
this.setState({ selectAll, selection });
}
isSelected(key) {
this.state.selection.includes(key);
}
setClientEmail(event) {
this.setState({ clientEmail: event.target.value.toLowerCase() });
}
exportExcel(event) {
this.setState({ selection: [], selectAll: false });
this.showExcelForm();
}
render() {
const { toggleSelection, toggleAll, isSelected } = this;
const { selectAll, } = this.state;
const checkboxProps = {
toggleSelection,
toggleAll,
isSelected,
selectAll,
selectType: 'checkbox',
};
const columns = [{
Header: 'DebtCltRefNo',
accessor: 'DebtCltRefNo'
}, {
Header: 'DbtrDebtorType',
accessor: 'DbtrDebtorType',
}];
return (
<div>
<Button onClick={this.showExcelForm} color="success" size="lg" block>Export Excel</Button>
<CheckboxTable
data={this.state.data}
columns={columns}
className="-striped -highlight"
defaultPageSize={10}
ref={(r) => this.checkboxTable = r}
filterable
defaultFilterMethod={(filter, row) =>
String(row[filter.id]) === filter.value}
getTdProps={(state, row, column, instance) => ({
onClick: e => {
const r = row.original;
this.setState({ state, row: r, column, instance });
this.showRow();
}})}
{...checkboxProps}
/>
<div>
<Modal isOpen={this.state.showRow} toggle={this.showRow}>
<ModalHeader toggle={this.showRow}>{this.state.row.DebtCltRefNo}</ModalHeader>
<ModalBody>
DbtrDebtorType: {this.state.row.DbtrDebtorType}<br />
DebtType: {this.state.row.DebtType}<br />
</ModalBody>
<ModalFooter>
<Button color="danger" onClick={this.toggle}>Cancel</Button>
</ModalFooter>
</Modal>
</div>
<div>
<Modal isOpen={this.state.showExcelForm} toggle={this.showExcelForm}>
<ModalHeader toggle={this.showExcelForm}>Grant Client Access</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label for="clientEmail">Client's Email Address</Label>
<Input value={this.state.clientEmail} onChange={this.setClientEmail} type="email" name="clientEmail" id="clientEmail" placeholder="Client's Email Address" />
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button color="danger" onClick={this.showExcelForm}>Cancel</Button>
<Button color="success" onClick={this.exportExcel}>Submit</Button>
</ModalFooter>
</Modal>
</div>
</div>
);
}
}
const mapStateToProps = state => {
if (state.Odx.odx) {
let data = state.Odx.odx;
data = _.forEach([...data], (o) => o._id = chance.guid());
return {
odxData: data
};
} else {
return {};
}
};
const mapDispatchToProps = dispatch => ({
fetchOdx: () => dispatch(fetchOdx()),
});
export default connect(mapStateToProps, mapDispatchToProps)(TypeAHead);
Question
How do I make the table update after my form is submitted, so that no rows have a check box checked? Or in other words, how can I get the table to coincide with the state array which shows the rows that have been selected? In my case, after I update the state array to be an empty array, the selected rows are still selected in the UI.
Try useTable({columns, data, defaultColumn, autoResetPage: !skipPageReset, updateMyData, autoResetSelectedRows: false}, ...)
Adding the autoResetSelectedRows: false option worked for me.