Popover: assess all `position` to `placement` conversions, add unit tests
With the refactor of Popover to using floating-ui internally (#40740), a new placement prop was introduced with the objective of replacing the legacy position prop.
Currently, there is a function converting position to placement, but:
- its logic is complicated and difficult to follow
- its logic could potentially miss some edge cases:
- it looks like
positionaccepted the undocumented format[yAxis] [xAxis] [corner]) - it looks like the new
placementprop doesn't allow thePopoverto be placed centered on top of its anchor
- it looks like
- its logic is not covered by unit tests (which makes the previous checks necessary)
Therefore, to improve the situation and get ready to deprecate the position prop, we should:
- Assess that all possible values of
positionare converted correctly (or in a best-effort way) to the correspondingplacement - Write unit tests
- Potentially make changes to the conversion logic to respect the tests added from the previous point
I went ahead and checked out the repository before #40740 was merged. I was therefore able to render a Popover for all possible values of the position prop (even the ones that don't really make much sense), simulating a Popover wider and taller than its anchor.
I then repeated the same thing on trunk, taking a screenshot of all possible placement values.
Here's the comparison:
All position values
| Popover wider than anchor | Anchor wider than Popover |
|---|---|
![]() |
![]() |
All placement values
| Popover wider than anchor | Anchor wider than Popover |
|---|---|
![]() |
![]() |
Based on the images above, this should be the expected conversion:
(disclaimer: for completeness, I listed all possible combinations of values for the position prop, even if some of them don't really make sense)
| Position | Placement | Notes |
|---|---|---|
middle |
top |
There is no equivalent placement in floating-ui, use top as fallback |
bottom |
bottom |
- |
top |
top |
- |
middle left |
left |
- |
middle center |
top |
There is no equivalent placement in floating-ui, use top as fallback |
middle right |
right |
- |
bottom left |
bottom-end |
Not a perfect match, but likely the closest |
bottom center |
bottom |
- |
bottom right |
bottom-start |
Not a perfect match, but likely the closest |
top left |
top-end |
Not a perfect match, but likely the closest |
top center |
top |
- |
top right |
top-start |
Not a perfect match, but likely the closest |
middle left left |
left |
- |
middle left right |
left |
Not a perfect match, but likely the closest (not sure if this position even makes sense) |
middle left bottom |
left-end |
Not a perfect match, but likely the closest (this one is really up to interpretation) |
middle left top |
left-start |
Not a perfect match, but likely the closest (this one is really up to interpretation) |
middle center left |
top |
There is no equivalent placement in floating-ui, use top as fallback (not sure if this position even makes sense) |
middle center right |
top |
There is no equivalent placement in floating-ui, use top as fallback (not sure if this position even makes sense) |
middle center bottom |
top |
There is no equivalent placement in floating-ui, use top as fallback (not sure if this position even makes sense) |
middle center top |
top |
There is no equivalent placement in floating-ui, use top as fallback (not sure if this position even makes sense) |
middle right left |
right |
Not a perfect match, but likely the closest (not sure if this position even makes sense) |
middle right right |
right |
- |
middle right bottom |
right-end |
Not a perfect match, but likely the closest |
middle right top |
right-start |
Not a perfect match, but likely the closest |
bottom left left |
bottom-end |
Not a perfect match, but likely the closest |
bottom left right |
bottom-end |
- |
bottom left bottom |
bottom-end |
Not a perfect match, but likely the closest (not sure if this position even makes sense) |
bottom left top |
bottom-end |
Not a perfect match, but likely the closest (not sure if this position even makes sense) |
bottom center left |
bottom |
not sure if this position even makes sense |
bottom center right |
bottom |
not sure if this position even makes sense |
bottom center bottom |
bottom |
not sure if this position even makes sense |
bottom center top |
bottom |
not sure if this position even makes sense |
bottom right left |
bottom-start |
- |
bottom right right |
bottom-start |
Not a perfect match, but likely the closest |
bottom right bottom |
bottom-start |
Not a perfect match, but likely the closest (not sure if this position even makes sense) |
bottom right top |
bottom-start |
Not a perfect match, but likely the closest (not sure if this position even makes sense) |
top left left |
top-end |
Not a perfect match, but likely the closest |
top left right |
top-end |
- |
top left bottom |
top-end |
Not a perfect match, but likely the closest (not sure if this position even makes sense) |
top left top |
top-end |
Not a perfect match, but likely the closest (not sure if this position even makes sense) |
top center left |
top |
not sure if this position even makes sense |
top center right |
top |
not sure if this position even makes sense |
top center bottom |
top |
not sure if this position even makes sense |
top center top |
top |
not sure if this position even makes sense |
top right left |
top-start |
- |
top right right |
top-start |
Not a perfect match, but likely the closest |
top right bottom |
top-start |
Not a perfect match, but likely the closest (not sure if this position even makes sense) |
top right top |
top-start |
Not a perfect match, but likely the closest (not sure if this position even makes sense) |
In #44377 I introduced a comprehensive set of unit tests to assess the current status of the returned placement values for every possible position.
*: bottom placements marked with an asterisks are fallback values used when position has middle center values, since there's not equivalent placement value
position |
placement expected by test specs |
placement received (as on trunk) |
matches expected value? |
|---|---|---|---|
'middle' |
'bottom'* |
undefined |
❌ |
'bottom' |
'bottom' |
'bottom' |
✅ |
'top' |
'top' |
'top' |
✅ |
'middle left' |
'left' |
'left' |
✅ |
'middle center' |
'bottom'* |
'center' |
❌ |
'middle right' |
'right' |
'right' |
✅ |
'bottom left' |
'bottom-end' |
'bottom-end' |
✅ |
'bottom center' |
'bottom' |
'bottom' |
✅ |
'bottom right' |
'bottom-start' |
'bottom-start' |
✅ |
'top left' |
'top-end' |
'top-end' |
✅ |
'top center' |
'top' |
'top' |
✅ |
'top right' |
'top-start' |
'top-start' |
✅ |
'middle left left' |
'left' |
'left' |
✅ |
'middle left right' |
'left' |
'left' |
✅ |
'middle left bottom' |
'left-end' |
'left' |
❌ |
'middle left top' |
'left-start' |
'left' |
❌ |
'middle center left' |
'bottom'* |
'center' |
❌ |
'middle center right' |
'bottom'* |
'center' |
❌ |
'middle center bottom' |
'bottom'* |
'center' |
❌ |
'middle center top' |
'bottom'* |
'center' |
❌ |
'middle right left' |
'right' |
'right' |
✅ |
'middle right right' |
'right' |
'right' |
✅ |
'middle right bottom' |
'right-end' |
'right' |
❌ |
'middle right top' |
'right-start' |
'right' |
❌ |
'bottom left left' |
'bottom-end' |
'bottom-start' |
❌ |
'bottom left right' |
'bottom-end' |
'bottom-end' |
✅ |
'bottom left bottom' |
'bottom-end' |
'bottom-end' |
✅ |
'bottom left top' |
'bottom-end' |
'bottom-end' |
✅ |
'bottom center left' |
'bottom' |
'bottom-start' |
❌ |
'bottom center right' |
'bottom' |
'bottom-end' |
❌ |
'bottom center bottom' |
'bottom' |
'bottom' |
✅ |
'bottom center top' |
'bottom' |
'bottom' |
✅ |
'bottom right left' |
'bottom-start' |
'bottom-start' |
✅ |
'bottom right right' |
'bottom-start' |
'bottom-start' |
✅ |
'bottom right bottom' |
'bottom-start' |
'bottom-start' |
✅ |
'bottom right top' |
'bottom-start' |
'bottom-start' |
✅ |
'top left left' |
'top-end' |
'top-start' |
❌ |
'top left right' |
'top-end' |
'top-end' |
✅ |
'top left bottom' |
'top-end' |
'top-end' |
✅ |
'top left top' |
'top-end' |
'top-end' |
✅ |
'top center left' |
'top' |
'top-start' |
❌ |
'top center right' |
'top' |
'top-end' |
❌ |
'top center bottom' |
'top' |
'top' |
✅ |
'top center top' |
'top' |
'top' |
✅ |
'top right left' |
'top-start' |
'top-start' |
✅ |
'top right right' |
'top-start' |
'top-start' |
✅ |
'top right bottom' |
'top-start' |
'top-start' |
✅ |
'top right top' |
'top-start' |
'top-start' |
✅ |
The inconsistencies can be split into four main groups:
-
'middle'and'middle center'positions: these values are not handles correctly by thepositionToPlacementfunction, since the convertedplacementis eitherundefinedorcenter(which is not even a validplacement). Sincefloating-uidoesn't have a correspondingplacementfor themiddle centerpositions, my suggestion is to use a fallback value ofbottom(which is alsofloating-ui's default value). -
'middle [left|right] [bottom|top]'positions: for these positions,positionToPlacementcurrently returns vertically centered[left|right]placement, while the screenshots above suggest that the resulting placement should be aligned to the top or the bottom (i.e.'-start'or an'-end'alignment). -
'[bottom|top] center [left|right]'positions: for these positions,positionToPlacementcurrently returns an aligned-[start|end]placement, while the screenshots above suggest that the resulting placement should be centered on the x axis (ie. only'bottom'or'top'values) -
'[bottom|top] left left'positions: for these positions,positionToPlacementcurrently returns a'-start'alignment, while the screenshots above suggest more of an'-end'alignment. An'-end'alignment would also be more consistent with how other positions are converted (e.gtop leftis converted to an-endalignment and the popover looks like it's positioned in the same way,top right rightis converted to a-startalignment...)



