Skip to content

Commit 3b5faa7

Browse files
committed
Fix authoring nav behaviour for large screen sizes
1 parent 233a3a2 commit 3b5faa7

7 files changed

Lines changed: 169 additions & 45 deletions

File tree

met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React from 'react';
1+
import React, { useLayoutEffect, useRef, useState } from 'react';
2+
import { createPortal } from 'react-dom';
23
import { AppBar, Theme, ThemeProvider, Box, useMediaQuery, Select, MenuItem, SelectChangeEvent } from '@mui/material';
3-
import { Palette, colors, DarkTheme, BaseTheme } from 'styles/Theme';
4+
import { Palette, colors, DarkTheme, BaseTheme, ZIndex } from 'styles/Theme';
45
import { When, Unless } from 'react-if';
56
import { BodyText } from 'components/common/Typography';
67
import { elevations } from 'components/common';
@@ -21,6 +22,58 @@ const AuthoringBottomNav = ({ currentLanguage, setCurrentLanguage, languages, pa
2122
} = useFormContext<EngagementUpdateData>();
2223
const isMediumScreenOrLarger = useMediaQuery((theme: Theme) => theme.breakpoints.up('md'));
2324
const padding = { xs: '1rem 1rem', md: '1rem 1.5rem 1rem 2rem', lg: '1rem 3rem 1rem 2rem' };
25+
const [portalEl, setPortalEl] = useState<HTMLElement | null>(null);
26+
const [atFooter, setAtFooter] = useState(false);
27+
const navRef = useRef<HTMLDivElement | null>(null);
28+
const placeholderRef = useRef<HTMLDivElement | null>(null);
29+
30+
useLayoutEffect(() => {
31+
const footer = document.querySelector('footer');
32+
const hostParent = footer?.parentElement ?? document.body;
33+
const placeholder = document.createElement('div');
34+
placeholder.id = 'authoring-bottom-nav-portal';
35+
placeholder.style.position = 'relative';
36+
placeholder.style.zIndex = `${ZIndex.sideNav}`; // keep above page content
37+
placeholder.style.width = '100%';
38+
placeholder.style.top = '2em'; // slight offset to avoid any potential overlap with footer content
39+
hostParent.insertBefore(placeholder, footer ?? null);
40+
placeholderRef.current = placeholder;
41+
setPortalEl(placeholder);
42+
43+
let footerObserver: IntersectionObserver | null = null;
44+
if (footer) {
45+
footerObserver = new IntersectionObserver(
46+
(entries) => {
47+
const intersecting = entries.some((entry) => entry.isIntersecting);
48+
setAtFooter(intersecting);
49+
},
50+
{ threshold: 0 },
51+
);
52+
footerObserver.observe(footer);
53+
}
54+
55+
return () => {
56+
footerObserver?.disconnect();
57+
placeholder.remove();
58+
setPortalEl(null);
59+
placeholderRef.current = null;
60+
};
61+
}, []);
62+
63+
useLayoutEffect(() => {
64+
if (!portalEl || !navRef.current) return;
65+
const updateHeight = () => {
66+
portalEl.style.height = `${navRef.current?.offsetHeight ?? 0}px`;
67+
};
68+
updateHeight();
69+
const resizeObserver = new ResizeObserver(updateHeight);
70+
resizeObserver.observe(navRef.current);
71+
globalThis.addEventListener('resize', updateHeight, { passive: true });
72+
return () => {
73+
resizeObserver.disconnect();
74+
globalThis.removeEventListener('resize', updateHeight);
75+
};
76+
}, [portalEl]);
2477

2578
const buttonStyles = {
2679
height: '2.6rem',
@@ -31,10 +84,28 @@ const AuthoringBottomNav = ({ currentLanguage, setCurrentLanguage, languages, pa
3184
fontSize: '0.9rem',
3285
};
3386

34-
return (
87+
if (!portalEl) return null;
88+
89+
const fixedPosition = {
90+
position: 'fixed' as const,
91+
bottom: 0,
92+
left: 0,
93+
right: 0,
94+
width: '100%',
95+
};
96+
const absolutePosition = {
97+
position: 'absolute' as const,
98+
bottom: 0,
99+
left: 0,
100+
right: 0,
101+
width: '100%',
102+
};
103+
104+
return createPortal(
35105
<AppBar
36106
component={'nav'}
37-
position="fixed"
107+
position="static"
108+
ref={navRef}
38109
sx={{
39110
backgroundColor: 'transparent',
40111
borderTopRightRadius: '16px',
@@ -44,9 +115,8 @@ const AuthoringBottomNav = ({ currentLanguage, setCurrentLanguage, languages, pa
44115
backgroundClip: 'padding-box',
45116
overflow: 'hidden',
46117
top: 'auto',
47-
left: 0,
48-
bottom: 0,
49118
boxShadow: elevations.default,
119+
...(atFooter ? absolutePosition : fixedPosition),
50120
}}
51121
data-testid="appbar-authoring-bottom-nav"
52122
>
@@ -130,7 +200,8 @@ const AuthoringBottomNav = ({ currentLanguage, setCurrentLanguage, languages, pa
130200
<Box style={{ width: '25rem', display: 'flex' }}></Box>
131201
</ThemeProvider>
132202
</Box>
133-
</AppBar>
203+
</AppBar>,
204+
portalEl,
134205
);
135206
};
136207

met-web/src/components/engagement/admin/create/authoring/AuthoringSideNav.tsx

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useLayoutEffect, useState } from 'react';
22
import {
33
ListItemButton,
44
List,
@@ -186,19 +186,46 @@ const DrawerBox = ({ isMediumScreenOrLarger, setOpen, engagementId }: DrawerBoxP
186186

187187
const AuthoringSideNav = ({ open, setOpen, isMediumScreen, engagementId }: AuthoringNavProps) => {
188188
const currentUser = useAppSelector((state) => state.user.userDetail.user);
189+
const [sideNavOffset, setSideNavOffset] = useState(0);
190+
191+
useLayoutEffect(() => {
192+
if (!isMediumScreen) return;
193+
const handleScroll = () => {
194+
const footerBox = document.querySelector('footer')?.getBoundingClientRect();
195+
if (!footerBox) return;
196+
// How far down the side nav can be in "absolute" mode and not overlap the footer
197+
const footerClearance =
198+
footerBox.top - (document.querySelector('#authoring-sidenav-drawer')?.clientHeight ?? 0);
199+
setSideNavOffset(footerClearance);
200+
console.log('sideNavOffset set to:', footerClearance);
201+
};
202+
globalThis.addEventListener('scroll', handleScroll, { passive: true });
203+
globalThis.addEventListener('resize', handleScroll, { passive: true });
204+
handleScroll(); // Initial run on load
205+
return () => {
206+
globalThis.removeEventListener('scroll', handleScroll);
207+
globalThis.removeEventListener('resize', handleScroll);
208+
};
209+
}, [isMediumScreen]);
210+
189211
if (isMediumScreen)
190212
return (
191213
<Drawer
192214
ModalProps={{
193215
hideBackdrop: true,
194216
}}
195-
PaperProps={{
196-
sx: {
197-
border: 'none',
198-
width: '18.75rem',
199-
boxSizing: 'border-box',
200-
background: 'transparent',
201-
height: '50rem',
217+
slotProps={{
218+
paper: {
219+
id: 'authoring-sidenav-drawer',
220+
sx: {
221+
border: 'none',
222+
width: '18.75rem',
223+
boxSizing: 'border-box',
224+
background: 'transparent',
225+
height: '55rem',
226+
position: sideNavOffset <= 0 ? 'absolute' : 'fixed',
227+
top: sideNavOffset <= 0 ? sideNavOffset + globalThis.scrollY : 'auto',
228+
},
202229
},
203230
}}
204231
hideBackdrop

met-web/src/components/engagement/listing/index.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ const EngagementListing = () => {
414414
placeholder="Search by name"
415415
name="searchText"
416416
value={searchText}
417-
sx={{ height: '40px', pr: 0 }}
417+
sx={{ height: '40px', pr: 0, minWidth: '13em' }}
418418
onChange={(value) => setSearchText(value)}
419419
onKeyDown={(e) => {
420420
if (e.key === 'Enter') handleSearchBarClick(searchText);
@@ -435,7 +435,7 @@ const EngagementListing = () => {
435435
<When condition={!isMediumScreen}>
436436
<Button
437437
size="small"
438-
sx={{ minWidth: '200px' }}
438+
sx={{ minWidth: 'max-content' }}
439439
data-testid="engagement/listing/advancedSearch"
440440
name="advancedSearch"
441441
onClick={() => setAdvancedSearchOpen(!advancedSearchOpen)}
@@ -457,7 +457,6 @@ const EngagementListing = () => {
457457
<When condition={isMediumScreen}>
458458
<Button
459459
size="small"
460-
sx={{ width: '100%' }}
461460
data-testid="engagement/listing/advancedSearch"
462461
name="advancedSearch"
463462
onClick={() => setAdvancedSearchOpen(!advancedSearchOpen)}
@@ -481,7 +480,7 @@ const EngagementListing = () => {
481480
variant="primary"
482481
icon={<FontAwesomeIcon icon={faPlus} />}
483482
href="/engagements/create/wizard"
484-
sx={{ width: { xs: '100%', md: 'unset' } }}
483+
sx={{ minWidth: 'max-content' }}
485484
>
486485
Create Engagement
487486
</Button>

met-web/src/components/survey/listing/Surveys.tsx

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import React, { useContext, useState } from 'react';
22
import MetTable from 'components/common/Table';
33
import Grid from '@mui/material/Grid';
44
import { Link, useNavigate } from 'react-router-dom';
5-
import { MetPageGridContainer, MetTooltip, PrimaryButtonOld, SecondaryButtonOld } from 'components/common';
5+
import { MetPageGridContainer, MetTooltip } from 'components/common';
66
import { Survey } from 'models/survey';
77
import { HeadCell, PaginationOptions } from 'components/common/Table/types';
88
import { formatDate } from 'components/common/dateHelper';
99
import { Collapse, Link as MuiLink, Theme, useMediaQuery } from '@mui/material';
10-
import TextField from '@mui/material/TextField';
1110
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
1211
import { faMagnifyingGlass } from '@fortawesome/pro-regular-svg-icons/faMagnifyingGlass';
1312
import { faCircleExclamation } from '@fortawesome/pro-regular-svg-icons/faCircleExclamation';
@@ -30,6 +29,9 @@ import { Palette } from 'styles/Theme';
3029
import { AdvancedSearch } from './AdvancedSearch';
3130
import { SurveyListingContext } from './SurveyListingContext';
3231
import { USER_ROLES } from 'services/userService/constants';
32+
import { Button, TextInput } from 'components/common/Input';
33+
import { faPlus } from '@fortawesome/pro-regular-svg-icons';
34+
import { AutoBreadcrumbs } from 'components/common/Navigation/Breadcrumb';
3335

3436
const Surveys = () => {
3537
const {
@@ -395,31 +397,43 @@ const Surveys = () => {
395397
columnSpacing={2}
396398
rowSpacing={1}
397399
>
400+
<Grid item>
401+
<AutoBreadcrumbs />
402+
</Grid>
398403
<Grid item xs={12}>
399404
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1} width="100%" justifyContent="space-between">
400405
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1} alignItems="center">
401406
<Stack direction="row" spacing={1} width={{ xs: '100%', md: 'auto' }}>
402-
<TextField
403-
id="engagement-name"
404-
variant="outlined"
405-
label="Search by name"
407+
<TextInput
408+
id="survey-name"
409+
inputProps={{ 'aria-label': 'Search by name' }}
410+
placeholder="Search by name"
406411
fullWidth
407412
name="searchText"
408413
value={searchText}
409-
onChange={(e) => setSearchText(e.target.value)}
414+
sx={{ height: '40px', pr: 0, minWidth: '13em' }}
415+
onChange={(text) => setSearchText(text)}
410416
size="small"
417+
endAdornment={
418+
<Button
419+
variant="primary"
420+
size="small"
421+
data-testid="survey/listing/search-button"
422+
onClick={() => handleSearchBarClick(searchText)}
423+
sx={{ m: 0, borderRadius: '0px 8px 8px 0px' }}
424+
>
425+
<FontAwesomeIcon icon={faMagnifyingGlass} style={{ fontSize: '20px' }} />
426+
</Button>
427+
}
411428
/>
412-
<PrimaryButtonOld
413-
data-testid="survey/listing/search-button"
414-
onClick={() => handleSearchBarClick(searchText)}
415-
>
416-
<FontAwesomeIcon icon={faMagnifyingGlass} style={{ fontSize: '20px' }} />
417-
</PrimaryButtonOld>
418429
</Stack>
419-
<SecondaryButtonOld
430+
<Button
431+
variant="secondary"
432+
size="small"
420433
data-testid="survey-listing/advanced-search-button"
421434
onClick={() => setIsAdvancedSearchOpen(!isAdvancedSearchOpen)}
422-
startIcon={
435+
sx={{ minWidth: 'max-content' }}
436+
icon={
423437
<FontAwesomeIcon
424438
icon={faChevronDown}
425439
style={{
@@ -432,12 +446,19 @@ const Surveys = () => {
432446
fullWidth={isMediumScreen ? true : false}
433447
>
434448
Advanced Search
435-
</SecondaryButtonOld>
449+
</Button>
436450
</Stack>
437451
<PermissionsGate scopes={[USER_ROLES.CREATE_SURVEY]} errorProps={{ disabled: true }}>
438-
<PrimaryButtonOld component={Link} to="/surveys/create">
439-
+ Create Survey
440-
</PrimaryButtonOld>
452+
<Button
453+
variant="primary"
454+
size="small"
455+
sx={{ minWidth: 'max-content' }}
456+
component={Link}
457+
icon={<FontAwesomeIcon icon={faPlus} />}
458+
href="/surveys/create"
459+
>
460+
Create Survey
461+
</Button>
441462
</PermissionsGate>
442463
</Stack>
443464
</Grid>

met-web/src/components/survey/report/SearchBar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ const SearchBar = ({
1616
<>
1717
<Stack direction="row" spacing={1} alignItems="flex-end">
1818
<TextField
19-
id="engagement-name"
19+
id="question-name"
2020
title="Search by name"
21+
inputProps={{ 'aria-label': 'Filter questions by name' }}
22+
sx={{ height: '48px', pr: 0 }}
2123
name="searchText"
2224
value={searchValue}
2325
onChange={setSearchValue}

met-web/src/components/survey/report/SettingsForm.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,16 @@ const SettingsForm = () => {
119119
<FormField title="Link to Public Dashboard Report">
120120
<Tooltip
121121
title="Link copied!"
122-
PopperProps={{
123-
disablePortal: true,
124-
sx: {
125-
pointerEvents: 'none',
126-
'.MuiTooltip-tooltip': { backgroundColor: 'primary.main' },
122+
slotProps={{
123+
popper: {
124+
disablePortal: true,
125+
sx: {
126+
pointerEvents: 'none',
127+
'.MuiTooltip-tooltip': { backgroundColor: 'primary.main' },
128+
},
127129
},
128130
}}
131+
sx={{ height: '40px', pr: 0 }}
129132
onClose={handleTooltipClose}
130133
open={copyTooltip}
131134
disableFocusListener
@@ -147,6 +150,7 @@ const SettingsForm = () => {
147150
padding: 0,
148151
},
149152
}}
153+
size="small"
150154
endAdornment={
151155
engagementSlug && (
152156
<ClickAwayListener onClickAway={handleTooltipClose}>

met-web/src/routes/AuthenticatedRoutes.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const AuthenticatedRoutes = resolveLazyRouteTree(
1717
shouldRevalidate={() => false} // Cache the root loader data for the authenticated area
1818
>
1919
<LazyRoute path="/home" ComponentLazy={() => import('components/dashboard')} />
20-
<Route path="/surveys">
20+
<Route path="/surveys" handle={{ crumb: () => ({ name: 'Surveys', link: '/surveys' }) }}>
2121
<LazyRoute index ComponentLazy={() => import('components/survey/listing')} />
2222
<LazyRoute path="create" ComponentLazy={() => import('components/survey/create')} />
2323
<LazyRoute

0 commit comments

Comments
 (0)