From c47a5e542e5a3194fbb321ebfe2a5e6306abec34 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Mon, 27 May 2024 18:39:28 +0400 Subject: [PATCH] Splitter: improve collapsed functionality (#27420) --- .../js/__internal/ui/collection/base.ts | 3 +- .../js/__internal/ui/splitter/splitter.ts | 338 ++++++++++-------- .../js/__internal/ui/splitter/utils/layout.ts | 15 +- .../js/__internal/ui/splitter/utils/types.ts | 4 + .../DevExpress.ui.widgets/splitter.tests.js | 247 ++++++++++++- 5 files changed, 461 insertions(+), 146 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/collection/base.ts b/packages/devextreme/js/__internal/ui/collection/base.ts index 53c4b21f3cea..980b146366dc 100644 --- a/packages/devextreme/js/__internal/ui/collection/base.ts +++ b/packages/devextreme/js/__internal/ui/collection/base.ts @@ -52,7 +52,8 @@ declare class Base< _getIndexByItemData(item: TItem): number; _findItemElementByItem(item: TItem): dxElementWrapper; - _itemOptionChanged(item: TItem, property: string, value: unknown): void; + _itemOptionChanged(item: TItem, property: string, value: unknown, prevValue: unknown): void; + _itemEventHandler($item: dxElementWrapper, eventName: string, eventData: unknown): void; } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/devextreme/js/__internal/ui/splitter/splitter.ts b/packages/devextreme/js/__internal/ui/splitter/splitter.ts index 1b9d122c1741..cb0c3bebf7fc 100644 --- a/packages/devextreme/js/__internal/ui/splitter/splitter.ts +++ b/packages/devextreme/js/__internal/ui/splitter/splitter.ts @@ -41,6 +41,7 @@ import { calculateDelta, convertSizeToRatio, findIndexOfNextVisibleItem, + findLastIndexOfNonCollapsedItem, findLastIndexOfVisibleItem, getElementSize, getNextLayout, @@ -48,12 +49,14 @@ import { setFlexProp, } from './utils/layout'; import { getDefaultLayout } from './utils/layout_default'; -import type { - EventMap, - FlexProperty, - HandlerMap, - PaneRestrictions, - RenderQueueItem, +import { + CollapseExpandDirection, + type EventMap, + type FlexProperty, + type HandlerMap, + type InteractionEvent, + type PaneRestrictions, + type RenderQueueItem, } from './utils/types'; const SPLITTER_CLASS = 'dx-splitter'; @@ -96,24 +99,31 @@ export interface Properties< _renderQueue?: RenderQueueItem[]; } +interface PaneCache { + size: number; + direction: CollapseExpandDirection; +} + class Splitter extends CollectionWidget { static ItemClass = SplitterItem; private _renderQueue: RenderQueueItem[] = []; - private _panesCacheSize: Record = {}; + private _panesCacheSize: (PaneCache | undefined)[] = []; private _collapsedItemSize?: number; + private _savedCollapsingEvent?: InteractionEvent; + private _shouldRecalculateLayout?: boolean; private _layout?: number[]; private _currentLayout?: number[]; - private _activeResizeHandleIndex = -1; + private _activeResizeHandleIndex?: number; - private _collapseButton?: string; + private _collapseDirection?: CollapseExpandDirection; private _itemRestrictions: PaneRestrictions[] = []; @@ -183,7 +193,7 @@ class Splitter extends CollectionWidget { super._initMarkup(); - this._panesCacheSize = {}; + this._panesCacheSize = []; this._attachResizeObserverSubscription(); } @@ -321,7 +331,7 @@ class Splitter extends CollectionWidget { _updateResizeHandlesCollapsibleState(): void { this._getResizeHandles().forEach((resizeHandle) => { - const $resizeHandle = (resizeHandle.$element() as unknown) as dxElementWrapper; + const $resizeHandle = $(resizeHandle.element()); const $leftItem = this._getResizeHandleLeftItem($resizeHandle); const $rightItem = this._getResizeHandleRightItem($resizeHandle); @@ -414,116 +424,20 @@ class Splitter extends CollectionWidget { onCollapsePrev: (e: ItemCollapsedEvent | ItemExpandedEvent): void => { e.event?.stopPropagation(); - const $resizeHandle = $(e.element); - - const $leftItem = this._getResizeHandleLeftItem($resizeHandle); - - const leftItemData = this._getItemData($leftItem); - const leftItemIndex = this._getIndexByItem(leftItemData); - const $rightItem = this._getResizeHandleRightItem($resizeHandle); - const rightItemData = this._getItemData($rightItem); - const rightItemIndex = this._getIndexByItem(rightItemData); - - const isRightItemCollapsed = rightItemData.collapsed === true; - - this._activeResizeHandleIndex = leftItemIndex; - this._collapseButton = 'prev'; - - if (isRightItemCollapsed) { - this._collapsedItemSize = this._panesCacheSize[rightItemIndex]; - - if (!this._collapsedItemSize) { - for (let i = leftItemIndex; i >= 0; i -= 1) { - // @ts-expect-error badly typed base class - // eslint-disable-next-line max-depth - if (this.option('items')[i].collapsed !== true) { - this._collapsedItemSize = this.getLayout()[i] / 2; - } - } - } - - this._panesCacheSize[rightItemIndex] = undefined; - this._updateItemData('collapsed', rightItemIndex, false, false); - - this._getAction(ITEM_EXPANDED_EVENT)({ - event: e.event, - itemData: rightItemData, - itemElement: getPublicElement($rightItem), - itemIndex: rightItemIndex, - }); - - return; - } - - this._panesCacheSize[leftItemIndex] = this.getLayout()[leftItemIndex]; - this._collapsedItemSize = this.getLayout()[leftItemIndex]; - - this._updateItemData('collapsed', leftItemIndex, true, false); - - this._getAction(ITEM_COLLAPSED_EVENT)({ - event: e.event, - itemData: leftItemData, - itemElement: getPublicElement($leftItem), - itemIndex: leftItemIndex, - }); + this._savedCollapsingEvent = e.event; + this.handleCollapseEvent( + this._getResizeHandleLeftItem($(e.element)), + CollapseExpandDirection.Previous, + ); }, onCollapseNext: (e: ItemCollapsedEvent | ItemExpandedEvent): void => { e.event?.stopPropagation(); - const $resizeHandle = $(e.element); - - const $leftItem = this._getResizeHandleLeftItem($resizeHandle); - const leftItemData = this._getItemData($leftItem); - const leftItemIndex = this._getIndexByItem(leftItemData); - const $rightItem = this._getResizeHandleRightItem($resizeHandle); - const rightItemData = this._getItemData($rightItem); - const rightItemIndex = this._getIndexByItem(rightItemData); - - const isLeftItemCollapsed = leftItemData.collapsed === true; - - this._activeResizeHandleIndex = leftItemIndex; - - this._collapseButton = 'next'; - - if (isLeftItemCollapsed) { - this._collapsedItemSize = this._panesCacheSize[leftItemIndex]; - - if (!this._collapsedItemSize) { - // @ts-expect-error badly typed base class - for (let i = rightItemIndex; i <= this.option('items').length - 1; i += 1) { - // @ts-expect-error badly typed base class - // eslint-disable-next-line max-depth - if (this.option('items')[i].collapsed !== true) { - this._collapsedItemSize = this.getLayout()[i] / 2; - } - } - } - - this._panesCacheSize[leftItemIndex] = undefined; - - this._updateItemData('collapsed', leftItemIndex, false, false); - - this._getAction(ITEM_EXPANDED_EVENT)({ - event: e.event, - itemData: leftItemData, - itemElement: getPublicElement($leftItem), - itemIndex: leftItemIndex, - }); - - return; - } - - this._panesCacheSize[rightItemIndex] = this.getLayout()[rightItemIndex]; - this._collapsedItemSize = this.getLayout()[rightItemIndex]; - - this._updateItemData('collapsed', rightItemIndex, true, false); - - this._getAction(ITEM_COLLAPSED_EVENT)({ - event: e.event, - itemData: rightItemData, - itemElement: getPublicElement($rightItem), - itemIndex: rightItemIndex, - }); + this._savedCollapsingEvent = e.event; + this.handleCollapseEvent( + this._getResizeHandleLeftItem($(e.element)), + CollapseExpandDirection.Next, + ); }, onResizeStart: (e: ResizeStartEvent): void => { const { element, event } = e; @@ -601,6 +515,8 @@ class Splitter extends CollectionWidget { onResizeEnd: (e: ResizeEndEvent): void => { const { element, event } = e; + this._activeResizeHandleIndex = undefined; + if (!event) { return; } const $resizeHandle = $(element); @@ -627,20 +543,71 @@ class Splitter extends CollectionWidget { }; } - _getResizeHandleLeftItem($resizeHandle: dxElementWrapper): dxElementWrapper { - let $leftItem = $resizeHandle.prev(); + handleCollapseEvent( + $resizeHandle: dxElementWrapper, + direction: CollapseExpandDirection, + isItemCollapsed?: boolean, + ): void { + const $leftItem = $resizeHandle; + const leftItemData = this._getItemData($leftItem); + const leftItemIndex = this._getIndexByItem(leftItemData); + const $rightItem = this._getResizeHandleRightItem($leftItem); + const rightItemData = this._getItemData($rightItem); + const rightItemIndex = this._getIndexByItem(rightItemData); + + const isCollapsed = direction === 'prev' ? isItemCollapsed ?? rightItemData.collapsed : isItemCollapsed ?? leftItemData.collapsed; + + this._activeResizeHandleIndex = leftItemIndex; + this._collapseDirection = direction; + + if (isCollapsed) { + const indexToExpand = direction === 'prev' ? rightItemIndex : leftItemIndex; + + const paneCache = this._panesCacheSize[direction === 'prev' ? rightItemIndex : leftItemIndex]; + + this._collapsedItemSize = paneCache?.direction === direction ? paneCache.size : undefined; + + if (!this._collapsedItemSize) { + this._collapsedItemSize = direction === 'prev' + ? this._calculateExpandToLeftSize(leftItemIndex) + : this._calculateExpandToRightSize(rightItemIndex); + } - while ($leftItem.hasClass(INVISIBLE_STATE_CLASS)) { + this._panesCacheSize[indexToExpand] = undefined; + + this._updateItemData('collapsed', indexToExpand, false, false); + + return; + } + + const indexToCollapse = direction === 'prev' ? leftItemIndex : rightItemIndex; + + this._panesCacheSize[indexToCollapse] = { + size: this.getLayout()[indexToCollapse], + direction: direction === CollapseExpandDirection.Next + ? CollapseExpandDirection.Previous + : CollapseExpandDirection.Next, + }; + + this._collapsedItemSize = this.getLayout()[indexToCollapse]; + + this._updateItemData('collapsed', indexToCollapse, true, false); + } + + _getResizeHandleLeftItem($element: dxElementWrapper): dxElementWrapper { + let $leftItem = $element.prev(); + + while ($leftItem.hasClass(INVISIBLE_STATE_CLASS) || $leftItem.hasClass(RESIZE_HANDLE_CLASS)) { $leftItem = $leftItem.prev(); } return $leftItem; } - _getResizeHandleRightItem($resizeHandle: dxElementWrapper): dxElementWrapper { - let $rightItem = $resizeHandle.next(); + _getResizeHandleRightItem($element: dxElementWrapper): dxElementWrapper { + let $rightItem = $element.next(); - while ($rightItem.hasClass(INVISIBLE_STATE_CLASS)) { + while ($rightItem.hasClass(INVISIBLE_STATE_CLASS) || $rightItem.hasClass(RESIZE_HANDLE_CLASS)) { $rightItem = $rightItem.next(); } @@ -689,7 +656,7 @@ class Splitter extends CollectionWidget { .toggleClass(VERTICAL_ORIENTATION_CLASS, !this._isHorizontalOrientation()); } - _itemOptionChanged(item: Item, property: string, value: unknown): void { + _itemOptionChanged(item: Item, property: string, value: unknown, prevValue: unknown): void { switch (property) { case 'size': case 'maxSize': @@ -701,7 +668,7 @@ class Splitter extends CollectionWidget { this._updateItemSizes(); break; case 'collapsed': - this._itemCollapsedOptionChanged(item); + this._itemCollapsedOptionChanged(item, value as boolean, prevValue as boolean); break; case 'resizable': this._updateResizeHandlesResizableState(); @@ -713,33 +680,109 @@ class Splitter extends CollectionWidget { this._invalidate(); break; default: - super._itemOptionChanged(item, property, value); + super._itemOptionChanged(item, property, value, prevValue); } } - _itemCollapsedOptionChanged(item: Item): void { + _itemCollapsedOptionChanged(item: Item, value: boolean, prevValue: boolean): void { + if (Boolean(value) === Boolean(prevValue)) { + return; + } + this._updateItemsRestrictions(true); - this._updateResizeHandlesResizableState(); - this._updateResizeHandlesCollapsibleState(); + const itemIndex = this._getIndexByItem(item); + const $item = $(this._itemElements()[itemIndex]); + const { items = [] } = this.option(); - if (isDefined(this._collapsedItemSize)) { - this._layout = getNextLayout( - this.getLayout(), - this._getCollapseDelta(item), - this._activeResizeHandleIndex, - this._itemRestrictions, - true, - ); - } else { - this._layout = this._getDefaultLayoutBasedOnSize(); + if (!isDefined(this._activeResizeHandleIndex)) { + if (value) { + const isLastNonCollapsedItem = itemIndex > findLastIndexOfNonCollapsedItem(items); + + if (this._isLastVisibleItem(itemIndex) || isLastNonCollapsedItem) { + this.handleCollapseEvent( + this._getResizeHandleLeftItem($item), + CollapseExpandDirection.Next, + !!prevValue, + ); + } else { + this.handleCollapseEvent( + $item, + CollapseExpandDirection.Previous, + !!prevValue, + ); + } + } else { + const isLastNonCollapsedItem = itemIndex >= findLastIndexOfNonCollapsedItem(items); + + if (this._isLastVisibleItem(itemIndex) || isLastNonCollapsedItem) { + this.handleCollapseEvent( + this._getResizeHandleLeftItem($item), + CollapseExpandDirection.Previous, + !!prevValue, + ); + } else if (this._panesCacheSize[itemIndex]?.direction + === CollapseExpandDirection.Previous) { + this.handleCollapseEvent( + this._getResizeHandleLeftItem($item), + CollapseExpandDirection.Previous, + !!prevValue, + ); + } else { + this.handleCollapseEvent( + $item, + CollapseExpandDirection.Next, + !!prevValue, + ); + } + } } - this._collapseButton = undefined; - this._collapsedItemSize = undefined; + const collapsedDelta = this._getCollapseDelta(item); + + this._layout = getNextLayout( + this.getLayout(), + collapsedDelta, + this._activeResizeHandleIndex, + this._itemRestrictions, + true, + ); this._applyStylesFromLayout(this.getLayout()); this._updateItemSizes(); + + this._updateResizeHandlesResizableState(); + this._updateResizeHandlesCollapsibleState(); + + this._fireCollapsedStateChanged(!value, $item, this._savedCollapsingEvent); + + this._savedCollapsingEvent = undefined; + this._collapseDirection = undefined; + this._collapsedItemSize = undefined; + this._activeResizeHandleIndex = undefined; + } + + _calculateExpandToLeftSize(leftItemIndex: number): number { + const { items = [] } = this.option(); + + for (let i = leftItemIndex; i >= 0; i -= 1) { + if (items[i].collapsed !== true) { + return this.getLayout()[i] / 2; + } + } + + return 0; + } + + _calculateExpandToRightSize(rightItemIndex: number): number { + const { items = [] } = this.option(); + for (let i = rightItemIndex; i <= items.length - 1; i += 1) { + if (items[i].collapsed !== true) { + return this.getLayout()[i] / 2; + } + } + + return 0; } _getCollapseDelta(item: Item): number { @@ -751,12 +794,23 @@ class Splitter extends CollectionWidget { ? this._collapsedItemSize : minSize; - const deltaSign = this._collapseButton === 'prev' ? -1 : 1; + const deltaSign = this._collapseDirection === 'prev' ? -1 : 1; + const delta = Math.abs(itemSize - collapsedSize) * deltaSign; return delta; } + _fireCollapsedStateChanged( + isExpanded: boolean, + $item: dxElementWrapper, + e?: unknown, + ): void { + const eventName = isExpanded ? ITEM_EXPANDED_EVENT : ITEM_COLLAPSED_EVENT; + + this._itemEventHandler($item, eventName, { event: e }); + } + _getDefaultLayoutBasedOnSize(): number[] { this._updateItemsRestrictions(); diff --git a/packages/devextreme/js/__internal/ui/splitter/utils/layout.ts b/packages/devextreme/js/__internal/ui/splitter/utils/layout.ts index 4212284da3cc..49317ab89008 100644 --- a/packages/devextreme/js/__internal/ui/splitter/utils/layout.ts +++ b/packages/devextreme/js/__internal/ui/splitter/utils/layout.ts @@ -31,6 +31,15 @@ export function findLastIndexOfVisibleItem(items: Item[]): number { return -1; } +export function findLastIndexOfNonCollapsedItem(items: Item[]): number { + for (let i = items.length - 1; i >= 0; i -= 1) { + if (items[i].collapsed !== true) { + return i; + } + } + return -1; +} + export function findIndexOfNextVisibleItem(items: Item[], index: number): number { for (let i = index + 1; i < items.length; i += 1) { if (items[i].visible !== false) { @@ -111,10 +120,14 @@ function findMaxAvailableDelta( export function getNextLayout( currentLayout: number[], delta: number, - prevPaneIndex: number, + prevPaneIndex: number | undefined, paneRestrictions: PaneRestrictions[], collapseMode = false, ): number[] { + if (!isDefined(prevPaneIndex)) { + return currentLayout; + } + const nextLayout = [...currentLayout]; const nextPaneIndex = prevPaneIndex + 1; diff --git a/packages/devextreme/js/__internal/ui/splitter/utils/types.ts b/packages/devextreme/js/__internal/ui/splitter/utils/types.ts index 27028a99ea01..69dd6ffcae67 100644 --- a/packages/devextreme/js/__internal/ui/splitter/utils/types.ts +++ b/packages/devextreme/js/__internal/ui/splitter/utils/types.ts @@ -39,5 +39,9 @@ export type HandlerMap = { export type InteractionEvent = KeyboardEvent | PointerEvent | MouseEvent | TouchEvent; export type ResizeEvents = 'onResize' | 'onResizeStart' | 'onResizeEnd'; export type CollapseEvents = 'onCollapsePrev' | 'onCollapseNext'; +export enum CollapseExpandDirection { + Previous = 'prev', + Next = 'next', +} export type FlexProperty = 'flexGrow' | 'flexDirection' | 'flexBasis' | 'flexShrink'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/splitter.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/splitter.tests.js index bd54c79d0106..7788062cf62e 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/splitter.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/splitter.tests.js @@ -814,6 +814,154 @@ QUnit.module('Pane sizing', moduleConfig, () => { }); }); + [ + { + items: [{ size: '30%', collapsible: true }, { size: '10%', collapsible: true }, { collapsible: true }, { collapsible: true }, { collapsible: true }], + scenarios: [ + { targetButton: 'prev', resizeHandleIndex: 1, expectedLayout: ['30.9917', '0', '29.8898', '19.5592', '19.5592'] }, + { targetButton: 'prev', resizeHandleIndex: 0, expectedLayout: ['15.4959', '15.4959', '29.8898', '19.5592', '19.5592'] }, + { targetButton: 'prev', resizeHandleIndex: 0, expectedLayout: ['0', '30.9917', '29.8898', '19.5592', '19.5592'] }, + { targetButton: 'next', resizeHandleIndex: 3, expectedLayout: ['0', '30.9917', '29.8898', '39.1185', '0'] }, + { targetButton: 'next', resizeHandleIndex: 2, expectedLayout: ['0', '30.9917', '69.0083', '0', '0'] }, + { targetButton: 'next', resizeHandleIndex: 3, expectedLayout: ['0', '30.9917', '69.0083', '0', '0'] }, + { targetButton: 'prev', resizeHandleIndex: 2, expectedLayout: ['0', '30.9917', '0', '69.0083', '0'] }, + { targetButton: 'prev', resizeHandleIndex: 1, expectedLayout: ['0', '15.4959', '15.4959', '69.0083', '0'] }, + { targetButton: 'next', resizeHandleIndex: 0, expectedLayout: ['15.4959', '0', '15.4959', '69.0083', '0'] }, + ] + }, + { + items: [{ collapsible: true }, { collapsible: true }, { collapsible: true }, { collapsible: true }], + scenarios: [ + { targetButton: 'next', resizeHandleIndex: 2, expectedLayout: ['25', '25', '50', '0'] }, + { targetButton: 'next', resizeHandleIndex: 1, expectedLayout: ['25', '75', '0', '0'] }, + ] + }, + ].forEach(({ items, scenarios }) => { + QUnit.test(`The pane should restore its size after collapsing and expanding by click, items: ${JSON.stringify(items)}`, function(assert) { + this.reinit({ items }); + + scenarios.forEach(({ targetButton, expectedLayout, resizeHandleIndex = 0 }) => { + const $resizeHandle = this.getResizeHandles().eq(resizeHandleIndex); + + const $collapseButton = targetButton === 'prev' + ? this.getCollapsePrevButton($resizeHandle) + : this.getCollapseNextButton($resizeHandle); + + $collapseButton.trigger('dxclick'); + + this.assertLayout(expectedLayout); + }); + }); + }); + + [ + { + items: [{ collapsible: true }, { collapsible: true }, { collapsible: true }, { collapsible: true, size: '40%' }], + scenarios: [ + { targetButton: 'next', resizeHandleIndex: 2, expectedLayout: ['19.6721', '19.6721', '60.6557', '0'] }, + { newCollapsedValue: false, paneIndex: 3, expectedLayout: ['19.6721', '19.6721', '19.6721', '40.9836'] }, + ] + }, + { + items: [{ collapsible: true }, { collapsible: true }, { collapsible: true }, { collapsible: true, size: '40%' }], + scenarios: [ + { targetButton: 'next', resizeHandleIndex: 2, expectedLayout: ['19.6721', '19.6721', '60.6557', '0'] }, + { newCollapsedValue: true, paneIndex: 2, expectedLayout: ['19.6721', '80.3279', '0', '0'] }, + { newCollapsedValue: true, paneIndex: 0, expectedLayout: ['0', '100', '0', '0'] }, + { newCollapsedValue: false, paneIndex: 0, expectedLayout: ['19.6721', '80.3279', '0', '0'] }, + { newCollapsedValue: false, paneIndex: 2, expectedLayout: ['19.6721', '19.6721', '60.6557', '0'] }, + ] + }, + { + items: [{ collapsible: true }, { collapsible: true, size: '30%' }, { collapsible: true }, { collapsible: true }], + scenarios: [ + { targetButton: 'next', resizeHandleIndex: 1, expectedLayout: ['23.0874', '53.8251', '0', '23.0874'] }, + { newCollapsedValue: false, paneIndex: 2, expectedLayout: ['23.0874', '30.7377', '23.0874', '23.0874'] }, + ] + }, + ].forEach(({ items, scenarios }) => { + QUnit.test(`The pane collapse/expand scenarios using API and UI, items: ${JSON.stringify(items)}`, function(assert) { + this.reinit({ items }); + + scenarios.forEach((scenario) => { + const { newCollapsedValue, paneIndex, expectedLayout, targetButton, resizeHandleIndex } = scenario; + + if(targetButton) { + const $resizeHandle = this.getResizeHandles().eq(resizeHandleIndex); + + const $collapseButton = targetButton === 'prev' + ? this.getCollapsePrevButton($resizeHandle) + : this.getCollapseNextButton($resizeHandle); + + $collapseButton.trigger('dxclick'); + } else { + this.instance.option(`items[${paneIndex}].collapsed`, newCollapsedValue); + } + + this.assertLayout(expectedLayout); + }); + }); + }); + + [ + { + items: [{ }, { }, { }, { }], + scenarios: [ + { newCollapsedValue: false, paneIndex: 0, expectedLayout: ['25', '25', '25', '25'] }, + ] + }, + { + items: [{ size: '150px' }, { }, { }, { }], + scenarios: [ + { newCollapsedValue: true, paneIndex: 0, expectedLayout: ['0', '43.5792', '28.2104', '28.2104'] }, + { newCollapsedValue: true, paneIndex: 1, expectedLayout: ['0', '0', '71.7896', '28.2104'] }, + { newCollapsedValue: false, paneIndex: 0, expectedLayout: ['15.3689', '0', '56.4208', '28.2104'] }, + ] + }, + { + items: [{ collapsed: true, size: '150px', collapsible: true }, { collapsible: true }, { collapsed: true, collapsible: true }, { }], + scenarios: [ + { newCollapsedValue: false, paneIndex: 0, expectedLayout: ['25', '25', '0', '50'] }, + { newCollapsedValue: false, paneIndex: 2, expectedLayout: ['25', '25', '25', '25'] }, + ] + }, + { + items: [{ collapsed: false, collapsible: true }, { collapsed: true, collapsible: true }, { collapsible: true }, { collapsed: true, size: '150px', collapsible: true }], + scenarios: [ + { newCollapsedValue: false, paneIndex: 3, expectedLayout: ['50', '0', '25', '25'] }, + { newCollapsedValue: false, paneIndex: 1, expectedLayout: ['50', '12.5', '12.5', '25'] }, + ] + }, + { + items: [{ collapsed: true, collapsible: true }, { collapsed: true, collapsible: true }, { collapsible: true }, { collapsed: false, collapsible: true }], + scenarios: [ + { newCollapsedValue: false, paneIndex: 1, expectedLayout: ['0', '25', '25', '50'] }, + { newCollapsedValue: false, paneIndex: 0, expectedLayout: ['12.5', '12.5', '25', '50'] }, + { newCollapsedValue: true, paneIndex: 1, expectedLayout: ['12.5', '0', '37.5', '50'] }, + ] + }, + { + items: [{ collapsed: true, collapsible: true }, { collapsed: true, collapsible: true }, { collapsible: true }, { size: '20%', collapsed: false, collapsible: true }], + scenarios: [ + { newCollapsedValue: false, paneIndex: 0, expectedLayout: ['39.7541', '0', '39.7541', '20.4655674103'] }, + { newCollapsedValue: false, paneIndex: 1, expectedLayout: ['39.7541', '19.877', '19.877', '20.4655674103'] }, + { newCollapsedValue: true, paneIndex: 0, expectedLayout: ['0', '59.6311', '19.877', '20.4655674103'] }, + ] + } + ].forEach(({ items, scenarios }) => { + QUnit.test(`The pane should restore its size after collapsing and expanding at runtime, items: ${JSON.stringify(items)}`, function(assert) { + this.reinit({ items }); + + scenarios.forEach((scenario) => { + const { newCollapsedValue, paneIndex, expectedLayout } = scenario; + + this.instance.option(`items[${paneIndex}].collapsed`, newCollapsedValue); + + this.assertLayout(expectedLayout); + }); + }); + }); + [{ items: [{ collapsible: true }, { collapsible: true }], expectedLayout: ['0', '100'], @@ -999,7 +1147,7 @@ QUnit.module('Pane sizing', moduleConfig, () => { this.instance.option('items[0].collapsed', true); - this.assertLayout(['0', '33.3333', '66.6667']); + this.assertLayout(['0', '66.6667', '33.3333']); }); QUnit.test('Whole layout should be repositined on change Pane.collapsedSize option at runtime', function(assert) { @@ -2350,7 +2498,6 @@ QUnit.module('Events', moduleConfig, () => { dataSource: [{ text: 'Pane_1' }, { text: 'Pane_2' }] }); - const pointer = pointerMock(this.getResizeHandles(false).eq(0)); pointer.start().dragStart().drag(0, 50).dragEnd(); @@ -2494,6 +2641,38 @@ QUnit.module('Events', moduleConfig, () => { assert.strictEqual(onItemExpanded.callCount, 1, 'onItemExpanded called'); }); + QUnit.test('onItemExpanded should be called after change collapsed option at runtime', function(assert) { + const onItemCollapsed = sinon.stub(); + const onItemExpanded = sinon.stub(); + + this.reinit({ + onItemCollapsed, + onItemExpanded, + dataSource: [{ collapsed: true, collapsible: true }, { collapsible: true }] + }); + + this.instance.option('items[0].collapsed', false); + + assert.strictEqual(onItemCollapsed.callCount, 0, 'onItemCollapsed not called'); + assert.strictEqual(onItemExpanded.callCount, 1, 'onItemExpanded called'); + }); + + QUnit.test('onItemCollapsed should be called after change collapsed option at runtime', function(assert) { + const onItemCollapsed = sinon.stub(); + const onItemExpanded = sinon.stub(); + + this.reinit({ + onItemCollapsed, + onItemExpanded, + dataSource: [{ collapsed: false, collapsible: true }, { collapsible: true }] + }); + + this.instance.option('items[0].collapsed', true); + + assert.strictEqual(onItemCollapsed.callCount, 1, 'onItemCollapsed called'); + assert.strictEqual(onItemExpanded.callCount, 0, 'onItemExpanded not called'); + }); + QUnit.test('Two clicks on collapse buttons should not trigger double click event on resizeHandle', function(assert) { const doubleClickStub = sinon.stub(); @@ -2577,6 +2756,70 @@ QUnit.module('Events', moduleConfig, () => { }); }); + QUnit.test('onItemCollapsed should have correct argument fields on item collapse at runtime', function(assert) { + assert.expect(8); + + this.reinit({ + onItemCollapsed: (e) => { + const { component, element, event, itemData, itemElement, itemIndex } = e; + + const $item = this.getPanes().eq(0); + + assert.strictEqual(component, this.instance, 'component field is correct'); + assert.strictEqual(isRenderer(element), !!config().useJQuery, 'element is correct'); + assert.strictEqual($(element).is(this.$element), true, 'element field is correct'); + assert.strictEqual(event, undefined, 'event is not defined'); + assert.strictEqual(isRenderer(itemElement), !!config().useJQuery, 'itemElement is correct'); + assert.strictEqual($(itemElement).is($item), true, 'itemElement field is correct'); + assert.deepEqual(itemData, { collapsed: true, size: 0, collapsible: true }, 'itemData field is correct'); + assert.strictEqual(itemIndex, 0, 'itemIndex is correct'); + }, + dataSource: [{ collapsible: true, }, { collapsible: true, }] + }); + + this.instance.option('items[0].collapsed', true); + }); + + QUnit.test('onItemExpanded should have correct argument fields on item expand at runtime', function(assert) { + assert.expect(8); + + this.reinit({ + onItemExpanded: (e) => { + const { component, element, event, itemData, itemElement, itemIndex } = e; + + const $item = this.getPanes().eq(0); + + assert.strictEqual(component, this.instance, 'component field is correct'); + assert.strictEqual(isRenderer(element), !!config().useJQuery, 'element is correct'); + assert.strictEqual($(element).is(this.$element), true, 'element field is correct'); + assert.strictEqual(event, undefined, 'event is not defined'); + assert.strictEqual(isRenderer(itemElement), !!config().useJQuery, 'itemElement is correct'); + assert.strictEqual($(itemElement).is($item), true, 'itemElement field is correct'); + assert.strictEqual(itemData.collapsed, false, 'itemData is correct'); + assert.strictEqual(itemIndex, 0, 'itemIndex'); + }, + dataSource: [{ collapsed: true, collapsible: true }, { collapsible: true }] + }); + + this.instance.option('items[0].collapsed', false); + }); + + QUnit.test('onItemCollapsed events should not be called if non declared collapsed option is changing to false', function(assert) { + const onItemCollapsed = sinon.stub(); + const onItemExpanded = sinon.stub(); + + this.reinit({ + onItemCollapsed, + onItemExpanded, + dataSource: [{ collapsible: true }, { collapsible: true }] + }); + + this.instance.option('items[0].collapsed', false); + + assert.strictEqual(onItemCollapsed.callCount, 0, 'onItemCollapsed not called'); + assert.strictEqual(onItemExpanded.callCount, 0, 'onItemExpanded not called'); + }); + ['onItemCollapsed', 'onItemExpanded'].forEach(eventHandler => { QUnit.test(`${eventHandler} event handler should be able to be updated at runtime`, function(assert) { const handlerStub = sinon.stub();