Repository: exadel-inc/esl
Pull request #2795 (2024-11-28 17:01:48)

ESLPopup: optimize position calculations + test


Patch:
diff --git a/site/views/examples/popup.njk b/site/views/examples/popup.njk
index 21197b001..c6793d348 100644
--- a/site/views/examples/popup.njk
+++ b/site/views/examples/popup.njk
@@ -12,6 +12,18 @@ aside:
     - esl-trigger
 ---
 
+<style>
+  .trigger-for-inner-popup-v {
+    height: 200px;
+    width: 350px;
+    padding: 80px;
+  }
+  .trigger-for-inner-popup-h {
+    height: 350px;
+    width: 200px;
+  }
+</style>
+
 <section id="popup-controls">
   <div>
     <label for="marginArrow">Arrow margin:</label>
@@ -87,6 +99,68 @@ aside:
 
 <hr/>
 
+<h2>Inner popups with an arrow</h2>
+
+<section class="row justify-content-between">
+  <div>
+    <esl-trigger target="::next" mode="toggle" class="btn btn-sec-blue trigger-for-inner-popup-v">Top Inner Popup</esl-trigger>
+    <esl-popup class="demo-top-popup" position="top" position-origin="inner">
+      <div style="height: 50px; width:250px;"></div>
+    </esl-popup>
+  </div>
+  <div>
+    <esl-trigger target="::next" mode="toggle" class="btn btn-sec-blue trigger-for-inner-popup-h">Left Inner Popup</esl-trigger>
+    <esl-popup class="demo-left-popup" position="left" position-origin="inner">
+      <div style="height: 250px; width:50px;"></div>
+    </esl-popup>
+  </div>
+  <div>
+    <esl-trigger target="::next" mode="toggle" class="btn btn-sec-blue trigger-for-inner-popup-h">Right Inner Popup</esl-trigger>
+    <esl-popup class="demo-right-popup" position="right" position-origin="inner">
+      <div style="height: 250px; width:50px;"></div>
+    </esl-popup>
+  </div>
+  <div>
+    <esl-trigger target="::next" mode="toggle" class="btn btn-sec-blue trigger-for-inner-popup-v">Bottom Inner Popup</esl-trigger>
+    <esl-popup class="demo-bottom-popup" position="bottom" position-origin="inner">
+      <div style="height: 50px; width:250px;"></div>
+    </esl-popup>
+  </div>
+</section>
+
+<hr/>
+
+<h2>Inner popups without an arrow</h2>
+
+<section class="row justify-content-between">
+  <div>
+    <esl-trigger target="::next" mode="toggle" class="btn btn-sec-blue trigger-for-inner-popup-v">Top Inner Popup</esl-trigger>
+    <esl-popup class="demo-top-popup disable-arrow" position="top" position-origin="inner">
+      <div style="height: 50px; width:250px;"></div>
+    </esl-popup>
+  </div>
+  <div>
+    <esl-trigger target="::next" mode="toggle" class="btn btn-sec-blue trigger-for-inner-popup-h">Left Inner Popup</esl-trigger>
+    <esl-popup class="demo-left-popup disable-arrow" position="left" position-origin="inner">
+      <div style="height: 250px; width:50px;"></div>
+    </esl-popup>
+  </div>
+  <div>
+    <esl-trigger target="::next" mode="toggle" class="btn btn-sec-blue trigger-for-inner-popup-h">Right Inner Popup</esl-trigger>
+    <esl-popup class="demo-right-popup disable-arrow" position="right" position-origin="inner">
+      <div style="height: 250px; width:50px;"></div>
+    </esl-popup>
+  </div>
+  <div>
+    <esl-trigger target="::next" mode="toggle" class="btn btn-sec-blue trigger-for-inner-popup-v">Bottom Inner Popup</esl-trigger>
+    <esl-popup class="demo-bottom-popup disable-arrow" position="bottom" position-origin="inner">
+      <div style="height: 50px; width:250px;"></div>
+    </esl-popup>
+  </div>
+</section>
+
+<hr/>
+
 <h2>Examples of popups when they are defined in the neighborhood of a trigger</h2>
 
 <h3>Trigger target uses class</h3>
diff --git a/src/modules/esl-popup/README.md b/src/modules/esl-popup/README.md
index ddb722771..56b768228 100644
--- a/src/modules/esl-popup/README.md
+++ b/src/modules/esl-popup/README.md
@@ -91,7 +91,7 @@ You can also add additional classes and styles when activating and displaying th
   
 - `position-origin` <i class="badge badge-sup badge-warning">beta</i> - this attribute specifies from which side of the trigger grows popup to achieve the desired position ('outer' by default). Available options:
   - `outer` - popup grows from the outside of the trigger relative to the positioning direction and cannot overlap the trigger
-  - `inner` - popup grows from the inside of the trigger relative to the positioning direction and will overlap the trigger
+  - `inner` - popup grows to the inside of the trigger and can overlap the trigger if it is bigger than the trigger element
   
 - `behavior` - popup behavior if it does not fit in the window ('fit' by default). Available options:
   - `fit` - default, the popup will always be positioned in the right place. Position dynamically updates so it will
@@ -110,6 +110,8 @@ You can also add additional classes and styles when activating and displaying th
   
 - `offset-arrow` - offset of the arrow as a percentage of the popup edge. 0% - at the left edge, 100% - at the right edge, for RTL it is vice versa (50 by default)
 
+- `offset-trigger` - offset of the popup with an arrow from the trigger element. This is the value in pixels and also can be negative (3 by default)
+
 ESLPopup extends [ESLToggleable](../esl-toggleable/README.md) you can find other supported options in its documentation.
 
 #### Readonly Attributes
diff --git a/src/modules/esl-popup/core/esl-popup-position.ts b/src/modules/esl-popup/core/esl-popup-position.ts
index bdf5a448d..d6059ba18 100644
--- a/src/modules/esl-popup/core/esl-popup-position.ts
+++ b/src/modules/esl-popup/core/esl-popup-position.ts
@@ -2,11 +2,13 @@ import {Rect} from '../../esl-utils/dom/rect';
 
 import type {Point} from '../../esl-utils/dom/point';
 
+export type DimensionNameType = 'width' | 'height';
 export type PositionType = 'top' | 'bottom' | 'left' | 'right';
 export type PositionOriginType = 'inner' | 'outer';
+export type PlacedAtType = 'top' | 'bottom' | 'left' | 'right' | 'top-inner' | 'bottom-inner' | 'left-inner' | 'right-inner';
 
 export interface PopupPositionValue {
-  placedAt: PositionType;
+  placedAt: PlacedAtType;
   popup: Rect;
   arrow: Point;
 }
@@ -58,14 +60,48 @@ function isStartingSide(position: PositionType): boolean {
   return ['left', 'top'].includes(position);
 }
 
