├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.js ├── .storybook ├── addons.js ├── config.js ├── main.ts ├── preview-head.html └── webpack.config.js ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── bg.png ├── dist │ ├── .keep │ └── bundle.js ├── index.html ├── screenshot.gif └── src │ ├── example.js │ └── index.js ├── lib └── .keep ├── logo.png ├── package.json ├── playwright-ct.config.ts ├── playwright ├── index.html └── index.tsx ├── pnpm-lock.yaml ├── renovate.json ├── scripts ├── deploy-minor.sh ├── deploy-patch.sh ├── dev.js ├── prod.common.js ├── prod.es5.js └── prod.js ├── src ├── index.spec.tsx ├── index.tsx └── resizer.tsx ├── stories ├── aspect.stories.tsx ├── auto.stories.tsx ├── basic.stories.tsx ├── bounds.stories.tsx ├── extra.stories.tsx ├── flex.stories.tsx ├── grid.stories.tsx ├── handle.stories.tsx ├── max.stories.tsx ├── min.stories.tsx ├── multiple.stories.tsx ├── nested.stories.tsx ├── ratio.stories.tsx ├── scaled.stories.tsx ├── size.stories.tsx ├── snap.stories.tsx ├── style.ts ├── styles.css ├── vwvh.stories.tsx └── wrapper.stories.tsx ├── test └── fixture.html ├── tsconfig.json ├── tsconfig.test.json └── tslint.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [bokuweb] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ### Overview of the problem 7 | 8 | 9 | I'm using re-resizable **version** [x.x.x] 10 | 11 | 12 | 13 | My **browser** is: 14 | 15 | I am sure this issue is **not a duplicate**? 16 | 17 | ### Description 18 | 19 | 20 | 21 | ### Steps to Reproduce 22 | 23 | 1. First Step 24 | 2. Second Step 25 | 3. and so on... 26 | 27 | https://codesandbox.io/s/ll587k677z 28 | 29 | ### Expected behavior 30 | 31 | 32 | 33 | ### Actual behavior 34 | 35 | 36 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ### Proposed solution 5 | 6 | 7 | 8 | ### Tradeoffs 9 | 10 | 11 | 12 | 13 | ### Testing Done 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - uses: actions/setup-node@master 10 | with: 11 | node-version: 20 12 | - uses: pnpm/action-setup@v2 13 | with: 14 | version: 9 15 | - name: Install dependencies 16 | run: pnpm i --frozen-lockfile 17 | - name: lint 18 | run: pnpm lint 19 | 20 | tsc: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@master 24 | - uses: actions/setup-node@master 25 | with: 26 | node-version: 20 27 | - uses: pnpm/action-setup@v2 28 | with: 29 | version: 9 30 | - name: Install dependencies 31 | run: pnpm i --frozen-lockfile 32 | - name: tsc 33 | run: pnpm tsc 34 | 35 | test: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@master 39 | - uses: actions/setup-node@master 40 | with: 41 | node-version: 20 42 | - uses: pnpm/action-setup@v2 43 | with: 44 | version: 9 45 | - name: Install dependencies 46 | run: pnpm i --frozen-lockfile 47 | - name: test 48 | run: npm exec playwright install && pnpm test 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | example/dist/ 3 | lib/ 4 | npm-debug.log 5 | .rpt2_cache 6 | /test-results/ 7 | /playwright-report/ 8 | /blob-report/ 9 | /playwright/.cache/ 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useTabs: false, 3 | printWidth: 120, 4 | tabWidth: 2, 5 | singleQuote: true, 6 | trailingComma: "all", 7 | jsxBracketSameLine: false, 8 | parser: "typescript", 9 | semi: true 10 | }; 11 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | // import "@storybook/addon-options/register"; 2 | // import "@storybook/addon-actions/register"; 3 | 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | const req = require.context('../stories', true, /.stories.(ts|tsx)$/); 4 | 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 5 | addons: [ 6 | '@storybook/addon-onboarding', 7 | '@storybook/addon-essentials', 8 | '@chromatic-com/storybook', 9 | '@storybook/addon-interactions', 10 | ], 11 | framework: { 12 | name: '@storybook/react-vite', 13 | options: {}, 14 | }, 15 | }; 16 | export default config; 17 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = async ({ config, mode }) => { 4 | config.module.rules.push({ 5 | test: /.*\.(ts|tsx|js|jsx)$/, 6 | loader: require.resolve('babel-loader'), 7 | }); 8 | 9 | config.resolve.extensions.push('.ts', '.tsx', '.js', '.jsx'); 10 | 11 | return config; 12 | }; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | addons: 5 | apt: 6 | packages: 7 | - xvfb 8 | before_install: 9 | - export DISPLAY=':99.0' 10 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 11 | 12 | install: 13 | - sudo apt -y install libgconf2-4 14 | - yarn install --pure-lockfile 15 | 16 | script: 17 | - yarn run tsc 18 | - yarn run lint 19 | - yarn run test 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 11 | 12 | 23 | 24 | ## [6.11.2 (2025-02-23)](https://github.com/bokuweb/re-resizable/compare/6.11.2...6.10.4) 25 | 26 | ### :nail_care: Enhancement 27 | 28 | - fix: Add z-index: 1 for edge resizer 29 | - fix: Use stored delta in onResizeStop 30 | 31 | ## [6.10.4 (2025-02-23)](https://github.com/bokuweb/re-resizable/compare/6.10.4...6.10.3) 32 | 33 | ### :nail_care: Enhancement 34 | 35 | - fix: generated JSX namespace not working with React 19 36 | 37 | ## [6.10.1 (2024-11-06)](https://github.com/bokuweb/re-resizable/compare/6.10.0...6.10.1) 38 | 39 | ### :bug: Bug Fix 40 | 41 | - Revert #827 because regression 42 | 43 | ## [6.10.0 (2024-09-22)](https://github.com/bokuweb/re-resizable/compare/6.9.18...6.10.0) 44 | 45 | ### :nail_care: Enhancement 46 | 47 | - Match focus order to visual order #827 48 | 49 | ## [6.9.18 (2024-09-10)](https://github.com/bokuweb/re-resizable/compare/6.9.16...6.9.18) 50 | 51 | ### :nail_care: Enhancement 52 | 53 | - added grid gap prop to support adding any gaps to width/height. Fixes #822 54 | 55 | ### :bug: Bug Fix 56 | 57 | - fix-boundary-scale: adds scaling to boundary Fixes #820 58 | 59 | ## [6.9.17 (2024-05-25)] 60 | 61 | - Define callback refs inline to work with latest versions of Next.js / React #819 62 | 63 | ## [6.9.16 (2024-04-25)](https://github.com/bokuweb/re-resizable/compare/v6.9.16...v6.9.14) 64 | 65 | ### :nail_care: Enhancement 66 | 67 | - Fixes resizable field always being null in React 18.3. 68 | 69 | ## [6.9.14 (2024-04-21)](https://github.com/bokuweb/re-resizable/compare/v6.9.14...v6.9.11) 70 | 71 | ### :bug: Bug Fix 72 | 73 | - Fixed a bug, onResize fired before snapping to grid #783 74 | 75 | ## [6.9.11 (2023-08-10)](https://github.com/bokuweb/re-resizable/compare/v6.9.11...v6.9.9) 76 | 77 | ### :nail_care: Enhancement 78 | 79 | - improve `enable` type. 80 | 81 | ## [6.9.9 (2022-04-26)](https://github.com/bokuweb/re-resizable/compare/v6.9.9...v6.9.8) 82 | 83 | ### :nail_care: Enhancement 84 | 85 | - use native `endsWith`. 86 | - remove `fast-memoize`. 87 | 88 | ## [6.9.8 (2022-04-22)](https://github.com/bokuweb/re-resizable/compare/v6.9.8...v6.9.6) 89 | 90 | ### :nail_care: Enhancement 91 | 92 | - use `flushSync` in mouseMove. 93 | 94 | ## [6.9.6 (2022-04-22)](https://github.com/bokuweb/re-resizable/compare/v6.9.5...v6.9.6) 95 | 96 | ### :nail_care: Enhancement 97 | 98 | - add `react` and `react-dom` to peer deps. 99 | 100 | ## [6.9.5 (2022-03-14)](https://github.com/bokuweb/re-resizable/compare/v6.9.2...v6.9.3) 101 | 102 | ### :bug: Bug Fix: Fixed a bug, calculate parent height even when the parent is a flex container [#765](https://github.com/bokuweb/re-resizable/pull/765) 103 | 104 | ## [6.9.2 (2022-02-24)](https://github.com/bokuweb/re-resizable/compare/v6.9.1...v6.9.2) 105 | 106 | ### :bug: Bug Fix: Fixed a bug, `lockAspectRatio` is not work when `snap` is set. [#759](https://github.com/bokuweb/re-resizable/pull/759) 107 | 108 | ## [6.9.1 (2021-09-14)](https://github.com/bokuweb/re-resizable/compare/v6.8.0...v6.9.1) 109 | 110 | ### :nail_care: Enhancement 111 | 112 | - Fixed a issue, using CTRL when resizing doesn't work #747 113 | 114 | ## [6.8.0 (2021-11-05)](https://github.com/bokuweb/re-resizable/compare/v6.6.1...v6.8.0) 115 | 116 | ### :nail_care: Enhancement 117 | 118 | - Feature: boundsByDirection #689 119 | 120 | ## [6.6.1 (2020-09-22)](https://github.com/bokuweb/re-resizable/compare/v6.6.0...v6.6.1) 121 | 122 | ### :bug: Bug Fix 123 | 124 | - Fixed a bug, resize is not work when set `xxxpx` to max/min width/height 125 | 126 | ## [6.6.0 (2020-09-22)](https://github.com/bokuweb/re-resizable/compare/v6.5.5...v6.6.0) 127 | 128 | ### :bug: Bug Fix 129 | 130 | - Fixed a bug, a base element is removed even though there are other resizable components. (#667) 131 | 132 | ### :nail_care: Enhancement 133 | 134 | - Expose `NumberSize`. 135 | 136 | ## [6.5.5 (2020-08-28)](https://github.com/bokuweb/re-resizable/compare/v6.5.4...v6.5.5) 137 | 138 | ### :nail_care: Enhancement 139 | 140 | - fix: instanceof check fails when window is a proxy (#659) 141 | 142 | ## [6.5.4 (2020-07-13)](https://github.com/bokuweb/re-resizable/compare/v6.5.2...v6.5.4) 143 | 144 | ### :bug: Bug Fix 145 | 146 | - Fixed a bug, when touched in mobile some execption throwed. 147 | 148 | ## [6.5.2 (2020-06-26)](https://github.com/bokuweb/re-resizable/compare/v6.5.1...v6.5.2) 149 | 150 | ### :bug: Bug Fix 151 | 152 | - Fixes #522 - Resize without page scrolling on mobile 153 | 154 | ## [6.5.1 (2020-06-25)](https://github.com/bokuweb/re-resizable/compare/v6.5.0...v6.5.1) 155 | 156 | ### :bug: Bug Fix 157 | 158 | - Make `as` optional 159 | 160 | ## [6.5.0 (2020-06-17)](https://github.com/bokuweb/re-resizable/compare/v6.4.0...v6.5.0) 161 | 162 | ### :bug: Bug Fix 163 | 164 | - Fix ES Module Output #634 165 | 166 | ## [6.4.0 (2020-05-14)](https://github.com/bokuweb/re-resizable/compare/v6.3.2...v6.4.0) 167 | 168 | ### :nail_care: Enhancement 169 | 170 | - Support the "as" prop to change the wrapper #614 171 | 172 | ## [6.3.2 (2020-03-28)](https://github.com/bokuweb/re-resizable/compare/v6.3.1...v6.3.2) 173 | 174 | ### :nail_care: Enhancement 175 | 176 | - Avoid a useless re-render #587 177 | 178 | 179 | ## [6.3.1 (2020-03-28)](https://github.com/bokuweb/re-resizable/compare/v6.2.0...v6.3.1) 180 | 181 | ### :nail_care: Enhancement 182 | 183 | - Makes the component window agnostic, which means that the component can be run inside an iframe. (#598) 184 | 185 | 186 | ## [6.2.0 (2020-02-05)](https://github.com/bokuweb/re-resizable/compare/v6.1.1...v6.2.0) 187 | 188 | ### :bug: Bug Fix 189 | 190 | - Fixed a bug, resizing does not work when flex-basis set. 191 | 192 | ## [6.1.1 (2019-11-30)](https://github.com/bokuweb/re-resizable/compare/v6.1.0...v6.1.1) 193 | 194 | ### :bug: Bug Fix 195 | 196 | - Fixed a bug, `Handle loses mouse as edge gets further away from other side #537` 197 | 198 | ## [6.1.0 (2019-09-28)](https://github.com/bokuweb/re-resizable/compare/v6.0.0...v6.1.0) 199 | 200 | ### :nail_care: Enhancement 201 | 202 | - Improve perf #529 203 | - Support `vh` and `vw` for max size #526 204 | 205 | ## [6.0.0 (2019-08-12)](https://github.com/bokuweb/re-resizable/compare/v5.0.0...v6.0.0) 206 | 207 | ### :nail_care: Enhancement 208 | 209 | - Fix deprecated componentWillRecieveProps lifecycle method usage #504 210 | - Feature request: Allow early exiting for onResizeStart #494 211 | 212 | ### :zap: Breaking changes 213 | 214 | - use `PureComponent` 215 | 216 | ## [5.0.0 (2019-06-05)](https://github.com/bokuweb/re-resizable/compare/v5.0.0-beta.0...v5.0.0) 217 | 218 | Please see also 5.0.0-beta.0 change. 219 | 220 | ### :nail_care: Enhancement 221 | 222 | - Add `snapGap` property #446 223 | 224 | ### :house: Internal 225 | 226 | - Upgrade some deps. 227 | 228 | ## [5.0.0-beta.0 (2019-03-17)](https://github.com/bokuweb/re-resizable/compare/v4.11.0...v5.0.0-beta.0) 229 | 230 | ### :nail_care: Enhancement 231 | 232 | - Use typeScript instead of flowtype in [#413] 233 | - Improve some perf. 234 | - Support `vw` and `vh`. Please see [story](https://bokuweb.github.io/re-resizable/?selectedKind=vw%20vh&selectedStory=vw&full=0&addons=1&stories=1&panelRight=0). 235 | 236 | ### :zap: Breaking changes 237 | 238 | - Support only named import. Please import like following. 239 | 240 | ``` 241 | import { Resizable } from 're-resizable'; 242 | ``` 243 | 244 | ### :memo: Documentation 245 | 246 | - Extract LICENSE from README file ([@MichaelDeBoey](https://github.com/MichaelDeBoey) in [#397](https://github.com/bokuweb/re-resizable/pull/397)) 247 | - Extract CHANGELOG from README file ([@MichaelDeBoey](https://github.com/MichaelDeBoey) in [#397](https://github.com/bokuweb/re-resizable/pull/397)) 248 | 249 | ### :house: Internal 250 | - Update `react` & `react-dom` to `v16.7.0` ([#395](https://github.com/bokuweb/re-resizable/pull/395)) 251 | 252 | 253 | ## [4.11.0 (2018-12-14)](https://github.com/bokuweb/re-resizable/compare/v4.10.0...v4.11.0) 254 | 255 | ### :rocket: New Feature 256 | - Add `resizeRatio` prop ([@martinmcneela](https://github.com/martinmcneela) in [#391](https://github.com/bokuweb/re-resizable/pull/391) & [@bokuweb](https://github.com/bokuweb) in [31ce82b2](https://github.com/bokuweb/re-resizable/commit/31ce82b219238de82034c3e8bc8b3acc9cc51dde)) 257 | 258 | ### :house: Internal 259 | - Update `npm-run-all` to `v4.1.5` ([#389](https://github.com/bokuweb/re-resizable/pull/389)) 260 | - Update `react` & `react-dom` to `v16.6.3` ([#387](https://github.com/bokuweb/re-resizable/pull/387)) 261 | - Update `sinon` to `v7.2.2` ([#393](https://github.com/bokuweb/re-resizable/pull/393)) 262 | - Update `rollup-plugin-node-resolve` to `v4.0.0` ([#392](https://github.com/bokuweb/re-resizable/pull/392)) 263 | - Update `flow-bin` to `v0.89.0` ([#385](https://github.com/bokuweb/re-resizable/pull/385)) 264 | - Update `prettier` to `v1.15.3` ([#386](https://github.com/bokuweb/re-resizable/pull/386)) 265 | 266 | ## [4.10.0 (2018-11-16)](https://github.com/bokuweb/re-resizable/compare/v4.9.3...v4.10.0) 267 | 268 | ### :rocket: New Feature 269 | - Add `scale` prop ([@wootencl](https://github.com/wootencl) in [#391](https://github.com/bokuweb/re-resizable/pull/391) & [@bokuweb](https://github.com/bokuweb) in [6825ed9a](https://github.com/bokuweb/re-resizable/commit/6825ed9a3166fa5c5990ea96852ed1c21e436eb6)) 270 | 271 | ### :house: Internal 272 | - Update `react` & `react-dom` to `v16.6.1` ([#384](https://github.com/bokuweb/re-resizable/pull/384)) 273 | - Update `prettier` to `v1.15.1` ([#383](https://github.com/bokuweb/re-resizable/pull/383)) 274 | - Update `sinon` to `v7.1.1` ([#379](https://github.com/bokuweb/re-resizable/pull/379)) 275 | - Update `flow-bin` to `v0.85.0` ([#378](https://github.com/bokuweb/re-resizable/pull/378)) 276 | - Update `eslint-plugin-flowtype` to `v3.2.0` ([#375](https://github.com/bokuweb/re-resizable/pull/375)) 277 | - Update `rollup-plugin-node-globals` to `v1.4.0` ([#344](https://github.com/bokuweb/re-resizable/pull/344)) 278 | 279 | ## [4.9.3 (2018-11-06)](https://github.com/bokuweb/re-resizable/compare/v4.9.2...v4.9.3) 280 | 281 | ### :bug: Bug Fix 282 | - Don't add `px` when setting `scale` to `auto` ([@jrainville](https://github.com/jrainville) in [#382](https://github.com/bokuweb/re-resizable/pull/382) & [@bokuweb](https://github.com/bokuweb) in [62254a2b](https://github.com/bokuweb/re-resizable/commit/62254a2b11f7e6a420487d10b41991d8b558edfe)) 283 | 284 | ### :house: Internal 285 | - Update `sinon` to `v7.1.0` ([#373](https://github.com/bokuweb/re-resizable/pull/373)) 286 | - Update `react` & `react-dom` to `v16.6.0` ([#371](https://github.com/bokuweb/re-resizable/pull/371)) 287 | - Update `gh-pages` to `v2.0.1` ([#352](https://github.com/bokuweb/re-resizable/pull/352)) 288 | - Update `flow-bin` to `v0.84.0` ([#342](https://github.com/bokuweb/re-resizable/pull/342)) 289 | 290 | ## [4.9.2 (2018-10-26)](https://github.com/bokuweb/re-resizable/compare/v4.9.1...v4.9.2) 291 | 292 | ### :bug: Bug Fix 293 | - Fix initial left position of element for Safari ([@jnelson180](https://github.com/jnelson180) in [#374](https://github.com/bokuweb/re-resizable/pull/374) & [@bokuweb](https://github.com/bokuweb) in [54d86200](https://github.com/bokuweb/re-resizable/commit/54d86200562fe6f3e95396679264318ee8a9f7c9)) 294 | 295 | ### :house: Internal 296 | - Update `eslint-plugin-jsx-a11y` to `v6.1.2` ([#363](https://github.com/bokuweb/re-resizable/pull/363)) 297 | - Update `react` & `react-dom` to `v16.5.2` ([#357](https://github.com/bokuweb/re-resizable/pull/357)) 298 | - Update `rollup-plugin-commonjs` to `v9.2.0` ([#356](https://github.com/bokuweb/re-resizable/pull/356)) 299 | - Update `@​storybook/addon-info` & `@​storybook/react` to `v3.4.11` ([#355](https://github.com/bokuweb/re-resizable/pull/355)) 300 | 301 | ## [4.9.1 (2018-10-21)](https://github.com/bokuweb/re-resizable/compare/v4.9.0...v4.9.1) 302 | 303 | ### :bug: Bug Fix 304 | - Fix `flow` types ([@amccloud](https://github.com/amccloud) in [#364](https://github.com/bokuweb/re-resizable/pull/364) & [@bokuweb](https://github.com/bokuweb) in [424a208a](https://github.com/bokuweb/re-resizable/commit/424a208a25e07e5bec09ca22bdcbc93708c9e34e)) 305 | 306 | ### :nail_care: Enhancement 307 | - Add defaultStyle to default-size stories ([@liorbentov](https://github.com/liorbentov) in [#361](https://github.com/bokuweb/re-resizable/pull/361)) 308 | 309 | ### :memo: Documentation 310 | - Add `Storybook` badge to README ([@bokuweb](https://github.com/bokuweb) in [16458a2d](https://github.com/bokuweb/re-resizable/commit/16458a2dad38699f01592f266471878b40c3f1d8)) 311 | 312 | ### :house: Internal 313 | - Update `rollup` to `v0.65.2` ([#347](https://github.com/bokuweb/re-resizable/pull/347)) 314 | - Update `react` & `react-dom` to `v16.5.1` ([#350](https://github.com/bokuweb/re-resizable/pull/350)) 315 | - Update `sinon` to `v7.0.0` ([#368](https://github.com/bokuweb/re-resizable/pull/368)) 316 | - Update `eslint-plugin-flowtype` to `v3.0.0` ([#367](https://github.com/bokuweb/re-resizable/pull/367)) 317 | - Update `rollup-plugin-replace` to `v2.1.0` ([#365](https://github.com/bokuweb/re-resizable/pull/365)) 318 | - Update `rollup-plugin-replace` to `v10.0.1` ([#360](https://github.com/bokuweb/re-resizable/pull/360)) 319 | - Update `prettier` to `v1.14.3` ([#359](https://github.com/bokuweb/re-resizable/pull/359)) 320 | 321 | ## [4.9.0 (2018-10-13)](https://github.com/bokuweb/re-resizable/compare/v4.8.1...v4.9.0) 322 | 323 | ### :rocket: New Feature 324 | - Allow relative units for `scale` prop ([@haakemon](https://github.com/liorbentov) in [#349](https://github.com/bokuweb/re-resizable/pull/349)) 325 | 326 | ### :memo: Documentation 327 | - Add `CodeSandbox` (TypeScript) link to README ([@bokuweb](https://github.com/bokuweb) in [e17509a5](https://github.com/bokuweb/re-resizable/commit/e17509a569b630314f9a5b79fea88230035f1928)) 328 | 329 | ### :house: Internal 330 | - Update `rollup` to `v0.65.0` ([#339](https://github.com/bokuweb/re-resizable/pull/339)) 331 | - Update `rollup-plugin-commonjs` to `v9.1.6` ([#338](https://github.com/bokuweb/re-resizable/pull/338)) 332 | - Update `react` & `react-dom` to `v16.5.0` ([#348](https://github.com/bokuweb/re-resizable/pull/348)) 333 | - Update `sinon` to `v6.3.1` ([#345](https://github.com/bokuweb/re-resizable/pull/345)) 334 | 335 | ## [4.8.1 (2018-08-24)](https://github.com/bokuweb/re-resizable/compare/v4.8.0...v4.8.1) 336 | 337 | ### :bug: Bug Fix 338 | - Fix `TypeScript` types ([@bokuweb](https://github.com/bokuweb) in [22b895b5](https://github.com/bokuweb/re-resizable/commit/22b895b59f35fa473a7c0195b8383d81274760cc) & [55d0eff3](https://github.com/bokuweb/re-resizable/commit/55d0eff3268ff06cfec406b3e34cbb2051357a82)) 339 | 340 | ### :memo: Documentation 341 | - Add `CodeSandbox` link to README ([@bokuweb](https://github.com/bokuweb) in [738edd71](https://github.com/bokuweb/re-resizable/commit/738edd71b97a85823e48b1b1914b5f8a7051c2d7) & [b93fa52e](https://github.com/bokuweb/re-resizable/commit/b93fa52e728ced9e0fcc2276b035fbc558d0e1ba)) 342 | 343 | ### :house: Internal 344 | - Update `flow-bin` to `v0.79.1` ([#336](https://github.com/bokuweb/re-resizable/pull/336)) 345 | - Update `sinon` to `v6.1.5` ([#327](https://github.com/bokuweb/re-resizable/pull/327)) 346 | - Update `rollup-plugin-babel` to `v3.0.7` ([#305](https://github.com/bokuweb/re-resizable/pull/305)) 347 | - Update `rollup` to `v0.64.1` ([#296](https://github.com/bokuweb/re-resizable/pull/296)) 348 | 349 | ## [4.8.0 (2018-08-23)](https://github.com/bokuweb/re-resizable/compare/v4.7.1...v4.8.0) 350 | 351 | ### :rocket: New Feature 352 | - Add absolute snap dimensions ([@therebelrobot](https://github.com/therebelrobot) in [#337](https://github.com/bokuweb/re-resizable/pull/337) & [@bokuweb](https://github.com/bokuweb) in [e9f0df99](https://github.com/bokuweb/re-resizable/commit/e9f0df99ff85ab70542a4c0d568f57c5e7cca6fb)) 353 | 354 | ### :memo: Documentation 355 | - Change `Greenkeeper` badges to `Renovate` in README ([@bokuweb](https://github.com/bokuweb) in [7903d50e](https://github.com/bokuweb/re-resizable/commit/7903d50e796dce5b4e535ca3858db78269fb4aa0)) 356 | - Fix `ResizeCallback` types in README ([@mdanka](https://github.com/mdanka) in [#325](https://github.com/bokuweb/re-resizable/pull/325)) 357 | 358 | ### :house: Internal 359 | - Update `prettier` to `v1.14.2` ([#311](https://github.com/bokuweb/re-resizable/pull/311), [#312](https://github.com/bokuweb/re-resizable/pull/312) & [#329](https://github.com/bokuweb/re-resizable/pull/329)) 360 | - Update `flow-bin` to `v0.78.0` ([#298](https://github.com/bokuweb/re-resizable/pull/298), [#320](https://github.com/bokuweb/re-resizable/pull/320), [#326](https://github.com/bokuweb/re-resizable/pull/326) & [#332](https://github.com/bokuweb/re-resizable/pull/332)) 361 | - Update `@​storybook/addon-info` & `@​storybook/react` to `v3.4.10` ([#297](https://github.com/bokuweb/re-resizable/pull/297) & [#331](https://github.com/bokuweb/re-resizable/pull/331)) 362 | - Update `flow-copy-source` to `v2.0.2` ([#313](https://github.com/bokuweb/re-resizable/pull/313) & [#324](https://github.com/bokuweb/re-resizable/pull/324)) 363 | - Update `sinon` to `v6.1.3` ([#309](https://github.com/bokuweb/re-resizable/pull/309), [#314](https://github.com/bokuweb/re-resizable/pull/314) & [#317](https://github.com/bokuweb/re-resizable/pull/317)) 364 | - Update `eslint-plugin-react` to `v7.11.1` ([#310](https://github.com/bokuweb/re-resizable/pull/310), [#334](https://github.com/bokuweb/re-resizable/pull/334) & [#335](https://github.com/bokuweb/re-resizable/pull/335)) 365 | - Update `eslint-plugin-import` to `v2.14.0` ([#308](https://github.com/bokuweb/re-resizable/pull/308) & [#333](https://github.com/bokuweb/re-resizable/pull/333)) 366 | - Update `prettier-eslint` to `v8.8.2` ([#301](https://github.com/bokuweb/re-resizable/pull/301)) 367 | - Update `eslint-plugin-jsx-a11y` to `v6.1.1` ([#315](https://github.com/bokuweb/re-resizable/pull/315) & [#323](https://github.com/bokuweb/re-resizable/pull/323)) 368 | - Update `babel-eslint` to `v8.2.6` ([#302](https://github.com/bokuweb/re-resizable/pull/302) & [#322](https://github.com/bokuweb/re-resizable/pull/322)) 369 | - Update `flow-typed` to `v2.5.1` ([#318](https://github.com/bokuweb/re-resizable/pull/318)) 370 | - Update `eslint-plugin-flowtype` to `v2.50.0` ([#321](https://github.com/bokuweb/re-resizable/pull/321)) 371 | - Update `react` & `react-dom` to `v16.4.2` ([#330](https://github.com/bokuweb/re-resizable/pull/330)) 372 | - Update `rollup-plugin-commonjs` to `v9.1.5` ([#328](https://github.com/bokuweb/re-resizable/pull/328)) 373 | 374 | ## [4.7.1 (2018-06-24)](https://github.com/bokuweb/re-resizable/compare/v4.7.0...v4.7.1) 375 | 376 | ### :bug: Bug Fix 377 | - Fix behaviour when setting `auto` ([@bokuweb](https://github.com/bokuweb) in [#307](https://github.com/bokuweb/re-resizable/pull/307) & [ce04f529](https://github.com/bokuweb/re-resizable/commit/ce04f52924f2d085462e2a0be2c2c4592cf290d8)) 378 | 379 | ## [4.7.0 (2018-06-24)](https://github.com/bokuweb/re-resizable/compare/v4.6.1...v4.7.0) 380 | 381 | ### :bug: Bug Fix 382 | - Fix behaviour when setting absolute position ([@bokuweb](https://github.com/bokuweb) in [#306](https://github.com/bokuweb/re-resizable/pull/306) & [a64ba810](https://github.com/bokuweb/re-resizable/commit/a64ba8109e05923189004b71ec1e010e09ae9b0a)) 383 | 384 | ## [4.6.1 (2018-06-23)](https://github.com/bokuweb/re-resizable/compare/v4.6.0...v4.6.1) 385 | 386 | ### :bug: Bug Fix 387 | - Downgrade `rollup`, since it's breaking our build ([@bokuweb](https://github.com/bokuweb) in [#304](https://github.com/bokuweb/re-resizable/pull/304) & [2daa83aa](https://github.com/bokuweb/re-resizable/commit/2daa83aaa1c31100621fffda48b17c8a05374dc8)) 388 | 389 | ## [4.6.0 (2018-06-23)](https://github.com/bokuweb/re-resizable/compare/v4.5.2...v4.6.0) 390 | 391 | **Note: this release has a critical issue and was deprecated. Please update to 4.6.1 or higher.** 392 | 393 | ### :bug: Bug Fix 394 | - Fix `TypeScript` types ([@bokuweb](https://github.com/bokuweb) in [1db61d42](https://github.com/bokuweb/re-resizable/commit/1db61d42b0c4d33dbbbdde49a84695e70d6cfe2c), [2a02d211](https://github.com/bokuweb/re-resizable/commit/2a02d2111b7c0e7b47c688b633d0249fd0231358) & [27117f46](https://github.com/bokuweb/re-resizable/commit/27117f46be0916eba7012eef1f38b3b13f9d53fc)) 395 | 396 | ## [4.5.2 (2018-06-23)](https://github.com/bokuweb/re-resizable/compare/v4.5.1...v4.5.2) 397 | 398 | **Note: this release has a critical issue and was deprecated. Please update to 4.6.1 or higher.** 399 | 400 | ### :bug: Bug Fix 401 | - Fix `TypeScript` types ([@bokuweb](https://github.com/bokuweb) in [e43e042e](https://github.com/bokuweb/re-resizable/commit/e43e042edcb09f7a5723491de1c6ebb981a7049a) & [af559e74](https://github.com/bokuweb/re-resizable/commit/af559e7462dc5fa366d77b54ca3d0fc5264317fc)) 402 | 403 | ### :house: Internal 404 | - Update `rollup` to `v0.61.0` ([#290](https://github.com/bokuweb/re-resizable/pull/290) & [#295](https://github.com/bokuweb/re-resizable/pull/295)) 405 | - Update `@​storybook/addon-info` & `@​storybook/react` to `v3.4.7` ([#288](https://github.com/bokuweb/re-resizable/pull/288)) 406 | - Update `prettier` to `v1.13.5` ([#285](https://github.com/bokuweb/re-resizable/pull/285)) 407 | - Update `sinon` to `v6.0.0` ([#289](https://github.com/bokuweb/re-resizable/pull/289)) 408 | - Update `flow-copy-source` to `v2.0.0` ([#280](https://github.com/bokuweb/re-resizable/pull/280)) 409 | - Update `eslint-plugin-react` to `v7.9.1` ([#279](https://github.com/bokuweb/re-resizable/pull/279)) 410 | - Update `avaron` to `v0.2.0` ([#300](https://github.com/bokuweb/re-resizable/pull/300)) 411 | 412 | ## [4.5.1 (2018-06-19)](https://github.com/bokuweb/re-resizable/compare/v4.5.0...v4.5.1) 413 | 414 | ### :bug: Bug Fix 415 | - Fix `TypeScript` types ([@maksis](https://github.com/maksis) in [#293](https://github.com/bokuweb/re-resizable/pull/293) & [@bokuweb](https://github.com/bokuweb) in [e43e042e](https://github.com/bokuweb/re-resizable/commit/e43e042edcb09f7a5723491de1c6ebb981a7049a)) 416 | 417 | ### :house: Internal 418 | - Update `react` & `react-dom` to `v16.4.1` ([#291](https://github.com/bokuweb/re-resizable/pull/291)) 419 | 420 | ## [4.5.0 (2018-06-19)](https://github.com/bokuweb/re-resizable/compare/v4.4.10...v4.5.0) 421 | 422 | ### :bug: Bug Fix 423 | - Fix `TypeScript` types ([@bokuweb](https://github.com/bokuweb) in [ec3a4b64](https://github.com/bokuweb/re-resizable/commit/ec3a4b64c32484515891375d9dce73ec9d079c23)) 424 | 425 | ### :house: Internal 426 | - Drop Node 6/7 support in CI ([@bokuweb](https://github.com/bokuweb) in [1b6480cf](https://github.com/bokuweb/re-resizable/commit/1b6480cfae02a2cb8cc10ffb4c571818f48b0d5c)) 427 | - Update `flow-bin` to `v0.74.0` ([#284](https://github.com/bokuweb/re-resizable/pull/284)) 428 | - Update `sinon` to `v5.1.0` ([#282](https://github.com/bokuweb/re-resizable/pull/282)) 429 | - Update `rollup` to `v0.60.1` ([#281](https://github.com/bokuweb/re-resizable/pull/281)) 430 | 431 | ## [4.4.10 (2018-06-07)](https://github.com/bokuweb/re-resizable/compare/v4.4.9...v4.4.10) 432 | 433 | ### :bug: Bug Fix 434 | - Fix `Array.from` error in IE11 ([@bokuweb](https://github.com/bokuweb) in [6caf5593](https://github.com/bokuweb/re-resizable/commit/6caf559367cf00579fe9203c0694c9cfc7b5799f) & [f1bceab6](https://github.com/bokuweb/re-resizable/commit/f1bceab642cd40d0263e78983e144bbbc2fc937c)) 435 | 436 | ## [4.4.9 (2018-06-07)](https://github.com/bokuweb/re-resizable/compare/v4.4.8...v4.4.9) 437 | 438 | ### :bug: Bug Fix 439 | - Fix `Array.from` error in IE11 ([@bokuweb](https://github.com/bokuweb) in [#283](https://github.com/bokuweb/re-resizable/pull/283)) 440 | 441 | ### :memo: Documentation 442 | - Change `CodeSandbox` link in README ([@bokuweb](https://github.com/bokuweb) in [d31007dc](https://github.com/bokuweb/re-resizable/commit/d31007dc025df06ffe892ac4907d0639f5d06a47)) 443 | 444 | ### :house: Internal 445 | - Update `sinon` to `v5.0.10` ([#223](https://github.com/bokuweb/re-resizable/pull/223), [#227](https://github.com/bokuweb/re-resizable/pull/227), [#251](https://github.com/bokuweb/re-resizable/pull/251), [#252](https://github.com/bokuweb/re-resizable/pull/252), [#254](https://github.com/bokuweb/re-resizable/pull/254) & [#268](https://github.com/bokuweb/re-resizable/pull/268)) 446 | - Use specific Docker image in CI ([#225](https://github.com/bokuweb/re-resizable/pull/225)) 447 | - Update `flow-bin` to `v0.73.0` ([#226](https://github.com/bokuweb/re-resizable/pull/226), [#242](https://github.com/bokuweb/re-resizable/pull/242), [#247](https://github.com/bokuweb/re-resizable/pull/247), [#258](https://github.com/bokuweb/re-resizable/pull/258) & [#271](https://github.com/bokuweb/re-resizable/pull/271)) 448 | - Update `react` & `react-dom` to `v16.4.0` ([#228](https://github.com/bokuweb/re-resizable/pull/228), [#231](https://github.com/bokuweb/re-resizable/pull/231), [#241](https://github.com/bokuweb/re-resizable/pull/241) & [#269](https://github.com/bokuweb/re-resizable/pull/269)) 449 | - Update `eslint-plugin-import` to `v2.12.0` ([#229](https://github.com/bokuweb/re-resizable/pull/229), [#236](https://github.com/bokuweb/re-resizable/pull/236) & [#264](https://github.com/bokuweb/re-resizable/pull/264)) 450 | - Update `@​storybook/addon-info` & `@​storybook/react` to `v3.4.6` ([#230](https://github.com/bokuweb/re-resizable/pull/230), [#233](https://github.com/bokuweb/re-resizable/pull/233), [#244](https://github.com/bokuweb/re-resizable/pull/244), [#249](https://github.com/bokuweb/re-resizable/pull/249), [#261](https://github.com/bokuweb/re-resizable/pull/261), [#265](https://github.com/bokuweb/re-resizable/pull/265) & [#272](https://github.com/bokuweb/re-resizable/pull/272)) 451 | - Update `prettier` to `v1.13.4` ([#235](https://github.com/bokuweb/re-resizable/pull/235), [#243](https://github.com/bokuweb/re-resizable/pull/243), [#273](https://github.com/bokuweb/re-resizable/pull/273), [#275](https://github.com/bokuweb/re-resizable/pull/275) & [#276](https://github.com/bokuweb/re-resizable/pull/276)) 452 | - Update `rollup-plugin-babel` to `v3.0.4` ([#246](https://github.com/bokuweb/re-resizable/pull/246)) 453 | - Update `rollup` to `v0.59.4` ([#240](https://github.com/bokuweb/re-resizable/pull/240), [#263](https://github.com/bokuweb/re-resizable/pull/263), [#266](https://github.com/bokuweb/re-resizable/pull/266) & [#270](https://github.com/bokuweb/re-resizable/pull/270)) 454 | - Update `eslint-plugin-flowtype` to `v2.49.3` ([#239](https://github.com/bokuweb/re-resizable/pull/239), [#267](https://github.com/bokuweb/re-resizable/pull/267) & [#277](https://github.com/bokuweb/re-resizable/pull/277)) 455 | - Update `rollup-plugin-commonjs` to `v9.1.3` ([#250](https://github.com/bokuweb/re-resizable/pull/250)) 456 | - Update `babel-eslint` to `v8.2.3` ([#237](https://github.com/bokuweb/re-resizable/pull/237)) 457 | - Update `rollup-plugin-node-globals` to `v1.2.1` ([#255](https://github.com/bokuweb/re-resizable/pull/255)) 458 | - Update `npm-run-all` to `v4.1.3` ([#253](https://github.com/bokuweb/re-resizable/pull/253)) 459 | - Update `babel-preset-env` to `v1.7.0` ([#259](https://github.com/bokuweb/re-resizable/pull/259)) 460 | - Update `eslint-plugin-react` to `v7.8.2` ([#260](https://github.com/bokuweb/re-resizable/pull/260) & [#262](https://github.com/bokuweb/re-resizable/pull/262)) 461 | - Update `gh-pages` to `v1.2.0` ([#278](https://github.com/bokuweb/re-resizable/pull/278)) 462 | 463 | ## [4.4.8 (2018-03-27)](https://github.com/bokuweb/re-resizable/compare/v4.4.7...v4.4.8) 464 | 465 | ### :bug: Bug Fix 466 | - Fix for nexted instances ([@bokuweb](https://github.com/bokuweb) in [#222](https://github.com/bokuweb/re-resizable/pull/222) & [064a09d6](https://github.com/bokuweb/re-resizable/commit/064a09d6a5806d923135ff351a9893612476a1b5)) 467 | 468 | ### :house: Internal 469 | - Update `sinon` to `v4.4.9` ([#221](https://github.com/bokuweb/re-resizable/pull/221)) 470 | 471 | ## [4.4.7 (2018-03-26)](https://github.com/bokuweb/re-resizable/compare/v4.4.6...v4.4.7) 472 | 473 | ### :bug: Bug Fix 474 | - Fix update when no props are passed ([@bokuweb](https://github.com/bokuweb) in [#219](https://github.com/bokuweb/re-resizable/pull/219) & [49422c1b](https://github.com/bokuweb/re-resizable/commit/49422c1b26fbc8336825b2225cdd9931272ff4a3)) 475 | 476 | ### :memo: Documentation 477 | - Change `CodeSandbox` link in README ([@bokuweb](https://github.com/bokuweb) in [0ec36a6e](https://github.com/bokuweb/re-resizable/commit/0ec36a6ea597df272c4a7674c4037ee57d4aade7)) 478 | 479 | ### :house: Internal 480 | - Update `eslint` to `v4.19.1` ([#217](https://github.com/bokuweb/re-resizable/pull/217)) 481 | - Update `sinon` to `v4.4.8` ([#216](https://github.com/bokuweb/re-resizable/pull/216)) 482 | 483 | ## [4.4.6 (2018-03-21)](https://github.com/bokuweb/re-resizable/compare/v4.4.5...v4.4.6) 484 | 485 | ### :bug: Bug Fix 486 | - Use `relative` position as default for `base` ([@bokuweb](https://github.com/bokuweb) in [#213](https://github.com/bokuweb/re-resizable/pull/213) & [f4963eb9](https://github.com/bokuweb/re-resizable/commit/f4963eb96c1421b195671642e80cfa3d91b94e74)) 487 | 488 | ### :house: Internal 489 | - Update `rollup` to `v0.57.1` ([#211](https://github.com/bokuweb/re-resizable/pull/211)) 490 | - Update `rollup-plugin-node-resolve` to `v3.3.0` ([#212](https://github.com/bokuweb/re-resizable/pull/212)) 491 | 492 | ## v4.4.5 493 | 494 | - chore: upgrade flow-bin 495 | 496 | ## v4.4.4 497 | 498 | - fix: base finder 499 | - fix: add mouse leave 500 | 501 | ## v4.4.3 502 | 503 | - fix: fix type issues in index.d.ts. 504 | 505 | ## v4.4.2 506 | 507 | - fix: fixed bug where base could not be found 508 | 509 | ## v4.4.1 510 | 511 | - fix: add guard to avoid error without parent 512 | 513 | ## v4.4.0 514 | 515 | - fix: bug behavior with flex layout 516 | - chore: refactor 517 | - chore: update deps 518 | - chore: update d.ts 519 | - chore: add some stories 520 | 521 | ## v4.3.2 522 | 523 | - Fixed a bug, when resizing sometimes causes text-selection in some browser #182 524 | 525 | ## v4.3.1 526 | 527 | - Fixed a bug, `auto` overwritten by px value #179 528 | 529 | ## v4.3.0 530 | 531 | - Allow 0 as minWidth and minHeight #178 532 | 533 | ## v4.2.0 534 | 535 | - Add a option for passing custom handle components #170 536 | 537 | ## v4.1.2 538 | 539 | - Fixed a bug, Text select while resizing in IE11 #166 540 | 541 | ## v4.1.1 542 | 543 | - Fixed a bug, Element width id="__resizable0" breaks my layout #162 544 | 545 | ## v4.1.0 546 | 547 | - Additional height and width with lockAspectRatio #163 548 | 549 | ## v4.0.3 550 | 551 | - Use ES5-compatible prototype methods #160 552 | 553 | ## v4.0.2 554 | 555 | - Fix using right click on resize #152 556 | - Add workaround when base Node not found. 557 | 558 | ## v4.0.1 559 | 560 | - Update index.d.ts, Fixes #153 561 | 562 | ## v4.0.0 563 | 564 | - Remove `width` and `height`. 565 | - Add `defaultSize` and `size`, 566 | 567 | ## v3.0.0 568 | 569 | - Fix flowtype annotation. 570 | - Remove `extendsProps`. 571 | 572 | You can add extendsProps as follows. 573 | 574 | ``` 575 | 576 | ``` 577 | 578 | ## v3.0.0-beta.3 579 | 580 | - fix typo. `ResizeStartCallBack` -> `ResizeStartCallback`. 581 | 582 | ## v3.0.0-beta.2 583 | 584 | - export `ResizeDirection` type. 585 | - rename `Callback` to `ResizeCallback`. 586 | 587 | ## v3.0.0-beta.1 588 | 589 | - Fix flow filename. 590 | - Change logo 591 | 592 | ## v3.0.0-beta.0 593 | 594 | - Change package name, `react-resizable-box` -> `re-resizable`. 595 | - Add `handleWrapperStyle` and `handleWrapperClass` props. 596 | - Change behavior that is set percentage size to width or height as props. 597 | - Support percentage max/min size. 598 | - Use rollup. 599 | - Fix props name. 600 | - `handersClasses` -> `handleClasses` 601 | - `handersStyles` -> `handleStyles` 602 | 603 | ## v2.1.0 604 | 605 | - Remove `shouldUpdateComponent` (#135). 606 | - Remove `lodash.isEqual`. 607 | 608 | ## v2.0.6 609 | 610 | - Update README. 611 | 612 | ## v2.0.5 613 | 614 | - Fix remove event listener 615 | 616 | ## v2.0.4 617 | 618 | - Fix receiveProps. (related #85) 619 | 620 | ## v2.0.3 621 | 622 | - Update dev dependencies. 623 | - Modify index.js.flow. 624 | 625 | ## v2.0.2 626 | 627 | - Remove offset state. 628 | - Use `border-box`. 629 | - Fix boundary size. 630 | 631 | ## v2.0.1 632 | 633 | - Add offset state for rnd component. 634 | 635 | ## v2.0.0 636 | 637 | - Update index.js.flow 638 | 639 | ## v2.0.0-rc.2 640 | 641 | - Use `flowtype`. 642 | - Change callback args. 643 | - Change some props name. 644 | - isResizable => enable. 645 | - customClass => className. 646 | - customStyle => style. 647 | - handleStyle => handlerStyles. 648 | - handleClass => handlerClasses. 649 | - Add bounds feature. 650 | - Fix min/max size checker when aspect ratio locked. 651 | 652 | ## v1.8.4 653 | 654 | - Fix cursor 655 | 656 | ## v1.8.3 657 | 658 | - Fix npm readme 659 | 660 | ## v1.8.2 661 | 662 | - Add index.d.ts. 663 | - Fix resize glitch when aspct ratio locked. 664 | 665 | ## v1.8.1 666 | 667 | - Fixing issue on resizing with touch events 668 | 669 | ## v1.8.0 670 | 671 | - Add `extendsProps` prop to other props (e.g. `data-*`, `aria-*`, and other ). 672 | 673 | ## v1.7.0 674 | 675 | - Support siver side rendering #43 676 | 677 | ## v1.6.0 678 | 679 | - Add `updateSize` method. 680 | 681 | ## v1.5.1 682 | 683 | - Add `lockAspectRatio` property. 684 | 685 | ## v1.4.3 686 | 687 | - Avoid unnecessary rendering on resizer 688 | 689 | ## v1.4.2 690 | 691 | - Fix onTouchStart bind timing to avoid re-rendering 692 | 693 | ## v1.4.1 694 | 695 | - Support preserving auto size #40 (thanks @noradaiko) 696 | 697 | ## v1.4.0 698 | 699 | - Add `grid` props to snap grid. (thanks @paulyoung) 700 | 701 | ## v1.3.0 702 | 703 | - Add `userSelect: none` when resize get srated. 704 | - Add shouldComponentUpdate. 705 | - Add handle custom className. 706 | 707 | ## v1.2.0 708 | 709 | - Add module export plugin for `require`. 710 | 711 | ## v1.1.3 712 | 713 | - Update document. 714 | 715 | ## v1.1.2 716 | 717 | - Add size argument to resizeStart callback. 718 | - Fix bug 719 | 720 | ## v1.1.1 721 | 722 | - Fix delta value bug 723 | 724 | ## v1.1.0 725 | 726 | - Add delta argument to onResize and onResizeStop callback. 727 | 728 | ## v1.0.0 729 | 730 | - Rename and add resizer. 731 | 732 | ## v0.4.2 733 | 734 | - Support react v15 735 | - ESLint run when push 736 | 737 | ## v0.4.1 738 | 739 | - Add mousedown event object to `onResizeStart` callback argument. 740 | 741 | ## v0.4.0 742 | 743 | - Support `'px'` and `'%'` for width and height props. 744 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 @bokuweb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

