Problem:
I have multiple textareas that can be navigated using arrow keys. The textareas are focused using ref.focus().
When the textareas are focused this way, the text selection is not cleared?
Screenshot
Expect
The text selection in the second textarea should be cleared when the first is clicked, or when the second textarea is focused again.
Code
import React, { useEffect, useRef, useState } from "react";
export const Test = () => {
const [editingBlock, setEditingBlock] = useState<Number | null>(null);
const textArea1Ref = useRef<HTMLTextAreaElement | null>(null);
const textArea2Ref = useRef<HTMLTextAreaElement | null>(null);
useEffect(() => {
// set 1 focus
if (editingBlock === 1 && textArea1Ref.current) {
textArea1Ref.current.focus();
// blur 2
if (textArea2Ref.current) {
textArea2Ref.current.blur();
}
// set 2 focus
} else if (editingBlock === 2 && textArea2Ref.current) {
textArea2Ref.current.focus();
// blur 1
if (textArea1Ref.current) {
textArea1Ref.current.blur();
}
}
}, [editingBlock]);
return (
<>
<div>
<textarea
ref={textArea1Ref}
value={"a really long string"}
onBlur={(e) => setEditingBlock(null)}
onKeyDown={(e) => {
if (e.key === "ArrowDown") setEditingBlock(2);
}}
onClick={(e) => setEditingBlock(1)}
/>
</div>
<div>
<textarea
ref={textArea2Ref}
value={"a really long string"}
onBlur={(e) => {
if (window.getSelection()) {
window.getSelection()!.removeAllRanges(); // doesn't work
}
setEditingBlock(null);
}}
onClick={(e) => setEditingBlock(2)}
/>
</div>
</>
);
};
The value will always be the same
You have set the value with a string, this will never change since it's a static value in the field.
If you would like to make the field changeable use some state, reducer or library to handle fields.
Here's an example
const [textArea1, setTextArea1] = useState<string>('');
...
return (
...
<textarea
ref={textArea1Ref}
value={textArea1}
onChange={(event) => setTextArea1(event.target.value)}
/>
...
)
To clear the field on blur you just need fire setTextArea1('').
Hacky solution
I still don't fully understand why this is happening, but found a workaround solution for now:
In the textarea's onBlur callback, simply use the following code:
if (textArea2Ref.current) {
// if there's a range selection
if (textArea2Ref.current.selectionStart !== textArea2Ref.current.selectionEnd) {
// reset the range
textArea2Ref.current.selectionStart = textArea2Ref.current.selectionEnd;
// blur the textarea again, because setting the selection focuses it?
textArea2Ref.current.blur();
}
}
Related
I am trying to implement a carousel using Flatlist or ScrollView (i tried both). The Carousel is compound of TextInputs.
What I wanna accomplish is the following:
There are 4 TextInputs.
when user type 6 digits on the first input, it automatically scrolls to the second and auto-focus the second input.
when the user type 6 digits on the second, it automatically scrolls to the third and auto-focus the third input.
etc...
the user must be enable to swipe back and change the TextInputs without being focused on other input as he types in something.
I already tried a switch case on the onScroll event of ScrollView.
It sounds like you need to use Flatlist's scrollToItem or scrollToOffset method and TextInput's focus method, and then listen to each text input as it changes.
...
handleInput = (event) => {
// calculate next position based on input text
const nextPosition = this.getNextPosition(event);
this.scrollViewRef.scrollToItem(nextPosition);
/**
* or if you can calculate the exact next pixel offset for the desired
* item based on its dimensions:
*
* this.scrollViewRef.scrollToOffset(nextPosition);
**/
const { name } = event.target.dataset;
const { text } = event.target;
if (name === 'textInput1' && text.length === 6) {
this.textInput2Ref.focus();
} else if (name === 'textInput2' && text.length === 6) {
this.textInput3Ref.focus();
} else if (/*...*/) {
/*...*/
}
}
...
render() {
<View>
<TextInput
data-name={'textInput1'}
ref={(ref) => this.textInput1Ref = ref}
onChange={this.handleInput}
/>
<TextInput
data-name={'textInput2'}
ref={(ref) => this.textInput2Ref = ref}
onChange={this.handleInput}
/>
{/*...TextInput3, 4, etc.*/}
<FlatList ref={(ref) => this.scrollViewRef = ref}/>
</View>
}
So, I have 100 mapped buttons from array, something like that
{
buttons.map((button, index) =>
<StyledGameButton
key={index}
value={button}
onClick={this.checkButton}>
{ button < 10 ? `0${button}` : button }
</StyledGameButton>
)
}
I want user to click all the buttons from 0 to 99, so when he click for example 0 the button should change color. I made function checking if he clicked correct button, and if yes then I am addind data-attr to that button (thats how I am changing the color of buttons):
checkButton = e => {
const buttonId = e.currentTarget.getAttribute('value');
const nextButton = this.state.currentButton === null ? 0 : this.state.currentButton + 1;
if (buttonId == nextButton){
this.setState({
currentButton: parseInt(buttonId),
});
if (this.state.currentButton === 99) {
this.gameEnd();
};
e.currentTarget.setAttribute('data-status', 'correct');
}
}
The problem is that I want to make reset button, that will change all buttons to 'unclicked' so I have to delete data-attr from all buttons on one click. How can I do this? Or is there a better solution to manage 'state' of single button without making 100 states?
100 checkboxes demo
use an Array to store the state would be fine.
const list = [...Array(100).keys()].map(x => ({ id: x }));
const App = () => {
const [selected, setSelected] = React.useState([]); // init with empty list
const onChangeHandler = id => () => { // pass index/identify params
selected.includes(id) // check whether been checked
? setSelected(selected.filter(x => x !== id)) // yes, remove
: setSelected([...selected, id]); // no, add
};
const onRemove = () => {
setSelected([]); // remove all, set to empty list
};
return (
<div className="App">
{list.map(item => (
<input
type="checkbox"
key={item.id}
checked={selected.includes(item.id)}
onChange={onChangeHandler(item.id)}
/>
))}
<br />
<button onClick={onRemove}>Remove all</button>
<div>{selected.join(",")}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>
I am trying to build an input field where it accepts time as an input here is my code
<InputMask
mask="99:99"
onBlur={handleOnBlur}
onChange={(e) => {
const text = e.target.value
setInputValue(text)
setValueValid(true)
const fixedText = text.replace(/:/g, '')
if (onChange) {
onChange({
...e,
target: {
...e.target,
value: fixedText,
},
})
}
}}
//value={inputValue}
{...rest}
>
{(inputProps) => (
<Tooltip
open={!valueValid}
placement="bottom-start"
title="Ops Wrong Time Format!"
>
<StyledInput
{...inputProps}
autoFocus={rest.autoFocus}
className={rest.className}
onKeyDown={(e) => {
if (e.keyCode === 13) {
checkTimeFormat(inputValue)
if (valueValid) {
if (rest.onBlur) rest.onBlur(e)
e.target.blur()
if (onSubmit) {
const fixedText = inputValue.replace(/:/g, '')
onSubmit({
...e,
target: {
...e.target,
value: fixedText,
},
}, fixedText)
}
}
}
}}
/>
</Tooltip>
)}
</InputMask>
)
I am trying to use InputMask to set the correct format and to make sure only numbers are used as an input. I would like to display a tooltip if the time is not an acceptable time (eg/ 75:89, 99:78...etc) So everything seems to be working and I am able to see the tooltip if i type in something invalid, however, I am not able to get rid of a strange behavior where the cursor always ends up at the end of the input box after any sort of change... I have searched online for an answer but it seems that nothing was of much help. Note that if I remove the tooltip component wrapping my custom input (StyledInput) then everything turns back to normal (cursor stops where the user is currently modifying).
At first I thought this was a rendering issue so I tried commenting out the hooks
setInputValue(text)
setValueValid(true)
and even tried to remove the toggle for the tooltip
open={!valueValid}
but it seems that the tooltip is what's causing the issue... does anyone have any idea how to solve this? Any recommendations or reference to a doc would be of great help!
Here is the sandbox code:
https://codesandbox.io/s/wandering-frost-0dy78
If you put the Tooltip around the entire InputMask element (rather than around the input inside InputMask) it works fine. I haven't taken the time to fully understand why the Tooltip causes issues in the previous location (which would require digging deeper into the internals of InputMask to understand how it manages cursor position), but it isn't surprising to me that it causes issues there.
Below is a modified version of your sandbox:
import React, { useState, useEffect } from "react";
import Tooltip from "#material-ui/core/Tooltip";
import InputMask from "react-input-mask";
import "./styles.css";
export default function App() {
const [inputValue, setInputValue] = useState("value");
const [valueValid, setValueValid] = useState(true);
const checkTimeFormat = time => {
const [hour, minute] = time.split(":");
if (parseInt(hour, 10) > 23 || parseInt(minute, 10) > 59) {
setValueValid(false);
}
};
return (
<Tooltip
open={!valueValid}
placement="bottom-start"
title="Ops Wrong Time Format"
>
<InputMask
mask="99:99"
//beforeMaskedValueChange={beforeMaskedValueChange}
onChange={e => {
const text = e.target.value;
setInputValue(text);
setValueValid(true);
const fixedText = text.replace(/:/g, "");
}}
value={inputValue}
//{...rest}
>
{inputProps => (
<input
{...inputProps}
//autoFocus={rest.autoFocus}
//className={rest.className}
onSubmit={checkTimeFormat(inputValue)}
onKeyDown={e => {
if (e.keyCode === 13) {
if (valueValid) {
console.log("Value is valid");
}
}
}}
/>
)}
</InputMask>
</Tooltip>
);
}
First I'd like to say that I have a solution, but it seems ridiculous and there has to be a better way. So I'll start with my ridiculous solution.
I have a form that shows what page I'm on. I want the user to be able to enter a page and go to that page on enter.
My component looks something like:
const BookPagination = (props) => {
const history = useHistory()
const { items, bookTitle, currentPageId } = props
const [currentPage, setCurrentPage] = useState(currentPageId)
const totalPages = items.length
const ENTER = 13
// items is just a bunch of arrays with info about each page (each of which have an id)
const handlePageInput = (e) => {
if (e.keyCode === ENTER) {
e.preventDefault()
history.push({
pathname: `/books/${bookTitle}/id/${items[currentPage - 1].id}` //page id's start # 1
})
} else {
setCurrentPage(e.target.value)
}
}
return (
<div className={'.pageSelector'}>
<span>
<input type="number"
min="1"
max={totalPages}
value={currentPage}
onKeyDown={handlePageInput}
onChange={handlePageInput}
/>
of {totalPages}
</span>
</div>
)
}
export default BookPagination
So this works... If I had a bookmark on page 20 of Moby Dick, I can go to /books/mobydick/id/20 and the form will start with 20 in the input box and it will navigate me anywhere I want (I've excluded code for bounds checking).
My problem:
This thing renders every single time I make a change to the input.. If I erase the 20 and put in 19 and hit enter.. that's 6 renders
Initial, 20->2, 2->0, 0->1, 1->19, render new page This is because my state is updating every time.. This is the only way I could figure out how to start with the current page number on page load and edit it... other tries wouldn't let me update the value if I started with one..
How do I fix this?
I think what I'm looking for is React.createRef()
but I've tried the following:
Add value={currentPageId} and ref={inputElement} to the form
Add const inputElement = React.createRef() to the top of the component
Remove the state variable, currentPage
Edit handlePageInput to the following:
const handlePageInput = (e) => {
if (e.keyCode === ENTER) {
e.preventDefault()
history.push({
pathname: `/books/${bookTitle}/id/${items[inputElement.current.value - 1].id}`
})
} else {
inputElement.current.value = e.target.value
}
}
but it won't let me erase the value of the input box... clicking up and down within the input box doesn't do anything either. If I entered on page 200, I can't change it to any other page.
Using this method, if I remove value={currentPageId} and leave the form without a value attribute,
updating my history.push pathname to .../id/${items[inputElement.current.valueAsNumber - 1].id}
I can enter in any page I want, and navigate correctly... my problem is that when I come to a page, there's nothing in the input box to start... which is how I got to updating it with useState() in the first place...
What am I missing?
You can try an Uncontrolled Component with a default value
Remove the onChange from your input and leave the onKeyDown. Inside your handlePageInput function get the current value from a ref.
Something like this:
const BookPagination = (props) => {
const history = useHistory()
const { items, bookTitle, currentPageId } = props
const totalPages = items.length
const ENTER = 13
const inputElement = useRef(null);
// items is just a bunch of arrays with info about each page (each of which have an id)
const handlePageInput = (e) => {
if (e.keyCode === ENTER) {
e.preventDefault()
const currentValue = inputElement.current.value;
history.push({
pathname: `/books/${bookTitle}/id/${items[currentValue - 1].id}` //page id's start # 1
})
}
}
return (
<div className={'.pageSelector'}>
<span>
<input type="number"
ref={inputElement}
min="1"
max={totalPages}
defaltValue={currentPageId}
onKeyDown={handlePageInput}
/>
of {totalPages}
</span>
</div>
)
}
export default BookPagination
I have two inputs.
Email Address
First Name
On initial load, first name is hidden.
Clicking on the Email address input, will hide a div and show the first name input.
It will also hide a div above the inputs and the inputs will fill that space.
Now, the functionality I am looking for is after a user clicks the email address input, and then the user clicks the first name the first name should not disappear and the div to return.
To do so, at the moment, I have added a lodash debounce delay on the onBlur event listener to wait to see if a user focuses on the first name input.
I have a state variable onBlurWait that defaults to false and gets updated to true when a user focuses on the first name input.
Focusing on the first name input, calls the onBlur event listener to check the value of the onBlurWait state. This listener for now, just returns a console.log or onBlurWait.
The onBlur on the email address input seems to get called before the onFocus of the first name input. So it seems that the onBlurWait state does not get updated so the console.log in the onBlur event listener will return false.
Here is a CodeSandbox that will help you play around with it, so you can hopefully understand what I am referencing above.
Below is my Component
import React, { useState } from "react";
import ReactDOM from "react-dom";
import _ from "lodash";
import InputControl from "./InputControl";
import "./styles.css";
const App = () => {
const [hideContent, setHideContent] = useState(false);
const [emailSignUpRef, setEmailSignUpRef] = useState(null);
const [firstNameSignUpRef, setFirstNameEmailSignUpRef] = useState(null);
const [onBlurWait, setOnBlurWait] = useState(false);
let registerEmailInput = null;
let firstNameInput = null;
// If you click on email address then on first name, I would expect the console.log below to return true
// due to when a user focuses on the first name input the handleInputFocus function gets called
// inside that function, setOnBlurWait(true); is ran updating the value of onBlurWait to true
// But it seems like it has not actually been updated
// Now when you click on email address to first name, back to email address and first name again, the console log now returns true?
const waitOnUpdateState = _.debounce(() => console.log(onBlurWait), 2000);
const hideContentAfterState = _.debounce(
() =>
setHideContent(
emailSignUpRef.length > 0 || firstNameSignUpRef.length > 0 || onBlurWait
),
2000
);
const handleOnFocus = event => {
setEmailSignUpRef(registerEmailInput.value);
setFirstNameEmailSignUpRef(firstNameInput.value);
setHideContent(true);
};
const handleOnInput = () => {
setEmailSignUpRef(registerEmailInput.value);
setFirstNameEmailSignUpRef(firstNameInput.value);
};
const handleInputFocus = () => {
console.log("clicked on first name, onBlurWait should be set as true");
setOnBlurWait(true);
};
const handleOnBlur = () => {
console.log("clicked away from email address");
// waitOnUpdateState is just a test function to return a console log, hideContentAfterState is what is going to be used
waitOnUpdateState();
hideContentAfterState();
};
return (
<div className="App">
<div className={hideContent ? "hidden" : "visible"} />
<InputControl
autoComplete="off"
refProp={input => {
registerEmailInput = input;
}}
onInput={handleOnInput}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
type="text"
name="emailAddressRegister"
placeholder="Email address"
label="Email Address"
/>
<InputControl
className={
!hideContent ? "InputControl--hidden" : "InputControl--visible"
}
autoComplete="off"
refProp={input => {
firstNameInput = input;
}}
onInput={handleOnInput}
onFocus={handleInputFocus}
type="text"
name="firstName"
placeholder="First Name"
label="First Name"
/>
<input type="submit" value="Submit" />
</div>
);
};
export default App;
Consider making it simpler, you are overthinking it.
onFocus and onBlur both receive relatedTarget as one of the properties in their SyntheticEvent. In case of onBlur, relatedTarget is the DOM element for which the focus is leaving the target.
What you could do is handle the blur event and compare relatedTarget property of the event to the input refs. Then you will be able to figure out whether focus is leaving for another form element or some other DOM element on the page.