Skip to content

Commit

Permalink
Feat: Handle Accordion uncontrolled component state
Browse files Browse the repository at this point in the history
  • Loading branch information
HariBhandari07 committed Jan 16, 2024
1 parent e20149f commit 4e43a30
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 10 deletions.
8 changes: 4 additions & 4 deletions src/App.tsx
Expand Up @@ -3,16 +3,16 @@ import { Accordion } from "./components/Accordion.tsx";
function App() {
return (
<>
<Accordion>
<Accordion.Item>
<Accordion defaultIndex={[0, 1]} multiple collapsible>
<Accordion.Item index={0}>
<Accordion.Button>Button 1</Accordion.Button>
<Accordion.Panel>Panel 1</Accordion.Panel>
</Accordion.Item>
<Accordion.Item>
<Accordion.Item index={1}>
<Accordion.Button>Button 2</Accordion.Button>
<Accordion.Panel>Panel 2</Accordion.Panel>
</Accordion.Item>
<Accordion.Item>
<Accordion.Item index={2}>
<Accordion.Button>Button 3</Accordion.Button>
<Accordion.Panel>Panel 3</Accordion.Panel>
</Accordion.Item>
Expand Down
127 changes: 121 additions & 6 deletions src/components/Accordion.tsx
Expand Up @@ -4,7 +4,9 @@ import {
ElementType,
forwardRef,
ReactNode,
useCallback,
useContext,
useState,
} from "react";

const ACCORDION_NAME = "Accordion";
Expand All @@ -14,6 +16,13 @@ const PANEL_NAME = "AccordionPanel";
const ACCORDION_CONTEXT = "AccordionContext";
const ACCORDION_ITEM_CONTEXT = "AccordionItemContext";

export function noop() {}

enum AccordionStates {
Open = "OPEN",
Collapsed = "COLLAPSED",
}

const Accordion = forwardRef(function (
{
children,
Expand All @@ -28,8 +37,57 @@ const Accordion = forwardRef(function (
}: AccordionProps,
forwardedRef
) {
const [openPanels, setOpenPanels] = useState(() => {
if (defaultIndex != null) {
if (multiple) {
// If multiple is set to true, we need to make sure the `defaultIndex`
// is an array (and vice versa).
return Array.isArray(defaultIndex) ? defaultIndex : [defaultIndex];
} else {
return Array.isArray(defaultIndex)
? defaultIndex[0] ?? 0
: defaultIndex;
}
}
if (collapsible) {
// collapsible with no defaultIndex will start with all panels collapsed
return multiple ? [] : -1;
}
return multiple ? [0] : 0;
});

const onAccordionItemClick = useCallback(
(index: number) => {
setOpenPanels((prevOpenPanels) => {
if (multiple) {
prevOpenPanels = prevOpenPanels as number[];
// close open panels
if (prevOpenPanels.includes(index)) {
// other panels are open OR accordion is allowed to collapse
if (prevOpenPanels.length > 1 || collapsible) {
return prevOpenPanels.filter((i) => i !== index);
}
} else {
// open panel
return [...prevOpenPanels, index].sort();
}
} else {
return prevOpenPanels === index && collapsible ? -1 : index;
}
return prevOpenPanels;
});
},
[multiple, collapsible]
);

const context = {
openPanels,
onAccordionItemClick: readOnly ? noop : onAccordionItemClick,
readOnly,
};

return (
<AccordionContext.Provider value={{}}>
<AccordionContext.Provider value={context}>
<Comp {...props} ref={forwardedRef} data-hb-accordion="">
{children}
</Comp>
Expand All @@ -42,12 +100,27 @@ const AccordionItem = forwardRef(function (
children,
as: Comp = "div",
disabled = false,
index,
...props
}: AccordionItemProps,
forwardedRef
) {
const { openPanels } = useAccordionContext();

const state =
(Array.isArray(openPanels)
? openPanels.includes(index) && AccordionStates.Open
: openPanels === index && AccordionStates.Open) ||
AccordionStates.Collapsed;

const context = {
index,
state,
disabled,
};

return (
<AccordionItemContext.Provider value={{}}>
<AccordionItemContext.Provider value={context}>
<Comp {...props} ref={forwardedRef} data-hb-accordion-item="">
{children}
</Comp>
Expand All @@ -59,8 +132,24 @@ const AccordionButton = forwardRef(function (
{ children, as: Comp = "button", ...props }: AccordionButtonProps,
forwardedRef
) {
const { onAccordionItemClick } = useAccordionContext();
const { disabled, index } = useAccordionItemContext();

const handleTriggerClick = (e: React.MouseEvent) => {
e.preventDefault();
if (disabled) {
return;
}
onAccordionItemClick(index);
};

return (
<Comp {...props} ref={forwardedRef} data-hb-accordion-button="">
<Comp
{...props}
ref={forwardedRef}
data-hb-accordion-button=""
onClick={handleTriggerClick}
>
{children}
</Comp>
);
Expand All @@ -70,15 +159,26 @@ const AccordionPanel = forwardRef(function (
{ children, as: Comp = "div", ...props }: AccordionPanelProps,
forwardedRef
) {
const { state } = useAccordionItemContext();

return (
<Comp {...props} ref={forwardedRef} data-hb-accordion-panel="">
<Comp
{...props}
ref={forwardedRef}
data-hb-accordion-panel=""
hidden={state !== AccordionStates.Open}
>
{children}
</Comp>
);
});

const AccordionContext = createContext({});
const AccordionItemContext = createContext({});
const AccordionContext = createContext<
InternalAccordionContextValue | undefined
>(undefined);
const AccordionItemContext = createContext<
InternalAccordionItemContextValue | undefined
>(undefined);

const useAccordionContext = () => {
const context = useContext(AccordionContext);
Expand Down Expand Up @@ -130,6 +230,7 @@ interface AccordionItemProps {
as?: ElementType | ComponentType;
children: ReactNode;
disabled?: boolean;
index: number;
}

interface AccordionButtonProps {
Expand All @@ -141,3 +242,17 @@ interface AccordionPanelProps {
as?: ElementType | ComponentType;
children: ReactNode;
}

interface InternalAccordionContextValue {
openPanels: AccordionIndex;

onAccordionItemClick(index: AccordionIndex): void;

readOnly: boolean;
}

interface InternalAccordionItemContextValue {
index: number;
disabled: boolean;
state: AccordionStates;
}

0 comments on commit 4e43a30

Please sign in to comment.