📏 A resizable component for React.

4 | 5 |

Build Status 6 | 7 | Build Status 8 | 9 | 10 | 11 | 12 | 13 | 14 |

15 | 16 | ## Table of Contents 17 | 18 | - [Screenshot](#Screenshot) 19 | - [Live Demo](#live-demo) 20 | - [Storybook](#storybook) 21 | - [CodeSandbox](#codesandbox) 22 | - [Install](#install) 23 | - [Usage](#usage) 24 | - [Props](#props) 25 | - [Instance API](#instance-api) 26 | - [updateSize(size: { width: number | string, height: number | string }): void](#updateSize-void) 27 | - [Test](#test) 28 | - [Related](#related) 29 | 30 | ## Screenshot 31 | 32 | ![screenshot](https://github.com/bokuweb/re-resizable/blob/master/docs/screenshot.gif?raw=true) 33 | 34 | ## Live Demo 35 | 36 | ### Storybook 37 | 38 | [Storybook](http://bokuweb.github.io/re-resizable/) 39 | 40 | ### CodeSandbox 41 | 42 | [![Edit xp9p7272m4](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/xp9p7272m4) 43 | [CodeSandbox](https://codesandbox.io/s/xp9p7272m4) 44 | [CodeSandbox(TypeScript)](https://codesandbox.io/s/1vwo2p4l64) 45 | [CodeSandbox(With hooks)](https://codesandbox.io/s/blissful-joliot-d3unx) 46 | 47 | ## Install 48 | 49 | ```sh 50 | $ npm install --save re-resizable 51 | ``` 52 | 53 | ## Usage 54 | 55 | ### Example with `defaultSize` 56 | 57 | ```javascript 58 | import { Resizable } from 're-resizable'; 59 | 60 | 66 | Sample with default size 67 | 68 | ``` 69 | 70 | If you only want to set the width, you can do so by providing just the width property. 71 | The height property will automatically be set to auto, which means it will adjust 100% of its parent's height: 72 | 73 | ```javascript 74 | import { Resizable } from 're-resizable'; 75 | 76 | 81 | Sample with default size 82 | 83 | ``` 84 | ### Example with `size` 85 | 86 | If you use `size` props, please manage state by yourself. 87 | 88 | ```javascript 89 | import { Resizable } from 're-resizable'; 90 | 91 | { 94 | this.setState({ 95 | width: this.state.width + d.width, 96 | height: this.state.height + d.height, 97 | }); 98 | }} 99 | > 100 | Sample with size 101 | 102 | ``` 103 | 104 | ## Props 105 | 106 | #### `defaultSize?: { width?: (number | string), height?: (number | string) };` 107 | 108 | Specifies the `width` and `height` that the dragged item should start at. 109 | For example, you can set `300`, `'300px'`, `50%`. 110 | If both `defaultSize` and `size` omitted, set `'auto'`. 111 | 112 | `defaultSize` will be ignored when `size` set. 113 | 114 | #### `size?: { width?: (number | string), height?: (number | string) };` 115 | 116 | The `size` property is used to set the size of the component. 117 | For example, you can set `300`, `'300px'`, `50%`. 118 | 119 | Use `size` if you need to control size state by yourself. 120 | 121 | #### `className?: string;` 122 | 123 | The `className` property is used to set the custom `className` of a resizable component. 124 | 125 | #### `style?: { [key: string]: string };` 126 | 127 | The `style` property is used to set the custom `style` of a resizable component. 128 | 129 | #### `minWidth?: number | string;` 130 | 131 | The `minWidth` property is used to set the minimum width of a resizable component. Defaults to 10px. 132 | 133 | It accepts viewport as well as parent relative units. For example, you can set `300`, `50%`, `50vw` or `50vh`. 134 | 135 | Same type of values can be applied to `minHeight`, `maxWidth` and `maxHeight`. 136 | 137 | #### `minHeight?: number | string;` 138 | 139 | The `minHeight` property is used to set the minimum height of a resizable component. Defaults to 10px. 140 | 141 | #### `maxWidth?: number | string;` 142 | 143 | The `maxWidth` property is used to set the maximum width of a resizable component. 144 | 145 | #### `maxHeight?: number | string`; 146 | 147 | The `maxHeight` property is used to set the maximum height of a resizable component. 148 | 149 | #### `grid?: [number, number];` 150 | 151 | The `grid` property is used to specify the increments that resizing should snap to. Defaults to `[1, 1]`. 152 | 153 | #### `gridGap?: [number, number];` 154 | 155 | The `gridGap` property is used to specify any gaps between your grid cells that should be accounted for when resizing. Defaults to `[0, 0]`. 156 | The value provided for each axis will always add the grid gap amount times grid cells spanned minus one. 157 | 158 | #### `snap?: { x?: Array, y?: Array };` 159 | 160 | The `snap` property is used to specify absolute pixel values that resizing should snap to. `x` and `y` are both optional, allowing you to only include the axis you want to define. Defaults to `null`. 161 | 162 | #### `snapGap?: number` 163 | 164 | The `snapGap` property is used to specify the minimum gap required in order to move to the next snapping target. Defaults to `0` which means that snap targets are always used. 165 | 166 | #### `resizeRatio?: number | [number, number];` 167 | 168 | The `resizeRatio` property is used to set the number of pixels the resizable component scales by compared to the number of pixels the mouse/touch moves. Defaults to `1` (for a 1:1 ratio). The number set is the left side of the ratio, `2` will give a 2:1 ratio. 169 | 170 | For [number, number] means [resizeRatioX, resizeRatioY], more precise control. 171 | 172 | #### `lockAspectRatio?: boolean | number;` 173 | 174 | The `lockAspectRatio` property is used to lock aspect ratio. 175 | Set to `true` to lock the aspect ratio based on the initial size. 176 | Set to a numeric value to lock a specific aspect ratio (such as `16/9`). 177 | If set to numeric, make sure to set initial height/width to values with correct aspect ratio. 178 | If omitted, set `false`. 179 | 180 | #### `lockAspectRatioExtraWidth?: number;` 181 | 182 | The `lockAspectRatioExtraWidth` property enables a resizable component to maintain an aspect ratio plus extra width. 183 | For instance, a video could be displayed 16:9 with a 50px side bar. 184 | If omitted, set `0`. 185 | 186 | #### `lockAspectRatioExtraHeight?: number;` 187 | 188 | The `lockAspectRatioExtraHeight` property enables a resizable component to maintain an aspect ratio plus extra height. 189 | For instance, a video could be displayed 16:9 with a 50px header bar. 190 | If omitted, set `0`. 191 | 192 | #### `bounds?: ('window' | 'parent' | HTMLElement);` 193 | 194 | Specifies resize boundaries. 195 | 196 | #### `boundsByDirection?: boolean;` 197 | 198 | By default max dimensions based on left and top element position. 199 | Width grow to right side, height grow to bottom side. 200 | Set `true` for detect max dimensions by direction. 201 | For example: enable `boundsByDirection` when resizable component stick on right side of screen and you want resize by left handler; 202 | 203 | `false` by default. 204 | 205 | #### `handleStyles?: HandleStyles;` 206 | 207 | The `handleStyles` property is used to override the style of one or more resize handles. 208 | Only the axis you specify will have its handle style replaced. 209 | If you specify a value for `right` it will completely replace the styles for the `right` resize handle, 210 | but other handle will still use the default styles. 211 | 212 | #### `handleClasses?: HandleClassName;` 213 | 214 | The `handleClasses` property is used to set the className of one or more resize handles. 215 | 216 | #### `handleComponent?: HandleComponent;` 217 | 218 | The `handleComponent` property is used to pass a React Component to be rendered as one or more resize handle. For example, this could be used to use an arrow icon as a handle.. 219 | 220 | #### `handleWrapperStyle?: { [key: string]: string };` 221 | 222 | The `handleWrapperStyle` property is used to override the style of resize handles wrapper. 223 | 224 | #### `handleWrapperClass?: string;` 225 | 226 | The `handleWrapperClass` property is used to override the className of resize handles wrapper. 227 | 228 | #### `enable?: ?Enable | false;` 229 | 230 | The `enable` property is used to set the resizable permission of a resizable component. 231 | 232 | The permission of `top`, `right`, `bottom`, `left`, `topRight`, `bottomRight`, `bottomLeft`, `topLeft` direction resizing. 233 | If omitted, all resizer are enabled. 234 | If you want to permit only right direction resizing, set `{ top:false, right:true, bottom:false, left:false, topRight:false, bottomRight:false, bottomLeft:false, topLeft:false }`. 235 | 236 | #### `onResizeStart?: ResizeStartCallBack;` 237 | 238 | `ResizeStartCallBack` type is below. 239 | 240 | ```javascript 241 | type ResizeStartCallback = ( 242 | e: SyntheticMouseEvent | SyntheticTouchEvent, 243 | dir: ResizableDirection, 244 | refToElement: HTMLDivElement, 245 | ) => void; 246 | ``` 247 | 248 | Calls when resizable component resize start. 249 | 250 | #### `onResize?: ResizeCallback;` 251 | 252 | #### `scale?: number`; 253 | 254 | The `scale` property is used in the scenario where the resizable element is a descendent of an element using css scaling (e.g. - `transform: scale(0.5)`). 255 | 256 | #### `as?: string | React.ComponentType`; 257 | 258 | By default the `Resizable` component will render a `div` as a wrapper. The `as` property is used to change the element used. 259 | 260 | ### Basic 261 | 262 | `ResizeCallback` type is below. 263 | 264 | ```javascript 265 | type ResizeCallback = ( 266 | event: MouseEvent | TouchEvent, 267 | direction: ResizableDirection, 268 | refToElement: HTMLDivElement, 269 | delta: NumberSize, 270 | ) => void; 271 | ``` 272 | 273 | Calls when resizable component resizing. 274 | 275 | #### `onResizeStop?: ResizeCallback;` 276 | 277 | `ResizeCallback` type is below. 278 | 279 | ```javascript 280 | type ResizeCallback = ( 281 | event: MouseEvent | TouchEvent, 282 | direction: ResizableDirection, 283 | refToElement: HTMLDivElement, 284 | delta: NumberSize, 285 | ) => void; 286 | ``` 287 | 288 | Calls when resizable component resize stop. 289 | 290 | ## Instance API 291 | 292 | #### * `updateSize(size: { width: number | string, height: number | string }): void` 293 | 294 | Update component size. 295 | 296 | `grid`, `snap`, `max/minWidth`, `max/minHeight` props is ignored, when this method called. 297 | 298 | - for example 299 | 300 | ```javascript 301 | class YourComponent extends Component { 302 | 303 | // ... 304 | 305 | update() { 306 | this.resizable.updateSize({ width: 200, height: 300 }); 307 | } 308 | 309 | render() { 310 | return ( 311 | { this.resizable = c; }}> 312 | example 313 | 314 | ); 315 | } 316 | 317 | // ... 318 | } 319 | ``` 320 | 321 | ## Contribute 322 | 323 | If you have a feature request, please add it as an issue or make a pull request. 324 | 325 | If you have a bug to report, please reproduce the bug in [CodeSandbox](https://codesandbox.io/s/ll587k677z) to help us easily isolate it. 326 | 327 | ## Test 328 | 329 | ``` sh 330 | npm test 331 | ``` 332 | 333 | ## Related 334 | 335 | - [react-rnd](https://github.com/bokuweb/react-rnd) 336 | - [react-sortable-pane](https://github.com/bokuweb/react-sortable-pane) 337 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-typescript', '@babel/preset-react'], 3 | plugins: ['@babel/plugin-transform-modules-commonjs', '@babel/plugin-proposal-class-properties'], 4 | }; 5 | -------------------------------------------------------------------------------- /docs/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bokuweb/re-resizable/dcadcbed0470ef9f75a22f660285d91f414958a3/docs/bg.png -------------------------------------------------------------------------------- /docs/dist/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bokuweb/re-resizable/dcadcbed0470ef9f75a22f660285d91f414958a3/docs/dist/.keep -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React-resizable-box example 6 | 7 | 8 | 53 | 54 | 55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bokuweb/re-resizable/dcadcbed0470ef9f75a22f660285d91f414958a3/docs/screenshot.gif -------------------------------------------------------------------------------- /docs/src/example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Resizable from '../../src'; 3 | 4 | const handlerClasses = { 5 | wrapper: 'react-resize-wrapper' 6 | } 7 | 8 | export default () => ( 9 |
10 | 20 |
21 | Resize me!!
22 | 23 | max 800 * 600 / min 240 * 120 24 | 25 |
26 |
27 |
28 | ); 29 | -------------------------------------------------------------------------------- /docs/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import Example from './example'; 4 | 5 | render( 6 | , document.querySelector('#content')); 7 | -------------------------------------------------------------------------------- /lib/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bokuweb/re-resizable/dcadcbed0470ef9f75a22f660285d91f414958a3/lib/.keep -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bokuweb/re-resizable/dcadcbed0470ef9f75a22f660285d91f414958a3/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "re-resizable", 3 | "version": "6.11.2", 4 | "description": "Resizable component for React.", 5 | "title": "re-resizable", 6 | "main": "./lib/index.es5.js", 7 | "module": "./lib/index.js", 8 | "jsnext:main": "./lib/index.js", 9 | "keywords": [ 10 | "react", 11 | "resize", 12 | "resizable", 13 | "component" 14 | ], 15 | "scripts": { 16 | "lint": "tslint -c tslint.json src/index.tsx", 17 | "tsc": "tsc -p tsconfig.json --skipLibCheck", 18 | "build:prod:main": "rollup -c scripts/prod.js", 19 | "build:prod:es5": "rollup -c scripts/prod.es5.js", 20 | "build": "npm-run-all --serial build:prod:* && tsc", 21 | "start": "npm-run-all --parallel storybook", 22 | "test": "npm run test-ct", 23 | "test:ci": "npm run flow && npm run build", 24 | "prepublish": "npm run build", 25 | "format": "prettier --write '**/*.{tsx,ts}'", 26 | "format:ci": "prettier '**/*.{tsx,ts}'", 27 | "storybook": "start-storybook -p 6066", 28 | "build-storybook": "build-storybook", 29 | "deploy": "npm run build-storybook && gh-pages -d storybook-static", 30 | "test-ct": "playwright test -c playwright-ct.config.ts" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/bokuweb/react-resizable-box.git" 35 | }, 36 | "author": "bokuweb", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/bokuweb/react-resizable-box/issues" 40 | }, 41 | "homepage": "https://github.com/bokuweb/react-resizable-box", 42 | "devDependencies": { 43 | "@babel/cli": "7.11.6", 44 | "@babel/core": "7.11.6", 45 | "@babel/eslint-parser": "7.11.0", 46 | "@babel/plugin-proposal-class-properties": "7.10.4", 47 | "@babel/plugin-transform-modules-commonjs": "7.10.4", 48 | "@babel/preset-react": "7.10.4", 49 | "@babel/preset-typescript": "7.10.4", 50 | "@babel/traverse": "7.23.2", 51 | "@babel/types": "7.11.5", 52 | "@emotion/core": "10.0.35", 53 | "@playwright/experimental-ct-react": "^1.43.1", 54 | "@storybook/addon-info": "5.3.21", 55 | "@storybook/addon-options": "5.3.21", 56 | "@storybook/react": "8.5.8", 57 | "@types/node": "22.13.5", 58 | "@types/react": "19.0.10", 59 | "@types/react-dom": "19.0.4", 60 | "@types/sinon": "17.0.4", 61 | "babel-core": "7.0.0-bridge.0", 62 | "babel-loader": "9.2.1", 63 | "babel-plugin-external-helpers": "6.22.0", 64 | "babel-plugin-transform-class-properties": "6.24.1", 65 | "babel-plugin-transform-object-assign": "6.22.0", 66 | "babel-plugin-transform-object-rest-spread": "6.26.0", 67 | "babel-polyfill": "6.26.0", 68 | "babel-preset-env": "1.7.0", 69 | "babel-preset-es2015": "6.24.1", 70 | "babel-preset-flow": "6.23.0", 71 | "babel-preset-react": "6.24.1", 72 | "babel-register": "6.26.0", 73 | "cross-env": "7.0.3", 74 | "gh-pages": "5.0.0", 75 | "npm-run-all2": "5.0.2", 76 | "playwright-core": "^1.43.1", 77 | "prettier": "1.19.1", 78 | "react": "^19.0.0", 79 | "react-dom": "^19.0.0", 80 | "rollup": "1.32.1", 81 | "rollup-plugin-babel": "4.4.0", 82 | "rollup-plugin-commonjs": "10.1.0", 83 | "rollup-plugin-node-globals": "1.4.0", 84 | "rollup-plugin-node-resolve": "5.2.0", 85 | "rollup-plugin-replace": "2.2.0", 86 | "rollup-plugin-typescript2": "0.27.3", 87 | "rollup-watch": "4.3.1", 88 | "sinon": "9.0.3", 89 | "tslint": "6.1.3", 90 | "tslint-config-google": "1.0.1", 91 | "tslint-config-prettier": "1.18.0", 92 | "tslint-plugin-prettier": "2.3.0", 93 | "typescript": "5.7.3" 94 | }, 95 | "typings": "./lib/index.d.ts", 96 | "types": "./lib/index.d.ts", 97 | "files": [ 98 | "lib" 99 | ], 100 | "peerDependencies": { 101 | "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", 102 | "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /playwright-ct.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/experimental-ct-react'; 2 | 3 | /** 4 | * See https://playwright.dev/docs/test-configuration. 5 | */ 6 | export default defineConfig({ 7 | testDir: 'src', 8 | /* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */ 9 | snapshotDir: './__snapshots__', 10 | /* Maximum time one test can run for. */ 11 | timeout: 10 * 1000, 12 | /* Run tests in files in parallel */ 13 | fullyParallel: true, 14 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 15 | forbidOnly: !!process.env.CI, 16 | /* Retry on CI only */ 17 | retries: process.env.CI ? 2 : 0, 18 | /* Opt out of parallel tests on CI. */ 19 | workers: process.env.CI ? 1 : undefined, 20 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 21 | reporter: 'html', 22 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 23 | use: { 24 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 25 | trace: 'on-first-retry', 26 | 27 | /* Port to use for Playwright component endpoint. */ 28 | ctPort: 3100, 29 | }, 30 | 31 | /* Configure projects for major browsers */ 32 | projects: [ 33 | { 34 | name: 'chromium', 35 | use: { ...devices['Desktop Chrome'] }, 36 | }, 37 | // { 38 | // name: 'firefox', 39 | // use: { ...devices['Desktop Firefox'] }, 40 | // }, 41 | // { 42 | // name: 'webkit', 43 | // use: { ...devices['Desktop Safari'] }, 44 | // }, 45 | ], 46 | }); 47 | -------------------------------------------------------------------------------- /playwright/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Testing Page 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /playwright/index.tsx: -------------------------------------------------------------------------------- 1 | // Import styles, initialize component theme here. 2 | // import '../src/common.css'; 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "timezone": "Asia/Tokyo", 6 | "schedule": ["every weekend"], 7 | "labels": ["renovate"], 8 | "patch": { "automerge": true } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/deploy-minor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git config --global -l 4 | git config --global user.email bokuweb12@gmail.com 5 | git config --global user.name bokuweb 6 | git remote --v 7 | git reset --hard HEAD 8 | npm version minor 9 | git push origin master 10 | git push --tags 11 | npm publish 12 | -------------------------------------------------------------------------------- /scripts/deploy-patch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git config --global -l 4 | git config --global user.email bokuweb12@gmail.com 5 | git config --global user.name bokuweb 6 | git remote --v 7 | git reset --hard HEAD 8 | npm version patch 9 | git push origin master 10 | git push --tags 11 | npm publish 12 | -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | // Rollup plugins. 2 | 3 | import babel from 'rollup-plugin-babel'; 4 | import cjs from 'rollup-plugin-commonjs'; 5 | import globals from 'rollup-plugin-node-globals'; 6 | import replace from 'rollup-plugin-replace'; 7 | import resolve from 'rollup-plugin-node-resolve'; 8 | 9 | export default { 10 | dest: 'build/index.js', 11 | entry: 'src/index.js', 12 | format: 'iife', 13 | moduleName: 'Example', 14 | plugins: [ 15 | babel({ 16 | babelrc: true, 17 | exclude: 'node_modules/**', 18 | presets: [['es2015', { modules: false }], 'react'], 19 | plugins: ['external-helpers', 'transform-class-properties'], 20 | }), 21 | cjs({ 22 | exclude: 'node_modules/process-es6/**', 23 | include: [ 24 | 'node_modules/fbjs/**', 25 | 'node_modules/object-assign/**', 26 | 'node_modules/react/**', 27 | 'node_modules/react-dom/**', 28 | 'node_modules/prop-types/**', 29 | 'node_modules/lodash.isequal/**', 30 | 'node_modules/create-react-class/**', 31 | 'node_modules/performance-now/**', 32 | 'node_modules/raf/**', 33 | ], 34 | }), 35 | globals(), 36 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }), 37 | resolve({ 38 | browser: true, 39 | main: true, 40 | }), 41 | ], 42 | globals: { 43 | react: 'React', 44 | }, 45 | sourceMap: true, 46 | }; 47 | -------------------------------------------------------------------------------- /scripts/prod.common.js: -------------------------------------------------------------------------------- 1 | import replace from 'rollup-plugin-replace'; 2 | import typescript from 'rollup-plugin-typescript2'; 3 | 4 | export default { 5 | input: 'src/index.tsx', 6 | plugins: [ 7 | typescript({ 8 | tsconfig: 'tsconfig.json', 9 | exclude: ['stories'], 10 | }), 11 | replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), 12 | ], 13 | output: { 14 | sourcemap: true, 15 | exports: 'named', 16 | name: 're-resizable', 17 | globals: { 18 | react: 'React', 19 | memoize: 'fast-memoize' 20 | }, 21 | }, 22 | external: ['react', 'fast-memoize'], 23 | }; -------------------------------------------------------------------------------- /scripts/prod.es5.js: -------------------------------------------------------------------------------- 1 | import common from './prod.common'; 2 | 3 | export default Object.assign({}, common, { 4 | output: { 5 | file: 'lib/index.es5.js', 6 | format: 'cjs', 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /scripts/prod.js: -------------------------------------------------------------------------------- 1 | import common from './prod.common'; 2 | 3 | export default Object.assign({}, common, { 4 | output: { 5 | file: 'lib/index.js', 6 | format: 'es', 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/experimental-ct-react'; 2 | import React from 'react'; 3 | import { spy } from 'sinon'; 4 | import { Resizable } from '.'; 5 | 6 | test.use({ viewport: { width: 500, height: 500 } }); 7 | 8 | test('should box width and height equal 100px', async ({ mount }) => { 9 | const resizable = await mount(); 10 | const divs = resizable.locator('div'); 11 | const width = await resizable.evaluate(node => node.style.width); 12 | const height = await resizable.evaluate(node => node.style.height); 13 | const position = await resizable.evaluate(node => node.style.position); 14 | 15 | expect(await divs.count()).toBe(9); 16 | expect(width).toBe('100px'); 17 | expect(height).toBe('100px'); 18 | expect(position).toBe('relative'); 19 | }); 20 | 21 | test('should allow vh, vw relative units', async ({ mount }) => { 22 | const resizable = await mount(); 23 | 24 | const divs = resizable.locator('div'); 25 | const width = await resizable.evaluate(node => node.style.width); 26 | const height = await resizable.evaluate(node => node.style.height); 27 | const position = await resizable.evaluate(node => node.style.position); 28 | 29 | expect(await divs.count()).toBe(9); 30 | expect(width).toBe('100vw'); 31 | expect(height).toBe('100vh'); 32 | expect(position).toBe('relative'); 33 | }); 34 | 35 | test('should allow vmax, vmin relative units', async ({ mount }) => { 36 | const resizable = await mount(); 37 | 38 | const divs = resizable.locator('div'); 39 | const width = await resizable.evaluate(node => node.style.width); 40 | const height = await resizable.evaluate(node => node.style.height); 41 | const position = await resizable.evaluate(node => node.style.position); 42 | 43 | expect(await divs.count()).toBe(9); 44 | expect(width).toBe('100vmax'); 45 | expect(height).toBe('100vmin'); 46 | expect(position).toBe('relative'); 47 | }); 48 | 49 | test('should box width and height equal auto when size omitted', async ({ mount }) => { 50 | const resizable = await mount(); 51 | const divs = resizable.locator('div'); 52 | expect(await divs.count()).toBe(9); 53 | expect(await resizable.evaluate(node => node.style.width)).toBe('auto'); 54 | expect(await resizable.evaluate(node => node.style.height)).toBe('auto'); 55 | expect(await resizable.evaluate(node => node.style.position)).toBe('relative'); 56 | }); 57 | 58 | test('should box width and height equal auto when set auto', async ({ mount }) => { 59 | const resizable = await mount(); 60 | const divs = resizable.locator('div'); 61 | expect(await divs.count()).toBe(9); 62 | expect(await resizable.evaluate(node => node.style.width)).toBe('auto'); 63 | expect(await resizable.evaluate(node => node.style.height)).toBe('auto'); 64 | expect(await resizable.evaluate(node => node.style.position)).toBe('relative'); 65 | }); 66 | 67 | test('Should style is applied to box', async ({ mount }) => { 68 | const resizable = await mount(); 69 | const divs = resizable.locator('div'); 70 | expect(await divs.count()).toBe(9); 71 | expect(await resizable.evaluate(node => node.style.position)).toBe('absolute'); 72 | }); 73 | 74 | test('Should custom class name be applied to box', async ({ mount }) => { 75 | const resizable = await mount(); 76 | 77 | const divs = resizable.locator('div'); 78 | expect(await divs.count()).toBe(9); 79 | expect(await resizable.evaluate(node => node.className)).toBe('custom-class-name'); 80 | }); 81 | 82 | test('Should use a custom wrapper element', async ({ mount }) => { 83 | const resizable = await mount(); 84 | 85 | expect(await resizable.evaluate(node => node.tagName)).toBe('HEADER'); 86 | }); 87 | 88 | test('Should custom class name be applied to resizer', async ({ mount }) => { 89 | const resizable = await mount(); 90 | expect(await resizable.evaluate(node => node.querySelector('.right-handle-class'))).toBeTruthy(); 91 | }); 92 | 93 | test('Should create custom span that wraps resizable divs ', async ({ mount }) => { 94 | const resizable = await mount(); 95 | 96 | const divs = resizable.locator('div'); 97 | 98 | expect(await (await divs.all())[0].evaluate(node => node.className)).toBe('wrapper-class'); 99 | }); 100 | 101 | test('Should not render resizer when enable props all false', async ({ mount }) => { 102 | const resizable = await mount( 103 | , 115 | ); 116 | 117 | const divs = resizable.locator('div'); 118 | expect(await divs.count()).toBe(1); 119 | }); 120 | 121 | test('Should disable all resizer', async ({ mount }) => { 122 | const resizable = await mount(); 123 | 124 | const divs = resizable.locator('div'); 125 | expect(await divs.count()).toBe(0); 126 | }); 127 | 128 | test('Should render one resizer when one enable props set true', async ({ mount }) => { 129 | const resizable = await mount( 130 | , 142 | ); 143 | const divs = resizable.locator('div'); 144 | expect(await divs.count()).toBe(2); 145 | }); 146 | 147 | test('Should render two resizer when two enable props set true', async ({ mount }) => { 148 | const resizable = await mount( 149 | , 161 | ); 162 | const divs = resizable.locator('div'); 163 | expect(await divs.count()).toBe(3); 164 | }); 165 | 166 | test('Should render three resizer when three enable props set true', async ({ mount }) => { 167 | const resizable = await mount( 168 | , 180 | ); 181 | const divs = resizable.locator('div'); 182 | expect(await divs.count()).toBe(4); 183 | }); 184 | 185 | test('Should only right is resizable and call onResizeStart when mousedown', async ({ mount }) => { 186 | const onResizeStart = spy(); 187 | const resizable = await mount( 188 | , 201 | ); 202 | const divs = resizable.locator('div'); 203 | expect(await divs.count()).toBe(2); 204 | await (await divs.all())[1].dispatchEvent('mousedown'); 205 | expect(onResizeStart.callCount).toBe(1); 206 | expect(onResizeStart.getCall(0).args[1]).toBe('right'); 207 | }); 208 | 209 | test('Should only bottom is resizable and call onResizeStart when mousedown', async ({ mount }) => { 210 | const onResizeStart = spy(); 211 | const resizable = await mount( 212 | , 225 | ); 226 | const divs = resizable.locator('div'); 227 | expect(await divs.count()).toBe(2); 228 | await (await divs.all())[1].dispatchEvent('mousedown'); 229 | expect(onResizeStart.callCount).toBe(1); 230 | expect(onResizeStart.getCall(0).args[1]).toBe('bottom'); 231 | }); 232 | 233 | test('Should only bottomRight is resizable and call onResizeStart when mousedown', async ({ mount }) => { 234 | const onResizeStart = spy(); 235 | const resizable = await mount( 236 | , 249 | ); 250 | const divs = resizable.locator('div'); 251 | expect(await divs.count()).toBe(2); 252 | await (await divs.all())[1].dispatchEvent('mousedown'); 253 | expect(onResizeStart.callCount).toBe(1); 254 | expect(onResizeStart.getCall(0).args[1]).toBe('bottomRight'); 255 | }); 256 | 257 | // TODO: flacky 258 | // test('Should not begin resize when onResizeStart returns false', async ({ mount, page }) => { 259 | // const onResizeStart = () => { 260 | // return false; 261 | // }; 262 | // const onResize = spy(); 263 | // const resizable = await mount(); 264 | // const divs = resizable.locator('div'); 265 | // await (await divs.all())[1].dispatchEvent('mousedown'); 266 | // await page.mouse.down(); 267 | // await page.mouse.move(100, 200); 268 | // await page.mouse.up(); 269 | // expect(onResize.callCount).toBe(0); 270 | // }); 271 | 272 | // test('should call onResize with expected args when resize direction right', async ({ mount, page }) => { 273 | // const onResize = spy(); 274 | // const resizable = await mount( 275 | // , 276 | // ); 277 | // const divs = resizable.locator('div'); 278 | // const node = (await divs.all())[2]; 279 | // node.dispatchEvent('mousedown'); 280 | // await page.mouse.move(200, 220); 281 | // await page.mouse.up(); 282 | // expect(onResize.callCount).toBe(1); 283 | // expect(onResize.getCall(0).args[1]).toBe('right'); 284 | // expect(onResize.getCall(0).args[2].clientWidth).toBe(300); 285 | // console.log(onResize.getCall(0).args[2]) 286 | // // t.deepEqual(onResize.getCall(0).args[2].clientHeight, 100); 287 | // // t.deepEqual(onResize.getCall(0).args[3], { width: 200, height: 0 }); 288 | // }); 289 | 290 | test('should call onResize with expected args when resize direction bottom', async ({ mount, page }) => { 291 | const onResize = spy(); 292 | const onResizeStart = spy(); 293 | const resizable = await mount( 294 | , 300 | ); 301 | const divs = resizable.locator('div'); 302 | const bottomHandle = (await divs.all())[3]; 303 | await bottomHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 304 | await page.mouse.move(200, 220); 305 | await page.mouse.up(); 306 | expect(onResize.callCount).toBe(1); 307 | expect(onResize.getCall(0).args[0].isTrusted).toBe(true); 308 | expect(onResize.getCall(0).args[1]).toBe('bottom'); 309 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 310 | expect(clientWidth).toBe(100); 311 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 312 | expect(clientHeight).toBe(320); 313 | expect(onResize.getCall(0).args[3]).toEqual({ width: 0, height: 220 }); 314 | }); 315 | 316 | test('should call onResize with expected args when resize direction bottomRight', async ({ mount, page }) => { 317 | const onResize = spy(); 318 | const onResizeStart = spy(); 319 | const resizable = await mount( 320 | , 326 | ); 327 | const divs = resizable.locator('div'); 328 | const bottomRightHandle = (await divs.all())[6]; 329 | await bottomRightHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 330 | await page.mouse.move(200, 220); 331 | await page.mouse.up(); 332 | expect(onResize.callCount).toBe(1); 333 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 334 | expect(onResize.getCall(0).args[1]).toBe('bottomRight'); 335 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 336 | expect(clientWidth).toBe(300); 337 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 338 | expect(clientHeight).toBe(320); 339 | expect(onResize.getCall(0).args[3]).toEqual({ width: 200, height: 220 }); 340 | }); 341 | 342 | test('should call onResizeStop when resize stop direction right', async ({ mount, page }) => { 343 | const onResize = spy(); 344 | const onResizeStart = spy(); 345 | const onResizeStop = spy(); 346 | const resizable = await mount( 347 | , 354 | ); 355 | const divs = resizable.locator('div'); 356 | const rightHandle = (await divs.all())[2]; 357 | await rightHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 358 | await page.mouse.move(200, 220); 359 | await page.mouse.up(); 360 | expect(onResizeStop.callCount).toBe(1); 361 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 362 | expect(onResizeStop.getCall(0).args[1]).toBe('right'); 363 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 364 | expect(clientWidth).toBe(300); 365 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 366 | expect(clientHeight).toBe(100); 367 | expect(onResizeStop.getCall(0).args[3]).toEqual({ width: 200, height: 0 }); 368 | }); 369 | 370 | test('should call onResizeStop when resize stop direction bottom', async ({ mount, page }) => { 371 | const onResize = spy(); 372 | const onResizeStart = spy(); 373 | const onResizeStop = spy(); 374 | const resizable = await mount( 375 | , 382 | ); 383 | const divs = resizable.locator('div'); 384 | const handles = await divs.all(); 385 | const handle = handles[3]; 386 | if (!handle) throw new Error('Handle not found'); 387 | await handle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 388 | await page.mouse.move(200, 220); 389 | await page.mouse.up(); 390 | expect(onResizeStop.callCount).toBe(1); 391 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 392 | expect(onResizeStop.getCall(0).args[1]).toBe('bottom'); 393 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 394 | expect(clientWidth).toBe(100); 395 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 396 | expect(clientHeight).toBe(320); 397 | expect(onResizeStop.getCall(0).args[3]).toEqual({ width: 0, height: 220 }); 398 | }); 399 | 400 | test('should call onResizeStop when resize stop direction bottomRight', async ({ mount, page }) => { 401 | const onResize = spy(); 402 | const onResizeStart = spy(); 403 | const onResizeStop = spy(); 404 | const resizable = await mount( 405 | , 412 | ); 413 | const divs = resizable.locator('div'); 414 | const handles = await divs.all(); 415 | const handle = handles[6]; 416 | if (!handle) throw new Error('Handle not found'); 417 | await handle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 418 | await page.mouse.move(200, 220); 419 | await page.mouse.up(); 420 | expect(onResizeStop.callCount).toBe(1); 421 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 422 | expect(onResizeStop.getCall(0).args[1]).toBe('bottomRight'); 423 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 424 | expect(clientHeight).toBe(320); 425 | expect(onResizeStop.getCall(0).args[3]).toEqual({ width: 200, height: 220 }); 426 | }); 427 | 428 | // test('should component size updated when updateSize method called', async ({ mount }) => { 429 | // const ref = React.createRef(); 430 | // await mount(); 431 | // // Call updateSize on the component instance obtained via ref 432 | // // @ts-ignore 433 | // ref.current.updateSize({ width: 200, height: 300 }); 434 | // // @ts-ignore 435 | // expect(ref.current.state.width).toBe(200); 436 | // // @ts-ignore 437 | // expect(ref.current.state.height).toBe(300); 438 | // }); 439 | 440 | test('should snapped by grid value', async ({ mount, page }) => { 441 | const onResize = spy(); 442 | const onResizeStart = spy(); 443 | const onResizeStop = spy(); 444 | 445 | const resizable = await mount( 446 | , 453 | ); 454 | 455 | const divs = resizable.locator('div'); 456 | const handle = (await divs.all())[6]; 457 | await handle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 458 | await page.mouse.move(12, 12); 459 | await page.mouse.up(); 460 | 461 | expect(onResize.firstCall.args[0].isTrusted).toBe(true); 462 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 463 | expect(clientHeight).toBe(110); 464 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 465 | expect(clientWidth).toBe(110); 466 | expect(onResize.firstCall.args[3]).toEqual({ width: 10, height: 10 }); 467 | }); 468 | 469 | test('should snapped by absolute snap value', async ({ mount, page }) => { 470 | const onResize = spy(); 471 | const onResizeStart = spy(); 472 | const onResizeStop = spy(); 473 | const resizable = await mount( 474 | , 481 | ); 482 | const divs = resizable.locator('div'); 483 | const handle = (await divs.all())[6]; 484 | 485 | await handle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 486 | await page.mouse.move(12, 12); 487 | await page.mouse.up(); 488 | 489 | expect(onResize.firstCall.args[0].isTrusted).toBeTruthy(); 490 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 491 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 492 | expect(clientHeight).toBe(100); 493 | expect(clientWidth).toBe(30); 494 | expect(onResize.firstCall.args[3]).toEqual({ width: -70, height: 0 }); 495 | }); 496 | 497 | // test('should only snap if the gap is small enough', async ({ mount, page }) => { 498 | // const onResize = spy(); 499 | // const onResizeStart = spy(); 500 | // const onResizeStop = spy(); 501 | // 502 | // const resizable = await mount( 503 | // , 511 | // ); 512 | // const divs = resizable.locator('div'); 513 | // const handle = (await divs.all())[6]; 514 | // await handle.dispatchEvent('mousedown', { button: 0, clientX: 40, clientY: 40 }); 515 | // await page.mouse.move(15, 15); 516 | // { 517 | // const clientHeight = await resizable.evaluate(el => el.clientHeight); 518 | // const clientWidth = await resizable.evaluate(el => el.clientWidth); 519 | // expect(onResize.firstCall.args[0].isTrusted).toBeTruthy(); 520 | // expect(clientHeight).toBe(15); 521 | // expect(clientWidth).toBe(15); 522 | // expect(onResize.firstCall.args[3]).toEqual({ width: 15, height: 15 }); 523 | // } 524 | // 525 | // await page.mouse.move(35, 35); 526 | // const clientHeight = await resizable.evaluate(el => el.clientHeight); 527 | // const clientWidth = await resizable.evaluate(el => el.clientWidth); 528 | // expect(clientHeight).toBe(80); 529 | // expect(clientWidth).toBe(80); 530 | // expect(onResize.getCall(1).args[3]).toEqual({ width: 40, height: 40 }); 531 | // }); 532 | 533 | test('should clamped by max width', async ({ mount, page }) => { 534 | const onResize = spy(); 535 | const onResizeStart = spy(); 536 | const onResizeStop = spy(); 537 | const resizable = await mount( 538 | , 545 | ); 546 | const divs = resizable.locator('div'); 547 | const handle = (await divs.all())[6]; 548 | if (!handle) throw new Error('Handle not found'); 549 | await handle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 550 | await page.mouse.move(200, 0); 551 | await page.mouse.up(); 552 | expect(onResize.getCall(0).args[0].isTrusted).toBe(true); 553 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 554 | expect(clientWidth).toBe(200); 555 | expect(onResize.getCall(0).args[3]).toEqual({ width: 100, height: 0 }); 556 | }); 557 | 558 | test('should clamped by min width', async ({ mount, page }) => { 559 | const onResize = spy(); 560 | const onResizeStart = spy(); 561 | const onResizeStop = spy(); 562 | const resizable = await mount( 563 | , 570 | ); 571 | const divs = resizable.locator('div'); 572 | const handle = (await divs.all())[5]; 573 | if (!handle) throw new Error('Handle not found'); 574 | await handle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 575 | await page.mouse.down(); 576 | await page.mouse.move(-100, 0); 577 | await page.mouse.up(); 578 | expect(onResize.getCall(0).args[0].isTrusted).toBe(true); 579 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 580 | expect(clientWidth).toBe(50); 581 | expect(onResize.getCall(0).args[3]).toEqual({ width: -50, height: 0 }); 582 | }); 583 | 584 | test('should allow 0 as minWidth', async ({ mount, page }) => { 585 | const onResize = spy(); 586 | const onResizeStart = spy(); 587 | const onResizeStop = spy(); 588 | const resizable = await mount( 589 | , 596 | ); 597 | const divs = resizable.locator('div'); 598 | const handle = (await divs.all())[5]; 599 | if (!handle) throw new Error('Handle not found'); 600 | await handle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 601 | await page.mouse.down(); 602 | await page.mouse.move(-100, 0); 603 | await page.mouse.up(); 604 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 605 | expect(clientWidth).toBe(0); 606 | expect(onResize.firstCall.args[3]).toEqual({ width: -100, height: 0 }); 607 | }); 608 | 609 | test('should clamped by max height', async ({ mount, page }) => { 610 | const onResize = spy(); 611 | const onResizeStart = spy(); 612 | const onResizeStop = spy(); 613 | const resizable = await mount( 614 | , 621 | ); 622 | const divs = resizable.locator('div'); 623 | const bottomHandle = (await divs.all())[3]; 624 | await bottomHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 625 | await page.mouse.move(0, 200); 626 | await page.mouse.up(); 627 | expect(onResize.getCall(0).args[0].isTrusted).toBe(true); 628 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 629 | expect(clientHeight).toBe(200); 630 | expect(onResize.getCall(0).args[3]).toEqual({ width: 0, height: 100 }); 631 | }); 632 | 633 | test('should clamped by min height', async ({ mount, page }) => { 634 | const onResize = spy(); 635 | const onResizeStart = spy(); 636 | const onResizeStop = spy(); 637 | const resizable = await mount( 638 | , 645 | ); 646 | const divs = resizable.locator('div'); 647 | const bottomHandle = (await divs.all())[3]; 648 | await bottomHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 649 | await page.mouse.down(); 650 | await page.mouse.move(0, -100); 651 | await page.mouse.up(); 652 | expect(onResize.getCall(0).args[0].isTrusted).toBe(true); 653 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 654 | expect(clientHeight).toBe(50); 655 | expect(onResize.getCall(0).args[3]).toEqual({ width: 0, height: -50 }); 656 | }); 657 | 658 | test('should allow 0 as minHeight', async ({ mount, page }) => { 659 | const onResize = spy(); 660 | const onResizeStart = spy(); 661 | const onResizeStop = spy(); 662 | const resizable = await mount( 663 | , 670 | ); 671 | const divs = resizable.locator('div'); 672 | const bottomHandle = (await divs.all())[3]; 673 | if (!bottomHandle) throw new Error('Handle not found'); 674 | await bottomHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 675 | await page.mouse.down(); 676 | await page.mouse.move(0, -100); 677 | await page.mouse.up(); 678 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 679 | expect(clientHeight).toBe(0); 680 | expect(onResize.firstCall.args[3]).toEqual({ width: 0, height: -100 }); 681 | }); 682 | 683 | test('should aspect ratio locked when resize to right', async ({ mount, page }) => { 684 | const onResize = spy(); 685 | const onResizeStart = spy(); 686 | const onResizeStop = spy(); 687 | const resizable = await mount( 688 | , 695 | ); 696 | const divs = resizable.locator('div'); 697 | const rightHandle = (await divs.all())[2]; 698 | await rightHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 699 | await page.mouse.down(); 700 | await page.mouse.move(200, 0); 701 | await page.mouse.up(); 702 | expect(onResizeStop.callCount).toBe(1); 703 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 704 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 705 | expect(clientWidth).toBe(300); 706 | expect(clientHeight).toBe(300); 707 | expect(onResize.getCall(0).args[3]).toEqual({ width: 200, height: 200 }); 708 | }); 709 | 710 | test('should aspect ratio locked with 1:1 ratio when resize to right', async ({ mount, page }) => { 711 | const onResize = spy(); 712 | const onResizeStart = spy(); 713 | const onResizeStop = spy(); 714 | const resizable = await mount( 715 | , 722 | ); 723 | const divs = resizable.locator('div'); 724 | const rightHandle = (await divs.all())[2]; 725 | await rightHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 726 | await page.mouse.down(); 727 | await page.mouse.move(200, 0); 728 | await page.mouse.up(); 729 | expect(onResizeStop.callCount).toBe(1); 730 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 731 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 732 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 733 | expect(clientWidth).toBe(300); 734 | expect(clientHeight).toBe(300); 735 | expect(onResize.getCall(0).args[3]).toEqual({ width: 200, height: 200 }); 736 | }); 737 | 738 | test('should aspect ratio locked with 2:1 ratio when resize to right', async ({ mount, page }) => { 739 | const onResize = spy(); 740 | const onResizeStart = spy(); 741 | const onResizeStop = spy(); 742 | 743 | const resizable = await mount( 744 | , 751 | ); 752 | 753 | const divs = resizable.locator('div'); 754 | const rightHandle = (await divs.all())[2]; 755 | await rightHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 756 | await page.mouse.down(); 757 | await page.mouse.move(200, 0); 758 | await page.mouse.up(); 759 | 760 | expect(onResizeStop.callCount).toBe(1); 761 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 762 | 763 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 764 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 765 | expect(clientWidth).toBe(400); 766 | expect(clientHeight).toBe(200); 767 | 768 | expect(onResize.getCall(0).args[3]).toEqual({ width: 200, height: 100 }); 769 | }); 770 | 771 | test('should aspect ratio locked with 2:1 ratio with extra width/height when resize to right', async ({ 772 | mount, 773 | page, 774 | }) => { 775 | const onResize = spy(); 776 | const onResizeStart = spy(); 777 | const onResizeStop = spy(); 778 | const resizable = await mount( 779 | , 788 | ); 789 | const divs = resizable.locator('div'); 790 | const rightHandle = (await divs.all())[2]; 791 | await rightHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 792 | await page.mouse.down(); 793 | await page.mouse.move(200, 0); 794 | await page.mouse.up(); 795 | expect(onResizeStop.callCount).toBe(1); 796 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 797 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 798 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 799 | expect(clientWidth).toBe(450); 800 | expect(clientHeight).toBe(250); 801 | expect(onResize.getCall(0).args[3]).toEqual({ width: 200, height: 100 }); 802 | }); 803 | 804 | test('should aspect ratio locked when resize to bottom', async ({ mount, page }) => { 805 | const onResize = spy(); 806 | const onResizeStart = spy(); 807 | const onResizeStop = spy(); 808 | const resizable = await mount( 809 | , 816 | ); 817 | const divs = resizable.locator('div'); 818 | const bottomHandle = (await divs.all())[3]; 819 | await bottomHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 820 | await page.mouse.down(); 821 | await page.mouse.move(0, 200); 822 | await page.mouse.up(); 823 | expect(onResizeStop.callCount).toBe(1); 824 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 825 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 826 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 827 | expect(clientWidth).toBe(300); 828 | expect(clientHeight).toBe(300); 829 | expect(onResize.getCall(0).args[3]).toEqual({ width: 200, height: 200 }); 830 | }); 831 | 832 | test('should aspect ratio locked with 1:1 ratio when resize to bottom', async ({ mount, page }) => { 833 | const onResize = spy(); 834 | const onResizeStart = spy(); 835 | const onResizeStop = spy(); 836 | const resizable = await mount( 837 | , 844 | ); 845 | const divs = resizable.locator('div'); 846 | const bottomHandle = (await divs.all())[3]; 847 | await bottomHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 848 | await page.mouse.down(); 849 | await page.mouse.move(0, 200); 850 | await page.mouse.up(); 851 | expect(onResizeStop.callCount).toBe(1); 852 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 853 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 854 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 855 | expect(clientWidth).toBe(300); 856 | expect(clientHeight).toBe(300); 857 | expect(onResize.getCall(0).args[3]).toEqual({ width: 200, height: 200 }); 858 | }); 859 | 860 | test('should aspect ratio locked with 2:1 ratio when resize to bottom', async ({ mount, page }) => { 861 | const onResize = spy(); 862 | const onResizeStart = spy(); 863 | const onResizeStop = spy(); 864 | const resizable = await mount( 865 | , 872 | ); 873 | const divs = resizable.locator('div'); 874 | const bottomHandle = (await divs.all())[3]; 875 | await bottomHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 876 | await page.mouse.down(); 877 | await page.mouse.move(0, 200); 878 | await page.mouse.up(); 879 | expect(onResizeStop.callCount).toBe(1); 880 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 881 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 882 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 883 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 884 | expect(clientWidth).toBe(600); 885 | expect(clientHeight).toBe(300); 886 | expect(onResize.getCall(0).args[3]).toEqual({ width: 400, height: 200 }); 887 | }); 888 | 889 | test('should aspect ratio locked with 2:1 ratio with extra width/height when resize to bottom', async ({ 890 | mount, 891 | page, 892 | }) => { 893 | const onResize = spy(); 894 | const onResizeStart = spy(); 895 | const onResizeStop = spy(); 896 | const resizable = await mount( 897 | , 906 | ); 907 | const divs = resizable.locator('div'); 908 | const bottomHandle = (await divs.all())[3]; 909 | await bottomHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 910 | await page.mouse.down(); 911 | await page.mouse.move(0, 200); 912 | await page.mouse.up(); 913 | expect(onResizeStop.callCount).toBe(1); 914 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 915 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 916 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 917 | expect(clientWidth).toBe(650); 918 | expect(clientHeight).toBe(350); 919 | expect(onResize.getCall(0).args[3]).toEqual({ width: 400, height: 200 }); 920 | }); 921 | 922 | test('should clamped by parent width', async ({ mount, page }) => { 923 | const onResize = spy(); 924 | const onResizeStart = spy(); 925 | const onResizeStop = spy(); 926 | const resizable = await mount( 927 |
928 | 935 |
, 936 | ); 937 | 938 | const divs = resizable.locator('div'); 939 | const handle = (await divs.all())[7]; 940 | if (!handle) throw new Error('Handle not found'); 941 | 942 | await handle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 943 | await page.mouse.down(); 944 | await page.mouse.move(200, 0); 945 | await page.mouse.up(); 946 | 947 | expect(onResize.getCall(0).args[0].isTrusted).toBe(true); 948 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 949 | expect(clientWidth).toBe(200); 950 | expect(onResize.getCall(0).args[3]).toEqual({ width: 100, height: 0 }); 951 | }); 952 | 953 | test('should clamped by parent height', async ({ mount, page }) => { 954 | const onResize = spy(); 955 | const onResizeStart = spy(); 956 | const onResizeStop = spy(); 957 | const resizable = await mount( 958 |
959 | 966 |
, 967 | ); 968 | 969 | const divs = resizable.locator('div'); 970 | const handle = (await divs.all())[7]; 971 | if (!handle) throw new Error('Handle not found'); 972 | 973 | await handle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 974 | await page.mouse.down(); 975 | await page.mouse.move(0, 200); 976 | await page.mouse.up(); 977 | 978 | expect(onResize.getCall(0).args[0].isTrusted).toBe(true); 979 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 980 | expect(clientHeight).toBe(200); 981 | expect(onResize.getCall(0).args[3]).toEqual({ width: 0, height: 100 }); 982 | }); 983 | 984 | test('should defaultSize ignored when size set', async ({ mount }) => { 985 | const resizable = await mount( 986 | , 987 | ); 988 | const divs = resizable.locator('div'); 989 | expect(await divs.count()).toBe(9); 990 | expect(await resizable.evaluate(node => node.style.width)).toBe('200px'); 991 | expect(await resizable.evaluate(node => node.style.height)).toBe('300px'); 992 | expect(await resizable.evaluate(node => node.style.position)).toBe('relative'); 993 | }); 994 | 995 | test('should render a handleComponent for right', async ({ mount }) => { 996 | const CustomComponent =
; 997 | const resizable = await mount(); 998 | const divs = resizable.locator('div'); 999 | const handles = await divs.all(); 1000 | const rightHandle = handles[2]; 1001 | 1002 | expect(await rightHandle.evaluate(node => node.children.length)).toBe(1); 1003 | expect(await rightHandle.locator('.customHandle-right').count()).toBe(1); 1004 | }); 1005 | 1006 | test('should adjust resizing for specified scale', async ({ mount, page }) => { 1007 | const onResize = spy(); 1008 | const onResizeStart = spy(); 1009 | const onResizeStop = spy(); 1010 | const resizable = await mount( 1011 | , 1019 | ); 1020 | const divs = resizable.locator('div'); 1021 | const handle = (await divs.all())[6]; 1022 | if (!handle) throw new Error('Handle not found'); 1023 | await handle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 1024 | await page.mouse.move(200, 220); 1025 | await page.mouse.up(); 1026 | expect(onResize.callCount).toBe(1); 1027 | expect(onResize.getCall(0).args[0].isTrusted).toBeTruthy(); 1028 | expect(onResize.getCall(0).args[1]).toBe('bottomRight'); 1029 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 1030 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 1031 | expect(clientWidth).toBe(500); 1032 | expect(clientHeight).toBe(540); 1033 | expect(onResize.getCall(0).args[3]).toEqual({ width: 400, height: 440 }); 1034 | }); 1035 | 1036 | test('should call onResizeStop with expected delta when resize stop direction right', async ({ mount, page }) => { 1037 | const onResize = spy(); 1038 | const onResizeStart = spy(); 1039 | const onResizeStop = spy(); 1040 | const resizable = await mount( 1041 | , 1048 | ); 1049 | const divs = resizable.locator('div'); 1050 | const rightHandle = (await divs.all())[2]; 1051 | await rightHandle.dispatchEvent('mousedown', { button: 0, clientX: 0, clientY: 0 }); 1052 | await page.mouse.move(200, 220); 1053 | await page.mouse.up(); 1054 | expect(onResizeStop.callCount).toBe(1); 1055 | expect(onResizeStop.getCall(0).args[1]).toBe('right'); 1056 | const clientWidth = await resizable.evaluate(el => el.clientWidth); 1057 | expect(clientWidth).toBe(300); 1058 | const clientHeight = await resizable.evaluate(el => el.clientHeight); 1059 | expect(clientHeight).toBe(150); 1060 | expect(onResizeStop.getCall(0).args[3]).toEqual({ width: 200, height: 100 }); 1061 | }); 1062 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { PureComponent } from 'react'; 2 | import { flushSync } from 'react-dom'; 3 | 4 | import { Resizer, Direction } from './resizer'; 5 | 6 | const DEFAULT_SIZE = { 7 | width: 'auto', 8 | height: 'auto', 9 | }; 10 | 11 | export type ResizeDirection = Direction; 12 | 13 | export interface Enable { 14 | top?: boolean; 15 | right?: boolean; 16 | bottom?: boolean; 17 | left?: boolean; 18 | topRight?: boolean; 19 | bottomRight?: boolean; 20 | bottomLeft?: boolean; 21 | topLeft?: boolean; 22 | } 23 | 24 | export interface HandleStyles { 25 | top?: React.CSSProperties; 26 | right?: React.CSSProperties; 27 | bottom?: React.CSSProperties; 28 | left?: React.CSSProperties; 29 | topRight?: React.CSSProperties; 30 | bottomRight?: React.CSSProperties; 31 | bottomLeft?: React.CSSProperties; 32 | topLeft?: React.CSSProperties; 33 | } 34 | 35 | export interface HandleClassName { 36 | top?: string; 37 | right?: string; 38 | bottom?: string; 39 | left?: string; 40 | topRight?: string; 41 | bottomRight?: string; 42 | bottomLeft?: string; 43 | topLeft?: string; 44 | } 45 | 46 | export interface Size { 47 | width?: string | number; 48 | height?: string | number; 49 | } 50 | 51 | export interface NumberSize { 52 | width: number; 53 | height: number; 54 | } 55 | 56 | export interface HandleComponent { 57 | top?: React.ReactElement; 58 | right?: React.ReactElement; 59 | bottom?: React.ReactElement; 60 | left?: React.ReactElement; 61 | topRight?: React.ReactElement; 62 | bottomRight?: React.ReactElement; 63 | bottomLeft?: React.ReactElement; 64 | topLeft?: React.ReactElement; 65 | } 66 | 67 | export type ResizeCallback = ( 68 | event: MouseEvent | TouchEvent, 69 | direction: Direction, 70 | elementRef: HTMLElement, 71 | delta: NumberSize, 72 | ) => void; 73 | 74 | export type ResizeStartCallback = ( 75 | e: React.MouseEvent | React.TouchEvent, 76 | dir: Direction, 77 | elementRef: HTMLElement, 78 | ) => void | boolean; 79 | 80 | export interface ResizableProps { 81 | as?: string | React.ComponentType; 82 | style?: React.CSSProperties; 83 | className?: string; 84 | grid?: [number, number]; 85 | gridGap?: [number, number]; 86 | snap?: { 87 | x?: number[]; 88 | y?: number[]; 89 | }; 90 | snapGap?: number; 91 | bounds?: 'parent' | 'window' | HTMLElement; 92 | boundsByDirection?: boolean; 93 | size?: Size; 94 | minWidth?: string | number; 95 | minHeight?: string | number; 96 | maxWidth?: string | number; 97 | maxHeight?: string | number; 98 | lockAspectRatio?: boolean | number; 99 | lockAspectRatioExtraWidth?: number; 100 | lockAspectRatioExtraHeight?: number; 101 | enable?: Enable | false; 102 | handleStyles?: HandleStyles; 103 | handleClasses?: HandleClassName; 104 | handleWrapperStyle?: React.CSSProperties; 105 | handleWrapperClass?: string; 106 | handleComponent?: HandleComponent; 107 | children?: React.ReactNode; 108 | onResizeStart?: ResizeStartCallback; 109 | onResize?: ResizeCallback; 110 | onResizeStop?: ResizeCallback; 111 | defaultSize?: Size; 112 | scale?: number; 113 | resizeRatio?: number | [number, number]; 114 | } 115 | 116 | interface State { 117 | isResizing: boolean; 118 | direction: Direction; 119 | original: { 120 | x: number; 121 | y: number; 122 | width: number; 123 | height: number; 124 | }; 125 | width: number | string; 126 | height: number | string; 127 | 128 | backgroundStyle: React.CSSProperties; 129 | flexBasis?: string | number; 130 | } 131 | 132 | const clamp = (n: number, min: number, max: number): number => Math.max(Math.min(n, max), min); 133 | const snap = (n: number, size: number, gridGap: number): number => { 134 | const v = Math.round(n / size); 135 | 136 | return v * size + gridGap * (v - 1); 137 | }; 138 | const hasDirection = (dir: 'top' | 'right' | 'bottom' | 'left', target: string): boolean => 139 | new RegExp(dir, 'i').test(target); 140 | 141 | // INFO: In case of window is a Proxy and does not porxy Events correctly, use isTouchEvent & isMouseEvent to distinguish event type instead of `instanceof`. 142 | const isTouchEvent = (event: MouseEvent | TouchEvent): event is TouchEvent => { 143 | return Boolean((event as TouchEvent).touches && (event as TouchEvent).touches.length); 144 | }; 145 | 146 | const isMouseEvent = (event: MouseEvent | TouchEvent): event is MouseEvent => { 147 | return Boolean( 148 | ((event as MouseEvent).clientX || (event as MouseEvent).clientX === 0) && 149 | ((event as MouseEvent).clientY || (event as MouseEvent).clientY === 0), 150 | ); 151 | }; 152 | 153 | const findClosestSnap = (n: number, snapArray: number[], snapGap: number = 0): number => { 154 | const closestGapIndex = snapArray.reduce( 155 | (prev, curr, index) => (Math.abs(curr - n) < Math.abs(snapArray[prev] - n) ? index : prev), 156 | 0, 157 | ); 158 | const gap = Math.abs(snapArray[closestGapIndex] - n); 159 | 160 | return snapGap === 0 || gap < snapGap ? snapArray[closestGapIndex] : n; 161 | }; 162 | 163 | const getStringSize = (n: number | string): string => { 164 | n = n.toString(); 165 | if (n === 'auto') { 166 | return n; 167 | } 168 | if (n.endsWith('px')) { 169 | return n; 170 | } 171 | if (n.endsWith('%')) { 172 | return n; 173 | } 174 | if (n.endsWith('vh')) { 175 | return n; 176 | } 177 | if (n.endsWith('vw')) { 178 | return n; 179 | } 180 | if (n.endsWith('vmax')) { 181 | return n; 182 | } 183 | if (n.endsWith('vmin')) { 184 | return n; 185 | } 186 | return `${n}px`; 187 | }; 188 | 189 | const getPixelSize = ( 190 | size: undefined | string | number, 191 | parentSize: number, 192 | innerWidth: number, 193 | innerHeight: number, 194 | ) => { 195 | if (size && typeof size === 'string') { 196 | if (size.endsWith('px')) { 197 | return Number(size.replace('px', '')); 198 | } 199 | if (size.endsWith('%')) { 200 | const ratio = Number(size.replace('%', '')) / 100; 201 | return parentSize * ratio; 202 | } 203 | if (size.endsWith('vw')) { 204 | const ratio = Number(size.replace('vw', '')) / 100; 205 | return innerWidth * ratio; 206 | } 207 | if (size.endsWith('vh')) { 208 | const ratio = Number(size.replace('vh', '')) / 100; 209 | return innerHeight * ratio; 210 | } 211 | } 212 | return size; 213 | }; 214 | 215 | const calculateNewMax = ( 216 | parentSize: { width: number; height: number }, 217 | innerWidth: number, 218 | innerHeight: number, 219 | maxWidth?: string | number, 220 | maxHeight?: string | number, 221 | minWidth?: string | number, 222 | minHeight?: string | number, 223 | ) => { 224 | maxWidth = getPixelSize(maxWidth, parentSize.width, innerWidth, innerHeight); 225 | maxHeight = getPixelSize(maxHeight, parentSize.height, innerWidth, innerHeight); 226 | minWidth = getPixelSize(minWidth, parentSize.width, innerWidth, innerHeight); 227 | minHeight = getPixelSize(minHeight, parentSize.height, innerWidth, innerHeight); 228 | return { 229 | maxWidth: typeof maxWidth === 'undefined' ? undefined : Number(maxWidth), 230 | maxHeight: typeof maxHeight === 'undefined' ? undefined : Number(maxHeight), 231 | minWidth: typeof minWidth === 'undefined' ? undefined : Number(minWidth), 232 | minHeight: typeof minHeight === 'undefined' ? undefined : Number(minHeight), 233 | }; 234 | }; 235 | 236 | /** 237 | * transform T | [T, T] to [T, T] 238 | * @param val 239 | * @returns 240 | */ 241 | // tslint:disable-next-line 242 | const normalizeToPair = (val: T | [T, T]): [T, T] => (Array.isArray(val) ? val : [val, val]); 243 | 244 | const definedProps = [ 245 | 'as', 246 | 'ref', 247 | 'style', 248 | 'className', 249 | 'grid', 250 | 'gridGap', 251 | 'snap', 252 | 'bounds', 253 | 'boundsByDirection', 254 | 'size', 255 | 'defaultSize', 256 | 'minWidth', 257 | 'minHeight', 258 | 'maxWidth', 259 | 'maxHeight', 260 | 'lockAspectRatio', 261 | 'lockAspectRatioExtraWidth', 262 | 'lockAspectRatioExtraHeight', 263 | 'enable', 264 | 'handleStyles', 265 | 'handleClasses', 266 | 'handleWrapperStyle', 267 | 'handleWrapperClass', 268 | 'children', 269 | 'onResizeStart', 270 | 'onResize', 271 | 'onResizeStop', 272 | 'handleComponent', 273 | 'scale', 274 | 'resizeRatio', 275 | 'snapGap', 276 | ]; 277 | 278 | // HACK: This class is used to calculate % size. 279 | const baseClassName = '__resizable_base__'; 280 | 281 | declare global { 282 | interface Window { 283 | MouseEvent: typeof MouseEvent; 284 | TouchEvent: typeof TouchEvent; 285 | } 286 | } 287 | 288 | interface NewSize { 289 | newHeight: number | string; 290 | newWidth: number | string; 291 | } 292 | export class Resizable extends PureComponent { 293 | flexDir?: 'row' | 'column'; 294 | 295 | get parentNode(): HTMLElement | null { 296 | if (!this.resizable) { 297 | return null; 298 | } 299 | return this.resizable.parentNode as HTMLElement; 300 | } 301 | 302 | get window(): Window | null { 303 | if (!this.resizable) { 304 | return null; 305 | } 306 | if (!this.resizable.ownerDocument) { 307 | return null; 308 | } 309 | return this.resizable.ownerDocument.defaultView as Window; 310 | } 311 | 312 | get propsSize(): Size { 313 | return this.props.size || this.props.defaultSize || DEFAULT_SIZE; 314 | } 315 | 316 | get size(): NumberSize { 317 | let width = 0; 318 | let height = 0; 319 | if (this.resizable && this.window) { 320 | const orgWidth = this.resizable.offsetWidth; 321 | const orgHeight = this.resizable.offsetHeight; 322 | // HACK: Set position `relative` to get parent size. 323 | // This is because when re-resizable set `absolute`, I can not get base width correctly. 324 | const orgPosition = this.resizable.style.position; 325 | if (orgPosition !== 'relative') { 326 | this.resizable.style.position = 'relative'; 327 | } 328 | // INFO: Use original width or height if set auto. 329 | width = this.resizable.style.width !== 'auto' ? this.resizable.offsetWidth : orgWidth; 330 | height = this.resizable.style.height !== 'auto' ? this.resizable.offsetHeight : orgHeight; 331 | // Restore original position 332 | this.resizable.style.position = orgPosition; 333 | } 334 | return { width, height }; 335 | } 336 | 337 | get sizeStyle(): { width: string; height: string } { 338 | const { size } = this.props; 339 | const getSize = (key: 'width' | 'height'): string => { 340 | if (typeof this.state[key] === 'undefined' || this.state[key] === 'auto') { 341 | return 'auto'; 342 | } 343 | if (this.propsSize && this.propsSize[key] && this.propsSize[key]?.toString().endsWith('%')) { 344 | if (this.state[key].toString().endsWith('%')) { 345 | return this.state[key].toString(); 346 | } 347 | const parentSize = this.getParentSize(); 348 | const value = Number(this.state[key].toString().replace('px', '')); 349 | const percent = (value / parentSize[key]) * 100; 350 | return `${percent}%`; 351 | } 352 | return getStringSize(this.state[key]); 353 | }; 354 | const width = 355 | size && typeof size.width !== 'undefined' && !this.state.isResizing 356 | ? getStringSize(size.width) 357 | : getSize('width'); 358 | const height = 359 | size && typeof size.height !== 'undefined' && !this.state.isResizing 360 | ? getStringSize(size.height) 361 | : getSize('height'); 362 | return { width, height }; 363 | } 364 | 365 | public static defaultProps = { 366 | as: 'div', 367 | onResizeStart: () => {}, 368 | onResize: () => {}, 369 | onResizeStop: () => {}, 370 | enable: { 371 | top: true, 372 | right: true, 373 | bottom: true, 374 | left: true, 375 | topRight: true, 376 | bottomRight: true, 377 | bottomLeft: true, 378 | topLeft: true, 379 | }, 380 | style: {}, 381 | grid: [1, 1], 382 | gridGap: [0, 0], 383 | lockAspectRatio: false, 384 | lockAspectRatioExtraWidth: 0, 385 | lockAspectRatioExtraHeight: 0, 386 | scale: 1, 387 | resizeRatio: 1, 388 | snapGap: 0, 389 | }; 390 | ratio = 1; 391 | resizable: HTMLElement | null = null; 392 | // For parent boundary 393 | parentLeft = 0; 394 | parentTop = 0; 395 | // For boundary 396 | resizableLeft = 0; 397 | resizableRight = 0; 398 | resizableTop = 0; 399 | resizableBottom = 0; 400 | // For target boundary 401 | targetLeft = 0; 402 | targetTop = 0; 403 | delta = { 404 | width: 0, 405 | height: 0, 406 | }; 407 | 408 | constructor(props: ResizableProps) { 409 | super(props); 410 | this.state = { 411 | isResizing: false, 412 | width: this.propsSize?.width ?? 'auto', 413 | height: this.propsSize?.height ?? 'auto', 414 | direction: 'right', 415 | original: { 416 | x: 0, 417 | y: 0, 418 | width: 0, 419 | height: 0, 420 | }, 421 | backgroundStyle: { 422 | height: '100%', 423 | width: '100%', 424 | backgroundColor: 'rgba(0,0,0,0)', 425 | cursor: 'auto', 426 | opacity: 0, 427 | position: 'fixed', 428 | zIndex: 9999, 429 | top: '0', 430 | left: '0', 431 | bottom: '0', 432 | right: '0', 433 | }, 434 | flexBasis: undefined, 435 | }; 436 | 437 | this.onResizeStart = this.onResizeStart.bind(this); 438 | this.onMouseMove = this.onMouseMove.bind(this); 439 | this.onMouseUp = this.onMouseUp.bind(this); 440 | } 441 | 442 | getParentSize(): { width: number; height: number } { 443 | if (!this.parentNode) { 444 | if (!this.window) { 445 | return { width: 0, height: 0 }; 446 | } 447 | return { width: this.window.innerWidth, height: this.window.innerHeight }; 448 | } 449 | const base = this.appendBase(); 450 | if (!base) { 451 | return { width: 0, height: 0 }; 452 | } 453 | // INFO: To calculate parent width with flex layout 454 | let wrapChanged = false; 455 | const wrap = this.parentNode.style.flexWrap; 456 | if (wrap !== 'wrap') { 457 | wrapChanged = true; 458 | this.parentNode.style.flexWrap = 'wrap'; 459 | // HACK: Use relative to get parent padding size 460 | } 461 | base.style.position = 'relative'; 462 | base.style.minWidth = '100%'; 463 | base.style.minHeight = '100%'; 464 | const size = { 465 | width: base.offsetWidth, 466 | height: base.offsetHeight, 467 | }; 468 | if (wrapChanged) { 469 | this.parentNode.style.flexWrap = wrap; 470 | } 471 | this.removeBase(base); 472 | return size; 473 | } 474 | 475 | bindEvents() { 476 | if (this.window) { 477 | this.window.addEventListener('mouseup', this.onMouseUp); 478 | this.window.addEventListener('mousemove', this.onMouseMove); 479 | this.window.addEventListener('mouseleave', this.onMouseUp); 480 | this.window.addEventListener('touchmove', this.onMouseMove, { 481 | capture: true, 482 | passive: false, 483 | }); 484 | this.window.addEventListener('touchend', this.onMouseUp); 485 | } 486 | } 487 | 488 | unbindEvents() { 489 | if (this.window) { 490 | this.window.removeEventListener('mouseup', this.onMouseUp); 491 | this.window.removeEventListener('mousemove', this.onMouseMove); 492 | this.window.removeEventListener('mouseleave', this.onMouseUp); 493 | this.window.removeEventListener('touchmove', this.onMouseMove, true); 494 | this.window.removeEventListener('touchend', this.onMouseUp); 495 | } 496 | } 497 | 498 | componentDidMount() { 499 | if (!this.resizable || !this.window) { 500 | return; 501 | } 502 | const computedStyle = this.window.getComputedStyle(this.resizable); 503 | this.setState({ 504 | width: this.state.width || this.size.width, 505 | height: this.state.height || this.size.height, 506 | flexBasis: computedStyle.flexBasis !== 'auto' ? computedStyle.flexBasis : undefined, 507 | }); 508 | } 509 | 510 | appendBase = () => { 511 | if (!this.resizable || !this.window) { 512 | return null; 513 | } 514 | const parent = this.parentNode; 515 | if (!parent) { 516 | return null; 517 | } 518 | const element = this.window.document.createElement('div'); 519 | element.style.width = '100%'; 520 | element.style.height = '100%'; 521 | element.style.position = 'absolute'; 522 | element.style.transform = 'scale(0, 0)'; 523 | element.style.left = '0'; 524 | element.style.flex = '0 0 100%'; 525 | if (element.classList) { 526 | element.classList.add(baseClassName); 527 | } else { 528 | element.className += baseClassName; 529 | } 530 | parent.appendChild(element); 531 | return element; 532 | }; 533 | 534 | removeBase = (base: HTMLElement) => { 535 | const parent = this.parentNode; 536 | if (!parent) { 537 | return; 538 | } 539 | parent.removeChild(base); 540 | }; 541 | 542 | componentWillUnmount() { 543 | if (this.window) { 544 | this.unbindEvents(); 545 | } 546 | } 547 | 548 | createSizeForCssProperty(newSize: number | string, kind: 'width' | 'height'): number | string { 549 | const propsSize = this.propsSize && this.propsSize[kind]; 550 | return this.state[kind] === 'auto' && 551 | this.state.original[kind] === newSize && 552 | (typeof propsSize === 'undefined' || propsSize === 'auto') 553 | ? 'auto' 554 | : newSize; 555 | } 556 | 557 | calculateNewMaxFromBoundary(maxWidth?: number, maxHeight?: number) { 558 | const { boundsByDirection } = this.props; 559 | const { direction } = this.state; 560 | const widthByDirection = boundsByDirection && hasDirection('left', direction); 561 | const heightByDirection = boundsByDirection && hasDirection('top', direction); 562 | let boundWidth; 563 | let boundHeight; 564 | if (this.props.bounds === 'parent') { 565 | const parent = this.parentNode; 566 | if (parent) { 567 | boundWidth = widthByDirection 568 | ? this.resizableRight - this.parentLeft 569 | : parent.offsetWidth + (this.parentLeft - this.resizableLeft); 570 | boundHeight = heightByDirection 571 | ? this.resizableBottom - this.parentTop 572 | : parent.offsetHeight + (this.parentTop - this.resizableTop); 573 | } 574 | } else if (this.props.bounds === 'window') { 575 | if (this.window) { 576 | boundWidth = widthByDirection ? this.resizableRight : this.window.innerWidth - this.resizableLeft; 577 | boundHeight = heightByDirection ? this.resizableBottom : this.window.innerHeight - this.resizableTop; 578 | } 579 | } else if (this.props.bounds) { 580 | boundWidth = widthByDirection 581 | ? this.resizableRight - this.targetLeft 582 | : this.props.bounds.offsetWidth + (this.targetLeft - this.resizableLeft); 583 | boundHeight = heightByDirection 584 | ? this.resizableBottom - this.targetTop 585 | : this.props.bounds.offsetHeight + (this.targetTop - this.resizableTop); 586 | } 587 | if (boundWidth && Number.isFinite(boundWidth)) { 588 | maxWidth = maxWidth && maxWidth < boundWidth ? maxWidth : boundWidth; 589 | } 590 | if (boundHeight && Number.isFinite(boundHeight)) { 591 | maxHeight = maxHeight && maxHeight < boundHeight ? maxHeight : boundHeight; 592 | } 593 | return { maxWidth, maxHeight }; 594 | } 595 | 596 | calculateNewSizeFromDirection(clientX: number, clientY: number) { 597 | const scale = this.props.scale || 1; 598 | const [resizeRatioX, resizeRatioY] = normalizeToPair(this.props.resizeRatio || 1); 599 | const { direction, original } = this.state; 600 | const { lockAspectRatio, lockAspectRatioExtraHeight, lockAspectRatioExtraWidth } = this.props; 601 | let newWidth = original.width; 602 | let newHeight = original.height; 603 | const extraHeight = lockAspectRatioExtraHeight || 0; 604 | const extraWidth = lockAspectRatioExtraWidth || 0; 605 | if (hasDirection('right', direction)) { 606 | newWidth = original.width + ((clientX - original.x) * resizeRatioX) / scale; 607 | if (lockAspectRatio) { 608 | newHeight = (newWidth - extraWidth) / this.ratio + extraHeight; 609 | } 610 | } 611 | if (hasDirection('left', direction)) { 612 | newWidth = original.width - ((clientX - original.x) * resizeRatioX) / scale; 613 | if (lockAspectRatio) { 614 | newHeight = (newWidth - extraWidth) / this.ratio + extraHeight; 615 | } 616 | } 617 | if (hasDirection('bottom', direction)) { 618 | newHeight = original.height + ((clientY - original.y) * resizeRatioY) / scale; 619 | if (lockAspectRatio) { 620 | newWidth = (newHeight - extraHeight) * this.ratio + extraWidth; 621 | } 622 | } 623 | if (hasDirection('top', direction)) { 624 | newHeight = original.height - ((clientY - original.y) * resizeRatioY) / scale; 625 | if (lockAspectRatio) { 626 | newWidth = (newHeight - extraHeight) * this.ratio + extraWidth; 627 | } 628 | } 629 | return { newWidth, newHeight }; 630 | } 631 | 632 | calculateNewSizeFromAspectRatio( 633 | newWidth: number, 634 | newHeight: number, 635 | max: { width?: number; height?: number }, 636 | min: { width?: number; height?: number }, 637 | ) { 638 | const { lockAspectRatio, lockAspectRatioExtraHeight, lockAspectRatioExtraWidth } = this.props; 639 | const computedMinWidth = typeof min.width === 'undefined' ? 10 : min.width; 640 | const computedMaxWidth = typeof max.width === 'undefined' || max.width < 0 ? newWidth : max.width; 641 | const computedMinHeight = typeof min.height === 'undefined' ? 10 : min.height; 642 | const computedMaxHeight = typeof max.height === 'undefined' || max.height < 0 ? newHeight : max.height; 643 | const extraHeight = lockAspectRatioExtraHeight || 0; 644 | const extraWidth = lockAspectRatioExtraWidth || 0; 645 | if (lockAspectRatio) { 646 | const extraMinWidth = (computedMinHeight - extraHeight) * this.ratio + extraWidth; 647 | const extraMaxWidth = (computedMaxHeight - extraHeight) * this.ratio + extraWidth; 648 | const extraMinHeight = (computedMinWidth - extraWidth) / this.ratio + extraHeight; 649 | const extraMaxHeight = (computedMaxWidth - extraWidth) / this.ratio + extraHeight; 650 | const lockedMinWidth = Math.max(computedMinWidth, extraMinWidth); 651 | const lockedMaxWidth = Math.min(computedMaxWidth, extraMaxWidth); 652 | const lockedMinHeight = Math.max(computedMinHeight, extraMinHeight); 653 | const lockedMaxHeight = Math.min(computedMaxHeight, extraMaxHeight); 654 | newWidth = clamp(newWidth, lockedMinWidth, lockedMaxWidth); 655 | newHeight = clamp(newHeight, lockedMinHeight, lockedMaxHeight); 656 | } else { 657 | newWidth = clamp(newWidth, computedMinWidth, computedMaxWidth); 658 | newHeight = clamp(newHeight, computedMinHeight, computedMaxHeight); 659 | } 660 | return { newWidth, newHeight }; 661 | } 662 | 663 | setBoundingClientRect() { 664 | const adjustedScale = 1 / (this.props.scale || 1); 665 | 666 | // For parent boundary 667 | if (this.props.bounds === 'parent') { 668 | const parent = this.parentNode; 669 | if (parent) { 670 | const parentRect = parent.getBoundingClientRect(); 671 | this.parentLeft = parentRect.left * adjustedScale; 672 | this.parentTop = parentRect.top * adjustedScale; 673 | } 674 | } 675 | 676 | // For target(html element) boundary 677 | if (this.props.bounds && typeof this.props.bounds !== 'string') { 678 | const targetRect = this.props.bounds.getBoundingClientRect(); 679 | this.targetLeft = targetRect.left * adjustedScale; 680 | this.targetTop = targetRect.top * adjustedScale; 681 | } 682 | 683 | // For boundary 684 | if (this.resizable) { 685 | const { left, top, right, bottom } = this.resizable.getBoundingClientRect(); 686 | this.resizableLeft = left * adjustedScale; 687 | this.resizableRight = right * adjustedScale; 688 | this.resizableTop = top * adjustedScale; 689 | this.resizableBottom = bottom * adjustedScale; 690 | } 691 | } 692 | 693 | onResizeStart(event: React.MouseEvent | React.TouchEvent, direction: Direction) { 694 | if (!this.resizable || !this.window) { 695 | return; 696 | } 697 | let clientX = 0; 698 | let clientY = 0; 699 | if (event.nativeEvent && isMouseEvent(event.nativeEvent)) { 700 | clientX = event.nativeEvent.clientX; 701 | clientY = event.nativeEvent.clientY; 702 | } else if (event.nativeEvent && isTouchEvent(event.nativeEvent)) { 703 | clientX = (event.nativeEvent as TouchEvent).touches[0].clientX; 704 | clientY = (event.nativeEvent as TouchEvent).touches[0].clientY; 705 | } 706 | if (this.props.onResizeStart) { 707 | if (this.resizable) { 708 | const startResize = this.props.onResizeStart(event, direction, this.resizable); 709 | if (startResize === false) { 710 | return; 711 | } 712 | } 713 | } 714 | 715 | // Fix #168 716 | if (this.props.size) { 717 | if (typeof this.props.size.height !== 'undefined' && this.props.size.height !== this.state.height) { 718 | this.setState({ height: this.props.size.height }); 719 | } 720 | if (typeof this.props.size.width !== 'undefined' && this.props.size.width !== this.state.width) { 721 | this.setState({ width: this.props.size.width }); 722 | } 723 | } 724 | 725 | // For lockAspectRatio case 726 | this.ratio = 727 | typeof this.props.lockAspectRatio === 'number' ? this.props.lockAspectRatio : this.size.width / this.size.height; 728 | 729 | let flexBasis; 730 | const computedStyle = this.window.getComputedStyle(this.resizable); 731 | if (computedStyle.flexBasis !== 'auto') { 732 | const parent = this.parentNode; 733 | if (parent) { 734 | const dir = this.window.getComputedStyle(parent).flexDirection; 735 | this.flexDir = dir.startsWith('row') ? 'row' : 'column'; 736 | flexBasis = computedStyle.flexBasis; 737 | } 738 | } 739 | // For boundary 740 | this.setBoundingClientRect(); 741 | this.bindEvents(); 742 | const state = { 743 | original: { 744 | x: clientX, 745 | y: clientY, 746 | width: this.size.width, 747 | height: this.size.height, 748 | }, 749 | isResizing: true, 750 | backgroundStyle: { 751 | ...this.state.backgroundStyle, 752 | cursor: this.window.getComputedStyle(event.target as HTMLElement).cursor || 'auto', 753 | }, 754 | direction, 755 | flexBasis, 756 | }; 757 | 758 | this.setState(state); 759 | } 760 | 761 | onMouseMove(event: MouseEvent | TouchEvent) { 762 | if (!this.state.isResizing || !this.resizable || !this.window) { 763 | return; 764 | } 765 | if (this.window.TouchEvent && isTouchEvent(event)) { 766 | try { 767 | event.preventDefault(); 768 | event.stopPropagation(); 769 | } catch (e) { 770 | // Ignore on fail 771 | } 772 | } 773 | let { maxWidth, maxHeight, minWidth, minHeight } = this.props; 774 | const clientX = isTouchEvent(event) ? event.touches[0].clientX : event.clientX; 775 | const clientY = isTouchEvent(event) ? event.touches[0].clientY : event.clientY; 776 | const { direction, original, width, height } = this.state; 777 | const parentSize = this.getParentSize(); 778 | const max = calculateNewMax( 779 | parentSize, 780 | this.window.innerWidth, 781 | this.window.innerHeight, 782 | maxWidth, 783 | maxHeight, 784 | minWidth, 785 | minHeight, 786 | ); 787 | 788 | maxWidth = max.maxWidth; 789 | maxHeight = max.maxHeight; 790 | minWidth = max.minWidth; 791 | minHeight = max.minHeight; 792 | 793 | // Calculate new size 794 | let { newHeight, newWidth }: NewSize = this.calculateNewSizeFromDirection(clientX, clientY); 795 | 796 | // Calculate max size from boundary settings 797 | const boundaryMax = this.calculateNewMaxFromBoundary(maxWidth, maxHeight); 798 | 799 | if (this.props.snap && this.props.snap.x) { 800 | newWidth = findClosestSnap(newWidth, this.props.snap.x, this.props.snapGap); 801 | } 802 | if (this.props.snap && this.props.snap.y) { 803 | newHeight = findClosestSnap(newHeight, this.props.snap.y, this.props.snapGap); 804 | } 805 | 806 | // Calculate new size from aspect ratio 807 | const newSize = this.calculateNewSizeFromAspectRatio( 808 | newWidth, 809 | newHeight, 810 | { width: boundaryMax.maxWidth, height: boundaryMax.maxHeight }, 811 | { width: minWidth, height: minHeight }, 812 | ); 813 | newWidth = newSize.newWidth; 814 | newHeight = newSize.newHeight; 815 | 816 | if (this.props.grid) { 817 | const newGridWidth = snap(newWidth, this.props.grid[0], this.props.gridGap ? this.props.gridGap[0] : 0); 818 | const newGridHeight = snap(newHeight, this.props.grid[1], this.props.gridGap ? this.props.gridGap[1] : 0); 819 | const gap = this.props.snapGap || 0; 820 | const w = gap === 0 || Math.abs(newGridWidth - newWidth) <= gap ? newGridWidth : newWidth; 821 | const h = gap === 0 || Math.abs(newGridHeight - newHeight) <= gap ? newGridHeight : newHeight; 822 | newWidth = w; 823 | newHeight = h; 824 | } 825 | 826 | const delta = { 827 | width: newWidth - original.width, 828 | height: newHeight - original.height, 829 | }; 830 | this.delta = delta; 831 | 832 | if (width && typeof width === 'string') { 833 | if (width.endsWith('%')) { 834 | const percent = (newWidth / parentSize.width) * 100; 835 | newWidth = `${percent}%`; 836 | } else if (width.endsWith('vw')) { 837 | const vw = (newWidth / this.window.innerWidth) * 100; 838 | newWidth = `${vw}vw`; 839 | } else if (width.endsWith('vh')) { 840 | const vh = (newWidth / this.window.innerHeight) * 100; 841 | newWidth = `${vh}vh`; 842 | } 843 | } 844 | 845 | if (height && typeof height === 'string') { 846 | if (height.endsWith('%')) { 847 | const percent = (newHeight / parentSize.height) * 100; 848 | newHeight = `${percent}%`; 849 | } else if (height.endsWith('vw')) { 850 | const vw = (newHeight / this.window.innerWidth) * 100; 851 | newHeight = `${vw}vw`; 852 | } else if (height.endsWith('vh')) { 853 | const vh = (newHeight / this.window.innerHeight) * 100; 854 | newHeight = `${vh}vh`; 855 | } 856 | } 857 | 858 | const newState: { width: string | number; height: string | number; flexBasis?: string | number } = { 859 | width: this.createSizeForCssProperty(newWidth, 'width'), 860 | height: this.createSizeForCssProperty(newHeight, 'height'), 861 | }; 862 | 863 | if (this.flexDir === 'row') { 864 | newState.flexBasis = newState.width; 865 | } else if (this.flexDir === 'column') { 866 | newState.flexBasis = newState.height; 867 | } 868 | 869 | const widthChanged = this.state.width !== newState.width; 870 | const heightChanged = this.state.height !== newState.height; 871 | const flexBaseChanged = this.state.flexBasis !== newState.flexBasis; 872 | const changed = widthChanged || heightChanged || flexBaseChanged; 873 | 874 | if (changed) { 875 | // For v18, update state sync 876 | flushSync(() => { 877 | this.setState(newState); 878 | }); 879 | } 880 | 881 | if (this.props.onResize) { 882 | if (changed) { 883 | this.props.onResize(event, direction, this.resizable, delta); 884 | } 885 | } 886 | } 887 | 888 | onMouseUp(event: MouseEvent | TouchEvent) { 889 | const { isResizing, direction, original } = this.state; 890 | if (!isResizing || !this.resizable) { 891 | return; 892 | } 893 | if (this.props.onResizeStop) { 894 | this.props.onResizeStop(event, direction, this.resizable, this.delta); 895 | } 896 | if (this.props.size) { 897 | this.setState({ width: this.props.size.width ?? 'auto', height: this.props.size.height ?? 'auto' }); 898 | } 899 | this.unbindEvents(); 900 | this.setState({ 901 | isResizing: false, 902 | backgroundStyle: { ...this.state.backgroundStyle, cursor: 'auto' }, 903 | }); 904 | } 905 | 906 | updateSize(size: Size) { 907 | this.setState({ width: size.width ?? 'auto', height: size.height ?? 'auto' }); 908 | } 909 | 910 | renderResizer() { 911 | const { enable, handleStyles, handleClasses, handleWrapperStyle, handleWrapperClass, handleComponent } = this.props; 912 | if (!enable) { 913 | return null; 914 | } 915 | const resizers = Object.keys(enable).map(dir => { 916 | if (enable[dir as Direction] !== false) { 917 | return ( 918 | 925 | {handleComponent && handleComponent[dir as Direction] ? handleComponent[dir as Direction] : null} 926 | 927 | ); 928 | } 929 | return null; 930 | }); 931 | // #93 Wrap the resize box in span (will not break 100% width/height) 932 | return ( 933 |
934 | {resizers} 935 |
936 | ); 937 | } 938 | 939 | render() { 940 | const extendsProps = Object.keys(this.props).reduce((acc, key) => { 941 | if (definedProps.indexOf(key) !== -1) { 942 | return acc; 943 | } 944 | acc[key] = this.props[key as keyof ResizableProps]; 945 | return acc; 946 | }, {} as { [key: string]: any }); 947 | 948 | const style: React.CSSProperties = { 949 | position: 'relative', 950 | userSelect: this.state.isResizing ? 'none' : 'auto', 951 | ...this.props.style, 952 | ...this.sizeStyle, 953 | maxWidth: this.props.maxWidth, 954 | maxHeight: this.props.maxHeight, 955 | minWidth: this.props.minWidth, 956 | minHeight: this.props.minHeight, 957 | boxSizing: 'border-box', 958 | flexShrink: 0, 959 | }; 960 | 961 | if (this.state.flexBasis) { 962 | style.flexBasis = this.state.flexBasis; 963 | } 964 | 965 | const Wrapper = this.props.as || 'div'; 966 | 967 | return ( 968 | { 975 | if (c) { 976 | this.resizable = c; 977 | } 978 | }} 979 | > 980 | {this.state.isResizing &&
} 981 | {this.props.children} 982 | {this.renderResizer()} 983 | 984 | ); 985 | } 986 | } 987 | -------------------------------------------------------------------------------- /src/resizer.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useCallback, useMemo } from 'react'; 2 | 3 | const rowSizeBase = { 4 | width: '100%', 5 | height: '10px', 6 | top: '0px', 7 | left: '0px', 8 | cursor: 'row-resize', 9 | } as const; 10 | 11 | const colSizeBase = { 12 | width: '10px', 13 | height: '100%', 14 | top: '0px', 15 | left: '0px', 16 | cursor: 'col-resize', 17 | } as const; 18 | 19 | const edgeBase = { 20 | width: '20px', 21 | height: '20px', 22 | position: 'absolute', 23 | zIndex: 1, 24 | } as const; 25 | 26 | const styles: { [key: string]: React.CSSProperties } = { 27 | top: { 28 | ...rowSizeBase, 29 | top: '-5px', 30 | }, 31 | right: { 32 | ...colSizeBase, 33 | left: undefined, 34 | right: '-5px', 35 | }, 36 | bottom: { 37 | ...rowSizeBase, 38 | top: undefined, 39 | bottom: '-5px', 40 | }, 41 | left: { 42 | ...colSizeBase, 43 | left: '-5px', 44 | }, 45 | topRight: { 46 | ...edgeBase, 47 | right: '-10px', 48 | top: '-10px', 49 | cursor: 'ne-resize', 50 | }, 51 | bottomRight: { 52 | ...edgeBase, 53 | right: '-10px', 54 | bottom: '-10px', 55 | cursor: 'se-resize', 56 | }, 57 | bottomLeft: { 58 | ...edgeBase, 59 | left: '-10px', 60 | bottom: '-10px', 61 | cursor: 'sw-resize', 62 | }, 63 | topLeft: { 64 | ...edgeBase, 65 | left: '-10px', 66 | top: '-10px', 67 | cursor: 'nw-resize', 68 | }, 69 | } as const; 70 | 71 | export type Direction = 'top' | 'right' | 'bottom' | 'left' | 'topRight' | 'bottomRight' | 'bottomLeft' | 'topLeft'; 72 | 73 | export type OnStartCallback = ( 74 | e: React.MouseEvent | React.TouchEvent, 75 | dir: Direction, 76 | ) => void; 77 | 78 | export interface Props { 79 | direction: Direction; 80 | className?: string; 81 | replaceStyles?: React.CSSProperties; 82 | onResizeStart: OnStartCallback; 83 | children: React.ReactNode; 84 | } 85 | 86 | export const Resizer = memo((props: Props) => { 87 | const { onResizeStart, direction, children, replaceStyles, className } = props; 88 | const onMouseDown = useCallback( 89 | (e: React.MouseEvent) => { 90 | onResizeStart(e, direction); 91 | }, 92 | [onResizeStart, direction], 93 | ); 94 | 95 | const onTouchStart = useCallback( 96 | (e: React.TouchEvent) => { 97 | onResizeStart(e, direction); 98 | }, 99 | [onResizeStart, direction], 100 | ); 101 | 102 | const style: React.CSSProperties = useMemo(() => { 103 | return { 104 | position: 'absolute', 105 | userSelect: 'none', 106 | ...styles[direction], 107 | ...(replaceStyles ?? {}), 108 | }; 109 | }, [replaceStyles, direction]); 110 | 111 | return ( 112 |
113 | {children} 114 |
115 | ); 116 | }); 117 | -------------------------------------------------------------------------------- /stories/aspect.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('aspect', module).add('default', () => ( 7 | 15 | 001 16 | 17 | )); 18 | -------------------------------------------------------------------------------- /stories/auto.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('auto', module) 7 | .add('default', () => ( 8 | console.log(e)}> 9 | 001 10 | 11 | )) 12 | .add('height', () => ( 13 | 20 | 001 21 | 22 | )) 23 | .add('width', () => ( 24 | 31 | 001 32 | 33 | )); 34 | -------------------------------------------------------------------------------- /stories/basic.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('basic', module).add('default', () => ( 7 | 14 | 001 15 | 16 | )); 17 | -------------------------------------------------------------------------------- /stories/bounds.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('bounds', module) 7 | .add('parent', () => ( 8 | 16 | 001 17 | 18 | )) 19 | .add('window', () => ( 20 | 28 | 001 29 | 30 | )); 31 | -------------------------------------------------------------------------------- /stories/extra.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | const wrapper = { 6 | flex: 1, 7 | display: 'flex', 8 | }; 9 | 10 | const style = { 11 | display: 'flex', 12 | flexDirection: 'column' as 'column', 13 | background: '#f0f0f0', 14 | border: 0, 15 | padding: 0, 16 | }; 17 | 18 | const content = { 19 | flex: 1, 20 | display: 'flex', 21 | alignItems: 'center', 22 | justifyContent: 'center', 23 | }; 24 | 25 | const header = { 26 | background: '#999999', 27 | color: 'white', 28 | height: '50px', 29 | display: 'flex', 30 | alignItems: 'center', 31 | justifyContent: 'center', 32 | }; 33 | 34 | const sidebar = { 35 | background: '#999999', 36 | color: 'white', 37 | width: '50px', 38 | display: 'flex', 39 | alignItems: 'center', 40 | justifyContent: 'center', 41 | }; 42 | 43 | const aspectRatio = 16 / 9; 44 | 45 | storiesOf('extra', module) 46 | .add('header', () => ( 47 | 56 |
Header
57 |
001
58 |
59 | )) 60 | .add('sidebar', () => ( 61 | 71 |
Header
72 |
73 |
Nav
74 |
001
75 |
76 |
77 | )); 78 | -------------------------------------------------------------------------------- /stories/flex.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('flex', module) 7 | .add('default', () => ( 8 |
9 | 001 10 | 002 11 |
12 | )) 13 | .add('flex row', () => ( 14 |
15 | 001 16 | 002 17 |
18 | )) 19 | .add('flex column', () => ( 20 |
21 | 001 22 | 002 23 |
24 | )); 25 | -------------------------------------------------------------------------------- /stories/grid.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | const cell: React.CSSProperties = { 7 | display: 'flex', 8 | alignItems: 'center', 9 | justifyContent: 'center', 10 | width: '100px', 11 | height: '50px', 12 | backgroundColor: '#f8f8f8', 13 | border: '1px solid #f0f0f0', 14 | boxSizing: 'border-box', 15 | }; 16 | 17 | const container: React.CSSProperties = { 18 | display: 'flex', 19 | gap: '3px', 20 | }; 21 | 22 | const verticalContainer: React.CSSProperties = { 23 | display: 'flex', 24 | flexDirection: 'column', 25 | gap: '3px', 26 | }; 27 | 28 | storiesOf('grid', module) 29 | .add('default', () => ( 30 | { 35 | console.log(a); 36 | }} 37 | > 38 | 001 39 | 40 | )) 41 | .add('grid gap', () => ( 42 |
43 |
44 |
45 | {Array.from({ length: 3 }, (_, idx) => ( 46 |
h: 50px
47 | ))} 48 |
49 |
50 |
51 | {Array.from({ length: 3 }, (_, idx) => ( 52 |
53 | w: 100px 54 |
55 | ))} 56 |
57 | { 75 | console.log(a); 76 | }} 77 | > 78 | 001 79 | 80 |
81 |
82 | )); 83 | -------------------------------------------------------------------------------- /stories/handle.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | const SouthEastArrow = () => ( 7 | 8 | 9 | 10 | ); 11 | 12 | const CustomHandle = props => ( 13 |
25 | ); 26 | const BottomRightHandle = () => ( 27 | 28 | 29 | 30 | ); 31 | 32 | storiesOf('handle', module).add('bottomRight', () => ( 33 | , 41 | }} 42 | > 43 | bottomRight 44 | 45 | )); 46 | -------------------------------------------------------------------------------- /stories/max.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('max', module) 7 | .add('height', () => ( 8 | 16 | 001 17 | 18 | )) 19 | .add('width', () => ( 20 | 28 | 001 29 | 30 | )) 31 | .add('percentage', () => ( 32 | 41 | 001 42 | 43 | )); 44 | -------------------------------------------------------------------------------- /stories/min.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('min', module) 7 | .add('height', () => ( 8 | 16 | 001 17 | 18 | )) 19 | .add('width', () => ( 20 | 28 | 001 29 | 30 | )) 31 | .add('percentage', () => ( 32 | 41 | 001 42 | 43 | )); 44 | -------------------------------------------------------------------------------- /stories/multiple.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('multiple', module) 7 | .add('horizontal', () => ( 8 |
15 | 24 | 001 25 | 26 |
002
27 |
28 | )) 29 | .add('vertical', () => ( 30 |
39 | 48 | 001 49 | 50 |
002
51 |
52 | )); 53 | -------------------------------------------------------------------------------- /stories/nested.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | const style = { 6 | display: 'flex', 7 | alignItems: 'center', 8 | justifyContent: 'center', 9 | border: 'solid 1px #ddd', 10 | background: '#f0f0f0', 11 | padding: '10px', 12 | }; 13 | 14 | storiesOf('nested', module).add('default', () => ( 15 |
16 | 17 | 18 | Nested 19 | 20 | 21 |
22 | )); 23 | -------------------------------------------------------------------------------- /stories/ratio.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('ratio', module).add('double', () => ( 7 | 15 | 001 16 | 17 | )); 18 | -------------------------------------------------------------------------------- /stories/scaled.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('scaled', module).add('half', () => ( 7 |
8 | 16 | transform: scale(0.5) 17 | 18 |
19 | )); 20 | -------------------------------------------------------------------------------- /stories/size.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('size', module).add('percentage', () => ); 7 | 8 | export default class Size extends React.Component { 9 | state = { 10 | width: '30%', 11 | height: '20%', 12 | }; 13 | constructor(props: any) { 14 | super(props); 15 | } 16 | 17 | render() { 18 | return ( 19 | { 23 | this.setState({ 24 | width: ref.style.width, 25 | height: ref.style.height, 26 | }); 27 | }} 28 | > 29 | 001 30 | 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /stories/snap.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('snapping', module) 7 | .add('absolute', () => ( 8 | { 14 | console.log(a); 15 | }} 16 | > 17 | 001 18 | 19 | )) 20 | .add('grid', () => ( 21 | { 27 | console.log(a); 28 | }} 29 | > 30 | 001 31 | 32 | )); 33 | -------------------------------------------------------------------------------- /stories/style.ts: -------------------------------------------------------------------------------- 1 | export const style = { 2 | display: 'flex', 3 | alignItems: 'center', 4 | justifyContent: 'center', 5 | border: 'solid 1px #ddd', 6 | background: '#f0f0f0', 7 | }; 8 | -------------------------------------------------------------------------------- /stories/styles.css: -------------------------------------------------------------------------------- 1 | html, body, #root, #root > div { 2 | height: 100%; 3 | margin: 0; 4 | padding: 10px; 5 | box-sizing: border-box; 6 | } -------------------------------------------------------------------------------- /stories/vwvh.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import * as React from 'react'; 4 | import { Resizable } from '../src'; 5 | import { storiesOf } from '@storybook/react'; 6 | import { style } from './style'; 7 | 8 | storiesOf('vw vh', module) 9 | .add('vw', () => ( 10 | 11 | 001 12 | 13 | )) 14 | .add('vh', () => ( 15 | 16 | 001 17 | 18 | )); 19 | -------------------------------------------------------------------------------- /stories/wrapper.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Resizable } from '../src'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { style } from './style'; 5 | 6 | storiesOf('wrapper', module).add('default', () => ( 7 | 16 | This is a "header" element 17 | 18 | )); 19 | -------------------------------------------------------------------------------- /test/fixture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "es2015", 5 | "lib": ["es5", "es2015", "dom"], 6 | "jsx": "react-jsx", 7 | "declaration": true, 8 | "skipLibCheck": true, 9 | "outDir": "./lib", 10 | "strict": true, 11 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 12 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 13 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 14 | }, 15 | "exclude": ["stories", "lib", "src/index.spec.tsx"], 16 | "include": [ 17 | "src" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "lib": ["es5", "es2015", "dom"], 5 | "jsx": "react-jsx", 6 | "declaration": true, 7 | "outDir": "./lib", 8 | "strict": true, 9 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 10 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 12 | }, 13 | "exclude": ["stories", "lib"], 14 | "include": [ 15 | "src" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["tslint-plugin-prettier"], 3 | "extends": [ 4 | "tslint:latest", 5 | "tslint-config-google", 6 | "tslint-config-prettier" 7 | ], 8 | "linterOptions": { 9 | "exclude": [ 10 | "config/**/*.js", 11 | "node_modules/**/*.ts", 12 | "coverage/lcov-report/*.js" 13 | ] 14 | }, 15 | "rules": { 16 | "prettier": [ 17 | true, 18 | { 19 | "semi": true, 20 | "singleQuote": true, 21 | "printWidth": 120, 22 | "trailingComma": "all" 23 | } 24 | ], 25 | "no-console": false, 26 | "no-bitwise": false, 27 | "variable-name": [ 28 | true, 29 | "ban-keywords", 30 | "check-format", 31 | "allow-pascal-case", 32 | "allow-leading-underscore" 33 | ], 34 | "member-access": false, 35 | "import-name": false, 36 | "ordered-imports": false, 37 | "interface-name": false, 38 | "no-empty-interface": false, 39 | "object-literal-sort-keys": false, 40 | "object-literal-shorthand": false, 41 | "no-empty": [true, "allow-empty-functions"], 42 | "no-implicit-dependencies": [true, "dev"], 43 | "no-submodule-imports": false, 44 | "no-unused-expression": [true, "allow-fast-null-checks"], 45 | "no-object-literal-type-assertion": false, 46 | "member-ordering": false 47 | }, 48 | "jsRules": {} 49 | } 50 | --------------------------------------------------------------------------------