adobe / react-spectrum

A collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.
https://react-spectrum.adobe.com
Apache License 2.0
12.9k stars 1.12k forks source link

Input onChange does not expose native event #3247

Open joepuzzo opened 2 years ago

joepuzzo commented 2 years ago

🐛 Bug Report

Input onChange does not expose native event. This is bad as it does not enable access to the native event which ultimately allows for complex cursor tracing when formatting text fields.

🤔 Expected Behavior

https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/textfield/src/useTextField.ts#L141

This ^^^ should be passing e instead of e.target.value. As this would be huge breaking change. I suggest you simply pass e as a second parameter so users can use it.

😯 Current Behavior

Native e event is not passed

💁 Possible Solution

I suggest you simply pass e as a second parameter so users can use it.

        onChange: (e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value, e),

🔦 Context

I am unable to do cursor tracking to format user input.

https://teslamotors.github.io/informed/?path=/story/formatting--number-formatter

Above is example of where compex cursor tracking is used when formatting input fields.

LFDanLu commented 2 years ago

Mind expanding on what cursor tracking is being done in the example you linked? Could you use onInput or onBeforeInput instead?

EDIT: ah is it to calculate where to place the cursor when a comma is added/removed?

devongovett commented 2 years ago

This has been discussed before: #1860.

TL;DR: There are two reasons:

a) Consistency. All of our components expose a value as part of the onChange event, not a native event. In some components, there is no native event that would make sense. b) Cross platform support. Our stately hooks are designed to work across platforms with the same API, e.g. web and react native.

But, that's not to say it's impossible to do what you want. With React Aria, you control the DOM structure, so it's easy to override props we return from our hooks where needed. In this example, we chain React Aria's handler with our own.

import {useTextField} from '@react-aria/textfield';
import {chain} from '@react-aria/utils';

function MyInput(props) {
  let {inputProps} = useTextField(props);
  let myOnChange = e => { /* ... */ };
  return <input {...inputProps} onChange={chain(inputProps.onChange, myOnChange)} />
}
joepuzzo commented 2 years ago

@LFDanLu Yes it is for setting the position of cursor when the commas are added. @devongovett Hmm so the hook basically turns a basic DOM input into a react spectrum input ? It seems to me I would be missing out on all the goodies in here tho .. https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/textfield/src/TextField.tsx

joepuzzo commented 2 years ago

Specifically the TextFieldBase... that has a bunch of things I want to keep. going the custom route just so I can get the native e event seems to throw half of the benefits away ? correct me if im wrong.

devongovett commented 2 years ago

Ah, didn't realize you were using React Spectrum - assumed you were using the React Aria hooks with your own design.

In that case, you might be able to do what you want with a ref? TextField's ref exposes a getInputElement() function.

let ref = useRef();
let onChange = () => {
  let input = ref.current.getInputElement();
  // do stuff.
};
<TextField onChange={onChange} />
joepuzzo commented 2 years ago

Ahh a hack but might work non the less! Thanks!

PS: I might make a wrapper library or even use react-spectrum as the main design system for my form docs. Its one of the better design systems I have ever used!

joepuzzo commented 2 years ago

Ok so now im having issues because I think you guys are doing some fancyness with the ref here https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/textfield/src/TextFieldBase.tsx

Im getting

inputRef.current.setSelectionRange is not a function
joepuzzo commented 2 years ago

Internally to my form library i check if they user has passed a DOM ref and call setSelectionRange .. problem is I cant go changing my library to call a getInputElement as that is custom logic to react-spectrum. I wish I could just access the real ref 🤔 any ideas?

joepuzzo commented 2 years ago

A really easy solve for this is to allow the user to pass their own inputRef

   let inputRef = useRef<HTMLInputElement>();
   inputRef = userInputRef ?? inputRef;

https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/textfield/src/TextField.tsx#L22

I do same thing here

https://github.com/teslamotors/informed/blob/master/src/hooks/useField.js#L161

joepuzzo commented 2 years ago

Following up with this @devongovett as its still a blocker for advanced cursor positioning when formatting fields

mjr commented 6 months ago

Is there a way to do this check in the onChange of the confirmPassword field using react-aria-components components?

'use client'

import { useFormState } from 'react-dom'
import { Button, FieldError, Form, Input, Label, TextField } from 'react-aria-components'
import { createUser } from './actions'

export function AddForm() {
  let [{ nonFieldErrors, errors }, formAction] = useFormState(createUser, { nonFieldErrors: null, errors: {} })

  return (
    <Form action={formAction} validationErrors={errors}>
      {nonFieldErrors &&
        <div role="alert" tabIndex={-1} ref={e => e?.focus()}>
          <h3>Unable to submit</h3>
          <p>{nonFieldErrors}</p>
        </div>
      }
      <TextField name="name" isRequired>
        <Label>Name</Label>
        <Input />
        <FieldError />
      </TextField>
      <TextField name="email" type="email" isRequired>
        <Label>Email</Label>
        <Input />
        <FieldError />
      </TextField>
      <TextField
        name="phone"
        type="tel"
        isRequired
        pattern="\((\d{2})\) (\d?)(\d{4})-(\d{4})"
      >
        <Label>Phone</Label>
        <Input />
        <FieldError />
      </TextField>
      <div>
        <label htmlFor="id-password">Password</label>
        <input type="password" id="id-password" name="password" required />
      </div>
      <div>
        <label htmlFor="id-confirm-password">Confirm password</label>
        <input
          type="password"
          id="id-confirm-password"
          name="confirmPassword"
          required
          onChange={(e) => {
            const field = e.currentTarget
            const password = field.form.password
            if (field.value !== password.value) {
              field.setCustomValidity('Passwords do not match')
            } else {
              field.setCustomValidity('')
            }
          }}
        />
      </div>
      <Button type="submit">Add</Button>
    </Form>
  )
}