diff --git a/src/modules/esl-popup/core/esl-popup-position.ts b/src/modules/esl-popup/core/esl-popup-position.ts index 243068253..4a8a75037 100644 --- a/src/modules/esl-popup/core/esl-popup-position.ts +++ b/src/modules/esl-popup/core/esl-popup-position.ts @@ -1,5 +1,3 @@ -// TODO: make implemenatation immutable - import {Rect} from '../../esl-utils/dom/rect'; import type {Point} from '../../esl-utils/dom/point'; @@ -36,10 +34,18 @@ export interface PopupPositionConfig { * Checks that the position along the horizontal axis * @param position - name of position */ -export function isMajorAxisHorizontal(position: PositionType): boolean { +export function isOnHorizontalAxis(position: PositionType): boolean { return ['left', 'right'].includes(position); } +/** + * Checks whether the specified position corresponds to the starting side + * @param position - name of position + */ +function isStartingSide(position: PositionType): boolean { + return ['left', 'top'].includes(position); +} + /** * Calculates the position of the popup on the minor axis * @param cfg - popup position config @@ -51,29 +57,41 @@ function calcPopupPositionByMinorAxis(cfg: PopupPositionConfig, centerPosition: } /** - * TODO: optimize switch * Calculates Rect for given popup position config. * @param cfg - popup position config * */ function calcPopupBasicRect(cfg: PopupPositionConfig): Rect { - let x = calcPopupPositionByMinorAxis(cfg, cfg.inner.cx, 'width'); - let y = cfg.inner.y - cfg.element.height; + const {position, inner, element} = cfg; + let x = isOnHorizontalAxis(position) ? 0 : calcPopupPositionByMinorAxis(cfg, inner.cx, 'width'); + let y = isOnHorizontalAxis(position) ? calcPopupPositionByMinorAxis(cfg, inner.cy, 'height') : 0; switch (cfg.position) { case 'left': - x = cfg.inner.x - cfg.element.width; - y = calcPopupPositionByMinorAxis(cfg, cfg.inner.cy, 'height'); + x = inner.x - element.width; break; case 'right': - x = cfg.inner.right; - y = calcPopupPositionByMinorAxis(cfg, cfg.inner.cy, 'height'); + x = inner.right; break; case 'bottom': - x = calcPopupPositionByMinorAxis(cfg, cfg.inner.cx, 'width'); - y = cfg.inner.bottom; + y = inner.bottom; + break; + default: + y = inner.y - element.height; break; } + return new Rect(x, y, element.width, element.height); +} - return new Rect(x, y, cfg.element.width, cfg.element.height); +/** + * Calculates position for all sub-parts of popup for given popup position config. + * @param cfg - popup position config + * */ +function calcBasicPosition(cfg: PopupPositionConfig): PopupPositionValue { + const popup = calcPopupBasicRect(cfg); + const arrow = { + x: calcArrowPosition(cfg, 'width'), + y: calcArrowPosition(cfg, 'height'), + }; + return {arrow, popup, placedAt: cfg.position}; } /** @@ -90,63 +108,59 @@ function getOppositePosition(position: PositionType): PositionType { } /** - * TODO: move the actionsToFit definition outside the function and optimize + * Checks and updates popup and arrow positions to fit on major axis. + * @param cfg - popup position config + * @param value - current popup's position value + * @returns updated popup position value + * */ +function fitOnMajorAxis(cfg: PopupPositionConfig, value: PopupPositionValue): PopupPositionValue { + if (cfg.behavior !== 'fit' && cfg.behavior !== 'fit-major') 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; + + return isRequireAdjusting ? adjustAlongMajorAxis(cfg, value) : value; +} + +/** * Updates popup and arrow positions to fit on major axis. * @param cfg - popup position config - * @param rect - popup position rect - * @param arrow - arrow position value + * @param value - current popup's position value + * @returns updated popup position value * */ -function fitOnMajorAxis(cfg: PopupPositionConfig, rect: Rect): PositionType { - if (cfg.behavior !== 'fit' && cfg.behavior !== 'fit-on-major') return cfg.position; - - let isMirrored = false; - const actionsToFit: Record void> = { - 'bottom': () => { - if (cfg.intersectionRatio.bottom || cfg.outer.bottom < rect.bottom) { - rect.y = cfg.inner.y - cfg.element.height; - isMirrored = true; - } - }, - 'left': () => { - if (cfg.intersectionRatio.left || rect.x < cfg.outer.x) { - rect.x = cfg.inner.right; - isMirrored = true; - } - }, - 'right': () => { - if (cfg.intersectionRatio.right || cfg.outer.right < rect.right) { - rect.x = cfg.inner.x - cfg.element.width; - isMirrored = true; - } - }, - 'top': () => { - if (cfg.intersectionRatio.top || rect.y < cfg.outer.y) { - rect.y = cfg.inner.bottom; - isMirrored = true; - } - } - }; - actionsToFit[cfg.position](); +function adjustAlongMajorAxis(cfg: PopupPositionConfig, value: PopupPositionValue): PopupPositionValue { + let {popup, placedAt} = value; + let {x, y} = popup; + if (isStartingSide(cfg.position)) { + x = cfg.position === 'left' ? cfg.inner.right : x; + y = cfg.position === 'top' ? cfg.inner.bottom : y; + } else { + x = cfg.position === 'right' ? cfg.inner.x - popup.width : x; + y = cfg.position === 'bottom' ? cfg.inner.y - popup.height : y; + } + popup = new Rect(x, y, popup.width, popup.height); + placedAt = getOppositePosition(cfg.position); - return isMirrored ? getOppositePosition(cfg.position) : cfg.position; + return {...value, popup, placedAt}; } /** * Calculates adjust for popup position to fit container bounds - * @param elCoord - coordinate of the popup - * @param outerCoord - coordinate of the outer border element + * @param diffCoord - distance between the popup and the outer (container) bounding * @param arrowCoord - coordinate of the arrow * @param arrowLimit - the limit value of the arrow coordinate - * @param startingSide - is it starting side? + * @param isStart - is it starting side? * @returns adjustment value for the coordinates of the arrow and the popup */ -function adjustAlignmentBySide(elCoord: number, outerCoord: number, arrowCoord: number, arrowLimit: number, isStartingSide: boolean): number { +function adjustAlignmentBySide(diffCoord: number, arrowCoord: number, arrowLimit: number, isStart: boolean): number { let arrowAdjust = 0; - if (isStartingSide ? elCoord < outerCoord : elCoord > outerCoord) { - arrowAdjust = elCoord - outerCoord; + if (isStart ? diffCoord < 0 : diffCoord > 0) { + arrowAdjust = diffCoord; const newCoord = arrowCoord + arrowAdjust; - if (isStartingSide ? newCoord < arrowLimit : newCoord > arrowLimit) { + if (isStart ? newCoord < arrowLimit : newCoord > arrowLimit) { arrowAdjust = 0; } /** It was decided that if the relative positions of the trigger and container @@ -158,7 +172,7 @@ function adjustAlignmentBySide(elCoord: number, outerCoord: number, arrowCoord: * Perhaps in the future, we should allow the user to choose the strategy. * * const func = isStartingSide ? Math.max : Math.min; - * arrowAdjust = func(elCoord - outerCoord, arrowLimit - arrowCoord); + * arrowAdjust = func(diffCoord, arrowLimit - arrowCoord); */ } @@ -168,13 +182,13 @@ function adjustAlignmentBySide(elCoord: number, outerCoord: number, arrowCoord: /** * Updates popup and arrow positions to fit on minor axis. * @param cfg - popup position config - * @param rect - popup position rect - * @param arrow - arrow position value + * @param value - current popup's position value + * @returns updated popup position value * */ -function fitOnMinorAxis(cfg: PopupPositionConfig, rect: Rect, arrow: Point): void { - if (cfg.behavior !== 'fit' && cfg.behavior !== 'fit-on-minor') return; +function fitOnMinorAxis(cfg: PopupPositionConfig, value: PopupPositionValue): PopupPositionValue { + if (cfg.behavior !== 'fit' && cfg.behavior !== 'fit-minor') return value; - const isHorizontal = isMajorAxisHorizontal(cfg.position); + const isHorizontal = isOnHorizontalAxis(cfg.position); const start = isHorizontal ? 'y' : 'x'; const end = isHorizontal ? 'bottom' : 'right'; const dimension = isHorizontal ? 'height' : 'width'; @@ -182,23 +196,24 @@ function fitOnMinorAxis(cfg: PopupPositionConfig, rect: Rect, arrow: Point): voi if (cfg.outer[dimension] < cfg.element[dimension] || // cancel fit mode if the popup size is greater than the outer limiter size cfg.trigger[start] < cfg.outer[start] || // or the trigger is outside the outer limiting element cfg.trigger[end] > cfg.outer[end] - ) return; + ) return value; - let coordAdjust = 0; + const {popup, arrow} = value; // check the starting side of the axis let arrowLimit = cfg.marginArrow; - coordAdjust = adjustAlignmentBySide(rect[start], cfg.outer[start], arrow[start], arrowLimit, true); + let coordAdjust = adjustAlignmentBySide(popup[start] - cfg.outer[start], arrow[start], arrowLimit, true); if (coordAdjust) { - rect[start] -= coordAdjust; + popup[start] -= coordAdjust; arrow[start] += coordAdjust; } // check the final side of the axis arrowLimit += calcUsableSizeForArrow(cfg, dimension); - coordAdjust = adjustAlignmentBySide(rect[end], cfg.outer[end], arrow[start], arrowLimit, false); + coordAdjust = adjustAlignmentBySide(popup[end] - cfg.outer[end], arrow[start], arrowLimit, false); if (coordAdjust) { - rect[start] -= coordAdjust; + popup[start] -= coordAdjust; arrow[start] += coordAdjust; } + return {...value, popup, arrow}; } /** @@ -224,19 +239,5 @@ function calcArrowPosition(cfg: PopupPositionConfig, dimensionName: 'height' | ' * @param cfg - popup position config * */ export function calcPopupPosition(cfg: PopupPositionConfig): PopupPositionValue { - const popup = calcPopupBasicRect(cfg); - const arrow = { - x: calcArrowPosition(cfg, 'width'), - y: calcArrowPosition(cfg, 'height'), - position: cfg.position - }; - - const placedAt = fitOnMajorAxis(cfg, popup); - fitOnMinorAxis(cfg, popup, arrow); - - return { - popup, - placedAt, - arrow - }; + return fitOnMinorAxis(cfg, fitOnMajorAxis(cfg, calcBasicPosition(cfg))); } diff --git a/src/modules/esl-popup/core/esl-popup.ts b/src/modules/esl-popup/core/esl-popup.ts index 7f269f3ac..485d8610b 100644 --- a/src/modules/esl-popup/core/esl-popup.ts +++ b/src/modules/esl-popup/core/esl-popup.ts @@ -11,7 +11,7 @@ import {getViewportRect} from '../../esl-utils/dom/window'; import {parseBoolean, parseNumber, toBooleanAttribute} from '../../esl-utils/misc/format'; import {copyDefinedKeys} from '../../esl-utils/misc/object'; import {ESLIntersectionTarget, ESLIntersectionEvent} from '../../esl-event-listener/core/targets/intersection.target'; -import {calcPopupPosition, isMajorAxisHorizontal} from './esl-popup-position'; +import {calcPopupPosition, isOnHorizontalAxis} from './esl-popup-position'; import {ESLPopupPlaceholder} from './esl-popup-placeholder'; import type {ESLToggleableActionParams} from '../../esl-toggleable/core'; @@ -309,7 +309,7 @@ export class ESLPopup extends ESLToggleable { return; } - const isHorizontal = isMajorAxisHorizontal(this.position); + const isHorizontal = isOnHorizontalAxis(this.position); const checkIntersection = (isMajorAxis: boolean, intersectionRatio: number): void => { if (isMajorAxis && intersectionRatio < INTERSECTION_LIMIT_FOR_ADJACENT_AXIS) this.hide(); }; @@ -421,12 +421,11 @@ export class ESLPopup extends ESLToggleable { // set popup position this.style.left = `${popup.x}px`; this.style.top = `${popup.y}px`; + if (!this.$arrow) return; // set arrow position - if (this.$arrow) { - const isHorizontal = isMajorAxisHorizontal(this.position); - this.$arrow.style.left = isHorizontal ? '' : `${arrow.x}px`; - this.$arrow.style.top = isHorizontal ? `${arrow.y}px` : ''; - } + const isHorizontal = isOnHorizontalAxis(this.position); + this.$arrow.style.left = isHorizontal ? '' : `${arrow.x}px`; + this.$arrow.style.top = isHorizontal ? `${arrow.y}px` : ''; } } diff --git a/src/modules/esl-popup/test/esl-popup-position.behaviour-fit-major.test.ts b/src/modules/esl-popup/test/esl-popup-position.behaviour-fit-major.test.ts new file mode 100644 index 000000000..5850579c3 --- /dev/null +++ b/src/modules/esl-popup/test/esl-popup-position.behaviour-fit-major.test.ts @@ -0,0 +1,94 @@ +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, 300, 200); + const trigger = new Rect(500, 500, 20, 20); + const container = new Rect(0, 0, 1000, 1000); + const intersectionRatio = {top: 0, left: 0, right: 0, bottom: 0}; + const cfgRef = { + behavior: 'fit-major', + position: 'top', + marginArrow: 7, + offsetArrowRatio: 0, + intersectionRatio, + arrow, + element: popup, + inner: trigger.grow(10), + outer: container, + trigger + } as PopupPositionConfig; + + const expectedRef = { + arrow: {x: 7, y: 7}, + placedAt: 'top', + 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(488, 530); + expected.placedAt = 'bottom'; + 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(530, 488); + expected.placedAt = 'right'; + 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(488, 290); + 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(190, 488); + expected.placedAt = 'left'; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when the activator is crossing the top edge of the container', () => { + const cfg = {...cfgRef, position: 'top', intersectionRatio: {top: 0.5, left: 0, right: 0, bottom: 0}} as PopupPositionConfig; + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(488, 530); + expected.placedAt = 'bottom'; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when the activator is crossing the left edge of the container', () => { + const cfg = {...cfgRef, position: 'left', intersectionRatio: {top: 0, left: 0.5, right: 0, bottom: 0}} as PopupPositionConfig; + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(530, 488); + expected.placedAt = 'right'; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when the activator is crossing the bottom edge of the container', () => { + const cfg = {...cfgRef, position: 'bottom', intersectionRatio: {top: 0, left: 0, right: 0, bottom: 0.5}} as PopupPositionConfig; + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(488, 290); + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when the activator is crossing the right edge of the container', () => { + const cfg = {...cfgRef, position: 'right', intersectionRatio: {top: 0, left: 0, right: 0.5, bottom: 0}} as PopupPositionConfig; + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(190, 488); + expected.placedAt = 'left'; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + }); +}); diff --git a/src/modules/esl-popup/test/esl-popup-position.behaviour-fit-minor.test.ts b/src/modules/esl-popup/test/esl-popup-position.behaviour-fit-minor.test.ts new file mode 100644 index 000000000..e8c215d34 --- /dev/null +++ b/src/modules/esl-popup/test/esl-popup-position.behaviour-fit-minor.test.ts @@ -0,0 +1,137 @@ +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', () => { + const arrow = new Rect(0, 0, 30, 30); + const popup = new Rect(0, 0, 300, 200); + const trigger = new Rect(500, 500, 20, 20); + const container = new Rect(0, 0, 1000, 1000); + const intersectionRatio = {top: 0, left: 0, right: 0, bottom: 0}; + const cfgRef = { + behavior: 'fit-minor', + position: 'top', + marginArrow: 20, + offsetArrowRatio: 0.5, + intersectionRatio, + arrow, + element: popup, + inner: trigger.grow(10), + outer: container, + trigger + } as PopupPositionConfig; + + const expectedRef = { + arrow: {x: 135, y: 85}, + placedAt: 'top', + popup + }; + + 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: 235, y: 85}; + expected.popup = popup.shift(260, 290); + 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: 55, y: 85}; + expected.popup = popup.shift(440, 290); + 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: 235, y: 85}; + expected.popup = popup.shift(260, 530); + expected.placedAt = 'bottom'; + 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: 55, y: 85}; + expected.popup = popup.shift(440, 530); + expected.placedAt = 'bottom'; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + }); + + describe('should not adjust the position in case:', () => { + test('when the popup size is greater than the outer limiter size', () => { + const narrowContainer = trigger.grow(50); + const cfg = {...cfgRef, position: 'top', outer: narrowContainer} as PopupPositionConfig; + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(360, 290); + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + describe('when the popup has a position on the vertical axis', () => { + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(360, 290); + test('when the trigger is outside of the outer limiting element by the own left edge', () => { + const cfg = {...cfgRef, position: 'top', outer: container.shift(-510, 0)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when the trigger is outside of the outer limiting element by the own right edge', () => { + const cfg = {...cfgRef, position: 'top', outer: container.shift(510, 0)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + }); + + describe('when the popup has a position on the horizontal axis', () => { + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(530, 410); + expected.placedAt = 'right'; + test('when the trigger is outside of the outer limiting element by the own top edge', () => { + const cfg = {...cfgRef, position: 'right', outer: container.shift(0, -510)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when the trigger is outside of the outer limiting element by the own right edge', () => { + const cfg = {...cfgRef, position: 'right', outer: container.shift(0, 510)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + }); + + describe('when there is a lack of space for position adjustment on the horizontal axis:', () => { + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(360, 530); + expected.placedAt = 'bottom'; + + test('when it is required to move to the left', () => { + const cfg = {...cfgRef, position: 'bottom', outer: container.shift(-460, 0)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when it is required to move to the right', () => { + const cfg = {...cfgRef, position: 'bottom', outer: container.shift(480, 0)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + }); + + describe('when there is a lack of space for position adjustment on the vertical axis:', () => { + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(530, 410); + expected.placedAt = 'right'; + + test('when it is required to move to the top', () => { + const cfg = {...cfgRef, position: 'right', outer: container.shift(0, -460)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when it is required to move to the bottom', () => { + const cfg = {...cfgRef, position: 'right', outer: container.shift(0, 480)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + }); + }); +}); diff --git a/src/modules/esl-popup/test/esl-popup-position.behaviour-fit.test.ts b/src/modules/esl-popup/test/esl-popup-position.behaviour-fit.test.ts new file mode 100644 index 000000000..28f15c85b --- /dev/null +++ b/src/modules/esl-popup/test/esl-popup-position.behaviour-fit.test.ts @@ -0,0 +1,202 @@ +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, 300, 200); + const trigger = new Rect(500, 500, 20, 20); + const container = new Rect(0, 0, 1000, 1000); + const intersectionRatio = {top: 0, left: 0, right: 0, bottom: 0}; + const cfgRef = { + behavior: 'fit', + position: 'top', + marginArrow: 20, + offsetArrowRatio: 0.5, + intersectionRatio, + arrow, + element: popup, + inner: trigger.grow(10), + outer: container, + trigger + } as PopupPositionConfig; + + const expectedRef = { + arrow: {x: 135, y: 85}, + placedAt: 'top', + 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(360, 530); + expected.placedAt = 'bottom'; + 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(530, 410); + expected.placedAt = 'right'; + 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(360, 290); + 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(190, 410); + expected.placedAt = 'left'; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when the activator is crossing the top edge of the container', () => { + const cfg = {...cfgRef, position: 'top', intersectionRatio: {top: 0.5, left: 0, right: 0, bottom: 0}} as PopupPositionConfig; + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(360, 530); + expected.placedAt = 'bottom'; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when the activator is crossing the left edge of the container', () => { + const cfg = {...cfgRef, position: 'left', intersectionRatio: {top: 0, left: 0.5, right: 0, bottom: 0}} as PopupPositionConfig; + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(530, 410); + expected.placedAt = 'right'; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when the activator is crossing the bottom edge of the container', () => { + const cfg = {...cfgRef, position: 'bottom', intersectionRatio: {top: 0, left: 0, right: 0, bottom: 0.5}} as PopupPositionConfig; + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(360, 290); + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when the activator is crossing the right edge of the container', () => { + const cfg = {...cfgRef, position: 'right', intersectionRatio: {top: 0, left: 0, right: 0.5, bottom: 0}} as PopupPositionConfig; + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(190, 410); + expected.placedAt = 'left'; + 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: 235, y: 85}; + expected.popup = popup.shift(260, 290); + 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: 55, y: 85}; + expected.popup = popup.shift(440, 290); + 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: 235, y: 85}; + expected.popup = popup.shift(260, 530); + expected.placedAt = 'bottom'; + 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: 55, y: 85}; + expected.popup = popup.shift(440, 530); + expected.placedAt = 'bottom'; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + }); + + describe('should not adjust the position in case:', () => { + test('when the popup size is greater than the outer limiter size', () => { + const narrowContainer = trigger.grow(50); + const cfg = {...cfgRef, position: 'top', outer: narrowContainer} as PopupPositionConfig; + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(360, 530); + expected.placedAt = 'bottom'; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + describe('when the popup has a position on the vertical axis', () => { + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(360, 290); + test('when the trigger is outside of the outer limiting element by the own left edge', () => { + const cfg = {...cfgRef, position: 'top', outer: container.shift(-510, 0)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when the trigger is outside of the outer limiting element by the own right edge', () => { + const cfg = {...cfgRef, position: 'top', outer: container.shift(510, 0)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + }); + + describe('when the popup has a position on the horizontal axis', () => { + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(530, 410); + expected.placedAt = 'right'; + test('when the trigger is outside of the outer limiting element by the own top edge', () => { + const cfg = {...cfgRef, position: 'right', outer: container.shift(0, -510)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when the trigger is outside of the outer limiting element by the own right edge', () => { + const cfg = {...cfgRef, position: 'right', outer: container.shift(0, 510)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + }); + + describe('when there is a lack of space for position adjustment on the horizontal axis:', () => { + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(360, 530); + expected.placedAt = 'bottom'; + + test('when it is required to move to the left', () => { + const cfg = {...cfgRef, position: 'bottom', outer: container.shift(-460, 0)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when it is required to move to the right', () => { + const cfg = {...cfgRef, position: 'bottom', outer: container.shift(480, 0)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + }); + + describe('when there is a lack of space for position adjustment on the vertical axis:', () => { + const expected = Object.assign({}, expectedRef) as PopupPositionValue; + expected.popup = popup.shift(530, 410); + expected.placedAt = 'right'; + + test('when it is required to move to the top', () => { + const cfg = {...cfgRef, position: 'right', outer: container.shift(0, -460)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + + test('when it is required to move to the bottom', () => { + const cfg = {...cfgRef, position: 'right', outer: container.shift(0, 480)} as PopupPositionConfig; + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + }); + }); +}); diff --git a/src/modules/esl-popup/test/esl-popup-position.behaviour-none.test.ts b/src/modules/esl-popup/test/esl-popup-position.behaviour-none.test.ts new file mode 100644 index 000000000..ee4ca56b0 --- /dev/null +++ b/src/modules/esl-popup/test/esl-popup-position.behaviour-none.test.ts @@ -0,0 +1,88 @@ +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', () => { + const arrow = new Rect(0, 0, 30, 30); + const popup = new Rect(0, 0, 300, 200); + const trigger = new Rect(500, 500, 20, 20); + const container = new Rect(0, 0, 1000, 1000); + const cfgRef = { + behavior: 'none', + position: 'top', + marginArrow: 7, + offsetArrowRatio: 0, + intersectionRatio: {top: 0, left: 0, right: 0, bottom: 0}, + arrow, + element: popup, + inner: trigger.grow(10), + outer: container, + trigger + } as PopupPositionConfig; + + const expectedRef = { + arrow: {x: 7, y: 7}, + placedAt: 'top', + 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(488, 290); + 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(190, 488); + expected.placedAt = 'left'; + 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(488, 530); + expected.placedAt = 'bottom'; + 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(530, 488); + expected.placedAt = 'right'; + 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(488, 290); + 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: 263, y: 163}; + expected.popup = popup.shift(232, 290); + 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: 135, y: 85}; + expected.popup = popup.shift(360, 290); + expect(calcPopupPosition(cfg)).toEqual(expected); + }); + }); +}); diff --git a/src/modules/esl-popup/test/esl-popup-position.test.ts b/src/modules/esl-popup/test/esl-popup-position.test.ts new file mode 100644 index 000000000..af549a2a4 --- /dev/null +++ b/src/modules/esl-popup/test/esl-popup-position.test.ts @@ -0,0 +1,21 @@ +import {isOnHorizontalAxis} from '../core/esl-popup-position'; + +describe('ESLPopup position: tests', () => { + describe('isMajorAxisHorizontal() function:', () => { + test('should return true for left position', () => { + expect(isOnHorizontalAxis('left')).toBe(true); + }); + + test('should return true for right position', () => { + expect(isOnHorizontalAxis('right')).toBe(true); + }); + + test('should return false for top position', () => { + expect(isOnHorizontalAxis('top')).toBe(false); + }); + + test('should return false for bottom position', () => { + expect(isOnHorizontalAxis('bottom')).toBe(false); + }); + }); +}); diff --git a/src/modules/esl-utils/dom/test/window.test.ts b/src/modules/esl-utils/dom/test/window.test.ts new file mode 100644 index 000000000..590d10925 --- /dev/null +++ b/src/modules/esl-utils/dom/test/window.test.ts @@ -0,0 +1,57 @@ +import {Rect} from '../rect'; +import {getWindow, getWindowRect, getViewportRect} from '../window'; + +describe('getWindow() function:', () => { + test('should return window when the passed node is null', () => { + const node = null as any; + expect(getWindow(node)).toBe(window); + }); + + test('should return window when the passed node is window', () => { + const node = window; + expect(getWindow(node)).toBe(window); + }); + + test('should return window for the passed node', () => { + const node = document.createElement('div'); + expect(getWindow(node)).toBe(window); + }); + + test('should return window when the passed node is document element', () => { + const node = document.createElement('div').ownerDocument; + expect(getWindow(node)).toBe(window); + }); + + test('should return window as fallback value when the passed node has ownerDocument without defaultView', () => { + const node = {ownerDocument: {}} as Node; + expect(getWindow(node)).toBe(window); + }); +}); + +describe('getWindowRect() function:', () => { + test('should return window rect', () => { + expect(getWindowRect()).toEqual(new Rect(0, 0, 1024, 768)); + }); +}); + +describe('getViewportRect() function:', () => { + const clientWidthOriginal = document.documentElement.clientWidth; + const clientHeightOriginal = document.documentElement.clientHeight; + + beforeAll(() => { + const scrollbarWidth = 17; + const clientWidth = window.innerWidth - scrollbarWidth; + const clientHeight = window.innerHeight - scrollbarWidth; + Object.defineProperty(document.documentElement, 'clientWidth', {value: clientWidth, configurable: true}); + Object.defineProperty(document.documentElement, 'clientHeight', {value: clientHeight, configurable: true}); + }); + + afterAll(() => { + Object.defineProperty(document.documentElement, 'clientWidth', {value: clientWidthOriginal, configurable: true}); + Object.defineProperty(document.documentElement, 'clientHeight', {value: clientHeightOriginal, configurable: true}); + }); + + test('should return viewport rect', () => { + expect(getViewportRect()).toEqual(new Rect(0, 0, 1007, 751)); + }); +});