Skip to content

Fixes #651 Add percentage support#980

Open
dwrth wants to merge 3 commits intobokuweb:masterfrom
dwrth:master
Open

Fixes #651 Add percentage support#980
dwrth wants to merge 3 commits intobokuweb:masterfrom
dwrth:master

Conversation

@dwrth
Copy link

@dwrth dwrth commented Feb 18, 2026

Proposed solution

Fixes #651 by adding support for percentages.

Tradeoffs

No known ones

Testing Done

Works nicely, also added a new storybook section for this

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds percentage-based positioning support to the react-rnd component to address issue #651, which requested the ability to position elements using percentages instead of pixels for responsive layouts. However, the implementation significantly extends beyond percentage support to include a comprehensive grid-based positioning system.

Changes:

  • Added positionUnit prop supporting "px", "%", and "grid" units for position values
  • Added complete grid system with sizeUnit, gridConfig, and layoutMode props
  • Implemented conversion functions between pixels, percentages, and grid coordinates
  • Updated callbacks to report positions in the specified units with optional grid placement data

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
src/index.tsx Core implementation: added type definitions, conversion functions (percent/px and grid/px), lifecycle changes to track parent size, updated callbacks to return converted positions, and render logic to handle multiple unit systems
src/index.js.flow Partial Flow type updates for percentage support (missing grid-related types)
package.json Version bump from 10.5.2 to 10.5.3
README.md Comprehensive documentation for percentage and grid positioning features, including API reference, examples, and changelog entry
stories/position/position-percent-uncontrolled.tsx New Storybook example demonstrating uncontrolled percentage positioning
stories/position/position-percent-controlled.tsx New Storybook example demonstrating controlled percentage positioning
stories/grid/percent-with-visual-grid.tsx New Storybook example combining percentage positioning with visual grid snapping
stories/grid/grid-units-controlled.tsx New Storybook example demonstrating grid unit positioning and sizing
stories/index.tsx Registered new Storybook examples for position and grid categories

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 109 to 120
@@ -114,6 +116,7 @@ export type Props = {
x: number,
y: number,
},
positionUnit?: PositionUnit,
size?: Size,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Flow type definitions are missing several critical types and signatures required for the grid feature. The Props type is missing sizeUnit, gridConfig, and layoutMode properties. Additionally, position and size don't support the grid types (GridPosition and GridSize). The default property also doesn't support RndDefaultGrid. These omissions will prevent Flow users from using the grid system.