+/**
+ * Gets the name of the dimension along the axis of the specified position
+ * @param position - name of position
+ * @param alter - should it be the opposite dimension?
+ */
+function getDimensionName(position: PositionType, alter: boolean = false): DimensionNameType {
+  const isHorizontal = isOnHorizontalAxis(position);
+  return (alter ? !isHorizontal : isHorizontal) ? 'width' : 'height';
+}
+
+/**
+ * Gets the name of the position where the arrow should be placed
+ * @param cfg - popup position config
+ * @param isOpposite - should it be the opposite position?
+ */
+function getPlacedAt(cfg: PopupPositionConfig, isOpposite: boolean = false): PlacedAtType {
+  const position = isOpposite ? getOppositePosition(cfg.position) : cfg.position;
+  return `${position}${cfg.hasInnerOrigin ? '-inner' : ''}`;
+}
+
+/**
+ * Calculates the position of the popup on the major axis
+ * @param cfg - popup position config
+ */
+function calcPopupPositionByMajorAxis(cfg: PopupPositionConfig): number {
+  const {position, inner, element, hasInnerOrigin} = cfg;
+  const coord = inner[position];
+  const size = element[getDimensionName(position)];
+  return isStartingSide(position)
+    ? (hasInnerOrigin ? coord : coord - size)
+    : (hasInnerOrigin ? coord - size : coord);
+}
+
 /**
  * Calculates the position of the popup on the minor axis
  * @param cfg - popup position config
- * @param centerPosition - position of the center of the trigger on the minor axis
- * @param dimensionName - the name of dimension (height or width)
  */
