From 90a96e6a94aa64d7b2cfc2a4ab42f849be2ccefe Mon Sep 17 00:00:00 2001 From: Jamie Henson Date: Thu, 7 Nov 2024 17:05:50 +0000 Subject: [PATCH] feat: port underlying Accordion to radix, bump to 14.8.0 --- package.json | 4 +- src/core/Accordion.tsx | 236 +++++++++-------------- src/core/Accordion/Accordion.stories.tsx | 14 +- src/core/Accordion/types.ts | 8 +- src/core/styles/colors/types.ts | 2 + tailwind.config.js | 12 ++ yarn.lock | 115 +++++++++++ 7 files changed, 239 insertions(+), 152 deletions(-) diff --git a/package.json b/package.json index 292214638..441596189 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/core/Accordion.tsx b/src/core/Accordion.tsx index 229b94ff0..7ff817dbe 100644 --- a/src/core/Accordion.tsx +++ b/src/core/Accordion.tsx @@ -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"; @@ -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 = { - 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; const AccordionRow = ({ name, children, - onClick, - open, rowIcon, options, toggleIcons, theme, + index, + onClick, }: AccordionRowProps) => { - const rowRef = useRef(null); - - const [contentHeight, setContentHeight] = useState(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 ( -
- -
+ +
{children}
-
-
+ + ); }; 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( - 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) => ( + { + item.onClick?.(index); + }} + > + {item.content} + + )); + + 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 ( -
- {data.map((item, currentIndex) => { - return ( - { - handleSetIndex(currentIndex); - item.onClick?.(currentIndex); - }} - toggleIcons={icons} - theme={theme} - options={options} - > - {item.content} - - ); - })} +
+ {options?.autoClose ? ( + {innerAccordion} + ) : ( + + {innerAccordion} + + )}
); }; diff --git a/src/core/Accordion/Accordion.stories.tsx b/src/core/Accordion/Accordion.stories.tsx index ff552e41f..89b96007c 100644 --- a/src/core/Accordion/Accordion.stories.tsx +++ b/src/core/Accordion/Accordion.stories.tsx @@ -57,7 +57,11 @@ const AccordionPresentation = ({ data, options }: AccordionProps) => ( key={theme} className={`p-16 rounded-lg ${theme.includes("dark") ? "bg-neutral-1300" : ""}`} > -

{theme}

+

+ {theme} +

+ AccordionPresentation({ + data, + options: { headerCSS: "bg-pink-400 hover:!bg-pink-600 h-40" }, + }), +}; diff --git a/src/core/Accordion/types.ts b/src/core/Accordion/types.ts index 21601d457..b6e422aaa 100644 --- a/src/core/Accordion/types.ts +++ b/src/core/Accordion/types.ts @@ -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; @@ -36,8 +36,8 @@ export type AccordionThemeColors = { hoverBg: string; text: ColorClass; toggleIconColor: ColorClass; - selectableBg?: ColorClass; - selectableText?: ColorClass; + selectableBg?: DataStateOpenColorClass; + selectableText?: DataStateOpenColorClass; border?: string; }; @@ -47,4 +47,6 @@ export type AccordionOptions = { sticky?: boolean; defaultOpenIndexes?: number[]; fullyOpen?: boolean; + headerCSS?: string; + hideBorders?: boolean; }; diff --git a/src/core/styles/colors/types.ts b/src/core/styles/colors/types.ts index 6214a5a06..3d58a431e 100644 --- a/src/core/styles/colors/types.ts +++ b/src/core/styles/colors/types.ts @@ -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", diff --git a/tailwind.config.js b/tailwind.config.js index c76d4a05e..ef36d97cc 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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: { diff --git a/yarn.lock b/yarn.lock index 2b8fd9488..dc4b9d531 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1304,6 +1304,116 @@ resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@radix-ui/primitive@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2" + integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA== + +"@radix-ui/react-accordion@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.2.1.tgz#5c942c42c24267376b26204ec6847b17d15659b3" + integrity sha512-bg/l7l5QzUjgsh8kjwDFommzAshnUsuVMV5NM56QVCm+7ZckYdd9P/ExR8xG/Oup0OajVxNLaHJ1tb8mXk+nzQ== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-collapsible" "1.1.1" + "@radix-ui/react-collection" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + +"@radix-ui/react-collapsible@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.1.tgz#1382cc9ec48f8b473c14f3779d317f0cdf6da5e9" + integrity sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-presence" "1.1.1" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-collection@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed" + integrity sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw== + dependencies: + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" + +"@radix-ui/react-compose-refs@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74" + integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw== + +"@radix-ui/react-context@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8" + integrity sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A== + +"@radix-ui/react-context@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a" + integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q== + +"@radix-ui/react-direction@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc" + integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg== + +"@radix-ui/react-id@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed" + integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-presence@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.1.tgz#98aba423dba5e0c687a782c0669dcd99de17f9b1" + integrity sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A== + dependencies: + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-primitive@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884" + integrity sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw== + dependencies: + "@radix-ui/react-slot" "1.1.0" + +"@radix-ui/react-slot@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84" + integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw== + dependencies: + "@radix-ui/react-compose-refs" "1.1.0" + +"@radix-ui/react-use-callback-ref@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1" + integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw== + +"@radix-ui/react-use-controllable-state@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0" + integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.0" + +"@radix-ui/react-use-layout-effect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" + integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w== + "@resvg/resvg-js-android-arm-eabi@2.6.2": version "2.6.2" resolved "https://registry.yarnpkg.com/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz#e761e0b688127db64879f455178c92468a9aeabe" @@ -3425,6 +3535,11 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + co@^4.6.0: version "4.6.0" resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz"