From bc97a70a36fec7addb6855bd7119dea04e2dfebd Mon Sep 17 00:00:00 2001 From: Raphael Lin Date: Mon, 8 Dec 2025 14:42:09 +0800 Subject: [PATCH 1/8] Add Accordion component and dependencies --- packages/material/src/Accordion/Accordion.tsx | 182 ++++++++++++++ .../src/Accordion/AccordionContext.tsx | 13 + .../material/src/Accordion/AccordionProps.ts | 64 +++++ .../src/Accordion/accordionClasses.ts | 31 +++ packages/material/src/Accordion/index.tsx | 4 + .../src/AccordionActions/AccordionActions.tsx | 70 ++++++ .../AccordionActions/AccordionActionsProps.ts | 35 +++ .../accordionActionsClasses.ts | 21 ++ .../material/src/AccordionActions/index.tsx | 4 + .../src/AccordionDetails/AccordionDetails.tsx | 49 ++++ .../AccordionDetails/AccordionDetailsProps.ts | 30 +++ .../accordionDetailsClasses.ts | 19 ++ .../material/src/AccordionDetails/index.tsx | 4 + .../src/AccordionSummary/AccordionSummary.tsx | 137 +++++++++++ .../AccordionSummary/AccordionSummaryProps.ts | 37 +++ .../accordionSummaryClasses.ts | 42 ++++ .../material/src/AccordionSummary/index.tsx | 4 + packages/material/src/Collapse/Collapse.tsx | 227 ++++++++++++++++++ .../material/src/Collapse/CollapseProps.ts | 88 +++++++ .../material/src/Collapse/collapseClasses.ts | 36 +++ packages/material/src/Collapse/index.tsx | 4 + packages/material/src/index.tsx | 5 + .../AccordionPage/AccordionPage.tsx | 22 ++ .../AccordionPage/BasicAccordionExample.tsx | 59 +++++ .../ControlledAccordionExample.tsx | 96 ++++++++ 25 files changed, 1283 insertions(+) create mode 100644 packages/material/src/Accordion/Accordion.tsx create mode 100644 packages/material/src/Accordion/AccordionContext.tsx create mode 100644 packages/material/src/Accordion/AccordionProps.ts create mode 100644 packages/material/src/Accordion/accordionClasses.ts create mode 100644 packages/material/src/Accordion/index.tsx create mode 100644 packages/material/src/AccordionActions/AccordionActions.tsx create mode 100644 packages/material/src/AccordionActions/AccordionActionsProps.ts create mode 100644 packages/material/src/AccordionActions/accordionActionsClasses.ts create mode 100644 packages/material/src/AccordionActions/index.tsx create mode 100644 packages/material/src/AccordionDetails/AccordionDetails.tsx create mode 100644 packages/material/src/AccordionDetails/AccordionDetailsProps.ts create mode 100644 packages/material/src/AccordionDetails/accordionDetailsClasses.ts create mode 100644 packages/material/src/AccordionDetails/index.tsx create mode 100644 packages/material/src/AccordionSummary/AccordionSummary.tsx create mode 100644 packages/material/src/AccordionSummary/AccordionSummaryProps.ts create mode 100644 packages/material/src/AccordionSummary/accordionSummaryClasses.ts create mode 100644 packages/material/src/AccordionSummary/index.tsx create mode 100644 packages/material/src/Collapse/Collapse.tsx create mode 100644 packages/material/src/Collapse/CollapseProps.ts create mode 100644 packages/material/src/Collapse/collapseClasses.ts create mode 100644 packages/material/src/Collapse/index.tsx create mode 100644 packages/site/src/pages/components/AccordionPage/AccordionPage.tsx create mode 100644 packages/site/src/pages/components/AccordionPage/BasicAccordionExample.tsx create mode 100644 packages/site/src/pages/components/AccordionPage/ControlledAccordionExample.tsx diff --git a/packages/material/src/Accordion/Accordion.tsx b/packages/material/src/Accordion/Accordion.tsx new file mode 100644 index 000000000..e4887ff59 --- /dev/null +++ b/packages/material/src/Accordion/Accordion.tsx @@ -0,0 +1,182 @@ +import Collapse from "../Collapse"; +import Paper from "../Paper"; +import styled from "../styles/styled"; +import AccordionContext, { AccordionContextValue } from "./AccordionContext"; +import { AccordionTypeMap } from "./AccordionProps"; +import accordionClasses, { getAccordionUtilityClass } from "./accordionClasses"; +import createComponentFactory from "@suid/base/createComponentFactory"; +import clsx from "clsx"; +import { children, createSignal, JSXElement } from "solid-js"; + +const $ = createComponentFactory()({ + name: "MuiAccordion", + selfPropNames: [ + "children", + "classes", + "defaultExpanded", + "disabled", + "disableGutters", + "expanded", + "onChange", + "square", + ], + propDefaults: ({ set }) => + set({ + defaultExpanded: false, + disabled: false, + disableGutters: false, + square: false, + }), + utilityClass: getAccordionUtilityClass, + slotClasses: (o) => ({ + root: [ + "root", + !o.square && "rounded", + o.expanded && "expanded", + o.disabled && "disabled", + !o.disableGutters && "gutters", + ], + heading: ["heading"], + region: ["region"], + }), +}); + +const AccordionRoot = styled(Paper, { + name: "MuiAccordion", + slot: "Root", +})(({ theme }) => { + const transition = { + duration: theme.transitions.duration.shortest, + }; + + return { + position: "relative", + transition: theme.transitions.create(["margin"], transition), + overflowAnchor: "none", + "&::before": { + position: "absolute", + left: 0, + top: -1, + right: 0, + height: 1, + content: '""', + opacity: 1, + backgroundColor: theme.palette.divider, + transition: theme.transitions.create( + ["opacity", "background-color"], + transition + ), + }, + "&:first-of-type": { + "&::before": { + display: "none", + }, + }, + [`&.${accordionClasses.expanded}`]: { + "&::before": { + opacity: 0, + }, + "&:first-of-type": { + marginTop: 0, + }, + "&:last-of-type": { + marginBottom: 0, + }, + "& + &": { + "&::before": { + display: "none", + }, + }, + }, + [`&.${accordionClasses.disabled}`]: { + backgroundColor: theme.palette.action.disabledBackground, + }, + }; +}); + +const AccordionHeading = styled("h3", { + name: "MuiAccordion", + slot: "Heading", +})({ + all: "unset", +}); + +const AccordionRegion = styled("div", { + name: "MuiAccordion", + slot: "Region", +})({}); + +/** + * + * Demos: + * + * - [Accordion](https://mui.com/components/accordion/) + * + * API: + * + * - [Accordion API](https://mui.com/api/accordion/) + * - inherits [Paper API](https://mui.com/api/paper/) + */ +const Accordion = $.component(function Accordion({ + allProps, + props, + otherProps, + classes, +}) { + const [expandedState, setExpandedState] = createSignal(props.defaultExpanded); + const expanded = () => + props.expanded !== undefined ? props.expanded : expandedState(); + + const handleChange = (event: Event) => { + console.log("Accordion handleChange called", { + currentExpanded: expanded(), + }); + const newExpanded = !expanded(); + console.log("Accordion setting expanded to:", newExpanded); + setExpandedState(newExpanded); + props.onChange?.(event, newExpanded); + }; + + return ( + + + + {(() => { + const c = children(() => props.children); + const kids = c.toArray(); + return kids[0]; + })()} + + + + + {(() => { + const c = children(() => props.children); + const kids = c.toArray(); + return kids.slice(1); + })()} + + + + ); +}); + +export default Accordion; diff --git a/packages/material/src/Accordion/AccordionContext.tsx b/packages/material/src/Accordion/AccordionContext.tsx new file mode 100644 index 000000000..f268bfac5 --- /dev/null +++ b/packages/material/src/Accordion/AccordionContext.tsx @@ -0,0 +1,13 @@ +import { createContext } from "solid-js"; + +export interface AccordionContextValue { + expanded: boolean; + disabled: boolean; + toggle: (event: Event) => void; +} + +const AccordionContext = createContext( + undefined +); + +export default AccordionContext; diff --git a/packages/material/src/Accordion/AccordionProps.ts b/packages/material/src/Accordion/AccordionProps.ts new file mode 100644 index 000000000..a1f275d03 --- /dev/null +++ b/packages/material/src/Accordion/AccordionProps.ts @@ -0,0 +1,64 @@ +import { PaperProps } from "../Paper/PaperProps"; +import { AccordionClasses } from "./accordionClasses"; +import { OverrideProps, ElementType } from "@suid/types"; +import { JSXElement } from "solid-js"; + +export interface AccordionOwnProps { + /** + * The content of the component. + */ + children: JSXElement; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * If `true`, expands the accordion by default. + * @default false + */ + defaultExpanded?: boolean; + /** + * If `true`, the component is disabled. + * @default false + */ + disabled?: boolean; + /** + * If `true`, it removes the margin between two expanded accordion items and the increase of height. + * @default false + */ + disableGutters?: boolean; + /** + * If `true`, expands the accordion, otherwise collapse it. + * Setting this prop enables control over the accordion. + */ + expanded?: boolean; + /** + * Callback fired when the expand/collapse state is changed. + * + * @param {React.SyntheticEvent} event The event source of the callback. **Warning**: This is a generic event not a change event. + * @param {boolean} expanded The `expanded` state of the accordion. + */ + onChange?: (event: Event, expanded: boolean) => void; + /** + * If `true`, rounded corners are disabled. + * @default false + */ + square?: boolean; +} + +export interface AccordionTypeMap

