Skip to content
Open
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
18 changes: 14 additions & 4 deletions src/components/ui/Accordion/contexts/AccordionContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import { createContext } from 'react';

interface AccordionContextType {
rootClass?: string | null;
activeItems: (number | string)[];
setActiveItems: (items: (number | string)[]) => void;
activeItems: string[];
setActiveItems: (items: string[]) => void;
accordionRef?: React.RefObject<HTMLDivElement | null>;
transitionDuration?: number;
transitionTimingFunction?: string;
openMultiple?: boolean;
type?: 'single' | 'multiple';
collapsible?: boolean;
disabled?: boolean;
dir?: 'ltr' | 'rtl';
forceMount?: boolean;
hiddenUntilFound?: boolean;
}

export const AccordionContext = createContext<AccordionContextType>({
Expand All @@ -17,5 +22,10 @@ export const AccordionContext = createContext<AccordionContextType>({
accordionRef: undefined,
transitionDuration: 0,
transitionTimingFunction: 'ease-out',
openMultiple: false
type: 'single',
collapsible: true,
disabled: false,
dir: 'ltr',
forceMount: false,
hiddenUntilFound: false
});
6 changes: 3 additions & 3 deletions src/components/ui/Accordion/contexts/AccordionItemContext.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { createContext } from 'react';

interface AccordionItemContextType {
itemValue: number | string;
setItemValue: (value: number | string) => void;
itemValue: string;
setItemValue: (value: string) => void;
disabled: boolean;
}

export const AccordionItemContext = createContext<AccordionItemContextType>({
itemValue: 0,
itemValue: '',
setItemValue: () => {},
disabled: false
});
69 changes: 49 additions & 20 deletions src/components/ui/Accordion/fragments/AccordionContent.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,66 @@
'use client';
import { clsx } from 'clsx';
import React, { useContext } from 'react';
import React, { useContext, useRef, useEffect } 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,
className? :string
export type AccordionContentProps = {
children: React.ReactNode;
index?: number;
className?: string;
forceMount?: boolean;
asChild?: boolean;
};

const AccordionContent: React.FC<AccordionContentProps> = ({ children, index, className = '' }: AccordionContentProps) => {
const { activeItems, rootClass } = useContext(AccordionContext);
const AccordionContent: React.FC<AccordionContentProps> = ({
children,
index,
className = '',
forceMount = false,
asChild = false
}: AccordionContentProps) => {
const {
activeItems,
rootClass,
hiddenUntilFound,
forceMount: rootForceMount
} = useContext(AccordionContext);
const { itemValue } = useContext(AccordionItemContext);
const contentRef = useRef<HTMLDivElement>(null);

// Use itemValue to determine if this content should be visible
const isOpen = activeItems.includes(itemValue);
const shouldRender = forceMount || rootForceMount || isOpen;
const shouldHide = hiddenUntilFound && !isOpen;

useEffect(() => {
if (contentRef.current) {
if (shouldHide) {
contentRef.current.setAttribute('hidden', 'until-found');
} else {
contentRef.current.removeAttribute('hidden');
}
}
}, [shouldHide]);

if (!shouldRender) {
return null;
}

return (
isOpen
? <CollapsiblePrimitive.Content
asChild
className={clsx(`${rootClass}-content`, className)}
id={`content-${index}`}
role="region"
aria-labelledby={`section-${index}`}
aria-hidden={!isOpen}
>
{children}
</CollapsiblePrimitive.Content>
: null
<CollapsiblePrimitive.Content
ref={contentRef}
asChild={asChild}
className={clsx(`${rootClass}-content`, className)}
id={`content-${index ?? itemValue}`}
role="region"
aria-labelledby={`section-${index ?? itemValue}`}
aria-hidden={!isOpen}
data-state={isOpen ? 'open' : 'closed'}
>
{children}
</CollapsiblePrimitive.Content>
);
};

Expand Down
47 changes: 26 additions & 21 deletions src/components/ui/Accordion/fragments/AccordionItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use client';
import React, { useState, useContext, useId, useEffect, useRef } from 'react';
import React, { useContext, useId, useRef } from 'react';
import { clsx } from 'clsx';
import { AccordionContext } from '../contexts/AccordionContext';
import { AccordionItemContext } from '../contexts/AccordionItemContext';
Expand All @@ -10,51 +10,56 @@ import Primitive from '~/core/primitives/Primitive';
export type AccordionItemProps = {
children: React.ReactNode;
className?: string;
value?: number | string;
value: string; // Required for Radix UI compatibility
disabled?: boolean;
asChild?: boolean;
}

const AccordionItem: React.FC<AccordionItemProps> = ({ children, value, className = '', disabled = false, asChild = false, ...props }) => {
const AccordionItem: React.FC<AccordionItemProps> = ({
children,
value,
className = '',
disabled = false,
asChild = false,
...props
}) => {
const accordionItemRef = useRef<HTMLDivElement>(null);
const [itemValue, setItemValue] = useState<number | string>(value ?? 0);
const { rootClass, activeItems, transitionDuration, transitionTimingFunction } = useContext(AccordionContext);
const {
rootClass,
activeItems,
transitionDuration,
transitionTimingFunction,
disabled: rootDisabled,
dir
} = useContext(AccordionContext);

const [isOpen, setIsOpen] = useState(activeItems.includes(itemValue));
useEffect(() => {
setIsOpen(activeItems.includes(itemValue));
}, [activeItems, itemValue]);
const isDisabled = rootDisabled || disabled;
const isOpen = activeItems.includes(value);

const id = useId();

// Update itemValue if value prop changes
useEffect(() => {
if (value !== undefined && value !== itemValue) {
setItemValue(value);
}
}, [value]);

return (
<AccordionItemContext.Provider value={{ itemValue, setItemValue, disabled }}>
<AccordionItemContext.Provider value={{ itemValue: value, setItemValue: () => {}, disabled: isDisabled }}>
<CollapsiblePrimitive.Root
open={isOpen}
onOpenChange={setIsOpen}
disabled={disabled}
disabled={isDisabled}
transitionDuration={transitionDuration}
transitionTimingFunction={transitionTimingFunction}
asChild
>
<Primitive.div
ref={accordionItemRef}
className={clsx(`${rootClass}-item`, className)} {...props}
className={clsx(`${rootClass}-item`, className)}
{...props}
id={`accordion-data-item-${id}`}
role="region"
data-state={isOpen ? 'open' : 'closed'}
data-disabled={isDisabled ? '' : undefined}
dir={dir}
>
{children}
</Primitive.div>
</CollapsiblePrimitive.Root>

</AccordionItemContext.Provider>
);
};
Expand Down
100 changes: 84 additions & 16 deletions src/components/ui/Accordion/fragments/AccordionRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,43 +18,111 @@ export type AccordionRootProps = {
asChild?: boolean;
loop?: boolean;
disableTabIndexing?: boolean;
type?: 'single' | 'multiple';
collapsible?: boolean;
disabled?: boolean;
dir?: 'ltr' | 'rtl';
forceMount?: boolean;
hiddenUntilFound?: boolean;
// Legacy props (deprecated)
openMultiple?: boolean;
value?: (number | string)[];
defaultValue?: (number | string)[];
onValueChange?: (value: (number | string)[]) => void;
keepMounted?: boolean;
// Value props (updated for Radix UI compatibility)
value?: string | string[];
defaultValue?: string | string[];
onValueChange?: (value: string | string[] | undefined) => void;
}

const AccordionRoot = ({ children, orientation = 'vertical', disableTabIndexing = true, asChild, transitionDuration = 0, transitionTimingFunction = 'linear', customRootClass, loop = true, openMultiple = false, value, defaultValue = [], onValueChange }: AccordionRootProps) => {
const AccordionRoot = ({
children,
orientation = 'vertical',
disableTabIndexing = true,
asChild,
transitionDuration = 0,
transitionTimingFunction = 'linear',
customRootClass,
loop = true,
type = 'single',
collapsible = true,
disabled = false,
dir = 'ltr',
forceMount = false,
hiddenUntilFound = false,
// Legacy props (deprecated)
openMultiple,
keepMounted,
// Value props
value,
defaultValue = [],
onValueChange
}: AccordionRootProps) => {
const accordionRef = useRef<HTMLDivElement | null>(null);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);

// Handle legacy props for backward compatibility
const actualType = openMultiple !== undefined ? (openMultiple ? 'multiple' : 'single') : type;
const actualForceMount = keepMounted !== undefined ? keepMounted : forceMount;

// Process values based on type
const processedValue = value !== undefined
? (openMultiple ? value : (value.length > 0 ? [value[0]] : []))
? (actualType === 'multiple'
? (Array.isArray(value) ? value : [value])
: (Array.isArray(value) ? (value.length > 0 ? [value[0]] : []) : [value]))
: undefined;

const processedDefaultValue = openMultiple
? defaultValue
: (defaultValue.length > 0 ? [defaultValue[0]] : []);
const processedDefaultValue = actualType === 'multiple'
? (Array.isArray(defaultValue) ? defaultValue : [defaultValue])
: (Array.isArray(defaultValue) ? (defaultValue.length > 0 ? [defaultValue[0]] : []) : [defaultValue]);

const [activeItems, setActiveItems] = useControllableState<(number | string)[]>(
const [activeItems, setActiveItems] = useControllableState<string[]>(
processedValue,
processedDefaultValue,
onValueChange
processedDefaultValue,
(next) => {
onValueChange?.(actualType === 'single' ? next[0] : next);
}
);

// Handle collapsible logic (only applies to single type)
const handleValueChange = (newValue: string[]) => {
if (actualType === 'single' && !collapsible && newValue.length === 0) {
// Prevent closing all items when collapsible is false
console.warn('Accordion: Cannot close all items when collapsible is false');
return;
}
setActiveItems(newValue);
};

return (
<AccordionContext.Provider
value={{
rootClass,
activeItems,
setActiveItems,
setActiveItems: handleValueChange,
accordionRef,
transitionDuration,
transitionTimingFunction,
openMultiple
type: actualType,
collapsible,
disabled,
dir,
forceMount: actualForceMount,
hiddenUntilFound
}}>
<RovingFocusGroup.Root orientation={orientation} loop={loop} disableTabIndexing={disableTabIndexing} >
<RovingFocusGroup.Group >
<Primitive.div className={clsx(`${rootClass}-root`)} ref={accordionRef} asChild={asChild}>
<RovingFocusGroup.Root
orientation={orientation}
loop={loop}
disableTabIndexing={disableTabIndexing}
dir={dir}
>
<RovingFocusGroup.Group>
<Primitive.div
className={clsx(`${rootClass}-root`)}
ref={accordionRef}
asChild={asChild}
dir={dir}
data-orientation={orientation}
data-type={actualType}
>
{children}
</Primitive.div>
</RovingFocusGroup.Group>
Expand Down
Loading
Loading