diff --git a/src/firefly/js/tables/__tests__/InputCell-test.jsx b/src/firefly/js/tables/__tests__/InputCell-test.jsx new file mode 100644 index 0000000000..f503fbb977 --- /dev/null +++ b/src/firefly/js/tables/__tests__/InputCell-test.jsx @@ -0,0 +1,54 @@ +/* + * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt + */ +/*eslint-env node, jest */ + +import React from 'react'; +import '@testing-library/jest-dom'; +import {act, fireEvent, render, screen} from '@testing-library/react'; + +import {createInputCell, inputColumnRenderer} from '../ui/TableRenderer.js'; + +const TIP = 'syntax help'; + +const cellBuilders = { + createInputCell: () => createInputCell(TIP, 10, () => ({valid: true})), + inputColumnRenderer: () => inputColumnRenderer({tbl_id: 't1', cname: 'c', tooltips: TIP}), +}; + +const hover = (el) => { + fireEvent.mouseOver(el); + act(() => jest.advanceTimersByTime(1000)); +}; + +describe.each(Object.keys(cellBuilders))('%s syntax tooltip', (name) => { + beforeEach(() => jest.useFakeTimers()); + afterEach(() => jest.useRealTimers()); + + const data = [['a'], ['b']]; + + test('hover alone shows no tooltip', () => { + const Cell = cellBuilders[name](); + render(); + hover(screen.getByRole('textbox')); + expect(screen.queryByRole('tooltip')).toBeNull(); + }); + + test('focus + hover shows the tooltip', () => { + const Cell = cellBuilders[name](); + render(); + const input = screen.getByRole('textbox'); + act(() => input.focus()); + hover(input); + expect(screen.getByRole('tooltip')).toHaveTextContent(TIP); + }); + + test('a cell reused for another row forgets focus that was never blurred', () => { + const Cell = cellBuilders[name](); + const {rerender} = render(); + act(() => screen.getByRole('textbox').focus()); + rerender(); // table swaps the row; the focused input is removed, no blur + hover(screen.getByRole('textbox')); + expect(screen.queryByRole('tooltip')).toBeNull(); + }); +}); diff --git a/src/firefly/js/tables/ui/TableRenderer.js b/src/firefly/js/tables/ui/TableRenderer.js index 8b67132172..51891c6f34 100644 --- a/src/firefly/js/tables/ui/TableRenderer.js +++ b/src/firefly/js/tables/ui/TableRenderer.js @@ -155,12 +155,14 @@ export function HeaderText({val, level='body-sm', sx, ...rest}) { } const blurEnter = ['blur','enter']; +const syntaxTipProps = {enterDelay: 300}; // filter boxes and constraint cells can open their syntax tooltip quickly function Filter({cname, onFilter, filterInfo, tbl_id}) { const colGetter= () => getColumn(getTblById((tbl_id)), cname) ?? {}; const col = useStoreConnector(colGetter); const [showTooltip, setShowTooltip] = useState(true); + const [editing, setEditing] = useState(false); // the syntax tooltip shows only while this box has focus const dropdownEl = useRef(null); useEffect(() => { @@ -191,13 +193,19 @@ function Filter({cname, onFilter, filterInfo, tbl_id}) { validator={validator} fieldKey={name} sx={{width: 1, '--Input-radius': ''}} - tooltip={showTooltip && FILTER_CONDITION_TTIPS} + tooltip={editing && showTooltip && FILTER_CONDITION_TTIPS} value={filterInfoCls.getFilter(name)} onChange={onFilter} actOn={blurEnter} showWarning={false} slotProps={{ - input: {size:'sm', endDecorator } + input: {size:'sm', endDecorator, autoComplete: 'off'}, + // Handle focus on the dropdown or checkboxes bubbling up and triggering the tooltip + control: { + onFocus: (e) => setEditing(e.target.tagName === 'INPUT' && e.currentTarget.contains(e.target)), + onBlur: () => setEditing(false), + }, + tooltip: syntaxTipProps, }} /> ); @@ -530,8 +538,14 @@ export const createInputCell = (tooltips, size = 10, validator, onChange, style) onChange && onChange(v, data, rowIndex, colIdx); }; - return ({rowIndex, data, colIdx}) => { - const val = get(data, [rowIndex, colIdx]); + return function InputCell({rowIndex, data, colIdx}) { + const [editing, setEditing] = useState(false); // the syntax tooltip shows only while this box has focus + const [shownRow, setShownRow] = useState(rowIndex); + if (shownRow !== rowIndex) { // clear editing when a scroll moves the cell to another row + setShownRow(rowIndex); + setEditing(false); + } + const val = data?.[rowIndex]?.[colIdx]; if (val === NOT_CELL_DATA) { @@ -543,13 +557,14 @@ export const createInputCell = (tooltips, size = 10, validator, onChange, style) key={rowIndex + '-' +colIdx} validator={(v) => validator(v, data, rowIndex, colIdx)} sx={{'.MuiInput-root':{'minHeight':'3px', 'borderRadius':4}}} - tooltip={tooltips} + tooltip={editing && tooltips} size={size} style={style} value={val} onChange={(v) => changeHandler(rowIndex, data, colIdx, v) } actOn={['blur','enter']} showWarning={false} + slotProps={{input: {autoComplete: 'off'}, control: {onFocus: () => setEditing(true), onBlur: () => setEditing(false)}, tooltip: syntaxTipProps}} /> ); @@ -571,7 +586,13 @@ export const createInputCell = (tooltips, size = 10, validator, onChange, style) */ export const inputColumnRenderer = ({tbl_id, cname, tooltips, style={}, isReadOnly, validator, onChange, ...props}) => { - return ({rowIndex, data, colIdx}) => { + return function InputColumnCell({rowIndex, data, colIdx}) { + const [editing, setEditing] = useState(false); // the tooltip shows only while this box has focus + const [shownRow, setShownRow] = useState(rowIndex); + if (shownRow !== rowIndex) { // clear editing when a scroll moves the cell to another row + setShownRow(rowIndex); + setEditing(false); + } const val = get(data, [rowIndex, colIdx]); if (isReadOnly && isReadOnly(rowIndex, data, colIdx)) { return
{val}
; @@ -582,11 +603,12 @@ export const inputColumnRenderer = ({tbl_id, cname, tooltips, style={}, isReadOn key={rowIndex + '-' +colIdx} sx={{'.MuiInput-root':{'minHeight':'3px', 'borderRadius':4}}} validator={ validator && ((v) => validator(v, data, rowIndex, colIdx))} - tooltip={tooltips} + tooltip={editing && tooltips} style={{width: '100%', boxSizing: 'border-box', ...style}} value={val} onChange={makeChangeHandler(rowIndex, data, colIdx, tbl_id, cname, validator, onChange)} actOn={['blur','enter']} + slotProps={{input: {autoComplete: 'off'}, control: {onFocus: () => setEditing(true), onBlur: () => setEditing(false)}, tooltip: syntaxTipProps}} /> ); diff --git a/src/firefly/js/ui/tap/TapViewType.jsx b/src/firefly/js/ui/tap/TapViewType.jsx index 5714d7ad33..a0e64cbf9b 100644 --- a/src/firefly/js/ui/tap/TapViewType.jsx +++ b/src/firefly/js/ui/tap/TapViewType.jsx @@ -548,8 +548,11 @@ function OpRender({ops, value, label='', sx, lineClamp, rowDesc='rows'}) { const op = ops.find((t) => t.value === value); if (!op) return 'none'; const details= cleanUp(op.label,0); + const text = op.displayLabel ?? op.label; + const shown = cleanUp(text); + const isCut = shown !== cleanUp(text, 0); // the tooltip only adds something when the shown text is cut short return ( -
@@ -580,7 +583,7 @@ function OpRender({ops, value, label='', sx, lineClamp, rowDesc='rows'}) { WebkitBoxOrient: 'vertical', } : {}} sx={{whiteSpace:'normal', textAlign:'left'}}> -
+