{ + name: "MuiAccordion"; + defaultPropNames: + | "defaultExpanded" + | "disabled" + | "disableGutters" + | "square"; + selfProps: AccordionOwnProps; + props: P & AccordionOwnProps & Omit; + defaultComponent: D; +} + +export type AccordionProps< + D extends ElementType = AccordionTypeMap["defaultComponent"], + P = {}, +> = OverrideProps, D>; diff --git a/packages/material/src/Accordion/accordionClasses.ts b/packages/material/src/Accordion/accordionClasses.ts new file mode 100644 index 000000000..db7c2e291 --- /dev/null +++ b/packages/material/src/Accordion/accordionClasses.ts @@ -0,0 +1,31 @@ +import { generateUtilityClass, generateUtilityClasses } from "@suid/base"; + +export interface AccordionClasses { + /** Styles applied to the root element. */ + root: string; + /** State class applied to the root element if `rounded={true}`. */ + rounded: string; + /** State class applied to the root element if `expanded={true}`. */ + expanded: string; + /** State class applied to the root element if `disabled={true}`. */ + disabled: string; + /** State class applied to the root element unless `disableGutters={true}`. */ + gutters: string; + /** Styles applied to the region element, the container of the children. */ + region: string; + /** Styles applied to the heading element. */ + heading: string; +} + +export type AccordionClassKey = keyof AccordionClasses; + +export function getAccordionUtilityClass(slot: string): string { + return generateUtilityClass("MuiAccordion", slot); +} + +const accordionClasses: AccordionClasses = generateUtilityClasses( + "MuiAccordion", + ["root", "rounded", "expanded", "disabled", "gutters", "region", "heading"] +); + +export default accordionClasses; diff --git a/packages/material/src/Accordion/index.tsx b/packages/material/src/Accordion/index.tsx new file mode 100644 index 000000000..01e00ef3e --- /dev/null +++ b/packages/material/src/Accordion/index.tsx @@ -0,0 +1,4 @@ +export { default } from "./Accordion"; +export * from "./AccordionProps"; +export { default as accordionClasses } from "./accordionClasses"; +export * from "./accordionClasses"; diff --git a/packages/material/src/AccordionActions/AccordionActions.tsx b/packages/material/src/AccordionActions/AccordionActions.tsx new file mode 100644 index 000000000..419a0e936 --- /dev/null +++ b/packages/material/src/AccordionActions/AccordionActions.tsx @@ -0,0 +1,70 @@ +import styled from "../styles/styled"; +import { AccordionActionsTypeMap } from "./AccordionActionsProps"; +import { getAccordionActionsUtilityClass } from "./accordionActionsClasses"; +import createComponentFactory from "@suid/base/createComponentFactory"; +import clsx from "clsx"; + +type OwnerState = { + disableSpacing: boolean; +}; + +const $ = createComponentFactory()({ + name: "MuiAccordionActions", + selfPropNames: ["children", "classes", "disableSpacing"], + propDefaults: ({ set }) => + set({ + disableSpacing: false, + }), + utilityClass: getAccordionActionsUtilityClass, + slotClasses: (ownerState: OwnerState) => ({ + root: ["root", !ownerState.disableSpacing && "spacing"], + }), +}); + +const AccordionActionsRoot = styled("div", { + name: "MuiAccordionActions", + slot: "Root", +})(({ theme, ownerState }) => ({ + display: "flex", + alignItems: "center", + padding: theme.spacing(1), + justifyContent: "flex-end", + ...(!ownerState.disableSpacing && { + "& > :not(style) ~ :not(style)": { + marginLeft: theme.spacing(1), + }, + }), +})); + +/** + * + * Demos: + * + * - [Accordion](https://mui.com/components/accordion/) + * + * API: + * + * - [AccordionActions API](https://mui.com/api/accordion-actions/) + */ +const AccordionActions = $.component(function AccordionActions({ + allProps, + props, + otherProps, + classes, +}) { + const ownerState: OwnerState = { + disableSpacing: props.disableSpacing, + }; + + return ( + + {props.children} + + ); +}); + +export default AccordionActions; diff --git a/packages/material/src/AccordionActions/AccordionActionsProps.ts b/packages/material/src/AccordionActions/AccordionActionsProps.ts new file mode 100644 index 000000000..270eb5243 --- /dev/null +++ b/packages/material/src/AccordionActions/AccordionActionsProps.ts @@ -0,0 +1,35 @@ +import { AccordionActionsClasses } from "./accordionActionsClasses"; +import { OverrideProps, ElementType } from "@suid/types"; +import { JSXElement } from "solid-js"; + +export interface AccordionActionsOwnProps { + /** + * The content of the component. + */ + children?: JSXElement; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * If `true`, the actions do not have additional margin. + * @default false + */ + disableSpacing?: boolean; +} + +export interface AccordionActionsTypeMap< + P = {}, + D extends ElementType = "div", +> { + name: "MuiAccordionActions"; + defaultPropNames: "disableSpacing"; + selfProps: AccordionActionsOwnProps; + props: P & AccordionActionsOwnProps; + defaultComponent: D; +} + +export type AccordionActionsProps< + D extends ElementType = AccordionActionsTypeMap["defaultComponent"], + P = {}, +> = OverrideProps, D>; diff --git a/packages/material/src/AccordionActions/accordionActionsClasses.ts b/packages/material/src/AccordionActions/accordionActionsClasses.ts new file mode 100644 index 000000000..cd1f321b6 --- /dev/null +++ b/packages/material/src/AccordionActions/accordionActionsClasses.ts @@ -0,0 +1,21 @@ +import { generateUtilityClass, generateUtilityClasses } from "@suid/base"; + +export interface AccordionActionsClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the root element unless `disableSpacing={true}`. */ + spacing: string; +} + +export type AccordionActionsClassKey = keyof AccordionActionsClasses; + +export function getAccordionActionsUtilityClass(slot: string): string { + return generateUtilityClass("MuiAccordionActions", slot); +} + +const accordionActionsClasses: AccordionActionsClasses = generateUtilityClasses( + "MuiAccordionActions", + ["root", "spacing"] +); + +export default accordionActionsClasses; diff --git a/packages/material/src/AccordionActions/index.tsx b/packages/material/src/AccordionActions/index.tsx new file mode 100644 index 000000000..c109f9074 --- /dev/null +++ b/packages/material/src/AccordionActions/index.tsx @@ -0,0 +1,4 @@ +export { default } from "./AccordionActions"; +export * from "./AccordionActionsProps"; +export { default as accordionActionsClasses } from "./accordionActionsClasses"; +export * from "./accordionActionsClasses"; diff --git a/packages/material/src/AccordionDetails/AccordionDetails.tsx b/packages/material/src/AccordionDetails/AccordionDetails.tsx new file mode 100644 index 000000000..0d0404767 --- /dev/null +++ b/packages/material/src/AccordionDetails/AccordionDetails.tsx @@ -0,0 +1,49 @@ +import styled from "../styles/styled"; +import { AccordionDetailsTypeMap } from "./AccordionDetailsProps"; +import { getAccordionDetailsUtilityClass } from "./accordionDetailsClasses"; +import createComponentFactory from "@suid/base/createComponentFactory"; +import clsx from "clsx"; + +const $ = createComponentFactory()({ + name: "MuiAccordionDetails", + selfPropNames: ["children", "classes"], + utilityClass: getAccordionDetailsUtilityClass, + slotClasses: () => ({ + root: ["root"], + }), +}); + +const AccordionDetailsRoot = styled("div", { + name: "MuiAccordionDetails", + slot: "Root", +})(({ theme }) => ({ + padding: theme.spacing(1, 2, 2), +})); + +/** + * + * Demos: + * + * - [Accordion](https://mui.com/components/accordion/) + * + * API: + * + * - [AccordionDetails API](https://mui.com/api/accordion-details/) + */ +const AccordionDetails = $.component(function AccordionDetails({ + allProps, + props, + otherProps, + classes, +}) { + return ( + + {props.children} + + ); +}); + +export default AccordionDetails; diff --git a/packages/material/src/AccordionDetails/AccordionDetailsProps.ts b/packages/material/src/AccordionDetails/AccordionDetailsProps.ts new file mode 100644 index 000000000..504848633 --- /dev/null +++ b/packages/material/src/AccordionDetails/AccordionDetailsProps.ts @@ -0,0 +1,30 @@ +import { AccordionDetailsClasses } from "./accordionDetailsClasses"; +import { OverrideProps, ElementType } from "@suid/types"; +import { JSXElement } from "solid-js"; + +export interface AccordionDetailsOwnProps { + /** + * The content of the component. + */ + children?: JSXElement; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; +} + +export interface AccordionDetailsTypeMap< + P = {}, + D extends ElementType = "div", +> { + name: "MuiAccordionDetails"; + defaultPropNames: never; + selfProps: AccordionDetailsOwnProps; + props: P & AccordionDetailsOwnProps; + defaultComponent: D; +} + +export type AccordionDetailsProps< + D extends ElementType = AccordionDetailsTypeMap["defaultComponent"], + P = {}, +> = OverrideProps, D>; diff --git a/packages/material/src/AccordionDetails/accordionDetailsClasses.ts b/packages/material/src/AccordionDetails/accordionDetailsClasses.ts new file mode 100644 index 000000000..f472af8bd --- /dev/null +++ b/packages/material/src/AccordionDetails/accordionDetailsClasses.ts @@ -0,0 +1,19 @@ +import { generateUtilityClass, generateUtilityClasses } from "@suid/base"; + +export interface AccordionDetailsClasses { + /** Styles applied to the root element. */ + root: string; +} + +export type AccordionDetailsClassKey = keyof AccordionDetailsClasses; + +export function getAccordionDetailsUtilityClass(slot: string): string { + return generateUtilityClass("MuiAccordionDetails", slot); +} + +const accordionDetailsClasses: AccordionDetailsClasses = generateUtilityClasses( + "MuiAccordionDetails", + ["root"] +); + +export default accordionDetailsClasses; diff --git a/packages/material/src/AccordionDetails/index.tsx b/packages/material/src/AccordionDetails/index.tsx new file mode 100644 index 000000000..7c7cf4c82 --- /dev/null +++ b/packages/material/src/AccordionDetails/index.tsx @@ -0,0 +1,4 @@ +export { default } from "./AccordionDetails"; +export * from "./AccordionDetailsProps"; +export { default as accordionDetailsClasses } from "./accordionDetailsClasses"; +export * from "./accordionDetailsClasses"; diff --git a/packages/material/src/AccordionSummary/AccordionSummary.tsx b/packages/material/src/AccordionSummary/AccordionSummary.tsx new file mode 100644 index 000000000..d61963a5d --- /dev/null +++ b/packages/material/src/AccordionSummary/AccordionSummary.tsx @@ -0,0 +1,137 @@ +import AccordionContext from "../Accordion/AccordionContext"; +import ButtonBase from "../ButtonBase"; +import styled from "../styles/styled"; +import { AccordionSummaryTypeMap } from "./AccordionSummaryProps"; +import accordionSummaryClasses, { + getAccordionSummaryUtilityClass, +} from "./accordionSummaryClasses"; +import createComponentFactory from "@suid/base/createComponentFactory"; +import clsx from "clsx"; +import { useContext, Show } from "solid-js"; + +const $ = createComponentFactory()({ + name: "MuiAccordionSummary", + selfPropNames: ["children", "classes", "expandIcon"], + utilityClass: getAccordionSummaryUtilityClass, + autoCallUseClasses: false, +}); + +const AccordionSummaryRoot = styled(ButtonBase, { + name: "MuiAccordionSummary", + slot: "Root", +})(({ theme }) => ({ + display: "flex", + minHeight: 48, + padding: "0px 16px", + transition: theme.transitions.create(["min-height", "background-color"], { + duration: theme.transitions.duration.shortest, + }), + [`&.${accordionSummaryClasses.focusVisible}`]: { + backgroundColor: theme.palette.action.focus, + }, + [`&.${accordionSummaryClasses.disabled}`]: { + opacity: theme.palette.action.disabledOpacity, + }, + [`&:hover:not(.${accordionSummaryClasses.disabled})`]: { + cursor: "pointer", + }, + [`&.${accordionSummaryClasses.expanded}`]: { + minHeight: 64, + }, +})); + +const AccordionSummaryContent = styled("div", { + name: "MuiAccordionSummary", + slot: "Content", +})(({ theme }) => ({ + display: "flex", + flexGrow: 1, + margin: "12px 0", + [`&.${accordionSummaryClasses.expanded}`]: { + margin: "20px 0", + }, +})); + +const AccordionSummaryExpandIconWrapper = styled("div", { + name: "MuiAccordionSummary", + slot: "ExpandIconWrapper", +})(({ theme }) => ({ + display: "flex", + color: theme.palette.action.active, + transform: "rotate(0deg)", + transition: theme.transitions.create("transform", { + duration: theme.transitions.duration.shortest, + }), + [`&.${accordionSummaryClasses.expanded}`]: { + transform: "rotate(180deg)", + }, +})); + +/** + * + * Demos: + * + * - [Accordion](https://mui.com/components/accordion/) + * + * API: + * + * - [AccordionSummary API](https://mui.com/api/accordion-summary/) + * - inherits [ButtonBase API](https://mui.com/api/button-base/) + */ +const AccordionSummary = $.component(function AccordionSummary({ + allProps, + props, + otherProps, +}) { + const context = useContext(AccordionContext); + + return ( + { + console.log("AccordionSummary clicked", { + context, + hasToggle: !!context?.toggle, + }); + if (context?.toggle) { + console.log("Calling context.toggle"); + context.toggle(e); + } + if (typeof otherProps.onClick === "function") { + otherProps.onClick(e); + } + }} + > + + {props.children} + + + + {props.expandIcon} + + + + ); +}); + +export default AccordionSummary; diff --git a/packages/material/src/AccordionSummary/AccordionSummaryProps.ts b/packages/material/src/AccordionSummary/AccordionSummaryProps.ts new file mode 100644 index 000000000..1b945e722 --- /dev/null +++ b/packages/material/src/AccordionSummary/AccordionSummaryProps.ts @@ -0,0 +1,37 @@ +import { ButtonBaseTypeMap } from "../ButtonBase/ButtonBaseProps"; +import { AccordionSummaryClasses } from "./accordionSummaryClasses"; +import { OverrideProps, ElementType } from "@suid/types"; +import { JSXElement } from "solid-js"; + +export interface AccordionSummaryOwnProps { + /** + * The content of the component. + */ + children?: JSXElement; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * The icon to display as the expand indicator. + */ + expandIcon?: JSXElement; +} + +export interface AccordionSummaryTypeMap< + P = {}, + D extends ElementType = "div", +> { + name: "MuiAccordionSummary"; + defaultPropNames: never; + selfProps: AccordionSummaryOwnProps; + props: P & + AccordionSummaryOwnProps & + Omit; + defaultComponent: D; +} + +export type AccordionSummaryProps< + D extends ElementType = AccordionSummaryTypeMap["defaultComponent"], + P = {}, +> = OverrideProps, D>; diff --git a/packages/material/src/AccordionSummary/accordionSummaryClasses.ts b/packages/material/src/AccordionSummary/accordionSummaryClasses.ts new file mode 100644 index 000000000..1c0912813 --- /dev/null +++ b/packages/material/src/AccordionSummary/accordionSummaryClasses.ts @@ -0,0 +1,42 @@ +import { generateUtilityClass, generateUtilityClasses } from "@suid/base"; + +export interface AccordionSummaryClasses { + /** Styles applied to the root element. */ + root: string; + /** State class applied to the root element, children wrapper element and `IconButton` component if `expanded={true}`. */ + expanded: string; + /** State class applied to the ButtonBase root element if the button is keyboard focused. */ + focusVisible: string; + /** State class applied to the root element if `disabled={true}`. */ + disabled: string; + /** Styles applied to the root element unless `disableGutters={true}`. */ + gutters: string; + /** Styles applied to the children wrapper element unless `disableGutters={true}`. */ + contentGutters: string; + /** Styles applied to the children wrapper element. */ + content: string; + /** Styles applied to the `expandIcon`'s wrapper element. */ + expandIconWrapper: string; +} + +export type AccordionSummaryClassKey = keyof AccordionSummaryClasses; + +export function getAccordionSummaryUtilityClass(slot: string): string { + return generateUtilityClass("MuiAccordionSummary", slot); +} + +const accordionSummaryClasses: AccordionSummaryClasses = generateUtilityClasses( + "MuiAccordionSummary", + [ + "root", + "expanded", + "focusVisible", + "disabled", + "gutters", + "contentGutters", + "content", + "expandIconWrapper", + ] +); + +export default accordionSummaryClasses; diff --git a/packages/material/src/AccordionSummary/index.tsx b/packages/material/src/AccordionSummary/index.tsx new file mode 100644 index 000000000..734abc4a1 --- /dev/null +++ b/packages/material/src/AccordionSummary/index.tsx @@ -0,0 +1,4 @@ +export { default } from "./AccordionSummary"; +export * from "./AccordionSummaryProps"; +export { default as accordionSummaryClasses } from "./accordionSummaryClasses"; +export * from "./accordionSummaryClasses"; diff --git a/packages/material/src/Collapse/Collapse.tsx b/packages/material/src/Collapse/Collapse.tsx new file mode 100644 index 000000000..240507c61 --- /dev/null +++ b/packages/material/src/Collapse/Collapse.tsx @@ -0,0 +1,227 @@ +import styled from "../styles/styled"; +import useTheme from "../styles/useTheme"; +import { reflow, getTransitionProps } from "../transitions/utils"; +import { CollapseTypeMap } from "./CollapseProps"; +import collapseClasses, { getCollapseUtilityClass } from "./collapseClasses"; +import createComponentFactory from "@suid/base/createComponentFactory"; +import createElementRef from "@suid/system/createElementRef"; +import clsx from "clsx"; +import { children, createEffect, createSignal, on, onMount } from "solid-js"; + +const $ = createComponentFactory()({ + name: "MuiCollapse", + selfPropNames: [ + "addEndListener", + "children", + "classes", + "collapsedSize", + "easing", + "in", + "onEnter", + "onEntered", + "onEntering", + "onExit", + "onExited", + "onExiting", + "orientation", + "timeout", + ], + propDefaults: ({ set }) => { + const theme = useTheme(); + return set({ + collapsedSize: "0px", + orientation: "vertical", + get timeout() { + return theme.transitions.duration.standard; + }, + }); + }, + utilityClass: getCollapseUtilityClass, + autoCallUseClasses: false, +}); + +const CollapseRoot = styled("div", { + name: "MuiCollapse", + slot: "Root", +})(({ theme }) => ({ + height: 0, + overflow: "hidden", + transition: theme.transitions.create("height"), +})); + +const CollapseWrapper = styled("div", { + name: "MuiCollapse", + slot: "Wrapper", +})({ + display: "flex", + width: "100%", +}); + +const CollapseWrapperInner = styled("div", { + name: "MuiCollapse", + slot: "WrapperInner", +})({ + width: "100%", +}); + +/** + * The Collapse transition is used by the Vertical Stepper StepContent component. + * It uses react-transition-group internally. + * + * Demos: + * + * - [Card](https://mui.com/components/cards/) + * - [Lists](https://mui.com/components/lists/) + * - [Transitions](https://mui.com/components/transitions/) + * + * API: + * + * - [Collapse API](https://mui.com/api/collapse/) + */ +const Collapse = $.component(function Collapse({ + allProps, + props, + otherProps, + classes, +}) { + const theme = useTheme(); + const element = createElementRef(otherProps); + const wrapperRef = createElementRef(); + const c = children(() => props.children) as unknown as () => HTMLElement[]; + + const collapsedSize = + typeof props.collapsedSize === "number" + ? `${props.collapsedSize}px` + : props.collapsedSize; + const isHorizontal = props.orientation === "horizontal"; + const size = isHorizontal ? "width" : "height"; + + const [state, setState] = createSignal< + "entering" | "entered" | "exiting" | "exited" + >(props.in ? "entered" : "exited"); + + const getWrapperSize = () => { + return wrapperRef.ref?.[isHorizontal ? "clientWidth" : "clientHeight"] || 0; + }; + + createEffect( + on( + () => props.in, + (inProp) => { + const el = element.ref; + if (!el) return; + + if (inProp) { + // Entering + setState("entering"); + props.onEnter?.(); + + el.style[size] = collapsedSize; + reflow(el); + + const wrapperSize = getWrapperSize(); + const transitionProps = getTransitionProps( + { + style: otherProps.style as any, + timeout: props.timeout === "auto" ? 0 : props.timeout, + easing: props.easing, + }, + { mode: "enter" } + ); + + el.style.transition = theme.transitions.create(size, transitionProps); + el.style[size] = `${wrapperSize}px`; + + props.onEntering?.(); + + const done = () => { + el.style[size] = "auto"; + setState("entered"); + props.onEntered?.(); + }; + + if (props.timeout === "auto") { + const transitionDuration = + theme.transitions.getAutoHeightDuration(wrapperSize); + el.addEventListener("transitionend", done, { once: true }); + setTimeout(done, transitionDuration); + } else { + const duration = + typeof props.timeout === "number" + ? props.timeout + : (props.timeout as any)?.enter || + theme.transitions.duration.standard; + el.addEventListener("transitionend", done, { once: true }); + setTimeout(done, duration); + } + } else { + // Exiting + setState("exiting"); + props.onExit?.(); + + const wrapperSize = getWrapperSize(); + el.style[size] = `${wrapperSize}px`; + reflow(el); + + const transitionProps = getTransitionProps( + { + style: otherProps.style as any, + timeout: props.timeout === "auto" ? 0 : props.timeout, + easing: props.easing, + }, + { mode: "exit" } + ); + + el.style.transition = theme.transitions.create(size, transitionProps); + el.style[size] = collapsedSize; + + props.onExiting?.(); + + const done = () => { + setState("exited"); + props.onExited?.(); + }; + + if (props.timeout === "auto") { + const transitionDuration = + theme.transitions.getAutoHeightDuration(wrapperSize); + el.addEventListener("transitionend", done, { once: true }); + setTimeout(done, transitionDuration); + } else { + const duration = + typeof props.timeout === "number" + ? props.timeout + : (props.timeout as any)?.exit || + theme.transitions.duration.standard; + el.addEventListener("transitionend", done, { once: true }); + setTimeout(done, duration); + } + } + }, + { defer: true } + ) + ); + + onMount(() => { + const el = element.ref; + if (el && props.in) { + el.style[size] = "auto"; + } + }); + + return ( + + + + {c()} + + + + ); +}); + +export default Collapse; diff --git a/packages/material/src/Collapse/CollapseProps.ts b/packages/material/src/Collapse/CollapseProps.ts new file mode 100644 index 000000000..d34472150 --- /dev/null +++ b/packages/material/src/Collapse/CollapseProps.ts @@ -0,0 +1,88 @@ +import { CollapseClasses } from "./collapseClasses"; +import { OverrideProps } from "@suid/types"; +import { ElementType } from "@suid/types"; +import { JSXElement } from "solid-js"; + +export interface CollapseOwnProps< + ElementType extends HTMLElement = HTMLDivElement, +> { + /** + * The content node to be collapsed. + */ + children?: JSXElement; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * The width (horizontal) or height (vertical) of the container when collapsed. + * @default '0px' + */ + collapsedSize?: string | number; + /** + * The transition timing function. + * You may specify a single easing or a object containing enter and exit values. + */ + easing?: { enter?: string; exit?: string } | string; + /** + * If `true`, the component will transition in. + */ + in?: boolean; + /** + * The transition orientation. + * @default 'vertical' + */ + orientation?: "horizontal" | "vertical"; + /** + * The duration for the transition, in milliseconds. + * You may specify a single timeout for all transitions, or individually with an object. + * + * Set to 'auto' to automatically calculate transition time based on height. + * @default duration.standard + */ + timeout?: + | "auto" + | number + | { appear?: number; enter?: number; exit?: number }; + /** + * Add a custom transition end trigger. + */ + addEndListener?: (node: ElementType, done: () => void) => void; + /** + * Callback fired before the component enters. + */ + onEnter?: () => void; + /** + * Callback fired when the component has entered. + */ + onEntered?: () => void; + /** + * Callback fired when the component is entering. + */ + onEntering?: () => void; + /** + * Callback fired before the component exits. + */ + onExit?: () => void; + /** + * Callback fired when the component has exited. + */ + onExited?: () => void; + /** + * Callback fired when the component is exiting. + */ + onExiting?: () => void; +} + +export interface CollapseTypeMap

{ + name: "MuiCollapse"; + defaultPropNames: "collapsedSize" | "orientation" | "timeout"; + selfProps: CollapseOwnProps; + props: P & CollapseOwnProps; + defaultComponent: D; +} + +export type CollapseProps< + D extends ElementType = CollapseTypeMap["defaultComponent"], + P = {}, +> = OverrideProps, D>; diff --git a/packages/material/src/Collapse/collapseClasses.ts b/packages/material/src/Collapse/collapseClasses.ts new file mode 100644 index 000000000..ea6e3865a --- /dev/null +++ b/packages/material/src/Collapse/collapseClasses.ts @@ -0,0 +1,36 @@ +import { generateUtilityClass, generateUtilityClasses } from "@suid/base"; + +export interface CollapseClasses { + /** Styles applied to the root element. */ + root: string; + /** State class applied to the root element if `orientation="horizontal"`. */ + horizontal: string; + /** State class applied to the root element if `orientation="vertical"`. */ + vertical: string; + /** Styles applied to the root element when the transition has entered. */ + entered: string; + /** Styles applied to the root element when the transition has exited and `collapsedSize` = 0px. */ + hidden: string; + /** Styles applied to the outer wrapper element. */ + wrapper: string; + /** Styles applied to the inner wrapper element. */ + wrapperInner: string; +} + +export type CollapseClassKey = keyof CollapseClasses; + +export function getCollapseUtilityClass(slot: string): string { + return generateUtilityClass("MuiCollapse", slot); +} + +const collapseClasses: CollapseClasses = generateUtilityClasses("MuiCollapse", [ + "root", + "horizontal", + "vertical", + "entered", + "hidden", + "wrapper", + "wrapperInner", +]); + +export default collapseClasses; diff --git a/packages/material/src/Collapse/index.tsx b/packages/material/src/Collapse/index.tsx new file mode 100644 index 000000000..9d0cbdb97 --- /dev/null +++ b/packages/material/src/Collapse/index.tsx @@ -0,0 +1,4 @@ +export { default } from "./Collapse"; +export * from "./CollapseProps"; +export { default as collapseClasses } from "./collapseClasses"; +export * from "./collapseClasses"; diff --git a/packages/material/src/index.tsx b/packages/material/src/index.tsx index 69167aa4b..91cc0f82f 100644 --- a/packages/material/src/index.tsx +++ b/packages/material/src/index.tsx @@ -4,6 +4,10 @@ export namespace PropTypes { // keeping the type structure for backwards compat export type Color = "inherit" | "primary" | "secondary" | "default"; } +export { default as Accordion } from "./Accordion"; +export { default as AccordionActions } from "./AccordionActions"; +export { default as AccordionDetails } from "./AccordionDetails"; +export { default as AccordionSummary } from "./AccordionSummary"; export { default as Alert } from "./Alert"; export { default as AlertTitle } from "./AlertTitle"; export { default as AppBar } from "./AppBar"; @@ -26,6 +30,7 @@ export { default as CardMedia } from "./CardMedia"; export { default as Checkbox } from "./Checkbox"; export { default as Chip } from "./Chip"; export { default as CircularProgress } from "./CircularProgress"; +export { default as Collapse } from "./Collapse"; export { default as Container } from "./Container"; export { default as CssBaseline } from "./CssBaseline"; export { default as Dialog } from "./Dialog"; diff --git a/packages/site/src/pages/components/AccordionPage/AccordionPage.tsx b/packages/site/src/pages/components/AccordionPage/AccordionPage.tsx new file mode 100644 index 000000000..99c88d20e --- /dev/null +++ b/packages/site/src/pages/components/AccordionPage/AccordionPage.tsx @@ -0,0 +1,22 @@ +import { Accordion } from "@suid/material"; +import ComponentInfo from "~/components/ComponentInfo"; +import BasicAccordionExample from "./BasicAccordionExample"; +import ControlledAccordionExample from "./ControlledAccordionExample"; + +export default function AccordionPage() { + return ( + + ); +} diff --git a/packages/site/src/pages/components/AccordionPage/BasicAccordionExample.tsx b/packages/site/src/pages/components/AccordionPage/BasicAccordionExample.tsx new file mode 100644 index 000000000..710218849 --- /dev/null +++ b/packages/site/src/pages/components/AccordionPage/BasicAccordionExample.tsx @@ -0,0 +1,59 @@ +import ExpandMoreIcon from "@suid/icons-material/ExpandMore"; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Typography, +} from "@suid/material"; + +export default function BasicAccordion() { + return ( +

+ + } + aria-controls="panel1-content" + id="panel1-header" + > + Accordion 1 + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. + + + + + } + aria-controls="panel2-content" + id="panel2-header" + > + Accordion 2 + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. + + + + + } + aria-controls="panel3-content" + id="panel3-header" + > + Accordion 3 (default expanded) + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + malesuada lacus ex, sit amet blandit leo lobortis eget. + + + +
+ ); +} diff --git a/packages/site/src/pages/components/AccordionPage/ControlledAccordionExample.tsx b/packages/site/src/pages/components/AccordionPage/ControlledAccordionExample.tsx new file mode 100644 index 000000000..e0172691f --- /dev/null +++ b/packages/site/src/pages/components/AccordionPage/ControlledAccordionExample.tsx @@ -0,0 +1,96 @@ +import ExpandMoreIcon from "@suid/icons-material/ExpandMore"; +import { + Accordion, + AccordionActions, + AccordionDetails, + AccordionSummary, + Button, + Typography, +} from "@suid/material"; +import { createSignal } from "solid-js"; + +export default function ControlledAccordion() { + const [expanded, setExpanded] = createSignal(false); + + const handleChange = + (panel: string) => (event: Event, isExpanded: boolean) => { + setExpanded(isExpanded ? panel : false); + }; + + return ( +
+ + } + aria-controls="panel1bh-content" + id="panel1bh-header" + > + + General settings + + + I am an accordion + + + + + Nulla facilisi. Phasellus sollicitudin nulla et quam mattis feugiat. + Aliquam eget maximus est, id dignissim quam. + + + + + } + aria-controls="panel2bh-content" + id="panel2bh-header" + > + Users + + You are currently not an owner + + + + + Donec placerat, lectus sed mattis semper, neque lectus feugiat + lectus, varius pulvinar diam eros in elit. Pellentesque convallis + laoreet laoreet. + + + + + } + aria-controls="panel3bh-content" + id="panel3bh-header" + > + + Advanced settings + + + Filtering has been entirely disabled for whole web server + + + + + Nunc vitae orci ultricies, auctor nunc in, volutpat nisl. Integer + sit amet egestas eros, vitae egestas augue. Duis vel est augue. + + + + + + + +
+ ); +} From e9471d3bf20e7e8d06b63c4e4eb780b813e41dde Mon Sep 17 00:00:00 2001 From: Raphael Lin Date: Mon, 8 Dec 2025 14:43:01 +0800 Subject: [PATCH 2/8] Fix ESLint warnings by removing unused imports --- packages/material/src/Accordion/Accordion.tsx | 4 ++-- packages/material/src/AccordionSummary/AccordionSummary.tsx | 2 +- packages/material/src/Collapse/Collapse.tsx | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/material/src/Accordion/Accordion.tsx b/packages/material/src/Accordion/Accordion.tsx index e4887ff59..49ba00f79 100644 --- a/packages/material/src/Accordion/Accordion.tsx +++ b/packages/material/src/Accordion/Accordion.tsx @@ -1,12 +1,12 @@ import Collapse from "../Collapse"; import Paper from "../Paper"; import styled from "../styles/styled"; -import AccordionContext, { AccordionContextValue } from "./AccordionContext"; +import AccordionContext from "./AccordionContext"; import { AccordionTypeMap } from "./AccordionProps"; import accordionClasses, { getAccordionUtilityClass } from "./accordionClasses"; import createComponentFactory from "@suid/base/createComponentFactory"; import clsx from "clsx"; -import { children, createSignal, JSXElement } from "solid-js"; +import { children, createSignal } from "solid-js"; const $ = createComponentFactory()({ name: "MuiAccordion", diff --git a/packages/material/src/AccordionSummary/AccordionSummary.tsx b/packages/material/src/AccordionSummary/AccordionSummary.tsx index d61963a5d..d93b3e5ff 100644 --- a/packages/material/src/AccordionSummary/AccordionSummary.tsx +++ b/packages/material/src/AccordionSummary/AccordionSummary.tsx @@ -43,7 +43,7 @@ const AccordionSummaryRoot = styled(ButtonBase, { const AccordionSummaryContent = styled("div", { name: "MuiAccordionSummary", slot: "Content", -})(({ theme }) => ({ +})(() => ({ display: "flex", flexGrow: 1, margin: "12px 0", diff --git a/packages/material/src/Collapse/Collapse.tsx b/packages/material/src/Collapse/Collapse.tsx index 240507c61..58725028f 100644 --- a/packages/material/src/Collapse/Collapse.tsx +++ b/packages/material/src/Collapse/Collapse.tsx @@ -2,7 +2,7 @@ import styled from "../styles/styled"; import useTheme from "../styles/useTheme"; import { reflow, getTransitionProps } from "../transitions/utils"; import { CollapseTypeMap } from "./CollapseProps"; -import collapseClasses, { getCollapseUtilityClass } from "./collapseClasses"; +import { getCollapseUtilityClass } from "./collapseClasses"; import createComponentFactory from "@suid/base/createComponentFactory"; import createElementRef from "@suid/system/createElementRef"; import clsx from "clsx"; @@ -96,6 +96,7 @@ const Collapse = $.component(function Collapse({ const isHorizontal = props.orientation === "horizontal"; const size = isHorizontal ? "width" : "height"; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [state, setState] = createSignal< "entering" | "entered" | "exiting" | "exited" >(props.in ? "entered" : "exited"); From 1c3e0d6e7c2a60e798c906806cb412e4fceac0a0 Mon Sep 17 00:00:00 2001 From: Raphael Lin Date: Mon, 8 Dec 2025 14:53:41 +0800 Subject: [PATCH 3/8] fix accordion caret position --- packages/material/src/AccordionSummary/AccordionSummary.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/material/src/AccordionSummary/AccordionSummary.tsx b/packages/material/src/AccordionSummary/AccordionSummary.tsx index d93b3e5ff..5b5d05e29 100644 --- a/packages/material/src/AccordionSummary/AccordionSummary.tsx +++ b/packages/material/src/AccordionSummary/AccordionSummary.tsx @@ -21,6 +21,7 @@ const AccordionSummaryRoot = styled(ButtonBase, { slot: "Root", })(({ theme }) => ({ display: "flex", + width: "100%", minHeight: 48, padding: "0px 16px", transition: theme.transitions.create(["min-height", "background-color"], { @@ -40,11 +41,12 @@ const AccordionSummaryRoot = styled(ButtonBase, { }, })); -const AccordionSummaryContent = styled("div", { +const AccordionSummaryContent = styled("span", { name: "MuiAccordionSummary", slot: "Content", })(() => ({ display: "flex", + textAlign: "start", flexGrow: 1, margin: "12px 0", [`&.${accordionSummaryClasses.expanded}`]: { @@ -52,7 +54,7 @@ const AccordionSummaryContent = styled("div", { }, })); -const AccordionSummaryExpandIconWrapper = styled("div", { +const AccordionSummaryExpandIconWrapper = styled("span", { name: "MuiAccordionSummary", slot: "ExpandIconWrapper", })(({ theme }) => ({ From 5967f0a96eacd5c91f33533a8b8a50b70ce43fa0 Mon Sep 17 00:00:00 2001 From: Raphael Lin Date: Mon, 8 Dec 2025 15:01:00 +0800 Subject: [PATCH 4/8] accordion transition --- packages/material/src/Collapse/Collapse.tsx | 113 ++++++++++++++------ 1 file changed, 80 insertions(+), 33 deletions(-) diff --git a/packages/material/src/Collapse/Collapse.tsx b/packages/material/src/Collapse/Collapse.tsx index 58725028f..778b7eef3 100644 --- a/packages/material/src/Collapse/Collapse.tsx +++ b/packages/material/src/Collapse/Collapse.tsx @@ -46,7 +46,6 @@ const CollapseRoot = styled("div", { })(({ theme }) => ({ height: 0, overflow: "hidden", - transition: theme.transitions.create("height"), })); const CollapseWrapper = styled("div", { @@ -117,7 +116,11 @@ const Collapse = $.component(function Collapse({ setState("entering"); props.onEnter?.(); + // Set initial collapsed state el.style[size] = collapsedSize; + el.style.overflow = "hidden"; + + // Force reflow to ensure initial state is applied reflow(el); const wrapperSize = getWrapperSize(); @@ -130,38 +133,54 @@ const Collapse = $.component(function Collapse({ { mode: "enter" } ); - el.style.transition = theme.transitions.create(size, transitionProps); + // Calculate duration + let duration: number; + if (props.timeout === "auto") { + duration = theme.transitions.getAutoHeightDuration(wrapperSize); + el.style.transitionDuration = `${duration}ms`; + } else { + duration = + typeof props.timeout === "number" + ? props.timeout + : (props.timeout as any)?.enter || + theme.transitions.duration.standard; + el.style.transitionDuration = + typeof transitionProps.duration === "string" + ? transitionProps.duration + : `${transitionProps.duration}ms`; + } + + el.style.transitionTimingFunction = transitionProps.easing || ""; + + // Start transition to expanded size el.style[size] = `${wrapperSize}px`; props.onEntering?.(); const done = () => { el.style[size] = "auto"; + el.style.overflow = "visible"; setState("entered"); props.onEntered?.(); }; - if (props.timeout === "auto") { - const transitionDuration = - theme.transitions.getAutoHeightDuration(wrapperSize); - el.addEventListener("transitionend", done, { once: true }); - setTimeout(done, transitionDuration); - } else { - const duration = - typeof props.timeout === "number" - ? props.timeout - : (props.timeout as any)?.enter || - theme.transitions.duration.standard; - el.addEventListener("transitionend", done, { once: true }); - setTimeout(done, duration); - } + el.addEventListener("transitionend", done, { once: true }); + setTimeout(done, duration); } else { // Exiting setState("exiting"); props.onExit?.(); const wrapperSize = getWrapperSize(); - el.style[size] = `${wrapperSize}px`; + + // If height is 'auto', we need to set it to a specific value first + if (el.style[size] === "auto" || el.style[size] === "") { + el.style[size] = `${wrapperSize}px`; + } + + el.style.overflow = "hidden"; + + // Force reflow to ensure the size is set before transition reflow(el); const transitionProps = getTransitionProps( @@ -173,7 +192,26 @@ const Collapse = $.component(function Collapse({ { mode: "exit" } ); - el.style.transition = theme.transitions.create(size, transitionProps); + // Calculate duration + let duration: number; + if (props.timeout === "auto") { + duration = theme.transitions.getAutoHeightDuration(wrapperSize); + el.style.transitionDuration = `${duration}ms`; + } else { + duration = + typeof props.timeout === "number" + ? props.timeout + : (props.timeout as any)?.exit || + theme.transitions.duration.standard; + el.style.transitionDuration = + typeof transitionProps.duration === "string" + ? transitionProps.duration + : `${transitionProps.duration}ms`; + } + + el.style.transitionTimingFunction = transitionProps.easing || ""; + + // Start transition to collapsed size el.style[size] = collapsedSize; props.onExiting?.(); @@ -183,20 +221,8 @@ const Collapse = $.component(function Collapse({ props.onExited?.(); }; - if (props.timeout === "auto") { - const transitionDuration = - theme.transitions.getAutoHeightDuration(wrapperSize); - el.addEventListener("transitionend", done, { once: true }); - setTimeout(done, transitionDuration); - } else { - const duration = - typeof props.timeout === "number" - ? props.timeout - : (props.timeout as any)?.exit || - theme.transitions.duration.standard; - el.addEventListener("transitionend", done, { once: true }); - setTimeout(done, duration); - } + el.addEventListener("transitionend", done, { once: true }); + setTimeout(done, duration); } }, { defer: true } @@ -205,8 +231,29 @@ const Collapse = $.component(function Collapse({ onMount(() => { const el = element.ref; - if (el && props.in) { + if (!el) return; + + if (props.in) { + el.style[size] = "auto"; + el.style.overflow = "visible"; + } else { + el.style[size] = collapsedSize; + el.style.overflow = "hidden"; + } + }); + + // Keep the element in sync with the state after transitions complete + createEffect(() => { + const el = element.ref; + const currentState = state(); + if (!el) return; + + if (currentState === "entered") { el.style[size] = "auto"; + el.style.overflow = "visible"; + } else if (currentState === "exited") { + el.style[size] = collapsedSize; + el.style.overflow = "hidden"; } }); From 5a2c6ad765d89a558fab081a4e6848e9c91233c2 Mon Sep 17 00:00:00 2001 From: Raphael Lin Date: Mon, 8 Dec 2025 15:02:59 +0800 Subject: [PATCH 5/8] remove debug log --- packages/material/src/Accordion/Accordion.tsx | 4 ---- packages/material/src/AccordionSummary/AccordionSummary.tsx | 5 ----- 2 files changed, 9 deletions(-) diff --git a/packages/material/src/Accordion/Accordion.tsx b/packages/material/src/Accordion/Accordion.tsx index 49ba00f79..90983cfbe 100644 --- a/packages/material/src/Accordion/Accordion.tsx +++ b/packages/material/src/Accordion/Accordion.tsx @@ -128,11 +128,7 @@ const Accordion = $.component(function Accordion({ props.expanded !== undefined ? props.expanded : expandedState(); const handleChange = (event: Event) => { - console.log("Accordion handleChange called", { - currentExpanded: expanded(), - }); const newExpanded = !expanded(); - console.log("Accordion setting expanded to:", newExpanded); setExpandedState(newExpanded); props.onChange?.(event, newExpanded); }; diff --git a/packages/material/src/AccordionSummary/AccordionSummary.tsx b/packages/material/src/AccordionSummary/AccordionSummary.tsx index 5b5d05e29..a8ab458cd 100644 --- a/packages/material/src/AccordionSummary/AccordionSummary.tsx +++ b/packages/material/src/AccordionSummary/AccordionSummary.tsx @@ -101,12 +101,7 @@ const AccordionSummary = $.component(function AccordionSummary({ allProps.class )} onClick={(e: any) => { - console.log("AccordionSummary clicked", { - context, - hasToggle: !!context?.toggle, - }); if (context?.toggle) { - console.log("Calling context.toggle"); context.toggle(e); } if (typeof otherProps.onClick === "function") { From 88121cedbd07c851ca2ffe9f50f2938bf241b374 Mon Sep 17 00:00:00 2001 From: Raphael Lin Date: Mon, 8 Dec 2025 15:08:49 +0800 Subject: [PATCH 6/8] the margin of expanded accordion item --- packages/material/src/Accordion/Accordion.tsx | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/material/src/Accordion/Accordion.tsx b/packages/material/src/Accordion/Accordion.tsx index 90983cfbe..cd767302a 100644 --- a/packages/material/src/Accordion/Accordion.tsx +++ b/packages/material/src/Accordion/Accordion.tsx @@ -91,6 +91,17 @@ const AccordionRoot = styled(Paper, { [`&.${accordionClasses.disabled}`]: { backgroundColor: theme.palette.action.disabledBackground, }, + [`&.${accordionClasses.gutters}`]: { + [`&.${accordionClasses.expanded}`]: { + margin: "16px 0", + "&:first-of-type": { + marginTop: 0, + }, + "&:last-of-type": { + marginBottom: 0, + }, + }, + }, }; }); @@ -133,13 +144,19 @@ const Accordion = $.component(function Accordion({ props.onChange?.(event, newExpanded); }; + // Create owner state object that includes the expanded state + const ownerState = () => ({ + ...allProps, + expanded: expanded(), + }); + return ( - + From b666f98b57bbb07c4b8625f3e966891bcbd6d912 Mon Sep 17 00:00:00 2001 From: Raphael Lin Date: Mon, 8 Dec 2025 17:10:20 +0800 Subject: [PATCH 7/8] fix(Accordion): Fix onChange prop type conflict Use ChangeEventHandler and omit onChange from inherited PaperProps to prevent type intersection conflict with base HTML onChange event handler. --- packages/material/src/Accordion/AccordionProps.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/material/src/Accordion/AccordionProps.ts b/packages/material/src/Accordion/AccordionProps.ts index a1f275d03..71d390f4a 100644 --- a/packages/material/src/Accordion/AccordionProps.ts +++ b/packages/material/src/Accordion/AccordionProps.ts @@ -1,6 +1,6 @@ import { PaperProps } from "../Paper/PaperProps"; import { AccordionClasses } from "./accordionClasses"; -import { OverrideProps, ElementType } from "@suid/types"; +import { OverrideProps, ElementType, ChangeEventHandler } from "@suid/types"; import { JSXElement } from "solid-js"; export interface AccordionOwnProps { @@ -38,7 +38,7 @@ export interface AccordionOwnProps { * @param {React.SyntheticEvent} event The event source of the callback. **Warning**: This is a generic event not a change event. * @param {boolean} expanded The `expanded` state of the accordion. */ - onChange?: (event: Event, expanded: boolean) => void; + onChange?: ChangeEventHandler; /** * If `true`, rounded corners are disabled. * @default false @@ -54,7 +54,7 @@ export interface AccordionTypeMap

{ | "disableGutters" | "square"; selfProps: AccordionOwnProps; - props: P & AccordionOwnProps & Omit; + props: P & AccordionOwnProps & Omit; defaultComponent: D; } From 479797837ad9f8ccb6431450af775482398dd88f Mon Sep 17 00:00:00 2001 From: Raphael Lin Date: Mon, 8 Dec 2025 17:16:24 +0800 Subject: [PATCH 8/8] fix(Accordion): Cast event to ChangeEvent in onChange handler --- packages/material/src/Accordion/Accordion.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/material/src/Accordion/Accordion.tsx b/packages/material/src/Accordion/Accordion.tsx index cd767302a..7013b97bb 100644 --- a/packages/material/src/Accordion/Accordion.tsx +++ b/packages/material/src/Accordion/Accordion.tsx @@ -5,6 +5,7 @@ import AccordionContext from "./AccordionContext"; import { AccordionTypeMap } from "./AccordionProps"; import accordionClasses, { getAccordionUtilityClass } from "./accordionClasses"; import createComponentFactory from "@suid/base/createComponentFactory"; +import { ChangeEvent } from "@suid/types"; import clsx from "clsx"; import { children, createSignal } from "solid-js"; @@ -141,7 +142,7 @@ const Accordion = $.component(function Accordion({ const handleChange = (event: Event) => { const newExpanded = !expanded(); setExpandedState(newExpanded); - props.onChange?.(event, newExpanded); + props.onChange?.(event as ChangeEvent, newExpanded); }; // Create owner state object that includes the expanded state