Skip to content

Commit

Permalink
Disclosure component (#657)
Browse files Browse the repository at this point in the history
Co-authored-by: Pranay Kothapalli <[email protected]>
  • Loading branch information
jindaljyoti and kotAPI authored Jan 6, 2025
1 parent c3aa440 commit cea0ec7
Show file tree
Hide file tree
Showing 14 changed files with 459 additions and 3 deletions.
2 changes: 1 addition & 1 deletion docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"eslint": "8.48.0",
"eslint-config-next": "15.1.2",
"js-cookie": "^3.0.5",
"next": "15.1.2",
"next": "^15.1.2",
"next-cookies": "^2.0.3",
"node-fetch": "^3.3.2",
"nookies": "^2.5.2",
Expand Down
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions src/components/ui/Disclosure/Disclosure.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react";
import DisclosureRoot from "./fragments/DisclosureRoot";
import DisclosureItem from "./fragments/DisclosureItem";
import DisclosureTrigger from "./fragments/DisclosureTrigger";
import DisclosureContent from "./fragments/DisclosureContent";

export type DisclosureProps = {

items:{title:string, content: React.ReactNode}[]
}

const Disclosure = ({items}:DisclosureProps) => {
return(

<DisclosureRoot>
{items.map((item,index) => (
<DisclosureItem key={index} value={index}>
<DisclosureTrigger>
{item.title}
</DisclosureTrigger>
<DisclosureContent>
{item.content}
</DisclosureContent>
</DisclosureItem>

))}

</DisclosureRoot>
)
}

Disclosure.Root = DisclosureRoot;
Disclosure.Item = DisclosureItem;
Disclosure.Trigger = DisclosureTrigger;
Disclosure.Content = DisclosureContent;

export default Disclosure;
13 changes: 13 additions & 0 deletions src/components/ui/Disclosure/contexts/DisclosureContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {createContext} from "react";

export type DisclosureContextType = {
rootClass: string;
activeItem: number | null;
setActiveItem: (item: number | null) => void;
focusItem: HTMLElement | null;
setFocusItem: (node: HTMLElement | null) => void;
disclosureRef: any;
focusNextItem: () => void;
focusPrevItem: () => void;
}
export const DisclosureContext = createContext<DisclosureContextType>({} as DisclosureContextType)
11 changes: 11 additions & 0 deletions src/components/ui/Disclosure/contexts/DisclosureItemContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {createContext} from "react";

export type DisclosureItemContextType = {
itemValue: number;
setItemValue: (value: number) => void;
handleBlurEvent: () => void;
handleClickEvent: () => void;
handleFocusEvent: () => void;
}

export const DisclosureItemContext = createContext<DisclosureItemContextType>({} as DisclosureItemContextType)
28 changes: 28 additions & 0 deletions src/components/ui/Disclosure/fragments/DisclosureContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useContext, useState } from "react";
import clsx from "clsx";
import { DisclosureContext } from "../contexts/DisclosureContext";
import { DisclosureItemContext } from "../contexts/DisclosureItemContext";

export type DisclosureContentProps = {
children: React.ReactNode;
className?: string;

}

const DisclosureContent = ({children, className=''}:DisclosureContentProps) => {

const {activeItem,rootClass} = useContext(DisclosureContext)
const {itemValue} = useContext(DisclosureItemContext)
return(
<div
className={clsx(`${rootClass}-content`, className)}
hidden={activeItem !== itemValue}
role="region"
aria-hidden={activeItem !== itemValue}
>
{children}
</div>
)
}

export default DisclosureContent
85 changes: 85 additions & 0 deletions src/components/ui/Disclosure/fragments/DisclosureItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { useContext, useEffect, useRef, useState, useId } from "react";
import { DisclosureContext } from "../contexts/DisclosureContext";
import { DisclosureItemContext } from "../contexts/DisclosureItemContext";
import { clsx } from "clsx";

export type DisclosureItemProps = {
children: React.ReactNode;
className?: string;
value: number;
}

