Skip to content

Commit

Permalink
feat: port underlying Accordion to radix, bump to 14.8.0
Browse files Browse the repository at this point in the history
  • Loading branch information
jamiehenson committed Nov 7, 2024
1 parent 7c403b3 commit 7f24c8e
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 99 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ably/ui",
"version": "14.7.8",
"version": "14.8.0",
"description": "Home of the Ably design system library ([design.ably.com](https://design.ably.com)). It provides a showcase, development/test environment and a publishing pipeline for different distributables.",
"repository": {
"type": "git",
Expand Down Expand Up @@ -74,8 +74,10 @@
"test:update-snapshots": "npx concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn build-storybook && yarn http-server preview --port 6007 --silent\" \"wait-on tcp:6007 && yarn test-storybook -u --url http://127.0.0.1:6007\""
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.1",
"addsearch-js-client": "^0.8.11",
"array-flat-polyfill": "^1.0.1",
"clsx": "^2.1.1",
"dompurify": "^3.1.4",
"highlight.js": "^11.9.0",
"highlightjs-curl": "^1.3.0",
Expand Down
185 changes: 87 additions & 98 deletions src/core/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, ReactNode, useRef, useEffect } from "react";
import React, { ReactNode, useMemo } from "react";
import Icon from "./Icon";
import type { IconName } from "./Icon/types";
import type { ColorClass } from "./styles/colors/types";
Expand All @@ -9,43 +9,48 @@ import type {
AccordionTheme,
AccordionThemeColors,
} from "./Accordion/types";
import {
AccordionContent,
AccordionItem,
AccordionTrigger,
Accordion as RadixAccordion,
} from "@radix-ui/react-accordion";
import clsx from "clsx";

type AccordionRowProps = {
children: ReactNode;
name: string;
onClick: () => void;
open: boolean;
rowIcon?: IconName;
theme: AccordionTheme;
toggleIcons: AccordionIcons;
options?: AccordionOptions;
index: number;
onClick: () => void;
};

export type AccordionProps = {
className?: string;
data: AccordionData[];
icons?: AccordionIcons;
id?: string;
theme?: AccordionTheme;
options?: AccordionOptions;
};
} & React.HTMLAttributes<HTMLDivElement>;

