@awsui/components-react
Version:
On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en
275 lines • 12.4 kB
JavaScript
const ARROW_OFFSET = 12;
export const PRIORITY_MAPPING = {
top: [
'top-center',
'top-right',
'top-left',
'bottom-center',
'bottom-right',
'bottom-left',
'right-top',
'right-bottom',
'left-top',
'left-bottom',
],
bottom: [
'bottom-center',
'bottom-right',
'bottom-left',
'top-center',
'top-right',
'top-left',
'right-top',
'right-bottom',
'left-top',
'left-bottom',
],
left: [
'left-top',
'left-bottom',
'right-top',
'right-bottom',
'bottom-center',
'top-center',
'bottom-left',
'top-left',
'bottom-right',
'top-right',
],
right: [
'right-top',
'right-bottom',
'left-top',
'left-bottom',
'bottom-center',
'top-center',
'bottom-right',
'top-right',
'bottom-left',
'top-left',
],
};
const RECTANGLE_CALCULATIONS = {
'top-center': ({ body, trigger, arrow }) => {
return {
insetBlockStart: trigger.insetBlockStart - body.blockSize - arrow.blockSize,
insetInlineStart: trigger.insetInlineStart + trigger.inlineSize / 2 - body.inlineSize / 2,
inlineSize: body.inlineSize,
blockSize: body.blockSize,
};
},
'top-right': ({ body, trigger, arrow }) => {
return {
insetBlockStart: trigger.insetBlockStart - body.blockSize - arrow.blockSize,
insetInlineStart: trigger.insetInlineStart + trigger.inlineSize / 2 - ARROW_OFFSET - arrow.inlineSize / 2,
inlineSize: body.inlineSize,
blockSize: body.blockSize,
};
},
'top-left': ({ body, trigger, arrow }) => {
return {
insetBlockStart: trigger.insetBlockStart - body.blockSize - arrow.blockSize,
insetInlineStart: trigger.insetInlineStart + trigger.inlineSize / 2 + ARROW_OFFSET + arrow.inlineSize / 2 - body.inlineSize,
inlineSize: body.inlineSize,
blockSize: body.blockSize,
};
},
'bottom-center': ({ body, trigger, arrow }) => {
return {
insetBlockStart: trigger.insetBlockStart + trigger.blockSize + arrow.blockSize,
insetInlineStart: trigger.insetInlineStart + trigger.inlineSize / 2 - body.inlineSize / 2,
inlineSize: body.inlineSize,
blockSize: body.blockSize,
};
},
'bottom-right': ({ body, trigger, arrow }) => {
return {
insetBlockStart: trigger.insetBlockStart + trigger.blockSize + arrow.blockSize,
insetInlineStart: trigger.insetInlineStart + trigger.inlineSize / 2 - ARROW_OFFSET - arrow.inlineSize / 2,
inlineSize: body.inlineSize,
blockSize: body.blockSize,
};
},
'bottom-left': ({ body, trigger, arrow }) => {
return {
insetBlockStart: trigger.insetBlockStart + trigger.blockSize + arrow.blockSize,
insetInlineStart: trigger.insetInlineStart + trigger.inlineSize / 2 + ARROW_OFFSET + arrow.inlineSize / 2 - body.inlineSize,
inlineSize: body.inlineSize,
blockSize: body.blockSize,
};
},
'right-top': ({ body, trigger, arrow }) => {
return {
insetBlockStart: trigger.insetBlockStart + trigger.blockSize / 2 - ARROW_OFFSET - arrow.blockSize,
insetInlineStart: trigger.insetInlineStart + trigger.inlineSize + arrow.blockSize,
inlineSize: body.inlineSize,
blockSize: body.blockSize,
};
},
'right-bottom': ({ body, trigger, arrow }) => {
return {
insetBlockStart: trigger.insetBlockStart + trigger.blockSize / 2 - body.blockSize + ARROW_OFFSET + arrow.blockSize,
insetInlineStart: trigger.insetInlineStart + trigger.inlineSize + arrow.blockSize,
inlineSize: body.inlineSize,
blockSize: body.blockSize,
};
},
'left-top': ({ body, trigger, arrow }) => {
return {
insetBlockStart: trigger.insetBlockStart + trigger.blockSize / 2 - ARROW_OFFSET - arrow.blockSize,
insetInlineStart: trigger.insetInlineStart - body.inlineSize - arrow.blockSize,
inlineSize: body.inlineSize,
blockSize: body.blockSize,
};
},
'left-bottom': ({ body, trigger, arrow }) => {
return {
insetBlockStart: trigger.insetBlockStart + trigger.blockSize / 2 - body.blockSize + ARROW_OFFSET + arrow.blockSize,
insetInlineStart: trigger.insetInlineStart - body.inlineSize - arrow.blockSize,
inlineSize: body.inlineSize,
blockSize: body.blockSize,
};
},
};
function fitIntoContainer(inner, outer) {
let { insetInlineStart, inlineSize, insetBlockStart, blockSize } = inner;
// Adjust left boundary.
if (insetInlineStart < outer.insetInlineStart) {
inlineSize = insetInlineStart + inlineSize - outer.insetInlineStart;
insetInlineStart = outer.insetInlineStart;
}
// Adjust right boundary.
else if (insetInlineStart + inlineSize > outer.insetInlineStart + outer.inlineSize) {
inlineSize = outer.insetInlineStart + outer.inlineSize - insetInlineStart;
}
// Adjust top boundary.
if (insetBlockStart < outer.insetBlockStart) {
blockSize = insetBlockStart + blockSize - outer.insetBlockStart;
insetBlockStart = outer.insetBlockStart;
}
// Adjust bottom boundary.
else if (insetBlockStart + blockSize > outer.insetBlockStart + outer.blockSize) {
blockSize = outer.insetBlockStart + outer.blockSize - insetBlockStart;
}
return { insetInlineStart, inlineSize, insetBlockStart, blockSize };
}
function getTallestRect(rect1, rect2) {
return rect1.blockSize >= rect2.blockSize ? rect1 : rect2;
}
function getIntersection(rectangles) {
let boundingBox = null;
for (const currentRect of rectangles) {
if (!boundingBox) {
boundingBox = currentRect;
continue;
}
const insetInlineStart = Math.max(boundingBox.insetInlineStart, currentRect.insetInlineStart);
const insetBlockStart = Math.max(boundingBox.insetBlockStart, currentRect.insetBlockStart);
const insetInlineEnd = Math.min(boundingBox.insetInlineStart + boundingBox.inlineSize, currentRect.insetInlineStart + currentRect.inlineSize);
const insetBlockEnd = Math.min(boundingBox.insetBlockStart + boundingBox.blockSize, currentRect.insetBlockStart + currentRect.blockSize);
if (insetInlineEnd < insetInlineStart || insetBlockEnd < insetBlockStart) {
return null;
}
boundingBox = {
insetInlineStart,
insetBlockStart,
inlineSize: insetInlineEnd - insetInlineStart,
blockSize: insetBlockEnd - insetBlockStart,
};
}
return boundingBox;
}
/**
* Returns the area of the intersection of passed in rectangles or a null, if there is no intersection
*/
export function intersectRectangles(rectangles) {
const boundingBox = getIntersection(rectangles);
return boundingBox && boundingBox.blockSize * boundingBox.inlineSize;
}
/**
* A functions that returns the correct popover position based on screen dimensions.
*/
export function calculatePosition({ preferredPosition, fixedInternalPosition, trigger, arrow, body, container, viewport,
// the popover is only bound by the viewport if it is rendered in a portal
renderWithPortal, allowVerticalOverflow, minVisibleBlockSize, }) {
let bestOption = null;
// If a fixed internal position is passed, only consider this one.
const preferredInternalPositions = fixedInternalPosition
? [fixedInternalPosition]
: PRIORITY_MAPPING[preferredPosition];
// Attempt to position the popover based on the priority list for this position.
for (const internalPosition of preferredInternalPositions) {
const rect = RECTANGLE_CALCULATIONS[internalPosition]({ body, trigger, arrow });
const visibleArea = renderWithPortal
? getIntersection([rect, viewport])
: getIntersection([rect, viewport, container]);
// When min visible block size is set, the popover is considered fitting the container if the available space
// is the same or larger than min allowed, even if it means the scrollbar is needed.
const fitsBlockSize = minVisibleBlockSize === undefined
? visibleArea && visibleArea.blockSize === body.blockSize
: visibleArea && visibleArea.blockSize >= Math.min(body.blockSize, minVisibleBlockSize);
const fitsInlineSize = visibleArea && visibleArea.inlineSize === body.inlineSize;
if (fitsBlockSize && fitsInlineSize) {
const scrollable = visibleArea && visibleArea.blockSize < body.blockSize;
return { internalPosition, rect: scrollable ? fitIntoContainer(rect, viewport) : rect, scrollable };
}
const newOption = { rect, internalPosition, visibleArea };
bestOption = getBestOption(newOption, bestOption);
}
// Use best possible placement.
const internalPosition = (bestOption === null || bestOption === void 0 ? void 0 : bestOption.internalPosition) || 'right-top';
// Get default rect for that placement.
const rect = RECTANGLE_CALCULATIONS[internalPosition]({ body, trigger, arrow });
// Get largest possible rect that fits into the viewport or container.
// We allow the popover to overflow the viewport if allowVerticalOverflow is true _and_ the popover will be anchored to the top or the bottom.
// If it is anchored to the right or left, we consider that it should have enough vertical space so that applying scroll to it is a better option.
const tallestBoundingContainer = getTallestRect(viewport, container);
const boundingContainer = allowVerticalOverflow && isTopOrBottom(internalPosition)
? {
insetBlockStart: tallestBoundingContainer.insetBlockStart,
blockSize: tallestBoundingContainer.blockSize,
insetInlineStart: viewport.insetInlineStart,
inlineSize: viewport.inlineSize,
}
: viewport;
const optimizedRect = fitIntoContainer(rect, boundingContainer);
// If largest possible rect is shorter than original - set body scroll.
const scrollable = optimizedRect.blockSize < rect.blockSize;
return { internalPosition, rect: optimizedRect, scrollable };
}
function getBestOption(option1, option2) {
// Within calculatePosition, the only case where option2 will not be defined will be in the first call.
// The only case where the visibleArea of an option will be null is when the popover is totally outside of the viewport.
if (!(option2 === null || option2 === void 0 ? void 0 : option2.visibleArea)) {
return option1;
}
if (!option1.visibleArea) {
return option2;
}
// Only if none of the two options overflows horizontally, choose the best based on the visible height.
if (option1.visibleArea.inlineSize === option2.visibleArea.inlineSize) {
return option1.visibleArea.blockSize > option2.visibleArea.blockSize ? option1 : option2;
}
// Otherwise, choose the option that is less cut off horizontally.
return option1.visibleArea.inlineSize > option2.visibleArea.inlineSize ? option1 : option2;
}
export function getOffsetDimensions(element) {
return { offsetHeight: element.offsetHeight, offsetWidth: element.offsetWidth };
}
export function getDimensions(element) {
const computedStyle = getComputedStyle(element);
return {
inlineSize: parseFloat(computedStyle.inlineSize),
blockSize: parseFloat(computedStyle.blockSize),
};
}
function isTopOrBottom(internalPosition) {
return ['top', 'bottom'].includes(internalPosition.split('-')[0]);
}
export function isCenterOutside(child, parent) {
const childCenter = child.insetBlockStart + child.blockSize / 2;
const overflowsBlockStart = childCenter < parent.insetBlockStart;
const overflowsBlockEnd = childCenter > parent.insetBlockEnd;
return overflowsBlockStart || overflowsBlockEnd;
}
//# sourceMappingURL=positions.js.map