Skip to content

Commit

Permalink
Improve beamline attribute controls
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc committed Oct 1, 2024
1 parent 4513a37 commit 63c6121
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 564 deletions.
1 change: 0 additions & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"immutability-helper": "^3.0.2",
"isomorphic-fetch": "^2.2.0",
"lodash": "^4.17.21",
"rc-input-number": "8.0.3",
"react": "^17.0.2",
"react-bootstrap": "2.8.0",
"react-chat-widget": "^3.1.4",
Expand Down
84 changes: 15 additions & 69 deletions ui/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

82 changes: 82 additions & 0 deletions ui/src/components/BeamlineAttribute/BeamlineAttribute.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useRef } from 'react';
import { Button, OverlayTrigger, Popover } from 'react-bootstrap';

import { STATE } from '../../actions/beamline';
import BeamlineAttributeForm from './BeamlineAttributeForm';
import styles from './BeamlineAttribute.module.css';
import '../input.css';

const STATE_CLASS = {
[STATE.BUSY]: 'input-bg-moving',
[STATE.ABORT]: 'input-bg-fault',
};

function BeamlineAttribute(props) {
const { attribute, format, precision = 1, suffix, onSave, onCancel } = props;
const {
state = STATE.IDLE,
value = 0,
step = 0.1,
msg,
readonly = false,
} = attribute;

const btnRef = useRef(null);

const valStr = value
? format === 'expo'
? value.toExponential(precision)
: value.toFixed(precision)
: '-';

if (readonly) {
return (
<span className={styles.value}>
{valStr}
{suffix && ` ${suffix}`}
</span>
);
}

return (
<OverlayTrigger
rootClose
trigger="click"
placement="right"
onToggle={(show) => {
if (!show) {
// Bring focus back on button when popover closes
btnRef.current?.focus({ preventScroll: true });
}
}}
overlay={
<Popover
id="beamline-attribute-popover"
className="d-flex align-items-center p-2"
>
<BeamlineAttributeForm
value={value}
isBusy={state === STATE.BUSY}
step={step}
precision={precision}
onSave={onSave}
onCancel={onCancel}
/>
{msg && <div className="mx-3">{msg}</div>}
</Popover>
}
>
<Button
ref={btnRef}
className={`${styles.valueBtn} ${STATE_CLASS[state] || ''}`}
variant="link"
data-default-styles
>
{valStr}
{suffix && ` ${suffix}`}
</Button>
</OverlayTrigger>
);
}

export default BeamlineAttribute;
26 changes: 26 additions & 0 deletions ui/src/components/BeamlineAttribute/BeamlineAttribute.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.value {
display: inline-flex;
padding: 0.5rem 0.25rem;
white-space: nowrap;
}

.valueBtn {
composes: value;
font-weight: inherit;
text-decoration: none;
}

.valueBtn:hover,
.valueBtn:focus-visible {
text-decoration: underline dotted;
}

.valueBtn:focus-visible {
outline: auto !important;
outline-offset: -2px;
}

.input {
height: 36px;
appearance: auto !important;
}
63 changes: 63 additions & 0 deletions ui/src/components/BeamlineAttribute/BeamlineAttributeForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useEffect, useRef } from 'react';
import { Button, ButtonToolbar, Form } from 'react-bootstrap';
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);

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();
}, 0);
}
}, [isBusy]);

function handleSubmit(evt) {
evt.preventDefault();
const formData = new FormData(evt.target);
const strVal = formData.get('value');

const numVal =
typeof strVal === 'string' ? Number.parseFloat(strVal) : Number.NaN;

if (!Number.isNaN(numVal)) {
onSave(numVal);
}
}

return (
<Form className="d-flex" noValidate onSubmit={handleSubmit}>
<input
ref={inputRef}
className={`form-control rw-input ${styles.input}`}
type="number"
name="value"
aria-label="Value"
step={step}
size={10}
defaultValue={value.toFixed(precision)}
disabled={isBusy}
/>
<ButtonToolbar className="ms-1">
{isBusy ? (
<Button variant="danger" size="sm" onClick={() => onCancel()}>
<TiTimes size="1.5em" />
</Button>
) : (
<Button type="submit" variant="success" size="sm">
<TiTick size="1.5em" />
</Button>
)}
</ButtonToolbar>
</Form>
);
}

export default BeamlineAttributeForm;
Loading

0 comments on commit 63c6121

Please sign in to comment.