diff --git a/.changeset/big-spiders-develop.md b/.changeset/big-spiders-develop.md
new file mode 100644
index 000000000..ac01c6a3a
--- /dev/null
+++ b/.changeset/big-spiders-develop.md
@@ -0,0 +1,5 @@
+---
+"@hopper-ui/components": patch
+---
+
+Created the Disclosure component with its sub components: DisclosureHeader and DisclosurePanel.
diff --git a/apps/docs/components/collapsible/Collapsible.tsx b/apps/docs/components/collapsible/Collapsible.tsx
index fb477dd00..83a79fb92 100644
--- a/apps/docs/components/collapsible/Collapsible.tsx
+++ b/apps/docs/components/collapsible/Collapsible.tsx
@@ -3,7 +3,7 @@
import { CollapseIcon, Icon } from "@/components/icon";
import clsx from "clsx";
import type { ReactNode } from "react";
-import { Button, composeRenderProps, UNSTABLE_Disclosure as Disclosure, UNSTABLE_DisclosurePanel as DisclosurePanel, type DisclosureProps } from "react-aria-components";
+import { Button, composeRenderProps, Disclosure, DisclosurePanel, type DisclosureProps } from "react-aria-components";
import "./collapsible.css";
diff --git a/apps/docs/content/components/navigation/Disclosure.mdx b/apps/docs/content/components/navigation/Disclosure.mdx
new file mode 100644
index 000000000..a7f7bf806
--- /dev/null
+++ b/apps/docs/content/components/navigation/Disclosure.mdx
@@ -0,0 +1,119 @@
+---
+title: Disclosure
+description: The disclosure component is used to put long sections of information under a block that users can expand or collapse.
+category: "navigation"
+links:
+ source: https://github.com/gsoft-inc/wl-hopper/blob/main/packages/components/src/Disclosure/src/Disclosure.tsx
+ aria: https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
+---
+
+
+
+
+ ## 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
+
+A `Disclosure` uses the following components.
+
+
+
+## Usage
+
+### Disabled
+
+A disclosure can be disabled.
+
+
+
+### Variants
+
+A disclosure has multiple variants.
+
+
+
+**Standalone** - Used when the disclosure is not inside a container.
+
+**Inline** - Used when placing a disclosure inside a container.
+
+### Icon
+
+A disclosure can contain an icon.
+
+
+
+### Description
+
+A disclosure can contain a description.
+
+
+
+### Controlled
+
+A disclosure can handle its expanded state in controlled mode.
+
+
+
+### Custom Header
+
+A disclosure can have a custom header. To accomplish this, do not use `DisclosureHeader` and use the [Button](./Button) component with `slot="trigger"` instead.
+Using React Aria's [Button](https://react-spectrum.adobe.com/react-aria/Button.html) component will also work.
+
+
+
+
+ ## 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
+
+### Disclosure
+
+
+### Disclosure Header
+
+
+### Disclosure Panel
+
+
+## Progressive Enhancement
+
+Our Disclosure component uses experimental CSS features like:
+
+- **interpolate-size: allow-keywords**: Enables fluid transitions between dynamic sizes.
+
+- **transition-behavior: allow-discrete**: Allows transitions for properties like content-visibility.
+
+These features enhance animations where supported. In browsers without support, the component remains fully usable without animations, maintaining progressive enhancement.
+
+## Migration Notes
+
+
diff --git a/apps/docs/examples/Preview.ts b/apps/docs/examples/Preview.ts
index 3963febcd..754b7a66a 100644
--- a/apps/docs/examples/Preview.ts
+++ b/apps/docs/examples/Preview.ts
@@ -776,6 +776,27 @@ export const Previews: Record = {
"layout/docs/stack/alignY": {
component: lazy(() => import("@/../../packages/components/src/layout/docs/stack/alignY.tsx"))
},
+ "Disclosure/docs/preview": {
+ component: lazy(() => import("@/../../packages/components/src/Disclosure/docs/preview.tsx"))
+ },
+ "Disclosure/docs/disabled": {
+ component: lazy(() => import("@/../../packages/components/src/Disclosure/docs/disabled.tsx"))
+ },
+ "Disclosure/docs/variants": {
+ component: lazy(() => import("@/../../packages/components/src/Disclosure/docs/variants.tsx"))
+ },
+ "Disclosure/docs/icon": {
+ component: lazy(() => import("@/../../packages/components/src/Disclosure/docs/icon.tsx"))
+ },
+ "Disclosure/docs/description": {
+ component: lazy(() => import("@/../../packages/components/src/Disclosure/docs/description.tsx"))
+ },
+ "Disclosure/docs/controlled": {
+ component: lazy(() => import("@/../../packages/components/src/Disclosure/docs/controlled.tsx"))
+ },
+ "Disclosure/docs/customHeader": {
+ component: lazy(() => import("@/../../packages/components/src/Disclosure/docs/customHeader.tsx"))
+ },
"Link/docs/preview": {
component: lazy(() => import("@/../../packages/components/src/Link/docs/preview.tsx"))
},
diff --git a/apps/docs/examples/overview/Disclosure.svg b/apps/docs/examples/overview/Disclosure.svg
new file mode 100644
index 000000000..a4d0727c5
--- /dev/null
+++ b/apps/docs/examples/overview/Disclosure.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/docs/examples/overview/index.ts b/apps/docs/examples/overview/index.ts
index aa00e534b..5d1032f3f 100644
--- a/apps/docs/examples/overview/index.ts
+++ b/apps/docs/examples/overview/index.ts
@@ -9,6 +9,7 @@ import Checkbox from "./Checkbox.svg";
import CheckboxGroup from "./CheckboxGroup.svg";
import ComboBox from "./ComboBox.svg";
import Content from "./Content.svg";
+import Disclosure from "./Disclosure.svg";
import Divider from "./Divider.svg";
import ErrorMessage from "./ErrorMessage.svg";
import Flex from "./Flex.svg";
@@ -57,6 +58,7 @@ export const OverviewComponents: OverviewComponentsType = {
CheckboxGroup,
ComboBox,
Content,
+ Disclosure,
Divider,
ErrorMessage,
Flex,
diff --git a/apps/docs/package.json b/apps/docs/package.json
index 24c0e062c..b85682fba 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -22,8 +22,8 @@
"copy:images": "tsx scripts/copyImages.ts"
},
"peerDependencies": {
- "react-aria": "^3.35",
- "react-aria-components": "^1.4"
+ "react-aria": "^3.36",
+ "react-aria-components": "^1.5"
},
"dependencies": {
"@tanstack/react-table": "^8.20.5",
@@ -33,8 +33,8 @@
"next-contentlayer": "0.3.4",
"next-mdx-remote": "^5.0.0",
"react": "18.3.1",
- "react-aria": "3.35.1",
- "react-aria-components": "1.4.1",
+ "react-aria": "3.36.0",
+ "react-aria-components": "1.5.0",
"react-dom": "18.3.1",
"react-toggle": "4.1.3",
"rehype-parse": "^9.0.1",
diff --git a/package.json b/package.json
index 07dead61b..5c64914c3 100644
--- a/package.json
+++ b/package.json
@@ -46,23 +46,23 @@
},
"devDependencies": {
"@changesets/cli": "2.27.9",
- "@chromatic-com/storybook": "2.0.2",
+ "@chromatic-com/storybook": "^3.2.2",
"@hopper-ui/tokens": "workspace:*",
"@internationalized/string-compiler": "3.2.5",
"@netlify/plugin-nextjs": "5.8.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
- "@storybook/addon-a11y": "^8.3.5",
- "@storybook/addon-essentials": "^8.3.5",
- "@storybook/addon-interactions": "^8.3.5",
- "@storybook/addon-links": "^8.3.5",
- "@storybook/addon-mdx-gfm": "^8.3.5",
+ "@storybook/addon-a11y": "^8.4.5",
+ "@storybook/addon-essentials": "^8.4.5",
+ "@storybook/addon-interactions": "^8.4.5",
+ "@storybook/addon-links": "^8.4.5",
+ "@storybook/addon-mdx-gfm": "^8.4.5",
"@storybook/addon-webpack5-compiler-swc": "^1.0.5",
- "@storybook/blocks": "^8.3.5",
- "@storybook/react": "^8.3.5",
- "@storybook/react-webpack5": "^8.3.5",
- "@storybook/test": "^8.3.5",
+ "@storybook/blocks": "^8.4.5",
+ "@storybook/react": "^8.4.5",
+ "@storybook/react-webpack5": "^8.4.5",
+ "@storybook/test": "^8.4.5",
"@storybook/test-runner": "0.19.1",
- "@storybook/types": "^8.3.5",
+ "@storybook/types": "^8.4.5",
"@types/eslint": "8.56.12",
"@types/jest": "29.5.13",
"@types/node": "22.7.5",
@@ -73,7 +73,7 @@
"cross-env": "7.0.3",
"eslint": "8.57.1",
"eslint-plugin-hopper-monorepo": "workspace:*",
- "eslint-plugin-storybook": "^0.9.0",
+ "eslint-plugin-storybook": "^0.11.1",
"jest": "29.7.0",
"plop": "4.0.1",
"prettier": "3.3.3",
@@ -82,7 +82,7 @@
"react-dom": "18.3.1",
"react-refresh": "0.14.2",
"react-router-dom": "6.27.0",
- "storybook": "^8.3.5",
+ "storybook": "^8.4.5",
"stylelint": "16.10.0",
"stylelint-config-clean-order": "6.1.0",
"stylelint-use-logical": "2.1.2",
diff --git a/packages/components/package.json b/packages/components/package.json
index c4da9bf09..e380ca623 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -42,16 +42,16 @@
"peerDependencies": {
"@hopper-ui/styled-system": "^2.4",
"react": "^18",
- "react-aria": "^3.35",
- "react-aria-components": "^1.4",
+ "react-aria": "^3.36",
+ "react-aria-components": "^1.5",
"react-dom": "^18"
},
"dependencies": {
"@hopper-ui/icons": "workspace:*",
- "@react-aria/utils": "^3.25.3",
- "@react-stately/data": "^3.11.7",
- "@react-stately/utils": "^3.10.4",
- "@react-types/shared": "^3.25.0",
+ "@react-aria/utils": "^3.26.0",
+ "@react-stately/data": "^3.12.0",
+ "@react-stately/utils": "^3.10.5",
+ "@react-types/shared": "^3.26.0",
"clsx": "^2.1.1"
},
"devDependencies": {
@@ -79,8 +79,8 @@
"jest-fail-on-console": "3.3.1",
"jest-fetch-mock": "3.0.3",
"react": "18.3.1",
- "react-aria": "3.35.1",
- "react-aria-components": "1.4.1",
+ "react-aria": "3.36.0",
+ "react-aria-components": "1.5.0",
"react-dom": "18.3.1",
"react-test-renderer": "18.3.1",
"ts-jest": "29.2.5",
diff --git a/packages/components/src/ComboBox/src/ComboBox.module.css b/packages/components/src/ComboBox/src/ComboBox.module.css
index 86cbd610b..61e2367b6 100644
--- a/packages/components/src/ComboBox/src/ComboBox.module.css
+++ b/packages/components/src/ComboBox/src/ComboBox.module.css
@@ -32,9 +32,6 @@
/* ComboBox Button */
--hop-ComboBox-button-padding-inline: var(--hop-space-inset-md);
- /* Button Icon */
- --hop-ComboBox-button-icon-color: var(--hop-neutral-icon-weak);
-
/* Prefix */
--hop-ComboBox-prefix-color: var(--hop-neutral-text-weak);
@@ -42,13 +39,11 @@
--hop-ComboBox-trigger-background-color-hovered: var(--hop-neutral-surface-hover);
--hop-ComboBox-trigger-border-color-hovered: var(--hop-neutral-border-hover);
--hop-ComboBox-trigger-color-hovered: var(--hop-neutral-text-hover);
- --hop-ComboBox-button-icon-color-hovered: var(--hop-neutral-icon-hover);
/* Focused */
--hop-ComboBox-trigger-background-color-focused: var(--hop-neutral-surface-hover);
--hop-ComboBox-trigger-border-color-focused: var(--hop-primary-border-focus);
--hop-ComboBox-trigger-color-focused: var(--hop-neutral-text-hover);
- --hop-ComboBox-button-icon-color-focused: var(--hop-neutral-icon-hover);
--hop-ComboBox-trigger-outline-color-focused: var(--hop-primary-border-focus);
/**
@@ -59,7 +54,6 @@
--hop-ComboBox-button-background-color-selected: var(--hop-neutral-surface);
--hop-ComboBox-button-border-color-selected: var(--hop-neutral-border-selected);
--hop-ComboBox-button-color-selected: var(--hop-neutral-text);
- --hop-ComboBox-button-icon-color-selected: var(--hop-neutral-icon);
/* Invalid */
--hop-ComboBox-trigger-border-color-invalid: var(--hop-danger-border-strong);
@@ -68,7 +62,6 @@
--hop-ComboBox-trigger-background-color-disabled: var(--hop-neutral-surface-disabled);
--hop-ComboBox-trigger-border-color-disabled: var(--hop-neutral-border-disabled);
--hop-ComboBox-trigger-color-disabled: var(--hop-neutral-text-disabled);
- --hop-ComboBox-button-icon-color-disabled: var(--hop-neutral-icon-disabled);
--hop-ComboBox-trigger-cursor-disabled: not-allowed;
/* Internal Variables */
@@ -77,7 +70,6 @@
--trigger-border-color: var(--hop-ComboBox-trigger-border-color);
--trigger-cursor: var(--hop-ComboBox-trigger-cursor);
--trigger-color: var(--hop-ComboBox-trigger-color);
- --button-icon-color: var(--hop-ComboBox-button-icon-color);
--input-font-family: var(--hop-ComboBox-input-md-font-family);
--input-font-size: var(--hop-ComboBox-input-md-font-size);
--input-font-weight: var(--hop-ComboBox-input-md-font-weight);
@@ -108,6 +100,7 @@
column-gap: var(--hop-ComboBox-trigger-column-gap);
order: 2;
+ box-sizing: border-box;
block-size: var(--trigger-block-size);
padding-inline-start: var(--hop-ComboBox-trigger-padding-inline-start);
@@ -134,14 +127,12 @@
--trigger-background-color: var(--hop-ComboBox-trigger-background-color-hovered);
--trigger-border-color: var(--hop-ComboBox-trigger-border-color-hovered);
--trigger-color: var(--hop-ComboBox-trigger-color-hovered);
- --button-icon-color: var(--hop-ComboBox-button-icon-color-hovered);
}
.hop-ComboBox__trigger[data-selected] {
--trigger-background-color: var(--hop-ComboBox-button-background-color-selected);
--trigger-border-color: var(--hop-ComboBox-button-border-color-selected);
--trigger-color: var(--hop-ComboBox-button-color-selected);
- --button-icon-color: var(--hop-ComboBox-button-icon-color-selected);
}
.hop-ComboBox__trigger[data-invalid] {
@@ -152,7 +143,6 @@
--trigger-background-color: var(--hop-ComboBox-trigger-background-color-focused);
--trigger-border-color: var(--hop-ComboBox-trigger-border-color-focused);
--trigger-color: var(--hop-ComboBox-trigger-color-focused);
- --button-icon-color: var(--hop-ComboBox-button-icon-color-focused);
outline: var(--hop-ComboBox-trigger-border-size) solid var(--hop-ComboBox-trigger-outline-color-focused);
outline-offset: var(--hop-ComboBox-trigger-outline-offset);
@@ -162,7 +152,6 @@
--trigger-background-color: var(--hop-ComboBox-trigger-background-color-disabled);
--trigger-border-color: var(--hop-ComboBox-trigger-border-color-disabled);
--trigger-color: var(--hop-ComboBox-trigger-color-disabled);
- --button-icon-color: var(--hop-ComboBox-button-icon-color-disabled);
--trigger-cursor: var(--hop-ComboBox-trigger-cursor-disabled);
}
@@ -196,6 +185,8 @@
padding-inline: var(--hop-ComboBox-button-padding-inline);
+ color: inherit;
+
background-color: var(--trigger-background-color);
border: none;
outline: none;
@@ -208,7 +199,6 @@
.hop-ComboBox__button-icon {
margin-inline-start: auto;
- color: var(--button-icon-color);
}
.hop-ComboBox__helper-message,
diff --git a/packages/components/src/ComboBox/src/ComboBox.tsx b/packages/components/src/ComboBox/src/ComboBox.tsx
index 4b6b593e9..7de157898 100644
--- a/packages/components/src/ComboBox/src/ComboBox.tsx
+++ b/packages/components/src/ComboBox/src/ComboBox.tsx
@@ -1,4 +1,4 @@
-import { AngleDownIcon, AngleUpIcon, IconContext } from "@hopper-ui/icons";
+import { IconContext } from "@hopper-ui/icons";
import { useResponsiveValue, useStyledSystem, type ResponsiveProp, type StyledComponentProps } from "@hopper-ui/styled-system";
import { mergeRefs, useObjectRef, useResizeObserver } from "@react-aria/utils";
import { forwardRef, useCallback, useRef, useState, type Context, type ForwardedRef, type MouseEventHandler, type MutableRefObject, type NamedExoticComponent, type ReactNode } from "react";
@@ -20,10 +20,12 @@ import {
import { BadgeContext } from "../../Badge/index.ts";
import { ErrorMessage } from "../../ErrorMessage/index.ts";
+import { useFormProps } from "../../Form/index.ts";
import { HelperMessage } from "../../HelperMessage/index.ts";
import { Footer } from "../../layout/index.ts";
import { ListBox, ListBoxItem, type ListBoxProps, type SelectionIndicator } from "../../ListBox/index.ts";
import { Popover, type PopoverProps } from "../../overlays/index.ts";
+import { ToggleArrow } from "../../ToggleArrow/index.ts";
import { Label, TextContext } from "../../typography/index.ts";
import { ClearContainerSlots, ClearProviders, composeClassnameRenderProps, cssModule, ensureTextWrapper, SlotProvider, type FieldProps, type MenuAlignment, type MenuDirection } from "../../utils/index.ts";
@@ -114,6 +116,7 @@ function ComboBox(props: ComboBoxProps, ref: ForwardedRef(props: ComboBoxProps, ref: ForwardedRef
{comboBoxRenderProps => {
const { isOpen } = comboBoxRenderProps;
- const ButtonIcon = isOpen ? AngleUpIcon : AngleDownIcon;
return (
<>
@@ -350,7 +352,10 @@ function ComboBox(props: ComboBoxProps, ref: ForwardedRef
{description && (
diff --git a/packages/components/src/Disclosure/docs/controlled.tsx b/packages/components/src/Disclosure/docs/controlled.tsx
new file mode 100644
index 000000000..a096dd58d
--- /dev/null
+++ b/packages/components/src/Disclosure/docs/controlled.tsx
@@ -0,0 +1,23 @@
+import { Disclosure, DisclosureHeader, DisclosurePanel, Div } from "@hopper-ui/components";
+import { useState } from "react";
+
+export default function Example() {
+ const [isExpanded, setIsExpanded] = useState(true);
+
+ return (
+
+
+
+ This disclosure is {isExpanded ? "expanded" : "collapsed"}
+
+
+ Tackle the challenges of hybrid, remote and distributed work, no matter what.
+ The Workleap platform builds solutions tailored to your existing HR and productivity tools to answer these challenges.
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/packages/components/src/Disclosure/docs/customHeader.tsx b/packages/components/src/Disclosure/docs/customHeader.tsx
new file mode 100644
index 000000000..bb6518db7
--- /dev/null
+++ b/packages/components/src/Disclosure/docs/customHeader.tsx
@@ -0,0 +1,22 @@
+import { Button, Disclosure, DisclosurePanel, Div, Text } from "@hopper-ui/components";
+
+import { ToggleArrow } from "../../ToggleArrow/index.ts";
+
+export default function Example() {
+ return (
+
+
+
+
+ Tackle the challenges of hybrid, remote and distributed work, no matter what.
+ The Workleap platform builds solutions tailored to your existing HR and productivity tools to answer these challenges.
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/packages/components/src/Disclosure/docs/description.tsx b/packages/components/src/Disclosure/docs/description.tsx
new file mode 100644
index 000000000..4ae399822
--- /dev/null
+++ b/packages/components/src/Disclosure/docs/description.tsx
@@ -0,0 +1,20 @@
+import { Disclosure, DisclosureHeader, DisclosurePanel, Div, Inline, Text } from "@hopper-ui/components";
+
+export default function Example() {
+ return (
+
+
+
+
+ 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.
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/packages/components/src/Disclosure/docs/disabled.tsx b/packages/components/src/Disclosure/docs/disabled.tsx
new file mode 100644
index 000000000..0e2827907
--- /dev/null
+++ b/packages/components/src/Disclosure/docs/disabled.tsx
@@ -0,0 +1,22 @@
+import { Disclosure, DisclosureHeader, DisclosurePanel, Div, Inline, Text } from "@hopper-ui/components";
+import { SparklesIcon } from "@hopper-ui/icons";
+
+export default function Example() {
+ return (
+
+
+
+
+
+ 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.
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/packages/components/src/Disclosure/docs/icon.tsx b/packages/components/src/Disclosure/docs/icon.tsx
new file mode 100644
index 000000000..5c50a1770
--- /dev/null
+++ b/packages/components/src/Disclosure/docs/icon.tsx
@@ -0,0 +1,19 @@
+import { Disclosure, DisclosureHeader, DisclosurePanel, Div, Text } from "@hopper-ui/components";
+import { SparklesIcon } from "@hopper-ui/icons";
+
+export default function Example() {
+ return (
+
+
+
+
+ Help your people work better
+
+
+ Tackle the challenges of hybrid, remote and distributed work, no matter what.
+ The Workleap platform builds solutions tailored to your existing HR and productivity tools to answer these challenges.
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/packages/components/src/Disclosure/docs/migration-notes.md b/packages/components/src/Disclosure/docs/migration-notes.md
new file mode 100644
index 000000000..877a1b16a
--- /dev/null
+++ b/packages/components/src/Disclosure/docs/migration-notes.md
@@ -0,0 +1,7 @@
+Coming from Orbiter, you should be aware of the following changes:
+
+- Disclosure now has two children components called `DisclosureHeader` and `DisclosurePanel`.
+- `DisclosurePanel` is optional and most likely wouldn't be used for Orbiter. A custom header can be used by using a Button component inside Disclosure.
+- `defaultOpen` is renamed to `defaultExpanded`.
+- `open` is renamed to `isExpanded`.
+- `onOpenChange` is renamed to `onExpandedChange`.
\ No newline at end of file
diff --git a/packages/components/src/Disclosure/docs/preview.tsx b/packages/components/src/Disclosure/docs/preview.tsx
new file mode 100644
index 000000000..5711f791b
--- /dev/null
+++ b/packages/components/src/Disclosure/docs/preview.tsx
@@ -0,0 +1,17 @@
+import { Disclosure, DisclosureHeader, DisclosurePanel, Div } from "@hopper-ui/components";
+
+export default function Example() {
+ return (
+
+
+
+ Help your people work better
+
+
+ Tackle the challenges of hybrid, remote and distributed work, no matter what.
+ The Workleap platform builds solutions tailored to your existing HR and productivity tools to answer these challenges.
+
+
+
+
+
+
+
+ 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.
+
+
+
Style
+
+
+ Disclosure Header
+
+
+ Disclosure Panel
+
+
+
+ ),
+ args: {
+ defaultExpanded: true
+ }
+} satisfies Story;
+
+export const InlineVariant = {
+ ...Default,
+ args: {
+ defaultExpanded: true,
+ variant: "inline"
+ }
+} satisfies Story;
+
+export const CustomHeader = {
+ render: args => (
+
+
+
+ Disclosure Panel
+
+
+ )
+} satisfies Story;
+
+const StateTemplate = (args: Partial) => (
+
+
+
+
+ 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.
+
+
+);
+
+export const DefaultStates = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const triggers = canvas.getAllByRole("button");
+
+ triggers.forEach(trigger => {
+ if (trigger.getAttribute("disabled") !== "") {
+ const disclosureElem = trigger.closest(".hop-Disclosure");
+
+ if (disclosureElem?.getAttribute("data-chromatic-force-focus")) {
+ trigger?.setAttribute("data-focus-visible", "true");
+ disclosureElem?.removeAttribute("data-chromatic-force-focus");
+ }
+
+ if (disclosureElem?.getAttribute("data-chromatic-force-press")) {
+ trigger?.setAttribute("data-pressed", "true");
+ disclosureElem?.removeAttribute("data-chromatic-force-press");
+ }
+
+ if (disclosureElem?.getAttribute("data-chromatic-force-hover")) {
+ trigger.setAttribute("data-hovered", "true");
+ disclosureElem?.removeAttribute("data-chromatic-force-hover");
+ }
+ }
+ });
+ },
+ render: args => (
+
+
Default
+
+
Disabled
+
+
Focus Visible
+
+
Hovered
+
+
Pressed
+
+
Focus Visible & Disabled
+
+
+ ),
+ args: {
+ defaultExpanded: true
+ }
+} satisfies Story;
+
+export const InlineStates = {
+ ...DefaultStates,
+ args: {
+ 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/Disclosure/tests/jest/Disclosure.ssr.test.tsx b/packages/components/src/Disclosure/tests/jest/Disclosure.ssr.test.tsx
new file mode 100644
index 000000000..3035b834f
--- /dev/null
+++ b/packages/components/src/Disclosure/tests/jest/Disclosure.ssr.test.tsx
@@ -0,0 +1,25 @@
+/**
+ * @jest-environment node
+ */
+import { renderToString } from "react-dom/server";
+
+import { DisclosureHeader, DisclosurePanel } from "../../index.ts";
+import { Disclosure } from "../../src/Disclosure.tsx";
+
+describe("Disclosure", () => {
+ it("should render on the server", () => {
+ const renderOnServer = () =>
+ renderToString(
+
+
+ Disclosure Header
+
+
+ Disclosure Panel
+
+
+ );
+
+ expect(renderOnServer).not.toThrow();
+ });
+});
diff --git a/packages/components/src/Disclosure/tests/jest/Disclosure.test.tsx b/packages/components/src/Disclosure/tests/jest/Disclosure.test.tsx
new file mode 100644
index 000000000..bfcbda1d1
--- /dev/null
+++ b/packages/components/src/Disclosure/tests/jest/Disclosure.test.tsx
@@ -0,0 +1,144 @@
+import { render, screen } from "@hopper-ui/test-utils";
+import { createRef } from "react";
+
+import { DisclosureHeader, DisclosurePanel } from "../../index.ts";
+import { Disclosure } from "../../src/Disclosure.tsx";
+import { DisclosureContext } from "../../src/DisclosureContext.ts";
+
+describe("Disclosure", () => {
+ it("should render with default class", () => {
+ render(
+
+
+ Disclosure Header
+
+
+ Disclosure Panel
+
+
+ );
+
+ const element = screen.getByTestId("disclosure");
+ expect(element).toHaveClass("hop-Disclosure");
+ });
+
+ it("should support custom class", () => {
+ render(
+
+
+ Disclosure Header
+
+
+ Disclosure Panel
+
+
+ );
+
+ const element = screen.getByTestId("disclosure");
+ expect(element).toHaveClass("hop-Disclosure");
+ expect(element).toHaveClass("test");
+ });
+
+ it("should support custom style", () => {
+ render(
+
+
+ Disclosure Header
+
+
+ Disclosure Panel
+
+
+ );
+
+ const element = screen.getByTestId("disclosure");
+ 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("disclosure");
+ expect(element).toHaveAttribute("data-foo", "bar");
+ });
+
+ it("should support slots", () => {
+ render(
+
+
+
+ Disclosure Header
+
+
+ Disclosure Panel
+
+
+
+ );
+
+ const element = screen.getByTestId("disclosure");
+ 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("disclosure");
+ const button = screen.getByRole("button");
+ expect(element).toHaveClass("hop-Disclosure--standalone");
+ expect(button).toHaveClass("hop-DisclosureHeader__button--standalone");
+ });
+
+ it("should render a class for inline variant", () => {
+ render(
+
+
+ Disclosure Header
+
+
+ Disclosure Panel
+
+
+ );
+
+ const element = screen.getByTestId("disclosure");
+ const button = screen.getByRole("button");
+ expect(element).toHaveClass("hop-Disclosure--inline");
+ expect(button).toHaveClass("hop-DisclosureHeader__button--inline");
+ });
+});
diff --git a/packages/components/src/Form/src/Form.tsx b/packages/components/src/Form/src/Form.tsx
index 52f95b026..72828f42a 100644
--- a/packages/components/src/Form/src/Form.tsx
+++ b/packages/components/src/Form/src/Form.tsx
@@ -5,22 +5,13 @@ import {
type StyledComponentProps
} from "@hopper-ui/styled-system";
import clsx from "clsx";
-import { forwardRef, type CSSProperties, type ForwardedRef } from "react";
+import { forwardRef, useContext, useMemo, type CSSProperties, type ForwardedRef } from "react";
import {
Form as RACForm,
- useContextProps,
type FormProps as RACFormProps
} from "react-aria-components";
-import { ButtonContext, LinkButtonContext } from "../../buttons/index.ts";
-import { CheckboxContext, CheckboxFieldContext, CheckboxGroupContext } from "../../checkbox/index.ts";
-import { ComboBoxContext } from "../../ComboBox/index.ts";
-import { NumberFieldContext, PasswordFieldContext, SearchFieldContext, TextAreaContext, TextFieldContext } from "../../inputs/index.ts";
-import { RadioGroupContext } from "../../radio/index.ts";
-import { SelectContext } from "../../Select/index.ts";
-import { TagGroupContext } from "../../tag/index.ts";
-import { LabelContext } from "../../typography/index.ts";
-import { cssModule, SlotProvider, type FieldSize, type NecessityIndicator } from "../../utils/index.ts";
+import { cssModule, type FieldSize, type NecessityIndicator } from "../../utils/index.ts";
import { FormContext } from "./FormContext.ts";
@@ -28,7 +19,7 @@ import styles from "./Form.module.css";
export const GlobalFormCssSelector = "hop-Form";
-export interface FormProps extends StyledComponentProps {
+export interface FormStyleProps {
/**
* Whether the form elements are disabled.
*/
@@ -51,8 +42,30 @@ export interface FormProps extends StyledComponentProps {
size?: ResponsiveProp;
}
+export interface FormProps extends StyledComponentProps, FormStyleProps {}
+
+export function useFormProps(props: T): T {
+ const ctx = useContext(FormContext);
+
+ return useMemo(() => {
+ let result: T = props;
+
+ if (ctx) {
+ result = { ...props };
+
+ // This is a subset of mergeProps. We just need to merge non-undefined values.
+ for (const key in ctx) {
+ if (result[key as keyof T] === undefined) {
+ result[key as keyof T] = ctx[key as keyof FormStyleProps] as T[keyof T];
+ }
+ }
+ }
+
+ return result;
+ }, [ctx, props]);
+}
+
function Form(props: FormProps, ref: ForwardedRef) {
- [props, ref] = useContextProps(props, ref, FormContext);
const { stylingProps, ...ownProps } = useStyledSystem(props);
const {
className,
@@ -83,92 +96,16 @@ function Form(props: FormProps, ref: ForwardedRef) {
};
return (
-
- {/* Put these in a separate SlotProvider due to a typing error */}
-
+
-
- {children}
-
-
-
+ {children}
+
+
);
}
diff --git a/packages/components/src/Form/src/FormContext.ts b/packages/components/src/Form/src/FormContext.ts
index 98f88e907..30ab7d2d6 100644
--- a/packages/components/src/Form/src/FormContext.ts
+++ b/packages/components/src/Form/src/FormContext.ts
@@ -1,8 +1,7 @@
import { createContext } from "react";
-import type { ContextValue } from "react-aria-components";
-import type { FormProps } from "./Form.tsx";
+import type { FormStyleProps } from "./Form.tsx";
-export const FormContext = createContext>({});
+export const FormContext = createContext(null);
FormContext.displayName = "FormContext";
diff --git a/packages/components/src/Form/tests/jest/Form.test.tsx b/packages/components/src/Form/tests/jest/Form.test.tsx
index 8d0e5abe8..421898563 100644
--- a/packages/components/src/Form/tests/jest/Form.test.tsx
+++ b/packages/components/src/Form/tests/jest/Form.test.tsx
@@ -50,7 +50,7 @@ describe("Form", () => {
expect(ref.current instanceof HTMLFormElement).toBeTruthy();
});
- it("should aupport disabled state", () => {
+ it("should support disabled state", () => {
render(