Cursor moves to end of input when tooltip in react - javascript

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>
);
}

Related

How to remove value from state?

I need simple functionality: show Button when value is more than 0. And for this I use code below.
I created some Text Fields with similar states (4) and I don't understand why only in 3rd this didn't work.
My code:
export default function TextFields() {
...
const [showButton3, setShowButton3] = useState("");
...
const handleChange = (event) => {
setShowButton3(event.target.value);
console.log("value is:", event.target.value);
};
return (
<InputOutlined
type={"text"}
id={"text3"}
name={"text3"}
value={showButton3}
onChange={handleChange}
leftElement={
<Img
width={36}
height={36}
radius={12}
src={
"https://images.unsplash.com/photo-1665174271625-178021f8b1a5?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1160&q=80"
}
/>
}
rightElement={
showButton3 ? (
<IconButton
icon={<MdClose size={24} />}
variant={"surface"}
type="reset"
onClick={() => setShowButton3("")}
></IconButton>
) : null
}
>
Your name
</InputOutlined>
)}
I have checked Component, here I add some text
And when I want to clear value, I get this
You can see, value is cleared. But I still see it in my input. How to fix that? Or maybe I doing something wrong?
Proof:
This is very similar Components. I changed id, but I don't understand why value isn't removed. Maybe I need to use useRef or useId. But I have 4 different inputs and only 1 have this issue.

React Mui TextField for currency input - how to permit enter digits only in iPhone Safari browser

I have a Mui TextField component for currency input and I made it to show numeric keyboard only in Safari Browser.
But when the user tries to paste literal string into the field, I'd like to prevent it and make it sure that allows enter currency number inputs only.
import { TextField } from '#mui/material';
export default function CustomCurrencyTestScreen(props){
const [amount, setAmount] = useState('');
const handleChange = e => {
setAmount(e.target.value);
}
const handleBlur = e => {
//some validation functionalities
}
return (
<TextField
size="small"
id="amount"
name="amount"
onChange={handleChange}
onBlur={handleBlur}
inputProps={{
style:{
fontSize:14,
}
inputMode:'numeric',
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
$
</InputAdornment>
),
endAdornment:(
<InputAdornment position='end'>
{
Boolean(amount) &&
<CancelRounded size="small" color="grey" sx={{padding:0}} onClick={()=>
setAmount('')}/>
}
</InputAdornment>
),
}}
/>
);
}
It shows numeric keyboard successfully in Safari browser, however, when you paste the literal string into the field, the field shows it.
How can I prevent user not to input or paste other characters than numbers(float value also)?
I tried with CurrencyInput and IMaskInput, but there's a problem in implementing startAdornment and endAdornment, so I don't prefer it.
Is there any solution to implement Currency Input with Adornment in Mui, working correctly in Safari Browser?
I don't think there's any way to prevent an actual paste, but you could use a useEffect on amount, and strip out all non-numeric characters:
useEffect(() => {
function hasNonNumeric(s) {
// return true if has non numeric
}
function stripNonNumeric(s) {
// logic here that returns a string with non-numeric chars stripped
}
if(hasNonNumeric(amount)) {
setAmount(stripNonNumeric(amount))
}
}, [amount])
That being said, I probably wouldn't recommend doing it, I hate when websites mess with stuff I paste. Rather, I'd recommend having validation, and showing an error if it contains non-numeric allowing the user to fix it on their terms.
You can add onPaste handler to your TextField component so that you can implement your logic within this function.
const handleOnPaste = e => {
//do whatever you want with the pasted value
//add your logic here
}
<TextField
...
onPaste={handleOnPaste}
...

Clicking up arrow on a react IMask number input breaks on safari

I have an integer input using react-imask, which works fine on google chrome. However on safari, when I click the arrows without focusing on the input first, (say from 10 to 11), it will add the new value to the start of the input i.e 1110.
These are my props being passed in:
<InputInteger
required={true}
className={"w-[65px] text-center"}
min={0}
value={updatedStock}
onChange={(e) => setUpdatedStock(parseInt(e.target.value))}
/>
and this is the InputInteger component:
<IMaskInput
{...props}
mask={Number}
scale={0}
lazy={false}
min={props.min ?? 0}
max={props.max ?? 9999}
radix={"."}
signed={false}
mapToRadix={[","]}
inputMode={"numeric"}
value={props?.value?.toString()}
onAccept={(value) => {
if (!props.onChange) return;
if (value === "0" && !props?.required) value = "";
const payload = { target: { value: value } };
return props.onChange(payload);
}}
onChange={undefined}
/>
As mentioned, this only seems to be an issue on safari.

React-input-mask inability to paste correct phone

My input has following code
<InputMask
id='phone'
name='phone'
type='tel'
value={this.state.phone}
mask='+63(\999) 999-99-99'
maskChar='X'
onChange={(e: SyntheticEvent<HTMLInputElement>): void => {
this.setState({
phone: e.currentTarget.value.replace(/[^\d.+]/g, ''),
});
}}
onBlur={this.validateInput}
onPaste={(e) => this.handlePaste(e)}
className='form-control'
placeholder='Your Phone'
/>
here +639 digits that can not be deleted. But when I try to paste phone number I get incorrect phone.
For example i paste +639055943784
and get +63(963) 963-90-55 which is not desired result.
My paste handler is
handlePaste = (e) => {
this.setState({phone: e.clipboardData.getData("Text")})
}
Try that one
handlePaste = (e) => {
const number = e.clipboardData.getData("Text");
const formattedNumber = number.replace(/639+/i, '')
this.setState({phone: formattedNumber})
}
There also could be edge cases when pasted value will contain another 639s, but I think you can fetch it independently
I created a package that exposes an input component that displays a masked value according to the mask it receives.
The mask will change keeping the cursor at the correct position (even if you change part of the value in the middle of the input, paste some characters, or delete a part of it, and even if the mask changes).
I created a live demo for your use case, using the mask +63(9XX) XXX-XX-XX and defining custom mask rules that uses X as the user provided characters (instead of 9, because you want a digit 9 static in the mask) and associated it with a regex that only accepts a digit/number:
https://codesandbox.io/s/react-phone-mask-70fwc?file=/src/index.js:123-141
import React from "react";
import { MaskedInput } from "react-hook-mask";
const mask = "+63(9XX) XXX-XX-XX";
const maskGenerator = {
rules: new Map([["X", /\d/]]),
generateMask: () => mask
};
const PhoneMaskedInput = () => {
const [value, setValue] = React.useState("");
return (
<div>
<p>Phone Number:</p>
<MaskedInput
maskGenerator={maskGenerator}
value={value}
onChange={setValue}
/>
<p>Value: {value ? "+639" + value : undefined}</p>
</div>
);
};
You can see a demo with several examples at:
https://lucasbasquerotto.github.io/react-masked-input
To install the package: npm i react-hook-mask
This component wraps a default input, but the package also expose hooks to make you able to use it with any kind of component as well as the option to use dynamic masks and dynamic rules (as in the example above).
You can see more information about the package at:
https://www.npmjs.com/package/react-hook-mask

Can't clear text selection from textarea

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();
}
}

Categories