Skip to content

Commit

Permalink
Merge pull request #650 from bitmovin/feature/PW-11902/improved-drop-…
Browse files Browse the repository at this point in the history
…down-list-auto-hide-behavior

[PW-11902] Improve auto-hide behavior with active dropdown menus
  • Loading branch information
wasp898 authored Sep 6, 2024
2 parents aab5b98 + 150bf10 commit b93fcd3
Show file tree
Hide file tree
Showing 13 changed files with 782 additions and 81 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added
- `Component` now has a `ViewMode` that can either be `Persistent` or `Temporary`

### Fixed
- `selectbox` dropdown not closing in Safari when the UI is hidden

### Changed
- `selectbox` now sets its `ViewMode` to `Persistent` whenever and as long as the select-dropdown is shown
- `uicontainer` and `settingspanel` will no longer auto-hide if there are any components that are in the `Persistent` view mode

## [3.72.0] - 2024-08-30

### Added
Expand Down
325 changes: 325 additions & 0 deletions spec/components/selectbox.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
import type { PlayerAPI } from 'bitmovin-player';

import type { Component, ViewModeChangedEventArgs } from '../../src/ts/components/component';
import { ViewMode } from '../../src/ts/components/component';
import type { ListSelectorConfig } from '../../src/ts/components/listselector';
import { SelectBox } from '../../src/ts/components/selectbox';
import type { Event } from '../../src/ts/eventdispatcher';
import { PlayerUtils } from '../../src/ts/playerutils';
import type { UIInstanceManager } from '../../src/ts/uimanager';
import { MockHelper } from '../helper/MockHelper';
import getUiInstanceManagerMock = MockHelper.getUiInstanceManagerMock;
import getPlayerMock = MockHelper.getPlayerMock;
import generateDOMMock = MockHelper.generateDOMMock;
import PlayerState = PlayerUtils.PlayerState;
import type { DOM } from '../../src/ts/dom';

jest.mock('../../src/ts/dom', generateDOMMock);

