-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Dynamically adjust between grid & flex layout based on actual list width
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
Showing
3 changed files
with
189 additions
and
126 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 84 additions & 31 deletions
115
src/scenes/Projects/WorkflowEvents/WorkflowEventsDrawer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
187
src/scenes/Projects/WorkflowEvents/WorkflowEventsList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} | ||
); |