-
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.
- Loading branch information
Showing
8 changed files
with
2,233 additions
and
360 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import type {Meta, StoryObj} from '@storybook/react'; | ||
import Accordion from "@components/elements/accordion"; | ||
|
||
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction | ||
const meta: Meta<typeof Accordion> = { | ||
title: 'Design/Elements/Accordion', | ||
component: Accordion, | ||
tags: ['autodocs'], | ||
argTypes: { | ||
button: { | ||
control: "text" | ||
}, | ||
onClick: { | ||
table: { | ||
disable: true, | ||
} | ||
}, | ||
buttonProps: { | ||
table: { | ||
disable: true, | ||
} | ||
}, | ||
panelProps: { | ||
table: { | ||
disable: true, | ||
} | ||
}, | ||
isVisible: { | ||
table: { | ||
disable: true, | ||
} | ||
} | ||
} | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof Accordion>; | ||
|
||
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args | ||
export const AccordionElement: Story = { | ||
render: ({onClick, ...args}) => { | ||
return <Accordion {...args}/> | ||
}, | ||
args: { | ||
button: "Id arcu nec vel tempus rutrum.", | ||
children: "Mi amet tempus congue erat fusce euismod eros cursus morbi amet amet diam tristique bibendum hendrerit sed commodo quisque cursus scelerisque morbi placerat tristique magna." | ||
}, | ||
}; |
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,85 @@ | ||
"use client"; | ||
|
||
import {HTMLAttributes, JSX, useId} from "react"; | ||
import {useBoolean} from "usehooks-ts"; | ||
import {H2, H3, H4} from "@components/elements/headers"; | ||
import {ChevronDownIcon} from "@heroicons/react/20/solid"; | ||
import {clsx} from "clsx"; | ||
import {twMerge} from "tailwind-merge"; | ||
|
||
type Props = HTMLAttributes<HTMLElement> & { | ||
/** | ||
* Button clickable element or string. | ||
*/ | ||
button: JSX.Element | string | ||
/** | ||
* Heading level element. | ||
*/ | ||
headingLevel?: 'h2' | 'h3' | 'h4' | ||
/** | ||
* If the accordion should be visible on first render. | ||
*/ | ||
initiallyVisible?: boolean | ||
/** | ||
* Button click event if the component is controlled. | ||
*/ | ||
onClick?: () => void | ||
/** | ||
* Panel visibility state if the component is controlled. | ||
*/ | ||
isVisible?: boolean | ||
buttonProps?: HTMLAttributes<HTMLButtonElement> | ||
panelProps?: HTMLAttributes<HTMLDivElement> | ||
} | ||
|
||
const Accordion = ({ | ||
button, | ||
children, | ||
headingLevel = 'h2', | ||
onClick, | ||
isVisible, | ||
initiallyVisible = false, | ||
buttonProps, | ||
panelProps, | ||
...props | ||
}: Props) => { | ||
const {value: expanded, toggle: toggleExpanded} = useBoolean(initiallyVisible) | ||
const id = useId(); | ||
|
||
const onButtonClick = () => { | ||
onClick ? onClick() : toggleExpanded() | ||
} | ||
|
||
// When the accordion is externally controlled. | ||
const isExpanded = onClick ? isVisible : expanded; | ||
|
||
const Heading = headingLevel === 'h2' ? H2 : headingLevel === 'h3' ? H3 : H4; | ||
return ( | ||
<section aria-labelledby={`${id}-button`} {...props}> | ||
<Heading> | ||
<button | ||
{...buttonProps} | ||
className={twMerge("w-full items-center flex border-b border-transparent hocus:border-black-true", buttonProps?.className)} | ||
id={`${id}-button`} | ||
aria-expanded={isExpanded} | ||
aria-controls={`${id}-panel`} | ||
onClick={onButtonClick} | ||
> | ||
{button} | ||
<ChevronDownIcon height={30} className={clsx("shrink-0 ml-auto duration-150", {"rotate-180": isExpanded})}/> | ||
</button> | ||
</Heading> | ||
|
||
<div | ||
{...panelProps} | ||
id={`${id}-panel`} | ||
className={twMerge(isExpanded ? "block" : "hidden", panelProps?.className)} | ||
role="region" | ||
aria-labelledby={`${id}-button`} | ||
> | ||
{children} | ||
</div> | ||
</section> | ||
) | ||
} | ||
export default Accordion; |
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,125 @@ | ||
"use client"; | ||
|
||
import {AriaTabListOptions, Key, useTab, useTabList, useTabPanel} from 'react-aria'; | ||
import {Item, TabListState, useTabListState} from 'react-stately'; | ||
import {JSX, useRef} from "react"; | ||
import {TabListStateOptions} from "@react-stately/tabs"; | ||
import {Node} from "@react-types/shared/src/collections"; | ||
import {AriaTabPanelProps} from "@react-types/tabs"; | ||
import {clsx} from "clsx"; | ||
import {useRouter, useSearchParams} from "next/navigation"; | ||
import {useIsClient, useWindowSize} from "usehooks-ts"; | ||
|
||
type Props = AriaTabListOptions<JSX.Element> & TabListStateOptions<JSX.Element> & { | ||
paramId?: string | ||
tabs: { id: string, title: string, contents: JSX.Element | string }[] | ||
} | ||
|
||
const Tabs = ({tabs, orientation = "horizontal", ...props}: Props) => { | ||
if (!props['aria-label'] && !props['aria-labelledby']) console.warn('Tabs missing appropriate aria labelling'); | ||
|
||
const isClient = useIsClient(); | ||
const {width: windowWidth = 0} = useWindowSize({debounceDelay: 200}) | ||
const adjustedOrientation = windowWidth < 550 ? "vertical" : windowWidth >= 992 ? orientation : "horizontal" | ||
|
||
if (isClient) return ( | ||
<TabsWrapper {...props} isDisabled={tabs.length === 1} orientation={adjustedOrientation}> | ||
{tabs.map(tab => | ||
<Item key={`tab-${tab.id}`} title={tab.title}> | ||
{tab.contents} | ||
</Item> | ||
)} | ||
</TabsWrapper> | ||
) | ||
|
||
// Server rendering only prints the tabs. We also want to display at least the first tab panel on the server and | ||
// allow the client to rehydrate. | ||
return ( | ||
<div className={clsx("flex", {"flex-col gap-5": orientation !== "vertical"})}> | ||
<div className={clsx("flex", {"flex-col w-1/3": orientation === "vertical"})}> | ||
{tabs.map((item, i) => ( | ||
<div | ||
key={item.id} | ||
className={clsx('text-left p-5', { | ||
'border-l-3': orientation === "vertical", | ||
'border-b-3': orientation !== "vertical", | ||
'border-cardinal-red': i === 0, | ||
'border-transparent': i !== 0, | ||
})} | ||
> | ||
{item.title} | ||
</div> | ||
))} | ||
</div> | ||
<div> | ||
{tabs[0]?.contents} | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
const TabsWrapper = ({paramId = 'tab', ...props}: AriaTabListOptions<JSX.Element> & TabListStateOptions<JSX.Element> & { | ||
paramId?: string | ||
}) => { | ||
const router = useRouter() | ||
const searchParams = useSearchParams(); | ||
const defaultSelectedKey = paramId ? (searchParams.get(paramId) || undefined) : undefined | ||
|
||
const onSelectionChange = (key: Key) => { | ||
const params = new URLSearchParams(searchParams); | ||
key === state.collection.getFirstKey() ? params.delete(paramId) : params.set(paramId, `${key}`) | ||
router.replace(`?${params.toString()}`, {scroll: false}) | ||
} | ||
|
||
let state = useTabListState({...props, onSelectionChange, defaultSelectedKey}); | ||
let ref = useRef<HTMLDivElement>(null); | ||
let {tabListProps} = useTabList(props, state, ref); | ||
|
||
return ( | ||
<div className={clsx("flex flex-col", {"lg:flex-row gap-5": props.orientation === "vertical"})}> | ||
<div {...tabListProps} ref={ref} className={clsx("flex", {"flex-col w-1/3": props.orientation === "vertical"})}> | ||
{[...state.collection].map(item => ( | ||
<Tab key={item.key} item={item} state={state} orientation={props.orientation}/> | ||
))} | ||
</div> | ||
<TabPanel key={state.selectedItem?.key} state={state}/> | ||
</div> | ||
); | ||
} | ||
|
||
|
||
const Tab = ({item, state, orientation}: { | ||
item: Node<JSX.Element>, | ||
state: TabListState<JSX.Element>, | ||
orientation?: AriaTabListOptions<JSX.Element>["orientation"] | ||
}) => { | ||
let {key, rendered} = item; | ||
let ref = useRef<HTMLButtonElement>(null); | ||
let {tabProps} = useTab({key}, state, ref); | ||
const isActive = tabProps['aria-selected'] | ||
return ( | ||
<button | ||
{...tabProps} | ||
ref={ref} | ||
className={clsx('text-left p-5', { | ||
'border-l-3': orientation === "vertical", | ||
'border-b-3': orientation !== "vertical", | ||
'border-cardinal-red': isActive, | ||
'border-transparent': !isActive | ||
})} | ||
> | ||
{rendered} | ||
</button> | ||
); | ||
} | ||
|
||
const TabPanel = ({state, ...props}: { state: TabListState<JSX.Element> } & AriaTabPanelProps) => { | ||
let ref = useRef<HTMLDivElement>(null); | ||
let {tabPanelProps} = useTabPanel(props, state, ref); | ||
return ( | ||
<div {...tabPanelProps} ref={ref}> | ||
{state.selectedItem?.props.children} | ||
</div> | ||
); | ||
} | ||
export default Tabs; |
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
Oops, something went wrong.