├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── package.json ├── rollup.config.js ├── src ├── AnimatedComponent │ ├── index.jsx │ └── props.js ├── construct │ ├── css.js │ ├── index.js │ └── styled.js ├── index.js ├── states.js └── utils │ └── domElements.js ├── test ├── mocha.opts ├── setup.js └── transition.spec.jsx └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": "commonjs" 5 | }], 6 | "stage-1", 7 | "react" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "plugins": [ 9 | "react", 10 | "jsx-a11y", 11 | "import" 12 | ], 13 | "parser": "babel-eslint", 14 | "parserOptions": { 15 | "sourceType": "module" 16 | }, 17 | "settings": { 18 | "import/resolver": { 19 | "babel-module": {} 20 | } 21 | }, 22 | "rules": { 23 | "import/no-extraneous-dependencies": ["error", { 24 | "devDependencies": true, 25 | "peerDependencies": true 26 | }], 27 | "quotes": ["error", "double", { 28 | "avoidEscape": true 29 | }], 30 | "keyword-spacing": ["error", { 31 | "overrides": { 32 | "if": { "after": false }, 33 | "for": { "after": false }, 34 | "while": { "after": false }, 35 | "catch": { "after": false }, 36 | "switch": { "after": false } 37 | } 38 | }], 39 | "generator-star-spacing": ["error", { 40 | "before": true, 41 | "after": true 42 | }], 43 | "no-plusplus": ["error", { 44 | "allowForLoopAfterthoughts": true 45 | }], 46 | "indent": ["error", 2, { 47 | "SwitchCase": 1 48 | }], 49 | "func-names": "off", 50 | "comma-dangle": ["error", "never"], 51 | "array-bracket-spacing": ["error", "always"], 52 | "arrow-body-style": "off", 53 | "semi": ["error", "never"], 54 | "global-require": "warn", 55 | "class-methods-use-this": "off", 56 | "react/forbid-prop-types": "off", 57 | "react/require-default-props": "off", 58 | "react/jsx-closing-bracket-location": ["error", "after-props"] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # IDE 5 | .vscode 6 | .idea 7 | 8 | # Logs 9 | *.log 10 | tmp 11 | 12 | # Build 13 | lib 14 | dist 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # IDE 5 | .vscode 6 | 7 | # Logs 8 | *.log 9 | tmp 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Released under MIT License 2 | 3 | Copyright (c) 2013 Mark Otto. 4 | 5 | Copyright (c) 2017 Andrew Fong. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Styled Transition Group 2 | 3 | [![npm version](https://badge.fury.io/js/styled-transition-group.svg)](https://badge.fury.io/js/styled-transition-group) 4 | 5 | Inspired by issue [#1036](https://github.com/styled-components/styled-components/issues/1036) of styled-components, this package exports a `styled` object for generating animations with react-transition-group's `CSSTransition`. 6 | 7 | ## Getting Started 8 | 9 | Add `styled-transition-group` and it's peer dependencies to your package: 10 | `styled-transition-group@1` is compatible with `styled-components` v2 - v3. 11 | `styled-transition-group@2` is compatible with `styled-components` v4. 12 | 13 | ```shell 14 | yarn add styled-components react-transition-group 15 | yarn add styled-transition-group 16 | ``` 17 | 18 | ## Usage 19 | 20 | The `transition` object has the same interface as styled-component's `styled` object, except it wraps the target component in a `CSSTransition` component and passes down it's props. 21 | 22 | ### Basic 23 | 24 | To style a transition state use an `&:{state}` selector. See [react-transition-group's docs](https://reactcommunity.org/react-transition-group/#CSSTransition-prop-classNames) for available transition states (State names are hyphenated). 25 | 26 | [Live example on Stackblitz](https://stackblitz.com/edit/01-styled-transition-group?file=Fade.js) 27 | 28 | ```jsx 29 | import transition from "styled-transition-group"; 30 | 31 | const Fade = transition.div` 32 | &:enter { opacity: 0.01; } 33 | &:enter-active { 34 | opacity: 1; 35 | transition: opacity 1000ms ease-in; 36 | } 37 | &:exit { opacity: 1; } 38 | &:exit-active { 39 | opacity: 0.01; 40 | transition: opacity 800ms ease-in; 41 | } 42 | `; 43 | ``` 44 | 45 | ### Attach transition props 46 | 47 | Styled component's `attrs()` method can be used to attach transition props to a component. Props unrelated to CSSTransition are passed to the child component. 48 | 49 | [Live example on Stackblitz](https://stackblitz.com/edit/02-styled-transition-group?file=Fade.js) 50 | 51 | ```jsx 52 | import transition from "styled-transition-group"; 53 | 54 | const Fade = transition.div.attrs({ 55 | unmountOnExit: true, 56 | timeout: 1000 57 | })` 58 | &:enter { opacity: 0.01; } 59 | &:enter-active { 60 | opacity: 1; 61 | transition: opacity 1000ms ease-in; 62 | } 63 | &:exit { opacity: 1; } 64 | &:exit-active { 65 | opacity: 0.01; 66 | transition: opacity 800ms ease-in; 67 | } 68 | `; 69 | ``` 70 | 71 | ### Transition Group 72 | 73 | Styled transitions can be used with `TransitionGroup` 74 | 75 | [Live example on Stackblitz](https://stackblitz.com/edit/03-styled-transition-group?file=Fade.js) 76 | 77 | ### Selectors 78 | 79 | Using `styled-transition-group`'s css helper, selectors can target the transition it's included in (`&`) or other transition components. It replaces the selectors with the actual `styled-transition-group` component's class names. 80 | 81 | _Warning:_ Nesting doesn't work here. `&` targets the top level component regardless of nesting. 82 | 83 | ```jsx 84 | import styled from "styled-components"; 85 | import transition, { css } from "styled-transition-group"; 86 | 87 | const Fade = transition.div` /* ... */ `; 88 | 89 | const style = css` 90 | ${Fade}:enter & { 91 | color: green; 92 | } 93 | ${Fade}:exit & { 94 | color: red; 95 | } 96 | `; 97 | 98 | const Button = styled.div` 99 | ${style} /* ... */ 100 | `; 101 | ``` 102 | 103 | [Live example on Stackblitz](https://stackblitz.com/edit/04-styled-transition-group?file=Text.js) 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styled-transition-group", 3 | "description": "Write react-transition-group animations with styled-components", 4 | "version": "2.0.1", 5 | "main": "dist/bundle.js", 6 | "module": "dist/bundle.es.js", 7 | "author": "Gabriela Seabra ", 8 | "repository": "gabiseabra/styled-transition-group", 9 | "license": "MIT", 10 | "scripts": { 11 | "prepublishOnly": "yarn run clean && yarn run build", 12 | "lint": "eslint src", 13 | "test": "mocha 'test/**/*.spec.{js,jsx}'", 14 | "build": "rollup -c -m", 15 | "clean": "rimraf ./dist" 16 | }, 17 | "dependencies": { 18 | "lodash.invert": "^4.3.0", 19 | "lodash.isempty": "^4.4.0", 20 | "prop-types": "^15.6.0" 21 | }, 22 | "peerDependencies": { 23 | "react": "^16.3.0", 24 | "react-dom": "^16.3.0", 25 | "react-transition-group": ">= 2.2.1", 26 | "styled-components": ">= 4" 27 | }, 28 | "devDependencies": { 29 | "babel-cli": "^6.26.0", 30 | "babel-eslint": "^10.0.1", 31 | "babel-plugin-external-helpers": "^6.22.0", 32 | "babel-polyfill": "^6.26.0", 33 | "babel-preset-env": "^1.6.1", 34 | "babel-preset-react": "^6.24.1", 35 | "babel-preset-stage-1": "^6.24.1", 36 | "chai": "^4.1.2", 37 | "chai-enzyme": "1.0.0-beta.1", 38 | "enzyme": "^3.7.0", 39 | "enzyme-adapter-react-16": "^1.6.0", 40 | "eslint": "^6.8.0", 41 | "eslint-config-airbnb": "^18.0.1", 42 | "eslint-plugin-import": "^2.14.0", 43 | "eslint-plugin-jsx-a11y": "^6.1.2", 44 | "eslint-plugin-react": "^7.11.1", 45 | "jsdom": "^16.2.0", 46 | "mocha": "^7.0.1", 47 | "react": "^16.3.0", 48 | "react-dom": "^16.3.0", 49 | "react-transition-group": "^4.3.0", 50 | "rimraf": "^3.0.2", 51 | "rollup": "^1.31.1", 52 | "rollup-plugin-babel": "^3.0.3", 53 | "rollup-plugin-commonjs": "^10.1.0", 54 | "rollup-plugin-node-resolve": "^5.2.0", 55 | "styled-components": "^5.0.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import cjs from "rollup-plugin-commonjs" 2 | import babel from "rollup-plugin-babel" 3 | import resolve from "rollup-plugin-node-resolve" 4 | import pkg from "./package.json" 5 | 6 | const include = [ "src/**" ] 7 | 8 | const deps = Object.keys(pkg.dependencies).concat(Object.keys(pkg.peerDependencies)) 9 | 10 | const EXTERNALS = new RegExp(`^(${deps.join("|")})|node_modules`) 11 | 12 | export default { 13 | input: "src/index.js", 14 | output: [ 15 | { file: "dist/bundle.js", format: "cjs" }, 16 | { file: "dist/bundle.es.js", format: "es" } 17 | ], 18 | external: name => EXTERNALS.test(name), 19 | plugins: [ 20 | resolve({ extensions: [ ".js", ".jsx" ] }), 21 | cjs({ 22 | exclude: include 23 | }), 24 | babel({ 25 | include, 26 | babelrc: false, 27 | presets: [ [ "env", { modules: false } ], "stage-1", "react" ], 28 | plugins: [ "external-helpers" ] 29 | }) 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/AnimatedComponent/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import CSSTransition from "react-transition-group/CSSTransition" 3 | import { classNames } from "../states" 4 | import groupProps from "./props" 5 | 6 | const animated = options => function animatedWithOptions(Target) { 7 | const { attrs } = options 8 | class AnimatedComponent extends Component { 9 | static displayName = `Animated(${Target.displayName})` 10 | 11 | static styledComponentId = Target.styledComponentId 12 | 13 | static attrs = attrs 14 | 15 | static get classNames() { 16 | return classNames(this.styledComponentId) 17 | } 18 | 19 | static withComponent(...props) { 20 | return animatedWithOptions(Target.withComponent(...props)) 21 | } 22 | 23 | renderTarget({ forwardedRef, ...props }) { 24 | return ( 25 | 29 | ) 30 | } 31 | 32 | renderChildren({ children, ...props }) { 33 | if(typeof children === "function") { 34 | return state => this.renderTarget({ 35 | ...props, 36 | children: children(state) 37 | }) 38 | } 39 | return this.renderTarget({ children, ...props }) 40 | } 41 | 42 | render() { 43 | const { transition, props } = groupProps(this.props) 44 | const transitionClassNames = this.constructor.classNames 45 | 46 | return ( 47 | 53 | {this.renderChildren({ ...props, transitionClassNames })} 54 | 55 | ) 56 | } 57 | } 58 | 59 | const AnimatedComponentWithRef = React.forwardRef( 60 | (props, ref) => 61 | ) 62 | 63 | AnimatedComponentWithRef.Target = Target 64 | 65 | return AnimatedComponentWithRef 66 | } 67 | 68 | export default animated 69 | 70 | export const isAnimatedComponent = Klass => "styledComponentId" in Klass && "classNames" in Klass 71 | -------------------------------------------------------------------------------- /src/AnimatedComponent/props.js: -------------------------------------------------------------------------------- 1 | const transitionProps = [ 2 | "in", 3 | "mountOnEnter", 4 | "unmountOnExit", 5 | "onEnter", 6 | "onEntering", 7 | "onEntered", 8 | "onExit", 9 | "onExited", 10 | "onExiting", 11 | "onExit", 12 | "onExited", 13 | "onExiting", 14 | "onAppear" 15 | ] 16 | 17 | const bothProps = [ 18 | // Pass timeout through to use in css templates 19 | "timeout" 20 | ] 21 | 22 | /** 23 | * Select which props to pass to transition and child component. 24 | * @param {Object} props All props 25 | * @returns {Object} groups 26 | * @returns {Object} groups.transition Transition props 27 | * @returns {Object} groups.props Child component props 28 | */ 29 | export default function partition(props) { 30 | const groups = { transition: {}, props: {} } 31 | Object.entries(props).forEach(([ key, value ]) => { 32 | if(bothProps.includes(key)) { 33 | groups.transition[key] = value 34 | groups.props[key] = value 35 | } else if(transitionProps.includes(key)) { 36 | groups.transition[key] = value 37 | } else { 38 | groups.props[key] = value 39 | } 40 | }) 41 | return groups 42 | } 43 | -------------------------------------------------------------------------------- /src/construct/css.js: -------------------------------------------------------------------------------- 1 | import invert from "lodash.invert" 2 | import { css } from "styled-components" 3 | import { isAnimatedComponent } from "../AnimatedComponent" 4 | import STATES from "../states" 5 | 6 | const STATES_BY_NAME = invert(STATES) 7 | 8 | const PATTERN = new RegExp(`([^\\s;}]+|^):(${Object.keys(STATES).map(key => STATES[key]).join("|")})(?=[\\s\\+~,{])`, "g") 9 | 10 | const getClassName = state => props => `&.${props.transitionClassNames[state]}` 11 | 12 | // Replace &:state with a class name selector 13 | const walkChunk = ({ strings, interpolations }) => (_, chunk) => { 14 | let match 15 | let lastIndex = 0 16 | // eslint-disable-next-line no-cond-assign 17 | while(match = PATTERN.exec(chunk)) { 18 | const [ target, element ] = match.slice(1) 19 | const state = STATES_BY_NAME[element] 20 | const len = match[0].length 21 | if(target === "&") { 22 | strings.push(chunk.substring(lastIndex, match.index)) 23 | interpolations.splice(strings.length - 1, 0, getClassName(state)) 24 | } else if(target === "") { 25 | const targetIndex = strings.length - 1 26 | const Target = interpolations[targetIndex] 27 | if(!isAnimatedComponent(Target)) { 28 | const name = Target && Target.constructor ? Target.constructor.name : Target 29 | throw new Error(`Invalid transition target "${name}". Target must be an AnimatedComponent.`) 30 | } 31 | interpolations.splice(targetIndex, 1, `.${Target.classNames[state]}`) 32 | } else { 33 | throw new Error(`Invalid transition target "${target}".`) 34 | } 35 | lastIndex = match.index + len 36 | } 37 | strings.push(chunk.substring(lastIndex, chunk.length)) 38 | } 39 | 40 | export default function parseCss(strings, ...interpolations) { 41 | const next = { 42 | strings: [], 43 | interpolations: [ ...interpolations ] 44 | } 45 | strings.reduce(walkChunk(next), null) 46 | return css(next.strings, ...next.interpolations) 47 | } 48 | -------------------------------------------------------------------------------- /src/construct/index.js: -------------------------------------------------------------------------------- 1 | import isEmpty from "lodash.isempty" 2 | import styled from "styled-components" 3 | import groupProps from "../AnimatedComponent/props" 4 | import css from "./css" 5 | 6 | const delegate = animated => fun => (...props) => animated(fun`${css(...props)}`) 7 | 8 | const groupConfig = ({ attrs, ...config }) => { 9 | const groups = { transition: {}, styled: { ...config } } 10 | if(attrs) { 11 | const { transition, props } = groupProps(attrs) 12 | groups.transition.attrs = transition 13 | if(!isEmpty(props)) groups.styled.attrs = props 14 | } 15 | return groups 16 | } 17 | 18 | export const extend = (animated, target, config = {}) => { 19 | const { transition, styled: styledConfig } = groupConfig(config) 20 | const delegateThis = cfg => delegate(animated({ ...transition, ...cfg })) 21 | if(!isEmpty(styledConfig)) { 22 | return extend(animated, target.withConfig(styledConfig), transition) 23 | } 24 | const result = delegateThis({})(target) 25 | result.withConfig = (options) => { 26 | const cfg = groupConfig(options) 27 | return delegateThis(cfg.transition)(target.withConfig(cfg.styled)) 28 | } 29 | result.attrs = (attrs) => { 30 | const cfg = groupConfig({ attrs }) 31 | return delegateThis(cfg.transition)(target.attrs(cfg.styled.attrs)) 32 | } 33 | return result 34 | } 35 | 36 | export default (animated, Tag, config) => extend(animated, styled(Tag), config) 37 | -------------------------------------------------------------------------------- /src/construct/styled.js: -------------------------------------------------------------------------------- 1 | import domElements from "../utils/domElements" 2 | 3 | export default (styledComponent, constructWithOptions) => { 4 | const styled = tag => constructWithOptions(styledComponent, tag) 5 | 6 | // Shorthands for all valid HTML Elements 7 | domElements.forEach((domElement) => { 8 | styled[domElement] = styled(domElement) 9 | }) 10 | 11 | return styled 12 | } 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import styled from "./construct/styled" 2 | import animated from "./AnimatedComponent" 3 | import construct from "./construct" 4 | import css from "./construct/css" 5 | 6 | export default styled( 7 | animated, 8 | construct 9 | ) 10 | 11 | export { css } 12 | -------------------------------------------------------------------------------- /src/states.js: -------------------------------------------------------------------------------- 1 | const STATES = { 2 | appear: "appear", 3 | appearActive: "appear-active", 4 | enter: "enter", 5 | enterActive: "enter-active", 6 | exit: "exit", 7 | exitActive: "exit-active" 8 | } 9 | 10 | export const classNames = id => Object.keys(STATES).reduce((result, type) => { 11 | result[type] = `${id}-${STATES[type]}` 12 | return result 13 | }, {}) 14 | 15 | export default STATES 16 | -------------------------------------------------------------------------------- /src/utils/domElements.js: -------------------------------------------------------------------------------- 1 | // Thanks to ReactDOMFactories for this handy list! 2 | 3 | export default [ 4 | "a", 5 | "abbr", 6 | "address", 7 | "area", 8 | "article", 9 | "aside", 10 | "audio", 11 | "b", 12 | "base", 13 | "bdi", 14 | "bdo", 15 | "big", 16 | "blockquote", 17 | "body", 18 | "br", 19 | "button", 20 | "canvas", 21 | "caption", 22 | "cite", 23 | "code", 24 | "col", 25 | "colgroup", 26 | "data", 27 | "datalist", 28 | "dd", 29 | "del", 30 | "details", 31 | "dfn", 32 | "dialog", 33 | "div", 34 | "dl", 35 | "dt", 36 | "em", 37 | "embed", 38 | "fieldset", 39 | "figcaption", 40 | "figure", 41 | "footer", 42 | "form", 43 | "h1", 44 | "h2", 45 | "h3", 46 | "h4", 47 | "h5", 48 | "h6", 49 | "head", 50 | "header", 51 | "hgroup", 52 | "hr", 53 | "html", 54 | "i", 55 | "iframe", 56 | "img", 57 | "input", 58 | "ins", 59 | "kbd", 60 | "keygen", 61 | "label", 62 | "legend", 63 | "li", 64 | "link", 65 | "main", 66 | "map", 67 | "mark", 68 | "marquee", 69 | "menu", 70 | "menuitem", 71 | "meta", 72 | "meter", 73 | "nav", 74 | "noscript", 75 | "object", 76 | "ol", 77 | "optgroup", 78 | "option", 79 | "output", 80 | "p", 81 | "param", 82 | "picture", 83 | "pre", 84 | "progress", 85 | "q", 86 | "rp", 87 | "rt", 88 | "ruby", 89 | "s", 90 | "samp", 91 | "script", 92 | "section", 93 | "select", 94 | "small", 95 | "source", 96 | "span", 97 | "strong", 98 | "style", 99 | "sub", 100 | "summary", 101 | "sup", 102 | "table", 103 | "tbody", 104 | "td", 105 | "textarea", 106 | "tfoot", 107 | "th", 108 | "thead", 109 | "time", 110 | "title", 111 | "tr", 112 | "track", 113 | "u", 114 | "ul", 115 | "var", 116 | "video", 117 | "wbr", 118 | 119 | // SVG 120 | "circle", 121 | "clipPath", 122 | "defs", 123 | "ellipse", 124 | "g", 125 | "image", 126 | "line", 127 | "linearGradient", 128 | "mask", 129 | "path", 130 | "pattern", 131 | "polygon", 132 | "polyline", 133 | "radialGradient", 134 | "rect", 135 | "stop", 136 | "svg", 137 | "text", 138 | "tspan" 139 | ] 140 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require babel-core/register 2 | --require ./test/setup.js 3 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import "babel-polyfill" 2 | import { JSDOM } from "jsdom" 3 | import chai from "chai" 4 | import chaiEnzyme from "chai-enzyme" 5 | import enzyme from "enzyme" 6 | import Adapter from "enzyme-adapter-react-16" 7 | 8 | const dom = new JSDOM("") 9 | 10 | enzyme.configure({ adapter: new Adapter() }) 11 | 12 | global.window = dom.window 13 | global.document = dom.window.document 14 | global.navigator = { userAgent: "node.js" } 15 | 16 | chai.use(chaiEnzyme()) 17 | 18 | global.should = chai.should() 19 | global.expect = chai.expect 20 | -------------------------------------------------------------------------------- /test/transition.spec.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import React from "react" 3 | import { mount } from "enzyme" 4 | import { css } from "styled-components" 5 | import { CSSTransition } from "react-transition-group" 6 | import transition from "../dist/bundle" 7 | 8 | const Tag = () =>
Test
9 | 10 | describe("transition", () => { 11 | it("wraps tag in CSSTransition", () => { 12 | const Component = transition(Tag)`` 13 | const context = mount() 14 | const cssTransition = context.find(CSSTransition) 15 | const component = cssTransition.find(Tag) 16 | cssTransition.should.have.length(1) 17 | component.should.have.length(1) 18 | }) 19 | 20 | it("passes props to child component", () => { 21 | const Component = transition(Tag)`` 22 | const context = mount() 23 | context.find(CSSTransition).props().should.include.keys("timeout", "classNames") 24 | context.find(Tag).props().should.include.keys("foo", "bar", "className") 25 | }) 26 | 27 | it("passes ref to child component", () => { 28 | const Component = transition.div`` 29 | class Wrapper extends React.Component { 30 | render() { 31 | return { this.innerRef = node }} /> 32 | } 33 | } 34 | const context = mount() 35 | const ref = context.find(Wrapper).instance().innerRef 36 | ref.should.equal(context.find("div").getDOMNode()) 37 | }) 38 | 39 | it("omits transition props from children", () => { 40 | const Component = transition.div`` 41 | const context = mount() 42 | context.find(CSSTransition).props().should.include.keys("timeout", "classNames", "unmountOnExit") 43 | context.find("div").props().should.not.include.keys("unmountOnExit") 44 | }) 45 | 46 | it("omits attrs transition props", () => { 47 | const Component = transition.div.attrs({ 48 | unmountOnExit: true, 49 | timeout: 100 50 | })`` 51 | const context = mount() 52 | context.find(CSSTransition).props().should.include.keys("timeout", "classNames", "unmountOnExit") 53 | context.find("div").props().should.not.include.keys("unmountOnExit") 54 | }) 55 | 56 | it("omits transition attrs", () => { 57 | const Component = transition(Tag).attrs({ 58 | unmountOnExit: true, 59 | timeout: 100, 60 | onExit: () => "..." 61 | })`` 62 | const context = mount() 63 | 64 | context.find(CSSTransition).props().should.include.keys("timeout", "unmountOnExit", "onExit") 65 | context.find(Tag).props().should.not.include.keys("in", "unmountOnExit", "onExit") 66 | }) 67 | 68 | it("works with css()", () => { 69 | const Component = transition.div(css`foo: bar`) 70 | const context = mount() 71 | context.find(CSSTransition).should.have.length(1) 72 | context.find("div").should.have.length(1) 73 | Component.Target.componentStyle.rules.should.include("foo: bar") 74 | }) 75 | 76 | it("renders children callback", () => { 77 | const Component = transition.div`` 78 | const context = mount( 79 | 80 | {state => {state}} 81 | 82 | ) 83 | context.find(CSSTransition).should.have.length(1) 84 | context.setProps({ in: true }) 85 | context.find("div").text().should.equal("entering") 86 | context.setProps({ in: false }) 87 | context.find("div").text().should.equal("exiting") 88 | }) 89 | }) 90 | --------------------------------------------------------------------------------