Skip to content

Commit 2d99d59

Browse files
Merge pull request #1791 from Caltech-IPAC/FIREFLY-1754-bgmon-hint
FIREFLY-1754: Fix poor positioning of App Hints
2 parents 58c8f9e + 78bc7ad commit 2d99d59

File tree

6 files changed

+152
-86
lines changed

6 files changed

+152
-86
lines changed

src/firefly/js/core/LayoutCntlr.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,4 +558,7 @@ function getColFitIdx(gridView, row, testIdx, gridColumns, testWidth) {
558558

559559
}
560560

561-
561+
// getter/setters for the DOM nodes of Menu Tabs, stored in the layout info
562+
export const MENU_TAB_NODES = 'menuTabNodes';
563+
export const getMenuTabNodes = () => getLayouInfo()?.[MENU_TAB_NODES] ?? {};
564+
export const dispatchUpdateMenuTabNodes = (menuTabNodes) => dispatchUpdateLayoutInfo({[MENU_TAB_NODES]: menuTabNodes});

src/firefly/js/templates/common/FireflyLayout.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {AppConfigDrawer} from '../../ui/AppConfigDrawer.jsx';
1515
import {getActionFromUrl} from 'firefly/core/History';
1616
import {getDropDownInfo, SHOW_DROPDOWN} from 'firefly/core/LayoutCntlr';
1717
import {flux} from 'firefly/core/ReduxFlux';
18-
import {APP_HINT_IDS, appHintPrefName} from 'firefly/templates/fireflyviewer/LandingPage';
18+
import {APP_HINT_IDS, appHintPrefName} from 'firefly/ui/AppHint';
1919
import {InitArgsCtx} from './InitArgsCtx';
2020