-function calcPopupPositionByMinorAxis(cfg: PopupPositionConfig, centerPosition: number, dimensionName: 'height' | 'width'): number {
-  return centerPosition - cfg.arrow[dimensionName] / 2 - cfg.marginArrow - calcUsableSizeForArrow(cfg, dimensionName) * cfg.offsetArrowRatio;
+function calcPopupPositionByMinorAxis(cfg: PopupPositionConfig): number {
+  const {position, inner, arrow, marginArrow, offsetArrowRatio} = cfg;
+  const centerPosition = inner[isOnHorizontalAxis(position) ? 'cy' : 'cx'];
+  const dimensionName = getDimensionName(position, true);
+  return centerPosition - arrow[dimensionName] / 2 - marginArrow - calcUsableSizeForArrow(cfg, dimensionName) * offsetArrowRatio;
 }
 
 /**
@@ -73,24 +109,12 @@ function calcPopupPositionByMinorAxis(cfg: PopupPositionConfig, centerPosition:
  * @param cfg - popup position config
  * */
 function calcPopupBasicRect(cfg: PopupPositionConfig): Rect {
-  const {position, inner, element, hasInnerOrigin} = cfg;
-  let x = isOnHorizontalAxis(position) ? 0 : calcPopupPositionByMinorAxis(cfg, inner.cx, 'width');
-  let y = isOnHorizontalAxis(position) ? calcPopupPositionByMinorAxis(cfg, inner.cy, 'height') : 0;
-  switch (position) {
-    case 'left':
-      x = (hasInnerOrigin ? inner.right : inner.x) - element.width;
-      break;
-    case 'right':
-      x = hasInnerOrigin ? inner.x : inner.right;
-      break;
-    case 'bottom':
-      y = hasInnerOrigin ? inner.y : inner.bottom;
-      break;
-    default:
-      y = (hasInnerOrigin ? inner.bottom : inner.y) - element.height;
-      break;
-  }
-  return new Rect(x, y, element.width, element.height);
+  const {width, height} = cfg.element;
+  const coordForMajor = calcPopupPositionByMajorAxis(cfg);
+  const coordForMinor = calcPopupPositionByMinorAxis(cfg);
+  return isOnHorizontalAxis(cfg.position)
+    ? new Rect(coordForMajor, coordForMinor, width, height)
+    : new Rect(coordForMinor, coordForMajor, width, height);
 }
 
 /**
@@ -103,7 +127,7 @@ function calcBasicPosition(cfg: PopupPositionConfig): PopupPositionValue {
     x: calcArrowPosition(cfg, 'width'),
     y: calcArrowPosition(cfg, 'height'),
   };
-  return {arrow, popup, placedAt: cfg.position};
+  return {arrow, popup, placedAt: getPlacedAt(cfg)};
 }
 
 /**
@@ -128,10 +152,15 @@ function getOppositePosition(position: PositionType): PositionType {
 function fitOnMajorAxis(cfg: PopupPositionConfig, value: PopupPositionValue): PopupPositionValue {
   if (!['fit', 'fit-major'].includes(cfg.behavior)) return value;
 
-  const intersectionRatio = cfg.intersectionRatio[cfg.position] || 0;
-  const leftComparand = isStartingSide(cfg.position) ? value.popup[cfg.position] : cfg.outer[cfg.position];
-  const rightComparand = isStartingSide(cfg.position) ? cfg.outer[cfg.position] : value.popup[cfg.position];
-  const isRequireAdjusting = intersectionRatio > 0 || leftComparand < rightComparand;
+  const {position, hasInnerOrigin, outer} = cfg;
+  const intersectionRatio = cfg.intersectionRatio[hasInnerOrigin ? getOppositePosition(position) : position] || 0;
+  const valueToCheck = hasInnerOrigin ? cfg.inner : value.popup;
+  const isComingOut = isStartingSide(position)
+    ? valueToCheck[position] < outer[position]
+    : outer[position] < valueToCheck[position];
+  const isRequireAdjusting = hasInnerOrigin
+    ? intersectionRatio === 0 && isComingOut
+    : intersectionRatio > 0 || isComingOut;
 
   return isRequireAdjusting ? adjustAlongMajorAxis(cfg, value) : value;
 }
@@ -143,51 +172,13 @@ function fitOnMajorAxis(cfg: PopupPositionConfig, value: PopupPositionValue): Po
  * @returns updated popup position value
  * */
 function adjustAlongMajorAxis(cfg: PopupPositionConfig, value: PopupPositionValue): PopupPositionValue {
-  const popup = isStartingSide(cfg.position)
-    ? adjustForStartingSide(cfg, value.popup)
-    : adjustForEndingSide(cfg, value.popup);
-  const placedAt = getOppositePosition(cfg.position);
-  return {...value, popup, placedAt};
-}
-
-/**
- * Updates popup rect to fit on major axis in case positioning on the starting side.
- * @param cfg - popup position config
- * @param popup - popup rect
- * @returns updated popup rect
- * */
-function adjustForStartingSide(cfg: PopupPositionConfig, popup: Rect): Rect {
-  const {position, inner, hasInnerOrigin} = cfg;
-  let {x, y} = popup;
-  switch (position) {
-    case 'left':
-      x = hasInnerOrigin ? inner.x : inner.right;
-      break;
-    case 'top':
-      y = hasInnerOrigin ? inner.y : inner.bottom;
-      break;
-  }
-  return new Rect(x, y, popup.width, popup.height);
-}
-
-/**
- * Updates popup rect to fit on major axis in case positioning on the ending side.
- * @param cfg - popup position config
- * @param popup - popup rect
- * @returns updated popup rect
- * */
-function adjustForEndingSide(cfg: PopupPositionConfig, popup: Rect): Rect {
-  const {position, inner, hasInnerOrigin} = cfg;
-  let {x, y} = popup;
-  switch (position) {
-    case 'right':
-      x = (hasInnerOrigin ? inner.right : inner.x) - popup.width;
-      break;
-    case 'bottom':
-      y = (hasInnerOrigin ? inner.bottom : inner.y) - popup.height;
-      break;
-  }
-  return new Rect(x, y, popup.width, popup.height);
+  const oppositeConfig = {...cfg, position: getOppositePosition(cfg.position)};
+  const {x, y, width, height} = value.popup;
+  const adjustedCoord = calcPopupPositionByMajorAxis(oppositeConfig);
+  const popup = isOnHorizontalAxis(cfg.position)
+    ? new Rect(adjustedCoord, y, width, height)
+    : new Rect(x, adjustedCoord, width, height);
+  return {...value, popup, placedAt: getPlacedAt(cfg, true)};
 }
 
 /**
@@ -204,7 +195,7 @@ function adjustAlignmentBySide(cfg: PopupPositionConfig, diffCoord: number, arro
   if (isStart ? diffCoord < 0 : diffCoord > 0) {
     arrowAdjust = diffCoord;
     const newCoord = arrowCoord + arrowAdjust;
-    const dimension = isOnHorizontalAxis(cfg.position) ? 'height' : 'width';
+    const dimension = getDimensionName(cfg.position, true);
     const arrowLimit = cfg.marginArrow + (isStart ? 0 : calcUsableSizeForArrow(cfg, dimension));
     if (isStart ? newCoord < arrowLimit : newCoord > arrowLimit) {
       arrowAdjust -= newCoord - arrowLimit;
@@ -224,7 +215,7 @@ function setupAlignmentBySide(cfg: PopupPositionConfig, popup: Rect): PopupAlign
   const isHorizontal = isOnHorizontalAxis(cfg.position);
   const start = isHorizontal ? 'y' : 'x';
   const end = isHorizontal ? 'bottom' : 'right';
-  const dimension = isHorizontal ? 'height' : 'width';
+  const dimension = getDimensionName(cfg.position, true);
   const isOutAtStart = popup[start] < cfg.outer[start];
   const isOutAtEnd = popup[end] > cfg.outer[end];
   const isWider = cfg.outer[dimension] < cfg.element[dimension];
@@ -269,7 +260,7 @@ function fitOnMinorAxis(cfg: PopupPositionConfig, value: PopupPositionValue): Po
  * @param cfg - popup position config
  * @param dimensionName - the name of dimension (height or width)
  */
-function calcUsableSizeForArrow(cfg: PopupPositionConfig, dimensionName: 'height' | 'width'): number {
+function calcUsableSizeForArrow(cfg: PopupPositionConfig, dimensionName: DimensionNameType): number {
   return cfg.element[dimensionName] - cfg.arrow[dimensionName] - 2 * cfg.marginArrow;
 }
 
@@ -278,7 +269,7 @@ function calcUsableSizeForArrow(cfg: PopupPositionConfig, dimensionName: 'height
  * @param cfg - popup position config
  * @param dimensionName - the name of dimension (height or width)
  */
-function calcArrowPosition(cfg: PopupPositionConfig, dimensionName: 'height' | 'width'): number {
+function calcArrowPosition(cfg: PopupPositionConfig, dimensionName: DimensionNameType): number {
   return cfg.marginArrow + calcUsableSizeForArrow(cfg, dimensionName) * cfg.offsetArrowRatio;
 }
 
diff --git a/src/modules/esl-popup/core/esl-popup.less b/src/modules/esl-popup/core/esl-popup.less
index 69170d09f..2b40ff8ab 100644
--- a/src/modules/esl-popup/core/esl-popup.less
+++ b/src/modules/esl-popup/core/esl-popup.less
@@ -50,7 +50,7 @@ esl-popup:not(.esl-popup) {
     background: inherit;
   }
 
-  &[placed-at='top'] {
+  &:is([placed-at='top'], [placed-at='bottom-inner']) {
     .esl-popup-arrow {
       top: auto;
       bottom: calc(-1 * var(--_half) - var(--_border-width));
@@ -58,7 +58,7 @@ esl-popup:not(.esl-popup) {
     }
   }
 
-  &[placed-at='left'] {
+  &:is([placed-at='left'], [placed-at='right-inner']) {
     .esl-popup-arrow {
       right: calc(-1 * var(--_half) - var(--_border-width));
       left: auto;
@@ -67,7 +67,7 @@ esl-popup:not(.esl-popup) {
     }
   }
 
-  &[placed-at='right'] {
+  &:is([placed-at='right'], [placed-at='left-inner']) {
     .esl-popup-arrow {
       left: calc(-1 * var(--_half) - var(--_border-width) - var(--_shift));
       right: auto;
diff --git a/src/modules/esl-popup/core/esl-popup.mixin.less b/src/modules/esl-popup/core/esl-popup.mixin.less
index 19d86dabf..365384f12 100644
--- a/src/modules/esl-popup/core/esl-popup.mixin.less
+++ b/src/modules/esl-popup/core/esl-popup.mixin.less
@@ -22,7 +22,7 @@
     background: @popupBackgroundColor;
   }
 
-  &[placed-at='top'] {
+  &:is([placed-at='top'], [placed-at='bottom-inner']) {
     .@{arrowClassName} {
       top: auto;
       bottom: (-@popupArrowHalf - @popupBorderWidth);
@@ -30,7 +30,7 @@
     }
   }
 
-  &[placed-at='left'] {
+  &:is([placed-at='left'], [placed-at='right-inner']) {
     .@{arrowClassName} {
       right: (-@popupArrowHalf - @popupBorderWidth);
       left: auto;
@@ -39,7 +39,7 @@
     }
   }
 
-  &[placed-at='right'] {
+  &:is([placed-at='right'], [placed-at='left-inner']) {
     .@{arrowClassName} {
       left: (-@popupArrowHalf - @popupBorderWidth - @popupArrowShift);
       right: auto;
diff --git a/src/modules/esl-popup/core/esl-popup.shape.ts b/src/modules/esl-popup/core/esl-popup.shape.ts
index f3a86d3fb..b764902de 100644
--- a/src/modules/esl-popup/core/esl-popup.shape.ts
+++ b/src/modules/esl-popup/core/esl-popup.shape.ts
@@ -19,11 +19,15 @@ export interface ESLPopupTagShape<T extends ESLPopup = ESLPopup> extends ESLTogg
   'margin-arrow'?: string;
   /** Offset of the arrow as a percentage of the popup edge (0% - at the left edge, 100% - at the right edge, for RTL it is vice versa) */
   'offset-arrow'?: string;
+  /** Offset from the trigger element */
+  'offset-trigger'?: string;
   /**
    * Popup position relative to the trigger.
    * Currently supported: 'top', 'bottom', 'left', 'right' position types ('top' by default)
    */
   position?: PositionType;
+  /** clarification of the popup position, whether it should start on the outside of trigger or the inside of trigger ('outer' by default) */
+  'position-origin'?: 'inner' | 'outer';
 
   /** Default params to merge into passed action params */
   'default-params'?: string;
diff --git a/src/modules/esl-popup/core/esl-popup.ts b/src/modules/esl-popup/core/esl-popup.ts
index d305e7e63..645581bd7 100644
--- a/src/modules/esl-popup/core/esl-popup.ts
+++ b/src/modules/esl-popup/core/esl-popup.ts
@@ -12,7 +12,7 @@ import {calcPopupPosition, isOnHorizontalAxis} from './esl-popup-position';
 import {ESLPopupPlaceholder} from './esl-popup-placeholder';
 
 import type {ESLToggleableActionParams, ESLFocusFlowType} from '../../esl-toggleable/core';
-import type {PositionType, PositionOriginType, IntersectionRatioRect} from './esl-popup-position';
+import type {PopupPositionConfig, PositionType, PositionOriginType, IntersectionRatioRect} from './esl-popup-position';
 
 const INTERSECTION_LIMIT_FOR_ADJACENT_AXIS = 0.7;
 const DEFAULT_OFFSET_ARROW = 50;
@@ -28,17 +28,17 @@ export interface ESLPopupActionParams extends ESLToggleableActionParams {
   disableActivatorObservation?: boolean;
   /** Margins on the edges of the arrow. */
   marginArrow?: number;
-  /** offset of the arrow as a percentage of the popup edge (0% - at the left edge, 100% - at the right edge, for RTL it is vice versa) */
+  /** Offset of the arrow as a percentage of the popup edge (0% - at the left edge, 100% - at the right edge, for RTL it is vice versa) */
   offsetArrow?: string;
-  /** offset in pixels from trigger element */
+  /** Offset in pixels from the trigger element */
   offsetTrigger?: number;
   /**
-   * offset in pixels from the edges of the container (or window if the container is not defined)
+   * Offset in pixels from the edges of the container (or window if the container is not defined)
    *  value as a number for equals x and y offsets
    *  value as an array for different x and y offsets
    */
   offsetContainer?: number | [number, number];
-  /** margin around the element that is used as the viewport for checking the visibility of the popup activator */
+  /** Margin around the element that is used as the viewport for checking the visibility of the popup activator */
   intersectionMargin?: string;
   /** Target to container element to define bounds of popups visibility */
   container?: string;
@@ -60,7 +60,6 @@ export class ESLPopup extends ESLToggleable {
 
   /** Default params to pass into the popup on show/hide actions */
   public static override DEFAULT_PARAMS: ESLPopupActionParams = {
-    offsetTrigger: 3,
     offsetContainer: 15,
     intersectionMargin: '0px'
   };
@@ -97,6 +96,9 @@ export class ESLPopup extends ESLToggleable {
    *  for RTL it is vice versa) */
   @attr({defaultValue: `${DEFAULT_OFFSET_ARROW}`}) public offsetArrow: string;
 
+  /** offset in pixels from the trigger element */
+  @attr({defaultValue: 3, parser: parseInt}) public offsetTrigger: number;
+
   /** Target to container element {@link ESLTraversingQuery} to define bounds of popups visibility (window by default) */
   @attr() public container: string;
 
@@ -125,7 +127,6 @@ export class ESLPopup extends ESLToggleable {
   protected _extraStyle?: string;
 
   protected _containerEl?: HTMLElement;
-  protected _offsetTrigger: number;
   protected _offsetContainer: number | [number, number];
   protected _intersectionMargin: string;
   protected _intersectionRatio: IntersectionRatioRect = {};
@@ -220,13 +221,13 @@ export class ESLPopup extends ESLToggleable {
       container: params.container,
       marginArrow: params.marginArrow,
       offsetArrow: params.offsetArrow,
+      offsetTrigger: params.offsetTrigger,
       disableActivatorObservation: params.disableActivatorObservation
     }));
 
     this._extraClass = params.extraClass;
     this._extraStyle = params.extraStyle;
     this._containerEl = params.containerEl;
-    this._offsetTrigger = params.offsetTrigger || 0;
     this._offsetContainer = params.offsetContainer || 0;
     this._intersectionMargin = params.intersectionMargin || '0px';
 
@@ -402,18 +403,15 @@ export class ESLPopup extends ESLToggleable {
     this._updateLoopID = 0;
   }
 
-  /** Updates position of popup and its arrow */
-  protected _updatePosition(): void {
-    if (!this.activator) return;
-
+  protected get positionConfig(): PopupPositionConfig {
     const popupRect = Rect.from(this);
     const arrowRect = this.$arrow ? Rect.from(this.$arrow) : new Rect();
-    const triggerRect = Rect.from(this.activator).shift(window.scrollX, window.scrollY);
+    const triggerRect = this.activator ? Rect.from(this.activator).shift(window.scrollX, window.scrollY) : new Rect();
     const {containerRect} = this;
 
-    const innerMargin = this._offsetTrigger + (this.positionOrigin === 'inner' ? 0 : arrowRect.width / 2);
+    const innerMargin = this.offsetTrigger + arrowRect.width / 2;
 
-    const config = {
+    return {
       position: this.position,
       hasInnerOrigin: this.positionOrigin === 'inner',
       behavior: this.behavior,
@@ -423,14 +421,19 @@ export class ESLPopup extends ESLToggleable {
       arrow: arrowRect,
       element: popupRect,
       trigger: triggerRect,
-      inner: triggerRect.grow(innerMargin),
+      inner: this.positionOrigin === 'inner' ? triggerRect.shrink(innerMargin) : triggerRect.grow(innerMargin),
       outer: (typeof this._offsetContainer === 'number') ?
         containerRect.shrink(this._offsetContainer) :
         containerRect.shrink(...this._offsetContainer),
       isRTL: isRTL(this)
     };
+  }
+
+  /** Updates position of popup and its arrow */
+  protected _updatePosition(): void {
+    if (!this.activator) return;
 
-    const {placedAt, popup, arrow} = calcPopupPosition(config);
+    const {placedAt, popup, arrow} = calcPopupPosition(this.positionConfig);
 
     this.setAttribute('placed-at', placedAt);
     // set popup position


Test patch:
diff --git a/src/modules/esl-popup/test/esl-popup-position.behaviour-fit-major..origin-inner.test.ts b/src/modules/esl-popup/test/esl-popup-position.behaviour-fit-major..origin-inner.test.ts
new file mode 100644
index 000000000..6cbb32b2b
--- /dev/null
+++ b/src/modules/esl-popup/test/esl-popup-position.behaviour-fit-major..origin-inner.test.ts
@@ -0,0 +1,116 @@
+import {calcPopupPosition} from '../core/esl-popup-position';
+import {Rect} from '../../esl-utils/dom';
+
+import type {PopupPositionConfig, PopupPositionValue} from '../core/esl-popup-position';
+
+describe('ESLPopup position: calcPopupPosition(): behavior set to fit-major and origin set to inner', () => {
+  const arrow = new Rect(0, 0, 30, 30);
+  const popup = new Rect(0, 0, 200, 200);
+  const trigger = new Rect(250, 250, 500, 500);
+  const container = new Rect(0, 0, 1000, 1000);
+  const cfgRef = {
+    behavior: 'fit-major',
+    position: 'top',
+    hasInnerOrigin: true,
+    marginArrow: 10,
+    offsetArrowRatio: 0.5,
+    intersectionRatio: {},
+    arrow,
+    element: popup,
+    inner: trigger.shrink(25),
+    outer: container,
+    trigger
+  } as PopupPositionConfig;
+
+  const expectedRef = {
+    arrow: {x: 85, y: 85},
+    placedAt: 'top-inner',
+    popup
+  };
+
+  describe('should flip to the opposite position:', () => {
+    test('when there is a lack of space at the top', () => {
+      const cfg = {...cfgRef, position: 'top', outer: container.shift(0, 400)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(400, 525);
+      expected.placedAt = 'bottom-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when there is a lack of space at the left', () => {
+      const cfg = {...cfgRef, position: 'left', outer: container.shift(400, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(525, 400);
+      expected.placedAt = 'right-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when there is a lack of space at the bottom', () => {
+      const cfg = {...cfgRef, position: 'bottom', outer: container.shift(0, -400)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(400, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when there is a lack of space at the right', () => {
+      const cfg = {...cfgRef, position: 'right', outer: container.shift(-400, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(275, 400);
+      expected.placedAt = 'left-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+  });
+
+  describe('should not flip to the opposite position if the activator intersects the container by the opposite side:', () => {
+    test('when there is a lack of space at the top', () => {
+      const cfg = {
+        ...cfgRef,
+        position: 'top',
+        outer: container.shift(0, 400),
+        intersectionRatio: {bottom: 0.1}
+      } as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(400, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when there is a lack of space at the left', () => {
+      const cfg = {
+        ...cfgRef,
+        position: 'left',
+        outer: container.shift(400, 0),
+        intersectionRatio: {right: 0.1}
+      } as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(275, 400);
+      expected.placedAt = 'left-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when there is a lack of space at the bottom', () => {
+      const cfg = {
+        ...cfgRef,
+        position: 'bottom',
+        outer: container.shift(0, -400),
+        intersectionRatio: {top: 0.1}
+      } as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(400, 525);
+      expected.placedAt = 'bottom-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when there is a lack of space at the right', () => {
+      const cfg = {
+        ...cfgRef,
+        position: 'right',
+        outer: container.shift(-400, 0),
+        intersectionRatio: {left: 0.1}
+      } as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(525, 400);
+      expected.placedAt = 'right-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+  });
+});
diff --git a/src/modules/esl-popup/test/esl-popup-position.behaviour-fit-minor.origin-inner.test.ts b/src/modules/esl-popup/test/esl-popup-position.behaviour-fit-minor.origin-inner.test.ts
new file mode 100644
index 000000000..d55290d97
--- /dev/null
+++ b/src/modules/esl-popup/test/esl-popup-position.behaviour-fit-minor.origin-inner.test.ts
@@ -0,0 +1,244 @@
+import {calcPopupPosition} from '../core/esl-popup-position';
+import {Rect} from '../../esl-utils/dom';
+
+import type {PopupPositionConfig, PopupPositionValue} from '../core/esl-popup-position';
+
+describe('ESLPopup position: calcPopupPosition(): behavior set to fit-minor and origin set to inner', () => {
+  const arrow = new Rect(0, 0, 30, 30);
+  const popup = new Rect(0, 0, 200, 200);
+  const trigger = new Rect(250, 250, 500, 500);
+  const container = new Rect(0, 0, 1000, 1000);
+  const cfgRef = {
+    behavior: 'fit-minor',
+    position: 'top',
+    hasInnerOrigin: true,
+    marginArrow: 10,
+    offsetArrowRatio: 0.5,
+    intersectionRatio: {},
+    arrow,
+    element: popup,
+    inner: trigger.shrink(25),
+    outer: container,
+    trigger
+  } as PopupPositionConfig;
+
+  const expectedRef = {
+    arrow: {x: 85, y: 85},
+    placedAt: 'top-inner',
+    popup
+  };
+
+  describe('should not adjust when there is enough space for the popup:', () => {
+    test('on the top position', () => {
+      const cfg = {...cfgRef, position: 'top'} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(400, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('on the bottom position', () => {
+      const cfg = {...cfgRef, position: 'bottom'} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(400, 525);
+      expected.placedAt = 'bottom-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('on the left position', () => {
+      const cfg = {...cfgRef, position: 'left'} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(275, 400);
+      expected.placedAt = 'left-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('on the right position', () => {
+      const cfg = {...cfgRef, position: 'right'} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(525, 400);
+      expected.placedAt = 'right-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+  });
+
+  describe('should adjust the position by moving to:', () => {
+    test('left when there is a lack of space on the right side in the top position', () => {
+      const cfg = {...cfgRef, position: 'top', outer: container.shift(-440, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 125, y: 85};
+      expected.popup = popup.shift(360, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('right when there is a lack of space on the left side in the top position', () => {
+      const cfg = {...cfgRef, position: 'top', outer: container.shift(440, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 45, y: 85};
+      expected.popup = popup.shift(440, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('left when there is a lack of space on the right side in the bottom position', () => {
+      const cfg = {...cfgRef, position: 'bottom', outer: container.shift(-440, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 125, y: 85};
+      expected.popup = popup.shift(360, 525);
+      expected.placedAt = 'bottom-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('right when there is a lack of space on the left side in the bottom position', () => {
+      const cfg = {...cfgRef, position: 'bottom', outer: container.shift(440, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 45, y: 85};
+      expected.popup = popup.shift(440, 525);
+      expected.placedAt = 'bottom-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+  });
+
+  describe('should adjust when the popup is outing both sides of the container', () => {
+    test('when the popup has a position on the vertical axis and has LTR text direction', () => {
+      const cfg = {...cfgRef, position: 'top', element: popup.resize(1000, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 485, y: 85};
+      expected.popup = popup.shift(0, 275).resize(1000, 0);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+    test('when the popup has a position on the vertical axis and has RTL text direction', () => {
+      const cfg = {...cfgRef, position: 'top', element: popup.resize(1000, 0), isRTL: true} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 685, y: 85};
+      expected.popup = popup.shift(-200, 275).resize(1000, 0);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+    test('when the popup has a position on the horizontal axis and has LTR text direction', () => {
+      const cfg = {...cfgRef, position: 'right', element: popup.resize(0, 1000)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 85, y: 485};
+      expected.popup = popup.shift(525, 0).resize(0, 1000);
+      expected.placedAt = 'right-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+    test('when the popup has a position on the horizontal axis and has RTL text direction', () => {
+      const cfg = {...cfgRef, position: 'right', element: popup.resize(0, 1000), isRTL: true} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 85, y: 685};
+      expected.popup = popup.shift(525, -200).resize(0, 1000);
+      expected.placedAt = 'right-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+  });
+
+  describe('should adjust when popup is outing at one side of the container and has size greater than the container', () => {
+    describe('should stick to the initial side of the container in case LTR text direction', () => {
+      test('when the popup has a position on the vertical axis and crosses the initial side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'top',
+          element: popup.resize(1000, 0),
+          outer: container.shift(400, 0)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 85, y: 85};
+        expected.popup = popup.shift(400, 275).resize(1000, 0);
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+      test('when the popup has a position on the vertical axis and crosses the final side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'top',
+          element: popup.resize(1000, 0),
+          outer: container.shift(-400, 0)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 885, y: 85};
+        expected.popup = popup.shift(-400, 275).resize(1000, 0);
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+      test('when the popup has a position on the horizontal axis and crosses the initial side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'right',
+          element: popup.resize(0, 1000),
+          outer: container.shift(0, 400)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 85, y: 85};
+        expected.popup = popup.shift(525, 400).resize(0, 1000);
+        expected.placedAt = 'right-inner';
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+      test('when the popup has a position on the horizontal axis and crosses the final side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'right',
+          element: popup.resize(0, 1000),
+          outer: container.shift(0, -400)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 85, y: 885};
+        expected.popup = popup.shift(525, -400).resize(0, 1000);
+        expected.placedAt = 'right-inner';
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+    });
+
+    describe('should stick to the final side of the container in case RTL text direction', () => {
+      test('when the popup has a position on the vertical axis and crosses the initial side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'top',
+          isRTL: true,
+          element: popup.resize(1000, 0),
+          outer: container.shift(400, 0)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 285, y: 85};
+        expected.popup = popup.shift(200, 275).resize(1000, 0);
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+      test('when the popup has a position on the vertical axis and crosses the final side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'top',
+          isRTL: true,
+          element: popup.resize(1000, 0),
+          outer: container.shift(-400, 0)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 1085, y: 85};
+        expected.popup = popup.shift(-600, 275).resize(1000, 0);
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+      test('when the popup has a position on the horizontal axis and crosses the initial side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'right',
+          isRTL: true,
+          element: popup.resize(0, 1000),
+          outer: container.shift(0, 400)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 85, y: 285};
+        expected.popup = popup.shift(525, 200).resize(0, 1000);
+        expected.placedAt = 'right-inner';
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+      test('when the popup has a position on the horizontal axis and crosses the final side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'right',
+          isRTL: true,
+          element: popup.resize(0, 1000),
+          outer: container.shift(0, -400)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 85, y: 1085};
+        expected.popup = popup.shift(525, -600).resize(0, 1000);
+        expected.placedAt = 'right-inner';
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+    });
+  });
+});
diff --git a/src/modules/esl-popup/test/esl-popup-position.behaviour-fit.origin-inner.test.ts b/src/modules/esl-popup/test/esl-popup-position.behaviour-fit.origin-inner.test.ts
new file mode 100644
index 000000000..cee61bc0d
--- /dev/null
+++ b/src/modules/esl-popup/test/esl-popup-position.behaviour-fit.origin-inner.test.ts
@@ -0,0 +1,330 @@
+import {calcPopupPosition} from '../core/esl-popup-position';
+import {Rect} from '../../esl-utils/dom';
+
+import type {PopupPositionConfig, PopupPositionValue} from '../core/esl-popup-position';
+
+describe('ESLPopup position: calcPopupPosition(): behavior set to fit-major', () => {
+  const arrow = new Rect(0, 0, 30, 30);
+  const popup = new Rect(0, 0, 200, 200);
+  const trigger = new Rect(250, 250, 500, 500);
+  const container = new Rect(0, 0, 1000, 1000);
+  const cfgRef = {
+    behavior: 'fit',
+    position: 'top',
+    hasInnerOrigin: true,
+    marginArrow: 10,
+    offsetArrowRatio: 0.5,
+    intersectionRatio: {},
+    arrow,
+    element: popup,
+    inner: trigger.shrink(25),
+    outer: container,
+    trigger
+  } as PopupPositionConfig;
+
+  const expectedRef = {
+    arrow: {x: 85, y: 85},
+    placedAt: 'top-inner',
+    popup
+  };
+
+  describe('should flip to the opposite position:', () => {
+    test('when there is a lack of space at the top', () => {
+      const cfg = {...cfgRef, position: 'top', outer: container.shift(0, 400)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(400, 525);
+      expected.placedAt = 'bottom-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when there is a lack of space at the left', () => {
+      const cfg = {...cfgRef, position: 'left', outer: container.shift(400, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(525, 400);
+      expected.placedAt = 'right-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when there is a lack of space at the bottom', () => {
+      const cfg = {...cfgRef, position: 'bottom', outer: container.shift(0, -400)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(400, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when there is a lack of space at the right', () => {
+      const cfg = {...cfgRef, position: 'right', outer: container.shift(-400, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(275, 400);
+      expected.placedAt = 'left-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+  });
+
+  describe('should not flip to the opposite position if the activator intersects the container by the opposite side:', () => {
+    test('when there is a lack of space at the top', () => {
+      const cfg = {
+        ...cfgRef,
+        position: 'top',
+        outer: container.shift(0, 400),
+        intersectionRatio: {bottom: 0.1}
+      } as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(400, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when there is a lack of space at the left', () => {
+      const cfg = {
+        ...cfgRef,
+        position: 'left',
+        outer: container.shift(400, 0),
+        intersectionRatio: {right: 0.1}
+      } as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(275, 400);
+      expected.placedAt = 'left-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when there is a lack of space at the bottom', () => {
+      const cfg = {
+        ...cfgRef,
+        position: 'bottom',
+        outer: container.shift(0, -400),
+        intersectionRatio: {top: 0.1}
+      } as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(400, 525);
+      expected.placedAt = 'bottom-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when there is a lack of space at the right', () => {
+      const cfg = {
+        ...cfgRef,
+        position: 'right',
+        outer: container.shift(-400, 0),
+        intersectionRatio: {left: 0.1}
+      } as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(525, 400);
+      expected.placedAt = 'right-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+  });
+
+  describe('should not adjust when there is enough space for the popup:', () => {
+    test('on the top position', () => {
+      const cfg = {...cfgRef, position: 'top'} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(400, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('on the bottom position', () => {
+      const cfg = {...cfgRef, position: 'bottom'} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(400, 525);
+      expected.placedAt = 'bottom-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('on the left position', () => {
+      const cfg = {...cfgRef, position: 'left'} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(275, 400);
+      expected.placedAt = 'left-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('on the right position', () => {
+      const cfg = {...cfgRef, position: 'right'} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(525, 400);
+      expected.placedAt = 'right-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+  });
+
+  describe('should adjust the position by moving to:', () => {
+    test('left when there is a lack of space on the right side in the top position', () => {
+      const cfg = {...cfgRef, position: 'top', outer: container.shift(-440, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 125, y: 85};
+      expected.popup = popup.shift(360, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('right when there is a lack of space on the left side in the top position', () => {
+      const cfg = {...cfgRef, position: 'top', outer: container.shift(440, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 45, y: 85};
+      expected.popup = popup.shift(440, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('left when there is a lack of space on the right side in the bottom position', () => {
+      const cfg = {...cfgRef, position: 'bottom', outer: container.shift(-440, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 125, y: 85};
+      expected.popup = popup.shift(360, 525);
+      expected.placedAt = 'bottom-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('right when there is a lack of space on the left side in the bottom position', () => {
+      const cfg = {...cfgRef, position: 'bottom', outer: container.shift(440, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 45, y: 85};
+      expected.popup = popup.shift(440, 525);
+      expected.placedAt = 'bottom-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+  });
+
+  describe('should adjust when the popup is outing both sides of the container', () => {
+    test('when the popup has a position on the vertical axis and has LTR text direction', () => {
+      const cfg = {...cfgRef, position: 'top', element: popup.resize(1000, 0)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 485, y: 85};
+      expected.popup = popup.shift(0, 275).resize(1000, 0);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+    test('when the popup has a position on the vertical axis and has RTL text direction', () => {
+      const cfg = {...cfgRef, position: 'top', element: popup.resize(1000, 0), isRTL: true} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 685, y: 85};
+      expected.popup = popup.shift(-200, 275).resize(1000, 0);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+    test('when the popup has a position on the horizontal axis and has LTR text direction', () => {
+      const cfg = {...cfgRef, position: 'right', element: popup.resize(0, 1000)} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 85, y: 485};
+      expected.popup = popup.shift(525, 0).resize(0, 1000);
+      expected.placedAt = 'right-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+    test('when the popup has a position on the horizontal axis and has RTL text direction', () => {
+      const cfg = {...cfgRef, position: 'right', element: popup.resize(0, 1000), isRTL: true} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 85, y: 685};
+      expected.popup = popup.shift(525, -200).resize(0, 1000);
+      expected.placedAt = 'right-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+  });
+
+  describe('should adjust when popup is outing at one side of the container and has size greater than the container', () => {
+    describe('should stick to the initial side of the container in case LTR text direction', () => {
+      test('when the popup has a position on the vertical axis and crosses the initial side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'top',
+          element: popup.resize(1000, 0),
+          outer: container.shift(400, 0)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 85, y: 85};
+        expected.popup = popup.shift(400, 275).resize(1000, 0);
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+      test('when the popup has a position on the vertical axis and crosses the final side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'top',
+          element: popup.resize(1000, 0),
+          outer: container.shift(-400, 0)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 885, y: 85};
+        expected.popup = popup.shift(-400, 275).resize(1000, 0);
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+      test('when the popup has a position on the horizontal axis and crosses the initial side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'right',
+          element: popup.resize(0, 1000),
+          outer: container.shift(0, 400)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 85, y: 85};
+        expected.popup = popup.shift(525, 400).resize(0, 1000);
+        expected.placedAt = 'right-inner';
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+      test('when the popup has a position on the horizontal axis and crosses the final side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'right',
+          element: popup.resize(0, 1000),
+          outer: container.shift(0, -400)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 85, y: 885};
+        expected.popup = popup.shift(525, -400).resize(0, 1000);
+        expected.placedAt = 'right-inner';
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+    });
+
+    describe('should stick to the final side of the container in case RTL text direction', () => {
+      test('when the popup has a position on the vertical axis and crosses the initial side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'top',
+          isRTL: true,
+          element: popup.resize(1000, 0),
+          outer: container.shift(400, 0)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 285, y: 85};
+        expected.popup = popup.shift(200, 275).resize(1000, 0);
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+      test('when the popup has a position on the vertical axis and crosses the final side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'top',
+          isRTL: true,
+          element: popup.resize(1000, 0),
+          outer: container.shift(-400, 0)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 1085, y: 85};
+        expected.popup = popup.shift(-600, 275).resize(1000, 0);
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+      test('when the popup has a position on the horizontal axis and crosses the initial side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'right',
+          isRTL: true,
+          element: popup.resize(0, 1000),
+          outer: container.shift(0, 400)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 85, y: 285};
+        expected.popup = popup.shift(525, 200).resize(0, 1000);
+        expected.placedAt = 'right-inner';
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+      test('when the popup has a position on the horizontal axis and crosses the final side', () => {
+        const cfg = {
+          ...cfgRef,
+          position: 'right',
+          isRTL: true,
+          element: popup.resize(0, 1000),
+          outer: container.shift(0, -400)
+        } as PopupPositionConfig;
+        const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+        expected.arrow = {x: 85, y: 1085};
+        expected.popup = popup.shift(525, -600).resize(0, 1000);
+        expected.placedAt = 'right-inner';
+        expect(calcPopupPosition(cfg)).toEqual(expected);
+      });
+    });
+  });
+});
diff --git a/src/modules/esl-popup/test/esl-popup-position.behaviour-none.origin-inner.test.ts b/src/modules/esl-popup/test/esl-popup-position.behaviour-none.origin-inner.test.ts
new file mode 100644
index 000000000..c6f36c1b8
--- /dev/null
+++ b/src/modules/esl-popup/test/esl-popup-position.behaviour-none.origin-inner.test.ts
@@ -0,0 +1,89 @@
+import {calcPopupPosition} from '../core/esl-popup-position';
+import {Rect} from '../../esl-utils/dom';
+
+import type {PopupPositionConfig, PopupPositionValue} from '../core/esl-popup-position';
+
+describe('ESLPopup position: calcPopupPosition(): behavior set to none and origin set to inner', () => {
+  const arrow = new Rect(0, 0, 30, 30);
+  const popup = new Rect(0, 0, 200, 200);
+  const trigger = new Rect(250, 250, 500, 500);
+  const container = new Rect(0, 0, 1000, 1000);
+  const cfgRef = {
+    behavior: 'none',
+    position: 'top',
+    hasInnerOrigin: true,
+    marginArrow: 10,
+    offsetArrowRatio: 0,
+    intersectionRatio: {},
+    arrow,
+    element: popup,
+    inner: trigger.shrink(25),
+    outer: container,
+    trigger
+  } as PopupPositionConfig;
+
+  const expectedRef = {
+    arrow: {x: 10, y: 10},
+    placedAt: 'top-inner',
+    popup
+  };
+
+  describe('should calc position without adjustments:', () => {
+    test('when the popup placed at the top', () => {
+      const cfg = {...cfgRef, position: 'top'} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(475, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when the popup placed at the left', () => {
+      const cfg = {...cfgRef, position: 'left'} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(275, 475);
+      expected.placedAt = 'left-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when the popup placed at the bottom', () => {
+      const cfg = {...cfgRef, position: 'bottom'} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(475, 525);
+      expected.placedAt = 'bottom-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when the popup placed at the right', () => {
+      const cfg = {...cfgRef, position: 'right'} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(525, 475);
+      expected.placedAt = 'right-inner';
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+  });
+
+  describe('should calc arrow position:', () => {
+
+    test('when arrow at the start edge', () => {
+      const cfg = {...cfgRef, offsetArrowRatio: 0} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.popup = popup.shift(475, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when arrow at the end edge', () => {
+      const cfg = {...cfgRef, offsetArrowRatio: 1} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 160, y: 160};
+      expected.popup = popup.shift(325, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+
+    test('when arrow at the middle', () => {
+      const cfg = {...cfgRef, offsetArrowRatio: 0.5} as PopupPositionConfig;
+      const expected = Object.assign({}, expectedRef) as PopupPositionValue;
+      expected.arrow = {x: 85, y: 85};
+      expected.popup = popup.shift(400, 275);
+      expect(calcPopupPosition(cfg)).toEqual(expected);
+    });
+  });
+});
diff --git a/src/modules/esl-popup/test/esl-popup.position-config.test.ts b/src/modules/esl-popup/test/esl-popup.position-config.test.ts
new file mode 100644
index 000000000..9765af993
--- /dev/null
+++ b/src/modules/esl-popup/test/esl-popup.position-config.test.ts
@@ -0,0 +1,106 @@
+import {ESLPopup, type ESLPopupActionParams} from '../core/esl-popup';
+import {Rect} from '../../esl-utils/dom/rect';
+import {createPopupMock, createDimensional} from './popup.mock';
+
+describe('ESLPopup: position config merging logic', () => {
+  let $popup: ESLPopup;
+
+  ESLPopup.register();
+  const $container = createDimensional(new Rect(0, 0, 1000, 1000));
+  const $trigger = createDimensional(new Rect(330, 330, 330, 330));
+  const $arrow = createDimensional(new Rect(20, 20, 30, 30));
+  $arrow.className = 'esl-popup-arrow';
+
+  const paramsRef: ESLPopupActionParams = {
+    activator: $trigger,
+    containerEl: $container
+  };
+  const configRef = {
+    arrow: new Rect(20, 20, 30, 30),
+    behavior: 'fit',
+    element: new Rect(20, 20, 100, 100),
+    hasInnerOrigin: false,
+    inner: new Rect(312, 312, 366, 366),
+    intersectionRatio: {},
+    isRTL: false,
+    marginArrow: 5,
+    offsetArrowRatio: 0.5,
+    outer: new Rect(15, 15, 970, 970),
+    position: 'top',
+    trigger: new Rect(330, 330, 330, 330)
+  };
+
+  beforeEach(() => {
+    $popup = createPopupMock(new Rect(20, 20, 100, 100));
+    $popup.appendChild($arrow);
+  });
+
+  test('should return the correct positionConfig with default params', () => {
+    $popup.show(paramsRef);
+    expect(($popup as any).positionConfig).toEqual(configRef);
+  });
+
+  test('should return the correct positionConfig for the specified position', () => {
+    $popup.show({...paramsRef, position: 'bottom'});
+    expect(($popup as any).positionConfig).toEqual({...configRef, position: 'bottom'});
+  });
+
+  test('should return the correct positionConfig for the inner position origin', () => {
+    $popup.show({...paramsRef, positionOrigin: 'inner'});
+    expect(($popup as any).positionConfig).toEqual({
+      ...configRef,
+      hasInnerOrigin: true,
+      inner: new Rect(348, 348, 294, 294)
+    });
+  });
+
+  test('should return the correct positionConfig for the outer position origin', () => {
+    $popup.show({...paramsRef, positionOrigin: 'outer'});
+    expect(($popup as any).positionConfig).toEqual(configRef);
+  });
+
+  test('should return the correct positionConfig for the specified behavior', () => {
+    $popup.show({...paramsRef, behavior: 'fit-minor'});
+    expect(($popup as any).positionConfig).toEqual({...configRef, behavior: 'fit-minor'});
+  });
+
+  test('should return the correct positionConfig for the specified marginArrow', () => {
+    $popup.show({...paramsRef, marginArrow: 15});
+    expect(($popup as any).positionConfig).toEqual({...configRef, marginArrow: 15});
+  });
+
+  test('should return the correct positionConfig for the specified offsetArrow', () => {
+    $popup.show({...paramsRef, offsetArrow: 3.33});
+    expect(($popup as any).positionConfig).toEqual({...configRef, offsetArrowRatio: 0.0333});
+  });
+
+  test('should return the correct positionConfig for the specified offsetTrigger', () => {
+    $popup.show({...paramsRef, offsetTrigger: 13});
+    expect(($popup as any).positionConfig).toEqual({...configRef, inner: new Rect(302, 302, 386, 386)});
+  });
+
+  test('should return the correct positionConfig for the specified offsetContainer', () => {
+    $popup.show({...paramsRef, offsetContainer: [25, 5]});
+    expect(($popup as any).positionConfig).toEqual({...configRef, outer: new Rect(25, 5, 950, 990)});
+  });
+
+  test('should return the correct positionConfig with default params and disabled arrow', () => {
+    const $disabledArrow = createDimensional(new Rect(0, 0, 0, 0));
+    $disabledArrow.className = 'esl-popup-arrow';
+    $popup.replaceChildren($disabledArrow);
+    $popup.show(paramsRef);
+    expect(($popup as any).positionConfig).toEqual({
+      ...configRef,
+      arrow: new Rect(0, 0, 0, 0),
+      inner: new Rect(327, 327, 336, 336)
+    });
+  });
+
+  test('should return the correct positionConfig for the popup with RTL direction', () => {
+    const getComputedStyleSpy = jest.spyOn(window, 'getComputedStyle');
+    getComputedStyleSpy.mockImplementation(() => ({direction: 'rtl'} as CSSStyleDeclaration));
+    $popup.show(paramsRef);
+    expect(($popup as any).positionConfig).toEqual({...configRef, isRTL: true});
+    getComputedStyleSpy.mockRestore();
+  });
+});
diff --git a/src/modules/esl-popup/test/popup.mock.ts b/src/modules/esl-popup/test/popup.mock.ts
new file mode 100644
index 000000000..5fce60935
--- /dev/null
+++ b/src/modules/esl-popup/test/popup.mock.ts
@@ -0,0 +1,14 @@
+import {ESLPopup} from '../core/esl-popup';
+import type {Rect} from '../../esl-utils/dom/rect';
+
+export function createPopupMock(expectedRect: Rect): ESLPopup {
+  const popup = new ESLPopup();
+  jest.spyOn(popup, 'getBoundingClientRect').mockImplementation(() => (expectedRect as unknown) as DOMRect);
+  return popup;
+}
+
+export function createDimensional(expectedRect: Rect): HTMLElement {
+  const el = document.createElement('div');
+  jest.spyOn(el, 'getBoundingClientRect').mockImplementation(() => (expectedRect as unknown) as DOMRect);
+  return el;
+}