diff --git a/fork/react-bootstrap/CONTRIBUTING.md b/fork/react-bootstrap/CONTRIBUTING.md index fa50d59b568..76d19f11920 100644 --- a/fork/react-bootstrap/CONTRIBUTING.md +++ b/fork/react-bootstrap/CONTRIBUTING.md @@ -69,9 +69,9 @@ propTypes: { There are a few caveats to this format that differ from conventional JSDoc comments. - Only specific doclets (the @ things) should be used, and only when the data cannot be parsed from the component itself - - `@type`: Override the "type", use the same names as the default React PropTypes: string, func, bool, number, object. You can express enum and oneOfType types, Like `{("optionA"|"optionB")}`. - - `@required`: to mark a prop as required (use the normal React isRequired if possible) - - `@private`: Will hide the prop in the documentation + - `@type`: Override the "type", use the same names as the default React PropTypes: string, func, bool, number, object. You can express enum and oneOfType types, Like `{("optionA"|"optionB")}`. + - `@required`: to mark a prop as required (use the normal React isRequired if possible) + - `@private`: Will hide the prop in the documentation - All description text should be above the doclets. ## Implement additional components and features @@ -99,9 +99,7 @@ Please see the [Maintaining](./MAINTAINING.md) documentation. [huboard-badge]: https://img.shields.io/badge/Hu-Board-7965cc.svg [huboard]: https://huboard.com/react-bootstrap/react-bootstrap - [issues]: https://github.com/react-bootstrap/react-bootstrap/issues - [editorconfig]: http://editorconfig.org [eslint]: http://eslint.org [commit-message]: http://robots.thoughtbot.com/5-useful-tips-for-a-better-commit-message diff --git a/fork/react-bootstrap/MAINTAINING.md b/fork/react-bootstrap/MAINTAINING.md index 86c8d312844..abd561b5586 100644 --- a/fork/react-bootstrap/MAINTAINING.md +++ b/fork/react-bootstrap/MAINTAINING.md @@ -55,15 +55,15 @@ less active when you don't. If you get a new job and get busy, that's alright. Releases should include documentation, git tag, bower package preparation and finally the actual npm module publish. We have all of this automated by running -`npm run release`. __PLEASE DO NOT RUN `npm -publish` BY ITSELF__. The `release-script` will do that. We want to prevent issues +`npm run release`. **PLEASE DO NOT RUN `npm +publish` BY ITSELF**. The `release-script` will do that. We want to prevent issues like [#325](https://github.com/react-bootstrap/react-bootstrap/issues/325) and [#218](https://github.com/react-bootstrap/react-bootstrap/issues/218) from ever happening again. In order to run the `release-script` you will need permission to publish the package to npm. Those with this permission are in the [publishers team](https://github.com/orgs/react-bootstrap/teams/publishers) -*Note: The publishers team does exist. If you see 404 that means you just have no permissions to publish.* +_Note: The publishers team does exist. If you see 404 that means you just have no permissions to publish._ Example usages of the `release-script`: @@ -76,13 +76,16 @@ $ npm run release minor -- --preid beta --run Use both bump and preid for first $ npm run release -- --preid beta --run For follow on prereleases of the next version just use this ``` -*Note additional `--` double-dash. It is important.* +_Note additional `--` double-dash. It is important._ Or if you have this line + ```sh export PATH="./node_modules/.bin:$PATH" ``` + in your shell config, then you can run it just as: + ```bash $ release patch // without "--run" it will run in "dry run" mode $ release patch --run @@ -115,27 +118,30 @@ that you start from that tag. To live patch the documentation in between release follow these steps 0. Find the latest documentation release. - - Check the latest release tag (lets say `v0.22.1`). - - Look for a docs-release tag for that version ex: `v0.22.1-docs.X` - - If one exists, check-it-out. If not checkout the latest release tag. - - *Note: Checkout the tag and not master directly because some live - documentation changes on master that could related to new components - or updates for the upcoming release* + +- Check the latest release tag (lets say `v0.22.1`). +- Look for a docs-release tag for that version ex: `v0.22.1-docs.X` +- If one exists, check-it-out. If not checkout the latest release tag. +- _Note: Checkout the tag and not master directly because some live + documentation changes on master that could related to new components + or updates for the upcoming release_ + 0. Create a new branch from there (for example `git checkout -b docs/v0.22.1`) -0. Cherry-pick the commits you want to include in the live update -`git cherry-pick ...` -0. Use the +1. Cherry-pick the commits you want to include in the live update + `git cherry-pick ...` +2. Use the + ```bash $ npm run release -- --only-docs --run // or $ release --only-docs --run ``` + to push and tag to the documentation repository. -*Note: The branch name you checkout to cherry-picked the commit is not enforced. +_Note: The branch name you checkout to cherry-picked the commit is not enforced. Though keeping similar names ex: `docs/` helps finding the branch -easily.* - +easily._ ### Check everything is OK before releasing @@ -143,8 +149,10 @@ Release tools are run in "dry run" mode by default. It prevents `danger` steps (`git push`, `npm publish` etc) from accidental running. You can use it + - to learn how releasing tools are working. - to ensure there are no side issues before you release anything. + ```bash $ npm run release -- --only-docs $ npm run release major diff --git a/fork/react-bootstrap/README.md b/fork/react-bootstrap/README.md index e4c1f93a0fe..db01f542f76 100644 --- a/fork/react-bootstrap/README.md +++ b/fork/react-bootstrap/README.md @@ -6,7 +6,7 @@ [![Discord][discord-badge]][discord] [![Netlify][netlify-badge]][netlify] -__Under active development - APIs will change.__ Check out the [1.0.0 roadmap](https://github.com/react-bootstrap/react-bootstrap/wiki#100-roadmap) and [contributing guidelines][contributing] to see where you can help out. Prior to the 1.0.0 release, deprecations or breaking changes will result in a minor version bump. +**Under active development - APIs will change.** Check out the [1.0.0 roadmap](https://github.com/react-bootstrap/react-bootstrap/wiki#100-roadmap) and [contributing guidelines][contributing] to see where you can help out. Prior to the 1.0.0 release, deprecations or breaking changes will result in a minor version bump. ## Docs @@ -36,26 +36,19 @@ Yes please! See the [contributing guidelines][contributing] for details. [bootstrap]: https://getbootstrap.com/docs/3.3/ [react]: http://facebook.github.io/react/ - [documentation]: http://react-bootstrap.github.io [contributing]: CONTRIBUTING.md - [build-badge]: https://travis-ci.org/react-bootstrap/react-bootstrap.svg?branch=master [build]: https://travis-ci.org/react-bootstrap/react-bootstrap - [npm-badge]: https://badge.fury.io/js/react-bootstrap.svg [npm]: http://badge.fury.io/js/react-bootstrap - [react-router-bootstrap]: https://github.com/react-bootstrap/react-router-bootstrap [react-router]: https://github.com/reactjs/react-router [react-bootstrap-extended]: https://github.com/rbalicki2/react-bootstrap-extended [awesome-react-bootstrap-components]: https://github.com/Hermanya/awesome-react-bootstrap-components - [codecov-badge]: https://img.shields.io/codecov/c/github/react-bootstrap/react-bootstrap/master.svg [codecov]: https://codecov.io/gh/react-bootstrap/react-bootstrap - [discord-badge]: https://img.shields.io/badge/Discord-Join%20chat%20%E2%86%92-738bd7.svg [discord]: https://discord.gg/0ZcbPKXt5bXLs9XK - [netlify-badge]: https://api.netlify.com/api/v1/badges/a74fbeb8-f950-4c97-854d-7c8363bef45e/deploy-status [netlify]: https://app.netlify.com/sites/react-bootstrap-v3/deploys diff --git a/fork/react-bootstrap/package.json b/fork/react-bootstrap/package.json index 308c560a669..e7efe577b7a 100644 --- a/fork/react-bootstrap/package.json +++ b/fork/react-bootstrap/package.json @@ -20,7 +20,11 @@ "build:lib:esm": "talend-scripts build --esm", "build:lib": "talend-scripts build", "lint": "talend-scripts lint", - "test": "talend-scripts test", + "lint:fix": "talend-scripts lint --fix", + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage", "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook" }, @@ -46,22 +50,26 @@ "devDependencies": { "@talend/eslint-config": "^13.5.0", "@talend/eslint-plugin": "^1.6.0", - "@talend/scripts-config-typescript": "^11.4.0", - "@talend/scripts-core": "^16.8.0", "@talend/scripts-config-babel": "^13.8.0", "@talend/scripts-config-react-webpack": "^16.11.0", + "@talend/scripts-config-typescript": "^11.4.0", + "@talend/scripts-core": "^16.8.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^14.3.1", "@testing-library/user-event": "^14.6.1", + "@vitejs/plugin-react": "^5.1.2", + "@vitest/ui": "^4.0.17", "chai": "^4.5.0", "chalk": "^2.4.2", "create-react-class": "^15.7.0", "cross-env": "^7.0.3", + "jsdom": "^27.4.0", "lodash": "^4.17.21", "react": "^18.3.1", "react-dom": "^18.3.1", "react-test-renderer": "^18.3.1", - "sinon": "^11.1.2" + "sinon": "^11.1.2", + "vitest": "^4.0.17" }, "dependencies": { "classnames": "^2.5.1", diff --git a/fork/react-bootstrap/server/ModalSpec.js b/fork/react-bootstrap/server/ModalSpec.js index 3d288929f4c..f630499adc3 100644 --- a/fork/react-bootstrap/server/ModalSpec.js +++ b/fork/react-bootstrap/server/ModalSpec.js @@ -6,15 +6,15 @@ import ReactDOMServer from 'react-dom/server'; import Modal from '../src/Modal'; describe('Modal', () => { - it('Should be rendered on the server side', () => { - const noOp = () => {}; + it('Should be rendered on the server side', () => { + const noOp = () => {}; - assert.doesNotThrow(() => - ReactDOMServer.renderToString( - - Message - , - ), - ); - }); + assert.doesNotThrow(() => + ReactDOMServer.renderToString( + + Message + , + ), + ); + }); }); diff --git a/fork/react-bootstrap/src/Accordion.js b/fork/react-bootstrap/src/Accordion.jsx similarity index 51% rename from fork/react-bootstrap/src/Accordion.js rename to fork/react-bootstrap/src/Accordion.jsx index 6cd5981df57..0444a82e138 100644 --- a/fork/react-bootstrap/src/Accordion.js +++ b/fork/react-bootstrap/src/Accordion.jsx @@ -3,13 +3,13 @@ import React from 'react'; import PanelGroup from './PanelGroup'; class Accordion extends React.Component { - render() { - return ( - - {this.props.children} - - ); - } + render() { + return ( + + {this.props.children} + + ); + } } export default Accordion; diff --git a/fork/react-bootstrap/src/Alert.js b/fork/react-bootstrap/src/Alert.js deleted file mode 100644 index 26ecfa4b6c5..00000000000 --- a/fork/react-bootstrap/src/Alert.js +++ /dev/null @@ -1,55 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; - -import { - bsClass, - bsStyles, - getClassSet, - prefix, - splitBsProps -} from './utils/bootstrapUtils'; -import { State } from './utils/StyleConfig'; -import CloseButton from './CloseButton'; - -const propTypes = { - onDismiss: PropTypes.func, - closeLabel: PropTypes.string -}; - -const defaultProps = { - closeLabel: 'Close alert' -}; - -class Alert extends React.Component { - render() { - const { onDismiss, closeLabel, className, children, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const dismissable = !!onDismiss; - const classes = { - ...getClassSet(bsProps), - [prefix(bsProps, 'dismissable')]: dismissable - }; - - return ( -
- {dismissable && } - {children} -
- ); - } -} - -Alert.propTypes = propTypes; -Alert.defaultProps = defaultProps; - -export default bsStyles( - Object.values(State), - State.INFO, - bsClass('alert', Alert) -); diff --git a/fork/react-bootstrap/src/Alert.jsx b/fork/react-bootstrap/src/Alert.jsx new file mode 100644 index 00000000000..85288b71c68 --- /dev/null +++ b/fork/react-bootstrap/src/Alert.jsx @@ -0,0 +1,41 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import { bsClass, bsStyles, getClassSet, prefix, splitBsProps } from './utils/bootstrapUtils'; +import { State } from './utils/StyleConfig'; +import CloseButton from './CloseButton'; + +const propTypes = { + onDismiss: PropTypes.func, + closeLabel: PropTypes.string, +}; + +const defaultProps = { + closeLabel: 'Close alert', +}; + +class Alert extends React.Component { + render() { + const { onDismiss, closeLabel, className, children, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const dismissable = !!onDismiss; + const classes = { + ...getClassSet(bsProps), + [prefix(bsProps, 'dismissable')]: dismissable, + }; + + return ( +
+ {dismissable && } + {children} +
+ ); + } +} + +Alert.propTypes = propTypes; +Alert.defaultProps = defaultProps; + +export default bsStyles(Object.values(State), State.INFO, bsClass('alert', Alert)); diff --git a/fork/react-bootstrap/src/Alert.test.js b/fork/react-bootstrap/src/Alert.test.jsx similarity index 100% rename from fork/react-bootstrap/src/Alert.test.js rename to fork/react-bootstrap/src/Alert.test.jsx diff --git a/fork/react-bootstrap/src/Badge.js b/fork/react-bootstrap/src/Badge.js deleted file mode 100644 index 7533b32841a..00000000000 --- a/fork/react-bootstrap/src/Badge.js +++ /dev/null @@ -1,57 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; - -import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; - -// TODO: `pullRight` doesn't belong here. There's no special handling here. - -const propTypes = { - pullRight: PropTypes.bool -}; - -const defaultProps = { - pullRight: false -}; - -class Badge extends React.Component { - hasContent(children) { - let result = false; - - React.Children.forEach(children, child => { - if (result) { - return; - } - - if (child || child === 0) { - result = true; - } - }); - - return result; - } - - render() { - const { pullRight, className, children, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = { - ...getClassSet(bsProps), - 'pull-right': pullRight, - - // Hack for collapsing on IE8. - hidden: !this.hasContent(children) - }; - - return ( - - {children} - - ); - } -} - -Badge.propTypes = propTypes; -Badge.defaultProps = defaultProps; - -export default bsClass('badge', Badge); diff --git a/fork/react-bootstrap/src/Badge.jsx b/fork/react-bootstrap/src/Badge.jsx new file mode 100644 index 00000000000..143ea46fa9f --- /dev/null +++ b/fork/react-bootstrap/src/Badge.jsx @@ -0,0 +1,57 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; + +// TODO: `pullRight` doesn't belong here. There's no special handling here. + +const propTypes = { + pullRight: PropTypes.bool, +}; + +const defaultProps = { + pullRight: false, +}; + +class Badge extends React.Component { + hasContent(children) { + let result = false; + + React.Children.forEach(children, child => { + if (result) { + return; + } + + if (child || child === 0) { + result = true; + } + }); + + return result; + } + + render() { + const { pullRight, className, children, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = { + ...getClassSet(bsProps), + 'pull-right': pullRight, + + // Hack for collapsing on IE8. + hidden: !this.hasContent(children), + }; + + return ( + + {children} + + ); + } +} + +Badge.propTypes = propTypes; +Badge.defaultProps = defaultProps; + +export default bsClass('badge', Badge); diff --git a/fork/react-bootstrap/src/Badge.test.js b/fork/react-bootstrap/src/Badge.test.js deleted file mode 100644 index a1a15749e5e..00000000000 --- a/fork/react-bootstrap/src/Badge.test.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; - -import Badge from './Badge'; - -describe('', () => { - it('Should output a badge with content', () => { - // when - render( - - Content - - ); - - // then - expect(screen.getByText('Content')).toBeInTheDocument(); - }); - - it('Should have a badge class', () => { - // when - render( - - Content - - ); - - // then - expect(screen.getByTestId('test')).toHaveClass('badge'); - }); - - it('Should have a badge class pulled right', () => { - // when - render( - - Content - - ); - - // then - expect(screen.getByTestId('test')).toHaveClass('pull-right'); - }); - - describe('Hides when empty', () => { - it('should hide with no children', () => { - // when - render(); - - // then - expect(screen.getByTestId('test')).toHaveClass('hidden'); - }); - - it('should not hide 0', () => { - // when - render({0}); - - // then - expect(screen.getByTestId('test')).not.toHaveClass('hidden'); - }); - }); -}); diff --git a/fork/react-bootstrap/src/Badge.test.jsx b/fork/react-bootstrap/src/Badge.test.jsx new file mode 100644 index 00000000000..aae99c463d2 --- /dev/null +++ b/fork/react-bootstrap/src/Badge.test.jsx @@ -0,0 +1,59 @@ +import { render, screen } from '@testing-library/react'; + +import Badge from './Badge'; + +describe('', () => { + it('Should output a badge with content', () => { + // when + render( + + Content + , + ); + + // then + expect(screen.getByText('Content')).toBeInTheDocument(); + }); + + it('Should have a badge class', () => { + // when + render( + + Content + , + ); + + // then + expect(screen.getByTestId('test')).toHaveClass('badge'); + }); + + it('Should have a badge class pulled right', () => { + // when + render( + + Content + , + ); + + // then + expect(screen.getByTestId('test')).toHaveClass('pull-right'); + }); + + describe('Hides when empty', () => { + it('should hide with no children', () => { + // when + render(); + + // then + expect(screen.getByTestId('test')).toHaveClass('hidden'); + }); + + it('should not hide 0', () => { + // when + render({0}); + + // then + expect(screen.getByTestId('test')).not.toHaveClass('hidden'); + }); + }); +}); diff --git a/fork/react-bootstrap/src/Breadcrumb.js b/fork/react-bootstrap/src/Breadcrumb.js deleted file mode 100644 index 2be980ecb20..00000000000 --- a/fork/react-bootstrap/src/Breadcrumb.js +++ /dev/null @@ -1,27 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; - -import BreadcrumbItem from './BreadcrumbItem'; -import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; - -class Breadcrumb extends React.Component { - render() { - const { className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = getClassSet(bsProps); - - return ( -
    - ); - } -} - -Breadcrumb.Item = BreadcrumbItem; - -export default bsClass('breadcrumb', Breadcrumb); diff --git a/fork/react-bootstrap/src/Breadcrumb.jsx b/fork/react-bootstrap/src/Breadcrumb.jsx new file mode 100644 index 00000000000..ba8dfffd816 --- /dev/null +++ b/fork/react-bootstrap/src/Breadcrumb.jsx @@ -0,0 +1,27 @@ +import classNames from 'classnames'; +import React from 'react'; + +import BreadcrumbItem from './BreadcrumbItem'; +import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; + +class Breadcrumb extends React.Component { + render() { + const { className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = getClassSet(bsProps); + + return ( +
      + ); + } +} + +Breadcrumb.Item = BreadcrumbItem; + +export default bsClass('breadcrumb', Breadcrumb); diff --git a/fork/react-bootstrap/src/Breadcrumb.test.js b/fork/react-bootstrap/src/Breadcrumb.test.js deleted file mode 100644 index d2a87ac9ad6..00000000000 --- a/fork/react-bootstrap/src/Breadcrumb.test.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; - -import Breadcrumb from './Breadcrumb'; - -describe('', () => { - it('Should apply id to the wrapper ol element', () => { - // when - render(); - - // then - expect(screen.getByRole('navigation')).toHaveAttribute('id', 'custom-id'); - }); - - it('Should have breadcrumb class', () => { - // when - render(); - - // then - expect(screen.getByRole('navigation')).toHaveClass('breadcrumb'); - }); - - it('Should have custom classes', () => { - // when - render(); - - // then - expect(screen.getByRole('navigation')).toHaveClass('breadcrumb'); - expect(screen.getByRole('navigation')).toHaveClass('custom-one'); - expect(screen.getByRole('navigation')).toHaveClass('custom-two'); - }); - - it('Should have an aria-label in ol', () => { - // when - render(); - - // then - expect(screen.getByRole('navigation')).toHaveAttribute( - 'aria-label', - 'breadcrumbs' - ); - }); -}); diff --git a/fork/react-bootstrap/src/Breadcrumb.test.jsx b/fork/react-bootstrap/src/Breadcrumb.test.jsx new file mode 100644 index 00000000000..50f037523ae --- /dev/null +++ b/fork/react-bootstrap/src/Breadcrumb.test.jsx @@ -0,0 +1,39 @@ +import { render, screen } from '@testing-library/react'; + +import Breadcrumb from './Breadcrumb'; + +describe('', () => { + it('Should apply id to the wrapper ol element', () => { + // when + render(); + + // then + expect(screen.getByRole('navigation')).toHaveAttribute('id', 'custom-id'); + }); + + it('Should have breadcrumb class', () => { + // when + render(); + + // then + expect(screen.getByRole('navigation')).toHaveClass('breadcrumb'); + }); + + it('Should have custom classes', () => { + // when + render(); + + // then + expect(screen.getByRole('navigation')).toHaveClass('breadcrumb'); + expect(screen.getByRole('navigation')).toHaveClass('custom-one'); + expect(screen.getByRole('navigation')).toHaveClass('custom-two'); + }); + + it('Should have an aria-label in ol', () => { + // when + render(); + + // then + expect(screen.getByRole('navigation')).toHaveAttribute('aria-label', 'breadcrumbs'); + }); +}); diff --git a/fork/react-bootstrap/src/BreadcrumbItem.js b/fork/react-bootstrap/src/BreadcrumbItem.js deleted file mode 100644 index aad14b85dbc..00000000000 --- a/fork/react-bootstrap/src/BreadcrumbItem.js +++ /dev/null @@ -1,52 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; - -import SafeAnchor from './SafeAnchor'; - -const propTypes = { - /** - * If set to true, renders `span` instead of `a` - */ - active: PropTypes.bool, - /** - * `href` attribute for the inner `a` element - */ - href: PropTypes.string, - /** - * `title` attribute for the inner `a` element - */ - title: PropTypes.node, - /** - * `target` attribute for the inner `a` element - */ - target: PropTypes.string -}; - -const defaultProps = { - active: false -}; - -class BreadcrumbItem extends React.Component { - render() { - const { active, href, title, target, className, ...props } = this.props; - - // Don't try to render these props on non-active . - const linkProps = { href, title, target }; - - return ( -
    1. - {active ? ( - - ) : ( - - )} -
    2. - ); - } -} - -BreadcrumbItem.propTypes = propTypes; -BreadcrumbItem.defaultProps = defaultProps; - -export default BreadcrumbItem; diff --git a/fork/react-bootstrap/src/BreadcrumbItem.jsx b/fork/react-bootstrap/src/BreadcrumbItem.jsx new file mode 100644 index 00000000000..caaf7942846 --- /dev/null +++ b/fork/react-bootstrap/src/BreadcrumbItem.jsx @@ -0,0 +1,48 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import SafeAnchor from './SafeAnchor'; + +const propTypes = { + /** + * If set to true, renders `span` instead of `a` + */ + active: PropTypes.bool, + /** + * `href` attribute for the inner `a` element + */ + href: PropTypes.string, + /** + * `title` attribute for the inner `a` element + */ + title: PropTypes.node, + /** + * `target` attribute for the inner `a` element + */ + target: PropTypes.string, +}; + +const defaultProps = { + active: false, +}; + +class BreadcrumbItem extends React.Component { + render() { + const { active, href, title, target, className, ...props } = this.props; + + // Don't try to render these props on non-active . + const linkProps = { href, title, target }; + + return ( +
    3. + {active ? : } +
    4. + ); + } +} + +BreadcrumbItem.propTypes = propTypes; +BreadcrumbItem.defaultProps = defaultProps; + +export default BreadcrumbItem; diff --git a/fork/react-bootstrap/src/BreadcrumbItem.test.js b/fork/react-bootstrap/src/BreadcrumbItem.test.jsx similarity index 100% rename from fork/react-bootstrap/src/BreadcrumbItem.test.js rename to fork/react-bootstrap/src/BreadcrumbItem.test.jsx diff --git a/fork/react-bootstrap/src/Button.js b/fork/react-bootstrap/src/Button.js deleted file mode 100644 index 03761cc050a..00000000000 --- a/fork/react-bootstrap/src/Button.js +++ /dev/null @@ -1,92 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import elementType from 'prop-types-extra/lib/elementType'; - -import { - bsClass, - bsSizes, - bsStyles, - getClassSet, - prefix, - splitBsProps -} from './utils/bootstrapUtils'; -import { Size, State, Style } from './utils/StyleConfig'; - -import SafeAnchor from './SafeAnchor'; - -const propTypes = { - active: PropTypes.bool, - disabled: PropTypes.bool, - block: PropTypes.bool, - onClick: PropTypes.func, - componentClass: elementType, - href: PropTypes.string, - /** - * Defines HTML button type attribute - * @defaultValue 'button' - */ - type: PropTypes.oneOf(['button', 'reset', 'submit']) -}; - -const defaultProps = { - active: false, - block: false, - disabled: false -}; - -class Button extends React.Component { - renderAnchor(elementProps, className) { - return ( - - ); - } - - renderButton({ componentClass, ...elementProps }, className) { - const Component = componentClass || 'button'; - - return ( - - ); - } - - render() { - const { active, block, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = { - ...getClassSet(bsProps), - active, - [prefix(bsProps, 'block')]: block - }; - const fullClassName = classNames(className, classes); - - if (elementProps.href) { - return this.renderAnchor(elementProps, fullClassName); - } - - return this.renderButton(elementProps, fullClassName); - } -} - -Button.propTypes = propTypes; -Button.defaultProps = defaultProps; - -export default bsClass( - 'btn', - bsSizes( - [Size.LARGE, Size.SMALL, Size.XSMALL], - bsStyles( - [...Object.values(State), Style.DEFAULT, Style.PRIMARY, Style.LINK], - Style.DEFAULT, - Button - ) - ) -); diff --git a/fork/react-bootstrap/src/Button.jsx b/fork/react-bootstrap/src/Button.jsx new file mode 100644 index 00000000000..ba76fc00eda --- /dev/null +++ b/fork/react-bootstrap/src/Button.jsx @@ -0,0 +1,88 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import elementType from 'prop-types-extra/lib/elementType'; + +import { + bsClass, + bsSizes, + bsStyles, + getClassSet, + prefix, + splitBsProps, +} from './utils/bootstrapUtils'; +import { Size, State, Style } from './utils/StyleConfig'; + +import SafeAnchor from './SafeAnchor'; + +const propTypes = { + active: PropTypes.bool, + disabled: PropTypes.bool, + block: PropTypes.bool, + onClick: PropTypes.func, + componentClass: elementType, + href: PropTypes.string, + /** + * Defines HTML button type attribute + * @defaultValue 'button' + */ + type: PropTypes.oneOf(['button', 'reset', 'submit']), +}; + +const defaultProps = { + active: false, + block: false, + disabled: false, +}; + +class Button extends React.Component { + renderAnchor(elementProps, className) { + return ( + + ); + } + + renderButton({ componentClass, ...elementProps }, className) { + const Component = componentClass || 'button'; + + return ( + + ); + } + + render() { + const { active, block, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = { + ...getClassSet(bsProps), + active, + [prefix(bsProps, 'block')]: block, + }; + const fullClassName = classNames(className, classes); + + if (elementProps.href) { + return this.renderAnchor(elementProps, fullClassName); + } + + return this.renderButton(elementProps, fullClassName); + } +} + +Button.propTypes = propTypes; +Button.defaultProps = defaultProps; + +export default bsClass( + 'btn', + bsSizes( + [Size.LARGE, Size.SMALL, Size.XSMALL], + bsStyles( + [...Object.values(State), Style.DEFAULT, Style.PRIMARY, Style.LINK], + Style.DEFAULT, + Button, + ), + ), +); diff --git a/fork/react-bootstrap/src/Button.test.js b/fork/react-bootstrap/src/Button.test.jsx similarity index 100% rename from fork/react-bootstrap/src/Button.test.js rename to fork/react-bootstrap/src/Button.test.jsx diff --git a/fork/react-bootstrap/src/ButtonGroup.js b/fork/react-bootstrap/src/ButtonGroup.js deleted file mode 100644 index 1ea2a68845a..00000000000 --- a/fork/react-bootstrap/src/ButtonGroup.js +++ /dev/null @@ -1,59 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import all from 'prop-types-extra/lib/all'; - -import Button from './Button'; -import { - bsClass, - getClassSet, - prefix, - splitBsProps -} from './utils/bootstrapUtils'; - -const propTypes = { - vertical: PropTypes.bool, - justified: PropTypes.bool, - - /** - * Display block buttons; only useful when used with the "vertical" prop. - * @type {bool} - */ - block: all( - PropTypes.bool, - ({ block, vertical }) => - block && !vertical - ? new Error('`block` requires `vertical` to be set to have any effect') - : null - ) -}; - -const defaultProps = { - block: false, - justified: false, - vertical: false -}; - -class ButtonGroup extends React.Component { - render() { - const { block, justified, vertical, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = { - ...getClassSet(bsProps), - [prefix(bsProps)]: !vertical, - [prefix(bsProps, 'vertical')]: vertical, - [prefix(bsProps, 'justified')]: justified, - - // this is annoying, since the class is `btn-block` not `btn-group-block` - [prefix(Button.defaultProps, 'block')]: block - }; - - return
      ; - } -} - -ButtonGroup.propTypes = propTypes; -ButtonGroup.defaultProps = defaultProps; - -export default bsClass('btn-group', ButtonGroup); diff --git a/fork/react-bootstrap/src/ButtonGroup.jsx b/fork/react-bootstrap/src/ButtonGroup.jsx new file mode 100644 index 00000000000..41cea46f582 --- /dev/null +++ b/fork/react-bootstrap/src/ButtonGroup.jsx @@ -0,0 +1,52 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import all from 'prop-types-extra/lib/all'; + +import Button from './Button'; +import { bsClass, getClassSet, prefix, splitBsProps } from './utils/bootstrapUtils'; + +const propTypes = { + vertical: PropTypes.bool, + justified: PropTypes.bool, + + /** + * Display block buttons; only useful when used with the "vertical" prop. + * @type {bool} + */ + block: all(PropTypes.bool, ({ block, vertical }) => + block && !vertical + ? new Error('`block` requires `vertical` to be set to have any effect') + : null, + ), +}; + +const defaultProps = { + block: false, + justified: false, + vertical: false, +}; + +class ButtonGroup extends React.Component { + render() { + const { block, justified, vertical, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = { + ...getClassSet(bsProps), + [prefix(bsProps)]: !vertical, + [prefix(bsProps, 'vertical')]: vertical, + [prefix(bsProps, 'justified')]: justified, + + // this is annoying, since the class is `btn-block` not `btn-group-block` + [prefix(Button.defaultProps, 'block')]: block, + }; + + return
      ; + } +} + +ButtonGroup.propTypes = propTypes; +ButtonGroup.defaultProps = defaultProps; + +export default bsClass('btn-group', ButtonGroup); diff --git a/fork/react-bootstrap/src/ButtonGroup.test.js b/fork/react-bootstrap/src/ButtonGroup.test.js deleted file mode 100644 index 746f7a1b428..00000000000 --- a/fork/react-bootstrap/src/ButtonGroup.test.js +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; - -import ButtonGroup from './ButtonGroup'; -import Button from './Button'; - -describe('ButtonGroup', () => { - it('Should output a button group', () => { - // when - render( - - - - ); - - // then - const buttonGroup = screen.getByRole('button').parentElement; - expect(buttonGroup).toBeInTheDocument(); - expect(buttonGroup).toHaveClass('btn-group'); - }); - - it('Should add size', () => { - // when - render( - - - - ); - - // then - const buttonGroup = screen.getByRole('button').parentElement; - expect(buttonGroup).toHaveClass('btn-group-lg'); - }); - - it('Should add vertical variation', () => { - // when - render( - - - - ); - - // then - const buttonGroup = screen.getByRole('button').parentElement; - expect(buttonGroup).toHaveClass('btn-group-vertical'); - }); - - it('Should add block variation', () => { - // when - render( - - - - ); - - // then - const buttonGroup = screen.getByRole('button').parentElement; - expect(buttonGroup).toHaveClass('btn-block'); - }); - - it('Should warn about block without vertical', () => { - // given - console.error = jest.fn(); - - // when - render( - - - - ); - - // then - expect(console.error.mock.calls[0]).toContain( - '`block` requires `vertical` to be set to have any effect' - ); - }); - - it('Should add justified variation', () => { - // when - render( - - - - ); - - // then - const buttonGroup = screen.getByRole('button').parentElement; - expect(buttonGroup).toHaveClass('btn-group-justified'); - }); -}); diff --git a/fork/react-bootstrap/src/ButtonGroup.test.jsx b/fork/react-bootstrap/src/ButtonGroup.test.jsx new file mode 100644 index 00000000000..a0c62509a2c --- /dev/null +++ b/fork/react-bootstrap/src/ButtonGroup.test.jsx @@ -0,0 +1,89 @@ +import { render, screen } from '@testing-library/react'; + +import ButtonGroup from './ButtonGroup'; +import Button from './Button'; + +describe('ButtonGroup', () => { + it('Should output a button group', () => { + // when + render( + + + , + ); + + // then + const buttonGroup = screen.getByRole('button').parentElement; + expect(buttonGroup).toBeInTheDocument(); + expect(buttonGroup).toHaveClass('btn-group'); + }); + + it('Should add size', () => { + // when + render( + + + , + ); + + // then + const buttonGroup = screen.getByRole('button').parentElement; + expect(buttonGroup).toHaveClass('btn-group-lg'); + }); + + it('Should add vertical variation', () => { + // when + render( + + + , + ); + + // then + const buttonGroup = screen.getByRole('button').parentElement; + expect(buttonGroup).toHaveClass('btn-group-vertical'); + }); + + it('Should add block variation', () => { + // when + render( + + + , + ); + + // then + const buttonGroup = screen.getByRole('button').parentElement; + expect(buttonGroup).toHaveClass('btn-block'); + }); + + it('Should warn about block without vertical', () => { + // given + console.error = jest.fn(); + + // when + render( + + + , + ); + + // then + expect(console.error.mock.calls[0]).toContain( + '`block` requires `vertical` to be set to have any effect', + ); + }); + + it('Should add justified variation', () => { + // when + render( + + + , + ); + + // then + const buttonGroup = screen.getByRole('button').parentElement; + expect(buttonGroup).toHaveClass('btn-group-justified'); + }); +}); diff --git a/fork/react-bootstrap/src/ButtonToolbar.js b/fork/react-bootstrap/src/ButtonToolbar.js deleted file mode 100644 index 4a0fb176019..00000000000 --- a/fork/react-bootstrap/src/ButtonToolbar.js +++ /dev/null @@ -1,23 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; - -import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; - -class ButtonToolbar extends React.Component { - render() { - const { className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = getClassSet(bsProps); - - return ( -
      - ); - } -} - -export default bsClass('btn-toolbar', ButtonToolbar); diff --git a/fork/react-bootstrap/src/ButtonToolbar.jsx b/fork/react-bootstrap/src/ButtonToolbar.jsx new file mode 100644 index 00000000000..61dd5926a19 --- /dev/null +++ b/fork/react-bootstrap/src/ButtonToolbar.jsx @@ -0,0 +1,17 @@ +import classNames from 'classnames'; +import React from 'react'; + +import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; + +class ButtonToolbar extends React.Component { + render() { + const { className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = getClassSet(bsProps); + + return
      ; + } +} + +export default bsClass('btn-toolbar', ButtonToolbar); diff --git a/fork/react-bootstrap/src/ButtonToolbar.test.js b/fork/react-bootstrap/src/ButtonToolbar.test.js deleted file mode 100644 index ea895a37aa3..00000000000 --- a/fork/react-bootstrap/src/ButtonToolbar.test.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; - -import Button from './Button'; -import ButtonGroup from './ButtonGroup'; -import ButtonToolbar from './ButtonToolbar'; - -describe('ButtonToolbar', () => { - it('Should output a button toolbar', () => { - // when - render( - - - - - - ); - - // then - const buttonToolbar = screen.getByRole('toolbar'); - expect(buttonToolbar).toBeInTheDocument(); - expect(buttonToolbar).toHaveClass('btn-toolbar'); - }); -}); diff --git a/fork/react-bootstrap/src/ButtonToolbar.test.jsx b/fork/react-bootstrap/src/ButtonToolbar.test.jsx new file mode 100644 index 00000000000..898c82fe916 --- /dev/null +++ b/fork/react-bootstrap/src/ButtonToolbar.test.jsx @@ -0,0 +1,23 @@ +import { render, screen } from '@testing-library/react'; + +import Button from './Button'; +import ButtonGroup from './ButtonGroup'; +import ButtonToolbar from './ButtonToolbar'; + +describe('ButtonToolbar', () => { + it('Should output a button toolbar', () => { + // when + render( + + + + + , + ); + + // then + const buttonToolbar = screen.getByRole('toolbar'); + expect(buttonToolbar).toBeInTheDocument(); + expect(buttonToolbar).toHaveClass('btn-toolbar'); + }); +}); diff --git a/fork/react-bootstrap/src/Carousel.js b/fork/react-bootstrap/src/Carousel.js deleted file mode 100644 index cb8dd57902d..00000000000 --- a/fork/react-bootstrap/src/Carousel.js +++ /dev/null @@ -1,416 +0,0 @@ -import classNames from 'classnames'; -import React, { cloneElement } from 'react'; -import PropTypes from 'prop-types'; - -import CarouselCaption from './CarouselCaption'; -import CarouselItem from './CarouselItem'; -import Glyphicon from './Glyphicon'; -import SafeAnchor from './SafeAnchor'; -import { - bsClass, - getClassSet, - prefix, - splitBsPropsAndOmit, -} from './utils/bootstrapUtils'; -import ValidComponentChildren from './utils/ValidComponentChildren'; - -// TODO: `slide` should be `animate`. - -// TODO: Use uncontrollable. - -const propTypes = { - slide: PropTypes.bool, - indicators: PropTypes.bool, - /** - * The amount of time to delay between automatically cycling an item. - * If `null`, carousel will not automatically cycle. - */ - interval: PropTypes.number, - controls: PropTypes.bool, - pauseOnHover: PropTypes.bool, - wrap: PropTypes.bool, - /** - * Callback fired when the active item changes. - * - * ```js - * (eventKey: any, ?event: Object) => any - * ``` - * - * If this callback takes two or more arguments, the second argument will - * be a persisted event object with `direction` set to the direction of the - * transition. - */ - onSelect: PropTypes.func, - onSlideEnd: PropTypes.func, - activeIndex: PropTypes.number, - defaultActiveIndex: PropTypes.number, - direction: PropTypes.oneOf(['prev', 'next']), - prevIcon: PropTypes.node, - /** - * Label shown to screen readers only, can be used to show the previous element - * in the carousel. - * Set to null to deactivate. - */ - prevLabel: PropTypes.string, - nextIcon: PropTypes.node, - /** - * Label shown to screen readers only, can be used to show the next element - * in the carousel. - * Set to null to deactivate. - */ - nextLabel: PropTypes.string, -}; - -const defaultProps = { - slide: true, - interval: 5000, - pauseOnHover: true, - wrap: true, - indicators: true, - controls: true, - prevIcon: , - prevLabel: 'Previous', - nextIcon: , - nextLabel: 'Next', -}; - -class Carousel extends React.Component { - constructor(props, context) { - super(props, context); - - this.handleMouseOver = this.handleMouseOver.bind(this); - this.handleMouseOut = this.handleMouseOut.bind(this); - this.handlePrev = this.handlePrev.bind(this); - this.handleNext = this.handleNext.bind(this); - this.handleItemAnimateOutEnd = this.handleItemAnimateOutEnd.bind(this); - - const { defaultActiveIndex } = props; - - this.state = { - activeIndex: defaultActiveIndex != null ? defaultActiveIndex : 0, - previousActiveIndex: null, - direction: null, - }; - - this.isUnmounted = false; - } - - componentDidMount() { - this.waitForNext(); - } - - componentDidUpdate(prevProps) { - // eslint-disable-line - const activeIndex = this.getActiveIndex(); - - if ( - this.props.activeIndex != null && - this.props.activeIndex !== activeIndex - ) { - clearTimeout(this.timeout); - - this.setState({ - previousActiveIndex: activeIndex, - direction: - this.props.direction != null - ? this.props.direction - : this.getDirection(activeIndex, this.props.activeIndex), - }); - } - - if ( - this.props.activeIndex == null && - this.state.activeIndex >= this.props.children.length - ) { - this.setState({ - activeIndex: 0, - previousActiveIndex: null, - direction: null, - }); - } - } - - componentWillUnmount() { - clearTimeout(this.timeout); - this.isUnmounted = true; - } - - getActiveIndex() { - const activeIndexProp = this.props.activeIndex; - return activeIndexProp != null ? activeIndexProp : this.state.activeIndex; - } - - getDirection(prevIndex, index) { - if (prevIndex === index) { - return null; - } - - return prevIndex > index ? 'prev' : 'next'; - } - - handleItemAnimateOutEnd() { - this.setState( - { - previousActiveIndex: null, - direction: null, - }, - () => { - this.waitForNext(); - - if (this.props.onSlideEnd) { - this.props.onSlideEnd(); - } - } - ); - } - - handleMouseOut() { - if (this.isPaused) { - this.play(); - } - } - - handleMouseOver() { - if (this.props.pauseOnHover) { - this.pause(); - } - } - - handleNext(e) { - let index = this.getActiveIndex() + 1; - const count = ValidComponentChildren.count(this.props.children); - - if (index > count - 1) { - if (!this.props.wrap) { - return; - } - index = 0; - } - - this.select(index, e, 'next'); - } - - handlePrev(e) { - let index = this.getActiveIndex() - 1; - if (index < 0) { - if (!this.props.wrap) { - return; - } - index = ValidComponentChildren.count(this.props.children) - 1; - } - - this.select(index, e, 'prev'); - } - - // This might be a public API. - pause() { - this.isPaused = true; - clearTimeout(this.timeout); - } - - // This might be a public API. - play() { - this.isPaused = false; - this.waitForNext(); - } - - select(index, e, direction) { - clearTimeout(this.timeout); - - // TODO: Is this necessary? Seems like the only risk is if the component - // unmounts while handleItemAnimateOutEnd fires. - if (this.isUnmounted) { - return; - } - - const previousActiveIndex = this.props.slide ? this.getActiveIndex() : null; - direction = direction || this.getDirection(previousActiveIndex, index); - - const { onSelect } = this.props; - - if (onSelect) { - if (onSelect.length > 1) { - // React SyntheticEvents are pooled, so we need to remove this event - // from the pool to add a custom property. To avoid unnecessarily - // removing objects from the pool, only do this when the listener - // actually wants the event. - if (e) { - e.persist(); - e.direction = direction; - } else { - e = { direction }; - } - - onSelect(index, e); - } else { - onSelect(index); - } - } - - if (this.props.activeIndex == null && index !== previousActiveIndex) { - if (this.state.previousActiveIndex != null) { - // If currently animating don't activate the new index. - // TODO: look into queueing this canceled call and - // animating after the current animation has ended. - return; - } - - this.setState({ - activeIndex: index, - previousActiveIndex, - direction, - }); - } - } - - waitForNext() { - const { slide, interval, activeIndex: activeIndexProp } = this.props; - - if (!this.isPaused && slide && interval && activeIndexProp == null) { - this.timeout = setTimeout(this.handleNext, interval); - } - } - - renderControls(properties) { - const { - wrap, - children, - activeIndex, - prevIcon, - nextIcon, - bsProps, - prevLabel, - nextLabel, - } = properties; - const controlClassName = prefix(bsProps, 'control'); - const count = ValidComponentChildren.count(children); - - return [ - (wrap || activeIndex !== 0) && ( - - {prevIcon} - {prevLabel && {prevLabel}} - - ), - - (wrap || activeIndex !== count - 1) && ( - - {nextIcon} - {nextLabel && {nextLabel}} - - ), - ]; - } - - renderIndicators(children, activeIndex, bsProps) { - let indicators = []; - - ValidComponentChildren.forEach(children, (child, index) => { - indicators.push( -
    5. this.select(index, e)} - />, - - // Force whitespace between indicator elements. Bootstrap requires - // this for correct spacing of elements. - ' ' - ); - }); - - return
        {indicators}
      ; - } - - render() { - const { - slide, - indicators, - controls, - wrap, - prevIcon, - prevLabel, - nextIcon, - nextLabel, - className, - children, - ...props - } = this.props; - - const { previousActiveIndex, direction } = this.state; - - const [bsProps, elementProps] = splitBsPropsAndOmit(props, [ - 'interval', - 'pauseOnHover', - 'onSelect', - 'onSlideEnd', - 'activeIndex', // Accessed via this.getActiveIndex(). - 'defaultActiveIndex', - 'direction', - ]); - - const activeIndex = this.getActiveIndex(); - - const classes = { - ...getClassSet(bsProps), - slide, - }; - - return ( -
      - {indicators && this.renderIndicators(children, activeIndex, bsProps)} - -
      - {ValidComponentChildren.map(children, (child, index) => { - const active = index === activeIndex; - const previousActive = slide && index === previousActiveIndex; - - return cloneElement(child, { - active, - index, - animateOut: previousActive, - animateIn: active && previousActiveIndex != null && slide, - direction, - onAnimateOutEnd: previousActive - ? this.handleItemAnimateOutEnd - : null, - }); - })} -
      - - {controls && - this.renderControls({ - wrap, - children, - activeIndex, - prevIcon, - prevLabel, - nextIcon, - nextLabel, - bsProps, - })} -
      - ); - } -} - -Carousel.propTypes = propTypes; -Carousel.defaultProps = defaultProps; - -Carousel.Caption = CarouselCaption; -Carousel.Item = CarouselItem; - -export default bsClass('carousel', Carousel); diff --git a/fork/react-bootstrap/src/Carousel.jsx b/fork/react-bootstrap/src/Carousel.jsx new file mode 100644 index 00000000000..f137294ace6 --- /dev/null +++ b/fork/react-bootstrap/src/Carousel.jsx @@ -0,0 +1,395 @@ +import classNames from 'classnames'; +import React, { cloneElement } from 'react'; +import PropTypes from 'prop-types'; + +import CarouselCaption from './CarouselCaption'; +import CarouselItem from './CarouselItem'; +import Glyphicon from './Glyphicon'; +import SafeAnchor from './SafeAnchor'; +import { bsClass, getClassSet, prefix, splitBsPropsAndOmit } from './utils/bootstrapUtils'; +import ValidComponentChildren from './utils/ValidComponentChildren'; + +// TODO: `slide` should be `animate`. + +// TODO: Use uncontrollable. + +const propTypes = { + slide: PropTypes.bool, + indicators: PropTypes.bool, + /** + * The amount of time to delay between automatically cycling an item. + * If `null`, carousel will not automatically cycle. + */ + interval: PropTypes.number, + controls: PropTypes.bool, + pauseOnHover: PropTypes.bool, + wrap: PropTypes.bool, + /** + * Callback fired when the active item changes. + * + * ```js + * (eventKey: any, ?event: Object) => any + * ``` + * + * If this callback takes two or more arguments, the second argument will + * be a persisted event object with `direction` set to the direction of the + * transition. + */ + onSelect: PropTypes.func, + onSlideEnd: PropTypes.func, + activeIndex: PropTypes.number, + defaultActiveIndex: PropTypes.number, + direction: PropTypes.oneOf(['prev', 'next']), + prevIcon: PropTypes.node, + /** + * Label shown to screen readers only, can be used to show the previous element + * in the carousel. + * Set to null to deactivate. + */ + prevLabel: PropTypes.string, + nextIcon: PropTypes.node, + /** + * Label shown to screen readers only, can be used to show the next element + * in the carousel. + * Set to null to deactivate. + */ + nextLabel: PropTypes.string, +}; + +const defaultProps = { + slide: true, + interval: 5000, + pauseOnHover: true, + wrap: true, + indicators: true, + controls: true, + prevIcon: , + prevLabel: 'Previous', + nextIcon: , + nextLabel: 'Next', +}; + +class Carousel extends React.Component { + constructor(props, context) { + super(props, context); + + this.handleMouseOver = this.handleMouseOver.bind(this); + this.handleMouseOut = this.handleMouseOut.bind(this); + this.handlePrev = this.handlePrev.bind(this); + this.handleNext = this.handleNext.bind(this); + this.handleItemAnimateOutEnd = this.handleItemAnimateOutEnd.bind(this); + + const { defaultActiveIndex } = props; + + this.state = { + activeIndex: defaultActiveIndex != null ? defaultActiveIndex : 0, + previousActiveIndex: null, + direction: null, + }; + + this.isUnmounted = false; + } + + componentDidMount() { + this.waitForNext(); + } + + componentDidUpdate(prevProps) { + // eslint-disable-line + const activeIndex = this.getActiveIndex(); + + if (this.props.activeIndex != null && this.props.activeIndex !== activeIndex) { + clearTimeout(this.timeout); + + this.setState({ + previousActiveIndex: activeIndex, + direction: + this.props.direction != null + ? this.props.direction + : this.getDirection(activeIndex, this.props.activeIndex), + }); + } + + if (this.props.activeIndex == null && this.state.activeIndex >= this.props.children.length) { + this.setState({ + activeIndex: 0, + previousActiveIndex: null, + direction: null, + }); + } + } + + componentWillUnmount() { + clearTimeout(this.timeout); + this.isUnmounted = true; + } + + getActiveIndex() { + const activeIndexProp = this.props.activeIndex; + return activeIndexProp != null ? activeIndexProp : this.state.activeIndex; + } + + getDirection(prevIndex, index) { + if (prevIndex === index) { + return null; + } + + return prevIndex > index ? 'prev' : 'next'; + } + + handleItemAnimateOutEnd() { + this.setState( + { + previousActiveIndex: null, + direction: null, + }, + () => { + this.waitForNext(); + + if (this.props.onSlideEnd) { + this.props.onSlideEnd(); + } + }, + ); + } + + handleMouseOut() { + if (this.isPaused) { + this.play(); + } + } + + handleMouseOver() { + if (this.props.pauseOnHover) { + this.pause(); + } + } + + handleNext(e) { + let index = this.getActiveIndex() + 1; + const count = ValidComponentChildren.count(this.props.children); + + if (index > count - 1) { + if (!this.props.wrap) { + return; + } + index = 0; + } + + this.select(index, e, 'next'); + } + + handlePrev(e) { + let index = this.getActiveIndex() - 1; + if (index < 0) { + if (!this.props.wrap) { + return; + } + index = ValidComponentChildren.count(this.props.children) - 1; + } + + this.select(index, e, 'prev'); + } + + // This might be a public API. + pause() { + this.isPaused = true; + clearTimeout(this.timeout); + } + + // This might be a public API. + play() { + this.isPaused = false; + this.waitForNext(); + } + + select(index, e, direction) { + clearTimeout(this.timeout); + + // TODO: Is this necessary? Seems like the only risk is if the component + // unmounts while handleItemAnimateOutEnd fires. + if (this.isUnmounted) { + return; + } + + const previousActiveIndex = this.props.slide ? this.getActiveIndex() : null; + direction = direction || this.getDirection(previousActiveIndex, index); + + const { onSelect } = this.props; + + if (onSelect) { + if (onSelect.length > 1) { + // React SyntheticEvents are pooled, so we need to remove this event + // from the pool to add a custom property. To avoid unnecessarily + // removing objects from the pool, only do this when the listener + // actually wants the event. + if (e) { + e.persist(); + e.direction = direction; + } else { + e = { direction }; + } + + onSelect(index, e); + } else { + onSelect(index); + } + } + + if (this.props.activeIndex == null && index !== previousActiveIndex) { + if (this.state.previousActiveIndex != null) { + // If currently animating don't activate the new index. + // TODO: look into queueing this canceled call and + // animating after the current animation has ended. + return; + } + + this.setState({ + activeIndex: index, + previousActiveIndex, + direction, + }); + } + } + + waitForNext() { + const { slide, interval, activeIndex: activeIndexProp } = this.props; + + if (!this.isPaused && slide && interval && activeIndexProp == null) { + this.timeout = setTimeout(this.handleNext, interval); + } + } + + renderControls(properties) { + const { wrap, children, activeIndex, prevIcon, nextIcon, bsProps, prevLabel, nextLabel } = + properties; + const controlClassName = prefix(bsProps, 'control'); + const count = ValidComponentChildren.count(children); + + return [ + (wrap || activeIndex !== 0) && ( + + {prevIcon} + {prevLabel && {prevLabel}} + + ), + + (wrap || activeIndex !== count - 1) && ( + + {nextIcon} + {nextLabel && {nextLabel}} + + ), + ]; + } + + renderIndicators(children, activeIndex, bsProps) { + let indicators = []; + + ValidComponentChildren.forEach(children, (child, index) => { + indicators.push( +
    6. this.select(index, e)} + />, + + // Force whitespace between indicator elements. Bootstrap requires + // this for correct spacing of elements. + ' ', + ); + }); + + return
        {indicators}
      ; + } + + render() { + const { + slide, + indicators, + controls, + wrap, + prevIcon, + prevLabel, + nextIcon, + nextLabel, + className, + children, + ...props + } = this.props; + + const { previousActiveIndex, direction } = this.state; + + const [bsProps, elementProps] = splitBsPropsAndOmit(props, [ + 'interval', + 'pauseOnHover', + 'onSelect', + 'onSlideEnd', + 'activeIndex', // Accessed via this.getActiveIndex(). + 'defaultActiveIndex', + 'direction', + ]); + + const activeIndex = this.getActiveIndex(); + + const classes = { + ...getClassSet(bsProps), + slide, + }; + + return ( +
      + {indicators && this.renderIndicators(children, activeIndex, bsProps)} + +
      + {ValidComponentChildren.map(children, (child, index) => { + const active = index === activeIndex; + const previousActive = slide && index === previousActiveIndex; + + return cloneElement(child, { + active, + index, + animateOut: previousActive, + animateIn: active && previousActiveIndex != null && slide, + direction, + onAnimateOutEnd: previousActive ? this.handleItemAnimateOutEnd : null, + }); + })} +
      + + {controls && + this.renderControls({ + wrap, + children, + activeIndex, + prevIcon, + prevLabel, + nextIcon, + nextLabel, + bsProps, + })} +
      + ); + } +} + +Carousel.propTypes = propTypes; +Carousel.defaultProps = defaultProps; + +Carousel.Caption = CarouselCaption; +Carousel.Item = CarouselItem; + +export default bsClass('carousel', Carousel); diff --git a/fork/react-bootstrap/src/Carousel.test.js b/fork/react-bootstrap/src/Carousel.test.jsx similarity index 99% rename from fork/react-bootstrap/src/Carousel.test.js rename to fork/react-bootstrap/src/Carousel.test.jsx index a908f0de81d..4f387ba539e 100644 --- a/fork/react-bootstrap/src/Carousel.test.js +++ b/fork/react-bootstrap/src/Carousel.test.jsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; diff --git a/fork/react-bootstrap/src/CarouselCaption.js b/fork/react-bootstrap/src/CarouselCaption.jsx similarity index 56% rename from fork/react-bootstrap/src/CarouselCaption.js rename to fork/react-bootstrap/src/CarouselCaption.jsx index 0ec62805c9b..97b87cdf89e 100644 --- a/fork/react-bootstrap/src/CarouselCaption.js +++ b/fork/react-bootstrap/src/CarouselCaption.jsx @@ -5,24 +5,22 @@ import elementType from 'prop-types-extra/lib/elementType'; import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; const propTypes = { - componentClass: elementType + componentClass: elementType, }; const defaultProps = { - componentClass: 'div' + componentClass: 'div', }; class CarouselCaption extends React.Component { - render() { - const { componentClass: Component, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); + render() { + const { componentClass: Component, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); - const classes = getClassSet(bsProps); + const classes = getClassSet(bsProps); - return ( - - ); - } + return ; + } } CarouselCaption.propTypes = propTypes; diff --git a/fork/react-bootstrap/src/CarouselCaption.test.js b/fork/react-bootstrap/src/CarouselCaption.test.js deleted file mode 100644 index 2d618c9090b..00000000000 --- a/fork/react-bootstrap/src/CarouselCaption.test.js +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; - -import Carousel from './Carousel'; - -describe('', () => { - it('uses "div" by default', () => { - // when - render(Carousel.Caption content); - - // then - const caption = screen.getByText('Carousel.Caption content'); - expect(caption).toBeInTheDocument(); - expect(caption.tagName).toBe('DIV'); - }); - - it('has "carousel-caption" class', () => { - // when - render(Carousel.Caption content); - - // then - const caption = screen.getByText('Carousel.Caption content'); - expect(caption).toHaveClass('carousel-caption'); - }); - - it('Should merge additional classes passed in', () => { - // when - render( - - Carousel.Caption content - - ); - - // then - const caption = screen.getByText('Carousel.Caption content'); - expect(caption).toHaveClass('carousel-caption'); - }); - - it('allows custom elements instead of "div"', () => { - // given - render( - - Carousel.Caption content - - ); - - // then - const caption = screen.getByText('Carousel.Caption content'); - expect(caption.tagName).toBe('SECTION'); - }); -}); diff --git a/fork/react-bootstrap/src/CarouselCaption.test.jsx b/fork/react-bootstrap/src/CarouselCaption.test.jsx new file mode 100644 index 00000000000..18a03a8df5b --- /dev/null +++ b/fork/react-bootstrap/src/CarouselCaption.test.jsx @@ -0,0 +1,42 @@ +import { render, screen } from '@testing-library/react'; + +import Carousel from './Carousel'; + +describe('', () => { + it('uses "div" by default', () => { + // when + render(Carousel.Caption content); + + // then + const caption = screen.getByText('Carousel.Caption content'); + expect(caption).toBeInTheDocument(); + expect(caption.tagName).toBe('DIV'); + }); + + it('has "carousel-caption" class', () => { + // when + render(Carousel.Caption content); + + // then + const caption = screen.getByText('Carousel.Caption content'); + expect(caption).toHaveClass('carousel-caption'); + }); + + it('Should merge additional classes passed in', () => { + // when + render(Carousel.Caption content); + + // then + const caption = screen.getByText('Carousel.Caption content'); + expect(caption).toHaveClass('carousel-caption'); + }); + + it('allows custom elements instead of "div"', () => { + // given + render(Carousel.Caption content); + + // then + const caption = screen.getByText('Carousel.Caption content'); + expect(caption.tagName).toBe('SECTION'); + }); +}); diff --git a/fork/react-bootstrap/src/CarouselItem.js b/fork/react-bootstrap/src/CarouselItem.js deleted file mode 100644 index d703cf14c35..00000000000 --- a/fork/react-bootstrap/src/CarouselItem.js +++ /dev/null @@ -1,104 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; -import transition from 'dom-helpers/transition'; - -const propTypes = { - direction: PropTypes.oneOf(['prev', 'next']), - onAnimateOutEnd: PropTypes.func, - active: PropTypes.bool, - animateIn: PropTypes.bool, - animateOut: PropTypes.bool, - index: PropTypes.number, -}; - -const defaultProps = { - active: false, - animateIn: false, - animateOut: false, -}; - -class CarouselItem extends React.Component { - constructor(props, context) { - super(props, context); - - this.handleAnimateOutEnd = this.handleAnimateOutEnd.bind(this); - - this.state = { - direction: null, - }; - - this.isUnmounted = false; - } - - componentDidUpdate(prevProps) { - // eslint-disable-line - if (this.props.active !== prevProps.active) { - this.setState({ direction: null }); - } - } - - componentDidUpdate(prevProps) { - const { active } = this.props; - const prevActive = prevProps.active; - - if (!active && prevActive) { - transition.end(ReactDOM.findDOMNode(this), this.handleAnimateOutEnd); - } - - if (active !== prevActive) { - setTimeout(() => this.startAnimation(), 20); - } - } - - componentWillUnmount() { - this.isUnmounted = true; - } - - handleAnimateOutEnd() { - if (this.isUnmounted) { - return; - } - - if (this.props.onAnimateOutEnd) { - this.props.onAnimateOutEnd(this.props.index); - } - } - - startAnimation() { - if (this.isUnmounted) { - return; - } - - this.setState({ - direction: this.props.direction === 'prev' ? 'right' : 'left', - }); - } - - render() { - const { direction, active, animateIn, animateOut, className, ...props } = - this.props; - - delete props.onAnimateOutEnd; - delete props.index; - - const classes = { - item: true, - active: (active && !animateIn) || animateOut, - }; - if (direction && active && animateIn) { - classes[direction] = true; - } - if (this.state.direction && (animateIn || animateOut)) { - classes[this.state.direction] = true; - } - - return
      ; - } -} - -CarouselItem.propTypes = propTypes; -CarouselItem.defaultProps = defaultProps; - -export default CarouselItem; diff --git a/fork/react-bootstrap/src/CarouselItem.jsx b/fork/react-bootstrap/src/CarouselItem.jsx new file mode 100644 index 00000000000..8c7ca3c064e --- /dev/null +++ b/fork/react-bootstrap/src/CarouselItem.jsx @@ -0,0 +1,103 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import transition from 'dom-helpers/transition'; + +const propTypes = { + direction: PropTypes.oneOf(['prev', 'next']), + onAnimateOutEnd: PropTypes.func, + active: PropTypes.bool, + animateIn: PropTypes.bool, + animateOut: PropTypes.bool, + index: PropTypes.number, +}; + +const defaultProps = { + active: false, + animateIn: false, + animateOut: false, +}; + +class CarouselItem extends React.Component { + constructor(props, context) { + super(props, context); + + this.handleAnimateOutEnd = this.handleAnimateOutEnd.bind(this); + + this.state = { + direction: null, + }; + + this.isUnmounted = false; + } + + componentDidUpdate(prevProps) { + // eslint-disable-line + if (this.props.active !== prevProps.active) { + this.setState({ direction: null }); + } + } + + componentDidUpdate(prevProps) { + const { active } = this.props; + const prevActive = prevProps.active; + + if (!active && prevActive) { + transition.end(ReactDOM.findDOMNode(this), this.handleAnimateOutEnd); + } + + if (active !== prevActive) { + setTimeout(() => this.startAnimation(), 20); + } + } + + componentWillUnmount() { + this.isUnmounted = true; + } + + handleAnimateOutEnd() { + if (this.isUnmounted) { + return; + } + + if (this.props.onAnimateOutEnd) { + this.props.onAnimateOutEnd(this.props.index); + } + } + + startAnimation() { + if (this.isUnmounted) { + return; + } + + this.setState({ + direction: this.props.direction === 'prev' ? 'right' : 'left', + }); + } + + render() { + const { direction, active, animateIn, animateOut, className, ...props } = this.props; + + delete props.onAnimateOutEnd; + delete props.index; + + const classes = { + item: true, + active: (active && !animateIn) || animateOut, + }; + if (direction && active && animateIn) { + classes[direction] = true; + } + if (this.state.direction && (animateIn || animateOut)) { + classes[this.state.direction] = true; + } + + return
      ; + } +} + +CarouselItem.propTypes = propTypes; +CarouselItem.defaultProps = defaultProps; + +export default CarouselItem; diff --git a/fork/react-bootstrap/src/Checkbox.js b/fork/react-bootstrap/src/Checkbox.js deleted file mode 100644 index d46c38630e5..00000000000 --- a/fork/react-bootstrap/src/Checkbox.js +++ /dev/null @@ -1,113 +0,0 @@ -/* eslint-disable jsx-a11y/label-has-for */ - -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import warning from 'warning'; - -import { - bsClass, - getClassSet, - prefix, - splitBsProps -} from './utils/bootstrapUtils'; - -const propTypes = { - inline: PropTypes.bool, - disabled: PropTypes.bool, - title: PropTypes.string, - /** - * Only valid if `inline` is not set. - */ - validationState: PropTypes.oneOf(['success', 'warning', 'error', null]), - /** - * Attaches a ref to the `` element. Only functions can be used here. - * - * ```js - * { this.input = ref; }} /> - * ``` - */ - inputRef: PropTypes.func -}; - -const defaultProps = { - inline: false, - disabled: false, - title: '' -}; - -class Checkbox extends React.Component { - render() { - const { - inline, - disabled, - validationState, - inputRef, - className, - style, - title, - children, - ...props - } = this.props; - - const [bsProps, elementProps] = splitBsProps(props); - - const input = ( - - ); - - if (inline) { - const classes = { - [prefix(bsProps, 'inline')]: true, - disabled - }; - - // Use a warning here instead of in propTypes to get better-looking - // generated documentation. - warning( - !validationState, - '`validationState` is ignored on ``. To display ' + - 'validation state on an inline checkbox, set `validationState` on a ' + - 'parent `` or other element instead.' - ); - - return ( - - ); - } - - const classes = { - ...getClassSet(bsProps), - disabled - }; - if (validationState) { - classes[`has-${validationState}`] = true; - } - - return ( -
      - -
      - ); - } -} - -Checkbox.propTypes = propTypes; -Checkbox.defaultProps = defaultProps; - -export default bsClass('checkbox', Checkbox); diff --git a/fork/react-bootstrap/src/Checkbox.jsx b/fork/react-bootstrap/src/Checkbox.jsx new file mode 100644 index 00000000000..b0293e7c807 --- /dev/null +++ b/fork/react-bootstrap/src/Checkbox.jsx @@ -0,0 +1,97 @@ +/* eslint-disable jsx-a11y/label-has-for */ + +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import warning from 'warning'; + +import { bsClass, getClassSet, prefix, splitBsProps } from './utils/bootstrapUtils'; + +const propTypes = { + inline: PropTypes.bool, + disabled: PropTypes.bool, + title: PropTypes.string, + /** + * Only valid if `inline` is not set. + */ + validationState: PropTypes.oneOf(['success', 'warning', 'error', null]), + /** + * Attaches a ref to the `` element. Only functions can be used here. + * + * ```js + * { this.input = ref; }} /> + * ``` + */ + inputRef: PropTypes.func, +}; + +const defaultProps = { + inline: false, + disabled: false, + title: '', +}; + +class Checkbox extends React.Component { + render() { + const { + inline, + disabled, + validationState, + inputRef, + className, + style, + title, + children, + ...props + } = this.props; + + const [bsProps, elementProps] = splitBsProps(props); + + const input = ; + + if (inline) { + const classes = { + [prefix(bsProps, 'inline')]: true, + disabled, + }; + + // Use a warning here instead of in propTypes to get better-looking + // generated documentation. + warning( + !validationState, + '`validationState` is ignored on ``. To display ' + + 'validation state on an inline checkbox, set `validationState` on a ' + + 'parent `` or other element instead.', + ); + + return ( + + ); + } + + const classes = { + ...getClassSet(bsProps), + disabled, + }; + if (validationState) { + classes[`has-${validationState}`] = true; + } + + return ( +
      + +
      + ); + } +} + +Checkbox.propTypes = propTypes; +Checkbox.defaultProps = defaultProps; + +export default bsClass('checkbox', Checkbox); diff --git a/fork/react-bootstrap/src/Checkbox.test.js b/fork/react-bootstrap/src/Checkbox.test.js deleted file mode 100644 index 076e32f2bb8..00000000000 --- a/fork/react-bootstrap/src/Checkbox.test.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; - -import Checkbox from './Checkbox'; - -describe('', () => { - it('should render correctly', () => { - // when - render( - - My label - - ); - - // then - const checkbox = screen.getByRole('checkbox', { name: 'My label' }); - expect(checkbox).toBeInTheDocument(); - expect(checkbox.parentElement.parentElement).toHaveClass('my-checkbox'); - expect(checkbox).toBeChecked(); - expect(checkbox).toHaveAttribute('name', 'foo'); - }); - - it('should support inline', () => { - // when - render( - - My label - - ); - - // then - const checkbox = screen.getByRole('checkbox', { name: 'My label' }); - expect(checkbox).toBeInTheDocument(); - expect(checkbox.parentElement).toHaveClass('checkbox-inline'); - }); - - it('should support validation state', () => { - // when - render(); - - // then - const checkbox = screen.getByRole('checkbox'); - expect(checkbox.parentElement.parentElement).toHaveClass('has-success'); - }); - - it('should not support validation state when inline', () => { - // given - console.error = jest.fn(); - - // when - render(); - - // then - const checkbox = screen.getByRole('checkbox'); - expect(checkbox.parentElement.parentElement).not.toHaveClass('has-success'); - expect(console.error).toHaveBeenCalledWith( - 'Warning: `validationState` is ignored on ``. To display validation state on an inline checkbox, set `validationState` on a parent `` or other element instead.' - ); - }); - - it('should support inputRef', () => { - // given - let input; - - // when - render( - { - input = ref; - }} - /> - ); - - expect(input.tagName).toBe('INPUT'); - }); -}); diff --git a/fork/react-bootstrap/src/Checkbox.test.jsx b/fork/react-bootstrap/src/Checkbox.test.jsx new file mode 100644 index 00000000000..c2c93b8a357 --- /dev/null +++ b/fork/react-bootstrap/src/Checkbox.test.jsx @@ -0,0 +1,75 @@ +import { render, screen } from '@testing-library/react'; + +import Checkbox from './Checkbox'; + +describe('', () => { + it('should render correctly', () => { + // when + render( + + My label + , + ); + + // then + const checkbox = screen.getByRole('checkbox', { name: 'My label' }); + expect(checkbox).toBeInTheDocument(); + expect(checkbox.parentElement.parentElement).toHaveClass('my-checkbox'); + expect(checkbox).toBeChecked(); + expect(checkbox).toHaveAttribute('name', 'foo'); + }); + + it('should support inline', () => { + // when + render( + + My label + , + ); + + // then + const checkbox = screen.getByRole('checkbox', { name: 'My label' }); + expect(checkbox).toBeInTheDocument(); + expect(checkbox.parentElement).toHaveClass('checkbox-inline'); + }); + + it('should support validation state', () => { + // when + render(); + + // then + const checkbox = screen.getByRole('checkbox'); + expect(checkbox.parentElement.parentElement).toHaveClass('has-success'); + }); + + it('should not support validation state when inline', () => { + // given + console.error = jest.fn(); + + // when + render(); + + // then + const checkbox = screen.getByRole('checkbox'); + expect(checkbox.parentElement.parentElement).not.toHaveClass('has-success'); + expect(console.error).toHaveBeenCalledWith( + 'Warning: `validationState` is ignored on ``. To display validation state on an inline checkbox, set `validationState` on a parent `` or other element instead.', + ); + }); + + it('should support inputRef', () => { + // given + let input; + + // when + render( + { + input = ref; + }} + />, + ); + + expect(input.tagName).toBe('INPUT'); + }); +}); diff --git a/fork/react-bootstrap/src/Clearfix.js b/fork/react-bootstrap/src/Clearfix.js deleted file mode 100644 index 69f1638c69a..00000000000 --- a/fork/react-bootstrap/src/Clearfix.js +++ /dev/null @@ -1,76 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import elementType from 'prop-types-extra/lib/elementType'; - -import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; -import capitalize from './utils/capitalize'; -import { DEVICE_SIZES } from './utils/StyleConfig'; - -const propTypes = { - componentClass: elementType, - - /** - * Apply clearfix - * - * on Extra small devices Phones - * - * adds class `visible-xs-block` - */ - visibleXsBlock: PropTypes.bool, - /** - * Apply clearfix - * - * on Small devices Tablets - * - * adds class `visible-sm-block` - */ - visibleSmBlock: PropTypes.bool, - /** - * Apply clearfix - * - * on Medium devices Desktops - * - * adds class `visible-md-block` - */ - visibleMdBlock: PropTypes.bool, - /** - * Apply clearfix - * - * on Large devices Desktops - * - * adds class `visible-lg-block` - */ - visibleLgBlock: PropTypes.bool -}; - -const defaultProps = { - componentClass: 'div' -}; - -class Clearfix extends React.Component { - render() { - const { componentClass: Component, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = getClassSet(bsProps); - - DEVICE_SIZES.forEach(size => { - const propName = `visible${capitalize(size)}Block`; - if (elementProps[propName]) { - classes[`visible-${size}-block`] = true; - } - - delete elementProps[propName]; - }); - - return ( - - ); - } -} - -Clearfix.propTypes = propTypes; -Clearfix.defaultProps = defaultProps; - -export default bsClass('clearfix', Clearfix); diff --git a/fork/react-bootstrap/src/Clearfix.jsx b/fork/react-bootstrap/src/Clearfix.jsx new file mode 100644 index 00000000000..baf5b7f0cad --- /dev/null +++ b/fork/react-bootstrap/src/Clearfix.jsx @@ -0,0 +1,74 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import elementType from 'prop-types-extra/lib/elementType'; + +import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; +import capitalize from './utils/capitalize'; +import { DEVICE_SIZES } from './utils/StyleConfig'; + +const propTypes = { + componentClass: elementType, + + /** + * Apply clearfix + * + * on Extra small devices Phones + * + * adds class `visible-xs-block` + */ + visibleXsBlock: PropTypes.bool, + /** + * Apply clearfix + * + * on Small devices Tablets + * + * adds class `visible-sm-block` + */ + visibleSmBlock: PropTypes.bool, + /** + * Apply clearfix + * + * on Medium devices Desktops + * + * adds class `visible-md-block` + */ + visibleMdBlock: PropTypes.bool, + /** + * Apply clearfix + * + * on Large devices Desktops + * + * adds class `visible-lg-block` + */ + visibleLgBlock: PropTypes.bool, +}; + +const defaultProps = { + componentClass: 'div', +}; + +class Clearfix extends React.Component { + render() { + const { componentClass: Component, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = getClassSet(bsProps); + + DEVICE_SIZES.forEach(size => { + const propName = `visible${capitalize(size)}Block`; + if (elementProps[propName]) { + classes[`visible-${size}-block`] = true; + } + + delete elementProps[propName]; + }); + + return ; + } +} + +Clearfix.propTypes = propTypes; +Clearfix.defaultProps = defaultProps; + +export default bsClass('clearfix', Clearfix); diff --git a/fork/react-bootstrap/src/Clearfix.test.js b/fork/react-bootstrap/src/Clearfix.test.js deleted file mode 100644 index 835e37870e8..00000000000 --- a/fork/react-bootstrap/src/Clearfix.test.js +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; - -import Clearfix from './Clearfix'; - -describe('', () => { - it('uses "div" by default', () => { - // when - render(); - - // then - expect(screen.getByTestId('test').tagName).toBe('DIV'); - }); - - it('has "clearfix" class', () => { - // when - render(); - - // then - expect(screen.getByTestId('test')).toHaveClass('clearfix'); - }); - - it('Defaults to no visible block classes', () => { - // when - render(); - - // then - const div = screen.getByTestId('test'); - expect(div).not.toHaveClass('visible-xs-block'); - expect(div).not.toHaveClass('visible-sm-block'); - expect(div).not.toHaveClass('visible-md-block'); - expect(div).not.toHaveClass('visible-lg-block'); - }); - - it('Should apply visible block classes', () => { - render( - - ); - - // then - const div = screen.getByTestId('test'); - expect(div).toHaveClass('visible-xs-block'); - expect(div).toHaveClass('visible-sm-block'); - expect(div).toHaveClass('visible-md-block'); - expect(div).toHaveClass('visible-lg-block'); - }); - - it('Should merge additional classes passed in', () => { - // when - render(); - - // then - expect(screen.getByTestId('test')).toHaveClass('clearfix bob'); - }); - - it('allows custom elements instead of "div"', () => { - // when - render(); - - // then - expect(screen.getByTestId('test').tagName).toBe('SECTION'); - }); -}); diff --git a/fork/react-bootstrap/src/Clearfix.test.jsx b/fork/react-bootstrap/src/Clearfix.test.jsx new file mode 100644 index 00000000000..8a977234e87 --- /dev/null +++ b/fork/react-bootstrap/src/Clearfix.test.jsx @@ -0,0 +1,62 @@ +import { render, screen } from '@testing-library/react'; + +import Clearfix from './Clearfix'; + +describe('', () => { + it('uses "div" by default', () => { + // when + render(); + + // then + expect(screen.getByTestId('test').tagName).toBe('DIV'); + }); + + it('has "clearfix" class', () => { + // when + render(); + + // then + expect(screen.getByTestId('test')).toHaveClass('clearfix'); + }); + + it('Defaults to no visible block classes', () => { + // when + render(); + + // then + const div = screen.getByTestId('test'); + expect(div).not.toHaveClass('visible-xs-block'); + expect(div).not.toHaveClass('visible-sm-block'); + expect(div).not.toHaveClass('visible-md-block'); + expect(div).not.toHaveClass('visible-lg-block'); + }); + + it('Should apply visible block classes', () => { + render( + , + ); + + // then + const div = screen.getByTestId('test'); + expect(div).toHaveClass('visible-xs-block'); + expect(div).toHaveClass('visible-sm-block'); + expect(div).toHaveClass('visible-md-block'); + expect(div).toHaveClass('visible-lg-block'); + }); + + it('Should merge additional classes passed in', () => { + // when + render(); + + // then + expect(screen.getByTestId('test')).toHaveClass('clearfix bob'); + }); + + it('allows custom elements instead of "div"', () => { + // when + render(); + + // then + expect(screen.getByTestId('test').tagName).toBe('SECTION'); + }); +}); diff --git a/fork/react-bootstrap/src/CloseButton.js b/fork/react-bootstrap/src/CloseButton.js deleted file mode 100644 index c31abaeae5c..00000000000 --- a/fork/react-bootstrap/src/CloseButton.js +++ /dev/null @@ -1,28 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -const propTypes = { - label: PropTypes.string.isRequired, - onClick: PropTypes.func -}; - -const defaultProps = { - label: 'Close' -}; - -class CloseButton extends React.Component { - render() { - const { label, onClick } = this.props; - return ( - - ); - } -} - -CloseButton.propTypes = propTypes; -CloseButton.defaultProps = defaultProps; - -export default CloseButton; diff --git a/fork/react-bootstrap/src/CloseButton.jsx b/fork/react-bootstrap/src/CloseButton.jsx new file mode 100644 index 00000000000..b383d2a61b9 --- /dev/null +++ b/fork/react-bootstrap/src/CloseButton.jsx @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +const propTypes = { + label: PropTypes.string.isRequired, + onClick: PropTypes.func, +}; + +const defaultProps = { + label: 'Close', +}; + +class CloseButton extends React.Component { + render() { + const { label, onClick } = this.props; + return ( + + ); + } +} + +CloseButton.propTypes = propTypes; +CloseButton.defaultProps = defaultProps; + +export default CloseButton; diff --git a/fork/react-bootstrap/src/CloseButton.test.js b/fork/react-bootstrap/src/CloseButton.test.jsx similarity index 98% rename from fork/react-bootstrap/src/CloseButton.test.js rename to fork/react-bootstrap/src/CloseButton.test.jsx index fe1fdedbc5a..c3e39d8af36 100644 --- a/fork/react-bootstrap/src/CloseButton.test.js +++ b/fork/react-bootstrap/src/CloseButton.test.jsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; diff --git a/fork/react-bootstrap/src/Col.js b/fork/react-bootstrap/src/Col.js deleted file mode 100644 index ea599b43bf7..00000000000 --- a/fork/react-bootstrap/src/Col.js +++ /dev/null @@ -1,218 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import elementType from 'prop-types-extra/lib/elementType'; - -import { bsClass, prefix, splitBsProps } from './utils/bootstrapUtils'; -import { DEVICE_SIZES } from './utils/StyleConfig'; - -const propTypes = { - componentClass: elementType, - - /** - * The number of columns you wish to span - * - * for Extra small devices Phones (<768px) - * - * class-prefix `col-xs-` - */ - xs: PropTypes.number, - /** - * The number of columns you wish to span - * - * for Small devices Tablets (≥768px) - * - * class-prefix `col-sm-` - */ - sm: PropTypes.number, - /** - * The number of columns you wish to span - * - * for Medium devices Desktops (≥992px) - * - * class-prefix `col-md-` - */ - md: PropTypes.number, - /** - * The number of columns you wish to span - * - * for Large devices Desktops (≥1200px) - * - * class-prefix `col-lg-` - */ - lg: PropTypes.number, - /** - * Hide column - * - * on Extra small devices Phones - * - * adds class `hidden-xs` - */ - xsHidden: PropTypes.bool, - /** - * Hide column - * - * on Small devices Tablets - * - * adds class `hidden-sm` - */ - smHidden: PropTypes.bool, - /** - * Hide column - * - * on Medium devices Desktops - * - * adds class `hidden-md` - */ - mdHidden: PropTypes.bool, - /** - * Hide column - * - * on Large devices Desktops - * - * adds class `hidden-lg` - */ - lgHidden: PropTypes.bool, - /** - * Move columns to the right - * - * for Extra small devices Phones - * - * class-prefix `col-xs-offset-` - */ - xsOffset: PropTypes.number, - /** - * Move columns to the right - * - * for Small devices Tablets - * - * class-prefix `col-sm-offset-` - */ - smOffset: PropTypes.number, - /** - * Move columns to the right - * - * for Medium devices Desktops - * - * class-prefix `col-md-offset-` - */ - mdOffset: PropTypes.number, - /** - * Move columns to the right - * - * for Large devices Desktops - * - * class-prefix `col-lg-offset-` - */ - lgOffset: PropTypes.number, - /** - * Change the order of grid columns to the right - * - * for Extra small devices Phones - * - * class-prefix `col-xs-push-` - */ - xsPush: PropTypes.number, - /** - * Change the order of grid columns to the right - * - * for Small devices Tablets - * - * class-prefix `col-sm-push-` - */ - smPush: PropTypes.number, - /** - * Change the order of grid columns to the right - * - * for Medium devices Desktops - * - * class-prefix `col-md-push-` - */ - mdPush: PropTypes.number, - /** - * Change the order of grid columns to the right - * - * for Large devices Desktops - * - * class-prefix `col-lg-push-` - */ - lgPush: PropTypes.number, - /** - * Change the order of grid columns to the left - * - * for Extra small devices Phones - * - * class-prefix `col-xs-pull-` - */ - xsPull: PropTypes.number, - /** - * Change the order of grid columns to the left - * - * for Small devices Tablets - * - * class-prefix `col-sm-pull-` - */ - smPull: PropTypes.number, - /** - * Change the order of grid columns to the left - * - * for Medium devices Desktops - * - * class-prefix `col-md-pull-` - */ - mdPull: PropTypes.number, - /** - * Change the order of grid columns to the left - * - * for Large devices Desktops - * - * class-prefix `col-lg-pull-` - */ - lgPull: PropTypes.number -}; - -const defaultProps = { - componentClass: 'div' -}; - -class Col extends React.Component { - render() { - const { componentClass: Component, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = []; - - DEVICE_SIZES.forEach(size => { - function popProp(propSuffix, modifier) { - const propName = `${size}${propSuffix}`; - const propValue = elementProps[propName]; - - if (propValue != null) { - classes.push(prefix(bsProps, `${size}${modifier}-${propValue}`)); - } - - delete elementProps[propName]; - } - - popProp('', ''); - popProp('Offset', '-offset'); - popProp('Push', '-push'); - popProp('Pull', '-pull'); - - const hiddenPropName = `${size}Hidden`; - if (elementProps[hiddenPropName]) { - classes.push(`hidden-${size}`); - } - delete elementProps[hiddenPropName]; - }); - - return ( - - ); - } -} - -Col.propTypes = propTypes; -Col.defaultProps = defaultProps; - -export default bsClass('col', Col); diff --git a/fork/react-bootstrap/src/Col.jsx b/fork/react-bootstrap/src/Col.jsx new file mode 100644 index 00000000000..5ba41b7db42 --- /dev/null +++ b/fork/react-bootstrap/src/Col.jsx @@ -0,0 +1,216 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import elementType from 'prop-types-extra/lib/elementType'; + +import { bsClass, prefix, splitBsProps } from './utils/bootstrapUtils'; +import { DEVICE_SIZES } from './utils/StyleConfig'; + +const propTypes = { + componentClass: elementType, + + /** + * The number of columns you wish to span + * + * for Extra small devices Phones (<768px) + * + * class-prefix `col-xs-` + */ + xs: PropTypes.number, + /** + * The number of columns you wish to span + * + * for Small devices Tablets (≥768px) + * + * class-prefix `col-sm-` + */ + sm: PropTypes.number, + /** + * The number of columns you wish to span + * + * for Medium devices Desktops (≥992px) + * + * class-prefix `col-md-` + */ + md: PropTypes.number, + /** + * The number of columns you wish to span + * + * for Large devices Desktops (≥1200px) + * + * class-prefix `col-lg-` + */ + lg: PropTypes.number, + /** + * Hide column + * + * on Extra small devices Phones + * + * adds class `hidden-xs` + */ + xsHidden: PropTypes.bool, + /** + * Hide column + * + * on Small devices Tablets + * + * adds class `hidden-sm` + */ + smHidden: PropTypes.bool, + /** + * Hide column + * + * on Medium devices Desktops + * + * adds class `hidden-md` + */ + mdHidden: PropTypes.bool, + /** + * Hide column + * + * on Large devices Desktops + * + * adds class `hidden-lg` + */ + lgHidden: PropTypes.bool, + /** + * Move columns to the right + * + * for Extra small devices Phones + * + * class-prefix `col-xs-offset-` + */ + xsOffset: PropTypes.number, + /** + * Move columns to the right + * + * for Small devices Tablets + * + * class-prefix `col-sm-offset-` + */ + smOffset: PropTypes.number, + /** + * Move columns to the right + * + * for Medium devices Desktops + * + * class-prefix `col-md-offset-` + */ + mdOffset: PropTypes.number, + /** + * Move columns to the right + * + * for Large devices Desktops + * + * class-prefix `col-lg-offset-` + */ + lgOffset: PropTypes.number, + /** + * Change the order of grid columns to the right + * + * for Extra small devices Phones + * + * class-prefix `col-xs-push-` + */ + xsPush: PropTypes.number, + /** + * Change the order of grid columns to the right + * + * for Small devices Tablets + * + * class-prefix `col-sm-push-` + */ + smPush: PropTypes.number, + /** + * Change the order of grid columns to the right + * + * for Medium devices Desktops + * + * class-prefix `col-md-push-` + */ + mdPush: PropTypes.number, + /** + * Change the order of grid columns to the right + * + * for Large devices Desktops + * + * class-prefix `col-lg-push-` + */ + lgPush: PropTypes.number, + /** + * Change the order of grid columns to the left + * + * for Extra small devices Phones + * + * class-prefix `col-xs-pull-` + */ + xsPull: PropTypes.number, + /** + * Change the order of grid columns to the left + * + * for Small devices Tablets + * + * class-prefix `col-sm-pull-` + */ + smPull: PropTypes.number, + /** + * Change the order of grid columns to the left + * + * for Medium devices Desktops + * + * class-prefix `col-md-pull-` + */ + mdPull: PropTypes.number, + /** + * Change the order of grid columns to the left + * + * for Large devices Desktops + * + * class-prefix `col-lg-pull-` + */ + lgPull: PropTypes.number, +}; + +const defaultProps = { + componentClass: 'div', +}; + +class Col extends React.Component { + render() { + const { componentClass: Component, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = []; + + DEVICE_SIZES.forEach(size => { + function popProp(propSuffix, modifier) { + const propName = `${size}${propSuffix}`; + const propValue = elementProps[propName]; + + if (propValue != null) { + classes.push(prefix(bsProps, `${size}${modifier}-${propValue}`)); + } + + delete elementProps[propName]; + } + + popProp('', ''); + popProp('Offset', '-offset'); + popProp('Push', '-push'); + popProp('Pull', '-pull'); + + const hiddenPropName = `${size}Hidden`; + if (elementProps[hiddenPropName]) { + classes.push(`hidden-${size}`); + } + delete elementProps[hiddenPropName]; + }); + + return ; + } +} + +Col.propTypes = propTypes; +Col.defaultProps = defaultProps; + +export default bsClass('col', Col); diff --git a/fork/react-bootstrap/src/Col.test.js b/fork/react-bootstrap/src/Col.test.js deleted file mode 100644 index 3a76ee65c79..00000000000 --- a/fork/react-bootstrap/src/Col.test.js +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; -import { assert } from 'chai'; - -import Col from './Col'; - -describe('Col', () => { - it('Should set Offset of zero', () => { - let instance = ReactTestUtils.renderIntoDocument( - - ); - - let instanceClassName = ReactDOM.findDOMNode(instance).className; - assert.ok(instanceClassName.match(/\bcol-xs-offset-0\b/)); - assert.ok(instanceClassName.match(/\bcol-sm-offset-0\b/)); - assert.ok(instanceClassName.match(/\bcol-md-offset-0\b/)); - assert.ok(instanceClassName.match(/\bcol-lg-offset-0\b/)); - }); - - it('Should set Pull of zero', () => { - let instance = ReactTestUtils.renderIntoDocument( - - ); - - let instanceClassName = ReactDOM.findDOMNode(instance).className; - assert.ok(instanceClassName.match(/\bcol-xs-pull-0\b/)); - assert.ok(instanceClassName.match(/\bcol-sm-pull-0\b/)); - assert.ok(instanceClassName.match(/\bcol-md-pull-0\b/)); - assert.ok(instanceClassName.match(/\bcol-lg-pull-0\b/)); - }); - - it('Should set Push of zero', () => { - let instance = ReactTestUtils.renderIntoDocument( - - ); - - let instanceClassName = ReactDOM.findDOMNode(instance).className; - assert.ok(instanceClassName.match(/\bcol-xs-push-0\b/)); - assert.ok(instanceClassName.match(/\bcol-sm-push-0\b/)); - assert.ok(instanceClassName.match(/\bcol-md-push-0\b/)); - assert.ok(instanceClassName.match(/\bcol-lg-push-0\b/)); - }); - - it('Should set Hidden to true', () => { - let instance = ReactTestUtils.renderIntoDocument( - - ); - - let instanceClassName = ReactDOM.findDOMNode(instance).className; - assert.ok(instanceClassName.match(/\bhidden-xs\b/)); - assert.ok(instanceClassName.match(/\bhidden-sm\b/)); - assert.ok(instanceClassName.match(/\bhidden-md\b/)); - assert.ok(instanceClassName.match(/\bhidden-lg\b/)); - }); -}); diff --git a/fork/react-bootstrap/src/Col.test.jsx b/fork/react-bootstrap/src/Col.test.jsx new file mode 100644 index 00000000000..776d5252ba6 --- /dev/null +++ b/fork/react-bootstrap/src/Col.test.jsx @@ -0,0 +1,53 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; +import { assert } from 'chai'; + +import Col from './Col'; + +describe('Col', () => { + it('Should set Offset of zero', () => { + let instance = ReactTestUtils.renderIntoDocument( + , + ); + + let instanceClassName = ReactDOM.findDOMNode(instance).className; + assert.ok(instanceClassName.match(/\bcol-xs-offset-0\b/)); + assert.ok(instanceClassName.match(/\bcol-sm-offset-0\b/)); + assert.ok(instanceClassName.match(/\bcol-md-offset-0\b/)); + assert.ok(instanceClassName.match(/\bcol-lg-offset-0\b/)); + }); + + it('Should set Pull of zero', () => { + let instance = ReactTestUtils.renderIntoDocument( + , + ); + + let instanceClassName = ReactDOM.findDOMNode(instance).className; + assert.ok(instanceClassName.match(/\bcol-xs-pull-0\b/)); + assert.ok(instanceClassName.match(/\bcol-sm-pull-0\b/)); + assert.ok(instanceClassName.match(/\bcol-md-pull-0\b/)); + assert.ok(instanceClassName.match(/\bcol-lg-pull-0\b/)); + }); + + it('Should set Push of zero', () => { + let instance = ReactTestUtils.renderIntoDocument( + , + ); + + let instanceClassName = ReactDOM.findDOMNode(instance).className; + assert.ok(instanceClassName.match(/\bcol-xs-push-0\b/)); + assert.ok(instanceClassName.match(/\bcol-sm-push-0\b/)); + assert.ok(instanceClassName.match(/\bcol-md-push-0\b/)); + assert.ok(instanceClassName.match(/\bcol-lg-push-0\b/)); + }); + + it('Should set Hidden to true', () => { + let instance = ReactTestUtils.renderIntoDocument(); + + let instanceClassName = ReactDOM.findDOMNode(instance).className; + assert.ok(instanceClassName.match(/\bhidden-xs\b/)); + assert.ok(instanceClassName.match(/\bhidden-sm\b/)); + assert.ok(instanceClassName.match(/\bhidden-md\b/)); + assert.ok(instanceClassName.match(/\bhidden-lg\b/)); + }); +}); diff --git a/fork/react-bootstrap/src/Collapse.js b/fork/react-bootstrap/src/Collapse.js deleted file mode 100644 index 1e4f9bfd7da..00000000000 --- a/fork/react-bootstrap/src/Collapse.js +++ /dev/null @@ -1,233 +0,0 @@ -/* eslint-disable arrow-parens */ -import classNames from 'classnames'; -import css from 'dom-helpers/style'; -import React from 'react'; -import PropTypes from 'prop-types'; -import Transition, { - EXITED, - ENTERED, - ENTERING, - EXITING, -} from 'react-transition-group/Transition'; - -import capitalize from './utils/capitalize'; -import createChainedFunction from './utils/createChainedFunction'; - -const MARGINS = { - height: ['marginTop', 'marginBottom'], - width: ['marginLeft', 'marginRight'], -}; - -// reading a dimension prop will cause the browser to recalculate, -// which will let our animations work -function triggerBrowserReflow(node) { - // eslint-disable-next-line mdx/no-unused-expressions - node.offsetHeight; -} - -function getDimensionValue(dimension, elem) { - let value = elem[`offset${capitalize(dimension)}`]; - let margins = MARGINS[dimension]; - - return ( - value + - parseInt(css(elem, margins[0]), 10) + - parseInt(css(elem, margins[1]), 10) - ); -} - -const collapseStyles = { - [EXITED]: 'collapse', - [EXITING]: 'collapsing', - [ENTERING]: 'collapsing', - [ENTERED]: 'collapse in', -}; - -const propTypes = { - className: PropTypes.string, - children: PropTypes.node, - /** - * Show the component; triggers the expand or collapse animation - */ - in: PropTypes.bool, - - /** - * Wait until the first "enter" transition to mount the component (add it to the DOM) - */ - mountOnEnter: PropTypes.bool, - - /** - * Unmount the component (remove it from the DOM) when it is collapsed - */ - unmountOnExit: PropTypes.bool, - - /** - * Run the expand animation when the component mounts, if it is initially - * shown - */ - appear: PropTypes.bool, - - /** - * Duration of the collapse animation in milliseconds, to ensure that - * finishing callbacks are fired even if the original browser transition end - * events are canceled - */ - timeout: PropTypes.number, - - /** - * Callback fired before the component expands - */ - onEnter: PropTypes.func, - /** - * Callback fired after the component starts to expand - */ - onEntering: PropTypes.func, - /** - * Callback fired after the component has expanded - */ - onEntered: PropTypes.func, - /** - * Callback fired before the component collapses - */ - onExit: PropTypes.func, - /** - * Callback fired after the component starts to collapse - */ - onExiting: PropTypes.func, - /** - * Callback fired after the component has collapsed - */ - onExited: PropTypes.func, - - /** - * The dimension used when collapsing, or a function that returns the - * dimension - * - * _Note: Bootstrap only partially supports 'width'! - * You will need to supply your own CSS animation for the `.width` CSS class._ - */ - dimension: PropTypes.oneOfType([ - PropTypes.oneOf(['height', 'width']), - PropTypes.func, - ]), - - /** - * Function that returns the height or width of the animating DOM node - * - * Allows for providing some custom logic for how much the Collapse component - * should animate in its specified dimension. Called with the current - * dimension prop value and the DOM node. - */ - getDimensionValue: PropTypes.func, - - /** - * ARIA role of collapsible element - */ - role: PropTypes.string, -}; - -const defaultProps = { - in: false, - timeout: 300, - mountOnEnter: false, - unmountOnExit: false, - appear: false, - - dimension: 'height', - getDimensionValue, -}; - -class Collapse extends React.Component { - getDimension() { - return typeof this.props.dimension === 'function' - ? this.props.dimension() - : this.props.dimension; - } - - // for testing - _getScrollDimensionValue(elem, dimension) { - return `${elem[`scroll${capitalize(dimension)}`]}px`; - } - - /* -- Expanding -- */ - handleEnter = (elem) => { - elem.style[this.getDimension()] = '0'; - }; - - handleEntering = (elem) => { - const dimension = this.getDimension(); - elem.style[dimension] = this._getScrollDimensionValue(elem, dimension); - }; - - handleEntered = (elem) => { - elem.style[this.getDimension()] = null; - }; - - /* -- Collapsing -- */ - handleExit = (elem) => { - const dimension = this.getDimension(); - elem.style[dimension] = `${this.props.getDimensionValue( - dimension, - elem - )}px`; - triggerBrowserReflow(elem); - }; - - handleExiting = (elem) => { - elem.style[this.getDimension()] = '0'; - }; - - render() { - const { - onEnter, - onEntering, - onEntered, - onExit, - onExiting, - className, - children, - ...props - } = this.props; - - delete props.dimension; - delete props.getDimensionValue; - - const handleEnter = createChainedFunction(this.handleEnter, onEnter); - const handleEntering = createChainedFunction( - this.handleEntering, - onEntering - ); - const handleEntered = createChainedFunction(this.handleEntered, onEntered); - const handleExit = createChainedFunction(this.handleExit, onExit); - const handleExiting = createChainedFunction(this.handleExiting, onExiting); - - return ( - - {(state, innerProps) => - React.cloneElement(children, { - ...innerProps, - className: classNames( - className, - children.props.className, - collapseStyles[state], - this.getDimension() === 'width' && 'width' - ), - }) - } - - ); - } -} - -Collapse.propTypes = propTypes; -Collapse.defaultProps = defaultProps; - -export default Collapse; diff --git a/fork/react-bootstrap/src/Collapse.jsx b/fork/react-bootstrap/src/Collapse.jsx new file mode 100644 index 00000000000..462c44a94c5 --- /dev/null +++ b/fork/react-bootstrap/src/Collapse.jsx @@ -0,0 +1,207 @@ +/* eslint-disable arrow-parens */ +import classNames from 'classnames'; +import css from 'dom-helpers/style'; +import React from 'react'; +import PropTypes from 'prop-types'; +import Transition, { EXITED, ENTERED, ENTERING, EXITING } from 'react-transition-group/Transition'; + +import capitalize from './utils/capitalize'; +import createChainedFunction from './utils/createChainedFunction'; + +const MARGINS = { + height: ['marginTop', 'marginBottom'], + width: ['marginLeft', 'marginRight'], +}; + +// reading a dimension prop will cause the browser to recalculate, +// which will let our animations work +function triggerBrowserReflow(node) { + // eslint-disable-next-line mdx/no-unused-expressions + node.offsetHeight; +} + +function getDimensionValue(dimension, elem) { + let value = elem[`offset${capitalize(dimension)}`]; + let margins = MARGINS[dimension]; + + return value + parseInt(css(elem, margins[0]), 10) + parseInt(css(elem, margins[1]), 10); +} + +const collapseStyles = { + [EXITED]: 'collapse', + [EXITING]: 'collapsing', + [ENTERING]: 'collapsing', + [ENTERED]: 'collapse in', +}; + +const propTypes = { + className: PropTypes.string, + children: PropTypes.node, + /** + * Show the component; triggers the expand or collapse animation + */ + in: PropTypes.bool, + + /** + * Wait until the first "enter" transition to mount the component (add it to the DOM) + */ + mountOnEnter: PropTypes.bool, + + /** + * Unmount the component (remove it from the DOM) when it is collapsed + */ + unmountOnExit: PropTypes.bool, + + /** + * Run the expand animation when the component mounts, if it is initially + * shown + */ + appear: PropTypes.bool, + + /** + * Duration of the collapse animation in milliseconds, to ensure that + * finishing callbacks are fired even if the original browser transition end + * events are canceled + */ + timeout: PropTypes.number, + + /** + * Callback fired before the component expands + */ + onEnter: PropTypes.func, + /** + * Callback fired after the component starts to expand + */ + onEntering: PropTypes.func, + /** + * Callback fired after the component has expanded + */ + onEntered: PropTypes.func, + /** + * Callback fired before the component collapses + */ + onExit: PropTypes.func, + /** + * Callback fired after the component starts to collapse + */ + onExiting: PropTypes.func, + /** + * Callback fired after the component has collapsed + */ + onExited: PropTypes.func, + + /** + * The dimension used when collapsing, or a function that returns the + * dimension + * + * _Note: Bootstrap only partially supports 'width'! + * You will need to supply your own CSS animation for the `.width` CSS class._ + */ + dimension: PropTypes.oneOfType([PropTypes.oneOf(['height', 'width']), PropTypes.func]), + + /** + * Function that returns the height or width of the animating DOM node + * + * Allows for providing some custom logic for how much the Collapse component + * should animate in its specified dimension. Called with the current + * dimension prop value and the DOM node. + */ + getDimensionValue: PropTypes.func, + + /** + * ARIA role of collapsible element + */ + role: PropTypes.string, +}; + +const defaultProps = { + in: false, + timeout: 300, + mountOnEnter: false, + unmountOnExit: false, + appear: false, + + dimension: 'height', + getDimensionValue, +}; + +class Collapse extends React.Component { + getDimension() { + return typeof this.props.dimension === 'function' + ? this.props.dimension() + : this.props.dimension; + } + + // for testing + _getScrollDimensionValue(elem, dimension) { + return `${elem[`scroll${capitalize(dimension)}`]}px`; + } + + /* -- Expanding -- */ + handleEnter = elem => { + elem.style[this.getDimension()] = '0'; + }; + + handleEntering = elem => { + const dimension = this.getDimension(); + elem.style[dimension] = this._getScrollDimensionValue(elem, dimension); + }; + + handleEntered = elem => { + elem.style[this.getDimension()] = null; + }; + + /* -- Collapsing -- */ + handleExit = elem => { + const dimension = this.getDimension(); + elem.style[dimension] = `${this.props.getDimensionValue(dimension, elem)}px`; + triggerBrowserReflow(elem); + }; + + handleExiting = elem => { + elem.style[this.getDimension()] = '0'; + }; + + render() { + const { onEnter, onEntering, onEntered, onExit, onExiting, className, children, ...props } = + this.props; + + delete props.dimension; + delete props.getDimensionValue; + + const handleEnter = createChainedFunction(this.handleEnter, onEnter); + const handleEntering = createChainedFunction(this.handleEntering, onEntering); + const handleEntered = createChainedFunction(this.handleEntered, onEntered); + const handleExit = createChainedFunction(this.handleExit, onExit); + const handleExiting = createChainedFunction(this.handleExiting, onExiting); + + return ( + + {(state, innerProps) => + React.cloneElement(children, { + ...innerProps, + className: classNames( + className, + children.props.className, + collapseStyles[state], + this.getDimension() === 'width' && 'width', + ), + }) + } + + ); + } +} + +Collapse.propTypes = propTypes; +Collapse.defaultProps = defaultProps; + +export default Collapse; diff --git a/fork/react-bootstrap/src/Collapse.test.js b/fork/react-bootstrap/src/Collapse.test.js deleted file mode 100644 index e3d2c14e416..00000000000 --- a/fork/react-bootstrap/src/Collapse.test.js +++ /dev/null @@ -1,292 +0,0 @@ -/* eslint-disable arrow-parens */ -/** - * @jest-environment jsdom - */ -import React from 'react'; -import { render, screen } from '@testing-library/react'; - -import Collapse from './Collapse'; - -describe('', () => { - // eslint-disable-next-line react/prop-types - const Component = ({ children, ...props }) => ( - 15} {...props}> -
      {children}
      -
      - ); - - it('Should default to collapsed', () => { - // when - render(Panel content); - - // then - expect(screen.getByText('Panel content')).toHaveClass('collapse'); - }); - - describe('from collapsed to expanded', () => { - const originalOffsetHeight = Object.getOwnPropertyDescriptor( - HTMLElement.prototype, - 'scrollHeight' - ); - const originalOffsetWidth = Object.getOwnPropertyDescriptor( - HTMLElement.prototype, - 'scrollWidth' - ); - beforeAll(() => { - Object.defineProperty(HTMLElement.prototype, 'scrollHeight', { - configurable: true, - value: 100, - }); - Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { - configurable: true, - value: 100, - }); - }); - - afterAll(() => { - if (originalOffsetHeight) { - Object.defineProperty( - HTMLElement.prototype, - 'scrollHeight', - originalOffsetHeight - ); - } - if (originalOffsetWidth) { - Object.defineProperty( - HTMLElement.prototype, - 'scrollWidth', - originalOffsetWidth - ); - } - }); - - it('Should have collapsing class', () => { - // given - const { rerender } = render(Panel content); - expect(screen.getByText('Panel content')).not.toHaveClass('collapsing'); - - // when - rerender(Panel content); - - // then - expect(screen.getByText('Panel content')).toHaveClass('collapsing'); - }); - - it('Should set initial 0px height', (done) => { - // given - const onEnter = () => { - // then - expect(screen.getByText('Panel content')).toHaveStyle('height: 0px'); - done(); - }; - const { rerender } = render( - Panel content - ); - - // when - rerender( - - Panel content - - ); - }); - - it('Should set node to height', (done) => { - // given - const onEntering = () => { - // then - expect(screen.getByText('Panel content')).toHaveStyle('height: 100px'); - done(); - }; - const { rerender } = render( - Panel content - ); - - // when - rerender( - - Panel content - - ); - }); - - it('Should transition from collapsing to not collapsing', (done) => { - // given - const onEntering = () => { - // then - expect(screen.getByText('Panel content')).toHaveClass('collapsing'); - }; - const onEntered = () => { - // then - expect(screen.getByText('Panel content')).toHaveClass('collapse in'); - done(); - }; - const { rerender } = render( - - Panel content - - ); - - // when - rerender( - - Panel content - - ); - }); - - it('Should clear height after transition complete', (done) => { - // given - const onEntered = () => { - // eslint-disable-next-line jest-dom/prefer-to-have-style - expect(screen.getByText('Panel content')).toHaveAttribute('style', ''); - done(); - }; - - const { rerender } = render( - Panel content - ); - - // when - rerender( - - Panel content - - ); - }); - }); - - describe('from expanded to collapsed', () => { - it('Should have collapsing class', () => { - // given - const { rerender } = render(Panel content); - - // when - rerender(Panel content); - - // then - expect(screen.getByText('Panel content')).toHaveClass('collapsing'); - }); - - it('Should set initial height', (done) => { - // given - const onExit = () => { - expect(screen.getByText('Panel content')).toHaveStyle('height: 15px'); - done(); - }; - const { rerender } = render( - - Panel content - - ); - - // when - rerender(Panel content); - }); - - it('Should set node to height', (done) => { - // given - const onExiting = () => { - expect(screen.getByText('Panel content')).toHaveStyle('height: 0px'); - done(); - }; - const { rerender } = render( - - Panel content - - ); - - // when - rerender(Panel content); - }); - - it('Should transition from collapsing to not collapsing', (done) => { - // given - const onExited = () => { - expect(screen.getByText('Panel content')).toHaveClass('collapse'); - done(); - }; - const { rerender } = render( - - Panel content - - ); - - // when - rerender(Panel content); - - // then - expect(screen.getByText('Panel content')).toHaveClass('collapsing'); - }); - - it('Should have 0px height after transition complete', () => { - // given - const { rerender } = render(Panel content); - expect(screen.getByText('Panel content')).not.toHaveAttribute('style'); - - // when - rerender(Panel content); - - // then - expect(screen.getByText('Panel content')).toHaveStyle('height: 0px'); - }); - }); - - describe('expanded', () => { - it('Should have collapse and in class', () => { - // when - render(Panel content); - - // then - expect(screen.getByText('Panel content')).toHaveClass('collapse in'); - }); - }); - - describe('with a role', () => { - it('sets aria-expanded true when expanded', () => { - // given - const { rerender } = render( - Panel content - ); - expect(screen.getByText('Panel content')).not.toHaveAttribute( - 'aria-expanded', - 'true' - ); - - // when - rerender( - - Panel content - - ); - - // then - expect(screen.getByText('Panel content')).toHaveAttribute( - 'aria-expanded', - 'true' - ); - }); - - it('sets aria-expanded false when collapsed', () => { - // given - const { rerender } = render( - - Panel content - - ); - expect(screen.getByText('Panel content')).toHaveAttribute( - 'aria-expanded', - 'true' - ); - - // when - rerender(Panel content); - - // then - expect(screen.getByText('Panel content')).toHaveAttribute( - 'aria-expanded', - 'false' - ); - }); - }); -}); diff --git a/fork/react-bootstrap/src/Collapse.test.jsx b/fork/react-bootstrap/src/Collapse.test.jsx new file mode 100644 index 00000000000..0784892afd2 --- /dev/null +++ b/fork/react-bootstrap/src/Collapse.test.jsx @@ -0,0 +1,277 @@ +/* eslint-disable arrow-parens */ +/** + * @jest-environment jsdom + */ +import { render, screen } from '@testing-library/react'; + +import Collapse from './Collapse'; + +describe('', () => { + // eslint-disable-next-line react/prop-types + const Component = ({ children, ...props }) => ( + 15} {...props}> +
      {children}
      +
      + ); + + it('Should default to collapsed', () => { + // when + render(Panel content); + + // then + expect(screen.getByText('Panel content')).toHaveClass('collapse'); + }); + + describe('from collapsed to expanded', () => { + const originalOffsetHeight = Object.getOwnPropertyDescriptor( + HTMLElement.prototype, + 'scrollHeight', + ); + const originalOffsetWidth = Object.getOwnPropertyDescriptor( + HTMLElement.prototype, + 'scrollWidth', + ); + beforeAll(() => { + Object.defineProperty(HTMLElement.prototype, 'scrollHeight', { + configurable: true, + value: 100, + }); + Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { + configurable: true, + value: 100, + }); + }); + + afterAll(() => { + if (originalOffsetHeight) { + Object.defineProperty(HTMLElement.prototype, 'scrollHeight', originalOffsetHeight); + } + if (originalOffsetWidth) { + Object.defineProperty(HTMLElement.prototype, 'scrollWidth', originalOffsetWidth); + } + }); + + it('Should have collapsing class', () => { + // given + const { rerender } = render(Panel content); + expect(screen.getByText('Panel content')).not.toHaveClass('collapsing'); + + // when + rerender(Panel content); + + // then + expect(screen.getByText('Panel content')).toHaveClass('collapsing'); + }); + + it('Should set initial 0px height', () => { + return new Promise(resolve => { + // given + const onEnter = () => { + // then + expect(screen.getByText('Panel content')).toHaveStyle('height: 0px'); + resolve(); + }; + const { rerender } = render(Panel content); + + // when + rerender( + + Panel content + , + ); + }); + }); + + it('Should set node to height', () => { + return new Promise(resolve => { + // given + const onEntering = () => { + // then + expect(screen.getByText('Panel content')).toHaveStyle('height: 100px'); + resolve(); + }; + const { rerender } = render(Panel content); + + // when + rerender( + + Panel content + , + ); + }); + }); + + it('Should transition from collapsing to not collapsing', () => { + return new Promise(resolve => { + // given + const onEntering = () => { + // then + expect(screen.getByText('Panel content')).toHaveClass('collapsing'); + }; + const onEntered = () => { + // then + expect(screen.getByText('Panel content')).toHaveClass('collapse in'); + resolve(); + }; + const { rerender } = render( + + Panel content + , + ); + + // when + rerender( + + Panel content + , + ); + }); + }); + + it('Should clear height after transition complete', () => { + return new Promise(resolve => { + // given + const onEntered = () => { + // eslint-disable-next-line jest-dom/prefer-to-have-style + expect(screen.getByText('Panel content')).toHaveAttribute('style', ''); + resolve(); + }; + + const { rerender } = render(Panel content); + + // when + rerender( + + Panel content + , + ); + }); + }); + }); + + describe('from expanded to collapsed', () => { + it('Should have collapsing class', () => { + // given + const { rerender } = render(Panel content); + + // when + rerender(Panel content); + + // then + expect(screen.getByText('Panel content')).toHaveClass('collapsing'); + }); + + it('Should set initial height', () => { + return new Promise(resolve => { + // given + const onExit = () => { + expect(screen.getByText('Panel content')).toHaveStyle('height: 15px'); + resolve(); + }; + const { rerender } = render( + + Panel content + , + ); + + // when + rerender(Panel content); + }); + }); + + it('Should set node to height', () => { + return new Promise(resolve => { + // given + const onExiting = () => { + expect(screen.getByText('Panel content')).toHaveStyle('height: 0px'); + resolve(); + }; + const { rerender } = render( + + Panel content + , + ); + + // when + rerender(Panel content); + }); + }); + + it('Should transition from collapsing to not collapsing', () => { + return new Promise(resolve => { + // given + const onExited = () => { + expect(screen.getByText('Panel content')).toHaveClass('collapse'); + resolve(); + }; + const { rerender } = render( + + Panel content + , + ); + + // when + rerender(Panel content); + + // then + expect(screen.getByText('Panel content')).toHaveClass('collapsing'); + }); + }); + + it('Should have 0px height after transition complete', () => { + // given + const { rerender } = render(Panel content); + expect(screen.getByText('Panel content')).not.toHaveAttribute('style'); + + // when + rerender(Panel content); + + // then + expect(screen.getByText('Panel content')).toHaveStyle('height: 0px'); + }); + }); + + describe('expanded', () => { + it('Should have collapse and in class', () => { + // when + render(Panel content); + + // then + expect(screen.getByText('Panel content')).toHaveClass('collapse in'); + }); + }); + + describe('with a role', () => { + it('sets aria-expanded true when expanded', () => { + // given + const { rerender } = render(Panel content); + expect(screen.getByText('Panel content')).not.toHaveAttribute('aria-expanded', 'true'); + + // when + rerender( + + Panel content + , + ); + + // then + expect(screen.getByText('Panel content')).toHaveAttribute('aria-expanded', 'true'); + }); + + it('sets aria-expanded false when collapsed', () => { + // given + const { rerender } = render( + + Panel content + , + ); + expect(screen.getByText('Panel content')).toHaveAttribute('aria-expanded', 'true'); + + // when + rerender(Panel content); + + // then + expect(screen.getByText('Panel content')).toHaveAttribute('aria-expanded', 'false'); + }); + }); +}); diff --git a/fork/react-bootstrap/src/ControlLabel.js b/fork/react-bootstrap/src/ControlLabel.js deleted file mode 100644 index 497e51d130d..00000000000 --- a/fork/react-bootstrap/src/ControlLabel.js +++ /dev/null @@ -1,56 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import warning from 'warning'; - -import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; - -const propTypes = { - /** - * Uses `controlId` from `` if not explicitly specified. - */ - htmlFor: PropTypes.string, - srOnly: PropTypes.bool -}; - -const defaultProps = { - srOnly: false -}; - -const contextTypes = { - $bs_formGroup: PropTypes.object -}; - -class ControlLabel extends React.Component { - render() { - const formGroup = this.context.$bs_formGroup; - const controlId = formGroup && formGroup.controlId; - - const { htmlFor = controlId, srOnly, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - warning( - controlId == null || htmlFor === controlId, - '`controlId` is ignored on `` when `htmlFor` is specified.' - ); - - const classes = { - ...getClassSet(bsProps), - 'sr-only': srOnly - }; - - return ( -
    7. - {this.props.children} -
    8. - ); - } - } - - let instance = ReactTestUtils.renderIntoDocument( - - Child - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group') - ); - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); - assert.equal(ReactDOM.findDOMNode(instance).firstChild.nodeName, 'LI'); - }); - - xit('Should use a "componentClass" prop if specified if any children are custom components', () => { - class CustomComponent extends React.Component { - render() { - return ( -
    9. - {this.props.children} -
    10. - ); - } - } - - let instance = ReactTestUtils.renderIntoDocument( - - Custom Child - Custom Child - RB Child - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group') - ); - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'UL'); - assert.equal(ReactDOM.findDOMNode(instance).lastChild.nodeName, 'LI'); - }); - }); -}); diff --git a/fork/react-bootstrap/src/ListGroup.test.jsx b/fork/react-bootstrap/src/ListGroup.test.jsx new file mode 100644 index 00000000000..e1c5e963a42 --- /dev/null +++ b/fork/react-bootstrap/src/ListGroup.test.jsx @@ -0,0 +1,188 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import ListGroup from '../src/ListGroup'; +import ListGroupItem from '../src/ListGroupItem'; + +describe('', () => { + describe('All children are of type ListGroupItem', () => { + xit('Should output a "div" with the class "list-group"', () => { + let instance = ReactTestUtils.renderIntoDocument(); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group')); + }); + + xit('Should support a single "ListGroupItem" child', () => { + let instance = ReactTestUtils.renderIntoDocument( + + Only Child + , + ); + + let items = ReactTestUtils.scryRenderedComponentsWithType(instance, ListGroupItem); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(items[0], 'list-group-item')); + }); + + xit('Should support a single "ListGroupItem" child contained in an array', () => { + let child = [Only Child in array]; + let instance = ReactTestUtils.renderIntoDocument({child}); + + let items = ReactTestUtils.scryRenderedComponentsWithType(instance, ListGroupItem); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(items[0], 'list-group-item')); + }); + + xit('Should output a "ul" when single "ListGroupItem" child is a list item', () => { + let instance = ReactTestUtils.renderIntoDocument( + + Only Child + , + ); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'UL'); + assert.equal(ReactDOM.findDOMNode(instance).firstChild.nodeName, 'LI'); + }); + + xit('Should output a "div" when single "ListGroupItem" child is an anchor', () => { + let instance = ReactTestUtils.renderIntoDocument( + + Only Child + , + ); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + assert.equal(ReactDOM.findDOMNode(instance).firstChild.nodeName, 'A'); + }); + + xit('Should support multiple "ListGroupItem" children', () => { + let instance = ReactTestUtils.renderIntoDocument( + + 1st Child + 2nd Child + , + ); + + let items = ReactTestUtils.scryRenderedComponentsWithType(instance, ListGroupItem); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(items[0], 'list-group-item')); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(items[1], 'list-group-item')); + }); + + xit('Should support multiple "ListGroupItem" children including a subset contained in an array', () => { + let itemArray = [ + 2nd Child nested, + 3rd Child nested, + ]; + + let instance = ReactTestUtils.renderIntoDocument( + + 1st Child + {itemArray} + 4th Child + , + ); + + let items = ReactTestUtils.scryRenderedComponentsWithType(instance, ListGroupItem); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(items[0], 'list-group-item')); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(items[1], 'list-group-item')); + }); + + xit('Should output a "ul" when children are list items', () => { + let instance = ReactTestUtils.renderIntoDocument( + + 1st Child + 2nd Child + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group')); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'UL'); + assert.equal(ReactDOM.findDOMNode(instance).firstChild.nodeName, 'LI'); + assert.equal(ReactDOM.findDOMNode(instance).lastChild.nodeName, 'LI'); + }); + + xit('Should output a "div" when "ListGroupItem" children are anchors and spans', () => { + let instance = ReactTestUtils.renderIntoDocument( + + 1st Child + 2nd Child + , + ); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + assert.equal(ReactDOM.findDOMNode(instance).firstChild.nodeName, 'A'); + assert.equal(ReactDOM.findDOMNode(instance).lastChild.nodeName, 'SPAN'); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group')); + }); + + xit('Should output a "div" when "ListGroupItem" children have an onClick handler', () => { + let instance = ReactTestUtils.renderIntoDocument( + + null}>1st Child + 2nd Child + , + ); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + assert.equal(ReactDOM.findDOMNode(instance).firstChild.nodeName, 'BUTTON'); + assert.equal(ReactDOM.findDOMNode(instance).lastChild.nodeName, 'SPAN'); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group')); + }); + + xit('Should support an element id through "id" prop', () => { + let instance = ReactTestUtils.renderIntoDocument( + + Child + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group')); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'UL'); + assert.equal(ReactDOM.findDOMNode(instance).id, 'testItem'); + }); + }); + + describe('Some or all children are user-defined custom components', () => { + xit('Should output a div by default when children are custom components', () => { + class CustomComponent extends React.Component { + render() { + return ( +
    11. + {this.props.children} +
    12. + ); + } + } + + let instance = ReactTestUtils.renderIntoDocument( + + Child + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group')); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + assert.equal(ReactDOM.findDOMNode(instance).firstChild.nodeName, 'LI'); + }); + + xit('Should use a "componentClass" prop if specified if any children are custom components', () => { + class CustomComponent extends React.Component { + render() { + return ( +
    13. + {this.props.children} +
    14. + ); + } + } + + let instance = ReactTestUtils.renderIntoDocument( + + Custom Child + Custom Child + RB Child + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group')); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'UL'); + assert.equal(ReactDOM.findDOMNode(instance).lastChild.nodeName, 'LI'); + }); + }); +}); diff --git a/fork/react-bootstrap/src/ListGroupItem.js b/fork/react-bootstrap/src/ListGroupItem.js deleted file mode 100644 index 0df5320a202..00000000000 --- a/fork/react-bootstrap/src/ListGroupItem.js +++ /dev/null @@ -1,94 +0,0 @@ -import classNames from 'classnames'; -import React, { cloneElement } from 'react'; -import PropTypes from 'prop-types'; - -import { - bsClass, - bsStyles, - getClassSet, - prefix, - splitBsProps -} from './utils/bootstrapUtils'; -import { State } from './utils/StyleConfig'; - -const propTypes = { - active: PropTypes.any, - disabled: PropTypes.any, - header: PropTypes.node, - listItem: PropTypes.bool, - onClick: PropTypes.func, - href: PropTypes.string, - type: PropTypes.string -}; - -const defaultProps = { - listItem: false -}; - -class ListGroupItem extends React.Component { - renderHeader(header, headingClassName) { - if (React.isValidElement(header)) { - return cloneElement(header, { - className: classNames(header.props.className, headingClassName) - }); - } - - return

      {header}

      ; - } - - render() { - const { - active, - disabled, - className, - header, - listItem, - children, - ...props - } = this.props; - - const [bsProps, elementProps] = splitBsProps(props); - - const classes = { - ...getClassSet(bsProps), - active, - disabled - }; - - let Component; - - if (elementProps.href) { - Component = 'a'; - } else if (elementProps.onClick) { - Component = 'button'; - elementProps.type = elementProps.type || 'button'; - } else if (listItem) { - Component = 'li'; - } else { - Component = 'span'; - } - - elementProps.className = classNames(className, classes); - - // TODO: Deprecate `header` prop. - if (header) { - return ( - - {this.renderHeader(header, prefix(bsProps, 'heading'))} - -

      {children}

      -
      - ); - } - - return {children}; - } -} - -ListGroupItem.propTypes = propTypes; -ListGroupItem.defaultProps = defaultProps; - -export default bsClass( - 'list-group-item', - bsStyles(Object.values(State), ListGroupItem) -); diff --git a/fork/react-bootstrap/src/ListGroupItem.jsx b/fork/react-bootstrap/src/ListGroupItem.jsx new file mode 100644 index 00000000000..d140f20fade --- /dev/null +++ b/fork/react-bootstrap/src/ListGroupItem.jsx @@ -0,0 +1,77 @@ +import classNames from 'classnames'; +import React, { cloneElement } from 'react'; +import PropTypes from 'prop-types'; + +import { bsClass, bsStyles, getClassSet, prefix, splitBsProps } from './utils/bootstrapUtils'; +import { State } from './utils/StyleConfig'; + +const propTypes = { + active: PropTypes.any, + disabled: PropTypes.any, + header: PropTypes.node, + listItem: PropTypes.bool, + onClick: PropTypes.func, + href: PropTypes.string, + type: PropTypes.string, +}; + +const defaultProps = { + listItem: false, +}; + +class ListGroupItem extends React.Component { + renderHeader(header, headingClassName) { + if (React.isValidElement(header)) { + return cloneElement(header, { + className: classNames(header.props.className, headingClassName), + }); + } + + return

      {header}

      ; + } + + render() { + const { active, disabled, className, header, listItem, children, ...props } = this.props; + + const [bsProps, elementProps] = splitBsProps(props); + + const classes = { + ...getClassSet(bsProps), + active, + disabled, + }; + + let Component; + + if (elementProps.href) { + Component = 'a'; + } else if (elementProps.onClick) { + Component = 'button'; + elementProps.type = elementProps.type || 'button'; + } else if (listItem) { + Component = 'li'; + } else { + Component = 'span'; + } + + elementProps.className = classNames(className, classes); + + // TODO: Deprecate `header` prop. + if (header) { + return ( + + {this.renderHeader(header, prefix(bsProps, 'heading'))} + +

      {children}

      +
      + ); + } + + return {children}; + } +} + +ListGroupItem.propTypes = propTypes; +ListGroupItem.defaultProps = defaultProps; + +export default bsClass('list-group-item', bsStyles(Object.values(State), ListGroupItem)); diff --git a/fork/react-bootstrap/src/ListGroupItem.test.js b/fork/react-bootstrap/src/ListGroupItem.test.js deleted file mode 100644 index e8d5b6a984e..00000000000 --- a/fork/react-bootstrap/src/ListGroupItem.test.js +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import ListGroupItem from '../src/ListGroupItem'; - -describe('', () => { - xit('Should output a "span" with the class "list-group-item"', () => { - let instance = ReactTestUtils.renderIntoDocument( - Text - ); - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'SPAN'); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'list-group-item' - ) - ); - }); - - xit('Should output an "anchor" if "href" prop is set', () => { - let instance = ReactTestUtils.renderIntoDocument( - Anchor - ); - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'A'); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'list-group-item' - ) - ); - }); - - xit('Should output a "button" if an "onClick" handler is set', () => { - let noop = () => {}; - let instance = ReactTestUtils.renderIntoDocument( - Button - ); - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'BUTTON'); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'list-group-item' - ) - ); - }); - - xit('Should output an "li" if "listItem" prop is set', () => { - let instance = ReactTestUtils.renderIntoDocument( - Item 1 - ); - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'LI'); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'list-group-item' - ) - ); - }); - - xit('Should support "bsStyle" prop', () => { - let instance = ReactTestUtils.renderIntoDocument( - Item 1 - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'list-group-item-success' - ) - ); - }); - - xit('Should support "active" and "disabled" prop', () => { - let instance = ReactTestUtils.renderIntoDocument( - Item 1 - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'active') - ); - }); - - xit('Should support "disabled" prop', () => { - let instance = ReactTestUtils.renderIntoDocument( - Item 2 - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'disabled') - ); - }); - - xit('Should support "header" prop as a string', () => { - let instance = ReactTestUtils.renderIntoDocument( - Item text - ); - - let node = ReactDOM.findDOMNode(instance); - assert.equal(node.firstChild.nodeName, 'H4'); - assert.equal(node.firstChild.textContent, 'Heading'); - assert.ok(node.firstChild.className.match(/\blist-group-item-heading\b/)); - assert.equal(node.lastChild.nodeName, 'P'); - assert.equal(node.lastChild.textContent, 'Item text'); - assert.ok(node.lastChild.className.match(/\blist-group-item-text\b/)); - }); - - xit('Should support "header" prop as a ReactComponent', () => { - let header =

      Heading

      ; - let instance = ReactTestUtils.renderIntoDocument( - Item text - ); - - let node = ReactDOM.findDOMNode(instance); - assert.equal(node.firstChild.nodeName, 'H2'); - assert.equal(node.firstChild.textContent, 'Heading'); - assert.ok(node.firstChild.className.match(/\blist-group-item-heading\b/)); - assert.equal(node.lastChild.nodeName, 'P'); - assert.equal(node.lastChild.textContent, 'Item text'); - assert.ok(node.lastChild.className.match(/\blist-group-item-text\b/)); - }); -}); diff --git a/fork/react-bootstrap/src/ListGroupItem.test.jsx b/fork/react-bootstrap/src/ListGroupItem.test.jsx new file mode 100644 index 00000000000..fb2517585ea --- /dev/null +++ b/fork/react-bootstrap/src/ListGroupItem.test.jsx @@ -0,0 +1,87 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import ListGroupItem from '../src/ListGroupItem'; + +describe('', () => { + xit('Should output a "span" with the class "list-group-item"', () => { + let instance = ReactTestUtils.renderIntoDocument(Text); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'SPAN'); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group-item')); + }); + + xit('Should output an "anchor" if "href" prop is set', () => { + let instance = ReactTestUtils.renderIntoDocument( + Anchor, + ); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'A'); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group-item')); + }); + + xit('Should output a "button" if an "onClick" handler is set', () => { + let noop = () => {}; + let instance = ReactTestUtils.renderIntoDocument( + Button, + ); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'BUTTON'); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group-item')); + }); + + xit('Should output an "li" if "listItem" prop is set', () => { + let instance = ReactTestUtils.renderIntoDocument( + Item 1, + ); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'LI'); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group-item')); + }); + + xit('Should support "bsStyle" prop', () => { + let instance = ReactTestUtils.renderIntoDocument( + Item 1, + ); + assert.ok( + ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'list-group-item-success'), + ); + }); + + xit('Should support "active" and "disabled" prop', () => { + let instance = ReactTestUtils.renderIntoDocument(Item 1); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'active')); + }); + + xit('Should support "disabled" prop', () => { + let instance = ReactTestUtils.renderIntoDocument( + Item 2, + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'disabled')); + }); + + xit('Should support "header" prop as a string', () => { + let instance = ReactTestUtils.renderIntoDocument( + Item text, + ); + + let node = ReactDOM.findDOMNode(instance); + assert.equal(node.firstChild.nodeName, 'H4'); + assert.equal(node.firstChild.textContent, 'Heading'); + assert.ok(node.firstChild.className.match(/\blist-group-item-heading\b/)); + assert.equal(node.lastChild.nodeName, 'P'); + assert.equal(node.lastChild.textContent, 'Item text'); + assert.ok(node.lastChild.className.match(/\blist-group-item-text\b/)); + }); + + xit('Should support "header" prop as a ReactComponent', () => { + let header =

      Heading

      ; + let instance = ReactTestUtils.renderIntoDocument( + Item text, + ); + + let node = ReactDOM.findDOMNode(instance); + assert.equal(node.firstChild.nodeName, 'H2'); + assert.equal(node.firstChild.textContent, 'Heading'); + assert.ok(node.firstChild.className.match(/\blist-group-item-heading\b/)); + assert.equal(node.lastChild.nodeName, 'P'); + assert.equal(node.lastChild.textContent, 'Item text'); + assert.ok(node.lastChild.className.match(/\blist-group-item-text\b/)); + }); +}); diff --git a/fork/react-bootstrap/src/Media.js b/fork/react-bootstrap/src/Media.jsx similarity index 69% rename from fork/react-bootstrap/src/Media.js rename to fork/react-bootstrap/src/Media.jsx index 0d4b2ca16d8..2903ce7a36e 100644 --- a/fork/react-bootstrap/src/Media.js +++ b/fork/react-bootstrap/src/Media.jsx @@ -11,24 +11,22 @@ import MediaRight from './MediaRight'; import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; const propTypes = { - componentClass: elementType + componentClass: elementType, }; const defaultProps = { - componentClass: 'div' + componentClass: 'div', }; class Media extends React.Component { - render() { - const { componentClass: Component, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); + render() { + const { componentClass: Component, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); - const classes = getClassSet(bsProps); + const classes = getClassSet(bsProps); - return ( - - ); - } + return ; + } } Media.propTypes = propTypes; diff --git a/fork/react-bootstrap/src/Media.test.js b/fork/react-bootstrap/src/Media.test.js deleted file mode 100644 index 823a9cd0517..00000000000 --- a/fork/react-bootstrap/src/Media.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Media from '../src/Media'; - -describe('Media', () => { - xit('uses "div" by default', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); - }); - - xit('has "media" class', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.include(ReactDOM.findDOMNode(instance).className, 'media'); - }); - - xit('should merge additional classes passed in', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.include(ReactDOM.findDOMNode(instance).className, 'media'); - assert.include(ReactDOM.findDOMNode(instance).className, 'custom-class'); - }); - - xit('should allow custom elements instead of "div"', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'SECTION'); - }); - - xit('should render children', () => { - const instance = ReactTestUtils.renderIntoDocument( - - Children - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong') - ); - }); -}); diff --git a/fork/react-bootstrap/src/Media.test.jsx b/fork/react-bootstrap/src/Media.test.jsx new file mode 100644 index 00000000000..0f763054395 --- /dev/null +++ b/fork/react-bootstrap/src/Media.test.jsx @@ -0,0 +1,40 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Media from '../src/Media'; + +describe('Media', () => { + xit('uses "div" by default', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + }); + + xit('has "media" class', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.include(ReactDOM.findDOMNode(instance).className, 'media'); + }); + + xit('should merge additional classes passed in', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.include(ReactDOM.findDOMNode(instance).className, 'media'); + assert.include(ReactDOM.findDOMNode(instance).className, 'custom-class'); + }); + + xit('should allow custom elements instead of "div"', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'SECTION'); + }); + + xit('should render children', () => { + const instance = ReactTestUtils.renderIntoDocument( + + Children + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong')); + }); +}); diff --git a/fork/react-bootstrap/src/MediaBody.js b/fork/react-bootstrap/src/MediaBody.js deleted file mode 100644 index f1a397d5a0c..00000000000 --- a/fork/react-bootstrap/src/MediaBody.js +++ /dev/null @@ -1,52 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import elementType from 'prop-types-extra/lib/elementType'; - -import { - bsClass, - getClassSet, - prefix, - splitBsProps, -} from './utils/bootstrapUtils'; - -const propTypes = { - /** - * Align the media to the top, middle, or bottom of the media object. - */ - align: PropTypes.oneOf(['top', 'middle', 'bottom']), - - componentClass: elementType, -}; - -const defaultProps = { - componentClass: 'div', -}; - -class MediaBody extends React.Component { - render() { - const { - componentClass: Component, - align, - className, - ...props - } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = getClassSet(bsProps); - - if (align) { - // The class is e.g. `media-top`, not `media-left-top`. - classes[prefix({ bsClass: 'media' }, align)] = true; - } - - return ( - - ); - } -} - -MediaBody.propTypes = propTypes; -MediaBody.defaultProps = defaultProps; - -export default bsClass('media-body', MediaBody); diff --git a/fork/react-bootstrap/src/MediaBody.jsx b/fork/react-bootstrap/src/MediaBody.jsx new file mode 100644 index 00000000000..b93380e1d31 --- /dev/null +++ b/fork/react-bootstrap/src/MediaBody.jsx @@ -0,0 +1,40 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import elementType from 'prop-types-extra/lib/elementType'; + +import { bsClass, getClassSet, prefix, splitBsProps } from './utils/bootstrapUtils'; + +const propTypes = { + /** + * Align the media to the top, middle, or bottom of the media object. + */ + align: PropTypes.oneOf(['top', 'middle', 'bottom']), + + componentClass: elementType, +}; + +const defaultProps = { + componentClass: 'div', +}; + +class MediaBody extends React.Component { + render() { + const { componentClass: Component, align, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = getClassSet(bsProps); + + if (align) { + // The class is e.g. `media-top`, not `media-left-top`. + classes[prefix({ bsClass: 'media' }, align)] = true; + } + + return ; + } +} + +MediaBody.propTypes = propTypes; +MediaBody.defaultProps = defaultProps; + +export default bsClass('media-body', MediaBody); diff --git a/fork/react-bootstrap/src/MediaBody.test.js b/fork/react-bootstrap/src/MediaBody.test.js deleted file mode 100644 index 57ff9662a91..00000000000 --- a/fork/react-bootstrap/src/MediaBody.test.js +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Media from '../src/Media'; - -describe('', () => { - xit('uses "div" by default', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); - }); - - xit('has "media-body" class', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.include(ReactDOM.findDOMNode(instance).className, 'media-body'); - }); - - xit('should be able to change alignment to middle', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.ok( - ReactDOM.findDOMNode(instance).className.match(/\bmedia-middle\b/) - ); - }); - - xit('should be able to change alignment to bottom', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.ok( - ReactDOM.findDOMNode(instance).className.match(/\bmedia-bottom\b/) - ); - }); - - xit('should merge additional classes passed in', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - const classes = ReactDOM.findDOMNode(instance).className; - - assert.include(classes, 'media-body'); - assert.include(classes, 'custom-class'); - }); - - xit('should allow custom elements instead of "div"', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'SECTION'); - }); - - xit('should render children', () => { - const instance = ReactTestUtils.renderIntoDocument( - - Content - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong') - ); - }); -}); diff --git a/fork/react-bootstrap/src/MediaBody.test.jsx b/fork/react-bootstrap/src/MediaBody.test.jsx new file mode 100644 index 00000000000..0328aa8c0ef --- /dev/null +++ b/fork/react-bootstrap/src/MediaBody.test.jsx @@ -0,0 +1,53 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Media from '../src/Media'; + +describe('', () => { + xit('uses "div" by default', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + }); + + xit('has "media-body" class', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.include(ReactDOM.findDOMNode(instance).className, 'media-body'); + }); + + xit('should be able to change alignment to middle', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bmedia-middle\b/)); + }); + + xit('should be able to change alignment to bottom', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bmedia-bottom\b/)); + }); + + xit('should merge additional classes passed in', () => { + const instance = ReactTestUtils.renderIntoDocument(); + const classes = ReactDOM.findDOMNode(instance).className; + + assert.include(classes, 'media-body'); + assert.include(classes, 'custom-class'); + }); + + xit('should allow custom elements instead of "div"', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'SECTION'); + }); + + xit('should render children', () => { + const instance = ReactTestUtils.renderIntoDocument( + + Content + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong')); + }); +}); diff --git a/fork/react-bootstrap/src/MediaHeading.js b/fork/react-bootstrap/src/MediaHeading.jsx similarity index 55% rename from fork/react-bootstrap/src/MediaHeading.js rename to fork/react-bootstrap/src/MediaHeading.jsx index 0be2107b467..0b4e1c3320b 100644 --- a/fork/react-bootstrap/src/MediaHeading.js +++ b/fork/react-bootstrap/src/MediaHeading.jsx @@ -5,24 +5,22 @@ import elementType from 'prop-types-extra/lib/elementType'; import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; const propTypes = { - componentClass: elementType + componentClass: elementType, }; const defaultProps = { - componentClass: 'h4' + componentClass: 'h4', }; class MediaHeading extends React.Component { - render() { - const { componentClass: Component, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); + render() { + const { componentClass: Component, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); - const classes = getClassSet(bsProps); + const classes = getClassSet(bsProps); - return ( - - ); - } + return ; + } } MediaHeading.propTypes = propTypes; diff --git a/fork/react-bootstrap/src/MediaHeading.test.js b/fork/react-bootstrap/src/MediaHeading.test.js deleted file mode 100644 index 6515ac13467..00000000000 --- a/fork/react-bootstrap/src/MediaHeading.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Media from '../src/Media'; - -describe('Media.Heading', () => { - xit('uses "h4" by default', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'H4'); - }); - - xit('has "media-heading" class', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.include(ReactDOM.findDOMNode(instance).className, 'media-heading'); - }); - - xit('should merge additional classes passed in', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.include(ReactDOM.findDOMNode(instance).className, 'media-heading'); - assert.include(ReactDOM.findDOMNode(instance).className, 'custom-class'); - }); - - xit('should allow custom elements instead of "h4"', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'H2'); - }); - - xit('should render children', () => { - const instance = ReactTestUtils.renderIntoDocument( - - Children - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong') - ); - }); -}); diff --git a/fork/react-bootstrap/src/MediaHeading.test.jsx b/fork/react-bootstrap/src/MediaHeading.test.jsx new file mode 100644 index 00000000000..c3a6133c7df --- /dev/null +++ b/fork/react-bootstrap/src/MediaHeading.test.jsx @@ -0,0 +1,30 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; +import Media from '../src/Media'; +describe('Media.Heading', () => { + xit('uses "h4" by default', () => { + const instance = ReactTestUtils.renderIntoDocument(); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'H4'); + }); + xit('has "media-heading" class', () => { + const instance = ReactTestUtils.renderIntoDocument(); + assert.include(ReactDOM.findDOMNode(instance).className, 'media-heading'); + }); + xit('should merge additional classes passed in', () => { + const instance = ReactTestUtils.renderIntoDocument(); + assert.include(ReactDOM.findDOMNode(instance).className, 'media-heading'); + assert.include(ReactDOM.findDOMNode(instance).className, 'custom-class'); + }); + xit('should allow custom elements instead of "h4"', () => { + const instance = ReactTestUtils.renderIntoDocument(); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'H2'); + }); + xit('should render children', () => { + const instance = ReactTestUtils.renderIntoDocument( + + Children + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong')); + }); +}); diff --git a/fork/react-bootstrap/src/MediaLeft.js b/fork/react-bootstrap/src/MediaLeft.js deleted file mode 100644 index 464578f6374..00000000000 --- a/fork/react-bootstrap/src/MediaLeft.js +++ /dev/null @@ -1,37 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; - -import { - bsClass, - getClassSet, - prefix, - splitBsProps, -} from './utils/bootstrapUtils'; - -const propTypes = { - /** - * Align the media to the top, middle, or bottom of the media object. - */ - align: PropTypes.oneOf(['top', 'middle', 'bottom']), -}; - -class MediaLeft extends React.Component { - render() { - const { align, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = getClassSet(bsProps); - - if (align) { - // The class is e.g. `media-top`, not `media-left-top`. - classes[prefix({ bsClass: 'media' }, align)] = true; - } - - return
      ; - } -} - -MediaLeft.propTypes = propTypes; - -export default bsClass('media-left', MediaLeft); diff --git a/fork/react-bootstrap/src/MediaLeft.jsx b/fork/react-bootstrap/src/MediaLeft.jsx new file mode 100644 index 00000000000..060c1cc8296 --- /dev/null +++ b/fork/react-bootstrap/src/MediaLeft.jsx @@ -0,0 +1,32 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import { bsClass, getClassSet, prefix, splitBsProps } from './utils/bootstrapUtils'; + +const propTypes = { + /** + * Align the media to the top, middle, or bottom of the media object. + */ + align: PropTypes.oneOf(['top', 'middle', 'bottom']), +}; + +class MediaLeft extends React.Component { + render() { + const { align, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = getClassSet(bsProps); + + if (align) { + // The class is e.g. `media-top`, not `media-left-top`. + classes[prefix({ bsClass: 'media' }, align)] = true; + } + + return
      ; + } +} + +MediaLeft.propTypes = propTypes; + +export default bsClass('media-left', MediaLeft); diff --git a/fork/react-bootstrap/src/MediaLeft.test.js b/fork/react-bootstrap/src/MediaLeft.test.js deleted file mode 100644 index a5852ab57a4..00000000000 --- a/fork/react-bootstrap/src/MediaLeft.test.js +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Media from '../src/Media'; - -describe('Media.Left', () => { - xit('uses "div"', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); - }); - - xit('has "media-left" class', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bmedia-left\b/)); - }); - - xit('should be able to change alignment to middle', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.ok( - ReactDOM.findDOMNode(instance).className.match(/\bmedia-middle\b/) - ); - }); - - xit('should be able to change alignment to bottom', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.ok( - ReactDOM.findDOMNode(instance).className.match(/\bmedia-bottom\b/) - ); - }); - - xit('should merge additional classes passed in', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.include(ReactDOM.findDOMNode(instance).className, 'media-left'); - assert.include(ReactDOM.findDOMNode(instance).className, 'custom-class'); - }); - - xit('should render children', () => { - const instance = ReactTestUtils.renderIntoDocument( - - - - ); - assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'img')); - }); -}); diff --git a/fork/react-bootstrap/src/MediaLeft.test.jsx b/fork/react-bootstrap/src/MediaLeft.test.jsx new file mode 100644 index 00000000000..370e8791456 --- /dev/null +++ b/fork/react-bootstrap/src/MediaLeft.test.jsx @@ -0,0 +1,46 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Media from '../src/Media'; + +describe('Media.Left', () => { + xit('uses "div"', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + }); + + xit('has "media-left" class', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bmedia-left\b/)); + }); + + xit('should be able to change alignment to middle', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bmedia-middle\b/)); + }); + + xit('should be able to change alignment to bottom', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bmedia-bottom\b/)); + }); + + xit('should merge additional classes passed in', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.include(ReactDOM.findDOMNode(instance).className, 'media-left'); + assert.include(ReactDOM.findDOMNode(instance).className, 'custom-class'); + }); + + xit('should render children', () => { + const instance = ReactTestUtils.renderIntoDocument( + + + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'img')); + }); +}); diff --git a/fork/react-bootstrap/src/MediaList.js b/fork/react-bootstrap/src/MediaList.js deleted file mode 100644 index fc6615c4077..00000000000 --- a/fork/react-bootstrap/src/MediaList.js +++ /dev/null @@ -1,17 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; - -import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; - -class MediaList extends React.Component { - render() { - const { className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = getClassSet(bsProps); - - return
        ; - } -} - -export default bsClass('media-list', MediaList); diff --git a/fork/react-bootstrap/src/MediaList.jsx b/fork/react-bootstrap/src/MediaList.jsx new file mode 100644 index 00000000000..c5f51115630 --- /dev/null +++ b/fork/react-bootstrap/src/MediaList.jsx @@ -0,0 +1,17 @@ +import classNames from 'classnames'; +import React from 'react'; + +import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; + +class MediaList extends React.Component { + render() { + const { className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = getClassSet(bsProps); + + return
          ; + } +} + +export default bsClass('media-list', MediaList); diff --git a/fork/react-bootstrap/src/MediaList.test.js b/fork/react-bootstrap/src/MediaList.test.js deleted file mode 100644 index 00635fd6f3f..00000000000 --- a/fork/react-bootstrap/src/MediaList.test.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Media from '../src/Media'; - -describe('Media.List', () => { - xit('uses "ul"', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'UL'); - }); - xit('has "media-list" class', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.include(ReactDOM.findDOMNode(instance).className, 'media-list'); - }); - xit('should merge additional classes passed in', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - const classes = ReactDOM.findDOMNode(instance).className; - - assert.include(classes, 'media-list'); - assert.include(classes, 'custom-class'); - }); - xit('should render children', () => { - const instance = ReactTestUtils.renderIntoDocument( - - Content - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong') - ); - }); -}); diff --git a/fork/react-bootstrap/src/MediaList.test.jsx b/fork/react-bootstrap/src/MediaList.test.jsx new file mode 100644 index 00000000000..a06cb3c328f --- /dev/null +++ b/fork/react-bootstrap/src/MediaList.test.jsx @@ -0,0 +1,32 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Media from '../src/Media'; + +describe('Media.List', () => { + xit('uses "ul"', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'UL'); + }); + xit('has "media-list" class', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.include(ReactDOM.findDOMNode(instance).className, 'media-list'); + }); + xit('should merge additional classes passed in', () => { + const instance = ReactTestUtils.renderIntoDocument(); + const classes = ReactDOM.findDOMNode(instance).className; + + assert.include(classes, 'media-list'); + assert.include(classes, 'custom-class'); + }); + xit('should render children', () => { + const instance = ReactTestUtils.renderIntoDocument( + + Content + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong')); + }); +}); diff --git a/fork/react-bootstrap/src/MediaListItem.js b/fork/react-bootstrap/src/MediaListItem.js deleted file mode 100644 index 1b9da0dd211..00000000000 --- a/fork/react-bootstrap/src/MediaListItem.js +++ /dev/null @@ -1,17 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; - -import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; - -class MediaListItem extends React.Component { - render() { - const { className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = getClassSet(bsProps); - - return
        • ; - } -} - -export default bsClass('media', MediaListItem); diff --git a/fork/react-bootstrap/src/MediaListItem.jsx b/fork/react-bootstrap/src/MediaListItem.jsx new file mode 100644 index 00000000000..14d281e6c17 --- /dev/null +++ b/fork/react-bootstrap/src/MediaListItem.jsx @@ -0,0 +1,17 @@ +import classNames from 'classnames'; +import React from 'react'; + +import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; + +class MediaListItem extends React.Component { + render() { + const { className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = getClassSet(bsProps); + + return
        • ; + } +} + +export default bsClass('media', MediaListItem); diff --git a/fork/react-bootstrap/src/MediaListItem.test.js b/fork/react-bootstrap/src/MediaListItem.test.js deleted file mode 100644 index 7672b9d1b37..00000000000 --- a/fork/react-bootstrap/src/MediaListItem.test.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Media from '../src/Media'; - -describe('Media.ListItem', () => { - xit('uses "li"', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'LI'); - }); - xit('has "media" class', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.include(ReactDOM.findDOMNode(instance).className, 'media'); - }); - xit('should merge additional classes passed in', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - const classes = ReactDOM.findDOMNode(instance).className; - - assert.include(classes, 'media'); - assert.include(classes, 'custom-class'); - }); - xit('should render children', () => { - const instance = ReactTestUtils.renderIntoDocument( - - Content - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong') - ); - }); -}); diff --git a/fork/react-bootstrap/src/MediaListItem.test.jsx b/fork/react-bootstrap/src/MediaListItem.test.jsx new file mode 100644 index 00000000000..864d14ca46a --- /dev/null +++ b/fork/react-bootstrap/src/MediaListItem.test.jsx @@ -0,0 +1,32 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Media from '../src/Media'; + +describe('Media.ListItem', () => { + xit('uses "li"', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'LI'); + }); + xit('has "media" class', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.include(ReactDOM.findDOMNode(instance).className, 'media'); + }); + xit('should merge additional classes passed in', () => { + const instance = ReactTestUtils.renderIntoDocument(); + const classes = ReactDOM.findDOMNode(instance).className; + + assert.include(classes, 'media'); + assert.include(classes, 'custom-class'); + }); + xit('should render children', () => { + const instance = ReactTestUtils.renderIntoDocument( + + Content + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong')); + }); +}); diff --git a/fork/react-bootstrap/src/MediaRight.js b/fork/react-bootstrap/src/MediaRight.js deleted file mode 100644 index 16a5a22ab6b..00000000000 --- a/fork/react-bootstrap/src/MediaRight.js +++ /dev/null @@ -1,37 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; - -import { - bsClass, - getClassSet, - prefix, - splitBsProps, -} from './utils/bootstrapUtils'; - -const propTypes = { - /** - * Align the media to the top, middle, or bottom of the media object. - */ - align: PropTypes.oneOf(['top', 'middle', 'bottom']), -}; - -class MediaRight extends React.Component { - render() { - const { align, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = getClassSet(bsProps); - - if (align) { - // The class is e.g. `media-top`, not `media-right-top`. - classes[prefix({ bsClass: 'media' }, align)] = true; - } - - return
          ; - } -} - -MediaRight.propTypes = propTypes; - -export default bsClass('media-right', MediaRight); diff --git a/fork/react-bootstrap/src/MediaRight.jsx b/fork/react-bootstrap/src/MediaRight.jsx new file mode 100644 index 00000000000..bc992d23432 --- /dev/null +++ b/fork/react-bootstrap/src/MediaRight.jsx @@ -0,0 +1,32 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import { bsClass, getClassSet, prefix, splitBsProps } from './utils/bootstrapUtils'; + +const propTypes = { + /** + * Align the media to the top, middle, or bottom of the media object. + */ + align: PropTypes.oneOf(['top', 'middle', 'bottom']), +}; + +class MediaRight extends React.Component { + render() { + const { align, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = getClassSet(bsProps); + + if (align) { + // The class is e.g. `media-top`, not `media-right-top`. + classes[prefix({ bsClass: 'media' }, align)] = true; + } + + return
          ; + } +} + +MediaRight.propTypes = propTypes; + +export default bsClass('media-right', MediaRight); diff --git a/fork/react-bootstrap/src/MediaRight.test.js b/fork/react-bootstrap/src/MediaRight.test.js deleted file mode 100644 index f27cf3d65b6..00000000000 --- a/fork/react-bootstrap/src/MediaRight.test.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Media from '../src/Media'; - -describe('Media.Right', () => { - xit('uses "div"', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); - }); - - xit('has "media-right" class', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.ok( - ReactDOM.findDOMNode(instance).className.match(/\bmedia-right\b/) - ); - }); - - xit('should be able to change alignment to middle', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.ok( - ReactDOM.findDOMNode(instance).className.match(/\bmedia-middle\b/) - ); - }); - - xit('should be able to change alignment to bottom', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.ok( - ReactDOM.findDOMNode(instance).className.match(/\bmedia-bottom\b/) - ); - }); - - xit('should merge additional classes passed in', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.include(ReactDOM.findDOMNode(instance).className, 'media-right'); - assert.include(ReactDOM.findDOMNode(instance).className, 'custom-class'); - }); - - xit('should render children', () => { - const instance = ReactTestUtils.renderIntoDocument( - - - - ); - assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'img')); - }); -}); diff --git a/fork/react-bootstrap/src/MediaRight.test.jsx b/fork/react-bootstrap/src/MediaRight.test.jsx new file mode 100644 index 00000000000..14f6c4a313f --- /dev/null +++ b/fork/react-bootstrap/src/MediaRight.test.jsx @@ -0,0 +1,46 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Media from '../src/Media'; + +describe('Media.Right', () => { + xit('uses "div"', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + }); + + xit('has "media-right" class', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bmedia-right\b/)); + }); + + xit('should be able to change alignment to middle', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bmedia-middle\b/)); + }); + + xit('should be able to change alignment to bottom', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bmedia-bottom\b/)); + }); + + xit('should merge additional classes passed in', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.include(ReactDOM.findDOMNode(instance).className, 'media-right'); + assert.include(ReactDOM.findDOMNode(instance).className, 'custom-class'); + }); + + xit('should render children', () => { + const instance = ReactTestUtils.renderIntoDocument( + + + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'img')); + }); +}); diff --git a/fork/react-bootstrap/src/MenuItem.js b/fork/react-bootstrap/src/MenuItem.js deleted file mode 100644 index 9805bda8ae0..00000000000 --- a/fork/react-bootstrap/src/MenuItem.js +++ /dev/null @@ -1,154 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import all from 'prop-types-extra/lib/all'; - -import SafeAnchor from './SafeAnchor'; -import { bsClass, prefix, splitBsPropsAndOmit } from './utils/bootstrapUtils'; -import createChainedFunction from './utils/createChainedFunction'; - -const propTypes = { - /** - * Highlight the menu item as active. - */ - active: PropTypes.bool, - - /** - * Disable the menu item, making it unselectable. - */ - disabled: PropTypes.bool, - - /** - * Styles the menu item as a horizontal rule, providing visual separation between - * groups of menu items. - */ - divider: all( - PropTypes.bool, - ({ divider, children }) => - divider && children - ? new Error('Children will not be rendered for dividers') - : null - ), - - /** - * Value passed to the `onSelect` handler, useful for identifying the selected menu item. - */ - eventKey: PropTypes.any, - - /** - * Styles the menu item as a header label, useful for describing a group of menu items. - */ - header: PropTypes.bool, - - /** - * HTML `href` attribute corresponding to `a.href`. - */ - href: PropTypes.string, - - /** - * Callback fired when the menu item is clicked. - */ - onClick: PropTypes.func, - - /** - * Callback fired when the menu item is selected. - * - * ```js - * (eventKey: any, event: Object) => any - * ``` - */ - onSelect: PropTypes.func -}; - -const defaultProps = { - divider: false, - disabled: false, - header: false -}; - -class MenuItem extends React.Component { - constructor(props, context) { - super(props, context); - - this.handleClick = this.handleClick.bind(this); - } - - handleClick(event) { - const { href, disabled, onSelect, eventKey } = this.props; - - if (!href || disabled) { - event.preventDefault(); - } - - if (disabled) { - return; - } - - if (onSelect) { - onSelect(eventKey, event); - } - } - - render() { - const { - active, - disabled, - divider, - header, - onClick, - className, - style, - ...props - } = this.props; - - const [bsProps, elementProps] = splitBsPropsAndOmit(props, [ - 'eventKey', - 'onSelect' - ]); - - if (divider) { - // Forcibly blank out the children; separators shouldn't render any. - elementProps.children = undefined; - - return ( -
        • - ); - } - - if (header) { - return ( -
        • - ); - } - - return ( -
        • - -
        • - ); - } -} - -MenuItem.propTypes = propTypes; -MenuItem.defaultProps = defaultProps; - -export default bsClass('dropdown', MenuItem); diff --git a/fork/react-bootstrap/src/MenuItem.jsx b/fork/react-bootstrap/src/MenuItem.jsx new file mode 100644 index 00000000000..9b2e3911f41 --- /dev/null +++ b/fork/react-bootstrap/src/MenuItem.jsx @@ -0,0 +1,134 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import all from 'prop-types-extra/lib/all'; + +import SafeAnchor from './SafeAnchor'; +import { bsClass, prefix, splitBsPropsAndOmit } from './utils/bootstrapUtils'; +import createChainedFunction from './utils/createChainedFunction'; + +const propTypes = { + /** + * Highlight the menu item as active. + */ + active: PropTypes.bool, + + /** + * Disable the menu item, making it unselectable. + */ + disabled: PropTypes.bool, + + /** + * Styles the menu item as a horizontal rule, providing visual separation between + * groups of menu items. + */ + divider: all(PropTypes.bool, ({ divider, children }) => + divider && children ? new Error('Children will not be rendered for dividers') : null, + ), + + /** + * Value passed to the `onSelect` handler, useful for identifying the selected menu item. + */ + eventKey: PropTypes.any, + + /** + * Styles the menu item as a header label, useful for describing a group of menu items. + */ + header: PropTypes.bool, + + /** + * HTML `href` attribute corresponding to `a.href`. + */ + href: PropTypes.string, + + /** + * Callback fired when the menu item is clicked. + */ + onClick: PropTypes.func, + + /** + * Callback fired when the menu item is selected. + * + * ```js + * (eventKey: any, event: Object) => any + * ``` + */ + onSelect: PropTypes.func, +}; + +const defaultProps = { + divider: false, + disabled: false, + header: false, +}; + +class MenuItem extends React.Component { + constructor(props, context) { + super(props, context); + + this.handleClick = this.handleClick.bind(this); + } + + handleClick(event) { + const { href, disabled, onSelect, eventKey } = this.props; + + if (!href || disabled) { + event.preventDefault(); + } + + if (disabled) { + return; + } + + if (onSelect) { + onSelect(eventKey, event); + } + } + + render() { + const { active, disabled, divider, header, onClick, className, style, ...props } = this.props; + + const [bsProps, elementProps] = splitBsPropsAndOmit(props, ['eventKey', 'onSelect']); + + if (divider) { + // Forcibly blank out the children; separators shouldn't render any. + elementProps.children = undefined; + + return ( +
        • + ); + } + + if (header) { + return ( +
        • + ); + } + + return ( +
        • + +
        • + ); + } +} + +MenuItem.propTypes = propTypes; +MenuItem.defaultProps = defaultProps; + +export default bsClass('dropdown', MenuItem); diff --git a/fork/react-bootstrap/src/MenuItem.test.js b/fork/react-bootstrap/src/MenuItem.test.js deleted file mode 100644 index c80e37f4cad..00000000000 --- a/fork/react-bootstrap/src/MenuItem.test.js +++ /dev/null @@ -1,248 +0,0 @@ -import { render, screen } from '@testing-library/react'; - -import MenuItem from './MenuItem'; - -describe('', () => { - it('renders divider', () => { - render(); - const node = screen.getByRole('separator'); - expect(node.tagName).toBe('LI'); - expect(node).toHaveClass('divider'); - }); - - // xit('renders divider className and style', () => { - // const instance = ReactTestUtils.renderIntoDocument( - // - // ); - // const node = ReactDOM.findDOMNode(instance); - - // node.className.should.match(/\bfoo bar divider\b/); - // node.style.height.should.equal('100px'); - // }); - - // xit('renders divider not children', () => { - // shouldWarn('Children will not be rendered for dividers'); - - // const instance = ReactTestUtils.renderIntoDocument( - // Some child - // ); - // const node = ReactDOM.findDOMNode(instance); - - // node.className.should.match(/\bdivider\b/); - // node.innerHTML.should.not.match(/Some child/); - // }); - - // xit('renders header', () => { - // const instance = ReactTestUtils.renderIntoDocument( - // Header Text - // ); - // const node = ReactDOM.findDOMNode(instance); - - // node.className.should.match(/\bdropdown-header\b/); - // node.getAttribute('role').should.equal('heading'); - // node.innerHTML.should.match(/Header Text/); - // }); - - // xit('renders header className and style', () => { - // const instance = ReactTestUtils.renderIntoDocument( - // - // Header Text - // - // ); - // const node = ReactDOM.findDOMNode(instance); - - // node.className.should.match(/\bfoo bar dropdown-header\b/); - // node.style.height.should.equal('100px'); - // }); - - // xit('renders menu item link', (done) => { - // const instance = ReactTestUtils.renderIntoDocument( - // done()} href="/herpa-derpa"> - // Item - // - // ); - // const node = ReactDOM.findDOMNode(instance); - // const anchor = ReactTestUtils.findRenderedDOMComponentWithTag( - // instance, - // 'A' - // ); - - // node.getAttribute('role').should.equal('presentation'); - // anchor.getAttribute('role').should.equal('menuitem'); - // anchor.getAttribute('tabIndex').should.equal('-1'); - // anchor.getAttribute('href').should.equal('/herpa-derpa'); - - // anchor.innerHTML.should.match(/Item/); - - // ReactTestUtils.Simulate.keyDown(anchor, { keyCode: 1 }); - // }); - - // xit('click handling with onSelect prop', () => { - // const handleSelect = (eventKey) => { - // eventKey.should.equal('1'); - // }; - // const instance = ReactTestUtils.renderIntoDocument( - // - // Item - // - // ); - // const anchor = ReactTestUtils.findRenderedDOMComponentWithTag( - // instance, - // 'A' - // ); - - // ReactTestUtils.Simulate.click(anchor); - // }); - - // xit('click handling with onSelect prop (no eventKey)', () => { - // const handleSelect = (eventKey) => { - // expect(eventKey).to.be.undefined; - // }; - // const instance = ReactTestUtils.renderIntoDocument( - // Item - // ); - // const anchor = ReactTestUtils.findRenderedDOMComponentWithTag( - // instance, - // 'A' - // ); - - // ReactTestUtils.Simulate.click(anchor); - // }); - - // xit('should call custom onClick', () => { - // const handleClick = sinon.spy(); - // const handleSelect = sinon.spy(); - - // const instance = ReactTestUtils.renderIntoDocument( - // - // Item - // - // ); - // const anchor = ReactTestUtils.findRenderedDOMComponentWithTag( - // instance, - // 'A' - // ); - - // ReactTestUtils.Simulate.click(anchor); - - // expect(handleClick).to.have.been.called; - // expect(handleSelect).to.have.been.called; - // }); - - // xit('does not fire onSelect when divider is clicked', () => { - // const handleSelect = () => { - // throw new Error('Should not invoke onSelect with divider flag applied'); - // }; - // const instance = ReactTestUtils.renderIntoDocument( - // - // ); - // ReactTestUtils.scryRenderedDOMComponentsWithTag( - // instance, - // 'A' - // ).length.should.equal(0); - // const li = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'li'); - - // ReactTestUtils.Simulate.click(li); - // }); - - // xit('does not fire onSelect when header is clicked', () => { - // const handleSelect = () => { - // throw new Error('Should not invoke onSelect with divider flag applied'); - // }; - // const instance = ReactTestUtils.renderIntoDocument( - // - // Header content - // - // ); - // ReactTestUtils.scryRenderedDOMComponentsWithTag( - // instance, - // 'A' - // ).length.should.equal(0); - // const li = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'li'); - - // ReactTestUtils.Simulate.click(li); - // }); - - // xit('does not pass onClick to DOM node', () => { - // shallow( {}}>Item) - // .children() - // .props() - // .should.not.have.property('onSelect'); - // }); - - // xit('does not pass onClick to children', () => { - // shallow( {}}>Item) - // .find('SafeAnchor') - // .props() - // .should.not.have.property('onSelect'); - // }); - - // xit('disabled link', () => { - // const handleSelect = () => { - // throw new Error('Should not invoke onSelect event'); - // }; - // const instance = ReactTestUtils.renderIntoDocument( - // - // Text - // - // ); - // const node = ReactDOM.findDOMNode(instance); - // const anchor = ReactTestUtils.findRenderedDOMComponentWithTag( - // instance, - // 'A' - // ); - - // node.className.should.match(/\bdisabled\b/); - - // ReactTestUtils.Simulate.click(anchor); - // }); - - // xit('should pass through props', () => { - // let instance = ReactTestUtils.renderIntoDocument( - // - // Title - // - // ); - - // let node = ReactDOM.findDOMNode(instance); - - // assert(node.className.match(/\btest-class\b/)); - // assert.equal(node.style.height, '100px'); - // assert.equal(node.getAttribute('href'), null); - // assert.equal(node.getAttribute('title'), null); - - // let anchorNode = ReactTestUtils.findRenderedDOMComponentWithTag( - // instance, - // 'a' - // ); - - // assert.notOk(anchorNode.className.match(/\btest-class\b/)); - // assert.equal(anchorNode.getAttribute('href'), '#hi-mom!'); - // assert.equal(anchorNode.getAttribute('title'), 'hi mom!'); - // }); - - // xit('Should set target attribute on anchor', () => { - // let instance = ReactTestUtils.renderIntoDocument( - // Title - // ); - - // let anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'); - // assert.equal(anchor.getAttribute('target'), '_blank'); - // }); - - // xit('should output an li', () => { - // let instance = ReactTestUtils.renderIntoDocument( - // Title - // ); - // assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'LI'); - // assert.equal( - // ReactDOM.findDOMNode(instance).getAttribute('role'), - // 'presentation' - // ); - // }); -}); diff --git a/fork/react-bootstrap/src/MenuItem.test.jsx b/fork/react-bootstrap/src/MenuItem.test.jsx new file mode 100644 index 00000000000..2a94b9e0021 --- /dev/null +++ b/fork/react-bootstrap/src/MenuItem.test.jsx @@ -0,0 +1,248 @@ +import { render, screen } from '@testing-library/react'; + +import MenuItem from './MenuItem'; + +describe('', () => { + it('renders divider', () => { + render(); + const node = screen.getByRole('separator'); + expect(node.tagName).toBe('LI'); + expect(node).toHaveClass('divider'); + }); + + // xit('renders divider className and style', () => { + // const instance = ReactTestUtils.renderIntoDocument( + // + // ); + // const node = ReactDOM.findDOMNode(instance); + + // node.className.should.match(/\bfoo bar divider\b/); + // node.style.height.should.equal('100px'); + // }); + + // xit('renders divider not children', () => { + // shouldWarn('Children will not be rendered for dividers'); + + // const instance = ReactTestUtils.renderIntoDocument( + // Some child + // ); + // const node = ReactDOM.findDOMNode(instance); + + // node.className.should.match(/\bdivider\b/); + // node.innerHTML.should.not.match(/Some child/); + // }); + + // xit('renders header', () => { + // const instance = ReactTestUtils.renderIntoDocument( + // Header Text + // ); + // const node = ReactDOM.findDOMNode(instance); + + // node.className.should.match(/\bdropdown-header\b/); + // node.getAttribute('role').should.equal('heading'); + // node.innerHTML.should.match(/Header Text/); + // }); + + // xit('renders header className and style', () => { + // const instance = ReactTestUtils.renderIntoDocument( + // + // Header Text + // + // ); + // const node = ReactDOM.findDOMNode(instance); + + // node.className.should.match(/\bfoo bar dropdown-header\b/); + // node.style.height.should.equal('100px'); + // }); + + // xit('renders menu item link', (done) => { + // const instance = ReactTestUtils.renderIntoDocument( + // done()} href="/herpa-derpa"> + // Item + // + // ); + // const node = ReactDOM.findDOMNode(instance); + // const anchor = ReactTestUtils.findRenderedDOMComponentWithTag( + // instance, + // 'A' + // ); + + // node.getAttribute('role').should.equal('presentation'); + // anchor.getAttribute('role').should.equal('menuitem'); + // anchor.getAttribute('tabIndex').should.equal('-1'); + // anchor.getAttribute('href').should.equal('/herpa-derpa'); + + // anchor.innerHTML.should.match(/Item/); + + // ReactTestUtils.Simulate.keyDown(anchor, { keyCode: 1 }); + // }); + + // xit('click handling with onSelect prop', () => { + // const handleSelect = (eventKey) => { + // eventKey.should.equal('1'); + // }; + // const instance = ReactTestUtils.renderIntoDocument( + // + // Item + // + // ); + // const anchor = ReactTestUtils.findRenderedDOMComponentWithTag( + // instance, + // 'A' + // ); + + // ReactTestUtils.Simulate.click(anchor); + // }); + + // xit('click handling with onSelect prop (no eventKey)', () => { + // const handleSelect = (eventKey) => { + // expect(eventKey).to.be.undefined; + // }; + // const instance = ReactTestUtils.renderIntoDocument( + // Item + // ); + // const anchor = ReactTestUtils.findRenderedDOMComponentWithTag( + // instance, + // 'A' + // ); + + // ReactTestUtils.Simulate.click(anchor); + // }); + + // xit('should call custom onClick', () => { + // const handleClick = sinon.spy(); + // const handleSelect = sinon.spy(); + + // const instance = ReactTestUtils.renderIntoDocument( + // + // Item + // + // ); + // const anchor = ReactTestUtils.findRenderedDOMComponentWithTag( + // instance, + // 'A' + // ); + + // ReactTestUtils.Simulate.click(anchor); + + // expect(handleClick).to.have.been.called; + // expect(handleSelect).to.have.been.called; + // }); + + // xit('does not fire onSelect when divider is clicked', () => { + // const handleSelect = () => { + // throw new Error('Should not invoke onSelect with divider flag applied'); + // }; + // const instance = ReactTestUtils.renderIntoDocument( + // + // ); + // ReactTestUtils.scryRenderedDOMComponentsWithTag( + // instance, + // 'A' + // ).length.should.equal(0); + // const li = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'li'); + + // ReactTestUtils.Simulate.click(li); + // }); + + // xit('does not fire onSelect when header is clicked', () => { + // const handleSelect = () => { + // throw new Error('Should not invoke onSelect with divider flag applied'); + // }; + // const instance = ReactTestUtils.renderIntoDocument( + // + // Header content + // + // ); + // ReactTestUtils.scryRenderedDOMComponentsWithTag( + // instance, + // 'A' + // ).length.should.equal(0); + // const li = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'li'); + + // ReactTestUtils.Simulate.click(li); + // }); + + // xit('does not pass onClick to DOM node', () => { + // shallow( {}}>Item) + // .children() + // .props() + // .should.not.have.property('onSelect'); + // }); + + // xit('does not pass onClick to children', () => { + // shallow( {}}>Item) + // .find('SafeAnchor') + // .props() + // .should.not.have.property('onSelect'); + // }); + + // xit('disabled link', () => { + // const handleSelect = () => { + // throw new Error('Should not invoke onSelect event'); + // }; + // const instance = ReactTestUtils.renderIntoDocument( + // + // Text + // + // ); + // const node = ReactDOM.findDOMNode(instance); + // const anchor = ReactTestUtils.findRenderedDOMComponentWithTag( + // instance, + // 'A' + // ); + + // node.className.should.match(/\bdisabled\b/); + + // ReactTestUtils.Simulate.click(anchor); + // }); + + // xit('should pass through props', () => { + // let instance = ReactTestUtils.renderIntoDocument( + // + // Title + // + // ); + + // let node = ReactDOM.findDOMNode(instance); + + // assert(node.className.match(/\btest-class\b/)); + // assert.equal(node.style.height, '100px'); + // assert.equal(node.getAttribute('href'), null); + // assert.equal(node.getAttribute('title'), null); + + // let anchorNode = ReactTestUtils.findRenderedDOMComponentWithTag( + // instance, + // 'a' + // ); + + // assert.notOk(anchorNode.className.match(/\btest-class\b/)); + // assert.equal(anchorNode.getAttribute('href'), '#hi-mom!'); + // assert.equal(anchorNode.getAttribute('title'), 'hi mom!'); + // }); + + // xit('Should set target attribute on anchor', () => { + // let instance = ReactTestUtils.renderIntoDocument( + // Title + // ); + + // let anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'); + // assert.equal(anchor.getAttribute('target'), '_blank'); + // }); + + // xit('should output an li', () => { + // let instance = ReactTestUtils.renderIntoDocument( + // Title + // ); + // assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'LI'); + // assert.equal( + // ReactDOM.findDOMNode(instance).getAttribute('role'), + // 'presentation' + // ); + // }); +}); diff --git a/fork/react-bootstrap/src/Modal.js b/fork/react-bootstrap/src/Modal.js deleted file mode 100644 index 82db21c678c..00000000000 --- a/fork/react-bootstrap/src/Modal.js +++ /dev/null @@ -1,310 +0,0 @@ -import classNames from 'classnames'; -import events from 'dom-helpers/events'; -import ownerDocument from 'dom-helpers/ownerDocument'; -import canUseDOM from 'dom-helpers/util/inDOM'; -import getScrollbarSize from 'dom-helpers/util/scrollbarSize'; -import React from 'react'; -import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; -import BaseModal from 'react-overlays/lib/Modal'; -import isOverflowing from 'react-overlays/lib/utils/isOverflowing'; -import elementType from 'prop-types-extra/lib/elementType'; - -import Fade from './Fade'; -import Body from './ModalBody'; -import ModalDialog from './ModalDialog'; -import Footer from './ModalFooter'; -import Header from './ModalHeader'; -import Title from './ModalTitle'; -import { bsClass, bsSizes, prefix } from './utils/bootstrapUtils'; -import createChainedFunction from './utils/createChainedFunction'; -import splitComponentProps from './utils/splitComponentProps'; -import { Size } from './utils/StyleConfig'; - -const propTypes = { - ...BaseModal.propTypes, - ...ModalDialog.propTypes, - - /** - * Include a backdrop component. Specify 'static' for a backdrop that doesn't - * trigger an "onHide" when clicked. - */ - backdrop: PropTypes.oneOf(['static', true, false]), - - /** - * Add an optional extra class name to .modal-backdrop - * It could end up looking like class="modal-backdrop foo-modal-backdrop in". - */ - backdropClassName: PropTypes.string, - - /** - * Close the modal when escape key is pressed - */ - keyboard: PropTypes.bool, - - /** - * Open and close the Modal with a slide and fade animation. - */ - animation: PropTypes.bool, - - /** - * A Component type that provides the modal content Markup. This is a useful - * prop when you want to use your own styles and markup to create a custom - * modal component. - */ - dialogComponentClass: elementType, - - /** - * When `true` The modal will automatically shift focus to itself when it - * opens, and replace it to the last focused element when it closes. - * Generally this should never be set to false as it makes the Modal less - * accessible to assistive technologies, like screen-readers. - */ - autoFocus: PropTypes.bool, - - /** - * When `true` The modal will prevent focus from leaving the Modal while - * open. Consider leaving the default value here, as it is necessary to make - * the Modal work well with assistive technologies, such as screen readers. - */ - enforceFocus: PropTypes.bool, - - /** - * When `true` The modal will restore focus to previously focused element once - * modal is hidden - */ - restoreFocus: PropTypes.bool, - - /** - * When `true` The modal will show itself. - */ - show: PropTypes.bool, - - /** - * A callback fired when the header closeButton or non-static backdrop is - * clicked. Required if either are specified. - */ - onHide: PropTypes.func, - - /** - * Callback fired before the Modal transitions in - */ - onEnter: PropTypes.func, - - /** - * Callback fired as the Modal begins to transition in - */ - onEntering: PropTypes.func, - - /** - * Callback fired after the Modal finishes transitioning in - */ - onEntered: PropTypes.func, - - /** - * Callback fired right before the Modal transitions out - */ - onExit: PropTypes.func, - - /** - * Callback fired as the Modal begins to transition out - */ - onExiting: PropTypes.func, - - /** - * Callback fired after the Modal finishes transitioning out - */ - onExited: PropTypes.func, - - /** - * @private - */ - container: BaseModal.propTypes.container -}; - -const defaultProps = { - ...BaseModal.defaultProps, - animation: true, - dialogComponentClass: ModalDialog -}; - -const childContextTypes = { - $bs_modal: PropTypes.shape({ - onHide: PropTypes.func - }) -}; - -/* eslint-disable no-use-before-define, react/no-multi-comp */ -function DialogTransition(props) { - return ; -} - -function BackdropTransition(props) { - return ; -} - -/* eslint-enable no-use-before-define */ - -class Modal extends React.Component { - constructor(props, context) { - super(props, context); - - this.handleEntering = this.handleEntering.bind(this); - this.handleExited = this.handleExited.bind(this); - this.handleWindowResize = this.handleWindowResize.bind(this); - this.handleDialogClick = this.handleDialogClick.bind(this); - this.setModalRef = this.setModalRef.bind(this); - - this.state = { - style: {} - }; - } - - getChildContext() { - return { - $bs_modal: { - onHide: this.props.onHide - } - }; - } - - componentWillUnmount() { - // Clean up the listener if we need to. - this.handleExited(); - } - - setModalRef(ref) { - this._modal = ref; - } - - handleDialogClick(e) { - if (this._ignoreBackdropClick || e.target !== e.currentTarget) { - this._ignoreBackdropClick = false; - return; - } - - this.props.onHide(); - } - - handleEntering() { - // FIXME: This should work even when animation is disabled. - events.on(window, 'resize', this.handleWindowResize); - this.updateStyle(); - } - - handleExited() { - // FIXME: This should work even when animation is disabled. - events.off(window, 'resize', this.handleWindowResize); - } - - handleWindowResize() { - this.updateStyle(); - } - - updateStyle() { - if (!canUseDOM) { - return; - } - - const dialogNode = this._modal.getDialogElement(); - const dialogHeight = dialogNode.scrollHeight; - - const document = ownerDocument(dialogNode); - const bodyIsOverflowing = isOverflowing( - ReactDOM.findDOMNode(this.props.container || document.body) - ); - const modalIsOverflowing = - dialogHeight > document.documentElement.clientHeight; - - this.setState({ - style: { - paddingRight: - bodyIsOverflowing && !modalIsOverflowing - ? getScrollbarSize() - : undefined, - paddingLeft: - !bodyIsOverflowing && modalIsOverflowing - ? getScrollbarSize() - : undefined - } - }); - } - - handleDialogBackdropMouseDown = () => { - this._waitingForMouseUp = true; - }; - - handleMouseUp = ev => { - const dialogNode = this._modal.getDialogElement(); - if (this._waitingForMouseUp && ev.target === dialogNode) { - this._ignoreBackdropClick = true; - } - this._waitingForMouseUp = false; - }; - - render() { - const { - backdrop, - backdropClassName, - animation, - show, - dialogComponentClass: Dialog, - className, - style, - children, // Just in case this get added to BaseModal propTypes. - onEntering, - onExited, - ...props - } = this.props; - - const [baseModalProps, dialogProps] = splitComponentProps(props, BaseModal); - - const inClassName = show && !animation && 'in'; - - return ( - - - {children} - - - ); - } -} - -Modal.propTypes = propTypes; -Modal.defaultProps = defaultProps; -Modal.childContextTypes = childContextTypes; - -Modal.Body = Body; -Modal.Header = Header; -Modal.Title = Title; -Modal.Footer = Footer; - -Modal.Dialog = ModalDialog; - -Modal.TRANSITION_DURATION = 300; -Modal.BACKDROP_TRANSITION_DURATION = 150; - -export default bsClass('modal', bsSizes([Size.LARGE, Size.SMALL], Modal)); diff --git a/fork/react-bootstrap/src/Modal.jsx b/fork/react-bootstrap/src/Modal.jsx new file mode 100644 index 00000000000..47926d69401 --- /dev/null +++ b/fork/react-bootstrap/src/Modal.jsx @@ -0,0 +1,299 @@ +import classNames from 'classnames'; +import events from 'dom-helpers/events'; +import ownerDocument from 'dom-helpers/ownerDocument'; +import canUseDOM from 'dom-helpers/util/inDOM'; +import getScrollbarSize from 'dom-helpers/util/scrollbarSize'; +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import BaseModal from 'react-overlays/lib/Modal'; +import isOverflowing from 'react-overlays/lib/utils/isOverflowing'; +import elementType from 'prop-types-extra/lib/elementType'; + +import Fade from './Fade'; +import Body from './ModalBody'; +import ModalDialog from './ModalDialog'; +import Footer from './ModalFooter'; +import Header from './ModalHeader'; +import Title from './ModalTitle'; +import { bsClass, bsSizes, prefix } from './utils/bootstrapUtils'; +import createChainedFunction from './utils/createChainedFunction'; +import splitComponentProps from './utils/splitComponentProps'; +import { Size } from './utils/StyleConfig'; + +const propTypes = { + ...BaseModal.propTypes, + ...ModalDialog.propTypes, + + /** + * Include a backdrop component. Specify 'static' for a backdrop that doesn't + * trigger an "onHide" when clicked. + */ + backdrop: PropTypes.oneOf(['static', true, false]), + + /** + * Add an optional extra class name to .modal-backdrop + * It could end up looking like class="modal-backdrop foo-modal-backdrop in". + */ + backdropClassName: PropTypes.string, + + /** + * Close the modal when escape key is pressed + */ + keyboard: PropTypes.bool, + + /** + * Open and close the Modal with a slide and fade animation. + */ + animation: PropTypes.bool, + + /** + * A Component type that provides the modal content Markup. This is a useful + * prop when you want to use your own styles and markup to create a custom + * modal component. + */ + dialogComponentClass: elementType, + + /** + * When `true` The modal will automatically shift focus to itself when it + * opens, and replace it to the last focused element when it closes. + * Generally this should never be set to false as it makes the Modal less + * accessible to assistive technologies, like screen-readers. + */ + autoFocus: PropTypes.bool, + + /** + * When `true` The modal will prevent focus from leaving the Modal while + * open. Consider leaving the default value here, as it is necessary to make + * the Modal work well with assistive technologies, such as screen readers. + */ + enforceFocus: PropTypes.bool, + + /** + * When `true` The modal will restore focus to previously focused element once + * modal is hidden + */ + restoreFocus: PropTypes.bool, + + /** + * When `true` The modal will show itself. + */ + show: PropTypes.bool, + + /** + * A callback fired when the header closeButton or non-static backdrop is + * clicked. Required if either are specified. + */ + onHide: PropTypes.func, + + /** + * Callback fired before the Modal transitions in + */ + onEnter: PropTypes.func, + + /** + * Callback fired as the Modal begins to transition in + */ + onEntering: PropTypes.func, + + /** + * Callback fired after the Modal finishes transitioning in + */ + onEntered: PropTypes.func, + + /** + * Callback fired right before the Modal transitions out + */ + onExit: PropTypes.func, + + /** + * Callback fired as the Modal begins to transition out + */ + onExiting: PropTypes.func, + + /** + * Callback fired after the Modal finishes transitioning out + */ + onExited: PropTypes.func, + + /** + * @private + */ + container: BaseModal.propTypes.container, +}; + +const defaultProps = { + ...BaseModal.defaultProps, + animation: true, + dialogComponentClass: ModalDialog, +}; + +const childContextTypes = { + $bs_modal: PropTypes.shape({ + onHide: PropTypes.func, + }), +}; + +/* eslint-disable no-use-before-define, react/no-multi-comp */ +function DialogTransition(props) { + return ; +} + +function BackdropTransition(props) { + return ; +} + +/* eslint-enable no-use-before-define */ + +class Modal extends React.Component { + constructor(props, context) { + super(props, context); + + this.handleEntering = this.handleEntering.bind(this); + this.handleExited = this.handleExited.bind(this); + this.handleWindowResize = this.handleWindowResize.bind(this); + this.handleDialogClick = this.handleDialogClick.bind(this); + this.setModalRef = this.setModalRef.bind(this); + + this.state = { + style: {}, + }; + } + + getChildContext() { + return { + $bs_modal: { + onHide: this.props.onHide, + }, + }; + } + + componentWillUnmount() { + // Clean up the listener if we need to. + this.handleExited(); + } + + setModalRef(ref) { + this._modal = ref; + } + + handleDialogClick(e) { + if (this._ignoreBackdropClick || e.target !== e.currentTarget) { + this._ignoreBackdropClick = false; + return; + } + + this.props.onHide(); + } + + handleEntering() { + // FIXME: This should work even when animation is disabled. + events.on(window, 'resize', this.handleWindowResize); + this.updateStyle(); + } + + handleExited() { + // FIXME: This should work even when animation is disabled. + events.off(window, 'resize', this.handleWindowResize); + } + + handleWindowResize() { + this.updateStyle(); + } + + updateStyle() { + if (!canUseDOM) { + return; + } + + const dialogNode = this._modal.getDialogElement(); + const dialogHeight = dialogNode.scrollHeight; + + const document = ownerDocument(dialogNode); + const bodyIsOverflowing = isOverflowing( + ReactDOM.findDOMNode(this.props.container || document.body), + ); + const modalIsOverflowing = dialogHeight > document.documentElement.clientHeight; + + this.setState({ + style: { + paddingRight: bodyIsOverflowing && !modalIsOverflowing ? getScrollbarSize() : undefined, + paddingLeft: !bodyIsOverflowing && modalIsOverflowing ? getScrollbarSize() : undefined, + }, + }); + } + + handleDialogBackdropMouseDown = () => { + this._waitingForMouseUp = true; + }; + + handleMouseUp = ev => { + const dialogNode = this._modal.getDialogElement(); + if (this._waitingForMouseUp && ev.target === dialogNode) { + this._ignoreBackdropClick = true; + } + this._waitingForMouseUp = false; + }; + + render() { + const { + backdrop, + backdropClassName, + animation, + show, + dialogComponentClass: Dialog, + className, + style, + children, // Just in case this get added to BaseModal propTypes. + onEntering, + onExited, + ...props + } = this.props; + + const [baseModalProps, dialogProps] = splitComponentProps(props, BaseModal); + + const inClassName = show && !animation && 'in'; + + return ( + + + {children} + + + ); + } +} + +Modal.propTypes = propTypes; +Modal.defaultProps = defaultProps; +Modal.childContextTypes = childContextTypes; + +Modal.Body = Body; +Modal.Header = Header; +Modal.Title = Title; +Modal.Footer = Footer; + +Modal.Dialog = ModalDialog; + +Modal.TRANSITION_DURATION = 300; +Modal.BACKDROP_TRANSITION_DURATION = 150; + +export default bsClass('modal', bsSizes([Size.LARGE, Size.SMALL], Modal)); diff --git a/fork/react-bootstrap/src/Modal.test.js b/fork/react-bootstrap/src/Modal.test.js deleted file mode 100644 index 971fb48a23d..00000000000 --- a/fork/react-bootstrap/src/Modal.test.js +++ /dev/null @@ -1,274 +0,0 @@ -import events from 'dom-helpers/events'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; -import BaseModal from 'react-overlays/lib/Modal'; - -import Modal from '../src/Modal'; - -import { render } from './helpers'; - -describe('', () => { - let mountPoint; - - beforeEach(() => { - mountPoint = document.createElement('div'); - document.body.appendChild(mountPoint); - }); - - afterEach(() => { - ReactDOM.unmountComponentAtNode(mountPoint); - document.body.removeChild(mountPoint); - }); - - xit('Should render the modal content', () => { - const noOp = () => {}; - const instance = render( - - Message - , - mountPoint - ); - - assert.ok(instance._modal.getDialogElement().querySelector('strong')); - }); - - xit('Should close the modal when the modal dialog is clicked', (done) => { - const doneOp = () => { - done(); - }; - - const instance = render( - - Message - , - mountPoint - ); - - const dialog = instance._modal.getDialogElement(); - - ReactTestUtils.Simulate.click(dialog); - }); - - xit('Should not close the modal when the "static" dialog is clicked', () => { - const onHideSpy = sinon.spy(); - const instance = render( - - Message - , - mountPoint - ); - - const dialog = instance._modal.getDialogElement(); - - ReactTestUtils.Simulate.click(dialog); - - expect(onHideSpy).to.not.have.been.called; - }); - - xit('Should close the modal when the modal close button is clicked', (done) => { - const doneOp = () => { - done(); - }; - - const instance = render( - - - Message - , - mountPoint - ); - - const button = instance._modal - .getDialogElement() - .getElementsByClassName('close')[0]; - - ReactTestUtils.Simulate.click(button); - }); - - xit('Should pass className to the dialog', () => { - const noOp = () => {}; - const instance = render( - - Message - , - mountPoint - ); - - const dialog = instance._modal.getDialogElement(); - - assert.ok(dialog.className.match(/\bmymodal\b/)); - }); - - xit('Should use bsClass on the dialog', () => { - const noOp = () => {}; - const instance = render( - - Message - , - mountPoint - ); - - const modal = instance._modal.getDialogElement(); - - assert.ok(modal.className.match(/\bmymodal\b/)); - assert.ok(modal.children[0].className.match(/\bmymodal-dialog\b/)); - assert.ok( - modal.children[0].children[0].className.match(/\bmymodal-content\b/) - ); - - const baseModal = ReactTestUtils.findRenderedComponentWithType( - instance, - BaseModal - ); - assert.ok(baseModal.backdrop.className.match(/\bmymodal-backdrop\b/)); - }); - - xit('Should use backdropClassName to add classes to the backdrop', () => { - const noOp = () => {}; - const instance = render( - - Message - , - mountPoint - ); - - const baseModal = ReactTestUtils.findRenderedComponentWithType( - instance, - BaseModal - ); - assert.ok( - baseModal.backdrop.className.match(/\bmodal-backdrop my-modal-backdrop\b/) - ); - }); - - xit('Should pass bsSize to the dialog', () => { - const noOp = () => {}; - const instance = render( - - Message - , - mountPoint - ); - - const dialog = instance._modal - .getDialogElement() - .getElementsByClassName('modal-dialog')[0]; - - assert.ok(dialog.className.match(/\bmodal-sm\b/)); - }); - - xit('Should pass dialog style to the dialog', () => { - const noOp = () => {}; - const instance = render( - - Message - , - mountPoint - ); - - const dialog = instance._modal.getDialogElement(); - - assert.ok(dialog.style.top === '1000px'); - }); - - xit('Should pass dialogClassName to the dialog', () => { - const noOp = () => {}; - const instance = render( - - Message - , - mountPoint - ); - - const dialog = instance._modal - .getDialogElement() - .querySelector('.modal-dialog'); - - assert.ok(dialog.className.match(/\btestCss\b/)); - }); - - xit('Should use dialogComponentClass', () => { - const noOp = () => {}; - - function CustomDialog() { - return
          ; - } - - const instance = render( - - Message - , - mountPoint - ); - - assert.equal(instance._modal.getDialogElement().className, 'custom-dialog'); - }); - - xit('Should pass transition callbacks to Transition', (done) => { - let count = 0; - const increment = () => { - ++count; - }; - - const instance = render( - {}} - onExit={increment} - onExiting={increment} - onExited={() => { - increment(); - expect(count).to.equal(6); - done(); - }} - onEnter={increment} - onEntering={increment} - onEntered={() => { - increment(); - instance.renderWithProps({ show: false }); - }} - > - Message - , - mountPoint - ); - }); - - describe('cleanup', () => { - let offSpy; - - beforeEach(() => { - offSpy = sinon.spy(events, 'off'); - }); - - afterEach(() => { - events.off.restore(); - }); - - xit('should remove resize listener when unmounted', () => { - class Component extends React.Component { - constructor(props, context) { - super(props, context); - - this.state = { - show: true, - }; - } - - render() { - if (!this.state.show) { - return null; - } - - return Foo; - } - } - - const instance = render(, mountPoint); - instance.setState({ show: false }); - - expect(offSpy).to.have.been.calledWith(window, 'resize'); - }); - }); -}); diff --git a/fork/react-bootstrap/src/Modal.test.jsx b/fork/react-bootstrap/src/Modal.test.jsx new file mode 100644 index 00000000000..dd7f2b1bc92 --- /dev/null +++ b/fork/react-bootstrap/src/Modal.test.jsx @@ -0,0 +1,258 @@ +import events from 'dom-helpers/events'; + +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; +import BaseModal from 'react-overlays/lib/Modal'; + +import Modal from '../src/Modal'; + +import { render } from './helpers'; + +describe('', () => { + let mountPoint; + + beforeEach(() => { + mountPoint = document.createElement('div'); + document.body.appendChild(mountPoint); + }); + + afterEach(() => { + ReactDOM.unmountComponentAtNode(mountPoint); + document.body.removeChild(mountPoint); + }); + + xit('Should render the modal content', () => { + const noOp = () => {}; + const instance = render( + + Message + , + mountPoint, + ); + + assert.ok(instance._modal.getDialogElement().querySelector('strong')); + }); + + xit('Should close the modal when the modal dialog is clicked', done => { + const doneOp = () => { + done(); + }; + + const instance = render( + + Message + , + mountPoint, + ); + + const dialog = instance._modal.getDialogElement(); + + ReactTestUtils.Simulate.click(dialog); + }); + + xit('Should not close the modal when the "static" dialog is clicked', () => { + const onHideSpy = sinon.spy(); + const instance = render( + + Message + , + mountPoint, + ); + + const dialog = instance._modal.getDialogElement(); + + ReactTestUtils.Simulate.click(dialog); + + expect(onHideSpy).to.not.have.been.called; + }); + + xit('Should close the modal when the modal close button is clicked', done => { + const doneOp = () => { + done(); + }; + + const instance = render( + + + Message + , + mountPoint, + ); + + const button = instance._modal.getDialogElement().getElementsByClassName('close')[0]; + + ReactTestUtils.Simulate.click(button); + }); + + xit('Should pass className to the dialog', () => { + const noOp = () => {}; + const instance = render( + + Message + , + mountPoint, + ); + + const dialog = instance._modal.getDialogElement(); + + assert.ok(dialog.className.match(/\bmymodal\b/)); + }); + + xit('Should use bsClass on the dialog', () => { + const noOp = () => {}; + const instance = render( + + Message + , + mountPoint, + ); + + const modal = instance._modal.getDialogElement(); + + assert.ok(modal.className.match(/\bmymodal\b/)); + assert.ok(modal.children[0].className.match(/\bmymodal-dialog\b/)); + assert.ok(modal.children[0].children[0].className.match(/\bmymodal-content\b/)); + + const baseModal = ReactTestUtils.findRenderedComponentWithType(instance, BaseModal); + assert.ok(baseModal.backdrop.className.match(/\bmymodal-backdrop\b/)); + }); + + xit('Should use backdropClassName to add classes to the backdrop', () => { + const noOp = () => {}; + const instance = render( + + Message + , + mountPoint, + ); + + const baseModal = ReactTestUtils.findRenderedComponentWithType(instance, BaseModal); + assert.ok(baseModal.backdrop.className.match(/\bmodal-backdrop my-modal-backdrop\b/)); + }); + + xit('Should pass bsSize to the dialog', () => { + const noOp = () => {}; + const instance = render( + + Message + , + mountPoint, + ); + + const dialog = instance._modal.getDialogElement().getElementsByClassName('modal-dialog')[0]; + + assert.ok(dialog.className.match(/\bmodal-sm\b/)); + }); + + xit('Should pass dialog style to the dialog', () => { + const noOp = () => {}; + const instance = render( + + Message + , + mountPoint, + ); + + const dialog = instance._modal.getDialogElement(); + + assert.ok(dialog.style.top === '1000px'); + }); + + xit('Should pass dialogClassName to the dialog', () => { + const noOp = () => {}; + const instance = render( + + Message + , + mountPoint, + ); + + const dialog = instance._modal.getDialogElement().querySelector('.modal-dialog'); + + assert.ok(dialog.className.match(/\btestCss\b/)); + }); + + xit('Should use dialogComponentClass', () => { + const noOp = () => {}; + + function CustomDialog() { + return
          ; + } + + const instance = render( + + Message + , + mountPoint, + ); + + assert.equal(instance._modal.getDialogElement().className, 'custom-dialog'); + }); + + xit('Should pass transition callbacks to Transition', done => { + let count = 0; + const increment = () => { + ++count; + }; + + const instance = render( + {}} + onExit={increment} + onExiting={increment} + onExited={() => { + increment(); + expect(count).to.equal(6); + done(); + }} + onEnter={increment} + onEntering={increment} + onEntered={() => { + increment(); + instance.renderWithProps({ show: false }); + }} + > + Message + , + mountPoint, + ); + }); + + describe('cleanup', () => { + let offSpy; + + beforeEach(() => { + offSpy = sinon.spy(events, 'off'); + }); + + afterEach(() => { + events.off.restore(); + }); + + xit('should remove resize listener when unmounted', () => { + class Component extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = { + show: true, + }; + } + + render() { + if (!this.state.show) { + return null; + } + + return Foo; + } + } + + const instance = render(, mountPoint); + instance.setState({ show: false }); + + expect(offSpy).to.have.been.calledWith(window, 'resize'); + }); + }); +}); diff --git a/fork/react-bootstrap/src/ModalBody.js b/fork/react-bootstrap/src/ModalBody.jsx similarity index 55% rename from fork/react-bootstrap/src/ModalBody.js rename to fork/react-bootstrap/src/ModalBody.jsx index 147f3d7b21b..c152008c870 100644 --- a/fork/react-bootstrap/src/ModalBody.js +++ b/fork/react-bootstrap/src/ModalBody.jsx @@ -5,24 +5,22 @@ import elementType from 'prop-types-extra/lib/elementType'; import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; const propTypes = { - componentClass: elementType + componentClass: elementType, }; const defaultProps = { - componentClass: 'div' + componentClass: 'div', }; class ModalBody extends React.Component { - render() { - const { componentClass: Component, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); + render() { + const { componentClass: Component, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); - const classes = getClassSet(bsProps); + const classes = getClassSet(bsProps); - return ( - - ); - } + return ; + } } ModalBody.propTypes = propTypes; diff --git a/fork/react-bootstrap/src/ModalBody.test.js b/fork/react-bootstrap/src/ModalBody.test.js deleted file mode 100644 index 85e8ff33f4a..00000000000 --- a/fork/react-bootstrap/src/ModalBody.test.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Modal from '../src/Modal'; - -describe('Modal.Body', () => { - xit('uses "div" by default', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); - }); - - xit('has "modal-body" class', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.include(ReactDOM.findDOMNode(instance).className, 'modal-body'); - }); - - xit('should merge additional classes passed in', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - const classes = ReactDOM.findDOMNode(instance).className; - - assert.include(classes, 'modal-body'); - assert.include(classes, 'custom-class'); - }); - - xit('should allow custom elements instead of "div"', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'SECTION'); - }); - - xit('should render children', () => { - const instance = ReactTestUtils.renderIntoDocument( - - Content - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong') - ); - }); -}); diff --git a/fork/react-bootstrap/src/ModalBody.test.jsx b/fork/react-bootstrap/src/ModalBody.test.jsx new file mode 100644 index 00000000000..0846971e681 --- /dev/null +++ b/fork/react-bootstrap/src/ModalBody.test.jsx @@ -0,0 +1,31 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; +import Modal from '../src/Modal'; +describe('Modal.Body', () => { + xit('uses "div" by default', () => { + const instance = ReactTestUtils.renderIntoDocument(); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + }); + xit('has "modal-body" class', () => { + const instance = ReactTestUtils.renderIntoDocument(); + assert.include(ReactDOM.findDOMNode(instance).className, 'modal-body'); + }); + xit('should merge additional classes passed in', () => { + const instance = ReactTestUtils.renderIntoDocument(); + const classes = ReactDOM.findDOMNode(instance).className; + assert.include(classes, 'modal-body'); + assert.include(classes, 'custom-class'); + }); + xit('should allow custom elements instead of "div"', () => { + const instance = ReactTestUtils.renderIntoDocument(); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'SECTION'); + }); + xit('should render children', () => { + const instance = ReactTestUtils.renderIntoDocument( + + Content + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong')); + }); +}); diff --git a/fork/react-bootstrap/src/ModalDialog.js b/fork/react-bootstrap/src/ModalDialog.js deleted file mode 100644 index 8624558e445..00000000000 --- a/fork/react-bootstrap/src/ModalDialog.js +++ /dev/null @@ -1,68 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; - -import { - bsClass, - bsSizes, - getClassSet, - prefix, - splitBsProps -} from './utils/bootstrapUtils'; -import { Size } from './utils/StyleConfig'; - -const propTypes = { - /** - * A css class to apply to the Modal dialog DOM node. - */ - dialogClassName: PropTypes.string -}; - -class ModalDialog extends React.Component { - render() { - const { - dialogClassName, - className, - style, - children, - onMouseDownDialog, - ...props - } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const bsClassName = prefix(bsProps); - - const modalStyle = { display: 'block', ...style }; - - const dialogClasses = { - ...getClassSet(bsProps), - [bsClassName]: false, - [prefix(bsProps, 'dialog')]: true - }; - - return ( -
          - { - // eslint-disable-next-line jsx-a11y/no-static-element-interactions, prettier/prettier - }
          -
          - {children} -
          -
          -
          - ); - } -} - -ModalDialog.propTypes = propTypes; - -export default bsClass('modal', bsSizes([Size.LARGE, Size.SMALL], ModalDialog)); diff --git a/fork/react-bootstrap/src/ModalDialog.jsx b/fork/react-bootstrap/src/ModalDialog.jsx new file mode 100644 index 00000000000..39685268843 --- /dev/null +++ b/fork/react-bootstrap/src/ModalDialog.jsx @@ -0,0 +1,53 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import { bsClass, bsSizes, getClassSet, prefix, splitBsProps } from './utils/bootstrapUtils'; +import { Size } from './utils/StyleConfig'; + +const propTypes = { + /** + * A css class to apply to the Modal dialog DOM node. + */ + dialogClassName: PropTypes.string, +}; + +class ModalDialog extends React.Component { + render() { + const { dialogClassName, className, style, children, onMouseDownDialog, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const bsClassName = prefix(bsProps); + + const modalStyle = { display: 'block', ...style }; + + const dialogClasses = { + ...getClassSet(bsProps), + [bsClassName]: false, + [prefix(bsProps, 'dialog')]: true, + }; + + return ( +
          + { + // eslint-disable-next-line jsx-a11y/no-static-element-interactions, prettier/prettier + } +
          +
          + {children} +
          +
          +
          + ); + } +} + +ModalDialog.propTypes = propTypes; + +export default bsClass('modal', bsSizes([Size.LARGE, Size.SMALL], ModalDialog)); diff --git a/fork/react-bootstrap/src/ModalFooter.js b/fork/react-bootstrap/src/ModalFooter.jsx similarity index 55% rename from fork/react-bootstrap/src/ModalFooter.js rename to fork/react-bootstrap/src/ModalFooter.jsx index fcb770ed4c7..c4222547f98 100644 --- a/fork/react-bootstrap/src/ModalFooter.js +++ b/fork/react-bootstrap/src/ModalFooter.jsx @@ -5,24 +5,22 @@ import elementType from 'prop-types-extra/lib/elementType'; import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; const propTypes = { - componentClass: elementType + componentClass: elementType, }; const defaultProps = { - componentClass: 'div' + componentClass: 'div', }; class ModalFooter extends React.Component { - render() { - const { componentClass: Component, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); + render() { + const { componentClass: Component, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); - const classes = getClassSet(bsProps); + const classes = getClassSet(bsProps); - return ( - - ); - } + return ; + } } ModalFooter.propTypes = propTypes; diff --git a/fork/react-bootstrap/src/ModalFooter.test.js b/fork/react-bootstrap/src/ModalFooter.test.js deleted file mode 100644 index 92891625123..00000000000 --- a/fork/react-bootstrap/src/ModalFooter.test.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Modal from '../src/Modal'; - -describe('Modal.Footer', () => { - xit('uses "div" by default', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); - }); - - xit('has "modal-footer" class', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.include(ReactDOM.findDOMNode(instance).className, 'modal-footer'); - }); - - xit('should merge additional classes passed in', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - const classes = ReactDOM.findDOMNode(instance).className; - - assert.include(classes, 'modal-footer'); - assert.include(classes, 'custom-class'); - }); - - xit('should allow custom elements instead of "div"', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'SECTION'); - }); - - xit('should render children', () => { - const instance = ReactTestUtils.renderIntoDocument( - - Content - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong') - ); - }); -}); diff --git a/fork/react-bootstrap/src/ModalFooter.test.jsx b/fork/react-bootstrap/src/ModalFooter.test.jsx new file mode 100644 index 00000000000..a49163eeda5 --- /dev/null +++ b/fork/react-bootstrap/src/ModalFooter.test.jsx @@ -0,0 +1,41 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Modal from '../src/Modal'; + +describe('Modal.Footer', () => { + xit('uses "div" by default', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + }); + + xit('has "modal-footer" class', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.include(ReactDOM.findDOMNode(instance).className, 'modal-footer'); + }); + + xit('should merge additional classes passed in', () => { + const instance = ReactTestUtils.renderIntoDocument(); + const classes = ReactDOM.findDOMNode(instance).className; + + assert.include(classes, 'modal-footer'); + assert.include(classes, 'custom-class'); + }); + + xit('should allow custom elements instead of "div"', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'SECTION'); + }); + + xit('should render children', () => { + const instance = ReactTestUtils.renderIntoDocument( + + Content + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong')); + }); +}); diff --git a/fork/react-bootstrap/src/ModalHeader.js b/fork/react-bootstrap/src/ModalHeader.js deleted file mode 100644 index 78286299441..00000000000 --- a/fork/react-bootstrap/src/ModalHeader.js +++ /dev/null @@ -1,79 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; -import createChainedFunction from './utils/createChainedFunction'; -import CloseButton from './CloseButton'; - -// TODO: `aria-label` should be `closeLabel`. - -const propTypes = { - /** - * Provides an accessible label for the close - * button. It is used for Assistive Technology when the label text is not - * readable. - */ - closeLabel: PropTypes.string, - - /** - * Specify whether the Component should contain a close button - */ - closeButton: PropTypes.bool, - - /** - * A Callback fired when the close button is clicked. If used directly inside - * a Modal component, the onHide will automatically be propagated up to the - * parent Modal `onHide`. - */ - onHide: PropTypes.func -}; - -const defaultProps = { - closeLabel: 'Close', - closeButton: false -}; - -const contextTypes = { - $bs_modal: PropTypes.shape({ - onHide: PropTypes.func - }) -}; - -class ModalHeader extends React.Component { - render() { - const { - closeLabel, - closeButton, - onHide, - className, - children, - ...props - } = this.props; - - const modal = this.context.$bs_modal; - - const [bsProps, elementProps] = splitBsProps(props); - - const classes = getClassSet(bsProps); - - return ( -
          - {closeButton && ( - - )} - - {children} -
          - ); - } -} - -ModalHeader.propTypes = propTypes; -ModalHeader.defaultProps = defaultProps; -ModalHeader.contextTypes = contextTypes; - -export default bsClass('modal-header', ModalHeader); diff --git a/fork/react-bootstrap/src/ModalHeader.jsx b/fork/react-bootstrap/src/ModalHeader.jsx new file mode 100644 index 00000000000..1010b3af08f --- /dev/null +++ b/fork/react-bootstrap/src/ModalHeader.jsx @@ -0,0 +1,72 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; +import createChainedFunction from './utils/createChainedFunction'; +import CloseButton from './CloseButton'; + +// TODO: `aria-label` should be `closeLabel`. + +const propTypes = { + /** + * Provides an accessible label for the close + * button. It is used for Assistive Technology when the label text is not + * readable. + */ + closeLabel: PropTypes.string, + + /** + * Specify whether the Component should contain a close button + */ + closeButton: PropTypes.bool, + + /** + * A Callback fired when the close button is clicked. If used directly inside + * a Modal component, the onHide will automatically be propagated up to the + * parent Modal `onHide`. + */ + onHide: PropTypes.func, +}; + +const defaultProps = { + closeLabel: 'Close', + closeButton: false, +}; + +const contextTypes = { + $bs_modal: PropTypes.shape({ + onHide: PropTypes.func, + }), +}; + +class ModalHeader extends React.Component { + render() { + const { closeLabel, closeButton, onHide, className, children, ...props } = this.props; + + const modal = this.context.$bs_modal; + + const [bsProps, elementProps] = splitBsProps(props); + + const classes = getClassSet(bsProps); + + return ( +
          + {closeButton && ( + + )} + + {children} +
          + ); + } +} + +ModalHeader.propTypes = propTypes; +ModalHeader.defaultProps = defaultProps; +ModalHeader.contextTypes = contextTypes; + +export default bsClass('modal-header', ModalHeader); diff --git a/fork/react-bootstrap/src/ModalHeader.test.js b/fork/react-bootstrap/src/ModalHeader.test.js deleted file mode 100644 index 775a4be9fa5..00000000000 --- a/fork/react-bootstrap/src/ModalHeader.test.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Modal from '../src/Modal'; - -describe('Modal.Header', () => { - xit('uses "div" by default', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); - }); - - xit('has "modal-header" class', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.include(ReactDOM.findDOMNode(instance).className, 'modal-header'); - }); - - xit('should merge additional classes passed in', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - const classes = ReactDOM.findDOMNode(instance).className; - - assert.include(classes, 'modal-header'); - assert.include(classes, 'custom-class'); - }); - - xit('should render children', () => { - const instance = ReactTestUtils.renderIntoDocument( - - Content - - ); - - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong') - ); - }); - - xit('has closeButton without a containing Modal and renders', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.isNotNull(ReactDOM.findDOMNode(instance)); - }); - - xit('Should trigger onHide when modal is closed', () => { - const onHideSpy = sinon.spy(); - const instance = ReactTestUtils.renderIntoDocument( - - ); - - const closeButton = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'close' - ); - - ReactTestUtils.Simulate.click(closeButton); - - expect(onHideSpy).to.have.been.called; - }); -}); diff --git a/fork/react-bootstrap/src/ModalHeader.test.jsx b/fork/react-bootstrap/src/ModalHeader.test.jsx new file mode 100644 index 00000000000..bb958f7d408 --- /dev/null +++ b/fork/react-bootstrap/src/ModalHeader.test.jsx @@ -0,0 +1,55 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Modal from '../src/Modal'; + +describe('Modal.Header', () => { + xit('uses "div" by default', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + }); + + xit('has "modal-header" class', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.include(ReactDOM.findDOMNode(instance).className, 'modal-header'); + }); + + xit('should merge additional classes passed in', () => { + const instance = ReactTestUtils.renderIntoDocument(); + const classes = ReactDOM.findDOMNode(instance).className; + + assert.include(classes, 'modal-header'); + assert.include(classes, 'custom-class'); + }); + + xit('should render children', () => { + const instance = ReactTestUtils.renderIntoDocument( + + Content + , + ); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong')); + }); + + xit('has closeButton without a containing Modal and renders', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.isNotNull(ReactDOM.findDOMNode(instance)); + }); + + xit('Should trigger onHide when modal is closed', () => { + const onHideSpy = sinon.spy(); + const instance = ReactTestUtils.renderIntoDocument( + , + ); + + const closeButton = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'close'); + + ReactTestUtils.Simulate.click(closeButton); + + expect(onHideSpy).to.have.been.called; + }); +}); diff --git a/fork/react-bootstrap/src/ModalTitle.js b/fork/react-bootstrap/src/ModalTitle.jsx similarity index 55% rename from fork/react-bootstrap/src/ModalTitle.js rename to fork/react-bootstrap/src/ModalTitle.jsx index 86bde453236..e9fc6824783 100644 --- a/fork/react-bootstrap/src/ModalTitle.js +++ b/fork/react-bootstrap/src/ModalTitle.jsx @@ -5,24 +5,22 @@ import elementType from 'prop-types-extra/lib/elementType'; import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; const propTypes = { - componentClass: elementType + componentClass: elementType, }; const defaultProps = { - componentClass: 'h4' + componentClass: 'h4', }; class ModalTitle extends React.Component { - render() { - const { componentClass: Component, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); + render() { + const { componentClass: Component, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); - const classes = getClassSet(bsProps); + const classes = getClassSet(bsProps); - return ( - - ); - } + return ; + } } ModalTitle.propTypes = propTypes; diff --git a/fork/react-bootstrap/src/ModalTitle.test.js b/fork/react-bootstrap/src/ModalTitle.test.js deleted file mode 100644 index cfbd755b416..00000000000 --- a/fork/react-bootstrap/src/ModalTitle.test.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Modal from '../src/Modal'; - -describe('Modal.Title', () => { - xit('uses "h4" by default', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'H4'); - }); - - xit('has "modal-title" class', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - assert.include(ReactDOM.findDOMNode(instance).className, 'modal-title'); - }); - - xit('should merge additional classes passed in', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - const classes = ReactDOM.findDOMNode(instance).className; - - assert.include(classes, 'modal-title'); - assert.include(classes, 'custom-class'); - }); - - xit('should allow custom elements instead of "h4"', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'H3'); - }); - - xit('should render children', () => { - const instance = ReactTestUtils.renderIntoDocument( - - Children - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong') - ); - }); -}); diff --git a/fork/react-bootstrap/src/ModalTitle.test.jsx b/fork/react-bootstrap/src/ModalTitle.test.jsx new file mode 100644 index 00000000000..18fb79e9053 --- /dev/null +++ b/fork/react-bootstrap/src/ModalTitle.test.jsx @@ -0,0 +1,41 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Modal from '../src/Modal'; + +describe('Modal.Title', () => { + xit('uses "h4" by default', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'H4'); + }); + + xit('has "modal-title" class', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.include(ReactDOM.findDOMNode(instance).className, 'modal-title'); + }); + + xit('should merge additional classes passed in', () => { + const instance = ReactTestUtils.renderIntoDocument(); + const classes = ReactDOM.findDOMNode(instance).className; + + assert.include(classes, 'modal-title'); + assert.include(classes, 'custom-class'); + }); + + xit('should allow custom elements instead of "h4"', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'H3'); + }); + + xit('should render children', () => { + const instance = ReactTestUtils.renderIntoDocument( + + Children + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong')); + }); +}); diff --git a/fork/react-bootstrap/src/Nav.js b/fork/react-bootstrap/src/Nav.jsx similarity index 100% rename from fork/react-bootstrap/src/Nav.js rename to fork/react-bootstrap/src/Nav.jsx diff --git a/fork/react-bootstrap/src/Nav.test.js b/fork/react-bootstrap/src/Nav.test.jsx similarity index 100% rename from fork/react-bootstrap/src/Nav.test.js rename to fork/react-bootstrap/src/Nav.test.jsx diff --git a/fork/react-bootstrap/src/NavDropdown.js b/fork/react-bootstrap/src/NavDropdown.js deleted file mode 100644 index 3120de66292..00000000000 --- a/fork/react-bootstrap/src/NavDropdown.js +++ /dev/null @@ -1,95 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; - -import Dropdown from './Dropdown'; -import splitComponentProps from './utils/splitComponentProps'; -import ValidComponentChildren from './utils/ValidComponentChildren'; - -const propTypes = { - ...Dropdown.propTypes, - - // Toggle props. - title: PropTypes.node.isRequired, - noCaret: PropTypes.bool, - active: PropTypes.bool, - activeKey: PropTypes.any, - activeHref: PropTypes.string, - - // Override generated docs from . - /** - * @private - */ - children: PropTypes.node -}; - -class NavDropdown extends React.Component { - isActive({ props }, activeKey, activeHref) { - if ( - props.active || - (activeKey != null && props.eventKey === activeKey) || - (activeHref && props.href === activeHref) - ) { - return true; - } - - if ( - ValidComponentChildren.some(props.children, child => - this.isActive(child, activeKey, activeHref) - ) - ) { - return true; - } - - return props.active; - } - - render() { - const { - title, - activeKey, - activeHref, - className, - style, - children, - ...props - } = this.props; - - const active = this.isActive(this, activeKey, activeHref); - delete props.active; // Accessed via this.isActive(). - delete props.eventKey; // Accessed via this.isActive(). - - const [dropdownProps, toggleProps] = splitComponentProps( - props, - Dropdown.ControlledComponent - ); - - // Unlike for the other dropdowns, styling needs to go to the `` - // rather than the ``. - - return ( - - - {title} - - - - {ValidComponentChildren.map(children, child => - React.cloneElement(child, { - active: this.isActive(child, activeKey, activeHref) - }) - )} - - - ); - } -} - -NavDropdown.propTypes = propTypes; - -export default NavDropdown; diff --git a/fork/react-bootstrap/src/NavDropdown.jsx b/fork/react-bootstrap/src/NavDropdown.jsx new file mode 100644 index 00000000000..79a8831b124 --- /dev/null +++ b/fork/react-bootstrap/src/NavDropdown.jsx @@ -0,0 +1,84 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import Dropdown from './Dropdown'; +import splitComponentProps from './utils/splitComponentProps'; +import ValidComponentChildren from './utils/ValidComponentChildren'; + +const propTypes = { + ...Dropdown.propTypes, + + // Toggle props. + title: PropTypes.node.isRequired, + noCaret: PropTypes.bool, + active: PropTypes.bool, + activeKey: PropTypes.any, + activeHref: PropTypes.string, + + // Override generated docs from . + /** + * @private + */ + children: PropTypes.node, +}; + +class NavDropdown extends React.Component { + isActive({ props }, activeKey, activeHref) { + if ( + props.active || + (activeKey != null && props.eventKey === activeKey) || + (activeHref && props.href === activeHref) + ) { + return true; + } + + if ( + ValidComponentChildren.some(props.children, child => + this.isActive(child, activeKey, activeHref), + ) + ) { + return true; + } + + return props.active; + } + + render() { + const { title, activeKey, activeHref, className, style, children, ...props } = this.props; + + const active = this.isActive(this, activeKey, activeHref); + delete props.active; // Accessed via this.isActive(). + delete props.eventKey; // Accessed via this.isActive(). + + const [dropdownProps, toggleProps] = splitComponentProps(props, Dropdown.ControlledComponent); + + // Unlike for the other dropdowns, styling needs to go to the `` + // rather than the ``. + + return ( + + + {title} + + + + {ValidComponentChildren.map(children, child => + React.cloneElement(child, { + active: this.isActive(child, activeKey, activeHref), + }), + )} + + + ); + } +} + +NavDropdown.propTypes = propTypes; + +export default NavDropdown; diff --git a/fork/react-bootstrap/src/NavDropdown.test.js b/fork/react-bootstrap/src/NavDropdown.test.js deleted file mode 100644 index 9b06c8be9af..00000000000 --- a/fork/react-bootstrap/src/NavDropdown.test.js +++ /dev/null @@ -1,164 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import MenuItem from '../src/MenuItem'; -import Nav from '../src/Nav'; -import NavDropdown from '../src/NavDropdown'; - -describe('', () => { - xit('Should render li when in nav', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - const dropdown = ReactDOM.findDOMNode( - ReactTestUtils.findRenderedComponentWithType(instance, NavDropdown) - ); - const button = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'dropdown-toggle' - ); - - assert.equal(dropdown.nodeName, 'LI'); - assert.ok(dropdown.className.match(/\bdropdown\b/)); - assert.ok(dropdown.className.match(/\btest-class\b/)); - assert.notOk(dropdown.className.match(/\bactive\b/)); - assert.equal(button.nodeName, 'A'); - assert.equal(button.textContent.trim(), 'Title'); - }); - - xit('renders div with active class', () => { - const instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); - - const li = ReactDOM.findDOMNode(instance); - - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'active') - ); - assert.ok(li.className.match(/\btest-class\b/)); // it still has the given className - assert.ok(li.className.match(/\bactive\b/)); // plus the active class - }); - - xit('is open with explicit prop', () => { - class OpenProp extends React.Component { - constructor(props) { - super(props); - - this.state = { - open: false, - }; - } - - render() { - return ( -
          - - {}} - title="Prop open control" - id="test-id" - > - Item 1 - -
          - ); - } - } - - const instance = ReactTestUtils.renderIntoDocument(); - const outerToggle = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'outer-button' - ); - const dropdownNode = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'dropdown' - ); - - dropdownNode.className.should.not.match(/\bopen\b/); - ReactTestUtils.Simulate.click(outerToggle); - dropdownNode.className.should.match(/\bopen\b/); - ReactTestUtils.Simulate.click(outerToggle); - dropdownNode.className.should.not.match(/\bopen\b/); - }); - - xit('should handle child active state', () => { - const instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - MenuItem 3 content - - ); - - expect(ReactDOM.findDOMNode(instance).className).to.match(/active/); - - const items = ReactTestUtils.scryRenderedComponentsWithType( - instance, - MenuItem - ); - expect(ReactDOM.findDOMNode(items[0]).className).to.not.match(/active/); - expect(ReactDOM.findDOMNode(items[1]).className).to.match(/active/); - expect(ReactDOM.findDOMNode(items[2]).className).to.not.match(/active/); - }); - - xit('should handle nested child null active state', () => { - class Container extends React.Component { - render() { - return null; - } - } - - const instance = ReactTestUtils.renderIntoDocument( - - - MenuItem 1 content - - - ); - - const container = ReactTestUtils.findRenderedComponentWithType( - instance, - Container - ); - expect(container.props.active).to.not.be.false; - }); - - xit('should derive bsClass from parent', () => { - const instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - - ); - - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'my-dropdown-toggle' - ) - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'my-dropdown-menu' - ) - ); - }); -}); diff --git a/fork/react-bootstrap/src/NavDropdown.test.jsx b/fork/react-bootstrap/src/NavDropdown.test.jsx new file mode 100644 index 00000000000..8f4d90cd534 --- /dev/null +++ b/fork/react-bootstrap/src/NavDropdown.test.jsx @@ -0,0 +1,136 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import MenuItem from '../src/MenuItem'; +import Nav from '../src/Nav'; +import NavDropdown from '../src/NavDropdown'; + +describe('', () => { + xit('Should render li when in nav', () => { + const instance = ReactTestUtils.renderIntoDocument( + , + ); + + const dropdown = ReactDOM.findDOMNode( + ReactTestUtils.findRenderedComponentWithType(instance, NavDropdown), + ); + const button = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'dropdown-toggle'); + + assert.equal(dropdown.nodeName, 'LI'); + assert.ok(dropdown.className.match(/\bdropdown\b/)); + assert.ok(dropdown.className.match(/\btest-class\b/)); + assert.notOk(dropdown.className.match(/\bactive\b/)); + assert.equal(button.nodeName, 'A'); + assert.equal(button.textContent.trim(), 'Title'); + }); + + xit('renders div with active class', () => { + const instance = ReactTestUtils.renderIntoDocument( + + MenuItem 1 content + MenuItem 2 content + , + ); + + const li = ReactDOM.findDOMNode(instance); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'active')); + assert.ok(li.className.match(/\btest-class\b/)); // it still has the given className + assert.ok(li.className.match(/\bactive\b/)); // plus the active class + }); + + xit('is open with explicit prop', () => { + class OpenProp extends React.Component { + constructor(props) { + super(props); + + this.state = { + open: false, + }; + } + + render() { + return ( +
          + + {}} + title="Prop open control" + id="test-id" + > + Item 1 + +
          + ); + } + } + + const instance = ReactTestUtils.renderIntoDocument(); + const outerToggle = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'outer-button'); + const dropdownNode = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'dropdown'); + + dropdownNode.className.should.not.match(/\bopen\b/); + ReactTestUtils.Simulate.click(outerToggle); + dropdownNode.className.should.match(/\bopen\b/); + ReactTestUtils.Simulate.click(outerToggle); + dropdownNode.className.should.not.match(/\bopen\b/); + }); + + xit('should handle child active state', () => { + const instance = ReactTestUtils.renderIntoDocument( + + MenuItem 1 content + MenuItem 2 content + MenuItem 3 content + , + ); + + expect(ReactDOM.findDOMNode(instance).className).to.match(/active/); + + const items = ReactTestUtils.scryRenderedComponentsWithType(instance, MenuItem); + expect(ReactDOM.findDOMNode(items[0]).className).to.not.match(/active/); + expect(ReactDOM.findDOMNode(items[1]).className).to.match(/active/); + expect(ReactDOM.findDOMNode(items[2]).className).to.not.match(/active/); + }); + + xit('should handle nested child null active state', () => { + class Container extends React.Component { + render() { + return null; + } + } + + const instance = ReactTestUtils.renderIntoDocument( + + + MenuItem 1 content + + , + ); + + const container = ReactTestUtils.findRenderedComponentWithType(instance, Container); + expect(container.props.active).to.not.be.false; + }); + + xit('should derive bsClass from parent', () => { + const instance = ReactTestUtils.renderIntoDocument( + + MenuItem 1 content + , + ); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'my-dropdown-toggle')); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'my-dropdown-menu')); + }); +}); diff --git a/fork/react-bootstrap/src/NavItem.js b/fork/react-bootstrap/src/NavItem.js deleted file mode 100644 index a7a083f275d..00000000000 --- a/fork/react-bootstrap/src/NavItem.js +++ /dev/null @@ -1,85 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; - -import SafeAnchor from './SafeAnchor'; -import createChainedFunction from './utils/createChainedFunction'; - -const propTypes = { - active: PropTypes.bool, - disabled: PropTypes.bool, - role: PropTypes.string, - href: PropTypes.string, - onClick: PropTypes.func, - onSelect: PropTypes.func, - eventKey: PropTypes.any -}; - -const defaultProps = { - active: false, - disabled: false -}; - -class NavItem extends React.Component { - constructor(props, context) { - super(props, context); - - this.handleClick = this.handleClick.bind(this); - } - - handleClick(e) { - if (this.props.disabled) { - e.preventDefault(); - return; - } - - if (this.props.onSelect) { - this.props.onSelect(this.props.eventKey, e); - } - } - - render() { - const { - active, - disabled, - onClick, - className, - style, - ...props - } = this.props; - - delete props.onSelect; - delete props.eventKey; - - // These are injected down by `
          }> - - - ); - const overlayTrigger = ReactDOM.findDOMNode(instance); - ReactTestUtils.Simulate.click(overlayTrigger); - - instance.state.show.should.be.true; - }); - - xit('Should not set aria-describedby if the state is not show', () => { - const instance = ReactTestUtils.renderIntoDocument( - test
          }> - - - ); - const overlayTrigger = ReactDOM.findDOMNode(instance); - - assert.equal(overlayTrigger.getAttribute('aria-describedby'), null); - }); - - xit('Should set aria-describedby if the state is show', () => { - const instance = ReactTestUtils.renderIntoDocument( - test
      }> - - - ); - const overlayTrigger = ReactDOM.findDOMNode(instance); - ReactTestUtils.Simulate.click(overlayTrigger); - - overlayTrigger.getAttribute('aria-describedby').should.be; - }); - - describe('trigger handlers', () => { - let mountPoint; - - beforeEach(() => { - mountPoint = document.createElement('div'); - document.body.appendChild(mountPoint); - }); - - afterEach(() => { - ReactDOM.unmountComponentAtNode(mountPoint); - document.body.removeChild(mountPoint); - }); - - xit('Should keep trigger handlers', (done) => { - const instance = render( -
      - test
      }> - - - -
      , - mountPoint - ); - - const overlayTrigger = instance.firstChild; - ReactTestUtils.Simulate.blur(overlayTrigger); - }); - }); - - xit('Should maintain overlay classname', () => { - const instance = ReactTestUtils.renderIntoDocument( - test
      } - > - - - ); - - const overlayTrigger = ReactDOM.findDOMNode(instance); - ReactTestUtils.Simulate.click(overlayTrigger); - - expect(document.getElementsByClassName('test-overlay').length).to.equal(1); - }); - - xit('Should pass transition callbacks to Transition', (done) => { - let count = 0; - const increment = () => count++; - - let overlayTrigger; - - const instance = ReactTestUtils.renderIntoDocument( - test
      } - onExit={increment} - onExiting={increment} - onExited={() => { - increment(); - expect(count).to.equal(6); - done(); - }} - onEnter={increment} - onEntering={increment} - onEntered={() => { - increment(); - ReactTestUtils.Simulate.click(overlayTrigger); - }} - > - - - ); - - overlayTrigger = ReactDOM.findDOMNode(instance); - ReactTestUtils.Simulate.click(overlayTrigger); - }); - - xit('Should forward requested context', () => { - const contextTypes = { - key: PropTypes.string, - }; - - const contextSpy = sinon.spy(); - - class ContextReader extends React.Component { - render() { - contextSpy(this.context.key); - return
      ; - } - } - - ContextReader.contextTypes = contextTypes; - - class ContextHolder extends React.Component { - getChildContext() { - return { key: 'value' }; - } - - render() { - return ( - }> - - - ); - } - } - ContextHolder.childContextTypes = contextTypes; - - const instance = ReactTestUtils.renderIntoDocument(); - const overlayTrigger = ReactDOM.findDOMNode(instance); - ReactTestUtils.Simulate.click(overlayTrigger); - - contextSpy.calledWith('value').should.be.true; - }); - - describe('overlay types', () => { - [ - { - name: 'Popover', - overlay: test, - }, - { - name: 'Tooltip', - overlay: test, - }, - ].forEach((testCase) => { - describe(testCase.name, () => { - let instance; - let overlayTrigger; - - beforeEach(() => { - instance = ReactTestUtils.renderIntoDocument( - - - - ); - overlayTrigger = ReactDOM.findDOMNode(instance); - }); - - xit('Should handle trigger without warnings', () => { - ReactTestUtils.Simulate.click(overlayTrigger); - }); - }); - }); - }); - - describe('rootClose', () => { - [ - { - label: 'true', - rootClose: true, - shownAfterClick: false, - }, - { - label: 'default (false)', - rootClose: null, - shownAfterClick: true, - }, - ].forEach((testCase) => { - describe(testCase.label, () => { - let instance; - - beforeEach(() => { - instance = ReactTestUtils.renderIntoDocument( - test
      } - trigger="click" - rootClose={testCase.rootClose} - > - - - ); - const overlayTrigger = ReactDOM.findDOMNode(instance); - ReactTestUtils.Simulate.click(overlayTrigger); - }); - - xit('Should have correct show state', () => { - // Need to click this way for it to propagate to document element. - document.documentElement.click(); - - expect(instance.state.show).to.equal(testCase.shownAfterClick); - }); - }); - }); - - describe('clicking on trigger to hide', () => { - let mountNode; - - beforeEach(() => { - mountNode = document.createElement('div'); - document.body.appendChild(mountNode); - }); - - afterEach(() => { - ReactDOM.unmountComponentAtNode(mountNode); - document.body.removeChild(mountNode); - }); - - xit('should hide after clicking on trigger', () => { - const instance = ReactDOM.render( - test
      } trigger="click" rootClose> - - , - mountNode - ); - - const node = ReactDOM.findDOMNode(instance); - expect(instance.state.show).to.be.false; - - node.click(); - expect(instance.state.show).to.be.true; - - // Need to click this way for it to propagate to document element. - node.click(); - expect(instance.state.show).to.be.false; - }); - }); - - describe('replaced overlay', () => { - let instance; - - beforeEach(() => { - class ReplacedOverlay extends React.Component { - constructor(props) { - super(props); - - this.handleClick = this.handleClick.bind(this); - this.state = { replaced: false }; - } - - handleClick() { - this.setState({ replaced: true }); - } - - render() { - if (this.state.replaced) { - return
      replaced
      ; - } - - return ( - - ); - } - } - - instance = ReactTestUtils.renderIntoDocument( - } - trigger="click" - rootClose - > - - - ); - const overlayTrigger = ReactDOM.findDOMNode(instance); - ReactTestUtils.Simulate.click(overlayTrigger); - }); - - xit('Should still be shown', () => { - // Need to click this way for it to propagate to document element. - const replaceOverlay = document.getElementById('replace-overlay'); - replaceOverlay.click(); - - instance.state.show.should.be.true; - }); - }); - }); -}); diff --git a/fork/react-bootstrap/src/OverlayTrigger.test.jsx b/fork/react-bootstrap/src/OverlayTrigger.test.jsx new file mode 100644 index 00000000000..dbc13626524 --- /dev/null +++ b/fork/react-bootstrap/src/OverlayTrigger.test.jsx @@ -0,0 +1,337 @@ +import PropTypes from 'prop-types'; + +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import OverlayTrigger from '../src/OverlayTrigger'; +import Popover from '../src/Popover'; +import Tooltip from '../src/Tooltip'; + +import { render } from './helpers'; + +describe('', () => { + // Swallow extra props. + const Div = ({ className, children }) =>
      {children}
      ; + + xit('Should create OverlayTrigger element', () => { + const instance = ReactTestUtils.renderIntoDocument( + test
      }> + + , + ); + const overlayTrigger = ReactDOM.findDOMNode(instance); + assert.equal(overlayTrigger.nodeName, 'BUTTON'); + }); + + xit('Should pass OverlayTrigger onClick prop to child', () => { + const callback = sinon.spy(); + const instance = ReactTestUtils.renderIntoDocument( + test} onClick={callback}> + + , + ); + const overlayTrigger = ReactDOM.findDOMNode(instance); + ReactTestUtils.Simulate.click(overlayTrigger); + callback.called.should.be.true; + }); + + xit('Should show after click trigger', () => { + const instance = ReactTestUtils.renderIntoDocument( + test}> + + , + ); + const overlayTrigger = ReactDOM.findDOMNode(instance); + ReactTestUtils.Simulate.click(overlayTrigger); + + instance.state.show.should.be.true; + }); + + xit('Should not set aria-describedby if the state is not show', () => { + const instance = ReactTestUtils.renderIntoDocument( + test}> + + , + ); + const overlayTrigger = ReactDOM.findDOMNode(instance); + + assert.equal(overlayTrigger.getAttribute('aria-describedby'), null); + }); + + xit('Should set aria-describedby if the state is show', () => { + const instance = ReactTestUtils.renderIntoDocument( + test}> + + , + ); + const overlayTrigger = ReactDOM.findDOMNode(instance); + ReactTestUtils.Simulate.click(overlayTrigger); + + overlayTrigger.getAttribute('aria-describedby').should.be; + }); + + describe('trigger handlers', () => { + let mountPoint; + + beforeEach(() => { + mountPoint = document.createElement('div'); + document.body.appendChild(mountPoint); + }); + + afterEach(() => { + ReactDOM.unmountComponentAtNode(mountPoint); + document.body.removeChild(mountPoint); + }); + + xit('Should keep trigger handlers', done => { + const instance = render( +
      + test
      }> + + + + , + mountPoint, + ); + + const overlayTrigger = instance.firstChild; + ReactTestUtils.Simulate.blur(overlayTrigger); + }); + }); + + xit('Should maintain overlay classname', () => { + const instance = ReactTestUtils.renderIntoDocument( + test}> + + , + ); + + const overlayTrigger = ReactDOM.findDOMNode(instance); + ReactTestUtils.Simulate.click(overlayTrigger); + + expect(document.getElementsByClassName('test-overlay').length).to.equal(1); + }); + + xit('Should pass transition callbacks to Transition', done => { + let count = 0; + const increment = () => count++; + + let overlayTrigger; + + const instance = ReactTestUtils.renderIntoDocument( + test} + onExit={increment} + onExiting={increment} + onExited={() => { + increment(); + expect(count).to.equal(6); + done(); + }} + onEnter={increment} + onEntering={increment} + onEntered={() => { + increment(); + ReactTestUtils.Simulate.click(overlayTrigger); + }} + > + + , + ); + + overlayTrigger = ReactDOM.findDOMNode(instance); + ReactTestUtils.Simulate.click(overlayTrigger); + }); + + xit('Should forward requested context', () => { + const contextTypes = { + key: PropTypes.string, + }; + + const contextSpy = sinon.spy(); + + class ContextReader extends React.Component { + render() { + contextSpy(this.context.key); + return
      ; + } + } + + ContextReader.contextTypes = contextTypes; + + class ContextHolder extends React.Component { + getChildContext() { + return { key: 'value' }; + } + + render() { + return ( + }> + + + ); + } + } + ContextHolder.childContextTypes = contextTypes; + + const instance = ReactTestUtils.renderIntoDocument(); + const overlayTrigger = ReactDOM.findDOMNode(instance); + ReactTestUtils.Simulate.click(overlayTrigger); + + contextSpy.calledWith('value').should.be.true; + }); + + describe('overlay types', () => { + [ + { + name: 'Popover', + overlay: test, + }, + { + name: 'Tooltip', + overlay: test, + }, + ].forEach(testCase => { + describe(testCase.name, () => { + let instance; + let overlayTrigger; + + beforeEach(() => { + instance = ReactTestUtils.renderIntoDocument( + + + , + ); + overlayTrigger = ReactDOM.findDOMNode(instance); + }); + + xit('Should handle trigger without warnings', () => { + ReactTestUtils.Simulate.click(overlayTrigger); + }); + }); + }); + }); + + describe('rootClose', () => { + [ + { + label: 'true', + rootClose: true, + shownAfterClick: false, + }, + { + label: 'default (false)', + rootClose: null, + shownAfterClick: true, + }, + ].forEach(testCase => { + describe(testCase.label, () => { + let instance; + + beforeEach(() => { + instance = ReactTestUtils.renderIntoDocument( + test
      } + trigger="click" + rootClose={testCase.rootClose} + > + + , + ); + const overlayTrigger = ReactDOM.findDOMNode(instance); + ReactTestUtils.Simulate.click(overlayTrigger); + }); + + xit('Should have correct show state', () => { + // Need to click this way for it to propagate to document element. + document.documentElement.click(); + + expect(instance.state.show).to.equal(testCase.shownAfterClick); + }); + }); + }); + + describe('clicking on trigger to hide', () => { + let mountNode; + + beforeEach(() => { + mountNode = document.createElement('div'); + document.body.appendChild(mountNode); + }); + + afterEach(() => { + ReactDOM.unmountComponentAtNode(mountNode); + document.body.removeChild(mountNode); + }); + + xit('should hide after clicking on trigger', () => { + const instance = ReactDOM.render( + test} trigger="click" rootClose> + + , + mountNode, + ); + + const node = ReactDOM.findDOMNode(instance); + expect(instance.state.show).to.be.false; + + node.click(); + expect(instance.state.show).to.be.true; + + // Need to click this way for it to propagate to document element. + node.click(); + expect(instance.state.show).to.be.false; + }); + }); + + describe('replaced overlay', () => { + let instance; + + beforeEach(() => { + class ReplacedOverlay extends React.Component { + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + this.state = { replaced: false }; + } + + handleClick() { + this.setState({ replaced: true }); + } + + render() { + if (this.state.replaced) { + return
      replaced
      ; + } + + return ( + + ); + } + } + + instance = ReactTestUtils.renderIntoDocument( + } trigger="click" rootClose> + + , + ); + const overlayTrigger = ReactDOM.findDOMNode(instance); + ReactTestUtils.Simulate.click(overlayTrigger); + }); + + xit('Should still be shown', () => { + // Need to click this way for it to propagate to document element. + const replaceOverlay = document.getElementById('replace-overlay'); + replaceOverlay.click(); + + instance.state.show.should.be.true; + }); + }); + }); +}); diff --git a/fork/react-bootstrap/src/PageHeader.js b/fork/react-bootstrap/src/PageHeader.js deleted file mode 100644 index 436821ab934..00000000000 --- a/fork/react-bootstrap/src/PageHeader.js +++ /dev/null @@ -1,21 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; - -import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; - -class PageHeader extends React.Component { - render() { - const { className, children, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = getClassSet(bsProps); - - return ( -
      -

      {children}

      -
      - ); - } -} - -export default bsClass('page-header', PageHeader); diff --git a/fork/react-bootstrap/src/PageHeader.jsx b/fork/react-bootstrap/src/PageHeader.jsx new file mode 100644 index 00000000000..5eb45d2d820 --- /dev/null +++ b/fork/react-bootstrap/src/PageHeader.jsx @@ -0,0 +1,21 @@ +import classNames from 'classnames'; +import React from 'react'; + +import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; + +class PageHeader extends React.Component { + render() { + const { className, children, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = getClassSet(bsProps); + + return ( +
      +

      {children}

      +
      + ); + } +} + +export default bsClass('page-header', PageHeader); diff --git a/fork/react-bootstrap/src/PageHeader.test.js b/fork/react-bootstrap/src/PageHeader.test.js deleted file mode 100644 index dc3a94c86f9..00000000000 --- a/fork/react-bootstrap/src/PageHeader.test.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import PageHeader from '../src/PageHeader'; - -describe('PageHeader', () => { - xit('Should output a div with content', () => { - let instance = ReactTestUtils.renderIntoDocument( - - Content - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong') - ); - }); - - xit('Should have a page-header class', () => { - let instance = ReactTestUtils.renderIntoDocument( - Content - ); - assert.ok( - ReactDOM.findDOMNode(instance).className.match(/\bpage-header\b/) - ); - }); -}); diff --git a/fork/react-bootstrap/src/PageHeader.test.jsx b/fork/react-bootstrap/src/PageHeader.test.jsx new file mode 100644 index 00000000000..c55aa34a3df --- /dev/null +++ b/fork/react-bootstrap/src/PageHeader.test.jsx @@ -0,0 +1,20 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import PageHeader from '../src/PageHeader'; + +describe('PageHeader', () => { + xit('Should output a div with content', () => { + let instance = ReactTestUtils.renderIntoDocument( + + Content + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong')); + }); + + xit('Should have a page-header class', () => { + let instance = ReactTestUtils.renderIntoDocument(Content); + assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bpage-header\b/)); + }); +}); diff --git a/fork/react-bootstrap/src/PageItem.js b/fork/react-bootstrap/src/PageItem.js index 168fdb4b54e..d0366ec7ed9 100644 --- a/fork/react-bootstrap/src/PageItem.js +++ b/fork/react-bootstrap/src/PageItem.js @@ -1,8 +1,4 @@ import PagerItem from './PagerItem'; import deprecationWarning from './utils/deprecationWarning'; -export default deprecationWarning.wrapper( - PagerItem, - '``', - '``' -); +export default deprecationWarning.wrapper(PagerItem, '``', '``'); diff --git a/fork/react-bootstrap/src/Pager.js b/fork/react-bootstrap/src/Pager.jsx similarity index 51% rename from fork/react-bootstrap/src/Pager.js rename to fork/react-bootstrap/src/Pager.jsx index ceef92a05ff..dbc565dce89 100644 --- a/fork/react-bootstrap/src/Pager.js +++ b/fork/react-bootstrap/src/Pager.jsx @@ -8,26 +8,26 @@ import createChainedFunction from './utils/createChainedFunction'; import ValidComponentChildren from './utils/ValidComponentChildren'; const propTypes = { - onSelect: PropTypes.func + onSelect: PropTypes.func, }; class Pager extends React.Component { - render() { - const { onSelect, className, children, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - const classes = getClassSet(bsProps); - - return ( -
        - {ValidComponentChildren.map(children, child => - cloneElement(child, { - onSelect: createChainedFunction(child.props.onSelect, onSelect) - }) - )} -
      - ); - } + render() { + const { onSelect, className, children, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + const classes = getClassSet(bsProps); + + return ( +
        + {ValidComponentChildren.map(children, child => + cloneElement(child, { + onSelect: createChainedFunction(child.props.onSelect, onSelect), + }), + )} +
      + ); + } } Pager.propTypes = propTypes; diff --git a/fork/react-bootstrap/src/Pager.test.js b/fork/react-bootstrap/src/Pager.test.js deleted file mode 100644 index e431db89749..00000000000 --- a/fork/react-bootstrap/src/Pager.test.js +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Pager from '../src/Pager'; - -describe('Pager', () => { - xit('Should output a unordered list as root element with class "pager"', () => { - let instance = ReactTestUtils.renderIntoDocument(); - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'UL'); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'pager') - ); - }); - - xit('Should allow "Pager.Item" as child element', () => { - let instance = ReactTestUtils.renderIntoDocument( - - Top - - ); - assert.equal(ReactDOM.findDOMNode(instance).children.length, 1); - assert.equal(ReactDOM.findDOMNode(instance).children[0].nodeName, 'LI'); - }); - - xit('Should allow multiple "Pager.Item" as child elements', () => { - let instance = ReactTestUtils.renderIntoDocument( - - - Previous - - - Top - - - Next - - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'previous') - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'disabled') - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'next') - ); - }); - - xit('Should call "onSelect" when item is clicked', (done) => { - function handleSelect(key, e) { - assert.equal(key, 2); - assert.equal(e.target.hash, '#next'); - done(); - } - let instance = ReactTestUtils.renderIntoDocument( - - - Previous - - - Next - - - ); - - let items = ReactTestUtils.scryRenderedComponentsWithType( - instance, - Pager.Item - ); - - ReactTestUtils.Simulate.click( - ReactTestUtils.findRenderedDOMComponentWithTag(items[1], 'a') - ); - }); -}); diff --git a/fork/react-bootstrap/src/Pager.test.jsx b/fork/react-bootstrap/src/Pager.test.jsx new file mode 100644 index 00000000000..7d41570790d --- /dev/null +++ b/fork/react-bootstrap/src/Pager.test.jsx @@ -0,0 +1,63 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Pager from '../src/Pager'; + +describe('Pager', () => { + xit('Should output a unordered list as root element with class "pager"', () => { + let instance = ReactTestUtils.renderIntoDocument(); + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'UL'); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'pager')); + }); + + xit('Should allow "Pager.Item" as child element', () => { + let instance = ReactTestUtils.renderIntoDocument( + + Top + , + ); + assert.equal(ReactDOM.findDOMNode(instance).children.length, 1); + assert.equal(ReactDOM.findDOMNode(instance).children[0].nodeName, 'LI'); + }); + + xit('Should allow multiple "Pager.Item" as child elements', () => { + let instance = ReactTestUtils.renderIntoDocument( + + + Previous + + + Top + + + Next + + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'previous')); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'disabled')); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'next')); + }); + + xit('Should call "onSelect" when item is clicked', done => { + function handleSelect(key, e) { + assert.equal(key, 2); + assert.equal(e.target.hash, '#next'); + done(); + } + let instance = ReactTestUtils.renderIntoDocument( + + + Previous + + + Next + + , + ); + + let items = ReactTestUtils.scryRenderedComponentsWithType(instance, Pager.Item); + + ReactTestUtils.Simulate.click(ReactTestUtils.findRenderedDOMComponentWithTag(items[1], 'a')); + }); +}); diff --git a/fork/react-bootstrap/src/PagerItem.js b/fork/react-bootstrap/src/PagerItem.js deleted file mode 100644 index 8d7927a136a..00000000000 --- a/fork/react-bootstrap/src/PagerItem.js +++ /dev/null @@ -1,75 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; - -import SafeAnchor from './SafeAnchor'; -import createChainedFunction from './utils/createChainedFunction'; - -const propTypes = { - disabled: PropTypes.bool, - previous: PropTypes.bool, - next: PropTypes.bool, - onClick: PropTypes.func, - onSelect: PropTypes.func, - eventKey: PropTypes.any -}; - -const defaultProps = { - disabled: false, - previous: false, - next: false -}; - -class PagerItem extends React.Component { - constructor(props, context) { - super(props, context); - - this.handleSelect = this.handleSelect.bind(this); - } - - handleSelect(e) { - const { disabled, onSelect, eventKey } = this.props; - - if (disabled) { - e.preventDefault(); - return; - } - - if (onSelect) { - onSelect(eventKey, e); - } - } - - render() { - const { - disabled, - previous, - next, - onClick, - className, - style, - ...props - } = this.props; - - delete props.onSelect; - delete props.eventKey; - - return ( -
    15. - -
    16. - ); - } -} - -PagerItem.propTypes = propTypes; -PagerItem.defaultProps = defaultProps; - -export default PagerItem; diff --git a/fork/react-bootstrap/src/PagerItem.jsx b/fork/react-bootstrap/src/PagerItem.jsx new file mode 100644 index 00000000000..422e2016fc7 --- /dev/null +++ b/fork/react-bootstrap/src/PagerItem.jsx @@ -0,0 +1,64 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import SafeAnchor from './SafeAnchor'; +import createChainedFunction from './utils/createChainedFunction'; + +const propTypes = { + disabled: PropTypes.bool, + previous: PropTypes.bool, + next: PropTypes.bool, + onClick: PropTypes.func, + onSelect: PropTypes.func, + eventKey: PropTypes.any, +}; + +const defaultProps = { + disabled: false, + previous: false, + next: false, +}; + +class PagerItem extends React.Component { + constructor(props, context) { + super(props, context); + + this.handleSelect = this.handleSelect.bind(this); + } + + handleSelect(e) { + const { disabled, onSelect, eventKey } = this.props; + + if (disabled) { + e.preventDefault(); + return; + } + + if (onSelect) { + onSelect(eventKey, e); + } + } + + render() { + const { disabled, previous, next, onClick, className, style, ...props } = this.props; + + delete props.onSelect; + delete props.eventKey; + + return ( +
    17. + +
    18. + ); + } +} + +PagerItem.propTypes = propTypes; +PagerItem.defaultProps = defaultProps; + +export default PagerItem; diff --git a/fork/react-bootstrap/src/PagerItem.test.js b/fork/react-bootstrap/src/PagerItem.test.js deleted file mode 100644 index d1cd484648f..00000000000 --- a/fork/react-bootstrap/src/PagerItem.test.js +++ /dev/null @@ -1,106 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Pager from '../src/Pager'; - -describe('PagerItem', () => { - xit('Should output a "list item" as root element, and an "anchor" as a child item', () => { - let instance = ReactTestUtils.renderIntoDocument( - Text - ); - - let node = ReactDOM.findDOMNode(instance); - assert.equal(node.nodeName, 'LI'); - assert.equal(node.children.length, 1); - assert.equal(node.children[0].nodeName, 'A'); - }); - - xit('Should output "disabled" attribute as a class', () => { - let instance = ReactTestUtils.renderIntoDocument( - - Text - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'disabled') - ); - }); - - xit('Should output "next" attribute as a class', () => { - let instance = ReactTestUtils.renderIntoDocument( - - Previous - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'previous') - ); - }); - - xit('Should output "previous" attribute as a class', () => { - let instance = ReactTestUtils.renderIntoDocument( - - Next - - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'next') - ); - }); - - xit('Should call "onSelect" when item is clicked', (done) => { - function handleSelect(key) { - assert.equal(key, 1); - done(); - } - let instance = ReactTestUtils.renderIntoDocument( - - Next - - ); - ReactTestUtils.Simulate.click( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a') - ); - }); - - xit('Should not call "onSelect" when item disabled and is clicked', () => { - function handleSelect() { - throw new Error('onSelect should not be called'); - } - let instance = ReactTestUtils.renderIntoDocument( - - Next - - ); - ReactTestUtils.Simulate.click( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a') - ); - }); - - xit('Should set target attribute on anchor', () => { - let instance = ReactTestUtils.renderIntoDocument( - - Next - - ); - - let anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'); - assert.equal(anchor.getAttribute('target'), '_blank'); - }); - - xit('Should call "onSelect" with target attribute', (done) => { - function handleSelect(key, e) { - assert.equal(e.target.target, '_blank'); - done(); - } - let instance = ReactTestUtils.renderIntoDocument( - - Next - - ); - ReactTestUtils.Simulate.click( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a') - ); - }); -}); diff --git a/fork/react-bootstrap/src/PagerItem.test.jsx b/fork/react-bootstrap/src/PagerItem.test.jsx new file mode 100644 index 00000000000..380ce426da0 --- /dev/null +++ b/fork/react-bootstrap/src/PagerItem.test.jsx @@ -0,0 +1,91 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Pager from '../src/Pager'; + +describe('PagerItem', () => { + xit('Should output a "list item" as root element, and an "anchor" as a child item', () => { + let instance = ReactTestUtils.renderIntoDocument(Text); + + let node = ReactDOM.findDOMNode(instance); + assert.equal(node.nodeName, 'LI'); + assert.equal(node.children.length, 1); + assert.equal(node.children[0].nodeName, 'A'); + }); + + xit('Should output "disabled" attribute as a class', () => { + let instance = ReactTestUtils.renderIntoDocument( + + Text + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'disabled')); + }); + + xit('Should output "next" attribute as a class', () => { + let instance = ReactTestUtils.renderIntoDocument( + + Previous + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'previous')); + }); + + xit('Should output "previous" attribute as a class', () => { + let instance = ReactTestUtils.renderIntoDocument( + + Next + , + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'next')); + }); + + xit('Should call "onSelect" when item is clicked', done => { + function handleSelect(key) { + assert.equal(key, 1); + done(); + } + let instance = ReactTestUtils.renderIntoDocument( + + Next + , + ); + ReactTestUtils.Simulate.click(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a')); + }); + + xit('Should not call "onSelect" when item disabled and is clicked', () => { + function handleSelect() { + throw new Error('onSelect should not be called'); + } + let instance = ReactTestUtils.renderIntoDocument( + + Next + , + ); + ReactTestUtils.Simulate.click(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a')); + }); + + xit('Should set target attribute on anchor', () => { + let instance = ReactTestUtils.renderIntoDocument( + + Next + , + ); + + let anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'); + assert.equal(anchor.getAttribute('target'), '_blank'); + }); + + xit('Should call "onSelect" with target attribute', done => { + function handleSelect(key, e) { + assert.equal(e.target.target, '_blank'); + done(); + } + let instance = ReactTestUtils.renderIntoDocument( + + Next + , + ); + ReactTestUtils.Simulate.click(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a')); + }); +}); diff --git a/fork/react-bootstrap/src/Pagination.js b/fork/react-bootstrap/src/Pagination.jsx similarity index 51% rename from fork/react-bootstrap/src/Pagination.js rename to fork/react-bootstrap/src/Pagination.jsx index 6666b4366f3..39d1f829185 100644 --- a/fork/react-bootstrap/src/Pagination.js +++ b/fork/react-bootstrap/src/Pagination.jsx @@ -1,29 +1,23 @@ import classNames from 'classnames'; import React from 'react'; -import PaginationItem, { - First, - Prev, - Ellipsis, - Next, - Last -} from './PaginationItem'; +import PaginationItem, { First, Prev, Ellipsis, Next, Last } from './PaginationItem'; import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; class Pagination extends React.Component { - render() { - const { className, children, ...props } = this.props; + render() { + const { className, children, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); + const [bsProps, elementProps] = splitBsProps(props); - const classes = getClassSet(bsProps); + const classes = getClassSet(bsProps); - return ( -
        - {children} -
      - ); - } + return ( +
        + {children} +
      + ); + } } bsClass('pagination', Pagination); diff --git a/fork/react-bootstrap/src/Pagination.test.js b/fork/react-bootstrap/src/Pagination.test.js deleted file mode 100644 index 431fae84b19..00000000000 --- a/fork/react-bootstrap/src/Pagination.test.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Pagination from '../src/Pagination'; - -describe('', () => { - xit('should have class', () => { - const instance = ReactTestUtils.renderIntoDocument( - Item content - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'pagination') - ); - }); -}); diff --git a/fork/react-bootstrap/src/Pagination.test.jsx b/fork/react-bootstrap/src/Pagination.test.jsx new file mode 100644 index 00000000000..5b71d7a6623 --- /dev/null +++ b/fork/react-bootstrap/src/Pagination.test.jsx @@ -0,0 +1,10 @@ +import ReactTestUtils from 'react-dom/test-utils'; + +import Pagination from '../src/Pagination'; + +describe('', () => { + xit('should have class', () => { + const instance = ReactTestUtils.renderIntoDocument(Item content); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'pagination')); + }); +}); diff --git a/fork/react-bootstrap/src/PaginationItem.js b/fork/react-bootstrap/src/PaginationItem.js deleted file mode 100644 index 2ce066c15ad..00000000000 --- a/fork/react-bootstrap/src/PaginationItem.js +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-disable react/no-multi-comp */ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import SafeAnchor from './SafeAnchor'; - -const propTypes = { - eventKey: PropTypes.any, - className: PropTypes.string, - onSelect: PropTypes.func, - disabled: PropTypes.bool, - active: PropTypes.bool, - activeLabel: PropTypes.string.isRequired -}; - -const defaultProps = { - active: false, - disabled: false, - activeLabel: '(current)' -}; - -export default function PaginationItem({ - active, - disabled, - className, - style, - activeLabel, - children, - ...props -}) { - const Component = active || disabled ? 'span' : SafeAnchor; - return ( -
    19. - - {children} - {active && {activeLabel}} - -
    20. - ); -} - -PaginationItem.propTypes = propTypes; -PaginationItem.defaultProps = defaultProps; - -function createButton(name, defaultValue, label = name) { - return class extends React.Component { - static displayName = name; - - static propTypes = { disabled: PropTypes.bool }; - - render() { - const { disabled, children, className, ...props } = this.props; - const Component = disabled ? 'span' : SafeAnchor; - - return ( -
    21. - {children || defaultValue} -
    22. - ); - } - }; -} - -export const First = createButton('First', '\u00ab'); -export const Prev = createButton('Prev', '\u2039'); -export const Ellipsis = createButton('Ellipsis', '\u2026', 'More'); -export const Next = createButton('Next', '\u203a'); -export const Last = createButton('Last', '\u00bb'); diff --git a/fork/react-bootstrap/src/PaginationItem.jsx b/fork/react-bootstrap/src/PaginationItem.jsx new file mode 100644 index 00000000000..290de71bcbb --- /dev/null +++ b/fork/react-bootstrap/src/PaginationItem.jsx @@ -0,0 +1,69 @@ +/* eslint-disable react/no-multi-comp */ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import SafeAnchor from './SafeAnchor'; + +const propTypes = { + eventKey: PropTypes.any, + className: PropTypes.string, + onSelect: PropTypes.func, + disabled: PropTypes.bool, + active: PropTypes.bool, + activeLabel: PropTypes.string.isRequired, +}; + +const defaultProps = { + active: false, + disabled: false, + activeLabel: '(current)', +}; + +export default function PaginationItem({ + active, + disabled, + className, + style, + activeLabel, + children, + ...props +}) { + const Component = active || disabled ? 'span' : SafeAnchor; + return ( +
    23. + + {children} + {active && {activeLabel}} + +
    24. + ); +} + +PaginationItem.propTypes = propTypes; +PaginationItem.defaultProps = defaultProps; + +function createButton(name, defaultValue, label = name) { + return class extends React.Component { + static displayName = name; + + static propTypes = { disabled: PropTypes.bool }; + + render() { + const { disabled, children, className, ...props } = this.props; + const Component = disabled ? 'span' : SafeAnchor; + + return ( +
    25. + {children || defaultValue} +
    26. + ); + } + }; +} + +export const First = createButton('First', '\u00ab'); +export const Prev = createButton('Prev', '\u2039'); +export const Ellipsis = createButton('Ellipsis', '\u2026', 'More'); +export const Next = createButton('Next', '\u203a'); +export const Last = createButton('Last', '\u00bb'); diff --git a/fork/react-bootstrap/src/Panel.js b/fork/react-bootstrap/src/Panel.js deleted file mode 100644 index 10f42ba0f52..00000000000 --- a/fork/react-bootstrap/src/Panel.js +++ /dev/null @@ -1,163 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { uncontrollable } from 'uncontrollable'; -import warning from 'warning'; - -import { - bsStyles, - bsClass, - getClassSet, - splitBsPropsAndOmit -} from './utils/bootstrapUtils'; -import { State, Style } from './utils/StyleConfig'; -import Body from './PanelBody'; -import Heading from './PanelHeading'; -import Title from './PanelTitle'; -import Footer from './PanelFooter'; -import Toggle from './PanelToggle'; -import Collapse from './PanelCollapse'; - -const has = Object.prototype.hasOwnProperty; - -const defaultGetId = (id, type) => (id ? `${id}--${type}` : null); - -const propTypes = { - /** - * Controls the collapsed/expanded state ofthe Panel. Requires - * a `Panel.Collapse` or `` child component - * in order to actually animate out or in. - * - * @controllable onToggle - */ - expanded: PropTypes.bool, - /** - * A callback fired when the collapse state changes. - * - * @controllable expanded - */ - onToggle: PropTypes.func, - eventKey: PropTypes.any, - - /** - * An HTML `id` attribute uniquely identifying the Panel component. - */ - id: PropTypes.string -}; - -const contextTypes = { - $bs_panelGroup: PropTypes.shape({ - getId: PropTypes.func, - activeKey: PropTypes.any, - onToggle: PropTypes.func - }) -}; - -const childContextTypes = { - $bs_panel: PropTypes.shape({ - headingId: PropTypes.string, - bodyId: PropTypes.string, - bsClass: PropTypes.string, - onToggle: PropTypes.func, - expanded: PropTypes.bool - }) -}; - -class Panel extends React.Component { - getChildContext() { - const { eventKey, id } = this.props; - const idKey = eventKey == null ? id : eventKey; - - let ids; - - if (idKey !== null) { - const panelGroup = this.context.$bs_panelGroup; - const getId = (panelGroup && panelGroup.getId) || defaultGetId; - - ids = { - headingId: getId(idKey, 'heading'), - bodyId: getId(idKey, 'body') - }; - } - - return { - $bs_panel: { - ...ids, - bsClass: this.props.bsClass, - expanded: this.getExpanded(), - onToggle: this.handleToggle - } - }; - } - - getExpanded() { - const panelGroup = this.context.$bs_panelGroup; - - if (panelGroup && has.call(panelGroup, 'activeKey')) { - warning( - this.props.expanded == null, - 'Specifying `` `expanded` in the context of an accordion ' + - '`` is not supported. Set `activeKey` on the ' + - '`` instead.' - ); - - return panelGroup.activeKey === this.props.eventKey; - } - - return !!this.props.expanded; - } - - handleToggle = e => { - const panelGroup = this.context.$bs_panelGroup; - const expanded = !this.getExpanded(); - - if (panelGroup && panelGroup.onToggle) { - panelGroup.onToggle(this.props.eventKey, expanded, e); - } else { - this.props.onToggle(expanded, e); - } - }; - - render() { - let { className, children } = this.props; - const [bsProps, props] = splitBsPropsAndOmit(this.props, [ - 'onToggle', - 'eventKey', - 'expanded' - ]); - - return ( -
      - {children} -
      - ); - } -} - -Panel.propTypes = propTypes; - -Panel.contextTypes = contextTypes; -Panel.childContextTypes = childContextTypes; - -const UncontrolledPanel = uncontrollable( - bsClass( - 'panel', - bsStyles( - [...Object.values(State), Style.DEFAULT, Style.PRIMARY], - Style.DEFAULT, - Panel - ) - ), - { expanded: 'onToggle' } -); - -Object.assign(UncontrolledPanel, { - Heading, - Title, - Body, - Footer, - Toggle, - Collapse -}); - -export default UncontrolledPanel; diff --git a/fork/react-bootstrap/src/Panel.jsx b/fork/react-bootstrap/src/Panel.jsx new file mode 100644 index 00000000000..bb449893012 --- /dev/null +++ b/fork/react-bootstrap/src/Panel.jsx @@ -0,0 +1,150 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { uncontrollable } from 'uncontrollable'; +import warning from 'warning'; + +import { bsStyles, bsClass, getClassSet, splitBsPropsAndOmit } from './utils/bootstrapUtils'; +import { State, Style } from './utils/StyleConfig'; +import Body from './PanelBody'; +import Heading from './PanelHeading'; +import Title from './PanelTitle'; +import Footer from './PanelFooter'; +import Toggle from './PanelToggle'; +import Collapse from './PanelCollapse'; + +const has = Object.prototype.hasOwnProperty; + +const defaultGetId = (id, type) => (id ? `${id}--${type}` : null); + +const propTypes = { + /** + * Controls the collapsed/expanded state ofthe Panel. Requires + * a `Panel.Collapse` or `` child component + * in order to actually animate out or in. + * + * @controllable onToggle + */ + expanded: PropTypes.bool, + /** + * A callback fired when the collapse state changes. + * + * @controllable expanded + */ + onToggle: PropTypes.func, + eventKey: PropTypes.any, + + /** + * An HTML `id` attribute uniquely identifying the Panel component. + */ + id: PropTypes.string, +}; + +const contextTypes = { + $bs_panelGroup: PropTypes.shape({ + getId: PropTypes.func, + activeKey: PropTypes.any, + onToggle: PropTypes.func, + }), +}; + +const childContextTypes = { + $bs_panel: PropTypes.shape({ + headingId: PropTypes.string, + bodyId: PropTypes.string, + bsClass: PropTypes.string, + onToggle: PropTypes.func, + expanded: PropTypes.bool, + }), +}; + +class Panel extends React.Component { + getChildContext() { + const { eventKey, id } = this.props; + const idKey = eventKey == null ? id : eventKey; + + let ids; + + if (idKey !== null) { + const panelGroup = this.context.$bs_panelGroup; + const getId = (panelGroup && panelGroup.getId) || defaultGetId; + + ids = { + headingId: getId(idKey, 'heading'), + bodyId: getId(idKey, 'body'), + }; + } + + return { + $bs_panel: { + ...ids, + bsClass: this.props.bsClass, + expanded: this.getExpanded(), + onToggle: this.handleToggle, + }, + }; + } + + getExpanded() { + const panelGroup = this.context.$bs_panelGroup; + + if (panelGroup && has.call(panelGroup, 'activeKey')) { + warning( + this.props.expanded == null, + 'Specifying `` `expanded` in the context of an accordion ' + + '`` is not supported. Set `activeKey` on the ' + + '`` instead.', + ); + + return panelGroup.activeKey === this.props.eventKey; + } + + return !!this.props.expanded; + } + + handleToggle = e => { + const panelGroup = this.context.$bs_panelGroup; + const expanded = !this.getExpanded(); + + if (panelGroup && panelGroup.onToggle) { + panelGroup.onToggle(this.props.eventKey, expanded, e); + } else { + this.props.onToggle(expanded, e); + } + }; + + render() { + let { className, children } = this.props; + const [bsProps, props] = splitBsPropsAndOmit(this.props, ['onToggle', 'eventKey', 'expanded']); + + return ( +
      + {children} +
      + ); + } +} + +Panel.propTypes = propTypes; + +Panel.contextTypes = contextTypes; +Panel.childContextTypes = childContextTypes; + +const UncontrolledPanel = uncontrollable( + bsClass( + 'panel', + bsStyles([...Object.values(State), Style.DEFAULT, Style.PRIMARY], Style.DEFAULT, Panel), + ), + { expanded: 'onToggle' }, +); + +Object.assign(UncontrolledPanel, { + Heading, + Title, + Body, + Footer, + Toggle, + Collapse, +}); + +export default UncontrolledPanel; diff --git a/fork/react-bootstrap/src/Panel.test.js b/fork/react-bootstrap/src/Panel.test.js deleted file mode 100644 index ca964d389d9..00000000000 --- a/fork/react-bootstrap/src/Panel.test.js +++ /dev/null @@ -1,219 +0,0 @@ -import { render } from '@testing-library/react'; -import Panel from './Panel'; - -describe('', () => { - it('Should have class and body', () => { - render( - - Panel content - - ); - - expect(document.querySelector('div.panel.panel-default')).toBeVisible(); - expect(document.querySelector('div.panel-body')).toBeVisible(); - }); - - // xit('Should have bootstrap style class', () => { - // mount( - // - // Panel content - // - // ).assertSingle('div.panel-primary'); - // }); - - // xit('Should honor additional classes passed in; adding not overriding', () => { - // mount().assertSingle('div.foo'); - // }); - - // xit('Should have unwrapped header', () => { - // mount( - // - // Heading - // - // ) - // .assertSingle('div.panel-heading') - // .text() - // .should.equal('Heading'); - // }); - - // xit('Should have custom component header', () => { - // mount( - // - // Heading - // - // ) - // .assertSingle('h3.panel-heading') - // .text() - // .should.equal('Heading'); - // }); - - // describe('', () => { - // xit('Should render a title', () => { - // mount(foo) - // .assertSingle('div.panel-title') - // .text() - // .should.equal('foo'); - // }); - - // xit('Should render a custom component', () => { - // mount(foo).assertSingle( - // 'h3.panel-title' - // ); - // }); - - // xit('Should render with a toggle', () => { - // mount(foo).assertSingle( - // '.panel-title > PanelToggle' - // ); - // }); - // }); - - // describe('', () => { - // xit('Should render a Toggle a SafeAnchor', () => { - // mount(foo) - // .assertSingle('SafeAnchor') - // .assertSingle('a[role="button"][href="#"]'); - // }); - - // xit('Should render a custom component', () => { - // mount(foo).assertSingle( - // 'h3' - // ); - // }); - - // xit('Should simulate onToggle', (done) => { - // mount( - // done()}> - // foo - // - // ) - // .assertSingle('PanelToggle') - // .simulate('click'); - // }); - // }); - - // xit('Should have a footer', () => { - // mount( - // - // foo - // - // ).assertSingle('div.panel-footer'); - // }); - - // xit('Should have collapse classes', () => { - // mount( - // - // Panel content - // - // ).assertSingle('div.panel-collapse.collapse.in'); - // }); - - // xit('Should pass through dom properties', () => { - // mount(Panel content).assertSingle('div#testid'); - // }); - - // xit('Should set ids on toggle and collapse', () => { - // const inst = mount( - // - // - // foo - // - // Panel content - // - // ); - - // inst.assertSingle('#testid--body.panel-collapse'); - // inst.assertSingle('#testid--heading.panel-heading'); - // }); - - // xit('Should be open', () => { - // const inst = mount( - // - // - // foo - // - - // Panel content - // - // ); - - // inst.assertSingle('.in.panel-collapse'); - // inst.assertNone('a.collapsed'); - // }); - - // xit('Should be closed', () => { - // const inst = mount( - // - // - // foo - // - - // Panel content - // - // ); - - // inst.assertNone('.in.panel-collapse'); - // inst.assertSingle('a.collapsed'); - // }); - - // xit('Should toggle when uncontrolled', () => { - // const wrapper = mount( - // - // - // foo - // - - // Panel content - // - // ); - - // wrapper.find('a').simulate('click'); - - // expect(wrapper.find(Panel.ControlledComponent).props().expanded).to.equal( - // true - // ); - // }); - - // describe('Web Accessibility', () => { - // xit('Should be aria-expanded=true', () => { - // mount( - // - // - // foo - // - - // Panel content - // - // ).assertSingle('.panel-title a[aria-expanded=true]'); - // }); - - // xit('Should be aria-expanded=false', () => { - // mount( - // - // - // foo - // - - // Panel content - // - // ) - // .assertSingle('.panel-title a') - // .assertSingle('[aria-expanded=false]'); - // }); - - // xit('Should add aria-controls with id', () => { - // const inst = mount( - // - // - // foo - // - - // Panel content - // - // ); - - // inst.assertSingle('a[aria-controls="testid--body"]'); - // inst.assertSingle('.panel-collapse[aria-labelledby="testid--heading"]'); - // }); - // }); -}); diff --git a/fork/react-bootstrap/src/Panel.test.jsx b/fork/react-bootstrap/src/Panel.test.jsx new file mode 100644 index 00000000000..a2e9a5178d2 --- /dev/null +++ b/fork/react-bootstrap/src/Panel.test.jsx @@ -0,0 +1,219 @@ +import { render } from '@testing-library/react'; +import Panel from './Panel'; + +describe('', () => { + it('Should have class and body', () => { + render( + + Panel content + , + ); + + expect(document.querySelector('div.panel.panel-default')).toBeVisible(); + expect(document.querySelector('div.panel-body')).toBeVisible(); + }); + + // xit('Should have bootstrap style class', () => { + // mount( + // + // Panel content + // + // ).assertSingle('div.panel-primary'); + // }); + + // xit('Should honor additional classes passed in; adding not overriding', () => { + // mount().assertSingle('div.foo'); + // }); + + // xit('Should have unwrapped header', () => { + // mount( + // + // Heading + // + // ) + // .assertSingle('div.panel-heading') + // .text() + // .should.equal('Heading'); + // }); + + // xit('Should have custom component header', () => { + // mount( + // + // Heading + // + // ) + // .assertSingle('h3.panel-heading') + // .text() + // .should.equal('Heading'); + // }); + + // describe('', () => { + // xit('Should render a title', () => { + // mount(foo) + // .assertSingle('div.panel-title') + // .text() + // .should.equal('foo'); + // }); + + // xit('Should render a custom component', () => { + // mount(foo).assertSingle( + // 'h3.panel-title' + // ); + // }); + + // xit('Should render with a toggle', () => { + // mount(foo).assertSingle( + // '.panel-title > PanelToggle' + // ); + // }); + // }); + + // describe('', () => { + // xit('Should render a Toggle a SafeAnchor', () => { + // mount(foo) + // .assertSingle('SafeAnchor') + // .assertSingle('a[role="button"][href="#"]'); + // }); + + // xit('Should render a custom component', () => { + // mount(foo).assertSingle( + // 'h3' + // ); + // }); + + // xit('Should simulate onToggle', (done) => { + // mount( + // done()}> + // foo + // + // ) + // .assertSingle('PanelToggle') + // .simulate('click'); + // }); + // }); + + // xit('Should have a footer', () => { + // mount( + // + // foo + // + // ).assertSingle('div.panel-footer'); + // }); + + // xit('Should have collapse classes', () => { + // mount( + // + // Panel content + // + // ).assertSingle('div.panel-collapse.collapse.in'); + // }); + + // xit('Should pass through dom properties', () => { + // mount(Panel content).assertSingle('div#testid'); + // }); + + // xit('Should set ids on toggle and collapse', () => { + // const inst = mount( + // + // + // foo + // + // Panel content + // + // ); + + // inst.assertSingle('#testid--body.panel-collapse'); + // inst.assertSingle('#testid--heading.panel-heading'); + // }); + + // xit('Should be open', () => { + // const inst = mount( + // + // + // foo + // + + // Panel content + // + // ); + + // inst.assertSingle('.in.panel-collapse'); + // inst.assertNone('a.collapsed'); + // }); + + // xit('Should be closed', () => { + // const inst = mount( + // + // + // foo + // + + // Panel content + // + // ); + + // inst.assertNone('.in.panel-collapse'); + // inst.assertSingle('a.collapsed'); + // }); + + // xit('Should toggle when uncontrolled', () => { + // const wrapper = mount( + // + // + // foo + // + + // Panel content + // + // ); + + // wrapper.find('a').simulate('click'); + + // expect(wrapper.find(Panel.ControlledComponent).props().expanded).to.equal( + // true + // ); + // }); + + // describe('Web Accessibility', () => { + // xit('Should be aria-expanded=true', () => { + // mount( + // + // + // foo + // + + // Panel content + // + // ).assertSingle('.panel-title a[aria-expanded=true]'); + // }); + + // xit('Should be aria-expanded=false', () => { + // mount( + // + // + // foo + // + + // Panel content + // + // ) + // .assertSingle('.panel-title a') + // .assertSingle('[aria-expanded=false]'); + // }); + + // xit('Should add aria-controls with id', () => { + // const inst = mount( + // + // + // foo + // + + // Panel content + // + // ); + + // inst.assertSingle('a[aria-controls="testid--body"]'); + // inst.assertSingle('.panel-collapse[aria-labelledby="testid--heading"]'); + // }); + // }); +}); diff --git a/fork/react-bootstrap/src/PanelBody.js b/fork/react-bootstrap/src/PanelBody.js deleted file mode 100644 index 9e9d5ac8918..00000000000 --- a/fork/react-bootstrap/src/PanelBody.js +++ /dev/null @@ -1,60 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import cn from 'classnames'; -import { prefix, splitBsPropsAndOmit, bsClass } from './utils/bootstrapUtils'; -import PanelCollapse from './PanelCollapse'; - -const propTypes = { - /** - * A convenience prop that renders a Collapse component around the Body for - * situations when the parent Panel only contains a single Panel.Body child. - * - * renders: - * ```jsx - * - * - * - * ``` - */ - collapsible: PropTypes.bool.isRequired -}; - -const defaultProps = { - collapsible: false -}; - -const contextTypes = { - $bs_panel: PropTypes.shape({ - bsClass: PropTypes.string - }) -}; - -class PanelBody extends React.Component { - render() { - const { children, className, collapsible } = this.props; - const { bsClass: _bsClass } = this.context.$bs_panel || {}; - - const [bsProps, elementProps] = splitBsPropsAndOmit(this.props, [ - 'collapsible' - ]); - bsProps.bsClass = _bsClass || bsProps.bsClass; - - let body = ( -
      - {children} -
      - ); - - if (collapsible) { - body = {body}; - } - - return body; - } -} - -PanelBody.propTypes = propTypes; -PanelBody.defaultProps = defaultProps; -PanelBody.contextTypes = contextTypes; - -export default bsClass('panel', PanelBody); diff --git a/fork/react-bootstrap/src/PanelBody.jsx b/fork/react-bootstrap/src/PanelBody.jsx new file mode 100644 index 00000000000..ee3d20fe391 --- /dev/null +++ b/fork/react-bootstrap/src/PanelBody.jsx @@ -0,0 +1,58 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import cn from 'classnames'; +import { prefix, splitBsPropsAndOmit, bsClass } from './utils/bootstrapUtils'; +import PanelCollapse from './PanelCollapse'; + +const propTypes = { + /** + * A convenience prop that renders a Collapse component around the Body for + * situations when the parent Panel only contains a single Panel.Body child. + * + * renders: + * ```jsx + * + * + * + * ``` + */ + collapsible: PropTypes.bool.isRequired, +}; + +const defaultProps = { + collapsible: false, +}; + +const contextTypes = { + $bs_panel: PropTypes.shape({ + bsClass: PropTypes.string, + }), +}; + +class PanelBody extends React.Component { + render() { + const { children, className, collapsible } = this.props; + const { bsClass: _bsClass } = this.context.$bs_panel || {}; + + const [bsProps, elementProps] = splitBsPropsAndOmit(this.props, ['collapsible']); + bsProps.bsClass = _bsClass || bsProps.bsClass; + + let body = ( +
      + {children} +
      + ); + + if (collapsible) { + body = {body}; + } + + return body; + } +} + +PanelBody.propTypes = propTypes; +PanelBody.defaultProps = defaultProps; +PanelBody.contextTypes = contextTypes; + +export default bsClass('panel', PanelBody); diff --git a/fork/react-bootstrap/src/PanelCollapse.js b/fork/react-bootstrap/src/PanelCollapse.js deleted file mode 100644 index 69d7a4e8cef..00000000000 --- a/fork/react-bootstrap/src/PanelCollapse.js +++ /dev/null @@ -1,70 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import { prefix, splitBsProps, bsClass } from './utils/bootstrapUtils'; -import Collapse from './Collapse'; - -const propTypes = { - /** - * Callback fired before the component expands - */ - onEnter: PropTypes.func, - /** - * Callback fired after the component starts to expand - */ - onEntering: PropTypes.func, - /** - * Callback fired after the component has expanded - */ - onEntered: PropTypes.func, - /** - * Callback fired before the component collapses - */ - onExit: PropTypes.func, - /** - * Callback fired after the component starts to collapse - */ - onExiting: PropTypes.func, - /** - * Callback fired after the component has collapsed - */ - onExited: PropTypes.func -}; - -const contextTypes = { - $bs_panel: PropTypes.shape({ - headingId: PropTypes.string, - bodyId: PropTypes.string, - bsClass: PropTypes.string, - expanded: PropTypes.bool - }) -}; - -class PanelCollapse extends React.Component { - render() { - const { children } = this.props; - const { headingId, bodyId, bsClass: _bsClass, expanded } = - this.context.$bs_panel || {}; - - const [bsProps, props] = splitBsProps(this.props); - - bsProps.bsClass = _bsClass || bsProps.bsClass; - - if (headingId && bodyId) { - props.id = bodyId; - props.role = props.role || 'tabpanel'; - props['aria-labelledby'] = headingId; - } - - return ( - -
      {children}
      -
      - ); - } -} - -PanelCollapse.propTypes = propTypes; -PanelCollapse.contextTypes = contextTypes; - -export default bsClass('panel', PanelCollapse); diff --git a/fork/react-bootstrap/src/PanelCollapse.jsx b/fork/react-bootstrap/src/PanelCollapse.jsx new file mode 100644 index 00000000000..84a20b3c5a7 --- /dev/null +++ b/fork/react-bootstrap/src/PanelCollapse.jsx @@ -0,0 +1,69 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import { prefix, splitBsProps, bsClass } from './utils/bootstrapUtils'; +import Collapse from './Collapse'; + +const propTypes = { + /** + * Callback fired before the component expands + */ + onEnter: PropTypes.func, + /** + * Callback fired after the component starts to expand + */ + onEntering: PropTypes.func, + /** + * Callback fired after the component has expanded + */ + onEntered: PropTypes.func, + /** + * Callback fired before the component collapses + */ + onExit: PropTypes.func, + /** + * Callback fired after the component starts to collapse + */ + onExiting: PropTypes.func, + /** + * Callback fired after the component has collapsed + */ + onExited: PropTypes.func, +}; + +const contextTypes = { + $bs_panel: PropTypes.shape({ + headingId: PropTypes.string, + bodyId: PropTypes.string, + bsClass: PropTypes.string, + expanded: PropTypes.bool, + }), +}; + +class PanelCollapse extends React.Component { + render() { + const { children } = this.props; + const { headingId, bodyId, bsClass: _bsClass, expanded } = this.context.$bs_panel || {}; + + const [bsProps, props] = splitBsProps(this.props); + + bsProps.bsClass = _bsClass || bsProps.bsClass; + + if (headingId && bodyId) { + props.id = bodyId; + props.role = props.role || 'tabpanel'; + props['aria-labelledby'] = headingId; + } + + return ( + +
      {children}
      +
      + ); + } +} + +PanelCollapse.propTypes = propTypes; +PanelCollapse.contextTypes = contextTypes; + +export default bsClass('panel', PanelCollapse); diff --git a/fork/react-bootstrap/src/PanelFooter.js b/fork/react-bootstrap/src/PanelFooter.js deleted file mode 100644 index 5c7aa061a36..00000000000 --- a/fork/react-bootstrap/src/PanelFooter.js +++ /dev/null @@ -1,33 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import cn from 'classnames'; -import { prefix, bsClass, splitBsProps } from './utils/bootstrapUtils'; - -const contextTypes = { - $bs_panel: PropTypes.shape({ - bsClass: PropTypes.string - }) -}; - -class PanelFooter extends React.Component { - render() { - let { children, className } = this.props; - let { bsClass: _bsClass } = this.context.$bs_panel || {}; - - const [bsProps, elementProps] = splitBsProps(this.props); - bsProps.bsClass = _bsClass || bsProps.bsClass; - - return ( -
      - {children} -
      - ); - } -} - -PanelFooter.contextTypes = contextTypes; - -export default bsClass('panel', PanelFooter); diff --git a/fork/react-bootstrap/src/PanelFooter.jsx b/fork/react-bootstrap/src/PanelFooter.jsx new file mode 100644 index 00000000000..92fa8bb1091 --- /dev/null +++ b/fork/react-bootstrap/src/PanelFooter.jsx @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import cn from 'classnames'; +import { prefix, bsClass, splitBsProps } from './utils/bootstrapUtils'; + +const contextTypes = { + $bs_panel: PropTypes.shape({ + bsClass: PropTypes.string, + }), +}; + +class PanelFooter extends React.Component { + render() { + let { children, className } = this.props; + let { bsClass: _bsClass } = this.context.$bs_panel || {}; + + const [bsProps, elementProps] = splitBsProps(this.props); + bsProps.bsClass = _bsClass || bsProps.bsClass; + + return ( +
      + {children} +
      + ); + } +} + +PanelFooter.contextTypes = contextTypes; + +export default bsClass('panel', PanelFooter); diff --git a/fork/react-bootstrap/src/PanelGroup.js b/fork/react-bootstrap/src/PanelGroup.js deleted file mode 100644 index 5cdaf7b7380..00000000000 --- a/fork/react-bootstrap/src/PanelGroup.js +++ /dev/null @@ -1,133 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { cloneElement } from 'react'; -import { uncontrollable } from 'uncontrollable'; - -import { - bsClass, - getClassSet, - splitBsPropsAndOmit -} from './utils/bootstrapUtils'; -import ValidComponentChildren from './utils/ValidComponentChildren'; -import { generatedId } from './utils/PropTypes'; - -const propTypes = { - accordion: PropTypes.bool, - /** - * When `accordion` is enabled, `activeKey` controls the which child `Panel` is expanded. `activeKey` should - * match a child Panel `eventKey` prop exactly. - * - * @controllable onSelect - */ - activeKey: PropTypes.any, - - /** - * A callback fired when a child Panel collapse state changes. It's called with the next expanded `activeKey` - * - * @controllable activeKey - */ - onSelect: PropTypes.func, - - /** - * An HTML role attribute - */ - role: PropTypes.string, - - /** - * A function that takes an eventKey and type and returns a - * unique id for each Panel heading and Panel Collapse. The function _must_ be a pure function, - * meaning it should always return the _same_ id for the same set of inputs. The default - * value requires that an `id` to be set for the PanelGroup. - * - * The `type` argument will either be `"body"` or `"heading"`. - * - * @defaultValue (eventKey, type) => `${this.props.id}-${type}-${key}` - */ - generateChildId: PropTypes.func, - - /** - * HTML id attribute, required if no `generateChildId` prop - * is specified. - */ - id: generatedId('PanelGroup') -}; - -const defaultProps = { - accordion: false -}; - -const childContextTypes = { - $bs_panelGroup: PropTypes.shape({ - getId: PropTypes.func, - headerRole: PropTypes.string, - panelRole: PropTypes.string, - activeKey: PropTypes.any, - onToggle: PropTypes.func - }) -}; - -class PanelGroup extends React.Component { - getChildContext() { - const { activeKey, accordion, generateChildId, id } = this.props; - let getId = null; - - if (accordion) { - getId = - generateChildId || - ((key, type) => (id ? `${id}-${type}-${key}` : null)); - } - - return { - $bs_panelGroup: { - getId, - headerRole: 'tab', - panelRole: 'tabpanel', - ...(accordion && { - activeKey, - onToggle: this.handleSelect - }) - } - }; - } - - handleSelect = (key, expanded, e) => { - if (expanded) { - this.props.onSelect(key, e); - } else if (this.props.activeKey === key) { - this.props.onSelect(null, e); - } - }; - - render() { - const { accordion, className, children, ...props } = this.props; - - const [bsProps, elementProps] = splitBsPropsAndOmit(props, [ - 'onSelect', - 'activeKey' - ]); - - if (accordion) { - elementProps.role = elementProps.role || 'tablist'; - } - - const classes = getClassSet(bsProps); - - return ( -
      - {ValidComponentChildren.map(children, child => - cloneElement(child, { - bsStyle: child.props.bsStyle || bsProps.bsStyle - }) - )} -
      - ); - } -} - -PanelGroup.propTypes = propTypes; -PanelGroup.defaultProps = defaultProps; -PanelGroup.childContextTypes = childContextTypes; - -export default uncontrollable(bsClass('panel-group', PanelGroup), { - activeKey: 'onSelect' -}); diff --git a/fork/react-bootstrap/src/PanelGroup.jsx b/fork/react-bootstrap/src/PanelGroup.jsx new file mode 100644 index 00000000000..1a63c465713 --- /dev/null +++ b/fork/react-bootstrap/src/PanelGroup.jsx @@ -0,0 +1,124 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { cloneElement } from 'react'; +import { uncontrollable } from 'uncontrollable'; + +import { bsClass, getClassSet, splitBsPropsAndOmit } from './utils/bootstrapUtils'; +import ValidComponentChildren from './utils/ValidComponentChildren'; +import { generatedId } from './utils/PropTypes'; + +const propTypes = { + accordion: PropTypes.bool, + /** + * When `accordion` is enabled, `activeKey` controls the which child `Panel` is expanded. `activeKey` should + * match a child Panel `eventKey` prop exactly. + * + * @controllable onSelect + */ + activeKey: PropTypes.any, + + /** + * A callback fired when a child Panel collapse state changes. It's called with the next expanded `activeKey` + * + * @controllable activeKey + */ + onSelect: PropTypes.func, + + /** + * An HTML role attribute + */ + role: PropTypes.string, + + /** + * A function that takes an eventKey and type and returns a + * unique id for each Panel heading and Panel Collapse. The function _must_ be a pure function, + * meaning it should always return the _same_ id for the same set of inputs. The default + * value requires that an `id` to be set for the PanelGroup. + * + * The `type` argument will either be `"body"` or `"heading"`. + * + * @defaultValue (eventKey, type) => `${this.props.id}-${type}-${key}` + */ + generateChildId: PropTypes.func, + + /** + * HTML id attribute, required if no `generateChildId` prop + * is specified. + */ + id: generatedId('PanelGroup'), +}; + +const defaultProps = { + accordion: false, +}; + +const childContextTypes = { + $bs_panelGroup: PropTypes.shape({ + getId: PropTypes.func, + headerRole: PropTypes.string, + panelRole: PropTypes.string, + activeKey: PropTypes.any, + onToggle: PropTypes.func, + }), +}; + +class PanelGroup extends React.Component { + getChildContext() { + const { activeKey, accordion, generateChildId, id } = this.props; + let getId = null; + + if (accordion) { + getId = generateChildId || ((key, type) => (id ? `${id}-${type}-${key}` : null)); + } + + return { + $bs_panelGroup: { + getId, + headerRole: 'tab', + panelRole: 'tabpanel', + ...(accordion && { + activeKey, + onToggle: this.handleSelect, + }), + }, + }; + } + + handleSelect = (key, expanded, e) => { + if (expanded) { + this.props.onSelect(key, e); + } else if (this.props.activeKey === key) { + this.props.onSelect(null, e); + } + }; + + render() { + const { accordion, className, children, ...props } = this.props; + + const [bsProps, elementProps] = splitBsPropsAndOmit(props, ['onSelect', 'activeKey']); + + if (accordion) { + elementProps.role = elementProps.role || 'tablist'; + } + + const classes = getClassSet(bsProps); + + return ( +
      + {ValidComponentChildren.map(children, child => + cloneElement(child, { + bsStyle: child.props.bsStyle || bsProps.bsStyle, + }), + )} +
      + ); + } +} + +PanelGroup.propTypes = propTypes; +PanelGroup.defaultProps = defaultProps; +PanelGroup.childContextTypes = childContextTypes; + +export default uncontrollable(bsClass('panel-group', PanelGroup), { + activeKey: 'onSelect', +}); diff --git a/fork/react-bootstrap/src/PanelGroup.test.js b/fork/react-bootstrap/src/PanelGroup.test.js deleted file mode 100644 index ac5223f703a..00000000000 --- a/fork/react-bootstrap/src/PanelGroup.test.js +++ /dev/null @@ -1,177 +0,0 @@ -import { render } from '@testing-library/react'; -import Panel from './Panel'; -import PanelGroup from './PanelGroup'; - -describe('', () => { - it('Should pass bsStyle to Panels', () => { - render( - - - Panel 1 - - - ); - expect(document.querySelector('div.panel.panel-default')).toBeVisible(); - }); - - // xit('Should not override bsStyle on Panel', () => { - // let wrapper = mount( - // - // - // Panel 1 - // - // - // ); - - // let panel = wrapper.find(Panel); - - // assert.equal(panel.props().bsStyle, 'primary'); - // }); - - // describe('accordion', () => { - // xit('Should not collapse panel by bubbling onSelect callback', () => { - // mount( - // { - // throw new Error(); - // }} - // > - // - // - // - // - // ) - // .assertSingle('input.changeme') - // .simulate('select'); - // }); - - // xit('Should call onSelect handler with eventKey', (done) => { - // function handleSelect(eventKey, e) { - // e.should.exist; - // eventKey.should.equal('1'); - // done(); - // } - - // mount( - // - // - // - // foo - // - - // Panel 1 - // - // - // ) - // .find('a') - // .simulate('click'); - // }); - - // xit('Should manage expanded panels', () => { - // const inst = mount( - // - // - // - // foo - // - - // Panel 1 - // - // - // - // foo - // - - // Panel 2 - // - // - // ); - - // const panel1 = inst.find('#panel1').find('a'); - // const panel2 = inst.find('#panel2').find('a'); - // const panel1Dom = panel1.getDOMNode(); - // const panel2Dom = panel2.getDOMNode(); - - // panel2.simulate('click'); - // assert.equal(panel1Dom.getAttribute('class'), 'collapsed'); - // assert.equal(panel2Dom.getAttribute('class'), ''); - - // panel1.simulate('click'); - // assert.equal(panel1Dom.getAttribute('class'), ''); - // assert.equal(panel2Dom.getAttribute('class'), 'collapsed'); - - // panel1.simulate('click'); - // assert.equal(panel1Dom.getAttribute('class'), 'collapsed'); - // assert.equal(panel2Dom.getAttribute('class'), 'collapsed'); - // }); - - // xit('Should warn if panel has explicit expanded', () => { - // shouldWarn('`` `expanded`'); - - // mount( - // - // - // {}} /> - // - // ); - // }); - // }); - - // describe('Web Accessibility', () => { - // let panelBodies, panelGroup, headers, links; // eslint-disable-line - - // beforeEach(() => { - // const inst = mount( - // - // - // - // foo - // - - // Panel 1 - // - // - // - // foo - // - - // Panel 2 - // - // - // ); - - // panelGroup = inst.getDOMNode(); - // panelBodies = inst.find('.panel-collapse').map((n) => n.getDOMNode()); - // headers = inst.find('.panel-heading').map((n) => n.getDOMNode()); - // links = inst.find('.panel-heading a').map((n) => n.getDOMNode()); - // }); - - // xit('Should have a role of tablist', () => { - // assert.equal(panelGroup.getAttribute('role'), 'tablist'); - // }); - - // xit('Should provide each header tab with role of tab', () => { - // assert.equal(headers[0].getAttribute('role'), 'tab'); - // assert.equal(headers[1].getAttribute('role'), 'tab'); - // }); - - // xit('Should provide the panelBodies with role of tabpanel', () => { - // assert.equal(panelBodies[0].getAttribute('role'), 'tabpanel'); - // }); - - // xit('Should provide each panel with an aria-labelledby referencing the corresponding header', () => { - // assert.equal(panelBodies[0].id, links[0].getAttribute('aria-controls')); - // assert.equal(panelBodies[1].id, links[1].getAttribute('aria-controls')); - // }); - - // xit('Should maintain each tab aria-expanded state', () => { - // assert.equal(links[0].getAttribute('aria-expanded'), 'true'); - // assert.equal(panelBodies[0].getAttribute('aria-expanded'), 'true'); - - // assert.equal(links[1].getAttribute('aria-expanded'), 'false'); - // assert.equal(panelBodies[1].getAttribute('aria-expanded'), 'false'); - // }); - // }); -}); diff --git a/fork/react-bootstrap/src/PanelGroup.test.jsx b/fork/react-bootstrap/src/PanelGroup.test.jsx new file mode 100644 index 00000000000..ff9d5ca6839 --- /dev/null +++ b/fork/react-bootstrap/src/PanelGroup.test.jsx @@ -0,0 +1,177 @@ +import { render } from '@testing-library/react'; +import Panel from './Panel'; +import PanelGroup from './PanelGroup'; + +describe('', () => { + it('Should pass bsStyle to Panels', () => { + render( + + + Panel 1 + + , + ); + expect(document.querySelector('div.panel.panel-default')).toBeVisible(); + }); + + // xit('Should not override bsStyle on Panel', () => { + // let wrapper = mount( + // + // + // Panel 1 + // + // + // ); + + // let panel = wrapper.find(Panel); + + // assert.equal(panel.props().bsStyle, 'primary'); + // }); + + // describe('accordion', () => { + // xit('Should not collapse panel by bubbling onSelect callback', () => { + // mount( + // { + // throw new Error(); + // }} + // > + // + // + // + // + // ) + // .assertSingle('input.changeme') + // .simulate('select'); + // }); + + // xit('Should call onSelect handler with eventKey', (done) => { + // function handleSelect(eventKey, e) { + // e.should.exist; + // eventKey.should.equal('1'); + // done(); + // } + + // mount( + // + // + // + // foo + // + + // Panel 1 + // + // + // ) + // .find('a') + // .simulate('click'); + // }); + + // xit('Should manage expanded panels', () => { + // const inst = mount( + // + // + // + // foo + // + + // Panel 1 + // + // + // + // foo + // + + // Panel 2 + // + // + // ); + + // const panel1 = inst.find('#panel1').find('a'); + // const panel2 = inst.find('#panel2').find('a'); + // const panel1Dom = panel1.getDOMNode(); + // const panel2Dom = panel2.getDOMNode(); + + // panel2.simulate('click'); + // assert.equal(panel1Dom.getAttribute('class'), 'collapsed'); + // assert.equal(panel2Dom.getAttribute('class'), ''); + + // panel1.simulate('click'); + // assert.equal(panel1Dom.getAttribute('class'), ''); + // assert.equal(panel2Dom.getAttribute('class'), 'collapsed'); + + // panel1.simulate('click'); + // assert.equal(panel1Dom.getAttribute('class'), 'collapsed'); + // assert.equal(panel2Dom.getAttribute('class'), 'collapsed'); + // }); + + // xit('Should warn if panel has explicit expanded', () => { + // shouldWarn('`` `expanded`'); + + // mount( + // + // + // {}} /> + // + // ); + // }); + // }); + + // describe('Web Accessibility', () => { + // let panelBodies, panelGroup, headers, links; // eslint-disable-line + + // beforeEach(() => { + // const inst = mount( + // + // + // + // foo + // + + // Panel 1 + // + // + // + // foo + // + + // Panel 2 + // + // + // ); + + // panelGroup = inst.getDOMNode(); + // panelBodies = inst.find('.panel-collapse').map((n) => n.getDOMNode()); + // headers = inst.find('.panel-heading').map((n) => n.getDOMNode()); + // links = inst.find('.panel-heading a').map((n) => n.getDOMNode()); + // }); + + // xit('Should have a role of tablist', () => { + // assert.equal(panelGroup.getAttribute('role'), 'tablist'); + // }); + + // xit('Should provide each header tab with role of tab', () => { + // assert.equal(headers[0].getAttribute('role'), 'tab'); + // assert.equal(headers[1].getAttribute('role'), 'tab'); + // }); + + // xit('Should provide the panelBodies with role of tabpanel', () => { + // assert.equal(panelBodies[0].getAttribute('role'), 'tabpanel'); + // }); + + // xit('Should provide each panel with an aria-labelledby referencing the corresponding header', () => { + // assert.equal(panelBodies[0].id, links[0].getAttribute('aria-controls')); + // assert.equal(panelBodies[1].id, links[1].getAttribute('aria-controls')); + // }); + + // xit('Should maintain each tab aria-expanded state', () => { + // assert.equal(links[0].getAttribute('aria-expanded'), 'true'); + // assert.equal(panelBodies[0].getAttribute('aria-expanded'), 'true'); + + // assert.equal(links[1].getAttribute('aria-expanded'), 'false'); + // assert.equal(panelBodies[1].getAttribute('aria-expanded'), 'false'); + // }); + // }); +}); diff --git a/fork/react-bootstrap/src/PanelHeading.js b/fork/react-bootstrap/src/PanelHeading.js deleted file mode 100644 index ddb61ffeacd..00000000000 --- a/fork/react-bootstrap/src/PanelHeading.js +++ /dev/null @@ -1,56 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import cn from 'classnames'; -import elementType from 'react-prop-types/lib/elementType'; - -import { prefix, bsClass, splitBsProps } from './utils/bootstrapUtils'; - -const propTypes = { - componentClass: elementType -}; - -const defaultProps = { - componentClass: 'div' -}; - -const contextTypes = { - $bs_panel: PropTypes.shape({ - headingId: PropTypes.string, - bsClass: PropTypes.string - }) -}; - -class PanelHeading extends React.Component { - render() { - const { - children, - className, - componentClass: Component, - ...props - } = this.props; - const { headingId, bsClass: _bsClass } = this.context.$bs_panel || {}; - - const [bsProps, elementProps] = splitBsProps(props); - bsProps.bsClass = _bsClass || bsProps.bsClass; - - if (headingId) { - elementProps.role = elementProps.role || 'tab'; - elementProps.id = headingId; - } - - return ( - - {children} - - ); - } -} - -PanelHeading.propTypes = propTypes; -PanelHeading.defaultProps = defaultProps; -PanelHeading.contextTypes = contextTypes; - -export default bsClass('panel', PanelHeading); diff --git a/fork/react-bootstrap/src/PanelHeading.jsx b/fork/react-bootstrap/src/PanelHeading.jsx new file mode 100644 index 00000000000..215e479dbf9 --- /dev/null +++ b/fork/react-bootstrap/src/PanelHeading.jsx @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import cn from 'classnames'; +import elementType from 'react-prop-types/lib/elementType'; + +import { prefix, bsClass, splitBsProps } from './utils/bootstrapUtils'; + +const propTypes = { + componentClass: elementType, +}; + +const defaultProps = { + componentClass: 'div', +}; + +const contextTypes = { + $bs_panel: PropTypes.shape({ + headingId: PropTypes.string, + bsClass: PropTypes.string, + }), +}; + +class PanelHeading extends React.Component { + render() { + const { children, className, componentClass: Component, ...props } = this.props; + const { headingId, bsClass: _bsClass } = this.context.$bs_panel || {}; + + const [bsProps, elementProps] = splitBsProps(props); + bsProps.bsClass = _bsClass || bsProps.bsClass; + + if (headingId) { + elementProps.role = elementProps.role || 'tab'; + elementProps.id = headingId; + } + + return ( + + {children} + + ); + } +} + +PanelHeading.propTypes = propTypes; +PanelHeading.defaultProps = defaultProps; +PanelHeading.contextTypes = contextTypes; + +export default bsClass('panel', PanelHeading); diff --git a/fork/react-bootstrap/src/PanelTitle.js b/fork/react-bootstrap/src/PanelTitle.js deleted file mode 100644 index 2d102324b33..00000000000 --- a/fork/react-bootstrap/src/PanelTitle.js +++ /dev/null @@ -1,62 +0,0 @@ -import cn from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import elementType from 'react-prop-types/lib/elementType'; - -import { prefix, splitBsProps, bsClass } from './utils/bootstrapUtils'; -import PanelToggle from './PanelToggle'; - -const propTypes = { - componentClass: elementType, - /** - * A convenience prop that renders the Panel.Title as a panel collapse toggle component - * for the common use-case. - */ - toggle: PropTypes.bool -}; - -const contextTypes = { - $bs_panel: PropTypes.shape({ - bsClass: PropTypes.string - }) -}; - -const defaultProps = { - componentClass: 'div' -}; - -class PanelTitle extends React.Component { - render() { - let { - children, - className, - toggle, - componentClass: Component, - ...props - } = this.props; - - const { bsClass: _bsClass } = this.context.$bs_panel || {}; - - const [bsProps, elementProps] = splitBsProps(props); - bsProps.bsClass = _bsClass || bsProps.bsClass; - - if (toggle) { - children = {children}; - } - - return ( - - {children} - - ); - } -} - -PanelTitle.propTypes = propTypes; -PanelTitle.defaultProps = defaultProps; -PanelTitle.contextTypes = contextTypes; - -export default bsClass('panel', PanelTitle); diff --git a/fork/react-bootstrap/src/PanelTitle.jsx b/fork/react-bootstrap/src/PanelTitle.jsx new file mode 100644 index 00000000000..ea640ab2cf9 --- /dev/null +++ b/fork/react-bootstrap/src/PanelTitle.jsx @@ -0,0 +1,53 @@ +import cn from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import elementType from 'react-prop-types/lib/elementType'; + +import { prefix, splitBsProps, bsClass } from './utils/bootstrapUtils'; +import PanelToggle from './PanelToggle'; + +const propTypes = { + componentClass: elementType, + /** + * A convenience prop that renders the Panel.Title as a panel collapse toggle component + * for the common use-case. + */ + toggle: PropTypes.bool, +}; + +const contextTypes = { + $bs_panel: PropTypes.shape({ + bsClass: PropTypes.string, + }), +}; + +const defaultProps = { + componentClass: 'div', +}; + +class PanelTitle extends React.Component { + render() { + let { children, className, toggle, componentClass: Component, ...props } = this.props; + + const { bsClass: _bsClass } = this.context.$bs_panel || {}; + + const [bsProps, elementProps] = splitBsProps(props); + bsProps.bsClass = _bsClass || bsProps.bsClass; + + if (toggle) { + children = {children}; + } + + return ( + + {children} + + ); + } +} + +PanelTitle.propTypes = propTypes; +PanelTitle.defaultProps = defaultProps; +PanelTitle.contextTypes = contextTypes; + +export default bsClass('panel', PanelTitle); diff --git a/fork/react-bootstrap/src/PanelToggle.js b/fork/react-bootstrap/src/PanelToggle.js deleted file mode 100644 index cb8605317c6..00000000000 --- a/fork/react-bootstrap/src/PanelToggle.js +++ /dev/null @@ -1,70 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import classNames from 'classnames'; -import elementType from 'react-prop-types/lib/elementType'; -import SafeAnchor from './SafeAnchor'; -import createChainedFunction from './utils/createChainedFunction'; - -const propTypes = { - /** - * only here to satisfy linting, just the html onClick handler. - * - * @private - */ - onClick: PropTypes.func, - /** - * You can use a custom element for this component - */ - componentClass: elementType -}; - -const defaultProps = { - componentClass: SafeAnchor -}; - -const contextTypes = { - $bs_panel: PropTypes.shape({ - bodyId: PropTypes.string, - onToggle: PropTypes.func, - expanded: PropTypes.bool - }) -}; - -class PanelToggle extends React.Component { - constructor(...args) { - super(...args); - - this.handleToggle = this.handleToggle.bind(this); - } - - handleToggle(event) { - const { onToggle } = this.context.$bs_panel || {}; - - if (onToggle) { - onToggle(event); - } - } - - render() { - const { onClick, className, componentClass, ...props } = this.props; - const { expanded, bodyId } = this.context.$bs_panel || {}; - const Component = componentClass; - - props.onClick = createChainedFunction(onClick, this.handleToggle); - - props['aria-expanded'] = expanded; - props.className = classNames(className, !expanded && 'collapsed'); - - if (bodyId) { - props['aria-controls'] = bodyId; - } - - return ; - } -} - -PanelToggle.propTypes = propTypes; -PanelToggle.defaultProps = defaultProps; -PanelToggle.contextTypes = contextTypes; - -export default PanelToggle; diff --git a/fork/react-bootstrap/src/PanelToggle.jsx b/fork/react-bootstrap/src/PanelToggle.jsx new file mode 100644 index 00000000000..9c1a3c3f416 --- /dev/null +++ b/fork/react-bootstrap/src/PanelToggle.jsx @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import elementType from 'react-prop-types/lib/elementType'; +import SafeAnchor from './SafeAnchor'; +import createChainedFunction from './utils/createChainedFunction'; + +const propTypes = { + /** + * only here to satisfy linting, just the html onClick handler. + * + * @private + */ + onClick: PropTypes.func, + /** + * You can use a custom element for this component + */ + componentClass: elementType, +}; + +const defaultProps = { + componentClass: SafeAnchor, +}; + +const contextTypes = { + $bs_panel: PropTypes.shape({ + bodyId: PropTypes.string, + onToggle: PropTypes.func, + expanded: PropTypes.bool, + }), +}; + +class PanelToggle extends React.Component { + constructor(...args) { + super(...args); + + this.handleToggle = this.handleToggle.bind(this); + } + + handleToggle(event) { + const { onToggle } = this.context.$bs_panel || {}; + + if (onToggle) { + onToggle(event); + } + } + + render() { + const { onClick, className, componentClass, ...props } = this.props; + const { expanded, bodyId } = this.context.$bs_panel || {}; + const Component = componentClass; + + props.onClick = createChainedFunction(onClick, this.handleToggle); + + props['aria-expanded'] = expanded; + props.className = classNames(className, !expanded && 'collapsed'); + + if (bodyId) { + props['aria-controls'] = bodyId; + } + + return ; + } +} + +PanelToggle.propTypes = propTypes; +PanelToggle.defaultProps = defaultProps; +PanelToggle.contextTypes = contextTypes; + +export default PanelToggle; diff --git a/fork/react-bootstrap/src/Popover.js b/fork/react-bootstrap/src/Popover.js deleted file mode 100644 index 8b78ceef3cc..00000000000 --- a/fork/react-bootstrap/src/Popover.js +++ /dev/null @@ -1,110 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import isRequiredForA11y from 'prop-types-extra/lib/isRequiredForA11y'; - -import { - bsClass, - getClassSet, - prefix, - splitBsProps -} from './utils/bootstrapUtils'; - -const propTypes = { - /** - * An html id attribute, necessary for accessibility - * @type {string} - * @required - */ - id: isRequiredForA11y( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]) - ), - - /** - * Sets the direction the Popover is positioned towards. - */ - placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), - - /** - * The "top" position value for the Popover. - */ - positionTop: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * The "left" position value for the Popover. - */ - positionLeft: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** - * The "top" position value for the Popover arrow. - */ - arrowOffsetTop: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * The "left" position value for the Popover arrow. - */ - arrowOffsetLeft: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** - * Title content - */ - title: PropTypes.node -}; - -const defaultProps = { - placement: 'right' -}; - -class Popover extends React.Component { - render() { - const { - placement, - positionTop, - positionLeft, - arrowOffsetTop, - arrowOffsetLeft, - title, - className, - style, - children, - ...props - } = this.props; - - const [bsProps, elementProps] = splitBsProps(props); - - const classes = { - ...getClassSet(bsProps), - [placement]: true - }; - - const outerStyle = { - display: 'block', - top: positionTop, - left: positionLeft, - ...style - }; - - const arrowStyle = { - top: arrowOffsetTop, - left: arrowOffsetLeft - }; - - return ( -
      -
      - - {title &&

      {title}

      } - -
      {children}
      -
      - ); - } -} - -Popover.propTypes = propTypes; -Popover.defaultProps = defaultProps; - -export default bsClass('popover', Popover); diff --git a/fork/react-bootstrap/src/Popover.jsx b/fork/react-bootstrap/src/Popover.jsx new file mode 100644 index 00000000000..492b16fa898 --- /dev/null +++ b/fork/react-bootstrap/src/Popover.jsx @@ -0,0 +1,103 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import isRequiredForA11y from 'prop-types-extra/lib/isRequiredForA11y'; + +import { bsClass, getClassSet, prefix, splitBsProps } from './utils/bootstrapUtils'; + +const propTypes = { + /** + * An html id attribute, necessary for accessibility + * @type {string} + * @required + */ + id: isRequiredForA11y(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + + /** + * Sets the direction the Popover is positioned towards. + */ + placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + + /** + * The "top" position value for the Popover. + */ + positionTop: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * The "left" position value for the Popover. + */ + positionLeft: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + + /** + * The "top" position value for the Popover arrow. + */ + arrowOffsetTop: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * The "left" position value for the Popover arrow. + */ + arrowOffsetLeft: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + + /** + * Title content + */ + title: PropTypes.node, +}; + +const defaultProps = { + placement: 'right', +}; + +class Popover extends React.Component { + render() { + const { + placement, + positionTop, + positionLeft, + arrowOffsetTop, + arrowOffsetLeft, + title, + className, + style, + children, + ...props + } = this.props; + + const [bsProps, elementProps] = splitBsProps(props); + + const classes = { + ...getClassSet(bsProps), + [placement]: true, + }; + + const outerStyle = { + display: 'block', + top: positionTop, + left: positionLeft, + ...style, + }; + + const arrowStyle = { + top: arrowOffsetTop, + left: arrowOffsetLeft, + }; + + return ( +
      +
      + + {title &&

      {title}

      } + +
      {children}
      +
      + ); + } +} + +Popover.propTypes = propTypes; +Popover.defaultProps = defaultProps; + +export default bsClass('popover', Popover); diff --git a/fork/react-bootstrap/src/Popover.test.js b/fork/react-bootstrap/src/Popover.test.js deleted file mode 100644 index 93393391174..00000000000 --- a/fork/react-bootstrap/src/Popover.test.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Popover from '../src/Popover'; - -describe('Popover', () => { - xit('Should output a popover title and content', () => { - let instance = ReactTestUtils.renderIntoDocument( - - Popover Content - - ); - - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'popover-title' - ) - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'popover-content' - ) - ); - - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong') - ); - - assert.equal(ReactDOM.findDOMNode(instance).style.display, 'block'); - }); -}); diff --git a/fork/react-bootstrap/src/Popover.test.jsx b/fork/react-bootstrap/src/Popover.test.jsx new file mode 100644 index 00000000000..2330c9a7929 --- /dev/null +++ b/fork/react-bootstrap/src/Popover.test.jsx @@ -0,0 +1,21 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Popover from '../src/Popover'; + +describe('Popover', () => { + xit('Should output a popover title and content', () => { + let instance = ReactTestUtils.renderIntoDocument( + + Popover Content + , + ); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'popover-title')); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'popover-content')); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong')); + + assert.equal(ReactDOM.findDOMNode(instance).style.display, 'block'); + }); +}); diff --git a/fork/react-bootstrap/src/ProgressBar.js b/fork/react-bootstrap/src/ProgressBar.js deleted file mode 100644 index 63aa3068736..00000000000 --- a/fork/react-bootstrap/src/ProgressBar.js +++ /dev/null @@ -1,170 +0,0 @@ -import classNames from 'classnames'; -import React, { cloneElement } from 'react'; -import PropTypes from 'prop-types'; - -import { - bsClass as setBsClass, - bsStyles, - getClassSet, - prefix, - splitBsProps -} from './utils/bootstrapUtils'; -import { State } from './utils/StyleConfig'; -import ValidComponentChildren from './utils/ValidComponentChildren'; - -const ROUND_PRECISION = 1000; - -/** - * Validate that children, if any, are instances of ``. - */ -function onlyProgressBar(props, propName, componentName) { - const children = props[propName]; - if (!children) { - return null; - } - - let error = null; - - React.Children.forEach(children, child => { - if (error) { - return; - } - - /** - * Compare types in a way that works with libraries that patch and proxy - * components like react-hot-loader. - * - * see https://github.com/gaearon/react-hot-loader#checking-element-types - */ - const element = ; - if (child.type === element.type) return; - - const childIdentifier = React.isValidElement(child) - ? child.type.displayName || child.type.name || child.type - : child; - error = new Error( - `Children of ${componentName} can contain only ProgressBar ` + - `components. Found ${childIdentifier}.` - ); - }); - - return error; -} - -const propTypes = { - min: PropTypes.number, - now: PropTypes.number, - max: PropTypes.number, - label: PropTypes.node, - srOnly: PropTypes.bool, - striped: PropTypes.bool, - active: PropTypes.bool, - children: onlyProgressBar, - - /** - * @private - */ - isChild: PropTypes.bool -}; - -const defaultProps = { - min: 0, - max: 100, - active: false, - isChild: false, - srOnly: false, - striped: false -}; - -function getPercentage(now, min, max) { - const percentage = ((now - min) / (max - min)) * 100; - return Math.round(percentage * ROUND_PRECISION) / ROUND_PRECISION; -} - -class ProgressBar extends React.Component { - renderProgressBar({ - min, - now, - max, - label, - srOnly, - striped, - active, - className, - style, - ...props - }) { - const [bsProps, elementProps] = splitBsProps(props); - - const classes = { - ...getClassSet(bsProps), - active, - [prefix(bsProps, 'striped')]: active || striped - }; - - return ( -
      - {srOnly ? {label} : label} -
      - ); - } - - render() { - const { isChild, ...props } = this.props; - - if (isChild) { - return this.renderProgressBar(props); - } - - const { - min, - now, - max, - label, - srOnly, - striped, - active, - bsClass, - bsStyle, - className, - children, - ...wrapperProps - } = props; - - return ( -
      - {children - ? ValidComponentChildren.map(children, child => - cloneElement(child, { isChild: true }) - ) - : this.renderProgressBar({ - min, - now, - max, - label, - srOnly, - striped, - active, - bsClass, - bsStyle - })} -
      - ); - } -} - -ProgressBar.propTypes = propTypes; -ProgressBar.defaultProps = defaultProps; - -export default setBsClass( - 'progress-bar', - bsStyles(Object.values(State), ProgressBar) -); diff --git a/fork/react-bootstrap/src/ProgressBar.jsx b/fork/react-bootstrap/src/ProgressBar.jsx new file mode 100644 index 00000000000..f8b4037b4bf --- /dev/null +++ b/fork/react-bootstrap/src/ProgressBar.jsx @@ -0,0 +1,154 @@ +import classNames from 'classnames'; +import React, { cloneElement } from 'react'; +import PropTypes from 'prop-types'; + +import { + bsClass as setBsClass, + bsStyles, + getClassSet, + prefix, + splitBsProps, +} from './utils/bootstrapUtils'; +import { State } from './utils/StyleConfig'; +import ValidComponentChildren from './utils/ValidComponentChildren'; + +const ROUND_PRECISION = 1000; + +/** + * Validate that children, if any, are instances of ``. + */ +function onlyProgressBar(props, propName, componentName) { + const children = props[propName]; + if (!children) { + return null; + } + + let error = null; + + React.Children.forEach(children, child => { + if (error) { + return; + } + + /** + * Compare types in a way that works with libraries that patch and proxy + * components like react-hot-loader. + * + * see https://github.com/gaearon/react-hot-loader#checking-element-types + */ + const element = ; + if (child.type === element.type) return; + + const childIdentifier = React.isValidElement(child) + ? child.type.displayName || child.type.name || child.type + : child; + error = new Error( + `Children of ${componentName} can contain only ProgressBar ` + + `components. Found ${childIdentifier}.`, + ); + }); + + return error; +} + +const propTypes = { + min: PropTypes.number, + now: PropTypes.number, + max: PropTypes.number, + label: PropTypes.node, + srOnly: PropTypes.bool, + striped: PropTypes.bool, + active: PropTypes.bool, + children: onlyProgressBar, + + /** + * @private + */ + isChild: PropTypes.bool, +}; + +const defaultProps = { + min: 0, + max: 100, + active: false, + isChild: false, + srOnly: false, + striped: false, +}; + +function getPercentage(now, min, max) { + const percentage = ((now - min) / (max - min)) * 100; + return Math.round(percentage * ROUND_PRECISION) / ROUND_PRECISION; +} + +class ProgressBar extends React.Component { + renderProgressBar({ min, now, max, label, srOnly, striped, active, className, style, ...props }) { + const [bsProps, elementProps] = splitBsProps(props); + + const classes = { + ...getClassSet(bsProps), + active, + [prefix(bsProps, 'striped')]: active || striped, + }; + + return ( +
      + {srOnly ? {label} : label} +
      + ); + } + + render() { + const { isChild, ...props } = this.props; + + if (isChild) { + return this.renderProgressBar(props); + } + + const { + min, + now, + max, + label, + srOnly, + striped, + active, + bsClass, + bsStyle, + className, + children, + ...wrapperProps + } = props; + + return ( +
      + {children + ? ValidComponentChildren.map(children, child => cloneElement(child, { isChild: true })) + : this.renderProgressBar({ + min, + now, + max, + label, + srOnly, + striped, + active, + bsClass, + bsStyle, + })} +
      + ); + } +} + +ProgressBar.propTypes = propTypes; +ProgressBar.defaultProps = defaultProps; + +export default setBsClass('progress-bar', bsStyles(Object.values(State), ProgressBar)); diff --git a/fork/react-bootstrap/src/ProgressBar.test.js b/fork/react-bootstrap/src/ProgressBar.test.js deleted file mode 100644 index 2e09c85747b..00000000000 --- a/fork/react-bootstrap/src/ProgressBar.test.js +++ /dev/null @@ -1,268 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import ProgressBar from '../src/ProgressBar'; - -import { getOne, shouldWarn } from './helpers'; - -function getProgressBarNode(wrapper) { - return ReactTestUtils.findRenderedDOMComponentWithClass( - wrapper, - 'progress-bar' - ); -} - -describe('', () => { - xit('Should output a progress bar with wrapper', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); - assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bprogress\b/)); - assert.ok(getProgressBarNode(instance).className.match(/\bprogress-bar\b/)); - assert.equal( - getProgressBarNode(instance).getAttribute('role'), - 'progressbar' - ); - }); - - xit('Should have the default class', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.ok(getProgressBarNode(instance).className.match(/\bprogress-bar\b/)); - }); - - xit('Should have the success class', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.ok( - getProgressBarNode(instance).className.match(/\bprogress-bar-success\b/) - ); - }); - - xit('Should have the warning class', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.ok( - getProgressBarNode(instance).className.match(/\bprogress-bar-warning\b/) - ); - }); - - xit('Should default to min:0, max:100', () => { - const instance = ReactTestUtils.renderIntoDocument(); - const bar = getProgressBarNode(instance); - - assert.equal(bar.getAttribute('aria-valuemin'), '0'); - assert.equal(bar.getAttribute('aria-valuemax'), '100'); - }); - - xit('Should have 0% computed width', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(getProgressBarNode(instance).style.width, '0%'); - }); - - xit('Should have 10% computed width', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(getProgressBarNode(instance).style.width, '10%'); - }); - - xit('Should have 100% computed width', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(getProgressBarNode(instance).style.width, '100%'); - }); - - xit('Should have 50% computed width with non-zero min', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(getProgressBarNode(instance).style.width, '50%'); - }); - - xit('Should not have label', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(ReactDOM.findDOMNode(instance).textContent, ''); - }); - - xit('Should have label', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal( - ReactDOM.findDOMNode(instance).textContent, - 'progress bar label' - ); - }); - - xit('Should have screen reader only label', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - const srLabel = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'sr-only' - ); - - assert.equal(srLabel.textContent, 'progress bar label'); - }); - - xit('Should have a label that is a React component', () => { - const customLabel = My label; - - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'special-label' - ) - ); - }); - - xit('Should have screen reader only label that wraps a React component', () => { - const customLabel = My label; - - const instance = ReactTestUtils.renderIntoDocument( - - ); - - const srLabel = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'sr-only' - ); - const component = getOne(srLabel.getElementsByClassName('special-label')); - - assert.ok(component); - }); - - xit('Should show striped bar', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.ok( - ReactDOM.findDOMNode(instance).firstChild.className.match( - /\bprogress-bar-striped\b/ - ) - ); - }); - - xit('Should show animated striped bar', () => { - const instance = ReactTestUtils.renderIntoDocument( - - ); - - const barClassName = ReactDOM.findDOMNode(instance).firstChild.className; - - assert.ok(barClassName.match(/\bprogress-bar-striped\b/)); - assert.ok(barClassName.match(/\bactive\b/)); - }); - - xit('Should show stacked bars', () => { - const instance = ReactTestUtils.renderIntoDocument( - - - - - ); - const wrapper = ReactDOM.findDOMNode(instance); - const bar1 = wrapper.firstChild; - const bar2 = wrapper.lastChild; - - assert.ok(wrapper.className.match(/\bprogress\b/)); - assert.ok(bar1.className.match(/\bprogress-bar\b/)); - assert.equal(bar1.style.width, '50%'); - assert.ok(bar2.className.match(/\bprogress-bar\b/)); - assert.equal(bar2.style.width, '30%'); - }); - - xit('Should render active and striped children in stacked bar too', () => { - const instance = ReactTestUtils.renderIntoDocument( - - - - - ); - const wrapper = ReactDOM.findDOMNode(instance); - const bar1 = wrapper.firstChild; - const bar2 = wrapper.lastChild; - - assert.ok(wrapper.className.match(/\bprogress\b/)); - - assert.ok(bar1.className.match(/\bprogress-bar\b/)); - assert.ok(bar1.className.match(/\bactive\b/)); - assert.ok(bar1.className.match(/\bprogress-bar-striped\b/)); - - assert.ok(bar2.className.match(/\bprogress-bar\b/)); - assert.ok(bar2.className.match(/\bprogress-bar-striped\b/)); - assert.notOk(bar2.className.match(/\bactive\b/)); - }); - - xit('Should forward className and style to nested bars', () => { - const instance = ReactTestUtils.renderIntoDocument( - - - - - ); - const wrapper = ReactDOM.findDOMNode(instance); - const bar1 = wrapper.firstChild; - const bar2 = wrapper.lastChild; - - assert.ok(bar1.className.match(/\bbar1\b/)); - assert.equal(bar2.style.minWidth, '10px'); - }); - - xit('allows only ProgressBar in children', () => { - shouldWarn('Failed prop'); - - function NotProgressBar() { - return null; - } - - ReactTestUtils.renderIntoDocument( - - - - foo - - - ); - }); -}); diff --git a/fork/react-bootstrap/src/ProgressBar.test.jsx b/fork/react-bootstrap/src/ProgressBar.test.jsx new file mode 100644 index 00000000000..b6bd010bcf8 --- /dev/null +++ b/fork/react-bootstrap/src/ProgressBar.test.jsx @@ -0,0 +1,214 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import ProgressBar from '../src/ProgressBar'; + +import { getOne, shouldWarn } from './helpers'; + +function getProgressBarNode(wrapper) { + return ReactTestUtils.findRenderedDOMComponentWithClass(wrapper, 'progress-bar'); +} + +describe('', () => { + xit('Should output a progress bar with wrapper', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bprogress\b/)); + assert.ok(getProgressBarNode(instance).className.match(/\bprogress-bar\b/)); + assert.equal(getProgressBarNode(instance).getAttribute('role'), 'progressbar'); + }); + + xit('Should have the default class', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.ok(getProgressBarNode(instance).className.match(/\bprogress-bar\b/)); + }); + + xit('Should have the success class', () => { + const instance = ReactTestUtils.renderIntoDocument( + , + ); + + assert.ok(getProgressBarNode(instance).className.match(/\bprogress-bar-success\b/)); + }); + + xit('Should have the warning class', () => { + const instance = ReactTestUtils.renderIntoDocument( + , + ); + + assert.ok(getProgressBarNode(instance).className.match(/\bprogress-bar-warning\b/)); + }); + + xit('Should default to min:0, max:100', () => { + const instance = ReactTestUtils.renderIntoDocument(); + const bar = getProgressBarNode(instance); + + assert.equal(bar.getAttribute('aria-valuemin'), '0'); + assert.equal(bar.getAttribute('aria-valuemax'), '100'); + }); + + xit('Should have 0% computed width', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(getProgressBarNode(instance).style.width, '0%'); + }); + + xit('Should have 10% computed width', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(getProgressBarNode(instance).style.width, '10%'); + }); + + xit('Should have 100% computed width', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(getProgressBarNode(instance).style.width, '100%'); + }); + + xit('Should have 50% computed width with non-zero min', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(getProgressBarNode(instance).style.width, '50%'); + }); + + xit('Should not have label', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).textContent, ''); + }); + + xit('Should have label', () => { + const instance = ReactTestUtils.renderIntoDocument( + , + ); + + assert.equal(ReactDOM.findDOMNode(instance).textContent, 'progress bar label'); + }); + + xit('Should have screen reader only label', () => { + const instance = ReactTestUtils.renderIntoDocument( + , + ); + const srLabel = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'sr-only'); + + assert.equal(srLabel.textContent, 'progress bar label'); + }); + + xit('Should have a label that is a React component', () => { + const customLabel = My label; + + const instance = ReactTestUtils.renderIntoDocument( + , + ); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'special-label')); + }); + + xit('Should have screen reader only label that wraps a React component', () => { + const customLabel = My label; + + const instance = ReactTestUtils.renderIntoDocument( + , + ); + + const srLabel = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'sr-only'); + const component = getOne(srLabel.getElementsByClassName('special-label')); + + assert.ok(component); + }); + + xit('Should show striped bar', () => { + const instance = ReactTestUtils.renderIntoDocument( + , + ); + + assert.ok( + ReactDOM.findDOMNode(instance).firstChild.className.match(/\bprogress-bar-striped\b/), + ); + }); + + xit('Should show animated striped bar', () => { + const instance = ReactTestUtils.renderIntoDocument( + , + ); + + const barClassName = ReactDOM.findDOMNode(instance).firstChild.className; + + assert.ok(barClassName.match(/\bprogress-bar-striped\b/)); + assert.ok(barClassName.match(/\bactive\b/)); + }); + + xit('Should show stacked bars', () => { + const instance = ReactTestUtils.renderIntoDocument( + + + + , + ); + const wrapper = ReactDOM.findDOMNode(instance); + const bar1 = wrapper.firstChild; + const bar2 = wrapper.lastChild; + + assert.ok(wrapper.className.match(/\bprogress\b/)); + assert.ok(bar1.className.match(/\bprogress-bar\b/)); + assert.equal(bar1.style.width, '50%'); + assert.ok(bar2.className.match(/\bprogress-bar\b/)); + assert.equal(bar2.style.width, '30%'); + }); + + xit('Should render active and striped children in stacked bar too', () => { + const instance = ReactTestUtils.renderIntoDocument( + + + + , + ); + const wrapper = ReactDOM.findDOMNode(instance); + const bar1 = wrapper.firstChild; + const bar2 = wrapper.lastChild; + + assert.ok(wrapper.className.match(/\bprogress\b/)); + + assert.ok(bar1.className.match(/\bprogress-bar\b/)); + assert.ok(bar1.className.match(/\bactive\b/)); + assert.ok(bar1.className.match(/\bprogress-bar-striped\b/)); + + assert.ok(bar2.className.match(/\bprogress-bar\b/)); + assert.ok(bar2.className.match(/\bprogress-bar-striped\b/)); + assert.notOk(bar2.className.match(/\bactive\b/)); + }); + + xit('Should forward className and style to nested bars', () => { + const instance = ReactTestUtils.renderIntoDocument( + + + + , + ); + const wrapper = ReactDOM.findDOMNode(instance); + const bar1 = wrapper.firstChild; + const bar2 = wrapper.lastChild; + + assert.ok(bar1.className.match(/\bbar1\b/)); + assert.equal(bar2.style.minWidth, '10px'); + }); + + xit('allows only ProgressBar in children', () => { + shouldWarn('Failed prop'); + + function NotProgressBar() { + return null; + } + + ReactTestUtils.renderIntoDocument( + + + + foo + + , + ); + }); +}); diff --git a/fork/react-bootstrap/src/Radio.js b/fork/react-bootstrap/src/Radio.js deleted file mode 100644 index 6543cfa1da2..00000000000 --- a/fork/react-bootstrap/src/Radio.js +++ /dev/null @@ -1,113 +0,0 @@ -/* eslint-disable jsx-a11y/label-has-for */ - -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import warning from 'warning'; - -import { - bsClass, - getClassSet, - prefix, - splitBsProps -} from './utils/bootstrapUtils'; - -const propTypes = { - inline: PropTypes.bool, - disabled: PropTypes.bool, - title: PropTypes.string, - /** - * Only valid if `inline` is not set. - */ - validationState: PropTypes.oneOf(['success', 'warning', 'error', null]), - /** - * Attaches a ref to the `` element. Only functions can be used here. - * - * ```js - * { this.input = ref; }} /> - * ``` - */ - inputRef: PropTypes.func -}; - -const defaultProps = { - inline: false, - disabled: false, - title: '' -}; - -class Radio extends React.Component { - render() { - const { - inline, - disabled, - validationState, - inputRef, - className, - style, - title, - children, - ...props - } = this.props; - - const [bsProps, elementProps] = splitBsProps(props); - - const input = ( - - ); - - if (inline) { - const classes = { - [prefix(bsProps, 'inline')]: true, - disabled - }; - - // Use a warning here instead of in propTypes to get better-looking - // generated documentation. - warning( - !validationState, - '`validationState` is ignored on ``. To display ' + - 'validation state on an inline radio, set `validationState` on a ' + - 'parent `` or other element instead.' - ); - - return ( - - ); - } - - const classes = { - ...getClassSet(bsProps), - disabled - }; - if (validationState) { - classes[`has-${validationState}`] = true; - } - - return ( -
      - -
      - ); - } -} - -Radio.propTypes = propTypes; -Radio.defaultProps = defaultProps; - -export default bsClass('radio', Radio); diff --git a/fork/react-bootstrap/src/Radio.jsx b/fork/react-bootstrap/src/Radio.jsx new file mode 100644 index 00000000000..0a48c00627d --- /dev/null +++ b/fork/react-bootstrap/src/Radio.jsx @@ -0,0 +1,97 @@ +/* eslint-disable jsx-a11y/label-has-for */ + +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import warning from 'warning'; + +import { bsClass, getClassSet, prefix, splitBsProps } from './utils/bootstrapUtils'; + +const propTypes = { + inline: PropTypes.bool, + disabled: PropTypes.bool, + title: PropTypes.string, + /** + * Only valid if `inline` is not set. + */ + validationState: PropTypes.oneOf(['success', 'warning', 'error', null]), + /** + * Attaches a ref to the `` element. Only functions can be used here. + * + * ```js + * { this.input = ref; }} /> + * ``` + */ + inputRef: PropTypes.func, +}; + +const defaultProps = { + inline: false, + disabled: false, + title: '', +}; + +class Radio extends React.Component { + render() { + const { + inline, + disabled, + validationState, + inputRef, + className, + style, + title, + children, + ...props + } = this.props; + + const [bsProps, elementProps] = splitBsProps(props); + + const input = ; + + if (inline) { + const classes = { + [prefix(bsProps, 'inline')]: true, + disabled, + }; + + // Use a warning here instead of in propTypes to get better-looking + // generated documentation. + warning( + !validationState, + '`validationState` is ignored on ``. To display ' + + 'validation state on an inline radio, set `validationState` on a ' + + 'parent `` or other element instead.', + ); + + return ( + + ); + } + + const classes = { + ...getClassSet(bsProps), + disabled, + }; + if (validationState) { + classes[`has-${validationState}`] = true; + } + + return ( +
      + +
      + ); + } +} + +Radio.propTypes = propTypes; +Radio.defaultProps = defaultProps; + +export default bsClass('radio', Radio); diff --git a/fork/react-bootstrap/src/Radio.test.js b/fork/react-bootstrap/src/Radio.test.js deleted file mode 100644 index 11a910b58bb..00000000000 --- a/fork/react-bootstrap/src/Radio.test.js +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { screen, render } from '@testing-library/react'; - -import Radio from './Radio'; - -import { shouldWarn } from './helpers'; - -describe('', () => { - it('should render correctly', () => { - render( - - My label - - ); - - expect(document.querySelector('div.radio.my-radio')).toBeVisible(); - expect( - document.querySelector('input[type="radio"][name="foo"][checked]') - ).toBeVisible(); - - expect(screen.getByText('My label')).toBeInTheDocument(); - }); - - // xit('should support inline', () => { - // const wrapper = shallow( - // - // My label - // - // ); - - // wrapper - // .assertSingle('label.radio-inline.my-radio') - // .assertSingle('input[type="radio"][name="foo"]'); - - // wrapper.assertSingle('label').text().should.equal('My label'); - // }); - - // xit('should support validation state', () => { - // shallow().assertSingle('.has-success'); - // }); - - // xit('should not support validation state when inline', () => { - // shouldWarn('ignored'); - - // shallow() - // .find('.has-success') - // .should.have.length(0); - // }); - - // xit('should support inputRef', () => { - // class Container extends React.Component { - // render() { - // return ( - // { - // this.input = ref; - // }} - // /> - // ); - // } - // } - - // const instance = mount().instance(); - - // expect(instance.input.tagName).to.equal('INPUT'); - // }); -}); diff --git a/fork/react-bootstrap/src/Radio.test.jsx b/fork/react-bootstrap/src/Radio.test.jsx new file mode 100644 index 00000000000..a062d6d54ab --- /dev/null +++ b/fork/react-bootstrap/src/Radio.test.jsx @@ -0,0 +1,64 @@ +import { screen, render } from '@testing-library/react'; + +import Radio from './Radio'; + +import { shouldWarn } from './helpers'; + +describe('', () => { + it('should render correctly', () => { + render( + + My label + , + ); + + expect(document.querySelector('div.radio.my-radio')).toBeVisible(); + expect(document.querySelector('input[type="radio"][name="foo"][checked]')).toBeVisible(); + + expect(screen.getByText('My label')).toBeInTheDocument(); + }); + + // xit('should support inline', () => { + // const wrapper = shallow( + // + // My label + // + // ); + + // wrapper + // .assertSingle('label.radio-inline.my-radio') + // .assertSingle('input[type="radio"][name="foo"]'); + + // wrapper.assertSingle('label').text().should.equal('My label'); + // }); + + // xit('should support validation state', () => { + // shallow().assertSingle('.has-success'); + // }); + + // xit('should not support validation state when inline', () => { + // shouldWarn('ignored'); + + // shallow() + // .find('.has-success') + // .should.have.length(0); + // }); + + // xit('should support inputRef', () => { + // class Container extends React.Component { + // render() { + // return ( + // { + // this.input = ref; + // }} + // /> + // ); + // } + // } + + // const instance = mount().instance(); + + // expect(instance.input.tagName).to.equal('INPUT'); + // }); +}); diff --git a/fork/react-bootstrap/src/ResponsiveEmbed.js b/fork/react-bootstrap/src/ResponsiveEmbed.js deleted file mode 100644 index faa92ffb9e9..00000000000 --- a/fork/react-bootstrap/src/ResponsiveEmbed.js +++ /dev/null @@ -1,63 +0,0 @@ -import classNames from 'classnames'; -import React, { cloneElement } from 'react'; -import PropTypes from 'prop-types'; -import warning from 'warning'; - -import { - bsClass, - getClassSet, - prefix, - splitBsProps -} from './utils/bootstrapUtils'; - -// TODO: This should probably take a single `aspectRatio` prop. - -const propTypes = { - /** - * This component requires a single child element - */ - children: PropTypes.element.isRequired, - /** - * 16by9 aspect ratio - */ - a16by9: PropTypes.bool, - /** - * 4by3 aspect ratio - */ - a4by3: PropTypes.bool -}; - -const defaultProps = { - a16by9: false, - a4by3: false -}; - -class ResponsiveEmbed extends React.Component { - render() { - const { a16by9, a4by3, className, children, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); - - warning(a16by9 || a4by3, 'Either `a16by9` or `a4by3` must be set.'); - warning(!(a16by9 && a4by3), 'Only one of `a16by9` or `a4by3` can be set.'); - - const classes = { - ...getClassSet(bsProps), - [prefix(bsProps, '16by9')]: a16by9, - [prefix(bsProps, '4by3')]: a4by3 - }; - - return ( -
      - {cloneElement(children, { - ...elementProps, - className: classNames(className, prefix(bsProps, 'item')) - })} -
      - ); - } -} - -ResponsiveEmbed.propTypes = propTypes; -ResponsiveEmbed.defaultProps = defaultProps; - -export default bsClass('embed-responsive', ResponsiveEmbed); diff --git a/fork/react-bootstrap/src/ResponsiveEmbed.jsx b/fork/react-bootstrap/src/ResponsiveEmbed.jsx new file mode 100644 index 00000000000..0b32bbad54d --- /dev/null +++ b/fork/react-bootstrap/src/ResponsiveEmbed.jsx @@ -0,0 +1,58 @@ +import classNames from 'classnames'; +import React, { cloneElement } from 'react'; +import PropTypes from 'prop-types'; +import warning from 'warning'; + +import { bsClass, getClassSet, prefix, splitBsProps } from './utils/bootstrapUtils'; + +// TODO: This should probably take a single `aspectRatio` prop. + +const propTypes = { + /** + * This component requires a single child element + */ + children: PropTypes.element.isRequired, + /** + * 16by9 aspect ratio + */ + a16by9: PropTypes.bool, + /** + * 4by3 aspect ratio + */ + a4by3: PropTypes.bool, +}; + +const defaultProps = { + a16by9: false, + a4by3: false, +}; + +class ResponsiveEmbed extends React.Component { + render() { + const { a16by9, a4by3, className, children, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); + + warning(a16by9 || a4by3, 'Either `a16by9` or `a4by3` must be set.'); + warning(!(a16by9 && a4by3), 'Only one of `a16by9` or `a4by3` can be set.'); + + const classes = { + ...getClassSet(bsProps), + [prefix(bsProps, '16by9')]: a16by9, + [prefix(bsProps, '4by3')]: a4by3, + }; + + return ( +
      + {cloneElement(children, { + ...elementProps, + className: classNames(className, prefix(bsProps, 'item')), + })} +
      + ); + } +} + +ResponsiveEmbed.propTypes = propTypes; +ResponsiveEmbed.defaultProps = defaultProps; + +export default bsClass('embed-responsive', ResponsiveEmbed); diff --git a/fork/react-bootstrap/src/ResponsiveEmbed.test.js b/fork/react-bootstrap/src/ResponsiveEmbed.test.js deleted file mode 100644 index b05cd7ed2ae..00000000000 --- a/fork/react-bootstrap/src/ResponsiveEmbed.test.js +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import ResponsiveEmbed from '../src/ResponsiveEmbed'; - -import { shouldWarn } from './helpers'; - -describe('ResponsiveEmbed', () => { - xit('should contain `embed-responsive` class', () => { - let instance = ReactTestUtils.renderIntoDocument( - -
      - - ); - - let instanceClassName = ReactDOM.findDOMNode(instance).className; - assert.ok(instanceClassName, 'embed-responsive'); - }); - - xit('should warn if neither `a16by9` nor `a4by3` is set', () => { - shouldWarn('Either `a16by9` or `a4by3` must be set.'); - - ReactTestUtils.renderIntoDocument( - -
      - - ); - }); - - xit('should warn about both `a16by9` or `a4by3` attributes set', () => { - shouldWarn('Only one of `a16by9` or `a4by3` can be set.'); - - ReactTestUtils.renderIntoDocument( - -
      - - ); - }); - - xit('should add `embed-responsive-item` class to child element', () => { - const instance = ReactTestUtils.renderIntoDocument( - -
      - - ); - - let child = ReactDOM.findDOMNode(instance).firstChild; - assert.ok(child.className.match(/\bembed-responsive-item\b/)); - }); - - xit('should add custom classes to child element', () => { - const instance = ReactTestUtils.renderIntoDocument( - -
      - - ); - - let child = ReactDOM.findDOMNode(instance).firstChild; - assert.ok(child.className.match(/\bcustom-class\b/)); - }); - - xit('should pass custom attributes to child element', () => { - const instance = ReactTestUtils.renderIntoDocument( - -
      - - ); - - let child = ReactDOM.findDOMNode(instance).firstChild; - assert.equal(child.style.color, 'white'); - }); - - xit('should add `embed-responsive-16by9` class with `a16by9` attribute set', () => { - const instance = ReactTestUtils.renderIntoDocument( - -
      - - ); - - let wrapper = ReactDOM.findDOMNode(instance); - assert.ok(wrapper.className.match(/\bembed-responsive-16by9\b/)); - }); - - xit('should add `embed-responsive-4by3` class with `a4by3` attribute set', () => { - const instance = ReactTestUtils.renderIntoDocument( - -
      - - ); - - let wrapper = ReactDOM.findDOMNode(instance); - assert.ok(wrapper.className.match(/\bembed-responsive-4by3\b/)); - }); -}); diff --git a/fork/react-bootstrap/src/ResponsiveEmbed.test.jsx b/fork/react-bootstrap/src/ResponsiveEmbed.test.jsx new file mode 100644 index 00000000000..8e3c01be7ca --- /dev/null +++ b/fork/react-bootstrap/src/ResponsiveEmbed.test.jsx @@ -0,0 +1,76 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; +import ResponsiveEmbed from '../src/ResponsiveEmbed'; +import { shouldWarn } from './helpers'; +describe('ResponsiveEmbed', () => { + xit('should contain `embed-responsive` class', () => { + let instance = ReactTestUtils.renderIntoDocument( + +
      + , + ); + let instanceClassName = ReactDOM.findDOMNode(instance).className; + assert.ok(instanceClassName, 'embed-responsive'); + }); + xit('should warn if neither `a16by9` nor `a4by3` is set', () => { + shouldWarn('Either `a16by9` or `a4by3` must be set.'); + ReactTestUtils.renderIntoDocument( + +
      + , + ); + }); + xit('should warn about both `a16by9` or `a4by3` attributes set', () => { + shouldWarn('Only one of `a16by9` or `a4by3` can be set.'); + ReactTestUtils.renderIntoDocument( + +
      + , + ); + }); + xit('should add `embed-responsive-item` class to child element', () => { + const instance = ReactTestUtils.renderIntoDocument( + +
      + , + ); + let child = ReactDOM.findDOMNode(instance).firstChild; + assert.ok(child.className.match(/\bembed-responsive-item\b/)); + }); + xit('should add custom classes to child element', () => { + const instance = ReactTestUtils.renderIntoDocument( + +
      + , + ); + let child = ReactDOM.findDOMNode(instance).firstChild; + assert.ok(child.className.match(/\bcustom-class\b/)); + }); + xit('should pass custom attributes to child element', () => { + const instance = ReactTestUtils.renderIntoDocument( + +
      + , + ); + let child = ReactDOM.findDOMNode(instance).firstChild; + assert.equal(child.style.color, 'white'); + }); + xit('should add `embed-responsive-16by9` class with `a16by9` attribute set', () => { + const instance = ReactTestUtils.renderIntoDocument( + +
      + , + ); + let wrapper = ReactDOM.findDOMNode(instance); + assert.ok(wrapper.className.match(/\bembed-responsive-16by9\b/)); + }); + xit('should add `embed-responsive-4by3` class with `a4by3` attribute set', () => { + const instance = ReactTestUtils.renderIntoDocument( + +
      + , + ); + let wrapper = ReactDOM.findDOMNode(instance); + assert.ok(wrapper.className.match(/\bembed-responsive-4by3\b/)); + }); +}); diff --git a/fork/react-bootstrap/src/Row.js b/fork/react-bootstrap/src/Row.jsx similarity index 53% rename from fork/react-bootstrap/src/Row.js rename to fork/react-bootstrap/src/Row.jsx index d8adea097f4..1d889cfa3ba 100644 --- a/fork/react-bootstrap/src/Row.js +++ b/fork/react-bootstrap/src/Row.jsx @@ -5,24 +5,22 @@ import elementType from 'prop-types-extra/lib/elementType'; import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; const propTypes = { - componentClass: elementType + componentClass: elementType, }; const defaultProps = { - componentClass: 'div' + componentClass: 'div', }; class Row extends React.Component { - render() { - const { componentClass: Component, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsProps(props); + render() { + const { componentClass: Component, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsProps(props); - const classes = getClassSet(bsProps); + const classes = getClassSet(bsProps); - return ( - - ); - } + return ; + } } Row.propTypes = propTypes; diff --git a/fork/react-bootstrap/src/Row.test.js b/fork/react-bootstrap/src/Row.test.js deleted file mode 100644 index 19539e96412..00000000000 --- a/fork/react-bootstrap/src/Row.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Row from '../src/Row'; - -describe('Row', () => { - xit('uses "div" by default', () => { - let instance = ReactTestUtils.renderIntoDocument(); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); - }); - - xit('has "row" class', () => { - let instance = ReactTestUtils.renderIntoDocument(Row content); - assert.equal(ReactDOM.findDOMNode(instance).className, 'row'); - }); - - xit('Should merge additional classes passed in', () => { - let instance = ReactTestUtils.renderIntoDocument(); - assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bbob\b/)); - assert.ok(ReactDOM.findDOMNode(instance).className.match(/\brow\b/)); - }); - - xit('allows custom elements instead of "div"', () => { - let instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'SECTION'); - }); -}); diff --git a/fork/react-bootstrap/src/Row.test.jsx b/fork/react-bootstrap/src/Row.test.jsx new file mode 100644 index 00000000000..a49e887fa34 --- /dev/null +++ b/fork/react-bootstrap/src/Row.test.jsx @@ -0,0 +1,29 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Row from '../src/Row'; + +describe('Row', () => { + xit('uses "div" by default', () => { + let instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'DIV'); + }); + + xit('has "row" class', () => { + let instance = ReactTestUtils.renderIntoDocument(Row content); + assert.equal(ReactDOM.findDOMNode(instance).className, 'row'); + }); + + xit('Should merge additional classes passed in', () => { + let instance = ReactTestUtils.renderIntoDocument(); + assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bbob\b/)); + assert.ok(ReactDOM.findDOMNode(instance).className.match(/\brow\b/)); + }); + + xit('allows custom elements instead of "div"', () => { + let instance = ReactTestUtils.renderIntoDocument(); + + assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'SECTION'); + }); +}); diff --git a/fork/react-bootstrap/src/SafeAnchor.js b/fork/react-bootstrap/src/SafeAnchor.js deleted file mode 100644 index d1e9b125cce..00000000000 --- a/fork/react-bootstrap/src/SafeAnchor.js +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import elementType from 'prop-types-extra/lib/elementType'; - -import createChainedFunction from './utils/createChainedFunction'; - -const propTypes = { - href: PropTypes.string, - onClick: PropTypes.func, - onKeyDown: PropTypes.func, - disabled: PropTypes.bool, - role: PropTypes.string, - tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * this is sort of silly but needed for Button - */ - componentClass: elementType -}; - -const defaultProps = { - componentClass: 'a' -}; - -function isTrivialHref(href) { - return !href || href.trim() === '#'; -} - -/** - * There are situations due to browser quirks or Bootstrap CSS where - * an anchor tag is needed, when semantically a button tag is the - * better choice. SafeAnchor ensures that when an anchor is used like a - * button its accessible. It also emulates input `disabled` behavior for - * links, which is usually desirable for Buttons, NavItems, MenuItems, etc. - */ -class SafeAnchor extends React.Component { - constructor(props, context) { - super(props, context); - - this.handleClick = this.handleClick.bind(this); - this.handleKeyDown = this.handleKeyDown.bind(this); - } - - handleClick(event) { - const { disabled, href, onClick } = this.props; - - if (disabled || isTrivialHref(href)) { - event.preventDefault(); - } - - if (disabled) { - event.stopPropagation(); - return; - } - - if (onClick) { - onClick(event); - } - } - - handleKeyDown(event) { - if (event.key === ' ') { - event.preventDefault(); - this.handleClick(event); - } - } - - render() { - const { - componentClass: Component, - disabled, - onKeyDown, - ...props - } = this.props; - - if (isTrivialHref(props.href)) { - props.role = props.role || 'button'; - // we want to make sure there is a href attribute on the node - // otherwise, the cursor incorrectly styled (except with role='button') - props.href = props.href || '#'; - } - - if (disabled) { - props.tabIndex = -1; - props.style = { pointerEvents: 'none', ...props.style }; - } - - return ( - - ); - } -} - -SafeAnchor.propTypes = propTypes; -SafeAnchor.defaultProps = defaultProps; - -export default SafeAnchor; diff --git a/fork/react-bootstrap/src/SafeAnchor.jsx b/fork/react-bootstrap/src/SafeAnchor.jsx new file mode 100644 index 00000000000..b7af83236fb --- /dev/null +++ b/fork/react-bootstrap/src/SafeAnchor.jsx @@ -0,0 +1,95 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import elementType from 'prop-types-extra/lib/elementType'; + +import createChainedFunction from './utils/createChainedFunction'; + +const propTypes = { + href: PropTypes.string, + onClick: PropTypes.func, + onKeyDown: PropTypes.func, + disabled: PropTypes.bool, + role: PropTypes.string, + tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * this is sort of silly but needed for Button + */ + componentClass: elementType, +}; + +const defaultProps = { + componentClass: 'a', +}; + +function isTrivialHref(href) { + return !href || href.trim() === '#'; +} + +/** + * There are situations due to browser quirks or Bootstrap CSS where + * an anchor tag is needed, when semantically a button tag is the + * better choice. SafeAnchor ensures that when an anchor is used like a + * button its accessible. It also emulates input `disabled` behavior for + * links, which is usually desirable for Buttons, NavItems, MenuItems, etc. + */ +class SafeAnchor extends React.Component { + constructor(props, context) { + super(props, context); + + this.handleClick = this.handleClick.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + } + + handleClick(event) { + const { disabled, href, onClick } = this.props; + + if (disabled || isTrivialHref(href)) { + event.preventDefault(); + } + + if (disabled) { + event.stopPropagation(); + return; + } + + if (onClick) { + onClick(event); + } + } + + handleKeyDown(event) { + if (event.key === ' ') { + event.preventDefault(); + this.handleClick(event); + } + } + + render() { + const { componentClass: Component, disabled, onKeyDown, ...props } = this.props; + + if (isTrivialHref(props.href)) { + props.role = props.role || 'button'; + // we want to make sure there is a href attribute on the node + // otherwise, the cursor incorrectly styled (except with role='button') + props.href = props.href || '#'; + } + + if (disabled) { + props.tabIndex = -1; + props.style = { pointerEvents: 'none', ...props.style }; + } + + return ( + + ); + } +} + +SafeAnchor.propTypes = propTypes; +SafeAnchor.defaultProps = defaultProps; + +export default SafeAnchor; diff --git a/fork/react-bootstrap/src/SafeAnchor.test.js b/fork/react-bootstrap/src/SafeAnchor.test.js deleted file mode 100644 index a0eaeaebe01..00000000000 --- a/fork/react-bootstrap/src/SafeAnchor.test.js +++ /dev/null @@ -1,116 +0,0 @@ -import { screen, render } from '@testing-library/react'; -import SafeAnchor from './SafeAnchor'; - -describe('SafeAnchor', () => { - it('renders an anchor tag', () => { - render(); - expect(screen.getByRole('button').tagName).toBe('A'); - }); - - // xit('forwards provided href', () => { - // shallow() - // .find('a') - // .prop('href') - // .should.equal('http://google.com'); - // }); - - // xit('ensures that an href is provided', () => { - // mount() - // .getDOMNode() - // .hasAttribute('href').should.be.true; - // }); - - // xit('forwards onClick handler', () => { - // const handleClick = sinon.spy(); - - // shallow() - // .find('a') - // .simulate('click', { preventDefault() {} }); - - // handleClick.should.have.been.calledOnce; - // }); - - // xit('provides onClick handler as onKeyDown handler for "space"', () => { - // const handleClick = sinon.spy(); - - // shallow() - // .find('a') - // .simulate('keyDown', { key: ' ', preventDefault() {} }); - - // handleClick.should.have.been.calledOnce; - // }); - - // xit('prevents default when no href is provided', () => { - // const handleClick = sinon.spy(); - - // const wrapper = mount(); - // wrapper.find('a').simulate('click'); - - // wrapper.setProps({ href: '#' }).find('a').simulate('click'); - - // expect(handleClick).to.have.been.calledTwice; - // expect(handleClick.getCall(0).args[0].isDefaultPrevented()).to.be.true; - // expect(handleClick.getCall(1).args[0].isDefaultPrevented()).to.be.true; - // }); - - // xit('does not prevent default when href is provided', () => { - // const handleClick = sinon.spy(); - - // mount() - // .find('a') - // .simulate('click'); - - // expect(handleClick).to.have.been.calledOnce; - // expect(handleClick.getCall(0).args[0].isDefaultPrevented()).to.be.false; - // }); - - // xit('Should disable link behavior', () => { - // let clickSpy = sinon.spy(); - // let spy = sinon.spy(SafeAnchor.prototype, 'handleClick'); - - // mount( - // - // Title - // - // ).simulate('click'); - - // expect(spy).to.have.been.calledOnce; - // expect(clickSpy).to.have.not.been.called; - // expect(spy.getCall(0).args[0].isDefaultPrevented()).to.equal(true); - // expect(spy.getCall(0).args[0].isPropagationStopped()).to.equal(true); - // }); - - // xit('forwards provided role', () => { - // shallow() - // .find('a') - // .prop('role') - // .should.equal('test'); - // }); - - // xit('forwards provided role with href', () => { - // shallow() - // .find('a') - // .prop('role') - // .should.equal('test'); - // }); - - // xit('set role=button with no provided href', () => { - // shallow() - // .find('a') - // .prop('role') - // .should.equal('button'); - - // shallow() - // .find('a') - // .prop('role') - // .should.equal('button'); - // }); - - // xit('sets no role with provided href', () => { - // expect( - // shallow() - // .find('a') - // .prop('role') - // ).to.not.exist; - // }); -}); diff --git a/fork/react-bootstrap/src/SafeAnchor.test.jsx b/fork/react-bootstrap/src/SafeAnchor.test.jsx new file mode 100644 index 00000000000..e9908ef4f9e --- /dev/null +++ b/fork/react-bootstrap/src/SafeAnchor.test.jsx @@ -0,0 +1,116 @@ +import { screen, render } from '@testing-library/react'; +import SafeAnchor from './SafeAnchor'; + +describe('SafeAnchor', () => { + it('renders an anchor tag', () => { + render(); + expect(screen.getByRole('button').tagName).toBe('A'); + }); + + // xit('forwards provided href', () => { + // shallow() + // .find('a') + // .prop('href') + // .should.equal('http://google.com'); + // }); + + // xit('ensures that an href is provided', () => { + // mount() + // .getDOMNode() + // .hasAttribute('href').should.be.true; + // }); + + // xit('forwards onClick handler', () => { + // const handleClick = sinon.spy(); + + // shallow() + // .find('a') + // .simulate('click', { preventDefault() {} }); + + // handleClick.should.have.been.calledOnce; + // }); + + // xit('provides onClick handler as onKeyDown handler for "space"', () => { + // const handleClick = sinon.spy(); + + // shallow() + // .find('a') + // .simulate('keyDown', { key: ' ', preventDefault() {} }); + + // handleClick.should.have.been.calledOnce; + // }); + + // xit('prevents default when no href is provided', () => { + // const handleClick = sinon.spy(); + + // const wrapper = mount(); + // wrapper.find('a').simulate('click'); + + // wrapper.setProps({ href: '#' }).find('a').simulate('click'); + + // expect(handleClick).to.have.been.calledTwice; + // expect(handleClick.getCall(0).args[0].isDefaultPrevented()).to.be.true; + // expect(handleClick.getCall(1).args[0].isDefaultPrevented()).to.be.true; + // }); + + // xit('does not prevent default when href is provided', () => { + // const handleClick = sinon.spy(); + + // mount() + // .find('a') + // .simulate('click'); + + // expect(handleClick).to.have.been.calledOnce; + // expect(handleClick.getCall(0).args[0].isDefaultPrevented()).to.be.false; + // }); + + // xit('Should disable link behavior', () => { + // let clickSpy = sinon.spy(); + // let spy = sinon.spy(SafeAnchor.prototype, 'handleClick'); + + // mount( + // + // Title + // + // ).simulate('click'); + + // expect(spy).to.have.been.calledOnce; + // expect(clickSpy).to.have.not.been.called; + // expect(spy.getCall(0).args[0].isDefaultPrevented()).to.equal(true); + // expect(spy.getCall(0).args[0].isPropagationStopped()).to.equal(true); + // }); + + // xit('forwards provided role', () => { + // shallow() + // .find('a') + // .prop('role') + // .should.equal('test'); + // }); + + // xit('forwards provided role with href', () => { + // shallow() + // .find('a') + // .prop('role') + // .should.equal('test'); + // }); + + // xit('set role=button with no provided href', () => { + // shallow() + // .find('a') + // .prop('role') + // .should.equal('button'); + + // shallow() + // .find('a') + // .prop('role') + // .should.equal('button'); + // }); + + // xit('sets no role with provided href', () => { + // expect( + // shallow() + // .find('a') + // .prop('role') + // ).to.not.exist; + // }); +}); diff --git a/fork/react-bootstrap/src/SplitButton.js b/fork/react-bootstrap/src/SplitButton.js deleted file mode 100644 index 36fffaf299a..00000000000 --- a/fork/react-bootstrap/src/SplitButton.js +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Button from './Button'; -import Dropdown from './Dropdown'; -import SplitToggle from './SplitToggle'; -import splitComponentProps from './utils/splitComponentProps'; - -const propTypes = { - ...Dropdown.propTypes, - - // Toggle props. - bsStyle: PropTypes.string, - bsSize: PropTypes.string, - href: PropTypes.string, - onClick: PropTypes.func, - /** - * The content of the split button. - */ - title: PropTypes.node.isRequired, - /** - * Accessible label for the toggle; the value of `title` if not specified. - */ - toggleLabel: PropTypes.string, - - // Override generated docs from . - /** - * @private - */ - children: PropTypes.node -}; - -class SplitButton extends React.Component { - render() { - const { - bsSize, - bsStyle, - title, - toggleLabel, - children, - ...props - } = this.props; - - const [dropdownProps, buttonProps] = splitComponentProps( - props, - Dropdown.ControlledComponent - ); - - return ( - - - - - {children} - - ); - } -} - -SplitButton.propTypes = propTypes; - -SplitButton.Toggle = SplitToggle; - -export default SplitButton; diff --git a/fork/react-bootstrap/src/SplitButton.jsx b/fork/react-bootstrap/src/SplitButton.jsx new file mode 100644 index 00000000000..74de415aae1 --- /dev/null +++ b/fork/react-bootstrap/src/SplitButton.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Button from './Button'; +import Dropdown from './Dropdown'; +import SplitToggle from './SplitToggle'; +import splitComponentProps from './utils/splitComponentProps'; + +const propTypes = { + ...Dropdown.propTypes, + + // Toggle props. + bsStyle: PropTypes.string, + bsSize: PropTypes.string, + href: PropTypes.string, + onClick: PropTypes.func, + /** + * The content of the split button. + */ + title: PropTypes.node.isRequired, + /** + * Accessible label for the toggle; the value of `title` if not specified. + */ + toggleLabel: PropTypes.string, + + // Override generated docs from . + /** + * @private + */ + children: PropTypes.node, +}; + +class SplitButton extends React.Component { + render() { + const { bsSize, bsStyle, title, toggleLabel, children, ...props } = this.props; + + const [dropdownProps, buttonProps] = splitComponentProps(props, Dropdown.ControlledComponent); + + return ( + + + + + {children} + + ); + } +} + +SplitButton.propTypes = propTypes; + +SplitButton.Toggle = SplitToggle; + +export default SplitButton; diff --git a/fork/react-bootstrap/src/SplitButton.test.js b/fork/react-bootstrap/src/SplitButton.test.js deleted file mode 100644 index bab09d1d6b5..00000000000 --- a/fork/react-bootstrap/src/SplitButton.test.js +++ /dev/null @@ -1,168 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; -import { assert } from 'chai'; -import sinon from 'sinon'; - -import SplitButton from './SplitButton'; -import MenuItem from './MenuItem'; -import Button from './Button'; - -describe('', () => { - const simple = ( - - Item 1 - Item 2 - Item 3 - Item 4 - - ); - - xit('should open the menu when dropdown button is clicked', () => { - const instance = ReactTestUtils.renderIntoDocument(simple); - - const toggleNode = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'dropdown-toggle' - ); - const splitButtonNode = ReactDOM.findDOMNode(instance); - - splitButtonNode.className.should.not.match(/open/); - ReactTestUtils.Simulate.click(toggleNode); - splitButtonNode.className.should.match(/open/); - }); - - xit('should not open the menu when other button is clicked', () => { - const instance = ReactTestUtils.renderIntoDocument(simple); - - const buttonNode = ReactDOM.findDOMNode( - ReactTestUtils.scryRenderedComponentsWithType(instance, Button)[0] - ); - const splitButtonNode = ReactDOM.findDOMNode(instance); - - splitButtonNode.className.should.not.match(/open/); - ReactTestUtils.Simulate.click(buttonNode); - splitButtonNode.className.should.not.match(/open/); - }); - - xit('should invoke onClick when SplitButton.Button is clicked (prop)', (done) => { - const instance = ReactTestUtils.renderIntoDocument( - done()}> - Item 1 - - ); - - const buttonNode = ReactDOM.findDOMNode( - ReactTestUtils.scryRenderedComponentsWithType(instance, Button)[0] - ); - ReactTestUtils.Simulate.click(buttonNode); - }); - - xit('should not invoke onClick when SplitButton.Toggle is clicked (prop)', (done) => { - let onClickSpy = sinon.spy(); - - const instance = ReactTestUtils.renderIntoDocument( - - Item 1 - - ); - - const toggleNode = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'dropdown-toggle' - ); - - ReactTestUtils.Simulate.click(toggleNode); - - setTimeout(() => { - onClickSpy.should.not.have.been.called; - done(); - }, 10); - }); - - xit('Should pass disabled to both buttons', () => { - const instance = ReactTestUtils.renderIntoDocument( - - Item 1 - - ); - - const toggleNode = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'dropdown-toggle' - ); - - const buttonNode = ReactDOM.findDOMNode( - ReactTestUtils.scryRenderedComponentsWithType(instance, Button)[0] - ); - - expect(toggleNode.disabled).to.be.true; - expect(buttonNode.disabled).to.be.true; - }); - - xit('Should set target attribute on anchor', () => { - const instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - - ); - - let anchors = ReactTestUtils.scryRenderedDOMComponentsWithTag( - instance, - 'a' - ); - let linkElement = anchors[0]; - - assert.equal(linkElement.target, '_blank'); - }); - - xit('should set aria-label on toggle from title', () => { - const instance = ReactTestUtils.renderIntoDocument(simple); - - const toggleNode = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'dropdown-toggle' - ); - expect(toggleNode.getAttribute('aria-label')).to.equal('Title'); - }); - - xit('should set aria-label on toggle from toggleLabel', () => { - const instance = ReactTestUtils.renderIntoDocument( - - Item 1 - - ); - - const toggleNode = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'dropdown-toggle' - ); - expect(toggleNode.getAttribute('aria-label')).to.equal('Label'); - }); - - xit('should derive bsClass from parent', () => { - const instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - - ); - - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'my-dropdown-toggle' - ) - ); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass( - instance, - 'my-dropdown-menu' - ) - ); - }); -}); diff --git a/fork/react-bootstrap/src/SplitButton.test.jsx b/fork/react-bootstrap/src/SplitButton.test.jsx new file mode 100644 index 00000000000..06564373d9f --- /dev/null +++ b/fork/react-bootstrap/src/SplitButton.test.jsx @@ -0,0 +1,149 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; +import { assert } from 'chai'; +import sinon from 'sinon'; + +import SplitButton from './SplitButton'; +import MenuItem from './MenuItem'; +import Button from './Button'; + +describe('', () => { + const simple = ( + + Item 1 + Item 2 + Item 3 + Item 4 + + ); + + xit('should open the menu when dropdown button is clicked', () => { + const instance = ReactTestUtils.renderIntoDocument(simple); + + const toggleNode = ReactTestUtils.findRenderedDOMComponentWithClass( + instance, + 'dropdown-toggle', + ); + const splitButtonNode = ReactDOM.findDOMNode(instance); + + splitButtonNode.className.should.not.match(/open/); + ReactTestUtils.Simulate.click(toggleNode); + splitButtonNode.className.should.match(/open/); + }); + + xit('should not open the menu when other button is clicked', () => { + const instance = ReactTestUtils.renderIntoDocument(simple); + + const buttonNode = ReactDOM.findDOMNode( + ReactTestUtils.scryRenderedComponentsWithType(instance, Button)[0], + ); + const splitButtonNode = ReactDOM.findDOMNode(instance); + + splitButtonNode.className.should.not.match(/open/); + ReactTestUtils.Simulate.click(buttonNode); + splitButtonNode.className.should.not.match(/open/); + }); + + xit('should invoke onClick when SplitButton.Button is clicked (prop)', done => { + const instance = ReactTestUtils.renderIntoDocument( + done()}> + Item 1 + , + ); + + const buttonNode = ReactDOM.findDOMNode( + ReactTestUtils.scryRenderedComponentsWithType(instance, Button)[0], + ); + ReactTestUtils.Simulate.click(buttonNode); + }); + + xit('should not invoke onClick when SplitButton.Toggle is clicked (prop)', done => { + let onClickSpy = sinon.spy(); + + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 + , + ); + + const toggleNode = ReactTestUtils.findRenderedDOMComponentWithClass( + instance, + 'dropdown-toggle', + ); + + ReactTestUtils.Simulate.click(toggleNode); + + setTimeout(() => { + onClickSpy.should.not.have.been.called; + done(); + }, 10); + }); + + xit('Should pass disabled to both buttons', () => { + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 + , + ); + + const toggleNode = ReactTestUtils.findRenderedDOMComponentWithClass( + instance, + 'dropdown-toggle', + ); + + const buttonNode = ReactDOM.findDOMNode( + ReactTestUtils.scryRenderedComponentsWithType(instance, Button)[0], + ); + + expect(toggleNode.disabled).to.be.true; + expect(buttonNode.disabled).to.be.true; + }); + + xit('Should set target attribute on anchor', () => { + const instance = ReactTestUtils.renderIntoDocument( + + MenuItem 1 content + , + ); + + let anchors = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'a'); + let linkElement = anchors[0]; + + assert.equal(linkElement.target, '_blank'); + }); + + xit('should set aria-label on toggle from title', () => { + const instance = ReactTestUtils.renderIntoDocument(simple); + + const toggleNode = ReactTestUtils.findRenderedDOMComponentWithClass( + instance, + 'dropdown-toggle', + ); + expect(toggleNode.getAttribute('aria-label')).to.equal('Title'); + }); + + xit('should set aria-label on toggle from toggleLabel', () => { + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 + , + ); + + const toggleNode = ReactTestUtils.findRenderedDOMComponentWithClass( + instance, + 'dropdown-toggle', + ); + expect(toggleNode.getAttribute('aria-label')).to.equal('Label'); + }); + + xit('should derive bsClass from parent', () => { + const instance = ReactTestUtils.renderIntoDocument( + + MenuItem 1 content + , + ); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'my-dropdown-toggle')); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'my-dropdown-menu')); + }); +}); diff --git a/fork/react-bootstrap/src/SplitToggle.js b/fork/react-bootstrap/src/SplitToggle.jsx similarity index 67% rename from fork/react-bootstrap/src/SplitToggle.js rename to fork/react-bootstrap/src/SplitToggle.jsx index ccce524421b..e9a932957a8 100644 --- a/fork/react-bootstrap/src/SplitToggle.js +++ b/fork/react-bootstrap/src/SplitToggle.jsx @@ -3,9 +3,9 @@ import React from 'react'; import DropdownToggle from './DropdownToggle'; class SplitToggle extends React.Component { - render() { - return ; - } + render() { + return ; + } } SplitToggle.defaultProps = DropdownToggle.defaultProps; diff --git a/fork/react-bootstrap/src/Tab.js b/fork/react-bootstrap/src/Tab.js deleted file mode 100644 index 5d01305d551..00000000000 --- a/fork/react-bootstrap/src/Tab.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import TabContainer from './TabContainer'; -import TabContent from './TabContent'; -import TabPane from './TabPane'; - -const propTypes = { - ...TabPane.propTypes, - - disabled: PropTypes.bool, - - title: PropTypes.node, - - /** - * tabClassName is used as className for the associated NavItem - */ - tabClassName: PropTypes.string -}; - -class Tab extends React.Component { - render() { - const props = { ...this.props }; - - // These props are for the parent `` rather than the ``. - delete props.title; - delete props.disabled; - delete props.tabClassName; - - return ; - } -} - -Tab.propTypes = propTypes; - -Tab.Container = TabContainer; -Tab.Content = TabContent; -Tab.Pane = TabPane; - -export default Tab; diff --git a/fork/react-bootstrap/src/Tab.jsx b/fork/react-bootstrap/src/Tab.jsx new file mode 100644 index 00000000000..b97b2168755 --- /dev/null +++ b/fork/react-bootstrap/src/Tab.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import TabContainer from './TabContainer'; +import TabContent from './TabContent'; +import TabPane from './TabPane'; + +const propTypes = { + ...TabPane.propTypes, + + disabled: PropTypes.bool, + + title: PropTypes.node, + + /** + * tabClassName is used as className for the associated NavItem + */ + tabClassName: PropTypes.string, +}; + +class Tab extends React.Component { + render() { + const props = { ...this.props }; + + // These props are for the parent `` rather than the ``. + delete props.title; + delete props.disabled; + delete props.tabClassName; + + return ; + } +} + +Tab.propTypes = propTypes; + +Tab.Container = TabContainer; +Tab.Content = TabContent; +Tab.Pane = TabPane; + +export default Tab; diff --git a/fork/react-bootstrap/src/Tab.test.js b/fork/react-bootstrap/src/Tab.test.js deleted file mode 100644 index c34b9c22856..00000000000 --- a/fork/react-bootstrap/src/Tab.test.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; - -import Tab from '../src/Tab'; - -describe('', () => { - xit('Should have class', () => { - let instance = ReactTestUtils.renderIntoDocument(Item content); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'tab-pane') - ); - }); - - xit('Should add active class', () => { - let instance = ReactTestUtils.renderIntoDocument(Item content); - assert.ok( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'active') - ); - }); - - xit('Should not add active class when not visible', () => { - let instance = ReactTestUtils.renderIntoDocument( - Item content - ); - assert.lengthOf( - ReactTestUtils.scryRenderedDOMComponentsWithClass(instance, 'active'), - 0 - ); - }); - - describe('Web Accessibility', () => { - xit('Should have aria-hidden false when visible', () => { - let instance = ReactTestUtils.renderIntoDocument(Item content); - - assert.equal( - ReactDOM.findDOMNode(instance).getAttribute('aria-hidden'), - 'false' - ); - }); - - xit('Should have aria-hidden true when hidden', () => { - let instance = ReactTestUtils.renderIntoDocument( - Item content - ); - - assert.equal( - ReactDOM.findDOMNode(instance).getAttribute('aria-hidden'), - 'true' - ); - }); - - xit('Should have role', () => { - let instance = ReactTestUtils.renderIntoDocument(Item content); - - assert.equal( - ReactDOM.findDOMNode(instance).getAttribute('role'), - 'tabpanel' - ); - }); - }); -}); diff --git a/fork/react-bootstrap/src/Tab.test.jsx b/fork/react-bootstrap/src/Tab.test.jsx new file mode 100644 index 00000000000..0d60446918c --- /dev/null +++ b/fork/react-bootstrap/src/Tab.test.jsx @@ -0,0 +1,41 @@ +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; + +import Tab from '../src/Tab'; + +describe('', () => { + xit('Should have class', () => { + let instance = ReactTestUtils.renderIntoDocument(Item content); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'tab-pane')); + }); + + xit('Should add active class', () => { + let instance = ReactTestUtils.renderIntoDocument(Item content); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'active')); + }); + + xit('Should not add active class when not visible', () => { + let instance = ReactTestUtils.renderIntoDocument(Item content); + assert.lengthOf(ReactTestUtils.scryRenderedDOMComponentsWithClass(instance, 'active'), 0); + }); + + describe('Web Accessibility', () => { + xit('Should have aria-hidden false when visible', () => { + let instance = ReactTestUtils.renderIntoDocument(Item content); + + assert.equal(ReactDOM.findDOMNode(instance).getAttribute('aria-hidden'), 'false'); + }); + + xit('Should have aria-hidden true when hidden', () => { + let instance = ReactTestUtils.renderIntoDocument(Item content); + + assert.equal(ReactDOM.findDOMNode(instance).getAttribute('aria-hidden'), 'true'); + }); + + xit('Should have role', () => { + let instance = ReactTestUtils.renderIntoDocument(Item content); + + assert.equal(ReactDOM.findDOMNode(instance).getAttribute('role'), 'tabpanel'); + }); + }); +}); diff --git a/fork/react-bootstrap/src/TabContainer.js b/fork/react-bootstrap/src/TabContainer.js deleted file mode 100644 index af9184ef29c..00000000000 --- a/fork/react-bootstrap/src/TabContainer.js +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { uncontrollable } from 'uncontrollable'; - -const TAB = 'tab'; -const PANE = 'pane'; - -const idPropType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); - -const propTypes = { - /** - * HTML id attribute, required if no `generateChildId` prop - * is specified. - */ - id(props, ...args) { - let error = null; - - if (!props.generateChildId) { - error = idPropType(props, ...args); - - if (!error && !props.id) { - error = new Error( - 'In order to properly initialize Tabs in a way that is accessible ' + - 'to assistive technologies (such as screen readers) an `id` or a ' + - '`generateChildId` prop to TabContainer is required' - ); - } - } - - return error; - }, - - /** - * A function that takes an `eventKey` and `type` and returns a unique id for - * child tab ``s and ``s. The function _must_ be a pure - * function, meaning it should always return the _same_ id for the same set - * of inputs. The default value requires that an `id` to be set for the - * ``. - * - * The `type` argument will either be `"tab"` or `"pane"`. - * - * @defaultValue (eventKey, type) => `${this.props.id}-${type}-${key}` - */ - generateChildId: PropTypes.func, - - /** - * A callback fired when a tab is selected. - * - * @controllable activeKey - */ - onSelect: PropTypes.func, - - /** - * The `eventKey` of the currently active tab. - * - * @controllable onSelect - */ - activeKey: PropTypes.any -}; - -const childContextTypes = { - $bs_tabContainer: PropTypes.shape({ - activeKey: PropTypes.any, - onSelect: PropTypes.func.isRequired, - getTabId: PropTypes.func.isRequired, - getPaneId: PropTypes.func.isRequired - }) -}; - -class TabContainer extends React.Component { - getChildContext() { - const { activeKey, onSelect, generateChildId, id } = this.props; - - const getId = - generateChildId || ((key, type) => (id ? `${id}-${type}-${key}` : null)); - - return { - $bs_tabContainer: { - activeKey, - onSelect, - getTabId: key => getId(key, TAB), - getPaneId: key => getId(key, PANE) - } - }; - } - - render() { - const { children, ...props } = this.props; - - delete props.generateChildId; - delete props.onSelect; - delete props.activeKey; - - return React.cloneElement(React.Children.only(children), props); - } -} - -TabContainer.propTypes = propTypes; -TabContainer.childContextTypes = childContextTypes; - -export default uncontrollable(TabContainer, { activeKey: 'onSelect' }); diff --git a/fork/react-bootstrap/src/TabContainer.jsx b/fork/react-bootstrap/src/TabContainer.jsx new file mode 100644 index 00000000000..b8e25d657bd --- /dev/null +++ b/fork/react-bootstrap/src/TabContainer.jsx @@ -0,0 +1,100 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { uncontrollable } from 'uncontrollable'; + +const TAB = 'tab'; +const PANE = 'pane'; + +const idPropType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); + +const propTypes = { + /** + * HTML id attribute, required if no `generateChildId` prop + * is specified. + */ + id(props, ...args) { + let error = null; + + if (!props.generateChildId) { + error = idPropType(props, ...args); + + if (!error && !props.id) { + error = new Error( + 'In order to properly initialize Tabs in a way that is accessible ' + + 'to assistive technologies (such as screen readers) an `id` or a ' + + '`generateChildId` prop to TabContainer is required', + ); + } + } + + return error; + }, + + /** + * A function that takes an `eventKey` and `type` and returns a unique id for + * child tab ``s and ``s. The function _must_ be a pure + * function, meaning it should always return the _same_ id for the same set + * of inputs. The default value requires that an `id` to be set for the + * ``. + * + * The `type` argument will either be `"tab"` or `"pane"`. + * + * @defaultValue (eventKey, type) => `${this.props.id}-${type}-${key}` + */ + generateChildId: PropTypes.func, + + /** + * A callback fired when a tab is selected. + * + * @controllable activeKey + */ + onSelect: PropTypes.func, + + /** + * The `eventKey` of the currently active tab. + * + * @controllable onSelect + */ + activeKey: PropTypes.any, +}; + +const childContextTypes = { + $bs_tabContainer: PropTypes.shape({ + activeKey: PropTypes.any, + onSelect: PropTypes.func.isRequired, + getTabId: PropTypes.func.isRequired, + getPaneId: PropTypes.func.isRequired, + }), +}; + +class TabContainer extends React.Component { + getChildContext() { + const { activeKey, onSelect, generateChildId, id } = this.props; + + const getId = generateChildId || ((key, type) => (id ? `${id}-${type}-${key}` : null)); + + return { + $bs_tabContainer: { + activeKey, + onSelect, + getTabId: key => getId(key, TAB), + getPaneId: key => getId(key, PANE), + }, + }; + } + + render() { + const { children, ...props } = this.props; + + delete props.generateChildId; + delete props.onSelect; + delete props.activeKey; + + return React.cloneElement(React.Children.only(children), props); + } +} + +TabContainer.propTypes = propTypes; +TabContainer.childContextTypes = childContextTypes; + +export default uncontrollable(TabContainer, { activeKey: 'onSelect' }); diff --git a/fork/react-bootstrap/src/TabContainer.test.js b/fork/react-bootstrap/src/TabContainer.test.js deleted file mode 100644 index 4892e98f5a8..00000000000 --- a/fork/react-bootstrap/src/TabContainer.test.js +++ /dev/null @@ -1,271 +0,0 @@ -import { screen, render } from '@testing-library/react'; -import Nav from './Nav'; -import NavItem from './NavItem'; -import TabPane from './TabPane'; -import TabContent from './TabContent'; -import TabContainer from './TabContainer'; - -describe('', () => { - it('should not propagate context past TabPanes', () => { - render( - -
      - - - - - - -
      -
      - ); - - // let top = instance.find('div > Nav').first().instance() - // .context.$bs_tabContainer; - - // let nested = instance.find('TabPane Nav').first().instance() - // .context.$bs_tabContainer; - - // expect(top).to.exist; - // expect(nested).to.not.exist; - expect(screen.getByText('One')).toBeInTheDocument(); - expect(screen.getByText('Two')).toBeInTheDocument(); - // eslint-disable-next-line jest-dom/prefer-in-document - expect(screen.getAllByRole('tab')).toHaveLength(1); - }); - - // xit('should match up ids', () => { - // let instance = mount( - // - //
      - // - // - // - // - //
      - //
      - // ); - - // let tabId = instance.find('NavItem a').first().prop('id'); - - // let paneId = instance.find('TabPane div').first().prop('id'); - - // expect(tabId).to.exist; - // expect(paneId).to.exist; - - // instance.assertSingle(`a[aria-controls="${paneId}"]`); - // instance.assertSingle(`div[aria-labelledby="${tabId}"]`); - // }); - - // xit('should default Nav role to tablist', () => { - // let instance = mount( - // - //
      - // - //
      - //
      - // ); - - // instance - // .find(Nav) - // .getDOMNode() - // .getAttribute('role') - // .should.equal('tablist'); - - // instance - // .find('NavItem a') - // .first() - // .getDOMNode() - // .getAttribute('role') - // .should.equal('tab'); - // }); - - // xit('should use explicit Nav role', () => { - // let instance = mount( - // - //
      - // - //
      - //
      - // ); - - // instance - // .find(Nav) - // .getDOMNode() - // .getAttribute('role') - // .should.equal('navigation'); - - // // make sure its not passed to the NavItem - // expect(instance.find('NavItem a').first().getDOMNode().getAttribute('role')) - // .to.not.exist; - // }); - - // describe('tab switching edge cases', () => { - // class Switcher extends React.Component { - // state = { ...this.props }; - - // render() { - // const { - // eventKeys, - // show = true, - // onSelect = () => {}, - // tabProps = [], - // ...props - // } = this.state; - - // if (!show) { - // return null; - // } - - // return ( - // - // - // {eventKeys.map((eventKey, index) => ( - // - // ))} - // - // - // ); - // } - // } - - // xit('should not get stuck after tab becomes unmounted', () => { - // const instance = mount(); - - // instance.assertSingle(TabContent); - // instance.assertSingle('[eventKey=2]').assertSingle('.active'); - - // instance.setState({ eventKeys: [1] }); - // instance.assertNone('.active'); - - // instance.setState({ activeKey: 1 }); - // instance.assertSingle('[eventKey=1]').assertSingle('.active'); - // }); - - // xit('should handle closing tab and changing active tab', () => { - // const instance = mount(); - - // instance.assertSingle('[eventKey=2]').assertSingle('.active'); - - // instance.setState({ eventKeys: [1], activeKey: 1 }); - // // XXX I have no idea why this is needed but the test fails without it. - // instance.update(); - // instance.assertSingle('[eventKey=1]').assertSingle('.active'); - // }); - - // xit('should not call onSelect when container unmounts', () => { - // const spy = sinon.spy(); - // const instance = mount( - // - // ); - - // instance.assertSingle(TabPane); - - // instance.setState({ show: false }); - // spy.should.have.not.been.called; - // }); - - // xit('should clean up unmounted tab state', () => { - // const instance = mount(); - - // instance.find(TabPane).length.should.equal(3); - // instance.assertSingle('[eventKey=3]').assertSingle('.active'); - - // instance.setState({ eventKeys: [1, 2], activeKey: 2 }); - // // XXX I have no idea why this is needed but the test fails without it. - // instance.update(); - // instance.find(TabPane).length.should.equal(2); - // instance.assertSingle('[eventKey=2]').assertSingle('.active'); - // }); - - // xit('should not get stuck if tab stops animating', () => { - // const instance = mount(); - - // instance.assertSingle('[eventKey=1]').assertSingle('.active'); - - // instance.setState({ animation: false }); - // instance.assertSingle('[eventKey=1]').assertSingle('.active'); - - // instance.setState({ activeKey: 2 }); - // instance.assertSingle('[eventKey=2]').assertSingle('.active'); - - // instance.setState({ animation: true }); - // instance.setState({ activeKey: 1 }); - // instance.assertSingle('[eventKey=2]').assertSingle('.active'); - // }); - - // xit('should handle simultaneous eventKey and activeKey change', () => { - // const instance = mount(); - - // instance.assertSingle('[eventKey=2]').assertSingle('.active'); - - // instance.setState({ eventKeys: [1, 3], activeKey: 3 }); - // // XXX I have no idea why this is needed but the test fails without it. - // instance.update(); - // instance.assertSingle('[eventKey=3]').assertSingle('.active'); - - // instance.setState({ eventKeys: [1, 4], activeKey: 4 }); - // // XXX I have no idea why this is needed but the test fails without it. - // instance.update(); - // instance.assertSingle('[eventKey=4]').assertSingle('.active'); - // }); - - // xit('should not get stuck if eventKey ceases to exist', () => { - // const instance = mount(); - - // instance.assertSingle('[eventKey=2]').assertSingle('.active'); - - // instance.setState({ eventKeys: [1, 3] }); - // instance.assertNone('.active'); - - // instance.setState({ activeKey: 3 }); - // instance.assertSingle('[eventKey=3]').assertSingle('.active'); - - // // Check that active state lingers after changing event key. - // instance.setState({ activeKey: 1 }); - // instance.assertSingle('[eventKey=3]').assertSingle('.active'); - - // // But once event key changes again, make sure active state switches. - // instance.setState({ eventKeys: [1, 2] }); - // // XXX I have no idea why this is needed but the test fails without it. - // instance.update(); - // instance.assertSingle('[eventKey=1]').assertSingle('.active'); - // }); - - // [ - // [ - // [1, 2], - // [2, 1], - // ], - // [ - // [2, 1], - // [1, 2], - // ], - // ].forEach(([order1, order2]) => { - // xit('should handle event key swaps', () => { - // const instance = mount(); - - // instance.assertSingle('[eventKey=1]').assertSingle('.active'); - - // instance.setState({ eventKeys: order2 }); - // instance.assertSingle('[eventKey=1]').assertSingle('.active'); - - // // Check that the animation is still wired up. - // instance.setState({ activeKey: 2 }); - // instance.assertSingle('[eventKey=1]').assertSingle('.active'); - // }); - // }); - // }); -}); diff --git a/fork/react-bootstrap/src/TabContainer.test.jsx b/fork/react-bootstrap/src/TabContainer.test.jsx new file mode 100644 index 00000000000..a9f4e0a1975 --- /dev/null +++ b/fork/react-bootstrap/src/TabContainer.test.jsx @@ -0,0 +1,271 @@ +import { screen, render } from '@testing-library/react'; +import Nav from './Nav'; +import NavItem from './NavItem'; +import TabPane from './TabPane'; +import TabContent from './TabContent'; +import TabContainer from './TabContainer'; + +describe('', () => { + it('should not propagate context past TabPanes', () => { + render( + +
      + + + + + + +
      +
      , + ); + + // let top = instance.find('div > Nav').first().instance() + // .context.$bs_tabContainer; + + // let nested = instance.find('TabPane Nav').first().instance() + // .context.$bs_tabContainer; + + // expect(top).to.exist; + // expect(nested).to.not.exist; + expect(screen.getByText('One')).toBeInTheDocument(); + expect(screen.getByText('Two')).toBeInTheDocument(); + // eslint-disable-next-line jest-dom/prefer-in-document + expect(screen.getAllByRole('tab')).toHaveLength(1); + }); + + // xit('should match up ids', () => { + // let instance = mount( + // + //
      + // + // + // + // + //
      + //
      + // ); + + // let tabId = instance.find('NavItem a').first().prop('id'); + + // let paneId = instance.find('TabPane div').first().prop('id'); + + // expect(tabId).to.exist; + // expect(paneId).to.exist; + + // instance.assertSingle(`a[aria-controls="${paneId}"]`); + // instance.assertSingle(`div[aria-labelledby="${tabId}"]`); + // }); + + // xit('should default Nav role to tablist', () => { + // let instance = mount( + // + //
      + // + //
      + //
      + // ); + + // instance + // .find(Nav) + // .getDOMNode() + // .getAttribute('role') + // .should.equal('tablist'); + + // instance + // .find('NavItem a') + // .first() + // .getDOMNode() + // .getAttribute('role') + // .should.equal('tab'); + // }); + + // xit('should use explicit Nav role', () => { + // let instance = mount( + // + //
      + // + //
      + //
      + // ); + + // instance + // .find(Nav) + // .getDOMNode() + // .getAttribute('role') + // .should.equal('navigation'); + + // // make sure its not passed to the NavItem + // expect(instance.find('NavItem a').first().getDOMNode().getAttribute('role')) + // .to.not.exist; + // }); + + // describe('tab switching edge cases', () => { + // class Switcher extends React.Component { + // state = { ...this.props }; + + // render() { + // const { + // eventKeys, + // show = true, + // onSelect = () => {}, + // tabProps = [], + // ...props + // } = this.state; + + // if (!show) { + // return null; + // } + + // return ( + // + // + // {eventKeys.map((eventKey, index) => ( + // + // ))} + // + // + // ); + // } + // } + + // xit('should not get stuck after tab becomes unmounted', () => { + // const instance = mount(); + + // instance.assertSingle(TabContent); + // instance.assertSingle('[eventKey=2]').assertSingle('.active'); + + // instance.setState({ eventKeys: [1] }); + // instance.assertNone('.active'); + + // instance.setState({ activeKey: 1 }); + // instance.assertSingle('[eventKey=1]').assertSingle('.active'); + // }); + + // xit('should handle closing tab and changing active tab', () => { + // const instance = mount(); + + // instance.assertSingle('[eventKey=2]').assertSingle('.active'); + + // instance.setState({ eventKeys: [1], activeKey: 1 }); + // // XXX I have no idea why this is needed but the test fails without it. + // instance.update(); + // instance.assertSingle('[eventKey=1]').assertSingle('.active'); + // }); + + // xit('should not call onSelect when container unmounts', () => { + // const spy = sinon.spy(); + // const instance = mount( + // + // ); + + // instance.assertSingle(TabPane); + + // instance.setState({ show: false }); + // spy.should.have.not.been.called; + // }); + + // xit('should clean up unmounted tab state', () => { + // const instance = mount(); + + // instance.find(TabPane).length.should.equal(3); + // instance.assertSingle('[eventKey=3]').assertSingle('.active'); + + // instance.setState({ eventKeys: [1, 2], activeKey: 2 }); + // // XXX I have no idea why this is needed but the test fails without it. + // instance.update(); + // instance.find(TabPane).length.should.equal(2); + // instance.assertSingle('[eventKey=2]').assertSingle('.active'); + // }); + + // xit('should not get stuck if tab stops animating', () => { + // const instance = mount(); + + // instance.assertSingle('[eventKey=1]').assertSingle('.active'); + + // instance.setState({ animation: false }); + // instance.assertSingle('[eventKey=1]').assertSingle('.active'); + + // instance.setState({ activeKey: 2 }); + // instance.assertSingle('[eventKey=2]').assertSingle('.active'); + + // instance.setState({ animation: true }); + // instance.setState({ activeKey: 1 }); + // instance.assertSingle('[eventKey=2]').assertSingle('.active'); + // }); + + // xit('should handle simultaneous eventKey and activeKey change', () => { + // const instance = mount(); + + // instance.assertSingle('[eventKey=2]').assertSingle('.active'); + + // instance.setState({ eventKeys: [1, 3], activeKey: 3 }); + // // XXX I have no idea why this is needed but the test fails without it. + // instance.update(); + // instance.assertSingle('[eventKey=3]').assertSingle('.active'); + + // instance.setState({ eventKeys: [1, 4], activeKey: 4 }); + // // XXX I have no idea why this is needed but the test fails without it. + // instance.update(); + // instance.assertSingle('[eventKey=4]').assertSingle('.active'); + // }); + + // xit('should not get stuck if eventKey ceases to exist', () => { + // const instance = mount(); + + // instance.assertSingle('[eventKey=2]').assertSingle('.active'); + + // instance.setState({ eventKeys: [1, 3] }); + // instance.assertNone('.active'); + + // instance.setState({ activeKey: 3 }); + // instance.assertSingle('[eventKey=3]').assertSingle('.active'); + + // // Check that active state lingers after changing event key. + // instance.setState({ activeKey: 1 }); + // instance.assertSingle('[eventKey=3]').assertSingle('.active'); + + // // But once event key changes again, make sure active state switches. + // instance.setState({ eventKeys: [1, 2] }); + // // XXX I have no idea why this is needed but the test fails without it. + // instance.update(); + // instance.assertSingle('[eventKey=1]').assertSingle('.active'); + // }); + + // [ + // [ + // [1, 2], + // [2, 1], + // ], + // [ + // [2, 1], + // [1, 2], + // ], + // ].forEach(([order1, order2]) => { + // xit('should handle event key swaps', () => { + // const instance = mount(); + + // instance.assertSingle('[eventKey=1]').assertSingle('.active'); + + // instance.setState({ eventKeys: order2 }); + // instance.assertSingle('[eventKey=1]').assertSingle('.active'); + + // // Check that the animation is still wired up. + // instance.setState({ activeKey: 2 }); + // instance.assertSingle('[eventKey=1]').assertSingle('.active'); + // }); + // }); + // }); +}); diff --git a/fork/react-bootstrap/src/TabContent.js b/fork/react-bootstrap/src/TabContent.js deleted file mode 100644 index 8341d6771d6..00000000000 --- a/fork/react-bootstrap/src/TabContent.js +++ /dev/null @@ -1,176 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import elementType from 'prop-types-extra/lib/elementType'; - -import { - bsClass as setBsClass, - prefix, - splitBsPropsAndOmit, -} from './utils/bootstrapUtils'; - -const propTypes = { - componentClass: elementType, - - /** - * Sets a default animation strategy for all children ``s. Use - * `false` to disable, `true` to enable the default `` animation or - * a react-transition-group v2 `` component. - */ - animation: PropTypes.oneOfType([PropTypes.bool, elementType]), - - /** - * Wait until the first "enter" transition to mount tabs (add them to the DOM) - */ - mountOnEnter: PropTypes.bool, - - /** - * Unmount tabs (remove it from the DOM) when they are no longer visible - */ - unmountOnExit: PropTypes.bool, -}; - -const defaultProps = { - componentClass: 'div', - animation: true, - mountOnEnter: false, - unmountOnExit: false, -}; - -const contextTypes = { - $bs_tabContainer: PropTypes.shape({ - activeKey: PropTypes.any, - }), -}; - -const childContextTypes = { - $bs_tabContent: PropTypes.shape({ - bsClass: PropTypes.string, - animation: PropTypes.oneOfType([PropTypes.bool, elementType]), - activeKey: PropTypes.any, - mountOnEnter: PropTypes.bool, - unmountOnExit: PropTypes.bool, - onPaneEnter: PropTypes.func.isRequired, - onPaneExited: PropTypes.func.isRequired, - exiting: PropTypes.bool.isRequired, - }), -}; - -class TabContent extends React.Component { - constructor(props, context) { - super(props, context); - - this.handlePaneEnter = this.handlePaneEnter.bind(this); - this.handlePaneExited = this.handlePaneExited.bind(this); - - // Active entries in state will be `null` unless `animation` is set. Need - // to track active child in case keys swap and the active child changes - // but the active key does not. - this.state = { - activeKey: null, - activeChild: null, - }; - } - - getChildContext() { - const { bsClass, animation, mountOnEnter, unmountOnExit } = this.props; - - const stateActiveKey = this.state.activeKey; - const containerActiveKey = this.getContainerActiveKey(); - - const activeKey = - stateActiveKey != null ? stateActiveKey : containerActiveKey; - const exiting = - stateActiveKey != null && stateActiveKey !== containerActiveKey; - - return { - $bs_tabContent: { - bsClass, - animation, - activeKey, - mountOnEnter, - unmountOnExit, - onPaneEnter: this.handlePaneEnter, - onPaneExited: this.handlePaneExited, - exiting, - }, - }; - } - - componentDidUpdate(prevProps) { - if ( - this.props.animation !== prevProps.animation && - this.state.activeChild - ) { - this.setState({ activeKey: null, activeChild: null }); - } - } - - componentWillUnmount() { - this.isUnmounted = true; - } - - getContainerActiveKey() { - const tabContainer = this.context.$bs_tabContainer; - return tabContainer && tabContainer.activeKey; - } - - handlePaneEnter(child, childKey) { - if (!this.props.animation) { - return false; - } - - // It's possible that this child should be transitioning out. - if (childKey !== this.getContainerActiveKey()) { - return false; - } - - this.setState({ - activeKey: childKey, - activeChild: child, - }); - - return true; - } - - handlePaneExited(child) { - // This might happen as everything is unmounting. - if (this.isUnmounted) { - return; - } - - this.setState(({ activeChild }) => { - if (activeChild !== child) { - return null; - } - - return { - activeKey: null, - activeChild: null, - }; - }); - } - - render() { - const { componentClass: Component, className, ...props } = this.props; - const [bsProps, elementProps] = splitBsPropsAndOmit(props, [ - 'animation', - 'mountOnEnter', - 'unmountOnExit', - ]); - - return ( - - ); - } -} - -TabContent.propTypes = propTypes; -TabContent.defaultProps = defaultProps; -TabContent.contextTypes = contextTypes; -TabContent.childContextTypes = childContextTypes; - -export default setBsClass('tab', TabContent); diff --git a/fork/react-bootstrap/src/TabContent.jsx b/fork/react-bootstrap/src/TabContent.jsx new file mode 100644 index 00000000000..8770017b8a3 --- /dev/null +++ b/fork/react-bootstrap/src/TabContent.jsx @@ -0,0 +1,164 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import elementType from 'prop-types-extra/lib/elementType'; + +import { bsClass as setBsClass, prefix, splitBsPropsAndOmit } from './utils/bootstrapUtils'; + +const propTypes = { + componentClass: elementType, + + /** + * Sets a default animation strategy for all children ``s. Use + * `false` to disable, `true` to enable the default `` animation or + * a react-transition-group v2 `` component. + */ + animation: PropTypes.oneOfType([PropTypes.bool, elementType]), + + /** + * Wait until the first "enter" transition to mount tabs (add them to the DOM) + */ + mountOnEnter: PropTypes.bool, + + /** + * Unmount tabs (remove it from the DOM) when they are no longer visible + */ + unmountOnExit: PropTypes.bool, +}; + +const defaultProps = { + componentClass: 'div', + animation: true, + mountOnEnter: false, + unmountOnExit: false, +}; + +const contextTypes = { + $bs_tabContainer: PropTypes.shape({ + activeKey: PropTypes.any, + }), +}; + +const childContextTypes = { + $bs_tabContent: PropTypes.shape({ + bsClass: PropTypes.string, + animation: PropTypes.oneOfType([PropTypes.bool, elementType]), + activeKey: PropTypes.any, + mountOnEnter: PropTypes.bool, + unmountOnExit: PropTypes.bool, + onPaneEnter: PropTypes.func.isRequired, + onPaneExited: PropTypes.func.isRequired, + exiting: PropTypes.bool.isRequired, + }), +}; + +class TabContent extends React.Component { + constructor(props, context) { + super(props, context); + + this.handlePaneEnter = this.handlePaneEnter.bind(this); + this.handlePaneExited = this.handlePaneExited.bind(this); + + // Active entries in state will be `null` unless `animation` is set. Need + // to track active child in case keys swap and the active child changes + // but the active key does not. + this.state = { + activeKey: null, + activeChild: null, + }; + } + + getChildContext() { + const { bsClass, animation, mountOnEnter, unmountOnExit } = this.props; + + const stateActiveKey = this.state.activeKey; + const containerActiveKey = this.getContainerActiveKey(); + + const activeKey = stateActiveKey != null ? stateActiveKey : containerActiveKey; + const exiting = stateActiveKey != null && stateActiveKey !== containerActiveKey; + + return { + $bs_tabContent: { + bsClass, + animation, + activeKey, + mountOnEnter, + unmountOnExit, + onPaneEnter: this.handlePaneEnter, + onPaneExited: this.handlePaneExited, + exiting, + }, + }; + } + + componentDidUpdate(prevProps) { + if (this.props.animation !== prevProps.animation && this.state.activeChild) { + this.setState({ activeKey: null, activeChild: null }); + } + } + + componentWillUnmount() { + this.isUnmounted = true; + } + + getContainerActiveKey() { + const tabContainer = this.context.$bs_tabContainer; + return tabContainer && tabContainer.activeKey; + } + + handlePaneEnter(child, childKey) { + if (!this.props.animation) { + return false; + } + + // It's possible that this child should be transitioning out. + if (childKey !== this.getContainerActiveKey()) { + return false; + } + + this.setState({ + activeKey: childKey, + activeChild: child, + }); + + return true; + } + + handlePaneExited(child) { + // This might happen as everything is unmounting. + if (this.isUnmounted) { + return; + } + + this.setState(({ activeChild }) => { + if (activeChild !== child) { + return null; + } + + return { + activeKey: null, + activeChild: null, + }; + }); + } + + render() { + const { componentClass: Component, className, ...props } = this.props; + const [bsProps, elementProps] = splitBsPropsAndOmit(props, [ + 'animation', + 'mountOnEnter', + 'unmountOnExit', + ]); + + return ( + + ); + } +} + +TabContent.propTypes = propTypes; +TabContent.defaultProps = defaultProps; +TabContent.contextTypes = contextTypes; +TabContent.childContextTypes = childContextTypes; + +export default setBsClass('tab', TabContent); diff --git a/fork/react-bootstrap/src/TabPane.js b/fork/react-bootstrap/src/TabPane.js deleted file mode 100644 index e69bf15abaa..00000000000 --- a/fork/react-bootstrap/src/TabPane.js +++ /dev/null @@ -1,290 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import elementType from 'prop-types-extra/lib/elementType'; -import warning from 'warning'; - -import { - bsClass, - getClassSet, - prefix, - splitBsPropsAndOmit -} from './utils/bootstrapUtils'; -import createChainedFunction from './utils/createChainedFunction'; - -import Fade from './Fade'; - -const propTypes = { - /** - * Uniquely identify the `` among its siblings. - */ - eventKey: PropTypes.any, - - /** - * Use animation when showing or hiding ``s. Use `false` to disable, - * `true` to enable the default `` animation or - * a react-transition-group v2 `` component. - */ - animation: PropTypes.oneOfType([PropTypes.bool, elementType]), - - /** @private * */ - id: PropTypes.string, - - /** @private * */ - 'aria-labelledby': PropTypes.string, - - /** - * If not explicitly specified and rendered in the context of a - * ``, the `bsClass` of the `` suffixed by `-pane`. - * If otherwise not explicitly specified, `tab-pane`. - */ - bsClass: PropTypes.string, - - /** - * Transition onEnter callback when animation is not `false` - */ - onEnter: PropTypes.func, - - /** - * Transition onEntering callback when animation is not `false` - */ - onEntering: PropTypes.func, - - /** - * Transition onEntered callback when animation is not `false` - */ - onEntered: PropTypes.func, - - /** - * Transition onExit callback when animation is not `false` - */ - onExit: PropTypes.func, - - /** - * Transition onExiting callback when animation is not `false` - */ - onExiting: PropTypes.func, - - /** - * Transition onExited callback when animation is not `false` - */ - onExited: PropTypes.func, - - /** - * Wait until the first "enter" transition to mount the tab (add it to the DOM) - */ - mountOnEnter: PropTypes.bool, - - /** - * Unmount the tab (remove it from the DOM) when it is no longer visible - */ - unmountOnExit: PropTypes.bool -}; - -const contextTypes = { - $bs_tabContainer: PropTypes.shape({ - getTabId: PropTypes.func, - getPaneId: PropTypes.func - }), - $bs_tabContent: PropTypes.shape({ - bsClass: PropTypes.string, - animation: PropTypes.oneOfType([PropTypes.bool, elementType]), - activeKey: PropTypes.any, - mountOnEnter: PropTypes.bool, - unmountOnExit: PropTypes.bool, - onPaneEnter: PropTypes.func.isRequired, - onPaneExited: PropTypes.func.isRequired, - exiting: PropTypes.bool.isRequired - }) -}; - -/** - * We override the `` context so `