Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes for animated value container and unique keys for options #4860

5 changes: 5 additions & 0 deletions .changeset/silver-zebras-sing.md
@@ -0,0 +1,5 @@
---
'react-select': minor
---

Fix animated MultiValue transitions when being removed and change method used to generate unqiue keys for Option components. Closes #3648 , closes #4844 , closes #4602
3 changes: 2 additions & 1 deletion packages/react-select/src/Select.tsx
Expand Up @@ -1648,6 +1648,7 @@ export default class Select<
if (isMulti) {
return selectValue.map((opt, index) => {
const isOptionFocused = opt === focusedValue;
const key = `${this.getOptionLabel(opt)}-${this.getOptionValue(opt)}`;

return (
<MultiValue
Expand All @@ -1659,7 +1660,7 @@ export default class Select<
}}
isFocused={isOptionFocused}
isDisabled={isDisabled}
key={`${this.getOptionValue(opt)}-${index}`}
key={key}
index={index}
removeProps={{
onClick: () => this.removeValue(opt),
Expand Down
84 changes: 82 additions & 2 deletions packages/react-select/src/animated/ValueContainer.tsx
@@ -1,4 +1,4 @@
import React, { ReactElement } from 'react';
import React, { useEffect, useState, ReactElement, ReactNode } from 'react';
import { TransitionGroup } from 'react-transition-group';
import { ValueContainerProps } from '../components/containers';
import { GroupBase } from '../types';
Expand All @@ -11,12 +11,92 @@ export type ValueContainerComponent = <
props: ValueContainerProps<Option, IsMulti, Group>
) => ReactElement;

interface IsMultiValueContainerProps extends ValueContainerProps {
component: ValueContainerComponent;
}

// make ValueContainer a transition group
const AnimatedValueContainer =
(WrappedComponent: ValueContainerComponent) =>
<Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
props: ValueContainerProps<Option, IsMulti, Group>
) =>
<TransitionGroup component={WrappedComponent} {...(props as any)} />;
props.isMulti ? (
<IsMultiValueContainer component={WrappedComponent} {...(props as any)} />
) : (
<TransitionGroup component={WrappedComponent} {...(props as any)} />
);

const IsMultiValueContainer = ({
component,
...restProps
}: IsMultiValueContainerProps) => {
const multiProps = useIsMultiValueContainer(restProps);

return <TransitionGroup component={component} {...(multiProps as any)} />;
};

const useIsMultiValueContainer = ({
children,
...props
}: ValueContainerProps) => {
const {
isMulti,
hasValue,
innerProps,
selectProps: { components, controlShouldRenderValue },
} = props;

const [cssDisplayFlex, setCssDisplayFlex] = useState(
isMulti && controlShouldRenderValue && hasValue
);
const [removingValue, setRemovingValue] = useState(false);

useEffect(() => {
if (hasValue && !cssDisplayFlex) {
setCssDisplayFlex(true);
}
}, [hasValue, cssDisplayFlex]);

useEffect(() => {
if (removingValue && !hasValue && cssDisplayFlex) {
setCssDisplayFlex(false);
}
setRemovingValue(false);
}, [removingValue, hasValue, cssDisplayFlex]);

const onExited = () => setRemovingValue(true);

const childMapper = (child: ReactNode) => {
if (isMulti && React.isValidElement(child)) {
// Add onExited callback to MultiValues
if (child.type === components.MultiValue) {
return React.cloneElement(child, { onExited });
}
// While container flexed, Input cursor is shown after Placeholder text,
// so remove Placeholder until display is set back to grid
if (child.type === components.Placeholder && cssDisplayFlex) {
return null;
}
}
return child;
};

const newInnerProps = {
...innerProps,
style: {
...innerProps?.style,
display: cssDisplayFlex ? 'flex' : 'grid',
},
};

const newProps = {
...props,
innerProps: newInnerProps,
children: React.Children.toArray(children).map(childMapper),
};

return newProps;
};

export default AnimatedValueContainer;
9 changes: 8 additions & 1 deletion packages/react-select/src/animated/transitions.tsx
Expand Up @@ -127,7 +127,13 @@ export class Collapse extends Component<CollapseProps, CollapseState> {
getTransition = (state: TransitionStatus) => this.transition[state];

render() {
const { children, in: inProp } = this.props;
const { children, in: inProp, onExited } = this.props;
const exitedProp = () => {
if (this.nodeRef.current && onExited) {
onExited(this.nodeRef.current);
}
};

const { width } = this.state;

return (
Expand All @@ -136,6 +142,7 @@ export class Collapse extends Component<CollapseProps, CollapseState> {
mountOnEnter
unmountOnExit
in={inProp}
onExited={exitedProp}
timeout={this.duration}
nodeRef={this.nodeRef}
>
Expand Down
3 changes: 2 additions & 1 deletion packages/react-select/src/components/containers.tsx
Expand Up @@ -86,9 +86,10 @@ export const valueContainerCSS = <
theme: { spacing },
isMulti,
hasValue,
selectProps: { controlShouldRenderValue },
}: ValueContainerProps<Option, IsMulti, Group>): CSSObjectWithLabel => ({
alignItems: 'center',
display: isMulti && hasValue ? 'flex' : 'grid',
display: isMulti && hasValue && controlShouldRenderValue ? 'flex' : 'grid',
flex: 1,
flexWrap: 'wrap',
padding: `${spacing.baseUnit / 2}px ${spacing.baseUnit * 2}px`,
Expand Down