Skip to content

Commit

Permalink
Root subcomponent accepts optional render prop
Browse files Browse the repository at this point in the history
  • Loading branch information
mj12albert committed Aug 22, 2024
1 parent 3d2af1a commit c5289c7
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 23 deletions.
8 changes: 4 additions & 4 deletions docs/data/base/components/collapsible/collapsible.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import * as Collapsible from '@base_ui/react/Collapsible';

## Anatomy

- `<Collapsible.Root />` is a top-level component that facilitates communication between other components. It does not render to the DOM.
- `<Collapsible.Root />` is a top-level component that facilitates communication between other components. It does not render to the DOM by default.
- `<Collapsible.Trigger />` is the trigger element, a `<button>` by default, that toggles the open/closed state of the content
- `<Collapsible.Content />` is component that contains the Collapsible's content

Expand Down Expand Up @@ -207,14 +207,14 @@ function App() {
## Overriding default components
Use the `render` prop to override the rendered elements with your own components.
Use the `render` prop to override the rendered elements with your own components. The `Collapsible.Root` component does not render an element to the DOM by default, but can do so with the render prop:
```jsx
// Element shorthand
<Collapsible.Content render={<MyCollapsibleContent />} />
<Collapsible.Root render={<MyCollapsibleRoot />} />
```
```jsx
// Function
<Collapsible.Content render={(props) => <MyCollapsibleContent {...props} />} />
<Collapsible.Root render={(props) => <MyCollapsibleRoot {...props} />} />
```
7 changes: 5 additions & 2 deletions docs/pages/base-ui/api/collapsible-root.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
{
"props": {
"animated": { "type": { "name": "bool" }, "default": "true" },
"className": { "type": { "name": "union", "description": "func<br>&#124;&nbsp;string" } },
"defaultOpen": { "type": { "name": "bool" }, "default": "true" },
"disabled": { "type": { "name": "bool" }, "default": "false" },
"onOpenChange": { "type": { "name": "func" } },
"open": { "type": { "name": "bool" } }
"open": { "type": { "name": "bool" } },
"render": { "type": { "name": "union", "description": "element<br>&#124;&nbsp;func" } }
},
"name": "CollapsibleRoot",
"imports": [
"import * as Collapsible from '@base_ui/react/Collapsible';\nconst CollapsibleRoot = Collapsible.Root;"
],
"classes": [],
"spread": true,
"themeDefaultProps": null,
"themeDefaultProps": true,
"muiName": "CollapsibleRoot",
"forwardsRefTo": "HTMLDivElement",
"filename": "/packages/mui-base/src/Collapsible/Root/CollapsibleRoot.tsx",
"inheritance": null,
"demos": "<ul><li><a href=\"/base-ui/react-collapsible/\">Collapsible</a></li></ul>",
Expand Down
17 changes: 17 additions & 0 deletions docs/pages/experiments/collapsible.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ export default function CollapsibleDemo() {
</Collapsible.Content>
</Collapsible.Root>
</div>

<Collapsible.Root render="span" className="MyCollapsible-root">
<Collapsible.Trigger className="MyCollapsible-trigger">
<span className="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" focusable="false">
<path d="M70.3 13.8L40 66.3 9.7 13.8z" />
</svg>
</span>
Trigger (root renders a span + CSS transition)
</Collapsible.Trigger>
<Collapsible.Content className="MyCollapsible-content csstransition">
<p>This is the collapsed content</p>
<p>This component is animated with CSS transitions</p>
<p>demo: https://codepen.io/aardrian/pen/QWjBNQG</p>
<p>https://adrianroselli.com/2020/05/disclosure-widgets.html</p>
</Collapsible.Content>
</Collapsible.Root>
<Styles />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
"animated": {
"description": "If <code>true</code>, the component supports CSS/JS-based animations and transitions."
},
"className": {
"description": "Class names applied to the element or a function that returns them based on the component&#39;s state."
},
"defaultOpen": {
"description": "If <code>true</code>, the Collapsible is initially open. This is the uncontrolled counterpart of <code>open</code>."
},
"disabled": { "description": "If <code>true</code>, the component is disabled." },
"onOpenChange": { "description": "Callback fired when the Collapsible is opened or closed." },
"open": {
"description": "If <code>true</code>, the Collapsible is initially open. This is the controlled counterpart of <code>defaultOpen</code>."
}
},
"render": { "description": "A function to customize rendering of the component." }
},
"classDescriptions": {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ import { expect } from 'chai';
import { spy } from 'sinon';
import { createRenderer, act } from '@mui/internal-test-utils';
import * as Collapsible from '@base_ui/react/Collapsible';
import { describeConformance } from '../../../test/describeConformance';

describe('<Collapsible.Root />', () => {
const { render } = createRenderer();

// `render` is explicitly specified here because Collapsible.Root does not
// render an element to the DOM by default and so the conformance tests would be unapplicable
describeConformance(<Collapsible.Root render="div" />, () => ({
inheritComponent: 'div',
render,
refInstanceof: window.HTMLDivElement,
}));

describe('ARIA attributes', () => {
it('sets ARIA attributes', async () => {
const { getByTestId, getByRole } = await render(
Expand Down
76 changes: 61 additions & 15 deletions packages/mui-base/src/Collapsible/Root/CollapsibleRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import { BaseUIComponentProps } from '../../utils/types';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
import { useCollapsibleRoot } from './useCollapsibleRoot';
import { CollapsibleContext } from './CollapsibleContext';
import { collapsibleStyleHookMapping } from './styleHooks';

export function CollapsibleRoot(props: CollapsibleRoot.Props) {
const { animated, open, defaultOpen, onOpenChange, disabled, children } = props;
const CollapsibleRoot = React.forwardRef(function CollapsibleRoot(
props: CollapsibleRoot.Props,
forwardedRef: React.ForwardedRef<HTMLDivElement>,
) {
const {
animated,
children,
className,
defaultOpen,
disabled,
onOpenChange,
open,
render: renderProp,
...otherProps
} = props;

const collapsible = useCollapsibleRoot({
animated,
Expand All @@ -15,20 +31,46 @@ export function CollapsibleRoot(props: CollapsibleRoot.Props) {
disabled,
});

const ownerState: CollapsibleRoot.OwnerState = React.useMemo(
() => ({
open: collapsible.open,
disabled: collapsible.disabled,
transitionStatus: collapsible.transitionStatus,
}),
[collapsible.open, collapsible.disabled, collapsible.transitionStatus],
);

const contextValue: CollapsibleRoot.Context = React.useMemo(
() => ({
...collapsible,
ownerState: {
open: collapsible.open,
disabled: collapsible.disabled,
transitionStatus: collapsible.transitionStatus,
},
ownerState,
}),
[collapsible],
[collapsible, ownerState],
);

return <CollapsibleContext.Provider value={contextValue}>{children}</CollapsibleContext.Provider>;
}
const { renderElement } = useComponentRenderer({
render: renderProp ?? 'div',
className,
ownerState,
ref: forwardedRef,
extraProps: { children, ...otherProps },
customStyleHookMapping: collapsibleStyleHookMapping,
});

if (!renderProp) {
return (
<CollapsibleContext.Provider value={contextValue}>{children}</CollapsibleContext.Provider>
);
}

return (
<CollapsibleContext.Provider value={contextValue}>
{renderElement()}
</CollapsibleContext.Provider>
);
});

export { CollapsibleRoot };

export namespace CollapsibleRoot {
export interface Context extends useCollapsibleRoot.ReturnValue {
Expand All @@ -38,9 +80,9 @@ export namespace CollapsibleRoot {
export interface OwnerState
extends Pick<useCollapsibleRoot.ReturnValue, 'open' | 'disabled' | 'transitionStatus'> {}

export interface Props extends useCollapsibleRoot.Parameters {
children: React.ReactNode;
}
export interface Props
extends useCollapsibleRoot.Parameters,
BaseUIComponentProps<any, OwnerState> {}
}

CollapsibleRoot.propTypes /* remove-proptypes */ = {
Expand All @@ -54,9 +96,9 @@ CollapsibleRoot.propTypes /* remove-proptypes */ = {
*/
animated: PropTypes.bool,
/**
* @ignore
* Class names applied to the element or a function that returns them based on the component's state.
*/
children: PropTypes.node,
className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
/**
* If `true`, the Collapsible is initially open.
* This is the uncontrolled counterpart of `open`.
Expand All @@ -77,4 +119,8 @@ CollapsibleRoot.propTypes /* remove-proptypes */ = {
* This is the controlled counterpart of `defaultOpen`.
*/
open: PropTypes.bool,
/**
* A function to customize rendering of the component.
*/
render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
} as any;
2 changes: 1 addition & 1 deletion packages/mui-base/src/Collapsible/Root/styleHooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps';
import { CollapsibleRoot } from './CollapsibleRoot';
import type { CollapsibleRoot } from './CollapsibleRoot';

export const collapsibleStyleHookMapping: CustomStyleHookMapping<CollapsibleRoot.OwnerState> = {
open: (value) => {
Expand Down

0 comments on commit c5289c7

Please sign in to comment.