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.
Related
I am facing a weird situation while working with React. We have a custom control in Dynamics 365 in what we use a dynamic inputs generated via Add button and removing with Delete button. And since the labels of the inputs include "1st", "2nd" etc., we need to re-render the correct labels.
Example: If there are up to 3 inputs "1st Renewal", "2nd Renewal" and "3rd Renewal", when user deletes the 2nd input, the 3rd should refresh to "2nd Renewal".
Behind the scenes, we work with an array of numbers, from where we generate Fluent UI elements.
const getIndexationPeriodPrefixFor = (numberPrefix: number) => {
return `${getNumberPrefix(numberPrefix)} Renewal`;
}
const generateDynamicContractedValues = () : IContractedValue[] => {
let dynamicValues = new Array<IContractedValue>();
props.contracted_prices.forEach((value, index) => {
dynamicValues.push(new ContractedValue(getIndexationPeriodPrefixFor(index + 1), value));
});
if (dynamicValues.length == 0) {
// Create initial 1st Renewal input for new Contracted indexation type.
dynamicValues.push(new ContractedValue("1st Renewal", null));
}
return dynamicValues;
};
const dynamicContractedValuesElements =
dynamicContractedValues.map((value, index) => {
return {
renewalPeriod: value.renewalPeriod,
renewalFragment: <>
<TooltipHost content="Renewal price agreed with the purchaser for the renewal period, excluded from indexation.">
<TextField required={index == 0} disabled={shouldBeInputDisabled}
type="number" id={`Input-Renewal-${index+1}`}
value={value.renewalPrice?.toPrecision()}
onKeyDown={e => ValidationService.PreventMinusSign(e)}
onChange={e => updateRenewal(e, index)}
suffix={props.currency.name}></TextField></TooltipHost><ActionButton id={`Btn-Remove-Renewal-${index+1}`} iconProps={{ iconName: "Delete" }} onClick={() => { removeRenewalLine(index) }} disabled={index < 1} hidden={shouldBeInputDisabled || index < 1}></ActionButton>
{doesRenewalPriceHaveUnexpectedValue && unexpectedPriceWarning}</>
}
})
const removeRenewalLine = (index: number) => {
let newContractedPrices = [...contractedPrices];
newContractedPrices.splice(index, 1);
setContractedPrices(newContractedPrices);
let newDynamics = generateDynamicContractedValues();
// We want to reload the number prefixes, when the user deletes a contracted period.
// Example: When user deletes 2nd Renewal, the 3rd Renewal should change to 2nd Renewal.
setDynamicContractedValues(newDynamics);
}
We generate the UI elements via the contracted_prices array with generateDynamicContractedValues method.
The problem is with the line
<ActionButton id={`Btn-Remove-Renewal-${index+1}`} iconProps={{ iconName: "Delete" }} onClick={() => { removeRenewalLine(index) }}
Specifically with the removeRenewalLine(index) method - it's not causing any change in UI for the first click of the user. When you click delete, the array behind the control is changed, but the input is not being deleted and then the prefixes recalculated -> just for the first click. From the second click it works just as I wanted.
Any ideas please? Thank you
Store the array in a useState. If it's just a variable, it's not reactive. Thus the DOM will not react to a change of the variable.
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}
...
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>
}
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();
}
}
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>
);
}