From d221e2e59d4941a40a80a90b7d0f2cbfba256f57 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Fri, 23 Aug 2024 00:00:26 +0800 Subject: [PATCH] WIP - New Accordion hooks and components --- docs/pages/experiments/accordion.tsx | 73 ++++++++++ .../Heading/AccordionHeading.test.tsx | 73 ++++++++++ .../Accordion/Heading/AccordionHeading.tsx | 33 +++++ .../Accordion/Panel/AccordionPanel.test.tsx | 73 ++++++++++ .../src/Accordion/Panel/AccordionPanel.tsx | 55 ++++++++ .../src/Accordion/Root/AccordionRoot.test.tsx | 16 +++ .../src/Accordion/Root/AccordionRoot.tsx | 73 ++++++++++ .../Accordion/Root/AccordionRootContext.tsx | 22 +++ .../mui-base/src/Accordion/Root/styleHooks.ts | 0 .../src/Accordion/Root/useAccordionRoot.ts | 107 +++++++++++++++ .../Section/AccordionSection.test.tsx | 38 ++++++ .../Accordion/Section/AccordionSection.tsx | 128 ++++++++++++++++++ .../Section/AccordionSectionContext.tsx | 24 ++++ .../src/Accordion/Section/styleHooks.ts | 21 +++ .../Trigger/AccordionTrigger.test.tsx | 73 ++++++++++ .../Accordion/Trigger/AccordionTrigger.tsx | 44 ++++++ .../mui-base/src/Accordion/index.barrel.ts | 5 + packages/mui-base/src/Accordion/index.ts | 13 ++ packages/mui-base/src/index.ts | 1 + .../src/utils/defaultRenderFunctions.tsx | 4 + 20 files changed, 876 insertions(+) create mode 100644 docs/pages/experiments/accordion.tsx create mode 100644 packages/mui-base/src/Accordion/Heading/AccordionHeading.test.tsx create mode 100644 packages/mui-base/src/Accordion/Heading/AccordionHeading.tsx create mode 100644 packages/mui-base/src/Accordion/Panel/AccordionPanel.test.tsx create mode 100644 packages/mui-base/src/Accordion/Panel/AccordionPanel.tsx create mode 100644 packages/mui-base/src/Accordion/Root/AccordionRoot.test.tsx create mode 100644 packages/mui-base/src/Accordion/Root/AccordionRoot.tsx create mode 100644 packages/mui-base/src/Accordion/Root/AccordionRootContext.tsx create mode 100644 packages/mui-base/src/Accordion/Root/styleHooks.ts create mode 100644 packages/mui-base/src/Accordion/Root/useAccordionRoot.ts create mode 100644 packages/mui-base/src/Accordion/Section/AccordionSection.test.tsx create mode 100644 packages/mui-base/src/Accordion/Section/AccordionSection.tsx create mode 100644 packages/mui-base/src/Accordion/Section/AccordionSectionContext.tsx create mode 100644 packages/mui-base/src/Accordion/Section/styleHooks.ts create mode 100644 packages/mui-base/src/Accordion/Trigger/AccordionTrigger.test.tsx create mode 100644 packages/mui-base/src/Accordion/Trigger/AccordionTrigger.tsx create mode 100644 packages/mui-base/src/Accordion/index.barrel.ts create mode 100644 packages/mui-base/src/Accordion/index.ts diff --git a/docs/pages/experiments/accordion.tsx b/docs/pages/experiments/accordion.tsx new file mode 100644 index 000000000..66d75b3f1 --- /dev/null +++ b/docs/pages/experiments/accordion.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import * as Accordion from '@base_ui/react/Accordion'; + +export default function App() { + return ( +
+
Plain HTML
+
+
+

+ +

+
+ This the contents of Panel 1 +
+
+ +
+

+ +

+
+ This the contents of Panel 2 +
+
+
+ +
+
+
+
+
+ +
Base UI
+ + + + + Trigger 1 + + + This is the contents of Accordion.Panel 1 + + + + + + Trigger 2 + + + This is the contents of Accordion.Panel 2 + + + +
+ ); +} diff --git a/packages/mui-base/src/Accordion/Heading/AccordionHeading.test.tsx b/packages/mui-base/src/Accordion/Heading/AccordionHeading.test.tsx new file mode 100644 index 000000000..e536dd5a1 --- /dev/null +++ b/packages/mui-base/src/Accordion/Heading/AccordionHeading.test.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +// import { expect } from 'chai'; +// import { spy } from 'sinon'; +import { createRenderer /* , act */ } from '@mui/internal-test-utils'; +import * as Accordion from '@base_ui/react/Accordion'; +import * as Collapsible from '@base_ui/react/Collapsible'; +import { describeConformance } from '../../../test/describeConformance'; + +const { AccordionRootContext, AccordionSectionContext } = Accordion; + +const { CollapsibleContext } = Collapsible; + +const accordionRootContextValue: Accordion.Root.Context = { + accordionSectionRefs: { current: [] }, + animated: false, + disabled: false, + handleOpenChange() {}, + ownerState: { + value: [0], + disabled: false, + }, + value: [0], +}; + +const accordionSectionContextValue: Accordion.Section.Context = { + open: true, + ownerState: { + value: [0], + disabled: false, + index: 0, + open: true, + transitionStatus: undefined, + }, +}; + +const collapsibleContextValue: Collapsible.Root.Context = { + animated: false, + contentId: ':content:', + disabled: false, + mounted: true, + open: true, + setContentId() {}, + setMounted() {}, + setOpen() {}, + transitionStatus: undefined, + ownerState: { + open: true, + disabled: false, + transitionStatus: undefined, + }, +}; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'h3', + render: (node) => { + const { container, ...other } = render( + + + + {node} + + + , + ); + + return { container, ...other }; + }, + refInstanceof: window.HTMLHeadingElement, + })); +}); diff --git a/packages/mui-base/src/Accordion/Heading/AccordionHeading.tsx b/packages/mui-base/src/Accordion/Heading/AccordionHeading.tsx new file mode 100644 index 000000000..17c40954a --- /dev/null +++ b/packages/mui-base/src/Accordion/Heading/AccordionHeading.tsx @@ -0,0 +1,33 @@ +'use client'; +import * as React from 'react'; +import { BaseUIComponentProps } from '../../utils/types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { AccordionSection } from '../Section/AccordionSection'; +import { useAccordionSectionContext } from '../Section/AccordionSectionContext'; +import { accordionStyleHookMapping } from '../Section/styleHooks'; + +const AccordionHeading = React.forwardRef(function AccordionHeading( + props: AccordionHeading.Props, + forwardedRef: React.ForwardedRef, +) { + const { render, className, ...otherProps } = props; + + const { ownerState } = useAccordionSectionContext(); + + const { renderElement } = useComponentRenderer({ + render: render ?? 'h3', + ownerState, + className, + ref: forwardedRef, + extraProps: otherProps, + customStyleHookMapping: accordionStyleHookMapping, + }); + + return renderElement(); +}); + +export { AccordionHeading }; + +export namespace AccordionHeading { + export interface Props extends BaseUIComponentProps<'h3', AccordionSection.OwnerState> {} +} diff --git a/packages/mui-base/src/Accordion/Panel/AccordionPanel.test.tsx b/packages/mui-base/src/Accordion/Panel/AccordionPanel.test.tsx new file mode 100644 index 000000000..b6fa03663 --- /dev/null +++ b/packages/mui-base/src/Accordion/Panel/AccordionPanel.test.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +// import { expect } from 'chai'; +// import { spy } from 'sinon'; +import { createRenderer /* , act */ } from '@mui/internal-test-utils'; +import * as Accordion from '@base_ui/react/Accordion'; +import * as Collapsible from '@base_ui/react/Collapsible'; +import { describeConformance } from '../../../test/describeConformance'; + +const { AccordionRootContext, AccordionSectionContext } = Accordion; + +const { CollapsibleContext } = Collapsible; + +const accordionRootContextValue: Accordion.Root.Context = { + accordionSectionRefs: { current: [] }, + animated: false, + disabled: false, + handleOpenChange() {}, + ownerState: { + value: [0], + disabled: false, + }, + value: [0], +}; + +const accordionSectionContextValue: Accordion.Section.Context = { + open: true, + ownerState: { + value: [0], + disabled: false, + index: 0, + open: true, + transitionStatus: undefined, + }, +}; + +const collapsibleContextValue: Collapsible.Root.Context = { + animated: false, + contentId: ':content:', + disabled: false, + mounted: true, + open: true, + setContentId() {}, + setMounted() {}, + setOpen() {}, + transitionStatus: undefined, + ownerState: { + open: true, + disabled: false, + transitionStatus: undefined, + }, +}; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'div', + render: (node) => { + const { container, ...other } = render( + + + + {node} + + + , + ); + + return { container, ...other }; + }, + refInstanceof: window.HTMLDivElement, + })); +}); diff --git a/packages/mui-base/src/Accordion/Panel/AccordionPanel.tsx b/packages/mui-base/src/Accordion/Panel/AccordionPanel.tsx new file mode 100644 index 000000000..4795e869e --- /dev/null +++ b/packages/mui-base/src/Accordion/Panel/AccordionPanel.tsx @@ -0,0 +1,55 @@ +'use client'; +import * as React from 'react'; +import { BaseUIComponentProps } from '../../utils/types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useCollapsibleContext } from '../../Collapsible/Root/CollapsibleContext'; +import { useCollapsibleContent } from '../../Collapsible/Content/useCollapsibleContent'; +import type { AccordionSection } from '../Section/AccordionSection'; +import { useAccordionSectionContext } from '../Section/AccordionSectionContext'; +import { accordionStyleHookMapping } from '../Section/styleHooks'; + +export const AccordionPanel = React.forwardRef(function AccordionPanel( + props: AccordionPanel.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, htmlHidden, render, ...otherProps } = props; + + const { animated, mounted, open, contentId, setContentId, setMounted, setOpen } = + useCollapsibleContext(); + + const { getRootProps, height } = useCollapsibleContent({ + animated, + htmlHidden, + id: contentId, + mounted, + open, + ref: forwardedRef, + setContentId, + setMounted, + setOpen, + }); + + const { ownerState } = useAccordionSectionContext(); + + const { renderElement } = useComponentRenderer({ + propGetter: getRootProps, + render: render ?? 'div', + ownerState, + className, + extraProps: { + ...otherProps, + style: { + '--accordion-content-height': height ? `${height}px` : undefined, + }, + }, + customStyleHookMapping: accordionStyleHookMapping, + }); + + return renderElement(); +}); + +export namespace AccordionPanel { + export interface Props + extends BaseUIComponentProps<'div', AccordionSection.OwnerState>, + Pick {} +} diff --git a/packages/mui-base/src/Accordion/Root/AccordionRoot.test.tsx b/packages/mui-base/src/Accordion/Root/AccordionRoot.test.tsx new file mode 100644 index 000000000..6597bf2e1 --- /dev/null +++ b/packages/mui-base/src/Accordion/Root/AccordionRoot.test.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +// import { expect } from 'chai'; +// import { spy } from 'sinon'; +import { createRenderer /* , act */ } from '@mui/internal-test-utils'; +import * as Accordion from '@base_ui/react/Accordion'; +import { describeConformance } from '../../../test/describeConformance'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'div', + render, + refInstanceof: window.HTMLDivElement, + })); +}); diff --git a/packages/mui-base/src/Accordion/Root/AccordionRoot.tsx b/packages/mui-base/src/Accordion/Root/AccordionRoot.tsx new file mode 100644 index 000000000..b90f9f7de --- /dev/null +++ b/packages/mui-base/src/Accordion/Root/AccordionRoot.tsx @@ -0,0 +1,73 @@ +'use client'; +import * as React from 'react'; +import { FloatingList } from '@floating-ui/react'; +import { BaseUIComponentProps } from '../../utils/types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useAccordionRoot } from './useAccordionRoot'; +import { AccordionRootContext } from './AccordionRootContext'; + +const AccordionRoot = React.forwardRef(function AccordionRoot( + props: AccordionRoot.Props, + forwardedRef: React.ForwardedRef, +) { + const { animated, disabled, defaultValue, value, className, render, ...otherProps } = props; + + const { getRootProps, ...accordion } = useAccordionRoot({ + animated, + disabled, + defaultValue, + value, + }); + + const ownerState: AccordionRoot.OwnerState = React.useMemo( + () => ({ + value: accordion.value, + disabled: accordion.disabled, + // transitionStatus: accordion.transitionStatus, + }), + [accordion.value, accordion.disabled], + ); + + const contextValue: AccordionRoot.Context = React.useMemo( + () => ({ + ...accordion, + ownerState, + }), + [accordion, ownerState], + ); + + const { renderElement } = useComponentRenderer({ + propGetter: getRootProps, + render: render ?? 'div', + className, + ownerState, + ref: forwardedRef, + extraProps: otherProps, + customStyleHookMapping: { + value: () => null, + }, + }); + + return ( + + {renderElement()} + + ); +}); + +export { AccordionRoot }; + +export namespace AccordionRoot { + export interface Context extends Omit { + ownerState: OwnerState; + } + + export interface OwnerState { + value: useAccordionRoot.Value; + disabled: boolean; + } + + export interface Props + extends useAccordionRoot.Parameters, + BaseUIComponentProps {} +} diff --git a/packages/mui-base/src/Accordion/Root/AccordionRootContext.tsx b/packages/mui-base/src/Accordion/Root/AccordionRootContext.tsx new file mode 100644 index 000000000..0f862f8dd --- /dev/null +++ b/packages/mui-base/src/Accordion/Root/AccordionRootContext.tsx @@ -0,0 +1,22 @@ +'use client'; +import * as React from 'react'; +import type { AccordionRoot } from './AccordionRoot'; + +/** + * @ignore - internal component. + */ +export const AccordionRootContext = React.createContext( + undefined, +); + +if (process.env.NODE_ENV !== 'production') { + AccordionRootContext.displayName = 'AccordionRootContext'; +} + +export function useAccordionRootContext() { + const context = React.useContext(AccordionRootContext); + if (context === undefined) { + throw new Error('useAccordionRootContext must be used inside a Accordion component'); + } + return context; +} diff --git a/packages/mui-base/src/Accordion/Root/styleHooks.ts b/packages/mui-base/src/Accordion/Root/styleHooks.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/mui-base/src/Accordion/Root/useAccordionRoot.ts b/packages/mui-base/src/Accordion/Root/useAccordionRoot.ts new file mode 100644 index 000000000..60141ddda --- /dev/null +++ b/packages/mui-base/src/Accordion/Root/useAccordionRoot.ts @@ -0,0 +1,107 @@ +'use client'; +import * as React from 'react'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { useControlled } from '../../utils/useControlled'; + +export function useAccordionRoot( + parameters: useAccordionRoot.Parameters, +): useAccordionRoot.ReturnValue { + const { animated = true, defaultValue, value: valueParam, disabled = false } = parameters; + + const accordionSectionRefs = React.useRef<(HTMLElement | null)[]>([]); + + const [value, setValue] = useControlled({ + controlled: valueParam, + default: valueParam ?? defaultValue ?? [], + name: 'Accordion', + state: 'value', + }); + // console.log(value); + + const handleOpenChange = React.useCallback( + (newValue: number | string, nextOpen: boolean) => { + // console.group('useAccordionRoot handleOpenChange'); + // console.log('newValue', newValue, 'nextOpen', nextOpen, 'openValues', value); + if (nextOpen) { + const nextOpenValues = value.slice(); + nextOpenValues.push(newValue); + setValue(nextOpenValues); + } else { + const nextOpenValues = value.filter((v) => v !== newValue); + setValue(nextOpenValues); + } + // console.groupEnd(); + }, + [setValue, value], + ); + + const getRootProps = React.useCallback( + (externalProps = {}) => + mergeReactProps(externalProps, { + role: 'region', + }), + [], + ); + + return React.useMemo( + () => ({ + getRootProps, + accordionSectionRefs, + animated, + disabled, + handleOpenChange, + value, + }), + [getRootProps, accordionSectionRefs, animated, disabled, handleOpenChange, value], + ); +} + +export namespace useAccordionRoot { + export type Value = readonly (string | number)[]; + + export interface Parameters { + /** + * If `true`, the component supports CSS/JS-based animations and transitions. + * @default true + */ + animated?: boolean; + /** + * The value of the currently open `Accordion.Section` + * This is the controlled counterpart of `defaultValue`. + */ + value?: Value; + /** + * The default value representing the currently open `Accordion.Section` + * This is the uncontrolled counterpart of `value`. + * @default 0 + */ + defaultValue?: Value; + /** + * Callback fired when an Accordion section is opened or closed. + * The value representing the involved section is provided as an argument. + */ + onOpenChange?: (value: Value) => void; + /** + * If `true`, the component is disabled. + * @default false + */ + disabled?: boolean; + } + + export interface ReturnValue { + getRootProps: ( + externalProps?: React.ComponentPropsWithRef<'div'>, + ) => React.ComponentPropsWithRef<'div'>; + accordionSectionRefs: React.MutableRefObject<(HTMLElement | null)[]>; + animated: boolean; + /** + * The disabled state of the Accordion + */ + disabled: boolean; + handleOpenChange: (value: number | string, nextOpen: boolean) => void; + /** + * The open state of the Accordion + */ + value: Value; + } +} diff --git a/packages/mui-base/src/Accordion/Section/AccordionSection.test.tsx b/packages/mui-base/src/Accordion/Section/AccordionSection.test.tsx new file mode 100644 index 000000000..80e9d2ea7 --- /dev/null +++ b/packages/mui-base/src/Accordion/Section/AccordionSection.test.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +// import { expect } from 'chai'; +// import { spy } from 'sinon'; +import { createRenderer /* , act */ } from '@mui/internal-test-utils'; +import * as Accordion from '@base_ui/react/Accordion'; +import { describeConformance } from '../../../test/describeConformance'; + +const { AccordionRootContext } = Accordion; + +const accordionRootContextValue: Accordion.Root.Context = { + accordionSectionRefs: { current: [] }, + animated: false, + disabled: false, + handleOpenChange() {}, + ownerState: { + value: [0], + disabled: false, + }, + value: [0], +}; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'div', + render: (node) => { + const { container, ...other } = render( + + {node} + , + ); + + return { container, ...other }; + }, + refInstanceof: window.HTMLDivElement, + })); +}); diff --git a/packages/mui-base/src/Accordion/Section/AccordionSection.tsx b/packages/mui-base/src/Accordion/Section/AccordionSection.tsx new file mode 100644 index 000000000..7f0909c3a --- /dev/null +++ b/packages/mui-base/src/Accordion/Section/AccordionSection.tsx @@ -0,0 +1,128 @@ +'use client'; +import * as React from 'react'; +import { useListItem } from '@floating-ui/react'; +import { useForkRef } from '../../utils/useForkRef'; +import { BaseUIComponentProps } from '../../utils/types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { TransitionStatus } from '../../utils/useTransitionStatus'; +import { useCollapsibleRoot } from '../../Collapsible/Root/useCollapsibleRoot'; +import type { CollapsibleRoot } from '../../Collapsible/Root/CollapsibleRoot'; +import { CollapsibleContext } from '../../Collapsible/Root/CollapsibleContext'; +import type { AccordionRoot } from '../Root/AccordionRoot'; +import { useAccordionRootContext } from '../Root/AccordionRootContext'; +import { AccordionSectionContext } from './AccordionSectionContext'; +// import { useAccordionSection } from './useAccordionSection'; +import { accordionStyleHookMapping } from './styleHooks'; + +const AccordionSection = React.forwardRef(function AccordionSection( + props: AccordionSection.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, value: valueProp, ...otherProps } = props; + + const sectionRef = React.useRef(null); + const { ref: listItemRef, index } = useListItem(); + const mergedRef = useForkRef(forwardedRef, listItemRef, sectionRef); + + const { + animated, + disabled, + handleOpenChange, + ownerState: rootOwnerState, + value: openValues, + } = useAccordionRootContext(); + + const value = valueProp ?? index; + + const isOpen = React.useMemo(() => { + if (!openValues) { + return false; + } + + for (let i = 0; i < openValues.length; i += 1) { + if (openValues[i] === value) { + return true; + } + } + + return false; + }, [openValues, value]); + + const collapsible = useCollapsibleRoot({ + animated, + open: isOpen, + onOpenChange: (nextOpen) => handleOpenChange(value, nextOpen), + disabled, + }); + + const collapsibleOwnerState: CollapsibleRoot.OwnerState = React.useMemo( + () => ({ + open: collapsible.open, + disabled: collapsible.disabled, + transitionStatus: collapsible.transitionStatus, + }), + [collapsible.open, collapsible.disabled, collapsible.transitionStatus], + ); + + const collapsibleContext: CollapsibleRoot.Context = React.useMemo( + () => ({ + ...collapsible, + ownerState: collapsibleOwnerState, + }), + [collapsible, collapsibleOwnerState], + ); + + const ownerState: AccordionSection.OwnerState = React.useMemo( + () => ({ + ...rootOwnerState, + index, + open: isOpen, + transitionStatus: collapsible.transitionStatus, + }), + [collapsible.transitionStatus, index, isOpen, rootOwnerState], + ); + + const accordionSectionContext: AccordionSection.Context = React.useMemo( + () => ({ + open: isOpen, + ownerState, + }), + [isOpen, ownerState], + ); + + const { renderElement } = useComponentRenderer({ + render: render ?? 'div', + className, + ownerState, + ref: mergedRef, + extraProps: otherProps, + customStyleHookMapping: accordionStyleHookMapping, + }); + + return ( + + + {renderElement()} + + + ); +}); + +export { AccordionSection }; + +export namespace AccordionSection { + export interface Context { + open: boolean; + ownerState: OwnerState; + } + + export interface OwnerState extends AccordionRoot.OwnerState { + index: number; + open: boolean; + transitionStatus: TransitionStatus; + } + + export interface Props extends BaseUIComponentProps { + value: number | string; + } +} diff --git a/packages/mui-base/src/Accordion/Section/AccordionSectionContext.tsx b/packages/mui-base/src/Accordion/Section/AccordionSectionContext.tsx new file mode 100644 index 000000000..291a16a06 --- /dev/null +++ b/packages/mui-base/src/Accordion/Section/AccordionSectionContext.tsx @@ -0,0 +1,24 @@ +'use client'; +import * as React from 'react'; +import type { AccordionSection } from './AccordionSection'; + +/** + * @ignore - internal component. + */ +export const AccordionSectionContext = React.createContext( + undefined, +); + +if (process.env.NODE_ENV !== 'production') { + AccordionSectionContext.displayName = 'AccordionSectionContext'; +} + +export function useAccordionSectionContext() { + const context = React.useContext(AccordionSectionContext); + if (context === undefined) { + throw new Error( + 'useAccordionSectionContext must be used inside the component', + ); + } + return context; +} diff --git a/packages/mui-base/src/Accordion/Section/styleHooks.ts b/packages/mui-base/src/Accordion/Section/styleHooks.ts new file mode 100644 index 000000000..64e04c4cf --- /dev/null +++ b/packages/mui-base/src/Accordion/Section/styleHooks.ts @@ -0,0 +1,21 @@ +import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; +import type { AccordionSection } from './AccordionSection'; + +export const accordionStyleHookMapping: CustomStyleHookMapping = { + index: (value) => { + return Number.isInteger(value) ? { 'data-index': String(value) } : null; + }, + open: (value) => { + return value ? { 'data-state': 'open' } : { 'data-state': 'closed' }; + }, + transitionStatus: (value) => { + if (value === 'entering') { + return { 'data-entering': '' } as Record; + } + if (value === 'exiting') { + return { 'data-exiting': '' }; + } + return null; + }, + value: () => null, +}; diff --git a/packages/mui-base/src/Accordion/Trigger/AccordionTrigger.test.tsx b/packages/mui-base/src/Accordion/Trigger/AccordionTrigger.test.tsx new file mode 100644 index 000000000..71eb55479 --- /dev/null +++ b/packages/mui-base/src/Accordion/Trigger/AccordionTrigger.test.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +// import { expect } from 'chai'; +// import { spy } from 'sinon'; +import { createRenderer /* , act */ } from '@mui/internal-test-utils'; +import * as Accordion from '@base_ui/react/Accordion'; +import * as Collapsible from '@base_ui/react/Collapsible'; +import { describeConformance } from '../../../test/describeConformance'; + +const { AccordionRootContext, AccordionSectionContext } = Accordion; + +const { CollapsibleContext } = Collapsible; + +const accordionRootContextValue: Accordion.Root.Context = { + accordionSectionRefs: { current: [] }, + animated: false, + disabled: false, + handleOpenChange() {}, + ownerState: { + value: [0], + disabled: false, + }, + value: [0], +}; + +const accordionSectionContextValue: Accordion.Section.Context = { + open: true, + ownerState: { + value: [0], + disabled: false, + index: 0, + open: true, + transitionStatus: undefined, + }, +}; + +const collapsibleContextValue: Collapsible.Root.Context = { + animated: false, + contentId: ':content:', + disabled: false, + mounted: true, + open: true, + setContentId() {}, + setMounted() {}, + setOpen() {}, + transitionStatus: undefined, + ownerState: { + open: true, + disabled: false, + transitionStatus: undefined, + }, +}; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'button', + render: (node) => { + const { container, ...other } = render( + + + + {node} + + + , + ); + + return { container, ...other }; + }, + refInstanceof: window.HTMLButtonElement, + })); +}); diff --git a/packages/mui-base/src/Accordion/Trigger/AccordionTrigger.tsx b/packages/mui-base/src/Accordion/Trigger/AccordionTrigger.tsx new file mode 100644 index 000000000..c60dcacee --- /dev/null +++ b/packages/mui-base/src/Accordion/Trigger/AccordionTrigger.tsx @@ -0,0 +1,44 @@ +'use client'; +import * as React from 'react'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { BaseUIComponentProps } from '../../utils/types'; +import { useCollapsibleContext } from '../../Collapsible/Root/CollapsibleContext'; +import { useCollapsibleTrigger } from '../../Collapsible/Trigger/useCollapsibleTrigger'; +import type { AccordionSection } from '../Section/AccordionSection'; +import { useAccordionSectionContext } from '../Section/AccordionSectionContext'; +import { accordionStyleHookMapping } from '../Section/styleHooks'; + +const AccordionTrigger = React.forwardRef(function AccordionTrigger( + props: AccordionTrigger.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, ...otherProps } = props; + + const { contentId, open, setOpen } = useCollapsibleContext(); + + const { getRootProps } = useCollapsibleTrigger({ + contentId, + open, + setOpen, + }); + + const { ownerState } = useAccordionSectionContext(); + + const { renderElement } = useComponentRenderer({ + propGetter: getRootProps, + render: render ?? 'button', + ownerState, + className, + ref: forwardedRef, + extraProps: otherProps, + customStyleHookMapping: accordionStyleHookMapping, + }); + + return renderElement(); +}); + +export { AccordionTrigger }; + +namespace AccordionTrigger { + export interface Props extends BaseUIComponentProps<'button', AccordionSection.OwnerState> {} +} diff --git a/packages/mui-base/src/Accordion/index.barrel.ts b/packages/mui-base/src/Accordion/index.barrel.ts new file mode 100644 index 000000000..0d057e061 --- /dev/null +++ b/packages/mui-base/src/Accordion/index.barrel.ts @@ -0,0 +1,5 @@ +export * from './Root/AccordionRoot'; +export * from './Section/AccordionSection'; +export * from './Heading/AccordionHeading'; +export * from './Trigger/AccordionTrigger'; +export * from './Panel/AccordionPanel'; diff --git a/packages/mui-base/src/Accordion/index.ts b/packages/mui-base/src/Accordion/index.ts new file mode 100644 index 000000000..7a9efb6c1 --- /dev/null +++ b/packages/mui-base/src/Accordion/index.ts @@ -0,0 +1,13 @@ +export { AccordionRoot as Root } from './Root/AccordionRoot'; +export { useAccordionRoot } from './Root/useAccordionRoot'; +export { AccordionRootContext, useAccordionRootContext } from './Root/AccordionRootContext'; + +export { AccordionSection as Section } from './Section/AccordionSection'; +export { + AccordionSectionContext, + useAccordionSectionContext, +} from './Section/AccordionSectionContext'; + +export { AccordionHeading as Heading } from './Heading/AccordionHeading'; +export { AccordionTrigger as Trigger } from './Trigger/AccordionTrigger'; +export { AccordionPanel as Panel } from './Panel/AccordionPanel'; diff --git a/packages/mui-base/src/index.ts b/packages/mui-base/src/index.ts index 7f29893f0..24c3e0168 100644 --- a/packages/mui-base/src/index.ts +++ b/packages/mui-base/src/index.ts @@ -1,3 +1,4 @@ +export * from './Accordion/index.barrel'; export * from './AlertDialog/index.barrel'; export * from './Checkbox/index.barrel'; export * from './Dialog/index.barrel'; diff --git a/packages/mui-base/src/utils/defaultRenderFunctions.tsx b/packages/mui-base/src/utils/defaultRenderFunctions.tsx index 4fd8e3ef4..5bcd3fbc9 100644 --- a/packages/mui-base/src/utils/defaultRenderFunctions.tsx +++ b/packages/mui-base/src/utils/defaultRenderFunctions.tsx @@ -11,6 +11,10 @@ export const defaultRenderFunctions = { // eslint-disable-next-line jsx-a11y/heading-has-content return

; }, + h3: (props: React.ComponentPropsWithRef<'h3'>) => { + // eslint-disable-next-line jsx-a11y/heading-has-content + return

; + }, output: (props: React.ComponentPropsWithRef<'output'>) => { return ; },