Use mui react transition group

This commit is contained in:
Andrew Musgrave
2019-06-28 16:18:57 -04:00
parent e0ebffdfb6
commit 0130e7f8e1
16 changed files with 179 additions and 115 deletions

View File

@@ -0,0 +1,21 @@
declare module '@material-ui/react-transition-group' {
import {Component} from 'react';
import CSSTransitionNameSpace = require('react-transition-group/CSSTransition');
import {TransitionProps} from 'react-transition-group/Transition';
type FindDOMNode = () => HTMLElement | null;
interface CombinedTransitionProps extends TransitionProps {
findDOMNode: FindDOMNode;
}
type CombinedCSSTransitionProps = CSSTransitionNameSpace.CSSTransitionProps &
CombinedTransitionProps;
// eslint-disable-next-line react/prefer-stateless-function
class Transition extends Component<CombinedTransitionProps> {}
// eslint-disable-next-line react/prefer-stateless-function
class CSSTransition extends Component<CombinedCSSTransitionProps> {}
import TransitionGroup = require('react-transition-group/TransitionGroup');
export {CSSTransition, Transition, TransitionGroup};
}

View File

@@ -173,6 +173,7 @@
],
"dependencies": {
"@babel/runtime": "^7.1.6",
"@material-ui/react-transition-group": "^4.2.0",
"@shopify/app-bridge": "^1.3.0",
"@shopify/javascript-utilities": "^2.2.1",
"@shopify/polaris-icons": "^3.3.0",
@@ -190,7 +191,6 @@
"@types/react-transition-group": "^2.0.7",
"hoist-non-react-statics": "^2.5.0",
"lodash": "^4.17.4",
"react-transition-group": "^2.4.0",
"tslib": "^1.9.3"
}
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import React, {createRef} from 'react';
import {MobileCancelMajorMonotone} from '@shopify/polaris-icons';
import {durationSlow} from '@shopify/polaris-tokens';
import {CSSTransition} from 'react-transition-group';
import {CSSTransition} from '@material-ui/react-transition-group';
import {classNames} from '../../utilities/css';
import {navigationBarCollapsed} from '../../utilities/breakpoints';
import Button from '../Button';
@@ -66,6 +66,8 @@ class Frame extends React.PureComponent<CombinedProps, State> {
private contextualSaveBar: ContextualSaveBarProps | null;
private globalRibbonContainer: HTMLDivElement | null = null;
private navigationNode = createRef<HTMLDivElement>();
private contextualSaveBarNode = createRef<HTMLDivElement>();
componentDidMount() {
this.handleResize();
@@ -110,6 +112,7 @@ class Frame extends React.PureComponent<CombinedProps, State> {
const navigationMarkup = navigation ? (
<TrapFocus trapping={mobileNavShowing}>
<CSSTransition
findDOMNode={this.findNavigationNode}
appear={mobileView}
exit={mobileView}
in={showMobileNavigation}
@@ -117,6 +120,7 @@ class Frame extends React.PureComponent<CombinedProps, State> {
classNames={navTransitionClasses}
>
<div
ref={this.navigationNode}
className={navClassName}
onKeyDown={this.handleNavKeydown}
id={APP_FRAME_NAV}
@@ -152,6 +156,7 @@ class Frame extends React.PureComponent<CombinedProps, State> {
const contextualSaveBarMarkup = (
<CSSTransition
findDOMNode={this.findContextualSaveBarNode}
appear
exit
in={showContextualSaveBar}
@@ -160,7 +165,10 @@ class Frame extends React.PureComponent<CombinedProps, State> {
mountOnEnter
unmountOnExit
>
<div className={styles.ContextualSaveBar}>
<div
className={styles.ContextualSaveBar}
ref={this.contextualSaveBarNode}
>
<ContextualSaveBar {...this.contextualSaveBar} />
</div>
</CSSTransition>
@@ -371,6 +379,14 @@ class Frame extends React.PureComponent<CombinedProps, State> {
this.handleNavigationDismiss();
}
};
private findNavigationNode = () => {
return this.navigationNode.current;
};
private findContextualSaveBarNode = () => {
return this.contextualSaveBarNode.current;
};
}
const navTransitionClasses = {

View File

@@ -1,5 +1,8 @@
import React, {createRef, memo} from 'react';
import {TransitionGroup, CSSTransition} from 'react-transition-group';
import {
TransitionGroup,
CSSTransition,
} from '@material-ui/react-transition-group';
import {classNames} from '../../../../utilities/css';
import EventListener from '../../../EventListener';
import Portal from '../../../Portal';
@@ -50,6 +53,7 @@ function ToastManager({toastMessages}: Props) {
return (
<CSSTransition
findDOMNode={findDOMNode(index)}
key={toast.id}
timeout={{enter: 0, exit: 400}}
classNames={toastClasses}
@@ -69,6 +73,10 @@ function ToastManager({toastMessages}: Props) {
</div>
</Portal>
);
function findDOMNode(index: number) {
return () => toastNodes[index].current;
}
}
const toastClasses = {

View File

@@ -1,5 +1,5 @@
import React from 'react';
import {CSSTransition} from 'react-transition-group';
import {CSSTransition} from '@material-ui/react-transition-group';
import {animationFrame} from '@shopify/jest-dom-mocks';
import {documentHasStyle} from 'test-utilities';
import {mountWithAppProvider} from 'test-utilities/legacy';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import isEqual from 'lodash/isEqual';
import {TransitionGroup} from 'react-transition-group';
import {TransitionGroup} from '@material-ui/react-transition-group';
import {write} from '@shopify/javascript-utilities/fastdom';
import {focusFirstFocusableNode} from '@shopify/javascript-utilities/focus';
import {createUniqueIDFactory} from '@shopify/javascript-utilities/other';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import {Transition, CSSTransition} from 'react-transition-group';
import React, {useRef, useCallback} from 'react';
import {durationBase} from '@shopify/polaris-tokens';
import {Transition, CSSTransition} from '@material-ui/react-transition-group';
import {classNames} from '../../../../utilities/css';
import {AnimationProps, Key} from '../../../../types';
@@ -23,14 +23,6 @@ export interface DialogProps {
export type Props = DialogProps & AnimationProps;
function DialogContainer(props: {children: React.ReactNode}) {
return (
<div className={styles.Container} data-polaris-layer data-polaris-overlay>
{props.children}
</div>
);
}
export default function Dialog({
instant,
labelledBy,
@@ -42,6 +34,8 @@ export default function Dialog({
limitHeight,
...props
}: Props) {
const containerNode = useRef<HTMLDivElement>(null);
const findDOMNode = useCallback(() => containerNode.current, []);
const classes = classNames(
styles.Modal,
large && styles.sizeLarge,
@@ -52,13 +46,19 @@ export default function Dialog({
return (
<TransitionChild
{...props}
findDOMNode={findDOMNode}
mountOnEnter
unmountOnExit
timeout={durationBase}
onEntered={onEntered}
onExited={onExited}
>
<DialogContainer>
<div
className={styles.Container}
data-polaris-layer
data-polaris-overlay
ref={containerNode}
>
<TrapFocus>
<div
className={classes}
@@ -74,7 +74,7 @@ export default function Dialog({
{children}
</div>
</TrapFocus>
</DialogContainer>
</div>
</TransitionChild>
);
}

View File

@@ -124,6 +124,10 @@ $bulk-actions-offset-slide-in-start: rem(-40px);
}
}
.CheckableContainer {
flex: 1 1 0;
}
.Popover {
max-height: resource-list(button-min-height);
margin-left: -1px;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import {CSSTransition, Transition} from 'react-transition-group';
import React, {createRef} from 'react';
import debounce from 'lodash/debounce';
import {durationBase} from '@shopify/polaris-tokens';
import {CSSTransition, Transition} from '@material-ui/react-transition-group';
import {classNames} from '../../../../utilities/css';
import {DisableableAction, Action, ActionListSection} from '../../../../types';
import ActionList from '../../../ActionList';
@@ -76,6 +76,9 @@ class BulkActions extends React.PureComponent<CombinedProps, State> {
private containerNode: HTMLElement | null;
private largeScreenButtonsNode: HTMLElement | null;
private moreActionsNode: HTMLElement | null;
private checkableWrapperNode = createRef<HTMLDivElement>();
private largeScreenGroupNode = createRef<HTMLDivElement>();
private smallScreenGroupNode = createRef<HTMLDivElement>();
private promotedActionsWidths: number[] = [];
private bulkActionsWidth = 0;
private addedMoreActionsWidthForMeasuring = 0;
@@ -360,7 +363,12 @@ class BulkActions extends React.PureComponent<CombinedProps, State> {
};
const smallScreenGroup = (
<Transition timeout={0} in={selectMode} key="smallGroup">
<Transition
timeout={0}
in={selectMode}
key="smallGroup"
findDOMNode={this.findSmallScreenGroupNode}
>
{(status: TransitionStatus) => {
const smallScreenGroupClassName = classNames(
styles.Group,
@@ -368,15 +376,24 @@ class BulkActions extends React.PureComponent<CombinedProps, State> {
styles[`Group-${status}`],
);
return (
<div className={smallScreenGroupClassName}>
<div
className={smallScreenGroupClassName}
ref={this.smallScreenGroupNode}
>
<div className={styles.ButtonGroup}>
<CSSTransition
findDOMNode={this.findCheckableWrapperNode}
in={selectMode}
timeout={durationBase}
classNames={slideClasses}
appear
>
<CheckableButton {...checkableButtonProps} />
<div
className={styles.CheckableContainer}
ref={this.checkableWrapperNode}
>
<CheckableButton {...checkableButtonProps} />
</div>
</CSSTransition>
{allActionsPopover}
{cancelButton}
@@ -389,7 +406,12 @@ class BulkActions extends React.PureComponent<CombinedProps, State> {
);
const largeScreenGroup = (
<Transition timeout={0} in={selectMode} key="largeGroup">
<Transition
timeout={0}
in={selectMode}
key="largeGroup"
findDOMNode={this.findLargeScreenGroupNode}
>
{(status: TransitionStatus) => {
const largeScreenGroupClassName = classNames(
styles.Group,
@@ -398,7 +420,10 @@ class BulkActions extends React.PureComponent<CombinedProps, State> {
measuring && styles['Group-measuring'],
);
return (
<div className={largeScreenGroupClassName}>
<div
className={largeScreenGroupClassName}
ref={this.largeScreenGroupNode}
>
<EventListener event="resize" handler={this.handleResize} />
<div
className={styles.ButtonGroup}
@@ -460,6 +485,18 @@ class BulkActions extends React.PureComponent<CombinedProps, State> {
this.promotedActionsWidths.push(width);
}
};
private findLargeScreenGroupNode = () => {
return this.largeScreenGroupNode.current;
};
private findCheckableWrapperNode = () => {
return this.checkableWrapperNode.current;
};
private findSmallScreenGroupNode = () => {
return this.smallScreenGroupNode.current;
};
}
function instanceOfBulkActionListSectionArray(

View File

@@ -1,5 +1,5 @@
import React from 'react';
import {Transition, CSSTransition} from 'react-transition-group';
import {Transition, CSSTransition} from '@material-ui/react-transition-group';
import {mountWithAppProvider, findByTestID} from 'test-utilities/legacy';
import {Popover} from 'components';
import CheckableButton from '../../CheckableButton';

View File

@@ -18,7 +18,6 @@ $chekbox-label-margin: rem(20px);
cursor: pointer;
user-select: none;
text-decoration: none;
flex: 1 1 0;
text-align: left;
background: color('white');
border: border(dark);

View File

@@ -1,6 +1,12 @@
import React, {useCallback, useContext, useEffect, useState} from 'react';
import React, {
useCallback,
useContext,
useEffect,
useState,
useRef,
} from 'react';
import {CSSTransition} from 'react-transition-group';
import {CSSTransition} from '@material-ui/react-transition-group';
import debounce from 'lodash/debounce';
import {classNames} from '../../utilities/css';
@@ -47,10 +53,15 @@ export interface State {
}
function Sheet({children, open, onClose}: Props) {
const container = useRef<HTMLDivElement>(null);
const [mobile, setMobile] = useState(false);
const frame = useContext(FrameContext);
const intl = useI18n();
const findDOMNode = useCallback(() => {
return container.current;
}, []);
const handleResize = useCallback(
debounce(
() => {
@@ -85,13 +96,25 @@ function Sheet({children, open, onClose}: Props) {
return (
<Portal idPrefix="sheet">
<CSSTransition
findDOMNode={findDOMNode}
classNames={mobile ? BOTTOM_CLASS_NAMES : RIGHT_CLASS_NAMES}
timeout={Duration.Slow}
in={open}
mountOnEnter
unmountOnExit
>
<Container open={open}>{children}</Container>
<div
className={styles.Container}
{...layer.props}
{...overlay.props}
ref={container}
>
<TrapFocus trapping={open}>
<div role="dialog" tabIndex={-1} className={styles.Sheet}>
{children}
</div>
</TrapFocus>
</div>
</CSSTransition>
<KeypressListener keyCode={Key.Escape} handler={onClose} />
<EventListener event="resize" handler={handleResize} />
@@ -108,16 +131,4 @@ function isMobile(): boolean {
return navigationBarCollapsed().matches;
}
function Container(props: {children: React.ReactNode; open: boolean}) {
return (
<div className={styles.Container} {...layer.props} {...overlay.props}>
<TrapFocus trapping={props.open}>
<div role="dialog" tabIndex={-1} className={styles.Sheet}>
{props.children}
</div>
</TrapFocus>
</div>
);
}
export default withAppProvider<Props>()(Sheet);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import {mount} from 'enzyme';
import {CSSTransition} from 'react-transition-group';
import {CSSTransition} from '@material-ui/react-transition-group';
import {matchMedia} from '@shopify/jest-dom-mocks';
import {mountWithAppProvider} from 'test-utilities/legacy';

View File

@@ -182,23 +182,21 @@ export function polarisContextReactWrapper<P, S>(
}
return (
<React.StrictMode>
<I18nContext.Provider value={app.intl}>
<ScrollLockManagerContext.Provider value={app.scrollLockManager}>
<StickyManagerContext.Provider value={app.stickyManager}>
<ThemeProviderContext.Provider value={app.themeProvider}>
<AppBridgeContext.Provider value={app.appBridge as any}>
<LinkContext.Provider value={app.link}>
<FrameContext.Provider value={app.frame}>
{content}
</FrameContext.Provider>
</LinkContext.Provider>
</AppBridgeContext.Provider>
</ThemeProviderContext.Provider>
</StickyManagerContext.Provider>
</ScrollLockManagerContext.Provider>
</I18nContext.Provider>
</React.StrictMode>
<I18nContext.Provider value={app.intl}>
<ScrollLockManagerContext.Provider value={app.scrollLockManager}>
<StickyManagerContext.Provider value={app.stickyManager}>
<ThemeProviderContext.Provider value={app.themeProvider}>
<AppBridgeContext.Provider value={app.appBridge as any}>
<LinkContext.Provider value={app.link}>
<FrameContext.Provider value={app.frame}>
{content}
</FrameContext.Provider>
</LinkContext.Provider>
</AppBridgeContext.Provider>
</ThemeProviderContext.Provider>
</StickyManagerContext.Provider>
</ScrollLockManagerContext.Provider>
</I18nContext.Provider>
);
}

View File

@@ -1,39 +0,0 @@
import React from 'react';
import {mountWithAppProvider} from '../legacy';
describe('mountWithAppProvider', () => {
let consoleSpy: jest.SpyInstance;
beforeEach(() => {
consoleSpy = jest.spyOn(console, 'error');
consoleSpy.mockImplementation(() => {});
});
afterEach(() => {
consoleSpy.mockRestore();
});
it('renders in strict mode', () => {
// eslint-disable-next-line react/no-unsafe
class Child extends React.Component {
componentWillReceiveProps() {}
render() {
return null;
}
}
mountWithAppProvider(<Child />);
expect(consoleSpy).toHaveBeenCalledWith(
`Warning: Unsafe lifecycle methods were found within a strict-mode tree:%s
%s
Learn more about this warning here:
https://fb.me/react-strict-mode-warnings`,
expect.any(String),
'componentWillReceiveProps: Please update the following components to use static getDerivedStateFromProps instead: Child',
);
});
});

View File

@@ -952,6 +952,13 @@
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.3", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.4.tgz#dc2e34982eb236803aa27a07fea6857af1b9171d"
integrity sha512-w0+uT71b6Yi7i5SE0co4NioIpSYS6lLiXvCzWzGSKvpK5vdQtCbICHMj+gbAKAOtxiV6HsVh/MBdaF9EQ6faSg==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
@@ -1262,6 +1269,16 @@
"@types/istanbul-reports" "^1.1.1"
"@types/yargs" "^12.0.9"
"@material-ui/react-transition-group@^4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@material-ui/react-transition-group/-/react-transition-group-4.2.0.tgz#afec833bbcc79f05a9b4d4828b3e07965cc7e321"
integrity sha512-4zapZ0gW1ZTws5aH9OGy3IMvtTV/olc7YrVSkM1WFu1FsrEhL+qarEniRjx7LjHt0gukFqoINfElI8v2boVMQA==
dependencies:
"@babel/runtime" "^7.4.5"
dom-helpers "^3.4.0"
loose-envify "^1.4.0"
prop-types "^15.6.2"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -6336,10 +6353,12 @@ dom-converter@^0.2:
dependencies:
utila "~0.4"
dom-helpers@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"
integrity sha512-2Sm+JaYn74OiTM2wHvxJOo3roiq/h25Yi69Fqk269cNUwIXsCvATB6CRSFC9Am/20G2b28hGv/+7NiWydIrPvg==
dom-helpers@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
dependencies:
"@babel/runtime" "^7.1.2"
dom-serializer@0, dom-serializer@~0.1.1:
version "0.1.1"
@@ -11338,7 +11357,7 @@ longest@^1.0.0:
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
loose-envify@^1.0.0, loose-envify@^1.1.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
integrity sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=
@@ -14617,16 +14636,6 @@ react-textarea-autosize@^7.1.0:
"@babel/runtime" "^7.1.2"
prop-types "^15.6.0"
react-transition-group@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.4.0.tgz#1d9391fabfd82e016f26fabd1eec329dbd922b5a"
integrity sha512-Xv5d55NkJUxUzLCImGSanK8Cl/30sgpOEMGc5m86t8+kZwrPxPCPcFqyx83kkr+5Lz5gs6djuvE5By+gce+VjA==
dependencies:
dom-helpers "^3.3.1"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react@^16.7.0, react@^16.8.4, react@^16.8.6:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"