2121
/*

src/firefly/js/templates/fireflyviewer/LandingPage.jsx

Lines changed: 9 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
1-
import {Box, Button, Divider, Sheet, Snackbar, Stack, Typography} from '@mui/joy';
1+
import {Box, Divider, Sheet, Stack, Typography} from '@mui/joy';
22
import React, {useContext, useState} from 'react';
3-
import {elementType, shape, object, string, arrayOf, oneOf, node} from 'prop-types';
3+
import {elementType, shape, object, string, arrayOf, node} from 'prop-types';
44
import QueryStats from '@mui/icons-material/QueryStats';
5-
import TipsAndUpdates from '@mui/icons-material/TipsAndUpdates';
65

76
import {getBackgroundInfo, isMonitored, isSearchJob} from '../../core/background/BackgroundUtil.js';
8-
import {dispatchShowDropDown} from '../../core/LayoutCntlr.js';
7+
import {dispatchShowDropDown, getMenuTabNodes} from '../../core/LayoutCntlr.js';
98
import {AppPropertiesCtx} from '../../ui/AppPropertiesCtx.jsx';
109
import {Slot, useStoreConnector} from '../../ui/SimpleComponent.jsx';
1110
import {FileDropZone} from '../../visualize/ui/FileUploadViewPanel.jsx';
12-
import {dispatchAddPreference, getPreference} from 'firefly/core/AppDataCntlr';
13-
14-
export const APP_HINT_IDS = {
15-
TABS_MENU: 'tabsMenu',
16-
BG_MONITOR: 'bgMonitor'
17-
};
11+
import {APP_HINT_IDS, AppHint, HINT_TIP_PLACEMENTS} from 'firefly/ui/AppHint';
1812

1913

2014
export function LandingPage({slotProps={}, sx, ...props}) {
2115
const {appTitle,footer,
2216
fileDropEventAction='FileUploadDropDownCmd'} = useContext(AppPropertiesCtx);
2317

18+
const {first: tabsMenuHintAnchor, last: bgMonitorHintAnchor} = useStoreConnector(getMenuTabNodes);
19+
2420
const defSlotProps = {
25-
tabsMenuHint: {appTitle, id: APP_HINT_IDS.TABS_MENU, hintText: 'Choose a tab to search for or upload data.', sx: { left: '16rem' }},
26-
bgMonitorHint: {appTitle, id: APP_HINT_IDS.BG_MONITOR, hintText: 'Load job results from background monitor', tipPlacement: 'end', sx: { right: 8 }},
21+
tabsMenuHint: {appTitle, id: APP_HINT_IDS.TABS_MENU, anchorNode: tabsMenuHintAnchor, hintText: 'Choose a tab to search for or upload data.'},
22+
bgMonitorHint: {appTitle, id: APP_HINT_IDS.BG_MONITOR, anchorNode: bgMonitorHintAnchor, hintText: 'Load job results from background monitor', tipPlacement: HINT_TIP_PLACEMENTS.START},
2723
topSection: { title: `Welcome to ${appTitle}` },
2824
bottomSection: {
2925
icon: <QueryStats sx={{ width: '6rem', height: '6rem' }} />,
@@ -47,7 +43,7 @@ export function LandingPage({slotProps={}, sx, ...props}) {
4743

4844
return (
4945
<Sheet className='ff-ResultsPanel-StandardView' sx={{width: 1, height: 1, ...sx}} {...props}>
50-
<Slot component={AppHint} {...defSlotProps.tabsMenuHint} slotProps={slotProps?.tabsMenuHint}/>
46+
{tabsMenuHintAnchor && <Slot component={AppHint} {...defSlotProps.tabsMenuHint} slotProps={slotProps?.tabsMenuHint}/>}
5147
{haveBgJobs && <Slot component={AppHint} {...defSlotProps.bgMonitorHint} slotProps={slotProps?.bgMonitorHint}/>}
5248
<FileDropZone {...{
5349
dropEvent, setDropEvent,
@@ -118,61 +114,6 @@ function EmptyResults({icon, text, subtext, summaryText, actionItems, slotProps}
118114
);
119115
}
120116

121-
// An app hint needs to be shown only the first time user loads an app. So this is controlled by a flag saved as app preference
122-
export const appHintPrefName = (appTitle, hintId) => `showAppHint__${appTitle}--${hintId}`;
123-
124-
function AppHint({appTitle, id, hintText, tipPlacement='middle', sx={}}) {
125-
const showAppHint = useStoreConnector(() => getPreference(appHintPrefName(appTitle, id), true));
126-
127-
const arrowTip = {
128-
'&::before': {
129-
content: '""',
130-
width: '1rem',
131-
height: '1rem',
132-
backgroundColor: 'inherit',
133-
transform: 'rotate(-45deg)',
134-
position: 'absolute',
135-
top: '-0.5rem',
136-
left: 'calc(50% - 0.5rem)',
137-
...tipPlacement==='start' && {left: 'var(--Snackbar-padding)'},
138-
...tipPlacement==='end' && {left: 'auto', right: 'var(--Snackbar-padding)'}
139-
}
140-
};
141-
142-
// to undo positioning controlled by anchorOrigin prop of Snackbar, and to place it directly below Banner
143-
const positioningSx = {
144-
position: 'absolute',
145-
top: '0.75rem',
146-
left: 'auto',
147-
right: 'auto',
148-
bottom: 'auto'
149-
};
150-
151-
return (
152-
<Snackbar open={Boolean(showAppHint)}
153-
size='lg'
154-
variant='solid' //to make it look different from alerts
155-
color='primary'
156-
invertedColors={true}
157-
onClose={(e, reason)=> {
158-
//don't close a hint if the click made outside it (clickaway) originated from another hint
159-
if (reason==='clickaway' && e?.target?.closest('.MuiSnackbar-root')) return;
160-
dispatchAddPreference(appHintPrefName(appTitle, id), false);
161-
}}
162-
sx={{...positioningSx, ...sx, ...arrowTip}}
163-
startDecorator={<TipsAndUpdates/>}
164-
endDecorator={
165-
<Button
166-
onClick={() => dispatchAddPreference(appHintPrefName(appTitle, id), false)}
167-
variant='outlined'
168-
color='primary'>
169-
Got it
170-
</Button>
171-
}>
172-
{hintText}
173-
</Snackbar>
174-
);
175-
}
176117

177118

178119
LandingPage.propTypes = {
@@ -216,12 +157,3 @@ EmptyResults.propTypes = {
216157
})),
217158
slotProps: object,
218159
};
219-
220-
221-
AppHint.propTypes = {
222-
appTitle: string.isRequired,
223-
id: string.isRequired,
224-
hintText: string.isRequired,
225-
tipPlacement: oneOf(['start', 'middle', 'end']),
226-
sx: object,
227-
};

src/firefly/js/templates/hydra/HydraViewer.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ const HydraAppBranding = ({title, desc}) => (
161161
);
162162

163163
const defaultCommonProps = ({title, desc}) => ({
164-
bgMonitorHint: { sx: { right: 50 } },
165164
topSection: { title, desc }
166165
});
167166

src/firefly/js/ui/AppHint.jsx

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {useStoreConnector} from 'firefly/ui/SimpleComponent';
2+
import {dispatchAddPreference, getPreference} from 'firefly/core/AppDataCntlr';
3+
import React, {useLayoutEffect, useRef, useState} from 'react';
4+
import {Button, Snackbar} from '@mui/joy';
5+
import TipsAndUpdates from '@mui/icons-material/TipsAndUpdates';
6+
import {object, oneOf, string} from 'prop-types';
7+
8+
9+
export const APP_HINT_IDS = {
10+
TABS_MENU: 'tabsMenu',
11+
BG_MONITOR: 'bgMonitor'
12+
};
13+
14+
export const HINT_TIP_PLACEMENTS = {
15+
START: 'start',
16+
MIDDLE: 'middle',
17+
END: 'end'
18+
};
19+
20+
/**An app hint needs to be shown only the first time user loads an app. So this is controlled by a flag saved as app preference**/
21+
export const appHintPrefName = (appTitle, hintId) => `showAppHint__${appTitle}--${hintId}`;
22+
23+
export function AppHint({appTitle, id, anchorNode, hintText, tipPlacement=HINT_TIP_PLACEMENTS.MIDDLE, sx={}}) {
24+
const showAppHint = useStoreConnector(() => getPreference(appHintPrefName(appTitle, id), true));
25+
const appHintRef = useRef();
26+
27+
// AppHint is rendered inside LandingPage, so we cannot yet compute top/bottom from the anchor node but only left/right
28+
const [anchorRelativePosSx, setAnchorRelativePosSx] = useState({});
29+
useLayoutEffect(() => {
30+
if (anchorNode) {
31+
const anchorRect = anchorNode.getBoundingClientRect();
32+
const appHintRect = appHintRef.current?.getBoundingClientRect() ?? {width: 0};
33+
const posSx = {left: 'auto', right: 'auto'};
34+
switch (tipPlacement) {
35+
case HINT_TIP_PLACEMENTS.START:
36+
posSx.left = anchorRect.left;
37+
break;
38+
case HINT_TIP_PLACEMENTS.END:
39+
posSx.right = `calc(100vw - ${anchorRect.right}px)`;
40+
break;
41+
case HINT_TIP_PLACEMENTS.MIDDLE:
42+
default:
43+
posSx.left = anchorRect.left + (anchorRect.width/2) - (appHintRect.width/2); // to center the hint
44+
break;
45+
}
46+
setAnchorRelativePosSx(posSx);
47+
}
48+
}, [anchorNode]);
49+
50+
const arrowTip = {
51+
'&::before': {
52+
content: '""',
53+
width: '1rem',
54+
height: '1rem',
55+
backgroundColor: 'inherit',
56+
transform: 'rotate(-45deg)',
57+
position: 'absolute',
58+
top: '-0.5rem',
59+
left: 'calc(50% - 0.5rem)', //tipPlacement===HINT_TIP_PLACEMENTS.MIDDLE
60+
...tipPlacement===HINT_TIP_PLACEMENTS.START && {left: 'var(--Snackbar-padding)'},
61+
...tipPlacement===HINT_TIP_PLACEMENTS.END && {left: 'auto', right: 'var(--Snackbar-padding)'}
62+
}
63+
};
64+
65+
// to undo positioning controlled by anchorOrigin prop of Snackbar, and to place it directly below MenuTabBar
66+
const defaultPositionSx = {
67+
position: 'absolute',
68+
top: '0.75rem', // to create space for the arrow tip (with height: sqrt(2) * 1 rem / 2)
69+
bottom: 'auto',
70+
left: 'auto',
71+
right: 'auto',
72+
};
73+
74+
return (
75+
<Snackbar open={Boolean(showAppHint)}
76+
ref={appHintRef}
77+
size='lg'
78+
variant='solid' //to make it look different from alerts
79+
color='primary'
80+
invertedColors={true}
81+
onClose={(e, reason)=> {
82+
//don't close a hint if the click made outside it (clickaway) originated from another hint
83+
if (reason==='clickaway' && e?.target?.closest('.MuiSnackbar-root')) return;
84+
dispatchAddPreference(appHintPrefName(appTitle, id), false);
85+
}}
86+
sx={{...defaultPositionSx, ...anchorRelativePosSx, ...sx, ...arrowTip}}
87+
startDecorator={<TipsAndUpdates/>}
88+
endDecorator={
89+
<Button
90+
onClick={() => dispatchAddPreference(appHintPrefName(appTitle, id), false)}
91+
variant='outlined'
92+
color='primary'>
93+
Got it
94+
</Button>
95+
}>
96+
{hintText}
97+
</Snackbar>
98+
);
99+
}
100+
101+
AppHint.propTypes = {
102+
appTitle: string.isRequired,
103+
id: string.isRequired,
104+
anchorNode: object, // generally a Menu Tab DOM node
105+
hintText: string.isRequired,
106+
tipPlacement: oneOf(['start', 'middle', 'end']),
107+
sx: object,
108+
};
109+

