Conversation
There was a problem hiding this comment.
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
positionUnitprop supporting "px", "%", and "grid" units for position values - Added complete grid system with
sizeUnit,gridConfig, andlayoutModeprops - 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.
| @@ -114,6 +116,7 @@ export type Props = { | |||
| x: number, | |||
| y: number, | |||
| }, | |||
| positionUnit?: PositionUnit, | |||
| size?: Size, | |||
There was a problem hiding this comment.
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.
| { | ||
| "name": "react-rnd", | ||
| "version": "10.5.2", | ||
| "version": "10.5.3", |
There was a problem hiding this comment.
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.
| "version": "10.5.3", | |
| "version": "10.6.0", |
|
|
||
| #### v10.5.3 | ||
|
|
||
| - Add grid system: `positionUnit="grid"`, `sizeUnit="grid"`, `gridConfig`, and `layoutMode="grid"`. Callbacks report `gridPlacement`; `updatePosition`/`updateSize` accept grid types. |
There was a problem hiding this comment.
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.).
| - 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. |
| return { | ||
| columnWidth: parentSize.width / gridConfig.columns, | ||
| rowHeight: gridConfig.rowHeight, |
There was a problem hiding this comment.
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.
| 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, |
| const { left, top } = this.offsetFromParent; | ||
| this.draggable.setState({ x: px.x - left, y: px.y - top }); | ||
| } catch { | ||
| this.draggable.setState(position as Position); |
There was a problem hiding this comment.
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.
| this.draggable.setState(position as Position); | |
| // fallback no-op if refs not ready or parent size unavailable |
| 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, | ||
| }; | ||
| } |
There was a problem hiding this comment.
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.
| topLeft?: React.ReactElement<any>; | ||
| } | ||
|
|
||
| export type PositionUnit = 'px' | '%'; |
There was a problem hiding this comment.
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.
| export type PositionUnit = 'px' | '%'; | |
| export type PositionUnit = 'px' | '%' | 'grid'; |
| columnStart: Math.round(px.x / cell.columnWidth), | ||
| rowStart: Math.round(px.y / cell.rowHeight), |
There was a problem hiding this comment.
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.
| 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)), |
| /** 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"; |
There was a problem hiding this comment.
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.
| // 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, | ||
| }; | ||
| } |
There was a problem hiding this comment.
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.
|
@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! |
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