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
20 changes: 17 additions & 3 deletions src/components/ui/Accordion/fragments/AccordionRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use client';
import React, { useState, useRef } from 'react';

Check warning on line 2 in src/components/ui/Accordion/fragments/AccordionRoot.tsx

View workflow job for this annotation

GitHub Actions / lint

'useState' is defined but never used
import { clsx } from 'clsx';
import { customClassSwitcher } from '~/core';
import { AccordionContext } from '../contexts/AccordionContext';

import useControllableState from '~/core/hooks/useControllableState';
import RovingFocusGroup from '~/core/utils/RovingFocusGroup';
import Primitive from '~/core/primitives/Primitive';

Expand All @@ -19,13 +19,27 @@
loop?: boolean;
disableTabIndexing?: boolean;
openMultiple?: boolean;
value?: (number | string)[];
defaultValue?: (number | string)[];
onValueChange?: (value: (number | string)[]) => void;
}

const AccordionRoot = ({ children, orientation = 'vertical', disableTabIndexing = true, asChild, transitionDuration = 0, transitionTimingFunction = 'linear', customRootClass, loop = true, openMultiple = false }: AccordionRootProps) => {
const AccordionRoot = ({ children, orientation = 'vertical', disableTabIndexing = true, asChild, transitionDuration = 0, transitionTimingFunction = 'linear', customRootClass, loop = true, openMultiple = false, value, defaultValue = [], onValueChange }: AccordionRootProps) => {
const accordionRef = useRef<HTMLDivElement | null>(null);
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const processedValue = value !== undefined
? (openMultiple ? value : (value.length > 0 ? [value[0]] : []))
: undefined;

const processedDefaultValue = openMultiple
? defaultValue
: (defaultValue.length > 0 ? [defaultValue[0]] : []);

const [activeItems, setActiveItems] = useState<(number | string)[]>([]);
const [activeItems, setActiveItems] = useControllableState<(number | string)[]>(
processedValue,
processedDefaultValue,
onValueChange
);

return (
<AccordionContext.Provider
Expand Down
23 changes: 23 additions & 0 deletions src/components/ui/Accordion/stories/Accordion.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Accordion from '../Accordion';
import SandboxEditor from '~/components/tools/SandboxEditor/SandboxEditor';
import type { Meta, StoryObj } from '@storybook/react';
import Button from '../../Button/Button';
import React from 'react';

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
Expand Down Expand Up @@ -96,3 +97,25 @@ export const WithAnimation: Story = {
export const OpenMultiple: Story = {
render: () => <AccordionExample openMultiple />
};

export const WithDeafultValue: Story = {
render: () => <AccordionExample defaultValue={[2]} />
};

export const ControlledValue: Story = {
render: () => {
const [value, setValue] = React.useState<number[]>([]);
const [multiple, setMultiple] = React.useState(false);

return (
<>
<Button onClick={() => setMultiple(!multiple)}>{`Toggle Open Multiple (${multiple ? 'on' : 'off'})`}</Button>
<Button onClick={() => setValue([])}>Close All</Button>
<Button onClick={() => setValue([1])}>Open 2</Button>
<Button onClick={() => setValue([0])}>Open 0</Button>
<Button onClick={() => setValue([0, 1])}>Open 0, 1</Button>
<AccordionExample value={value} onValueChange={setValue} openMultiple={multiple} />
</>
);
}
};
54 changes: 52 additions & 2 deletions src/components/ui/Accordion/tests/Accordion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';
import * as axe from 'axe-core';

import Accordion from '../Accordion';
import { AccordionRootProps } from '../fragments/AccordionRoot';
import { ACCESSIBILITY_TEST_TAGS } from '~/setupTests';

// Test items to use in our composable accordion
Expand All @@ -13,9 +14,9 @@ const testItems = [
];

// Create a test accordion component using the composable pattern
const TestAccordion = () => {
const TestAccordion = (props: Partial<AccordionRootProps>) => {
return (
<Accordion.Root>
<Accordion.Root {...props}>
{testItems.map((item, index) => (
<Accordion.Item value={index} key={index}>
<Accordion.Header>
Expand Down Expand Up @@ -100,4 +101,53 @@ describe('Accordion Component', () => {
done();
});
});

test('controlled mode responds to external value changes', () => {
const TestWithControls = () => {
const [value, setValue] = React.useState<(number | string)[]>([]);

return (
<>
<button onClick={() => setValue([])}>Close All</button>
<button onClick={() => setValue([1])}>Open 2</button>
<button onClick={() => setValue([0, 2])}>Open 1 & 3</button>
<TestAccordion value={value} onValueChange={setValue} openMultiple />
</>
);
};

render(<TestWithControls />);

// Initially all closed
expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
expect(screen.queryByText('Content 2')).not.toBeInTheDocument();
expect(screen.queryByText('Content 3')).not.toBeInTheDocument();

// Open item 2
fireEvent.click(screen.getByText('Open 2'));
expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
expect(screen.getByText('Content 2')).toBeInTheDocument();
expect(screen.queryByText('Content 3')).not.toBeInTheDocument();

// Open items 1 & 3
fireEvent.click(screen.getByText('Open 1 & 3'));
expect(screen.getByText('Content 1')).toBeInTheDocument();
expect(screen.queryByText('Content 2')).not.toBeInTheDocument();
expect(screen.getByText('Content 3')).toBeInTheDocument();

// Close all
fireEvent.click(screen.getByText('Close All'));
expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
expect(screen.queryByText('Content 2')).not.toBeInTheDocument();
expect(screen.queryByText('Content 3')).not.toBeInTheDocument();
});

test('works with defaultValue to show initial item', () => {
render(<TestAccordion defaultValue={[2]} />);

// Item 3 content should be visible initially
expect(screen.getByText('Content 3')).toBeInTheDocument();
expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
expect(screen.queryByText('Content 2')).not.toBeInTheDocument();
});
});
34 changes: 17 additions & 17 deletions src/components/ui/Card/stories/Card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,17 @@ export const Size = () => {
<span key={index} className="inline-flex items-start space-x-2">
{Sizes.map((size, index) => {
return <Card key={index} size={size} variant={variant} >
<div className='flex items-center space-x-4'>
<Avatar
src='https://i.pravatar.cc/64'
alt='avatar'
<div className='flex items-center space-x-4'>
<Avatar
src='https://i.pravatar.cc/64'
alt='avatar'
/>
<div>
<p className='font-bold text-gray-1000'>John Doe</p>
<p className='text-xs text-gray-800'>
<div>
<p className='font-bold text-gray-1000'>John Doe</p>
<p className='text-xs text-gray-800'>
1 hour ago
</p>
</div>
</p>
</div>
</div>
</Card>;
})}
Expand All @@ -85,16 +85,16 @@ export const Variant = () => {
{Variants.map((variant, index) => {
return <Card key={index} variant={variant} >
<div className='flex items-center space-x-4'>
<Avatar
src='https://i.pravatar.cc/64'
alt='avatar'
<Avatar
src='https://i.pravatar.cc/64'
alt='avatar'
/>
<div>
<p className='font-bold text-gray-1000'>John Doe</p>
<p className='text-xs text-gray-800'>
<div>
<p className='font-bold text-gray-1000'>John Doe</p>
<p className='text-xs text-gray-800'>
1 hour ago
</p>
</div>
</p>
</div>
</div>
</Card>;
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const AvatarPrimitiveRoot = ({ children, className = '', customRootClass = '', a
hasError,
setHasError,
handleLoadImage,
handleErrorImage,
handleErrorImage
};

return <AvatarPrimitiveContext.Provider value={values} >
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ export type RovingFocusRootContextTypes = {
export const RovingFocusRootContext = createContext<RovingFocusRootContextTypes>({
orientation: 'horizontal',
loop: true,
disableTabIndexing: false,
disableTabIndexing: false
});
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const RovingFocusRoot = ({
const sendValues = {
orientation,
loop,
disableTabIndexing,
disableTabIndexing
};

return <RovingFocusRootContext.Provider value={sendValues}>
Expand Down
Loading