Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions __tests__/aspectConstraints.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import {render} from '@testing-library/react';
import Resizable from '../lib/Resizable';

// Bounds can come from the available container space rather than the image ratio.
describe('locked aspect ratio with independent size constraints', () => {
test.each([
['width maximum', [100, 50], [420, 400], [300, 150], [420, 210]],
['height maximum', [100, 50], [600, 210], [300, 150], [420, 210]],
['height minimum', [100, 130], [900, 450], [-200, -100], [260, 130]],
['width minimum', [260, 50], [900, 450], [-200, -100], [260, 130]],
])('%s preserves the ratio through overshoot, return and the next drag', (_label, min, max, delta, bound) => {
const ref = React.createRef<Resizable>();
let size = {width: 300, height: 150};
const observe = (_event, data) => { size = data.size; };
const element = () => <Resizable ref={ref} {...size} lockAspectRatio
minConstraints={min} maxConstraints={max}
onResizeStart={observe} onResize={observe} onResizeStop={observe}><div /></Resizable>;
const {rerender} = render(element());
const node = document.createElement('span');
const send = (name, x, y) => {
ref.current.resizeHandler(name, 'se')({} as React.SyntheticEvent, {node, deltaX: x, deltaY: y});
rerender(element());
expect(size.width).toBeCloseTo(2 * size.height);
expect(size.width).toBeGreaterThanOrEqual(min[0]);
expect(size.height).toBeGreaterThanOrEqual(min[1]);
expect(size.width).toBeLessThanOrEqual(max[0]);
expect(size.height).toBeLessThanOrEqual(max[1]);
};
send('onResizeStart', 0, 0);
send('onResize', ...delta);
expect(size).toEqual({width: bound[0], height: bound[1]});
send('onResize', ...delta);
send('onResize', 60 - 2 * delta[0], 30 - 2 * delta[1]);
expect(size).toEqual({width: 360, height: 180});
send('onResizeStop', 0, 0);
send('onResizeStart', 0, 0);
send('onResize', 20, 10);
send('onResizeStop', 0, 0);
expect(size).toEqual({width: 380, height: 190});
});

test('uses both bounds for a non-integer ratio', () => {
const ref = React.createRef<Resizable>();
render(<Resizable ref={ref} width={150} height={100} lockAspectRatio
minConstraints={[120, 90]} maxConstraints={[450, 200]}><div /></Resizable>);
expect(ref.current.runConstraints(600, 400)).toEqual([300, 200]);
ref.current.resetData();
expect(ref.current.runConstraints(30, 20)).toEqual([135, 90]);
});

test('keeps size bounds authoritative if they cannot satisfy the ratio', () => {
const ref = React.createRef<Resizable>();
render(<Resizable ref={ref} width={300} height={150} lockAspectRatio
minConstraints={[100, 200]} maxConstraints={[300, 250]}><div /></Resizable>);
expect(ref.current.runConstraints(600, 300)).toEqual([300, 250]);
});
});
27 changes: 18 additions & 9 deletions lib/Resizable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,12 @@ export default class Resizable extends React.Component<Props, {}> {
// Clamp width and height within provided constraints
runConstraints(width: number, height: number): [number, number] {
const {minConstraints, maxConstraints, lockAspectRatio} = this.props;
const ratio = this.props.width / this.props.height;
// short circuit
if (!minConstraints && !maxConstraints && !lockAspectRatio) return [width, height];

// If constraining to min and max, we need to also fit width and height to aspect ratio.
if (lockAspectRatio) {
const ratio = this.props.width / this.props.height;

// Project (width, height) onto the line w = ratio * h.
// Distributes tracking error across both axes instead of forcing one to overshoot.
// t = (w * ratio + h) / (ratio^2 + 1), new_w = t * ratio, new_h = t
Expand All @@ -58,13 +57,23 @@ export default class Resizable extends React.Component<Props, {}> {
width += slackW;
height += slackH;

if (minConstraints) {
width = Math.max(minConstraints[0], width);
height = Math.max(minConstraints[1], height);
}
if (maxConstraints) {
width = Math.min(maxConstraints[0], width);
height = Math.min(maxConstraints[1], height);
// Intersect both dimensions' bounds on the aspect-ratio line. Clamping
// each dimension independently would change the ratio at either bound.
const minWidth = minConstraints ? Math.max(minConstraints[0], minConstraints[1] * ratio) : -Infinity;
const maxWidth = maxConstraints ? Math.min(maxConstraints[0], maxConstraints[1] * ratio) : Infinity;
if (lockAspectRatio && ratio > 0 && Number.isFinite(ratio) && minWidth <= maxWidth) {
width = Math.max(minWidth, Math.min(maxWidth, width));
height = width / ratio;
} else {
// Preserve size-constraint precedence when no size can satisfy the ratio.
if (minConstraints) {
width = Math.max(minConstraints[0], width);
height = Math.max(minConstraints[1], height);
}
if (maxConstraints) {
width = Math.min(maxConstraints[0], width);
height = Math.min(maxConstraints[1], height);
}
}

// If the width or height changed, we must have introduced some slack. Record it for the next iteration.
Expand Down