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 90a96e6
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 152 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
236 changes: 89 additions & 147 deletions src/core/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import React, { useState, ReactNode, useRef, useEffect } from "react";
import React, { ReactNode, useMemo } from "react";
import {
AccordionContent,
AccordionItem,
AccordionTrigger,
Accordion as RadixAccordion,
} from "@radix-ui/react-accordion";
import clsx from "clsx";

import Icon from "./Icon";
import type { IconName } from "./Icon/types";
import type { ColorClass } from "./styles/colors/types";
Expand All @@ -7,214 +15,148 @@ import type {
AccordionIcons,
AccordionOptions,
AccordionTheme,
AccordionThemeColors,
} from "./Accordion/types";
import {
themeClasses,
isNonTransparentTheme,
isStaticTheme,
} from "./Accordion/utils";

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;
headerCSS?: string;
options?: AccordionOptions;
};

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",
},
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",
},
transparent: {
bg: "bg-transparent",
hoverBg: "hover:bg-transparent",
text: "text-neutral-1000",
toggleIconColor: "text-dark-grey",
border: "border-neutral-500 border-b last:border-none",
},
darkTransparent: {
bg: "bg-transparent",
hoverBg: "hover:bg-transparent",
text: "text-neutral-000",
toggleIconColor: "text-orange-600",
border: "border-neutral-900 border-b last:border-none",
},
static: {
bg: "bg-neutral-200",
hoverBg: "hover:bg-neutral-200",
text: "text-neutral-1300",
toggleIconColor: "text-neutral-200",
selectableBg: "bg-neutral-1200",
selectableText: "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",
},
};

const isNonTransparentTheme = (theme: AccordionTheme) =>
!["transparent", "darkTransparent"].includes(theme);

const isStaticTheme = (theme: AccordionTheme) =>
["static", "darkStatic"].includes(theme);
} & React.HTMLAttributes<HTMLDivElement>;

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 = ((selectable && selectableText) || text) as ColorClass;

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={clsx({
[`${border}`]: border && !options?.hideBorders,
})}
>
<AccordionTrigger
onClick={onClick}
className={clsx({
"flex w-full group/accordion-trigger 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 focus:outline-none": isStaticTheme(theme),
"sticky top-0": sticky,
[`${bg} ${hoverBg} ${text}`]: true,
[`${selectableBg} ${selectableText}`]: selectable,
[options?.headerCSS ?? ""]: true,
})}
>
{rowIcon ? <Icon name={rowIcon} color={textClass} size="32px" /> : null}
<span>{name}</span>
{!selectable ? (
{!selectable && !isStaticTheme ? (
<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}>
{innerAccordion}
</RadixAccordion>
)}
</div>
);
};
Expand Down
14 changes: 13 additions & 1 deletion src/core/Accordion/Accordion.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ const AccordionPresentation = ({ data, options }: AccordionProps) => (
key={theme}
className={`p-16 rounded-lg ${theme.includes("dark") ? "bg-neutral-1300" : ""}`}
>
<p className="ui-text-p3 mb-16 text-center font-mono">{theme}</p>
<p
className={`ui-text-p3 mb-16 text-center font-mono ${theme.includes("dark") ? "text-neutral-000" : ""}`}
>
{theme}
</p>
<Accordion
data={data}
options={options}
Expand Down Expand Up @@ -224,3 +228,11 @@ export const WithCustomOnClick = {
},
},
};

export const WithCustomHeaderCSS = {
render: () =>
AccordionPresentation({
data,
options: { headerCSS: "bg-pink-400 hover:!bg-pink-600 h-40" },
}),
};
8 changes: 5 additions & 3 deletions src/core/Accordion/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ReactNode } from "react";
import { IconName } from "../Icon/types";
import { ColorClass } from "../styles/colors/types";
import { ColorClass, DataStateOpenColorClass } from "../styles/colors/types";

export type AccordionData = {
name: string;
Expand Down Expand Up @@ -36,8 +36,8 @@ export type AccordionThemeColors = {
hoverBg: string;
text: ColorClass;
toggleIconColor: ColorClass;
selectableBg?: ColorClass;
selectableText?: ColorClass;
selectableBg?: DataStateOpenColorClass;
selectableText?: DataStateOpenColorClass;
border?: string;
};

Expand All @@ -47,4 +47,6 @@ export type AccordionOptions = {
sticky?: boolean;
defaultOpenIndexes?: number[];
fullyOpen?: boolean;
headerCSS?: string;
hideBorders?: boolean;
};
2 changes: 2 additions & 0 deletions src/core/styles/colors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export type Theme = "light" | "dark";
export type ColorClass =
`${ColorClassVariants}${ColorClassPrefixes}-${ColorName}`;

export type DataStateOpenColorClass = `data-[state=open]:${ColorClass}`;

export const neutralColors = [
"neutral-000",
"neutral-100",
Expand Down
Loading

0 comments on commit 90a96e6

Please sign in to comment.