|
1 |
| -import { useEffect, useRef } from 'react'; |
| 1 | +import { useEffect } from 'react'; |
2 | 2 | import { Button, ButtonToolbar, Form } from 'react-bootstrap';
|
| 3 | +import { useForm } from 'react-hook-form'; |
3 | 4 | import { TiTick, TiTimes } from 'react-icons/ti';
|
4 | 5 |
|
5 | 6 | import styles from './BeamlineAttribute.module.css';
|
6 | 7 |
|
7 | 8 | function BeamlineAttributeForm(props) {
|
8 |
| - const { value, isBusy, step, precision, onSave, onCancel } = props; |
9 |
| - const inputRef = useRef(null); |
| 9 | + const { value, isBusy, step, precision, limits, onSave, onCancel } = props; |
| 10 | + |
| 11 | + const { |
| 12 | + register, |
| 13 | + setFocus, |
| 14 | + setError, |
| 15 | + handleSubmit: makeOnSubmit, |
| 16 | + formState: { isDirty, errors }, |
| 17 | + } = useForm({ defaultValues: { value: value.toFixed(precision) } }); |
10 | 18 |
|
11 | 19 | useEffect(() => {
|
12 | 20 | if (!isBusy) {
|
13 | 21 | setTimeout(() => {
|
14 | 22 | /* Focus and select text when popover opens and every time a value is applied.
|
15 | 23 | * Timeout ensures this works when opening a popover while another is already opened. */
|
16 |
| - inputRef.current?.focus({ preventScroll: true }); |
17 |
| - inputRef.current?.select(); |
| 24 | + setFocus('value', { shouldSelect: true }); |
18 | 25 | }, 0);
|
19 | 26 | }
|
20 |
| - }, [isBusy]); |
21 |
| - |
22 |
| - function handleSubmit(evt) { |
23 |
| - evt.preventDefault(); |
24 |
| - const formData = new FormData(evt.target); |
25 |
| - const strVal = formData.get('value'); |
| 27 | + }, [isBusy, setFocus]); |
26 | 28 |
|
27 |
| - const numVal = |
28 |
| - typeof strVal === 'string' ? Number.parseFloat(strVal) : Number.NaN; |
29 |
| - |
30 |
| - if (!Number.isNaN(numVal)) { |
31 |
| - onSave(numVal); |
| 29 | + async function handleSubmit(data) { |
| 30 | + try { |
| 31 | + await onSave(data.value); |
| 32 | + } catch { |
| 33 | + setError('value', { message: 'Unable to set value' }); |
32 | 34 | }
|
33 | 35 | }
|
34 | 36 |
|
| 37 | + const minMaxMsg = `Allowed range: [${limits |
| 38 | + .map((v) => v.toFixed(precision)) |
| 39 | + .join(', ')}]`; |
| 40 | + |
35 | 41 | return (
|
36 |
| - <Form className="d-flex" noValidate onSubmit={handleSubmit}> |
37 |
| - <Form.Control |
38 |
| - ref={inputRef} |
39 |
| - className={styles.input} |
40 |
| - name="value" |
41 |
| - type="number" |
42 |
| - step={step} |
43 |
| - defaultValue={value.toFixed(precision)} |
44 |
| - disabled={isBusy} |
45 |
| - aria-label="Value" |
46 |
| - /> |
47 |
| - <ButtonToolbar className="ms-1"> |
48 |
| - {isBusy ? ( |
49 |
| - <Button variant="danger" size="sm" onClick={() => onCancel()}> |
50 |
| - <TiTimes size="1.5em" /> |
51 |
| - </Button> |
52 |
| - ) : ( |
53 |
| - <Button type="submit" variant="success" size="sm"> |
54 |
| - <TiTick size="1.5em" /> |
55 |
| - </Button> |
56 |
| - )} |
57 |
| - </ButtonToolbar> |
| 42 | + <Form noValidate onSubmit={makeOnSubmit(handleSubmit)}> |
| 43 | + <div className="d-flex"> |
| 44 | + <Form.Control |
| 45 | + {...register('value', { |
| 46 | + valueAsNumber: true, |
| 47 | + required: 'Please enter a valid number', |
| 48 | + min: { value: limits[0], message: minMaxMsg }, |
| 49 | + max: { value: limits[1], message: minMaxMsg }, |
| 50 | + disabled: isBusy, |
| 51 | + })} |
| 52 | + className={styles.input} |
| 53 | + type="number" |
| 54 | + step={step} |
| 55 | + aria-label="Value" |
| 56 | + isInvalid={isDirty && !!errors.value} |
| 57 | + /> |
| 58 | + <ButtonToolbar className="ms-1"> |
| 59 | + {isBusy ? ( |
| 60 | + <Button variant="danger" size="sm" onClick={() => onCancel()}> |
| 61 | + <TiTimes size="1.5em" /> |
| 62 | + </Button> |
| 63 | + ) : ( |
| 64 | + <Button type="submit" variant="success" size="sm"> |
| 65 | + <TiTick size="1.5em" /> |
| 66 | + </Button> |
| 67 | + )} |
| 68 | + </ButtonToolbar> |
| 69 | + </div> |
| 70 | + {errors.value && <p className={styles.error}>{errors.value.message}</p>} |
58 | 71 | </Form>
|
59 | 72 | );
|
60 | 73 | }
|
|
0 commit comments