-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add accordion features #518
Changes from all commits
91752e6
9c5a886
225c7a8
13ca291
cc2a041
50004a8
744d89f
a02b10d
d346d76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@cypress-design/react-accordion': minor | ||
--- | ||
|
||
Add `open`, `onClickSummary`, and ` onToggle` props and add `toggle` and `click` event listeners |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,19 @@ | ||
import * as React from 'react' | ||
import React, { useEffect, useState } from 'react' | ||
import clsx from 'clsx' | ||
import { CssClasses } from '@cypress-design/constants-accordion' | ||
import type { AccordionProps } from '@cypress-design/constants-accordion' | ||
import { DetailsAnimation } from '@cypress-design/details-animation' | ||
import { IconChevronDownSmall } from '@cypress-design/react-icon' | ||
|
||
export interface AccordionPropsReact extends AccordionProps { | ||
export interface AccordionProps { | ||
/** | ||
* Main indigo title. | ||
* [NOTE] Its color and font can be customized using `titleClassName`. | ||
*/ | ||
title: string | ||
/** | ||
* Second line in the heading. | ||
*/ | ||
description?: string | React.ReactNode | ||
/** | ||
* Icon to be displayed on the left of the the heading. Overridden by the iconEl prop, if both are provided. | ||
*/ | ||
|
@@ -14,10 +22,42 @@ export interface AccordionPropsReact extends AccordionProps { | |
* Element to be displayed on the left of the the heading. Overrides the icon prop, if both are provided. | ||
*/ | ||
iconEl?: React.ReactNode | ||
/** | ||
* Should we add a vertical separator between the icon and the text. | ||
*/ | ||
separator?: boolean | ||
/** | ||
* Change the font and color of the heading title | ||
*/ | ||
titleClassName?: string | ||
/** | ||
* Change the font and color of the heading description | ||
*/ | ||
descriptionClassName?: string | ||
/** | ||
* Additional classes to add to the header of the accordion | ||
* > [NOTE] useful to change the background color of the header | ||
*/ | ||
headingClassName?: string | ||
/** | ||
* When using content that needs ti be edge to edge, | ||
* removes the content wrapper from the content. | ||
*/ | ||
fullWidthContent?: boolean | ||
/** | ||
* Provides access to the onClick event of the summary element. | ||
* Allows for custom handling or cancellation of the default behavior. | ||
*/ | ||
onClickSummary?: (event: MouseEvent) => boolean | undefined | ||
/** | ||
* Callback triggered when the accordion toggles open or closed. | ||
* @param open - The new open state of the accordion. | ||
*/ | ||
onToggle?: (open: boolean) => void | ||
} | ||
|
||
export const Accordion: React.FC< | ||
AccordionPropsReact & React.HTMLProps<HTMLDetailsElement> | ||
AccordionProps & React.HTMLProps<HTMLDetailsElement> | ||
> = ({ | ||
title, | ||
description, | ||
|
@@ -29,22 +69,82 @@ export const Accordion: React.FC< | |
titleClassName, | ||
descriptionClassName, | ||
fullWidthContent, | ||
open, | ||
onClickSummary, | ||
onToggle = () => {}, | ||
...rest | ||
}) => { | ||
const details = React.useRef(null) | ||
const content = React.useRef(null) | ||
const [isOpen, setIsOpen] = useState(open) | ||
const details = React.useRef<HTMLDetailsElement>(null) | ||
const summary = React.useRef<HTMLDivElement>(null) | ||
const content = React.useRef<HTMLDivElement>(null) | ||
|
||
// Synchronize internal state with the `open` prop | ||
useEffect(() => { | ||
setIsOpen(open) | ||
}, [open]) | ||
|
||
React.useEffect(() => { | ||
if (details.current && content.current) { | ||
new DetailsAnimation(details.current, content.current) | ||
} | ||
}, []) | ||
|
||
useEffect(() => { | ||
const summaryElement = summary?.current | ||
|
||
if (summaryElement) { | ||
const handleSummaryClick = (event: MouseEvent) => { | ||
// Stop the native event | ||
event.preventDefault() | ||
event.stopPropagation() | ||
|
||
let onClickRet: boolean | undefined = true | ||
if (onClickSummary) { | ||
onClickRet = onClickSummary(event) | ||
} | ||
|
||
// TODO: Clone the original event then check propagation. | ||
// If the callback returns false, don't continue. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this an open todo? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is. The ideal code would clone the original event and link stopPropagation. For now, that's not ideal for the time we have to finish, we'll keep it simple for now and it's not a blocker. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there an issue to link to track that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No. Actually I'm not sure we need it until we know that we will need it. In other words, it's not absolutely required but I wanted to leave a note in there in case someone wonders why stopPropagation doesn't work. |
||
if (onClickRet === false) { | ||
return | ||
} | ||
|
||
const newIsOpen = !isOpen | ||
setIsOpen(newIsOpen) | ||
|
||
onToggle(newIsOpen) | ||
} | ||
|
||
summaryElement.addEventListener('click', handleSummaryClick, { | ||
capture: true, | ||
}) | ||
|
||
return () => { | ||
if (summaryElement) { | ||
summaryElement.removeEventListener('click', handleSummaryClick, { | ||
capture: true, | ||
}) | ||
} | ||
} | ||
} | ||
|
||
return | ||
}, [isOpen, onClickSummary, onToggle]) | ||
|
||
return ( | ||
<details {...rest} className={clsx(rest.className)} ref={details}> | ||
<details | ||
className={clsx(rest.className)} | ||
ref={details} | ||
open={isOpen} | ||
{...rest} | ||
> | ||
<summary | ||
className={clsx( | ||
CssClasses.summary, | ||
headingClassName ?? CssClasses.summaryColor, | ||
)} | ||
ref={summary} | ||
> | ||
<span className={CssClasses.summaryDiv}> | ||
{Boolean(iconEl) && <span className={CssClasses.icon}>{iconEl}</span>} | ||
|
@@ -76,7 +176,11 @@ export const Accordion: React.FC< | |
</span> | ||
<IconChevronDownSmall | ||
strokeColor="gray-300" | ||
className={clsx('open:icon-dark-gray-500', CssClasses.chevron)} | ||
className={clsx( | ||
'chevron', | ||
'open:icon-dark-gray-500', | ||
CssClasses.chevron, | ||
)} | ||
/> | ||
</span> | ||
</summary> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing
open
from prop typesThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see-
React.HTMLProps<HTMLDetailsElement>
. It's not missing.