Skip to content

Commit

Permalink
WIP - New Accordion hooks and components
Browse files Browse the repository at this point in the history
  • Loading branch information
mj12albert committed Sep 3, 2024
1 parent c2b4c77 commit b01bc68
Show file tree
Hide file tree
Showing 20 changed files with 948 additions and 0 deletions.
99 changes: 99 additions & 0 deletions docs/pages/experiments/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as React from 'react';
import * as Accordion from '@base_ui/react/Accordion';

export default function App() {
const [val, setVal] = React.useState(['one']);
return (
<div className="App AccordionDemo">
<h2>Plain HTML Accordion (no JS)</h2>
<div role="region" className="MyAccordion-root" aria-label="My Accordion Component">
<div className="MyAccordion-section">
<h3 className="MyAccordion-heading">
<button
type="button"
aria-controls="Panel1"
aria-expanded="true"
className="MyAccordion-trigger"
id="Trigger1"
>
Panel 1
</button>
</h3>
<div id="Panel1" className="MyAccordion-panel" role="region" aria-labelledby="Trigger1">
This the contents of Panel 1
</div>
</div>

<div className="MyAccordion-section">
<h3 className="MyAccordion-heading">
<button
type="button"
aria-controls="Panel2"
aria-expanded="false"
className="MyAccordion-trigger"
id="Trigger2"
>
Panel 2
</button>
</h3>
<div id="Panel2" className="MyAccordion-panel" role="region" aria-labelledby="Trigger2">
This the contents of Panel 2
</div>
</div>
</div>

<hr style={{ marginTop: 32, marginBottom: 32 }} role="presentation" />

<h2>Base UI Uncontrolled</h2>

<Accordion.Root className="MyAccordion-root" aria-label="Base UI Uncontrolled Accordion">
<Accordion.Section className="MyAccordion-section">
<Accordion.Heading className="MyAccordion-heading">
<Accordion.Trigger className="MyAccordion-trigger">Trigger 1</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel className="MyAccordion-panel">
This is the contents of Accordion.Panel 1
</Accordion.Panel>
</Accordion.Section>

<Accordion.Section className="MyAccordion-section">
<Accordion.Heading className="MyAccordion-heading">
<Accordion.Trigger className="MyAccordion-trigger">Trigger 2</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel className="MyAccordion-panel">
This is the contents of Accordion.Panel 2
</Accordion.Panel>
</Accordion.Section>
</Accordion.Root>

<hr style={{ marginTop: 32, marginBottom: 32 }} role="presentation" />

<h2>Base UI Controlled</h2>

<Accordion.Root
className="MyAccordion-root"
value={val}
onOpenChange={setVal}
aria-label="Base UI Controlled Accordion"
>
<Accordion.Section className="MyAccordion-section" value="one">
<Accordion.Heading className="MyAccordion-heading">
<Accordion.Trigger className="MyAccordion-trigger">Trigger 1</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel className="MyAccordion-panel">
This is the contents of Accordion.Panel 1, the value is &quot;one&quot;
</Accordion.Panel>
</Accordion.Section>

<Accordion.Section className="MyAccordion-section" value="two">
<Accordion.Heading className="MyAccordion-heading">
<Accordion.Trigger className="MyAccordion-trigger">Trigger 2</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel className="MyAccordion-panel">
This is the contents of Accordion.Panel 2, the value is &quot;two&quot;
</Accordion.Panel>
</Accordion.Section>
</Accordion.Root>
</div>
);
}
73 changes: 73 additions & 0 deletions packages/mui-base/src/Accordion/Heading/AccordionHeading.test.tsx
Original file line number Diff line number Diff line change
@@ -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('<Accordion.Heading />', () => {
const { render } = createRenderer();

describeConformance(<Accordion.Heading />, () => ({
inheritComponent: 'h3',
render: (node) => {
const { container, ...other } = render(
<AccordionRootContext.Provider value={accordionRootContextValue}>
<CollapsibleContext.Provider value={collapsibleContextValue}>
<AccordionSectionContext.Provider value={accordionSectionContextValue}>
{node}
</AccordionSectionContext.Provider>
</CollapsibleContext.Provider>
</AccordionRootContext.Provider>,
);

return { container, ...other };
},
refInstanceof: window.HTMLHeadingElement,
}));
});
33 changes: 33 additions & 0 deletions packages/mui-base/src/Accordion/Heading/AccordionHeading.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLHeadingElement>,
) {
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> {}
}
73 changes: 73 additions & 0 deletions packages/mui-base/src/Accordion/Panel/AccordionPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -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('<Accordion.Panel />', () => {
const { render } = createRenderer();

describeConformance(<Accordion.Panel />, () => ({
inheritComponent: 'div',
render: (node) => {
const { container, ...other } = render(
<AccordionRootContext.Provider value={accordionRootContextValue}>
<CollapsibleContext.Provider value={collapsibleContextValue}>
<AccordionSectionContext.Provider value={accordionSectionContextValue}>
{node}
</AccordionSectionContext.Provider>
</CollapsibleContext.Provider>
</AccordionRootContext.Provider>,
);

return { container, ...other };
},
refInstanceof: window.HTMLDivElement,
}));
});
57 changes: 57 additions & 0 deletions packages/mui-base/src/Accordion/Panel/AccordionPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'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<HTMLButtonElement>,
) {
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, triggerId } = useAccordionSectionContext();

const { renderElement } = useComponentRenderer({
propGetter: getRootProps,
render: render ?? 'div',
ownerState,
className,
extraProps: {
...otherProps,
'aria-labelledby': triggerId,
role: 'region',
style: {
'--accordion-content-height': height ? `${height}px` : undefined,
},
},
customStyleHookMapping: accordionStyleHookMapping,
});

return renderElement();
});

export namespace AccordionPanel {
export interface Props
extends BaseUIComponentProps<'div', AccordionSection.OwnerState>,
Pick<useCollapsibleContent.Parameters, 'htmlHidden'> {}
}
16 changes: 16 additions & 0 deletions packages/mui-base/src/Accordion/Root/AccordionRoot.test.tsx
Original file line number Diff line number Diff line change
@@ -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('<Accordion.Root />', () => {
const { render } = createRenderer();

describeConformance(<Accordion.Root />, () => ({
inheritComponent: 'div',
render,
refInstanceof: window.HTMLDivElement,
}));
});
Loading

0 comments on commit b01bc68

Please sign in to comment.