describe('SelectBox', () => {
let selectBox: SelectBox;
let playerMock: PlayerAPI;
let uiManagerMock: UIInstanceManager;

beforeEach(() => {
selectBox = new SelectBox();
playerMock = getPlayerMock();
uiManagerMock = getUiInstanceManagerMock();
});

describe('viewMode', () => {
it('should initialize the `ViewMode` to `Temporary`', () => {
expect(selectBox['viewMode']).toEqual(ViewMode.Temporary);
});
});

describe('configure', () => {
test.each`
event
${'onShow'}
${'onHide'}
${'onViewModeChanged'}
`('should subscribe to "$event"', ({ event }) => {
const subscribeSpy = jest.spyOn(selectBox[event as keyof SelectBox] as Event<unknown, unknown>, 'subscribe');

selectBox.configure(playerMock, uiManagerMock);

expect(subscribeSpy).toHaveBeenCalled();
});

test.each`
event
${'mouseenter'}
${'mouseleave'}
`('should add a "$event" listener to the DOM element', ({ event }) => {
const onSpy = jest.spyOn(selectBox.getDomElement(), 'on');

selectBox.configure(playerMock, uiManagerMock);

expect(onSpy).toHaveBeenCalledWith(event, expect.any(Function));
});
});

describe('onViewModeChangedEvent', () => {
let viewModeChangedSpy: jest.Mock<void, [Component<ListSelectorConfig>, ViewModeChangedEventArgs]>;

beforeEach(() => {
viewModeChangedSpy = jest.fn();
selectBox.onViewModeChanged.subscribe(viewModeChangedSpy);
});

it('should dispatch the onViewModeChanged event', () => {
selectBox['onViewModeChangedEvent'](ViewMode.Persistent);

expect(viewModeChangedSpy).toHaveBeenCalledWith(expect.any(SelectBox), { mode: ViewMode.Persistent });
});

it('should not dispatch the onViewModeChanged event if the view mode did not change', () => {
selectBox['onViewModeChangedEvent'](ViewMode.Temporary);

expect(viewModeChangedSpy).not.toHaveBeenCalled();
});
});

describe('toDomElement', () => {
test.each`
event
${'onDisabled'}
${'onHide'}
`('should subscribe to "$event" to close the dropdown', ({ event }) => {
const subscribeSpy = jest.spyOn(selectBox[event as keyof SelectBox] as Event<unknown, unknown>, 'subscribe');

selectBox.getDomElement();

expect(subscribeSpy).toHaveBeenCalledWith(selectBox.closeDropdown);
});

it('should add event dropdown open event listeners', () => {
const domElement = selectBox.getDomElement();

expect(domElement.on).toHaveBeenCalled();
});
});

describe('configure', () => {
it('should subscribe to player state changes', () => {
const uiContainer = uiManagerMock.getUI();

selectBox.configure(playerMock, uiManagerMock);

expect(uiContainer.onPlayerStateChange().subscribe).toHaveBeenCalledWith(selectBox['onPlayerStateChange']);
});
});

describe('closeDropdown', () => {
it('should call blur on the DOM select element', () => {
const element = document.createElement('select');
const blurSpy = jest.spyOn(element, 'blur');

jest.spyOn(selectBox as any, 'getSelectElement').mockReturnValue(element);
selectBox.closeDropdown();

expect(blurSpy).toHaveBeenCalled();
});
});

describe('onPlayerStateChange', () => {
test.each`
playerState | shouldClose
${PlayerState.Idle} | ${true}
${PlayerState.Finished} | ${true}
${PlayerState.Paused} | ${false}
${PlayerState.Playing} | ${false}
${PlayerState.Prepared} | ${false}
`(
`should close the dropdown=$shouldClose when the player state changes to $playerState`,
({ playerState, shouldClose }) => {
const closeDropdownSpy = jest.spyOn(selectBox, 'closeDropdown');

selectBox['onPlayerStateChange'](uiManagerMock.getUI(), playerState);

if (shouldClose) {
expect(closeDropdownSpy).toHaveBeenCalled();
} else {
expect(closeDropdownSpy).not.toHaveBeenCalled();
}
},
);
});

describe('onDropdownOpened', () => {
it('should clear the existing closed event listener addition timeout', () => {
const clearTimeoutSpy = jest.spyOn(window, 'clearTimeout');

selectBox['onDropdownOpened']();

expect(clearTimeoutSpy).toHaveBeenCalled();
});

it('should start a timeout to add closed event listeners', () => {
const setTimeoutSpy = jest.spyOn(window, 'setTimeout');

selectBox['onDropdownOpened']();

expect(setTimeoutSpy).toHaveBeenCalled();
});

it('should change the view mode to `Persistent`', () => {
const onViewModeChangedSpy = jest.fn();

selectBox.onViewModeChanged.subscribe(onViewModeChangedSpy);
selectBox['onDropdownOpened']();

expect(onViewModeChangedSpy).toHaveBeenCalledWith(expect.any(SelectBox), { mode: ViewMode.Persistent });
});
});

describe('onDropdownClosed', () => {
it('should clear the closed event listener addition timeout', () => {
const clearTimeoutSpy = jest.spyOn(window, 'clearTimeout');

selectBox['onDropdownClosed']();

expect(clearTimeoutSpy).toHaveBeenCalled();
});

it('should remove the close event listeners', () => {
const removeDropdownCloseListenersSpy = jest.fn();

selectBox['removeDropdownCloseListeners'] = removeDropdownCloseListenersSpy;
selectBox['onDropdownClosed']();

expect(removeDropdownCloseListenersSpy).toHaveBeenCalled();
});

it('should change the view mode to `Temporary`', () => {
const onViewModeChangedSpy = jest.fn();

selectBox['viewMode'] = ViewMode.Persistent;
selectBox.onViewModeChanged.subscribe(onViewModeChangedSpy);
selectBox['onDropdownClosed']();

expect(onViewModeChangedSpy).toHaveBeenCalledWith(expect.any(SelectBox), { mode: ViewMode.Temporary });
});
});

describe('addDropdownCloseListeners', () => {
let selectElement: DOM;

beforeEach(() => {
selectElement = { on: jest.fn(), off: jest.fn() } as unknown as DOM;
selectBox['selectElement'] = selectElement;
});

it('should remove existing close listeners', () => {
const removeDropdownCloseListenersSpy = jest.fn();

selectBox['removeDropdownCloseListeners'] = removeDropdownCloseListenersSpy;
selectBox['addDropdownCloseListeners']();

expect(removeDropdownCloseListenersSpy).toHaveBeenCalled();
});

it('should clear the closed event listener addition timeout', () => {
const clearTimeoutSpy = jest.spyOn(window, 'clearTimeout');

selectBox['addDropdownCloseListeners']();

expect(clearTimeoutSpy).toHaveBeenCalled();
});

it('should add close event listeners to the document', () => {
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');

selectBox['addDropdownCloseListeners']();

expect(addEventListenerSpy).toHaveBeenCalled();
});

it('should add close event listeners to the select element', () => {
selectBox['addDropdownCloseListeners']();

expect(selectElement.on).toHaveBeenCalled();
});

it('should set the removeDropdownCloseListeners', () => {
const initialRemoveDropdownCloseListenersFunction = selectBox['removeDropdownCloseListeners'];

selectBox['addDropdownCloseListeners']();

const newRemoveDropdownCloseListenersFunction = selectBox['removeDropdownCloseListeners'];

expect(initialRemoveDropdownCloseListenersFunction).not.toEqual(newRemoveDropdownCloseListenersFunction);
});
});

describe('removeDropdownCloseListeners', () => {
let selectElement: DOM;

beforeEach(() => {
selectElement = { on: jest.fn(), off: jest.fn() } as unknown as DOM;
selectBox['selectElement'] = selectElement;
selectBox['addDropdownCloseListeners']();
});

it('should remove close event listeners to the document', () => {
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');

selectBox['removeDropdownCloseListeners']();

expect(removeEventListenerSpy).toHaveBeenCalled();
});

it('should remove close event listeners from the select element', () => {
selectBox['removeDropdownCloseListeners']();

expect(selectElement.off).toHaveBeenCalled();
});
});

describe('addDropdownOpenedListeners', () => {
let selectElement: DOM;

beforeEach(() => {
selectElement = { on: jest.fn(), off: jest.fn() } as unknown as DOM;
selectBox['selectElement'] = selectElement;
});

it('should remove existing open listeners', () => {
const removeDropdownOpenedListenersSpy = jest.fn();

selectBox['removeDropdownOpenedListeners'] = removeDropdownOpenedListenersSpy;
selectBox['addDropdownOpenedListeners']();

expect(removeDropdownOpenedListenersSpy).toHaveBeenCalled();
});

it('should add event listener on the select element', () => {
selectBox['addDropdownOpenedListeners']();

expect(selectElement.on).toHaveBeenCalled();
});

it('should set the removeDropdownOpenedListeners', () => {
const initialRemoveDropdownOpenedListenersFunction = selectBox['removeDropdownOpenedListeners'];

selectBox['addDropdownOpenedListeners']();

const newRemoveDropdownOpenedListenersFunction = selectBox['removeDropdownOpenedListeners'];

expect(initialRemoveDropdownOpenedListenersFunction).not.toEqual(newRemoveDropdownOpenedListenersFunction);
});
});

describe('removeDropdownOpenedListeners', () => {
it('should remove opened event listeners from the select element', () => {
const selectElement = { on: jest.fn(), off: jest.fn() } as unknown as DOM;
selectBox['selectElement'] = selectElement;
selectBox['addDropdownOpenedListeners']();

selectBox['removeDropdownOpenedListeners']();

expect(selectElement.off).toHaveBeenCalled();
});
});
});
Loading

0 comments on commit b93fcd3

Please sign in to comment.