From 34794e35657aaa3c62d97ec799e0581ae0f4672e Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Sun, 21 May 2023 21:11:38 +0800 Subject: [PATCH] Accordion updated --- packages/ui/.storybook/main.ts | 2 +- .../Accordion/Accordion-old.stories.tsx | 166 +++++++++ .../components/Accordion/Accordion-old.tsx | 137 ++++++++ .../Accordion/Accordion.stories.tsx | 256 ++++---------- .../ui/src/components/Accordion/Accordion.tsx | 332 +++++++++++------- 5 files changed, 584 insertions(+), 309 deletions(-) create mode 100644 packages/ui/src/components/Accordion/Accordion-old.stories.tsx create mode 100644 packages/ui/src/components/Accordion/Accordion-old.tsx diff --git a/packages/ui/.storybook/main.ts b/packages/ui/.storybook/main.ts index 9af87dbdec2..a166e5ddacc 100644 --- a/packages/ui/.storybook/main.ts +++ b/packages/ui/.storybook/main.ts @@ -9,7 +9,7 @@ const config: StorybookConfig = { // // TODO: Temporally Import one by one story and fix build issue. // // After all stories are cleaned, revert to wildcard import. stories: [ - // '../src/components/Accordion/Accordion.stories.tsx', + '../src/components/Accordion/Accordion.stories.tsx', // '../src/components/Alert/Alert.stories.tsx', // '../src/components/Avatar/Avatar.stories.tsx', '../src/components/Button/Button.stories.tsx', diff --git a/packages/ui/src/components/Accordion/Accordion-old.stories.tsx b/packages/ui/src/components/Accordion/Accordion-old.stories.tsx new file mode 100644 index 00000000000..e11fc9edc0c --- /dev/null +++ b/packages/ui/src/components/Accordion/Accordion-old.stories.tsx @@ -0,0 +1,166 @@ +import React from 'react' + +import Accordion, { AccordionProps } from './Accordion' +import { Badge } from '../Badge' + +export default { + title: 'Displays/Accordion', + component: Accordion, +} + +export const OneItem = (args: AccordionProps) => ( + + + Title of the thing + Test badge + + } + id="first" + > + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore. + + + +) + +export const MultipleItems = (args: AccordionProps) => ( + + Title of the thing + } + id={'1'} + > + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore. + + + Title of the thing + } + id={'2'} + > + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore. + + + Title of the thing + } + id={'3'} + > + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore. + + + Title of the thing + } + id={'4'} + > + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore. + + + +) + +export const Bordered = (args: AccordionProps) => ( + + Title of the thing + } + id={'1'} + > + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore. + + + Title of the thing + } + id={'2'} + > + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore. + + + Title of the thing + } + id={'3'} + > + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore. + + + Title of the thing + } + id={'4'} + > + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore. + + + +) + +Bordered.args = { + type: 'bordered', +} + +export const LeftAlignedChevron = (args: AccordionProps) => ( + + + First item + Test badge + + } + id="first" + > + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore. + + + + Second item + + } + id="second" + > + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore. + + + +) + +// export const withDefaultActive = Multiple.bind({}) +// withDefaultActive.args = { +// defaultActiveId: [1], +// } + +// export const withIconLeft = Multiple.bind({}) +// withIconLeft.args = { +// iconPosition: 'left', +// } + +// export const withCustomIcon = Multiple.bind({}) +// withCustomIcon.args = { +// icon: , +// } diff --git a/packages/ui/src/components/Accordion/Accordion-old.tsx b/packages/ui/src/components/Accordion/Accordion-old.tsx new file mode 100644 index 00000000000..524bf7b9de9 --- /dev/null +++ b/packages/ui/src/components/Accordion/Accordion-old.tsx @@ -0,0 +1,137 @@ +import React, { createContext, useContext, useState } from 'react' + +import styleHandler from '../../lib/theme/styleHandler' + +import * as RadixAccordion from '@radix-ui/react-accordion' +import { IconChevronDown } from '../Icon/icons/IconChevronDown' +import { IconChevronUp } from '../Icon/icons/IconChevronUp' + +type Type = 'default' | 'bordered' +type Size = 'tiny' | 'small' | 'medium' | 'large' | 'xlarge' +type Align = 'left' | 'right' + +interface ContextValue { + bordered?: boolean + type: Type + justified: Boolean + chevronAlign: Align +} + +const AccordionContext = createContext({ + chevronAlign: 'left', + justified: true, + type: 'default', +}) + +export interface AccordionProps { + children?: React.ReactNode + className?: string + + onChange?: (item: string | string[]) => void + openBehaviour: 'single' | 'multiple' + type?: Type + defaultValue?: string | string[] | undefined + justified?: Boolean + chevronAlign?: Align +} + +function Accordion({ + children, + className, + onChange, + openBehaviour = 'multiple', + type = 'default', + defaultValue = undefined, + justified = false, + chevronAlign = 'left', +}: AccordionProps) { + const __styles = styleHandler('accordion') + + let containerClasses = [__styles.variants[type].base] + + if (className) { + containerClasses.push(className) + } + + const contextValue = { + chevronAlign, + justified, + type, + defaultValue, + } + + function handleOnChange(e: string | string[]) { + if (onChange) onChange(e) + const value = e == typeof String ? e.split(' ') : e + // setCurrentItems(e) + // console.log('about to change state') + // currentItems = e + // console.log('currentItems', currentItems) + } + + return ( + <> + +
{children}
+ + } + >
+ + ) +} + +interface ItemProps { + children?: React.ReactNode + className?: string + header: React.ReactNode + id: string + icon?: React.ReactNode + disabled?: boolean +} + +export function Item({ children, className, header, id, disabled }: ItemProps) { + const __styles = styleHandler('accordion') + const [open, setOpen] = useState(false) + + const { type, justified, chevronAlign } = useContext(AccordionContext) + + let triggerClasses = [__styles.variants[type].trigger] + if (justified) triggerClasses.push(__styles.justified) + if (className) triggerClasses.push(className) + + let chevronClasses = [__styles.chevron.base, __styles.chevron.align[chevronAlign]] + + if (open && !disabled) { + chevronClasses.unshift('!rotate-180') + } + + return ( + { + setOpen(!open) + }} + > + + {header} + {!disabled && ( + + )} + + +
{children}
+
+
+ ) +} + +Accordion.Item = Item +export default Accordion diff --git a/packages/ui/src/components/Accordion/Accordion.stories.tsx b/packages/ui/src/components/Accordion/Accordion.stories.tsx index 613caccc178..25886206394 100644 --- a/packages/ui/src/components/Accordion/Accordion.stories.tsx +++ b/packages/ui/src/components/Accordion/Accordion.stories.tsx @@ -1,199 +1,79 @@ -import React from 'react' +// Button.stories.ts|tsx -import Accordion, { AccordionProps } from './Accordion' +import type { Meta, StoryObj } from '@storybook/react' + +import { Accordion, AccordionItem } from './Accordion' import { Badge } from '../Badge' -export default { - title: 'Displays/Accordion', +const meta: Meta = { + /* 👇 The title prop is optional. + * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'General/Accordion', component: Accordion, } -export const OneItem = (args: AccordionProps) => ( - - - - Title of the thing - - Test badge - - } - id="first" - > - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur - amet labore. - - - -) +export default meta -export const MultipleItems = (args: AccordionProps) => ( - - - Title of the thing - - } - id={'1'} - > - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur - amet labore. - - - - Title of the thing - - } - id={'2'} - > - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur - amet labore. - - - - Title of the thing - - } - id={'3'} - > - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur - amet labore. - - - - Title of the thing - - } - id={'4'} - > - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur - amet labore. - - - -) +type Story = StoryObj -export const Bordered = (args: AccordionProps) => ( - - - Title of the thing - - } - id={'1'} - > - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur - amet labore. - - - - Title of the thing - - } - id={'2'} - > - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur - amet labore. - - - - Title of the thing - - } - id={'3'} - > - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur - amet labore. - - - - Title of the thing - - } - id={'4'} - > - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur - amet labore. - - - -) +/* + *👇 Render functions are a framework specific feature to allow you control on how the component renders. + * See https://storybook.js.org/docs/react/api/csf + * to learn how to use render functions. + */ -Bordered.args = { - type: 'bordered', +export const Default = { + args: { + type: 'default', + }, + render: ({ children, type }: any) => ( + + + + Title of the thing + + Yes. It adheres to the WAI-ARIA design pattern. + + + Is it styled? + + Yes. It comes with default styles that matches the other components' aesthetic. + + + + Is it animated? + + Yes. It's animated by default, but you can disable it if you prefer. + + + + ), } -export const LeftAlignedChevron = (args: AccordionProps) => ( - - - - First item - - Test badge - - } - id="first" - > - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur - amet labore. - - - - - Second item - - - } - id="second" - > - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur - amet labore. - - - -) - -// export const withDefaultActive = Multiple.bind({}) -// withDefaultActive.args = { -// defaultActiveId: [1], -// } - -// export const withIconLeft = Multiple.bind({}) -// withIconLeft.args = { -// iconPosition: 'left', -// } - -// export const withCustomIcon = Multiple.bind({}) -// withCustomIcon.args = { -// icon: , -// } +export const Bordered = { + args: { + type: 'bordered', + }, + render: ({ children, type }: any) => ( + + + Is it accessible? + Yes. It adheres to the WAI-ARIA design pattern. + + + Is it styled? + + Yes. It comes with default styles that matches the other components' aesthetic. + + + + Is it animated? + + Yes. It's animated by default, but you can disable it if you prefer. + + + + ), +} diff --git a/packages/ui/src/components/Accordion/Accordion.tsx b/packages/ui/src/components/Accordion/Accordion.tsx index 7488ee5666c..80a8cbd7d3e 100644 --- a/packages/ui/src/components/Accordion/Accordion.tsx +++ b/packages/ui/src/components/Accordion/Accordion.tsx @@ -1,142 +1,234 @@ +import * as AccordionPrimitive from '@radix-ui/react-accordion' +import { cn } from '@ui/lib/utils' +import { VariantProps, cva, cx } from 'class-variance-authority' +import { ChevronDown } from 'lucide-react' import React, { createContext, useContext, useState } from 'react' -import styleHandler from '../../lib/theme/styleHandler' - -import * as RadixAccordion from '@radix-ui/react-accordion' -import { IconChevronDown } from '../Icon/icons/IconChevronDown' -import { IconChevronUp } from '../Icon/icons/IconChevronUp' - -type Type = 'default' | 'bordered' -type Size = 'tiny' | 'small' | 'medium' | 'large' | 'xlarge' -type Align = 'left' | 'right' - -interface ContextValue { - bordered?: boolean - type: Type - justified: Boolean - chevronAlign: Align -} - -const AccordionContext = createContext({ - chevronAlign: 'left', - justified: true, - type: 'default', +export type AccordionRootVariantsProps = VariantProps +const accordionRootVariants = cva('flex flex-col', { + variants: { + type: { + default: 'space-y-3', + bordered: '-space-y-px', + }, + defaultVariants: { + type: 'default', + }, + }, }) -export interface AccordionProps { - children?: React.ReactNode - className?: string - defaultActiveId?: (string | number)[] - icon?: React.ReactNode - iconPosition?: Align - onChange?: (item: string | string[]) => void +export type AccordionItemVariantsProps = VariantProps +const accordionItemVariants = cva( + 'group first:rounded-tl-md first:rounded-tr-md last:rounded-bl-md last:rounded-br-md', + { + variants: { + type: { + default: 'overflow-hidden', + bordered: 'border border-scale-700', + }, + justified: { + true: 'justify-between', + false: '', + }, + }, + } +) + +const accordionTriggerVariants = cva( + `flex flex-row items-center w-full text-left cursor-pointer focus-visible:ring-1 focus-visible:z-10 ring-scale-1100 + justify-between transition-all hover:underline [&[data-state=open]>svg]:rotate-180`, + { + variants: { + type: { + default: 'gap-3', + bordered: ` + px-6 py-4 + font-medium text-base bg-transparent + transition-colors hover:bg-scale-200 + overflow-hidden + group-first:rounded-tl-md group-first:rounded-tr-md + group-last:rounded-bl-md group-last:rounded-br-md`, + }, + }, + } +) + +const accordionContentVariants = cva('pb-4 pt-0', { + variants: { + type: { + default: '', + bordered: 'px-6', + }, + }, +}) + +type Type = AccordionRootVariantsProps['type'] +type Align = 'left' | 'right' | undefined + +interface ContextValue { + chevronAlign: Align + justified: boolean + type: Type + defaultValue: AccordionPrimitive.AccordionImplSingleProps['defaultValue'] +} + +interface AdditionalProps { openBehaviour: 'single' | 'multiple' + variant?: Type + // Add other additional props here + onChange?: (item: string | string[]) => void type?: Type - size?: Size - defaultValue?: string | string[] | undefined - justified?: Boolean + justified?: AccordionItemVariantsProps['justified'] chevronAlign?: Align } -function Accordion({ - children, - className, - onChange, - openBehaviour = 'multiple', - type = 'default', - defaultValue = undefined, - justified = false, - chevronAlign = 'left', -}: AccordionProps) { - const __styles = styleHandler('accordion') - - let containerClasses = [__styles.variants[type].base] - - if (className) { - containerClasses.push(className) - } - - const contextValue = { - chevronAlign, - justified, - type, - defaultValue, - } - - function handleOnChange(e: string | string[]) { - if (onChange) onChange(e) - const value = e == typeof String ? e.split(' ') : e - // setCurrentItems(e) - // console.log('about to change state') - // currentItems = e - // console.log('currentItems', currentItems) - } - - return ( - <> - {/* // - @ts-expect-error */} - -
{children}
- - } - >
- - ) +interface AccordionComponent + extends React.ForwardRefExoticComponent< + ExtendedAccordionProps & React.RefAttributes + > { + Item: typeof AccordionItem + Content: typeof AccordionContent + Trigger: typeof AccordionTrigger } -interface ItemProps { - children?: React.ReactNode - className?: string - header: React.ReactNode - id: string - icon?: React.ReactNode - disabled?: boolean +export interface AccordionSingleProps extends AccordionPrimitive.AccordionImplSingleProps { + openBehaviour: 'single' + value?: string + // Add other props specific to single accordion } -export function Item({ children, className, header, id, disabled }: ItemProps) { - const __styles = styleHandler('accordion') +export interface AccordionMultipleProps extends AccordionPrimitive.AccordionImplMultipleProps { + openBehaviour: 'multiple' + value?: string[] + // Add other props specific to multiple accordion +} + +type ExtendedAccordionProps = (AccordionSingleProps | AccordionMultipleProps) & AdditionalProps + +const AccordionContext = createContext({ + chevronAlign: 'left', + justified: false, // Set the default value to false + type: undefined, + defaultValue: undefined, +}) + +const Accordion: AccordionComponent = React.forwardRef( + ({ openBehaviour, type, chevronAlign, justified, defaultValue, ...props }, ref) => { + function handleOnChange(e: string | string[]) { + if (props.onChange) props.onChange(e) + } + + const contextValue = { + chevronAlign, + justified: justified || false, // Provide a default value of false + type: type || 'default', // Provide a default value for the type + defaultValue: Array.isArray(defaultValue) ? defaultValue.join('') : defaultValue, // Convert array to string + } + + if (openBehaviour === 'single') { + const { value, children, className, ...singleProps } = props as AccordionSingleProps + return ( + + {children} + + } + {...singleProps} + /> + ) + } else { + const { value, children, className, ...multipleProps } = props as AccordionMultipleProps + return ( + +
{props.children}
+ + } + {...multipleProps} + /> + ) + } + } +) as AccordionComponent + +export interface ItemAdditionalProps + extends React.ComponentPropsWithoutRef {} + +export const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { const [open, setOpen] = useState(false) - const { type, justified, chevronAlign } = useContext(AccordionContext) - - let triggerClasses = [__styles.variants[type].trigger] - if (justified) triggerClasses.push(__styles.justified) - if (className) triggerClasses.push(className) - - let chevronClasses = [__styles.chevron.base, __styles.chevron.align[chevronAlign]] - - if (open && !disabled) { - chevronClasses.unshift('!rotate-180') - } + let { type, justified } = useContext(AccordionContext) return ( - { setOpen(!open) }} - > - - {header} - {!disabled && ( - - )} - - -
{children}
-
-
+ {...props} + /> ) -} +}) +AccordionItem.displayName = AccordionPrimitive.Item.displayName -Accordion.Item = Item -export default Accordion +export const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => { + const { type } = useContext(AccordionContext) + return ( + + + {children} + + + + ) +}) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +export const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => { + const { type } = useContext(AccordionContext) + return ( + +
{children}
+
+ ) +}) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +Accordion.Item = AccordionItem +Accordion.Content = AccordionContent +Accordion.Trigger = AccordionTrigger +export { Accordion }