├── .dumirc.ts ├── .editorconfig ├── .eslintrc.js ├── .fatherrc.ts ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── codeql.yml │ └── react-component-ci.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── assets ├── bootstrap.less ├── bootstrap │ ├── Dialog.less │ ├── effect.less │ └── variables.less ├── index.less └── index │ ├── Dialog.less │ └── Mask.less ├── bunfig.toml ├── docs ├── changelog.md ├── demo │ ├── ant-design.md │ ├── bootstrap.md │ ├── draggable.md │ ├── multiple-Portal.md │ └── pure.md ├── examples │ ├── ant-design.tsx │ ├── bootstrap.tsx │ ├── draggable.tsx │ ├── multiple-Portal.tsx │ └── pure.tsx └── index.md ├── jest.config.js ├── now.json ├── package.json ├── script └── update-content.js ├── src ├── Dialog │ ├── Content │ │ ├── MemoChildren.tsx │ │ ├── Panel.tsx │ │ └── index.tsx │ ├── Mask.tsx │ └── index.tsx ├── DialogWrap.tsx ├── IDialogPropTypes.tsx ├── context.ts ├── index.ts └── util.ts ├── tests ├── __snapshots__ │ └── index.spec.tsx.snap ├── index.spec.tsx ├── portal.spec.tsx ├── ref.spec.tsx ├── scroll.spec.tsx ├── setup.js ├── setupFilesAfterEnv.ts └── util.spec.tsx ├── tsconfig.json └── typings.d.ts /.dumirc.ts: -------------------------------------------------------------------------------- 1 | import os from 'node:os'; 2 | import { defineConfig } from 'dumi'; 3 | import path from 'path'; 4 | 5 | export default defineConfig({ 6 | alias: { 7 | 'rc-dialog$': path.resolve('src'), 8 | 'rc-dialog/es': path.resolve('src'), 9 | }, 10 | favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'], 11 | themeConfig: { 12 | name: 'Dialog', 13 | logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', 14 | }, 15 | mako: ['Darwin', 'Linux'].includes(os.type()) ? {} : false, 16 | });; 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const base = require("@umijs/fabric/dist/eslint"); 2 | 3 | module.exports = { 4 | ...base, 5 | rules: { 6 | ...base.rules, 7 | "arrow-parens": 0, 8 | "react/no-array-index-key": 0, 9 | "react/sort-comp": 0, 10 | "@typescript-eslint/no-explicit-any": 0, 11 | "@typescript-eslint/no-empty-interface": 0, 12 | "@typescript-eslint/no-inferrable-types": 0, 13 | "react/require-default-props": 0, 14 | "no-confusing-arrow": 0, 15 | "import/no-named-as-default-member": 0, 16 | "jsx-a11y/label-has-for": 0, 17 | "jsx-a11y/label-has-associated-control": 0, 18 | "import/no-extraneous-dependencies": 0, 19 | "jsx-a11y/no-noninteractive-tabindex": 0, 20 | "jsx-a11y/no-autofocus": 0, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | plugins: ['@rc-component/father-plugin'], 5 | }); 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ant-design # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: ant-design # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "21:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: rc-select 11 | versions: 12 | - 12.1.2 13 | - 12.1.3 14 | - 12.1.5 15 | - 12.1.6 16 | - 12.1.7 17 | - 12.1.9 18 | - dependency-name: "@types/react" 19 | versions: 20 | - 17.0.0 21 | - 17.0.1 22 | - 17.0.2 23 | - 17.0.3 24 | - dependency-name: "@types/react-dom" 25 | versions: 26 | - 17.0.0 27 | - 17.0.1 28 | - 17.0.2 29 | - dependency-name: rc-drawer 30 | versions: 31 | - 4.2.2 32 | - 4.3.0 33 | - dependency-name: less 34 | versions: 35 | - 4.1.0 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "57 7 * * 2" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/react-component-ci.yml: -------------------------------------------------------------------------------- 1 | name: ✅ test 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | uses: react-component/rc-test/.github/workflows/test.yml@main 6 | secrets: inherit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.log 3 | *.log.* 4 | .idea 5 | .ipr 6 | .iws 7 | *~ 8 | ~* 9 | *.diff 10 | *.patch 11 | *.bak 12 | .DS_Store 13 | Thumbs.db 14 | .project 15 | .*proj 16 | .svn 17 | *.swp 18 | *.swo 19 | *.pyc 20 | *.pyo 21 | node_modules 22 | .cache 23 | *.css 24 | build 25 | dist 26 | lib 27 | es 28 | coverage 29 | *.jsx 30 | *.map 31 | !tests/index.js 32 | !/index*.js 33 | /ios/ 34 | /android/ 35 | yarn.lock 36 | package-lock.json 37 | pnpm-lock.yaml 38 | .storybook 39 | .doc 40 | 41 | # umi 42 | .umi 43 | .umi-production 44 | .umi-test 45 | .env.local 46 | 47 | 48 | # dumi 49 | .dumi/tmp 50 | .dumi/tmp-production 51 | 52 | bun.lockb 53 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "proseWrap": "never", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 8.5.1 4 | 5 | `2021-01-07` 6 | 7 | - improve: ScrollLocker call related. [#227](https://github.com/react-component/dialog/pull/227) 8 | 9 | ## 8.5.0 10 | 11 | `2021-01-04` 12 | 13 | - refactor: use ScrollLocker. [#220](https://github.com/react-component/dialog/pull/220) 14 | 15 | ## 8.4.6 16 | 17 | `2020-12-19` 18 | 19 | - fix: trigger close only when click wrap itself. [#218](https://github.com/react-component/dialog/pull/218) 20 | 21 | ## 8.4.5 22 | 23 | `2020-12-07` 24 | 25 | - fix: Dialog should keep refresh when forceRender provided. [#217](https://github.com/react-component/dialog/pull/217) 26 | 27 | ## 8.4.4 28 | 29 | `2020-12-03` 30 | 31 | - fix: dialog dont close when mouseDown in content and mouseUp in wrapper. [#210](https://github.com/react-component/dialog/pull/210) 32 | 33 | - chore: Should not re-render when dialog is invisible. [#212](https://github.com/react-component/dialog/pull/212) 34 | 35 | 36 | ## 8.4.3 37 | 38 | `2020-10-21` 39 | 40 | - chore: support react 17. [#207](https://github.com/react-component/dialog/pull/207) 41 | 42 | ## 8.4.2 43 | 44 | `2020-10-14` 45 | 46 | - fix: Dialog should not auto destroy. [#206](https://github.com/react-component/dialog/pull/206) 47 | 48 | ## 8.4.1 49 | 50 | `2020-10-11` 51 | 52 | - fix: Portal event bubble. [#204](https://github.com/react-component/dialog/pull/204) 53 | 54 | ## 8.4.0 55 | 56 | `2020-09-29` 57 | 58 | - refactor: Use `rc-motion`. [#203](https://github.com/react-component/dialog/pull/203) 59 | 60 | ## 8.3.4 (8.2.2, 8.1.2) / 2020-09-04 61 | - fix: prevent scroll behavior when focus trigger. [ant-design/ant-design#26582](https://github.com/ant-design/ant-design/issues/26582) 62 | 63 | ## 8.3.3 / 2020-09-02 64 | - fix: page scroll position will jump after dialog is closed. [#202](https://github.com/react-component/dialog/pull/202) 65 | 66 | ## 8.3.2 / 2020-09-02 67 | - fix: remove typing from package.json. [#201](https://github.com/react-component/dialog/pull/201) 68 | 69 | ## 8.3.1 / 2020-09-02 70 | - fix: add displayName. [#200](https://github.com/react-component/dialog/pull/200) 71 | 72 | ## 8.3.0 / 2020-08-31 73 | - fate: add `modalRender`. [#195](https://github.com/react-component/dialog/pull/195) 74 | 75 | ## 8.2.0 / 2020-08-27 76 | - use `father`. [#197](https://github.com/react-component/dialog/pull/197) 77 | 78 | ## 8.1.1 / 2020-08-19 79 | 80 | - Fix dialog component will only show mask, if initialize a Dialog component with both forceRender and visible are true. [#194](https://github.com/react-component/dialog/pull/194) 81 | 82 | ## 8.1.0 / 2020-07-09 83 | 84 | - remove babel runtime. 85 | - up `rc-drawer` to `4.1.0`. 86 | 87 | ## 8.0.0 / 2020-05-29 88 | 89 | - upgrade `rc-util` to `5.0.0`. 90 | 91 | ## 7.7.0 / 2020-05-05 92 | 93 | - upgrade `rc-animate` to `3.0.0`. 94 | 95 | ## 7.4.0 / 2019-05-10 96 | 97 | - Update accessibility. 98 | 99 | ## 7.3.0 / 2018-12-06 100 | 101 | - Support `forceRender` for dialog. 102 | 103 | ## 7.2.0 / 2018-07-30 104 | 105 | - Add closeIcon. [#89](*https://github.com/react-component/dialog/pull/89) [@HeskeyBaozi ](https://github.com/HeskeyBaozi) 106 | 107 | ## 7.1.0 / 2017-12-28 108 | 109 | - Add destroyOnClose. [#72](https://github.com/react-component/dialog/pull/72) [@Rohanhacker](https://github.com/Rohanhacker) 110 | 111 | ## 7.0.0 / 2017-11-02 112 | 113 | 114 | - Remove ReactNative support, please use https://github.com/react-component/m-dialog instead. 115 | - Support React 16. 116 | 117 | Notable change: Close animation won't trigger when dialog unmounting after React 16, see [facebook/react#10826](https://github.com/facebook/react/issues/10826) 118 | 119 | ## 6.5.11 / 2017-8-21 120 | 121 | - fixed: RN modal support landscape orientation, https://github.com/react-component/dialog/pull/64 122 | 123 | ## 6.5.0 / 2016-10-25 124 | 125 | - remove rc-dialog/lib/Modal's entry prop, add animationType prop 126 | 127 | ## 6.4.0 / 2016-09-19 128 | 129 | - add rc-dialog/lib/Modal to support react-native 130 | 131 | ## 6.2.0 / 2016-07-18 132 | 133 | - use getContainerRenderMixin from 'rc-util' 134 | 135 | ## 6.0.0 / 2016-03-18 136 | 137 | - new html structure and class 138 | - disable window scroll when show 139 | 140 | ## 5.4.0 / 2016-02-27 141 | 142 | - add maskClosable 143 | 144 | ## 5.3.0 / 2015-11-23 145 | 146 | - separate close and header 147 | 148 | ## 5.1.0 / 2015-10-20 149 | 150 | - only support react 0.14 151 | 152 | ## 5.0.0 / 2015-08-17 153 | 154 | - refactor to clean api. remove onShow onBeforeClose 155 | 156 | ## 4.5.0 / 2015-07-23 157 | 158 | use rc-animate & rc-align 159 | 160 | ## 4.4.0 / 2015-07-03 161 | 162 | support esc to close 163 | 164 | ## 4.2.0 / 2015-06-09 165 | 166 | add renderToBody props 167 | 168 | ## 4.0.0 / 2015-04-28 169 | 170 | make dialog render to body and use [dom-align](https://github.com/yiminghe/dom-align) to align 171 | 172 | ## 3.0.0 / 2015-03-17 173 | 174 | support es6 and react 0.13 175 | 176 | ## 2.1.0 / 2015-03-05 177 | 178 | `new` [#3](https://github.com/react-component/dialog/issues/3) support closable requestClose onBeforeClose 179 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present yiminghe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rc-dialog 2 | 3 | react dialog component 4 | 5 | [![NPM version][npm-image]][npm-url] [![dumi](https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square)](https://github.com/umijs/dumi) [![build status][github-actions-image]][github-actions-url] [![Test coverage][codecov-image]][codecov-url] [![npm download][download-image]][download-url] [![bundle size][bundlephobia-image]][bundlephobia-url] 6 | 7 | [npm-image]: http://img.shields.io/npm/v/rc-dialog.svg?style=flat-square 8 | [npm-url]: http://npmjs.org/package/rc-dialog 9 | [github-actions-image]: https://github.com/react-component/dialog/workflows/CI/badge.svg 10 | [github-actions-url]: https://github.com/react-component/dialog/actions 11 | [circleci-image]: https://img.shields.io/circleci/react-component/dialog/master?style=flat-square 12 | [circleci-url]: https://circleci.com/gh/react-component/dialog 13 | [codecov-image]: https://img.shields.io/codecov/c/github/react-component/dialog/master.svg?style=flat-square 14 | [codecov-url]: https://app.codecov.io/gh/react-component/dialog 15 | [download-image]: https://img.shields.io/npm/dm/rc-dialog.svg?style=flat-square 16 | [download-url]: https://npmjs.org/package/rc-dialog 17 | [bundlephobia-url]: https://bundlephobia.com/result?p=rc-dialog 18 | [bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-dialog 19 | 20 | ## Screenshot 21 | 22 | 23 | 24 | ## Example 25 | 26 | http://localhost:8007/examples/ 27 | 28 | online example: https://dialog.react-component.vercel.app/ 29 | 30 | ## Install 31 | 32 | [![rc-dialog](https://nodei.co/npm/rc-dialog.png)](https://npmjs.org/package/rc-dialog) 33 | 34 | ## Usage 35 | 36 | ```js 37 | var Dialog = require('rc-dialog'); 38 | 39 | ReactDOM.render( 40 | 41 |

first dialog

