Skip to content

Commit

Permalink
Dynamically adjust between grid & flex layout based on actual list width
Browse files Browse the repository at this point in the history
Obnoxiously track the actual size of the event list via a duplicate list hidden in DOM
Use this along with window size to see if we need to adjust our layout to flexbox for smaller screens
  • Loading branch information
CarsonF committed Sep 25, 2024
1 parent d47c15a commit 676346b
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 126 deletions.
13 changes: 5 additions & 8 deletions src/scenes/Projects/Overview/ProjectOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,7 @@ import { ProjectListQueryVariables } from '../List/ProjectList.graphql';
import { EditableProjectField, UpdateProjectDialog } from '../Update';
import { ProjectWorkflowDialog } from '../Update/ProjectWorkflowDialog';
import { useProjectId } from '../useProjectId';
import {
WorkflowEventsDrawer,
WorkflowEventsIcon,
WorkflowEventsList,
} from '../WorkflowEvents';
import { WorkflowEventsDrawer, WorkflowEventsIcon } from '../WorkflowEvents';
import {
ProjectEngagementListOverviewDocument as EngagementList,
ProjectOverviewDocument,
Expand Down Expand Up @@ -586,9 +582,10 @@ export const ProjectOverview = () => {
<CreateEngagement project={project} {...createEngagementState} />
)}
{project && (
<WorkflowEventsDrawer {...workflowDrawerState}>
<WorkflowEventsList events={project.workflowEvents} />
</WorkflowEventsDrawer>
<WorkflowEventsDrawer
{...workflowDrawerState}
events={project.workflowEvents}
/>
)}
</main>
);
Expand Down
115 changes: 84 additions & 31 deletions src/scenes/Projects/WorkflowEvents/WorkflowEventsDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,92 @@
import { Close } from '@mui/icons-material';
import { Box, Divider, Drawer, DrawerProps, Typography } from '@mui/material';
import { useSize } from 'ahooks';
import { useEffect, useRef, useState } from 'react';
import { extendSx } from '~/common';
import { IconButton } from '~/components/IconButton';
import { ProjectWorkflowEventFragment as WorkflowEvent } from './projectWorkflowEvent.graphql';
import { WorkflowEventsList } from './WorkflowEventsList';

type WorkflowEventsDrawerProps = DrawerProps & {
TransitionProps?: DrawerProps['SlideProps'];
events: readonly WorkflowEvent[];
};
export const WorkflowEventsDrawer = ({
events,
TransitionProps,
...props
}: DrawerProps & { TransitionProps?: DrawerProps['SlideProps'] }) => (
<Drawer
SlideProps={TransitionProps} // normalize with Dialog
{...props}
anchor="right"
sx={[
{
'.MuiPaper-root': {
p: 2,
maxWidth: '100vw',
},
},
...extendSx(props.sx),
]}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography variant="h3">Status History Log</Typography>
<IconButton onClick={(e) => props.onClose?.(e, 'backdropClick')}>
<Close />
</IconButton>
</Box>
<Divider sx={{ pt: 1 }} />
{props.children}
</Drawer>
);
}: WorkflowEventsDrawerProps) => {
const listRef = useRef();
const listSize = useSize(listRef);
const windowSize = useSize(() => document.querySelector('body'));
const [needsWidthCalc, setNeedsWidthCalc] = useState(false);

const isFullWidth =
!!windowSize?.width &&
!!listSize?.width &&
windowSize.width <= listSize.width;

useEffect(() => setNeedsWidthCalc(true), [events]);
useEffect(() => {
listSize?.width && setNeedsWidthCalc(false);
}, [listSize?.width]);

return (
<>
<Drawer
SlideProps={TransitionProps} // normalize with Dialog
{...props}
anchor="right"
sx={[
{
'.MuiPaper-root': {
p: 2,
maxWidth: '100vw',
// Even if the smaller "full width" event list layout
// is narrower than the larger desktop layout, keep the drawer
// full width so the width doesn't snap around.
width: isFullWidth ? '100vw' : undefined,
},
},
...extendSx(props.sx),
]}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography variant="h3">Status History Log</Typography>
<IconButton onClick={(e) => props.onClose?.(e, 'backdropClick')}>
<Close />
</IconButton>
</Box>
<Divider sx={{ pt: 1 }} />

{/* Actual list shown in UI */}
<WorkflowEventsList events={events} fullWidth={isFullWidth} />
</Drawer>

{/*
Hidden list to track intrinsic size to calculate full width.
Outside the drawer so it is in DOM when needed regardless of drawer state.
*/}
{needsWidthCalc && (
<WorkflowEventsList
events={events}
ref={listRef}
sx={{
p: 2, // matches above
visibility: 'hidden',
position: 'absolute',
top: 0,
right: 0,
width: 'fit-content',
}}
/>
)}
</>
);
};
187 changes: 100 additions & 87 deletions src/scenes/Projects/WorkflowEvents/WorkflowEventsList.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,110 @@
import { ChevronRight } from '@mui/icons-material';
import { Box, Divider, List, Typography } from '@mui/material';
import { Box, Divider, List, ListProps, Typography } from '@mui/material';
import { forwardRef } from 'react';
import { ProjectStepLabels, ProjectStepList } from '~/api/schema/enumLists';
import { extendSx } from '~/common';
import { RelativeDateTime } from '~/components/Formatters';
import { Link } from '~/components/Routing';
import { TextChip } from '~/components/TextChip';
import { ProjectWorkflowEventFragment as WorkflowEvent } from './projectWorkflowEvent.graphql';

