From 91752e652ae4c589ab27037700962dfc9a8c253a Mon Sep 17 00:00:00 2001 From: Karl Snyder Date: Fri, 3 Jan 2025 13:56:26 -0500 Subject: [PATCH 1/9] Update Accordion --- components/Accordion/react/Accordion.tsx | 103 +++++++++++++++++++++-- 1 file changed, 97 insertions(+), 6 deletions(-) diff --git a/components/Accordion/react/Accordion.tsx b/components/Accordion/react/Accordion.tsx index e5c57e737..3035482b2 100644 --- a/components/Accordion/react/Accordion.tsx +++ b/components/Accordion/react/Accordion.tsx @@ -1,11 +1,14 @@ -import * as React from 'react' +import React, { ReactNode, 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 AccordionPropsReact + extends Omit { + title?: string | undefined + description: string | ReactNode | undefined /** * Icon to be displayed on the left of the the heading. Overridden by the iconEl prop, if both are provided. */ @@ -14,6 +17,24 @@ 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 + /** + * If true, prevents the accordion from toggling open or closed. + */ + locked?: 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 + /** + * Callback triggered when a toggle attempt is blocked because the accordion is locked. + */ + onToggleBlocked?: () => void } export const Accordion: React.FC< @@ -29,22 +50,88 @@ export const Accordion: React.FC< titleClassName, descriptionClassName, fullWidthContent, + open, + locked = false, + onClickSummary, + onToggle = () => {}, + onToggleBlocked = () => {}, ...rest }) => { - const details = React.useRef(null) - const content = React.useRef(null) + const [isOpen, setIsOpen] = useState(open) + const details = React.useRef(null) + const summary = React.useRef(null) + const content = React.useRef(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. + if (onClickRet === false) { + return + } + + if (locked) { + onToggleBlocked() + } else { + const newIsOpen = !isOpen + setIsOpen(newIsOpen) + + onToggle(newIsOpen) + } + } + + summaryElement.addEventListener('click', handleSummaryClick, { + capture: true, + }) + + return () => { + if (summaryElement) { + summaryElement.removeEventListener('click', handleSummaryClick, { + capture: true, + }) + } + } + } + + return + }, [isOpen, locked, onClickSummary, onToggle, onToggleBlocked]) + return ( -
+
{Boolean(iconEl) && {iconEl}} @@ -76,7 +163,11 @@ export const Accordion: React.FC< From 9c5a8863995c758234fdcb15c1696c9059c0aca1 Mon Sep 17 00:00:00 2001 From: Karl Snyder Date: Fri, 3 Jan 2025 14:04:43 -0500 Subject: [PATCH 2/9] Create famous-melons-ring.md --- .changeset/famous-melons-ring.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/famous-melons-ring.md diff --git a/.changeset/famous-melons-ring.md b/.changeset/famous-melons-ring.md new file mode 100644 index 000000000..2212227e0 --- /dev/null +++ b/.changeset/famous-melons-ring.md @@ -0,0 +1,5 @@ +--- +'@cypress-design/react-accordion': minor +--- + +Add lock, click summary event, toggle event, toggle blocked event, open state and click summary event cancel From 225c7a8e03c2fb8cdb32ff04880b9b7b079be4ce Mon Sep 17 00:00:00 2001 From: Karl Snyder Date: Fri, 3 Jan 2025 15:01:39 -0500 Subject: [PATCH 3/9] cleanup --- components/Accordion/constants/src/index.ts | 38 ------------------- components/Accordion/react/Accordion.tsx | 41 +++++++++++++++++---- 2 files changed, 34 insertions(+), 45 deletions(-) diff --git a/components/Accordion/constants/src/index.ts b/components/Accordion/constants/src/index.ts index 537e42da3..495302cbc 100644 --- a/components/Accordion/constants/src/index.ts +++ b/components/Accordion/constants/src/index.ts @@ -13,41 +13,3 @@ export const CssClasses = { contentWrapper: 'border border-gray-100 rounded-b-md border-t-0', content: 'px-[24px] py-[16px]', } - -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 - /** - * Icon to be displayed on the left of the the heading. - */ - icon?: any - /** - * 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 -} diff --git a/components/Accordion/react/Accordion.tsx b/components/Accordion/react/Accordion.tsx index 3035482b2..6e994c1ff 100644 --- a/components/Accordion/react/Accordion.tsx +++ b/components/Accordion/react/Accordion.tsx @@ -1,14 +1,19 @@ -import React, { ReactNode, useEffect, useState } 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 Omit { - title?: string | undefined - description: string | ReactNode | undefined +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. */ @@ -17,6 +22,28 @@ export interface AccordionPropsReact * 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 /** * If true, prevents the accordion from toggling open or closed. */ @@ -38,7 +65,7 @@ export interface AccordionPropsReact } export const Accordion: React.FC< - AccordionPropsReact & React.HTMLProps + AccordionProps & React.HTMLProps > = ({ title, description, From 13ca2918948da459c76ebeca8f5c69682ba964ad Mon Sep 17 00:00:00 2001 From: Karl Snyder Date: Fri, 3 Jan 2025 15:15:37 -0500 Subject: [PATCH 4/9] tests --- components/Accordion/assertions.ts | 62 ++++++++++++++++++- .../Accordion/react/Accordion.rootstory.tsx | 12 ++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/components/Accordion/assertions.ts b/components/Accordion/assertions.ts index 6a94506b4..cc0de8264 100644 --- a/components/Accordion/assertions.ts +++ b/components/Accordion/assertions.ts @@ -9,6 +9,12 @@ export interface AccordionStoryOptions { open?: boolean fullWidthContent?: boolean headingClassName?: string + titleClassName?: string + descriptionClassName?: string + locked?: boolean + onClickSummary?: (event: MouseEvent) => boolean | undefined + onToggle?: (open: boolean) => void + onToggleBlocked?: () => void } export default function assertions( @@ -37,7 +43,7 @@ export default function assertions( it('displays a separator when separator:true', () => { mountStory({ separator: true }) - // the separator has a with of 1px. For some reason cypress detects it as invisible. + // the separator has a width of 1px. For some reason cypress detects it as invisible. cy.get('[role="separator"]').should('exist') }) @@ -60,6 +66,60 @@ export default function assertions( cy.get('details summary').should('have.class', 'bg-gray-50') }) + it('applies the titleClassName correctly', () => { + mountStory({ titleClassName: 'text-indigo-600' }) + + cy.get('details summary span') + .first() + .should('have.class', 'text-indigo-600') + }) + + it('applies the descriptionClassName correctly', () => { + mountStory({ descriptionClassName: 'text-gray-500' }) + + cy.get('details summary span').eq(1).should('have.class', 'text-gray-500') + }) + + it('does not toggle when locked', () => { + mountStory({ locked: true }) + cy.get('details summary').click() + + cy.contains('Lorem ipsum, dolor sit amet').should('not.be.visible') + }) + + it('calls onClickSummary when summary is clicked', () => { + const onClickSummary = cy.stub() + mountStory({ onClickSummary }) + + cy.get('details summary') + .click() + .then(() => { + expect(onClickSummary).to.have.been.called + }) + }) + + it('calls onToggle with the new state when toggled', () => { + const onToggle = cy.stub() + mountStory({ onToggle }) + + cy.get('details summary') + .click() + .then(() => { + expect(onToggle).to.have.been.calledWith(true) + }) + }) + + it('calls onToggleBlocked when toggle attempt is blocked', () => { + const onToggleBlocked = cy.stub() + mountStory({ locked: true, onToggleBlocked }) + + cy.get('details summary') + .click() + .then(() => { + expect(onToggleBlocked).to.have.been.called + }) + }) + it('should not show a separator if no icon is provided', () => { mountStory({ separator: true, icon: null }) diff --git a/components/Accordion/react/Accordion.rootstory.tsx b/components/Accordion/react/Accordion.rootstory.tsx index 626ef1f0d..064c3c07d 100644 --- a/components/Accordion/react/Accordion.rootstory.tsx +++ b/components/Accordion/react/Accordion.rootstory.tsx @@ -13,6 +13,12 @@ export default (options: AccordionStoryOptions = {}) => { open = false, fullWidthContent = false, headingClassName, + titleClassName, + descriptionClassName, + locked = false, + onClickSummary, + onToggle, + onToggleBlocked, ...rest } = options return ( @@ -26,6 +32,12 @@ export default (options: AccordionStoryOptions = {}) => { open={open} fullWidthContent={fullWidthContent} headingClassName={headingClassName} + titleClassName={titleClassName} + descriptionClassName={descriptionClassName} + locked={locked} + onClickSummary={onClickSummary} + onToggle={onToggle} + onToggleBlocked={onToggleBlocked} {...rest} >

From cc2a041f51afac944214b8a9a8c21117659dc34c Mon Sep 17 00:00:00 2001 From: Karl Snyder Date: Fri, 3 Jan 2025 21:02:00 -0500 Subject: [PATCH 5/9] Fix tests --- ReadMe.md | 2 +- components/Accordion/assertions.ts | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index b491ffd32..49fcb9af1 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -67,7 +67,7 @@ This will: - install all dependencies if anything is missing - build all the components, specially the ones needed for the docs - start the docs website locally -- give you a link to open in your browser. +- give you a link to open in your browser. (http://localhost:5173/) ### Create a new component diff --git a/components/Accordion/assertions.ts b/components/Accordion/assertions.ts index cc0de8264..a4c6ada7e 100644 --- a/components/Accordion/assertions.ts +++ b/components/Accordion/assertions.ts @@ -38,7 +38,7 @@ export default function assertions( cy.get('details summary').click() - cy.contains('Lorem ipsum, dolor sit amet').should('not.be.visible') + cy.get('details').should('not.have.attr', 'open') }) it('displays a separator when separator:true', () => { @@ -70,21 +70,29 @@ export default function assertions( mountStory({ titleClassName: 'text-indigo-600' }) cy.get('details summary span') - .first() - .should('have.class', 'text-indigo-600') + .find('.text-indigo-600') + .should('exist') + .and('have.text', 'Accordion Title') }) it('applies the descriptionClassName correctly', () => { - mountStory({ descriptionClassName: 'text-gray-500' }) + mountStory({ descriptionClassName: 'text-gray-700' }) - cy.get('details summary span').eq(1).should('have.class', 'text-gray-500') + cy.get('details summary') + .find('.text-gray-700') + .should('exist') + .and( + 'have.text', + 'Vestibulum id ligula porta felis euismod semper. Nulla vitae elit libero, a pharetra augue. Aenean lacinia bibendum nulla.', + ) }) it('does not toggle when locked', () => { mountStory({ locked: true }) + cy.get('details summary').click() - cy.contains('Lorem ipsum, dolor sit amet').should('not.be.visible') + cy.get('details').should('not.have.attr', 'open') }) it('calls onClickSummary when summary is clicked', () => { From 50004a8011514b80ff98e396891727cd5b43ad28 Mon Sep 17 00:00:00 2001 From: Karl Snyder Date: Fri, 3 Jan 2025 21:32:03 -0500 Subject: [PATCH 6/9] Fix vue --- .../Accordion/vue/Accordion.rootstory.tsx | 12 +++ components/Accordion/vue/Accordion.vue | 75 ++++++++++++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/components/Accordion/vue/Accordion.rootstory.tsx b/components/Accordion/vue/Accordion.rootstory.tsx index 83ae92039..b5a2120d7 100644 --- a/components/Accordion/vue/Accordion.rootstory.tsx +++ b/components/Accordion/vue/Accordion.rootstory.tsx @@ -12,6 +12,12 @@ export default (options: AccordionStoryOptions = {}) => { open = false, fullWidthContent = false, headingClassName, + titleClassName, + descriptionClassName, + locked = false, + onClickSummary, + onToggle, + onToggleBlocked, } = options return ( @@ -25,6 +31,12 @@ export default (options: AccordionStoryOptions = {}) => { open={open} fullWidthContent={fullWidthContent} headingClassName={headingClassName} + titleClassName={titleClassName} + descriptionClassName={descriptionClassName} + locked={locked} + onClickSummary={onClickSummary} + onToggle={onToggle} + onToggleBlocked={onToggleBlocked} > {{ default: () => ( diff --git a/components/Accordion/vue/Accordion.vue b/components/Accordion/vue/Accordion.vue index 7ad19d3c2..c561b367e 100644 --- a/components/Accordion/vue/Accordion.vue +++ b/components/Accordion/vue/Accordion.vue @@ -1,6 +1,7 @@