diff --git a/.changeset/light-olives-grab.md b/.changeset/light-olives-grab.md new file mode 100644 index 000000000..fc28acf17 --- /dev/null +++ b/.changeset/light-olives-grab.md @@ -0,0 +1,5 @@ +--- +"@hopper-ui/components": patch +--- + +Added the Accordion component. diff --git a/apps/docs/components/card/card.css b/apps/docs/components/card/card.css index fb866a277..a5f1ca301 100644 --- a/apps/docs/components/card/card.css +++ b/apps/docs/components/card/card.css @@ -12,6 +12,7 @@ background: linear-gradient(90deg, var(--background) 0.625rem, transparent 1%) 50%, linear-gradient(var(--background) 0.625rem, transparent 1%) 50%, var(--dot-background); + background-position: 0 0; background-size: 0.75rem 0.75rem; color: var(--color); } diff --git a/apps/docs/content/components/navigation/Accordion.mdx b/apps/docs/content/components/navigation/Accordion.mdx new file mode 100644 index 000000000..e13da42ee --- /dev/null +++ b/apps/docs/content/components/navigation/Accordion.mdx @@ -0,0 +1,102 @@ +--- +title: Accordion +description: An Accordion is a grouping of related disclosures. It supports both single and multiple expanded items. +category: "navigation" +links: + source: https://github.com/gsoft-inc/wl-hopper/blob/main/packages/components/src/Accordion/src/Accordion.tsx +--- + + + + + ## Guidelines + + TODO: If we have some guidelines about this component's usage + + ### Accessibility ? + + TODO: If we have some guidelines about this component and accessibility + + +## Anatomy + + + TODO: We have anatomy screenshots from the Figma, we could most likely use them here + + ### Concepts + + TODO: links to related concepts + + +### Composed Components + +An `Accordion` uses the following components. + + + +## Usage + +### Disabled + +An accordion can be disabled. + + + +### Variants + +An accordion has multiple variants. + +**Standalone** - Used when the accordion is not inside a container. + + + +**Inline** - Used when placing a accordion inside a container. + + + +### Expanded + +By default, only one disclosure will be expanded at a time. Use `allowsMultipleExpanded` prop to expand multiple disclosures. + + + +### Icon + +An accordion heading can contain an icon. + + + +### Description + +An accordion heading can contain a description. + + + +### Controlled + +An accordion can handle its opened panels in controlled mode. + + + + + ## Advanced customization + + ### Contexts + TODO: Example of context + content about the context + + ### Custom Children + + TODO: Example of passing custom children to the components to fake a slot + + ### Custom Component + + TODO: Example of creating a custom version of this component + + +## Props + + + +## Migration Notes + + diff --git a/apps/docs/content/components/navigation/Disclosure.mdx b/apps/docs/content/components/navigation/Disclosure.mdx index e85743024..39e4aa325 100644 --- a/apps/docs/content/components/navigation/Disclosure.mdx +++ b/apps/docs/content/components/navigation/Disclosure.mdx @@ -33,7 +33,7 @@ links: A `Disclosure` uses the following components: - + ## Usage diff --git a/apps/docs/examples/Preview.ts b/apps/docs/examples/Preview.ts index 403776f49..055b1f324 100644 --- a/apps/docs/examples/Preview.ts +++ b/apps/docs/examples/Preview.ts @@ -854,6 +854,30 @@ export const Previews: Record = { "layout/docs/stack/alignY": { component: lazy(() => import("@/../../packages/components/src/layout/docs/stack/alignY.tsx")) }, + "Accordion/docs/preview": { + component: lazy(() => import("@/../../packages/components/src/Accordion/docs/preview.tsx")) + }, + "Accordion/docs/disabled": { + component: lazy(() => import("@/../../packages/components/src/Accordion/docs/disabled.tsx")) + }, + "Accordion/docs/standalone": { + component: lazy(() => import("@/../../packages/components/src/Accordion/docs/standalone.tsx")) + }, + "Accordion/docs/inline": { + component: lazy(() => import("@/../../packages/components/src/Accordion/docs/inline.tsx")) + }, + "Accordion/docs/multiple-selection": { + component: lazy(() => import("@/../../packages/components/src/Accordion/docs/multiple-selection.tsx")) + }, + "Accordion/docs/icon": { + component: lazy(() => import("@/../../packages/components/src/Accordion/docs/icon.tsx")) + }, + "Accordion/docs/description": { + component: lazy(() => import("@/../../packages/components/src/Accordion/docs/description.tsx")) + }, + "Accordion/docs/controlled": { + component: lazy(() => import("@/../../packages/components/src/Accordion/docs/controlled.tsx")) + }, "Disclosure/docs/preview": { component: lazy(() => import("@/../../packages/components/src/Disclosure/docs/preview.tsx")) }, diff --git a/apps/docs/examples/overview/Accordion.svg b/apps/docs/examples/overview/Accordion.svg new file mode 100644 index 000000000..4a050909e --- /dev/null +++ b/apps/docs/examples/overview/Accordion.svg @@ -0,0 +1 @@ + diff --git a/apps/docs/examples/overview/index.ts b/apps/docs/examples/overview/index.ts index cee951509..82cf6e54b 100644 --- a/apps/docs/examples/overview/index.ts +++ b/apps/docs/examples/overview/index.ts @@ -1,5 +1,6 @@ import type { FunctionComponent, SVGProps } from "react"; +import Accordion from "./Accordion.svg"; import Avatar from "./Avatar.svg"; import Badge from "./Badge.svg"; import Button from "./Button.svg"; @@ -48,6 +49,7 @@ interface OverviewComponentsType { } export const OverviewComponents: OverviewComponentsType = { + Accordion, Avatar, Badge, Button, diff --git a/packages/components/src/Accordion/docs/controlled.tsx b/packages/components/src/Accordion/docs/controlled.tsx new file mode 100644 index 000000000..9f68af615 --- /dev/null +++ b/packages/components/src/Accordion/docs/controlled.tsx @@ -0,0 +1,39 @@ +import { Accordion, Disclosure, DisclosureHeader, DisclosurePanel, Div, Span } from "@hopper-ui/components"; +import { useState } from "react"; + +export default function Example() { + const [expandedKeys, setExpandedKeys] = useState>(new Set()); + + const handleExpandedChange = (keys: Set) => { + setExpandedKeys(keys); + }; + + return ( +
+ + {expandedKeys.size > 0 ? `${Array.from(expandedKeys).join(", ")} is opened.` : "No sections are opened."} + + + + Workleap Officevibe + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + Workleap Pingboard + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + Workleap Performance + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +
+ ); +} diff --git a/packages/components/src/Accordion/docs/description.tsx b/packages/components/src/Accordion/docs/description.tsx new file mode 100644 index 000000000..7eaa0e02b --- /dev/null +++ b/packages/components/src/Accordion/docs/description.tsx @@ -0,0 +1,38 @@ +import { Accordion, Disclosure, DisclosureHeader, DisclosurePanel, Div, Inline, Text } from "@hopper-ui/components"; + +export default function Example() { + return ( +
+ + + + + Workleap Officevibe + Engagement survey and feedback + + + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + + + Workleap Pingboard + Interactive org chart and directory + + + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + + + Workleap Performance + Performance review management and tracking + + + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +
+ ); +} + diff --git a/packages/components/src/Accordion/docs/disabled.tsx b/packages/components/src/Accordion/docs/disabled.tsx new file mode 100644 index 000000000..125683010 --- /dev/null +++ b/packages/components/src/Accordion/docs/disabled.tsx @@ -0,0 +1,22 @@ +import { Accordion, Disclosure, DisclosureHeader, DisclosurePanel, Div } from "@hopper-ui/components"; + +export default function Example() { + return ( +
+ + + Workleap Officevibe + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + Workleap Pingboard + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + Workleap Performance + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +
+ ); +} diff --git a/packages/components/src/Accordion/docs/icon.tsx b/packages/components/src/Accordion/docs/icon.tsx new file mode 100644 index 000000000..4fda2424e --- /dev/null +++ b/packages/components/src/Accordion/docs/icon.tsx @@ -0,0 +1,32 @@ +import { Accordion, Disclosure, DisclosureHeader, DisclosurePanel, Div, Text } from "@hopper-ui/components"; +import { PinSolidIcon, SparklesIcon, SproutIcon } from "@hopper-ui/icons"; + +export default function Example() { + return ( +
+ + + + + Workleap Officevibe + + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + + + Workleap Pingboard + + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + + + Workleap Performance + + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +
+ ); +} diff --git a/packages/components/src/Accordion/docs/inline.tsx b/packages/components/src/Accordion/docs/inline.tsx new file mode 100644 index 000000000..497bb64e8 --- /dev/null +++ b/packages/components/src/Accordion/docs/inline.tsx @@ -0,0 +1,22 @@ +import { Accordion, Disclosure, DisclosureHeader, DisclosurePanel, Div } from "@hopper-ui/components"; + +export default function Example() { + return ( +
+ + + Workleap Officevibe + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + Workleap Pingboard + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + Workleap Performance + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +
+ ); +} diff --git a/packages/components/src/Accordion/docs/migration-notes.md b/packages/components/src/Accordion/docs/migration-notes.md new file mode 100644 index 000000000..66f03b6d1 --- /dev/null +++ b/packages/components/src/Accordion/docs/migration-notes.md @@ -0,0 +1,7 @@ +Coming from Orbiter, you should be aware of the following changes: + +- `expansionMode="multiple"` has been replaced with `allowsMultipleExpanded`. +- `borderless` and `bordered` variants are no more. `inline` and `standalone` are the new variants. There is no direct match; the new variants are context-based, depending on whether an accordion is contained or not. +- `autofocus` is removed. It did not make sense to have. +- The `disclosure` component is used instead of `Item`. +- `disabled` is renamed to `isDisabled` on the item/disclosure. diff --git a/packages/components/src/Accordion/docs/multiple-selection.tsx b/packages/components/src/Accordion/docs/multiple-selection.tsx new file mode 100644 index 000000000..1949c0972 --- /dev/null +++ b/packages/components/src/Accordion/docs/multiple-selection.tsx @@ -0,0 +1,22 @@ +import { Accordion, Disclosure, DisclosureHeader, DisclosurePanel, Div } from "@hopper-ui/components"; + +export default function Example() { + return ( +
+ + + Workleap Officevibe + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + Workleap Pingboard + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + Workleap Performance + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +
+ ); +} diff --git a/packages/components/src/Accordion/docs/preview.tsx b/packages/components/src/Accordion/docs/preview.tsx new file mode 100644 index 000000000..0eb0113e1 --- /dev/null +++ b/packages/components/src/Accordion/docs/preview.tsx @@ -0,0 +1,22 @@ +import { Accordion, Disclosure, DisclosureHeader, DisclosurePanel, Div } from "@hopper-ui/components"; + +export default function Example() { + return ( +
+ + + Workleap Officevibe + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + Workleap Pingboard + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + Workleap Performance + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +
+ ); +} diff --git a/packages/components/src/Accordion/docs/standalone.tsx b/packages/components/src/Accordion/docs/standalone.tsx new file mode 100644 index 000000000..c1c39ffea --- /dev/null +++ b/packages/components/src/Accordion/docs/standalone.tsx @@ -0,0 +1,22 @@ +import { Accordion, Disclosure, DisclosureHeader, DisclosurePanel, Div } from "@hopper-ui/components"; + +export default function Example() { + return ( +
+ + + Workleap Officevibe + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + Workleap Pingboard + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + Workleap Performance + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +
+ ); +} diff --git a/packages/components/src/Accordion/index.ts b/packages/components/src/Accordion/index.ts new file mode 100644 index 000000000..401c73ac2 --- /dev/null +++ b/packages/components/src/Accordion/index.ts @@ -0,0 +1 @@ +export * from "./src/index.ts"; diff --git a/packages/components/src/Accordion/src/Accordion.module.css b/packages/components/src/Accordion/src/Accordion.module.css new file mode 100644 index 000000000..e7c7606f4 --- /dev/null +++ b/packages/components/src/Accordion/src/Accordion.module.css @@ -0,0 +1,53 @@ +.hop-Accordion { + /* Standalone */ + --hop-Accordion-standalone-border-size: var(--hop-space-10); + --hop-Accordion-standalone-border-color: var(--hop-neutral-border-weak); + --hop-Accordion-standalone-border-radius: var(--hop-shape-rounded-md); + --hop-Accordion-standalone-box-shadow: var(--hop-elevation-raised); + --hop-Accordion-standalone-disclosure-border-radius-size: var(--hop-shape-rounded-md); + --hop-Accordion-standalone-disclosure-last-child-border-block-end-size: 0; + + /* Inline */ + --hop-Accordion-inline-border-size: var(--hop-space-10); + --hop-Accordion-inline-border-color: transparent; + --hop-Accordion-inline-border-radius: 0; + --hop-Accordion-inline-box-shadow: 0; + --hop-Accordion-inline-disclosure-border-radius-size: 0; + --hop-Accordion-inline-disclosure-last-child-border-block-end-size: var(--hop-space-10); + + /* Internal Variables */ + --border-size: var(--hop-Accordion-standalone-border-size); + --border-color: var(--hop-Accordion-standalone-border-color); + --border-radius: var(--hop-Accordion-standalone-border-radius); + --box-shadow: var(--hop-Accordion-standalone-box-shadow); + --disclosure-border-radius-size: var(--hop-Accordion-standalone-disclosure-border-radius-size); + --disclosure-last-child-border-block-end-size: var(--hop-Accordion-standalone-disclosure-last-child-border-block-end-size); + + overflow: hidden; + border: var(--border-size) solid var(--border-color); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); +} + +.hop-Accordion--inline { + --border-size: var(--hop-Accordion-inline-border-size); + --border-color: var(--hop-Accordion-inline-border-color); + --border-radius: var(--hop-Accordion-inline-border-radius); + --box-shadow: var(--hop-Accordion-inline-box-shadow); + --disclosure-border-radius-size: var(--hop-Accordion-inline-disclosure-border-radius-size); + --disclosure-last-child-border-block-end-size: var(--hop-Accordion-inline-disclosure-last-child-border-block-end-size); +} + +.hop-Accordion__disclosure:first-child .hop-Accordion__disclosure-header button[slot="trigger"] { + border-start-start-radius: var(--disclosure-border-radius-size); + border-start-end-radius: var(--disclosure-border-radius-size); +} + +.hop-Accordion__disclosure:last-child:not([data-expanded]) .hop-Accordion__disclosure-header button[slot="trigger"] { + border-end-start-radius: var(--disclosure-border-radius-size); + border-end-end-radius: var(--disclosure-border-radius-size); +} + +.hop-Accordion__disclosure:last-child .hop-Accordion__disclosure-panel { + border-block-end-width: var(--disclosure-last-child-border-block-end-size); +} diff --git a/packages/components/src/Accordion/src/Accordion.tsx b/packages/components/src/Accordion/src/Accordion.tsx new file mode 100644 index 000000000..85dfbe852 --- /dev/null +++ b/packages/components/src/Accordion/src/Accordion.tsx @@ -0,0 +1,94 @@ +import { useStyledSystem, type StyledComponentProps } from "@hopper-ui/styled-system"; +import type { DOMProps } from "@react-types/shared"; +import { forwardRef, type ForwardedRef } from "react"; +import { + composeRenderProps, + DisclosureGroup as RACDisclosureGroup, + useContextProps, + type DisclosureGroupProps as RACDisclosureGroupProps, + type SlotProps +} from "react-aria-components"; + +import { DisclosureContext, DisclosureHeaderContext, DisclosurePanelContext } from "../../Disclosure/index.ts"; +import { composeClassnameRenderProps, cssModule, SlotProvider } from "../../utils/index.ts"; + +import { AccordionContext } from "./AccordionContext.ts"; + +import styles from "./Accordion.module.css"; + +export const GlobalAccordionCssSelector = "hop-Accordion"; + +export interface AccordionProps extends StyledComponentProps, DOMProps, SlotProps { + variant?: "standalone" | "inline"; +} + +function Accordion(props:AccordionProps, ref: ForwardedRef) { + [props, ref] = useContextProps(props, ref, AccordionContext); + const { stylingProps, ...ownProps } = useStyledSystem(props); + const { + className, + children: childrenProp, + style: styleProp, + variant = "standalone", + ...otherProps + } = ownProps; + + const classNames = composeClassnameRenderProps( + className, + GlobalAccordionCssSelector, + cssModule( + styles, + "hop-Accordion", + variant + ), + stylingProps.className + ); + + const style = composeRenderProps(styleProp, prev => { + return { + ...stylingProps.style, + ...prev + }; + }); + + const children = composeRenderProps(childrenProp, prev => { + return prev; + }); + + return ( + + {accordionRenderProps => ( + + {children(accordionRenderProps)} + + )} + + ); +} + +/** + * An accordion is a container for multiple disclosures. + * + * [View Documentation](TODO) + */ +const _Accordion = forwardRef(Accordion); +_Accordion.displayName = "Accordion"; + +export { _Accordion as Accordion }; diff --git a/packages/components/src/Accordion/src/AccordionContext.ts b/packages/components/src/Accordion/src/AccordionContext.ts new file mode 100644 index 000000000..9e7ed96cf --- /dev/null +++ b/packages/components/src/Accordion/src/AccordionContext.ts @@ -0,0 +1,8 @@ +import { createContext } from "react"; +import type { ContextValue } from "react-aria-components"; + +import type { AccordionProps } from "./Accordion.tsx"; + +export const AccordionContext = createContext>({}); + +AccordionContext.displayName = "AccordionContext"; diff --git a/packages/components/src/Accordion/src/index.ts b/packages/components/src/Accordion/src/index.ts new file mode 100644 index 000000000..35f2d8742 --- /dev/null +++ b/packages/components/src/Accordion/src/index.ts @@ -0,0 +1,2 @@ +export * from "./Accordion.tsx"; +export * from "./AccordionContext.ts"; diff --git a/packages/components/src/Accordion/tests/chromatic/Accordion.stories.tsx b/packages/components/src/Accordion/tests/chromatic/Accordion.stories.tsx new file mode 100644 index 000000000..2bfb0dfe9 --- /dev/null +++ b/packages/components/src/Accordion/tests/chromatic/Accordion.stories.tsx @@ -0,0 +1,298 @@ +import { SparklesIcon } from "@hopper-ui/icons"; +import type { Meta, StoryObj } from "@storybook/react"; +import { within } from "@storybook/test"; + +import { Disclosure, DisclosureHeader, DisclosurePanel } from "../../../Disclosure/index.ts"; +import { Inline, Stack } from "../../../layout/index.ts"; +import { Text } from "../../../typography/Text/index.ts"; +import { Accordion, type AccordionProps } from "../../src/Accordion.tsx"; + +const meta = { + title: "Components/Accordion", + component: Accordion +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default = { + render: args => ( + +

Default

+ + + Workleap Officevibe + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + Workleap Pingboard + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + Workleap Performance + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +

Multiple Expanded

+ + + Workleap Officevibe + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + Workleap Pingboard + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + Workleap Performance + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +

Description

+ + + + + Workleap Officevibe + Engagement and Feedback + + + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + + + Workleap Pingboard + Org Chart + + + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + + + Workleap Performance + Performance Management + + + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +

Icon

+ + + + + + Workleap Officevibe + Engagement and Feedback + + + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + + + + Workleap Pingboard + Org Chart + + + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + + + + Workleap Performance + Performance Management + + + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +

Style

+ + + Workleap Officevibe + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + Workleap Pingboard + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + Workleap Performance + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +

Zoom

+ + + + + + Workleap Officevibe + Engagement and Feedback + + + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + + + + Workleap Pingboard + Org Chart + + + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + + + + Workleap Performance + Performance Management + + + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + + + + + + + Workleap Officevibe + Engagement and Feedback + + + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + + + + Workleap Pingboard + Org Chart + + + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + + + + Workleap Performance + Performance Management + + + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +
+ ), + args: { + defaultExpandedKeys: ["officevibe"] + } +} satisfies Story; + +export const InlineVariant = { + ...Default, + args: { + defaultExpandedKeys: ["officevibe"], + variant: "inline" + } +} satisfies Story; + +const StateTemplate = (args: Partial) => ( + + + + + + Workleap Officevibe + Engagement and Feedback + + + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + + + + Workleap Pingboard + Org Chart + + + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + + + + Workleap Performance + Performance Management + + + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + +); + +export const DefaultStates = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const triggers = canvas.getAllByRole("button"); + + triggers.forEach(trigger => { + if (trigger.getAttribute("disabled") !== "") { + const accordionElem = trigger.closest(".hop-Accordion"); + + if (accordionElem?.getAttribute("data-chromatic-force-focus")) { + trigger?.setAttribute("data-focus-visible", "true"); + accordionElem?.removeAttribute("data-chromatic-force-focus"); + } + + if (accordionElem?.getAttribute("data-chromatic-force-press")) { + trigger?.setAttribute("data-pressed", "true"); + accordionElem?.removeAttribute("data-chromatic-force-press"); + } + + if (accordionElem?.getAttribute("data-chromatic-force-hover")) { + trigger.setAttribute("data-hovered", "true"); + accordionElem?.removeAttribute("data-chromatic-force-hover"); + } + } + }); + }, + render: args => ( + +

Default

+ +

Disabled

+ +

Focus Visible

+ +

Hovered

+ +

Pressed

+ +

Focus Visible & Disabled

+ +
+ ), + args: { + defaultExpandedKeys: ["officevibe"] + } +} satisfies Story; + +export const InlineStates = { + ...DefaultStates, + args: { + defaultExpandedKeys: ["officevibe"], + variant: "inline" + } +} satisfies Story; \ No newline at end of file diff --git a/packages/components/src/Accordion/tests/jest/Accordion.ssr.test.tsx b/packages/components/src/Accordion/tests/jest/Accordion.ssr.test.tsx new file mode 100644 index 000000000..3adac5a33 --- /dev/null +++ b/packages/components/src/Accordion/tests/jest/Accordion.ssr.test.tsx @@ -0,0 +1,31 @@ +/** + * @jest-environment node + */ +import { renderToString } from "react-dom/server"; + +import { Disclosure, DisclosureHeader, DisclosurePanel } from "../../../Disclosure/index.ts"; +import { Accordion } from "../../src/Accordion.tsx"; + +describe("Accordion", () => { + it("should render on the server", () => { + const renderOnServer = () => + renderToString( + + + Workleap Officevibe + Help employees speak up and make sure they feel heard. Continuous and real-time surveys offer feedback to celebrate every win, recognize commitment, and uncover challenges. + + + Workleap Pingboard + Make teamwork work. Use your org chart to create lasting connections across your distributed and hybrid teams to make collaboration easier. + + + Workleap Performance + Drive impact by simplifying how your leaders and you manage team performance throughout the year. + + + ); + + expect(renderOnServer).not.toThrow(); + }); +}); diff --git a/packages/components/src/Accordion/tests/jest/Accordion.test.tsx b/packages/components/src/Accordion/tests/jest/Accordion.test.tsx new file mode 100644 index 000000000..307f71b14 --- /dev/null +++ b/packages/components/src/Accordion/tests/jest/Accordion.test.tsx @@ -0,0 +1,156 @@ +import { render, screen } from "@hopper-ui/test-utils"; +import { createRef } from "react"; + +import { Disclosure, DisclosureHeader, DisclosurePanel } from "../../../Disclosure/index.ts"; +import { Accordion } from "../../src/Accordion.tsx"; +import { AccordionContext } from "../../src/AccordionContext.ts"; + +describe("Accordion", () => { + it("should render with default class", () => { + render( + + + + Disclosure Header + + + Disclosure Panel + + + + ); + + const element = screen.getByTestId("accordion"); + expect(element).toHaveClass("hop-Accordion"); + }); + + it("should support custom class", () => { + render( + + + + Disclosure Header + + + Disclosure Panel + + + + ); + + const element = screen.getByTestId("accordion"); + expect(element).toHaveClass("hop-Accordion"); + expect(element).toHaveClass("test"); + }); + + it("should support custom style", () => { + render( + + + + Disclosure Header + + + Disclosure Panel + + + + ); + + const element = screen.getByTestId("accordion"); + expect(element).toHaveStyle({ marginTop: "var(--hop-space-stack-sm)", marginBottom: "13px" }); + }); + + it("should support DOM props", () => { + render( + + + + Disclosure Header + + + Disclosure Panel + + + + ); + + const element = screen.getByTestId("accordion"); + expect(element).toHaveAttribute("data-foo", "bar"); + }); + + it("should support slots", () => { + render( + + + + + Disclosure Header + + + Disclosure Panel + + + + + ); + + const element = screen.getByTestId("accordion"); + expect(element).toHaveClass("test"); + }); + + it("should support refs", () => { + const ref = createRef(); + render( + + + + Disclosure Header + + + Disclosure Panel + + + + ); + + expect(ref.current).not.toBeNull(); + expect(ref.current instanceof HTMLDivElement).toBeTruthy(); + }); + + it("should render a class for standalone variant by default", () => { + render( + + + + Disclosure Header + + + Disclosure Panel + + + + ); + + const element = screen.getByTestId("accordion"); + expect(element).toHaveClass("hop-Accordion--standalone"); + }); + + it("should render a class for inline variant", () => { + render( + + + + Disclosure Header + + + Disclosure Panel + + + + ); + + const element = screen.getByTestId("accordion"); + expect(element).toHaveClass("hop-Accordion--inline"); + }); +}); diff --git a/packages/components/src/Disclosure/src/Disclosure.module.css b/packages/components/src/Disclosure/src/Disclosure.module.css index 8096f7727..f97954f57 100644 --- a/packages/components/src/Disclosure/src/Disclosure.module.css +++ b/packages/components/src/Disclosure/src/Disclosure.module.css @@ -18,9 +18,9 @@ /* Inline */ --hop-Disclosure-inline-border-radius: 0; --hop-Disclosure-inline-box-shadow: none; - --hop-Disclosure-inline-panel-border-size: 0; - --hop-Disclosure-inline-panel-border-size-expanded: 0; - --hop-Disclosure-inline-panel-border-color: transparent; + --hop-Disclosure-inline-panel-border-size: 0 0 var(--hop-space-10) 0; + --hop-Disclosure-inline-panel-border-size-expanded: 0 0 var(--hop-space-10) 0; + --hop-Disclosure-inline-panel-border-color: var(--hop-neutral-border-weak); /* Disabled */ --hop-Disclosure-box-shadow-disabled: none; @@ -30,6 +30,7 @@ --panel-background-color: var(--hop-Disclosure-panel-background-color); --panel-padding: var(--hop-Disclosure-panel-padding); --panel-border-size: var(--hop-Disclosure-standalone-panel-border-size); + --panel-border-color: var(--hop-Disclosure-standalone-panel-border-color); box-sizing: border-box; border-radius: var(--border-radius); @@ -54,22 +55,18 @@ .hop-Disclosure:has(.hop-Disclosure__header)[data-expanded] { --panel-border-size: var(--hop-Disclosure-standalone-panel-border-size-expanded); - --panel-border-color: var(--hop-Disclosure-standalone-panel-border-color); --panel-padding: var(--hop-Disclosure-with-header-panel-padding); } -.hop-Disclosure--inline { - --panel-border-size: var(--hop-Disclosure-inline-panel-border-size); -} - .hop-Disclosure--inline:has(.hop-Disclosure__header) { --border-radius: var(--hop-Disclosure-inline-border-radius); --box-shadow: var(--hop-Disclosure-inline-box-shadow); + --panel-border-size: var(--hop-Disclosure-inline-panel-border-size); + --panel-border-color: var(--hop-Disclosure-inline-panel-border-color); } .hop-Disclosure--inline:has(.hop-Disclosure__header)[data-expanded] { --panel-border-size: var(--hop-Disclosure-inline-panel-border-size-expanded); - --panel-border-color: var(--hop-Disclosure-inline-panel-border-color); } .hop-Disclosure[data-disabled] { diff --git a/packages/components/src/Disclosure/src/Disclosure.tsx b/packages/components/src/Disclosure/src/Disclosure.tsx index 4034b5634..81ad55c05 100644 --- a/packages/components/src/Disclosure/src/Disclosure.tsx +++ b/packages/components/src/Disclosure/src/Disclosure.tsx @@ -1,6 +1,7 @@ import { useStyledSystem, type StyledComponentProps } from "@hopper-ui/styled-system"; +import clsx from "clsx"; import { forwardRef, type ForwardedRef } from "react"; -import { composeRenderProps, Disclosure as RACDisclosure, useContextProps, type DisclosureProps as RACDisclosureProps } from "react-aria-components"; +import { composeRenderProps, Disclosure as RACDisclosure, useContextProps, useSlottedContext, type DisclosureProps as RACDisclosureProps } from "react-aria-components"; import { ToggleArrowContext } from "../../ToggleArrow/index.ts"; import { composeClassnameRenderProps, cssModule, SlotProvider } from "../../utils/index.ts"; @@ -28,6 +29,9 @@ function Disclosure(props: DisclosureProps, ref: ForwardedRef) { ...otherProps } = ownProps; + const disclosureHeaderCtx = useSlottedContext(DisclosureHeaderContext); + const disclosurePanelCtx = useSlottedContext(DisclosurePanelContext); + const classNames = composeClassnameRenderProps( className, GlobalDisclosureCssSelector, @@ -64,10 +68,10 @@ function Disclosure(props: DisclosureProps, ref: ForwardedRef) { variant: variant }], [DisclosureHeaderContext, { - className: styles["hop-Disclosure__header"] + className: clsx(disclosureHeaderCtx?.className, styles["hop-Disclosure__header"]) }], [DisclosurePanelContext, { - className: styles["hop-Disclosure__panel"] + className: clsx(disclosurePanelCtx?.className, styles["hop-Disclosure__panel"]) }], [ToggleArrowContext, { isExpanded: disclosureRenderProps.isExpanded diff --git a/packages/components/src/Disclosure/src/DisclosureHeader.module.css b/packages/components/src/Disclosure/src/DisclosureHeader.module.css index 3263d2098..e5dfdf973 100644 --- a/packages/components/src/Disclosure/src/DisclosureHeader.module.css +++ b/packages/components/src/Disclosure/src/DisclosureHeader.module.css @@ -21,8 +21,8 @@ /* Inline */ --hop-DisclosureHeader-inline-outline-size: 0; --hop-DisclosureHeader-inline-outline-color: transparent; - --hop-DisclosureHeader-inline-border-size: 0 0 var(--hop-space-10) 0; - --hop-DisclosureHeader-inline-border-color: var(--hop-neutral-border-weak); + --hop-DisclosureHeader-inline-border-size: 0; + --hop-DisclosureHeader-inline-border-color: transparent; --hop-DisclosureHeader-inline-border-radius: 0; /* Expanded */ @@ -55,6 +55,7 @@ --hop-DisclosureHeader-standalone-outline-color-focused: var(--hop-primary-border-focus); --hop-DisclosureHeader-inline-outline-size-focused: var(--hop-space-20); --hop-DisclosureHeader-inline-outline-color-focused: var(--hop-primary-border-focus); + --hop-DisclosureHeader-outline-offset-focused: calc(-1 * var(--hop-space-20)); /* Disabled */ --hop-DisclosureHeader-background-color-disabled: var(--hop-neutral-surface-disabled); @@ -72,6 +73,7 @@ --description-color: var(--hop-DisclosureHeader-description-color); --outline-size: var(--hop-DisclosureHeader-standalone-outline-size); --outline-color: var(--hop-DisclosureHeader-standalone-outline-color); + --outline-offset: var(--hop-DisclosureHeader-outline-offset); --border-radius: var(--hop-DisclosureHeader-standalone-border-radius); --transition-info: var(--hop-easing-duration-2) var(--hop-easing-productive); --border-radius-transition-info: var(--hop-DisclosureHeader-border-radius-transition); @@ -107,7 +109,7 @@ border-width: var(--border-size); border-radius: var(--border-radius); outline: var(--outline-size) solid var(--outline-color); - outline-offset: var(--hop-DisclosureHeader-outline-offset); + outline-offset: var(--outline-offset); transition: var(--transition); } @@ -147,6 +149,7 @@ --icon-color: var(--hop-DisclosureHeader-icon-color-focused); --outline-size: var(--hop-DisclosureHeader-standalone-outline-size-focused); --outline-color: var(--hop-DisclosureHeader-standalone-outline-color-focused); + --outline-offset: var(--hop-DisclosureHeader-outline-offset-focused); } .hop-DisclosureHeader__button--inline { @@ -164,6 +167,7 @@ .hop-DisclosureHeader__button--inline[data-focus-visible] { --outline-size: var(--hop-DisclosureHeader-inline-outline-size-focused); --outline-color: var(--hop-DisclosureHeader-inline-outline-color-focused); + --outline-offset: var(--hop-DisclosureHeader-outline-offset-focused); } .hop-DisclosureHeader__button[data-disabled] { diff --git a/packages/components/src/Disclosure/tests/chromatic/Disclosure.stories.tsx b/packages/components/src/Disclosure/tests/chromatic/Disclosure.stories.tsx index cd12fe992..538b8204c 100644 --- a/packages/components/src/Disclosure/tests/chromatic/Disclosure.stories.tsx +++ b/packages/components/src/Disclosure/tests/chromatic/Disclosure.stories.tsx @@ -64,8 +64,15 @@ export const Default = { - We offer free standard shipping on all orders over $50. Orders are typically processed within 1-2 business days, and delivery times vary based on your location. Expedited shipping options are available for an additional fee. - Returns are easy and hassle-free. You have 30 days from the date of delivery to return items for a full refund. Items must be in their original condition and packaging. For further assistance, please contact our support team. + We offer free standard shipping on all orders over $50. Orders are typically processed within 1-2 business days, and delivery times vary based on your location. Expedited shipping options are available for an additional fee. + Returns are easy and hassle-free. You have 30 days from the date of delivery to return items for a full refund. Items must be in their original condition and packaging. For further assistance, please contact our support team. + + +

Custom Header

+ + + + Disclosure Panel

Style

@@ -77,6 +84,33 @@ export const Default = { Disclosure Panel +

Zoom

+ + + + + Shipping, Delivery Times, and Easy Returns Policy Overview + Explore our comprehensive shipping options, estimated delivery times for various regions, and our simple, customer-friendly returns process to make sure you feel comfortable with every purchase. + + + + We offer free standard shipping on all orders over $50. Orders are typically processed within 1-2 business days, and delivery times vary based on your location. Expedited shipping options are available for an additional fee. + Returns are easy and hassle-free. You have 30 days from the date of delivery to return items for a full refund. Items must be in their original condition and packaging. For further assistance, please contact our support team. + + + + + + + Shipping, Delivery Times, and Easy Returns Policy Overview + Explore our comprehensive shipping options, estimated delivery times for various regions, and our simple, customer-friendly returns process to make sure you feel comfortable with every purchase. + + + + We offer free standard shipping on all orders over $50. Orders are typically processed within 1-2 business days, and delivery times vary based on your location. Expedited shipping options are available for an additional fee. + Returns are easy and hassle-free. You have 30 days from the date of delivery to return items for a full refund. Items must be in their original condition and packaging. For further assistance, please contact our support team. + + ), args: { @@ -92,17 +126,6 @@ export const InlineVariant = { } } satisfies Story; -export const CustomHeader = { - render: args => ( - - - - Disclosure Panel - - - ) -} satisfies Story; - const StateTemplate = (args: Partial) => ( @@ -172,40 +195,4 @@ export const InlineStates = { variant: "inline", defaultExpanded: true } -} satisfies Story; - -export const Zoom = { - render: args => ( - - - - - - Shipping, Delivery Times, and Easy Returns Policy Overview - Explore our comprehensive shipping options, estimated delivery times for various regions, and our simple, customer-friendly returns process to make sure you feel comfortable with every purchase. - - - - We offer free standard shipping on all orders over $50. Orders are typically processed within 1-2 business days, and delivery times vary based on your location. Expedited shipping options are available for an additional fee. - Returns are easy and hassle-free. You have 30 days from the date of delivery to return items for a full refund. Items must be in their original condition and packaging. For further assistance, please contact our support team. - - - - - - - Shipping, Delivery Times, and Easy Returns Policy Overview - Explore our comprehensive shipping options, estimated delivery times for various regions, and our simple, customer-friendly returns process to make sure you feel comfortable with every purchase. - - - - We offer free standard shipping on all orders over $50. Orders are typically processed within 1-2 business days, and delivery times vary based on your location. Expedited shipping options are available for an additional fee. - Returns are easy and hassle-free. You have 30 days from the date of delivery to return items for a full refund. Items must be in their original condition and packaging. For further assistance, please contact our support team. - - - - ), - args: { - defaultExpanded: true - } } satisfies Story; \ No newline at end of file diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 204f3cb68..59f6c65d7 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -1,3 +1,4 @@ +export * from "./Accordion/index.ts"; export * from "./Avatar/index.ts"; export * from "./Badge/index.ts"; export * from "./buttons/index.ts";