Copilot uses AI. Check for mistakes.
{
"name": "react-rnd",
"version": "10.5.2",
"version": "10.5.3",
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version bump from 10.5.2 to 10.5.3 is only a patch version increment, but this PR adds significant new features (percentage positioning with positionUnit="%" and an entire grid system with positionUnit="grid", sizeUnit="grid", gridConfig, and layoutMode). According to semantic versioning, new features should result in a minor version bump (10.6.0), while patch versions are typically reserved for bug fixes. Consider updating the version to 10.6.0.

Suggested change
"version": "10.5.3",
"version": "10.6.0",

Copilot uses AI. Check for mistakes.

#### v10.5.3

- Add grid system: `positionUnit="grid"`, `sizeUnit="grid"`, `gridConfig`, and `layoutMode="grid"`. Callbacks report `gridPlacement`; `updatePosition`/`updateSize` accept grid types.
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changelog entry in the README describes the feature as "Add grid system" but doesn't mention the percentage positioning feature at all. Since issue #651 specifically requested percentage positioning and that's mentioned in the PR title, the changelog should explicitly mention both features: percentage positioning (positionUnit="%") and the grid system (positionUnit="grid", etc.).

Suggested change
- Add grid system: `positionUnit="grid"`, `sizeUnit="grid"`, `gridConfig`, and `layoutMode="grid"`. Callbacks report `gridPlacement`; `updatePosition`/`updateSize` accept grid types.
- Add percentage positioning and grid system: `positionUnit="%"` and `positionUnit="grid"`, `sizeUnit="grid"`, `gridConfig`, and `layoutMode="grid"`. Callbacks report `gridPlacement`; `updatePosition`/`updateSize` accept grid types.

Copilot uses AI. Check for mistakes.
Comment on lines +251 to +253
return {
columnWidth: parentSize.width / gridConfig.columns,
rowHeight: gridConfig.rowHeight,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getGridCellDimensions function can produce a columnWidth of 0 or Infinity if parentSize.width is 0 or gridConfig.columns is 0. This will cause issues in subsequent calculations. Consider adding validation to ensure parentSize.width > 0 and gridConfig.columns > 0, or handle these edge cases explicitly.

Suggested change
return {
columnWidth: parentSize.width / gridConfig.columns,
rowHeight: gridConfig.rowHeight,
const { width } = parentSize;
const { columns, rowHeight } = gridConfig;
const columnWidth = width <= 0 || columns <= 0 ? 0 : width / columns;
return {
columnWidth,
rowHeight,

Copilot uses AI. Check for mistakes.
const { left, top } = this.offsetFromParent;
this.draggable.setState({ x: px.x - left, y: px.y - top });
} catch {
this.draggable.setState(position as Position);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the catch block at line 814, when positionUnit is "%" and getParentSize() fails, the code falls back to this.draggable.setState(position as Position). However, position contains percentage values (0-100), not pixel values. Setting these directly to the draggable will treat them as pixels, resulting in incorrect positioning. The catch block should handle this error differently, perhaps by doing nothing or logging an error.

Suggested change
this.draggable.setState(position as Position);
// fallback no-op if refs not ready or parent size unavailable

Copilot uses AI. Check for mistakes.
Comment on lines +224 to +310
function positionPercentToPx(
percent: Position,
parentSize: { width: number; height: number },
): Position {
return {
x: (percent.x / 100) * parentSize.width,
y: (percent.y / 100) * parentSize.height,
};
}

function positionPxToPercent(
px: Position,
parentSize: { width: number; height: number },
): Position {
const { width, height } = parentSize;
return {
x: width <= 0 ? 0 : (px.x / width) * 100,
y: height <= 0 ? 0 : (px.y / height) * 100,
};
}

export type GridCellDimensions = { columnWidth: number; rowHeight: number };

export function getGridCellDimensions(
parentSize: { width: number; height: number },
gridConfig: GridConfig,
): GridCellDimensions {
return {
columnWidth: parentSize.width / gridConfig.columns,
rowHeight: gridConfig.rowHeight,
};
}

export function gridPositionToPx(
gridPos: GridPosition,
cell: GridCellDimensions,
): Position {
return {
x: gridPos.columnStart * cell.columnWidth,
y: gridPos.rowStart * cell.rowHeight,
};
}

export function gridSizeToPx(
gridSize: GridSize,
cell: GridCellDimensions,
): { width: number; height: number } {
return {
width: gridSize.columnSpan * cell.columnWidth,
height: gridSize.rowSpan * cell.rowHeight,
};
}

export function pxToGridPosition(
px: Position,
cell: GridCellDimensions,
): GridPosition {
return {
columnStart: Math.round(px.x / cell.columnWidth),
rowStart: Math.round(px.y / cell.rowHeight),
};
}

export function pxToGridSize(
size: { width: number; height: number },
cell: GridCellDimensions,
): GridSize {
return {
columnSpan: Math.max(1, Math.round(size.width / cell.columnWidth)),
rowSpan: Math.max(1, Math.round(size.height / cell.rowHeight)),
};
}

export function pxToGridPlacement(
position: Position,
size: { width: number; height: number },
cell: GridCellDimensions,
): GridPlacement {
const start = pxToGridPosition(position, cell);
const span = pxToGridSize(size, cell);
return {
columnStart: start.columnStart,
rowStart: start.rowStart,
columnEnd: start.columnStart + span.columnSpan,
rowEnd: start.rowStart + span.rowSpan,
};
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new percentage and grid positioning features lack test coverage. The test file src/index.test.tsx contains no tests for positionUnit, sizeUnit, gridConfig, or any of the conversion functions like positionPercentToPx, positionPxToPercent, gridPositionToPx, etc. Given the complexity of these features and the potential edge cases (division by zero, negative positions, percentage/grid conversions), comprehensive unit tests should be added.

Copilot uses AI. Check for mistakes.
topLeft?: React.ReactElement<any>;
}

export type PositionUnit = 'px' | '%';
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Flow type definition for PositionUnit is incomplete. It only includes 'px' | '%' but the TypeScript implementation also supports 'grid'. This inconsistency will cause issues for Flow users who want to use the grid feature.

Suggested change
export type PositionUnit = 'px' | '%';
export type PositionUnit = 'px' | '%' | 'grid';

Copilot uses AI. Check for mistakes.
Comment on lines +282 to +283
columnStart: Math.round(px.x / cell.columnWidth),
rowStart: Math.round(px.y / cell.rowHeight),
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pxToGridPosition function uses Math.round for both x and y coordinates. This can result in negative grid positions if px.x or px.y are negative (e.g., when dragging outside the parent container). Consider using Math.max(0, Math.round(...)) to ensure grid positions are never negative.

Suggested change
columnStart: Math.round(px.x / cell.columnWidth),
rowStart: Math.round(px.y / cell.rowHeight),
columnStart: Math.max(0, Math.round(px.x / cell.columnWidth)),
rowStart: Math.max(0, Math.round(px.y / cell.rowHeight)),

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +167
/** When 'grid', position is GridPosition and gridConfig is required; callbacks receive grid placement. */
positionUnit?: PositionUnit;
/** When 'grid', size is GridSize and gridConfig is required; callbacks receive grid placement. Default 'px'. */
sizeUnit?: SizeUnit;
/** When 'grid', the wrapper uses grid-column/grid-row instead of position/left/top (parent must be display:grid). */
layoutMode?: "absolute" | "grid";
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title and description claim this "Fixes #651 Add percentage support", but the implementation also adds a complete grid system with positionUnit="grid", sizeUnit="grid", gridConfig, and layoutMode="grid". While this is a valuable addition, it significantly expands the scope beyond percentage support. The PR description doesn't mention the grid feature at all, which could cause confusion during review and future maintenance.

Copilot uses AI. Check for mistakes.
Comment on lines +969 to +980
// Default size for Resizable: when default is grid shape, convert to px
let defaultSizeForResizable: { x?: number; y?: number; width: number; height: number } | undefined = defaultValue as any;
if (defaultValue && "columnSpan" in defaultValue && gridCell) {
const px = gridSizeToPx(
{ columnSpan: defaultValue.columnSpan, rowSpan: defaultValue.rowSpan },
gridCell,
);
defaultSizeForResizable = {
width: px.width,
height: px.height,
};
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the render method, when defaultValue is a grid type (columnSpan in line 971), the code converts only the size to px but doesn't include the position in defaultSizeForResizable (lines 976-979). If the default includes grid position properties like columnStart and rowStart, they won't be passed to the Draggable component's defaultPosition, which could cause issues. The logic at lines 983-990 handles position separately, but the type annotation on line 970 suggests defaultSizeForResizable could have x and y properties.

Copilot uses AI. Check for mistakes.
@dwrth
Copy link
Author

dwrth commented Feb 23, 2026

@bokuweb I totally forgot I added a PR for this and just started pushing more changes that are fully out of scope :D I'm happy to close this and open another PR with a branch that is checked out at just the percentage implementation. Let me know what you think!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Positions in percentages

2 participants