Skip to content

Commit

Permalink
[Collapse] Add orientation and horizontal support
Browse files Browse the repository at this point in the history
* BREAKING CHANGE: Remove collapsedHeight property - use collapsedSize

(#10051)
  • Loading branch information
darkowic committed Jun 26, 2020
1 parent 2ed1144 commit e8f6272
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 40 deletions.
6 changes: 4 additions & 2 deletions docs/pages/api-docs/collapse.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ The `MuiCollapse` name can be used for providing [default props](/customization/
|:-----|:-----|:--------|:------------|
| <span class="prop-name">children</span> | <span class="prop-type">node</span> | | The content node to be collapsed. |
| <span class="prop-name">classes</span> | <span class="prop-type">object</span> | | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. |
| <span class="prop-name">collapsedHeight</span> | <span class="prop-type">number<br>&#124;&nbsp;string</span> | <span class="prop-default">'0px'</span> | The height of the container when collapsed. |
| <span class="prop-name">collapsedSize</span> | <span class="prop-type">number<br>&#124;&nbsp;string</span> | <span class="prop-default">'0px'</span> | The width (horizontal) or height (vertical) of the container when collapsed. |
| <span class="prop-name">component</span> | <span class="prop-type">elementType</span> | <span class="prop-default">'div'</span> | The component used for the root node. Either a string to use a HTML element or a component. |
| <span class="prop-name">disableStrictModeCompat</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | Enable this prop if you encounter 'Function components cannot be given refs', use `unstable_createStrictModeTheme`, and can't forward the ref in the passed `Component`. |
| <span class="prop-name">in</span> | <span class="prop-type">bool</span> | | If `true`, the component will transition in. |
| <span class="prop-name">orientation</span> | <span class="prop-type">'horizontal'<br>&#124;&nbsp;'vertical'</span> | <span class="prop-default">'vertical'</span> | The collapse transition orientation. |
| <span class="prop-name">timeout</span> | <span class="prop-type">'auto'<br>&#124;&nbsp;number<br>&#124;&nbsp;{ appear?: number, enter?: number, exit?: number }</span> | <span class="prop-default">duration.standard</span> | The duration for the transition, in milliseconds. You may specify a single timeout for all transitions, or individually with an object.<br>Set to 'auto' to automatically calculate transition time based on height. |

The `ref` is forwarded to the root element.
Expand All @@ -47,8 +48,9 @@ Any other props supplied will be provided to the root element ([Transition](http
| Rule name | Global class | Description |
|:-----|:-------------|:------------|
| <span class="prop-name">container</span> | <span class="prop-name">.MuiCollapse-container</span> | Styles applied to the container element.
| <span class="prop-name">horizontal</span> | <span class="prop-name">.MuiCollapse-horizontal</span> | Pseudo-class applied to the root element if `orientation="horizontal"`.
| <span class="prop-name">entered</span> | <span class="prop-name">.MuiCollapse-entered</span> | Styles applied to the container element when the transition has entered.
| <span class="prop-name">hidden</span> | <span class="prop-name">.MuiCollapse-hidden</span> | Styles applied to the container element when the transition has exited and `collapsedHeight` != 0px.
| <span class="prop-name">hidden</span> | <span class="prop-name">.MuiCollapse-hidden</span> | Styles applied to the container element when the transition has exited and `collapsedSize` != 0px.
| <span class="prop-name">wrapper</span> | <span class="prop-name">.MuiCollapse-wrapper</span> | Styles applied to the outer wrapper element.
| <span class="prop-name">wrapperInner</span> | <span class="prop-name">.MuiCollapse-wrapperInner</span> | Styles applied to the inner wrapper element.

Expand Down
29 changes: 27 additions & 2 deletions docs/src/pages/components/transitions/SimpleCollapse.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import FormControlLabel from '@material-ui/core/FormControlLabel';

const useStyles = makeStyles((theme) => ({
root: {
height: 180,
height: 300,
},
container: {
display: 'flex',
justifyContent: 'space-between',
height: 120,
width: 250,
},
paper: {
margin: theme.spacing(1),
Expand Down Expand Up @@ -51,7 +54,29 @@ export default function SimpleCollapse() {
</svg>
</Paper>
</Collapse>
<Collapse in={checked} collapsedHeight={40}>
<Collapse in={checked} collapsedSize={40}>
<Paper elevation={4} className={classes.paper}>
<svg className={classes.svg}>
<polygon
points="0,100 50,00, 100,100"
className={classes.polygon}
/>
</svg>
</Paper>
</Collapse>
</div>
<div className={classes.container}>
<Collapse orientation="horizontal" in={checked}>
<Paper elevation={4} className={classes.paper}>
<svg className={classes.svg}>
<polygon
points="0,100 50,00, 100,100"
className={classes.polygon}
/>
</svg>
</Paper>
</Collapse>
<Collapse orientation="horizontal" in={checked} collapsedSize={40}>
<Paper elevation={4} className={classes.paper}>
<svg className={classes.svg}>
<polygon
Expand Down
29 changes: 27 additions & 2 deletions docs/src/pages/components/transitions/SimpleCollapse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import FormControlLabel from '@material-ui/core/FormControlLabel';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
height: 180,
height: 300,
},
container: {
display: 'flex',
justifyContent: 'space-between',
height: 120,
width: 250,
},
paper: {
margin: theme.spacing(1),
Expand Down Expand Up @@ -53,7 +56,29 @@ export default function SimpleCollapse() {
</svg>
</Paper>
</Collapse>
<Collapse in={checked} collapsedHeight={40}>
<Collapse in={checked} collapsedSize={40}>
<Paper elevation={4} className={classes.paper}>
<svg className={classes.svg}>
<polygon
points="0,100 50,00, 100,100"
className={classes.polygon}
/>
</svg>
</Paper>
</Collapse>
</div>
<div className={classes.container}>
<Collapse orientation="horizontal" in={checked}>
<Paper elevation={4} className={classes.paper}>
<svg className={classes.svg}>
<polygon
points="0,100 50,00, 100,100"
className={classes.polygon}
/>
</svg>
</Paper>
</Collapse>
<Collapse orientation="horizontal" in={checked} collapsedSize={40}>
<Paper elevation={4} className={classes.paper}>
<svg className={classes.svg}>
<polygon
Expand Down
5 changes: 3 additions & 2 deletions docs/src/pages/components/transitions/transitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ export default Main() {

## Collapse

Expand vertically from the top of the child element.
The `collapsedHeight` property can be used to set the minimum height when not expanded.
Expand from the start edge of the child element.
Use the `orientation` prop if you need a horizontal collapse.
The `collapsedSize` prop can be used to set the minimum width/height when not expanded.

{{"demo": "pages/components/transitions/SimpleCollapse.js", "bg": true}}

Expand Down
8 changes: 6 additions & 2 deletions packages/material-ui/src/Collapse/Collapse.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ export interface CollapseProps extends StandardProps<TransitionProps, CollapseCl
*/
children?: React.ReactNode;
/**
* The height of the container when collapsed.
* The width (horizontal) or height (vertical) of the container when collapsed.
*/
collapsedHeight?: string | number;
collapsedSize?: string | number;
/**
* The component used for the root node.
* Either a string to use a HTML element or a component.
Expand All @@ -27,6 +27,10 @@ export interface CollapseProps extends StandardProps<TransitionProps, CollapseCl
* If `true`, the component will transition in.
*/
in?: boolean;
/**
* The collapse transition orientation.
*/
orientation?: 'horizontal' | 'vertical';
/**
* The duration for the transition, in milliseconds.
* You may specify a single timeout for all transitions, or individually with an object.
Expand Down
96 changes: 74 additions & 22 deletions packages/material-ui/src/Collapse/Collapse.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,48 @@ import { useForkRef } from '../utils';
export const styles = (theme) => ({
/* Styles applied to the container element. */
container: {
height: 0,
position: 'relative',
overflow: 'hidden',
height: 0,
transition: theme.transitions.create('height'),
'&$horizontal': {
width: 0,
height: 'auto',
transition: theme.transitions.create('width'),
},
},
/* Pseudo-class applied to the root element if `orientation="horizontal"`. */
horizontal: {},
/* Styles applied to the container element when the transition has entered. */
entered: {
height: 'auto',
overflow: 'visible',
'&$horizontal': {
height: 'initial',
width: 'auto',
},
},
/* Styles applied to the container element when the transition has exited and `collapsedHeight` != 0px. */
/* Styles applied to the container element when the transition has exited and `collapsedSize` != 0px. */
hidden: {
visibility: 'hidden',
},
/* Styles applied to the outer wrapper element. */
wrapper: {
// Hack to get children with a negative margin to not falsify the height computation.
display: 'flex',
width: '100%',
'&$horizontal': {
width: 'initial',
height: '100%',
},
},
/* Styles applied to the inner wrapper element. */
wrapperInner: {
width: '100%',
'&$horizontal': {
width: 'initial',
height: '100%',
},
},
});

Expand All @@ -45,7 +66,7 @@ const Collapse = React.forwardRef(function Collapse(props, ref) {
children,
classes,
className,
collapsedHeight: collapsedHeightProp = '0px',
collapsedSize: collapsedSizeProp = '0px',
component: Component = 'div',
disableStrictModeCompat = false,
in: inProp,
Expand All @@ -55,6 +76,7 @@ const Collapse = React.forwardRef(function Collapse(props, ref) {
onExit,
onExited,
onExiting,
orientation = 'vertical',
style,
timeout = duration.standard,
// eslint-disable-next-line react/prop-types
Expand All @@ -65,8 +87,10 @@ const Collapse = React.forwardRef(function Collapse(props, ref) {
const timer = React.useRef();
const wrapperRef = React.useRef(null);
const autoTransitionDuration = React.useRef();
const collapsedHeight =
typeof collapsedHeightProp === 'number' ? `${collapsedHeightProp}px` : collapsedHeightProp;
const collapsedSize =
typeof collapsedSizeProp === 'number' ? `${collapsedSizeProp}px` : collapsedSizeProp;
const isHorizontal = orientation === 'horizontal';
const size = isHorizontal ? 'width' : 'height';

React.useEffect(() => {
return () => {
Expand All @@ -93,16 +117,28 @@ const Collapse = React.forwardRef(function Collapse(props, ref) {
}
};

const getWrapperSize = () =>
wrapperRef.current ? wrapperRef.current[isHorizontal ? 'clientWidth' : 'clientHeight'] : 0;

const handleEnter = normalizedTransitionCallback((node, isAppearing) => {
node.style.height = collapsedHeight;
if (wrapperRef.current) {
// Set absolute position to get the size of collapsed content
wrapperRef.current.style.position = 'absolute';
}
node.style[size] = collapsedSize;

if (onEnter) {
onEnter(node, isAppearing);
}
});

const handleEntering = normalizedTransitionCallback((node, isAppearing) => {
const wrapperHeight = wrapperRef.current ? wrapperRef.current.clientHeight : 0;
const wrapperSize = getWrapperSize();

if (wrapperRef.current) {
// After the size is read reset the position back to default
wrapperRef.current.style.position = '';
}

const { duration: transitionDuration } = getTransitionProps(
{ style, timeout },
Expand All @@ -112,32 +148,31 @@ const Collapse = React.forwardRef(function Collapse(props, ref) {
);

if (timeout === 'auto') {
const duration2 = theme.transitions.getAutoHeightDuration(wrapperHeight);
const duration2 = theme.transitions.getAutoHeightDuration(wrapperSize);
node.style.transitionDuration = `${duration2}ms`;
autoTransitionDuration.current = duration2;
} else {
node.style.transitionDuration =
typeof transitionDuration === 'string' ? transitionDuration : `${transitionDuration}ms`;
}

node.style.height = `${wrapperHeight}px`;
node.style[size] = `${wrapperSize}px`;

if (onEntering) {
onEntering(node, isAppearing);
}
});

const handleEntered = normalizedTransitionCallback((node, isAppearing) => {
node.style.height = 'auto';
node.style[size] = 'auto';

if (onEntered) {
onEntered(node, isAppearing);
}
});

const handleExit = normalizedTransitionCallback((node) => {
const wrapperHeight = wrapperRef.current ? wrapperRef.current.clientHeight : 0;
node.style.height = `${wrapperHeight}px`;
node.style[size] = `${getWrapperSize()}px`;

if (onExit) {
onExit(node);
Expand All @@ -147,8 +182,7 @@ const Collapse = React.forwardRef(function Collapse(props, ref) {
const handleExited = normalizedTransitionCallback(onExited);

const handleExiting = normalizedTransitionCallback((node) => {
const wrapperHeight = wrapperRef.current ? wrapperRef.current.clientHeight : 0;

const wrapperSize = getWrapperSize();
const { duration: transitionDuration } = getTransitionProps(
{ style, timeout },
{
Expand All @@ -157,15 +191,17 @@ const Collapse = React.forwardRef(function Collapse(props, ref) {
);

if (timeout === 'auto') {
const duration2 = theme.transitions.getAutoHeightDuration(wrapperHeight);
// TODO: rename getAutoHeightDuration to something more generic (width support)
// Actually it just calculates animation duration based on size
const duration2 = theme.transitions.getAutoHeightDuration(wrapperSize);
node.style.transitionDuration = `${duration2}ms`;
autoTransitionDuration.current = duration2;
} else {
node.style.transitionDuration =
typeof transitionDuration === 'string' ? transitionDuration : `${transitionDuration}ms`;
}

node.style.height = collapsedHeight;
node.style[size] = collapsedSize;

if (onExiting) {
onExiting(node);
Expand Down Expand Up @@ -198,20 +234,32 @@ const Collapse = React.forwardRef(function Collapse(props, ref) {
className={clsx(
classes.container,
{
[classes.horizontal]: isHorizontal,
[classes.entered]: state === 'entered',
[classes.hidden]: state === 'exited' && !inProp && collapsedHeight === '0px',
[classes.hidden]: state === 'exited' && !inProp && collapsedSize === '0px',
},
className,
)}
style={{
minHeight: collapsedHeight,
[isHorizontal ? 'minWidth' : 'minHeight']: collapsedSize,
...style,
}}
ref={handleRef}
{...childProps}
>
<div className={classes.wrapper} ref={wrapperRef}>
<div className={classes.wrapperInner}>{children}</div>
<div
className={clsx(classes.wrapper, {
[classes.horizontal]: isHorizontal,
})}
ref={wrapperRef}
>
<div
className={clsx(classes.wrapperInner, {
[classes.horizontal]: isHorizontal,
})}
>
{children}
</div>
</div>
</Component>
)}
Expand All @@ -238,9 +286,9 @@ Collapse.propTypes = {
*/
className: PropTypes.string,
/**
* The height of the container when collapsed.
* The width (horizontal) or height (vertical) of the container when collapsed.
*/
collapsedHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
collapsedSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/**
* The component used for the root node.
* Either a string to use a HTML element or a component.
Expand Down Expand Up @@ -280,6 +328,10 @@ Collapse.propTypes = {
* @ignore
*/
onExiting: PropTypes.func,
/**
* The collapse transition orientation.
*/
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
/**
* @ignore
*/
Expand Down
Loading

0 comments on commit e8f6272

Please sign in to comment.