Skip to content

Commit

Permalink
Handle large elements with new "unaligned" alignment fallback options…
Browse files Browse the repository at this point in the history
… that

nudge the element onto the screen.

Solves issue #17
  • Loading branch information
Macil committed Sep 29, 2023
1 parent 02e5b75 commit aa50be0
Show file tree
Hide file tree
Showing 3 changed files with 317 additions and 24 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ be the button that triggered a dropdown menu to appear.
relative to its anchor. The horizontal alignment mode is used if the element
is positioned in the top, bottom, or cover positions relative to the anchor,
and causes the element to be moved horizontally in order to make a specific
edge align. It may be set to null, "center", "left", "right", or an array of
some of those string values. The element will attempt to use this alignment
(or each value in the array in order) unless it is not possible to do so while
fitting the element on-screen.
edge align. It may be set to null, "center", "left", "right", "unaligned", or
an array of some of those string values. The element will attempt to use this
alignment (or each value in the array in order) unless it is not possible to
do so while fitting the element on-screen.

- `forceHAlign` is a boolean which controls whether the configured hAlign value
will be used even if it results in the element going off of the screen.
Expand All @@ -57,10 +57,10 @@ be the button that triggered a dropdown menu to appear.
to its anchor. The vertical alignment mode is used if the element is
positioned in the left, right, or cover positions relative to the anchor, and
causes the element to be moved vertically in order to make a specific edge
align. It may be set to null, "center", "top", "bottom", or an array of some
of those string values. The element will attempt to use this alignment (or
each value in the array in order) unless it is not possible to do so while
fitting the element on-screen.
align. It may be set to null, "center", "top", "bottom", "unaligned", or an
array of some of those string values. The element will attempt to use this
alignment (or each value in the array in order) unless it is not possible to
do so while fitting the element on-screen.

- `forceVAlign` is a boolean which controls whether the configured vAlign value
will be used even if it results in the element going off of the screen.
Expand Down
144 changes: 130 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,20 @@ import uniq from "lodash/uniq";
import { isNotNil } from "./isNotNil";

export type PositionOption = "top" | "bottom" | "left" | "right" | "cover";
export type HAlignOption = "center" | "left" | "right";
export type VAlignOption = "center" | "top" | "bottom";
/**
* This option is used when position is "cover", "top", or "bottom".
* It controls the horizontal alignment of the element relative to the anchor.
* "center" means the element's center is aligned with the anchor's center.
* "left" and "right" means that edge of the element is aligned with the same edge of the anchor.
* "unaligned" means the element may not be aligned with the anchor at all. Currently
* this works by doing the same thing as "center" and then adjusting the result to fit on screen.
*/
export type HAlignOption = "center" | "left" | "right" | "unaligned";
/**
* Similar to {@link HAlignOption}, except this controls the vertical alignment of the element
* relative to the anchor when the position is "cover", "left", or "right".
*/
export type VAlignOption = "center" | "top" | "bottom" | "unaligned";

export type Position = PositionOption | PositionOption[];
export type HAlign = HAlignOption | HAlignOption[];
Expand Down Expand Up @@ -129,29 +141,73 @@ export function getContainByScreenResults(
),
);

// Try unaligned versions at the end
if (!hAligns.includes("unaligned")) {
allPossibleChoices.push(
...flatten(
positions.map((position) =>
!["cover", "top", "bottom"].includes(position)
? []
: vAligns.map((vAlign) => ({
position,
hAlign: "unaligned" as const,
vAlign,
})),
),
),
);
}
if (!vAligns.includes("unaligned")) {
allPossibleChoices.push(
...flatten(
positions.map((position) =>
!["cover", "left", "right"].includes(position)
? []
: hAligns.map((hAlign) => ({
position,
hAlign,
vAlign: "unaligned" as const,
})),
),
),
);
}

