diff --git a/CHANGELOG.md b/CHANGELOG.md index d733826c6..00c31f2c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### 0.79.5 (2024-07-11) + + +### Bug Fixes + +* **FEC-14034:** Player v7 | Safari | Opening the CC menu cause size a… ([#905](https://github.com/kaltura/playkit-js-ui/issues/905)) ([be28ea2](https://github.com/kaltura/playkit-js-ui/commit/be28ea2)), closes [#871](https://github.com/kaltura/playkit-js-ui/issues/871) + + + +### 0.79.4 (2024-07-04) + + +### Bug Fixes + +* **FEC-14023:** add strictPosition property to tooltip ([#901](https://github.com/kaltura/playkit-js-ui/issues/901)) ([7a9a78f](https://github.com/kaltura/playkit-js-ui/commit/7a9a78f)) + + + +### 0.79.3 (2024-06-30) + + +### Bug Fixes + +* **FEC-13506_REG:** fix PR [#871](https://github.com/kaltura/playkit-js-ui/issues/871) regression ([#895](https://github.com/kaltura/playkit-js-ui/issues/895)) ([97b1aa4](https://github.com/kaltura/playkit-js-ui/commit/97b1aa4)) + + + ### 0.79.2 (2024-06-02) diff --git a/karma.conf.js b/karma.conf.js index 493df6588..af4d8639b 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -33,6 +33,10 @@ module.exports = function (config) { ChromeHeadlessWithFlags: { base: 'ChromeHeadless', flags: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required', '--mute-audio', '--max-web-media-player-count=1000'] + }, + ChromeWithFlags: { + base: 'Chrome', + flags: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required', '--mute-audio', '--max-web-media-player-count=1000'] } }, browsers: ['ChromeHeadlessWithFlags'], diff --git a/package.json b/package.json index 7990c37fe..5b40b83a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@playkit-js/playkit-js-ui", - "version": "0.79.2", + "version": "0.79.5", "description": "", "keywords": [ "kaltura", @@ -41,7 +41,7 @@ "prettier": "prettier --write .", "test": "karma start karma.conf.js", "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", "clean": "rimraf ./dist", "prebuild": "npm run clean", "precommit": "npm run build:prod && npm run type-check && npm run lint:fix", diff --git a/src/components/advanced-audio-desc/advanced-audio-desc.tsx b/src/components/advanced-audio-desc/advanced-audio-desc.tsx index 1cbb4c55a..a1e783438 100644 --- a/src/components/advanced-audio-desc/advanced-audio-desc.tsx +++ b/src/components/advanced-audio-desc/advanced-audio-desc.tsx @@ -9,6 +9,9 @@ import {connect} from 'react-redux'; import {ButtonControl} from '../button-control'; import {bindActions} from '../../utils'; import {actions} from '../../reducers/settings'; +import {IconComponent, registerToBottomBar} from '../bottom-bar'; +import {redux} from '../../index'; +import {withPlayer} from '../player'; const COMPONENT_NAME = 'AdvancedAudioDesc'; @@ -29,12 +32,38 @@ const mapStateToProps = state => ({ * @extends {Component} */ @connect(mapStateToProps, bindActions(actions)) +@withPlayer @withEventDispatcher(COMPONENT_NAME) @withText({AdvancedAudioDescText: 'settings.advancedAudioDescription'}) -class AdvancedAudioDesc extends Component { +class AdvancedAudioDesc extends Component implements IconComponent { constructor(props: any) { super(); this.state = {toggle: false}; + registerToBottomBar(COMPONENT_NAME, props.player, () => this.registerComponent()); + } + + registerComponent(): any { + return { + ariaLabel: () => this.getComponentText(), + displayName: COMPONENT_NAME, + order: 5, + svgIcon: () => this.getSvgIcon(), + onClick: () => this.onClick(), + component: () => { + return getComponent({...this.props, classNames: [style.upperBarIcon]}); + }, + shouldHandleOnClick: false + }; + } + + getComponentText = (): any => { + return this.props.AdvancedAudioDescText; + } + + getSvgIcon = (): any => { + return { + type: redux.useStore().getState().settings.advancedAudioDesc ? IconType.AdvancedAudioDescriptionActive : IconType.AdvancedAudioDescription + }; } /** @@ -69,7 +98,7 @@ class AdvancedAudioDesc extends Component { */ render({AdvancedAudioDescText, innerRef}: any): VNode | undefined { return !this._shouldRender() ? undefined : ( - + + + + ) : undefined; +}); + +const getComponent = (props: any): VNode => { + return ; +} + +CaptionsControlMini.displayName = COMPONENT_NAME; +export {CaptionsControlMini}; diff --git a/src/components/captions-control/captions-control.tsx b/src/components/captions-control/captions-control.tsx new file mode 100644 index 000000000..88496d0e0 --- /dev/null +++ b/src/components/captions-control/captions-control.tsx @@ -0,0 +1,120 @@ +import {h, Fragment} from 'preact'; +import {connect} from 'react-redux'; +import {CaptionsMenu} from '../captions-menu'; +import {ButtonControl} from '../button-control'; +import {Tooltip} from '../tooltip'; +import {Button} from '../button'; +import style from '../../styles/style.scss'; +import {Icon, IconType} from '../icon'; +import {SmartContainer} from '../smart-container'; +import {Text, withText} from 'preact-i18n'; +import {useRef, useState, useEffect} from 'preact/hooks'; +import {focusElement} from '../../utils'; +import {createPortal} from 'preact/compat'; +import {CVAAOverlay} from '../cvaa-overlay'; +import {CaptionsControlMini} from './captions-control-mini'; + +/** + * mapping state to props + * @param {*} state - redux store state + * @returns {Object} - mapped state to this component + */ +const mapStateToProps = state => ({ + textTracks: state.engine.textTracks, + showCCButton: state.config.showCCButton, + openMenuFromCCButton: state.config.openMenuFromCCButton, + isMobile: state.shell.isMobile, + isSmallSize: state.shell.isSmallSize, + isCVAAOverlayOpen: state.shell.isCVAAOverlayOpen +}); + +const COMPONENT_NAME = 'CaptionsControl'; + +/** + * CaptionsControl component + * + * @class CaptionsControl + * @example + * @extends {Component} + */ +const CaptionsControl = connect(mapStateToProps)( + withText({ + captionsLabelText: 'captions.captions' + })((props, context) => { + const [smartContainerOpen, setSmartContainerOpen] = useState(false); + const [cvaaOverlay, setCVAAOverlay] = useState(false); + const [ccOn, setCCOn] = useState(false); + const buttonRef = useRef(null); + const controlCaptionsElement = useRef(null); + + const {player} = context; + const {isSmallSize, isMobile, textTracks} = props; + const activeTextTrack = textTracks.find(textTrack => textTrack.active); + + const onControlButtonClick = (e?: KeyboardEvent, byKeyboard?: boolean): void => { + setSmartContainerOpen(smartContainerOpen => !smartContainerOpen); + if (byKeyboard && smartContainerOpen) { + focusElement(buttonRef.current); + } + }; + + const toggleCVAAOverlay = (): void => { + setCVAAOverlay(cvaaOverlay => !cvaaOverlay); + }; + + const onCVAAOverlayClose = (e?: KeyboardEvent, byKeyboard?: boolean): void => { + toggleCVAAOverlay(); + onControlButtonClick(e, byKeyboard); + }; + + const handleClickOutside = (e: any) => { + if (!isMobile && !isSmallSize && !!controlCaptionsElement.current && !controlCaptionsElement.current.contains(e.target)) { + setSmartContainerOpen(false); + } + }; + + useEffect(() => { + document.addEventListener('click', handleClickOutside); + return () => document.removeEventListener('click', handleClickOutside); + }, [isSmallSize, isMobile]); + + useEffect(() => { + setCCOn(activeTextTrack?.language !== 'off'); + }, [activeTextTrack]); + + const shouldRender = !!textTracks?.length && props.showCCButton && props.openMenuFromCCButton; + props.onToggle(COMPONENT_NAME, shouldRender); + if (!shouldRender) return undefined; + + const targetId: HTMLDivElement | Document = (document.getElementById(player.config.targetId) as HTMLDivElement) || document; + const portalSelector = `.overlay-portal`; + + return ( + <> + + + + + {smartContainerOpen && !cvaaOverlay && ( + setSmartContainerOpen(false)} title={}> + + + )} + {cvaaOverlay ? createPortal(, targetId.querySelector(portalSelector)!) :
} + + + + ); + }) +); + +CaptionsControl.displayName = COMPONENT_NAME; +export {CaptionsControl}; diff --git a/src/components/captions-control/index.ts b/src/components/captions-control/index.ts new file mode 100644 index 000000000..de9a877af --- /dev/null +++ b/src/components/captions-control/index.ts @@ -0,0 +1 @@ +export {CaptionsControl} from './captions-control'; diff --git a/src/components/captions-menu/captions-menu.tsx b/src/components/captions-menu/captions-menu.tsx index 7bde5c866..abc5c6253 100644 --- a/src/components/captions-menu/captions-menu.tsx +++ b/src/components/captions-menu/captions-menu.tsx @@ -10,6 +10,8 @@ import {withEventManager} from '../../event'; import {withLogger} from '../logger'; import {withEventDispatcher} from '../event-dispatcher'; import {withKeyboardEvent} from '../../components/keyboard'; +import {KeyboardEventHandlers} from '../../types'; +import {Menu} from '../menu'; /** * mapping state to props @@ -86,17 +88,21 @@ class CaptionsMenu extends Component { textOptions.push({label: props.advancedCaptionsSettingsText, value: props.advancedCaptionsSettingsText, active: false}); } - return ( - { - props.pushRef(el); - }} - icon={IconType.Captions} - label={this.props.captionsLabelText} - options={textOptions} - onMenuChosen={textTrack => this.onCaptionsChange(textTrack)} - /> - ); + if (this.props.asDropdown) { + return ( + { + props.pushRef(el); + }} + icon={IconType.Captions} + label={this.props.captionsLabelText} + options={textOptions} + onMenuChosen={textTrack => this.onCaptionsChange(textTrack)} + /> + ); + } else { + return this.onCaptionsChange(textTrack)} onClose={() => {}} />; + } } } diff --git a/src/components/closed-captions/closed-captions.tsx b/src/components/closed-captions/closed-captions.tsx index c5181bc7b..d58054f6e 100644 --- a/src/components/closed-captions/closed-captions.tsx +++ b/src/components/closed-captions/closed-captions.tsx @@ -1,5 +1,5 @@ import style from '../../styles/style.scss'; -import {h} from 'preact'; +import {h, VNode} from 'preact'; import {useEffect, useState} from 'preact/hooks'; import {withText} from 'preact-i18n'; import {Icon, IconType} from '../icon'; @@ -9,6 +9,9 @@ import {withLogger} from '../logger'; import {Tooltip} from '../tooltip'; import {Button} from '../button'; import {ButtonControl} from '../button-control'; +import {registerToBottomBar} from '../bottom-bar'; +import {redux} from '../../index'; + /** * mapping state to props * @param {*} state - redux store state @@ -16,7 +19,8 @@ import {ButtonControl} from '../button-control'; */ const mapStateToProps = state => ({ textTracks: state.engine.textTracks, - showCCButton: state.config.showCCButton + showCCButton: state.config.showCCButton, + openMenuFromCCButton: state.config.openMenuFromCCButton }); const COMPONENT_NAME = 'ClosedCaptions'; @@ -44,11 +48,50 @@ const ClosedCaptions = connect(mapStateToProps)( setCCOn(activeTextTrack?.language !== 'off'); }, [activeTextTrack]); - const shouldRender = !!(props.textTracks?.length && props.showCCButton); + useEffect(() => { + registerToBottomBar(COMPONENT_NAME, player, () => registerComponent()); + }, []); + + const registerComponent = (): any => { + return { + ariaLabel: () => getAriaLabel(), + displayName: COMPONENT_NAME, + order: 5, + svgIcon: () => getSvgIcon(), + onClick: () => onClick(), + component: () => { + return getComponent({...props, classNames: [style.upperBarIcon]}); + }, + shouldHandleOnClick: false + }; + }; + + const isCaptionsEnabled = (): boolean => { + return redux.useStore().getState().settings.isCaptionsEnabled; + }; + + const getAriaLabel = (): any => { + return isCaptionsEnabled() ? props.closedCaptionsOnText : props.closedCaptionsOffText; + }; + + const getSvgIcon = (): any => { + return { + type: isCaptionsEnabled() ? IconType.ClosedCaptionsOn : IconType.ClosedCaptionsOff + }; + }; + + const onClick = () => { + const isCCOn = isCaptionsEnabled(); + props.notifyClick(isCCOn); + isCCOn ? player.hideTextTrack() : player.showTextTrack(); + }; + + const shouldRender = !!textTracks?.length && props.showCCButton && !props.openMenuFromCCButton; props.onToggle(COMPONENT_NAME, shouldRender); if (!shouldRender) return undefined; + return ( - + {ccOn ? (