Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions src/components/ui/Accordion/contexts/AccordionContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,17 @@ import { createContext } from 'react';
interface AccordionContextType {
rootClass?: string | null;
activeItem?: number | null;
focusItem?: Element | null;
setActiveItem: (item: number | null) => void;
setFocusItem: (item: Element) => void;
focusNextItem: () => void;
focusPrevItem: () => void;
accordionRef?: React.RefObject<HTMLDivElement | null>;
}
transitionDuration?: number;
transitionTimingFunction?: string;
}

export const AccordionContext = createContext<AccordionContextType>({
rootClass: '',
activeItem: null,
focusItem: null,
setActiveItem: () => {},
setFocusItem: () => {},
focusNextItem: () => {},
focusPrevItem: () => {},
accordionRef: undefined
accordionRef: undefined,
transitionDuration: 0,
transitionTimingFunction: 'ease-out'
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@ import { createContext } from 'react';
interface AccordionItemContextType {
itemValue: number;
setItemValue: (value: number) => void;
handleBlurEvent: (e: React.FocusEvent<HTMLButtonElement>) => void;
handleClickEvent: (e: React.MouseEvent<HTMLButtonElement>) => void;
handleFocusEvent: (e: React.FocusEvent<HTMLButtonElement>) => void;
}

export const AccordionItemContext = createContext<AccordionItemContextType>({
itemValue: 0,
setItemValue: () => {},
handleBlurEvent: () => {},
handleClickEvent: () => {},
handleFocusEvent: () => {}
setItemValue: () => {}
});
7 changes: 5 additions & 2 deletions src/components/ui/Accordion/fragments/AccordionContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import React, { useContext } from 'react';
import { AccordionContext } from '../contexts/AccordionContext';
import { AccordionItemContext } from '../contexts/AccordionItemContext';

import CollapsiblePrimitive from '~/core/primitives/Collapsible';

type AccordionContentProps = {
children: React.ReactNode;
index: number,
Expand All @@ -19,7 +21,8 @@ const AccordionContent: React.FC<AccordionContentProps> = ({ children, index, cl
return (
itemValue !== activeItem
? null
: <div
: <CollapsiblePrimitive.Content
asChild
className={clsx(`${rootClass}-content`, className)}
id={`content-${index}`}
role="region"
Expand All @@ -29,7 +32,7 @@ const AccordionContent: React.FC<AccordionContentProps> = ({ children, index, cl

{children}

</div>
</CollapsiblePrimitive.Content>
);
};

Expand Down
6 changes: 4 additions & 2 deletions src/components/ui/Accordion/fragments/AccordionHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React from 'react';
import React, { useContext } from 'react';
import { clsx } from 'clsx';
import { AccordionContext } from '../contexts/AccordionContext';

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

const AccordionHeader: React.FC<AccordionHeaderProps> = ({ children, className = '' }) => {
const { rootClass } = useContext(AccordionContext);
return (
<div className={clsx(className)}>
<div className={clsx(`${rootClass}-header`, className)}>
{children}
</div>
);
Expand Down
64 changes: 20 additions & 44 deletions src/components/ui/Accordion/fragments/AccordionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { clsx } from 'clsx';
import { AccordionContext } from '../contexts/AccordionContext';
import { AccordionItemContext } from '../contexts/AccordionItemContext';

import CollapsiblePrimitive from '~/core/primitives/Collapsible';

export type AccordionItemProps = {
children: React.ReactNode;
className?: string;
Expand All @@ -13,7 +15,7 @@ export type AccordionItemProps = {
const AccordionItem: React.FC<AccordionItemProps> = ({ children, value, className = '', ...props }) => {
const accordionItemRef = useRef<HTMLDivElement>(null);
const [itemValue, setItemValue] = useState(value ?? 0);
const { rootClass, activeItem, focusItem } = useContext(AccordionContext);
const { rootClass, activeItem, transitionDuration, transitionTimingFunction } = useContext(AccordionContext);

const [isOpen, setIsOpen] = useState(itemValue === activeItem);
useEffect(() => {
Expand All @@ -25,52 +27,26 @@ const AccordionItem: React.FC<AccordionItemProps> = ({ children, value, classNam
}, [activeItem]);

const id = useId();
let shouldAddFocusDataAttribute = false; // this flag is used to indicate if we should add `data-rad-ui-focus-element` attribute to the accordion item on mount
const focusItemId = focusItem?.id;

if (focusItemId === `accordion-data-item-${id}`) {
shouldAddFocusDataAttribute = true;
}

const focusCurrentItem = () => {
const elem = accordionItemRef?.current;
// set `data-rad-ui-focus-element` we are making it active and focusing on this item
if (elem) {
elem.setAttribute('data-rad-ui-focus-element', '');
}
};

const handleBlurEvent = (e: any) => {
// if clicked outside of the accordion, set activeItem to null
const elem = accordionItemRef?.current;

// remove `data-rad-ui-focus-element` attribute as we are not focusing on this item anymore
if (elem) {
elem.removeAttribute('data-rad-ui-focus-element');
}
};

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

const handleFocusEvent = () => {
focusCurrentItem();
};

return (
<AccordionItemContext.Provider value={{ itemValue, setItemValue, handleBlurEvent, handleClickEvent, handleFocusEvent }}>
<div
ref={accordionItemRef}
className={clsx(`${rootClass}-item`, className)} {...props}
id={`accordion-data-item-${id}`}
role="region"
data-state={isOpen ? 'open' : 'closed'}
data-rad-ui-batch-element
{...shouldAddFocusDataAttribute ? { 'data-rad-ui-focus-element': '' } : {}}
<AccordionItemContext.Provider value={{ itemValue, setItemValue }}>
<CollapsiblePrimitive.Root
open={isOpen}
transitionDuration={transitionDuration}
transitionTimingFunction={transitionTimingFunction}
asChild
>
{children}
</div>
<div
ref={accordionItemRef}
className={clsx(`${rootClass}-item`, className)} {...props}
id={`accordion-data-item-${id}`}
role="region"
data-state={isOpen ? 'open' : 'closed'}
>
{children}
</div>
</CollapsiblePrimitive.Root>

</AccordionItemContext.Provider>
);
};
Expand Down
57 changes: 17 additions & 40 deletions src/components/ui/Accordion/fragments/AccordionRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,44 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef } from 'react';
import { clsx } from 'clsx';
import { customClassSwitcher } from '~/core';
import { AccordionContext } from '../contexts/AccordionContext';
import { getAllBatchElements, getNextBatchItem, getPrevBatchItem } from '~/core/batches';

import RovingFocusGroup from '~/core/utils/RovingFocusGroup';

const COMPONENT_NAME = 'Accordion';

export type AccordionRootProps = {
children: React.ReactNode;
customRootClass?: string;
transitionDuration?: number;
transitionTimingFunction?: string;
direction?: 'horizontal' | 'vertical';
}

const AccordionRoot = ({ children, customRootClass }: AccordionRootProps) => {
const AccordionRoot = ({ children, direction = 'vertical', transitionDuration = 0, transitionTimingFunction = 'linear', customRootClass }: AccordionRootProps) => {
const accordionRef = useRef<HTMLDivElement | null>(null);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);

const [activeItem, setActiveItem] = useState<number | null>(null);
const [focusItem, setFocusItem] = useState<Element | null>(null); // stores the id of the item that should be focused

useEffect(() => {}, []);

const focusNextItem = () => {
if (!accordionRef.current) return;
const batches = getAllBatchElements(accordionRef?.current);
const nextItem = getNextBatchItem(batches);
setFocusItem(nextItem);
if (nextItem) {
const button = nextItem.querySelector('button');
// focus button
button?.focus();
}
};

const focusPrevItem = () => {
if (!accordionRef.current) return;
const batches = getAllBatchElements(accordionRef?.current);
const prevItem = getPrevBatchItem(batches);
setFocusItem(prevItem);
if (prevItem) {
const button = prevItem.querySelector('button');
// focus button
button?.focus();
}
};

return (
<AccordionContext.Provider
value={{
rootClass,
activeItem,
setActiveItem,
focusNextItem,
focusPrevItem,
focusItem,
setFocusItem,
accordionRef

accordionRef,
transitionDuration,
transitionTimingFunction
}}>
<div className={clsx(`${rootClass}-root`)} ref={accordionRef}>
{children}
</div>
<RovingFocusGroup.Root direction={direction}>
<RovingFocusGroup.Group className={clsx(`${rootClass}-root`)}>
<div ref={accordionRef}>
{children}
</div>
</RovingFocusGroup.Group>
</RovingFocusGroup.Root>
</AccordionContext.Provider>

);
};

Expand Down
53 changes: 21 additions & 32 deletions src/components/ui/Accordion/fragments/AccordionTrigger.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { clsx } from 'clsx';
import React, { useContext } from 'react';
import React, { useContext, useRef } from 'react';
import { AccordionContext } from '../contexts/AccordionContext';
import { AccordionItemContext } from '../contexts/AccordionItemContext';

import CollapsiblePrimitive from '~/core/primitives/Collapsible';
import RovingFocusGroup from '~/core/utils/RovingFocusGroup';
import Primitive from '~/core/primitives/Primitive';

type AccordionTriggerProps = {
children: React.ReactNode;
className?: string,
Expand All @@ -12,47 +16,32 @@ type AccordionTriggerProps = {
};

const AccordionTrigger: React.FC<AccordionTriggerProps> = ({ children, index, className = '' }) => {
const { setActiveItem, rootClass, focusNextItem, focusPrevItem, activeItem } = useContext(AccordionContext);
const { itemValue, handleBlurEvent, handleClickEvent, handleFocusEvent } = useContext(AccordionItemContext);
const triggerRef = useRef<HTMLButtonElement>(null);
const { setActiveItem, rootClass, activeItem } = useContext(AccordionContext);
const { itemValue } = useContext(AccordionItemContext);

const onClickHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
if (activeItem === itemValue) {
setActiveItem(null);
} else if (activeItem !== itemValue) {
setActiveItem(itemValue);
handleClickEvent(e);
}
};

const onFocusHandler = (e: React.FocusEvent<HTMLButtonElement>) => {
handleFocusEvent(e);
};

return (

<button
type="button"
className={clsx(`${rootClass}-trigger`, className)}
onBlur={handleBlurEvent}
onFocus={onFocusHandler}
onKeyDown={(e) => {
if (e.key === 'ArrowDown') {
// prevent scrolling when pressing arrow keys
e.preventDefault();
focusNextItem();
}
if (e.key === 'ArrowUp') {
// prevent scrolling when pressing arrow keys
e.preventDefault();
focusPrevItem();
}
}}
onClick={onClickHandler}
aria-expanded={activeItem === itemValue}
aria-controls={`content-${index}`}
>
{children}
</button>
<RovingFocusGroup.Item>
<CollapsiblePrimitive.Trigger asChild>
<Primitive.button
className={clsx(`${rootClass}-trigger`, className)}
ref={triggerRef}
onClick={onClickHandler}
aria-expanded={activeItem === itemValue}
aria-controls={`content-${index}`}
>
{children}
</Primitive.button>
</CollapsiblePrimitive.Trigger>
</RovingFocusGroup.Item>

);
};
Expand Down
Loading
Loading