let choiceAndCoord: ChoiceAndCoordinates | null = null;
for (let i = 0; i < allPossibleChoices.length; i++) {
const choice = allPossibleChoices[i];
const coordinates = positionAndAlign(elRect, anchorRect, choice, buffers);
const { top, left } = coordinates;
if (
top - buffers.all - buffers.top >= 0 &&
left - buffers.all - buffers.left >= 0 &&
top + elRect.height + buffers.all + buffers.bottom <=
window.innerHeight &&
left + elRect.width + buffers.all + buffers.right <= window.innerWidth
) {

const ignoreHorizontalConstraints =
choice.hAlign === "unaligned" &&
["cover", "top", "bottom"].includes(choice.position);
const ignoreVerticalConstraints =
choice.vAlign === "unaligned" &&
["cover", "left", "right"].includes(choice.position);

const hasHorizontalFit =
ignoreHorizontalConstraints ||
(left - buffers.all - buffers.left >= 0 &&
left + elRect.width + buffers.all + buffers.right <= window.innerWidth);
const hasVerticalFit =
ignoreVerticalConstraints ||
(top - buffers.all - buffers.top >= 0 &&
top + elRect.height + buffers.all + buffers.bottom <=
window.innerHeight);

if (hasHorizontalFit && hasVerticalFit) {
choiceAndCoord = { choice, coordinates };
break;
}
}

// Fallback if we failed to find a position that fit on the screen.
// Fallback if we failed to find a choice that fit on the screen.
if (!choiceAndCoord) {
const choice = {
position: optionPositions[0] || "top",
hAlign: optionHAligns[0] || "center",
vAlign: optionVAligns[0] || "center",
const choice: Choice = {
position: "cover",
hAlign: "unaligned",
vAlign: "unaligned",
};
choiceAndCoord = {
choice,
Expand Down Expand Up @@ -204,6 +260,19 @@ function positionAndAlign(
case "right":
left = Math.ceil(anchorRect.right - elRect.width);
break;
case "unaligned": {
left = Math.max(
buffers.all + buffers.left,
Math.round((anchorRect.left + anchorRect.right - elRect.width) / 2),
);
const overhang = Math.ceil(
left + elRect.width + buffers.all + buffers.right - window.innerWidth,
);
if (overhang > 0) {
left -= overhang;
}
break;
}
default:
hAlign satisfies never;
throw new Error("Should not happen");
Expand All @@ -220,6 +289,23 @@ function positionAndAlign(
case "bottom":
top = Math.ceil(anchorRect.bottom - elRect.height);
break;
case "unaligned": {
top = Math.max(
buffers.all + buffers.top,
Math.round((anchorRect.top + anchorRect.bottom - elRect.height) / 2),
);
const overhang = Math.ceil(
top +
elRect.height +
buffers.all +
buffers.bottom -
window.innerHeight,
);
if (overhang > 0) {
top -= overhang;
}
break;
}
default:
vAlign satisfies never;
throw new Error("Should not happen");
Expand Down Expand Up @@ -250,6 +336,19 @@ function positionAndAlign(
case "right":
left = Math.round(anchorRect.right - elRect.width);
break;
case "unaligned": {
left = Math.max(
buffers.all + buffers.left,
Math.round((anchorRect.left + anchorRect.right - elRect.width) / 2),
);
const overhang = Math.ceil(
left + elRect.width + buffers.all + buffers.right - window.innerWidth,
);
if (overhang > 0) {
left -= overhang;
}
break;
}
default:
hAlign satisfies never;
throw new Error("Should not happen");
Expand Down Expand Up @@ -280,6 +379,23 @@ function positionAndAlign(
case "bottom":
top = Math.round(anchorRect.bottom - elRect.height);
break;
case "unaligned": {
top = Math.max(
buffers.all + buffers.top,
Math.round((anchorRect.top + anchorRect.bottom - elRect.height) / 2),
);
const overhang = Math.ceil(
top +
elRect.height +
buffers.all +
buffers.bottom -
window.innerHeight,
);
if (overhang > 0) {
top -= overhang;
}
break;
}
default:
vAlign satisfies never;
throw new Error("Should not happen");
Expand Down
Loading

0 comments on commit aa50be0

Please sign in to comment.