-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Clone Agenda/TabGroup component and other utilities from Tour * Update font
- Loading branch information
1 parent
752d816
commit 8b869ac
Showing
10 changed files
with
335 additions
and
6 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
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
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 |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { storyblokEditable } from '@storyblok/react/rsc'; | ||
import { Tabs } from '@/components/Tabs'; | ||
import { type SbTabItemType } from '@/components/Storyblok/Storyblok.types'; | ||
import { type HeadingType } from '@/components/Typography'; | ||
import { type AnimationType } from '@/components/Animate'; | ||
|
||
export type SbTabGroupProps = { | ||
blok: { | ||
_uid: string; | ||
isHidden?: boolean; | ||
tabItems?: SbTabItemType[]; | ||
id?: string; | ||
headingLevel?: HeadingType; | ||
isSerifHeading?: boolean; | ||
isLightText?: boolean; | ||
animation?: AnimationType; | ||
}; | ||
}; | ||
export const SbTabGroup = ({ | ||
blok: { | ||
isHidden, | ||
tabItems, | ||
id, | ||
headingLevel, | ||
isSerifHeading, | ||
isLightText, | ||
animation, | ||
}, | ||
blok, | ||
}: SbTabGroupProps) => { | ||
if (isHidden || !tabItems?.length) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<Tabs | ||
{...storyblokEditable(blok)} | ||
id={id} | ||
tabItems={tabItems} | ||
headingLevel={headingLevel || 'h3'} | ||
isSerifHeading={isSerifHeading} | ||
isLightText={isLightText} | ||
animation={animation} | ||
/> | ||
); | ||
}; |
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
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
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 |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { cnb } from 'cnbuilder'; | ||
|
||
export const headingSizes = { | ||
small: 'fluid-type-4', | ||
medium: 'fluid-type-4 md:fluid-type-5', | ||
large: 'fluid-type-4 md:fluid-type-6', | ||
}; | ||
export type TabItemHeadingSizeType = keyof typeof headingSizes; | ||
|
||
export const root = 'relative cc break-words scroll-mt-80'; | ||
export const tabGroup = 'hidden sm:grid grid-cols-12'; | ||
export const tabList = (isKeyboardUser: boolean) => cnb('flex flex-col col-span-4', isKeyboardUser && 'focus-within:outline focus-within:outline-digital-blue'); | ||
export const tabItem = (isLightText: boolean) => cnb('relative data-[hover]:underline data-[hover]:underline-offset-4 data-[selected]:outline-none text-left md:pl-20 pr-20 lg:px-26 py-20 lg:py-26 type-1 lg:text-[2.7rem] xl:text-[3.4rem] font-normal data-[selected]:underline data-[selected]:underline-offset-4 leading-display transition-colors', | ||
isLightText ? | ||
'text-black-30 data-[selected]:text-white hover:text-white' | ||
: 'text-black/60 data-[selected]:text-gc-black hover:text-gc-black', | ||
); | ||
export const tabItemBar = (isLightText: boolean) => cnb('h-full w-20 absolute right-0 top-0 ', | ||
isLightText ? 'bg-digital-red-light' : 'bg-digital-red', | ||
); | ||
export const tabPanel = (isLightText: boolean) => cnb('border-l pl-20 lg:pl-30 xl:pl-36 col-span-8', | ||
isLightText ? 'border-black-50' : 'border-black-60', | ||
); | ||
|
||
export const superhead = 'rs-mt-2 first:mt-0 mb-04em'; | ||
export const heading = (headingSize: TabItemHeadingSizeType) => headingSizes[headingSize || 'medium']; | ||
export const mobileGrid = 'sm:hidden list-unstyled'; | ||
export const li = 'scroll-mt-30'; |
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 |
---|---|---|
@@ -0,0 +1,205 @@ | ||
import { | ||
Fragment, useEffect, useId, useRef, useState, | ||
} from 'react'; | ||
import { | ||
Tab, TabGroup, TabList, TabPanel, TabPanels, | ||
} from '@headlessui/react'; | ||
import { m } from 'framer-motion'; | ||
import { useMediaQuery } from 'usehooks-ts'; | ||
import { useKeyboard } from '@/hooks/useKeyboard'; | ||
import { AnimateInView, type AnimationType } from '@/components/Animate'; | ||
import { CreateBloks } from '@/components/CreateBloks'; | ||
import { Grid } from '@/components/Grid'; | ||
import { RichText } from '@/components/RichText'; | ||
import { | ||
Heading, SrOnlyText, Text, type HeadingType, | ||
} from '@/components/Typography'; | ||
import { type SbTabItemType } from '@/components/Storyblok/Storyblok.types'; | ||
import { hasRichText } from '@/utilities/hasRichText'; | ||
import { slugify } from '@/utilities/slugify'; | ||
import { config } from '@/utilities/config'; | ||
import * as styles from './Tabs.styles'; | ||
|
||
type TabsProps = React.HTMLAttributes<HTMLDivElement> & { | ||
tabItems: SbTabItemType[]; | ||
isSerifHeading?: boolean; | ||
headingLevel?: HeadingType; | ||
isLightText?: boolean; | ||
animation?: AnimationType; | ||
}; | ||
|
||
type TabContentProps = Omit<TabsProps, 'tabItems'> & Omit<SbTabItemType, '_uid'>; | ||
|
||
/** | ||
* Content inside each tab item that will be display expanded on mobile | ||
*/ | ||
const TabContent = ({ | ||
isSerifHeading, | ||
headingLevel, | ||
headingSize, | ||
isLightText, | ||
animation, | ||
label, | ||
heading, | ||
featuredMedia, | ||
id, | ||
body, | ||
otherContent, | ||
}: TabContentProps) => ( | ||
<AnimateInView animation={animation} id={id}> | ||
<CreateBloks blokSection={featuredMedia} /> | ||
<Text | ||
size={1} | ||
weight="semibold" | ||
aria-hidden="true" | ||
color={isLightText ? 'white' : 'black'} | ||
leading="display" | ||
className={styles.superhead} | ||
> | ||
{label} | ||
</Text> | ||
<Heading | ||
as={headingLevel} | ||
font={isSerifHeading ? 'serif' : 'druk'} | ||
color={isLightText ? 'white' : 'black'} | ||
className={styles.heading(headingSize)} | ||
> | ||
<SrOnlyText>{`${label}:`}</SrOnlyText>{heading} | ||
</Heading> | ||
{hasRichText(body) && ( | ||
<RichText | ||
wysiwyg={body} | ||
textColor={isLightText ? 'white' : 'black'} | ||
linkColor={isLightText ? 'digital-red-xlight' : 'unset'} | ||
/> | ||
)} | ||
<CreateBloks blokSection={otherContent} /> | ||
</AnimateInView> | ||
); | ||
|
||
export const Tabs = ({ | ||
tabItems, | ||
isSerifHeading, | ||
headingLevel, | ||
isLightText, | ||
animation, | ||
id, | ||
...props | ||
}: TabsProps) => { | ||
const isKeyboardUser = useKeyboard(); | ||
|
||
// We only render the component as tabs on SM breakpoint and above | ||
const isRenderTabs = useMediaQuery(`(min-width: ${config.breakpoints.sm}px)`); | ||
|
||
/** | ||
* We need a unique id for each tab group when there are multiple | ||
* on a page for the framer motion layout animation to work | ||
*/ | ||
const tabGroupId = encodeURIComponent(useId()); | ||
const tabGroupRef = useRef<HTMLDivElement>(null); | ||
const [selectedIndex, setSelectedIndex] = useState(0); | ||
|
||
/** | ||
* We need a unique prefix for each tab group to set the hash in the URL | ||
* The user can add an id through storyblok, but we use the tabGroupId as a fallback | ||
*/ | ||
const uniquePrefix = `${id || tabGroupId}-`; | ||
|
||
const handleTabChange = (index: number) => { | ||
setSelectedIndex(index); | ||
const tabHash = `#${uniquePrefix}${slugify(tabItems[index].label)}`; | ||
window.history.replaceState(null, '', tabHash); // Update hash without adding to history | ||
}; | ||
|
||
// Check URL hash on initial load, update the selected tab and scroll to the correct position | ||
useEffect(() => { | ||
const pageHash = window.location.hash.slice(1); // Remove the "#" from the hash | ||
|
||
// Check if the current page hash starts with the unique prefix | ||
if (pageHash.startsWith(uniquePrefix)) { | ||
// Remove the unique prefix from the page hash | ||
const strippedHash = pageHash.replace(uniquePrefix, ''); | ||
|
||
// Find the index of the tab item with a tab hash that matches the stripped page shash | ||
const index = tabItems.findIndex(tabItem => slugify(tabItem.label) === strippedHash); | ||
|
||
if (index !== -1) { | ||
/** | ||
* For SM breakpoint and above, if the page hash matches a tab hash, | ||
* set that tab as active and scroll to the top of the correct tab group | ||
*/ | ||
if (isRenderTabs) { | ||
setSelectedIndex(index); | ||
tabGroupRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); | ||
} | ||
// On mobile (XS), scroll to the id anchor at the top of the exposed item content | ||
else { | ||
const element = document.getElementById(`#${uniquePrefix}${slugify(tabItems[index].label)}`); | ||
element?.scrollIntoView({ behavior: 'smooth', block: 'start' }); | ||
} | ||
} | ||
} | ||
}, [isRenderTabs, tabItems, uniquePrefix]); | ||
|
||
return ( | ||
<div ref={tabGroupRef} className={styles.root} {...props} id={id}> | ||
{/* For SM breakpoint and above, display tab group */} | ||
<TabGroup vertical className={styles.tabGroup} selectedIndex={selectedIndex} onChange={handleTabChange}> | ||
<TabList className={styles.tabList(isKeyboardUser)}> | ||
{tabItems?.map((tabItem) => ( | ||
<Tab as={Fragment} key={tabItem._uid}> | ||
{({ selected }) => ( | ||
<button className={styles.tabItem(isLightText)}> | ||
{tabItem.label} | ||
{selected && ( | ||
<m.div | ||
className={styles.tabItemBar(isLightText)} | ||
layoutId={tabGroupId} | ||
/> | ||
)} | ||
</button> | ||
)} | ||
</Tab> | ||
))} | ||
</TabList> | ||
<TabPanels className={styles.tabPanel(isLightText)}> | ||
{tabItems?.map((tabItem) => ( | ||
<TabPanel key={tabItem._uid}> | ||
<TabContent | ||
label={tabItem.label} | ||
heading={tabItem.heading} | ||
featuredMedia={tabItem.featuredMedia} | ||
body={tabItem.body} | ||
otherContent={tabItem.otherContent} | ||
headingSize={tabItem.headingSize} | ||
isSerifHeading={isSerifHeading} | ||
headingLevel={headingLevel || 'h3'} | ||
isLightText={isLightText} | ||
animation={animation} | ||
/> | ||
</TabPanel> | ||
))} | ||
</TabPanels> | ||
</TabGroup> | ||
{/* For mobile (XS only), display expanded list of all the tab item content */} | ||
<Grid as="ul" gap="card" className={styles.mobileGrid}> | ||
{tabItems.map((tabItem, index) => ( | ||
<li key={tabItem._uid} id={`#${uniquePrefix}${slugify(tabItems[index].label)}`} className={styles.li}> | ||
<TabContent | ||
label={tabItem.label} | ||
heading={tabItem.heading} | ||
featuredMedia={tabItem.featuredMedia} | ||
body={tabItem.body} | ||
otherContent={tabItem.otherContent} | ||
headingSize={tabItem.headingSize} | ||
isSerifHeading={isSerifHeading} | ||
headingLevel={headingLevel || 'h3'} | ||
isLightText={isLightText} | ||
animation={animation} | ||
/> | ||
</li> | ||
))} | ||
</Grid> | ||
</div> | ||
); | ||
}; |
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 |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './Tabs'; | ||
export * from './Tabs.styles'; |
Oops, something went wrong.