diff --git a/ui/src/components/BeamlineAttribute/BeamlineAttribute.jsx b/ui/src/components/BeamlineAttribute/BeamlineAttribute.jsx index fb426c389..b8638300a 100644 --- a/ui/src/components/BeamlineAttribute/BeamlineAttribute.jsx +++ b/ui/src/components/BeamlineAttribute/BeamlineAttribute.jsx @@ -7,7 +7,14 @@ import BeamlineAttributeForm from './BeamlineAttributeForm'; function BeamlineAttribute(props) { const { attribute, format, precision = 1, suffix, onSave, onCancel } = props; - const { state, value = 0, step = 0.1, msg, readonly = false } = attribute; + const { + state, + value = 0, + step = 0.1, + limits, + msg, + readonly = false, + } = attribute; const isBusy = state === HW_STATE.BUSY; @@ -49,6 +56,7 @@ function BeamlineAttribute(props) { isBusy={isBusy} step={step} precision={precision} + limits={limits} onSave={onSave} onCancel={onCancel} /> diff --git a/ui/src/components/BeamlineAttribute/BeamlineAttribute.module.css b/ui/src/components/BeamlineAttribute/BeamlineAttribute.module.css index a2ec6aa40..f40e48e21 100644 --- a/ui/src/components/BeamlineAttribute/BeamlineAttribute.module.css +++ b/ui/src/components/BeamlineAttribute/BeamlineAttribute.module.css @@ -26,7 +26,14 @@ } .input { - width: 8em; + width: 9em; height: 36px; appearance: auto !important; } + +.error { + color: var(--bs-form-invalid-color); + line-height: 1; + margin-top: 0.625rem; + margin-bottom: 0; +} diff --git a/ui/src/components/BeamlineAttribute/BeamlineAttributeForm.jsx b/ui/src/components/BeamlineAttribute/BeamlineAttributeForm.jsx index 76d55e7db..37b8ae088 100644 --- a/ui/src/components/BeamlineAttribute/BeamlineAttributeForm.jsx +++ b/ui/src/components/BeamlineAttribute/BeamlineAttributeForm.jsx @@ -1,60 +1,73 @@ -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { Button, ButtonToolbar, Form } from 'react-bootstrap'; +import { useForm } from 'react-hook-form'; import { TiTick, TiTimes } from 'react-icons/ti'; import styles from './BeamlineAttribute.module.css'; function BeamlineAttributeForm(props) { - const { value, isBusy, step, precision, onSave, onCancel } = props; - const inputRef = useRef(null); + const { value, isBusy, step, precision, limits, onSave, onCancel } = props; + + const { + register, + setFocus, + setError, + handleSubmit: makeOnSubmit, + formState: { isDirty, errors }, + } = useForm({ defaultValues: { value: value.toFixed(precision) } }); useEffect(() => { if (!isBusy) { setTimeout(() => { /* Focus and select text when popover opens and every time a value is applied. * Timeout ensures this works when opening a popover while another is already opened. */ - inputRef.current?.focus({ preventScroll: true }); - inputRef.current?.select(); + setFocus('value', { shouldSelect: true }); }, 0); } - }, [isBusy]); - - function handleSubmit(evt) { - evt.preventDefault(); - const formData = new FormData(evt.target); - const strVal = formData.get('value'); + }, [isBusy, setFocus]); - const numVal = - typeof strVal === 'string' ? Number.parseFloat(strVal) : Number.NaN; - - if (!Number.isNaN(numVal)) { - onSave(numVal); + async function handleSubmit(data) { + try { + await onSave(data.value); + } catch { + setError('value', { message: 'Unable to set value' }); } } + const minMaxMsg = `Allowed range: [${limits + .map((v) => v.toFixed(precision)) + .join(', ')}]`; + return ( -