const DisclosureItem = ({children, className='', value}:DisclosureItemProps) => {

const disclosureItemRef = useRef<HTMLDivElement>(null)
const { activeItem, rootClass, focusItem } = useContext(DisclosureContext)

const [itemValue, setItemValue] = useState<number>(value)
const [isOpen, setIsOpen] = useState(false)

useEffect(() => {
setIsOpen(activeItem === itemValue)

}, [activeItem, itemValue]);

const id = useId()
let shouldAddFocusDataAttribute = false;

const focusItemId = focusItem?.id;
if (focusItemId === `disclosure-data-item-${id}`)
{
shouldAddFocusDataAttribute = true;
}

const focusCurrentItem = () => {
const elem = disclosureItemRef?.current

if (elem) {
elem.setAttribute('data-rad-ui-focus-element', '')
}

}

const handleBlurEvent = () => {
const elem = disclosureItemRef?.current;

if (elem) {
elem.removeAttribute('data-rad-ui-focus-element')
}
}

const handleClickEvent = () => {
focusCurrentItem()
}

const handleFocusEvent = () => {
focusCurrentItem()
}
return(
<DisclosureItemContext.Provider
value={{
itemValue,
setItemValue,
handleBlurEvent,
handleClickEvent,
handleFocusEvent
}}>
<div
className={clsx(`${rootClass}-item`, className)}
ref={disclosureItemRef}
data-state={isOpen ? 'open' : 'closed'}
id={`disclosure-data-item-${id}`}
role="region"
aria-labelledby={`disclosure-trigger-${id}`}
aria-expanded={isOpen}
data-rad-ui-batch-element
{...shouldAddFocusDataAttribute ? {'data-rad-ui-focus-element': ''} : {}}
>
{children}

</div>
</DisclosureItemContext.Provider>
)
}

export default DisclosureItem
76 changes: 76 additions & 0 deletions src/components/ui/Disclosure/fragments/DisclosureRoot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useState, useRef } from "react";
import { customClassSwitcher } from "~/core";
import { clsx } from "clsx";
import { DisclosureContext } from "../contexts/DisclosureContext";
import { getAllBatchElements, getNextBatchItem, getPrevBatchItem } from "~/core/batches";

const COMPONENT_NAME = 'Disclosure';

export type DisclosureRootProps = {
children: React.ReactNode;
customRootClass?: string;
defaultOpen?: number | null;
'aria-label'?: string;

}

const DisclosureRoot = ({ children, customRootClass, 'aria-label': ariaLabel }:DisclosureRootProps) => {

const disclosureRef = useRef(null)
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME)

const [activeItem, setActiveItem] = useState<number | null>(null);
const [focusItem, setFocusItem] = useState<HTMLElement | null>(null);

const focusNextItem = () => {
const batches = getAllBatchElements(disclosureRef.current!)
const nextItem = getNextBatchItem(batches)
setFocusItem(nextItem as HTMLElement)

if (nextItem){
const button = nextItem.querySelector('button')
button?.focus()
}
}

const focusPrevItem = () => {
const batches = getAllBatchElements(disclosureRef?.current!)
const prevItem = getPrevBatchItem(batches)
setFocusItem(prevItem as HTMLElement)

if (prevItem){
const button = prevItem.querySelector('button')
button?.focus()
}
}

return(

<DisclosureContext.Provider
value={{
rootClass,
activeItem,
setActiveItem,
disclosureRef,
focusNextItem,
focusPrevItem,
focusItem,
setFocusItem

}}>

<div
className={clsx(`${rootClass}-root`)}
ref={disclosureRef}
role="region"
aria-label={ariaLabel}
data-testid='disclosure-root'
>

{children}
</div>
</DisclosureContext.Provider>
)
}

export default DisclosureRoot
56 changes: 56 additions & 0 deletions src/components/ui/Disclosure/fragments/DisclosureTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useContext } from "react"
import clsx from "clsx";
import { DisclosureContext } from "../contexts/DisclosureContext";
import { DisclosureItemContext } from "../contexts/DisclosureItemContext";

export type DisclosureTriggerProps = {
children: React.ReactNode;
className?: string;
}


const DisclosureTrigger = ({ children, className }:DisclosureTriggerProps) => {

const { activeItem, setActiveItem, rootClass, focusNextItem, focusPrevItem } = useContext(DisclosureContext)
const { itemValue, handleBlurEvent, handleClickEvent, handleFocusEvent} = useContext(DisclosureItemContext)

const handleDisclosure = () => {

setActiveItem(activeItem === itemValue ? null : itemValue)
handleClickEvent();
}

const onFocusHandler = () => {
handleFocusEvent()
}

return(

<button
type='button'
className={clsx(`${rootClass}-trigger`, className)}
onClick={handleDisclosure}
onBlur={handleBlurEvent}
onFocus={onFocusHandler}
onKeyDown={(e) => {
if (e.key === 'ArrowDown') {
e.preventDefault()
focusNextItem()
}

if (e.key === 'ArrowUp') {
e.preventDefault()
focusPrevItem()
}
}}
aria-expanded={activeItem === itemValue}
aria-haspopup='true'
>

{children}
</button>

)
}

export default DisclosureTrigger
Loading

0 comments on commit cea0ec7

Please sign in to comment.