Scroll with keyboard's up/down arrows keys in React? - javascript

I have a custom listbox, a div that contains a vertical list of other div children. I want to add an up/down arrows keys navigation to change which child is currently selected.
So when I click the first item and press the down arrow key, it should allow me to select the second item (following item). And if I click the up arrow key, it should select back the first item (previous item).
const renderInboxSummary = targetDetailsData.map((todo, index) => {
const hasNewMessageInd = todo.hasNewMessageInd;
return (
<div onClick={() => this.handleClick(targetDetailsData, todo.aprReference, index)}>
<div>
{todo.aprRecordUserName}
</div>
<div>
{todo.aprBranchCode}
</div>
<div>
{todo.aprScreeName}
</div>
</div>
);
});
Every div has a click event handler this.handleClick(targetDetailsData, todo.aprReference, index).

This can be done by using a ref in ReactJS and then adding an event listener for the keydown event and then moving the focus to the next or previous sibling.
Notes
I add tabindex attribute to each div to allow them to be focused upon
I use a ref on the wrapping element to listen for keydown
I check keycode for up/down to move to next/previous sibling
I believe the keycode for up/down on a full size keyboard is different, but I don't have one to test.
Solution
To test the demo, click on any div and then use up/down arrows
const { Component } = React;
class App extends Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount() {
this.moveFocus();
}
moveFocus() {
const node = this.myRef.current;
node.addEventListener('keydown', function(e) {
const active = document.activeElement;
if(e.keyCode === 40 && active.nextSibling) {
active.nextSibling.focus();
}
if(e.keyCode === 38 && active.previousSibling) {
active.previousSibling.focus();
}
});
}
render() {
return (
<div ref={this.myRef}>
<div tabindex="0">First</div>
<div tabindex="1">Second</div>
<div tabindex="2">Third</div>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
div:focus {
color: red;
}
<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>
<div id="root"></div>
Documentation
https://reactjs.org/docs/refs-and-the-dom.html
https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex

Related

React hook's state using event to target a specific div

I'm new to coding and react and i'm trying to update x4 divs so when the user clicks it will update the content inside. Currently i have the code updating however it updates all 4 div's instead of the specific div which is clicked.
I know this will be controlled by using event however slightly confused what exactly i need to include.
Any help in the correct direction would be greatly appreciated. Current code runs fine but content within all 4 divs update rather than the specific one which is clicked
const [isActive, setIsActive] = useState(false)
const toggleContent = e => {
e.preventDefault()
setIsActive(!isActive)
}
Example div i'm trying to update when user clicks (currently i have x4 others which are the same logic but different HTML content. Currently when any of the 4 buttons are clicked, all x4 divs content change however i only want it to change the specific div which contained the button that was clicked
{!isActive ?
<article>
<h1>BEFORE CLICK TEXT</h1>
<a onClick={toggleClass}>BUTTON</a>
</article>
:
<article>
<p>AFTER CLICK TEXT</p>
<a onClick={toggleClass}>Back</a>
</article>
}
maintain activeIndex instead of just active boolean. In render method, based on activeIndex and index decide the active attributes for div.
Try the snippet.
const content = ['abc', 'def', 'ghi', 'jkl'];
const Component = () => {
const [activeIndex, setActiveIndex] = React.useState(-1);
return (
<div>
{content.map((item, index) =>
<div key={item}
style={{color: activeIndex === index ? "red" : "black" }}
onClick={() => setActiveIndex(index)}> {item}
</div>)}
</div>
)
}
ReactDOM.render(<Component />, document.getElementById('app'));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="app"> </div>

ReactJS class component render array.map using contenteditable elements

I'm having an interesting issue that I cannot debug.
Goal
On a class component, inside of render function, iterate over an array of objects from state using this.state.items.map((item, index) => {}) and return a contentEditable paragraph element.
On each contentEditable paragraph element, listen for the onKeyUp event. If the key being used from e.which is the enter (13) key, add a new item to this.state.items using the index of the element that was keyed, in order to insert a new element after that index using splice.
Seeing Expected Result?
No. The newly added item is instead being put at the end of the loop when it is being rendered.
Example situation and steps to reproduce:
Type "test1" into the first P element
Hit enter (a new P element is created and focused)
Type "test2" into this second, newly created, P element
Refocus on the first P element, either by shift+tab or clicking
Hit enter
See observed results: a new P element is created and focused, but it is at the end of the list and not where it is intended to be, which is between the "test1" and "test2" P elements
Here is the code that I have so far:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
items: [this.paragraphTemplate()]
}
}
render() {
return (
<section>
<div>
{this.state.items.map((item, index) => {
return <p ref={item.ref}
key={index}
contentEditable
suppressContentEditableWarning
onKeyUp={e => this.handleParagraphKeyUp(e, index, item)}></p>
})}
</div>
</section>
)
}
handleParagraphKeyUp = (e, index, item) => {
if (e.which === 13) {
let addition = this.paragraphTemplate()
this.setState(state => {
state.items.splice(index + 1, 0, addition)
return {
blocks: state.items
}
}, () => {
addition.ref.current.focus()
/* clear out the br and div elements that the browser might auto-add on "enter" from the element that was focused when the "enter" key was used */
this.state.items[index].ref.current.innerHTML = this.state.items[index].ref.current.innerHTML.replace(/<br\s*[\/]?>/gi, '').replace(/<[\/]?div>/gi, '')
})
return false
}
}
paragraphTemplate = () => {
return {
ref: React.createRef()
}
}
}
export default MyComponent
Here is a jsfiddle with the code from above.
If you take the above steps, you will see the issue that I am having.
Let me know if you require any further information, thanks in advance!
P.S. Please let me know if there any improvements that I can make to the code. I have been working in React for a short amount of time, and would love any feedback on how to make it better/cleaner.
UPDATED
Added key={index} to the P element. Note: this does not reflect any answers, it was merely added to stay in line with ReactJS list rendering.
to render a list of items, React needs key to keep track of the element
see this: https://reactjs.org/docs/lists-and-keys.html
here is your updated fiddle that working..
<p ref={item.ref}
key={item.id}
contentEditable
suppressContentEditableWarning
onKeyUp={e => this.handleParagraphKeyUp(e,

Problems when updating state in React

Please, view my js fiddle where I've illustrated the problem
Here's my js fiddle:
https://jsfiddle.net/jajabya/fb93f7b0/
My goad is to get an input tag where special words (like dates or users' names could be highlighted by wrapping in span tags)
There must be a problem with div, because when I use input field instead everything works fine.
My problem is that I can't make the caret appear in the right place
Every time when the state updates in onInput
onInput(event) {
this.setState({
html: event.target.innerText.toUpperCase()
});
}
the caret rolls back to the beginning
My idea is to save the current caret position in the state, and set it back via a ref in componentDidUpdate() (since ref doesn't rerender the component).
Note: This is a prototype idea, that I've never battle tested, so use with caution.
The caret position code was take from this answers:
Code for getting the caret position
Code for setting caret position
class Editable extends React.Component {
componentDidUpdate(prev) {
const { position } = this.props;
if(position !== prev.position && this.ce.childNodes.length) {
const range = document.createRange();
const sel = window.getSelection();
range.setStart(this.ce.childNodes[0], position);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
render() {
return (
<div
contentEditable
className={this.props.className}
onInput={this.props.onInput}
ref={ce => this.ce = ce}
suppressContentEditableWarning>
{this.props.html}
</div>
);
}
}
class App extends React.Component {
state = {
html: 'Text',
caret: 0
};
handleInput = (event) => this.setState({
html: event.target.innerText.toUpperCase(),
position: window.getSelection().getRangeAt(0).startOffset
});
render() {
return (
<Editable
{...this.state}
className="Editable"
onInput={this.handleInput} />
);
}
}
ReactDOM.render(
<App />,
demo
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="demo"></div>

React onClick not firing inside map generated list

I have a simple list being displayed on the return value of a fetch call. I have some functions firing on selection of an item, but for the life of me, I can't fire the onClick event. I have events bound outside the map that work just fine (the onKeyUp and down), but the onClick while inside the map does not work. Not sure where to go from there.
handleClick() {
// won't fire
console.log('test')
}
render() {
return (
<div className="autocomplete">
<input type="text" placeholder={this.props.fieldName} onKeyDown={this.handleKeyDown} onKeyUp={this.handleKeyUp} onClick={this.handleClick} />
<div className="autocomplete__list" onClick={this.handleClick}>
<ul>
{this.state.list.map((item, index) => <li key={index} className={this.checkActive(index)} onClick={this.handleClick}>{item.firstName}</li>)}
</ul>
</div>
</div>
)
}
No errors thrown, the method does nothing when clicking those fields. If it matters at all, the "list" is absolutely positioned. And here is the constructor
constructor(props) {
super(props)
this.state = {
name: '',
list: [],
cursor: 0
}
this.handleKeyDown = this.handleKeyDown.bind(this)
this.handleKeyUp = this.handleKeyUp.bind(this)
this.checkActive = this.checkActive.bind(this)
this.handleClick = this.handleClick.bind(this)
}
The issue seems to be either:
You have the same handler on a parent element that wraps all of your li's.. Usually the click event would bubble upwards from the children to the parent so this is unlikely.
Your li's are a lower z-index than the parent element. Which means that the li elements lay under an element. so a click event only happens on the element on the top.
Try adding this to your css
.autocomplete__list li {
z-index: 100;
}

React - ul with onBlur event is preventing onClick from firing on li

I've created a mobile dropdown menu that toggles open and closed based on state. Once it's open, I would like the user to be able to close the dropdown by clicking anywhere outside the ul.
I'm setting the tabIndex attribute on the ul to 0, which gives the ul "focus". I've also added an onBlur event to the ul that triggers the state change (dropdownExpanded = false) that hides the ul.
<ul tabIndex="0" onBlur={this.hideDropdownMenu}>
<li onClick={this.handlePageNavigation}>Page 1</li>
<li onClick={this.handlePageNavigation}>Page 2</li>
<li onClick={this.handlePageNavigation}>Page 3</li>
</ul>
However, when I implement this fix, the onClick events that I have on each li element fail to fire.
I know something is going on with the event bubbling, but I am at a lose as to how to fix it. Can anyone help?
NOTE:
I know you can create a transparent div below the ul that spans the entire viewport and then just add an onClick even to that div that will change the state, but I read about this tabIndex/focus solution on Stack Overflow and I'd really like to get it working.
Here is a more complete view of the code (the dropdown is for users to select their home country, which updates the ui):
const mapStateToProps = (state) => {
return {
lang: state.lang
}
}
const mapDispatchToProps = (dispatch) => {
return { actions: bindActionCreators({ changeLang }, dispatch) };
}
class Header extends Component {
constructor() {
super();
this.state = {
langListExpanded: false
}
this.handleLangChange = this.handleLangChange.bind(this);
this.toggleLangMenu = this.toggleLangMenu.bind(this);
this.hideLangMenu = this.hideLangMenu.bind(this);
}
toggleLangMenu (){
this.setState({
langListExpanded: !this.state.langListExpanded
});
}
hideLangMenu (){
this.setState({
langListExpanded: false
});
}
handleLangChange(e) {
let newLang = e.target.attributes['0'].value;
let urlSegment = window.location.pathname.substr(7);
// blast it to shared state
this.props.actions.changeLang( newLang );
// update browser route to change locale, but stay where they are at
browserHistory.push(`/${ newLang }/${ urlSegment }`);
//close dropdown menu
this.hideLangMenu();
}
compileAvailableLocales() {
let locales = availableLangs;
let selectedLang = this.props.lang;
let markup = _.map(locales, (loc) => {
let readableName = language[ selectedLang ].navigation.locales[ loc ];
return (
<li
key={ loc }
value={ loc }
onMouseDown={ this.handleLangChange }>
{ readableName }
</li>
);
});
return markup;
}
render() {
let localeMarkup = this.compileAvailableLocales();
return (
<section className="header row expanded">
< Navigation />
<section className="locale_selection">
<button
className="btn-locale"
onClick={this.toggleLangMenu}>
{this.props.lang}
</button>
<ul
className={this.state.langListExpanded ? "mobile_open" : " "}
value={ this.props.lang }
tabIndex="0"
onBlur={this.hideLangMenu}>
>
{ localeMarkup }
</ul>
</section>
</section>
)
}
}
Try using onMouseDown instead of onClick.
The point is the onBlur is triggering a re-render which seems to lead the browser to do not follow up with the onClick: https://github.com/facebook/react/issues/4210
But if you check the onBlur event you can find some info about what's happening, event.relatedTarget is populated and you can use these info to detect when the onBlur is actually triggered by the onClick and chain whatever you need to do.
I just ran into this with an array of breadcrumb links, where an onBlur handler was causing a rerender, preventing the link click from working. The actual problem was that react was regenerating the link elements every time, so when it rerendered, it swapped the link out from under the mouse, which caused the browser to ignore the click.
The fix was to add key properties to my links, so that react would reuse the same DOM elements.
<ol>
{props.breadcrumbs.map(crumb => (
<li key={crumb.url}>
<Link to={crumb.url} >
{crumb.label}
</Link>
</li>
))}
</ol>

Categories

Resources