const gridAt = 'md' as const;

export const WorkflowEventsList = ({
events,
}: {
type WorkflowEventsListProps = {
events: readonly WorkflowEvent[];
}) => (
<List
sx={(theme) => ({
display: 'flex',
flexDirection: 'column',
[theme.breakpoints.up(gridAt)]: {
display: 'grid',
rowGap: 1,
columnGap: 2,
alignItems: 'center',
gridTemplateColumns:
'[at] min-content [from] min-content [arrow] min-content [to] min-content',
},
})}
>
{events.toReversed().map((event, index, array) => {
const prev = index >= 1 ? array[index - 1] : null;
const prevStatus = prev ? prev.to : ProjectStepList[0]!;
fullWidth?: boolean;
} & ListProps;

export const WorkflowEventsList = forwardRef<any, WorkflowEventsListProps>(
function WorkflowEventsList({ events, fullWidth = false, ...props }, ref) {
return (
<List
{...props}
ref={ref}
sx={[
{
display: 'flex',
flexDirection: 'column',
},
!fullWidth && {
display: 'grid',
rowGap: 1,
columnGap: 2,
alignItems: 'center',
gridTemplateColumns:
'[at] min-content [from] minmax(min-content, 1fr) [arrow] min-content [to] minmax(min-content, 1fr)',
},
...extendSx(props.sx),
]}
>
{events.toReversed().map((event, index, array) => {
const prev = index >= 1 ? array[index - 1] : null;
const prevStatus = prev ? prev.to : ProjectStepList[0]!;

return (
<Box
key={event.id}
sx={(theme) => ({
display: 'contents',
[theme.breakpoints.down(gridAt)]: {
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: 1,
padding: 1,
borderBottom: `thin solid ${theme.palette.divider}`,
},
})}
>
<Box
sx={{
gridColumn: 'at',
// add a bit more row padding between the two
mr: { xs: 1, [gridAt]: 0 },
}}
>
{event.who.value?.__typename === 'User' && (
<Link to={`/users/${event.who.value.id}`} color="inherit">
{event.who.value.fullName}
</Link>
)}
<Typography variant="subtitle2" color="text.secondary" noWrap>
<RelativeDateTime date={event.at} />
</Typography>
</Box>
<Box
sx={(theme) => ({
display: 'contents',
[theme.breakpoints.down(gridAt)]: {
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: 1,
},
})}
>
<TextChip sx={{ gridColumn: 'from' }}>
{ProjectStepLabels[prevStatus]}
</TextChip>
<ChevronRight
sx={{ gridColumn: 'arrow', flexGrow: 0 }}
aria-label="transitioned to"
/>
<TextChip sx={{ gridColumn: 'to' }}>
{ProjectStepLabels[event.to]}
</TextChip>
</Box>
<Divider
sx={{
gridColumn: '1/-1',
display: { xs: 'none', [gridAt]: 'unset' },
}}
/>
</Box>
);
})}
</List>
return (
<Box
key={event.id}
sx={[
{
display: 'contents',
},
fullWidth &&
((theme) => ({
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: 1,
padding: 1,
borderBottom: `thin solid ${theme.palette.divider}`,
})),
]}
>
<Box
sx={{
gridColumn: 'at',
// add a bit more row padding between the two
mr: fullWidth ? 1 : 0,
}}
>
{event.who.value?.__typename === 'User' && (
<Link to={`/users/${event.who.value.id}`} color="inherit">
{event.who.value.fullName}
</Link>
)}
<Typography variant="subtitle2" color="text.secondary" noWrap>
<RelativeDateTime date={event.at} />
</Typography>
</Box>
<Box
sx={[
{ display: 'contents' },
fullWidth && {
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: 1,
},
]}
>
<TextChip sx={{ gridColumn: 'from' }}>
{ProjectStepLabels[prevStatus]}
</TextChip>
<ChevronRight
sx={{ gridColumn: 'arrow', flexGrow: 0 }}
aria-label="transitioned to"
/>
<TextChip sx={{ gridColumn: 'to' }}>
{ProjectStepLabels[event.to]}
</TextChip>
</Box>
<Divider
sx={{
gridColumn: '1/-1',
display: fullWidth ? 'none' : undefined,
}}
/>
</Box>
);
})}
</List>
);
}
);

0 comments on commit 676346b

Please sign in to comment.