const themeClasses: Record<AccordionTheme, AccordionThemeColors> = {
dark: {
bg: "bg-neutral-1200",
hoverBg: "hover:bg-neutral-1100",
text: "text-white",
toggleIconColor: "text-orange-600",
selectableBg: "bg-neutral-300",
selectableText: "text-neutral-1300",
selectableBg: "data-[state=open]:bg-neutral-300",
selectableText: "data-[state=open]:text-neutral-1300",
},
light: {
bg: "bg-neutral-200",
hoverBg: "hover:bg-neutral-300",
text: "text-neutral-1300",
toggleIconColor: "text-neutral-1000",
selectableBg: "bg-neutral-1200",
selectableText: "text-white",
selectableBg: "data-[state=open]:bg-neutral-1200",
selectableText: "data-[state=open]:text-white",
},
transparent: {
bg: "bg-transparent",
Expand All @@ -66,16 +71,16 @@ const themeClasses: Record<AccordionTheme, AccordionThemeColors> = {
hoverBg: "hover:bg-neutral-200",
text: "text-neutral-1300",
toggleIconColor: "text-neutral-200",
selectableBg: "bg-neutral-1200",
selectableText: "text-white",
selectableBg: "data-[state=open]:bg-neutral-1200",
selectableText: "data-[state=open]:text-white",
},
darkStatic: {
bg: "bg-neutral-1200",
hoverBg: "hover:bg-neutral-1200",
text: "text-white",
toggleIconColor: "text-neutral-1200",
selectableBg: "bg-neutral-1200",
selectableText: "text-neutral-1300",
selectableBg: "data-[state=open]:bg-neutral-1200",
selectableText: "data-[state=open]:text-neutral-1300",
},
};

Expand All @@ -88,133 +93,117 @@ const isStaticTheme = (theme: AccordionTheme) =>
const AccordionRow = ({
name,
children,
onClick,
open,
rowIcon,
options,
toggleIcons,
theme,
index,
onClick,
}: AccordionRowProps) => {
const rowRef = useRef<HTMLDivElement>(null);

const [contentHeight, setContentHeight] = useState<number>(0);

useEffect(() => {
const resizeObserver = new ResizeObserver(() => {
if (rowRef.current) {
setContentHeight(rowRef.current.scrollHeight + 16);
}
});

if (rowRef.current) {
resizeObserver.observe(rowRef.current);
}

return () => {
if (rowRef.current) {
resizeObserver.unobserve(rowRef.current);
}
};
}, []);

const { selectable, sticky } = options || {};

const {
text,
bg,
hoverBg,
toggleIconColor,
selectableBg,
selectableText,
border,
toggleIconColor,
} = themeClasses[theme];

const bgClasses: string =
(selectable && open && selectableBg) || `${bg} ${hoverBg}`;

const textClass: ColorClass = (selectable && open && selectableText) || text;
const textClass: ColorClass = (selectable && selectableText) || text;

return (
<div className={`${border ?? ""}`}>
<button
type="button"
{...(!isStaticTheme(theme) ? { onClick } : {})}
className={`flex w-full ${sticky ? "sticky top-0" : ""} focus:outline-none py-16 rounded-lg ui-text-p1 font-bold text-left items-center gap-12 ${isNonTransparentTheme(theme) ? "px-16 mb-16" : ""} ${isStaticTheme(theme) ? "pointer-events-none" : ""} transition-colors ${bgClasses} ${textClass}`}
<AccordionItem value={`item-${index}`} className={`${border ?? ""}`}>
<AccordionTrigger
onClick={onClick}
className={clsx({
"flex w-full group/accordion-trigger focus:outline-none py-16 rounded-lg ui-text-p1 font-bold text-left items-center gap-12 transition-colors":
true,
"px-16": isNonTransparentTheme(theme),
"mb-16": isNonTransparentTheme(theme),
"pointer-events-none": isStaticTheme(theme),
"sticky top-0": sticky,
[`${bg} ${hoverBg} ${text}`]: true,
[`${selectableBg} ${selectableText}`]: selectable,
})}
>
{rowIcon ? <Icon name={rowIcon} color={textClass} size="32px" /> : null}
<span>{name}</span>
{!selectable ? (
<span className="flex-1 justify-end flex items-center">
<Icon
name={open ? toggleIcons.open.name : toggleIcons.closed.name}
additionalCSS="group-data-[state=closed]/accordion-trigger:hidden"
name={toggleIcons.open.name}
color={toggleIconColor}
size="16px"
/>
<Icon
additionalCSS="group-data-[state=open]/accordion-trigger:hidden"
name={toggleIcons.closed.name}
color={toggleIconColor}
size="16px"
/>{" "}
/>
</span>
) : null}
</button>
<div
className={`ui-text-p2 transition-[max-height] duration-500 overflow-y-hidden`}
style={{ maxHeight: open ? contentHeight : 0 }}
ref={rowRef}
>
</AccordionTrigger>
<AccordionContent className="ui-text-p2 overflow-hidden transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down">
<div className="pb-16">{children}</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
);
};

const Accordion = ({
data,
theme = "transparent",
id = "id-accordion",
className = "",
icons = {
closed: { name: "icon-gui-plus" },
open: { name: "icon-gui-minus" },
},
options,
...props
}: AccordionProps) => {
const { defaultOpenIndexes, autoClose, fullyOpen } = options || {};
const [openIndexes, setOpenIndexes] = useState<number[]>(
defaultOpenIndexes ?? [],
);

const handleSetIndex = (index: number) => {
const currentIndexIsOpen = openIndexes.includes(index);

if (autoClose) {
setOpenIndexes(currentIndexIsOpen ? [] : [index]);
} else {
setOpenIndexes(
currentIndexIsOpen
? openIndexes.filter((i) => i !== index)
: [...openIndexes, index],
);
}
};
const innerAccordion = data.map((item, index) => (
<AccordionRow
key={item.name}
name={item.name}
rowIcon={item.icon}
toggleIcons={icons}
theme={theme}
options={options}
index={index}
onClick={() => {
item.onClick?.(index);
}}
>
{item.content}
</AccordionRow>
));

const openIndexes = useMemo(() => {
const indexValues = data.map((_, i) => `item-${i}`);
return options?.fullyOpen
? indexValues
: indexValues.filter((_, index) =>
options?.defaultOpenIndexes?.includes(index),
);
}, [options?.defaultOpenIndexes, options?.fullyOpen]);

return (
<div className={className} id={id}>
{data.map((item, currentIndex) => {
return (
<AccordionRow
key={item.name}
name={item.name}
rowIcon={item.icon}
open={fullyOpen ?? openIndexes.includes(currentIndex)}
onClick={() => {
handleSetIndex(currentIndex);
item.onClick?.(currentIndex);
}}
toggleIcons={icons}
theme={theme}
options={options}
>
{item.content}
</AccordionRow>
);
})}
<div {...props}>
{options?.autoClose ? (
<RadixAccordion type="single">{innerAccordion}</RadixAccordion>
) : (
<RadixAccordion
type="multiple"
defaultValue={openIndexes}
onValueChange={(value) => console.log(value)}
>
{innerAccordion}
</RadixAccordion>
)}
</div>
);
};
Expand Down
2 changes: 2 additions & 0 deletions src/core/styles/colors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export const variants = [
"focus:",
"group-hover:",
"group-focus:",
"data-[state=open]:",
"data-[state=closed]:",
] as const;

type ColorClassVariants = (typeof variants)[number];
Expand Down
12 changes: 12 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,18 @@ module.exports = {
"0%": { opacity: 1 },
"100%": { opacity: 0 },
},
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
listStyleType: {
Expand Down
Loading

0 comments on commit 7f24c8e

Please sign in to comment.