Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(FEC-13945): move core controls to upper bar #59

Merged
merged 8 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,15 @@ module.exports = function (config) {
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,

// start these browsers
// custom browser launcher - chrome with flags
customLaunchers: {
ChromeWithFlags: {
base: 'Chrome',
flags: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required', '--mute-audio', '--max-web-media-player-count=1000']
}
},

// start these browsers by default
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromeHeadless'],

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"build": "webpack --mode production",
"test": "karma start --color --mode development",
"test:debug": "DEBUG_UNIT_TESTS=1 karma start karma.conf.js --auto-watch --no-single-run --browsers Chrome",
"test:watch": "karma start karma.conf.js --auto-watch --no-single-run",
"test:watch": "karma start karma.conf.js --auto-watch --no-single-run --browsers ChromeWithFlags",
"types:check": "tsc src/index.ts src/types/global.d.ts --jsx react --jsxFactory h --noEmit --target ESNext --moduleResolution node --experimentalDecorators --jsxFragmentFactory Fragment",
"types:generate": "tsc src/index.ts src/types/global.d.ts --declaration --emitDeclarationOnly --outFile types/modules.ts --jsx react --jsxFactory h --jsxFragmentFactory Fragment --stripInternal true --target ESNext --moduleResolution node --experimentalDecorators",
"docs:generate": "typedoc",
Expand Down
6 changes: 5 additions & 1 deletion src/services/upper-bar-manager/models/icon-dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface IconDto {
/**
* Icon that will appear in the dropdown menu
*/
svgIcon: SvgIcon;
svgIcon: SvgIcon | (() => SvgIcon);
/**
* The icon handler
*
Expand All @@ -35,4 +35,8 @@ export interface IconDto {
* Relevant presets for the icon
*/
presets?: PlaykitUI.ReservedPresetName[];
/**
* An indication whether the upper bar should handle the onClick callback of the component or not
*/
shouldHandleOnClick?: boolean;
}
4 changes: 3 additions & 1 deletion src/services/upper-bar-manager/models/icon-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export class IconModel {
public componentRef: RefObject<IconWrapper>;
public onClick: (e: MouseEvent | KeyboardEvent) => void;
public component: ComponentClass<Record<string, never>> | FunctionalComponent<Record<string, never>>;
public svgIcon: SvgIcon;
public svgIcon: SvgIcon | (() => SvgIcon);
Copy link
Contributor

@JonathanTGold JonathanTGold Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it should be a function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the controls that I handled (PiP, CC, AAD, etc..) have 2 possible icons, which depends on the system's state. Hence, I added to the type a function that returns the SvgIcon.

public presets: PlaykitUI.ReservedPresetName[];
public shouldHandleOnClick: boolean;
constructor(item: IconDto) {
this.id = ++IconModel.nextId;
this.displayName = item.displayName;
Expand All @@ -27,6 +28,7 @@ export class IconModel {
this.componentRef = createRef();
this.presets =
item.presets && item.presets.length > 0 ? item.presets : [ReservedPresetNames.Playback, ReservedPresetNames.Live];
this.shouldHandleOnClick = typeof item.shouldHandleOnClick === 'boolean' ? item.shouldHandleOnClick : true;
Copy link
Contributor

@JonathanTGold JonathanTGold Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this ? why just check if on click is supplied or not

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new shouldHandleOnClick flag is indicating whether the upper bar should call the icon's onClick or not.
the reason is that the controls i handled to move to the upper bar (PiP, CC, AAD, etc..) are not only icons like the vamb plugin icons- they are actually the component itself; the way they behave in bottom bar- same in upper bar. which means, they have their own states and handlers.

another explanation:
let's say we don't have that flag, then the onClick will happen twice- 1 time from within the component and 1 time from the upper bar. Thus, the solution for this issue was to tell the upper bar- "I can handle myself, don't trigger my onClick".

}

public update(): void {
Expand Down
56 changes: 56 additions & 0 deletions src/services/upper-bar-manager/move-controls-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { KalturaPlayer, Logger } from '@playkit-js/kaltura-player-js';
import { UpperBarManager } from './upper-bar-manager';
import { IconDto } from './models/icon-dto';

export class MoveControlsManager {
private readonly player: KalturaPlayer;
private readonly logger: Logger;
private store: any;
private upperBarManager: UpperBarManager;
private currentState: any;
private iconIds: Map<string, number>;

constructor(player: KalturaPlayer, logger: Logger, upperBarManager: UpperBarManager, redux: any) {
this.player = player;
this.logger = logger;
this.store = redux.useStore();
this.upperBarManager = upperBarManager;
this.store.subscribe(this.handleStoreChange.bind(this));
this.currentState = this.store.getState();
this.iconIds = new Map();
}

private get bottomBarRegistryManager(): any {
return (this.player.getService('bottomBarRegistryManager') as any) || undefined;
SivanA-Kaltura marked this conversation as resolved.
Show resolved Hide resolved
}

private get state(): any {
return this.store.getState();
}

private handleStoreChange(): void {
const newState = this.state;
const bottomBarRegistryManager = this.bottomBarRegistryManager;
if (bottomBarRegistryManager && this.currentState.bottomBar !== newState.bottomBar) {
SivanA-Kaltura marked this conversation as resolved.
Show resolved Hide resolved
this.logger.debug('Removing core controls from upper bar');
SivanA-Kaltura marked this conversation as resolved.
Show resolved Hide resolved
// remove all the core icons and clear map
[...this.iconIds.values()].forEach((iconId) => this.upperBarManager.remove(iconId));
this.iconIds.clear();

const { controlsToMove } = newState.bottomBar;
if (controlsToMove.length > 0) {
this.logger.debug('Adding core controls to upper bar: ', controlsToMove);
controlsToMove.forEach((componentName: string) => {
const componentToMove: IconDto = bottomBarRegistryManager.getComponentItem(componentName);
if (componentToMove) {
const iconId = this.upperBarManager.add(componentToMove);
if (typeof iconId === 'number') {
SivanA-Kaltura marked this conversation as resolved.
Show resolved Hide resolved
this.iconIds.set(componentName, iconId);
}
}
});
}
this.currentState = newState;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.right-upper-bar-wrapper-container {
direction: ltr;
display: flex;
align-items: center;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { h, Component, ComponentChild, RefObject } from 'preact';
import { IconModel } from '../../models/icon-model';
import { IconWrapper } from '../icon-wrapper/icon-wrapper.component';
import * as styles from './displayed-bar.component.scss';
import { ui } from '@playkit-js/kaltura-player-js';
import { KalturaPlayer, ui } from '@playkit-js/kaltura-player-js';
import { MoreIcon } from '../more-icon/more-icon.component';

const { PLAYER_SIZE } = ui.Components;
Expand All @@ -22,6 +22,7 @@ type DisplayedBarState = {
type DisplayedBarProps = {
getControls: () => IconModel[];
ref: RefObject<DisplayedBar>;
player: KalturaPlayer;
};
type PropsFromRedux = {
playerSize?: string;
Expand Down Expand Up @@ -85,13 +86,15 @@ export class DisplayedBar extends Component<DisplayedBarProps & PropsFromRedux,
const { displayedControls, dropdownControls } = this.splitControlsIntoDisplayedAndDropdown();
return displayedControls.length > 0 ? (
<div className={styles.rightUpperBarWrapperContainer}>
{displayedControls.map(({ id, component, onClick, componentRef }) => {
{displayedControls.map(({ id, component, onClick, componentRef, shouldHandleOnClick }) => {
const IconWrapperComponent = component!;
return (
<IconWrapper
key={id}
onClick={(...e): void => {
onClick(...e);
if (shouldHandleOnClick) {
onClick(...e);
}
this.closeDropdown();
}}
ref={componentRef}
Expand All @@ -101,7 +104,12 @@ export class DisplayedBar extends Component<DisplayedBarProps & PropsFromRedux,
);
})}
{dropdownControls.length > 0 && (
<MoreIcon showDropdown={this.state.showDropdown} onClick={this.handleOnClick} icons={dropdownControls} />
<MoreIcon
showDropdown={this.state.showDropdown}
onClick={this.handleOnClick}
icons={dropdownControls}
player={this.props.player}
/>
)}
</div>
) : undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@import '~@playkit-js/playkit-js-ui';

.dropdown-item {
border-radius: 4px;
padding: 4px 12px 4px 15px;
display: flex;
margin: 4px 0;
cursor: pointer;
align-items: center;

.icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;

i {
display: inline-block;
}
}

&:hover {
background-color: $tone-6-color;
}

.dropdown-item-description {
flex: 1;
font-size: 14px;
font-weight: 700;
padding-left: 11px;
overflow: hidden;
white-space: nowrap;

&.trim-text {
text-overflow: ellipsis;
}
}

.comparison-text {
position: absolute;
font-size: 14px;
font-weight: 700;
left: 0;
padding: 0;
}
}

.more-item-tooltip {
z-index: 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { h, Fragment, VNode } from 'preact';
import { useState, useRef, useLayoutEffect } from 'preact/hooks';
import * as styles from './dropdown-bar-item.scss';
import { ui } from '@playkit-js/kaltura-player-js';
import { A11yWrapper } from '@playkit-js/common/dist/hoc/a11y-wrapper';
import { SvgIcon } from '../../models/svg-icon';
const { Icon, Tooltip } = ui.Components;

type DropdownBarItemProps = {
displayName: string;
text: string;
icon: SvgIcon;
onClick: (e: KeyboardEvent | MouseEvent) => void;
onDropdownClick: () => void;
tooltipPosition: string;
};

const PADDING = 11;

const DropdownBarItem = ({ displayName, text, icon, onClick, onDropdownClick, tooltipPosition }: DropdownBarItemProps) => {
const comparisonTextRef = useRef<HTMLSpanElement | null>(null);
const textRef = useRef<HTMLSpanElement | null>(null);

const [showTooltip, setShowTooltip] = useState(false);
const [isFinalized, setIsFinalized] = useState(false);
SivanA-Kaltura marked this conversation as resolved.
Show resolved Hide resolved

useLayoutEffect(() => {
if (!isFinalized && textRef?.current && comparisonTextRef?.current) {
setIsFinalized(true);
const textWidth = textRef?.current.getBoundingClientRect().width - PADDING;
const comparisonTextWidth = comparisonTextRef?.current.getBoundingClientRect().width;
setShowTooltip(comparisonTextWidth > textWidth);
}
});

const renderIcon = (): VNode => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return <Icon type={icon.type} id={displayName} path={icon.path} viewBox={icon.viewBox || '0 0 32 32'} />;
};

const textElement = (
<span className={[styles.dropdownItemDescription, showTooltip ? styles.trimText : ''].join(' ')} ref={textRef}>
{text}
</span>
);
const comparisonTextElement = (
<span ref={comparisonTextRef} className={styles.comparisonText}>
{text}
</span>
);
const content = !isFinalized ? (
<>
{textElement}
{comparisonTextElement}
</>
) : (
textElement
);

const renderContent = (): VNode => {
return (
<A11yWrapper
onClick={(e): void => {
onClick(e);
onDropdownClick();
}}
role="menuitem"
>
<div className={styles.dropdownItem} tabIndex={0} aria-label={text}>
<div className={styles.icon}>{renderIcon()}</div>
{content}
</div>
</A11yWrapper>
);
};
return (
<Fragment>
{showTooltip ? (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<Tooltip label={text} type={tooltipPosition} className={styles.moreItemTooltip}>
{renderContent()}
</Tooltip>
) : (
renderContent()
)}
</Fragment>
);
};

export { DropdownBarItem };
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,10 @@
.more-dropdown {
position: absolute;
padding: 8px 4px;
width: 166px; // replace to min-width and grow with text
width: 200px; // replace to min-width and grow with text
background-color: $tone-7-color;
border-radius: 4px;
top: 44px;
right: 0;

.dropdown-item {
border-radius: 4px;
padding: 4px 12px 4px 15px;
display: flex;
margin: 4px 0;
cursor: pointer;

.icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}

&:hover {
background-color: $tone-6-color;
}

.dropdown-item-description {
display: flex;
flex: 1;
font-size: 14px;
font-weight: 700;
align-items: center;
padding-left: 11px;
}
}
overflow: hidden;
}
Loading
Loading