@playkit-js/transcript 3.7.11 → 3.7.12-canary.0-e2073db

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playkit-js/transcript",
3
- "version": "3.7.11",
3
+ "version": "3.7.12-canary.0-e2073db",
4
4
  "main": "dist/playkit-transcript.js",
5
5
  "license": "AGPL-3.0",
6
6
  "private": false,
@@ -153,6 +153,14 @@ class PopoverMenu extends Component<PopoverMenuProps, PopoverMenuState> {
153
153
  const padding = 14;
154
154
  popOverHeight = popOverMenuHeight - neededHeight <= 0 ? popOverMenuHeight - padding : neededHeight - padding;
155
155
  }
156
+ let popoverStyle = {};
157
+ if (shouldUseCalculatedHeight) {
158
+ if (kitchenSinkDetached) {
159
+ popoverStyle = { height: `${popOverHeight}px` };
160
+ } else {
161
+ popoverStyle = { height: `${popOverHeight}px`, overflowY: 'auto' };
162
+ }
163
+ }
156
164
 
157
165
  const popoverMenuContent = (
158
166
  <div className={styles.popoverContainer}>
@@ -179,7 +187,7 @@ class PopoverMenu extends Component<PopoverMenuProps, PopoverMenuState> {
179
187
  <div
180
188
  className={styles.popoverComponent}
181
189
  onKeyUp={this._handleKeyupEvent}
182
- style={shouldUseCalculatedHeight ? {height: `${popOverHeight}px`, overflowY: 'auto'} : {}}
190
+ style={popoverStyle}
183
191
  role="menu"
184
192
  aria-expanded={this.state.isOpen}
185
193
  id="popoverContent"
@@ -0,0 +1,2 @@
1
+ import {PopoverOverlay} from './popover-overlay';
2
+ export {PopoverOverlay};
@@ -0,0 +1,94 @@
1
+ @import './variables.scss';
2
+
3
+ .popoverOverlayContainer {
4
+ display: flex;
5
+ flex-direction: column;
6
+ justify-content: center;
7
+ align-items: center;
8
+ text-align: center;
9
+ padding: 2rem 2rem;
10
+ flex-wrap: wrap;
11
+ overflow-x: hidden;
12
+
13
+ .overlayTitle {
14
+ font-size: 18px;
15
+ font-style: normal;
16
+ font-weight: 700;
17
+ line-height: 20px;
18
+ margin-bottom: 42px;
19
+ margin-top: 0;
20
+ font-family: 'Lato', sans-serif;
21
+ }
22
+
23
+ .languageSelector {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 0.75rem;
27
+ width: 100%;
28
+ max-width: 340px;
29
+ padding: 0.5rem 0;
30
+
31
+ svg {
32
+ width: 24px;
33
+ height: 24px;
34
+ }
35
+
36
+ label {
37
+ font-size: 14px;
38
+ font-weight: 400;
39
+ color: $tone-1-color;
40
+ }
41
+
42
+ select {
43
+ margin-left: auto;
44
+ width: auto;
45
+ text-align: right;
46
+ font-size: 14px;
47
+ font-weight: 400;
48
+ font-family: 'Lato', sans-serif;
49
+ line-height: 18px;
50
+ cursor: pointer;
51
+ outline: none;
52
+
53
+ option {
54
+ color: #000;
55
+ }
56
+ }
57
+ }
58
+
59
+ .actionButtons {
60
+ display: flex;
61
+ flex-direction: column;
62
+ width: 100%;
63
+ max-width: 340px;
64
+ text-align: left;
65
+
66
+ button {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 0.75rem;
70
+ background: none;
71
+ border: none;
72
+ font-size: 14px;
73
+ font-weight: 400;
74
+ font-family: 'Lato', sans-serif;
75
+ line-height: 18px;
76
+ color: $tone-1-color;
77
+ padding: 0.5rem 0;
78
+ cursor: pointer;
79
+ text-align: left;
80
+ width: 100%;
81
+
82
+ span {
83
+ flex-grow: 1;
84
+ text-align: left;
85
+ }
86
+ }
87
+ }
88
+ &.compact {
89
+ padding: 4px;
90
+ .overlayTitle{
91
+ margin-bottom: 16px;
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,192 @@
1
+ import { h, Component, createRef } from 'preact';
2
+ import { createPortal } from 'preact/compat';
3
+ import { ui } from '@playkit-js/kaltura-player-js';
4
+ import {Icon, IconSize} from '@playkit-js/common/dist/icon';
5
+ import { PopoverMenuItemData } from '../popover-menu/popover-menu-item';
6
+ import * as styles from '../popover-overlay/popover-overlay.scss';
7
+ // @ts-ignore
8
+ const {withKeyboardA11y} = KalturaPlayer.ui.utils
9
+ const { Overlay } = KalturaPlayer.ui.components;
10
+ const { getOverlayPortalElement } = ui;
11
+ const { withText, Text } = ui.preacti18n;
12
+ const {PLAYER_BREAK_POINTS} = ui.Components;
13
+ const focusableElements = 'select, button, [href], input:not([type="hidden"]), textarea, [tabindex]:not([tabindex="-1"])';
14
+
15
+ const translates = {
16
+ moreOptionsLabel: <Text id="transcript.more_options">More transcript options</Text>,
17
+ languageSelectorLabel: <Text id="transcript.transcript">Transcript</Text>
18
+ };
19
+
20
+ interface PopoverOverlayProps {
21
+ player: any;
22
+ isOpen: boolean;
23
+ onClose: () => void;
24
+ items: Array<PopoverMenuItemData>;
25
+ textTracks: Array<any>;
26
+ changeLanguage: (track: any) => void;
27
+ playerSize?: number;
28
+ moreOptionsLabel?: preact.VNode;
29
+ handleKeyDown?: (e: KeyboardEvent) => void;
30
+ addAccessibleChild?: (element: HTMLElement, pushToBeginning?: boolean) => void;
31
+ setIsModal?: (isModal: boolean) => void;
32
+ }
33
+
34
+
35
+ const COMPACT_SIZES = [
36
+ PLAYER_BREAK_POINTS.TINY,
37
+ PLAYER_BREAK_POINTS.EXTRA_SMALL,
38
+ PLAYER_BREAK_POINTS.SMALL
39
+ ];
40
+
41
+ const iconsMap: Record<string, string> = {
42
+ 'transcript-detach-attach-button': 'detach',
43
+ 'download-menu-item': 'download',
44
+ 'print-menu-item': 'print'
45
+ };
46
+
47
+ interface TranscriptLanguageSelectorProps {
48
+ textTracks: Array<any>;
49
+ changeLanguage: (track: any) => void;
50
+ }
51
+
52
+ const TranscriptLanguageSelector = ({ textTracks, changeLanguage }: TranscriptLanguageSelectorProps) => {
53
+ // Filter out metadata tracks
54
+ const selectableTracks = (textTracks || []).filter(track => track.kind !== 'metadata');
55
+
56
+ if (selectableTracks.length <= 1) return null;
57
+
58
+ return (
59
+ <div className={styles.languageSelector}>
60
+ <Icon name="transcript" size={IconSize.medium} />
61
+ <label htmlFor="transcript-language">{translates.languageSelectorLabel}</label>
62
+ <select
63
+ id="transcript-language"
64
+ value={selectableTracks.findIndex(track => track.active)}
65
+ onChange={(e) => {
66
+ const selectedIdx = Number((e.target as HTMLSelectElement).value);
67
+ changeLanguage(selectableTracks[selectedIdx]);
68
+ }}
69
+ onFocus={(e) => e.currentTarget.classList.add(styles.open)}
70
+ onBlur={(e) => e.currentTarget.classList.remove(styles.open)}
71
+ >
72
+ {selectableTracks.map((track, idx) => (
73
+ <option key={idx} value={idx}>
74
+ {track.label}
75
+ </option>
76
+ ))}
77
+ </select>
78
+ </div>
79
+ );
80
+ };
81
+
82
+ interface OverlayActionItemProps {
83
+ item: PopoverMenuItemData;
84
+ }
85
+
86
+ const OverlayActionItem = ({ item }: OverlayActionItemProps) => {
87
+ const iconName = iconsMap[item.testId];
88
+ if (!iconName || item.items) return null;
89
+
90
+ return (
91
+ <button
92
+ data-testid={item.testId}
93
+ disabled={item.isDisabled}
94
+ onClick={() => item.onClick?.()}
95
+ >
96
+ <Icon name={iconName} size={IconSize.medium} />
97
+ <span>{item.label}</span>
98
+ </button>
99
+ );
100
+ };
101
+
102
+
103
+ @withKeyboardA11y
104
+ @withText(translates)
105
+ export class PopoverOverlay extends Component<PopoverOverlayProps> {
106
+ private _containerRef = createRef<HTMLDivElement>();
107
+ private _focusablesRegistered = false;
108
+
109
+ private _focusFirstItem() {
110
+ const container = this._containerRef.current;
111
+ if (!container) return;
112
+ const focusable = container.querySelector<HTMLElement>(focusableElements);
113
+ focusable?.focus();
114
+ }
115
+
116
+ componentDidMount() {
117
+ this.props.setIsModal?.(true);
118
+ }
119
+
120
+ componentDidUpdate(prevProps: PopoverOverlayProps) {
121
+ if (!prevProps.isOpen && this.props.isOpen) this._focusFirstItem();
122
+ // Register focusable elements when overlay opens
123
+ if (!prevProps.isOpen && this.props.isOpen) {
124
+ this._focusablesRegistered = false;
125
+ this._registerAccessibleChildren();
126
+ }
127
+ // If content changed while open, re-register once
128
+ if (this.props.isOpen && !this._focusablesRegistered && (prevProps.items !== this.props.items || prevProps.textTracks !== this.props.textTracks)) {
129
+ this._registerAccessibleChildren();
130
+ }
131
+ if (prevProps.isOpen && !this.props.isOpen) {
132
+ this._focusablesRegistered = false;
133
+ }
134
+ }
135
+
136
+ componentWillUnmount() {
137
+ this.props.setIsModal?.(false);
138
+ }
139
+
140
+ private _getFocusableElements(): HTMLElement[] {
141
+ const container = this._containerRef.current;
142
+ if (!container) return [];
143
+
144
+ return Array.from(
145
+ container.querySelectorAll<HTMLElement>(focusableElements)).filter(el => !el.hasAttribute('disabled') && el.offsetParent !== null);
146
+ }
147
+
148
+ private _registerAccessibleChildren() {
149
+ const { addAccessibleChild } = this.props;
150
+ if (!addAccessibleChild || this._focusablesRegistered) return;
151
+
152
+ this._getFocusableElements()
153
+ .reverse()
154
+ .forEach(el => addAccessibleChild(el, true));
155
+
156
+ this._focusablesRegistered = true;
157
+ }
158
+
159
+ render() {
160
+ const {
161
+ player,
162
+ isOpen,
163
+ onClose,
164
+ items,
165
+ textTracks,
166
+ changeLanguage,
167
+ playerSize
168
+ } = this.props;
169
+
170
+ if (!isOpen || !player) return null;
171
+ const overlayRoot = getOverlayPortalElement(player);
172
+ if (!overlayRoot) return null;
173
+
174
+ const isCompact = playerSize !== undefined ? COMPACT_SIZES.includes(playerSize) : false;
175
+
176
+ return createPortal(
177
+ <Overlay open onClose={onClose} ariaLabel={this.props.moreOptionsLabel} handleKeyDown={this.props.handleKeyDown} addAccessibleChild={this.props.addAccessibleChild}>
178
+ <div data-testid="popover-overlay" ref={this._containerRef} className={`${styles.popoverOverlayContainer} ${isCompact ? styles.compact : ''}`}>
179
+ <h3 className={styles.overlayTitle}>{this.props.moreOptionsLabel}</h3>
180
+ <TranscriptLanguageSelector textTracks={textTracks} changeLanguage={changeLanguage} />
181
+
182
+ <div className={styles.actionButtons}>
183
+ {items.map(item => (
184
+ <OverlayActionItem key={item.testId} item={item} />
185
+ ))}
186
+ </div>
187
+ </div>
188
+ </Overlay>,
189
+ overlayRoot
190
+ );
191
+ }
192
+ }
@@ -14,6 +14,7 @@ import {Button, ButtonType, ButtonSize} from '@playkit-js/common/dist/components
14
14
  import {ScreenReaderProvider} from '@playkit-js/common/dist/hoc/sr-wrapper';
15
15
  import {OnClickEvent, OnClick} from '@playkit-js/common/dist/hoc/a11y-wrapper';
16
16
  import {TranscriptEvents} from '../../events/events';
17
+ const {withPlayer} = KalturaPlayer.ui.components;
17
18
 
18
19
  const {ENTER, SPACE, TAB, ESC} = ui.utils.KeyMap;
19
20
  const {withText, Text} = ui.preacti18n;
@@ -78,6 +79,9 @@ export interface TranscriptProps {
78
79
  textTracks: Array<core.TextTrack>;
79
80
  changeLanguage: (textTrack: core.TextTrack) => void;
80
81
  sidePanelPosition: string;
82
+ player: any;
83
+ onOverlayOpen?: () => void;
84
+ onOverlayClose?: () => void;
81
85
  }
82
86
 
83
87
  interface TranscriptState {
@@ -110,6 +114,7 @@ const mapStateToProps = (state: any, ownProps: Pick<TranscriptProps, 'expandMode
110
114
  // @ts-ignore
111
115
  @connect(mapStateToProps)
112
116
  @withText(translates)
117
+ @withPlayer
113
118
  export class Transcript extends Component<TranscriptProps, TranscriptState> {
114
119
  private _transcriptListRef: HTMLElement | null = null;
115
120
  private _captionListRef: any = null;
@@ -335,7 +340,9 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
335
340
  onAttach,
336
341
  onDetach,
337
342
  textTracks,
338
- changeLanguage
343
+ changeLanguage,
344
+ isMobile,
345
+ smallScreen
339
346
  } = this.props;
340
347
  const {search, activeSearchIndex, totalSearchResults} = this.state;
341
348
  const widgetHeight = this._widgetRootRef?.getBoundingClientRect().height;
@@ -382,7 +389,14 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
382
389
  detachMenuItem,
383
390
  kitchenSinkDetached,
384
391
  textTracks,
385
- changeLanguage
392
+ changeLanguage,
393
+ sidePanelPosition,
394
+ isMobile,
395
+ smallScreen,
396
+ player: this.props.player,
397
+ onOverlayOpen: this.props.onOverlayOpen,
398
+ onOverlayClose: this.props.onOverlayClose,
399
+ playerWidth : this.props.playerWidth
386
400
  }}
387
401
  />
388
402
  {!detachMenuItem && this._renderDetachButton()}
@@ -1,11 +1,15 @@
1
- import {Component, h} from 'preact';
2
- import {core} from '@playkit-js/kaltura-player-js';
1
+ import {Component, h, Fragment, createRef} from 'preact';
2
+ import {core, ui} from '@playkit-js/kaltura-player-js';
3
3
  import {PopoverMenu} from '../popover-menu';
4
4
  import {PopoverMenuItemData} from '../popover-menu';
5
+ import { PopoverOverlay } from '../popover-overlay';
5
6
  import {capitalizeFirstLetter} from '../../utils';
6
7
 
7
8
  import {Button, ButtonType} from '@playkit-js/common/dist/components/button';
8
9
  const {withText, Text} = KalturaPlayer.ui.preacti18n;
10
+ const {SidePanelPositions, getOverlayPortalElement} = ui;
11
+ const {focusElement} = ui.Utils;
12
+ const {PLAYER_BREAK_POINTS} = ui.Components;
9
13
 
10
14
  interface TranscriptMenuProps {
11
15
  shouldUseCalculatedHeight: boolean;
@@ -27,9 +31,16 @@ interface TranscriptMenuProps {
27
31
  onClick: () => void;
28
32
  isDisabled: boolean;
29
33
  } | null;
34
+ sidePanelPosition: string;
35
+ isMobile?: boolean;
36
+ player: any;
37
+ onOverlayOpen?: () => void;
38
+ onOverlayClose?: () => void;
39
+ playerWidth?: number;
30
40
  }
31
41
 
32
42
  interface TranscriptMenuState {
43
+ isOverlayOpen: boolean;
33
44
  items: Array<PopoverMenuItemData>;
34
45
  }
35
46
 
@@ -41,6 +52,21 @@ const translates = {
41
52
 
42
53
  @withText(translates)
43
54
  class TranscriptMenu extends Component<TranscriptMenuProps, TranscriptMenuState> {
55
+ private _triggerButtonRef = createRef<HTMLButtonElement>();
56
+
57
+ state: TranscriptMenuState = {
58
+ isOverlayOpen: false,
59
+ items: []
60
+ };
61
+
62
+ private _focusTriggerButton = () => {
63
+ const ref = this._triggerButtonRef.current as any;
64
+ const innerBtn = ref?.base || ref?.buttonRef?.current;
65
+ if (innerBtn && typeof innerBtn.focus === 'function') {
66
+ focusElement(innerBtn);
67
+ }
68
+ };
69
+
44
70
  render() {
45
71
  const {
46
72
  shouldUseCalculatedHeight,
@@ -55,9 +81,13 @@ class TranscriptMenu extends Component<TranscriptMenuProps, TranscriptMenuState>
55
81
  detachMenuItem,
56
82
  kitchenSinkDetached,
57
83
  textTracks,
58
- changeLanguage
84
+ changeLanguage,
85
+ sidePanelPosition,
86
+ isMobile,
87
+ playerWidth
59
88
  } = this.props;
60
89
  const items = [];
90
+ const { isOverlayOpen } = this.state;
61
91
 
62
92
  if (textTracks?.length > 1) {
63
93
  const activeTextTrack = textTracks.find(track => track.active);
@@ -96,6 +126,40 @@ class TranscriptMenu extends Component<TranscriptMenuProps, TranscriptMenuState>
96
126
  isDisabled: isLoading
97
127
  });
98
128
  }
129
+ const isSmallPlayer = playerWidth! <= PLAYER_BREAK_POINTS.SMALL;
130
+ const shouldUseOverlay = !kitchenSinkDetached && (sidePanelPosition === SidePanelPositions.BOTTOM || isMobile || isSmallPlayer);
131
+
132
+ if (shouldUseOverlay) {
133
+ return (
134
+ <Fragment>
135
+ <Button
136
+ ref={this._triggerButtonRef}
137
+ type={ButtonType.borderless}
138
+ icon="more"
139
+ ariaLabel={this.props.moreOptionsLabel}
140
+ onClick={() => {
141
+ this.props.onOverlayOpen?.();
142
+ this.setState({ isOverlayOpen: true });
143
+ }}
144
+ />
145
+
146
+ <PopoverOverlay
147
+ player={this.props.player}
148
+ isOpen={isOverlayOpen}
149
+ items={items}
150
+ textTracks={this.props.textTracks}
151
+ changeLanguage={this.props.changeLanguage}
152
+ playerSize={this.props.playerWidth}
153
+ onClose={() => {
154
+ this.setState({ isOverlayOpen: false }, () => {
155
+ this.props.onOverlayClose?.();
156
+ this._focusTriggerButton();
157
+ });
158
+ }}
159
+ />
160
+ </Fragment>
161
+ );
162
+ }
99
163
 
100
164
  return items.length ? (
101
165
  <PopoverMenu
@@ -350,6 +350,9 @@ export class TranscriptPlugin extends KalturaPlayer.core.BasePlugin {
350
350
  textTracks={this._getTextTracks()}
351
351
  changeLanguage={this._changeLanguage}
352
352
  sidePanelPosition={position}
353
+ player={this.player}
354
+ onOverlayOpen={() => this.sidePanelsManager?.deactivateItem(this._transcriptPanel)}
355
+ onOverlayClose={() => this.sidePanelsManager?.activateItem(this._transcriptPanel)}
353
356
  />
354
357
  ) as any;
355
358
  },