-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[To Main] DESENG-632: Add engagement content tabs #2545
Changes from all commits
6371671
3019b2e
e67dacf
dba6605
c6f9472
2d7be4c
ce6a068
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import React, { Suspense, SyntheticEvent, useCallback, useState } from 'react'; | ||
import { Tab, Skeleton, Box } from '@mui/material'; | ||
import { TabContext, TabList, TabPanel } from '@mui/lab'; | ||
import { Await, useLoaderData } from 'react-router-dom'; | ||
import { EngagementContent } from 'models/engagementContent'; | ||
import { EngagementSummaryContent } from 'models/engagementSummaryContent'; | ||
import { Editor } from 'react-draft-wysiwyg'; | ||
import { getEditorStateFromRaw } from 'components/common/RichTextEditor/utils'; | ||
import { Header2 } from 'components/common/Typography'; | ||
import { colors } from 'components/common'; | ||
|
||
export const EngagementContentTabs = () => { | ||
const { content, contentSummary } = useLoaderData() as { | ||
content: Promise<EngagementContent[]>; | ||
contentSummary: Promise<EngagementSummaryContent[][]>; | ||
}; | ||
const [selectedTab, setSelectedTab] = useState('0'); | ||
const handleChange = (event: SyntheticEvent<Element, Event>, newValue: string) => { | ||
setSelectedTab(newValue); | ||
}; | ||
|
||
const panelContents = Promise.all([content, contentSummary]); | ||
|
||
const tabListRef = useCallback((node: HTMLButtonElement) => { | ||
if (!node) return; | ||
const scroller = node.getElementsByClassName('MuiTabs-scroller')[0]; | ||
scroller.addEventListener('scroll', () => checkFade(node)); // check when scrolling | ||
const resizeObserver = new ResizeObserver(() => checkFade(node)); | ||
resizeObserver.observe(scroller); // check when window resizes | ||
checkFade(node); // initial check when attaching the ref | ||
}, []); | ||
|
||
const checkFade = (node: HTMLButtonElement) => { | ||
if (!node) return; | ||
const scroller = node.getElementsByClassName('MuiTabs-scroller')[0]; | ||
const scrollPosition = scroller.scrollLeft; // distance from left edge | ||
const maxScroll = scroller.scrollWidth - scroller.clientWidth; // distance from right edge | ||
const fadeMargin = 64; // pixels | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fade is a nice idea to make it clear to users that there's more to the tablist if they scroll. As keyboard users tab to the right, will it be clear where their focus is at all times? Will focus ever be obscured by the fade? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discussed this offline: keyboard users should be ok |
||
if (maxScroll - scrollPosition < fadeMargin) { | ||
node.classList.remove('fade-right'); | ||
} else { | ||
node.classList.add('fade-right'); | ||
} | ||
}; | ||
|
||
return ( | ||
<section id="content-tabs" aria-label="Engagement content tabs"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice aria label for this region! We don't have a heading tag to lean on so I think this is adequately descriptive of the content to follow :) |
||
<Box | ||
sx={{ | ||
padding: { xs: '0 16px 24px 16px', md: '0 5vw 40px 5vw', lg: '0 156px 40px 156px' }, | ||
marginTop: '-32px', | ||
position: 'relative', | ||
zIndex: 10, | ||
}} | ||
> | ||
<TabContext value={selectedTab}> | ||
<Box sx={{}}> | ||
<Suspense fallback={<Skeleton variant="rectangular" sx={{ width: '300px', height: '81px' }} />}> | ||
<Await resolve={content}> | ||
{(resolvedContent: EngagementContent[]) => ( | ||
<TabList | ||
ref={tabListRef} | ||
onChange={handleChange} | ||
variant="scrollable" | ||
scrollButtons={false} | ||
TabIndicatorProps={{ | ||
sx: { marginBottom: '16px' }, | ||
}} | ||
sx={{ | ||
width: { | ||
xs: 'calc(100% + 16px)', | ||
md: 'calc(100% + 5vw)', | ||
lg: 'calc(100% + 156px)', | ||
}, | ||
'&.fade-right::after': { | ||
// fade out the right edge of the tab list | ||
content: '""', | ||
display: 'block', | ||
position: 'absolute', | ||
top: 0, | ||
right: 0, | ||
width: '48px', | ||
height: '100%', | ||
background: | ||
'linear-gradient(to left, rgba(255,255,255,1) 0%, rgba(255,255,255,0.8) 5%, rgba(255,255,255,0.8) 50%, rgba(255,255,255,0) 100%)', | ||
pointerEvents: 'none', //allow clicking on faded tabs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I never knew about this CSS attribute! Neat |
||
}, | ||
'& .MuiTabs-indicator': { | ||
display: 'flex', | ||
justifyContent: 'center', | ||
height: '6px', | ||
backgroundColor: colors.surface.blue[90], | ||
}, | ||
'& .MuiTabs-scroller': { | ||
width: 'max-content', | ||
paddingBottom: '16px', | ||
}, | ||
'& .MuiTabs-flexContainer': { | ||
justifyContent: 'flex-start', | ||
width: 'max-content', | ||
}, | ||
}} | ||
> | ||
<Box sx={{ width: '16px', background: 'white' }}></Box> | ||
{resolvedContent.map((section, index) => { | ||
return ( | ||
<Tab | ||
sx={{ | ||
background: 'white', | ||
color: colors.type.regular.primary, | ||
fontSize: '14px', | ||
fontWeight: 'normal', | ||
padding: '24px 8px', | ||
paddingLeft: '8px', | ||
'&.Mui-selected': { | ||
fontWeight: 'bold', | ||
color: colors.surface.blue[90], | ||
}, | ||
}} | ||
key={section.id} | ||
label={section.title} | ||
value={index.toString()} | ||
/> | ||
); | ||
})} | ||
<Box | ||
sx={{ | ||
background: 'white', | ||
width: '24px', | ||
borderRadius: '0px 24px 0px 0px', | ||
marginRight: { xs: '16px', md: '5vw', lg: '156px' }, | ||
}} | ||
></Box> | ||
</TabList> | ||
)} | ||
</Await> | ||
</Suspense> | ||
</Box> | ||
<Suspense fallback={<Skeleton variant="rectangular" sx={{ width: '100%', height: '120px' }} />}> | ||
<Await resolve={panelContents}> | ||
{([content, contentSummary]: [ | ||
content: EngagementContent[], | ||
contentSummary: EngagementSummaryContent[][], | ||
]) => | ||
contentSummary.map((summary, index) => ( | ||
<TabPanel key={summary[0].id} value={index.toString()} sx={{ padding: '24px 0px' }}> | ||
<Header2 decorated weight="thin"> | ||
{content[index].title} | ||
</Header2> | ||
{summary.map((content, index) => ( | ||
<Editor | ||
key={content.id} | ||
editorState={getEditorStateFromRaw(content.rich_content)} | ||
readOnly={true} | ||
toolbarHidden | ||
/> | ||
))} | ||
</TabPanel> | ||
)) | ||
} | ||
</Await> | ||
</Suspense> | ||
</TabContext> | ||
</Box> | ||
</section> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,37 @@ | ||
import { EngagementCustomContent } from 'models/engagementCustomContent'; | ||
import { EngagementSummaryContent } from 'models/engagementSummaryContent'; | ||
import { Params, defer } from 'react-router-dom'; | ||
import { getEngagementContent } from 'services/engagementContentService'; | ||
import { getCustomContent } from 'services/engagementCustomService'; | ||
import { getEngagement } from 'services/engagementService'; | ||
import { getEngagementIdBySlug } from 'services/engagementSlugService'; | ||
import { getSummaryContent } from 'services/engagementSummaryService'; | ||
import { getWidgets } from 'services/widgetService'; | ||
|
||
const castCustomContentToSummaryContent = (customContent: EngagementCustomContent[]): EngagementSummaryContent[] => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice descriptive function name |
||
return customContent.map((custom) => ({ | ||
id: custom.id, | ||
content: custom.custom_text_content, | ||
rich_content: custom.custom_json_content, | ||
engagement_id: custom.engagement_id, | ||
engagement_content_id: custom.engagement_content_id, | ||
})); | ||
}; | ||
|
||
export const engagementLoader = async ({ params }: { params: Params<string> }) => { | ||
const { slug } = params; | ||
const engagement = getEngagementIdBySlug(slug ?? '').then((response) => getEngagement(response.engagement_id)); | ||
const widgets = engagement.then((response) => getWidgets(response.id)); | ||
return defer({ engagement, slug, widgets }); | ||
const content = engagement.then((response) => getEngagementContent(response.id)); | ||
const contentSummary = content.then((response) => | ||
Promise.all( | ||
response.map((content) => { | ||
return content.content_type === 'Summary' | ||
? getSummaryContent(content.id) | ||
: getCustomContent(content.id).then(castCustomContentToSummaryContent); | ||
}), | ||
), | ||
); | ||
|
||
return defer({ engagement, slug, widgets, content, contentSummary }); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the comments so we can follow along more easily!