@ionic/core
Version:
Base components for Ionic
872 lines (871 loc) • 32.5 kB
JavaScript
/*!
* (C) Ionic http://ionicframework.com - MIT License
*/
import { getElementRoot, raf } from "../../utils/helpers";
/**
* Shared per-frame cache for safe-area insets. Avoids creating a temporary
* DOM element and forcing a synchronous reflow on every call within the same
* frame (e.g., multiple popovers presenting simultaneously). Invalidated
* after the next animation frame so values stay fresh across orientation
* changes and viewport resizes.
*/
let cachedInsets = null;
let cacheInvalidationScheduled = false;
/**
* Resolves the current --ion-safe-area-* CSS custom property values
* to actual pixel numbers. A temporary element is needed because
* getComputedStyle on :root returns the declared value of custom
* properties (e.g. "env(safe-area-inset-top)") rather than a
* resolved number. By assigning them to standard padding properties,
* the browser resolves the value.
*
* Results are cached for the current frame to avoid repeated reflows.
*/
export const getSafeAreaInsets = (doc) => {
if (cachedInsets !== null) {
return cachedInsets;
}
if (doc.body === null) {
return { top: 0, bottom: 0, left: 0, right: 0 };
}
const el = doc.createElement('div');
el.style.cssText =
'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' +
'padding-top:var(--ion-safe-area-top,0px);' +
'padding-bottom:var(--ion-safe-area-bottom,0px);' +
'padding-left:var(--ion-safe-area-left,0px);' +
'padding-right:var(--ion-safe-area-right,0px);';
doc.body.appendChild(el);
const style = getComputedStyle(el);
const insets = {
top: parseFloat(style.paddingTop) || 0,
bottom: parseFloat(style.paddingBottom) || 0,
left: parseFloat(style.paddingLeft) || 0,
right: parseFloat(style.paddingRight) || 0,
};
el.remove();
cachedInsets = insets;
if (!cacheInvalidationScheduled) {
cacheInvalidationScheduled = true;
raf(() => {
cachedInsets = null;
cacheInvalidationScheduled = false;
});
}
return insets;
};
/**
* Largest difference from 1 that the `offsetWidth` based zoom detection below
* attributes to integer rounding rather than to an actual CSS `zoom`. The
* rounding error is at most half a pixel over the width of the popover, which
* is well under this threshold for any realistic popover size.
*/
const ZOOM_ROUNDING_TOLERANCE = 0.01;
/**
* Returns the cumulative CSS `zoom` factor applied to an element.
*
* When a CSS `zoom` other than 1 is set on an ancestor (e.g. the `html`
* element, as recommended by the docs for dynamic font scaling on Chrome for
* Android), `getBoundingClientRect()`, `clientX`/`clientY` and other geometry
* APIs report values in the *zoomed* (visual) coordinate space, while inline
* `top`/`left`/`--width` styles we set are interpreted in the *unzoomed*
* (layout) space and re-scaled by the browser. Dividing the rect-derived
* values by this factor converts them back to layout space so the popover is
* positioned and sized correctly. Returns 1 when no zoom is applied.
*/
export const getElementCSSZoom = (el) => {
if (!el) {
return 1;
}
/**
* `currentCSSZoom` exposes the exact effective zoom of an element
* (Chromium 128+). When available we use it directly.
*/
const currentCSSZoom = el.currentCSSZoom;
if (typeof currentCSSZoom === 'number' && currentCSSZoom > 0) {
return currentCSSZoom;
}
/**
* Fallback for browsers without `currentCSSZoom`: compare the rendered
* (zoomed) width from `getBoundingClientRect()` against the layout width
* from `offsetWidth`, which is not affected by CSS `zoom`.
*/
const { width } = el.getBoundingClientRect();
const { offsetWidth } = el;
if (offsetWidth > 0 && width > 0) {
const ratio = width / offsetWidth;
/**
* `offsetWidth` is rounded to an integer while the bounding rect is not,
* so the ratio is rarely exactly 1 even when no zoom is applied. Treat
* sub-pixel differences as "no zoom" so that unzoomed popovers are not
* shifted by the rounding error. A real zoom deviates far more, though
* the same rounding leaves the detected factor approximate.
*/
return Math.abs(ratio - 1) < ZOOM_ROUNDING_TOLERANCE ? 1 : ratio;
}
return 1;
};
/**
* Returns the dimensions of the popover
* arrow on `ios` mode. If arrow is disabled
* returns (0, 0).
*/
export const getArrowDimensions = (arrowEl, zoom = 1) => {
if (!arrowEl) {
return { arrowWidth: 0, arrowHeight: 0 };
}
const { width, height } = arrowEl.getBoundingClientRect();
return { arrowWidth: width / zoom, arrowHeight: height / zoom };
};
/**
* Returns the recommended dimensions of the popover
* that takes into account whether or not the width
* should match the trigger width.
*/
export const getPopoverDimensions = (size, contentEl, triggerEl, zoom = 1) => {
const contentDimentions = contentEl.getBoundingClientRect();
const contentHeight = contentDimentions.height / zoom;
let contentWidth = contentDimentions.width / zoom;
if (size === 'cover' && triggerEl) {
const triggerDimensions = triggerEl.getBoundingClientRect();
contentWidth = triggerDimensions.width / zoom;
}
return {
contentWidth,
contentHeight,
};
};
export const configureDismissInteraction = (triggerEl, triggerAction, popoverEl, parentPopoverEl) => {
let dismissCallbacks = [];
const root = getElementRoot(parentPopoverEl);
const parentContentEl = root.querySelector('.popover-content');
switch (triggerAction) {
case 'hover':
dismissCallbacks = [
{
/**
* Do not use mouseover here
* as this will causes the event to
* be dispatched on each underlying
* element rather than on the popover
* content as a whole.
*/
eventName: 'mouseenter',
callback: (ev) => {
/**
* Do not dismiss the popover is we
* are hovering over its trigger.
* This would be easier if we used mouseover
* but this would cause the event to be dispatched
* more often than we would like, potentially
* causing performance issues.
*/
const element = document.elementFromPoint(ev.clientX, ev.clientY);
if (element === triggerEl) {
return;
}
popoverEl.dismiss(undefined, undefined, false);
},
},
];
break;
case 'context-menu':
case 'click':
default:
dismissCallbacks = [
{
eventName: 'click',
callback: (ev) => {
/**
* Do not dismiss the popover is we
* are hovering over its trigger.
*/
const target = ev.target;
const closestTrigger = target.closest('[data-ion-popover-trigger]');
if (closestTrigger === triggerEl) {
/**
* stopPropagation here so if the
* popover has dismissOnSelect="true"
* the popover does not dismiss since
* we just clicked a trigger element.
*/
ev.stopPropagation();
return;
}
popoverEl.dismiss(undefined, undefined, false);
},
},
];
break;
}
dismissCallbacks.forEach(({ eventName, callback }) => parentContentEl.addEventListener(eventName, callback));
return () => {
dismissCallbacks.forEach(({ eventName, callback }) => parentContentEl.removeEventListener(eventName, callback));
};
};
/**
* Configures the triggerEl to respond
* to user interaction based upon the triggerAction
* prop that devs have defined.
*/
export const configureTriggerInteraction = (triggerEl, triggerAction, popoverEl) => {
let triggerCallbacks = [];
/**
* Based upon the kind of trigger interaction
* the user wants, we setup the correct event
* listeners.
*/
switch (triggerAction) {
case 'hover':
let hoverTimeout;
triggerCallbacks = [
{
eventName: 'mouseenter',
callback: async (ev) => {
ev.stopPropagation();
if (hoverTimeout) {
clearTimeout(hoverTimeout);
}
/**
* Hovering over a trigger should not
* immediately open the next popover.
*/
hoverTimeout = setTimeout(() => {
raf(() => {
popoverEl.presentFromTrigger(ev);
hoverTimeout = undefined;
});
}, 100);
},
},
{
eventName: 'mouseleave',
callback: (ev) => {
if (hoverTimeout) {
clearTimeout(hoverTimeout);
}
/**
* If mouse is over another popover
* that is not this popover then we should
* close this popover.
*/
const target = ev.relatedTarget;
if (!target) {
return;
}
if (target.closest('ion-popover') !== popoverEl) {
popoverEl.dismiss(undefined, undefined, false);
}
},
},
{
/**
* stopPropagation here prevents the popover
* from dismissing when dismiss-on-select="true".
*/
eventName: 'click',
callback: (ev) => ev.stopPropagation(),
},
{
eventName: 'ionPopoverActivateTrigger',
callback: (ev) => popoverEl.presentFromTrigger(ev, true),
},
];
break;
case 'context-menu':
triggerCallbacks = [
{
eventName: 'contextmenu',
callback: (ev) => {
/**
* Prevents the platform context
* menu from appearing.
*/
ev.preventDefault();
popoverEl.presentFromTrigger(ev);
},
},
{
eventName: 'click',
callback: (ev) => ev.stopPropagation(),
},
{
eventName: 'ionPopoverActivateTrigger',
callback: (ev) => popoverEl.presentFromTrigger(ev, true),
},
];
break;
case 'click':
default:
triggerCallbacks = [
{
/**
* Do not do a stopPropagation() here
* because if you had two click triggers
* then clicking the first trigger and then
* clicking the second trigger would not cause
* the first popover to dismiss.
*/
eventName: 'click',
callback: (ev) => popoverEl.presentFromTrigger(ev),
},
{
eventName: 'ionPopoverActivateTrigger',
callback: (ev) => popoverEl.presentFromTrigger(ev, true),
},
];
break;
}
triggerCallbacks.forEach(({ eventName, callback }) => triggerEl.addEventListener(eventName, callback));
triggerEl.setAttribute('data-ion-popover-trigger', 'true');
return () => {
triggerCallbacks.forEach(({ eventName, callback }) => triggerEl.removeEventListener(eventName, callback));
triggerEl.removeAttribute('data-ion-popover-trigger');
};
};
/**
* Returns the index of an ion-item in an array of ion-items.
*/
export const getIndexOfItem = (items, item) => {
if (!item || item.tagName !== 'ION-ITEM') {
return -1;
}
return items.findIndex((el) => el === item);
};
/**
* Given an array of elements and a currently focused ion-item
* returns the next ion-item relative to the focused one or
* undefined.
*/
export const getNextItem = (items, currentItem) => {
const currentItemIndex = getIndexOfItem(items, currentItem);
return items[currentItemIndex + 1];
};
/**
* Given an array of elements and a currently focused ion-item
* returns the previous ion-item relative to the focused one or
* undefined.
*/
export const getPrevItem = (items, currentItem) => {
const currentItemIndex = getIndexOfItem(items, currentItem);
return items[currentItemIndex - 1];
};
/** Focus the internal button of the ion-item */
const focusItem = (item) => {
const root = getElementRoot(item);
const button = root.querySelector('button');
if (button) {
raf(() => button.focus());
}
};
/**
* Returns `true` if `el` has been designated
* as a trigger element for an ion-popover.
*/
export const isTriggerElement = (el) => el.hasAttribute('data-ion-popover-trigger');
export const configureKeyboardInteraction = (popoverEl) => {
const callback = async (ev) => {
const activeElement = document.activeElement;
let items = [];
const targetTagName = ev.target?.tagName;
/**
* Only handle custom keyboard interactions for the host popover element
* and children ion-item elements.
*/
if (targetTagName !== 'ION-POPOVER' && targetTagName !== 'ION-ITEM') {
return;
}
/**
* Complex selectors with :not() are :not supported
* in older versions of Chromium so we need to do a
* try/catch here so errors are not thrown.
*/
try {
/**
* Select all ion-items that are not children of child popovers.
* i.e. only select ion-item elements that are part of this popover
*/
items = Array.from(popoverEl.querySelectorAll('ion-item:not(ion-popover ion-popover *):not([disabled])'));
/* eslint-disable-next-line */
}
catch { }
switch (ev.key) {
/**
* If we are in a child popover
* then pressing the left arrow key
* should close this popover and move
* focus to the popover that presented
* this one.
*/
case 'ArrowLeft':
const parentPopover = await popoverEl.getParentPopover();
if (parentPopover) {
popoverEl.dismiss(undefined, undefined, false);
}
break;
/**
* ArrowDown should move focus to the next focusable ion-item.
*/
case 'ArrowDown':
// Disable movement/scroll with keyboard
ev.preventDefault();
const nextItem = getNextItem(items, activeElement);
if (nextItem !== undefined) {
focusItem(nextItem);
}
break;
/**
* ArrowUp should move focus to the previous focusable ion-item.
*/
case 'ArrowUp':
// Disable movement/scroll with keyboard
ev.preventDefault();
const prevItem = getPrevItem(items, activeElement);
if (prevItem !== undefined) {
focusItem(prevItem);
}
break;
/**
* Home should move focus to the first focusable ion-item.
*/
case 'Home':
ev.preventDefault();
const firstItem = items[0];
if (firstItem !== undefined) {
focusItem(firstItem);
}
break;
/**
* End should move focus to the last focusable ion-item.
*/
case 'End':
ev.preventDefault();
const lastItem = items[items.length - 1];
if (lastItem !== undefined) {
focusItem(lastItem);
}
break;
/**
* ArrowRight, Spacebar, or Enter should activate
* the currently focused trigger item to open a
* popover if the element is a trigger item.
*/
case 'ArrowRight':
case ' ':
case 'Enter':
if (activeElement && isTriggerElement(activeElement)) {
const rightEvent = new CustomEvent('ionPopoverActivateTrigger');
activeElement.dispatchEvent(rightEvent);
}
break;
default:
break;
}
};
popoverEl.addEventListener('keydown', callback);
return () => popoverEl.removeEventListener('keydown', callback);
};
/**
* Positions a popover by taking into account
* the reference point, preferred side, alignment
* and viewport dimensions.
*/
export const getPopoverPosition = (isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, triggerEl, event, zoom = 1) => {
let referenceCoordinates = {
top: 0,
left: 0,
width: 0,
height: 0,
};
/**
* Calculate position relative to the
* x-y coordinates in the event that
* was passed in
*/
switch (reference) {
case 'event':
if (!event) {
return defaultPosition;
}
const mouseEv = event;
referenceCoordinates = {
top: mouseEv.clientY / zoom,
left: mouseEv.clientX / zoom,
width: 1,
height: 1,
};
break;
/**
* Calculate position relative to the bounding
* box on either the trigger element
* specified via the `trigger` prop or
* the target specified on the event
* that was passed in.
*/
case 'trigger':
default:
const customEv = event;
/**
* ionShadowTarget is used when we need to align the
* popover with an element inside of the shadow root
* of an Ionic component. Ex: Presenting a popover
* by clicking on the collapsed indicator inside
* of `ion-breadcrumb` and centering it relative
* to the indicator rather than `ion-breadcrumb`
* as a whole.
*/
const actualTriggerEl = (triggerEl ||
customEv?.detail?.ionShadowTarget ||
customEv?.target);
if (!actualTriggerEl) {
return defaultPosition;
}
const triggerBoundingBox = actualTriggerEl.getBoundingClientRect();
referenceCoordinates = {
top: triggerBoundingBox.top / zoom,
left: triggerBoundingBox.left / zoom,
width: triggerBoundingBox.width / zoom,
height: triggerBoundingBox.height / zoom,
};
break;
}
/**
* Get top/left offset that would allow
* popover to be positioned on the
* preferred side of the reference.
*/
const coordinates = calculatePopoverSide(side, referenceCoordinates, contentWidth, contentHeight, arrowWidth, arrowHeight, isRTL);
/**
* Get the top/left adjustments that
* would allow the popover content
* to have the correct alignment.
*/
const alignedCoordinates = calculatePopoverAlign(align, side, referenceCoordinates, contentWidth, contentHeight);
const top = coordinates.top + alignedCoordinates.top;
const left = coordinates.left + alignedCoordinates.left;
const { arrowTop, arrowLeft } = calculateArrowPosition(side, arrowWidth, arrowHeight, top, left, contentWidth, contentHeight, isRTL);
const { originX, originY } = calculatePopoverOrigin(side, align, isRTL);
return { top, left, referenceCoordinates, arrowTop, arrowLeft, originX, originY };
};
/**
* Determines the transform-origin
* of the popover animation so that it
* is in line with what the side and alignment
* prop values are. Currently only used
* with the MD animation.
*/
const calculatePopoverOrigin = (side, align, isRTL) => {
switch (side) {
case 'top':
return { originX: getOriginXAlignment(align), originY: 'bottom' };
case 'bottom':
return { originX: getOriginXAlignment(align), originY: 'top' };
case 'left':
return { originX: 'right', originY: getOriginYAlignment(align) };
case 'right':
return { originX: 'left', originY: getOriginYAlignment(align) };
case 'start':
return { originX: isRTL ? 'left' : 'right', originY: getOriginYAlignment(align) };
case 'end':
return { originX: isRTL ? 'right' : 'left', originY: getOriginYAlignment(align) };
}
};
const getOriginXAlignment = (align) => {
switch (align) {
case 'start':
return 'left';
case 'center':
return 'center';
case 'end':
return 'right';
}
};
const getOriginYAlignment = (align) => {
switch (align) {
case 'start':
return 'top';
case 'center':
return 'center';
case 'end':
return 'bottom';
}
};
/**
* Calculates where the arrow positioning
* should be relative to the popover content.
*/
const calculateArrowPosition = (side, arrowWidth, arrowHeight, top, left, contentWidth, contentHeight, isRTL) => {
/**
* Note: When side is left, right, start, or end, the arrow is
* been rotated using a `transform`, so to move the arrow up or down
* by its dimension, you need to use `arrowWidth`.
*/
const leftPosition = {
arrowTop: top + contentHeight / 2 - arrowWidth / 2,
arrowLeft: left + contentWidth - arrowWidth / 2,
};
/**
* Move the arrow to the left by arrowWidth and then
* again by half of its width because we have rotated
* the arrow using a transform.
*/
const rightPosition = { arrowTop: top + contentHeight / 2 - arrowWidth / 2, arrowLeft: left - arrowWidth * 1.5 };
switch (side) {
case 'top':
return { arrowTop: top + contentHeight, arrowLeft: left + contentWidth / 2 - arrowWidth / 2 };
case 'bottom':
return { arrowTop: top - arrowHeight, arrowLeft: left + contentWidth / 2 - arrowWidth / 2 };
case 'left':
return leftPosition;
case 'right':
return rightPosition;
case 'start':
return isRTL ? rightPosition : leftPosition;
case 'end':
return isRTL ? leftPosition : rightPosition;
default:
return { arrowTop: 0, arrowLeft: 0 };
}
};
/**
* Calculates the required top/left
* values needed to position the popover
* content on the side specified in the
* `side` prop.
*/
const calculatePopoverSide = (side, triggerBoundingBox, contentWidth, contentHeight, arrowWidth, arrowHeight, isRTL) => {
const sideLeft = {
top: triggerBoundingBox.top,
left: triggerBoundingBox.left - contentWidth - arrowWidth,
};
const sideRight = {
top: triggerBoundingBox.top,
left: triggerBoundingBox.left + triggerBoundingBox.width + arrowWidth,
};
switch (side) {
case 'top':
return {
top: triggerBoundingBox.top - contentHeight - arrowHeight,
left: triggerBoundingBox.left,
};
case 'right':
return sideRight;
case 'bottom':
return {
top: triggerBoundingBox.top + triggerBoundingBox.height + arrowHeight,
left: triggerBoundingBox.left,
};
case 'left':
return sideLeft;
case 'start':
return isRTL ? sideRight : sideLeft;
case 'end':
return isRTL ? sideLeft : sideRight;
}
};
/**
* Calculates the required top/left
* offset values needed to provide the
* correct alignment regardless while taking
* into account the side the popover is on.
*/
const calculatePopoverAlign = (align, side, triggerBoundingBox, contentWidth, contentHeight) => {
switch (align) {
case 'center':
return calculatePopoverCenterAlign(side, triggerBoundingBox, contentWidth, contentHeight);
case 'end':
return calculatePopoverEndAlign(side, triggerBoundingBox, contentWidth, contentHeight);
case 'start':
default:
return { top: 0, left: 0 };
}
};
/**
* Calculate the end alignment for
* the popover. If side is on the x-axis
* then the align values refer to the top
* and bottom margins of the content.
* If side is on the y-axis then the
* align values refer to the left and right
* margins of the content.
*/
const calculatePopoverEndAlign = (side, triggerBoundingBox, contentWidth, contentHeight) => {
switch (side) {
case 'start':
case 'end':
case 'left':
case 'right':
return {
top: -(contentHeight - triggerBoundingBox.height),
left: 0,
};
case 'top':
case 'bottom':
default:
return {
top: 0,
left: -(contentWidth - triggerBoundingBox.width),
};
}
};
/**
* Calculate the center alignment for
* the popover. If side is on the x-axis
* then the align values refer to the top
* and bottom margins of the content.
* If side is on the y-axis then the
* align values refer to the left and right
* margins of the content.
*/
const calculatePopoverCenterAlign = (side, triggerBoundingBox, contentWidth, contentHeight) => {
switch (side) {
case 'start':
case 'end':
case 'left':
case 'right':
return {
top: -(contentHeight / 2 - triggerBoundingBox.height / 2),
left: 0,
};
case 'top':
case 'bottom':
default:
return {
top: 0,
left: -(contentWidth / 2 - triggerBoundingBox.width / 2),
};
}
};
/**
* Adjusts popover positioning coordinates
* such that popover does not appear offscreen
* or overlapping safe area bounds.
*
* @internal - This is not part of the public API.
*/
export const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
let arrowTop = coordArrowTop;
const arrowLeft = coordArrowLeft;
let left = coordLeft;
let top = coordTop;
let bottom;
let originX = contentOriginX;
let originY = contentOriginY;
let checkSafeAreaLeft = false;
let checkSafeAreaRight = false;
let checkSafeAreaTop = false;
let checkSafeAreaBottom = false;
const triggerTop = triggerCoordinates
? triggerCoordinates.top + triggerCoordinates.height
: bodyHeight / 2 - contentHeight / 2;
const triggerHeight = triggerCoordinates ? triggerCoordinates.height : 0;
let addPopoverBottomClass = false;
const hideArrow = false;
/**
* Adjust popover so it does not
* go off the left of the screen.
*/
if (left < bodyPadding + safeArea.left) {
left = bodyPadding;
checkSafeAreaLeft = true;
originX = 'left';
/**
* Adjust popover so it does not
* go off the right of the screen.
*/
}
else if (contentWidth + bodyPadding + left + safeArea.right > bodyWidth) {
checkSafeAreaRight = true;
left = bodyWidth - contentWidth - bodyPadding;
originX = 'right';
}
/**
* Adjust popover so it does not
* go off the bottom of the screen
* or overlap the bottom safe area.
* If popover is on the left or the right of
* the trigger, then we should not adjust top
* margins.
*/
if (triggerTop + triggerHeight + contentHeight > bodyHeight - safeArea.bottom &&
(side === 'top' || side === 'bottom')) {
/**
* Calculate where the popover top would be if flipped
* above the trigger. Check whether that position clears
* the top safe area with room for bodyPadding.
*/
const idealFlipTop = triggerTop - contentHeight - triggerHeight - (arrowHeight - 1);
if (idealFlipTop >= safeArea.top + bodyPadding) {
/**
* Popover fits above the trigger without overlapping
* the top safe area. Use the ideal position directly —
* no safe-area CSS vars needed since it clears both edges.
*/
top = idealFlipTop;
arrowTop = top + contentHeight;
originY = 'bottom';
addPopoverBottomClass = true;
}
else {
/**
* Can't flip above the trigger. Constrain the bottom
* edge while keeping the top near the trigger. This
* creates a scrollable area anchored to the trigger.
*/
bottom = bodyPadding;
checkSafeAreaBottom = true;
/**
* When the trigger is near the bottom of the screen,
* the calculated top may be at or past the bottom
* constraint, leaving zero visible height. In that
* case, pull top up to the top safe area so the
* popover fills the available space between safe areas.
*/
const bottomEdge = bodyHeight - safeArea.bottom - bodyPadding;
if (top >= bottomEdge) {
top = safeArea.top + bodyPadding;
checkSafeAreaTop = true;
}
}
}
return {
top,
left,
bottom,
originX,
originY,
checkSafeAreaLeft,
checkSafeAreaRight,
checkSafeAreaTop,
checkSafeAreaBottom,
arrowTop,
arrowLeft,
addPopoverBottomClass,
hideArrow,
};
};
export const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
/**
* If no event provided and
* we do not have a trigger,
* then this popover was likely
* presented via the popoverController
* or users called `present` manually.
* In this case, the arrow should not be
* shown as we do not have a reference.
*/
if (!ev && !trigger) {
return false;
}
/**
* If popover is on the left or the right
* of a trigger, but we needed to adjust the
* popover due to screen bounds, then we should
* hide the arrow as it will never be pointing
* at the trigger.
*/
if (side !== 'top' && side !== 'bottom' && didAdjustBounds) {
return false;
}
return true;
};