diff --git a/packages/material/src/Accordion/Accordion.tsx b/packages/material/src/Accordion/Accordion.tsx new file mode 100644 index 000000000..7013b97bb --- /dev/null +++ b/packages/material/src/Accordion/Accordion.tsx @@ -0,0 +1,196 @@ +import Collapse from "../Collapse"; +import Paper from "../Paper"; +import styled from "../styles/styled"; +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"; + +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, + }, + [`&.${accordionClasses.gutters}`]: { + [`&.${accordionClasses.expanded}`]: { + margin: "16px 0", + "&:first-of-type": { + marginTop: 0, + }, + "&:last-of-type": { + marginBottom: 0, + }, + }, + }, + }; +}); + +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) => { + const newExpanded = !expanded(); + setExpandedState(newExpanded); + props.onChange?.(event as ChangeEvent, newExpanded); + }; + + // Create owner state object that includes the expanded state + const ownerState = () => ({ + ...allProps, + expanded: expanded(), + }); + + 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..71d390f4a --- /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, ChangeEventHandler } 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?: ChangeEventHandler; + /** + * 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..a8ab458cd --- /dev/null +++ b/packages/material/src/AccordionSummary/AccordionSummary.tsx @@ -0,0 +1,134 @@ +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", + width: "100%", + 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("span", { + name: "MuiAccordionSummary", + slot: "Content", +})(() => ({ + display: "flex", + textAlign: "start", + flexGrow: 1, + margin: "12px 0", + [`&.${accordionSummaryClasses.expanded}`]: { + margin: "20px 0", + }, +})); + +const AccordionSummaryExpandIconWrapper = styled("span", { + 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 ( + { + if (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..778b7eef3 --- /dev/null +++ b/packages/material/src/Collapse/Collapse.tsx @@ -0,0 +1,275 @@ +import styled from "../styles/styled"; +import useTheme from "../styles/useTheme"; +import { reflow, getTransitionProps } from "../transitions/utils"; +import { CollapseTypeMap } from "./CollapseProps"; +import { 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", +})); + +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"; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + 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?.(); + + // 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(); + const transitionProps = getTransitionProps( + { + style: otherProps.style as any, + timeout: props.timeout === "auto" ? 0 : props.timeout, + easing: props.easing, + }, + { mode: "enter" } + ); + + // 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?.(); + }; + + el.addEventListener("transitionend", done, { once: true }); + setTimeout(done, duration); + } else { + // Exiting + setState("exiting"); + props.onExit?.(); + + const wrapperSize = getWrapperSize(); + + // 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( + { + style: otherProps.style as any, + timeout: props.timeout === "auto" ? 0 : props.timeout, + easing: props.easing, + }, + { mode: "exit" } + ); + + // 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?.(); + + const done = () => { + setState("exited"); + props.onExited?.(); + }; + + el.addEventListener("transitionend", done, { once: true }); + setTimeout(done, duration); + } + }, + { defer: true } + ) + ); + + onMount(() => { + const el = element.ref; + 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"; + } + }); + + 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. + + + + + + + +
+ ); +}