Skip to content

Commit

Permalink
Fix animated multi-select width bug
Browse files Browse the repository at this point in the history
  • Loading branch information
lukebennett88 committed Oct 13, 2022
1 parent 0d4d17a commit b89fbc8
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 93 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-clocks-trade.md
@@ -0,0 +1,5 @@
---
'react-select': patch
---

Fix bug with animated multi-value select width being too wide
170 changes: 77 additions & 93 deletions packages/react-select/src/animated/transitions.tsx
@@ -1,12 +1,5 @@
import * as React from 'react';
import {
Component,
ComponentType,
createRef,
CSSProperties,
ReactNode,
useRef,
} from 'react';
import { ComponentType, CSSProperties, ReactNode, useRef } from 'react';
import { Transition } from 'react-transition-group';
import {
ExitHandler,
Expand Down Expand Up @@ -67,98 +60,89 @@ export const Fade = <ComponentProps extends {}>({
export const collapseDuration = 260;

type Width = number | 'auto';
interface CollapseProps {
children: ReactNode;
in?: boolean;
onExited?: ExitHandler<undefined | HTMLElement>;
}
interface CollapseState {
width: Width;
}

// wrap each MultiValue with a collapse transition; decreases width until
// finally removing from DOM
export class Collapse extends Component<CollapseProps, CollapseState> {
duration = collapseDuration;
rafID?: number | null;
state: CollapseState = { width: 'auto' };
transition: { [K in TransitionStatus]?: CSSProperties } = {
exiting: { width: 0, transition: `width ${this.duration}ms ease-out` },
exited: { width: 0 },
};
nodeRef = createRef<HTMLDivElement>();

componentDidMount() {
const { current: ref } = this.nodeRef;

/*
A check on existence of ref should not be necessary at this point,
but TypeScript demands it.
*/
if (ref) {
/*
Here we're invoking requestAnimationFrame with a callback invoking our
call to getBoundingClientRect and setState in order to resolve an edge case
around portalling. Certain portalling solutions briefly remove children from the DOM
before appending them to the target node. This is to avoid us trying to call getBoundingClientrect
while the Select component is in this state.
*/
// cannot use `offsetWidth` because it is rounded
this.rafID = window.requestAnimationFrame(() => {
const { width } = ref.getBoundingClientRect();
this.setState({ width });
});
}
}

componentWillUnmount() {
if (this.rafID) {
window.cancelAnimationFrame(this.rafID);
}
}

// get base styles
getStyle = (width: Width): CSSProperties => ({
/** Get base styles. */
function getBaseStyles(width: Width): CSSProperties {
return {
overflow: 'hidden',
whiteSpace: 'nowrap',
width,
});
};
}

const transition: { [K in TransitionStatus]?: CSSProperties } = {
exiting: { width: 0, transition: `width ${collapseDuration}ms ease-out` },
exited: { width: 0 },
};

/** Get styles based on the transition state. */
function getTransitionStyles(state: TransitionStatus) {
return transition[state];
}

// get transition styles
getTransition = (state: TransitionStatus) => this.transition[state];
interface CollapseProps {
/** The children to be rendered. */
children: ReactNode;
/** Show the component; triggers the enter or exit states. */
in?: boolean;
/** Callback fired after the "exited" status is applied. */
onExited?: ExitHandler<undefined | HTMLElement>;
}

render() {
const { children, in: inProp, onExited } = this.props;
const exitedProp = () => {
if (this.nodeRef.current && onExited) {
onExited(this.nodeRef.current);
/**
* Wrap each MultiValue with a collapse transition; decreases width until
* finally removing from DOM.
*/
export function Collapse({ children, in: inProp, onExited }: CollapseProps) {
const [width, setWidth] = React.useState<Width>('auto');
const nodeRef = useRef<HTMLDivElement>(null);

React.useEffect(() => {
/**
* Here we're invoking requestAnimationFrame with a callback invoking our
* call to getBoundingClientRect and setWidth in order to resolve an
* edge-case around portalling.
* Certain portalling solutions briefly remove children from the DOM before
* appending them to the target node. This is to avoid us trying to call
* getBoundingClientRect while the Select component is in this state.
*
* NOTE: we cannot use offsetWidth here because it is rounded.
*/
const id = window.requestAnimationFrame(() => {
if (nodeRef.current) {
setWidth(nodeRef.current.getBoundingClientRect().width);
}
};
});
return () => window.cancelAnimationFrame(id);
}, []);

const { width } = this.state;
const exitedProp = React.useCallback(() => {
if (nodeRef.current && onExited) {
onExited(nodeRef.current);
}
}, [onExited]);

return (
<Transition
enter={false}
mountOnEnter
unmountOnExit
in={inProp}
onExited={exitedProp}
timeout={this.duration}
nodeRef={this.nodeRef}
>
{(state) => {
const style = {
...this.getStyle(width),
...this.getTransition(state),
};
return (
<div ref={this.nodeRef} style={style}>
{children}
</div>
);
}}
</Transition>
);
}
return (
<Transition
enter={false}
mountOnEnter
unmountOnExit
in={inProp}
onExited={exitedProp}
timeout={collapseDuration}
nodeRef={nodeRef}
>
{(state) => {
const style = {
...getBaseStyles(width),
...getTransitionStyles(state),
};
return (
<div ref={nodeRef} style={style}>
{children}
</div>
);
}}
</Transition>
);
}

0 comments on commit b89fbc8

Please sign in to comment.