├── .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 |
6 |
7 |
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 | 
33 |
34 | ## Live Demo
35 |
36 | ### Storybook
37 |
38 | [Storybook](http://bokuweb.github.io/re-resizable/)
39 |
40 | ### CodeSandbox
41 |
42 | [](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 |
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 |
--------------------------------------------------------------------------------