42 |
43 | ), document.getElementById('t1')); 44 | 45 | // use dialog 46 | ``` 47 | 48 | ## API 49 | 50 | ### rc-dialog 51 | 52 | | Name | Type | Default | Description | Version | 53 | | --- | --- | --- | --- | --- | 54 | | prefixCls | String | rc-dialog | The dialog dom node's prefixCls | | 55 | | className | String | | additional className for dialog | | 56 | | classNames | { header?: string; body?: string; footer?: string; mask?: string; content?: string; wrapper?: string; } | | pass className to target area | | 57 | | styles | { header?: CSSProperties; body?: CSSProperties; footer?: CSSProperties; mask?: CSSProperties; content?: CSSProperties; wrapper?: CSSProperties; } | | pass styles to target area | | 58 | | style | Object | {} | Root style for dialog element.Such as width, height | | 59 | | zIndex | Number | | | | 60 | | visible | Boolean | false | current dialog's visible status | | 61 | | animation | String | | part of dialog animation css class name | | 62 | | maskAnimation | String | | part of dialog's mask animation css class name | | 63 | | transitionName | String | | dialog animation css class name | | 64 | | maskTransitionName | String | | mask animation css class name | | 65 | | title | String\|React.Element | | Title of the dialog | | 66 | | footer | React.Element | | footer of the dialog | | 67 | | closable | Boolean \| ({ closeIcon?: React.ReactNode; disabled?: boolean } & React.AriaAttributes | true | whether show close button | | 68 | | mask | Boolean | true | whether show mask | | 69 | | maskClosable | Boolean | true | whether click mask to close | | 70 | | keyboard | Boolean | true | whether support press esc to close | | 71 | | mousePosition | {x:number,y:number} | | set pageX and pageY of current mouse(it will cause transform origin to be set). | | 72 | | onClose | function() | | called when click close button or mask | | 73 | | afterClose | function() | | called when close animation end | | 74 | | getContainer | function(): HTMLElement | | to determine where Dialog will be mounted | | 75 | | destroyOnHidden | Boolean | false | to unmount child compenents on onClose | | 76 | | closeIcon | ReactNode | | specific the close icon. | | 77 | | forceRender | Boolean | false | Create dialog dom node before dialog first show | | 78 | | focusTriggerAfterClose | Boolean | true | focus trigger element when dialog closed | | 79 | | modalRender | (node: ReactNode) => ReactNode | | Custom modal content render | 8.3.0 | 80 | 81 | ## Development 82 | 83 | ``` 84 | npm install 85 | npm start 86 | ``` 87 | 88 | ## Test Case 89 | 90 | ``` 91 | npm test 92 | npm run chrome-test 93 | ``` 94 | 95 | ## Coverage 96 | 97 | ``` 98 | npm run coverage 99 | ``` 100 | 101 | open coverage/ dir 102 | 103 | ## License 104 | 105 | rc-dialog is released under the MIT license. 106 | 107 | 108 | ## 🤝 Contributing 109 | 110 | 111 | Contribution Leaderboard 112 | 113 | -------------------------------------------------------------------------------- /assets/bootstrap.less: -------------------------------------------------------------------------------- 1 | @prefixCls: rc-dialog; 2 | @import "./bootstrap/Dialog.less"; 3 | @import "./index/Mask.less"; 4 | -------------------------------------------------------------------------------- /assets/bootstrap/Dialog.less: -------------------------------------------------------------------------------- 1 | @import "./variables.less"; 2 | 3 | .clearfix() { 4 | &:before, 5 | &:after { 6 | content: " "; // 1 7 | display: table; // 2 8 | } 9 | &:after { 10 | clear: both; 11 | } 12 | } 13 | 14 | .@{prefixCls} { 15 | // Container that the rc-dialog scrolls within 16 | &-wrap { 17 | position: fixed; 18 | overflow: auto; 19 | top: 0; 20 | right: 0; 21 | bottom: 0; 22 | left: 0; 23 | z-index: @zindex-modal; 24 | -webkit-overflow-scrolling: touch; 25 | 26 | // Prevent Chrome on Windows from adding a focus outline. For details, see 27 | // https://github.com/twbs/bootstrap/pull/10951. 28 | outline: 0; 29 | } 30 | 31 | // Shell div to position the rc-dialog with bottom padding 32 | position: relative; 33 | width: auto; 34 | margin: 10px; 35 | 36 | // Actual rc-dialog 37 | &-section { 38 | position: relative; 39 | background-color: @modal-section-bg; 40 | border: 1px solid @modal-section-fallback-border-color; //old browsers fallback (ie8 etc) 41 | border: 1px solid @modal-section-border-color; 42 | border-radius: @border-radius-large; 43 | box-shadow: 0 3px 9px rgba(0, 0, 0, .5); 44 | background-clip: padding-box; 45 | // Remove focus outline from opened rc-dialog 46 | outline: 0; 47 | } 48 | 49 | // Modal header 50 | // Top section of the rc-dialog w/ title and dismiss 51 | &-header { 52 | padding: @modal-title-padding; 53 | border-bottom: 1px solid @modal-header-border-color; 54 | &:extend(.clearfix all); 55 | } 56 | 57 | &-close { 58 | cursor: pointer; 59 | border: 0; 60 | background: transparent; 61 | font-size: 21px; 62 | position: absolute; 63 | right: 20px; 64 | top: 12px; 65 | font-weight: 700; 66 | line-height: 1; 67 | color: #000; 68 | text-shadow: 0 1px 0 #fff; 69 | filter: alpha(opacity=20); 70 | opacity: .2; 71 | text-decoration: none; 72 | 73 | &-x:after { 74 | content: '×' 75 | } 76 | 77 | &:hover { 78 | opacity: 1; 79 | filter: alpha(opacity=100); 80 | text-decoration: none; 81 | } 82 | } 83 | 84 | // Title text within header 85 | &-title { 86 | margin: 0; 87 | line-height: @modal-title-line-height; 88 | } 89 | 90 | // Modal body 91 | // Where all rc-dialog content resides (sibling of &-header and &-footer) 92 | &-body { 93 | position: relative; 94 | padding: @modal-inner-padding; 95 | } 96 | 97 | // Footer (for actions) 98 | &-footer { 99 | padding: @modal-inner-padding; 100 | text-align: right; // right align buttons 101 | border-top: 1px solid @modal-footer-border-color; 102 | &:extend(.clearfix all); // clear it in case folks use .pull-* classes on buttons 103 | 104 | // Properly space out buttons 105 | .btn + .btn { 106 | margin-left: 5px; 107 | margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs 108 | } 109 | // but override that for button groups 110 | .btn-group .btn + .btn { 111 | margin-left: -1px; 112 | } 113 | // and override it for block buttons as well 114 | .btn-block + .btn-block { 115 | margin-left: 0; 116 | } 117 | } 118 | } 119 | 120 | // Scale up the rc-dialog 121 | @media (min-width: @screen-sm-min) { 122 | .@{prefixCls} { 123 | // Automatically set rc-dialog's width for larger viewports 124 | width: @modal-md; 125 | margin: 30px auto; 126 | 127 | &-section { 128 | box-shadow: 0 5px 15px rgba(0, 0, 0, .5); 129 | } 130 | } 131 | } 132 | 133 | @import "./effect.less"; 134 | -------------------------------------------------------------------------------- /assets/bootstrap/effect.less: -------------------------------------------------------------------------------- 1 | .@{prefixCls} { 2 | &-slide-fade-enter, 3 | &-slide-fade-appear { 4 | transform: translate(0, -25%); 5 | } 6 | 7 | &-slide-fade-enter, 8 | &-slide-fade-appear, 9 | &-slide-fade-leave { 10 | animation-duration: .3s; 11 | animation-fill-mode: both; 12 | animation-timing-function: ease-out; 13 | animation-play-state: paused; 14 | } 15 | 16 | &-slide-fade-enter&-slide-fade-enter-active, &-slide-fade-appear&-slide-fade-appear-active { 17 | animation-name: rcDialogSlideFadeIn; 18 | animation-play-state: running; 19 | } 20 | 21 | &-slide-fade-leave&-slide-fade-leave-active { 22 | animation-name: rcDialogSlideFadeOut; 23 | animation-play-state: running; 24 | } 25 | 26 | @keyframes rcDialogSlideFadeIn { 27 | 0% { 28 | transform: translate(0, -25%); 29 | } 30 | 100% { 31 | transform: translate(0, 0); 32 | } 33 | } 34 | @keyframes rcDialogSlideFadeOut { 35 | 0% { 36 | transform: translate(0, 0); 37 | } 38 | 100% { 39 | transform: translate(0, -25%); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /assets/bootstrap/variables.less: -------------------------------------------------------------------------------- 1 | // 2 | // Variables 3 | // -------------------------------------------------- 4 | 5 | 6 | //== Colors 7 | // 8 | //## Gray and brand colors for use across Bootstrap. 9 | 10 | @gray-base: #000; 11 | @gray-darker: lighten(@gray-base, 13.5%); // #222 12 | @gray-dark: lighten(@gray-base, 20%); // #333 13 | @gray: lighten(@gray-base, 33.5%); // #555 14 | @gray-light: lighten(@gray-base, 46.7%); // #777 15 | @gray-lighter: lighten(@gray-base, 93.5%); // #eee 16 | 17 | @brand-primary: darken(#428bca, 6.5%); // #337ab7 18 | @brand-success: #5cb85c; 19 | @brand-info: #5bc0de; 20 | @brand-warning: #f0ad4e; 21 | @brand-danger: #d9534f; 22 | 23 | 24 | //== Scaffolding 25 | // 26 | //## Settings for some of the most global styles. 27 | 28 | //** Background color for ``. 29 | @body-bg: #fff; 30 | //** Global text color on ``. 31 | @text-color: @gray-dark; 32 | 33 | //** Global textual link color. 34 | @link-color: @brand-primary; 35 | //** Link hover color set via `darken()` function. 36 | @link-hover-color: darken(@link-color, 15%); 37 | //** Link hover decoration. 38 | @link-hover-decoration: underline; 39 | 40 | 41 | //== Typography 42 | // 43 | //## Font, line-height, and color for body text, headings, and more. 44 | 45 | @font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; 46 | @font-family-serif: Georgia, "Times New Roman", Times, serif; 47 | //** Default monospace fonts for ``, ``, and `
`.
 48 | @font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
 49 | @font-family-base:        @font-family-sans-serif;
 50 | 
 51 | @font-size-base:          14px;
 52 | @font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
 53 | @font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
 54 | 
 55 | @font-size-h1:            floor((@font-size-base * 2.6)); // ~36px
 56 | @font-size-h2:            floor((@font-size-base * 2.15)); // ~30px
 57 | @font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px
 58 | @font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px
 59 | @font-size-h5:            @font-size-base;
 60 | @font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
 61 | 
 62 | //** Unit-less `line-height` for use in components like buttons.
 63 | @line-height-base:        1.428571429; // 20/14
 64 | //** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
 65 | @line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
 66 | 
 67 | //** By default, this inherits from the ``.
 68 | @headings-font-family:    inherit;
 69 | @headings-font-weight:    500;
 70 | @headings-line-height:    1.1;
 71 | @headings-color:          inherit;
 72 | 
 73 | 
 74 | //== Iconography
 75 | //
 76 | //## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
 77 | 
 78 | //** Load fonts from this directory.
 79 | @icon-font-path:          "../fonts/";
 80 | //** File name for all font files.
 81 | @icon-font-name:          "glyphicons-halflings-regular";
 82 | //** Element ID within SVG icon file.
 83 | @icon-font-svg-id:        "glyphicons_halflingsregular";
 84 | 
 85 | 
 86 | //== Components
 87 | //
 88 | //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
 89 | 
 90 | @padding-base-vertical:     6px;
 91 | @padding-base-horizontal:   12px;
 92 | 
 93 | @padding-large-vertical:    10px;
 94 | @padding-large-horizontal:  16px;
 95 | 
 96 | @padding-small-vertical:    5px;
 97 | @padding-small-horizontal:  10px;
 98 | 
 99 | @padding-xs-vertical:       1px;
100 | @padding-xs-horizontal:     5px;
101 | 
102 | @line-height-large:         1.3333333; // extra decimals for Win 8.1 Chrome
103 | @line-height-small:         1.5;
104 | 
105 | @border-radius-base:        4px;
106 | @border-radius-large:       6px;
107 | @border-radius-small:       3px;
108 | 
109 | //** Global color for active items (e.g., navs or dropdowns).
110 | @component-active-color:    #fff;
111 | //** Global background color for active items (e.g., navs or dropdowns).
112 | @component-active-bg:       @brand-primary;
113 | 
114 | //** Width of the `border` for generating carets that indicate dropdowns.
115 | @caret-width-base:          4px;
116 | //** Carets increase slightly in size for larger components.
117 | @caret-width-large:         5px;
118 | 
119 | 
120 | //== Tables
121 | //
122 | //## Customizes the `.table` component with basic values, each used across all table variations.
123 | 
124 | //** Padding for ``s and ``s.
125 | @table-cell-padding:            8px;
126 | //** Padding for cells in `.table-condensed`.
127 | @table-condensed-cell-padding:  5px;
128 | 
129 | //** Default background color used for all tables.
130 | @table-bg:                      transparent;
131 | //** Background color used for `.table-striped`.
132 | @table-bg-accent:               #f9f9f9;
133 | //** Background color used for `.table-hover`.
134 | @table-bg-hover:                #f5f5f5;
135 | @table-bg-active:               @table-bg-hover;
136 | 
137 | //** Border color for table and cell borders.
138 | @table-border-color:            #ddd;
139 | 
140 | 
141 | //== Buttons
142 | //
143 | //## For each of Bootstrap's buttons, define text, background and border color.
144 | 
145 | @btn-font-weight:                normal;
146 | 
147 | @btn-default-color:              #333;
148 | @btn-default-bg:                 #fff;
149 | @btn-default-border:             #ccc;
150 | 
151 | @btn-primary-color:              #fff;
152 | @btn-primary-bg:                 @brand-primary;
153 | @btn-primary-border:             darken(@btn-primary-bg, 5%);
154 | 
155 | @btn-success-color:              #fff;
156 | @btn-success-bg:                 @brand-success;
157 | @btn-success-border:             darken(@btn-success-bg, 5%);
158 | 
159 | @btn-info-color:                 #fff;
160 | @btn-info-bg:                    @brand-info;
161 | @btn-info-border:                darken(@btn-info-bg, 5%);
162 | 
163 | @btn-warning-color:              #fff;
164 | @btn-warning-bg:                 @brand-warning;
165 | @btn-warning-border:             darken(@btn-warning-bg, 5%);
166 | 
167 | @btn-danger-color:               #fff;
168 | @btn-danger-bg:                  @brand-danger;
169 | @btn-danger-border:              darken(@btn-danger-bg, 5%);
170 | 
171 | @btn-link-disabled-color:        @gray-light;
172 | 
173 | // Allows for customizing button radius independently from global border radius
174 | @btn-border-radius-base:         @border-radius-base;
175 | @btn-border-radius-large:        @border-radius-large;
176 | @btn-border-radius-small:        @border-radius-small;
177 | 
178 | 
179 | //== Forms
180 | //
181 | //##
182 | 
183 | //** `` background color
184 | @input-bg:                       #fff;
185 | //** `` background color
186 | @input-bg-disabled:              @gray-lighter;
187 | 
188 | //** Text color for ``s
189 | @input-color:                    @gray;
190 | //** `` border color
191 | @input-border:                   #ccc;
192 | 
193 | // TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
194 | //** Default `.form-control` border radius
195 | // This has no effect on ``s in CSS.
196 | @input-border-radius:            @border-radius-base;
197 | //** Large `.form-control` border radius
198 | @input-border-radius-large:      @border-radius-large;
199 | //** Small `.form-control` border radius
200 | @input-border-radius-small:      @border-radius-small;
201 | 
202 | //** Border color for inputs on focus
203 | @input-border-focus:             #66afe9;
204 | 
205 | //** Placeholder text color
206 | @input-color-placeholder:        #999;
207 | 
208 | //** Default `.form-control` height
209 | @input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
210 | //** Large `.form-control` height
211 | @input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
212 | //** Small `.form-control` height
213 | @input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
214 | 
215 | //** `.form-group` margin
216 | @form-group-margin-bottom:       15px;
217 | 
218 | @legend-color:                   @gray-dark;
219 | @legend-border-color:            #e5e5e5;
220 | 
221 | //** Background color for textual input addons
222 | @input-group-addon-bg:           @gray-lighter;
223 | //** Border color for textual input addons
224 | @input-group-addon-border-color: @input-border;
225 | 
226 | //** Disabled cursor for form controls and buttons.
227 | @cursor-disabled:                not-allowed;
228 | 
229 | 
230 | //== Dropdowns
231 | //
232 | //## Dropdown menu container and contents.
233 | 
234 | //** Background for the dropdown menu.
235 | @dropdown-bg:                    #fff;
236 | //** Dropdown menu `border-color`.
237 | @dropdown-border:                rgba(0,0,0,.15);
238 | //** Dropdown menu `border-color` **for IE8**.
239 | @dropdown-fallback-border:       #ccc;
240 | //** Divider color for between dropdown items.
241 | @dropdown-divider-bg:            #e5e5e5;
242 | 
243 | //** Dropdown link text color.
244 | @dropdown-link-color:            @gray-dark;
245 | //** Hover color for dropdown links.
246 | @dropdown-link-hover-color:      darken(@gray-dark, 5%);
247 | //** Hover background for dropdown links.
248 | @dropdown-link-hover-bg:         #f5f5f5;
249 | 
250 | //** Active dropdown menu item text color.
251 | @dropdown-link-active-color:     @component-active-color;
252 | //** Active dropdown menu item background color.
253 | @dropdown-link-active-bg:        @component-active-bg;
254 | 
255 | //** Disabled dropdown menu item background color.
256 | @dropdown-link-disabled-color:   @gray-light;
257 | 
258 | //** Text color for headers within dropdown menus.
259 | @dropdown-header-color:          @gray-light;
260 | 
261 | //** Deprecated `@dropdown-caret-color` as of v3.1.0
262 | @dropdown-caret-color:           #000;
263 | 
264 | 
265 | //-- Z-index master list
266 | //
267 | // Warning: Avoid customizing these values. They're used for a bird's eye view
268 | // of components dependent on the z-axis and are designed to all work together.
269 | //
270 | // Note: These variables are not generated into the Customizer.
271 | 
272 | @zindex-navbar:            1000;
273 | @zindex-dropdown:          1000;
274 | @zindex-popover:           1060;
275 | @zindex-tooltip:           1070;
276 | @zindex-navbar-fixed:      1030;
277 | @zindex-modal-background:  1040;
278 | @zindex-modal:             1050;
279 | 
280 | 
281 | //== Media queries breakpoints
282 | //
283 | //## Define the breakpoints at which your layout will change, adapting to different screen sizes.
284 | 
285 | // Extra small screen / phone
286 | //** Deprecated `@screen-xs` as of v3.0.1
287 | @screen-xs:                  480px;
288 | //** Deprecated `@screen-xs-min` as of v3.2.0
289 | @screen-xs-min:              @screen-xs;
290 | //** Deprecated `@screen-phone` as of v3.0.1
291 | @screen-phone:               @screen-xs-min;
292 | 
293 | // Small screen / tablet
294 | //** Deprecated `@screen-sm` as of v3.0.1
295 | @screen-sm:                  768px;
296 | @screen-sm-min:              @screen-sm;
297 | //** Deprecated `@screen-tablet` as of v3.0.1
298 | @screen-tablet:              @screen-sm-min;
299 | 
300 | // Medium screen / desktop
301 | //** Deprecated `@screen-md` as of v3.0.1
302 | @screen-md:                  992px;
303 | @screen-md-min:              @screen-md;
304 | //** Deprecated `@screen-desktop` as of v3.0.1
305 | @screen-desktop:             @screen-md-min;
306 | 
307 | // Large screen / wide desktop
308 | //** Deprecated `@screen-lg` as of v3.0.1
309 | @screen-lg:                  1200px;
310 | @screen-lg-min:              @screen-lg;
311 | //** Deprecated `@screen-lg-desktop` as of v3.0.1
312 | @screen-lg-desktop:          @screen-lg-min;
313 | 
314 | // So media queries don't overlap when required, provide a maximum
315 | @screen-xs-max:              (@screen-sm-min - 1);
316 | @screen-sm-max:              (@screen-md-min - 1);
317 | @screen-md-max:              (@screen-lg-min - 1);
318 | 
319 | 
320 | //== Grid system
321 | //
322 | //## Define your custom responsive grid.
323 | 
324 | //** Number of columns in the grid.
325 | @grid-columns:              12;
326 | //** Padding between columns. Gets divided in half for the left and right.
327 | @grid-gutter-width:         30px;
328 | // Navbar collapse
329 | //** Point at which the navbar becomes uncollapsed.
330 | @grid-float-breakpoint:     @screen-sm-min;
331 | //** Point at which the navbar begins collapsing.
332 | @grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
333 | 
334 | 
335 | //== Container sizes
336 | //
337 | //## Define the maximum width of `.container` for different screen sizes.
338 | 
339 | // Small screen / tablet
340 | @container-tablet:             (720px + @grid-gutter-width);
341 | //** For `@screen-sm-min` and up.
342 | @container-sm:                 @container-tablet;
343 | 
344 | // Medium screen / desktop
345 | @container-desktop:            (940px + @grid-gutter-width);
346 | //** For `@screen-md-min` and up.
347 | @container-md:                 @container-desktop;
348 | 
349 | // Large screen / wide desktop
350 | @container-large-desktop:      (1140px + @grid-gutter-width);
351 | //** For `@screen-lg-min` and up.
352 | @container-lg:                 @container-large-desktop;
353 | 
354 | 
355 | //== Navbar
356 | //
357 | //##
358 | 
359 | // Basics of a navbar
360 | @navbar-height:                    50px;
361 | @navbar-margin-bottom:             @line-height-computed;
362 | @navbar-border-radius:             @border-radius-base;
363 | @navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
364 | @navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
365 | @navbar-collapse-max-height:       340px;
366 | 
367 | @navbar-default-color:             #777;
368 | @navbar-default-bg:                #f8f8f8;
369 | @navbar-default-border:            darken(@navbar-default-bg, 6.5%);
370 | 
371 | // Navbar links
372 | @navbar-default-link-color:                #777;
373 | @navbar-default-link-hover-color:          #333;
374 | @navbar-default-link-hover-bg:             transparent;
375 | @navbar-default-link-active-color:         #555;
376 | @navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);
377 | @navbar-default-link-disabled-color:       #ccc;
378 | @navbar-default-link-disabled-bg:          transparent;
379 | 
380 | // Navbar brand label
381 | @navbar-default-brand-color:               @navbar-default-link-color;
382 | @navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);
383 | @navbar-default-brand-hover-bg:            transparent;
384 | 
385 | // Navbar toggle
386 | @navbar-default-toggle-hover-bg:           #ddd;
387 | @navbar-default-toggle-icon-bar-bg:        #888;
388 | @navbar-default-toggle-border-color:       #ddd;
389 | 
390 | 
391 | //=== Inverted navbar
392 | // Reset inverted navbar basics
393 | @navbar-inverse-color:                      lighten(@gray-light, 15%);
394 | @navbar-inverse-bg:                         #222;
395 | @navbar-inverse-border:                     darken(@navbar-inverse-bg, 10%);
396 | 
397 | // Inverted navbar links
398 | @navbar-inverse-link-color:                 lighten(@gray-light, 15%);
399 | @navbar-inverse-link-hover-color:           #fff;
400 | @navbar-inverse-link-hover-bg:              transparent;
401 | @navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;
402 | @navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 10%);
403 | @navbar-inverse-link-disabled-color:        #444;
404 | @navbar-inverse-link-disabled-bg:           transparent;
405 | 
406 | // Inverted navbar brand label
407 | @navbar-inverse-brand-color:                @navbar-inverse-link-color;
408 | @navbar-inverse-brand-hover-color:          #fff;
409 | @navbar-inverse-brand-hover-bg:             transparent;
410 | 
411 | // Inverted navbar toggle
412 | @navbar-inverse-toggle-hover-bg:            #333;
413 | @navbar-inverse-toggle-icon-bar-bg:         #fff;
414 | @navbar-inverse-toggle-border-color:        #333;
415 | 
416 | 
417 | //== Navs
418 | //
419 | //##
420 | 
421 | //=== Shared nav styles
422 | @nav-link-padding:                          10px 15px;
423 | @nav-link-hover-bg:                         @gray-lighter;
424 | 
425 | @nav-disabled-link-color:                   @gray-light;
426 | @nav-disabled-link-hover-color:             @gray-light;
427 | 
428 | //== Tabs
429 | @nav-tabs-border-color:                     #ddd;
430 | 
431 | @nav-tabs-link-hover-border-color:          @gray-lighter;
432 | 
433 | @nav-tabs-active-link-hover-bg:             @body-bg;
434 | @nav-tabs-active-link-hover-color:          @gray;
435 | @nav-tabs-active-link-hover-border-color:   #ddd;
436 | 
437 | @nav-tabs-justified-link-border-color:            #ddd;
438 | @nav-tabs-justified-active-link-border-color:     @body-bg;
439 | 
440 | //== Pills
441 | @nav-pills-border-radius:                   @border-radius-base;
442 | @nav-pills-active-link-hover-bg:            @component-active-bg;
443 | @nav-pills-active-link-hover-color:         @component-active-color;
444 | 
445 | 
446 | //== Pagination
447 | //
448 | //##
449 | 
450 | @pagination-color:                     @link-color;
451 | @pagination-bg:                        #fff;
452 | @pagination-border:                    #ddd;
453 | 
454 | @pagination-hover-color:               @link-hover-color;
455 | @pagination-hover-bg:                  @gray-lighter;
456 | @pagination-hover-border:              #ddd;
457 | 
458 | @pagination-active-color:              #fff;
459 | @pagination-active-bg:                 @brand-primary;
460 | @pagination-active-border:             @brand-primary;
461 | 
462 | @pagination-disabled-color:            @gray-light;
463 | @pagination-disabled-bg:               #fff;
464 | @pagination-disabled-border:           #ddd;
465 | 
466 | 
467 | //== Pager
468 | //
469 | //##
470 | 
471 | @pager-bg:                             @pagination-bg;
472 | @pager-border:                         @pagination-border;
473 | @pager-border-radius:                  15px;
474 | 
475 | @pager-hover-bg:                       @pagination-hover-bg;
476 | 
477 | @pager-active-bg:                      @pagination-active-bg;
478 | @pager-active-color:                   @pagination-active-color;
479 | 
480 | @pager-disabled-color:                 @pagination-disabled-color;
481 | 
482 | 
483 | //== Jumbotron
484 | //
485 | //##
486 | 
487 | @jumbotron-padding:              30px;
488 | @jumbotron-color:                inherit;
489 | @jumbotron-bg:                   @gray-lighter;
490 | @jumbotron-heading-color:        inherit;
491 | @jumbotron-font-size:            ceil((@font-size-base * 1.5));
492 | @jumbotron-heading-font-size:    ceil((@font-size-base * 4.5));
493 | 
494 | 
495 | //== Form states and alerts
496 | //
497 | //## Define colors for form feedback states and, by default, alerts.
498 | 
499 | @state-success-text:             #3c763d;
500 | @state-success-bg:               #dff0d8;
501 | @state-success-border:           darken(spin(@state-success-bg, -10), 5%);
502 | 
503 | @state-info-text:                #31708f;
504 | @state-info-bg:                  #d9edf7;
505 | @state-info-border:              darken(spin(@state-info-bg, -10), 7%);
506 | 
507 | @state-warning-text:             #8a6d3b;
508 | @state-warning-bg:               #fcf8e3;
509 | @state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);
510 | 
511 | @state-danger-text:              #a94442;
512 | @state-danger-bg:                #f2dede;
513 | @state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);
514 | 
515 | 
516 | //== Tooltips
517 | //
518 | //##
519 | 
520 | //** Tooltip max width
521 | @tooltip-max-width:           200px;
522 | //** Tooltip text color
523 | @tooltip-color:               #fff;
524 | //** Tooltip background color
525 | @tooltip-bg:                  #000;
526 | @tooltip-opacity:             .9;
527 | 
528 | //** Tooltip arrow width
529 | @tooltip-arrow-width:         5px;
530 | //** Tooltip arrow color
531 | @tooltip-arrow-color:         @tooltip-bg;
532 | 
533 | 
534 | //== Popovers
535 | //
536 | //##
537 | 
538 | //** Popover body background color
539 | @popover-bg:                          #fff;
540 | //** Popover maximum width
541 | @popover-max-width:                   276px;
542 | //** Popover border color
543 | @popover-border-color:                rgba(0,0,0,.2);
544 | //** Popover fallback border color
545 | @popover-fallback-border-color:       #ccc;
546 | 
547 | //** Popover title background color
548 | @popover-title-bg:                    darken(@popover-bg, 3%);
549 | 
550 | //** Popover arrow width
551 | @popover-arrow-width:                 10px;
552 | //** Popover arrow color
553 | @popover-arrow-color:                 @popover-bg;
554 | 
555 | //** Popover outer arrow width
556 | @popover-arrow-outer-width:           (@popover-arrow-width + 1);
557 | //** Popover outer arrow color
558 | @popover-arrow-outer-color:           fadein(@popover-border-color, 5%);
559 | //** Popover outer arrow fallback color
560 | @popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
561 | 
562 | 
563 | //== Labels
564 | //
565 | //##
566 | 
567 | //** Default label background color
568 | @label-default-bg:            @gray-light;
569 | //** Primary label background color
570 | @label-primary-bg:            @brand-primary;
571 | //** Success label background color
572 | @label-success-bg:            @brand-success;
573 | //** Info label background color
574 | @label-info-bg:               @brand-info;
575 | //** Warning label background color
576 | @label-warning-bg:            @brand-warning;
577 | //** Danger label background color
578 | @label-danger-bg:             @brand-danger;
579 | 
580 | //** Default label text color
581 | @label-color:                 #fff;
582 | //** Default text color of a linked label
583 | @label-link-hover-color:      #fff;
584 | 
585 | 
586 | //== Modals
587 | //
588 | //##
589 | 
590 | //** Padding applied to the modal body
591 | @modal-inner-padding:         15px;
592 | 
593 | //** Padding applied to the modal title
594 | @modal-title-padding:         15px;
595 | //** Modal title line-height
596 | @modal-title-line-height:     @line-height-base;
597 | 
598 | //** Background color of modal content area
599 | @modal-section-bg:                             #fff;
600 | //** Modal content border color
601 | @modal-section-border-color:                   rgba(0,0,0,.2);
602 | //** Modal content border color **for IE8**
603 | @modal-section-fallback-border-color:          #999;
604 | 
605 | //** Modal backdrop background color
606 | @modal-backdrop-bg:           #000;
607 | //** Modal backdrop opacity
608 | @modal-backdrop-opacity:      .5;
609 | //** Modal header border color
610 | @modal-header-border-color:   #e5e5e5;
611 | //** Modal footer border color
612 | @modal-footer-border-color:   @modal-header-border-color;
613 | 
614 | @modal-lg:                    900px;
615 | @modal-md:                    600px;
616 | @modal-sm:                    300px;
617 | 
618 | 
619 | //== Alerts
620 | //
621 | //## Define alert colors, border radius, and padding.
622 | 
623 | @alert-padding:               15px;
624 | @alert-border-radius:         @border-radius-base;
625 | @alert-link-font-weight:      bold;
626 | 
627 | @alert-success-bg:            @state-success-bg;
628 | @alert-success-text:          @state-success-text;
629 | @alert-success-border:        @state-success-border;
630 | 
631 | @alert-info-bg:               @state-info-bg;
632 | @alert-info-text:             @state-info-text;
633 | @alert-info-border:           @state-info-border;
634 | 
635 | @alert-warning-bg:            @state-warning-bg;
636 | @alert-warning-text:          @state-warning-text;
637 | @alert-warning-border:        @state-warning-border;
638 | 
639 | @alert-danger-bg:             @state-danger-bg;
640 | @alert-danger-text:           @state-danger-text;
641 | @alert-danger-border:         @state-danger-border;
642 | 
643 | 
644 | //== Progress bars
645 | //
646 | //##
647 | 
648 | //** Background color of the whole progress component
649 | @progress-bg:                 #f5f5f5;
650 | //** Progress bar text color
651 | @progress-bar-color:          #fff;
652 | //** Variable for setting rounded corners on progress bar.
653 | @progress-border-radius:      @border-radius-base;
654 | 
655 | //** Default progress bar color
656 | @progress-bar-bg:             @brand-primary;
657 | //** Success progress bar color
658 | @progress-bar-success-bg:     @brand-success;
659 | //** Warning progress bar color
660 | @progress-bar-warning-bg:     @brand-warning;
661 | //** Danger progress bar color
662 | @progress-bar-danger-bg:      @brand-danger;
663 | //** Info progress bar color
664 | @progress-bar-info-bg:        @brand-info;
665 | 
666 | 
667 | //== List group
668 | //
669 | //##
670 | 
671 | //** Background color on `.list-group-item`
672 | @list-group-bg:                 #fff;
673 | //** `.list-group-item` border color
674 | @list-group-border:             #ddd;
675 | //** List group border radius
676 | @list-group-border-radius:      @border-radius-base;
677 | 
678 | //** Background color of single list items on hover
679 | @list-group-hover-bg:           #f5f5f5;
680 | //** Text color of active list items
681 | @list-group-active-color:       @component-active-color;
682 | //** Background color of active list items
683 | @list-group-active-bg:          @component-active-bg;
684 | //** Border color of active list elements
685 | @list-group-active-border:      @list-group-active-bg;
686 | //** Text color for content within active list items
687 | @list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
688 | 
689 | //** Text color of disabled list items
690 | @list-group-disabled-color:      @gray-light;
691 | //** Background color of disabled list items
692 | @list-group-disabled-bg:         @gray-lighter;
693 | //** Text color for content within disabled list items
694 | @list-group-disabled-text-color: @list-group-disabled-color;
695 | 
696 | @list-group-link-color:         #555;
697 | @list-group-link-hover-color:   @list-group-link-color;
698 | @list-group-link-heading-color: #333;
699 | 
700 | 
701 | //== Panels
702 | //
703 | //##
704 | 
705 | @panel-bg:                    #fff;
706 | @panel-body-padding:          15px;
707 | @panel-heading-padding:       10px 15px;
708 | @panel-footer-padding:        @panel-heading-padding;
709 | @panel-border-radius:         @border-radius-base;
710 | 
711 | //** Border color for elements within panels
712 | @panel-inner-border:          #ddd;
713 | @panel-footer-bg:             #f5f5f5;
714 | 
715 | @panel-default-text:          @gray-dark;
716 | @panel-default-border:        #ddd;
717 | @panel-default-heading-bg:    #f5f5f5;
718 | 
719 | @panel-primary-text:          #fff;
720 | @panel-primary-border:        @brand-primary;
721 | @panel-primary-heading-bg:    @brand-primary;
722 | 
723 | @panel-success-text:          @state-success-text;
724 | @panel-success-border:        @state-success-border;
725 | @panel-success-heading-bg:    @state-success-bg;
726 | 
727 | @panel-info-text:             @state-info-text;
728 | @panel-info-border:           @state-info-border;
729 | @panel-info-heading-bg:       @state-info-bg;
730 | 
731 | @panel-warning-text:          @state-warning-text;
732 | @panel-warning-border:        @state-warning-border;
733 | @panel-warning-heading-bg:    @state-warning-bg;
734 | 
735 | @panel-danger-text:           @state-danger-text;
736 | @panel-danger-border:         @state-danger-border;
737 | @panel-danger-heading-bg:     @state-danger-bg;
738 | 
739 | 
740 | //== Thumbnails
741 | //
742 | //##
743 | 
744 | //** Padding around the thumbnail image
745 | @thumbnail-padding:           4px;
746 | //** Thumbnail background color
747 | @thumbnail-bg:                @body-bg;
748 | //** Thumbnail border color
749 | @thumbnail-border:            #ddd;
750 | //** Thumbnail border radius
751 | @thumbnail-border-radius:     @border-radius-base;
752 | 
753 | //** Custom text color for thumbnail captions
754 | @thumbnail-caption-color:     @text-color;
755 | //** Padding around the thumbnail caption
756 | @thumbnail-caption-padding:   9px;
757 | 
758 | 
759 | //== Wells
760 | //
761 | //##
762 | 
763 | @well-bg:                     #f5f5f5;
764 | @well-border:                 darken(@well-bg, 7%);
765 | 
766 | 
767 | //== Badges
768 | //
769 | //##
770 | 
771 | @badge-color:                 #fff;
772 | //** Linked badge text color on hover
773 | @badge-link-hover-color:      #fff;
774 | @badge-bg:                    @gray-light;
775 | 
776 | //** Badge text color in active nav link
777 | @badge-active-color:          @link-color;
778 | //** Badge background color in active nav link
779 | @badge-active-bg:             #fff;
780 | 
781 | @badge-font-weight:           bold;
782 | @badge-line-height:           1;
783 | @badge-border-radius:         10px;
784 | 
785 | 
786 | //== Breadcrumbs
787 | //
788 | //##
789 | 
790 | @breadcrumb-padding-vertical:   8px;
791 | @breadcrumb-padding-horizontal: 15px;
792 | //** Breadcrumb background color
793 | @breadcrumb-bg:                 #f5f5f5;
794 | //** Breadcrumb text color
795 | @breadcrumb-color:              #ccc;
796 | //** Text color of current page in the breadcrumb
797 | @breadcrumb-active-color:       @gray-light;
798 | //** Textual separator for between breadcrumb elements
799 | @breadcrumb-separator:          "/";
800 | 
801 | 
802 | //== Carousel
803 | //
804 | //##
805 | 
806 | @carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
807 | 
808 | @carousel-control-color:                      #fff;
809 | @carousel-control-width:                      15%;
810 | @carousel-control-opacity:                    .5;
811 | @carousel-control-font-size:                  20px;
812 | 
813 | @carousel-indicator-active-bg:                #fff;
814 | @carousel-indicator-border-color:             #fff;
815 | 
816 | @carousel-caption-color:                      #fff;
817 | 
818 | 
819 | //== Close
820 | //
821 | //##
822 | 
823 | @close-font-weight:           bold;
824 | @close-color:                 #000;
825 | @close-text-shadow:           0 1px 0 #fff;
826 | 
827 | 
828 | //== Code
829 | //
830 | //##
831 | 
832 | @code-color:                  #c7254e;
833 | @code-bg:                     #f9f2f4;
834 | 
835 | @kbd-color:                   #fff;
836 | @kbd-bg:                      #333;
837 | 
838 | @pre-bg:                      #f5f5f5;
839 | @pre-color:                   @gray-dark;
840 | @pre-border-color:            #ccc;
841 | @pre-scrollable-max-height:   340px;
842 | 
843 | 
844 | //== Type
845 | //
846 | //##
847 | 
848 | //** Horizontal offset for forms and lists.
849 | @component-offset-horizontal: 180px;
850 | //** Text muted color
851 | @text-muted:                  @gray-light;
852 | //** Abbreviations and acronyms border color
853 | @abbr-border-color:           @gray-light;
854 | //** Headings small color
855 | @headings-small-color:        @gray-light;
856 | //** Blockquote small color
857 | @blockquote-small-color:      @gray-light;
858 | //** Blockquote font size
859 | @blockquote-font-size:        (@font-size-base * 1.25);
860 | //** Blockquote border color
861 | @blockquote-border-color:     @gray-lighter;
862 | //** Page header border color
863 | @page-header-border-color:    @gray-lighter;
864 | //** Width of horizontal description list titles
865 | @dl-horizontal-offset:        @component-offset-horizontal;
866 | //** Point at which .dl-horizontal becomes horizontal
867 | @dl-horizontal-breakpoint:    @grid-float-breakpoint;
868 | //** Horizontal line color.
869 | @hr-border:                   @gray-lighter;
870 | 


--------------------------------------------------------------------------------
/assets/index.less:
--------------------------------------------------------------------------------
1 | @prefixCls: rc-dialog;
2 | 
3 | @import "./index/Dialog.less";
4 | @import "./index/Mask.less";
5 | 


--------------------------------------------------------------------------------
/assets/index/Dialog.less:
--------------------------------------------------------------------------------
  1 | .@{prefixCls} {
  2 |   position: relative;
  3 |   width: auto;
  4 |   margin: 10px;
  5 | 
  6 |   &-wrap {
  7 |     position: fixed;
  8 |     overflow: auto;
  9 |     top: 0;
 10 |     right: 0;
 11 |     bottom: 0;
 12 |     left: 0;
 13 |     z-index: 1050;
 14 |     -webkit-overflow-scrolling: touch;
 15 |     outline: 0;
 16 |   }
 17 | 
 18 |   &-title {
 19 |     margin: 0;
 20 |     font-size: 14px;
 21 |     line-height: 21px;
 22 |     font-weight: bold;
 23 |   }
 24 | 
 25 |   &-section {
 26 |     position: relative;
 27 |     background-color: #ffffff;
 28 |     border: none;
 29 |     border-radius: 6px 6px;
 30 |     background-clip: padding-box;
 31 |   }
 32 | 
 33 |   &-close {
 34 |     cursor: pointer;
 35 |     border: 0;
 36 |     background: transparent;
 37 |     font-size: 21px;
 38 |     position: absolute;
 39 |     right: 20px;
 40 |     top: 12px;
 41 |     font-weight: 700;
 42 |     line-height: 1;
 43 |     color: #000;
 44 |     text-shadow: 0 1px 0 #fff;
 45 |     filter: alpha(opacity=20);
 46 |     opacity: .2;
 47 |     text-decoration: none;
 48 | 
 49 |     &:disabled {
 50 |       pointer-events: none;
 51 |     }
 52 | 
 53 |     &-x:after {
 54 |       content: '×'
 55 |     }
 56 | 
 57 |     &:hover {
 58 |       opacity: 1;
 59 |       filter: alpha(opacity=100);
 60 |       text-decoration: none;
 61 |     }
 62 |   }
 63 | 
 64 |   &-header {
 65 |     padding: 13px 20px 14px 20px;
 66 |     border-radius: 5px 5px 0 0;
 67 |     background: #fff;
 68 |     color: #666;
 69 |     border-bottom: 1px solid #e9e9e9;
 70 |   }
 71 | 
 72 |   &-body {
 73 |     padding: 20px;
 74 |   }
 75 | 
 76 |   &-footer {
 77 |     border-top: 1px solid #e9e9e9;
 78 |     padding: 10px 20px;
 79 |     text-align: right;
 80 |     border-radius: 0 0 5px 5px;
 81 |   }
 82 | 
 83 |   .effect() {
 84 |     animation-duration: 0.3s;
 85 |     animation-fill-mode: both;
 86 |   }
 87 | 
 88 |   &-zoom-enter, &-zoom-appear {
 89 |     opacity: 0;
 90 |     .effect();
 91 |     animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
 92 |     animation-play-state: paused;
 93 |   }
 94 | 
 95 |   &-zoom-leave {
 96 |     .effect();
 97 |     animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
 98 |     animation-play-state: paused;
 99 |   }
100 | 
101 |   &-zoom-enter&-zoom-enter-active, &-zoom-appear&-zoom-appear-active {
102 |     animation-name: rcDialogZoomIn;
103 |     animation-play-state: running;
104 |   }
105 | 
106 |   &-zoom-leave&-zoom-leave-active {
107 |     animation-name: rcDialogZoomOut;
108 |     animation-play-state: running;
109 |   }
110 | 
111 |   @keyframes rcDialogZoomIn {
112 |     0% {
113 |       opacity: 0;
114 |       transform: scale(0, 0);
115 |     }
116 |     100% {
117 |       opacity: 1;
118 |       transform: scale(1, 1);
119 |     }
120 |   }
121 |   @keyframes rcDialogZoomOut {
122 |     0% {
123 | 
124 |       transform: scale(1, 1);
125 |     }
126 |     100% {
127 |       opacity: 0;
128 |       transform: scale(0, 0);
129 |     }
130 |   }
131 | }
132 | 
133 | @media (min-width: 768px) {
134 |   .@{prefixCls} {
135 |     width: 600px;
136 |     margin: 30px auto;
137 |   }
138 | }
139 | 


--------------------------------------------------------------------------------
/assets/index/Mask.less:
--------------------------------------------------------------------------------
 1 | .@{prefixCls} {
 2 |   &-mask {
 3 |     position: fixed;
 4 |     top: 0;
 5 |     right: 0;
 6 |     left: 0;
 7 |     bottom: 0;
 8 |     background-color: rgb(55, 55, 55);
 9 |     background-color: rgba(55, 55, 55, 0.6);
10 |     height: 100%;
11 |     filter: alpha(opacity=50);
12 |     z-index: 1050;
13 | 
14 |     &-hidden {
15 |       display: none;
16 |     }
17 |   }
18 | 
19 |   .fade-effect() {
20 |     animation-duration: 0.3s;
21 |     animation-fill-mode: both;
22 |     animation-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2);
23 |   }
24 | 
25 |   &-fade-enter,&-fade-appear {
26 |     opacity: 0;
27 |     .fade-effect();
28 |     animation-play-state: paused;
29 |   }
30 | 
31 |   &-fade-leave {
32 |     .fade-effect();
33 |     animation-play-state: paused;
34 |   }
35 | 
36 |   &-fade-enter&-fade-enter-active,&-fade-appear&-fade-appear-active  {
37 |     animation-name: rcDialogFadeIn;
38 |     animation-play-state: running;
39 |   }
40 | 
41 |   &-fade-leave&-fade-leave-active {
42 |     animation-name: rcDialogFadeOut;
43 |     animation-play-state: running;
44 |   }
45 | 
46 |   @keyframes rcDialogFadeIn {
47 |     0% {
48 |       opacity: 0;
49 |     }
50 |     100% {
51 |       opacity: 1;
52 |     }
53 |   }
54 | 
55 |   @keyframes rcDialogFadeOut {
56 |     0% {
57 |       opacity: 1;
58 |     }
59 |     100% {
60 |       opacity: 0;
61 |     }
62 |   }
63 | }
64 | 


--------------------------------------------------------------------------------
/bunfig.toml:
--------------------------------------------------------------------------------
1 | [install]
2 | peer = false


--------------------------------------------------------------------------------
/docs/changelog.md:
--------------------------------------------------------------------------------
1 | 
2 | 


--------------------------------------------------------------------------------
/docs/demo/ant-design.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: ant-design
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | 
9 | 


--------------------------------------------------------------------------------
/docs/demo/bootstrap.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: bootstrap
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | 
9 | 


--------------------------------------------------------------------------------
/docs/demo/draggable.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: draggable
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | 
9 | 


--------------------------------------------------------------------------------
/docs/demo/multiple-Portal.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: multiple-Portal
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | 
9 | 


--------------------------------------------------------------------------------
/docs/demo/pure.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: pure-debug
3 | nav:
4 |   title: Demo
5 |   path: /demo
6 | ---
7 | 
8 | 
9 | 


--------------------------------------------------------------------------------
/docs/examples/ant-design.tsx:
--------------------------------------------------------------------------------
  1 | /* eslint no-console:0 */
  2 | import * as React from 'react';
  3 | import Select from '@rc-component/select';
  4 | import '@rc-component/select/assets/index.less';
  5 | import Dialog from '@rc-component/dialog';
  6 | import '../../assets/index.less';
  7 | 
  8 | const clearPath =
  9 |   'M793 242H366v-74c0-6.7-7.7-10.4-12.9' +
 10 |   '-6.3l-142 112c-4.1 3.2-4.1 9.4 0 12.6l142 112c' +
 11 |   '5.2 4.1 12.9 0.4 12.9-6.3v-74h415v470H175c-4.4' +
 12 |   ' 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h618c35.3 0 64-' +
 13 |   '28.7 64-64V306c0-35.3-28.7-64-64-64z';
 14 | 
 15 | const getSvg = (path: string, props = {}, align = false) => (
 16 |   
 17 |     
 24 |       
 25 |     
 26 |   
 27 | );
 28 | 
 29 | const MyControl: React.FC = () => {
 30 |   const [visible1, setVisible1] = React.useState(true);
 31 |   const [visible2, setVisible2] = React.useState(false);
 32 |   const [visible3, setVisible3] = React.useState(false);
 33 |   const [width, setWidth] = React.useState(600);
 34 |   const [destroyOnHidden, setDestroyOnHidden] = React.useState(false);
 35 |   const [center, setCenter] = React.useState(false);
 36 |   const [mousePosition, setMousePosition] = React.useState({ x: null, y: null });
 37 |   const [useIcon, setUseIcon] = React.useState(false);
 38 |   const [forceRender, setForceRender] = React.useState(false);
 39 | 
 40 |   const onClick = (e: React.MouseEvent) => {
 41 |     setMousePosition({ x: e.pageX, y: e.pageY });
 42 |     setVisible1(true);
 43 |   };
 44 | 
 45 |   const onClose = () => {
 46 |     setVisible1(false);
 47 |   };
 48 | 
 49 |   const onClose2 = () => {
 50 |     setVisible2(false);
 51 |   };
 52 | 
 53 |   const onClose3 = () => {
 54 |     setVisible3(false);
 55 |   };
 56 | 
 57 |   const closeAll = () => {
 58 |     setVisible1(false);
 59 |     setVisible2(false);
 60 |     setVisible3(false);
 61 |   };
 62 | 
 63 |   const onDestroyOnHiddenChange = (e: React.ChangeEvent) => {
 64 |     setDestroyOnHidden(e.target.checked);
 65 |   };
 66 | 
 67 |   const onForceRenderChange = (e: React.ChangeEvent) => {
 68 |     setForceRender(e.target.checked);
 69 |   };
 70 | 
 71 |   const changeWidth = () => {
 72 |     setWidth(width === 600 ? 800 : 600);
 73 |   };
 74 | 
 75 |   const centerEvent = (e: React.ChangeEvent) => {
 76 |     setCenter(e.target.checked);
 77 |   };
 78 | 
 79 |   const toggleCloseIcon = () => {
 80 |     setUseIcon(!useIcon);
 81 |   };
 82 | 
 83 |   const style = { width };
 84 | 
 85 |   let wrapClassName = '';
 86 |   if (center) {
 87 |     wrapClassName = 'center';
 88 |   }
 89 | 
 90 |   const dialog = (
 91 |     
105 |       
106 |       

basic modal

107 | 116 | 124 | 127 | 130 |
131 | 134 |
135 |
136 | ); 137 | 138 | const dialog2 = ( 139 | 140 | 141 |

basic modal

142 | 150 | 158 | 161 | 164 | 167 |
168 |
169 | ); 170 | 171 | const dialog3 = ( 172 | 173 |

initialized with forceRender and visbile true

174 | 182 | 185 | 188 | 191 |
192 |
193 | ); 194 | 195 | return ( 196 | 197 |
198 | 207 |

208 | 211 |   212 | 216 |   217 | 221 |   222 | 226 | 227 |

228 | {dialog} 229 | {dialog2} 230 | {dialog3} 231 |
232 |
233 | ); 234 | }; 235 | 236 | export default MyControl; 237 | -------------------------------------------------------------------------------- /docs/examples/bootstrap.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dialog from '@rc-component/dialog'; 3 | import 'bootstrap/dist/css/bootstrap.css'; 4 | import '../../assets/bootstrap.less'; 5 | 6 | // Check for memo update should work 7 | const InnerRender: React.FC = () => { 8 | console.log('Updated...', Date.now()); 9 | return null; 10 | }; 11 | 12 | const MyControl: React.FC = () => { 13 | const [visible, setVisible] = React.useState(false); 14 | const [destroyOnHidden, setDestroyOnHidden] = React.useState(false); 15 | 16 | const onClick = () => { 17 | setVisible(true); 18 | }; 19 | 20 | const onClose = () => { 21 | setVisible(false); 22 | }; 23 | 24 | const onDestroyOnHiddenChange = (e: React.ChangeEvent) => { 25 | setDestroyOnHidden(e.target.checked); 26 | }; 27 | 28 | const dialog = ( 29 | 第二个弹框} 37 | footer={[ 38 | , 41 | , 44 | ]} 45 | > 46 | 47 |

Text in a modal

48 |

Duis mollis, est non commodo luctus, nisi erat porttitor ligula.

49 |
50 |

Overflowing text to show scroll behavior

51 |

52 | Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, 53 | egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. 54 |

55 |

56 | {' '} 59 | {' '} 62 | {' '} 65 | {' '} 68 | {' '} 71 | {' '} 74 |

75 |

76 | Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus 77 | vel augue laoreet rutrum faucibus dolor auctor. 78 |

79 |
80 |

81 | Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel 82 | scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus 83 | auctor fringilla. 84 |

85 |

86 | Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis 87 | in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. 88 |

89 |

90 | Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus 91 | vel augue laoreet rutrum faucibus dolor auctor. 92 |

93 |

94 | Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel 95 | scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus 96 | auctor fringilla. 97 |

98 |

99 | Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis 100 | in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. 101 |

102 |

103 | Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus 104 | vel augue laoreet rutrum faucibus dolor auctor. 105 |

106 |

107 | Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel 108 | scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus 109 | auctor fringilla. 110 |

111 |
112 |
113 | ); 114 | 115 | return ( 116 |
117 |

118 | 121 |   122 | 126 |

127 | {dialog} 128 |
129 | ); 130 | }; 131 | 132 | export default MyControl; 133 | -------------------------------------------------------------------------------- /docs/examples/draggable.tsx: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.css'; 2 | import * as React from 'react'; 3 | import Draggable from 'react-draggable'; 4 | import Dialog from '@rc-component/dialog'; 5 | import '../../assets/index.less'; 6 | 7 | const MyControl: React.FC = () => { 8 | const [visible, setVisible] = React.useState(false); 9 | const [disabled, setDisabled] = React.useState(true); 10 | const onClick = () => { 11 | setVisible(true); 12 | }; 13 | const onClose = () => { 14 | setVisible(false); 15 | }; 16 | return ( 17 |
18 |

19 | 22 |

23 | { 33 | if (disabled) { 34 | setDisabled(false); 35 | } 36 | }} 37 | onMouseOut={() => { 38 | setDisabled(true); 39 | }} 40 | // fix eslintjsx-a11y/mouse-events-have-key-events 41 | // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/mouse-events-have-key-events.md 42 | onFocus={() => { }} 43 | onBlur={() => { }} 44 | // end 45 | > 46 | modal 47 |
48 | } 49 | modalRender={(modal) => {modal}} 50 | > 51 |
52 | Day before yesterday I saw a rabbit, and yesterday a deer, and today, you. 53 |
54 | 55 | 56 | ); 57 | }; 58 | 59 | export default MyControl; 60 | -------------------------------------------------------------------------------- /docs/examples/multiple-Portal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Drawer from '@rc-component/drawer'; 3 | import '@rc-component/drawer/assets/index.css'; 4 | import Dialog from '@rc-component/dialog'; 5 | import '../../assets/index.less'; 6 | 7 | const Demo: React.FC = () => { 8 | const [showDialog, setShowDialog] = React.useState(false); 9 | const [showDrawer, setShowDrawer] = React.useState(false); 10 | 11 | const onToggleDrawer = () => { 12 | setShowDrawer((value) => !value); 13 | }; 14 | 15 | const onToggleDialog = () => { 16 | setShowDialog((value) => !value); 17 | }; 18 | 19 | const dialog = ( 20 | 28 |

29 | 32 |

33 |
34 |
35 | ); 36 | const drawer = ( 37 | 38 | 41 | 42 | ); 43 | return ( 44 |
45 | 48 | 59 | {dialog} 60 | {drawer} 61 |
62 | ); 63 | }; 64 | 65 | export default Demo; 66 | -------------------------------------------------------------------------------- /docs/examples/pure.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Panel } from '@rc-component/dialog'; 3 | import '@rc-component/select/assets/index.less'; 4 | import '../../assets/index.less'; 5 | 6 | const Demo: React.FC = () => ( 7 | 8 | Hello World! 9 | 10 | ); 11 | 12 | export default Demo; 13 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: rc-dialog 4 | description: React Dialog Component 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ["./tests/setup.js"], 3 | setupFilesAfterEnv: ["./tests/setupFilesAfterEnv.ts"], 4 | }; 5 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "rc-dialog", 4 | "builds": [ 5 | { 6 | "src": "package.json", 7 | "use": "@now/static-build", 8 | "config": { "distDir": "dist" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rc-component/dialog", 3 | "version": "1.3.0", 4 | "description": "dialog ui component for react", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "react-dialog", 9 | "dialog", 10 | "ui" 11 | ], 12 | "homepage": "http://github.com/react-component/dialog", 13 | "bugs": { 14 | "url": "http://github.com/react-component/dialog/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:react-component/dialog.git" 19 | }, 20 | "license": "MIT", 21 | "author": "yiminghe@gmail.com", 22 | "main": "./lib/index", 23 | "module": "./es/index", 24 | "files": [ 25 | "lib", 26 | "es", 27 | "assets/*.css", 28 | "dist" 29 | ], 30 | "scripts": { 31 | "compile": "father build && lessc assets/index.less assets/index.css && lessc assets/bootstrap.less assets/bootstrap.css", 32 | "coverage": "rc-test --coverage", 33 | "deploy": "npm run docs:build && npm run docs:deploy", 34 | "docs:build": "dumi build", 35 | "docs:deploy": "gh-pages -d dist", 36 | "lint": "eslint src/ --ext .ts,.tsx,.jsx,.js,.md", 37 | "lint:tsc": "tsc -p tsconfig.json --noEmit", 38 | "now-build": "npm run docs:build", 39 | "prepare": "husky install", 40 | "prepublishOnly": "npm run compile && rc-np", 41 | "prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", 42 | "start": "dumi dev", 43 | "test": "rc-test", 44 | "tsc": "bunx tsc --noEmit" 45 | }, 46 | "lint-staged": { 47 | "**/*.{js,jsx,tsx,ts,md,json}": [ 48 | "prettier --write", 49 | "git add" 50 | ] 51 | }, 52 | "dependencies": { 53 | "@rc-component/motion": "^1.1.3", 54 | "@rc-component/portal": "^2.0.0", 55 | "@rc-component/util": "^1.0.1", 56 | "classnames": "^2.2.6" 57 | }, 58 | "devDependencies": { 59 | "@rc-component/drawer": "^1.0.0", 60 | "@rc-component/father-plugin": "^2.0.2", 61 | "@rc-component/np": "^1.0.3", 62 | "@rc-component/select": "^1.0.0", 63 | "@testing-library/jest-dom": "^6.1.6", 64 | "@testing-library/react": "^13.0.0", 65 | "@types/jest": "^29.4.0", 66 | "@types/keyv": "3.1.4", 67 | "@types/node": "^22.15.18", 68 | "@types/react": "^19.1.4", 69 | "@types/react-dom": "^19.1.5", 70 | "@umijs/fabric": "^3.0.0", 71 | "bootstrap": "^4.3.1", 72 | "cheerio": "1.0.0-rc.12", 73 | "cross-env": "^7.0.0", 74 | "dumi": "^2.1.3", 75 | "eslint": "^7.1.0", 76 | "eslint-config-airbnb": "^19.0.4", 77 | "eslint-plugin-react": "^7.20.6", 78 | "father": "^4.1.5", 79 | "gh-pages": "^6.1.1", 80 | "glob": "^11.0.0", 81 | "husky": "^9.1.6", 82 | "less": "^4.1.3", 83 | "lint-staged": "^15.2.0", 84 | "prettier": "^3.2.1", 85 | "rc-test": "^7.0.14", 86 | "react": "^18.0.0", 87 | "react-dom": "^18.0.0", 88 | "react-draggable": "^4.4.3", 89 | "typescript": "^5.4.3" 90 | }, 91 | "peerDependencies": { 92 | "react": ">=18.0.0", 93 | "react-dom": ">=18.0.0" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /script/update-content.js: -------------------------------------------------------------------------------- 1 | /* 2 | 用于 dumi 改造使用, 3 | 可用于将 examples 的文件批量修改为 demo 引入形式, 4 | 其他项目根据具体情况使用。 5 | */ 6 | 7 | const fs = require('fs'); 8 | const glob = require('glob'); 9 | 10 | const paths = glob.sync('./docs/examples/*.tsx'); 11 | 12 | paths.forEach(path => { 13 | const name = path.split('/').pop().split('.')[0]; 14 | fs.writeFile( 15 | `./docs/demo/${name}.md`, 16 | `## ${name} 17 | 18 | 19 | `, 20 | 'utf8', 21 | function(error) { 22 | if(error){ 23 | console.log(error); 24 | return false; 25 | } 26 | console.log(`${name} 更新成功~`); 27 | } 28 | ) 29 | }); 30 | -------------------------------------------------------------------------------- /src/Dialog/Content/MemoChildren.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type MemoChildrenProps = { 4 | shouldUpdate: boolean; 5 | children: React.ReactNode; 6 | }; 7 | 8 | export default React.memo( 9 | ({ children }: MemoChildrenProps) => children as React.ReactElement, 10 | (_, { shouldUpdate }) => !shouldUpdate, 11 | ); 12 | -------------------------------------------------------------------------------- /src/Dialog/Content/Panel.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { useComposeRef } from '@rc-component/util/lib/ref'; 3 | import React, { useMemo, useRef } from 'react'; 4 | import { RefContext } from '../../context'; 5 | import type { IDialogPropTypes } from '../../IDialogPropTypes'; 6 | import MemoChildren from './MemoChildren'; 7 | import pickAttrs from '@rc-component/util/lib/pickAttrs'; 8 | 9 | const sentinelStyle: React.CSSProperties = { 10 | width: 0, 11 | height: 0, 12 | overflow: 'hidden', 13 | outline: 'none', 14 | }; 15 | 16 | const entityStyle: React.CSSProperties = { 17 | outline: 'none', 18 | }; 19 | 20 | export interface PanelProps extends Omit { 21 | prefixCls: string; 22 | ariaId?: string; 23 | onMouseDown?: React.MouseEventHandler; 24 | onMouseUp?: React.MouseEventHandler; 25 | holderRef?: React.Ref; 26 | } 27 | 28 | export type PanelRef = { 29 | focus: () => void; 30 | changeActive: (next: boolean) => void; 31 | }; 32 | 33 | const Panel = React.forwardRef((props, ref) => { 34 | const { 35 | prefixCls, 36 | className, 37 | style, 38 | title, 39 | ariaId, 40 | footer, 41 | closable, 42 | closeIcon, 43 | onClose, 44 | children, 45 | bodyStyle, 46 | bodyProps, 47 | modalRender, 48 | onMouseDown, 49 | onMouseUp, 50 | holderRef, 51 | visible, 52 | forceRender, 53 | width, 54 | height, 55 | classNames: modalClassNames, 56 | styles: modalStyles, 57 | } = props; 58 | 59 | // ================================= Refs ================================= 60 | const { panel: panelRef } = React.useContext(RefContext); 61 | 62 | const mergedRef = useComposeRef(holderRef, panelRef); 63 | 64 | const sentinelStartRef = useRef(null); 65 | const sentinelEndRef = useRef(null); 66 | 67 | React.useImperativeHandle(ref, () => ({ 68 | focus: () => { 69 | sentinelStartRef.current?.focus({ preventScroll: true }); 70 | }, 71 | changeActive: (next) => { 72 | const { activeElement } = document; 73 | if (next && activeElement === sentinelEndRef.current) { 74 | sentinelStartRef.current.focus({ preventScroll: true }); 75 | } else if (!next && activeElement === sentinelStartRef.current) { 76 | sentinelEndRef.current.focus({ preventScroll: true }); 77 | } 78 | }, 79 | })); 80 | 81 | // ================================ Style ================================= 82 | const contentStyle: React.CSSProperties = {}; 83 | 84 | if (width !== undefined) { 85 | contentStyle.width = width; 86 | } 87 | if (height !== undefined) { 88 | contentStyle.height = height; 89 | } 90 | // ================================ Render ================================ 91 | const footerNode = footer ? ( 92 |
96 | {footer} 97 |
98 | ) : null; 99 | 100 | const headerNode = title ? ( 101 |
105 |
110 | {title} 111 |
112 |
113 | ) : null; 114 | 115 | const closableObj = useMemo(() => { 116 | if (typeof closable === 'object' && closable !== null) { 117 | return closable; 118 | } 119 | if (closable) { 120 | return { closeIcon: closeIcon ?? }; 121 | } 122 | return {}; 123 | }, [closable, closeIcon, prefixCls]); 124 | 125 | const ariaProps = pickAttrs(closableObj, true); 126 | const closeBtnIsDisabled = typeof closable === 'object' && closable.disabled; 127 | 128 | const closerNode = closable ? ( 129 | 139 | ) : null; 140 | 141 | const content = ( 142 |
146 | {closerNode} 147 | {headerNode} 148 |
153 | {children} 154 |
155 | {footerNode} 156 |
157 | ); 158 | 159 | return ( 160 |
171 |
172 | 173 | {modalRender ? modalRender(content) : content} 174 | 175 |
176 |
177 |
178 | ); 179 | }); 180 | 181 | if (process.env.NODE_ENV !== 'production') { 182 | Panel.displayName = 'Panel'; 183 | } 184 | 185 | export default Panel; 186 | -------------------------------------------------------------------------------- /src/Dialog/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useRef } from 'react'; 3 | import classNames from 'classnames'; 4 | import CSSMotion from '@rc-component/motion'; 5 | import { offset } from '../../util'; 6 | import type { PanelProps, PanelRef } from './Panel'; 7 | import Panel from './Panel'; 8 | import type { CSSMotionRef } from '@rc-component/motion/es/CSSMotion'; 9 | 10 | export type CSSMotionStateRef = Pick; 11 | 12 | export type ContentRef = PanelRef & CSSMotionStateRef; 13 | 14 | export type ContentProps = { 15 | motionName: string; 16 | ariaId: string; 17 | onVisibleChanged: (visible: boolean) => void; 18 | } & PanelProps; 19 | 20 | const Content = React.forwardRef((props, ref) => { 21 | const { 22 | prefixCls, 23 | title, 24 | style, 25 | className, 26 | visible, 27 | forceRender, 28 | destroyOnHidden, 29 | motionName, 30 | ariaId, 31 | onVisibleChanged, 32 | mousePosition, 33 | } = props; 34 | 35 | const dialogRef = useRef<{ nativeElement: HTMLElement } & CSSMotionStateRef>(null); 36 | 37 | const panelRef = useRef(null); 38 | 39 | // ============================== Refs ============================== 40 | React.useImperativeHandle(ref, () => ({ 41 | ...panelRef.current, 42 | inMotion: dialogRef.current.inMotion, 43 | enableMotion: dialogRef.current.enableMotion, 44 | })); 45 | 46 | // ============================= Style ============================== 47 | const [transformOrigin, setTransformOrigin] = React.useState(); 48 | const contentStyle: React.CSSProperties = {}; 49 | 50 | if (transformOrigin) { 51 | contentStyle.transformOrigin = transformOrigin; 52 | } 53 | 54 | function onPrepare() { 55 | const elementOffset = offset(dialogRef.current.nativeElement); 56 | 57 | setTransformOrigin( 58 | mousePosition && (mousePosition.x || mousePosition.y) 59 | ? `${mousePosition.x - elementOffset.left}px ${mousePosition.y - elementOffset.top}px` 60 | : '', 61 | ); 62 | } 63 | 64 | // ============================= Render ============================= 65 | return ( 66 | 76 | {({ className: motionClassName, style: motionStyle }, motionRef) => ( 77 | 87 | )} 88 | 89 | ); 90 | }); 91 | 92 | if (process.env.NODE_ENV !== 'production') { 93 | Content.displayName = 'Content'; 94 | } 95 | 96 | export default Content; 97 | -------------------------------------------------------------------------------- /src/Dialog/Mask.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classNames from 'classnames'; 3 | import CSSMotion from '@rc-component/motion'; 4 | 5 | export type MaskProps = { 6 | prefixCls: string; 7 | visible: boolean; 8 | motionName?: string; 9 | style?: React.CSSProperties; 10 | maskProps?: React.HTMLAttributes; 11 | className?: string; 12 | }; 13 | 14 | const Mask: React.FC = (props) => { 15 | const { prefixCls, style, visible, maskProps, motionName, className } = props; 16 | return ( 17 | 23 | {({ className: motionClassName, style: motionStyle }, ref) => ( 24 |
30 | )} 31 | 32 | ); 33 | }; 34 | 35 | export default Mask; 36 | -------------------------------------------------------------------------------- /src/Dialog/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import contains from '@rc-component/util/lib/Dom/contains'; 3 | import useId from '@rc-component/util/lib/hooks/useId'; 4 | import KeyCode from '@rc-component/util/lib/KeyCode'; 5 | import pickAttrs from '@rc-component/util/lib/pickAttrs'; 6 | import * as React from 'react'; 7 | import { useEffect, useRef } from 'react'; 8 | import type { IDialogPropTypes } from '../IDialogPropTypes'; 9 | import { getMotionName } from '../util'; 10 | import Content, { type ContentRef } from './Content'; 11 | import Mask from './Mask'; 12 | import { warning } from '@rc-component/util/lib/warning'; 13 | 14 | const Dialog: React.FC = (props) => { 15 | const { 16 | prefixCls = 'rc-dialog', 17 | zIndex, 18 | visible = false, 19 | keyboard = true, 20 | focusTriggerAfterClose = true, 21 | // scrollLocker, 22 | // Wrapper 23 | wrapStyle, 24 | wrapClassName, 25 | wrapProps, 26 | onClose, 27 | afterOpenChange, 28 | afterClose, 29 | 30 | // Dialog 31 | transitionName, 32 | animation, 33 | closable = true, 34 | 35 | // Mask 36 | mask = true, 37 | maskTransitionName, 38 | maskAnimation, 39 | maskClosable = true, 40 | maskStyle, 41 | maskProps, 42 | rootClassName, 43 | rootStyle, 44 | classNames: modalClassNames, 45 | styles: modalStyles, 46 | } = props; 47 | 48 | if (process.env.NODE_ENV !== 'production') { 49 | ['wrapStyle', 'bodyStyle', 'maskStyle'].forEach((prop) => { 50 | // (prop in props) && console.error(`Warning: ${prop} is deprecated, please use styles instead.`) 51 | warning(!(prop in props), `${prop} is deprecated, please use styles instead.`); 52 | }); 53 | if ('wrapClassName' in props) { 54 | warning(false, `wrapClassName is deprecated, please use classNames instead.`); 55 | } 56 | } 57 | 58 | const lastOutSideActiveElementRef = useRef(null); 59 | const wrapperRef = useRef(null); 60 | const contentRef = useRef(null); 61 | 62 | const [animatedVisible, setAnimatedVisible] = React.useState(visible); 63 | 64 | // ========================== Init ========================== 65 | const ariaId = useId(); 66 | 67 | function saveLastOutSideActiveElementRef() { 68 | if (!contains(wrapperRef.current, document.activeElement)) { 69 | lastOutSideActiveElementRef.current = document.activeElement as HTMLElement; 70 | } 71 | } 72 | 73 | function focusDialogContent() { 74 | if (!contains(wrapperRef.current, document.activeElement)) { 75 | contentRef.current?.focus(); 76 | } 77 | } 78 | 79 | // ========================= Events ========================= 80 | // Close action will trigger by: 81 | // 1. When hide motion end 82 | // 2. Controlled `open` to `false` immediately after set to `true` which will not trigger motion 83 | function doClose() { 84 | // Clean up scroll bar & focus back 85 | setAnimatedVisible(false); 86 | 87 | if (mask && lastOutSideActiveElementRef.current && focusTriggerAfterClose) { 88 | try { 89 | lastOutSideActiveElementRef.current.focus({ preventScroll: true }); 90 | } catch (e) { 91 | // Do nothing 92 | } 93 | lastOutSideActiveElementRef.current = null; 94 | } 95 | 96 | // Trigger afterClose only when change visible from true to false 97 | if (animatedVisible) { 98 | afterClose?.(); 99 | } 100 | } 101 | 102 | function onDialogVisibleChanged(newVisible: boolean) { 103 | // Try to focus 104 | if (newVisible) { 105 | focusDialogContent(); 106 | } else { 107 | doClose(); 108 | } 109 | afterOpenChange?.(newVisible); 110 | } 111 | 112 | function onInternalClose(e: React.SyntheticEvent) { 113 | onClose?.(e); 114 | } 115 | 116 | // >>> Content 117 | const contentClickRef = useRef(false); 118 | const contentTimeoutRef = useRef>(null); 119 | 120 | // We need record content click incase content popup out of dialog 121 | const onContentMouseDown: React.MouseEventHandler = () => { 122 | clearTimeout(contentTimeoutRef.current); 123 | contentClickRef.current = true; 124 | }; 125 | 126 | const onContentMouseUp: React.MouseEventHandler = () => { 127 | contentTimeoutRef.current = setTimeout(() => { 128 | contentClickRef.current = false; 129 | }); 130 | }; 131 | 132 | // >>> Wrapper 133 | // Close only when element not on dialog 134 | let onWrapperClick: (e: React.SyntheticEvent) => void = null; 135 | if (maskClosable) { 136 | onWrapperClick = (e) => { 137 | if (contentClickRef.current) { 138 | contentClickRef.current = false; 139 | } else if (wrapperRef.current === e.target) { 140 | onInternalClose(e); 141 | } 142 | }; 143 | } 144 | 145 | function onWrapperKeyDown(e: React.KeyboardEvent) { 146 | if (keyboard && e.keyCode === KeyCode.ESC) { 147 | e.stopPropagation(); 148 | onInternalClose(e); 149 | return; 150 | } 151 | 152 | // keep focus inside dialog 153 | if (visible && e.keyCode === KeyCode.TAB) { 154 | contentRef.current.changeActive(!e.shiftKey); 155 | } 156 | } 157 | 158 | // ========================= Effect ========================= 159 | useEffect(() => { 160 | if (visible) { 161 | setAnimatedVisible(true); 162 | saveLastOutSideActiveElementRef(); 163 | } else if ( 164 | animatedVisible && 165 | contentRef.current.enableMotion() && 166 | !contentRef.current.inMotion() 167 | ) { 168 | doClose(); 169 | } 170 | }, [visible]); 171 | 172 | // Remove direct should also check the scroll bar update 173 | useEffect( 174 | () => () => { 175 | clearTimeout(contentTimeoutRef.current); 176 | }, 177 | [], 178 | ); 179 | 180 | const mergedStyle: React.CSSProperties = { 181 | zIndex, 182 | ...wrapStyle, 183 | ...modalStyles?.wrapper, 184 | display: !animatedVisible ? 'none' : null, 185 | }; 186 | 187 | // ========================= Render ========================= 188 | return ( 189 |
194 | 202 |
211 | 224 |
225 |
226 | ); 227 | }; 228 | 229 | export default Dialog; 230 | -------------------------------------------------------------------------------- /src/DialogWrap.tsx: -------------------------------------------------------------------------------- 1 | import Portal from '@rc-component/portal'; 2 | import * as React from 'react'; 3 | import { RefContext } from './context'; 4 | import Dialog from './Dialog'; 5 | import type { IDialogPropTypes } from './IDialogPropTypes'; 6 | 7 | // fix issue #10656 8 | /* 9 | * getContainer remarks 10 | * Custom container should not be return, because in the Portal component, it will remove the 11 | * return container element here, if the custom container is the only child of it's component, 12 | * like issue #10656, It will has a conflict with removeChild method in react-dom. 13 | * So here should add a child (div element) to custom container. 14 | * */ 15 | 16 | const DialogWrap: React.FC = (props) => { 17 | const { 18 | visible, 19 | getContainer, 20 | forceRender, 21 | destroyOnHidden = false, 22 | afterClose, 23 | panelRef, 24 | } = props; 25 | const [animatedVisible, setAnimatedVisible] = React.useState(visible); 26 | 27 | const refContext = React.useMemo(() => ({ panel: panelRef }), [panelRef]); 28 | 29 | React.useEffect(() => { 30 | if (visible) { 31 | setAnimatedVisible(true); 32 | } 33 | }, [visible]); 34 | 35 | // Destroy on close will remove wrapped div 36 | if (!forceRender && destroyOnHidden && !animatedVisible) { 37 | return null; 38 | } 39 | 40 | return ( 41 | 42 | 48 | { 52 | afterClose?.(); 53 | setAnimatedVisible(false); 54 | }} 55 | /> 56 | 57 | 58 | ); 59 | }; 60 | 61 | if (process.env.NODE_ENV !== 'production') { 62 | DialogWrap.displayName = 'Dialog'; 63 | } 64 | 65 | export default DialogWrap; 66 | -------------------------------------------------------------------------------- /src/IDialogPropTypes.tsx: -------------------------------------------------------------------------------- 1 | import type { GetContainer } from '@rc-component/util/lib/PortalWrapper'; 2 | import type { CSSProperties, ReactNode, SyntheticEvent } from 'react'; 3 | 4 | export type SemanticName = 'header' | 'body' | 'footer' | 'section' | 'title' | 'wrapper' | 'mask'; 5 | 6 | export type ModalClassNames = Partial>; 7 | 8 | export type ModalStyles = Partial>; 9 | 10 | export type IDialogPropTypes = { 11 | className?: string; 12 | keyboard?: boolean; 13 | style?: CSSProperties; 14 | rootStyle?: CSSProperties; 15 | mask?: boolean; 16 | children?: React.ReactNode; 17 | afterClose?: () => any; 18 | afterOpenChange?: (open: boolean) => void; 19 | onClose?: (e: SyntheticEvent) => any; 20 | closable?: boolean | ({ closeIcon?: React.ReactNode; disabled?: boolean } & React.AriaAttributes); 21 | maskClosable?: boolean; 22 | visible?: boolean; 23 | destroyOnHidden?: boolean; 24 | mousePosition?: { 25 | x: number; 26 | y: number; 27 | } | null; 28 | title?: ReactNode; 29 | footer?: ReactNode; 30 | transitionName?: string; 31 | maskTransitionName?: string; 32 | animation?: any; 33 | maskAnimation?: any; 34 | wrapStyle?: Record; 35 | bodyStyle?: Record; 36 | maskStyle?: Record; 37 | prefixCls?: string; 38 | wrapClassName?: string; 39 | width?: string | number; 40 | height?: string | number; 41 | zIndex?: number; 42 | bodyProps?: any; 43 | maskProps?: any; 44 | rootClassName?: string; 45 | classNames?: ModalClassNames; 46 | styles?: ModalStyles; 47 | wrapProps?: any; 48 | getContainer?: GetContainer | false; 49 | closeIcon?: ReactNode; 50 | modalRender?: (node: ReactNode) => ReactNode; 51 | forceRender?: boolean; 52 | // https://github.com/ant-design/ant-design/issues/19771 53 | // https://github.com/react-component/dialog/issues/95 54 | focusTriggerAfterClose?: boolean; 55 | 56 | // Refs 57 | panelRef?: React.Ref; 58 | }; 59 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface RefContextProps { 4 | panel?: React.Ref; 5 | } 6 | 7 | export const RefContext = React.createContext({}); 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import DialogWrap from './DialogWrap'; 2 | import Panel from './Dialog/Content/Panel'; 3 | import type { IDialogPropTypes as DialogProps } from './IDialogPropTypes'; 4 | 5 | export type { DialogProps }; 6 | export { Panel }; 7 | 8 | export default DialogWrap; 9 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | // =============================== Motion =============================== 2 | export function getMotionName(prefixCls: string, transitionName?: string, animationName?: string) { 3 | let motionName = transitionName; 4 | if (!motionName && animationName) { 5 | motionName = `${prefixCls}-${animationName}`; 6 | } 7 | return motionName; 8 | } 9 | 10 | // =============================== Offset =============================== 11 | function getScroll(w: Window, top?: boolean): number { 12 | let ret = w[`page${top ? 'Y' : 'X'}Offset`]; 13 | const method = `scroll${top ? 'Top' : 'Left'}`; 14 | if (typeof ret !== 'number') { 15 | const d = w.document; 16 | ret = d.documentElement[method]; 17 | if (typeof ret !== 'number') { 18 | ret = d.body[method]; 19 | } 20 | } 21 | return ret; 22 | } 23 | 24 | type CompatibleDocument = { 25 | parentWindow?: Window; 26 | } & Document; 27 | 28 | export function offset(el: Element) { 29 | const rect = el.getBoundingClientRect(); 30 | const pos = { 31 | left: rect.left, 32 | top: rect.top, 33 | }; 34 | const doc = el.ownerDocument as CompatibleDocument; 35 | const w = doc.defaultView || doc.parentWindow; 36 | pos.left += getScroll(w); 37 | pos.top += getScroll(w, true); 38 | return pos; 39 | } 40 | -------------------------------------------------------------------------------- /tests/__snapshots__/index.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`dialog add rootClassName and rootStyle should render correct 1`] = ` 4 |
8 |
11 |
16 | 50 | `; 51 | 52 | exports[`dialog should render correct 1`] = ` 53 |
56 |
59 |
63 | 107 | `; 108 | 109 | exports[`dialog should support classNames 1`] = ` 110 |
113 |
116 |
120 | 170 | `; 171 | 172 | exports[`dialog should support styles 1`] = ` 173 |
176 |
180 |
185 | 240 | `; 241 | -------------------------------------------------------------------------------- /tests/index.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-render-return-value, max-classes-per-file, func-names, no-console */ 2 | import { fireEvent, render, act } from '@testing-library/react'; 3 | import { Provider } from '@rc-component/motion'; 4 | import KeyCode from '@rc-component/util/lib/KeyCode'; 5 | import React, { cloneElement, useEffect } from 'react'; 6 | import type { DialogProps } from '../src'; 7 | import Dialog from '../src'; 8 | 9 | jest.mock('@rc-component/motion', () => { 10 | const OriReact = jest.requireActual('react'); 11 | const origin = jest.requireActual('@rc-component/motion'); 12 | const OriCSSMotion = origin.default; 13 | 14 | const ProxyCSSMotion = OriReact.forwardRef((props: any, ref: any) => { 15 | global.onAppearPrepare = props.onAppearPrepare; 16 | 17 | return ; 18 | }); 19 | 20 | return { 21 | ...origin, 22 | default: ProxyCSSMotion, 23 | __esModule: true, 24 | }; 25 | }); 26 | 27 | describe('dialog', () => { 28 | async function runFakeTimer() { 29 | for (let i = 0; i < 100; i += 1) { 30 | await act(async () => { 31 | jest.advanceTimersByTime(100); 32 | await Promise.resolve(); 33 | }); 34 | } 35 | } 36 | 37 | beforeEach(() => { 38 | jest.useFakeTimers(); 39 | }); 40 | 41 | afterEach(() => { 42 | jest.clearAllTimers(); 43 | jest.useRealTimers(); 44 | }); 45 | 46 | it('should render correct', () => { 47 | render(); 48 | jest.runAllTimers(); 49 | 50 | expect(document.querySelector('.rc-dialog-root')).toMatchSnapshot(); 51 | }); 52 | 53 | it('add rootClassName and rootStyle should render correct', () => { 54 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); 55 | render( 56 | , 64 | ); 65 | jest.runAllTimers(); 66 | 67 | expect(document.querySelector('.rc-dialog-root')).toMatchSnapshot(); 68 | expect(spy).toHaveBeenCalledWith( 69 | `Warning: wrapStyle is deprecated, please use styles instead.`, 70 | ); 71 | expect(document.querySelector('.customize-root-class')).toBeTruthy(); 72 | expect(document.querySelector('.rc-dialog-wrap')).toHaveStyle('fontSize: 10px'); 73 | expect(document.querySelector('.rc-dialog-root')).toHaveStyle('fontSize: 20px'); 74 | expect(document.querySelector('.rc-dialog')).toHaveStyle('height: 903px'); 75 | expect(document.querySelector('.rc-dialog')).toHaveStyle('width: 600px'); 76 | }); 77 | 78 | it('show', () => { 79 | render(); 80 | jest.runAllTimers(); 81 | 82 | expect(document.querySelector('.rc-dialog-wrap')).toHaveStyle('display: block'); 83 | }); 84 | 85 | it('close', () => { 86 | const { rerender } = render(); 87 | jest.runAllTimers(); 88 | 89 | rerender(); 90 | jest.runAllTimers(); 91 | 92 | expect(document.querySelector('.rc-dialog-wrap')).toHaveStyle('display: none'); 93 | }); 94 | 95 | it('create & root & mask', () => { 96 | render(); 97 | jest.runAllTimers(); 98 | 99 | expect(document.querySelector('.rc-dialog-root')).toBeTruthy(); 100 | expect(document.querySelector('.rc-dialog-mask')).toBeTruthy(); 101 | }); 102 | 103 | it('click close', () => { 104 | const onClose = jest.fn(); 105 | render(); 106 | jest.runAllTimers(); 107 | 108 | const btn = document.querySelector('.rc-dialog-close'); 109 | expect(btn.textContent).toBe('test'); 110 | fireEvent.click(btn); 111 | 112 | jest.runAllTimers(); 113 | expect(onClose).toHaveBeenCalledTimes(1); 114 | }); 115 | 116 | describe('destroyOnHidden', () => { 117 | it('default is false', () => { 118 | const { rerender } = render( 119 | 120 | 121 | , 122 | ); 123 | 124 | rerender(); 125 | jest.runAllTimers(); 126 | 127 | expect(document.querySelectorAll('.test-destroy')).toHaveLength(1); 128 | }); 129 | 130 | it('destroy on hide should unmount child components on close', () => { 131 | const Demo: React.FC> = (props) => ( 132 | 133 | 134 | 135 | ); 136 | 137 | const { rerender } = render(); 138 | 139 | // Show 140 | rerender(); 141 | act(() => { 142 | jest.runAllTimers(); 143 | }); 144 | document.querySelector('.test-input').value = 'test'; 145 | expect(document.querySelector('.test-input')).toHaveValue('test'); 146 | 147 | // Hide 148 | rerender(); 149 | act(() => { 150 | jest.runAllTimers(); 151 | }); 152 | expect(document.querySelector('.test-input')).toBeFalsy(); 153 | 154 | // Show 155 | rerender(); 156 | act(() => { 157 | jest.runAllTimers(); 158 | }); 159 | 160 | expect(document.querySelector('.test-input')).toHaveValue(''); 161 | }); 162 | }); 163 | 164 | it('esc to close', () => { 165 | const onClose = jest.fn(); 166 | render(); 167 | jest.runAllTimers(); 168 | 169 | fireEvent.keyDown(document.querySelector('.rc-dialog'), { keyCode: KeyCode.ESC }); 170 | jest.runAllTimers(); 171 | expect(onClose).toHaveBeenCalled(); 172 | }); 173 | 174 | it('mask to close', () => { 175 | const onClose = jest.fn(); 176 | const { rerender } = render(); 177 | 178 | // Mask close 179 | fireEvent.click(document.querySelector('.rc-dialog-wrap')); 180 | jest.runAllTimers(); 181 | expect(onClose).toHaveBeenCalled(); 182 | onClose.mockReset(); 183 | 184 | // Mask can not close 185 | rerender(); 186 | fireEvent.click(document.querySelector('.rc-dialog-wrap')); 187 | jest.runAllTimers(); 188 | expect(onClose).not.toHaveBeenCalled(); 189 | }); 190 | 191 | it('renderToBody', () => { 192 | const container = document.createElement('div'); 193 | document.body.appendChild(container); 194 | render( 195 | 196 |

1

197 |
, 198 | { container }, 199 | ); 200 | 201 | act(() => { 202 | jest.runAllTimers(); 203 | }); 204 | 205 | expect(container.querySelector('.rc-dialog')).toBeFalsy(); 206 | expect(document.body.querySelector('.rc-dialog')).toBeTruthy(); 207 | 208 | document.body.removeChild(container); 209 | }); 210 | 211 | it('getContainer', () => { 212 | const returnedContainer = document.createElement('div'); 213 | render( 214 | returnedContainer}> 215 |

Hello world!

216 |
, 217 | ); 218 | 219 | expect(returnedContainer.querySelector('.rc-dialog')).toBeTruthy(); 220 | }); 221 | 222 | it('render title correctly', () => { 223 | render(); 224 | expect(document.querySelector('.rc-dialog-header').textContent).toBe('bamboo'); 225 | }); 226 | 227 | it('render footer correctly', () => { 228 | render(); 229 | expect(document.querySelector('.rc-dialog-footer').textContent).toBe('test'); 230 | }); 231 | 232 | // 失效了,需要修复 233 | it.skip('support input autoFocus', () => { 234 | render( 235 | 236 | 237 | , 238 | ); 239 | expect(document.querySelector('input')).toHaveFocus(); 240 | }); 241 | 242 | describe('Tab should keep focus in dialog', () => { 243 | it('basic tabbing', () => { 244 | render(); 245 | const sentinelEnd = document.querySelector('.rc-dialog > div:last-child'); 246 | sentinelEnd.focus(); 247 | 248 | fireEvent.keyDown(document.querySelector('.rc-dialog-wrap'), { 249 | keyCode: KeyCode.TAB, 250 | }); 251 | 252 | const sentinelStart = document.querySelector('.rc-dialog > div:first-child'); 253 | expect(document.activeElement).toBe(sentinelStart); 254 | }); 255 | 256 | it('trap focus after shift-tabbing', () => { 257 | render(); 258 | 259 | document.querySelector('.rc-dialog > div:first-child').focus(); 260 | 261 | fireEvent.keyDown(document.querySelector('.rc-dialog-wrap'), { 262 | keyCode: KeyCode.TAB, 263 | key: 'Tab', 264 | shiftKey: true, 265 | }); 266 | const sentinelEnd = document.querySelector('.rc-dialog > div:last-child'); 267 | expect(document.activeElement).toBe(sentinelEnd); 268 | }); 269 | }); 270 | 271 | describe('mousePosition', () => { 272 | function prepareModal(mousePosition: { x: number; y: number }) { 273 | const { container } = render( 274 | 275 |

the dialog

276 |
, 277 | ); 278 | 279 | // Trigger position align 280 | act(() => { 281 | global.onAppearPrepare?.(); 282 | }); 283 | 284 | return container; 285 | } 286 | 287 | it('sets transform-origin when property mousePosition is set', () => { 288 | prepareModal({ x: 100, y: 100 }); 289 | 290 | expect( 291 | document.querySelector('.rc-dialog').style['transform-origin'], 292 | ).toBeTruthy(); 293 | }); 294 | 295 | it('both undefined', () => { 296 | prepareModal({ x: undefined, y: undefined }); 297 | 298 | expect( 299 | document.querySelector('.rc-dialog').style['transform-origin'], 300 | ).toBeFalsy(); 301 | }); 302 | 303 | it('one valid', () => { 304 | prepareModal({ x: 10, y: 0 }); 305 | 306 | expect( 307 | document.querySelector('.rc-dialog').style['transform-origin'], 308 | ).toBeTruthy(); 309 | }); 310 | }); 311 | 312 | it('can get dom element before dialog first show when forceRender is set true ', () => { 313 | render( 314 | 315 |
forceRender element
316 |
, 317 | ); 318 | expect(document.querySelector('.rc-dialog-body > div').textContent).toEqual( 319 | 'forceRender element', 320 | ); 321 | }); 322 | 323 | describe('getContainer is false', () => { 324 | it('not set', () => { 325 | const { container } = render( 326 | 327 |
328 |
, 329 | ); 330 | 331 | expect(container.querySelector('.bamboo')).toBeFalsy(); 332 | expect(document.body.querySelector('.bamboo')).toBeTruthy(); 333 | }); 334 | 335 | it('set to false', () => { 336 | const { container } = render( 337 | 338 |
339 |
, 340 | ); 341 | 342 | expect(container.querySelector('.bamboo')).toBeTruthy(); 343 | }); 344 | }); 345 | 346 | it('should not close if mouse down in dialog', () => { 347 | const onClose = jest.fn(); 348 | render(); 349 | fireEvent.click(document.querySelector('.rc-dialog-body')); 350 | expect(onClose).not.toHaveBeenCalled(); 351 | }); 352 | 353 | it('zIndex', () => { 354 | render(); 355 | expect(document.querySelector('.rc-dialog-wrap')).toHaveStyle('z-index: 903'); 356 | }); 357 | 358 | it('should show dialog when initialize dialog, given forceRender and visible is true', () => { 359 | class DialogWrapTest extends React.Component { 360 | state = { 361 | visible: true, 362 | forceRender: true, 363 | }; 364 | 365 | render() { 366 | return ; 367 | } 368 | } 369 | 370 | render( 371 | 372 |
Show dialog with forceRender and visible is true
373 |
, 374 | ); 375 | 376 | act(() => { 377 | jest.runAllTimers(); 378 | }); 379 | 380 | expect(document.querySelector('.rc-dialog-wrap')).toHaveStyle('display: block'); 381 | }); 382 | 383 | it('modalRender', () => { 384 | render( 385 | ) => 388 | cloneElement(node, { ...node.props, style: { background: '#1890ff' } }) 389 | } 390 | />, 391 | ); 392 | expect(document.querySelector('.rc-dialog-section')).toHaveStyle('background: #1890ff'); 393 | }); 394 | 395 | describe('focusTriggerAfterClose', () => { 396 | it('should focus trigger after close dialog', () => { 397 | const Demo: React.FC = () => { 398 | const [visible, setVisible] = React.useState(false); 399 | return ( 400 | <> 401 | 402 | setVisible(false)}> 403 | content 404 | 405 | 406 | ); 407 | }; 408 | render(); 409 | const trigger = document.querySelector('button'); 410 | trigger.focus(); 411 | fireEvent.click(trigger); 412 | jest.runAllTimers(); 413 | const closeButton = document.querySelector('.rc-dialog-close'); 414 | fireEvent.click(closeButton); 415 | jest.runAllTimers(); 416 | expect(document.activeElement).toBe(trigger); 417 | }); 418 | 419 | it('should focus trigger after close dialog when contains focusable element', () => { 420 | const Demo: React.FC = () => { 421 | const [visible, setVisible] = React.useState(false); 422 | const inputRef = React.useRef(null); 423 | useEffect(() => { 424 | inputRef.current?.focus(); 425 | }, []); 426 | return ( 427 | <> 428 | 429 | setVisible(false)}> 430 | 431 | 432 | 433 | ); 434 | }; 435 | render(); 436 | const trigger = document.querySelector('button'); 437 | trigger.focus(); 438 | fireEvent.click(trigger); 439 | jest.runAllTimers(); 440 | const closeButton = document.querySelector('.rc-dialog-close'); 441 | fireEvent.click(closeButton); 442 | jest.runAllTimers(); 443 | expect(document.activeElement).toBe(trigger); 444 | }); 445 | }); 446 | 447 | describe('size should work', () => { 448 | it('width', () => { 449 | render(); 450 | expect(document.querySelector('.rc-dialog')).toHaveStyle('width: 1128px'); 451 | }); 452 | 453 | it('height', () => { 454 | render(); 455 | expect(document.querySelector('.rc-dialog')).toHaveStyle('height: 903px'); 456 | }); 457 | }); 458 | 459 | describe('re-render', () => { 460 | function createWrapper( 461 | props?: Partial, 462 | ): [ 463 | container: HTMLElement, 464 | getRenderTimes: () => number, 465 | updateProps: (props?: Partial) => void, 466 | ] { 467 | let renderTimes = 0; 468 | const RenderChecker = () => { 469 | renderTimes += 1; 470 | return null; 471 | }; 472 | 473 | const Demo = (demoProps?: any) => { 474 | return ( 475 | 476 | 477 | 478 | ); 479 | }; 480 | 481 | const { container, rerender } = render(); 482 | 483 | return [ 484 | container, 485 | () => renderTimes, 486 | (nextProps) => { 487 | rerender(); 488 | }, 489 | ]; 490 | } 491 | 492 | it('should not re-render when visible changed', () => { 493 | const [, getRenderTimes, updateProps] = createWrapper(); 494 | const lastRenderTimes = getRenderTimes(); 495 | expect(getRenderTimes()).toBeGreaterThan(0); 496 | 497 | // Hidden should not trigger render 498 | updateProps({ visible: false }); 499 | expect(getRenderTimes()).toEqual(lastRenderTimes); 500 | }); 501 | 502 | it('should re-render when forceRender', () => { 503 | const [, getRenderTimes, updateProps] = createWrapper({ forceRender: true }); 504 | const lastRenderTimes = getRenderTimes(); 505 | expect(getRenderTimes()).toBeGreaterThan(0); 506 | 507 | // Hidden should not trigger render 508 | updateProps({ visible: false }); 509 | expect(getRenderTimes()).toBeGreaterThan(lastRenderTimes); 510 | }); 511 | }); 512 | 513 | describe('afterClose', () => { 514 | it('should trigger afterClose when set visible to false', () => { 515 | const afterClose = jest.fn(); 516 | 517 | const { rerender } = render(); 518 | act(() => { 519 | jest.runAllTimers(); 520 | }); 521 | 522 | rerender(); 523 | act(() => { 524 | jest.runAllTimers(); 525 | }); 526 | 527 | expect(afterClose).toHaveBeenCalledTimes(1); 528 | }); 529 | 530 | it('should not trigger afterClose when mount dialog of getContainer={false}', () => { 531 | const afterClose = jest.fn(); 532 | 533 | const { container } = render(); 534 | jest.runAllTimers(); 535 | 536 | render(, { 537 | container, 538 | }); 539 | jest.runAllTimers(); 540 | 541 | expect(afterClose).toHaveBeenCalledTimes(0); 542 | }); 543 | 544 | it('should not trigger afterClose when mount dialog of forceRender={true}', () => { 545 | const afterClose = jest.fn(); 546 | 547 | const { container } = render(); 548 | jest.runAllTimers(); 549 | 550 | render(, { container }); 551 | jest.runAllTimers(); 552 | 553 | expect(afterClose).toHaveBeenCalledTimes(0); 554 | }); 555 | }); 556 | 557 | describe('afterOpenChange', () => { 558 | beforeEach(() => { 559 | jest.useFakeTimers(); 560 | }); 561 | 562 | afterEach(() => { 563 | jest.clearAllTimers(); 564 | jest.useRealTimers(); 565 | }); 566 | 567 | it('should trigger afterOpenChange when visible changed', async () => { 568 | const afterOpenChange = jest.fn(); 569 | 570 | const Demo = (props: any) => ( 571 | 572 | 573 | 574 | ); 575 | 576 | const { rerender } = render(); 577 | await runFakeTimer(); 578 | expect(afterOpenChange).toHaveBeenCalledWith(true); 579 | expect(afterOpenChange).toHaveBeenCalledTimes(1); 580 | 581 | rerender(); 582 | await runFakeTimer(); 583 | expect(afterOpenChange).toHaveBeenCalledWith(false); 584 | expect(afterOpenChange).toHaveBeenCalledTimes(2); 585 | }); 586 | }); 587 | 588 | it('should support classNames', () => { 589 | render( 590 | , 605 | ); 606 | jest.runAllTimers(); 607 | 608 | expect(document.querySelector('.rc-dialog-root')).toMatchSnapshot(); 609 | expect(document.querySelector('.rc-dialog-wrap').className).toContain('custom-wrapper'); 610 | expect(document.querySelector('.rc-dialog-body').className).toContain('custom-body'); 611 | expect(document.querySelector('.rc-dialog-header').className).toContain('custom-header'); 612 | expect(document.querySelector('.rc-dialog-footer').className).toContain('custom-footer'); 613 | expect(document.querySelector('.rc-dialog-mask').className).toContain('custom-mask'); 614 | expect(document.querySelector('.rc-dialog-section').className).toContain('custom-section'); 615 | }); 616 | 617 | it('should support styles', () => { 618 | render( 619 | , 635 | ); 636 | jest.runAllTimers(); 637 | 638 | expect(document.querySelector('.rc-dialog-root')).toMatchSnapshot(); 639 | expect(document.querySelector('.rc-dialog-wrap')).toHaveStyle('background: pink'); 640 | expect(document.querySelector('.rc-dialog-body')).toHaveStyle('background: green'); 641 | expect(document.querySelector('.rc-dialog-header')).toHaveStyle('background: red'); 642 | expect(document.querySelector('.rc-dialog-footer')).toHaveStyle('background: blue'); 643 | expect(document.querySelector('.rc-dialog-mask')).toHaveStyle('background: yellow'); 644 | expect(document.querySelector('.rc-dialog-section')).toHaveStyle('background: orange'); 645 | expect(document.querySelector('.rc-dialog-title')).toHaveStyle('background: orange'); 646 | }); 647 | 648 | it('should warning', () => { 649 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); 650 | render( 651 | , 661 | ); 662 | jest.runAllTimers(); 663 | 664 | expect(spy).toHaveBeenCalledWith( 665 | `Warning: bodyStyle is deprecated, please use styles instead.`, 666 | ); 667 | expect(spy).toHaveBeenCalledWith( 668 | `Warning: maskStyle is deprecated, please use styles instead.`, 669 | ); 670 | expect(spy).toHaveBeenCalledWith( 671 | `Warning: wrapClassName is deprecated, please use classNames instead.`, 672 | ); 673 | spy.mockRestore(); 674 | }); 675 | 676 | it('support aria-* in closable', () => { 677 | const onClose = jest.fn(); 678 | render( 679 | , 687 | ); 688 | jest.runAllTimers(); 689 | 690 | const btn = document.querySelector('.rc-dialog-close'); 691 | expect(btn.textContent).toBe('test'); 692 | expect(btn.getAttribute('aria-label')).toBe('test aria-label'); 693 | fireEvent.click(btn); 694 | 695 | jest.runAllTimers(); 696 | expect(onClose).toHaveBeenCalledTimes(1); 697 | }); 698 | 699 | it('support disable button in closable', () => { 700 | const onClose = jest.fn(); 701 | render( 702 | , 710 | ); 711 | jest.runAllTimers(); 712 | 713 | const btn = document.querySelector('.rc-dialog-close'); 714 | expect(btn.disabled).toBeTruthy(); 715 | fireEvent.click(btn); 716 | 717 | jest.runAllTimers(); 718 | expect(onClose).not.toHaveBeenCalled(); 719 | 720 | fireEvent.keyDown(btn, { key: 'Enter' }); 721 | jest.runAllTimers(); 722 | expect(onClose).not.toHaveBeenCalled(); 723 | }); 724 | 725 | it('should not display closeIcon when closable is false', () => { 726 | const onClose = jest.fn(); 727 | render(); 728 | 729 | act(() => { 730 | jest.runAllTimers(); 731 | }); 732 | 733 | expect(document.querySelector('.rc-dialog')).toBeTruthy(); 734 | expect(document.querySelector('.rc-dialog-close')).toBeFalsy(); 735 | }); 736 | }); 737 | -------------------------------------------------------------------------------- /tests/portal.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-render-return-value, max-classes-per-file, func-names, no-console */ 2 | import React from 'react'; 3 | import Select from '@rc-component/select'; 4 | import { render, fireEvent } from '@testing-library/react'; 5 | import Dialog from '../src'; 6 | 7 | /** 8 | * Since overflow scroll test need a clear env which may affect by other test. 9 | * Use a clean env instead. 10 | */ 11 | describe('Dialog.Portal', () => { 12 | beforeEach(() => { 13 | jest.useFakeTimers(); 14 | }); 15 | 16 | afterEach(() => { 17 | jest.useRealTimers(); 18 | }); 19 | 20 | it('event should bubble', () => { 21 | const onClose = jest.fn(); 22 | 23 | render( 24 | 25 | 28 | , 29 | ); 30 | 31 | jest.runAllTimers(); 32 | 33 | fireEvent.mouseDown(document.querySelector('.rc-dialog-section')); 34 | fireEvent.click(document.querySelector('.rc-select-item-option-content')); 35 | fireEvent.mouseUp(document.querySelector('.rc-dialog-section')); 36 | expect(onClose).not.toHaveBeenCalled(); 37 | }); 38 | 39 | it('dialog dont close when mouseDown in content and mouseUp in wrap', () => { 40 | const onClose = jest.fn(); 41 | 42 | render( 43 | 44 | content 45 | , 46 | ); 47 | 48 | jest.runAllTimers(); 49 | 50 | fireEvent.mouseDown(document.querySelector('.rc-dialog-section')); 51 | fireEvent.mouseUp(document.querySelector('.rc-dialog-wrap')); 52 | expect(onClose).not.toHaveBeenCalled(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/ref.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-render-return-value, max-classes-per-file, func-names, no-console */ 2 | import { render } from '@testing-library/react'; 3 | import { Provider } from '@rc-component/motion'; 4 | import React from 'react'; 5 | import Dialog from '../src'; 6 | 7 | describe('Dialog.ref', () => { 8 | beforeEach(() => { 9 | jest.useFakeTimers(); 10 | }); 11 | 12 | afterEach(() => { 13 | jest.clearAllTimers(); 14 | jest.useRealTimers(); 15 | }); 16 | 17 | it('support panelRef', () => { 18 | const panelRef = React.createRef(); 19 | 20 | render( 21 | 22 | 23 | , 24 | ); 25 | 26 | expect(panelRef.current).toHaveClass('rc-dialog'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/scroll.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-render-return-value, max-classes-per-file, func-names, no-console */ 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import Dialog from '../src'; 5 | 6 | /** 7 | * Since overflow scroll test need a clear env which may affect by other test. 8 | * Use a clean env instead. 9 | */ 10 | describe('Dialog.Scroll', () => { 11 | beforeEach(() => { 12 | jest.useFakeTimers(); 13 | }); 14 | 15 | afterEach(() => { 16 | jest.useRealTimers(); 17 | }); 18 | 19 | it('Single Dialog body overflow set correctly', () => { 20 | const { unmount, rerender } = render(); 21 | 22 | expect(document.body).toHaveStyle({ 23 | overflowY: 'hidden', 24 | }); 25 | 26 | rerender(); 27 | expect(document.body).not.toHaveStyle({ 28 | overflowY: 'hidden', 29 | }); 30 | 31 | // wrapper.unmount(); 32 | unmount(); 33 | }); 34 | 35 | it('Multiple Dialog body overflow set correctly', () => { 36 | const Demo = ({ visible = false, visible2 = false, ...restProps }) => ( 37 |
38 | 39 | 40 |
41 | ); 42 | 43 | const { rerender, unmount } = render(); 44 | 45 | expect(document.querySelector('.rc-dialog')).toBeFalsy(); 46 | 47 | rerender(); 48 | expect(document.querySelectorAll('.rc-dialog')).toHaveLength(1); 49 | expect(document.body).toHaveStyle({ 50 | overflowY: 'hidden', 51 | }); 52 | 53 | rerender(); 54 | expect(document.querySelectorAll('.rc-dialog')).toHaveLength(2); 55 | expect(document.body).toHaveStyle({ 56 | overflowY: 'hidden', 57 | }); 58 | 59 | rerender(); 60 | expect(document.body).not.toHaveStyle({ 61 | overflowY: 'hidden', 62 | }); 63 | 64 | rerender(); 65 | expect(document.body).toHaveStyle({ 66 | overflowY: 'hidden', 67 | }); 68 | 69 | rerender(); 70 | expect(document.body).toHaveStyle({ 71 | overflowY: 'hidden', 72 | }); 73 | 74 | rerender(); 75 | expect(document.body).not.toHaveStyle({ 76 | overflowY: 'hidden', 77 | }); 78 | 79 | unmount(); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | global.requestAnimationFrame = (cb) => { 3 | return global.setTimeout(cb, 0); 4 | }; 5 | global.cancelAnimationFrame = (cb) => { 6 | return global.clearTimeout(cb, 0); 7 | }; 8 | window.requestAnimationFrame = (cb) => { 9 | return window.setTimeout(cb, 0); 10 | }; 11 | window.cancelAnimationFrame = (cb) => { 12 | return window.clearTimeout(cb, 0); 13 | }; 14 | 15 | const originError = console.error; 16 | const ignoreList = [ 17 | 'Rendering components directly into document.body', 18 | 'Warning: unmountComponentAtNode():', 19 | ]; 20 | console.error = (...args) => { 21 | if (ignoreList.some((str) => String(args[0]).includes(str))) { 22 | return; 23 | } 24 | 25 | originError(...args); 26 | }; 27 | -------------------------------------------------------------------------------- /tests/setupFilesAfterEnv.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /tests/util.spec.tsx: -------------------------------------------------------------------------------- 1 | import { getMotionName, offset } from '../src/util'; 2 | 3 | describe('Dialog.Util', () => { 4 | describe('offset', () => { 5 | it('window do not have size', () => { 6 | const window = { 7 | document: { 8 | documentElement: { 9 | scrollTop: 1128, 10 | scrollLeft: 903, 11 | }, 12 | }, 13 | }; 14 | 15 | const element = { 16 | ownerDocument: { 17 | parentWindow: window, 18 | }, 19 | getBoundingClientRect: () => ({ left: 0, top: 0 }), 20 | } as any; 21 | 22 | expect(offset(element)).toEqual({ 23 | left: 903, 24 | top: 1128, 25 | }); 26 | }); 27 | 28 | it('window & document do not have size', () => { 29 | const window = { 30 | document: { 31 | documentElement: {}, 32 | body: { 33 | scrollTop: 1128, 34 | scrollLeft: 903, 35 | }, 36 | }, 37 | }; 38 | 39 | const element = { 40 | ownerDocument: { 41 | parentWindow: window, 42 | }, 43 | getBoundingClientRect: () => ({ left: 0, top: 0 }), 44 | } as any; 45 | 46 | expect(offset(element)).toEqual({ 47 | left: 903, 48 | top: 1128, 49 | }); 50 | }); 51 | }); 52 | 53 | describe('getMotionName', () => { 54 | it('transitionName', () => { 55 | expect(getMotionName('test', 'transition', 'animation')).toEqual('transition'); 56 | }); 57 | 58 | it('animation', () => { 59 | expect(getMotionName('test', null, 'animation')).toEqual('test-animation'); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "baseUrl": "./", 6 | "jsx": "preserve", 7 | "declaration": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "paths": { 11 | "@/*": [ 12 | "src/*" 13 | ], 14 | "@@/*": [ 15 | ".dumi/tmp/*" 16 | ], 17 | "@rc-component/dialog": [ 18 | "src/index.ts" 19 | ] 20 | } 21 | }, 22 | "include": [ 23 | ".dumirc.ts", 24 | "./src/**/*.ts", 25 | "./src/**/*.tsx", 26 | "./docs/**/*.tsx", 27 | "./tests/**/*.tsx" 28 | ] 29 | } -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | --------------------------------------------------------------------------------