src/firefly/js/ui/Menu.jsx

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ import {
1515
getMenu, getPreference, getSelectedMenuItem, getUserInfo
1616
} from '../core/AppDataCntlr.js';
1717
import {flux} from '../core/ReduxFlux.js';
18-
import {dispatchHideDropDown, dispatchShowDropDown, getLayouInfo, getResultCounts} from '../core/LayoutCntlr.js';
18+
import {
19+
dispatchHideDropDown,
20+
dispatchShowDropDown,
21+
dispatchUpdateMenuTabNodes,
22+
getLayouInfo, getMenuTabNodes,
23+
getResultCounts,
24+
} from '../core/LayoutCntlr.js';
1925
import QuizOutlinedIcon from '@mui/icons-material/QuizOutlined';
2026
import {AppPropertiesCtx} from './AppPropertiesCtx.jsx';
2127
import {useStoreConnector} from './SimpleComponent.jsx';
@@ -110,6 +116,10 @@ function AdjustableMenu({menuTabItems, helpItem, selected, dropDown, showUserInf
110116
const {current:tbarElement}= useRef({element:undefined});
111117
const {current:tabRenderedInfo}= useRef({tabWidths:{}});
112118
const {current:lastButtonSize}= useRef({size:'lg'});
119+
120+
const storedMenuTabNodes = useStoreConnector(getMenuTabNodes);
121+
const tabNodesRef = useRef({first: undefined, last: undefined});
122+
113123
const showHelp= Boolean(helpItem);
114124

115125
useEffect(() => {
@@ -123,15 +133,28 @@ function AdjustableMenu({menuTabItems, helpItem, selected, dropDown, showUserInf
123133
setTabCount(menuTabItems.length); // force a rerender if number of tabs change
124134
}, [menuTabItems.length]);
125135

136+
useEffect(() => {
137+
const newTabNodes = Object.fromEntries(Object.entries(tabNodesRef.current).filter(
138+
([k, node])=> storedMenuTabNodes?.[k] !== node));
139+
if (Object.keys(newTabNodes).length > 0) {
140+
dispatchUpdateMenuTabNodes(newTabNodes);
141+
}
142+
});
143+
126144
const setTabBarElement= useCallback((ts) => {
127145
tbarElement.element= ts;
128146
if (ts && tabCount===-1) setTabCount(menuTabItems.length); // force a rerender when we know the html element of the tab bar
129147
},[]);
130148

131-
const setElement= useCallback( (key,e) => {
132-
if (!e) return;
149+
const setElement= useCallback( (key,el) => {
150+
if (!el) return;
151+
152+
// save the first and last tab elements as they are needed for positioning the app hints
153+
if (key==='0') tabNodesRef.current['first'] = el;
154+
if (key===menuTabItems.length-1+'') tabNodesRef.current['last'] = el;
155+
133156
const {tabWidths}= tabRenderedInfo;
134-
tabWidths[key]= Math.trunc(e.getBoundingClientRect()?.width ?? 0);
157+
tabWidths[key]= Math.trunc(el.getBoundingClientRect()?.width ?? 0);
135158
Object.keys(tabWidths).forEach( (key) => {
136159
if (Number(key)>=menuTabItems.length) tabWidths[key]= undefined;
137160
});
@@ -192,13 +215,13 @@ function MenuTabBar({menuTabItems=[], size, selected, dropDown, displayMask, set
192215
const color='primary';
193216

194217
const tabItems= [
195-
<ResultsTab {...{key:'results-tab', size, color, variant, ref: (c) => setElement('results-tab ',c)}}/>,
218+
<ResultsTab {...{key:'results-tab', size, color, variant, ref: (el) => setElement('results-tab ', el)}}/>,
196219
...menuTabItems
197220
.filter( isItemEnabled)
198221
.map(({action,label,TabRenderer,title}, idx) =>
199222
{
200223
const tabProps = {key: idx, value:action, disableIndicator:true, color, variant,
201-
ref: (c) => setElement(idx+'',c),
224+
ref: (el) => setElement(idx + '', el),
202225
sx: (theme) => ({ ...setupTabCss(theme,size) })
203226
};
204227
const tab= TabRenderer ? <TabRenderer {...tabProps} /> : <Tab {...tabProps}> {label}</Tab>;

0 commit comments

Comments
 (0)